malt666 commited on
Commit
49a2030
·
verified ·
1 Parent(s): f3fc6fe

Upload 6 files

Browse files
Files changed (3) hide show
  1. app.py +72 -346
  2. templates/dashboard.html +661 -0
  3. templates/login.html +326 -0
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, request, jsonify, Response, render_template_string
2
  import requests
3
  import time
4
  import json
@@ -11,10 +11,12 @@ import hashlib
11
  import jwt
12
  import os
13
  import threading
14
- from datetime import datetime
15
  import tiktoken # 导入tiktoken来计算token数量
16
 
17
- app = Flask(__name__)
 
 
18
 
19
 
20
  API_ENDPOINT_URL = "https://abacus.ai/api/v0/describeDeployment"
@@ -69,323 +71,6 @@ compute_points_log = {
69
  }
70
 
71
 
72
- # HTML模板
73
- INDEX_HTML = """
74
- <!DOCTYPE html>
75
- <html lang="zh-CN">
76
- <head>
77
- <meta charset="UTF-8">
78
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
79
- <title>Abacus Chat Proxy</title>
80
- <style>
81
- * {
82
- margin: 0;
83
- padding: 0;
84
- box-sizing: border-box;
85
- }
86
- body {
87
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
88
- line-height: 1.6;
89
- color: #333;
90
- background: #f5f5f5;
91
- min-height: 100vh;
92
- display: flex;
93
- flex-direction: column;
94
- align-items: center;
95
- padding: 2rem;
96
- }
97
- .container {
98
- max-width: 800px;
99
- width: 100%;
100
- background: white;
101
- padding: 2rem;
102
- border-radius: 12px;
103
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
104
- }
105
- h1 {
106
- color: #2c3e50;
107
- margin-bottom: 1rem;
108
- text-align: center;
109
- font-size: 2.5rem;
110
- }
111
- h2 {
112
- color: #3a4a5c;
113
- margin: 1.5rem 0 1rem;
114
- font-size: 1.5rem;
115
- }
116
- .status-card {
117
- background: #f8f9fa;
118
- border-radius: 8px;
119
- padding: 1.5rem;
120
- margin: 1.5rem 0;
121
- }
122
- .status-item {
123
- display: flex;
124
- justify-content: space-between;
125
- align-items: center;
126
- padding: 0.5rem 0;
127
- border-bottom: 1px solid #dee2e6;
128
- }
129
- .status-item:last-child {
130
- border-bottom: none;
131
- }
132
- .status-label {
133
- color: #6c757d;
134
- font-weight: 500;
135
- }
136
- .status-value {
137
- color: #28a745;
138
- font-weight: 600;
139
- }
140
- .status-value.warning {
141
- color: #ffc107;
142
- }
143
- .status-value.danger {
144
- color: #dc3545;
145
- }
146
- .footer {
147
- margin-top: 2rem;
148
- text-align: center;
149
- color: #6c757d;
150
- }
151
- .models-list {
152
- list-style: none;
153
- display: flex;
154
- flex-wrap: wrap;
155
- gap: 0.5rem;
156
- margin-top: 0.5rem;
157
- }
158
- .model-tag {
159
- background: #e9ecef;
160
- padding: 0.25rem 0.75rem;
161
- border-radius: 16px;
162
- font-size: 0.875rem;
163
- color: #495057;
164
- }
165
- .endpoints {
166
- margin-top: 2rem;
167
- }
168
- .endpoint-item {
169
- background: #f8f9fa;
170
- padding: 1rem;
171
- border-radius: 8px;
172
- margin-bottom: 1rem;
173
- }
174
- .endpoint-url {
175
- font-family: monospace;
176
- background: #e9ecef;
177
- padding: 0.25rem 0.5rem;
178
- border-radius: 4px;
179
- }
180
- .usage-table {
181
- width: 100%;
182
- border-collapse: collapse;
183
- margin-top: 1rem;
184
- }
185
- .usage-table th, .usage-table td {
186
- padding: 0.5rem;
187
- text-align: left;
188
- border-bottom: 1px solid #dee2e6;
189
- }
190
- .usage-table th {
191
- background-color: #e9ecef;
192
- font-weight: 600;
193
- color: #495057;
194
- }
195
- .usage-table tbody tr:hover {
196
- background-color: #f1f3f5;
197
- }
198
- .token-count {
199
- font-family: monospace;
200
- color: #0366d6;
201
- }
202
- .call-count {
203
- font-family: monospace;
204
- color: #28a745;
205
- }
206
- .compute-points {
207
- font-family: monospace;
208
- color: #6f42c1;
209
- font-weight: bold;
210
- }
211
- .progress-container {
212
- width: 100%;
213
- height: 10px;
214
- background-color: #e9ecef;
215
- border-radius: 5px;
216
- margin-top: 0.5rem;
217
- overflow: hidden;
218
- }
219
- .progress-bar {
220
- height: 100%;
221
- border-radius: 5px;
222
- background-color: #28a745;
223
- }
224
- .progress-bar.warning {
225
- background-color: #ffc107;
226
- }
227
- .progress-bar.danger {
228
- background-color: #dc3545;
229
- }
230
- @media (max-width: 768px) {
231
- .container {
232
- padding: 1rem;
233
- }
234
- h1 {
235
- font-size: 2rem;
236
- }
237
- }
238
- </style>
239
- </head>
240
- <body>
241
- <div class="container">
242
- <h1>🤖 Abacus Chat Proxy</h1>
243
-
244
- <div class="status-card">
245
- <div class="status-item">
246
- <span class="status-label">服务状态</span>
247
- <span class="status-value">运行中</span>
248
- </div>
249
- <div class="status-item">
250
- <span class="status-label">运行时间</span>
251
- <span class="status-value">{{ uptime }}</span>
252
- </div>
253
- <div class="status-item">
254
- <span class="status-label">健康检查次数</span>
255
- <span class="status-value">{{ health_checks }}</span>
256
- </div>
257
- <div class="status-item">
258
- <span class="status-label">已配置用户数</span>
259
- <span class="status-value">{{ user_count }}</span>
260
- </div>
261
- <div class="status-item">
262
- <span class="status-label">可用模型</span>
263
- <div class="models-list">
264
- {% for model in models %}
265
- <span class="model-tag">{{ model }}</span>
266
- {% endfor %}
267
- </div>
268
- </div>
269
- </div>
270
-
271
- <h2>💰 计算点信息</h2>
272
- <div class="status-card">
273
- <div class="status-item">
274
- <span class="status-label">总计算点</span>
275
- <span class="status-value compute-points">{{ compute_points.total|int }}</span>
276
- </div>
277
- <div class="status-item">
278
- <span class="status-label">已使用</span>
279
- <span class="status-value compute-points">{{ compute_points.used|int }}</span>
280
- </div>
281
- <div class="status-item">
282
- <span class="status-label">剩余</span>
283
- <span class="status-value compute-points">{{ compute_points.left|int }}</span>
284
- </div>
285
- <div class="status-item">
286
- <span class="status-label">使用比例</span>
287
- <div style="width: 100%; text-align: right;">
288
- <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}">
289
- {{ compute_points.percentage }}%
290
- </span>
291
- <div class="progress-container">
292
- <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div>
293
- </div>
294
- </div>
295
- </div>
296
- {% if compute_points.last_update %}
297
- <div class="status-item">
298
- <span class="status-label">最后更新时间</span>
299
- <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span>
300
- </div>
301
- {% endif %}
302
- </div>
303
-
304
- <h2>📊 计算点使用日志</h2>
305
- <div class="status-card">
306
- <table class="usage-table">
307
- <thead>
308
- <tr>
309
- {% for key, value in compute_points_log.columns.items() %}
310
- <th>{{ value }}</th>
311
- {% endfor %}
312
- </tr>
313
- </thead>
314
- <tbody>
315
- {% for entry in compute_points_log.log %}
316
- <tr>
317
- {% for key, value in compute_points_log.columns.items() %}
318
- <td class="compute-points">{{ entry.get(key, 0) }}</td>
319
- {% endfor %}
320
- </tr>
321
- {% endfor %}
322
- </tbody>
323
- </table>
324
- </div>
325
-
326
- <h2>🔍 模型使用统计</h2>
327
- <div class="status-card">
328
- <div class="status-item">
329
- <span class="status-label">总Token使用量</span>
330
- <span class="status-value token-count">{{ total_tokens.total|int }}</span>
331
- </div>
332
- <div class="status-item">
333
- <span class="status-label">输入Token</span>
334
- <span class="status-value token-count">{{ total_tokens.prompt|int }}</span>
335
- </div>
336
- <div class="status-item">
337
- <span class="status-label">输出Token</span>
338
- <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
339
- </div>
340
-
341
- <table class="usage-table">
342
- <thead>
343
- <tr>
344
- <th>模型</th>
345
- <th>调用次数</th>
346
- <th>输入Token</th>
347
- <th>输出Token</th>
348
- <th>总Token</th>
349
- </tr>
350
- </thead>
351
- <tbody>
352
- {% for model, stats in model_stats.items() %}
353
- <tr>
354
- <td>{{ model }}</td>
355
- <td class="call-count">{{ stats.count }}</td>
356
- <td class="token-count">{{ stats.prompt_tokens|int }}</td>
357
- <td class="token-count">{{ stats.completion_tokens|int }}</td>
358
- <td class="token-count">{{ stats.total_tokens|int }}</td>
359
- </tr>
360
- {% endfor %}
361
- </tbody>
362
- </table>
363
- </div>
364
-
365
- <div class="endpoints">
366
- <h2>📡 API端点</h2>
367
- <div class="endpoint-item">
368
- <p>获取模型列表:</p>
369
- <code class="endpoint-url">GET /v1/models</code>
370
- </div>
371
- <div class="endpoint-item">
372
- <p>聊天补全:</p>
373
- <code class="endpoint-url">POST /v1/chat/completions</code>
374
- </div>
375
- <div class="endpoint-item">
376
- <p>健康检查:</p>
377
- <code class="endpoint-url">GET /health</code>
378
- </div>
379
- </div>
380
-
381
- <div class="footer">
382
- <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
383
- </div>
384
- </div>
385
- </body>
386
- </html>
387
- """
388
-
389
  # 记录启动时间
390
  START_TIME = datetime.now()
391
 
@@ -445,8 +130,17 @@ def require_auth(f):
445
  def decorated(*args, **kwargs):
446
  if not PASSWORD:
447
  return f(*args, **kwargs)
 
 
 
 
 
 
448
  auth = request.authorization
449
  if not auth or not check_auth(auth.token):
 
 
 
450
  return jsonify({"error": "Unauthorized access"}), 401
451
  return f(*args, **kwargs)
452
 
@@ -1082,33 +776,12 @@ def keep_alive():
1082
 
1083
  @app.route("/", methods=["GET"])
1084
  def index():
1085
- # 在每次访问首页时更新计算点信息
1086
- get_compute_points()
 
1087
 
1088
- uptime = datetime.now() - START_TIME
1089
- days = uptime.days
1090
- hours, remainder = divmod(uptime.seconds, 3600)
1091
- minutes, seconds = divmod(remainder, 60)
1092
-
1093
- if days > 0:
1094
- uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
1095
- elif hours > 0:
1096
- uptime_str = f"{hours}小时 {minutes}分钟"
1097
- else:
1098
- uptime_str = f"{minutes}分钟 {seconds}秒"
1099
-
1100
- return render_template_string(
1101
- INDEX_HTML,
1102
- uptime=uptime_str,
1103
- health_checks=health_check_counter,
1104
- user_count=USER_NUM,
1105
- models=sorted(list(MODELS)),
1106
- year=datetime.now().year,
1107
- model_stats=model_usage_stats,
1108
- total_tokens=total_tokens,
1109
- compute_points=compute_points,
1110
- compute_points_log=compute_points_log
1111
- )
1112
 
1113
 
1114
  # 获取OpenAI的tokenizer来计算token数
@@ -1249,6 +922,59 @@ def get_compute_points_log(session, cookies, session_token):
1249
  print(f"获取计算点使用日志异常: {e}")
1250
 
1251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
  if __name__ == "__main__":
1253
  # 启动保活线程
1254
  threading.Thread(target=keep_alive, daemon=True).start()
 
1
+ from flask import Flask, request, jsonify, Response, render_template_string, render_template, redirect, url_for, session as flask_session
2
  import requests
3
  import time
4
  import json
 
11
  import jwt
12
  import os
13
  import threading
14
+ from datetime import datetime, timedelta
15
  import tiktoken # 导入tiktoken来计算token数量
16
 
17
+ app = Flask(__name__, template_folder='templates')
18
+ app.secret_key = os.environ.get("SECRET_KEY", "abacus_chat_proxy_secret_key")
19
+ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
20
 
21
 
22
  API_ENDPOINT_URL = "https://abacus.ai/api/v0/describeDeployment"
 
71
  }
72
 
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  # 记录启动时间
75
  START_TIME = datetime.now()
76
 
 
130
  def decorated(*args, **kwargs):
131
  if not PASSWORD:
132
  return f(*args, **kwargs)
133
+
134
+ # 检查Flask会话是否已登录
135
+ if flask_session.get('logged_in'):
136
+ return f(*args, **kwargs)
137
+
138
+ # 如果是API请求,检查Authorization头
139
  auth = request.authorization
140
  if not auth or not check_auth(auth.token):
141
+ # 如果是浏览器请求,重定向到登录页面
142
+ if request.headers.get('Accept', '').find('text/html') >= 0:
143
+ return redirect(url_for('login'))
144
  return jsonify({"error": "Unauthorized access"}), 401
145
  return f(*args, **kwargs)
146
 
 
776
 
777
  @app.route("/", methods=["GET"])
778
  def index():
779
+ # 如果需要密码且用户未登录,重定向到登录页面
780
+ if PASSWORD and not flask_session.get('logged_in'):
781
+ return redirect(url_for('login'))
782
 
783
+ # 否则重定向到仪表盘
784
+ return redirect(url_for('dashboard'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
 
786
 
787
  # 获取OpenAI的tokenizer来计算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:
932
+ flask_session['logged_in'] = True
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")
941
+ def logout():
942
+ flask_session.clear()
943
+ return redirect(url_for('login'))
944
+
945
+
946
+ @app.route("/dashboard")
947
+ @require_auth
948
+ def dashboard():
949
+ # 在每次访问仪表盘时更新计算点信息
950
+ get_compute_points()
951
+
952
+ uptime = datetime.now() - START_TIME
953
+ days = uptime.days
954
+ hours, remainder = divmod(uptime.seconds, 3600)
955
+ minutes, seconds = divmod(remainder, 60)
956
+
957
+ if days > 0:
958
+ uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
959
+ elif hours > 0:
960
+ uptime_str = f"{hours}小时 {minutes}分钟"
961
+ else:
962
+ uptime_str = f"{minutes}分钟 {seconds}秒"
963
+
964
+ return render_template(
965
+ 'dashboard.html',
966
+ uptime=uptime_str,
967
+ health_checks=health_check_counter,
968
+ user_count=USER_NUM,
969
+ models=sorted(list(MODELS)),
970
+ year=datetime.now().year,
971
+ model_stats=model_usage_stats,
972
+ total_tokens=total_tokens,
973
+ compute_points=compute_points,
974
+ compute_points_log=compute_points_log
975
+ )
976
+
977
+
978
  if __name__ == "__main__":
979
  # 启动保活线程
980
  threading.Thread(target=keep_alive, daemon=True).start()
templates/dashboard.html ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
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
+ }
107
+
108
+ .navbar-actions {
109
+ display: flex;
110
+ gap: 1rem;
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;
173
+ }
174
+
175
+ .card-icon {
176
+ width: 32px;
177
+ height: 32px;
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
+ /* 状态项样式 */
187
+ .status-item {
188
+ display: flex;
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
+ /* 模型标签 */
222
+ .models-list {
223
+ display: flex;
224
+ flex-wrap: wrap;
225
+ gap: 0.5rem;
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 {
244
+ width: 100%;
245
+ border-collapse: collapse;
246
+ text-align: left;
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
+ /* 滚动条美化 */
401
+ ::-webkit-scrollbar {
402
+ width: 8px;
403
+ height: 8px;
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
+ @media (max-width: 768px) {
421
+ .container {
422
+ padding: 1rem;
423
+ }
424
+
425
+ .navbar {
426
+ padding: 1rem;
427
+ }
428
+
429
+ .card {
430
+ padding: 1rem;
431
+ }
432
+
433
+ .grid {
434
+ grid-template-columns: 1fr;
435
+ }
436
+ }
437
+ </style>
438
+ </head>
439
+ <body>
440
+ <div class="grid-background"></div>
441
+
442
+ <nav class="navbar">
443
+ <a href="/" class="navbar-brand">
444
+ <span class="navbar-logo">🤖</span>
445
+ <span class="navbar-title">Abacus Chat Proxy</span>
446
+ </a>
447
+ <div class="navbar-actions">
448
+ <a href="/logout" class="btn-logout">
449
+ <span>退出</span>
450
+ <span>↗</span>
451
+ </a>
452
+ </div>
453
+ </nav>
454
+
455
+ <div class="container">
456
+ <div class="card">
457
+ <div class="card-header">
458
+ <h2 class="card-title">
459
+ <span class="card-icon">📊</span>
460
+ 系统状态
461
+ </h2>
462
+ </div>
463
+ <div class="status-item">
464
+ <span class="status-label">服务状态</span>
465
+ <span class="status-value success">运行中</span>
466
+ </div>
467
+ <div class="status-item">
468
+ <span class="status-label">运行时间</span>
469
+ <span class="status-value">{{ uptime }}</span>
470
+ </div>
471
+ <div class="status-item">
472
+ <span class="status-label">健康检查次数</span>
473
+ <span class="status-value">{{ health_checks }}</span>
474
+ </div>
475
+ <div class="status-item">
476
+ <span class="status-label">已配置用户数</span>
477
+ <span class="status-value">{{ user_count }}</span>
478
+ </div>
479
+ <div class="status-item">
480
+ <span class="status-label">可用模型</span>
481
+ <div class="models-list">
482
+ {% for model in models %}
483
+ <span class="model-tag">{{ model }}</span>
484
+ {% endfor %}
485
+ </div>
486
+ </div>
487
+ </div>
488
+
489
+ <div class="grid">
490
+ <div class="card">
491
+ <div class="card-header">
492
+ <h2 class="card-title">
493
+ <span class="card-icon">💰</span>
494
+ 计算点信息
495
+ </h2>
496
+ </div>
497
+ <div class="status-item">
498
+ <span class="status-label">总计算点</span>
499
+ <span class="status-value compute-points">{{ compute_points.total|int }}</span>
500
+ </div>
501
+ <div class="status-item">
502
+ <span class="status-label">已使用</span>
503
+ <span class="status-value compute-points">{{ compute_points.used|int }}</span>
504
+ </div>
505
+ <div class="status-item">
506
+ <span class="status-label">剩余</span>
507
+ <span class="status-value compute-points">{{ compute_points.left|int }}</span>
508
+ </div>
509
+ <div class="status-item">
510
+ <span class="status-label">使用比例</span>
511
+ <div style="width: 100%; text-align: right;">
512
+ <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}">
513
+ {{ compute_points.percentage }}%
514
+ </span>
515
+ <div class="progress-container">
516
+ <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+ {% if compute_points.last_update %}
521
+ <div class="status-item">
522
+ <span class="status-label">最后更新时间</span>
523
+ <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span>
524
+ </div>
525
+ {% endif %}
526
+ </div>
527
+
528
+ <div class="card">
529
+ <div class="card-header">
530
+ <h2 class="card-title">
531
+ <span class="card-icon">🔍</span>
532
+ Token 使用统计
533
+ </h2>
534
+ </div>
535
+ <div class="status-item">
536
+ <span class="status-label">总Token使用量</span>
537
+ <span class="status-value token-count">{{ total_tokens.total|int }}</span>
538
+ </div>
539
+ <div class="status-item">
540
+ <span class="status-label">输入Token</span>
541
+ <span class="status-value token-count">{{ total_tokens.prompt|int }}</span>
542
+ </div>
543
+ <div class="status-item">
544
+ <span class="status-label">输出Token</span>
545
+ <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
546
+ </div>
547
+ </div>
548
+ </div>
549
+
550
+ <div class="card">
551
+ <div class="card-header">
552
+ <h2 class="card-title">
553
+ <span class="card-icon">📊</span>
554
+ 计算点使用日志
555
+ </h2>
556
+ </div>
557
+ <div class="table-container">
558
+ <table class="data-table">
559
+ <thead>
560
+ <tr>
561
+ {% for key, value in compute_points_log.columns.items() %}
562
+ <th>{{ value }}</th>
563
+ {% endfor %}
564
+ </tr>
565
+ </thead>
566
+ <tbody>
567
+ {% for entry in compute_points_log.log %}
568
+ <tr>
569
+ {% for key, value in compute_points_log.columns.items() %}
570
+ <td class="compute-points">{{ entry.get(key, 0) }}</td>
571
+ {% endfor %}
572
+ </tr>
573
+ {% endfor %}
574
+ </tbody>
575
+ </table>
576
+ </div>
577
+ </div>
578
+
579
+ <div class="card">
580
+ <div class="card-header">
581
+ <h2 class="card-title">
582
+ <span class="card-icon">📈</span>
583
+ 模型使用统计
584
+ </h2>
585
+ </div>
586
+ <div class="table-container">
587
+ <table class="data-table">
588
+ <thead>
589
+ <tr>
590
+ <th>模型</th>
591
+ <th>调用次数</th>
592
+ <th>输入Token</th>
593
+ <th>输出Token</th>
594
+ <th>总Token</th>
595
+ </tr>
596
+ </thead>
597
+ <tbody>
598
+ {% for model, stats in model_stats.items() %}
599
+ <tr>
600
+ <td>{{ model }}</td>
601
+ <td class="call-count">{{ stats.count }}</td>
602
+ <td class="token-count">{{ stats.prompt_tokens|int }}</td>
603
+ <td class="token-count">{{ stats.completion_tokens|int }}</td>
604
+ <td class="token-count">{{ stats.total_tokens|int }}</td>
605
+ </tr>
606
+ {% endfor %}
607
+ </tbody>
608
+ </table>
609
+ </div>
610
+ </div>
611
+
612
+ <div class="card">
613
+ <div class="card-header">
614
+ <h2 class="card-title">
615
+ <span class="card-icon">📡</span>
616
+ API 端点
617
+ </h2>
618
+ </div>
619
+ <div class="endpoint-item">
620
+ <p>获取模型列表:</p>
621
+ <code class="endpoint-url">GET /v1/models</code>
622
+ </div>
623
+ <div class="endpoint-item">
624
+ <p>聊天补全:</p>
625
+ <code class="endpoint-url">POST /v1/chat/completions</code>
626
+ </div>
627
+ <div class="endpoint-item">
628
+ <p>健康检查:</p>
629
+ <code class="endpoint-url">GET /health</code>
630
+ </div>
631
+ </div>
632
+
633
+ <div class="footer">
634
+ <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
635
+ </div>
636
+ </div>
637
+
638
+ <a href="#" class="float-btn" title="回到顶部">↑</a>
639
+
640
+ <script>
641
+ // 回到顶部按钮
642
+ document.querySelector('.float-btn').addEventListener('click', (e) => {
643
+ e.preventDefault();
644
+ window.scrollTo({ top: 0, behavior: 'smooth' });
645
+ });
646
+
647
+ // 显示/隐藏回到顶部按钮
648
+ window.addEventListener('scroll', () => {
649
+ const floatBtn = document.querySelector('.float-btn');
650
+ if (window.scrollY > 300) {
651
+ floatBtn.style.opacity = '1';
652
+ } else {
653
+ floatBtn.style.opacity = '0';
654
+ }
655
+ });
656
+
657
+ // 初始化隐藏回到顶部按钮
658
+ document.querySelector('.float-btn').style.opacity = '0';
659
+ </script>
660
+ </body>
661
+ </html>
templates/login.html ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
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
+ --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>