malt666 commited on
Commit
29c39b7
·
verified ·
1 Parent(s): dad1ae4

Upload 6 files

Browse files
Files changed (3) hide show
  1. app.py +25 -3
  2. templates/dashboard.html +1414 -358
  3. templates/login.html +218 -204
app.py CHANGED
@@ -922,10 +922,30 @@ def get_compute_points_log(session, cookies, session_token):
922
  print(f"获取计算点使用日志异常: {e}")
923
 
924
 
925
- # 添加登录相关路由
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  @app.route("/login", methods=["GET", "POST"])
927
  def login():
928
  error = None
 
 
929
  if request.method == "POST":
930
  password = request.form.get("password")
931
  if password and hashlib.sha256(password.encode()).hexdigest() == PASSWORD:
@@ -933,8 +953,10 @@ def login():
933
  flask_session.permanent = True
934
  return redirect(url_for('dashboard'))
935
  else:
936
- error = "密码不正确"
937
- return render_template('login.html', error=error)
 
 
938
 
939
 
940
  @app.route("/logout")
 
922
  print(f"获取计算点使用日志异常: {e}")
923
 
924
 
925
+ # 获取系统环境变量中的部署URL
926
+ def get_space_url():
927
+ # 尝试从Hugging Face Space环境变量获取
928
+ space_url = os.environ.get("SPACE_URL")
929
+ if space_url:
930
+ return space_url
931
+
932
+ # 尝试构建URL
933
+ space_username = os.environ.get("SPACE_USERNAME")
934
+ space_name = os.environ.get("SPACE_NAME")
935
+ if space_username and space_name:
936
+ return f"https://{space_username}-{space_name}.hf.space"
937
+
938
+ # 如果无法从环境变量获取,返回默认值或None
939
+ return os.environ.get("CUSTOM_URL", None)
940
+
941
+ # 获取部署URL
942
+ SPACE_URL = get_space_url()
943
+
944
  @app.route("/login", methods=["GET", "POST"])
945
  def login():
946
  error = None
947
+ space_url = SPACE_URL
948
+
949
  if request.method == "POST":
950
  password = request.form.get("password")
951
  if password and hashlib.sha256(password.encode()).hexdigest() == PASSWORD:
 
953
  flask_session.permanent = True
954
  return redirect(url_for('dashboard'))
955
  else:
956
+ # 在密码错误时提示用户环境变量
957
+ error = f"密码不正确。请使用在环境变量'password'中设置的密码。"
958
+
959
+ return render_template('login.html', error=error, space_url=space_url)
960
 
961
 
962
  @app.route("/logout")
templates/dashboard.html CHANGED
@@ -3,104 +3,122 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Abacus Chat Proxy - 仪表盘</title>
7
  <style>
8
  :root {
9
- --primary-color: #6f42c1;
10
- --secondary-color: #4a32a8;
11
- --accent-color: #5e85f1;
12
- --bg-color: #0a0a1a;
13
- --text-color: #e6e6ff;
14
- --card-bg: rgba(30, 30, 60, 0.7);
15
- --input-bg: rgba(40, 40, 80, 0.6);
16
- --success-color: #36d399;
17
- --warning-color: #fbbd23;
18
- --error-color: #f87272;
 
 
 
19
  }
20
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  * {
22
  margin: 0;
23
  padding: 0;
24
  box-sizing: border-box;
25
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 
26
  }
27
 
28
  body {
29
  min-height: 100vh;
30
- background-color: var(--bg-color);
31
- background-image:
32
- radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
33
- radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
34
  color: var(--text-color);
35
- position: relative;
36
  overflow-x: hidden;
37
  }
38
 
39
- /* 动态背景网格 */
40
- .grid-background {
41
  position: fixed;
42
  top: 0;
43
  left: 0;
44
  width: 100%;
45
  height: 100%;
46
- background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
47
- linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
48
- background-size: 30px 30px;
49
  z-index: -1;
50
- animation: grid-move 20s linear infinite;
 
 
 
 
 
 
 
 
51
  }
52
 
53
- @keyframes grid-move {
54
  0% {
55
- transform: translateY(0);
 
56
  }
57
  100% {
58
- transform: translateY(30px);
 
59
  }
60
  }
61
 
62
  /* 顶部导航栏 */
63
  .navbar {
 
 
 
 
 
 
 
64
  padding: 1rem 2rem;
65
- background: rgba(15, 15, 30, 0.8);
66
- backdrop-filter: blur(10px);
67
  display: flex;
68
  justify-content: space-between;
69
  align-items: center;
70
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
71
- position: sticky;
72
- top: 0;
73
- z-index: 100;
74
  }
75
 
76
  .navbar-brand {
77
  display: flex;
78
  align-items: center;
 
79
  text-decoration: none;
80
  color: var(--text-color);
81
  }
82
 
83
  .navbar-logo {
84
- font-size: 1.5rem;
85
- margin-right: 0.75rem;
86
- animation: pulse 2s infinite alternate;
 
 
87
  }
88
 
89
- @keyframes pulse {
90
- 0% {
91
- transform: scale(1);
92
- text-shadow: 0 0 5px rgba(111, 66, 193, 0.5);
93
- }
94
- 100% {
95
- transform: scale(1.05);
96
- text-shadow: 0 0 15px rgba(111, 66, 193, 0.8);
97
- }
98
  }
99
 
100
  .navbar-title {
101
  font-size: 1.25rem;
102
  font-weight: 600;
103
- background: linear-gradient(45deg, #6f42c1, #5181f1);
104
  -webkit-background-clip: text;
105
  -webkit-text-fill-color: transparent;
106
  }
@@ -111,62 +129,88 @@
111
  }
112
 
113
  .btn-logout {
114
- background: rgba(255, 255, 255, 0.1);
115
- color: var(--text-color);
116
  border: none;
117
- padding: 0.5rem 1rem;
118
- border-radius: 6px;
 
 
119
  cursor: pointer;
120
  transition: all 0.2s;
121
  display: flex;
122
  align-items: center;
123
  gap: 0.5rem;
 
 
124
  }
125
 
126
  .btn-logout:hover {
127
- background: rgba(255, 255, 255, 0.2);
 
128
  }
129
 
130
- /* 主内容区域 */
131
  .container {
132
- max-width: 1200px;
133
  margin: 0 auto;
134
  padding: 2rem;
135
  }
136
 
137
- /* 信息卡片样式 */
 
 
 
 
 
 
 
 
138
  .card {
139
  background: var(--card-bg);
140
- border-radius: 12px;
141
  padding: 1.5rem;
142
- margin-bottom: 2rem;
143
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
144
- backdrop-filter: blur(8px);
145
- border: 1px solid rgba(255, 255, 255, 0.1);
146
- animation: card-fade-in 0.6s ease-out;
 
 
 
 
147
  }
148
 
149
- @keyframes card-fade-in {
150
- from {
151
- opacity: 0;
152
- transform: translateY(20px);
153
- }
154
- to {
155
- opacity: 1;
156
- transform: translateY(0);
157
- }
 
 
 
 
 
 
 
 
 
158
  }
159
 
160
  .card-header {
161
- margin-bottom: 1rem;
162
  display: flex;
163
  align-items: center;
164
  justify-content: space-between;
 
165
  }
166
 
167
  .card-title {
168
  font-size: 1.25rem;
169
  font-weight: 600;
 
170
  display: flex;
171
  align-items: center;
172
  gap: 0.75rem;
@@ -178,9 +222,10 @@
178
  display: flex;
179
  align-items: center;
180
  justify-content: center;
181
- background: linear-gradient(45deg, rgba(111, 66, 193, 0.2), rgba(94, 133, 241, 0.2));
182
  border-radius: 8px;
183
  font-size: 1.25rem;
 
184
  }
185
 
186
  /* 状态项样式 */
@@ -189,33 +234,43 @@
189
  justify-content: space-between;
190
  align-items: center;
191
  padding: 0.75rem 0;
192
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
 
 
 
193
  }
194
 
195
  .status-item:last-child {
196
  border-bottom: none;
197
  }
198
 
 
 
 
 
 
 
 
199
  .status-label {
200
- color: rgba(230, 230, 255, 0.7);
201
  font-weight: 500;
202
  }
203
 
204
  .status-value {
205
- color: var(--text-color);
206
  font-weight: 600;
 
207
  }
208
 
209
  .status-value.success {
210
- color: var(--success-color);
211
  }
212
 
213
  .status-value.warning {
214
- color: var(--warning-color);
215
  }
216
 
217
  .status-value.danger {
218
- color: var(--error-color);
219
  }
220
 
221
  /* 模型标签 */
@@ -226,18 +281,186 @@
226
  }
227
 
228
  .model-tag {
229
- background: rgba(111, 66, 193, 0.2);
230
  padding: 0.25rem 0.75rem;
 
231
  border-radius: 16px;
232
  font-size: 0.875rem;
233
  color: var(--text-color);
234
- border: 1px solid rgba(111, 66, 193, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  }
236
 
237
  /* 表格样式 */
238
  .table-container {
239
  overflow-x: auto;
240
  margin-top: 1rem;
 
 
 
 
 
241
  }
242
 
243
  .data-table {
@@ -247,154 +470,203 @@
247
  }
248
 
249
  .data-table th {
250
- background-color: rgba(50, 50, 100, 0.3);
251
- padding: 0.75rem 1rem;
252
  font-weight: 600;
253
- color: rgba(230, 230, 255, 0.9);
254
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
 
255
  }
256
 
257
- .data-table td {
258
- padding: 0.75rem 1rem;
259
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
 
 
 
 
 
 
 
 
260
  }
261
 
262
- .data-table tbody tr {
263
- transition: background-color 0.2s;
264
  }
265
 
266
- .data-table tbody tr:hover {
267
- background-color: rgba(50, 50, 100, 0.2);
 
268
  }
269
 
270
- /* 特殊值样式 */
271
- .token-count {
272
- font-family: 'Consolas', monospace;
273
- color: var(--accent-color);
274
- font-weight: bold;
275
  }
276
 
277
- .call-count {
278
- font-family: 'Consolas', monospace;
279
- color: var(--success-color);
280
- font-weight: bold;
281
  }
282
 
283
- .compute-points {
284
- font-family: 'Consolas', monospace;
285
- color: var(--primary-color);
286
- font-weight: bold;
287
  }
288
 
289
- /* 进度条 */
290
- .progress-container {
291
- width: 100%;
292
- height: 8px;
293
- background-color: rgba(100, 100, 150, 0.2);
294
- border-radius: 4px;
295
- margin-top: 0.5rem;
296
- overflow: hidden;
297
  position: relative;
 
 
 
 
298
  }
299
 
300
- .progress-bar {
301
- height: 100%;
302
- border-radius: 4px;
303
- background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
304
- position: relative;
305
- overflow: hidden;
306
  }
307
 
308
- .progress-bar.warning {
309
- background: linear-gradient(90deg, #fbbd23, #f59e0b);
 
 
 
 
 
 
 
310
  }
311
 
312
- .progress-bar.danger {
313
- background: linear-gradient(90deg, #f87272, #ef4444);
 
314
  }
315
 
316
- /* 添加进度条闪光效果 */
317
- .progress-bar::after {
318
- content: '';
319
- position: absolute;
320
- top: 0;
321
- left: -100%;
322
- width: 100%;
323
- height: 100%;
324
- background: linear-gradient(90deg,
325
- transparent,
326
- rgba(255, 255, 255, 0.2),
327
- transparent);
328
- animation: progress-shine 3s infinite;
329
  }
330
 
331
- @keyframes progress-shine {
332
- 0% {
333
- left: -100%;
334
- }
335
- 50%, 100% {
336
- left: 100%;
337
- }
338
  }
339
 
340
  /* API端点卡片 */
341
  .endpoint-item {
342
- background: rgba(50, 50, 100, 0.2);
343
- padding: 1rem;
344
- border-radius: 8px;
345
  margin-bottom: 1rem;
346
  border-left: 3px solid var(--primary-color);
 
 
 
 
 
 
 
347
  }
348
 
349
  .endpoint-url {
350
- font-family: 'Consolas', monospace;
351
- background: rgba(0, 0, 0, 0.2);
352
- padding: 0.5rem;
353
- border-radius: 4px;
354
- margin-top: 0.25rem;
355
  display: inline-block;
 
 
 
356
  }
357
 
358
- /* 响应式布局 */
359
- .grid {
360
- display: grid;
361
- grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
362
- gap: 1.5rem;
363
  }
364
 
365
  /* 页脚 */
366
  .footer {
367
  text-align: center;
368
  padding: 2rem 0;
369
- color: rgba(230, 230, 255, 0.5);
370
  font-size: 0.9rem;
371
- border-top: 1px solid rgba(255, 255, 255, 0.05);
372
  margin-top: 2rem;
 
 
 
373
  }
374
 
375
- /* 悬浮图标按钮 */
376
- .float-btn {
377
- position: fixed;
378
- bottom: 2rem;
379
- right: 2rem;
380
- width: 50px;
381
- height: 50px;
382
- border-radius: 50%;
383
- background: linear-gradient(45deg, var(--primary-color), var(--accent-color));
384
- display: flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  align-items: center;
386
  justify-content: center;
387
- color: white;
388
- font-size: 1.5rem;
389
- box-shadow: 0 4px 20px rgba(111, 66, 193, 0.4);
390
- cursor: pointer;
391
  transition: all 0.3s;
392
- z-index: 50;
 
 
 
393
  }
394
 
395
- .float-btn:hover {
 
 
 
 
 
 
 
 
396
  transform: translateY(-5px);
397
- box-shadow: 0 8px 25px rgba(111, 66, 193, 0.5);
398
  }
399
 
400
  /* 滚动条美化 */
@@ -404,213 +676,792 @@
404
  }
405
 
406
  ::-webkit-scrollbar-track {
407
- background: rgba(50, 50, 100, 0.1);
 
408
  }
409
 
410
  ::-webkit-scrollbar-thumb {
411
- background: rgba(111, 66, 193, 0.5);
412
  border-radius: 4px;
 
413
  }
414
 
415
  ::-webkit-scrollbar-thumb:hover {
416
- background: rgba(111, 66, 193, 0.7);
417
  }
418
 
419
  /* 模型统计折叠样式 */
 
 
 
 
420
  .hidden-model {
421
  display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  }
423
 
424
  .btn-toggle {
425
- background: rgba(111, 66, 193, 0.2);
426
- border: 1px solid rgba(111, 66, 193, 0.3);
427
- border-radius: 4px;
428
- padding: 0.3rem 0.7rem;
429
- color: rgba(230, 230, 255, 0.9);
430
  cursor: pointer;
431
- transition: all 0.2s;
432
- font-size: 0.85rem;
 
 
 
 
433
  margin-left: auto;
434
  }
435
 
436
  .btn-toggle:hover {
437
- background: rgba(111, 66, 193, 0.4);
 
 
 
 
 
 
 
 
 
 
438
  }
439
 
440
  /* Token注释样式 */
441
  .token-note {
442
- margin-top: 0.75rem;
443
- color: rgba(230, 230, 255, 0.6);
444
  font-style: italic;
445
- line-height: 1.4;
446
- padding: 0.5rem;
447
- border-top: 1px dashed rgba(255, 255, 255, 0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  }
449
 
450
  .token-model-table {
451
- margin-top: 1rem;
452
  }
453
 
454
- /* 媒体查询 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  @media (max-width: 768px) {
456
- .container {
457
- padding: 1rem;
 
 
 
 
458
  }
459
 
460
  .navbar {
461
  padding: 1rem;
462
  }
463
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  .card {
 
 
 
 
465
  padding: 1rem;
466
  }
467
 
468
- .grid {
469
- grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
 
 
470
  }
471
  }
472
  </style>
473
  </head>
474
- <body>
475
- <div class="grid-background"></div>
476
 
477
  <nav class="navbar">
478
- <a href="/" class="navbar-brand">
479
- <span class="navbar-logo">🤖</span>
480
- <span class="navbar-title">Abacus Chat Proxy</span>
481
- </a>
482
- <div class="navbar-actions">
483
- <a href="/logout" class="btn-logout">
484
- <span>退出</span>
485
- <span>↗</span>
486
- </a>
487
  </div>
488
  </nav>
489
 
490
  <div class="container">
491
- <div class="card">
492
- <div class="card-header">
493
- <h2 class="card-title">
494
- <span class="card-icon">📊</span>
495
- 系统状态
496
- </h2>
497
- </div>
498
- <div class="status-item">
499
- <span class="status-label">服务状态</span>
500
- <span class="status-value success">运行中</span>
501
- </div>
502
- <div class="status-item">
503
- <span class="status-label">运行时间</span>
504
- <span class="status-value">{{ uptime }}</span>
505
- </div>
506
- <div class="status-item">
507
- <span class="status-label">健康检查次数</span>
508
- <span class="status-value">{{ health_checks }}</span>
509
- </div>
510
- <div class="status-item">
511
- <span class="status-label">已配置用户数</span>
512
- <span class="status-value">{{ user_count }}</span>
513
- </div>
514
- <div class="status-item">
515
- <span class="status-label">可用模型</span>
516
- <div class="models-list">
517
- {% for model in models %}
518
- <span class="model-tag">{{ model }}</span>
519
- {% endfor %}
520
- </div>
521
- </div>
522
- </div>
523
-
524
  <div class="grid">
 
525
  <div class="card">
526
  <div class="card-header">
527
  <h2 class="card-title">
528
- <span class="card-icon">💰</span>
529
- 计算点信息
530
  </h2>
531
  </div>
532
  <div class="status-item">
533
- <span class="status-label">总计算点</span>
534
- <span class="status-value compute-points">{{ compute_points.total|int }}</span>
 
 
 
 
 
 
 
535
  </div>
536
  <div class="status-item">
537
- <span class="status-label">已使用</span>
538
- <span class="status-value compute-points">{{ compute_points.used|int }}</span>
 
 
 
 
 
 
 
 
 
 
539
  </div>
540
  <div class="status-item">
541
- <span class="status-label">剩余</span>
542
- <span class="status-value compute-points">{{ compute_points.left|int }}</span>
543
  </div>
544
  <div class="status-item">
545
- <span class="status-label">使用比例</span>
546
- <div style="width: 100%; text-align: right;">
547
- <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}">
548
- {{ compute_points.percentage }}%
549
- </span>
550
- <div class="progress-container">
551
- <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div>
552
- </div>
553
- </div>
554
  </div>
555
- {% if compute_points.last_update %}
556
  <div class="status-item">
557
- <span class="status-label">最后更新时间</span>
558
- <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span>
 
 
 
559
  </div>
560
- {% endif %}
561
  </div>
562
-
 
563
  <div class="card">
564
  <div class="card-header">
565
  <h2 class="card-title">
566
- <span class="card-icon">🔍</span>
567
- Token 使用统计
568
  </h2>
569
  </div>
570
  <div class="status-item">
571
- <span class="status-label">总Token使用量</span>
572
- <span class="status-value token-count">{{ total_tokens.total|int }}</span>
573
  </div>
574
  <div class="status-item">
575
- <span class="status-label">输入Token</span>
576
- <span class="status-value token-count">{{ total_tokens.prompt|int }}</span>
577
  </div>
578
  <div class="status-item">
579
- <span class="status-label">输出Token</span>
580
- <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
581
  </div>
582
- <div class="token-note">
583
- <small>* 以上数据仅统计通过本代理使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small>
 
 
 
 
584
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  <div class="table-container">
586
- <table class="data-table token-model-table">
587
  <thead>
588
  <tr>
589
- <th>模型</th>
590
- <th>总Token</th>
591
  <th>输入Token</th>
592
  <th>输出Token</th>
 
593
  </tr>
594
  </thead>
595
  <tbody>
596
  {% for model, stats in model_stats.items() %}
597
- <tr>
598
  <td>{{ model }}</td>
599
- <td class="token-count">{{ stats.total_tokens|int }}</td>
600
- <td class="token-count">{{ stats.prompt_tokens|int }}</td>
601
- <td class="token-count">{{ stats.completion_tokens|int }}</td>
 
602
  </tr>
603
  {% endfor %}
604
  </tbody>
605
  </table>
606
  </div>
 
 
 
607
  </div>
608
  </div>
609
-
 
610
  <div class="card">
611
  <div class="card-header">
612
  <h2 class="card-title">
613
- <span class="card-icon">📊</span>
614
  计算点使用日志
615
  </h2>
616
  </div>
@@ -627,115 +1478,320 @@
627
  {% for entry in compute_points_log.log %}
628
  <tr>
629
  {% for key, value in compute_points_log.columns.items() %}
630
- <td class="compute-points">{{ entry.get(key, 0) }}</td>
 
 
 
 
 
 
 
 
 
 
631
  {% endfor %}
632
  </tr>
633
  {% endfor %}
634
  </tbody>
635
  </table>
636
  </div>
637
- </div>
638
-
639
- <div class="card">
640
- <div class="card-header">
641
- <h2 class="card-title">
642
- <span class="card-icon">📈</span>
643
- 模型使用统计
644
- </h2>
645
- <button id="toggleModelStats" class="btn-toggle">显示全部</button>
646
- </div>
647
- <div class="table-container">
648
- <table class="data-table">
649
- <thead>
650
- <tr>
651
- <th>模型</th>
652
- <th>调用次数</th>
653
- <th>输入Token</th>
654
- <th>输出Token</th>
655
- <th>总Token</th>
656
- </tr>
657
- </thead>
658
- <tbody>
659
- {% for model, stats in model_stats.items() %}
660
- <tr class="model-row {% if loop.index > 5 %}hidden-model{% endif %}">
661
- <td>{{ model }}</td>
662
- <td class="call-count">{{ stats.count }}</td>
663
- <td class="token-count">{{ stats.prompt_tokens|int }}</td>
664
- <td class="token-count">{{ stats.completion_tokens|int }}</td>
665
- <td class="token-count">{{ stats.total_tokens|int }}</td>
666
- </tr>
667
- {% endfor %}
668
- </tbody>
669
- </table>
670
- <div class="token-note">
671
- <small>* Token计算方式:部分模型使用tiktoken精确计算,其他模型使用估算方法(约4字符=1token),仅供参考。</small>
672
- </div>
673
  </div>
674
  </div>
675
-
 
676
  <div class="card">
677
  <div class="card-header">
678
  <h2 class="card-title">
679
- <span class="card-icon">📡</span>
680
- API 端点
681
  </h2>
682
  </div>
683
  <div class="endpoint-item">
684
- <p>获取模型列表:</p>
685
- <code class="endpoint-url">GET /v1/models</code>
686
  </div>
687
  <div class="endpoint-item">
688
- <p>聊天补全:</p>
689
- <code class="endpoint-url">POST /v1/chat/completions</code>
690
  </div>
691
  <div class="endpoint-item">
692
- <p>健康检查:</p>
693
- <code class="endpoint-url">GET /health</code>
694
  </div>
695
  </div>
696
-
697
- <div class="footer">
698
- <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
699
- </div>
700
  </div>
701
-
702
- <a href="#" class="float-btn" title="回到顶部">↑</a>
703
-
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  <script>
705
- // 回到顶部按钮
706
- document.querySelector('.float-btn').addEventListener('click', (e) => {
707
- e.preventDefault();
708
- window.scrollTo({ top: 0, behavior: 'smooth' });
709
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
 
711
- // 显示/隐藏回到顶部按钮
712
- window.addEventListener('scroll', () => {
713
- const floatBtn = document.querySelector('.float-btn');
714
- if (window.pageYOffset > 300) {
715
- floatBtn.style.opacity = '1';
716
  } else {
717
- floatBtn.style.opacity = '0';
718
  }
719
- });
720
-
721
- // 初始化隐藏回到顶部按钮
722
- document.querySelector('.float-btn').style.opacity = '0';
723
 
724
- // 模型统计折叠功能
725
- const toggleBtn = document.getElementById('toggleModelStats');
726
- const hiddenModels = document.querySelectorAll('.hidden-model');
727
- let isExpanded = false;
728
-
729
- if (toggleBtn) {
730
- toggleBtn.addEventListener('click', () => {
731
- hiddenModels.forEach(model => {
732
- model.classList.toggle('hidden-model');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
 
735
- isExpanded = !isExpanded;
736
- toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部';
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  });
738
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
  </script>
740
  </body>
741
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Abacus Chat代理仪表板</title>
7
  <style>
8
  :root {
9
+ --primary-color: #6366f1;
10
+ --primary-dark: #4f46e5;
11
+ --accent-color: #8b5cf6;
12
+ --background: #f8fafc;
13
+ --card-bg: #ffffff;
14
+ --text-color: #1e293b;
15
+ --text-light: #64748b;
16
+ --error: #ef4444;
17
+ --success: #10b981;
18
+ --warning: #f59e0b;
19
+ --surface-1: rgba(255, 255, 255, 0.05);
20
+ --surface-2: rgba(255, 255, 255, 0.1);
21
+ --blur-bg: rgba(15, 23, 42, 0.6);
22
  }
23
 
24
+ @media (prefers-color-scheme: dark) {
25
+ :root {
26
+ --primary-color: #818cf8;
27
+ --primary-dark: #6366f1;
28
+ --accent-color: #a78bfa;
29
+ --background: #0f172a;
30
+ --card-bg: #1e293b;
31
+ --text-color: #f1f5f9;
32
+ --text-light: #94a3b8;
33
+ }
34
+ }
35
+
36
  * {
37
  margin: 0;
38
  padding: 0;
39
  box-sizing: border-box;
40
+ font-family: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
41
+ transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
42
  }
43
 
44
  body {
45
  min-height: 100vh;
46
+ background: var(--background);
 
 
 
47
  color: var(--text-color);
48
+ line-height: 1.6;
49
  overflow-x: hidden;
50
  }
51
 
52
+ /* 动态背景 */
53
+ .background-animation {
54
  position: fixed;
55
  top: 0;
56
  left: 0;
57
  width: 100%;
58
  height: 100%;
 
 
 
59
  z-index: -1;
60
+ background: radial-gradient(
61
+ circle at 50% 50%,
62
+ rgba(99, 102, 241, 0.15),
63
+ rgba(139, 92, 246, 0.15),
64
+ transparent 60%
65
+ );
66
+ filter: blur(80px);
67
+ opacity: 0.5;
68
+ animation: pulse 8s ease-in-out infinite alternate;
69
  }
70
 
71
+ @keyframes pulse {
72
  0% {
73
+ transform: scale(1);
74
+ opacity: 0.5;
75
  }
76
  100% {
77
+ transform: scale(1.2);
78
+ opacity: 0.3;
79
  }
80
  }
81
 
82
  /* 顶部导航栏 */
83
  .navbar {
84
+ position: sticky;
85
+ top: 0;
86
+ z-index: 100;
87
+ background: var(--blur-bg);
88
+ backdrop-filter: blur(12px);
89
+ -webkit-backdrop-filter: blur(12px);
90
+ border-bottom: 1px solid var(--surface-1);
91
  padding: 1rem 2rem;
 
 
92
  display: flex;
93
  justify-content: space-between;
94
  align-items: center;
 
 
 
 
95
  }
96
 
97
  .navbar-brand {
98
  display: flex;
99
  align-items: center;
100
+ gap: 1rem;
101
  text-decoration: none;
102
  color: var(--text-color);
103
  }
104
 
105
  .navbar-logo {
106
+ font-size: 1.75rem;
107
+ background: linear-gradient(45deg, var(--primary-color), var(--accent-color));
108
+ -webkit-background-clip: text;
109
+ -webkit-text-fill-color: transparent;
110
+ animation: float 3s ease-in-out infinite;
111
  }
112
 
113
+ @keyframes float {
114
+ 0%, 100% { transform: translateY(0); }
115
+ 50% { transform: translateY(-5px); }
 
 
 
 
 
 
116
  }
117
 
118
  .navbar-title {
119
  font-size: 1.25rem;
120
  font-weight: 600;
121
+ background: linear-gradient(45deg, var(--primary-color), var(--accent-color));
122
  -webkit-background-clip: text;
123
  -webkit-text-fill-color: transparent;
124
  }
 
129
  }
130
 
131
  .btn-logout {
132
+ padding: 0.5rem 1.25rem;
 
133
  border: none;
134
+ border-radius: 8px;
135
+ background: var(--surface-1);
136
+ color: var(--text-color);
137
+ font-weight: 500;
138
  cursor: pointer;
139
  transition: all 0.2s;
140
  display: flex;
141
  align-items: center;
142
  gap: 0.5rem;
143
+ text-decoration: none;
144
+ backdrop-filter: blur(4px);
145
  }
146
 
147
  .btn-logout:hover {
148
+ background: var(--surface-2);
149
+ transform: translateY(-1px);
150
  }
151
 
152
+ /* 主容器 */
153
  .container {
154
+ max-width: 1400px;
155
  margin: 0 auto;
156
  padding: 2rem;
157
  }
158
 
159
+ /* 网格布局 */
160
+ .grid {
161
+ display: grid;
162
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
163
+ gap: 1.5rem;
164
+ margin-bottom: 2rem;
165
+ }
166
+
167
+ /* 卡片样式 */
168
  .card {
169
  background: var(--card-bg);
170
+ border-radius: 16px;
171
  padding: 1.5rem;
172
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
173
+ border: 1px solid var(--surface-1);
174
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
175
+ position: relative;
176
+ overflow: hidden;
177
+ transform: translateY(0) scale(1);
178
+ opacity: 0;
179
+ transform: translateY(20px);
180
+ transition: opacity 0.5s, transform 0.5s;
181
  }
182
 
183
+ .card:hover {
184
+ transform: translateY(-4px) scale(1.005);
185
+ }
186
+
187
+ .card::before {
188
+ content: '';
189
+ position: absolute;
190
+ top: 0;
191
+ left: 0;
192
+ right: 0;
193
+ height: 3px;
194
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
195
+ opacity: 0;
196
+ transition: opacity 0.3s;
197
+ }
198
+
199
+ .card:hover::before {
200
+ opacity: 1;
201
  }
202
 
203
  .card-header {
 
204
  display: flex;
205
  align-items: center;
206
  justify-content: space-between;
207
+ margin-bottom: 1.5rem;
208
  }
209
 
210
  .card-title {
211
  font-size: 1.25rem;
212
  font-weight: 600;
213
+ color: var(--text-color);
214
  display: flex;
215
  align-items: center;
216
  gap: 0.75rem;
 
222
  display: flex;
223
  align-items: center;
224
  justify-content: center;
225
+ background: linear-gradient(45deg, var(--primary-color), var(--accent-color));
226
  border-radius: 8px;
227
  font-size: 1.25rem;
228
+ color: white;
229
  }
230
 
231
  /* 状态项样式 */
 
234
  justify-content: space-between;
235
  align-items: center;
236
  padding: 0.75rem 0;
237
+ border-bottom: 1px solid var(--surface-1);
238
+ position: relative;
239
+ transition: all 0.2s;
240
+ overflow: hidden;
241
  }
242
 
243
  .status-item:last-child {
244
  border-bottom: none;
245
  }
246
 
247
+ .status-item:hover {
248
+ background: var(--surface-1);
249
+ border-radius: 8px;
250
+ padding-left: 0.5rem;
251
+ padding-right: 0.5rem;
252
+ }
253
+
254
  .status-label {
255
+ color: var(--text-light);
256
  font-weight: 500;
257
  }
258
 
259
  .status-value {
 
260
  font-weight: 600;
261
+ color: var(--text-color);
262
  }
263
 
264
  .status-value.success {
265
+ color: var(--success);
266
  }
267
 
268
  .status-value.warning {
269
+ color: var(--warning);
270
  }
271
 
272
  .status-value.danger {
273
+ color: var(--error);
274
  }
275
 
276
  /* 模型标签 */
 
281
  }
282
 
283
  .model-tag {
 
284
  padding: 0.25rem 0.75rem;
285
+ background: var(--surface-1);
286
  border-radius: 16px;
287
  font-size: 0.875rem;
288
  color: var(--text-color);
289
+ border: 1px solid var(--surface-2);
290
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
291
+ transform: translateY(0);
292
+ }
293
+
294
+ .model-tag:hover {
295
+ background: var(--surface-2);
296
+ transform: translateY(-2px);
297
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
298
+ }
299
+
300
+ /* 进度条 */
301
+ .progress-container {
302
+ width: 100%;
303
+ height: 8px;
304
+ background: var(--surface-1);
305
+ border-radius: 4px;
306
+ overflow: hidden;
307
+ margin-top: 0.5rem;
308
+ }
309
+
310
+ .progress-bar {
311
+ height: 100%;
312
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
313
+ border-radius: 4px;
314
+ position: relative;
315
+ overflow: hidden;
316
+ transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
317
+ }
318
+
319
+ .progress-bar::after {
320
+ content: '';
321
+ position: absolute;
322
+ top: 0;
323
+ left: -100%;
324
+ width: 100%;
325
+ height: 100%;
326
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
327
+ animation: shimmer 2s infinite;
328
+ }
329
+
330
+ @keyframes shimmer {
331
+ 100% { transform: translateX(200%); }
332
+ }
333
+
334
+ .progress-bar.warning {
335
+ background: linear-gradient(90deg, var(--warning), #fbbf24);
336
+ }
337
+
338
+ .progress-bar.danger {
339
+ background: linear-gradient(90deg, var(--error), #dc2626);
340
+ }
341
+
342
+ /* 响应式设计 */
343
+ @media (max-width: 1024px) {
344
+ .grid {
345
+ grid-template-columns: repeat(2, 1fr);
346
+ }
347
+
348
+ .container {
349
+ padding: 1.5rem;
350
+ }
351
+ }
352
+
353
+ @media (max-width: 768px) {
354
+ .grid {
355
+ grid-template-columns: 1fr;
356
+ }
357
+
358
+ .container {
359
+ padding: 1rem;
360
+ }
361
+
362
+ .navbar {
363
+ padding: 0.75rem 1rem;
364
+ }
365
+
366
+ .navbar-title {
367
+ font-size: 1rem;
368
+ }
369
+
370
+ .card {
371
+ padding: 1rem;
372
+ }
373
+
374
+ .card-title {
375
+ font-size: 1.1rem;
376
+ }
377
+
378
+ .table-container {
379
+ margin: -1rem;
380
+ width: calc(100% + 2rem);
381
+ border-radius: 0;
382
+ }
383
+
384
+ .data-table th,
385
+ .data-table td {
386
+ padding: 0.75rem;
387
+ font-size: 0.875rem;
388
+ }
389
+
390
+ .token-count,
391
+ .call-count,
392
+ .compute-points {
393
+ font-size: 0.875rem;
394
+ padding: 0.2rem 0.4rem;
395
+ }
396
+
397
+ .model-tag {
398
+ font-size: 0.75rem;
399
+ padding: 0.2rem 0.5rem;
400
+ }
401
+
402
+ .btn-toggle {
403
+ font-size: 0.75rem;
404
+ padding: 0.4rem 0.75rem;
405
+ }
406
+
407
+ .endpoint-url {
408
+ font-size: 0.875rem;
409
+ padding: 0.5rem 0.75rem;
410
+ }
411
+
412
+ .back-to-top {
413
+ width: 40px;
414
+ height: 40px;
415
+ font-size: 1rem;
416
+ right: 1rem;
417
+ bottom: 1rem;
418
+ }
419
+ }
420
+
421
+ @media (max-width: 480px) {
422
+ .navbar-logo {
423
+ font-size: 1.25rem;
424
+ }
425
+
426
+ .btn-logout {
427
+ padding: 0.4rem 0.75rem;
428
+ font-size: 0.875rem;
429
+ }
430
+
431
+ .card-header {
432
+ flex-direction: column;
433
+ align-items: flex-start;
434
+ gap: 0.75rem;
435
+ }
436
+
437
+ .btn-toggle {
438
+ margin-left: 0;
439
+ width: 100%;
440
+ justify-content: center;
441
+ }
442
+
443
+ .status-item {
444
+ flex-direction: column;
445
+ align-items: flex-start;
446
+ gap: 0.5rem;
447
+ }
448
+
449
+ .progress-container {
450
+ margin-top: 0.5rem;
451
+ width: 100%;
452
+ }
453
  }
454
 
455
  /* 表格样式 */
456
  .table-container {
457
  overflow-x: auto;
458
  margin-top: 1rem;
459
+ border-radius: 12px;
460
+ border: 1px solid var(--surface-1);
461
+ background: var(--card-bg);
462
+ position: relative;
463
+ overflow: hidden;
464
  }
465
 
466
  .data-table {
 
470
  }
471
 
472
  .data-table th {
473
+ background: var(--surface-1);
474
+ padding: 1rem;
475
  font-weight: 600;
476
+ color: var(--text-color);
477
+ position: relative;
478
+ overflow: hidden;
479
  }
480
 
481
+ .data-table th::after {
482
+ content: '';
483
+ position: absolute;
484
+ bottom: 0;
485
+ left: 0;
486
+ width: 100%;
487
+ height: 2px;
488
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
489
+ transform: scaleX(0);
490
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
491
+ transform-origin: left;
492
  }
493
 
494
+ .data-table th:hover::after {
495
+ transform: scaleX(1);
496
  }
497
 
498
+ .data-table td {
499
+ padding: 1rem;
500
+ border-bottom: 1px solid var(--surface-1);
501
  }
502
 
503
+ .data-table tbody tr {
504
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
505
  }
506
 
507
+ .data-table tbody tr:hover {
508
+ transform: scale(1.01);
509
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
510
+ z-index: 1;
511
  }
512
 
513
+ .data-table tbody tr:last-child td {
514
+ border-bottom: none;
 
 
515
  }
516
 
517
+ /* 特殊值样式 */
518
+ .token-count {
519
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
520
+ color: var(--primary-color);
521
+ font-weight: 600;
 
 
 
522
  position: relative;
523
+ padding: 0.25rem 0.5rem;
524
+ border-radius: 4px;
525
+ background: var(--surface-1);
526
+ transition: all 0.2s;
527
  }
528
 
529
+ .token-count:hover {
530
+ background: var(--surface-2);
531
+ transform: scale(1.1);
 
 
 
532
  }
533
 
534
+ .call-count {
535
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
536
+ color: var(--success);
537
+ font-weight: 600;
538
+ position: relative;
539
+ padding: 0.25rem 0.5rem;
540
+ border-radius: 4px;
541
+ background: var(--surface-1);
542
+ transition: all 0.2s;
543
  }
544
 
545
+ .call-count:hover {
546
+ background: var(--surface-2);
547
+ transform: scale(1.1);
548
  }
549
 
550
+ .compute-points {
551
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
552
+ color: var(--accent-color);
553
+ font-weight: 600;
554
+ position: relative;
555
+ padding: 0.25rem 0.5rem;
556
+ border-radius: 4px;
557
+ background: var(--surface-1);
558
+ transition: all 0.2s;
 
 
 
 
559
  }
560
 
561
+ .compute-points:hover {
562
+ background: var(--surface-2);
563
+ transform: scale(1.1);
 
 
 
 
564
  }
565
 
566
  /* API端点卡片 */
567
  .endpoint-item {
568
+ background: var(--surface-1);
569
+ padding: 1.25rem;
570
+ border-radius: 12px;
571
  margin-bottom: 1rem;
572
  border-left: 3px solid var(--primary-color);
573
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
574
+ transform: translateX(0);
575
+ }
576
+
577
+ .endpoint-item:hover {
578
+ background: var(--surface-2);
579
+ transform: translateX(8px);
580
  }
581
 
582
  .endpoint-url {
583
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
584
+ background: var(--card-bg);
585
+ padding: 0.75rem 1rem;
586
+ border-radius: 8px;
587
+ margin-top: 0.5rem;
588
  display: inline-block;
589
+ border: 1px solid var(--surface-1);
590
+ transition: all 0.2s;
591
+ cursor: pointer;
592
  }
593
 
594
+ .endpoint-url:hover {
595
+ border-color: var(--primary-color);
596
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1);
 
 
597
  }
598
 
599
  /* 页脚 */
600
  .footer {
601
  text-align: center;
602
  padding: 2rem 0;
603
+ color: var(--text-light);
604
  font-size: 0.9rem;
605
+ border-top: 1px solid var(--surface-1);
606
  margin-top: 2rem;
607
+ background: var(--card-bg);
608
+ position: relative;
609
+ overflow: hidden;
610
  }
611
 
612
+ .footer::before {
613
+ content: '';
614
+ position: absolute;
615
+ top: 0;
616
+ left: 0;
617
+ right: 0;
618
+ height: 1px;
619
+ background: linear-gradient(
620
+ 90deg,
621
+ transparent,
622
+ var(--primary-color),
623
+ var(--accent-color),
624
+ transparent
625
+ );
626
+ animation: footerGlow 3s infinite;
627
+ }
628
+
629
+ @keyframes footerGlow {
630
+ 0%, 100% {
631
+ opacity: 0.3;
632
+ }
633
+ 50% {
634
+ opacity: 0.7;
635
+ }
636
+ }
637
+
638
+ /* 返回顶部按钮 */
639
+ .back-to-top {
640
+ position: fixed;
641
+ bottom: 2rem;
642
+ right: 2rem;
643
+ width: 48px;
644
+ height: 48px;
645
+ border-radius: 50%;
646
+ background: var(--card-bg);
647
+ border: none;
648
+ color: var(--text-color);
649
+ cursor: pointer;
650
+ display: flex;
651
  align-items: center;
652
  justify-content: center;
 
 
 
 
653
  transition: all 0.3s;
654
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
655
+ opacity: 0;
656
+ visibility: hidden;
657
+ transform: translateY(20px);
658
  }
659
 
660
+ .back-to-top.visible {
661
+ opacity: 1;
662
+ visibility: visible;
663
+ transform: translateY(0);
664
+ }
665
+
666
+ .back-to-top:hover {
667
+ background: var(--primary-color);
668
+ color: white;
669
  transform: translateY(-5px);
 
670
  }
671
 
672
  /* 滚动条美化 */
 
676
  }
677
 
678
  ::-webkit-scrollbar-track {
679
+ background: var(--surface-1);
680
+ border-radius: 4px;
681
  }
682
 
683
  ::-webkit-scrollbar-thumb {
684
+ background: var(--primary-color);
685
  border-radius: 4px;
686
+ border: 2px solid var(--surface-1);
687
  }
688
 
689
  ::-webkit-scrollbar-thumb:hover {
690
+ background: var(--primary-dark);
691
  }
692
 
693
  /* 模型统计折叠样式 */
694
+ .model-stats {
695
+ position: relative;
696
+ }
697
+
698
  .hidden-model {
699
  display: none;
700
+ animation: fadeOut 0.3s ease;
701
+ }
702
+
703
+ .hidden-model.show {
704
+ display: block;
705
+ animation: fadeIn 0.3s ease;
706
+ }
707
+
708
+ @keyframes fadeIn {
709
+ from {
710
+ opacity: 0;
711
+ transform: translateY(-10px);
712
+ }
713
+ to {
714
+ opacity: 1;
715
+ transform: translateY(0);
716
+ }
717
+ }
718
+
719
+ @keyframes fadeOut {
720
+ from {
721
+ opacity: 1;
722
+ transform: translateY(0);
723
+ }
724
+ to {
725
+ opacity: 0;
726
+ transform: translateY(-10px);
727
+ }
728
  }
729
 
730
  .btn-toggle {
731
+ background: var(--surface-1);
732
+ border: 1px solid var(--surface-2);
733
+ border-radius: 8px;
734
+ padding: 0.5rem 1rem;
735
+ color: var(--text-color);
736
  cursor: pointer;
737
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
738
+ font-size: 0.875rem;
739
+ font-weight: 500;
740
+ display: flex;
741
+ align-items: center;
742
+ gap: 0.5rem;
743
  margin-left: auto;
744
  }
745
 
746
  .btn-toggle:hover {
747
+ background: var(--surface-2);
748
+ transform: translateY(-2px);
749
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
750
+ }
751
+
752
+ .btn-toggle .icon {
753
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
754
+ }
755
+
756
+ .btn-toggle.expanded .icon {
757
+ transform: rotate(180deg);
758
  }
759
 
760
  /* Token注释样式 */
761
  .token-note {
762
+ margin-top: 1rem;
763
+ color: var(--text-light);
764
  font-style: italic;
765
+ line-height: 1.6;
766
+ padding: 1rem;
767
+ border-radius: 8px;
768
+ background: var(--surface-1);
769
+ border: 1px solid var(--surface-2);
770
+ position: relative;
771
+ transition: all 0.3s;
772
+ }
773
+
774
+ .token-note::before {
775
+ content: '💡';
776
+ position: absolute;
777
+ top: -12px;
778
+ left: 1rem;
779
+ background: var(--card-bg);
780
+ padding: 0 0.5rem;
781
+ transition: all 0.3s;
782
+ }
783
+
784
+ .token-note:hover {
785
+ transform: translateY(-2px);
786
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1);
787
+ }
788
+
789
+ .token-note:hover::before {
790
+ transform: scale(1.2);
791
  }
792
 
793
  .token-model-table {
794
+ margin-top: 1.5rem;
795
  }
796
 
797
+ /* 健康检查状态 */
798
+ .health-status {
799
+ display: inline-flex;
800
+ align-items: center;
801
+ gap: 0.5rem;
802
+ padding: 0.25rem 0.75rem;
803
+ border-radius: 16px;
804
+ font-size: 0.875rem;
805
+ font-weight: 500;
806
+ background: var(--success);
807
+ color: white;
808
+ animation: pulse 2s infinite;
809
+ position: relative;
810
+ overflow: hidden;
811
+ }
812
+
813
+ .health-status.warning {
814
+ background: var(--warning);
815
+ }
816
+
817
+ .health-status.error {
818
+ background: var(--error);
819
+ }
820
+
821
+ .health-status::before {
822
+ content: '';
823
+ position: absolute;
824
+ top: -50%;
825
+ left: -50%;
826
+ width: 200%;
827
+ height: 200%;
828
+ background: linear-gradient(
829
+ 45deg,
830
+ transparent,
831
+ rgba(255, 255, 255, 0.1),
832
+ transparent
833
+ );
834
+ transform: rotate(45deg);
835
+ animation: shine 3s infinite;
836
+ }
837
+
838
+ @keyframes shine {
839
+ 0% {
840
+ transform: translateX(-100%) rotate(45deg);
841
+ }
842
+ 100% {
843
+ transform: translateX(100%) rotate(45deg);
844
+ }
845
+ }
846
+
847
+ @keyframes pulse {
848
+ 0% {
849
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
850
+ }
851
+ 70% {
852
+ box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
853
+ }
854
+ 100% {
855
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
856
+ }
857
+ }
858
+
859
+ /* 加载动画 */
860
+ @keyframes shimmer {
861
+ 0% {
862
+ background-position: -1000px 0;
863
+ }
864
+ 100% {
865
+ background-position: 1000px 0;
866
+ }
867
+ }
868
+
869
+ .loading {
870
+ position: relative;
871
+ overflow: hidden;
872
+ }
873
+
874
+ .loading::after {
875
+ content: '';
876
+ position: absolute;
877
+ top: 0;
878
+ left: 0;
879
+ width: 100%;
880
+ height: 100%;
881
+ background: linear-gradient(
882
+ 90deg,
883
+ transparent,
884
+ var(--surface-2),
885
+ transparent
886
+ );
887
+ animation: shimmer 2s infinite linear;
888
+ background-size: 1000px 100%;
889
+ }
890
+
891
+ /* 页面加载动画 */
892
+ .page-loader {
893
+ position: fixed;
894
+ top: 0;
895
+ left: 0;
896
+ width: 100%;
897
+ height: 100%;
898
+ background: var(--background);
899
+ display: flex;
900
+ justify-content: center;
901
+ align-items: center;
902
+ z-index: 9999;
903
+ opacity: 1;
904
+ visibility: visible;
905
+ transition: all 0.5s;
906
+ }
907
+
908
+ .page-loader.hidden {
909
+ opacity: 0;
910
+ visibility: hidden;
911
+ }
912
+
913
+ .loader {
914
+ width: 48px;
915
+ height: 48px;
916
+ border: 3px solid var(--surface-1);
917
+ border-radius: 50%;
918
+ display: inline-block;
919
+ position: relative;
920
+ box-sizing: border-box;
921
+ animation: rotation 1s linear infinite;
922
+ }
923
+
924
+ .loader::after {
925
+ content: '';
926
+ box-sizing: border-box;
927
+ position: absolute;
928
+ left: 50%;
929
+ top: 50%;
930
+ transform: translate(-50%, -50%);
931
+ width: 40px;
932
+ height: 40px;
933
+ border-radius: 50%;
934
+ border: 3px solid transparent;
935
+ border-bottom-color: var(--primary-color);
936
+ }
937
+
938
+ @keyframes rotation {
939
+ 0% {
940
+ transform: rotate(0deg);
941
+ }
942
+ 100% {
943
+ transform: rotate(360deg);
944
+ }
945
+ }
946
+
947
+ /* 数据加载中状态 */
948
+ .loading-overlay {
949
+ position: fixed;
950
+ top: 0;
951
+ left: 0;
952
+ width: 100%;
953
+ height: 100%;
954
+ background: var(--background);
955
+ display: flex;
956
+ flex-direction: column;
957
+ justify-content: center;
958
+ align-items: center;
959
+ z-index: 9999;
960
+ transition: opacity 0.3s, visibility 0.3s;
961
+ }
962
+
963
+ .loading .loading-overlay {
964
+ opacity: 1;
965
+ visibility: visible;
966
+ }
967
+
968
+ body:not(.loading) .loading-overlay {
969
+ opacity: 0;
970
+ visibility: hidden;
971
+ }
972
+
973
+ .loader {
974
+ width: 48px;
975
+ height: 48px;
976
+ border: 3px solid var(--primary-color);
977
+ border-radius: 50%;
978
+ display: inline-block;
979
+ position: relative;
980
+ box-sizing: border-box;
981
+ animation: rotation 1s linear infinite;
982
+ }
983
+
984
+ .loader::after {
985
+ content: '';
986
+ box-sizing: border-box;
987
+ position: absolute;
988
+ left: 50%;
989
+ top: 50%;
990
+ transform: translate(-50%, -50%);
991
+ width: 40px;
992
+ height: 40px;
993
+ border-radius: 50%;
994
+ border: 3px solid transparent;
995
+ border-bottom-color: var(--primary-dark);
996
+ }
997
+
998
+ @keyframes rotation {
999
+ 0% {
1000
+ transform: rotate(0deg);
1001
+ }
1002
+ 100% {
1003
+ transform: rotate(360deg);
1004
+ }
1005
+ }
1006
+
1007
+ /* 卡片动画 */
1008
+ .card {
1009
+ opacity: 0;
1010
+ transform: translateY(20px);
1011
+ transition: opacity 0.5s, transform 0.5s;
1012
+ }
1013
+
1014
+ .card.animate {
1015
+ opacity: 1;
1016
+ transform: translateY(0);
1017
+ }
1018
+
1019
+ /* 状态项动画 */
1020
+ .status-item {
1021
+ position: relative;
1022
+ overflow: hidden;
1023
+ }
1024
+
1025
+ .status-item::before {
1026
+ content: '';
1027
+ position: absolute;
1028
+ top: -50%;
1029
+ left: -50%;
1030
+ width: 200%;
1031
+ height: 200%;
1032
+ background: radial-gradient(
1033
+ circle,
1034
+ rgba(255, 255, 255, 0.1) 0%,
1035
+ transparent 70%
1036
+ );
1037
+ transform: rotate(0deg);
1038
+ animation: rotate 10s linear infinite;
1039
+ }
1040
+
1041
+ @keyframes rotate {
1042
+ 0% {
1043
+ transform: rotate(0deg);
1044
+ }
1045
+ 100% {
1046
+ transform: rotate(360deg);
1047
+ }
1048
+ }
1049
+
1050
+ /* 健康状态动画 */
1051
+ .health-status {
1052
+ position: relative;
1053
+ display: inline-flex;
1054
+ align-items: center;
1055
+ gap: 0.5rem;
1056
+ padding: 0.5rem 1rem;
1057
+ border-radius: 20px;
1058
+ background: var(--surface-1);
1059
+ transition: all 0.3s;
1060
+ }
1061
+
1062
+ .health-status .status-indicator {
1063
+ width: 10px;
1064
+ height: 10px;
1065
+ border-radius: 50%;
1066
+ background: var(--success);
1067
+ position: relative;
1068
+ }
1069
+
1070
+ .health-status[data-status="healthy"] .status-indicator {
1071
+ background: var(--success);
1072
+ }
1073
+
1074
+ .health-status[data-status="warning"] .status-indicator {
1075
+ background: var(--warning);
1076
+ }
1077
+
1078
+ .health-status[data-status="error"] .status-indicator {
1079
+ background: var(--error);
1080
+ }
1081
+
1082
+ .health-status .status-indicator::after {
1083
+ content: '';
1084
+ position: absolute;
1085
+ top: -2px;
1086
+ left: -2px;
1087
+ right: -2px;
1088
+ bottom: -2px;
1089
+ border-radius: 50%;
1090
+ border: 2px solid currentColor;
1091
+ opacity: 0;
1092
+ animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
1093
+ }
1094
+
1095
+ @keyframes ping {
1096
+ 75%, 100% {
1097
+ transform: scale(2);
1098
+ opacity: 0;
1099
+ }
1100
+ }
1101
+
1102
+ /* 表格动画 */
1103
+ .table-container {
1104
+ position: relative;
1105
+ overflow: hidden;
1106
+ }
1107
+
1108
+ .table-container::after {
1109
+ content: '';
1110
+ position: absolute;
1111
+ top: 0;
1112
+ right: 0;
1113
+ width: 40px;
1114
+ height: 100%;
1115
+ background: linear-gradient(
1116
+ to right,
1117
+ transparent,
1118
+ var(--card-bg) 50%
1119
+ );
1120
+ pointer-events: none;
1121
+ }
1122
+
1123
+ tbody tr {
1124
+ transition: all 0.2s;
1125
+ }
1126
+
1127
+ tbody tr:hover {
1128
+ background: var(--surface-1);
1129
+ transform: scale(1.01);
1130
+ }
1131
+
1132
+ /* API端点动画 */
1133
+ .endpoint-card {
1134
+ position: relative;
1135
+ overflow: hidden;
1136
+ }
1137
+
1138
+ .endpoint-card::after {
1139
+ content: '';
1140
+ position: absolute;
1141
+ top: -50%;
1142
+ left: -50%;
1143
+ width: 200%;
1144
+ height: 200%;
1145
+ background: linear-gradient(
1146
+ 45deg,
1147
+ transparent,
1148
+ rgba(255, 255, 255, 0.1),
1149
+ transparent
1150
+ );
1151
+ transform: translateX(-100%);
1152
+ transition: transform 0.3s;
1153
+ }
1154
+
1155
+ .endpoint-card:hover::after {
1156
+ transform: translateX(100%);
1157
+ }
1158
+
1159
+ /* 主题切换按钮动画 */
1160
+ .theme-toggle {
1161
+ position: fixed;
1162
+ bottom: 2rem;
1163
+ left: 2rem;
1164
+ width: 48px;
1165
+ height: 48px;
1166
+ border-radius: 50%;
1167
+ background: var(--card-bg);
1168
+ border: none;
1169
+ color: var(--text-color);
1170
+ cursor: pointer;
1171
+ display: flex;
1172
+ align-items: center;
1173
+ justify-content: center;
1174
+ transition: all 0.3s;
1175
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1176
+ }
1177
+
1178
+ .theme-toggle:hover {
1179
+ transform: rotate(180deg);
1180
+ background: var(--primary-color);
1181
+ color: white;
1182
+ }
1183
+
1184
+ /* 返回顶部按钮动画 */
1185
+ .back-to-top {
1186
+ position: fixed;
1187
+ bottom: 2rem;
1188
+ right: 2rem;
1189
+ width: 48px;
1190
+ height: 48px;
1191
+ border-radius: 50%;
1192
+ background: var(--card-bg);
1193
+ border: none;
1194
+ color: var(--text-color);
1195
+ cursor: pointer;
1196
+ display: flex;
1197
+ align-items: center;
1198
+ justify-content: center;
1199
+ transition: all 0.3s;
1200
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1201
+ opacity: 0;
1202
+ visibility: hidden;
1203
+ transform: translateY(20px);
1204
+ }
1205
+
1206
+ .back-to-top.visible {
1207
+ opacity: 1;
1208
+ visibility: visible;
1209
+ transform: translateY(0);
1210
+ }
1211
+
1212
+ .back-to-top:hover {
1213
+ background: var(--primary-color);
1214
+ color: white;
1215
+ transform: translateY(-5px);
1216
+ }
1217
+
1218
+ /* 响应式优化 */
1219
  @media (max-width: 768px) {
1220
+ .status-grid {
1221
+ grid-template-columns: 1fr;
1222
+ }
1223
+
1224
+ .endpoints-grid {
1225
+ grid-template-columns: 1fr;
1226
  }
1227
 
1228
  .navbar {
1229
  padding: 1rem;
1230
  }
1231
 
1232
+ .navbar .brand h1 {
1233
+ font-size: 1.2rem;
1234
+ }
1235
+
1236
+ .card {
1237
+ margin: 1rem;
1238
+ padding: 1rem;
1239
+ }
1240
+
1241
+ .theme-toggle,
1242
+ .back-to-top {
1243
+ width: 40px;
1244
+ height: 40px;
1245
+ }
1246
+ }
1247
+
1248
+ @media (max-width: 480px) {
1249
+ .token-overview,
1250
+ .points-overview {
1251
+ flex-direction: column;
1252
+ }
1253
+
1254
+ .table-container {
1255
+ margin: 0 -1rem;
1256
+ width: calc(100% + 2rem);
1257
+ }
1258
+
1259
+ .health-status {
1260
+ width: 100%;
1261
+ justify-content: center;
1262
+ }
1263
+ }
1264
+
1265
+ /* 打印优化 */
1266
+ @media print {
1267
+ .background-animation,
1268
+ .loading-overlay,
1269
+ .theme-toggle,
1270
+ .back-to-top {
1271
+ display: none !important;
1272
+ }
1273
+
1274
  .card {
1275
+ break-inside: avoid;
1276
+ page-break-inside: avoid;
1277
+ border: 1px solid #ddd;
1278
+ margin: 1rem 0;
1279
  padding: 1rem;
1280
  }
1281
 
1282
+ .status-grid,
1283
+ .endpoints-grid {
1284
+ grid-template-columns: 1fr !important;
1285
+ }
1286
+
1287
+ .token-overview,
1288
+ .points-overview {
1289
+ flex-direction: column;
1290
+ }
1291
+
1292
+ .table-container::after {
1293
+ display: none;
1294
  }
1295
  }
1296
  </style>
1297
  </head>
1298
+ <body class="loading">
1299
+ <div class="background-animation"></div>
1300
 
1301
  <nav class="navbar">
1302
+ <div class="brand">
1303
+ <img src="/static/logo.png" alt="Abacus Chat Logo" class="logo">
1304
+ <h1>Abacus Chat代理仪表板</h1>
1305
+ </div>
1306
+ <div class="nav-actions">
1307
+ <button class="btn btn-secondary" onclick="location.href='/logout'">
1308
+ <span class="material-icons">logout</span>
1309
+ 退出登录
1310
+ </button>
1311
  </div>
1312
  </nav>
1313
 
1314
  <div class="container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1315
  <div class="grid">
1316
+ <!-- 系统状态卡片 -->
1317
  <div class="card">
1318
  <div class="card-header">
1319
  <h2 class="card-title">
1320
+ <div class="card-icon">📊</div>
1321
+ 系统状态
1322
  </h2>
1323
  </div>
1324
  <div class="status-item">
1325
+ <span class="status-label">运行时间</span>
1326
+ <span class="status-value">{{ uptime }}</span>
1327
+ </div>
1328
+ <div class="status-item">
1329
+ <span class="status-label">健康检查</span>
1330
+ <span class="health-status">
1331
+ <span>✓</span>
1332
+ <span>{{ health_checks }}次</span>
1333
+ </span>
1334
  </div>
1335
  <div class="status-item">
1336
+ <span class="status-label">用户数量</span>
1337
+ <span class="status-value">{{ user_count }}</span>
1338
+ </div>
1339
+ </div>
1340
+
1341
+ <!-- Token使用统计卡片 -->
1342
+ <div class="card">
1343
+ <div class="card-header">
1344
+ <h2 class="card-title">
1345
+ <div class="card-icon">🎯</div>
1346
+ Token使用统计
1347
+ </h2>
1348
  </div>
1349
  <div class="status-item">
1350
+ <span class="status-label">输入Token</span>
1351
+ <span class="token-count">{{ "{:,}".format(total_tokens["prompt"]) }}</span>
1352
  </div>
1353
  <div class="status-item">
1354
+ <span class="status-label">输出Token</span>
1355
+ <span class="token-count">{{ "{:,}".format(total_tokens["completion"]) }}</span>
 
 
 
 
 
 
 
1356
  </div>
 
1357
  <div class="status-item">
1358
+ <span class="status-label">总Token</span>
1359
+ <span class="token-count">{{ "{:,}".format(total_tokens["total"]) }}</span>
1360
+ </div>
1361
+ <div class="token-note">
1362
+ 注意:此处显示的Token数量仅包含通过代理使用的Token,不包含在Abacus网站上直接使用的Token。这是一个粗略估计,可能与实际使用量有所偏差。
1363
  </div>
 
1364
  </div>
1365
+
1366
+ <!-- 计算点使用情况卡片 -->
1367
  <div class="card">
1368
  <div class="card-header">
1369
  <h2 class="card-title">
1370
+ <div class="card-icon">💎</div>
1371
+ 计算点使用情况
1372
  </h2>
1373
  </div>
1374
  <div class="status-item">
1375
+ <span class="status-label">剩余计算点</span>
1376
+ <span class="compute-points">{{ "{:,}".format(compute_points["left"]) }}</span>
1377
  </div>
1378
  <div class="status-item">
1379
+ <span class="status-label">已用计算点</span>
1380
+ <span class="compute-points">{{ "{:,}".format(compute_points["used"]) }}</span>
1381
  </div>
1382
  <div class="status-item">
1383
+ <span class="status-label">总计算点</span>
1384
+ <span class="compute-points">{{ "{:,}".format(compute_points["total"]) }}</span>
1385
  </div>
1386
+ <div class="status-item">
1387
+ <span class="status-label">使用百分比</span>
1388
+ <div class="progress-container">
1389
+ <div class="progress-bar{% if compute_points['percentage'] > 80 %} danger{% elif compute_points['percentage'] > 60 %} warning{% endif %}" style="width: {{ compute_points['percentage'] }}%"></div>
1390
+ </div>
1391
+ <span class="status-value{% if compute_points['percentage'] > 80 %} danger{% elif compute_points['percentage'] > 60 %} warning{% endif %}">{{ compute_points["percentage"] }}%</span>
1392
  </div>
1393
+ {% if compute_points["last_update"] %}
1394
+ <div class="status-item">
1395
+ <span class="status-label">最后更新</span>
1396
+ <span class="status-value">{{ compute_points["last_update"].strftime('%Y-%m-%d %H:%M:%S') }}</span>
1397
+ </div>
1398
+ {% endif %}
1399
+ </div>
1400
+ </div>
1401
+
1402
+ <!-- 可用模型卡片 -->
1403
+ <div class="card">
1404
+ <div class="card-header">
1405
+ <h2 class="card-title">
1406
+ <div class="card-icon">🤖</div>
1407
+ 可用模型
1408
+ </h2>
1409
+ </div>
1410
+ <div class="models-list">
1411
+ {% for model in models %}
1412
+ <span class="model-tag">{{ model }}</span>
1413
+ {% endfor %}
1414
+ </div>
1415
+ </div>
1416
+
1417
+ <!-- 模型使用统计卡片 -->
1418
+ <div class="card">
1419
+ <div class="card-header">
1420
+ <h2 class="card-title">
1421
+ <div class="card-icon">📈</div>
1422
+ 模型使用统计
1423
+ </h2>
1424
+ <button class="btn-toggle" onclick="toggleModels(this)">
1425
+ <span class="text">显示全部</span>
1426
+ <span class="icon">▼</span>
1427
+ </button>
1428
+ </div>
1429
+ <div class="model-stats">
1430
  <div class="table-container">
1431
+ <table class="data-table">
1432
  <thead>
1433
  <tr>
1434
+ <th>模型名称</th>
1435
+ <th>调用次数</th>
1436
  <th>输入Token</th>
1437
  <th>输出Token</th>
1438
+ <th>总Token</th>
1439
  </tr>
1440
  </thead>
1441
  <tbody>
1442
  {% for model, stats in model_stats.items() %}
1443
+ <tr class="{{ 'hidden-model' if loop.index > 5 }}">
1444
  <td>{{ model }}</td>
1445
+ <td><span class="call-count">{{ stats["count"] }}</span></td>
1446
+ <td><span class="token-count">{{ "{:,}".format(stats["prompt_tokens"]) }}</span></td>
1447
+ <td><span class="token-count">{{ "{:,}".format(stats["completion_tokens"]) }}</span></td>
1448
+ <td><span class="token-count">{{ "{:,}".format(stats["total_tokens"]) }}</span></td>
1449
  </tr>
1450
  {% endfor %}
1451
  </tbody>
1452
  </table>
1453
  </div>
1454
+ <div class="token-note">
1455
+ 💡 Token计算说明:输入Token包括用户发送的所有消息,输出Token包括AI助手的所有回复。总Token为两者之和。这些数据是使用tiktoken库计算的估算值,可能与实际计费有所差异。
1456
+ </div>
1457
  </div>
1458
  </div>
1459
+
1460
+ <!-- 计算点使用日志卡片 -->
1461
  <div class="card">
1462
  <div class="card-header">
1463
  <h2 class="card-title">
1464
+ <div class="card-icon">📊</div>
1465
  计算点使用日志
1466
  </h2>
1467
  </div>
 
1478
  {% for entry in compute_points_log.log %}
1479
  <tr>
1480
  {% for key, value in compute_points_log.columns.items() %}
1481
+ <td>
1482
+ {% if key == 'timestamp' %}
1483
+ {{ entry.get(key, '').split('T')[0] }}
1484
+ {% elif key == 'computePoints' %}
1485
+ <span class="compute-points">{{ "{:,}".format(entry.get(key, 0)) }}</span>
1486
+ {% elif key == 'llmName' %}
1487
+ <span class="model-tag">{{ entry.get(key, 'Unknown') }}</span>
1488
+ {% else %}
1489
+ {{ entry.get(key, 0) }}
1490
+ {% endif %}
1491
+ </td>
1492
  {% endfor %}
1493
  </tr>
1494
  {% endfor %}
1495
  </tbody>
1496
  </table>
1497
  </div>
1498
+ <div class="token-note">
1499
+ 💡 此日志显示了计算点的详细使用记录,包括使用时间、消耗的计算点数量以及对应的模型。数据每小时更新一次。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1500
  </div>
1501
  </div>
1502
+
1503
+ <!-- API端点卡片 -->
1504
  <div class="card">
1505
  <div class="card-header">
1506
  <h2 class="card-title">
1507
+ <div class="card-icon">🔌</div>
1508
+ API端点
1509
  </h2>
1510
  </div>
1511
  <div class="endpoint-item">
1512
+ <div>获取可用模型列表</div>
1513
+ <div class="endpoint-url" onclick="copyToClipboard(this)">GET /v1/models</div>
1514
  </div>
1515
  <div class="endpoint-item">
1516
+ <div>发送聊天请求</div>
1517
+ <div class="endpoint-url" onclick="copyToClipboard(this)">POST /v1/chat/completions</div>
1518
  </div>
1519
  <div class="endpoint-item">
1520
+ <div>健康检查</div>
1521
+ <div class="endpoint-url" onclick="copyToClipboard(this)">GET /health</div>
1522
  </div>
1523
  </div>
1524
+
1525
+ <footer class="footer">
1526
+ <p>© {{ year }} Abacus Chat Proxy. All rights reserved.</p>
1527
+ </footer>
1528
  </div>
1529
+
1530
+ <!-- 返回顶部按钮 -->
1531
+ <button class="back-to-top" onclick="scrollToTop()" title="返回顶部">
1532
+
1533
+ </button>
1534
+
1535
+ <!-- 添加页面加载器到body -->
1536
+ <div class="page-loader">
1537
+ <div class="loader"></div>
1538
+ </div>
1539
+
1540
+ <!-- 添加加载覆盖层到每个卡片 -->
1541
+ <div class="loading-overlay">
1542
+ <div class="loading-spinner"></div>
1543
+ </div>
1544
+
1545
  <script>
1546
+ // 复制到剪贴板
1547
+ function copyToClipboard(element) {
1548
+ const text = element.textContent;
1549
+ navigator.clipboard.writeText(text).then(() => {
1550
+ const originalText = element.textContent;
1551
+ element.textContent = '已复制!';
1552
+ element.style.color = 'var(--success)';
1553
+ setTimeout(() => {
1554
+ element.textContent = originalText;
1555
+ element.style.color = '';
1556
+ }, 1500);
1557
+ });
1558
+ }
1559
+
1560
+ // 切换模型显示
1561
+ function toggleModels(button) {
1562
+ const hiddenModels = document.querySelectorAll('.hidden-model');
1563
+ const textSpan = button.querySelector('.text');
1564
+ const iconSpan = button.querySelector('.icon');
1565
+
1566
+ hiddenModels.forEach(model => {
1567
+ model.classList.toggle('show');
1568
+ });
1569
+
1570
+ button.classList.toggle('expanded');
1571
+ textSpan.textContent = button.classList.contains('expanded') ? '隐藏部分' : '显示全部';
1572
+ }
1573
+
1574
+ // 返回顶部按钮
1575
+ const backToTopButton = document.querySelector('.back-to-top');
1576
 
1577
+ window.onscroll = function() {
1578
+ if (document.body.scrollTop > 500 || document.documentElement.scrollTop > 500) {
1579
+ backToTopButton.classList.add('visible');
 
 
1580
  } else {
1581
+ backToTopButton.classList.remove('visible');
1582
  }
1583
+ };
 
 
 
1584
 
1585
+ function scrollToTop() {
1586
+ window.scrollTo({
1587
+ top: 0,
1588
+ behavior: 'smooth'
1589
+ });
1590
+ }
1591
+
1592
+ // 健康状态动画
1593
+ const healthStatus = document.querySelector('.health-status');
1594
+ if (healthStatus) {
1595
+ setInterval(() => {
1596
+ healthStatus.style.animation = 'none';
1597
+ healthStatus.offsetHeight; // 触发重排
1598
+ healthStatus.style.animation = 'pulse 2s infinite';
1599
+ }, 2000);
1600
+ }
1601
+
1602
+ // 页面加载完成后隐藏加载器
1603
+ window.addEventListener('load', () => {
1604
+ const pageLoader = document.querySelector('.page-loader');
1605
+ pageLoader.classList.add('hidden');
1606
+ });
1607
+
1608
+ // 数据加载状态模拟
1609
+ function showLoading(element) {
1610
+ const overlay = element.querySelector('.loading-overlay');
1611
+ if (overlay) {
1612
+ overlay.classList.add('visible');
1613
+ }
1614
+ }
1615
+
1616
+ function hideLoading(element) {
1617
+ const overlay = element.querySelector('.loading-overlay');
1618
+ if (overlay) {
1619
+ overlay.classList.remove('visible');
1620
+ }
1621
+ }
1622
+
1623
+ // 示例:模拟数据加载
1624
+ document.querySelectorAll('.card').forEach(card => {
1625
+ showLoading(card);
1626
+ setTimeout(() => {
1627
+ hideLoading(card);
1628
+ }, Math.random() * 1000 + 500); // 随机延迟以模拟不同加载时间
1629
+ });
1630
+
1631
+ // 表格搜索功能
1632
+ function initTableSearch() {
1633
+ document.querySelectorAll('table').forEach(table => {
1634
+ const wrapper = document.createElement('div');
1635
+ wrapper.className = 'table-container';
1636
+ table.parentNode.insertBefore(wrapper, table);
1637
+ wrapper.appendChild(table);
1638
+
1639
+ const search = document.createElement('input');
1640
+ search.type = 'text';
1641
+ search.className = 'table-search';
1642
+ search.placeholder = '搜索表格内容...';
1643
+ wrapper.insertBefore(search, table);
1644
+
1645
+ search.addEventListener('input', e => {
1646
+ const searchText = e.target.value.toLowerCase();
1647
+ const rows = table.querySelectorAll('tbody tr');
1648
+
1649
+ rows.forEach(row => {
1650
+ const text = row.textContent.toLowerCase();
1651
+ row.style.display = text.includes(searchText) ? '' : 'none';
1652
+ });
1653
  });
1654
+ });
1655
+ }
1656
+
1657
+ // 卡片折叠功能
1658
+ function initCardCollapse() {
1659
+ document.querySelectorAll('.card .card-header').forEach(header => {
1660
+ if (!header.querySelector('.btn-toggle')) {
1661
+ const btn = document.createElement('button');
1662
+ btn.className = 'btn-toggle';
1663
+ btn.innerHTML = '<span class="sr-only">折叠卡片</span>▼';
1664
+ header.appendChild(btn);
1665
+
1666
+ btn.addEventListener('click', () => {
1667
+ const card = header.closest('.card');
1668
+ card.classList.toggle('collapsed');
1669
+ btn.innerHTML = card.classList.contains('collapsed') ?
1670
+ '<span class="sr-only">展开卡片</span>▶' :
1671
+ '<span class="sr-only">折叠卡片</span>▼';
1672
+ });
1673
+ }
1674
+ });
1675
+ }
1676
+
1677
+ // API端点点击复制
1678
+ function initApiEndpointCopy() {
1679
+ document.querySelectorAll('.api-endpoint').forEach(endpoint => {
1680
+ endpoint.style.cursor = 'pointer';
1681
+ endpoint.setAttribute('role', 'button');
1682
+ endpoint.setAttribute('tabindex', '0');
1683
 
1684
+ endpoint.addEventListener('click', async () => {
1685
+ try {
1686
+ await navigator.clipboard.writeText(endpoint.textContent);
1687
+ showNotification('API端点已复制到剪贴板', 'success');
1688
+ } catch (err) {
1689
+ showNotification('复制失败,请手动复制', 'error');
1690
+ }
1691
+ });
1692
+
1693
+ endpoint.addEventListener('keydown', e => {
1694
+ if (e.key === 'Enter' || e.key === ' ') {
1695
+ e.preventDefault();
1696
+ endpoint.click();
1697
+ }
1698
+ });
1699
  });
1700
  }
1701
+
1702
+ // 健康检查状态动画
1703
+ function updateHealthStatus(status) {
1704
+ const healthIndicator = document.querySelector('.health-status');
1705
+ if (!healthIndicator) return;
1706
+
1707
+ const oldStatus = healthIndicator.getAttribute('data-status');
1708
+ if (oldStatus === status) return;
1709
+
1710
+ healthIndicator.setAttribute('data-status', status);
1711
+ healthIndicator.classList.add('animate');
1712
+
1713
+ setTimeout(() => {
1714
+ healthIndicator.classList.remove('animate');
1715
+ }, 1000);
1716
+ }
1717
+
1718
+ // 数字动画
1719
+ function animateValue(element, start, end, duration) {
1720
+ if (start === end) return;
1721
+
1722
+ const range = end - start;
1723
+ const startTime = performance.now();
1724
+
1725
+ function update(currentTime) {
1726
+ const elapsed = currentTime - startTime;
1727
+ const progress = Math.min(elapsed / duration, 1);
1728
+
1729
+ const value = Math.floor(start + range * progress);
1730
+ element.textContent = new Intl.NumberFormat().format(value);
1731
+
1732
+ if (progress < 1) {
1733
+ requestAnimationFrame(update);
1734
+ }
1735
+ }
1736
+
1737
+ requestAnimationFrame(update);
1738
+ }
1739
+
1740
+ // 初始化所有功能
1741
+ document.addEventListener('DOMContentLoaded', () => {
1742
+ initTableSearch();
1743
+ initCardCollapse();
1744
+ initApiEndpointCopy();
1745
+
1746
+ // 初始化数字动画
1747
+ document.querySelectorAll('[data-value]').forEach(element => {
1748
+ const value = parseInt(element.getAttribute('data-value'));
1749
+ if (!isNaN(value)) {
1750
+ animateValue(element, 0, value, 1000);
1751
+ }
1752
+ });
1753
+
1754
+ // 初始化加载状态
1755
+ document.body.classList.remove('loading');
1756
+
1757
+ // 显示欢迎通知
1758
+ setTimeout(() => {
1759
+ showNotification('欢迎使用Abacus Chat代理仪表板', 'success');
1760
+ }, 1000);
1761
+ });
1762
+
1763
+ // 定期更新健康状态
1764
+ setInterval(async () => {
1765
+ try {
1766
+ const response = await fetch('/health');
1767
+ const data = await response.json();
1768
+ updateHealthStatus(data.status);
1769
+ } catch (err) {
1770
+ console.error('健康检查更新失败:', err);
1771
+ }
1772
+ }, 60000);
1773
+
1774
+ // 自动隐藏通知
1775
+ document.addEventListener('click', e => {
1776
+ if (e.target.closest('.notification')) {
1777
+ e.target.closest('.notification').classList.remove('show');
1778
+ }
1779
+ });
1780
+
1781
+ // 键盘导航支持
1782
+ document.addEventListener('keydown', e => {
1783
+ if (e.key === 'Escape') {
1784
+ const modal = document.querySelector('.modal.show');
1785
+ if (modal) {
1786
+ modal.classList.remove('show');
1787
+ }
1788
+
1789
+ const notifications = document.querySelectorAll('.notification.show');
1790
+ notifications.forEach(notification => {
1791
+ notification.classList.remove('show');
1792
+ });
1793
+ }
1794
+ });
1795
  </script>
1796
  </body>
1797
  </html>
templates/login.html CHANGED
@@ -6,321 +6,335 @@
6
  <title>Abacus Chat Proxy - 登录</title>
7
  <style>
8
  :root {
9
- --primary-color: #6f42c1;
10
- --secondary-color: #4a32a8;
11
- --bg-color: #0a0a1a;
12
- --text-color: #e6e6ff;
13
- --card-bg: rgba(30, 30, 60, 0.7);
14
- --input-bg: rgba(40, 40, 80, 0.6);
15
- --success-color: #36d399;
16
- --error-color: #f87272;
 
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  * {
20
  margin: 0;
21
  padding: 0;
22
  box-sizing: border-box;
23
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
  }
25
 
26
  body {
27
  min-height: 100vh;
 
 
28
  display: flex;
29
  align-items: center;
30
  justify-content: center;
31
- background-color: var(--bg-color);
32
- background-image:
33
- radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
34
- radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
35
- color: var(--text-color);
36
  position: relative;
37
  overflow: hidden;
38
  }
39
 
40
- /* 动态背景网格 */
41
- .grid-background {
42
- position: absolute;
43
  top: 0;
44
  left: 0;
45
  width: 100%;
46
  height: 100%;
47
- background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
48
- linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
49
- background-size: 30px 30px;
50
  z-index: -1;
51
- animation: grid-move 20s linear infinite;
 
 
 
 
52
  }
53
 
54
- @keyframes grid-move {
 
 
 
 
 
 
 
 
 
 
55
  0% {
56
- transform: translateY(0);
57
  }
58
  100% {
59
- transform: translateY(30px);
60
  }
61
  }
62
 
63
- /* 浮动粒子效果 */
64
- .particles {
65
- position: absolute;
66
- top: 0;
67
- left: 0;
68
- width: 100%;
69
- height: 100%;
70
- overflow: hidden;
71
- z-index: -1;
72
  }
73
 
74
- .particle {
75
- position: absolute;
76
- display: block;
77
- pointer-events: none;
78
- width: 6px;
79
- height: 6px;
80
- background-color: rgba(111, 66, 193, 0.2);
81
- border-radius: 50%;
82
- animation: float 20s infinite ease-in-out;
83
  }
84
 
85
- @keyframes float {
86
- 0%, 100% {
87
- transform: translateY(0) translateX(0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  opacity: 0;
89
  }
90
  50% {
91
- opacity: 0.5;
 
92
  }
93
- 25%, 75% {
94
- transform: translateY(-100px) translateX(50px);
 
95
  }
96
  }
97
 
98
- .login-card {
99
- width: 380px;
 
100
  padding: 2.5rem;
101
- border-radius: 16px;
102
  background: var(--card-bg);
103
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
104
- backdrop-filter: blur(8px);
105
- border: 1px solid rgba(255, 255, 255, 0.1);
106
- z-index: 10;
107
- animation: card-fade-in 0.6s ease-out;
 
 
108
  }
109
 
110
- @keyframes card-fade-in {
111
- from {
112
- opacity: 0;
113
- transform: translateY(20px);
114
- }
115
- to {
116
- opacity: 1;
117
- transform: translateY(0);
118
- }
119
  }
120
 
121
- .login-header {
122
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
123
  margin-bottom: 2rem;
 
124
  }
125
 
126
- .login-header h1 {
127
  font-size: 2rem;
128
- font-weight: 600;
129
- margin-bottom: 0.5rem;
130
- background: linear-gradient(45deg, #6f42c1, #5181f1);
131
  -webkit-background-clip: text;
132
  -webkit-text-fill-color: transparent;
133
- letter-spacing: 0.5px;
134
  }
135
 
136
- .login-header p {
137
- color: rgba(230, 230, 255, 0.7);
138
- font-size: 0.95rem;
 
 
 
 
139
  }
140
 
141
- .login-form {
142
- display: flex;
143
- flex-direction: column;
 
144
  }
145
 
146
  .form-group {
147
  margin-bottom: 1.5rem;
148
- position: relative;
149
  }
150
 
151
- .form-group label {
152
  display: block;
153
  margin-bottom: 0.5rem;
154
- font-size: 0.9rem;
155
  font-weight: 500;
156
- color: rgba(230, 230, 255, 0.9);
157
  }
158
 
159
- .form-control {
160
  width: 100%;
161
  padding: 0.75rem 1rem;
 
 
 
162
  font-size: 1rem;
163
- line-height: 1.5;
164
  color: var(--text-color);
165
- background-color: var(--input-bg);
166
- border: 1px solid rgba(255, 255, 255, 0.1);
167
- border-radius: 8px;
168
- transition: all 0.2s ease;
169
- outline: none;
170
  }
171
 
172
- .form-control:focus {
 
173
  border-color: var(--primary-color);
174
- box-shadow: 0 0 0 3px rgba(111, 66, 193, 0.2);
175
  }
176
 
177
- .btn {
178
- display: inline-block;
179
- font-weight: 500;
180
- text-align: center;
181
- vertical-align: middle;
182
- cursor: pointer;
183
- padding: 0.75rem 1rem;
184
- font-size: 1rem;
185
- line-height: 1.5;
186
- border-radius: 8px;
187
- transition: all 0.15s ease-in-out;
188
- border: none;
189
  }
190
 
191
- .btn-primary {
192
- color: #fff;
193
- background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
194
- box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
 
 
 
 
 
 
 
195
  position: relative;
196
  overflow: hidden;
197
  }
198
 
199
- .btn-primary:hover {
200
- transform: translateY(-2px);
201
- box-shadow: 0 6px 15px rgba(111, 66, 193, 0.4);
202
- }
203
-
204
- .btn-primary:active {
205
- transform: translateY(0);
206
  }
207
 
208
- /* 添加光效效果 */
209
- .btn-primary::before {
210
  content: '';
211
  position: absolute;
212
- top: -50%;
213
- left: -50%;
214
- width: 200%;
215
- height: 200%;
216
- background: linear-gradient(
217
- to bottom right,
218
- rgba(255, 255, 255, 0) 0%,
219
- rgba(255, 255, 255, 0.1) 50%,
220
- rgba(255, 255, 255, 0) 100%
221
- );
222
- transform: rotate(45deg);
223
- animation: btn-shine 3s infinite;
224
  }
225
 
226
- @keyframes btn-shine {
227
  0% {
228
- left: -50%;
229
  }
230
- 100% {
231
- left: 150%;
232
  }
233
  }
234
 
235
  .error-message {
236
- background-color: rgba(248, 114, 114, 0.2);
237
- color: var(--error-color);
 
238
  padding: 0.75rem;
239
- border-radius: 6px;
240
- margin-bottom: 1.5rem;
241
- font-size: 0.9rem;
242
- border-left: 3px solid var(--error-color);
243
- display: {{ 'block' if error else 'none' }};
244
  }
245
 
246
- .logo {
247
- margin-bottom: 1rem;
248
- font-size: 3rem;
249
- animation: glow 2s infinite alternate;
250
  }
251
 
252
- @keyframes glow {
253
- from {
254
- text-shadow: 0 0 5px rgba(111, 66, 193, 0.5), 0 0 10px rgba(111, 66, 193, 0.5);
255
- }
256
- to {
257
- text-shadow: 0 0 10px rgba(111, 66, 193, 0.8), 0 0 20px rgba(111, 66, 193, 0.8);
258
- }
 
 
 
 
 
 
 
 
 
259
  }
260
  </style>
261
  </head>
262
  <body>
263
- <div class="grid-background"></div>
264
- <div class="particles">
265
- <!-- 粒子元素会由JS生成 -->
266
- </div>
267
 
268
- <div class="login-card">
269
- <div class="login-header">
270
- <div class="logo">🤖</div>
271
- <h1>Abacus Chat Proxy</h1>
272
- <p>请输入访问密码</p>
273
- </div>
274
-
275
- <div class="error-message" id="error-message">
276
- {{ error }}
 
277
  </div>
278
 
279
- <form class="login-form" method="post" action="/login">
280
  <div class="form-group">
281
- <label for="password">密码</label>
282
- <input type="password" class="form-control" id="password" name="password" placeholder="请输入访问密码" required>
283
  </div>
284
 
285
- <button type="submit" class="btn btn-primary">登录</button>
286
- </form>
287
- </div>
288
-
289
- <script>
290
- // 创建浮动粒子
291
- function createParticles() {
292
- const particlesContainer = document.querySelector('.particles');
293
- const particleCount = 20;
294
 
295
- for (let i = 0; i < particleCount; i++) {
296
- const particle = document.createElement('div');
297
- particle.className = 'particle';
298
-
299
- // 随机位置和大小
300
- const size = Math.random() * 5 + 2;
301
- const x = Math.random() * 100;
302
- const y = Math.random() * 100;
303
-
304
- particle.style.width = `${size}px`;
305
- particle.style.height = `${size}px`;
306
- particle.style.left = `${x}%`;
307
- particle.style.top = `${y}%`;
308
-
309
- // 随机动画延迟
310
- particle.style.animationDelay = `${Math.random() * 10}s`;
311
- particle.style.animationDuration = `${Math.random() * 10 + 10}s`;
312
-
313
- // 随机透明度
314
- particle.style.opacity = Math.random() * 0.5;
315
-
316
- particlesContainer.appendChild(particle);
317
- }
318
- }
319
 
320
- // 页面加载时创建粒子
321
- window.addEventListener('load', () => {
322
- createParticles();
323
- });
324
- </script>
 
325
  </body>
326
  </html>
 
6
  <title>Abacus Chat Proxy - 登录</title>
7
  <style>
8
  :root {
9
+ --primary-color: #6366f1;
10
+ --primary-dark: #4f46e5;
11
+ --accent-color: #8b5cf6;
12
+ --background: #f8fafc;
13
+ --card-bg: #ffffff;
14
+ --text-color: #1e293b;
15
+ --text-light: #64748b;
16
+ --error: #ef4444;
17
+ --success: #10b981;
18
  }
19
 
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --primary-color: #6366f1;
23
+ --primary-dark: #4f46e5;
24
+ --accent-color: #a78bfa;
25
+ --background: #0f172a;
26
+ --card-bg: #1e293b;
27
+ --text-color: #f1f5f9;
28
+ --text-light: #94a3b8;
29
+ }
30
+ }
31
+
32
  * {
33
  margin: 0;
34
  padding: 0;
35
  box-sizing: border-box;
36
+ font-family: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
37
  }
38
 
39
  body {
40
  min-height: 100vh;
41
+ background: var(--background);
42
+ color: var(--text-color);
43
  display: flex;
44
  align-items: center;
45
  justify-content: center;
 
 
 
 
 
46
  position: relative;
47
  overflow: hidden;
48
  }
49
 
50
+ /* 动态背景 */
51
+ .background-animation {
52
+ position: fixed;
53
  top: 0;
54
  left: 0;
55
  width: 100%;
56
  height: 100%;
 
 
 
57
  z-index: -1;
58
+ background: linear-gradient(
59
+ 135deg,
60
+ rgba(99, 102, 241, 0.05) 0%,
61
+ rgba(139, 92, 246, 0.05) 100%
62
+ );
63
  }
64
 
65
+ .background-animation:before {
66
+ content: "";
67
+ position: absolute;
68
+ width: 100%;
69
+ height: 100%;
70
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%236366f1' fill-opacity='0.05'%3E%3Cpath d='M0 38.59l2.83-2.83 1.41 1.41L1.41 40H0v-1.41zM0 20.83l2.83-2.83 1.41 1.41L1.41 22.24H0v-1.41zM0 3.06l2.83-2.83 1.41 1.41L1.41 4.47H0V3.06zm20 0l2.83-2.83 1.41 1.41L21.41 4.47h-1.41V3.06zm20 0l2.83-2.83 1.41 1.41L41.41 4.47h-1.41V3.06zm0 17.77l2.83-2.83 1.41 1.41-2.83 2.83h-1.41v-1.41zM20 20.83l2.83-2.83 1.41 1.41-2.83 2.83h-1.41v-1.41zm0 17.76l2.83-2.83 1.41 1.41-2.83 2.83h-1.41v-1.41zm20 0l2.83-2.83 1.41 1.41-2.83 2.83h-1.41v-1.41z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
71
+ opacity: 0.2;
72
+ animation: backgroundMove 30s linear infinite;
73
+ }
74
+
75
+ @keyframes backgroundMove {
76
  0% {
77
+ background-position: 0 0;
78
  }
79
  100% {
80
+ background-position: 100px 100px;
81
  }
82
  }
83
 
84
+ /* 律动线条 */
85
+ .line {
86
+ position: fixed;
87
+ height: 2px;
88
+ background: linear-gradient(90deg, transparent, var(--primary-color), transparent);
89
+ animation: linePulse 8s infinite;
90
+ opacity: 0.1;
 
 
91
  }
92
 
93
+ .line-1 {
94
+ top: 20%;
95
+ width: 80%;
96
+ left: 10%;
97
+ animation-delay: 0s;
 
 
 
 
98
  }
99
 
100
+ .line-2 {
101
+ top: 40%;
102
+ width: 90%;
103
+ left: 5%;
104
+ animation-delay: 2s;
105
+ }
106
+
107
+ .line-3 {
108
+ top: 60%;
109
+ width: 70%;
110
+ left: 15%;
111
+ animation-delay: 4s;
112
+ }
113
+
114
+ .line-4 {
115
+ top: 80%;
116
+ width: 85%;
117
+ left: 7.5%;
118
+ animation-delay: 6s;
119
+ }
120
+
121
+ @keyframes linePulse {
122
+ 0% {
123
+ transform: scaleX(0);
124
  opacity: 0;
125
  }
126
  50% {
127
+ transform: scaleX(1);
128
+ opacity: 0.1;
129
  }
130
+ 100% {
131
+ transform: scaleX(0);
132
+ opacity: 0;
133
  }
134
  }
135
 
136
+ .login-container {
137
+ width: 100%;
138
+ max-width: 400px;
139
  padding: 2.5rem;
 
140
  background: var(--card-bg);
141
+ border-radius: 16px;
142
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
143
+ transform: translateY(0);
144
+ transition: all 0.3s;
145
+ position: relative;
146
+ z-index: 1;
147
+ overflow: hidden;
148
  }
149
 
150
+ .login-container:hover {
151
+ transform: translateY(-5px);
152
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
 
 
 
 
 
 
153
  }
154
 
155
+ .login-container::after {
156
+ content: '';
157
+ position: absolute;
158
+ top: 0;
159
+ left: 0;
160
+ right: 0;
161
+ height: 4px;
162
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
163
+ z-index: 2;
164
+ }
165
+
166
+ .login-title {
167
+ display: flex;
168
+ align-items: center;
169
  margin-bottom: 2rem;
170
+ position: relative;
171
  }
172
 
173
+ .login-icon {
174
  font-size: 2rem;
175
+ margin-right: 1rem;
176
+ background-image: linear-gradient(45deg, var(--primary-color), var(--accent-color));
 
177
  -webkit-background-clip: text;
178
  -webkit-text-fill-color: transparent;
179
+ animation: floating 3s ease-in-out infinite;
180
  }
181
 
182
+ @keyframes floating {
183
+ 0%, 100% {
184
+ transform: translateY(0);
185
+ }
186
+ 50% {
187
+ transform: translateY(-5px);
188
+ }
189
  }
190
 
191
+ .login-text {
192
+ font-size: 1.5rem;
193
+ font-weight: 700;
194
+ color: var(--text-color);
195
  }
196
 
197
  .form-group {
198
  margin-bottom: 1.5rem;
 
199
  }
200
 
201
+ .form-label {
202
  display: block;
203
  margin-bottom: 0.5rem;
204
+ color: var(--text-light);
205
  font-weight: 500;
206
+ font-size: 0.875rem;
207
  }
208
 
209
+ .form-input {
210
  width: 100%;
211
  padding: 0.75rem 1rem;
212
+ background: var(--background);
213
+ border: 1px solid rgba(99, 102, 241, 0.1);
214
+ border-radius: 8px;
215
  font-size: 1rem;
 
216
  color: var(--text-color);
217
+ transition: all 0.2s;
 
 
 
 
218
  }
219
 
220
+ .form-input:focus {
221
+ outline: none;
222
  border-color: var(--primary-color);
223
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
224
  }
225
 
226
+ .form-input::placeholder {
227
+ color: var(--text-light);
228
+ opacity: 0.7;
 
 
 
 
 
 
 
 
 
229
  }
230
 
231
+ .login-btn {
232
+ width: 100%;
233
+ padding: 0.75rem;
234
+ background: var(--primary-color);
235
+ color: white;
236
+ border: none;
237
+ border-radius: 8px;
238
+ font-size: 1rem;
239
+ font-weight: 600;
240
+ cursor: pointer;
241
+ transition: all 0.2s;
242
  position: relative;
243
  overflow: hidden;
244
  }
245
 
246
+ .login-btn:hover {
247
+ background: var(--primary-dark);
 
 
 
 
 
248
  }
249
 
250
+ .login-btn::after {
 
251
  content: '';
252
  position: absolute;
253
+ top: 0;
254
+ left: -100%;
255
+ width: 100%;
256
+ height: 100%;
257
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
258
+ animation: shine 3s infinite;
 
 
 
 
 
 
259
  }
260
 
261
+ @keyframes shine {
262
  0% {
263
+ left: -100%;
264
  }
265
+ 50%, 100% {
266
+ left: 100%;
267
  }
268
  }
269
 
270
  .error-message {
271
+ color: var(--error);
272
+ font-size: 0.875rem;
273
+ margin-top: 1rem;
274
  padding: 0.75rem;
275
+ background: rgba(239, 68, 68, 0.1);
276
+ border-radius: 8px;
277
+ animation: shake 0.5s linear;
 
 
278
  }
279
 
280
+ @keyframes shake {
281
+ 0%, 100% { transform: translateX(0); }
282
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
283
+ 20%, 40%, 60%, 80% { transform: translateX(5px); }
284
  }
285
 
286
+ .space-url {
287
+ margin-top: 1.5rem;
288
+ text-align: center;
289
+ color: var(--text-light);
290
+ font-size: 0.875rem;
291
+ }
292
+
293
+ .space-url a {
294
+ color: var(--primary-color);
295
+ text-decoration: none;
296
+ transition: all 0.2s;
297
+ }
298
+
299
+ .space-url a:hover {
300
+ color: var(--accent-color);
301
+ text-decoration: underline;
302
  }
303
  </style>
304
  </head>
305
  <body>
306
+ <div class="background-animation"></div>
 
 
 
307
 
308
+ <!-- 律动线条 -->
309
+ <div class="line line-1"></div>
310
+ <div class="line line-2"></div>
311
+ <div class="line line-3"></div>
312
+ <div class="line line-4"></div>
313
+
314
+ <div class="login-container">
315
+ <div class="login-title">
316
+ <div class="login-icon">🤖</div>
317
+ <div class="login-text">Abacus Chat Proxy</div>
318
  </div>
319
 
320
+ <form method="post">
321
  <div class="form-group">
322
+ <label class="form-label" for="password">请输入访问密码</label>
323
+ <input class="form-input" type="password" id="password" name="password" placeholder="输入密码访问仪表盘" required autofocus>
324
  </div>
325
 
326
+ <button class="login-btn" type="submit">登录</button>
 
 
 
 
 
 
 
 
327
 
328
+ {% if error %}
329
+ <div class="error-message">{{ error }}</div>
330
+ {% endif %}
331
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
+ {% if space_url %}
334
+ <div class="space-url">
335
+ <p>请访问 <a href="{{ space_url }}" target="_blank">{{ space_url }}</a> 查看使用情况</p>
336
+ </div>
337
+ {% endif %}
338
+ </div>
339
  </body>
340
  </html>