mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge branch 'Wei-Shaw:dev' into dev
This commit is contained in:
@@ -403,8 +403,8 @@
|
||||
:class="[
|
||||
'table-row transition-all duration-150',
|
||||
index % 2 === 0
|
||||
? 'bg-white dark:bg-gray-800/40'
|
||||
: 'bg-gray-50/70 dark:bg-gray-700/30',
|
||||
? 'bg-white dark:bg-gray-800/30'
|
||||
: 'bg-gray-50/70 dark:bg-gray-800/50',
|
||||
'border-b-2 border-gray-200/80 dark:border-gray-700/50',
|
||||
'hover:bg-blue-50/60 hover:shadow-sm dark:hover:bg-blue-900/20'
|
||||
]"
|
||||
@@ -432,7 +432,7 @@
|
||||
<!-- 显示所有者信息 -->
|
||||
<div
|
||||
v-if="isLdapEnabled && key.ownerDisplayName"
|
||||
class="mt-1 text-xs text-red-600"
|
||||
class="mt-1 text-xs text-red-600 dark:text-red-400"
|
||||
>
|
||||
<i class="fas fa-user mr-1" />
|
||||
{{ key.ownerDisplayName }}
|
||||
@@ -521,7 +521,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="!key.tags || key.tags.length === 0"
|
||||
class="text-xs text-gray-400"
|
||||
class="text-xs text-gray-400 dark:text-gray-500"
|
||||
>无标签</span
|
||||
>
|
||||
</div>
|
||||
@@ -555,7 +555,7 @@
|
||||
</td>
|
||||
<!-- 限制 -->
|
||||
<td class="px-2 py-2" style="font-size: 12px">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- 每日费用限制进度条 -->
|
||||
<LimitProgressBar
|
||||
v-if="key.dailyCostLimit > 0"
|
||||
@@ -574,6 +574,15 @@
|
||||
type="opus"
|
||||
/>
|
||||
|
||||
<!-- GPT-5 High 周费用限制进度条 -->
|
||||
<LimitProgressBar
|
||||
v-if="key.weeklyGPT5HighCostLimit > 0"
|
||||
:current="key.weeklyGPT5HighCost || 0"
|
||||
label="GPT-5H"
|
||||
:limit="key.weeklyGPT5HighCostLimit"
|
||||
type="gpt5-high"
|
||||
/>
|
||||
|
||||
<!-- 时间窗口限制进度条 -->
|
||||
<WindowLimitBar
|
||||
v-if="key.rateLimitWindow > 0"
|
||||
@@ -592,16 +601,17 @@
|
||||
v-if="
|
||||
!key.dailyCostLimit &&
|
||||
!key.weeklyOpusCostLimit &&
|
||||
!key.weeklyGPT5HighCostLimit &&
|
||||
!key.rateLimitWindow
|
||||
"
|
||||
class="dark:to-gray-750 relative h-7 w-full overflow-hidden rounded-md border border-gray-200 bg-gradient-to-r from-gray-50 to-gray-100 dark:border-gray-700 dark:from-gray-800"
|
||||
class="text-center"
|
||||
>
|
||||
<div class="flex h-full items-center justify-center gap-1.5">
|
||||
<i class="fas fa-infinity text-xs text-gray-400 dark:text-gray-500" />
|
||||
<span class="text-xs font-medium text-gray-400 dark:text-gray-500">
|
||||
无限制
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full bg-gray-200 px-2 py-1 text-xs text-gray-600 dark:bg-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-infinity" />
|
||||
<span>无限制</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -667,7 +677,7 @@
|
||||
<span v-else-if="key.expiresAt">
|
||||
<span
|
||||
v-if="isApiKeyExpired(key.expiresAt)"
|
||||
class="inline-flex cursor-pointer items-center text-red-600 hover:underline"
|
||||
class="inline-flex cursor-pointer items-center text-red-600 hover:underline dark:text-red-400"
|
||||
style="font-size: 13px"
|
||||
@click.stop="startEditExpiry(key)"
|
||||
>
|
||||
@@ -676,7 +686,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-else-if="isApiKeyExpiringSoon(key.expiresAt)"
|
||||
class="inline-flex cursor-pointer items-center text-orange-600 hover:underline"
|
||||
class="inline-flex cursor-pointer items-center text-orange-600 hover:underline dark:text-orange-400"
|
||||
style="font-size: 13px"
|
||||
@click.stop="startEditExpiry(key)"
|
||||
>
|
||||
@@ -707,7 +717,7 @@
|
||||
<td class="whitespace-nowrap px-3 py-3" style="font-size: 13px">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="rounded px-2 py-1 text-xs font-medium text-purple-600 transition-colors hover:bg-purple-50 hover:text-purple-900 dark:hover:bg-purple-900/20"
|
||||
class="rounded px-2 py-1 text-xs font-medium text-purple-600 transition-colors hover:bg-purple-50 hover:text-purple-900 dark:text-purple-400 dark:hover:bg-purple-900/20"
|
||||
title="查看详细统计"
|
||||
@click="showUsageDetails(key)"
|
||||
>
|
||||
@@ -716,7 +726,7 @@
|
||||
</button>
|
||||
<button
|
||||
v-if="key && key.id"
|
||||
class="rounded px-2 py-1 text-xs font-medium text-indigo-600 transition-colors hover:bg-indigo-50 hover:text-indigo-900 dark:hover:bg-indigo-900/20"
|
||||
class="rounded px-2 py-1 text-xs font-medium text-indigo-600 transition-colors hover:bg-indigo-50 hover:text-indigo-900 dark:text-indigo-400 dark:hover:bg-indigo-900/20"
|
||||
title="模型使用分布"
|
||||
@click="toggleApiKeyModelStats(key.id)"
|
||||
>
|
||||
@@ -729,7 +739,7 @@
|
||||
<span class="ml-1 hidden xl:inline">模型</span>
|
||||
</button>
|
||||
<button
|
||||
class="rounded px-2 py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-900 dark:hover:bg-blue-900/20"
|
||||
class="rounded px-2 py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-900 dark:text-blue-400 dark:hover:bg-blue-900/20"
|
||||
title="编辑"
|
||||
@click="openEditApiKeyModal(key)"
|
||||
>
|
||||
@@ -742,7 +752,7 @@
|
||||
(isApiKeyExpired(key.expiresAt) ||
|
||||
isApiKeyExpiringSoon(key.expiresAt))
|
||||
"
|
||||
class="rounded px-2 py-1 text-xs font-medium text-green-600 transition-colors hover:bg-green-50 hover:text-green-900 dark:hover:bg-green-900/20"
|
||||
class="rounded px-2 py-1 text-xs font-medium text-green-600 transition-colors hover:bg-green-50 hover:text-green-900 dark:text-green-400 dark:hover:bg-green-900/20"
|
||||
title="续期"
|
||||
@click="openRenewApiKeyModal(key)"
|
||||
>
|
||||
@@ -752,8 +762,8 @@
|
||||
<button
|
||||
:class="[
|
||||
key.isActive
|
||||
? 'text-orange-600 hover:bg-orange-50 hover:text-orange-900 dark:hover:bg-orange-900/20'
|
||||
: 'text-green-600 hover:bg-green-50 hover:text-green-900 dark:hover:bg-green-900/20',
|
||||
? 'text-orange-600 hover:bg-orange-50 hover:text-orange-900 dark:text-orange-400 dark:hover:bg-orange-900/20'
|
||||
: 'text-green-600 hover:bg-green-50 hover:text-green-900 dark:text-green-400 dark:hover:bg-green-900/20',
|
||||
'rounded px-2 py-1 text-xs font-medium transition-colors'
|
||||
]"
|
||||
:title="key.isActive ? '禁用' : '激活'"
|
||||
@@ -765,7 +775,7 @@
|
||||
}}</span>
|
||||
</button>
|
||||
<button
|
||||
class="rounded px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900 dark:hover:bg-red-900/20"
|
||||
class="rounded px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
title="删除"
|
||||
@click="deleteApiKey(key.id)"
|
||||
>
|
||||
@@ -910,7 +920,7 @@
|
||||
<i class="fas fa-dollar-sign mr-1 text-xs text-green-500" />
|
||||
费用:
|
||||
</span>
|
||||
<span class="font-semibold text-green-600">{{
|
||||
<span class="font-semibold text-green-600 dark:text-green-400">{{
|
||||
calculateModelCost(stat)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -941,7 +951,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="stat.cacheCreateTokens > 0"
|
||||
class="flex items-center justify-between text-xs text-purple-600"
|
||||
class="flex items-center justify-between text-xs text-purple-600 dark:text-purple-400"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-save mr-1" />
|
||||
@@ -953,7 +963,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="stat.cacheReadTokens > 0"
|
||||
class="flex items-center justify-between text-xs text-purple-600"
|
||||
class="flex items-center justify-between text-xs text-purple-600 dark:text-purple-400"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-download mr-1" />
|
||||
@@ -982,7 +992,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 text-right">
|
||||
<span class="text-xs font-medium text-indigo-600">
|
||||
<span
|
||||
class="text-xs font-medium text-indigo-600 dark:text-indigo-400"
|
||||
>
|
||||
{{
|
||||
calculateApiKeyModelPercentage(
|
||||
stat.allTokens,
|
||||
@@ -1152,7 +1164,10 @@
|
||||
使用共享池
|
||||
</div>
|
||||
<!-- 显示所有者信息 -->
|
||||
<div v-if="isLdapEnabled && key.ownerDisplayName" class="text-xs text-red-600">
|
||||
<div
|
||||
v-if="isLdapEnabled && key.ownerDisplayName"
|
||||
class="text-xs text-red-600 dark:text-red-400"
|
||||
>
|
||||
<i class="fas fa-user mr-1" />
|
||||
{{ key.ownerDisplayName }}
|
||||
</div>
|
||||
@@ -1167,7 +1182,7 @@
|
||||
globalDateFilter.type === 'custom' ? '累计统计' : '今日使用'
|
||||
}}</span>
|
||||
<button
|
||||
class="text-xs text-blue-600 hover:text-blue-800"
|
||||
class="text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400"
|
||||
@click="showUsageDetails(key)"
|
||||
>
|
||||
<i class="fas fa-chart-line mr-1" />详情
|
||||
@@ -1181,7 +1196,7 @@
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">请求</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-green-600">
|
||||
<p class="text-sm font-semibold text-green-600 dark:text-green-400">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">费用</p>
|
||||
@@ -1254,7 +1269,9 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
:class="
|
||||
isApiKeyExpiringSoon(key.expiresAt) ? 'font-semibold text-orange-600' : ''
|
||||
isApiKeyExpiringSoon(key.expiresAt)
|
||||
? 'font-semibold text-orange-600 dark:text-orange-400'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ key.expiresAt ? formatDate(key.expiresAt) : '永不过期' }}
|
||||
@@ -1291,7 +1308,7 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-3 flex gap-2 border-t border-gray-100 pt-3 dark:border-gray-600">
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-50 px-3 py-1.5 text-xs text-blue-600 transition-colors hover:bg-blue-100 dark:bg-blue-900/30 dark:hover:bg-blue-900/50"
|
||||
class="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-50 px-3 py-1.5 text-xs text-blue-600 transition-colors hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-400 dark:hover:bg-blue-900/50"
|
||||
@click="showUsageDetails(key)"
|
||||
>
|
||||
<i class="fas fa-chart-line" />
|
||||
@@ -1309,7 +1326,7 @@
|
||||
key.expiresAt &&
|
||||
(isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))
|
||||
"
|
||||
class="flex-1 rounded-lg bg-orange-50 px-3 py-1.5 text-xs text-orange-600 transition-colors hover:bg-orange-100 dark:bg-orange-900/30 dark:hover:bg-orange-900/50"
|
||||
class="flex-1 rounded-lg bg-orange-50 px-3 py-1.5 text-xs text-orange-600 transition-colors hover:bg-orange-100 dark:bg-orange-900/30 dark:text-orange-400 dark:hover:bg-orange-900/50"
|
||||
@click="openRenewApiKeyModal(key)"
|
||||
>
|
||||
<i class="fas fa-clock mr-1" />
|
||||
@@ -1318,8 +1335,8 @@
|
||||
<button
|
||||
:class="[
|
||||
key.isActive
|
||||
? 'bg-orange-50 text-orange-600 hover:bg-orange-100 dark:bg-orange-900/30 dark:hover:bg-orange-900/50'
|
||||
: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-900/30 dark:hover:bg-green-900/50',
|
||||
? 'bg-orange-50 text-orange-600 hover:bg-orange-100 dark:bg-orange-900/30 dark:text-orange-400 dark:hover:bg-orange-900/50'
|
||||
: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50',
|
||||
'rounded-lg px-3 py-1.5 text-xs transition-colors'
|
||||
]"
|
||||
@click="toggleApiKeyStatus(key)"
|
||||
@@ -1328,7 +1345,7 @@
|
||||
{{ key.isActive ? '禁用' : '激活' }}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-red-50 px-3 py-1.5 text-xs text-red-600 transition-colors hover:bg-red-100 dark:bg-red-900/30 dark:hover:bg-red-900/50"
|
||||
class="rounded-lg bg-red-50 px-3 py-1.5 text-xs text-red-600 transition-colors hover:bg-red-100 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
|
||||
@click="deleteApiKey(key.id)"
|
||||
>
|
||||
<i class="fas fa-trash" />
|
||||
@@ -1588,11 +1605,17 @@
|
||||
<!-- 创建者 -->
|
||||
<td v-if="isLdapEnabled" class="px-3 py-3">
|
||||
<div class="text-xs">
|
||||
<span v-if="key.createdBy === 'admin'" class="text-blue-600">
|
||||
<span
|
||||
v-if="key.createdBy === 'admin'"
|
||||
class="text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
<i class="fas fa-user-shield mr-1 text-xs" />
|
||||
管理员
|
||||
</span>
|
||||
<span v-else-if="key.userUsername" class="text-green-600">
|
||||
<span
|
||||
v-else-if="key.userUsername"
|
||||
class="text-green-600 dark:text-green-400"
|
||||
>
|
||||
<i class="fas fa-user mr-1 text-xs" />
|
||||
{{ key.userUsername }}
|
||||
</span>
|
||||
@@ -1612,11 +1635,17 @@
|
||||
<!-- 删除者 -->
|
||||
<td class="px-3 py-3">
|
||||
<div class="text-xs">
|
||||
<span v-if="key.deletedByType === 'admin'" class="text-blue-600">
|
||||
<span
|
||||
v-if="key.deletedByType === 'admin'"
|
||||
class="text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
<i class="fas fa-user-shield mr-1 text-xs" />
|
||||
{{ key.deletedBy }}
|
||||
</span>
|
||||
<span v-else-if="key.deletedByType === 'user'" class="text-green-600">
|
||||
<span
|
||||
v-else-if="key.deletedByType === 'user'"
|
||||
class="text-green-600 dark:text-green-400"
|
||||
>
|
||||
<i class="fas fa-user mr-1 text-xs" />
|
||||
{{ key.deletedBy }}
|
||||
</span>
|
||||
@@ -3311,6 +3340,7 @@ const formatDate = (dateString) => {
|
||||
// if (progress >= 100) return 'bg-red-500'
|
||||
// if (progress >= 80) return 'bg-yellow-500'
|
||||
// return 'bg-green-500'
|
||||
|
||||
// }
|
||||
|
||||
// 获取 Opus 周费用进度 - 已移到 LimitBadge 组件中
|
||||
|
||||
@@ -1,604 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-4 md:p-6" :class="isDarkMode ? 'gradient-bg-dark' : 'gradient-bg'">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="glass-strong mb-6 rounded-3xl p-4 shadow-xl md:mb-8 md:p-6">
|
||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||
<LogoTitle
|
||||
:loading="oemLoading"
|
||||
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
:subtitle="currentTab === 'stats' ? 'API Key 使用统计' : '使用教程'"
|
||||
:title="oemSettings.siteName"
|
||||
/>
|
||||
<div class="flex items-center gap-2 md:gap-4">
|
||||
<!-- 主题切换按钮 -->
|
||||
<div class="flex items-center">
|
||||
<ThemeToggle mode="dropdown" />
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
v-if="oemSettings.ldapEnabled || oemSettings.showAdminButton !== false"
|
||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
||||
/>
|
||||
|
||||
<!-- 用户登录按钮 (仅在 LDAP 启用时显示) -->
|
||||
<router-link
|
||||
v-if="oemSettings.ldapEnabled"
|
||||
class="user-login-button flex items-center gap-2 rounded-2xl px-4 py-2 text-white transition-all duration-300 md:px-5 md:py-2.5"
|
||||
to="/user-login"
|
||||
>
|
||||
<i class="fas fa-user text-sm md:text-base" />
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">用户登录</span>
|
||||
</router-link>
|
||||
<!-- 管理后台按钮 -->
|
||||
<router-link
|
||||
v-if="oemSettings.showAdminButton !== false"
|
||||
class="admin-button-refined flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
||||
to="/dashboard"
|
||||
>
|
||||
<i class="fas fa-shield-alt text-sm md:text-base" />
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">管理后台</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="mb-6 md:mb-8">
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="inline-flex w-full max-w-md rounded-full border border-white/20 bg-white/10 p-1 shadow-lg backdrop-blur-xl md:w-auto"
|
||||
>
|
||||
<button
|
||||
:class="['tab-pill-button', currentTab === 'stats' ? 'active' : '']"
|
||||
@click="currentTab = 'stats'"
|
||||
>
|
||||
<i class="fas fa-chart-line mr-1 md:mr-2" />
|
||||
<span class="text-sm md:text-base">统计查询</span>
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-pill-button', currentTab === 'tutorial' ? 'active' : '']"
|
||||
@click="currentTab = 'tutorial'"
|
||||
>
|
||||
<i class="fas fa-graduation-cap mr-1 md:mr-2" />
|
||||
<span class="text-sm md:text-base">使用教程</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计内容 -->
|
||||
<div v-if="currentTab === 'stats'" class="tab-content">
|
||||
<!-- API Key 输入区域 -->
|
||||
<ApiKeyInput />
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="error" class="mb-6 md:mb-8">
|
||||
<div
|
||||
class="rounded-xl border border-red-500/30 bg-red-500/20 p-3 text-sm text-red-800 backdrop-blur-sm dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-200 md:p-4 md:text-base"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-2" />
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计数据展示区域 -->
|
||||
<div v-if="statsData" class="fade-in">
|
||||
<div class="glass-strong rounded-3xl p-4 shadow-xl md:p-6">
|
||||
<!-- 时间范围选择器 -->
|
||||
<div class="mb-4 border-b border-gray-200 pb-4 dark:border-gray-700 md:mb-6 md:pb-6">
|
||||
<div
|
||||
class="flex flex-col items-start justify-between gap-3 md:flex-row md:items-center md:gap-4"
|
||||
>
|
||||
<div class="flex items-center gap-2 md:gap-3">
|
||||
<i class="fas fa-clock text-base text-blue-500 md:text-lg" />
|
||||
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg"
|
||||
>统计时间范围</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex w-full gap-2 md:w-auto">
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center gap-1 px-4 py-2 text-xs font-medium md:flex-none md:gap-2 md:px-6 md:text-sm"
|
||||
:class="['period-btn', { active: statsPeriod === 'daily' }]"
|
||||
:disabled="loading || modelStatsLoading"
|
||||
@click="switchPeriod('daily')"
|
||||
>
|
||||
<i class="fas fa-calendar-day text-xs md:text-sm" />
|
||||
今日
|
||||
</button>
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center gap-1 px-4 py-2 text-xs font-medium md:flex-none md:gap-2 md:px-6 md:text-sm"
|
||||
:class="['period-btn', { active: statsPeriod === 'monthly' }]"
|
||||
:disabled="loading || modelStatsLoading"
|
||||
@click="switchPeriod('monthly')"
|
||||
>
|
||||
<i class="fas fa-calendar-alt text-xs md:text-sm" />
|
||||
本月
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息和统计概览 -->
|
||||
<StatsOverview />
|
||||
|
||||
<!-- Token 分布和限制配置 -->
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:mb-8 md:gap-6 lg:grid-cols-2">
|
||||
<TokenDistribution />
|
||||
<!-- 单key模式下显示限制配置 -->
|
||||
<LimitConfig v-if="!multiKeyMode" />
|
||||
<!-- 多key模式下显示聚合统计卡片,填充右侧空白 -->
|
||||
<AggregatedStatsCard v-if="multiKeyMode" />
|
||||
</div>
|
||||
|
||||
<!-- 模型使用统计 -->
|
||||
<ModelUsageStats />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 教程内容 -->
|
||||
<div v-if="currentTab === 'tutorial'" class="tab-content">
|
||||
<div class="glass-strong rounded-3xl shadow-xl">
|
||||
<TutorialView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useApiStatsStore } from '@/stores/apistats'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import LogoTitle from '@/components/common/LogoTitle.vue'
|
||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||
import ApiKeyInput from '@/components/apistats/ApiKeyInput.vue'
|
||||
import StatsOverview from '@/components/apistats/StatsOverview.vue'
|
||||
import TokenDistribution from '@/components/apistats/TokenDistribution.vue'
|
||||
import LimitConfig from '@/components/apistats/LimitConfig.vue'
|
||||
import AggregatedStatsCard from '@/components/apistats/AggregatedStatsCard.vue'
|
||||
import ModelUsageStats from '@/components/apistats/ModelUsageStats.vue'
|
||||
import TutorialView from './TutorialView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const apiStatsStore = useApiStatsStore()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
// 当前标签页
|
||||
const currentTab = ref('stats')
|
||||
|
||||
// 主题相关
|
||||
const isDarkMode = computed(() => themeStore.isDarkMode)
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
apiId,
|
||||
loading,
|
||||
modelStatsLoading,
|
||||
oemLoading,
|
||||
error,
|
||||
statsPeriod,
|
||||
statsData,
|
||||
oemSettings,
|
||||
multiKeyMode
|
||||
} = storeToRefs(apiStatsStore)
|
||||
|
||||
const { queryStats, switchPeriod, loadStatsWithApiId, loadOemSettings, reset } = apiStatsStore
|
||||
|
||||
// 处理键盘快捷键
|
||||
const handleKeyDown = (event) => {
|
||||
// Ctrl/Cmd + Enter 查询
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
if (!loading.value && apiKey.value.trim()) {
|
||||
queryStats()
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// ESC 清除数据
|
||||
if (event.key === 'Escape') {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
console.log('API Stats Page loaded')
|
||||
|
||||
// 初始化主题(因为该页面不在 MainLayout 内)
|
||||
themeStore.initTheme()
|
||||
|
||||
// 加载 OEM 设置
|
||||
loadOemSettings()
|
||||
|
||||
// 检查 URL 参数
|
||||
const urlApiId = route.query.apiId
|
||||
const urlApiKey = route.query.apiKey
|
||||
|
||||
if (
|
||||
urlApiId &&
|
||||
urlApiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)
|
||||
) {
|
||||
// 如果 URL 中有 apiId,直接使用 apiId 加载数据
|
||||
apiId.value = urlApiId
|
||||
loadStatsWithApiId()
|
||||
} else if (urlApiKey && urlApiKey.length > 10) {
|
||||
// 向后兼容,支持 apiKey 参数
|
||||
apiKey.value = urlApiKey
|
||||
}
|
||||
|
||||
// 添加键盘事件监听
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
// 监听 API Key 变化
|
||||
watch(apiKey, (newValue) => {
|
||||
if (!newValue) {
|
||||
apiStatsStore.clearData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 渐变背景 */
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 暗色模式的渐变背景 */
|
||||
.gradient-bg-dark {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-bg::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(240, 147, 251, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(102, 126, 234, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(118, 75, 162, 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 暗色模式的背景覆盖 */
|
||||
.gradient-bg-dark::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(100, 116, 139, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(71, 85, 105, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(30, 41, 59, 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 玻璃态效果 - 使用CSS变量 */
|
||||
.glass-strong {
|
||||
background: var(--glass-strong-color);
|
||||
backdrop-filter: blur(25px);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 暗色模式的玻璃态效果 */
|
||||
:global(.dark) .glass-strong {
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.7),
|
||||
0 0 0 1px rgba(55, 65, 81, 0.3),
|
||||
inset 0 1px 0 rgba(75, 85, 99, 0.2);
|
||||
}
|
||||
|
||||
/* 标题渐变 */
|
||||
.header-title {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
/* 用户登录按钮 */
|
||||
.user-login-button {
|
||||
background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(52, 211, 153, 0.25),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 暗色模式下的用户登录按钮 */
|
||||
:global(.dark) .user-login-button {
|
||||
background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
|
||||
border: 1px solid rgba(52, 211, 153, 0.4);
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(52, 211, 153, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.user-login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.user-login-button:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(52, 211, 153, 0.35),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.user-login-button:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 暗色模式下的悬停效果 */
|
||||
:global(.dark) .user-login-button:hover {
|
||||
box-shadow:
|
||||
0 8px 20px rgba(52, 211, 153, 0.4),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(52, 211, 153, 0.5);
|
||||
}
|
||||
|
||||
.user-login-button:active {
|
||||
transform: translateY(-1px) scale(1);
|
||||
}
|
||||
|
||||
/* 确保图标和文字在所有模式下都清晰可见 */
|
||||
.user-login-button i,
|
||||
.user-login-button span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 管理后台按钮 - 精致版本 */
|
||||
.admin-button-refined {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(102, 126, 234, 0.25),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 暗色模式下的管理后台按钮 */
|
||||
:global(.dark) .admin-button-refined {
|
||||
background: rgba(55, 65, 81, 0.8);
|
||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
||||
color: #f3f4f6;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.admin-button-refined::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.admin-button-refined:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(118, 75, 162, 0.35),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-button-refined:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 暗色模式下的悬停效果 */
|
||||
:global(.dark) .admin-button-refined:hover {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-color: rgba(147, 51, 234, 0.4);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(102, 126, 234, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-button-refined:active {
|
||||
transform: translateY(-1px) scale(1);
|
||||
}
|
||||
|
||||
/* 确保图标和文字在所有模式下都清晰可见 */
|
||||
.admin-button-refined i,
|
||||
.admin-button-refined span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 时间范围按钮 */
|
||||
.period-btn {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.025em;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.period-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(102, 126, 234, 0.3),
|
||||
0 4px 6px -2px rgba(102, 126, 234, 0.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.period-btn:not(.active) {
|
||||
color: #374151;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border: 1px solid rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
:global(html.dark) .period-btn:not(.active) {
|
||||
color: #e5e7eb;
|
||||
background: rgba(55, 65, 81, 0.4);
|
||||
border: 1px solid rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
.period-btn:not(.active):hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #1f2937;
|
||||
border-color: rgba(209, 213, 219, 0.8);
|
||||
}
|
||||
|
||||
:global(html.dark) .period-btn:not(.active):hover {
|
||||
background: rgba(75, 85, 99, 0.6);
|
||||
color: #ffffff;
|
||||
border-color: rgba(107, 114, 128, 0.8);
|
||||
}
|
||||
|
||||
/* Tab 胶囊按钮样式 */
|
||||
.tab-pill-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 暗夜模式下的Tab按钮基础样式 */
|
||||
:global(html.dark) .tab-pill-button {
|
||||
color: rgba(209, 213, 219, 0.8);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tab-pill-button {
|
||||
padding: 0.625rem 1.25rem;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-pill-button:hover {
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(html.dark) .tab-pill-button:hover {
|
||||
color: #f3f4f6;
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
}
|
||||
|
||||
.tab-pill-button.active {
|
||||
background: white;
|
||||
color: #764ba2;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(html.dark) .tab-pill-button.active {
|
||||
background: rgba(71, 85, 105, 0.9);
|
||||
color: #f3f4f6;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tab-pill-button i {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Tab 内容切换动画 */
|
||||
.tab-content {
|
||||
animation: tabFadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes tabFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user