refactor(frontend): 优化用户视图和设置向导
- 改进API密钥管理界面 - 优化用户使用统计视图 - 完善初始化设置向导
This commit is contained in:
@@ -128,7 +128,7 @@
|
||||
v-model="formData.database.password"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Password"
|
||||
:placeholder="t('setup.database.passwordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,7 +234,7 @@
|
||||
v-model="formData.redis.password"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Password"
|
||||
:placeholder="t('setup.redis.passwordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -320,7 +320,7 @@
|
||||
v-model="formData.admin.password"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Min 6 characters"
|
||||
:placeholder="t('setup.admin.passwordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -330,13 +330,13 @@
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Confirm password"
|
||||
:placeholder="t('setup.admin.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
<p
|
||||
v-if="confirmPassword && formData.admin.password !== confirmPassword"
|
||||
class="input-error-text"
|
||||
>
|
||||
Passwords do not match
|
||||
{{ t('setup.admin.passwordMismatch') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -154,8 +154,7 @@
|
||||
<!-- Use Key Button -->
|
||||
<button
|
||||
@click="openUseKeyModal(row)"
|
||||
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-green-50 hover:text-green-600 dark:hover:bg-green-900/20 dark:hover:text-green-400"
|
||||
:title="t('keys.useKey')"
|
||||
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-green-50 hover:text-green-600 dark:hover:bg-green-900/20 dark:hover:text-green-400"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -170,12 +169,12 @@
|
||||
d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">{{ t('keys.useKey') }}</span>
|
||||
</button>
|
||||
<!-- Import to CC Switch Button -->
|
||||
<button
|
||||
@click="importToCcswitch(row.key)"
|
||||
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
|
||||
:title="t('keys.importToCcSwitch')"
|
||||
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -190,17 +189,17 @@
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">{{ t('keys.importToCcSwitch') }}</span>
|
||||
</button>
|
||||
<!-- Toggle Status Button -->
|
||||
<button
|
||||
@click="toggleKeyStatus(row)"
|
||||
:class="[
|
||||
'rounded-lg p-2 transition-colors',
|
||||
'flex flex-col items-center gap-0.5 rounded-lg p-1.5 transition-colors',
|
||||
row.status === 'active'
|
||||
? 'text-gray-500 hover:bg-yellow-50 hover:text-yellow-600 dark:hover:bg-yellow-900/20 dark:hover:text-yellow-400'
|
||||
: 'text-gray-500 hover:bg-green-50 hover:text-green-600 dark:hover:bg-green-900/20 dark:hover:text-green-400'
|
||||
]"
|
||||
:title="row.status === 'active' ? t('keys.disable') : t('keys.enable')"
|
||||
>
|
||||
<svg
|
||||
v-if="row.status === 'active'"
|
||||
@@ -230,12 +229,12 @@
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">{{ row.status === 'active' ? t('keys.disable') : t('keys.enable') }}</span>
|
||||
</button>
|
||||
<!-- Edit Button -->
|
||||
<button
|
||||
@click="editKey(row)"
|
||||
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400"
|
||||
:title="t('common.edit')"
|
||||
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -250,12 +249,12 @@
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">{{ t('common.edit') }}</span>
|
||||
</button>
|
||||
<!-- Delete Button -->
|
||||
<button
|
||||
@click="confirmDelete(row)"
|
||||
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400"
|
||||
:title="t('common.delete')"
|
||||
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -270,6 +269,7 @@
|
||||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">{{ t('common.delete') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -294,7 +294,11 @@
|
||||
${{ row.actual_cost.toFixed(6) }}
|
||||
</span>
|
||||
<!-- Cost Detail Tooltip -->
|
||||
<div class="group relative">
|
||||
<div
|
||||
class="group relative"
|
||||
@mouseenter="showTooltip($event, row)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<div
|
||||
class="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-100 transition-colors group-hover:bg-blue-100 dark:bg-gray-700 dark:group-hover:bg-blue-900/50"
|
||||
>
|
||||
@@ -310,39 +314,6 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Tooltip Content (right side) -->
|
||||
<div
|
||||
class="invisible absolute left-full top-1/2 z-[100] ml-2 -translate-y-1/2 opacity-0 transition-all duration-200 group-hover:visible group-hover:opacity-100"
|
||||
>
|
||||
<div
|
||||
class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
||||
<span class="font-semibold text-blue-400"
|
||||
>{{ (row.rate_multiplier || 1).toFixed(2) }}x</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<span class="text-gray-400">{{ t('usage.original') }}</span>
|
||||
<span class="font-medium text-white">${{ row.total_cost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between gap-6 border-t border-gray-700 pt-1.5"
|
||||
>
|
||||
<span class="text-gray-400">{{ t('usage.billed') }}</span>
|
||||
<span class="font-semibold text-green-400"
|
||||
>${{ row.actual_cost.toFixed(6) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tooltip Arrow (left side) -->
|
||||
<div
|
||||
class="absolute right-full top-1/2 h-0 w-0 -translate-y-1/2 border-b-[6px] border-r-[6px] border-t-[6px] border-b-transparent border-r-gray-900 border-t-transparent dark:border-r-gray-800"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -399,6 +370,45 @@
|
||||
</template>
|
||||
</TablePageLayout>
|
||||
</AppLayout>
|
||||
|
||||
<!-- Tooltip Portal -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="tooltipVisible"
|
||||
class="fixed z-[9999] pointer-events-none -translate-y-1/2"
|
||||
:style="{
|
||||
left: tooltipPosition.x + 'px',
|
||||
top: tooltipPosition.y + 'px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
||||
<span class="font-semibold text-blue-400"
|
||||
>{{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<span class="text-gray-400">{{ t('usage.original') }}</span>
|
||||
<span class="font-medium text-white">${{ tooltipData?.total_cost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6 border-t border-gray-700 pt-1.5">
|
||||
<span class="text-gray-400">{{ t('usage.billed') }}</span>
|
||||
<span class="font-semibold text-green-400"
|
||||
>${{ tooltipData?.actual_cost.toFixed(6) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tooltip Arrow (left side) -->
|
||||
<div
|
||||
class="absolute right-full top-1/2 h-0 w-0 -translate-y-1/2 border-b-[6px] border-r-[6px] border-t-[6px] border-b-transparent border-r-gray-900 border-t-transparent dark:border-r-gray-800"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -420,6 +430,11 @@ import { formatDateTime } from '@/utils/format'
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// Tooltip state
|
||||
const tooltipVisible = ref(false)
|
||||
const tooltipPosition = ref({ x: 0, y: 0 })
|
||||
const tooltipData = ref<UsageLog | null>(null)
|
||||
|
||||
// Usage stats from API
|
||||
const usageStats = ref<UsageStatsResponse | null>(null)
|
||||
|
||||
@@ -629,6 +644,23 @@ const exportToCSV = () => {
|
||||
appStore.showSuccess(t('usage.exportSuccess'))
|
||||
}
|
||||
|
||||
// Tooltip functions
|
||||
const showTooltip = (event: MouseEvent, row: UsageLog) => {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const rect = target.getBoundingClientRect()
|
||||
|
||||
tooltipData.value = row
|
||||
// Position to the right of the icon, vertically centered
|
||||
tooltipPosition.value.x = rect.right + 8
|
||||
tooltipPosition.value.y = rect.top + rect.height / 2
|
||||
tooltipVisible.value = true
|
||||
}
|
||||
|
||||
const hideTooltip = () => {
|
||||
tooltipVisible.value = false
|
||||
tooltipData.value = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeDateRange()
|
||||
loadApiKeys()
|
||||
|
||||
Reference in New Issue
Block a user