dfa32412 commited on
Commit
dcf9c72
·
verified ·
1 Parent(s): 2a0a433

Upload 2 files

Browse files
Files changed (2) hide show
  1. templates/login.html +77 -0
  2. templates/manager.html +577 -0
templates/login.html ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>登录</title>
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
7
+ <style>
8
+ :root {
9
+ --bg-primary: #f4f6f9;
10
+ --card-bg: #ffffff;
11
+ --text-primary: #2c3e50;
12
+ --text-secondary: #6c757d;
13
+ --border-color: #e2e8f0;
14
+ --primary-color: #3498db;
15
+ }
16
+ * { box-sizing: border-box; margin: 0; padding: 0; }
17
+ body {
18
+ font-family: 'Inter', sans-serif;
19
+ background-color: var(--bg-primary);
20
+ color: var(--text-primary);
21
+ display: flex;
22
+ justify-content: center;
23
+ align-items: center;
24
+ height: 100vh;
25
+ }
26
+ .login-form {
27
+ background: var(--card-bg);
28
+ padding: 40px;
29
+ border-radius: 16px;
30
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
31
+ width: 100%;
32
+ max-width: 400px;
33
+ }
34
+ h2 {
35
+ text-align: center;
36
+ margin-bottom: 20px;
37
+ color: var(--text-primary);
38
+ }
39
+ input {
40
+ width: 100%;
41
+ padding: 12px 15px;
42
+ margin: 10px 0;
43
+ border: 1px solid var(--border-color);
44
+ border-radius: 8px;
45
+ font-size: 16px;
46
+ }
47
+ button {
48
+ width: 100%;
49
+ padding: 12px;
50
+ background-color: var(--primary-color);
51
+ color: white;
52
+ border: none;
53
+ border-radius: 8px;
54
+ cursor: pointer;
55
+ transition: background-color 0.3s;
56
+ }
57
+ button:hover { background-color: #2980b9; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="login-form">
62
+ <h2>管理员登录</h2>
63
+ <form action="/manager/login" method="post">
64
+ <input type="password" name="password" placeholder="输入管理员密码" required>
65
+ <button type="submit">登录</button>
66
+ </form>
67
+ </div>
68
+ {% if error %}
69
+ <div id="notification" style="position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: #f44336; color: white; padding: 10px 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); display: block; z-index: 1000;">密码错误</div>
70
+ <script>
71
+ setTimeout(() => {
72
+ document.getElementById('notification').style.display = 'none';
73
+ }, 2000);
74
+ </script>
75
+ {% endif %}
76
+ </body>
77
+ </html>
templates/manager.html ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Grok Token 管理面板</title>
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
7
+ <style>
8
+ :root {
9
+ --bg-primary: #f4f6f9;
10
+ --card-bg: #ffffff;
11
+ --text-primary: #2c3e50;
12
+ --text-secondary: #6c757d;
13
+ --border-color: #e2e8f0;
14
+ --primary-color: #3498db;
15
+ --success-color: #4caf50;
16
+ --danger-color: #f44336;
17
+ --warning-color: #ff9800;
18
+ }
19
+ * { box-sizing: border-box; margin: 0; padding: 0; }
20
+ body {
21
+ font-family: 'Inter', sans-serif;
22
+ background-color: var(--bg-primary);
23
+ color: var(--text-primary);
24
+ line-height: 1.6;
25
+ }
26
+ .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
27
+ .search-section { margin-bottom: 20px; }
28
+ #searchInput { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 8px; font-size: 16px; }
29
+ .overview-panel {
30
+ background-color: var(--card-bg);
31
+ border-radius: 16px;
32
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
33
+ padding: 25px;
34
+ margin-bottom: 25px;
35
+ }
36
+ .header-actions {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ }
41
+ .overview-stats {
42
+ display: flex;
43
+ justify-content: stretch;
44
+ align-items: center;
45
+ width: 100%;
46
+ }
47
+ .model-remaining-stats {
48
+ display: flex;
49
+ gap: 30px;
50
+ flex-wrap: wrap;
51
+ justify-content: flex-end;
52
+ }
53
+ .model-stat {
54
+ display: flex;
55
+ flex-direction: column;
56
+ align-items: center;
57
+ }
58
+ .model-stat .stat-label {
59
+ font-size: 12px;
60
+ color: var(--text-secondary);
61
+ margin-bottom: 5px;
62
+ }
63
+ .model-stat .stat-value {
64
+ font-size: 16px;
65
+ font-weight: 600;
66
+ color: var(--primary-color);
67
+ min-width: 40px;
68
+ text-align: center;
69
+ }
70
+ .stat-item { text-align: center; }
71
+ .stat-value { font-size: 28px; font-weight: 600; color: var(--primary-color); }
72
+ .stat-label { font-size: 14px; color: var(--text-secondary); }
73
+ .refresh-icon {
74
+ cursor: pointer;
75
+ transition: transform 0.3s;
76
+ }
77
+ .refresh-icon:hover {
78
+ transform: rotate(180deg);
79
+ }
80
+ .token-management-section {
81
+ display: grid;
82
+ grid-template-columns: 1fr 1fr;
83
+ gap: 20px;
84
+ margin-bottom: 25px;
85
+ }
86
+ .token-management-section > div {
87
+ background-color: var(--card-bg);
88
+ border-radius: 16px;
89
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
90
+ padding: 25px;
91
+ }
92
+ .token-management-section h3 {
93
+ margin-bottom: 15px;
94
+ font-size: 16px;
95
+ color: var(--text-secondary);
96
+ }
97
+ .token-input-section { display: flex; gap: 15px; }
98
+ #tokenInput, #cfInput {
99
+ flex-grow: 1;
100
+ padding: 12px 15px;
101
+ border: 1px solid var(--border-color);
102
+ border-radius: 8px;
103
+ font-size: 16px;
104
+ }
105
+ #addTokenBtn, #setCfBtn {
106
+ padding: 12px 25px;
107
+ background-color: var(--primary-color);
108
+ color: white;
109
+ border: none;
110
+ border-radius: 8px;
111
+ cursor: pointer;
112
+ transition: background-color 0.3s;
113
+ }
114
+ #addTokenBtn:hover, #setCfBtn:hover { background-color: #2980b9; }
115
+ .token-grid {
116
+ display: grid;
117
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
118
+ gap: 20px;
119
+ }
120
+ .token-card {
121
+ background-color: var(--card-bg);
122
+ border-radius: 16px;
123
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
124
+ padding: 20px;
125
+ transition: transform 0.3s;
126
+ }
127
+ .token-card:hover { transform: translateY(-5px); }
128
+ .token-header {
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: center;
132
+ margin-bottom: 15px;
133
+ padding-bottom: 10px;
134
+ border-bottom: 1px solid var(--border-color);
135
+ }
136
+ .token-title {
137
+ font-size: 14px;
138
+ font-weight: 500;
139
+ max-width: 250px;
140
+ overflow: hidden;
141
+ text-overflow: ellipsis;
142
+ white-space: nowrap;
143
+ }
144
+ .delete-btn {
145
+ background-color: var(--danger-color);
146
+ color: white;
147
+ border: none;
148
+ padding: 5px 10px;
149
+ border-radius: 4px;
150
+ cursor: pointer;
151
+ font-size: 12px;
152
+ }
153
+ .delete-btn:hover { background-color: #c0392b; }
154
+ .model-row {
155
+ display: flex;
156
+ align-items: center;
157
+ margin-bottom: 10px;
158
+ gap: 15px;
159
+ }
160
+ .model-name {
161
+ flex: 2;
162
+ font-size: 14px;
163
+ color: var(--text-secondary);
164
+ }
165
+ .progress-container {
166
+ flex: 6;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 10px;
170
+ }
171
+ .progress-bar {
172
+ flex-grow: 1;
173
+ height: 8px;
174
+ background-color: #e0e0e0;
175
+ border-radius: 4px;
176
+ overflow: hidden;
177
+ }
178
+ .progress-bar-fill {
179
+ height: 100%;
180
+ transition: width 0.5s ease;
181
+ }
182
+ .progress-text {
183
+ font-size: 12px;
184
+ color: var(--text-secondary);
185
+ min-width: 50px;
186
+ text-align: right;
187
+ }
188
+ .status-badge {
189
+ font-size: 12px;
190
+ padding: 3px 8px;
191
+ border-radius: 12px;
192
+ font-weight: 600;
193
+ position: relative;
194
+ cursor: help;
195
+ }
196
+ .status-badge .tooltip {
197
+ visibility: hidden;
198
+ position: absolute;
199
+ z-index: 1;
200
+ bottom: 125%;
201
+ left: 50%;
202
+ transform: translateX(-50%);
203
+ background-color: rgba(0, 0, 0, 0.8);
204
+ color: white;
205
+ text-align: center;
206
+ border-radius: 6px;
207
+ padding: 5px 10px;
208
+ opacity: 0;
209
+ transition: opacity 0.3s;
210
+ white-space: nowrap;
211
+ }
212
+ .status-badge:hover .tooltip {
213
+ visibility: visible;
214
+ opacity: 1;
215
+ }
216
+ .status-badge::after {
217
+ content: '';
218
+ position: absolute;
219
+ bottom: 100%;
220
+ left: 50%;
221
+ margin-left: -5px;
222
+ border-width: 5px;
223
+ border-style: solid;
224
+ border-color: transparent transparent rgba(0, 0, 0, 0.8) transparent;
225
+ opacity: 0;
226
+ transition: opacity 0.3s;
227
+ }
228
+ .status-badge:hover::after { opacity: 1; }
229
+ .status-active {
230
+ background-color: rgba(76, 175, 80, 0.1);
231
+ color: var(--success-color);
232
+ }
233
+ .status-expired {
234
+ background-color: rgba(244, 67, 54, 0.1);
235
+ color: var(--danger-color);
236
+ }
237
+ #notification {
238
+ position: fixed;
239
+ top: 20px;
240
+ left: 50%;
241
+ transform: translateX(-50%);
242
+ background-color: #3498db;
243
+ color: white;
244
+ padding: 10px 20px;
245
+ border-radius: 8px;
246
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2);
247
+ display: none;
248
+ z-index: 1000;
249
+ }
250
+ </style>
251
+ </head>
252
+ <body>
253
+ <div class="container">
254
+ <div class="search-section">
255
+ <input type="text" id="searchInput" placeholder="搜索 Token...">
256
+ </div>
257
+
258
+ <div class="overview-panel">
259
+ <div class="header-actions">
260
+ <div class="overview-stats">
261
+ <div class="stat-item">
262
+ <div class="stat-value" id="totalTokens">0</div>
263
+ <div class="stat-label">Token 总数</div>
264
+ </div>
265
+ <div class="model-remaining-stats">
266
+ <div class="model-stat">
267
+ <div class="stat-label">grok-2可用次数</div>
268
+ <div class="stat-value" id="grok-2-count">0</div>
269
+ </div>
270
+ <div class="model-stat">
271
+ <div class="stat-label">grok-3可用次数</div>
272
+ <div class="stat-value" id="grok-3-count">0</div>
273
+ </div>
274
+ <div class="model-stat">
275
+ <div class="stat-label">grok-3-deepsearch可用次数</div>
276
+ <div class="stat-value" id="grok-3-deepsearch-count">0</div>
277
+ </div>
278
+ <div class="model-stat">
279
+ <div class="stat-label">grok-3-reasoning可用次数</div>
280
+ <div class="stat-value" id="grok-3-reasoning-count">0</div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ <div class="refresh-icon" id="refreshTokens">
285
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
286
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
287
+ <path d="M21 3v5h-5"/>
288
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
289
+ <path d="M3 21v-5h5"/>
290
+ </svg>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <div class="token-management-section">
296
+ <div class="token-input-container">
297
+ <h3>添加 SSO Token</h3>
298
+ <div class="token-input-section">
299
+ <input type="text" id="tokenInput" placeholder="输入新的 SSO Token">
300
+ <button id="addTokenBtn">添加 Token</button>
301
+ </div>
302
+ </div>
303
+
304
+ <div class="cf-input-container">
305
+ <h3>设置 CF Clearance</h3>
306
+ <div class="token-input-section">
307
+ <input type="text" id="cfInput" placeholder="输入 CF Clearance">
308
+ <button id="setCfBtn">设置 CF</button>
309
+ </div>
310
+ </div>
311
+ </div>
312
+
313
+ <div id="tokenGrid" class="token-grid"></div>
314
+ </div>
315
+
316
+ <div id="notification"></div>
317
+
318
+ <script>
319
+ // Token 模型配置
320
+ const modelConfig = {
321
+ "grok-2": { RequestFrequency: 30, ExpirationTime: 3600000 },
322
+ "grok-3": { RequestFrequency: 20, ExpirationTime: 7200000 },
323
+ "grok-3-deepsearch": { RequestFrequency: 10, ExpirationTime: 86400000 },
324
+ "grok-3-reasoning": { RequestFrequency: 10, ExpirationTime: 86400000 }
325
+ };
326
+
327
+ let tokenMap = {};
328
+
329
+ // 根据百分比返回进度条颜色
330
+ function getProgressColor(percentage) {
331
+ if (percentage > 70) return 'var(--danger-color)';
332
+ if (percentage > 30) return 'var(--warning-color)';
333
+ return 'var(--success-color)';
334
+ }
335
+
336
+ function calculateModelRemaining() {
337
+ const modelRemaining = {};
338
+ Object.keys(modelConfig).forEach(modelName => {
339
+ const maxRequests = modelConfig[modelName].RequestFrequency;
340
+ modelRemaining[modelName] = 0;
341
+
342
+ Object.values(tokenMap).forEach(tokenData => {
343
+ const modelData = tokenData[modelName];
344
+ if (modelData.isValid) {
345
+ modelRemaining[modelName] += maxRequests - modelData.totalRequestCount;
346
+ }
347
+ });
348
+ });
349
+ return modelRemaining;
350
+ }
351
+
352
+ function updateTokenCounters() {
353
+ document.getElementById('totalTokens').textContent = Object.keys(tokenMap).length;
354
+
355
+ const modelRemaining = calculateModelRemaining();
356
+
357
+ // 更新每个模型的可用次数
358
+ const modelIds = ['grok-2', 'grok-3', 'grok-3-deepsearch', 'grok-3-reasoning'];
359
+ modelIds.forEach(modelName => {
360
+ const countElement = document.getElementById(`${modelName}-count`);
361
+ if (countElement) {
362
+ countElement.textContent = modelRemaining[modelName] || 0;
363
+ }
364
+ });
365
+ }
366
+
367
+ // 更新失效 Token 的倒计时,并在时间到达时刷新状态
368
+ async function updateExpiredTokenTimers() {
369
+ const currentTime = Date.now();
370
+ const expiredBadges = document.querySelectorAll('.status-badge.status-expired');
371
+ for (const badge of expiredBadges) {
372
+ const invalidatedTime = parseInt(badge.getAttribute('data-invalidated-time'), 10);
373
+ const expirationTime = parseInt(badge.getAttribute('data-expiration-time'), 10);
374
+ const recoveryTime = invalidatedTime + expirationTime;
375
+ const remainingTime = recoveryTime - currentTime;
376
+ const tooltip = badge.querySelector('.tooltip');
377
+ if (tooltip) {
378
+ if (remainingTime > 0) {
379
+ const minutes = Math.floor(remainingTime / 60000);
380
+ const seconds = Math.floor((remainingTime % 60000) / 1000);
381
+ tooltip.textContent = `${minutes}分${seconds}秒后恢复`;
382
+ } else {
383
+ tooltip.textContent = '已可恢复';
384
+ await fetchTokenMap();
385
+ }
386
+ }
387
+ }
388
+ }
389
+
390
+ // 渲染 Token 卡片
391
+ function renderTokens() {
392
+ const tokenGrid = document.getElementById('tokenGrid');
393
+ tokenGrid.innerHTML = '';
394
+ Object.entries(tokenMap).forEach(([token, tokenData]) => {
395
+ const tokenCard = document.createElement('div');
396
+ tokenCard.className = 'token-card';
397
+ tokenCard.setAttribute('data-token', token);
398
+ const tokenHeader = document.createElement('div');
399
+ tokenHeader.className = 'token-header';
400
+ const tokenTitle = document.createElement('div');
401
+ tokenTitle.className = 'token-title';
402
+ tokenTitle.textContent = token;
403
+ tokenTitle.title = token;
404
+ tokenTitle.style.cursor = 'pointer';
405
+ tokenTitle.addEventListener('click', () => {
406
+ navigator.clipboard.writeText(token).then(() => {
407
+ showNotification('Token 已复制');
408
+ }).catch(err => {
409
+ showNotification('复制失败');
410
+ });
411
+ });
412
+ const deleteBtn = document.createElement('button');
413
+ deleteBtn.className = 'delete-btn';
414
+ deleteBtn.textContent = '删除';
415
+ deleteBtn.addEventListener('click', async () => {
416
+ if (confirm(`确认删除 token: ${token}?`)) {
417
+ try {
418
+ const response = await fetch('/manager/api/delete', {
419
+ method: 'POST',
420
+ headers: { 'Content-Type': 'application/json' },
421
+ body: JSON.stringify({ sso: token })
422
+ });
423
+ if (response.ok) {
424
+ await fetchTokenMap();
425
+ showNotification('Token 删除成功');
426
+ } else {
427
+ showNotification('删除 Token 失败');
428
+ }
429
+ } catch (error) {
430
+ showNotification('删除 Token 出错');
431
+ }
432
+ }
433
+ });
434
+ tokenHeader.appendChild(tokenTitle);
435
+ tokenHeader.appendChild(deleteBtn);
436
+ tokenCard.appendChild(tokenHeader);
437
+
438
+ Object.entries(modelConfig).forEach(([modelName, config]) => {
439
+ const modelRow = document.createElement('div');
440
+ modelRow.className = 'model-row';
441
+ const modelNameSpan = document.createElement('div');
442
+ modelNameSpan.className = 'model-name';
443
+ modelNameSpan.textContent = modelName;
444
+ const progressContainer = document.createElement('div');
445
+ progressContainer.className = 'progress-container';
446
+ const progressBar = document.createElement('div');
447
+ progressBar.className = 'progress-bar';
448
+ const progressBarFill = document.createElement('div');
449
+ progressBarFill.className = 'progress-bar-fill';
450
+ const modelData = tokenData[modelName];
451
+ const requestCount = modelData.totalRequestCount;
452
+ const maxRequests = config.RequestFrequency;
453
+ const percentage = (requestCount / maxRequests) * 100;
454
+ progressBarFill.style.width = `${percentage}%`;
455
+ progressBarFill.style.backgroundColor = getProgressColor(percentage);
456
+ progressBar.appendChild(progressBarFill);
457
+ const progressText = document.createElement('div');
458
+ progressText.className = 'progress-text';
459
+ progressText.textContent = `${requestCount}/${maxRequests}`;
460
+ const statusBadge = document.createElement('div');
461
+ statusBadge.className = 'status-badge';
462
+ if (!modelData.isValid) {
463
+ statusBadge.classList.add('status-expired');
464
+ statusBadge.textContent = '失效';
465
+ statusBadge.setAttribute('data-invalidated-time', modelData.invalidatedTime);
466
+ statusBadge.setAttribute('data-expiration-time', config.ExpirationTime);
467
+ const tooltip = document.createElement('div');
468
+ tooltip.className = 'tooltip';
469
+ statusBadge.appendChild(tooltip);
470
+ } else {
471
+ statusBadge.classList.add('status-active');
472
+ statusBadge.textContent = '活跃';
473
+ }
474
+ progressContainer.appendChild(progressBar);
475
+ progressContainer.appendChild(progressText);
476
+ modelRow.appendChild(modelNameSpan);
477
+ modelRow.appendChild(progressContainer);
478
+ modelRow.appendChild(statusBadge);
479
+ tokenCard.appendChild(modelRow);
480
+ });
481
+ tokenGrid.appendChild(tokenCard);
482
+ });
483
+ updateTokenCounters();
484
+ }
485
+
486
+ // 获取 Token 数据
487
+ async function fetchTokenMap() {
488
+ try {
489
+ const response = await fetch('/manager/api/get');
490
+ if (!response.ok) throw new Error('获取 Token 失败');
491
+ tokenMap = await response.json();
492
+ renderTokens();
493
+ } catch (error) {
494
+ showNotification('获取 Token 出错');
495
+ }
496
+ }
497
+
498
+ // 添加 Token 事件
499
+ document.getElementById('addTokenBtn').addEventListener('click', async () => {
500
+ const tokenInput = document.getElementById('tokenInput');
501
+ const newToken = tokenInput.value.trim();
502
+ if (newToken) {
503
+ try {
504
+ const response = await fetch('/manager/api/add', {
505
+ method: 'POST',
506
+ headers: { 'Content-Type': 'application/json' },
507
+ body: JSON.stringify({ sso: newToken })
508
+ });
509
+ if (response.ok) {
510
+ tokenInput.value = '';
511
+ await fetchTokenMap();
512
+ showNotification('Token 添加成功');
513
+ } else {
514
+ showNotification('添加 Token 失败');
515
+ }
516
+ } catch (error) {
517
+ showNotification('添加 Token 出错');
518
+ }
519
+ }
520
+ });
521
+
522
+ // 设置 CF Clearance 事件
523
+ document.getElementById('setCfBtn').addEventListener('click', async () => {
524
+ const cfInput = document.getElementById('cfInput');
525
+ const newCf = cfInput.value.trim();
526
+ if (newCf) {
527
+ try {
528
+ const response = await fetch('/manager/api/cf_clearance', {
529
+ method: 'POST',
530
+ headers: { 'Content-Type': 'application/json' },
531
+ body: JSON.stringify({ cf_clearance: newCf })
532
+ });
533
+ if (response.ok) {
534
+ cfInput.value = '';
535
+ showNotification('CF Clearance 设置成功');
536
+ } else {
537
+ showNotification('设置 CF Clearance 失败');
538
+ }
539
+ } catch (error) {
540
+ showNotification('设置 CF Clearance 出错');
541
+ }
542
+ }
543
+ });
544
+
545
+ // 搜索功能
546
+ document.getElementById('searchInput').addEventListener('input', (e) => {
547
+ const searchTerm = e.target.value.toLowerCase();
548
+ const tokenCards = document.querySelectorAll('.token-card');
549
+ tokenCards.forEach(card => {
550
+ const token = card.getAttribute('data-token').toLowerCase();
551
+ card.style.display = token.includes(searchTerm) ? 'block' : 'none';
552
+ });
553
+ });
554
+
555
+ // 刷新 Token 列表事件
556
+ document.getElementById('refreshTokens').addEventListener('click', async () => {
557
+ await fetchTokenMap();
558
+ showNotification('Token 列表已刷新');
559
+ });
560
+
561
+ // 初始化加载 Token 数据
562
+ fetchTokenMap();
563
+ // 每秒更新失效 Token 的状态
564
+ setInterval(updateExpiredTokenTimers, 1000);
565
+
566
+ // 显示气泡通知
567
+ function showNotification(message) {
568
+ const notification = document.getElementById('notification');
569
+ notification.textContent = message;
570
+ notification.style.display = 'block';
571
+ setTimeout(() => {
572
+ notification.style.display = 'none';
573
+ }, 2000);
574
+ }
575
+ </script>
576
+ </body>
577
+ </html>