github-actions[bot] commited on
Commit
4289eb1
·
1 Parent(s): f91fed0

Sync from GitHub Viciy2023/Qwen2API-A@b372de2fdb435c7fa78fc69c146257a58c842fba

Browse files
public/src/App.vue CHANGED
@@ -9,16 +9,6 @@
9
  </script>
10
 
11
  <style lang="css" scoped>
12
- body,
13
- html {
14
- margin: 0;
15
- padding: 0;
16
- width: 100%;
17
- height: 100%;
18
- overflow: hidden;
19
- position: relative;
20
- }
21
-
22
  #app-background {
23
  position: fixed;
24
  top: 0;
@@ -27,7 +17,7 @@ html {
27
  height: 100%;
28
  z-index: -1;
29
  background:
30
- radial-gradient(circle at top, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.2) 35%),
31
- linear-gradient(135deg, #dbeafe 0%, #f8fafc 45%, #fde68a 100%);
32
  }
33
  </style>
 
9
  </script>
10
 
11
  <style lang="css" scoped>
 
 
 
 
 
 
 
 
 
 
12
  #app-background {
13
  position: fixed;
14
  top: 0;
 
17
  height: 100%;
18
  z-index: -1;
19
  background:
20
+ radial-gradient(circle at top, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.55) 32%, rgba(248, 250, 252, 0.88) 62%),
21
+ linear-gradient(180deg, #ffffff 0%, #f8fafc 48%, #f1f5f9 100%);
22
  }
23
  </style>
public/src/style.css CHANGED
@@ -1,3 +1,91 @@
1
  @tailwind base;
2
  @tailwind components;
3
- @tailwind utilities;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @tailwind base;
2
  @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ color-scheme: light;
8
+ --app-bg: #fafafa;
9
+ --app-surface: rgba(255, 255, 255, 0.86);
10
+ --app-surface-strong: rgba(255, 255, 255, 0.96);
11
+ --app-border: rgba(15, 23, 42, 0.08);
12
+ --app-text: #0f172a;
13
+ --app-muted: #64748b;
14
+ --app-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ html,
22
+ body,
23
+ #app {
24
+ width: 100%;
25
+ min-height: 100%;
26
+ margin: 0;
27
+ padding: 0;
28
+ }
29
+
30
+ body {
31
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
32
+ background: var(--app-bg);
33
+ color: var(--app-text);
34
+ -webkit-font-smoothing: antialiased;
35
+ -moz-osx-font-smoothing: grayscale;
36
+ }
37
+
38
+ button,
39
+ input,
40
+ select,
41
+ textarea {
42
+ font: inherit;
43
+ }
44
+ }
45
+
46
+ @layer components {
47
+ .vc-shell {
48
+ @apply rounded-[28px] border backdrop-blur-xl;
49
+ background: var(--app-surface);
50
+ border-color: var(--app-border);
51
+ box-shadow: var(--app-shadow);
52
+ }
53
+
54
+ .vc-panel {
55
+ @apply rounded-3xl border bg-white/90;
56
+ border-color: var(--app-border);
57
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
58
+ }
59
+
60
+ .vc-title {
61
+ @apply text-3xl font-semibold tracking-tight text-slate-950;
62
+ }
63
+
64
+ .vc-subtitle {
65
+ @apply text-sm leading-6 text-slate-500;
66
+ }
67
+
68
+ .vc-button {
69
+ @apply inline-flex items-center justify-center rounded-2xl border px-4 py-2.5 text-sm font-medium transition-all duration-200;
70
+ border-color: var(--app-border);
71
+ }
72
+
73
+ .vc-button-primary {
74
+ @apply vc-button bg-slate-950 text-white hover:bg-slate-800;
75
+ border-color: #020617;
76
+ }
77
+
78
+ .vc-button-secondary {
79
+ @apply vc-button bg-white text-slate-700 hover:bg-slate-50;
80
+ }
81
+
82
+ .vc-input {
83
+ @apply w-full rounded-2xl border bg-white px-4 py-3 text-sm text-slate-700 shadow-sm outline-none transition-all duration-200;
84
+ border-color: rgba(15, 23, 42, 0.08);
85
+ }
86
+
87
+ .vc-input:focus {
88
+ border-color: rgba(15, 23, 42, 0.24);
89
+ box-shadow: 0 0 0 4px rgba(15, 23, 42, 0.05);
90
+ }
91
+ }
public/src/views/auth.vue CHANGED
@@ -1,15 +1,24 @@
1
  <template>
2
- <div class="flex flex-col items-center justify-center w-screen h-screen">
3
  <transition name="fade-slide">
4
  <div
5
- class="flex flex-col items-center w-4/5 h-1/2 bg-opacity-50 bg-white rounded-3xl shadow-xl border-2 border-gray-200 animate-panel"
6
  v-if="showPanel">
7
- <h1 class="block mt-24 mb-10 text-2xl font-bold">管理员身份验证</h1>
8
- <input type="text"
9
- class="w-4/5 h-16 rounded-2xl bg-opacity-80 bg-white border-2 border-gray-100 pl-10 placeholder:text-gray-500 focus:shadow-lg focus:scale-105 transition-all duration-300"
10
- placeholder="输入管理员账号" v-model="apiKey" @keyup.enter="handleLogin">
11
- <button class="mt-10 w-4/5 h-16 rounded-2xl bg-opacity-65 border-2 border-black bg-black text-white transition-transform duration-200 active:scale-95 hover:scale-105"
12
- @click="handleLogin">登录</button>
 
 
 
 
 
 
 
 
 
13
  </div>
14
  </transition>
15
  </div>
@@ -63,4 +72,4 @@ onMounted(() => {
63
  opacity: 1;
64
  transform: translateY(0);
65
  }
66
- </style>
 
1
  <template>
2
+ <div class="flex min-h-screen items-center justify-center px-4 py-10">
3
  <transition name="fade-slide">
4
  <div
5
+ class="vc-shell w-full max-w-xl p-8 md:p-10"
6
  v-if="showPanel">
7
+ <div class="mb-8">
8
+ <div class="inline-flex rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-500">Qwen2API Admin</div>
9
+ <h1 class="vc-title mt-5">管理员身份验证</h1>
10
+ <p class="vc-subtitle mt-3">输入管理员 API Key 进入控制台。整个界面已切换为更简洁的控制台风格。</p>
11
+ </div>
12
+
13
+ <div class="space-y-4">
14
+ <input type="text"
15
+ class="vc-input h-14"
16
+ placeholder="请输入管理员账号"
17
+ v-model="apiKey"
18
+ @keyup.enter="handleLogin">
19
+ <button class="vc-button-primary h-14 w-full rounded-2xl text-base"
20
+ @click="handleLogin">登录</button>
21
+ </div>
22
  </div>
23
  </transition>
24
  </div>
 
72
  opacity: 1;
73
  transform: translateY(0);
74
  }
75
+ </style>
public/src/views/dashboard.vue CHANGED
@@ -1,13 +1,44 @@
1
  <template>
2
  <div class="w-100vw h-100vh p-4 overflow-y-auto">
3
  <div class="container mx-auto pt-5">
4
- <div class="grid gap-6 lg:grid-cols-[240px_minmax(0,1fr)]">
5
- <aside class="rounded-3xl border border-white/50 bg-white/70 p-5 shadow-xl backdrop-blur-lg h-fit lg:sticky lg:top-4">
6
- <div class="mb-6">
7
- <h1 class="text-3xl font-bold text-slate-800">Token Manager</h1>
8
- <p class="mt-2 text-sm text-slate-500">左侧切换菜单,右侧查看对应内容</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  </div>
 
 
 
 
 
 
10
 
 
 
11
  <div class="space-y-3">
12
  <button @click="setDashboardTab('accounts')" :class="getSidebarItemClass('accounts', 'emerald')">账号列表</button>
13
  <button @click="showAddModal = true" :class="getActionSidebarClass('green')">添加账号</button>
@@ -19,23 +50,24 @@
19
  </button>
20
  <button @click="exportAccounts" :class="getActionSidebarClass('yellow')">导出账号</button>
21
  <button @click="openModelsPanel" :class="getSidebarItemClass('models', 'violet')">可用模型</button>
 
22
  <button @click="openLogsPanel" :class="getSidebarItemClass('logs', 'slate')">日志查看</button>
23
  <button @click="setDashboardTab('settings')" :class="getSidebarItemClass('settings', 'blue')">系统设置</button>
24
  </div>
25
  </aside>
26
 
27
- <section class="rounded-3xl border border-white/50 bg-white/65 p-4 shadow-xl backdrop-blur-lg md:p-6">
28
  <div v-if="activeDashboardTab === 'accounts'">
29
- <div class="mb-6 flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
30
  <div>
31
  <h2 class="text-2xl font-bold text-slate-800">账号列表</h2>
32
  <p class="mt-2 text-sm text-slate-500">集中管理邮箱账号、令牌与批量操作</p>
33
  </div>
34
- <div class="flex flex-wrap items-center gap-3 text-sm text-slate-600">
35
  <span>共 {{ totalItems }} 个账号</span>
36
- <div class="flex items-center gap-2">
37
  <span>每页显示</span>
38
- <select v-model="pageSize" @change="changePageSize" class="rounded-lg border-gray-300 bg-white/70 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 transition-all duration-300">
39
  <option :value="10">10</option>
40
  <option :value="20">20</option>
41
  <option :value="50">50</option>
@@ -47,26 +79,26 @@
47
  </div>
48
 
49
  <!-- 分页控制区 -->
50
- <div class="mb-4 flex flex-wrap justify-between gap-3 px-1">
51
- <div class="flex space-x-2 items-center">
52
- <span class="text-gray-700">共 {{ totalItems }} 项</span>
53
  <button
54
  @click="changePage(currentPage - 1)"
55
  :disabled="currentPage === 1"
56
  :class="[
57
- 'px-3 py-1 rounded-lg transition-all duration-300',
58
- currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-blue-50 text-blue-700 hover:bg-blue-100'
59
  ]"
60
  >
61
  上一页
62
  </button>
63
- <span class="text-gray-700">{{ currentPage }}/{{ totalPages }}</span>
64
  <button
65
  @click="changePage(currentPage + 1)"
66
  :disabled="currentPage === totalPages || totalPages === 0"
67
  :class="[
68
- 'px-3 py-1 rounded-lg transition-all duration-300',
69
- currentPage === totalPages || totalPages === 0 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-blue-50 text-blue-700 hover:bg-blue-100'
70
  ]"
71
  >
72
  下一页
@@ -76,27 +108,27 @@
76
 
77
  <!-- 多选操作区 -->
78
  <div class="mb-4 flex flex-wrap justify-between gap-3 px-1">
79
- <div class="flex items-center space-x-3">
80
  <label class="inline-flex items-center cursor-pointer group">
81
  <div class="relative">
82
  <input type="checkbox"
83
  v-model="selectAll"
84
  @change="toggleSelectAll"
85
  class="sr-only peer">
86
- <div class="w-6 h-6 bg-white border-2 border-gray-300 rounded-lg peer-checked:bg-indigo-500 peer-checked:border-indigo-500 transition-all duration-300 flex items-center justify-center">
87
  <svg v-show="selectAll" class="w-4 h-4 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
88
  <polyline points="20 6 9 17 4 12"></polyline>
89
  </svg>
90
  </div>
91
  </div>
92
- <span class="ml-2 text-gray-700 group-hover:text-indigo-700 transition-colors duration-200">全选</span>
93
  </label>
94
  <button
95
  @click="deleteSelected"
96
  :disabled="selectedTokens.length === 0"
97
  :class="[
98
  'px-4 py-1.5 rounded-lg transition-all duration-300 border flex items-center space-x-1',
99
- selectedTokens.length === 0 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-red-50 text-red-600 border-red-200 hover:bg-red-100'
100
  ]"
101
  >
102
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
@@ -107,7 +139,7 @@
107
  </div>
108
  <button
109
  @click="showDeleteAllConfirm = true"
110
- class="px-4 py-1.5 rounded-lg border border-red-300 bg-red-50 text-red-700 hover:bg-red-100 transition-all duration-300 flex items-center space-x-1"
111
  >
112
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
113
  <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
@@ -121,51 +153,51 @@
121
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4 p-1">
122
  <div v-for="token in displayedTokens"
123
  :key="token.email"
124
- class="token-card group relative overflow-hidden rounded-2xl transition-all duration-300 hover:shadow-2xl pt-3"
125
- :class="{'ring-2 ring-indigo-500 ring-opacity-75': isSelected(token.email)}">
126
  <div class="absolute top-3 left-3 z-10">
127
  <label class="custom-checkbox cursor-pointer">
128
  <input type="checkbox"
129
  :checked="isSelected(token.email)"
130
  @change="toggleSelect(token.email)"
131
  class="sr-only peer">
132
- <div class="checkbox-icon w-6 h-6 bg-white/70 backdrop-blur-sm border-2 border-gray-300 rounded-lg peer-checked:bg-indigo-500 peer-checked:border-indigo-500 transition-all duration-300 flex items-center justify-center shadow-sm hover:shadow">
133
  <svg v-show="isSelected(token.email)" class="w-4 h-4 text-white transform scale-0 peer-checked:scale-100 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
134
  <polyline points="20 6 9 17 4 12"></polyline>
135
  </svg>
136
  </div>
137
  </label>
138
  </div>
139
- <div class="absolute inset-0 bg-white/30 backdrop-blur-md border border-white/30"></div>
140
  <div class="relative p-4 flex flex-col gap-3">
141
  <div class="flex flex-col space-y-2">
142
- <div class="relative flex items-center bg-blue-50/80 rounded-lg px-2 py-1.5">
143
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
144
- <span class="text-gray-700 min-w-[74px] text-left text-sm font-semibold">📧 Email:</span>
145
  <span class="font-medium whitespace-nowrap text-left">{{ token.email }}</span>
146
  </div>
147
- <button @click="copyToClipboard(token.email)" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity bg-blue-200 hover:bg-blue-300 rounded px-2 py-1 text-base">📋</button>
148
  </div>
149
- <div class="relative flex items-center bg-blue-50/80 rounded-lg px-2 py-1.5">
150
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
151
- <span class="text-gray-700 min-w-[74px] text-left text-sm font-semibold">🔑 Passwd:</span>
152
  <span class="font-medium whitespace-nowrap text-left">{{ token.password }}</span>
153
  </div>
154
- <button @click="copyToClipboard(token.password)" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity bg-blue-200 hover:bg-blue-300 rounded px-2 py-1 text-base">📋</button>
155
  </div>
156
- <div class="relative flex items-center bg-blue-50/80 rounded-lg px-2 py-1.5">
157
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
158
- <span class="text-gray-700 min-w-[74px] text-left text-sm font-semibold">🔐 Token:</span>
159
  <span class="font-medium whitespace-nowrap text-left text-sm">{{ token.token }}</span>
160
  </div>
161
- <button @click="copyToClipboard(token.token)" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity bg-blue-200 hover:bg-blue-300 rounded px-2 py-1 text-base">📋</button>
162
  </div>
163
- <div class="relative flex items-center bg-blue-50/80 rounded-lg px-2 py-1.5">
164
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
165
- <span class="text-gray-700 min-w-[74px] text-left text-sm font-semibold">⏰ Expire:</span>
166
  <span class="font-medium whitespace-nowrap text-left text-sm">{{ new Date(token.expires * 1000).toLocaleString() }}</span>
167
  </div>
168
- <button @click="copyToClipboard(new Date(token.expires * 1000).toLocaleString())" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity bg-blue-200 hover:bg-blue-300 rounded px-2 py-1 text-base">📋</button>
169
  </div>
170
  </div>
171
 
@@ -173,10 +205,10 @@
173
  <button @click="refreshToken(token.email)"
174
  :disabled="refreshingTokens.includes(token.email)"
175
  :class="[
176
- 'w-full py-2 rounded-lg transition-all duration-300 flex items-center justify-center space-x-2 text-sm',
177
  refreshingTokens.includes(token.email)
178
  ? 'bg-green-400 text-white refreshing-button-green cursor-not-allowed'
179
- : 'macaron-green-button text-green-600 hover:bg-green-100 border border-green-200'
180
  ]">
181
  <span v-if="refreshingTokens.includes(token.email)" class="flex items-center space-x-2">
182
  <svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@@ -188,7 +220,7 @@
188
  <span v-else>刷新令牌</span>
189
  </button>
190
  <button @click="deleteToken(token.email)"
191
- class="w-full group-hover:bg-red-50 text-red-600 py-2 rounded-lg transition-all duration-300 hover:bg-red-100 text-sm">
192
  删除账号
193
  </button>
194
  </div>
@@ -207,53 +239,53 @@
207
  <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
208
  <div class="flex flex-wrap items-center gap-2 text-sm text-slate-600">
209
  <span>共 {{ availableModels.length }} 个模型</span>
210
- <button @click="setActiveModelFilter('all')" :class="['rounded-full px-3 py-1 text-xs font-semibold transition-all duration-300', activeModelFilter === 'all' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200']">全部 {{ availableModels.length }}</button>
211
- <button @click="setActiveModelFilter('base')" :class="getFilterBadgeClass('base', 'bg-slate-100', 'text-slate-600', 'hover:bg-slate-200', 'bg-slate-700')">基础 {{ allModelGroups.base.length }}</button>
212
- <button @click="setActiveModelFilter('thinking')" :class="getFilterBadgeClass('thinking', 'bg-amber-100', 'text-amber-700', 'hover:bg-amber-200', 'bg-amber-500')">Thinking {{ allModelGroups.thinking.length }}</button>
213
- <button @click="setActiveModelFilter('search')" :class="getFilterBadgeClass('search', 'bg-cyan-100', 'text-cyan-700', 'hover:bg-cyan-200', 'bg-cyan-500')">Search {{ allModelGroups.search.length }}</button>
214
- <button @click="setActiveModelFilter('image')" :class="getFilterBadgeClass('image', 'bg-rose-100', 'text-rose-700', 'hover:bg-rose-200', 'bg-rose-500')">Image {{ allModelGroups.image.length }}</button>
215
- <button @click="setActiveModelFilter('video')" :class="getFilterBadgeClass('video', 'bg-indigo-100', 'text-indigo-700', 'hover:bg-indigo-200', 'bg-indigo-500')">Video {{ allModelGroups.video.length }}</button>
216
- <button @click="setActiveModelFilter('imageEdit')" :class="getFilterBadgeClass('imageEdit', 'bg-emerald-100', 'text-emerald-700', 'hover:bg-emerald-200', 'bg-emerald-500')">Image Edit {{ allModelGroups.imageEdit.length }}</button>
217
  </div>
218
  <div class="flex w-full sm:w-auto gap-2">
219
  <div class="relative w-full sm:w-72">
220
  <input v-model="modelKeyword" type="text" placeholder="搜索模型 ID" class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2 shadow-sm focus:border-violet-400 focus:ring-violet-400 transition-all duration-300">
221
  </div>
222
- <button @click="refreshModels" :disabled="isLoadingModels" :class="['rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-300 border', isLoadingModels ? 'bg-violet-200 text-violet-600 border-violet-200 cursor-not-allowed' : 'bg-violet-600 text-white border-violet-600 hover:bg-violet-700']">{{ isLoadingModels ? '刷新中...' : '刷新模型列表' }}</button>
223
  </div>
224
  </div>
225
 
226
  <div v-if="isLoadingModels" class="py-12 text-center text-slate-500">正在加载模型列表...</div>
227
- <div v-else-if="modelsError" class="rounded-2xl border border-red-200 bg-red-50 px-4 py-8 text-center text-red-600">
228
  <div class="text-lg font-semibold">模型列表加载失败</div>
229
  <div class="mt-2 text-sm whitespace-pre-line">{{ modelsError }}</div>
230
- <button @click="refreshModels" class="mt-4 rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700 transition-all duration-300">重新加载</button>
231
  </div>
232
  <div v-else ref="modelsScrollContainer" class="max-h-[70vh] overflow-y-auto pr-2">
233
  <div class="space-y-5">
234
- <div v-for="group in groupedModelSections" :key="group.key" v-show="group.models.length" class="rounded-2xl border border-slate-200 bg-white/70 p-4 shadow-sm">
235
  <div class="mb-3 flex items-center justify-between gap-3">
236
  <div>
237
  <h3 class="text-lg font-semibold text-slate-800">{{ group.title }}</h3>
238
  <p class="text-xs text-slate-500 mt-1">{{ group.description }}</p>
239
  <p class="mt-2 text-xs text-slate-400">当前分类已按模型强度从高到低排序</p>
240
  </div>
241
- <span :class="group.badgeClass" class="rounded-full px-3 py-1 text-xs font-semibold">{{ group.models.length }} 个</span>
242
  </div>
243
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
244
- <div v-for="model in group.models" :key="model.id" class="rounded-2xl border border-slate-200 bg-white/80 px-4 py-4 shadow-sm hover:shadow-md transition-all duration-300">
245
  <div class="flex items-start justify-between gap-3">
246
  <div class="min-w-0 flex-1">
247
  <div class="font-semibold text-slate-800 break-all">{{ model.id }}</div>
248
  <div class="mt-2 text-xs text-slate-500 break-all">模型名称:{{ getModelDisplayName(model) }}</div>
249
  <div class="mt-2 flex flex-wrap gap-2">
250
- <span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">{{ model.owned_by || 'unknown' }}</span>
251
- <span v-if="modelTagSummary(model).length" class="rounded-full bg-violet-100 px-2 py-1 text-xs text-violet-700">{{ modelTagSummary(model).join(' / ') }}</span>
252
- <span class="rounded-full bg-emerald-100 px-2 py-1 text-xs text-emerald-700">推荐:{{ getModelUseCase(model) }}</span>
253
  <div class="relative group/tooltip inline-flex">
254
- <span class="cursor-help rounded-full bg-amber-100 px-2 py-1 text-xs text-amber-700">强度:{{ getModelPriorityLabel(model, group.key) }}</span>
255
- <div class="pointer-events-none absolute left-0 top-full z-20 mt-2 hidden w-64 rounded-2xl border border-amber-200 bg-white/95 p-3 text-xs text-slate-600 shadow-xl backdrop-blur-sm group-hover/tooltip:block">
256
- <div class="font-semibold text-amber-700">强度说明</div>
257
  <div class="mt-2 leading-5 whitespace-pre-line">{{ getModelPriorityTooltip(model, group.key) }}</div>
258
  </div>
259
  </div>
@@ -261,9 +293,9 @@
261
  </div>
262
  </div>
263
  <div class="mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3">
264
- <button @click="copyToClipboard(model.id)" class="rounded-lg bg-violet-100 px-2 py-2 text-sm text-violet-700 hover:bg-violet-200 transition-all duration-300">复制 ID</button>
265
- <button @click="copyToClipboard(getModelDisplayName(model))" class="rounded-lg bg-sky-100 px-2 py-2 text-sm text-sky-700 hover:bg-sky-200 transition-all duration-300">复制名字</button>
266
- <button @click="copyModelRequestExample(model)" class="col-span-2 sm:col-span-1 rounded-lg bg-amber-100 px-2 py-2 text-sm text-amber-700 hover:bg-amber-200 transition-all duration-300">复制示例</button>
267
  </div>
268
  </div>
269
  </div>
@@ -281,12 +313,12 @@
281
  </div>
282
  <div class="flex flex-wrap items-center gap-2">
283
  <button @click="fetchLogs" class="rounded-xl bg-slate-800 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-900 transition-all duration-300">刷新日志</button>
284
- <button @click="toggleAutoRefreshLogs" :class="['rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-300', logsAutoRefresh ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200']">
285
  {{ logsAutoRefresh ? '停止自动刷新' : '自动刷新' }}
286
  </button>
287
- <button @click="downloadLogs" class="rounded-xl bg-blue-100 px-4 py-2 text-sm font-semibold text-blue-700 hover:bg-blue-200 transition-all duration-300">下载日志</button>
288
- <button @click="copyLogs" class="rounded-xl bg-violet-100 px-4 py-2 text-sm font-semibold text-violet-700 hover:bg-violet-200 transition-all duration-300">复制日志</button>
289
- <button @click="clearLogs" class="rounded-xl bg-red-100 px-4 py-2 text-sm font-semibold text-red-700 hover:bg-red-200 transition-all duration-300">清空日志</button>
290
  </div>
291
  </div>
292
 
@@ -320,15 +352,15 @@
320
  <div class="flex flex-wrap items-center gap-3">
321
  <div class="inline-flex items-center gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 shadow-sm">
322
  <span>运行日志</span>
323
- <button @click="toggleRuntimeLog" :class="['rounded-full px-3 py-1 text-xs font-semibold transition-all duration-300', runtimeLogEnabled ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-slate-200 text-slate-700 hover:bg-slate-300']">
324
- {{ runtimeLogEnabled ? '已启用' : '已关闭' }}
325
- </button>
326
  </div>
327
  <div class="inline-flex items-center gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 shadow-sm">
328
  <span>自动滚底</span>
329
- <button @click="logsAutoScroll = !logsAutoScroll" :class="['rounded-full px-3 py-1 text-xs font-semibold transition-all duration-300', logsAutoScroll ? 'bg-indigo-600 text-white hover:bg-indigo-700' : 'bg-slate-200 text-slate-700 hover:bg-slate-300']">
330
- {{ logsAutoScroll ? '已开启' : '已关闭' }}
331
- </button>
332
  </div>
333
  </div>
334
  </div>
@@ -340,40 +372,212 @@
340
  </div>
341
 
342
  <div class="rounded-3xl border border-slate-200 bg-slate-950 text-slate-100 shadow-inner overflow-hidden">
343
- <div ref="logsScrollContainer" class="max-h-[70vh] overflow-y-auto p-4 font-mono text-xs leading-6 whitespace-pre-wrap break-words">
344
- <div v-if="isLoadingLogs" class="text-slate-400">正在加载日志...</div>
345
- <div v-else-if="logsError" class="text-red-300">{{ logsError }}</div>
346
- <div v-else-if="!logs.length" class="text-slate-500">当前没有可显示的日志</div>
347
- <div v-else>{{ formattedLogs }}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  </div>
349
  </div>
350
  </div>
351
 
352
- <div v-else class="rounded-3xl border border-blue-100 bg-blue-50/80 p-6">
353
- <h2 class="text-2xl font-bold text-slate-800">系统设置</h2>
354
- <p class="mt-3 text-sm leading-6 text-slate-600">系统设置仍使用独立页面管理。这里保留左右布局入口,点击下面按钮即可进入原设置页。</p>
355
- <div class="mt-5">
356
- <button @click="goToSettings" class="rounded-2xl bg-blue-600 px-5 py-3 text-sm font-semibold text-white transition-all duration-300 hover:bg-blue-700">进入系统设置页</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  </div>
358
  </div>
359
  </section>
360
  </div>
361
  </div>
362
 
 
 
 
 
 
 
 
 
 
 
 
363
  <!-- 删除全部确认对话框 -->
364
  <div v-if="showDeleteAllConfirm"
365
  class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
366
  @click.self="showDeleteAllConfirm = false">
367
- <div class="relative bg-white/90 backdrop-blur-lg rounded-2xl p-6 w-11/12 max-w-md transform transition-all duration-300 scale-100 opacity-100">
368
- <h2 class="text-2xl font-bold text-red-600 mb-4">⚠️ 危险操作</h2>
369
- <p class="text-gray-700 mb-6">您确定要删除<span class="font-bold">全部 {{ totalItems }} 个</span>账号吗?此操作不可恢复</p>
370
  <div class="flex justify-end space-x-4">
371
- <button @click="showDeleteAllConfirm = false"
372
- class="px-4 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 transition-all duration-300">
373
  取消
374
  </button>
375
- <button @click="deleteAllAccounts"
376
- class="px-4 py-2 rounded-xl bg-red-600 text-white hover:bg-red-700 transition-all duration-300">
377
  确认删除
378
  </button>
379
  </div>
@@ -384,32 +588,28 @@
384
  <div v-if="showAddModal"
385
  class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
386
  @click.self="showAddModal = false">
387
- <div class="relative bg-white/80 backdrop-blur-lg rounded-2xl p-6 w-11/12 max-w-md transform transition-all duration-300 scale-100 opacity-100">
388
- <div class="flex mb-6 border-b border-gray-200">
389
- <button :class="['flex-1 py-2 font-bold transition-all rounded-t-xl duration-300', addMode==='single' ? 'text-gray-600 border-b-2 border-gray-500 bg-gray-50/60' : 'text-gray-500 bg-transparent']" @click="addMode='single'">单账号添加</button>
390
- <button :class="['flex-1 py-2 font-bold transition-all rounded-t-xl duration-300', addMode==='batch' ? 'text-gray-600 border-b-2 border-gray-500 bg-gray-50/60' : 'text-gray-500 bg-transparent']" @click="addMode='batch'">批量添加</button>
391
  </div>
392
  <transition name="fade" mode="out-in">
393
  <div v-if="addMode==='single'" key="single">
394
  <h2 class="text-xl font-bold mb-4">添加账号</h2>
395
  <div class="space-y-4">
396
  <div>
397
- <label class="block text-sm font-medium text-gray-700">Email</label>
398
- <input v-model="newAccount.email" type="email"
399
- class="mt-1 block w-full rounded-xl border-gray-300 bg-white/50 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 transition-all duration-300 h-12 text-base px-4">
400
  </div>
401
  <div>
402
- <label class="block text-sm font-medium text-gray-700">Password</label>
403
- <input v-model="newAccount.password" type="password"
404
- class="mt-1 block w-full rounded-xl border-gray-300 bg-white/50 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 transition-all duration-300 h-12 text-base px-4">
405
  </div>
406
  <div class="flex justify-end space-x-4 pt-4">
407
- <button @click="showAddModal = false"
408
- class="px-4 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 transition-all duration-300">
409
  取消
410
  </button>
411
- <button @click="addToken"
412
- class="px-4 py-2 rounded-xl bg-black text-white hover:bg-white hover:text-black transition-all duration-300">
413
  添加
414
  </button>
415
  </div>
@@ -419,16 +619,14 @@
419
  <h2 class="text-xl font-bold mb-4 px-4">批量添加账号</h2>
420
  <div class="space-y-4">
421
  <div>
422
- <label class="block text-sm font-medium text-gray-700 px-4 pb-2">账号列表(每行一个,格式:email:password)</label>
423
- <textarea v-model="batchAccounts" rows="6" class="mt-1 block w-full rounded-xl border-gray-300 bg-white/50 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 transition-all duration-300 h-36 text-base px-4 py-3 resize-none"></textarea>
424
  </div>
425
  <div class="flex justify-end space-x-4 pt-4">
426
- <button @click="showAddModal = false"
427
- class="px-4 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 transition-all duration-300">
428
  取消
429
  </button>
430
- <button @click="addBatchTokens"
431
- class="px-4 py-2 rounded-xl bg-black text-white hover:bg-white hover:text-black transition-all duration-300">
432
  批量添加
433
  </button>
434
  </div>
@@ -442,7 +640,7 @@
442
  <div v-if="toast.show"
443
  :class="[
444
  'fixed top-4 right-4 z-50 px-6 py-4 rounded-xl shadow-lg transform transition-all duration-300',
445
- toast.type === 'success' ? 'bg-emerald-500 text-white' : 'bg-red-500 text-white'
446
  ]">
447
  <div class="flex items-center space-x-2">
448
  <svg v-if="toast.type === 'success'" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
@@ -473,6 +671,20 @@ const newAccount = ref({
473
  password: ''
474
  })
475
  const batchAccounts = ref('')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
  // 分页相关
478
  const displayedTokens = ref([])
@@ -510,6 +722,17 @@ const logsLimit = ref('200')
510
  const runtimeLogEnabled = ref(true)
511
  const logsAutoScroll = ref(true)
512
  const logsScrollContainer = ref(null)
 
 
 
 
 
 
 
 
 
 
 
513
  let logsRefreshTimer = null
514
 
515
  // Toast 通知
@@ -519,6 +742,12 @@ const toast = ref({
519
  type: 'success'
520
  })
521
 
 
 
 
 
 
 
522
  const filteredLogs = computed(() => {
523
  const keyword = logsKeyword.value.trim().toLowerCase()
524
 
@@ -545,6 +774,20 @@ const filteredLogs = computed(() => {
545
 
546
  const formattedLogs = computed(() => filteredLogs.value.map(item => item.text).join('\n'))
547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  const filteredModels = computed(() => {
549
  const keyword = modelKeyword.value.trim().toLowerCase()
550
  if (!keyword) {
@@ -718,37 +961,18 @@ const getModelDisplayName = (model) => {
718
  }
719
 
720
  const getSidebarItemClass = (tabKey, tone) => {
721
- const activeMap = {
722
- emerald: 'bg-emerald-600 text-white border-emerald-600 shadow-lg',
723
- violet: 'bg-violet-600 text-white border-violet-600 shadow-lg',
724
- blue: 'bg-blue-600 text-white border-blue-600 shadow-lg',
725
- slate: 'bg-slate-700 text-white border-slate-700 shadow-lg'
726
- }
727
-
728
- const idleMap = {
729
- emerald: 'bg-emerald-50 text-emerald-900 border-emerald-200 hover:bg-emerald-100',
730
- violet: 'bg-violet-50 text-violet-900 border-violet-200 hover:bg-violet-100',
731
- blue: 'bg-blue-50 text-blue-900 border-blue-200 hover:bg-blue-100',
732
- slate: 'bg-slate-100 text-slate-800 border-slate-200 hover:bg-slate-200'
733
- }
734
-
735
  return [
736
- 'w-full rounded-2xl border px-4 py-3 text-left text-sm font-semibold transition-all duration-300',
737
- activeDashboardTab.value === tabKey ? activeMap[tone] : idleMap[tone]
 
 
738
  ]
739
  }
740
 
741
  const getActionSidebarClass = (tone, disabled = false) => {
742
- const toneMap = {
743
- green: 'bg-green-50 text-green-900 border-green-200 hover:bg-green-100',
744
- purple: 'bg-purple-50 text-purple-900 border-purple-200 hover:bg-purple-100',
745
- pink: 'bg-pink-50 text-pink-900 border-pink-200 hover:bg-pink-100',
746
- yellow: 'bg-yellow-50 text-yellow-900 border-yellow-200 hover:bg-yellow-100'
747
- }
748
-
749
  return [
750
- 'w-full rounded-2xl border px-4 py-3 text-left text-sm font-semibold transition-all duration-300',
751
- disabled ? 'cursor-not-allowed bg-slate-200 text-slate-500 border-slate-200' : toneMap[tone]
752
  ]
753
  }
754
 
@@ -760,6 +984,229 @@ const goToSettings = () => {
760
  router.push('/settings')
761
  }
762
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  const fetchLogs = async () => {
764
  isLoadingLogs.value = true
765
  logsError.value = ''
@@ -879,12 +1326,12 @@ const setActiveModelFilter = (filterKey) => {
879
  }
880
  }
881
 
882
- const getFilterBadgeClass = (filterKey, bgClass, textClass, hoverClass, activeBgClass) => {
883
  return [
884
- 'rounded-full px-3 py-1 text-xs font-semibold transition-all duration-300',
885
  activeModelFilter.value === filterKey
886
- ? `${activeBgClass} text-white`
887
- : `${bgClass} ${textClass} ${hoverClass}`
888
  ]
889
  }
890
 
@@ -1365,6 +1812,8 @@ const openModelsPanel = async () => {
1365
 
1366
  onMounted(() => {
1367
  getTokens()
 
 
1368
  })
1369
  </script>
1370
 
@@ -1388,14 +1837,17 @@ onMounted(() => {
1388
  }
1389
 
1390
  .token-card {
1391
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.3));
1392
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
 
1393
  transform: translateY(0);
1394
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1395
  }
1396
 
1397
  .token-card:hover {
1398
- transform: translateY(-5px);
 
 
1399
  }
1400
 
1401
  .scrollbar-hide {
@@ -1491,7 +1943,7 @@ onMounted(() => {
1491
  left: 0;
1492
  width: 0;
1493
  height: 100%;
1494
- background: rgba(99, 102, 241, 0.1);
1495
  transition: width 0.3s ease;
1496
  }
1497
 
@@ -1519,159 +1971,52 @@ onMounted(() => {
1519
  /* 给选中的卡片添加动画效果 */
1520
  .token-card.ring-2 {
1521
  animation: selected-pulse 2s infinite;
 
1522
  }
1523
 
1524
  @keyframes selected-pulse {
1525
  0% {
1526
- box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
1527
  }
1528
  70% {
1529
- box-shadow: 0 0 0 6px rgba(99, 102, 241, 0);
1530
  }
1531
  100% {
1532
  box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
1533
  }
1534
  }
1535
 
1536
- /* 马卡龙紫色刷新按钮动画 */
1537
- @keyframes refresh-pulse-purple {
1538
- 0% {
1539
- box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.4);
1540
- }
1541
- 70% {
1542
- box-shadow: 0 0 0 6px rgba(168, 85, 247, 0);
1543
- }
1544
- 100% {
1545
- box-shadow: 0 0 0 0 rgba(168, 85, 247, 0);
1546
- }
1547
- }
1548
-
1549
- /* 马卡龙绿色刷新按钮动画 */
1550
- @keyframes refresh-pulse-green {
1551
- 0% {
1552
- box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.4);
1553
- }
1554
- 70% {
1555
- box-shadow: 0 0 0 6px rgba(74, 222, 128, 0);
1556
- }
1557
- 100% {
1558
- box-shadow: 0 0 0 0 rgba(74, 222, 128, 0);
1559
- }
1560
- }
1561
-
1562
- /* 马卡龙粉色刷新按钮动画 */
1563
- @keyframes refresh-pulse-pink {
1564
  0% {
1565
- box-shadow: 0 0 0 0 rgba(236, 72, 153, 0.4);
1566
  }
1567
  70% {
1568
- box-shadow: 0 0 0 6px rgba(236, 72, 153, 0);
1569
  }
1570
  100% {
1571
- box-shadow: 0 0 0 0 rgba(236, 72, 153, 0);
1572
  }
1573
  }
1574
 
1575
- .action-button:hover {
1576
- animation: refresh-pulse-purple 1.5s infinite;
1577
- }
1578
-
1579
- /* 刷新中的按钮样式 - 马卡龙紫色 */
1580
  .refreshing-button-purple {
1581
- background: linear-gradient(45deg, #c084fc, #a855f7);
1582
  color: white;
1583
- animation: refresh-pulse-purple 1.5s infinite;
1584
- box-shadow: 0 4px 15px rgba(168, 85, 247, 0.3);
1585
  }
1586
 
1587
- /* 刷新中的按钮样式 - 马卡龙绿色 */
1588
  .refreshing-button-green {
1589
- background: linear-gradient(45deg, #86efac, #4ade80);
1590
  color: white;
1591
- animation: refresh-pulse-green 1.5s infinite;
1592
- box-shadow: 0 4px 15px rgba(74, 222, 128, 0.3);
1593
  }
1594
 
1595
- /* 刷新中的按钮样式 - 马卡龙粉色 */
1596
  .refreshing-button-pink {
1597
- background: linear-gradient(45deg, #f472b6, #ec4899);
1598
  color: white;
1599
- animation: refresh-pulse-pink 1.5s infinite;
1600
- box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
1601
- }
1602
-
1603
- /* 马卡龙色系按钮增强效果 */
1604
- .action-button {
1605
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1606
- backdrop-filter: blur(10px);
1607
- }
1608
-
1609
- .action-button:hover {
1610
- transform: translateY(-2px);
1611
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
1612
- }
1613
-
1614
- /* 单个刷新按钮的马卡龙绿色样式增强 */
1615
- .text-green-600:hover {
1616
- background: linear-gradient(135deg, #dcfce7, #bbf7d0) !important;
1617
- border-color: #86efac !important;
1618
- transform: translateY(-1px);
1619
- box-shadow: 0 4px 12px rgba(74, 222, 128, 0.2);
1620
- }
1621
-
1622
- /* 绿色刷新按钮的基础样式 */
1623
- .bg-green-50 {
1624
- background: linear-gradient(135deg, #f0fdf4, #dcfce7);
1625
- border: 1px solid #bbf7d0;
1626
- }
1627
-
1628
- .bg-green-50:hover {
1629
- background: linear-gradient(135deg, #dcfce7, #bbf7d0);
1630
- border-color: #86efac;
1631
- transform: translateY(-1px);
1632
- box-shadow: 0 4px 12px rgba(74, 222, 128, 0.2);
1633
- }
1634
-
1635
- /* 马卡龙绿色按钮样式 */
1636
- .macaron-green-button {
1637
- background: linear-gradient(135deg, #f0fdf4, #dcfce7);
1638
- border: 1px solid #bbf7d0;
1639
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1640
- }
1641
-
1642
- .macaron-green-button:hover {
1643
- background: linear-gradient(135deg, #dcfce7, #bbf7d0);
1644
- border-color: #86efac;
1645
- transform: translateY(-1px);
1646
- box-shadow: 0 4px 12px rgba(74, 222, 128, 0.2);
1647
- }
1648
-
1649
- /* 马卡龙紫色按钮样式 */
1650
- .macaron-purple-button {
1651
- background: linear-gradient(135deg, #faf5ff, #f3e8ff);
1652
- border: 1px solid #e9d5ff;
1653
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1654
- }
1655
-
1656
- .macaron-purple-button:hover {
1657
- background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
1658
- border-color: #c4b5fd;
1659
- transform: translateY(-2px);
1660
- box-shadow: 0 4px 15px rgba(168, 85, 247, 0.2);
1661
- }
1662
-
1663
- /* 马卡龙粉色按钮样式 */
1664
- .macaron-pink-button {
1665
- background: linear-gradient(135deg, #fdf2f8, #fce7f3);
1666
- border: 1px solid #f9a8d4;
1667
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1668
- }
1669
-
1670
- .macaron-pink-button:hover {
1671
- background: linear-gradient(135deg, #fce7f3, #fbcfe8);
1672
- border-color: #f472b6;
1673
- transform: translateY(-2px);
1674
- box-shadow: 0 4px 15px rgba(236, 72, 153, 0.2);
1675
  }
1676
 
1677
  /* 响应式优化 */
 
1
  <template>
2
  <div class="w-100vw h-100vh p-4 overflow-y-auto">
3
  <div class="container mx-auto pt-5">
4
+ <div class="vc-shell mb-6 flex flex-col gap-4 px-5 py-4 md:flex-row md:items-center md:justify-between">
5
+ <div>
6
+ <h1 class="vc-title text-[28px]">Qwen2API Token Manager</h1>
7
+ <p class="vc-subtitle mt-1">统一管理账号、模型、统计、日志与系统设置</p>
8
+ </div>
9
+
10
+ <div class="flex flex-1 items-center justify-center">
11
+ <div class="flex max-w-2xl flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-white/90 px-4 py-3 text-sm text-slate-600 shadow-sm">
12
+ <span :class="['inline-flex items-center gap-2 rounded-full px-2.5 py-1 text-xs font-semibold', connectionStatus === 'connected' ? 'bg-slate-950 text-white' : connectionStatus === 'checking' ? 'bg-slate-200 text-slate-700' : 'bg-slate-100 text-slate-500']">
13
+ <span :class="[
14
+ 'h-2 w-2 rounded-full',
15
+ connectionStatus === 'connected'
16
+ ? 'bg-emerald-400 animate-pulse'
17
+ : connectionStatus === 'checking'
18
+ ? 'bg-slate-400 animate-pulse'
19
+ : 'bg-slate-400'
20
+ ]"></span>
21
+ {{ connectionStatusLabel }}
22
+ </span>
23
+ <span class="break-all text-slate-500">{{ siteBaseUrl }}</span>
24
+ <button @click="copyToClipboard(siteBaseUrl)" class="inline-flex h-9 items-center justify-center rounded-xl border border-slate-200 bg-white px-3 text-xs font-medium text-slate-700 transition-all duration-200 hover:bg-slate-50" title="复制当前站点地址">
25
+ 复制 URL
26
+ </button>
27
+ <button @click="refreshConnectionStatus" :disabled="isCheckingConnection" class="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-700 transition-all duration-200 hover:bg-slate-50 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400" title="刷新连接状态">
28
+ <svg :class="['h-4 w-4', isCheckingConnection ? 'animate-spin' : '']" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
29
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m14.836 2A8.001 8.001 0 005.582 9m0 0H9m11 11v-5h-.581m0 0A8.003 8.003 0 016.582 15m13.418 0H15" />
30
+ </svg>
31
+ </button>
32
  </div>
33
+ </div>
34
+
35
+ <div class="flex justify-end">
36
+ <button @click="logout" class="vc-button-secondary whitespace-nowrap">退出登录</button>
37
+ </div>
38
+ </div>
39
 
40
+ <div class="grid gap-6 lg:grid-cols-[240px_minmax(0,1fr)]">
41
+ <aside class="vc-shell h-fit p-5 lg:sticky lg:top-4">
42
  <div class="space-y-3">
43
  <button @click="setDashboardTab('accounts')" :class="getSidebarItemClass('accounts', 'emerald')">账号列表</button>
44
  <button @click="showAddModal = true" :class="getActionSidebarClass('green')">添加账号</button>
 
50
  </button>
51
  <button @click="exportAccounts" :class="getActionSidebarClass('yellow')">导出账号</button>
52
  <button @click="openModelsPanel" :class="getSidebarItemClass('models', 'violet')">可用模型</button>
53
+ <button @click="openUsagePanel" :class="getSidebarItemClass('usage', 'amber')">使用统计</button>
54
  <button @click="openLogsPanel" :class="getSidebarItemClass('logs', 'slate')">日志查看</button>
55
  <button @click="setDashboardTab('settings')" :class="getSidebarItemClass('settings', 'blue')">系统设置</button>
56
  </div>
57
  </aside>
58
 
59
+ <section class="vc-shell p-4 md:p-6">
60
  <div v-if="activeDashboardTab === 'accounts'">
61
+ <div class="mb-5 flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
62
  <div>
63
  <h2 class="text-2xl font-bold text-slate-800">账号列表</h2>
64
  <p class="mt-2 text-sm text-slate-500">集中管理邮箱账号、令牌与批量操作</p>
65
  </div>
66
+ <div class="flex flex-wrap items-center gap-2 text-sm text-slate-500">
67
  <span>共 {{ totalItems }} 个账号</span>
68
+ <div class="flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1.5">
69
  <span>每页显示</span>
70
+ <select v-model="pageSize" @change="changePageSize" class="rounded-lg border-none bg-transparent py-0 pr-6 text-sm text-slate-700 shadow-none focus:ring-0">
71
  <option :value="10">10</option>
72
  <option :value="20">20</option>
73
  <option :value="50">50</option>
 
79
  </div>
80
 
81
  <!-- 分页控制区 -->
82
+ <div class="mb-3 flex flex-wrap justify-between gap-3 px-1">
83
+ <div class="flex items-center gap-2 rounded-2xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-600">
84
+ <span>共 {{ totalItems }} 项</span>
85
  <button
86
  @click="changePage(currentPage - 1)"
87
  :disabled="currentPage === 1"
88
  :class="[
89
+ 'px-3 py-1 rounded-lg transition-all duration-200 text-sm',
90
+ currentPage === 1 ? 'bg-slate-100 text-slate-400 cursor-not-allowed' : 'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50'
91
  ]"
92
  >
93
  上一页
94
  </button>
95
+ <span>{{ currentPage }}/{{ totalPages }}</span>
96
  <button
97
  @click="changePage(currentPage + 1)"
98
  :disabled="currentPage === totalPages || totalPages === 0"
99
  :class="[
100
+ 'px-3 py-1 rounded-lg transition-all duration-200 text-sm',
101
+ currentPage === totalPages || totalPages === 0 ? 'bg-slate-100 text-slate-400 cursor-not-allowed' : 'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50'
102
  ]"
103
  >
104
  下一页
 
108
 
109
  <!-- 多选操作区 -->
110
  <div class="mb-4 flex flex-wrap justify-between gap-3 px-1">
111
+ <div class="flex flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-white px-3 py-2">
112
  <label class="inline-flex items-center cursor-pointer group">
113
  <div class="relative">
114
  <input type="checkbox"
115
  v-model="selectAll"
116
  @change="toggleSelectAll"
117
  class="sr-only peer">
118
+ <div class="w-6 h-6 bg-white border-2 border-slate-300 rounded-lg peer-checked:bg-slate-950 peer-checked:border-slate-950 transition-all duration-300 flex items-center justify-center">
119
  <svg v-show="selectAll" class="w-4 h-4 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
120
  <polyline points="20 6 9 17 4 12"></polyline>
121
  </svg>
122
  </div>
123
  </div>
124
+ <span class="ml-2 text-slate-700 group-hover:text-slate-950 transition-colors duration-200">全选</span>
125
  </label>
126
  <button
127
  @click="deleteSelected"
128
  :disabled="selectedTokens.length === 0"
129
  :class="[
130
  'px-4 py-1.5 rounded-lg transition-all duration-300 border flex items-center space-x-1',
131
+ selectedTokens.length === 0 ? 'bg-slate-100 text-slate-400 border-slate-200 cursor-not-allowed' : 'bg-white text-slate-700 border-slate-200 hover:bg-slate-50'
132
  ]"
133
  >
134
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
 
139
  </div>
140
  <button
141
  @click="showDeleteAllConfirm = true"
142
+ class="px-4 py-2 rounded-lg border border-slate-200 bg-white text-slate-700 hover:bg-slate-50 transition-all duration-200 flex items-center space-x-1 text-sm"
143
  >
144
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
145
  <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
 
153
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4 p-1">
154
  <div v-for="token in displayedTokens"
155
  :key="token.email"
156
+ class="token-card group relative overflow-hidden rounded-2xl transition-all duration-300 pt-3"
157
+ :class="{'ring-2 ring-slate-900 ring-opacity-20': isSelected(token.email)}">
158
  <div class="absolute top-3 left-3 z-10">
159
  <label class="custom-checkbox cursor-pointer">
160
  <input type="checkbox"
161
  :checked="isSelected(token.email)"
162
  @change="toggleSelect(token.email)"
163
  class="sr-only peer">
164
+ <div class="checkbox-icon w-6 h-6 bg-white/90 backdrop-blur-sm border-2 border-slate-300 rounded-lg peer-checked:bg-slate-950 peer-checked:border-slate-950 transition-all duration-300 flex items-center justify-center shadow-sm hover:shadow">
165
  <svg v-show="isSelected(token.email)" class="w-4 h-4 text-white transform scale-0 peer-checked:scale-100 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
166
  <polyline points="20 6 9 17 4 12"></polyline>
167
  </svg>
168
  </div>
169
  </label>
170
  </div>
171
+ <div class="absolute inset-0 bg-white/55 backdrop-blur-md border border-white/40"></div>
172
  <div class="relative p-4 flex flex-col gap-3">
173
  <div class="flex flex-col space-y-2">
174
+ <div class="relative flex items-center rounded-xl border border-slate-200 bg-slate-50/90 px-2 py-1.5">
175
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
176
+ <span class="text-slate-500 min-w-[74px] text-left text-xs font-semibold tracking-wide">EMAIL</span>
177
  <span class="font-medium whitespace-nowrap text-left">{{ token.email }}</span>
178
  </div>
179
+ <button @click="copyToClipboard(token.email)" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity rounded-lg border border-slate-200 bg-white px-2 py-1 text-base hover:bg-slate-100">📋</button>
180
  </div>
181
+ <div class="relative flex items-center rounded-xl border border-slate-200 bg-slate-50/90 px-2 py-1.5">
182
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
183
+ <span class="text-slate-500 min-w-[74px] text-left text-xs font-semibold tracking-wide">PASSWD</span>
184
  <span class="font-medium whitespace-nowrap text-left">{{ token.password }}</span>
185
  </div>
186
+ <button @click="copyToClipboard(token.password)" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity rounded-lg border border-slate-200 bg-white px-2 py-1 text-base hover:bg-slate-100">📋</button>
187
  </div>
188
+ <div class="relative flex items-center rounded-xl border border-slate-200 bg-slate-50/90 px-2 py-1.5">
189
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
190
+ <span class="text-slate-500 min-w-[74px] text-left text-xs font-semibold tracking-wide">TOKEN</span>
191
  <span class="font-medium whitespace-nowrap text-left text-sm">{{ token.token }}</span>
192
  </div>
193
+ <button @click="copyToClipboard(token.token)" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity rounded-lg border border-slate-200 bg-white px-2 py-1 text-base hover:bg-slate-100">📋</button>
194
  </div>
195
+ <div class="relative flex items-center rounded-xl border border-slate-200 bg-slate-50/90 px-2 py-1.5">
196
  <div class="overflow-x-auto scrollbar-hide flex-1 flex items-center space-x-2">
197
+ <span class="text-slate-500 min-w-[74px] text-left text-xs font-semibold tracking-wide">EXPIRE</span>
198
  <span class="font-medium whitespace-nowrap text-left text-sm">{{ new Date(token.expires * 1000).toLocaleString() }}</span>
199
  </div>
200
+ <button @click="copyToClipboard(new Date(token.expires * 1000).toLocaleString())" class="absolute right-2 opacity-0 hover:opacity-100 transition-opacity rounded-lg border border-slate-200 bg-white px-2 py-1 text-base hover:bg-slate-100">📋</button>
201
  </div>
202
  </div>
203
 
 
205
  <button @click="refreshToken(token.email)"
206
  :disabled="refreshingTokens.includes(token.email)"
207
  :class="[
208
+ 'w-full py-2 rounded-lg transition-all duration-300 flex items-center justify-center space-x-2 text-sm border',
209
  refreshingTokens.includes(token.email)
210
  ? 'bg-green-400 text-white refreshing-button-green cursor-not-allowed'
211
+ : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'
212
  ]">
213
  <span v-if="refreshingTokens.includes(token.email)" class="flex items-center space-x-2">
214
  <svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
 
220
  <span v-else>刷新令牌</span>
221
  </button>
222
  <button @click="deleteToken(token.email)"
223
+ class="w-full border border-slate-200 bg-white text-slate-700 py-2 rounded-lg transition-all duration-300 hover:bg-slate-50 text-sm">
224
  删除账号
225
  </button>
226
  </div>
 
239
  <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
240
  <div class="flex flex-wrap items-center gap-2 text-sm text-slate-600">
241
  <span>共 {{ availableModels.length }} 个模型</span>
242
+ <button @click="setActiveModelFilter('all')" :class="['rounded-full px-3 py-1 text-xs font-semibold transition-all duration-200', activeModelFilter === 'all' ? 'bg-slate-950 text-white' : 'border border-slate-200 bg-white text-slate-600 hover:bg-slate-50']">全部 {{ availableModels.length }}</button>
243
+ <button @click="setActiveModelFilter('base')" :class="getFilterBadgeClass('base')">基础 {{ allModelGroups.base.length }}</button>
244
+ <button @click="setActiveModelFilter('thinking')" :class="getFilterBadgeClass('thinking')">Thinking {{ allModelGroups.thinking.length }}</button>
245
+ <button @click="setActiveModelFilter('search')" :class="getFilterBadgeClass('search')">Search {{ allModelGroups.search.length }}</button>
246
+ <button @click="setActiveModelFilter('image')" :class="getFilterBadgeClass('image')">Image {{ allModelGroups.image.length }}</button>
247
+ <button @click="setActiveModelFilter('video')" :class="getFilterBadgeClass('video')">Video {{ allModelGroups.video.length }}</button>
248
+ <button @click="setActiveModelFilter('imageEdit')" :class="getFilterBadgeClass('imageEdit')">Image Edit {{ allModelGroups.imageEdit.length }}</button>
249
  </div>
250
  <div class="flex w-full sm:w-auto gap-2">
251
  <div class="relative w-full sm:w-72">
252
  <input v-model="modelKeyword" type="text" placeholder="搜索模型 ID" class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2 shadow-sm focus:border-violet-400 focus:ring-violet-400 transition-all duration-300">
253
  </div>
254
+ <button @click="refreshModels" :disabled="isLoadingModels" :class="['rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-200 border', isLoadingModels ? 'bg-slate-100 text-slate-400 border-slate-200 cursor-not-allowed' : 'bg-slate-950 text-white border-slate-950 hover:bg-slate-800']">{{ isLoadingModels ? '刷新中...' : '刷新模型列表' }}</button>
255
  </div>
256
  </div>
257
 
258
  <div v-if="isLoadingModels" class="py-12 text-center text-slate-500">正在加载模型列表...</div>
259
+ <div v-else-if="modelsError" class="rounded-2xl border border-slate-200 bg-white px-4 py-8 text-center text-slate-700">
260
  <div class="text-lg font-semibold">模型列表加载失败</div>
261
  <div class="mt-2 text-sm whitespace-pre-line">{{ modelsError }}</div>
262
+ <button @click="refreshModels" class="mt-4 rounded-xl bg-slate-950 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 transition-all duration-200">重新加载</button>
263
  </div>
264
  <div v-else ref="modelsScrollContainer" class="max-h-[70vh] overflow-y-auto pr-2">
265
  <div class="space-y-5">
266
+ <div v-for="group in groupedModelSections" :key="group.key" v-show="group.models.length" class="rounded-3xl border border-slate-200 bg-white/90 p-4 shadow-sm">
267
  <div class="mb-3 flex items-center justify-between gap-3">
268
  <div>
269
  <h3 class="text-lg font-semibold text-slate-800">{{ group.title }}</h3>
270
  <p class="text-xs text-slate-500 mt-1">{{ group.description }}</p>
271
  <p class="mt-2 text-xs text-slate-400">当前分类已按模型强度从高到低排序</p>
272
  </div>
273
+ <span class="rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold text-slate-600">{{ group.models.length }} 个</span>
274
  </div>
275
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
276
+ <div v-for="model in group.models" :key="model.id" class="rounded-2xl border border-slate-200 bg-white px-4 py-4 shadow-sm hover:shadow-md transition-all duration-200">
277
  <div class="flex items-start justify-between gap-3">
278
  <div class="min-w-0 flex-1">
279
  <div class="font-semibold text-slate-800 break-all">{{ model.id }}</div>
280
  <div class="mt-2 text-xs text-slate-500 break-all">模型名称:{{ getModelDisplayName(model) }}</div>
281
  <div class="mt-2 flex flex-wrap gap-2">
282
+ <span class="rounded-full border border-slate-200 bg-slate-50 px-2 py-1 text-xs text-slate-600">{{ model.owned_by || 'unknown' }}</span>
283
+ <span v-if="modelTagSummary(model).length" class="rounded-full border border-slate-200 bg-slate-50 px-2 py-1 text-xs text-slate-600">{{ modelTagSummary(model).join(' / ') }}</span>
284
+ <span class="rounded-full border border-slate-200 bg-slate-50 px-2 py-1 text-xs text-slate-600">推荐:{{ getModelUseCase(model) }}</span>
285
  <div class="relative group/tooltip inline-flex">
286
+ <span class="cursor-help rounded-full border border-slate-200 bg-slate-900 px-2 py-1 text-xs text-white">强度:{{ getModelPriorityLabel(model, group.key) }}</span>
287
+ <div class="pointer-events-none absolute left-0 top-full z-20 mt-2 hidden w-64 rounded-2xl border border-slate-200 bg-white/95 p-3 text-xs text-slate-600 shadow-xl backdrop-blur-sm group-hover/tooltip:block">
288
+ <div class="font-semibold text-slate-900">强度说明</div>
289
  <div class="mt-2 leading-5 whitespace-pre-line">{{ getModelPriorityTooltip(model, group.key) }}</div>
290
  </div>
291
  </div>
 
293
  </div>
294
  </div>
295
  <div class="mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3">
296
+ <button @click="copyToClipboard(model.id)" class="rounded-lg border border-slate-200 bg-white px-2 py-2 text-sm text-slate-700 hover:bg-slate-50 transition-all duration-200">复制 ID</button>
297
+ <button @click="copyToClipboard(getModelDisplayName(model))" class="rounded-lg border border-slate-200 bg-white px-2 py-2 text-sm text-slate-700 hover:bg-slate-50 transition-all duration-200">复制名字</button>
298
+ <button @click="copyModelRequestExample(model)" class="col-span-2 sm:col-span-1 rounded-lg bg-slate-950 px-2 py-2 text-sm text-white hover:bg-slate-800 transition-all duration-200">复制示例</button>
299
  </div>
300
  </div>
301
  </div>
 
313
  </div>
314
  <div class="flex flex-wrap items-center gap-2">
315
  <button @click="fetchLogs" class="rounded-xl bg-slate-800 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-900 transition-all duration-300">刷新日志</button>
316
+ <button @click="toggleAutoRefreshLogs" :class="['rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-200 border', logsAutoRefresh ? 'border-slate-950 bg-slate-950 text-white hover:bg-slate-800' : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50']">
317
  {{ logsAutoRefresh ? '停止自动刷新' : '自动刷新' }}
318
  </button>
319
+ <button @click="downloadLogs" class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-all duration-200">下载日志</button>
320
+ <button @click="copyLogs" class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-all duration-200">复制日志</button>
321
+ <button @click="clearLogs" class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-all duration-200">清空日志</button>
322
  </div>
323
  </div>
324
 
 
352
  <div class="flex flex-wrap items-center gap-3">
353
  <div class="inline-flex items-center gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 shadow-sm">
354
  <span>运行日志</span>
355
+ <button @click="toggleRuntimeLog" :class="['rounded-full px-3 py-1 text-xs font-semibold transition-all duration-200', runtimeLogEnabled ? 'bg-slate-950 text-white hover:bg-slate-800' : 'bg-slate-200 text-slate-700 hover:bg-slate-300']">
356
+ {{ runtimeLogEnabled ? '已启用' : '已关闭' }}
357
+ </button>
358
  </div>
359
  <div class="inline-flex items-center gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 shadow-sm">
360
  <span>自动滚底</span>
361
+ <button @click="logsAutoScroll = !logsAutoScroll" :class="['rounded-full px-3 py-1 text-xs font-semibold transition-all duration-200', logsAutoScroll ? 'bg-slate-950 text-white hover:bg-slate-800' : 'bg-slate-200 text-slate-700 hover:bg-slate-300']">
362
+ {{ logsAutoScroll ? '已开启' : '已关闭' }}
363
+ </button>
364
  </div>
365
  </div>
366
  </div>
 
372
  </div>
373
 
374
  <div class="rounded-3xl border border-slate-200 bg-slate-950 text-slate-100 shadow-inner overflow-hidden">
375
+ <div ref="logsScrollContainer" class="max-h-[70vh] overflow-y-auto p-4 font-mono text-xs leading-6 break-words">
376
+ <div v-if="isLoadingLogs" class="text-slate-400">正在加载日志...</div>
377
+ <div v-else-if="logsError" class="text-red-300">{{ logsError }}</div>
378
+ <div v-else-if="!logs.length" class="text-slate-500">当前没有可显示的日志</div>
379
+ <div v-else class="space-y-1" v-html="formattedLogsHtml"></div>
380
+ </div>
381
+ </div>
382
+ </div>
383
+
384
+ <div v-else-if="activeDashboardTab === 'usage'">
385
+ <div class="mb-6 flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
386
+ <div>
387
+ <h2 class="text-2xl font-bold text-slate-800">使用统计</h2>
388
+ <p class="mt-2 text-sm text-slate-500">按模型查看请求次数、Token 数量和成功率</p>
389
+ </div>
390
+ <div class="flex flex-wrap items-center gap-2">
391
+ <input v-model="usageKeyword" type="text" placeholder="搜索模型名称" class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 shadow-sm focus:border-slate-400 focus:ring-slate-400 transition-all duration-300">
392
+ <button @click="usageOnlyToday = !usageOnlyToday" :class="['rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-200 border', usageOnlyToday ? 'border-slate-950 bg-slate-950 text-white hover:bg-slate-800' : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50']">{{ usageOnlyToday ? '仅看今天中' : '仅看今天' }}</button>
393
+ <button @click="usageOnlyFailed = !usageOnlyFailed" :class="['rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-200 border', usageOnlyFailed ? 'border-slate-950 bg-slate-950 text-white hover:bg-slate-800' : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50']">{{ usageOnlyFailed ? '仅失败中' : '仅看失败模型' }}</button>
394
+ <select v-model="usageSortBy" class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 shadow-sm focus:border-slate-400 focus:ring-slate-400 transition-all duration-300">
395
+ <option value="totalTokens">按总 Token 排序</option>
396
+ <option value="requests">按请求次数排序</option>
397
+ </select>
398
+ <button @click="fetchUsageStats" class="rounded-xl bg-slate-800 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-900 transition-all duration-300">刷新统计</button>
399
+ <button @click="resetUsageStats" class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-all duration-200">清空统计</button>
400
+ </div>
401
+ </div>
402
+
403
+ <div class="grid gap-4 md:grid-cols-2 xl:grid-cols-7 mb-6">
404
+ <div class="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm"><div class="text-xs text-slate-500">总请求数</div><div class="mt-2 text-2xl font-bold text-slate-800">{{ usageSummary.requests }}</div></div>
405
+ <div class="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm"><div class="text-xs text-slate-500">成功请求</div><div class="mt-2 text-2xl font-bold text-slate-800">{{ usageSummary.success }}</div></div>
406
+ <div class="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm"><div class="text-xs text-slate-500">失败请求</div><div class="mt-2 text-2xl font-bold text-slate-800">{{ usageSummary.failed }}</div></div>
407
+ <div class="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm"><div class="text-xs text-slate-500">总 Token</div><div class="mt-2 text-2xl font-bold text-slate-800">{{ formatNumber(usageSummary.totalTokens) }}</div></div>
408
+ <div class="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm"><div class="text-xs text-slate-500">成功率</div><div class="mt-2 text-2xl font-bold text-slate-800">{{ usageSummary.successRate }}%</div></div>
409
+ <div class="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm"><div class="text-xs text-slate-500">今日请求</div><div class="mt-2 text-2xl font-bold text-slate-800">{{ usageSummary.today?.requests || 0 }}</div></div>
410
+ <div class="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm"><div class="text-xs text-slate-500">今日 Token</div><div class="mt-2 text-2xl font-bold text-slate-800">{{ formatNumber(usageSummary.today?.totalTokens || 0) }}</div></div>
411
+ </div>
412
+
413
+ <div class="mb-6 grid gap-6 xl:grid-cols-2">
414
+ <div class="rounded-3xl border border-slate-200 bg-white/80 p-5 shadow-sm">
415
+ <div class="mb-4">
416
+ <h3 class="text-lg font-semibold text-slate-800">今日请求趋势</h3>
417
+ <p class="mt-1 text-xs text-slate-500">按小时统计请求次数</p>
418
+ </div>
419
+ <div class="grid grid-cols-12 md:grid-cols-24 gap-2 items-end h-52 rounded-2xl border border-slate-200 bg-slate-50/70 p-4">
420
+ <div v-for="item in usageTrendData" :key="`requests-${item.hour}`" class="flex flex-col items-center justify-end gap-2 h-full">
421
+ <div class="w-full rounded-t-md bg-slate-900 min-h-[8px] shadow-[0_8px_20px_rgba(15,23,42,0.08)]" :style="{ height: item.requestHeight }"></div>
422
+ <div class="text-[10px] text-slate-400 tracking-wide">{{ String(item.hour).padStart(2, '0') }}</div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+
427
+ <div class="rounded-3xl border border-slate-200 bg-white/80 p-5 shadow-sm">
428
+ <div class="mb-4">
429
+ <h3 class="text-lg font-semibold text-slate-800">今日 Token 趋势</h3>
430
+ <p class="mt-1 text-xs text-slate-500">按小时统计 Token 消耗</p>
431
+ </div>
432
+ <div class="grid grid-cols-12 md:grid-cols-24 gap-2 items-end h-52 rounded-2xl border border-slate-200 bg-slate-50/70 p-4">
433
+ <div v-for="item in usageTrendData" :key="`tokens-${item.hour}`" class="flex flex-col items-center justify-end gap-2 h-full">
434
+ <div class="w-full rounded-t-md bg-slate-500 min-h-[8px] shadow-[0_8px_20px_rgba(15,23,42,0.06)]" :style="{ height: item.tokenHeight }"></div>
435
+ <div class="text-[10px] text-slate-400 tracking-wide">{{ String(item.hour).padStart(2, '0') }}</div>
436
+ </div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+
441
+ <div class="rounded-3xl border border-slate-200 bg-white/80 shadow-sm overflow-hidden">
442
+ <div class="max-h-[60vh] overflow-auto">
443
+ <table class="min-w-full text-sm">
444
+ <thead class="sticky top-0 z-10 bg-slate-100 text-slate-600">
445
+ <tr>
446
+ <th class="px-4 py-3.5 text-left text-xs font-semibold uppercase tracking-wide">模型名称</th>
447
+ <th class="px-4 py-3.5 text-right text-xs font-semibold uppercase tracking-wide">请求次数</th>
448
+ <th class="px-4 py-3.5 text-right text-xs font-semibold uppercase tracking-wide">Prompt Tokens</th>
449
+ <th class="px-4 py-3.5 text-right text-xs font-semibold uppercase tracking-wide">Completion Tokens</th>
450
+ <th class="px-4 py-3.5 text-right text-xs font-semibold uppercase tracking-wide">总 Token</th>
451
+ <th class="px-4 py-3.5 text-right text-xs font-semibold uppercase tracking-wide">成功率</th>
452
+ <th class="px-4 py-3.5 text-left text-xs font-semibold uppercase tracking-wide">最近使用</th>
453
+ </tr>
454
+ </thead>
455
+ <tbody>
456
+ <tr v-if="isLoadingUsageStats">
457
+ <td colspan="7" class="px-4 py-10 text-center text-slate-500">正在加载使用统计...</td>
458
+ </tr>
459
+ <tr v-else-if="usageStatsError">
460
+ <td colspan="7" class="px-4 py-10 text-center text-red-500">{{ usageStatsError }}</td>
461
+ </tr>
462
+ <tr v-else-if="!sortedUsageModels.length">
463
+ <td colspan="7" class="px-4 py-10 text-center text-slate-500">暂无统计数据</td>
464
+ </tr>
465
+ <tr v-for="item in sortedUsageModels" :key="item.model" class="border-t border-slate-100 hover:bg-slate-50/70">
466
+ <td class="px-4 py-3 font-medium text-slate-800 break-all">{{ item.model }}</td>
467
+ <td class="px-4 py-3 text-right text-slate-600">{{ formatNumber(item.requests) }}</td>
468
+ <td class="px-4 py-3 text-right text-slate-600">{{ formatNumber(item.promptTokens) }}</td>
469
+ <td class="px-4 py-3 text-right text-slate-600">{{ formatNumber(item.completionTokens) }}</td>
470
+ <td class="px-4 py-3 text-right font-semibold text-slate-800">{{ formatNumber(item.totalTokens) }}</td>
471
+ <td class="px-4 py-3 text-right"><span class="rounded-full border border-slate-200 bg-slate-50 px-2 py-1 text-xs font-semibold text-slate-700">{{ item.successRate }}%</span></td>
472
+ <td class="px-4 py-3 text-slate-500">{{ item.lastUsedAt ? new Date(item.lastUsedAt).toLocaleString() : '-' }}</td>
473
+ </tr>
474
+ </tbody>
475
+ </table>
476
  </div>
477
  </div>
478
  </div>
479
 
480
+ <div v-else>
481
+ <div class="mb-6">
482
+ <h2 class="text-2xl font-bold text-slate-800">系统设置</h2>
483
+ <p class="mt-2 text-sm text-slate-500">直接在当前仪表盘中维护 API Key 与系统行为配置</p>
484
+ </div>
485
+
486
+ <div class="grid grid-cols-1 gap-6">
487
+ <div class="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm">
488
+ <div class="flex items-center justify-between gap-4 mb-4">
489
+ <div>
490
+ <h3 class="text-lg font-semibold text-slate-800">API Key 管理</h3>
491
+ <p class="mt-1 text-sm text-slate-500">管理员密钥只读,普通密钥可动态增删</p>
492
+ </div>
493
+ <button @click="showAddKeyModal = true" class="vc-button-primary">添加密钥</button>
494
+ </div>
495
+
496
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 p-4 mb-4">
497
+ <div class="mb-2 flex items-center gap-2">
498
+ <span class="font-semibold text-slate-800">管理员密钥</span>
499
+ <span class="rounded-full bg-slate-200 px-2 py-1 text-xs text-slate-700">不可修改</span>
500
+ </div>
501
+ <input :value="settings.adminKey" type="text" readonly class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700">
502
+ </div>
503
+
504
+ <div class="space-y-3">
505
+ <div v-if="settings.regularKeys.length === 0" class="rounded-2xl border border-dashed border-slate-200 bg-slate-50 p-6 text-center text-sm text-slate-500">暂无普通密钥</div>
506
+ <div v-for="(key, index) in settings.regularKeys" :key="index" class="flex flex-col gap-3 rounded-2xl border border-slate-200 bg-slate-50 p-4 md:flex-row md:items-center">
507
+ <input :value="key" type="text" readonly class="flex-1 rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700">
508
+ <button @click="deleteRegularKey(index)" class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-all duration-200">删除</button>
509
+ </div>
510
+ </div>
511
+ </div>
512
+
513
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
514
+ <div class="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm">
515
+ <h3 class="text-lg font-semibold text-slate-800">自动刷新</h3>
516
+ <div class="mt-4 flex items-center gap-3">
517
+ <input v-model="settings.autoRefresh" type="checkbox" class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
518
+ <span class="text-sm text-slate-600">启用自动刷新</span>
519
+ </div>
520
+ <label class="mt-4 block text-sm text-slate-500">刷新间隔(秒)</label>
521
+ <input v-model.number="settings.autoRefreshInterval" type="number" class="mt-2 w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-700">
522
+ <button @click="saveAutoRefresh" class="mt-4 w-full rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white hover:bg-slate-800 transition-all duration-300">保存</button>
523
+ </div>
524
+
525
+ <div class="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm">
526
+ <h3 class="text-lg font-semibold text-slate-800">思考输出</h3>
527
+ <div class="mt-4 flex items-center gap-3">
528
+ <input v-model="settings.outThink" type="checkbox" class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
529
+ <span class="text-sm text-slate-600">启用思考输出</span>
530
+ </div>
531
+ <button @click="saveOutThink" class="mt-4 w-full rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white hover:bg-slate-800 transition-all duration-300">保存</button>
532
+ </div>
533
+
534
+ <div class="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm">
535
+ <h3 class="text-lg font-semibold text-slate-800">搜索信息显示模式</h3>
536
+ <select v-model="settings.searchInfoMode" class="mt-4 w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-700">
537
+ <option value="table">表格模式</option>
538
+ <option value="text">文本模式</option>
539
+ </select>
540
+ <button @click="saveSearchInfoMode" class="mt-4 w-full rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white hover:bg-slate-800 transition-all duration-300">保存</button>
541
+ </div>
542
+
543
+ <div class="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm">
544
+ <h3 class="text-lg font-semibold text-slate-800">简化模型映射</h3>
545
+ <div class="mt-4 flex items-start gap-3">
546
+ <input v-model="settings.simpleModelMap" type="checkbox" class="mt-1 h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
547
+ <span class="text-sm leading-6 text-slate-600">只返回基础模型,不包含 thinking、search、image 等变体</span>
548
+ </div>
549
+ <button @click="saveSimpleModelMap" class="mt-4 w-full rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white hover:bg-slate-800 transition-all duration-300">保存</button>
550
+ </div>
551
+ </div>
552
  </div>
553
  </div>
554
  </section>
555
  </div>
556
  </div>
557
 
558
+ <div v-if="showAddKeyModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50" @click.self="showAddKeyModal = false">
559
+ <div class="vc-shell w-96 max-w-[90vw] p-6">
560
+ <h3 class="text-lg font-semibold text-slate-800 mb-4">添加普通 API Key</h3>
561
+ <input v-model="newApiKey" type="text" placeholder="请输入 API Key" class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm text-slate-700 mb-4">
562
+ <div class="flex justify-end gap-3">
563
+ <button @click="showAddKeyModal = false" class="vc-button-secondary">取消</button>
564
+ <button @click="addRegularKey" class="vc-button-primary">添加</button>
565
+ </div>
566
+ </div>
567
+ </div>
568
+
569
  <!-- 删除全部确认对话框 -->
570
  <div v-if="showDeleteAllConfirm"
571
  class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
572
  @click.self="showDeleteAllConfirm = false">
573
+ <div class="vc-shell w-11/12 max-w-md p-6">
574
+ <h2 class="text-2xl font-bold text-slate-950 mb-4">危险操作确认</h2>
575
+ <p class="text-slate-600 mb-6">您确定要删除<span class="font-bold text-slate-950">全部 {{ totalItems }} 个</span>账号吗?此操作不可恢复</p>
576
  <div class="flex justify-end space-x-4">
577
+ <button @click="showDeleteAllConfirm = false" class="vc-button-secondary">
 
578
  取消
579
  </button>
580
+ <button @click="deleteAllAccounts" class="vc-button-primary bg-slate-950 hover:bg-slate-800">
 
581
  确认删除
582
  </button>
583
  </div>
 
588
  <div v-if="showAddModal"
589
  class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
590
  @click.self="showAddModal = false">
591
+ <div class="vc-shell w-11/12 max-w-md p-6">
592
+ <div class="mb-6 grid grid-cols-2 gap-2 rounded-2xl border border-slate-200 bg-slate-50 p-1">
593
+ <button :class="['rounded-2xl py-2 text-sm font-medium transition-all duration-200', addMode==='single' ? 'bg-slate-950 text-white shadow-sm' : 'text-slate-500 hover:bg-white']" @click="addMode='single'">单账号添加</button>
594
+ <button :class="['rounded-2xl py-2 text-sm font-medium transition-all duration-200', addMode==='batch' ? 'bg-slate-950 text-white shadow-sm' : 'text-slate-500 hover:bg-white']" @click="addMode='batch'">批量添加</button>
595
  </div>
596
  <transition name="fade" mode="out-in">
597
  <div v-if="addMode==='single'" key="single">
598
  <h2 class="text-xl font-bold mb-4">添加账号</h2>
599
  <div class="space-y-4">
600
  <div>
601
+ <label class="block text-sm font-medium text-slate-700">Email</label>
602
+ <input v-model="newAccount.email" type="email" class="vc-input mt-1 h-12">
 
603
  </div>
604
  <div>
605
+ <label class="block text-sm font-medium text-slate-700">Password</label>
606
+ <input v-model="newAccount.password" type="password" class="vc-input mt-1 h-12">
 
607
  </div>
608
  <div class="flex justify-end space-x-4 pt-4">
609
+ <button @click="showAddModal = false" class="vc-button-secondary">
 
610
  取消
611
  </button>
612
+ <button @click="addToken" class="vc-button-primary">
 
613
  添加
614
  </button>
615
  </div>
 
619
  <h2 class="text-xl font-bold mb-4 px-4">批量添加账号</h2>
620
  <div class="space-y-4">
621
  <div>
622
+ <label class="block text-sm font-medium text-slate-700 px-4 pb-2">账号列表(每行一个,格式:email:password)</label>
623
+ <textarea v-model="batchAccounts" rows="6" class="vc-input mt-1 h-36 resize-none"></textarea>
624
  </div>
625
  <div class="flex justify-end space-x-4 pt-4">
626
+ <button @click="showAddModal = false" class="vc-button-secondary">
 
627
  取消
628
  </button>
629
+ <button @click="addBatchTokens" class="vc-button-primary">
 
630
  批量添加
631
  </button>
632
  </div>
 
640
  <div v-if="toast.show"
641
  :class="[
642
  'fixed top-4 right-4 z-50 px-6 py-4 rounded-xl shadow-lg transform transition-all duration-300',
643
+ toast.type === 'success' ? 'bg-slate-950 text-white' : 'bg-slate-700 text-white'
644
  ]">
645
  <div class="flex items-center space-x-2">
646
  <svg v-if="toast.type === 'success'" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
 
671
  password: ''
672
  })
673
  const batchAccounts = ref('')
674
+ const settings = ref({
675
+ apiKey: localStorage.getItem('apiKey') || '',
676
+ adminKey: '',
677
+ regularKeys: [],
678
+ defaultHeaders: '',
679
+ defaultCookie: '',
680
+ autoRefresh: false,
681
+ autoRefreshInterval: 21600,
682
+ outThink: false,
683
+ searchInfoMode: 'table',
684
+ simpleModelMap: false
685
+ })
686
+ const showAddKeyModal = ref(false)
687
+ const newApiKey = ref('')
688
 
689
  // 分页相关
690
  const displayedTokens = ref([])
 
722
  const runtimeLogEnabled = ref(true)
723
  const logsAutoScroll = ref(true)
724
  const logsScrollContainer = ref(null)
725
+ const siteBaseUrl = ref(typeof window !== 'undefined' ? window.location.origin : '')
726
+ const connectionStatus = ref('checking')
727
+ const isCheckingConnection = ref(false)
728
+ const usageSummary = ref({ requests: 0, success: 0, failed: 0, totalTokens: 0, successRate: 0 })
729
+ const usageModels = ref([])
730
+ const isLoadingUsageStats = ref(false)
731
+ const usageStatsError = ref('')
732
+ const usageSortBy = ref('totalTokens')
733
+ const usageKeyword = ref('')
734
+ const usageOnlyToday = ref(false)
735
+ const usageOnlyFailed = ref(false)
736
  let logsRefreshTimer = null
737
 
738
  // Toast 通知
 
742
  type: 'success'
743
  })
744
 
745
+ const connectionStatusLabel = computed(() => {
746
+ if (connectionStatus.value === 'connected') return '已连接'
747
+ if (connectionStatus.value === 'failed') return '连接失败'
748
+ return '连接检测中'
749
+ })
750
+
751
  const filteredLogs = computed(() => {
752
  const keyword = logsKeyword.value.trim().toLowerCase()
753
 
 
774
 
775
  const formattedLogs = computed(() => filteredLogs.value.map(item => item.text).join('\n'))
776
 
777
+ const escapeHtml = (value) => String(value)
778
+ .replace(/&/g, '&amp;')
779
+ .replace(/</g, '&lt;')
780
+ .replace(/>/g, '&gt;')
781
+
782
+ const highlightLogLine = (text) => {
783
+ let line = escapeHtml(text)
784
+ line = line.replace(/\[(ERROR|WARN|INFO|DEBUG)\]/g, '<span class="inline-block rounded-md border border-slate-700 bg-slate-800 px-1.5 py-0.5 text-[10px] font-semibold text-white">[$1]</span>')
785
+ line = line.replace(/\[(SERVER|ACCOUNT|CHAT|CONFIG|CLI|SSXMOD|AUTO|REQUEST|PARSER|USAGE)\]/g, '<span class="inline-block rounded-md border border-slate-300 bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold text-slate-700">[$1]</span>')
786
+ return `<div>${line}</div>`
787
+ }
788
+
789
+ const formattedLogsHtml = computed(() => filteredLogs.value.map(item => highlightLogLine(item.text)).join(''))
790
+
791
  const filteredModels = computed(() => {
792
  const keyword = modelKeyword.value.trim().toLowerCase()
793
  if (!keyword) {
 
961
  }
962
 
963
  const getSidebarItemClass = (tabKey, tone) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
  return [
965
+ 'w-full rounded-2xl border px-4 py-3 text-left text-sm font-medium transition-all duration-200',
966
+ activeDashboardTab.value === tabKey
967
+ ? 'border-slate-900 bg-slate-950 text-white shadow-[0_10px_30px_rgba(15,23,42,0.18)]'
968
+ : 'border-slate-200 bg-white/90 text-slate-700 hover:bg-slate-50 hover:border-slate-300'
969
  ]
970
  }
971
 
972
  const getActionSidebarClass = (tone, disabled = false) => {
 
 
 
 
 
 
 
973
  return [
974
+ 'w-full rounded-2xl border px-4 py-3 text-left text-sm font-medium transition-all duration-200',
975
+ disabled ? 'cursor-not-allowed bg-slate-100 text-slate-400 border-slate-200' : 'border-slate-200 bg-white/90 text-slate-700 hover:bg-slate-50 hover:border-slate-300'
976
  ]
977
  }
978
 
 
984
  router.push('/settings')
985
  }
986
 
987
+ const refreshConnectionStatus = async () => {
988
+ isCheckingConnection.value = true
989
+ connectionStatus.value = 'checking'
990
+
991
+ try {
992
+ await axios.get('/models', { timeout: 10000 })
993
+ connectionStatus.value = 'connected'
994
+ showToast('连接状态已刷新')
995
+ } catch (error) {
996
+ console.error('刷新连接状态失败:', error)
997
+ connectionStatus.value = 'failed'
998
+ showToast('连接检测失败', 'error')
999
+ } finally {
1000
+ isCheckingConnection.value = false
1001
+ }
1002
+ }
1003
+
1004
+ const logout = () => {
1005
+ localStorage.removeItem('apiKey')
1006
+ router.replace('/auth')
1007
+ }
1008
+
1009
+ const formatNumber = (value) => {
1010
+ return new Intl.NumberFormat('zh-CN').format(value || 0)
1011
+ }
1012
+
1013
+ const sortedUsageModels = computed(() => {
1014
+ const keyword = usageKeyword.value.trim().toLowerCase()
1015
+ const items = usageModels.value.filter(item => {
1016
+ const source = usageOnlyToday.value ? (item.today || {}) : item
1017
+ const matchKeyword = !keyword || item.model.toLowerCase().includes(keyword)
1018
+ const matchFailed = !usageOnlyFailed.value || (source.failed || 0) > 0
1019
+ const matchToday = !usageOnlyToday.value || (source.requests || 0) > 0
1020
+ return matchKeyword && matchFailed && matchToday
1021
+ }).map(item => {
1022
+ if (!usageOnlyToday.value) {
1023
+ return item
1024
+ }
1025
+
1026
+ const today = item.today || { requests: 0, success: 0, failed: 0, totalTokens: 0 }
1027
+ const successRate = today.requests > 0 ? Number(((today.success / today.requests) * 100).toFixed(2)) : 0
1028
+
1029
+ return {
1030
+ ...item,
1031
+ requests: today.requests || 0,
1032
+ success: today.success || 0,
1033
+ failed: today.failed || 0,
1034
+ totalTokens: today.totalTokens || 0,
1035
+ promptTokens: today.totalTokens || 0,
1036
+ completionTokens: 0,
1037
+ successRate
1038
+ }
1039
+ })
1040
+
1041
+ if (usageSortBy.value === 'requests') {
1042
+ return items.sort((a, b) => b.requests - a.requests || b.totalTokens - a.totalTokens)
1043
+ }
1044
+
1045
+ return items.sort((a, b) => b.totalTokens - a.totalTokens || b.requests - a.requests)
1046
+ })
1047
+
1048
+ const usageTrendData = computed(() => {
1049
+ const hourly = Array.isArray(usageSummary.value.today?.hourly) ? usageSummary.value.today.hourly : []
1050
+ const buckets = Array.from({ length: 24 }, (_, hour) => {
1051
+ const item = hourly.find(entry => entry.hour === hour) || { hour, requests: 0, totalTokens: 0 }
1052
+ return item
1053
+ })
1054
+
1055
+ const maxRequests = Math.max(1, ...buckets.map(item => item.requests || 0))
1056
+ const maxTokens = Math.max(1, ...buckets.map(item => item.totalTokens || 0))
1057
+
1058
+ return buckets.map(item => ({
1059
+ ...item,
1060
+ requestHeight: `${Math.max(8, Math.round(((item.requests || 0) / maxRequests) * 100))}%`,
1061
+ tokenHeight: `${Math.max(8, Math.round(((item.totalTokens || 0) / maxTokens) * 100))}%`
1062
+ }))
1063
+ })
1064
+
1065
+ const fetchUsageStats = async () => {
1066
+ isLoadingUsageStats.value = true
1067
+ usageStatsError.value = ''
1068
+ try {
1069
+ const response = await axios.get('/api/usage-stats', {
1070
+ headers: {
1071
+ 'Authorization': localStorage.getItem('apiKey') || ''
1072
+ }
1073
+ })
1074
+ usageSummary.value = response.data?.summary || usageSummary.value
1075
+ usageModels.value = Array.isArray(response.data?.models) ? response.data.models : []
1076
+ } catch (error) {
1077
+ console.error('获取使用统计失败:', error)
1078
+ usageStatsError.value = error.response?.data?.error || error.message || '获取使用统计失败'
1079
+ } finally {
1080
+ isLoadingUsageStats.value = false
1081
+ }
1082
+ }
1083
+
1084
+ const loadSettings = async () => {
1085
+ try {
1086
+ const res = await axios.get('/api/settings', {
1087
+ headers: {
1088
+ 'Authorization': localStorage.getItem('apiKey') || ''
1089
+ }
1090
+ })
1091
+ settings.value.apiKey = res.data.apiKey
1092
+ settings.value.adminKey = res.data.adminKey || ''
1093
+ settings.value.regularKeys = res.data.regularKeys || []
1094
+ settings.value.defaultHeaders = JSON.stringify(res.data.defaultHeaders)
1095
+ settings.value.defaultCookie = res.data.defaultCookie
1096
+ settings.value.autoRefresh = res.data.autoRefresh
1097
+ settings.value.autoRefreshInterval = res.data.autoRefreshInterval
1098
+ settings.value.outThink = res.data.outThink
1099
+ settings.value.searchInfoMode = res.data.searchInfoMode
1100
+ settings.value.simpleModelMap = res.data.simpleModelMap
1101
+ } catch (error) {
1102
+ console.error('加载设置失败:', error)
1103
+ showToast('加载设置失败: ' + (error.response?.data?.error || error.message), 'error')
1104
+ }
1105
+ }
1106
+
1107
+ const saveAutoRefresh = async () => {
1108
+ try {
1109
+ await axios.post('/api/setAutoRefresh', {
1110
+ autoRefresh: settings.value.autoRefresh,
1111
+ autoRefreshInterval: settings.value.autoRefreshInterval
1112
+ }, {
1113
+ headers: { 'Authorization': localStorage.getItem('apiKey') || '' }
1114
+ })
1115
+ showToast('自动刷新设置保存成功')
1116
+ } catch (error) {
1117
+ showToast('自动刷新设置保存失败: ' + (error.response?.data?.error || error.message), 'error')
1118
+ }
1119
+ }
1120
+
1121
+ const saveOutThink = async () => {
1122
+ try {
1123
+ await axios.post('/api/setOutThink', { outThink: settings.value.outThink }, {
1124
+ headers: { 'Authorization': localStorage.getItem('apiKey') || '' }
1125
+ })
1126
+ showToast('思考输出设置保存成功')
1127
+ } catch (error) {
1128
+ showToast('思考输出设置保存失败: ' + (error.response?.data?.error || error.message), 'error')
1129
+ }
1130
+ }
1131
+
1132
+ const saveSearchInfoMode = async () => {
1133
+ try {
1134
+ await axios.post('/api/search-info-mode', { searchInfoMode: settings.value.searchInfoMode }, {
1135
+ headers: { 'Authorization': localStorage.getItem('apiKey') || '' }
1136
+ })
1137
+ showToast('搜索信息模式保存成功')
1138
+ } catch (error) {
1139
+ showToast('搜索信息模式保存失败: ' + (error.response?.data?.error || error.message), 'error')
1140
+ }
1141
+ }
1142
+
1143
+ const saveSimpleModelMap = async () => {
1144
+ try {
1145
+ await axios.post('/api/simple-model-map', { simpleModelMap: settings.value.simpleModelMap }, {
1146
+ headers: { 'Authorization': localStorage.getItem('apiKey') || '' }
1147
+ })
1148
+ showToast('简化模型映射设置保存成功')
1149
+ } catch (error) {
1150
+ showToast('简化模型映射设置保存失败: ' + (error.response?.data?.error || error.message), 'error')
1151
+ }
1152
+ }
1153
+
1154
+ const addRegularKey = async () => {
1155
+ if (!newApiKey.value.trim()) {
1156
+ showToast('请输入 API Key', 'error')
1157
+ return
1158
+ }
1159
+
1160
+ try {
1161
+ await axios.post('/api/addRegularKey', { apiKey: newApiKey.value.trim() }, {
1162
+ headers: { 'Authorization': localStorage.getItem('apiKey') || '' }
1163
+ })
1164
+ newApiKey.value = ''
1165
+ showAddKeyModal.value = false
1166
+ await loadSettings()
1167
+ showToast('API Key 添加成功')
1168
+ } catch (error) {
1169
+ showToast('API Key 添加失���: ' + (error.response?.data?.error || error.message), 'error')
1170
+ }
1171
+ }
1172
+
1173
+ const deleteRegularKey = async (index) => {
1174
+ if (!confirm('确定要删除此 API Key 吗?')) return
1175
+
1176
+ const keyToDelete = settings.value.regularKeys[index]
1177
+ try {
1178
+ await axios.post('/api/deleteRegularKey', { apiKey: keyToDelete }, {
1179
+ headers: { 'Authorization': localStorage.getItem('apiKey') || '' }
1180
+ })
1181
+ await loadSettings()
1182
+ showToast('API Key 删除成功')
1183
+ } catch (error) {
1184
+ showToast('API Key 删除失败: ' + (error.response?.data?.error || error.message), 'error')
1185
+ }
1186
+ }
1187
+
1188
+ const resetUsageStats = async () => {
1189
+ if (!confirm('确定要清空全部使用统计吗?')) return
1190
+
1191
+ try {
1192
+ await axios.post('/api/usage-stats/reset', {}, {
1193
+ headers: {
1194
+ 'Authorization': localStorage.getItem('apiKey') || ''
1195
+ }
1196
+ })
1197
+ await fetchUsageStats()
1198
+ showToast('使用统计已清空')
1199
+ } catch (error) {
1200
+ console.error('清空使用统计失败:', error)
1201
+ showToast('清空使用统计失败: ' + (error.response?.data?.error || error.message), 'error')
1202
+ }
1203
+ }
1204
+
1205
+ const openUsagePanel = async () => {
1206
+ activeDashboardTab.value = 'usage'
1207
+ await fetchUsageStats()
1208
+ }
1209
+
1210
  const fetchLogs = async () => {
1211
  isLoadingLogs.value = true
1212
  logsError.value = ''
 
1326
  }
1327
  }
1328
 
1329
+ const getFilterBadgeClass = (filterKey) => {
1330
  return [
1331
+ 'rounded-full px-3 py-1 text-xs font-semibold transition-all duration-200 border',
1332
  activeModelFilter.value === filterKey
1333
+ ? 'border-slate-950 bg-slate-950 text-white'
1334
+ : 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'
1335
  ]
1336
  }
1337
 
 
1812
 
1813
  onMounted(() => {
1814
  getTokens()
1815
+ loadSettings()
1816
+ refreshConnectionStatus()
1817
  })
1818
  </script>
1819
 
 
1837
  }
1838
 
1839
  .token-card {
1840
+ background: rgba(255, 255, 255, 0.9);
1841
+ border: 1px solid rgba(15, 23, 42, 0.08);
1842
+ box-shadow: 0 8px 28px rgba(15, 23, 42, 0.05);
1843
  transform: translateY(0);
1844
+ transition: all 0.2s ease;
1845
  }
1846
 
1847
  .token-card:hover {
1848
+ transform: translateY(-3px);
1849
+ border-color: rgba(15, 23, 42, 0.14);
1850
+ box-shadow: 0 18px 44px rgba(15, 23, 42, 0.08);
1851
  }
1852
 
1853
  .scrollbar-hide {
 
1943
  left: 0;
1944
  width: 0;
1945
  height: 100%;
1946
+ background: rgba(15, 23, 42, 0.06);
1947
  transition: width 0.3s ease;
1948
  }
1949
 
 
1971
  /* 给选中的卡片添加动画效果 */
1972
  .token-card.ring-2 {
1973
  animation: selected-pulse 2s infinite;
1974
+ border-color: rgba(15, 23, 42, 0.26);
1975
  }
1976
 
1977
  @keyframes selected-pulse {
1978
  0% {
1979
+ box-shadow: 0 0 0 0 rgba(15, 23, 42, 0.18);
1980
  }
1981
  70% {
1982
+ box-shadow: 0 0 0 6px rgba(15, 23, 42, 0);
1983
  }
1984
  100% {
1985
  box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
1986
  }
1987
  }
1988
 
1989
+ @keyframes refresh-pulse-neutral {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1990
  0% {
1991
+ box-shadow: 0 0 0 0 rgba(15, 23, 42, 0.16);
1992
  }
1993
  70% {
1994
+ box-shadow: 0 0 0 6px rgba(15, 23, 42, 0);
1995
  }
1996
  100% {
1997
+ box-shadow: 0 0 0 0 rgba(15, 23, 42, 0);
1998
  }
1999
  }
2000
 
 
 
 
 
 
2001
  .refreshing-button-purple {
2002
+ background: #0f172a;
2003
  color: white;
2004
+ animation: refresh-pulse-neutral 1.5s infinite;
2005
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
2006
  }
2007
 
 
2008
  .refreshing-button-green {
2009
+ background: #0f172a;
2010
  color: white;
2011
+ animation: refresh-pulse-neutral 1.5s infinite;
2012
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
2013
  }
2014
 
 
2015
  .refreshing-button-pink {
2016
+ background: #0f172a;
2017
  color: white;
2018
+ animation: refresh-pulse-neutral 1.5s infinite;
2019
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2020
  }
2021
 
2022
  /* 响应式优化 */
public/src/views/settings.vue CHANGED
@@ -1,36 +1,36 @@
1
  <template>
2
- <div class="w-full min-h-screen p-4">
3
- <div class="container mx-auto">
4
- <div class="flex flex-col md:flex-row justify-between items-center mb-6 px-4 space-y-4 md:space-y-0 pt-5">
5
- <h1 class="text-3xl font-bold">系统设置</h1>
 
 
 
6
  <router-link to="/"
7
- class="action-button font-bold border border-blue-200 bg-blue-50 text-blue-900 px-4 py-2 rounded-xl shadow-sm hover:bg-blue-100 hover:border-blue-400 transition-all duration-300 transform hover:-translate-y-1 active:translate-y-0 text-center">
8
  返回Token管理
9
  </router-link>
10
  </div>
11
- <div class="grid grid-cols-1 gap-6 p-4">
12
  <!-- API Key 管理 -->
13
- <div class="setting-card relative overflow-hidden rounded-2xl p-6 flex flex-col gap-4">
14
- <div class="absolute inset-0 bg-white/30 backdrop-blur-md border border-white/30 rounded-2xl"></div>
15
  <div class="relative flex flex-col gap-4">
16
- <label class="text-gray-700 font-semibold text-lg">🔑 API Key 管理</label>
17
 
18
  <!-- 管理员密钥 -->
19
- <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
20
  <div class="flex items-center gap-2 mb-2">
21
  <span class="text-yellow-600 font-semibold">👑 管理员密钥</span>
22
  <span class="text-xs bg-yellow-200 text-yellow-800 px-2 py-1 rounded">不可修改</span>
23
  </div>
24
- <input :value="settings.adminKey" type="text" readonly
25
- class="w-full rounded-lg border-gray-300 bg-gray-100 shadow-sm h-10 text-sm px-3 cursor-not-allowed">
26
  </div>
27
 
28
  <!-- 普通密钥列表 -->
29
  <div class="space-y-2">
30
  <div class="flex items-center justify-between">
31
  <span class="text-gray-700 font-semibold">🔐 普通密钥</span>
32
- <button @click="showAddKeyModal = true"
33
- class="bg-green-500 text-white px-3 py-1 rounded-lg text-sm hover:bg-green-600 transition-all">
34
  + 添加密钥
35
  </button>
36
  </div>
@@ -39,12 +39,9 @@
39
  暂无普通密钥
40
  </div>
41
 
42
- <div v-for="(key, index) in settings.regularKeys" :key="index"
43
- class="flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg p-3">
44
- <input :value="key" type="text" readonly
45
- class="flex-1 rounded-lg border-gray-300 bg-white shadow-sm h-8 text-sm px-3">
46
- <button @click="deleteRegularKey(index)"
47
- class="bg-red-500 text-white px-3 py-1 rounded-lg text-sm hover:bg-red-600 transition-all">
48
  删除
49
  </button>
50
  </div>
@@ -55,66 +52,52 @@
55
  <!-- 其他设置项 -->
56
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
57
  <!-- 自动刷新 -->
58
- <div class="setting-card relative overflow-hidden rounded-2xl p-6 flex flex-col gap-4">
59
- <div class="absolute inset-0 bg-white/30 backdrop-blur-md border border-white/30 rounded-2xl">
60
- </div>
61
  <div class="relative flex flex-col gap-2">
62
- <label class="text-gray-700 font-semibold">🔄 自动刷新</label>
63
  <div class="flex items-center gap-2">
64
  <input v-model="settings.autoRefresh" type="checkbox"
65
  class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
66
  <span>启用自动刷新</span>
67
  </div>
68
  <label class="text-gray-700">刷新间隔(秒)</label>
69
- <input v-model.number="settings.autoRefreshInterval" type="number"
70
- class="mt-1 block w-full rounded-xl border-gray-300 bg-white/60 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 transition-all duration-300 h-12 text-base px-4">
71
- <button @click="saveAutoRefresh"
72
- class="w-full mt-2 bg-black text-white rounded-lg py-2 hover:bg-white hover:text-black border border-black transition-all duration-300">保存</button>
73
  </div>
74
  </div>
75
  <!-- 思考输出 -->
76
- <div class="setting-card relative overflow-hidden rounded-2xl p-6 flex flex-col gap-4">
77
- <div class="absolute inset-0 bg-white/30 backdrop-blur-md border border-white/30 rounded-2xl">
78
- </div>
79
  <div class="relative flex flex-col gap-2">
80
- <label class="text-gray-700 font-semibold">💡 思考输出</label>
81
  <div class="flex items-center gap-2">
82
  <input v-model="settings.outThink" type="checkbox"
83
  class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
84
  <span>启用思考输出</span>
85
  </div>
86
- <button @click="saveOutThink"
87
- class="w-full mt-2 bg-black text-white rounded-lg py-2 hover:bg-white hover:text-black border border-black transition-all duration-300">保存</button>
88
  </div>
89
  </div>
90
  <!-- 搜索信息模式 -->
91
- <div class="setting-card relative overflow-hidden rounded-2xl p-6 flex flex-col gap-4">
92
- <div class="absolute inset-0 bg-white/30 backdrop-blur-md border border-white/30 rounded-2xl">
93
- </div>
94
  <div class="relative flex flex-col gap-2">
95
- <label class="text-gray-700 font-semibold">🔍 搜索信息显示模式</label>
96
- <select v-model="settings.searchInfoMode"
97
- class="mt-1 block w-full rounded-xl border-gray-300 bg-white/60 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 transition-all duration-300 h-12 text-base px-4">
98
  <option value="table">表格模式</option>
99
  <option value="text">文本模式</option>
100
  </select>
101
- <button @click="saveSearchInfoMode"
102
- class="w-full mt-2 bg-black text-white rounded-lg py-2 hover:bg-white hover:text-black border border-black transition-all duration-300">保存</button>
103
  </div>
104
  </div>
105
  <!-- 简化模型映射 -->
106
- <div class="setting-card relative overflow-hidden rounded-2xl p-6 flex flex-col gap-4">
107
- <div class="absolute inset-0 bg-white/30 backdrop-blur-md border border-white/30 rounded-2xl">
108
- </div>
109
  <div class="relative flex flex-col gap-2">
110
- <label class="text-gray-700 font-semibold">🎯 简化模型映射</label>
111
  <div class="flex items-center gap-2">
112
  <input v-model="settings.simpleModelMap" type="checkbox"
113
  class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
114
  <span>只返回基础模型,不包含thinking、search、image等变体</span>
115
  </div>
116
- <button @click="saveSimpleModelMap"
117
- class="w-full mt-2 bg-black text-white rounded-lg py-2 hover:bg-white hover:text-black border border-black transition-all duration-300">保存</button>
118
  </div>
119
  </div>
120
  </div>
@@ -122,18 +105,15 @@
122
 
123
  <!-- 添加API Key模态框 -->
124
  <div v-if="showAddKeyModal"
125
- class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
126
- <div class="bg-white rounded-lg p-6 w-96 max-w-90vw">
127
  <h3 class="text-lg font-semibold mb-4">添加普通API Key</h3>
128
- <input v-model="newApiKey" type="text" placeholder="请输入API Key"
129
- class="w-full rounded-lg border-gray-300 shadow-sm h-10 text-sm px-3 mb-4">
130
  <div class="flex gap-2 justify-end">
131
- <button @click="showAddKeyModal = false"
132
- class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-all">
133
  取消
134
  </button>
135
- <button @click="addRegularKey"
136
- class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all">
137
  添加
138
  </button>
139
  </div>
@@ -280,20 +260,4 @@ onMounted(() => {
280
  </script>
281
 
282
  <style lang="css" scoped>
283
- .setting-card {
284
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.3));
285
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.10);
286
- transition: box-shadow 0.3s, transform 0.3s;
287
- position: relative;
288
- }
289
-
290
- .setting-card:hover {
291
- box-shadow: 0 12px 36px 0 rgba(31, 38, 135, 0.18);
292
- transform: translateY(-2px) scale(1.01);
293
- }
294
-
295
- .action-button {
296
- backdrop-filter: blur(4px);
297
- -webkit-backdrop-filter: blur(4px);
298
- }
299
- </style>
 
1
  <template>
2
+ <div class="w-full min-h-screen p-4 md:p-6">
3
+ <div class="container mx-auto max-w-7xl">
4
+ <div class="flex flex-col md:flex-row justify-between items-center mb-8 px-2 space-y-4 md:space-y-0 pt-5">
5
+ <div>
6
+ <h1 class="vc-title">系统设置</h1>
7
+ <p class="vc-subtitle mt-2">统一管理 API Key、自动刷新与模型显示策略。</p>
8
+ </div>
9
  <router-link to="/"
10
+ class="vc-button-secondary">
11
  返回Token管理
12
  </router-link>
13
  </div>
14
+ <div class="grid grid-cols-1 gap-6 p-2">
15
  <!-- API Key 管理 -->
16
+ <div class="vc-panel relative overflow-hidden rounded-3xl p-6 flex flex-col gap-4">
 
17
  <div class="relative flex flex-col gap-4">
18
+ <label class="text-slate-800 font-semibold text-lg">API Key 管理</label>
19
 
20
  <!-- 管理员密钥 -->
21
+ <div class="rounded-2xl border border-yellow-200 bg-yellow-50 p-4">
22
  <div class="flex items-center gap-2 mb-2">
23
  <span class="text-yellow-600 font-semibold">👑 管理员密钥</span>
24
  <span class="text-xs bg-yellow-200 text-yellow-800 px-2 py-1 rounded">不可修改</span>
25
  </div>
26
+ <input :value="settings.adminKey" type="text" readonly class="vc-input h-11 cursor-not-allowed bg-white/80">
 
27
  </div>
28
 
29
  <!-- 普通密钥列表 -->
30
  <div class="space-y-2">
31
  <div class="flex items-center justify-between">
32
  <span class="text-gray-700 font-semibold">🔐 普通密钥</span>
33
+ <button @click="showAddKeyModal = true" class="vc-button-primary px-3 py-2 text-sm rounded-xl">
 
34
  + 添加密钥
35
  </button>
36
  </div>
 
39
  暂无普通密钥
40
  </div>
41
 
42
+ <div v-for="(key, index) in settings.regularKeys" :key="index" class="flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 p-3">
43
+ <input :value="key" type="text" readonly class="vc-input h-10 flex-1 bg-white/90">
44
+ <button @click="deleteRegularKey(index)" class="vc-button border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 hover:bg-red-100">
 
 
 
45
  删除
46
  </button>
47
  </div>
 
52
  <!-- 其他设置项 -->
53
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
54
  <!-- 自动刷新 -->
55
+ <div class="vc-panel relative overflow-hidden rounded-3xl p-6 flex flex-col gap-4">
 
 
56
  <div class="relative flex flex-col gap-2">
57
+ <label class="text-slate-800 font-semibold">自动刷新</label>
58
  <div class="flex items-center gap-2">
59
  <input v-model="settings.autoRefresh" type="checkbox"
60
  class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
61
  <span>启用自动刷新</span>
62
  </div>
63
  <label class="text-gray-700">刷新间隔(秒)</label>
64
+ <input v-model.number="settings.autoRefreshInterval" type="number" class="vc-input mt-1 h-12">
65
+ <button @click="saveAutoRefresh" class="vc-button-primary w-full mt-2">保存</button>
 
 
66
  </div>
67
  </div>
68
  <!-- 思考输出 -->
69
+ <div class="vc-panel relative overflow-hidden rounded-3xl p-6 flex flex-col gap-4">
 
 
70
  <div class="relative flex flex-col gap-2">
71
+ <label class="text-slate-800 font-semibold">思考输出</label>
72
  <div class="flex items-center gap-2">
73
  <input v-model="settings.outThink" type="checkbox"
74
  class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
75
  <span>启用思考输出</span>
76
  </div>
77
+ <button @click="saveOutThink" class="vc-button-primary w-full mt-2">保存</button>
 
78
  </div>
79
  </div>
80
  <!-- 搜索信息模式 -->
81
+ <div class="vc-panel relative overflow-hidden rounded-3xl p-6 flex flex-col gap-4">
 
 
82
  <div class="relative flex flex-col gap-2">
83
+ <label class="text-slate-800 font-semibold">搜索信息显示模式</label>
84
+ <select v-model="settings.searchInfoMode" class="vc-input mt-1 h-12">
 
85
  <option value="table">表格模式</option>
86
  <option value="text">文本模式</option>
87
  </select>
88
+ <button @click="saveSearchInfoMode" class="vc-button-primary w-full mt-2">保存</button>
 
89
  </div>
90
  </div>
91
  <!-- 简化模型映射 -->
92
+ <div class="vc-panel relative overflow-hidden rounded-3xl p-6 flex flex-col gap-4">
 
 
93
  <div class="relative flex flex-col gap-2">
94
+ <label class="text-slate-800 font-semibold">简化模型映射</label>
95
  <div class="flex items-center gap-2">
96
  <input v-model="settings.simpleModelMap" type="checkbox"
97
  class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
98
  <span>只返回基础模型,不包含thinking、search、image等变体</span>
99
  </div>
100
+ <button @click="saveSimpleModelMap" class="vc-button-primary w-full mt-2">保存</button>
 
101
  </div>
102
  </div>
103
  </div>
 
105
 
106
  <!-- 添加API Key模态框 -->
107
  <div v-if="showAddKeyModal"
108
+ class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
109
+ <div class="vc-shell w-96 max-w-[90vw] p-6">
110
  <h3 class="text-lg font-semibold mb-4">添加普通API Key</h3>
111
+ <input v-model="newApiKey" type="text" placeholder="请输入API Key" class="vc-input mb-4 h-11">
 
112
  <div class="flex gap-2 justify-end">
113
+ <button @click="showAddKeyModal = false" class="vc-button-secondary">
 
114
  取消
115
  </button>
116
+ <button @click="addRegularKey" class="vc-button-primary">
 
117
  添加
118
  </button>
119
  </div>
 
260
  </script>
261
 
262
  <style lang="css" scoped>
263
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/config/index.js CHANGED
@@ -42,6 +42,7 @@ const config = {
42
  dataDir: paths.dataDir,
43
  cacheDir: paths.cacheDir,
44
  dataFilePath: paths.dataFilePath,
 
45
  // 自定义反代URL配置
46
  qwenChatProxyUrl: process.env.QWEN_CHAT_PROXY_URL || "https://chat.qwen.ai",
47
  qwenCliProxyUrl: process.env.QWEN_CLI_PROXY_URL || "https://portal.qwen.ai",
 
42
  dataDir: paths.dataDir,
43
  cacheDir: paths.cacheDir,
44
  dataFilePath: paths.dataFilePath,
45
+ usageStatsFilePath: paths.usageStatsFilePath,
46
  // 自定义反代URL配置
47
  qwenChatProxyUrl: process.env.QWEN_CHAT_PROXY_URL || "https://chat.qwen.ai",
48
  qwenCliProxyUrl: process.env.QWEN_CLI_PROXY_URL || "https://portal.qwen.ai",
src/controllers/chat.image.video.js CHANGED
@@ -6,6 +6,7 @@ const { sleep } = require('../utils/tools.js')
6
  const { generateChatID } = require('../utils/request.js')
7
  const { getSsxmodItna, getSsxmodItna2 } = require('../utils/ssxmod-manager')
8
  const { getProxyAgent, getChatBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
 
9
 
10
  /**
11
  * 主要的聊天完成处理函数
@@ -199,11 +200,13 @@ const handleImageVideoCompletion = async (req, res) => {
199
  })
200
 
201
  response_data.data.on('end', () => {
 
202
  return returnResponse(res, model, contentUrl, req.body.stream)
203
  })
204
  } else if (newChatType == 'image_edit') {
205
  console.log(response_data.data)
206
  contentUrl = response_data.data?.data?.choices[0]?.message?.content[0]?.image
 
207
  return returnResponse(res, model, contentUrl, req.body.stream)
208
  } else if (newChatType == 't2v') {
209
  return handleVideoCompletion(req, res, response_data.data, token)
@@ -211,10 +214,12 @@ const handleImageVideoCompletion = async (req, res) => {
211
 
212
  } catch (error) {
213
  logger.error('图片处理错误', 'CHAT', error)
 
214
  res.status(500).json({ error: "服务错误!!!" })
215
  }
216
 
217
  } catch (error) {
 
218
  res.status(500).json({
219
  error: "服务错误,请稍后再试"
220
  })
@@ -304,6 +309,7 @@ ${content}
304
  } else {
305
  res.json(returnBody)
306
  }
 
307
  return
308
  } else if (content == null && req.body.stream) {
309
  // 发送空数据保活
@@ -314,6 +320,7 @@ ${content}
314
  }
315
  } catch (error) {
316
  logger.error('获取视频任务状态失败', 'CHAT', error)
 
317
  res.status(500).json({ error: error.response_data?.data?.code || "可能该帐号今日生成次数已用完" })
318
  }
319
  }
@@ -354,4 +361,4 @@ const getVideoTaskStatus = async (videoTaskID, token) => {
354
 
355
  module.exports = {
356
  handleImageVideoCompletion
357
- }
 
6
  const { generateChatID } = require('../utils/request.js')
7
  const { getSsxmodItna, getSsxmodItna2 } = require('../utils/ssxmod-manager')
8
  const { getProxyAgent, getChatBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
9
+ const usageStats = require('../utils/usage-stats')
10
 
11
  /**
12
  * 主要的聊天完成处理函数
 
200
  })
201
 
202
  response_data.data.on('end', () => {
203
+ usageStats.track({ model, success: !!contentUrl, usage: { total_tokens: 0 } })
204
  return returnResponse(res, model, contentUrl, req.body.stream)
205
  })
206
  } else if (newChatType == 'image_edit') {
207
  console.log(response_data.data)
208
  contentUrl = response_data.data?.data?.choices[0]?.message?.content[0]?.image
209
+ await usageStats.track({ model, success: !!contentUrl, usage: { total_tokens: 0 } })
210
  return returnResponse(res, model, contentUrl, req.body.stream)
211
  } else if (newChatType == 't2v') {
212
  return handleVideoCompletion(req, res, response_data.data, token)
 
214
 
215
  } catch (error) {
216
  logger.error('图片处理错误', 'CHAT', error)
217
+ await usageStats.track({ model, success: false, usage: { total_tokens: 0 } })
218
  res.status(500).json({ error: "服务错误!!!" })
219
  }
220
 
221
  } catch (error) {
222
+ await usageStats.track({ model, success: false, usage: { total_tokens: 0 } })
223
  res.status(500).json({
224
  error: "服务错误,请稍后再试"
225
  })
 
309
  } else {
310
  res.json(returnBody)
311
  }
312
+ await usageStats.track({ model: req.body.model, success: true, usage: { total_tokens: 0 } })
313
  return
314
  } else if (content == null && req.body.stream) {
315
  // 发送空数据保活
 
320
  }
321
  } catch (error) {
322
  logger.error('获取视频任务状态失败', 'CHAT', error)
323
+ await usageStats.track({ model: req.body.model, success: false, usage: { total_tokens: 0 } })
324
  res.status(500).json({ error: error.response_data?.data?.code || "可能该帐号今日生成次数已用完" })
325
  }
326
  }
 
361
 
362
  module.exports = {
363
  handleImageVideoCompletion
364
+ }
src/controllers/chat.js CHANGED
@@ -5,6 +5,7 @@ const accountManager = require('../utils/account.js')
5
  const config = require('../config/index.js')
6
  const axios = require('axios')
7
  const { logger } = require('../utils/logger')
 
8
 
9
  /**
10
  * 设置响应头
@@ -218,6 +219,7 @@ const handleStreamResponse = async (res, response, enable_thinking, enable_web_s
218
  // 发送结束标记
219
  res.write(`data: [DONE]\n\n`)
220
  res.end()
 
221
  } catch (e) {
222
  logger.error('流式响应处理错误', 'CHAT', '', e)
223
  res.status(500).json({ error: "服务错误!!!" })
@@ -392,8 +394,10 @@ const handleNonStreamResponse = async (res, response, enable_thinking, enable_we
392
  "usage": totalTokens
393
  }
394
  res.json(bodyTemplate)
 
395
  } catch (error) {
396
  logger.error('非流式聊天处理错误', 'CHAT', '', error)
 
397
  res.status(500)
398
  .json({
399
  error: "服务错误!!!"
@@ -417,6 +421,7 @@ const handleChatCompletion = async (req, res) => {
417
  const response_data = await sendChatRequest(req.body)
418
 
419
  if (!response_data.status || !response_data.response) {
 
420
  res.status(500)
421
  .json({
422
  error: "请求发送失败!!!"
@@ -434,6 +439,7 @@ const handleChatCompletion = async (req, res) => {
434
 
435
  } catch (error) {
436
  logger.error('聊天处理错误', 'CHAT', '', error)
 
437
  res.status(500)
438
  .json({
439
  error: "token无效,请求发送失败!!!"
 
5
  const config = require('../config/index.js')
6
  const axios = require('axios')
7
  const { logger } = require('../utils/logger')
8
+ const usageStats = require('../utils/usage-stats')
9
 
10
  /**
11
  * 设置响应头
 
219
  // 发送结束标记
220
  res.write(`data: [DONE]\n\n`)
221
  res.end()
222
+ await usageStats.track({ model: requestBody?.model, success: true, usage: totalTokens })
223
  } catch (e) {
224
  logger.error('流式响应处理错误', 'CHAT', '', e)
225
  res.status(500).json({ error: "服务错误!!!" })
 
394
  "usage": totalTokens
395
  }
396
  res.json(bodyTemplate)
397
+ await usageStats.track({ model, success: true, usage: totalTokens })
398
  } catch (error) {
399
  logger.error('非流式聊天处理错误', 'CHAT', '', error)
400
+ await usageStats.track({ model, success: false, usage: { total_tokens: 0 } })
401
  res.status(500)
402
  .json({
403
  error: "服务错误!!!"
 
421
  const response_data = await sendChatRequest(req.body)
422
 
423
  if (!response_data.status || !response_data.response) {
424
+ await usageStats.track({ model, success: false, usage: { total_tokens: 0 } })
425
  res.status(500)
426
  .json({
427
  error: "请求发送失败!!!"
 
439
 
440
  } catch (error) {
441
  logger.error('聊天处理错误', 'CHAT', '', error)
442
+ await usageStats.track({ model, success: false, usage: { total_tokens: 0 } })
443
  res.status(500)
444
  .json({
445
  error: "token无效,请求发送失败!!!"
src/controllers/cli.chat.js CHANGED
@@ -1,6 +1,7 @@
1
  const axios = require('axios')
2
  const { logger } = require('../utils/logger')
3
  const { getProxyAgent, getCliBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
 
4
 
5
  const MODEL_REDIRECT = {
6
  'qwen3.5-plus': 'coder-model',
@@ -169,14 +170,17 @@ const handleCliChatCompletion = async (req, res) => {
169
  response.data.on('end', () => {
170
  logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (流式) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
171
  res.end()
 
172
  })
173
  } else {
174
  // 处理JSON响应
175
  res.json(formatCliJsonResponse(response.data, body.model))
176
  logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (JSON) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
 
177
  }
178
  } catch (error) {
179
  logger.error(`CLI请求使用账号[${req.account.email}]处理异常 - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '💥', error.message)
 
180
 
181
  // 如果是axios错误,提供更详细的错误信息
182
  if (error.response) {
 
1
  const axios = require('axios')
2
  const { logger } = require('../utils/logger')
3
  const { getProxyAgent, getCliBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
4
+ const usageStats = require('../utils/usage-stats')
5
 
6
  const MODEL_REDIRECT = {
7
  'qwen3.5-plus': 'coder-model',
 
170
  response.data.on('end', () => {
171
  logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (流式) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
172
  res.end()
173
+ usageStats.track({ model: body.model, success: true, usage: { total_tokens: 0 } })
174
  })
175
  } else {
176
  // 处理JSON响应
177
  res.json(formatCliJsonResponse(response.data, body.model))
178
  logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (JSON) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
179
+ await usageStats.track({ model: body.model, success: true, usage: response.data?.usage || { total_tokens: 0 } })
180
  }
181
  } catch (error) {
182
  logger.error(`CLI请求使用账号[${req.account.email}]处理异常 - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '💥', error.message)
183
+ await usageStats.track({ model: req.body?.model, success: false, usage: { total_tokens: 0 } })
184
 
185
  // 如果是axios错误,提供更详细的错误信息
186
  if (error.response) {
src/routes/usage.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express')
2
+ const router = express.Router()
3
+ const { adminKeyVerify } = require('../middlewares/authorization')
4
+ const usageStats = require('../utils/usage-stats')
5
+ const { logger } = require('../utils/logger')
6
+
7
+ router.get('/usage-stats', adminKeyVerify, async (req, res) => {
8
+ try {
9
+ const data = await usageStats.getStats()
10
+ res.json(data)
11
+ } catch (error) {
12
+ logger.error('读取使用统计失败', 'USAGE', '', error)
13
+ res.status(500).json({ error: error.message })
14
+ }
15
+ })
16
+
17
+ router.post('/usage-stats/reset', adminKeyVerify, async (req, res) => {
18
+ try {
19
+ await usageStats.reset()
20
+ res.json({ message: '使用统计已清空' })
21
+ } catch (error) {
22
+ logger.error('清空使用统计失败', 'USAGE', '', error)
23
+ res.status(500).json({ error: error.message })
24
+ }
25
+ })
26
+
27
+ module.exports = router
src/server.js CHANGED
@@ -14,6 +14,7 @@ const cliChatRouter = require('./routes/cli.chat.js')
14
  const verifyRouter = require('./routes/verify.js')
15
  const accountsRouter = require('./routes/accounts.js')
16
  const settingsRouter = require('./routes/settings.js')
 
17
 
18
  if (config.dataSaveMode === 'file') {
19
  if (!fs.existsSync(paths.dataFilePath)) {
@@ -36,6 +37,7 @@ app.use(cliChatRouter)
36
  app.use(verifyRouter)
37
  app.use('/api', accountsRouter)
38
  app.use('/api', settingsRouter)
 
39
 
40
  app.use(express.static(path.join(__dirname, '../public/dist')))
41
 
 
14
  const verifyRouter = require('./routes/verify.js')
15
  const accountsRouter = require('./routes/accounts.js')
16
  const settingsRouter = require('./routes/settings.js')
17
+ const usageRouter = require('./routes/usage.js')
18
 
19
  if (config.dataSaveMode === 'file') {
20
  if (!fs.existsSync(paths.dataFilePath)) {
 
37
  app.use(verifyRouter)
38
  app.use('/api', accountsRouter)
39
  app.use('/api', settingsRouter)
40
+ app.use('/api', usageRouter)
41
 
42
  app.use(express.static(path.join(__dirname, '../public/dist')))
43
 
src/utils/paths.js CHANGED
@@ -16,5 +16,6 @@ module.exports = {
16
  dataDir,
17
  cacheDir,
18
  logDir,
19
- dataFilePath: path.join(dataDir, 'data.json')
 
20
  }
 
16
  dataDir,
17
  cacheDir,
18
  logDir,
19
+ dataFilePath: path.join(dataDir, 'data.json'),
20
+ usageStatsFilePath: path.join(dataDir, 'usage-stats.json')
21
  }
src/utils/usage-stats.js ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs').promises
2
+ const path = require('path')
3
+ const config = require('../config')
4
+ const { logger } = require('./logger')
5
+
6
+ class UsageStats {
7
+ constructor() {
8
+ this.filePath = config.usageStatsFilePath
9
+ }
10
+
11
+ createEmptyStats() {
12
+ return {
13
+ summary: {
14
+ requests: 0,
15
+ success: 0,
16
+ failed: 0,
17
+ promptTokens: 0,
18
+ completionTokens: 0,
19
+ totalTokens: 0,
20
+ updatedAt: null,
21
+ today: {
22
+ date: new Date().toISOString().slice(0, 10),
23
+ requests: 0,
24
+ success: 0,
25
+ failed: 0,
26
+ totalTokens: 0,
27
+ hourly: []
28
+ }
29
+ },
30
+ models: {}
31
+ }
32
+ }
33
+
34
+ async ensureFileExists() {
35
+ try {
36
+ await fs.access(this.filePath)
37
+ } catch {
38
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true })
39
+ await fs.writeFile(this.filePath, JSON.stringify(this.createEmptyStats(), null, 2), 'utf-8')
40
+ }
41
+ }
42
+
43
+ async readStats() {
44
+ await this.ensureFileExists()
45
+ const content = await fs.readFile(this.filePath, 'utf-8')
46
+ return JSON.parse(content)
47
+ }
48
+
49
+ async writeStats(stats) {
50
+ await fs.writeFile(this.filePath, JSON.stringify(stats, null, 2), 'utf-8')
51
+ }
52
+
53
+ async track({ model, success, usage = {} }) {
54
+ try {
55
+ const stats = await this.readStats()
56
+ const modelId = model || 'unknown-model'
57
+ const promptTokens = Math.max(0, usage.prompt_tokens || 0)
58
+ const completionTokens = Math.max(0, usage.completion_tokens || 0)
59
+ const totalTokens = Math.max(0, usage.total_tokens || (promptTokens + completionTokens))
60
+ const today = new Date().toISOString().slice(0, 10)
61
+
62
+ if (!stats.summary.today || stats.summary.today.date !== today) {
63
+ stats.summary.today = {
64
+ date: today,
65
+ requests: 0,
66
+ success: 0,
67
+ failed: 0,
68
+ totalTokens: 0
69
+ }
70
+ }
71
+
72
+ if (!stats.models[modelId]) {
73
+ stats.models[modelId] = {
74
+ model: modelId,
75
+ requests: 0,
76
+ success: 0,
77
+ failed: 0,
78
+ promptTokens: 0,
79
+ completionTokens: 0,
80
+ totalTokens: 0,
81
+ successRate: 0,
82
+ lastUsedAt: null,
83
+ today: {
84
+ date: today,
85
+ requests: 0,
86
+ success: 0,
87
+ failed: 0,
88
+ totalTokens: 0
89
+ }
90
+ }
91
+ }
92
+
93
+ const modelStats = stats.models[modelId]
94
+ if (!modelStats.today || modelStats.today.date !== today) {
95
+ modelStats.today = {
96
+ date: today,
97
+ requests: 0,
98
+ success: 0,
99
+ failed: 0,
100
+ totalTokens: 0
101
+ }
102
+ }
103
+
104
+ modelStats.requests += 1
105
+ modelStats.success += success ? 1 : 0
106
+ modelStats.failed += success ? 0 : 1
107
+ modelStats.promptTokens += promptTokens
108
+ modelStats.completionTokens += completionTokens
109
+ modelStats.totalTokens += totalTokens
110
+ modelStats.successRate = modelStats.requests > 0 ? Number(((modelStats.success / modelStats.requests) * 100).toFixed(2)) : 0
111
+ modelStats.lastUsedAt = new Date().toISOString()
112
+ modelStats.today.requests += 1
113
+ modelStats.today.success += success ? 1 : 0
114
+ modelStats.today.failed += success ? 0 : 1
115
+ modelStats.today.totalTokens += totalTokens
116
+
117
+ stats.summary.requests += 1
118
+ stats.summary.success += success ? 1 : 0
119
+ stats.summary.failed += success ? 0 : 1
120
+ stats.summary.promptTokens += promptTokens
121
+ stats.summary.completionTokens += completionTokens
122
+ stats.summary.totalTokens += totalTokens
123
+ stats.summary.updatedAt = new Date().toISOString()
124
+ stats.summary.today.requests += 1
125
+ stats.summary.today.success += success ? 1 : 0
126
+ stats.summary.today.failed += success ? 0 : 1
127
+ stats.summary.today.totalTokens += totalTokens
128
+
129
+ const hour = new Date().getHours()
130
+ if (!Array.isArray(stats.summary.today.hourly)) {
131
+ stats.summary.today.hourly = []
132
+ }
133
+
134
+ let hourlyItem = stats.summary.today.hourly.find(item => item.hour === hour)
135
+ if (!hourlyItem) {
136
+ hourlyItem = { hour, requests: 0, totalTokens: 0 }
137
+ stats.summary.today.hourly.push(hourlyItem)
138
+ }
139
+ hourlyItem.requests += 1
140
+ hourlyItem.totalTokens += totalTokens
141
+
142
+ await this.writeStats(stats)
143
+ } catch (error) {
144
+ logger.error('记录使用统计失败', 'USAGE', '', error)
145
+ }
146
+ }
147
+
148
+ async getStats() {
149
+ const stats = await this.readStats()
150
+ const today = new Date().toISOString().slice(0, 10)
151
+
152
+ if (!stats.summary.today || stats.summary.today.date !== today) {
153
+ stats.summary.today = {
154
+ date: today,
155
+ requests: 0,
156
+ success: 0,
157
+ failed: 0,
158
+ totalTokens: 0,
159
+ hourly: []
160
+ }
161
+ }
162
+
163
+ const models = Object.values(stats.models).sort((a, b) => b.totalTokens - a.totalTokens)
164
+ const successRate = stats.summary.requests > 0 ? Number(((stats.summary.success / stats.summary.requests) * 100).toFixed(2)) : 0
165
+
166
+ return {
167
+ summary: {
168
+ ...stats.summary,
169
+ successRate
170
+ },
171
+ models
172
+ }
173
+ }
174
+
175
+ async reset() {
176
+ await this.writeStats(this.createEmptyStats())
177
+ }
178
+ }
179
+
180
+ module.exports = new UsageStats()