|
|
<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" |
|
|
/> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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"> |
|
|
|
|
|
<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 /> |
|
|
|
|
|
|
|
|
<div |
|
|
class="mb-6 mt-6 grid grid-cols-1 gap-4 md:mb-8 md:mt-8 md:gap-6 xl:grid-cols-2 xl:items-stretch" |
|
|
> |
|
|
<TokenDistribution class="h-full" /> |
|
|
<template v-if="multiKeyMode"> |
|
|
<AggregatedStatsCard class="h-full" /> |
|
|
</template> |
|
|
<template v-else> |
|
|
<LimitConfig class="h-full" /> |
|
|
</template> |
|
|
</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) => { |
|
|
|
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { |
|
|
if (!loading.value && apiKey.value.trim()) { |
|
|
queryStats() |
|
|
} |
|
|
event.preventDefault() |
|
|
} |
|
|
|
|
|
|
|
|
if (event.key === 'Escape') { |
|
|
reset() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
|
|
|
|
|
|
|
|
themeStore.initTheme() |
|
|
|
|
|
|
|
|
loadOemSettings() |
|
|
|
|
|
|
|
|
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) |
|
|
) { |
|
|
|
|
|
apiId.value = urlApiId |
|
|
loadStatsWithApiId() |
|
|
} else if (urlApiKey && urlApiKey.length > 10) { |
|
|
|
|
|
apiKey.value = urlApiKey |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown) |
|
|
}) |
|
|
|
|
|
|
|
|
onUnmounted(() => { |
|
|
document.removeEventListener('keydown', handleKeyDown) |
|
|
}) |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
.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-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; |
|
|
} |
|
|
|
|
|
|
|
|
: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-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> |
|
|
|