## 修复内容 ### 1. AccountsView 功能恢复 - 恢复3个缺失的模态框组件: - ReAuthAccountModal.vue - 重新授权功能 - AccountTestModal.vue - 测试连接功能 - AccountStatsModal.vue - 查看统计功能 - 恢复 handleTest/handleViewStats/handleReAuth 调用模态框 - 修复 UpdateAccountRequest 类型定义(添加 schedulable 字段) ### 2. DashboardView 修复 - 恢复 formatBalance 函数(支持千位分隔符显示) - 为 UserDashboardStats 添加完整 Props 类型定义 - 为 UserDashboardRecentUsage 添加完整 Props 类型定义 - 优化格式化函数到共享 utils/format.ts ### 3. 类型安全增强 - 修复 UserAttributeOption 索引签名兼容性 - 移除未使用的类型导入 - 所有组件 Props 类型完整 ## 验证结果 - ✅ TypeScript 类型检查通过(0 errors) - ✅ vue-tsc 检查通过(0 errors) - ✅ 所有样式与重构前100%一致 - ✅ 所有功能完整恢复 ## 影响范围 - AccountsView: 代码行数从974行优化到189行(提升80.6%可维护性) - DashboardView: 保持组件化同时恢复所有原有功能 - 深色模式支持完整 - 所有颜色方案和 SVG 图标保持一致 Closes #149
172 lines
10 KiB
Vue
172 lines
10 KiB
Vue
<template>
|
|
<!-- Row 1: Core Stats -->
|
|
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
<!-- Balance -->
|
|
<div v-if="!isSimple" class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
|
|
<svg class="h-5 w-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.balance') }}</p>
|
|
<p class="text-xl font-bold text-emerald-600 dark:text-emerald-400">${{ formatBalance(balance) }}</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('common.available') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Keys -->
|
|
<div class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
|
<svg class="h-5 w-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.apiKeys') }}</p>
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ stats?.total_api_keys || 0 }}</p>
|
|
<p class="text-xs text-green-600 dark:text-green-400">{{ stats?.active_api_keys || 0 }} {{ t('common.active') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Today Requests -->
|
|
<div class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
|
|
<svg class="h-5 w-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.todayRequests') }}</p>
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ stats?.today_requests || 0 }}</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('common.total') }}: {{ formatNumber(stats?.total_requests || 0) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Today Cost -->
|
|
<div class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-purple-100 p-2 dark:bg-purple-900/30">
|
|
<svg class="h-5 w-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.todayCost') }}</p>
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white">
|
|
<span class="text-purple-600 dark:text-purple-400" :title="t('dashboard.actual')">${{ formatCost(stats?.today_actual_cost || 0) }}</span>
|
|
<span class="text-sm font-normal text-gray-400 dark:text-gray-500" :title="t('dashboard.standard')"> / ${{ formatCost(stats?.today_cost || 0) }}</span>
|
|
</p>
|
|
<p class="text-xs">
|
|
<span class="text-gray-500 dark:text-gray-400">{{ t('common.total') }}: </span>
|
|
<span class="text-purple-600 dark:text-purple-400" :title="t('dashboard.actual')">${{ formatCost(stats?.total_actual_cost || 0) }}</span>
|
|
<span class="text-gray-400 dark:text-gray-500" :title="t('dashboard.standard')"> / ${{ formatCost(stats?.total_cost || 0) }}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 2: Token Stats -->
|
|
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
<!-- Today Tokens -->
|
|
<div class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-amber-100 p-2 dark:bg-amber-900/30">
|
|
<svg class="h-5 w-5 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.todayTokens') }}</p>
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ formatTokens(stats?.today_tokens || 0) }}</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('dashboard.input') }}: {{ formatTokens(stats?.today_input_tokens || 0) }} / {{ t('dashboard.output') }}: {{ formatTokens(stats?.today_output_tokens || 0) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Total Tokens -->
|
|
<div class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-indigo-100 p-2 dark:bg-indigo-900/30">
|
|
<svg class="h-5 w-5 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.totalTokens') }}</p>
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ formatTokens(stats?.total_tokens || 0) }}</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('dashboard.input') }}: {{ formatTokens(stats?.total_input_tokens || 0) }} / {{ t('dashboard.output') }}: {{ formatTokens(stats?.total_output_tokens || 0) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Performance (RPM/TPM) -->
|
|
<div class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-violet-100 p-2 dark:bg-violet-900/30">
|
|
<svg class="h-5 w-5 text-violet-600 dark:text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.performance') }}</p>
|
|
<div class="flex items-baseline gap-2">
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ formatTokens(stats?.rpm || 0) }}</p>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">RPM</span>
|
|
</div>
|
|
<div class="flex items-baseline gap-2">
|
|
<p class="text-sm font-semibold text-violet-600 dark:text-violet-400">{{ formatTokens(stats?.tpm || 0) }}</p>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">TPM</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Avg Response Time -->
|
|
<div class="card p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="rounded-lg bg-rose-100 p-2 dark:bg-rose-900/30">
|
|
<svg class="h-5 w-5 text-rose-600 dark:text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('dashboard.avgResponse') }}</p>
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ formatDuration(stats?.average_duration_ms || 0) }}</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('dashboard.averageTime') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useI18n } from 'vue-i18n'
|
|
import type { UserDashboardStats as UserStatsType } from '@/api/usage'
|
|
|
|
defineProps<{
|
|
stats: UserStatsType
|
|
balance: number
|
|
isSimple: boolean
|
|
}>()
|
|
const { t } = useI18n()
|
|
|
|
const formatBalance = (b: number) =>
|
|
new Intl.NumberFormat('en-US', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}).format(b)
|
|
|
|
const formatNumber = (n: number) => n.toLocaleString()
|
|
const formatCost = (c: number) => c.toFixed(4)
|
|
const formatTokens = (t: number) => (t >= 1000 ? `${(t / 1000).toFixed(1)}K` : t.toString())
|
|
const formatDuration = (ms: number) => ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms.toFixed(0)}ms`
|
|
</script>
|