kuefr commited on
Commit
c58b8ed
·
verified ·
1 Parent(s): d22dd0d

Create logs.html

Browse files
Files changed (1) hide show
  1. logs.html +2086 -0
logs.html ADDED
@@ -0,0 +1,2086 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>API日志管理中心</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
17
+ background: linear-gradient(135deg, #e3f2fd 0%, #f0f8ff 100%);
18
+ min-height: 100vh;
19
+ color: #333;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ padding: 20px;
26
+ }
27
+
28
+ .header {
29
+ background: rgba(255, 255, 255, 0.9);
30
+ backdrop-filter: blur(10px);
31
+ border-radius: 20px;
32
+ padding: 30px;
33
+ margin-bottom: 30px;
34
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
35
+ border: 1px solid rgba(255, 255, 255, 0.2);
36
+ text-align: center;
37
+ }
38
+
39
+ .header h1 {
40
+ color: #1976d2;
41
+ font-size: 2.5em;
42
+ margin-bottom: 10px;
43
+ font-weight: 600;
44
+ }
45
+
46
+ .header p {
47
+ color: #666;
48
+ font-size: 1.1em;
49
+ }
50
+
51
+ .login-form {
52
+ background: rgba(255, 255, 255, 0.9);
53
+ backdrop-filter: blur(10px);
54
+ border-radius: 20px;
55
+ padding: 40px;
56
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
57
+ border: 1px solid rgba(255, 255, 255, 0.2);
58
+ max-width: 400px;
59
+ margin: 50px auto;
60
+ }
61
+
62
+ .login-form h2 {
63
+ color: #1976d2;
64
+ margin-bottom: 30px;
65
+ text-align: center;
66
+ font-size: 1.8em;
67
+ }
68
+
69
+ .form-group {
70
+ margin-bottom: 20px;
71
+ }
72
+
73
+ .form-group label {
74
+ display: block;
75
+ margin-bottom: 8px;
76
+ font-weight: 500;
77
+ color: #555;
78
+ }
79
+
80
+ .form-control {
81
+ width: 100%;
82
+ padding: 12px 16px;
83
+ border: 2px solid #e3f2fd;
84
+ border-radius: 12px;
85
+ font-size: 14px;
86
+ transition: all 0.3s ease;
87
+ background: rgba(255, 255, 255, 0.8);
88
+ }
89
+
90
+ .form-control:focus {
91
+ outline: none;
92
+ border-color: #1976d2;
93
+ box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
94
+ }
95
+
96
+ .btn {
97
+ padding: 12px 24px;
98
+ border: none;
99
+ border-radius: 12px;
100
+ font-size: 14px;
101
+ font-weight: 500;
102
+ cursor: pointer;
103
+ transition: all 0.3s ease;
104
+ text-decoration: none;
105
+ display: inline-block;
106
+ text-align: center;
107
+ }
108
+
109
+ .btn-primary {
110
+ background: linear-gradient(45deg, #1976d2, #42a5f5);
111
+ color: white;
112
+ }
113
+
114
+ .btn-primary:hover {
115
+ transform: translateY(-2px);
116
+ box-shadow: 0 6px 20px rgba(25, 118, 210, 0.3);
117
+ }
118
+
119
+ .btn-success {
120
+ background: linear-gradient(45deg, #4caf50, #66bb6a);
121
+ color: white;
122
+ }
123
+
124
+ .btn-warning {
125
+ background: linear-gradient(45deg, #ff9800, #ffb74d);
126
+ color: white;
127
+ }
128
+
129
+ .btn-danger {
130
+ background: linear-gradient(45deg, #f44336, #ef5350);
131
+ color: white;
132
+ }
133
+
134
+ .btn-secondary {
135
+ background: linear-gradient(45deg, #6c757d, #868e96);
136
+ color: white;
137
+ }
138
+
139
+ .btn:hover {
140
+ transform: translateY(-2px);
141
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
142
+ }
143
+
144
+ .dashboard {
145
+ display: none;
146
+ }
147
+
148
+ .nav-tabs {
149
+ display: flex;
150
+ background: rgba(255, 255, 255, 0.9);
151
+ backdrop-filter: blur(10px);
152
+ border-radius: 15px;
153
+ padding: 8px;
154
+ margin-bottom: 30px;
155
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
156
+ }
157
+
158
+ .nav-tab {
159
+ flex: 1;
160
+ padding: 12px 20px;
161
+ text-align: center;
162
+ background: transparent;
163
+ border: none;
164
+ border-radius: 10px;
165
+ cursor: pointer;
166
+ font-weight: 500;
167
+ transition: all 0.3s ease;
168
+ color: #666;
169
+ }
170
+
171
+ .nav-tab.active {
172
+ background: linear-gradient(45deg, #1976d2, #42a5f5);
173
+ color: white;
174
+ box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
175
+ }
176
+
177
+ .tab-content {
178
+ display: none;
179
+ }
180
+
181
+ .tab-content.active {
182
+ display: block;
183
+ }
184
+
185
+ .card {
186
+ background: rgba(255, 255, 255, 0.9);
187
+ backdrop-filter: blur(10px);
188
+ border-radius: 20px;
189
+ padding: 30px;
190
+ margin-bottom: 30px;
191
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
192
+ border: 1px solid rgba(255, 255, 255, 0.2);
193
+ }
194
+
195
+ .card-title {
196
+ color: #1976d2;
197
+ font-size: 1.5em;
198
+ margin-bottom: 20px;
199
+ font-weight: 600;
200
+ }
201
+
202
+ .stats-grid {
203
+ display: grid;
204
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
205
+ gap: 20px;
206
+ margin-bottom: 30px;
207
+ }
208
+
209
+ .stat-card {
210
+ background: rgba(255, 255, 255, 0.9);
211
+ backdrop-filter: blur(10px);
212
+ border-radius: 15px;
213
+ padding: 25px;
214
+ text-align: center;
215
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1);
216
+ border: 1px solid rgba(255, 255, 255, 0.2);
217
+ transition: transform 0.3s ease;
218
+ }
219
+
220
+ .stat-card:hover {
221
+ transform: translateY(-5px);
222
+ }
223
+
224
+ .stat-value {
225
+ font-size: 2.5em;
226
+ font-weight: 700;
227
+ color: #1976d2;
228
+ margin-bottom: 10px;
229
+ }
230
+
231
+ .stat-label {
232
+ color: #666;
233
+ font-size: 1em;
234
+ font-weight: 500;
235
+ }
236
+
237
+ /* 日志筛选区域样式 */
238
+ .log-filters {
239
+ background: rgba(255, 255, 255, 0.9);
240
+ backdrop-filter: blur(10px);
241
+ border-radius: 15px;
242
+ padding: 20px;
243
+ margin-bottom: 20px;
244
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
245
+ }
246
+
247
+ .filter-row {
248
+ display: grid;
249
+ grid-template-columns: 1fr 1fr 1fr;
250
+ gap: 15px;
251
+ margin-bottom: 15px;
252
+ }
253
+
254
+ .filter-row:last-child {
255
+ grid-template-columns: 1fr 1fr auto auto auto auto auto;
256
+ align-items: end;
257
+ }
258
+
259
+ .filter-group {
260
+ display: flex;
261
+ flex-direction: column;
262
+ }
263
+
264
+ .filter-group label {
265
+ color: #666;
266
+ font-size: 12px;
267
+ margin-bottom: 5px;
268
+ }
269
+
270
+ .filter-input {
271
+ padding: 8px 12px;
272
+ border: 1px solid #ddd;
273
+ border-radius: 8px;
274
+ font-size: 14px;
275
+ background: white;
276
+ }
277
+
278
+ .filter-input:focus {
279
+ outline: none;
280
+ border-color: #1976d2;
281
+ box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
282
+ }
283
+
284
+ .date-range {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 10px;
288
+ }
289
+
290
+ .date-separator {
291
+ color: #666;
292
+ font-weight: bold;
293
+ }
294
+
295
+ /* 表格样式 */
296
+ .table-container {
297
+ overflow-x: auto;
298
+ border-radius: 15px;
299
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
300
+ background: white;
301
+ }
302
+
303
+ .logs-table {
304
+ width: 100%;
305
+ border-collapse: collapse;
306
+ font-size: 14px;
307
+ }
308
+
309
+ .logs-table thead {
310
+ background: linear-gradient(45deg, #1976d2, #42a5f5);
311
+ }
312
+
313
+ .logs-table th {
314
+ padding: 12px 8px;
315
+ text-align: left;
316
+ font-weight: 500;
317
+ color: white;
318
+ font-size: 13px;
319
+ white-space: nowrap;
320
+ }
321
+
322
+ .logs-table td {
323
+ padding: 10px 8px;
324
+ border-bottom: 1px solid #f0f0f0;
325
+ font-size: 13px;
326
+ vertical-align: middle;
327
+ }
328
+
329
+ .logs-table tbody tr:hover {
330
+ background-color: #f8f9fa;
331
+ }
332
+
333
+ .logs-table tbody tr:nth-child(even) {
334
+ background-color: #fafafa;
335
+ }
336
+
337
+ /* 日志表格特殊列样式 */
338
+ .log-time {
339
+ color: #333;
340
+ white-space: nowrap;
341
+ min-width: 140px;
342
+ }
343
+
344
+ .log-user {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 8px;
348
+ min-width: 100px;
349
+ }
350
+
351
+ .user-avatar {
352
+ width: 24px;
353
+ height: 24px;
354
+ border-radius: 50%;
355
+ background: #42a5f5;
356
+ color: white;
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ font-size: 12px;
361
+ font-weight: bold;
362
+ }
363
+
364
+ .log-type-badge {
365
+ padding: 4px 8px;
366
+ border-radius: 12px;
367
+ font-size: 11px;
368
+ font-weight: 500;
369
+ text-align: center;
370
+ min-width: 60px;
371
+ }
372
+
373
+ .type-normal {
374
+ background: #e8f5e8;
375
+ color: #2e7d32;
376
+ }
377
+
378
+ .type-stream {
379
+ background: #e3f2fd;
380
+ color: #1976d2;
381
+ }
382
+
383
+ .type-fake-stream {
384
+ background: #fff3e0;
385
+ color: #f57c00;
386
+ }
387
+
388
+ .log-model {
389
+ background: #f3e5f5;
390
+ color: #7b1fa2;
391
+ padding: 4px 8px;
392
+ border-radius: 12px;
393
+ font-size: 11px;
394
+ text-align: center;
395
+ font-weight: 500;
396
+ }
397
+
398
+ .log-timing {
399
+ color: #4caf50;
400
+ font-size: 12px;
401
+ }
402
+
403
+ .log-status {
404
+ text-align: center;
405
+ }
406
+
407
+ .status-success {
408
+ color: #4caf50;
409
+ }
410
+
411
+ .status-error {
412
+ color: #f44336;
413
+ }
414
+
415
+ .log-tokens {
416
+ color: #666;
417
+ font-size: 12px;
418
+ text-align: right;
419
+ }
420
+
421
+ .log-detail {
422
+ max-width: 200px;
423
+ overflow: hidden;
424
+ text-overflow: ellipsis;
425
+ white-space: nowrap;
426
+ font-size: 12px;
427
+ color: #666;
428
+ }
429
+
430
+ /* 分页样式 */
431
+ .pagination-container {
432
+ display: flex;
433
+ justify-content: space-between;
434
+ align-items: center;
435
+ margin-top: 20px;
436
+ padding: 15px 0;
437
+ }
438
+
439
+ .pagination-info {
440
+ color: #666;
441
+ font-size: 14px;
442
+ }
443
+
444
+ .pagination-controls {
445
+ display: flex;
446
+ align-items: center;
447
+ gap: 10px;
448
+ }
449
+
450
+ .pagination-btn {
451
+ padding: 8px 16px;
452
+ border: 1px solid #ddd;
453
+ border-radius: 8px;
454
+ background: white;
455
+ cursor: pointer;
456
+ font-size: 14px;
457
+ transition: all 0.3s ease;
458
+ }
459
+
460
+ .pagination-btn:hover:not(:disabled) {
461
+ background: #1976d2;
462
+ color: white;
463
+ border-color: #1976d2;
464
+ }
465
+
466
+ .pagination-btn:disabled {
467
+ opacity: 0.5;
468
+ cursor: not-allowed;
469
+ }
470
+
471
+ .pagination-input {
472
+ width: 60px;
473
+ padding: 6px;
474
+ border: 1px solid #ddd;
475
+ border-radius: 4px;
476
+ text-align: center;
477
+ }
478
+
479
+ .page-size-select {
480
+ padding: 6px;
481
+ border: 1px solid #ddd;
482
+ border-radius: 4px;
483
+ }
484
+
485
+ /* 其他原有样式保持不变 */
486
+ .table {
487
+ width: 100%;
488
+ border-collapse: collapse;
489
+ background: white;
490
+ border-radius: 15px;
491
+ overflow: hidden;
492
+ }
493
+
494
+ .table th {
495
+ background: linear-gradient(45deg, #1976d2, #42a5f5);
496
+ color: white;
497
+ padding: 15px;
498
+ text-align: left;
499
+ font-weight: 500;
500
+ }
501
+
502
+ .table td {
503
+ padding: 15px;
504
+ border-bottom: 1px solid #f0f0f0;
505
+ transition: background-color 0.3s ease;
506
+ }
507
+
508
+ .table tr:hover td {
509
+ background-color: #f8f9fa;
510
+ }
511
+
512
+ .table tr:nth-child(even) {
513
+ background-color: #fafafa;
514
+ }
515
+
516
+ .charts-container {
517
+ display: grid;
518
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
519
+ gap: 30px;
520
+ margin-bottom: 30px;
521
+ }
522
+
523
+ .chart-card {
524
+ background: rgba(255, 255, 255, 0.9);
525
+ backdrop-filter: blur(10px);
526
+ border-radius: 20px;
527
+ padding: 30px;
528
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
529
+ border: 1px solid rgba(255, 255, 255, 0.2);
530
+ }
531
+
532
+ .chart-title {
533
+ color: #1976d2;
534
+ font-size: 1.3em;
535
+ margin-bottom: 20px;
536
+ font-weight: 600;
537
+ text-align: center;
538
+ }
539
+
540
+ .status-badge {
541
+ padding: 4px 12px;
542
+ border-radius: 20px;
543
+ font-size: 0.85em;
544
+ font-weight: 500;
545
+ }
546
+
547
+ .status-enabled {
548
+ background: linear-gradient(45deg, #4caf50, #66bb6a);
549
+ color: white;
550
+ }
551
+
552
+ .status-disabled {
553
+ background: linear-gradient(45deg, #ff9800, #ffb74d);
554
+ color: white;
555
+ }
556
+
557
+ .alert {
558
+ padding: 15px 20px;
559
+ border-radius: 12px;
560
+ margin-bottom: 20px;
561
+ border-left: 4px solid;
562
+ }
563
+
564
+ .alert-success {
565
+ background: rgba(76, 175, 80, 0.1);
566
+ border-color: #4caf50;
567
+ color: #2e7d32;
568
+ }
569
+
570
+ .alert-error {
571
+ background: rgba(244, 67, 54, 0.1);
572
+ border-color: #f44336;
573
+ color: #c62828;
574
+ }
575
+
576
+ .alert-info {
577
+ background: rgba(25, 118, 210, 0.1);
578
+ border-color: #1976d2;
579
+ color: #1565c0;
580
+ }
581
+
582
+ .modal {
583
+ display: none;
584
+ position: fixed;
585
+ top: 0;
586
+ left: 0;
587
+ width: 100%;
588
+ height: 100%;
589
+ background: rgba(0, 0, 0, 0.5);
590
+ backdrop-filter: blur(5px);
591
+ z-index: 1000;
592
+ }
593
+
594
+ .modal-content {
595
+ position: absolute;
596
+ top: 50%;
597
+ left: 50%;
598
+ transform: translate(-50%, -50%);
599
+ background: white;
600
+ border-radius: 20px;
601
+ padding: 30px;
602
+ max-width: 500px;
603
+ width: 90%;
604
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
605
+ }
606
+
607
+ .modal-header {
608
+ display: flex;
609
+ justify-content: space-between;
610
+ align-items: center;
611
+ margin-bottom: 20px;
612
+ padding-bottom: 15px;
613
+ border-bottom: 2px solid #f0f0f0;
614
+ }
615
+
616
+ .modal-title {
617
+ color: #1976d2;
618
+ font-size: 1.5em;
619
+ font-weight: 600;
620
+ }
621
+
622
+ .close {
623
+ background: none;
624
+ border: none;
625
+ font-size: 24px;
626
+ cursor: pointer;
627
+ color: #999;
628
+ transition: color 0.3s ease;
629
+ }
630
+
631
+ .close:hover {
632
+ color: #f44336;
633
+ }
634
+
635
+ .form-row {
636
+ display: flex;
637
+ gap: 15px;
638
+ margin-bottom: 20px;
639
+ }
640
+
641
+ .form-row .form-group {
642
+ flex: 1;
643
+ }
644
+
645
+ .toolbar {
646
+ display: flex;
647
+ justify-content: space-between;
648
+ align-items: center;
649
+ margin-bottom: 20px;
650
+ flex-wrap: wrap;
651
+ gap: 15px;
652
+ }
653
+
654
+ .toolbar-left {
655
+ display: flex;
656
+ gap: 10px;
657
+ flex-wrap: wrap;
658
+ }
659
+
660
+ .export-area {
661
+ background: #f8f9fa;
662
+ border-radius: 12px;
663
+ padding: 20px;
664
+ margin-top: 20px;
665
+ }
666
+
667
+ .export-content {
668
+ background: white;
669
+ border-radius: 8px;
670
+ padding: 15px;
671
+ font-family: 'Courier New', monospace;
672
+ font-size: 14px;
673
+ border: 2px solid #e3f2fd;
674
+ word-break: break-all;
675
+ }
676
+
677
+ .loading {
678
+ display: none;
679
+ text-align: center;
680
+ padding: 40px;
681
+ color: #666;
682
+ }
683
+
684
+ .loading.show {
685
+ display: block;
686
+ }
687
+
688
+ .spinner {
689
+ border: 3px solid #f3f3f3;
690
+ border-top: 3px solid #1976d2;
691
+ border-radius: 50%;
692
+ width: 40px;
693
+ height: 40px;
694
+ animation: spin 1s linear infinite;
695
+ margin: 0 auto 20px;
696
+ }
697
+
698
+ @keyframes spin {
699
+ 0% { transform: rotate(0deg); }
700
+ 100% { transform: rotate(360deg); }
701
+ }
702
+
703
+ .empty-state {
704
+ text-align: center;
705
+ padding: 60px 20px;
706
+ color: #666;
707
+ }
708
+
709
+ .empty-state h3 {
710
+ color: #999;
711
+ margin-bottom: 10px;
712
+ }
713
+
714
+ /* 编辑别名相关样式 */
715
+ .alias-edit {
716
+ display: flex;
717
+ align-items: center;
718
+ gap: 8px;
719
+ }
720
+
721
+ .alias-display {
722
+ cursor: pointer;
723
+ padding: 4px 8px;
724
+ border-radius: 4px;
725
+ transition: background-color 0.3s ease;
726
+ }
727
+
728
+ .alias-display:hover {
729
+ background-color: #f0f0f0;
730
+ }
731
+
732
+ .alias-edit-input {
733
+ padding: 4px 8px;
734
+ border: 1px solid #1976d2;
735
+ border-radius: 4px;
736
+ font-size: 14px;
737
+ min-width: 120px;
738
+ }
739
+
740
+ .alias-edit-buttons {
741
+ display: flex;
742
+ gap: 4px;
743
+ }
744
+
745
+ .btn-icon {
746
+ padding: 4px 8px;
747
+ font-size: 12px;
748
+ min-width: auto;
749
+ }
750
+
751
+ /* 文件日志控制区域 */
752
+ .file-logging-control {
753
+ background: rgba(255, 255, 255, 0.9);
754
+ backdrop-filter: blur(10px);
755
+ border-radius: 15px;
756
+ padding: 20px;
757
+ margin-bottom: 20px;
758
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
759
+ display: flex;
760
+ justify-content: space-between;
761
+ align-items: center;
762
+ flex-wrap: wrap;
763
+ gap: 15px;
764
+ }
765
+
766
+ .file-logging-info {
767
+ display: flex;
768
+ align-items: center;
769
+ gap: 15px;
770
+ }
771
+
772
+ .file-logging-actions {
773
+ display: flex;
774
+ gap: 10px;
775
+ flex-wrap: wrap;
776
+ }
777
+
778
+ .switch {
779
+ position: relative;
780
+ display: inline-block;
781
+ width: 60px;
782
+ height: 34px;
783
+ }
784
+
785
+ .switch input {
786
+ opacity: 0;
787
+ width: 0;
788
+ height: 0;
789
+ }
790
+
791
+ .slider {
792
+ position: absolute;
793
+ cursor: pointer;
794
+ top: 0;
795
+ left: 0;
796
+ right: 0;
797
+ bottom: 0;
798
+ background-color: #ccc;
799
+ transition: .4s;
800
+ border-radius: 34px;
801
+ }
802
+
803
+ .slider:before {
804
+ position: absolute;
805
+ content: "";
806
+ height: 26px;
807
+ width: 26px;
808
+ left: 4px;
809
+ bottom: 4px;
810
+ background-color: white;
811
+ transition: .4s;
812
+ border-radius: 50%;
813
+ }
814
+
815
+ input:checked + .slider {
816
+ background-color: #1976d2;
817
+ }
818
+
819
+ input:checked + .slider:before {
820
+ transform: translateX(26px);
821
+ }
822
+
823
+ @media (max-width: 768px) {
824
+ .container {
825
+ padding: 10px;
826
+ }
827
+
828
+ .nav-tabs {
829
+ flex-direction: column;
830
+ gap: 5px;
831
+ }
832
+
833
+ .charts-container {
834
+ grid-template-columns: 1fr;
835
+ }
836
+
837
+ .stats-grid {
838
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
839
+ }
840
+
841
+ .filter-row {
842
+ grid-template-columns: 1fr;
843
+ }
844
+
845
+ .filter-row:last-child {
846
+ grid-template-columns: 1fr;
847
+ }
848
+
849
+ .date-range {
850
+ flex-direction: column;
851
+ align-items: stretch;
852
+ }
853
+
854
+ .logs-table {
855
+ font-size: 12px;
856
+ }
857
+
858
+ .logs-table th,
859
+ .logs-table td {
860
+ padding: 8px 4px;
861
+ }
862
+
863
+ .file-logging-control {
864
+ flex-direction: column;
865
+ align-items: stretch;
866
+ }
867
+
868
+ .file-logging-info {
869
+ justify-content: center;
870
+ }
871
+
872
+ .file-logging-actions {
873
+ justify-content: center;
874
+ }
875
+ }
876
+ </style>
877
+ </head>
878
+ <body>
879
+ <div class="container">
880
+ <div class="header">
881
+ <h1>🚀 API日志管理中心</h1>
882
+ <p>OpenAI to Gemini Proxy 日志监控与管理系统</p>
883
+ </div>
884
+
885
+ <!-- 登录表单 -->
886
+ <div id="loginForm" class="login-form">
887
+ <h2>🔐 管理员登录</h2>
888
+ <div class="form-group">
889
+ <label for="password">管理员密码</label>
890
+ <input type="password" id="password" class="form-control" placeholder="请输入管理员密码">
891
+ </div>
892
+ <button onclick="login()" class="btn btn-primary" style="width: 100%;">登录</button>
893
+ <div id="loginError" class="alert alert-error" style="display: none; margin-top: 15px;"></div>
894
+ </div>
895
+
896
+ <!-- 管理面板 -->
897
+ <div id="dashboard" class="dashboard">
898
+ <!-- 导航标签 -->
899
+ <div class="nav-tabs">
900
+ <button class="nav-tab active" onclick="showTab('overview')">📊 概览统计</button>
901
+ <button class="nav-tab" onclick="showTab('logs')">📋 请求日志</button>
902
+ <button class="nav-tab" onclick="showTab('keys')">🔑 认证管理</button>
903
+ <button class="nav-tab" onclick="showTab('charts')">📈 统计图表</button>
904
+ </div>
905
+
906
+ <!-- 概览统计 -->
907
+ <div id="overview-tab" class="tab-content active">
908
+ <div class="stats-grid" id="statsGrid">
909
+ <!-- 统计卡片将动态生成 -->
910
+ </div>
911
+
912
+ <div class="card">
913
+ <h3 class="card-title">📋 最近请求</h3>
914
+ <div id="recentLogs">
915
+ <div class="loading show">
916
+ <div class="spinner"></div>
917
+ <p>加载中...</p>
918
+ </div>
919
+ </div>
920
+ </div>
921
+ </div>
922
+
923
+ <!-- 请求日志 -->
924
+ <div id="logs-tab" class="tab-content">
925
+ <!-- 文件日志控制 -->
926
+ <div class="file-logging-control">
927
+ <div class="file-logging-info">
928
+ <span>📄 文件日志:</span>
929
+ <label class="switch">
930
+ <input type="checkbox" id="fileLoggingSwitch" onchange="toggleFileLogging()">
931
+ <span class="slider"></span>
932
+ </label>
933
+ <span id="fileLoggingStatus">加载中...</span>
934
+ </div>
935
+ <div class="file-logging-actions">
936
+ <button onclick="downloadLogs('json')" class="btn btn-primary">📥 下载JSON</button>
937
+ <button onclick="downloadLogs('csv')" class="btn btn-secondary">📥 下载CSV</button>
938
+ </div>
939
+ </div>
940
+
941
+ <!-- 日志筛选器 -->
942
+ <div class="log-filters">
943
+ <div class="filter-row">
944
+ <div class="filter-group">
945
+ <label>开始时间 ~ 结束时间</label>
946
+ <div class="date-range">
947
+ <input type="datetime-local" id="startTime" class="filter-input">
948
+ <span class="date-separator">~</span>
949
+ <input type="datetime-local" id="endTime" class="filter-input">
950
+ </div>
951
+ </div>
952
+ <div class="filter-group">
953
+ <label>令牌名称</label>
954
+ <input type="text" id="tokenFilter" class="filter-input" placeholder="搜索...">
955
+ </div>
956
+ <div class="filter-group">
957
+ <label>模型名称</label>
958
+ <input type="text" id="modelFilter" class="filter-input" placeholder="搜索...">
959
+ </div>
960
+ </div>
961
+ <div class="filter-row">
962
+ <div class="filter-group">
963
+ <label>分组</label>
964
+ <select id="groupFilter" class="filter-input">
965
+ <option value="">全部</option>
966
+ </select>
967
+ </div>
968
+ <div class="filter-group">
969
+ <button onclick="applyFilters()" class="btn btn-primary">查询</button>
970
+ </div>
971
+ <div class="filter-group">
972
+ <button onclick="resetFilters()" class="btn btn-secondary">重置</button>
973
+ </div>
974
+ <div class="filter-group">
975
+ <button onclick="refreshLogs()" class="btn btn-warning">刷新</button>
976
+ </div>
977
+ <div class="filter-group">
978
+ <button onclick="clearAllLogs()" class="btn btn-danger">清除所有</button>
979
+ </div>
980
+ </div>
981
+ </div>
982
+
983
+ <!-- 日志表格 -->
984
+ <div class="card">
985
+ <div id="logsContainer">
986
+ <div class="loading show">
987
+ <div class="spinner"></div>
988
+ <p>加载日志中...</p>
989
+ </div>
990
+ </div>
991
+
992
+ <!-- 分页控制 -->
993
+ <div id="logsPagination" class="pagination-container" style="display: none;">
994
+ <div class="pagination-info" id="paginationInfo"></div>
995
+ <div class="pagination-controls">
996
+ <button id="firstPageBtn" class="pagination-btn" onclick="goToPage(1)">首页</button>
997
+ <button id="prevPageBtn" class="pagination-btn" onclick="goToPage(currentPage - 1)">上一页</button>
998
+ <input type="number" id="pageInput" class="pagination-input" min="1" onchange="goToInputPage()">
999
+ <button id="nextPageBtn" class="pagination-btn" onclick="goToPage(currentPage + 1)">下一页</button>
1000
+ <select id="pageSizeSelect" class="page-size-select" onchange="changePageSize()">
1001
+ <option value="50">每页50条</option>
1002
+ <option value="100" selected>每页100条</option>
1003
+ <option value="200">每页200条</option>
1004
+ </select>
1005
+ </div>
1006
+ </div>
1007
+ </div>
1008
+ </div>
1009
+
1010
+ <!-- 认证管理 -->
1011
+ <div id="keys-tab" class="tab-content">
1012
+ <div class="card">
1013
+ <div class="toolbar">
1014
+ <h3 class="card-title">🔑 认证Key管理</h3>
1015
+ <div>
1016
+ <button onclick="showAddKeyModal()" class="btn btn-success">➕ 添加Key</button>
1017
+ <button onclick="exportKeys()" class="btn btn-secondary">📤 导出配置</button>
1018
+ <button onclick="refreshKeys()" class="btn btn-primary">🔄 刷新</button>
1019
+ </div>
1020
+ </div>
1021
+
1022
+ <div class="table-container">
1023
+ <table class="table">
1024
+ <thead>
1025
+ <tr>
1026
+ <th>别名</th>
1027
+ <th>Token</th>
1028
+ <th>状态</th>
1029
+ <th>创建时间</th>
1030
+ <th>操作</th>
1031
+ </tr>
1032
+ </thead>
1033
+ <tbody id="keysTableBody">
1034
+ <tr>
1035
+ <td colspan="5" style="text-align: center; padding: 40px;">
1036
+ <div class="loading show">
1037
+ <div class="spinner"></div>
1038
+ <p>加载中...</p>
1039
+ </div>
1040
+ </td>
1041
+ </tr>
1042
+ </tbody>
1043
+ </table>
1044
+ </div>
1045
+
1046
+ <div id="exportArea" class="export-area" style="display: none;">
1047
+ <h4>环境变量配置</h4>
1048
+ <p>复制以下内容到您的 .env 文件中:</p>
1049
+ <div id="exportContent" class="export-content"></div>
1050
+ </div>
1051
+ </div>
1052
+ </div>
1053
+
1054
+ <!-- 统计图表 -->
1055
+ <div id="charts-tab" class="tab-content">
1056
+ <div class="charts-container">
1057
+ <div class="chart-card">
1058
+ <h3 class="chart-title">📊 请求类型分布</h3>
1059
+ <canvas id="requestTypeChart"></canvas>
1060
+ </div>
1061
+ <div class="chart-card">
1062
+ <h3 class="chart-title">🎯 模型使用统计</h3>
1063
+ <canvas id="modelUsageChart"></canvas>
1064
+ </div>
1065
+ <div class="chart-card">
1066
+ <h3 class="chart-title">✅ 请求状态分布</h3>
1067
+ <canvas id="statusChart"></canvas>
1068
+ </div>
1069
+ <div class="chart-card">
1070
+ <h3 class="chart-title">🔑 Key使用对比</h3>
1071
+ <canvas id="keyUsageChart"></canvas>
1072
+ </div>
1073
+ </div>
1074
+ </div>
1075
+ </div>
1076
+ </div>
1077
+
1078
+ <!-- 添加Key模态框 -->
1079
+ <div id="addKeyModal" class="modal">
1080
+ <div class="modal-content">
1081
+ <div class="modal-header">
1082
+ <h3 class="modal-title">➕ 添加新的认证Key</h3>
1083
+ <button class="close" onclick="closeModal('addKeyModal')">&times;</button>
1084
+ </div>
1085
+ <form onsubmit="addKey(event)">
1086
+ <div class="form-group">
1087
+ <label for="keyAlias">别名</label>
1088
+ <input type="text" id="keyAlias" class="form-control" placeholder="为这个Key设置一个别名" required>
1089
+ </div>
1090
+ <div class="form-group">
1091
+ <label for="keyToken">Token</label>
1092
+ <input type="text" id="keyToken" class="form-control" placeholder="输入认证Token" required>
1093
+ </div>
1094
+ <div style="display: flex; gap: 10px; justify-content: flex-end;">
1095
+ <button type="button" onclick="closeModal('addKeyModal')" class="btn btn-secondary">取消</button>
1096
+ <button type="submit" class="btn btn-success">添加</button>
1097
+ </div>
1098
+ </form>
1099
+ </div>
1100
+ </div>
1101
+
1102
+ <!-- 编辑别名模态框 -->
1103
+ <div id="editAliasModal" class="modal">
1104
+ <div class="modal-content">
1105
+ <div class="modal-header">
1106
+ <h3 class="modal-title">✏️ 编辑别名</h3>
1107
+ <button class="close" onclick="closeModal('editAliasModal')">&times;</button>
1108
+ </div>
1109
+ <form onsubmit="saveAlias(event)">
1110
+ <div class="form-group">
1111
+ <label for="editAlias">别名</label>
1112
+ <input type="text" id="editAlias" class="form-control" required>
1113
+ </div>
1114
+ <div style="display: flex; gap: 10px; justify-content: flex-end;">
1115
+ <button type="button" onclick="closeModal('editAliasModal')" class="btn btn-secondary">取消</button>
1116
+ <button type="submit" class="btn btn-success">保存</button>
1117
+ </div>
1118
+ </form>
1119
+ </div>
1120
+ </div>
1121
+
1122
+ <script>
1123
+ // 全局变量
1124
+ let authToken = null;
1125
+ let currentPage = 1;
1126
+ let pageSize = 100;
1127
+ let totalPages = 1;
1128
+ let chartInstances = {};
1129
+ let allLogs = [];
1130
+ let filteredLogs = [];
1131
+ let authKeys = [];
1132
+ let currentEditToken = null;
1133
+
1134
+ // 日志筛选参数
1135
+ let filters = {
1136
+ startTime: '',
1137
+ endTime: '',
1138
+ tokenFilter: '',
1139
+ modelFilter: '',
1140
+ groupFilter: ''
1141
+ };
1142
+
1143
+ // 登录功能
1144
+ async function login() {
1145
+ const password = document.getElementById('password').value;
1146
+ if (!password) {
1147
+ showError('loginError', '请输入密码');
1148
+ return;
1149
+ }
1150
+
1151
+ try {
1152
+ const response = await fetch('/admin/auth', {
1153
+ method: 'POST',
1154
+ headers: { 'Content-Type': 'application/json' },
1155
+ body: JSON.stringify({ password })
1156
+ });
1157
+
1158
+ const result = await response.json();
1159
+
1160
+ if (result.success) {
1161
+ authToken = result.token;
1162
+ document.getElementById('loginForm').style.display = 'none';
1163
+ document.getElementById('dashboard').style.display = 'block';
1164
+ await initDashboard();
1165
+ } else {
1166
+ showError('loginError', '密码错误,请重试');
1167
+ }
1168
+ } catch (error) {
1169
+ showError('loginError', '登录失败,请检查网络连接');
1170
+ }
1171
+ }
1172
+
1173
+ // 初始化仪表板
1174
+ async function initDashboard() {
1175
+ // 设置默认时间范围(最近7天)
1176
+ const now = new Date();
1177
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
1178
+
1179
+ document.getElementById('startTime').value = formatDateForInput(weekAgo);
1180
+ document.getElementById('endTime').value = formatDateForInput(now);
1181
+
1182
+ await Promise.all([
1183
+ loadStatistics(),
1184
+ loadAuthKeys(),
1185
+ loadAllLogs(),
1186
+ loadRecentLogs(),
1187
+ loadFileLoggingStatus()
1188
+ ]);
1189
+ }
1190
+
1191
+ // 加载文件日志状态
1192
+ async function loadFileLoggingStatus() {
1193
+ try {
1194
+ const response = await fetch('/admin/file-logging-status');
1195
+ const result = await response.json();
1196
+
1197
+ document.getElementById('fileLoggingSwitch').checked = result.enabled;
1198
+ document.getElementById('fileLoggingStatus').textContent = result.enabled ? '已启用' : '已禁用(仅内存)';
1199
+ } catch (error) {
1200
+ console.error('获取文件日志状态失败:', error);
1201
+ document.getElementById('fileLoggingStatus').textContent = '状态未知';
1202
+ }
1203
+ }
1204
+
1205
+ // 切换文件日志状态
1206
+ async function toggleFileLogging() {
1207
+ const enabled = document.getElementById('fileLoggingSwitch').checked;
1208
+
1209
+ try {
1210
+ const response = await fetch('/admin/file-logging', {
1211
+ method: 'POST',
1212
+ headers: { 'Content-Type': 'application/json' },
1213
+ body: JSON.stringify({ enabled })
1214
+ });
1215
+
1216
+ const result = await response.json();
1217
+
1218
+ if (result.success) {
1219
+ document.getElementById('fileLoggingStatus').textContent = enabled ? '已启用' : '已禁用(仅内存)';
1220
+ showSuccess(result.message);
1221
+ } else {
1222
+ // 恢复开关状态
1223
+ document.getElementById('fileLoggingSwitch').checked = !enabled;
1224
+ alert('设置失败: ' + result.message);
1225
+ }
1226
+ } catch (error) {
1227
+ // 恢复开关状态
1228
+ document.getElementById('fileLoggingSwitch').checked = !enabled;
1229
+ alert('设置失败: ' + error.message);
1230
+ }
1231
+ }
1232
+
1233
+ // 下载日志文件
1234
+ function downloadLogs(format) {
1235
+ const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
1236
+ const filename = `api-logs-${timestamp}.${format}`;
1237
+
1238
+ const link = document.createElement('a');
1239
+ link.href = `/admin/download-logs?format=${format}`;
1240
+ link.download = filename;
1241
+ document.body.appendChild(link);
1242
+ link.click();
1243
+ document.body.removeChild(link);
1244
+
1245
+ showSuccess(`正在下载 ${format.toUpperCase()} 格式的日志文件...`);
1246
+ }
1247
+
1248
+ // 格式化日期为input[datetime-local]格式
1249
+ function formatDateForInput(date) {
1250
+ const year = date.getFullYear();
1251
+ const month = String(date.getMonth() + 1).padStart(2, '0');
1252
+ const day = String(date.getDate()).padStart(2, '0');
1253
+ const hours = String(date.getHours()).padStart(2, '0');
1254
+ const minutes = String(date.getMinutes()).padStart(2, '0');
1255
+
1256
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
1257
+ }
1258
+
1259
+ // 显示错误信息
1260
+ function showError(elementId, message) {
1261
+ const element = document.getElementById(elementId);
1262
+ element.textContent = message;
1263
+ element.style.display = 'block';
1264
+ setTimeout(() => {
1265
+ element.style.display = 'none';
1266
+ }, 5000);
1267
+ }
1268
+
1269
+ // 显示成功信息
1270
+ function showSuccess(message) {
1271
+ const alertDiv = document.createElement('div');
1272
+ alertDiv.className = 'alert alert-success';
1273
+ alertDiv.textContent = message;
1274
+ alertDiv.style.position = 'fixed';
1275
+ alertDiv.style.top = '20px';
1276
+ alertDiv.style.right = '20px';
1277
+ alertDiv.style.zIndex = '9999';
1278
+ alertDiv.style.minWidth = '300px';
1279
+
1280
+ document.body.appendChild(alertDiv);
1281
+
1282
+ setTimeout(() => {
1283
+ alertDiv.remove();
1284
+ }, 3000);
1285
+ }
1286
+
1287
+ // 标签切换
1288
+ function showTab(tabName) {
1289
+ // 隐藏所有标签内容
1290
+ const contents = document.querySelectorAll('.tab-content');
1291
+ contents.forEach(content => content.classList.remove('active'));
1292
+
1293
+ // 移除所有标签的active状态
1294
+ const tabs = document.querySelectorAll('.nav-tab');
1295
+ tabs.forEach(tab => tab.classList.remove('active'));
1296
+
1297
+ // 显示选中的标签内容
1298
+ document.getElementById(tabName + '-tab').classList.add('active');
1299
+
1300
+ // 设置对应按钮为active
1301
+ event.target.classList.add('active');
1302
+
1303
+ // 特殊处理
1304
+ if (tabName === 'charts') {
1305
+ setTimeout(() => loadCharts(), 100);
1306
+ } else if (tabName === 'logs') {
1307
+ applyFilters();
1308
+ }
1309
+ }
1310
+
1311
+ // 加载统计信息
1312
+ async function loadStatistics() {
1313
+ try {
1314
+ const response = await fetch('/admin/statistics');
1315
+ const stats = await response.json();
1316
+
1317
+ const statsGrid = document.getElementById('statsGrid');
1318
+ let totalRequests = 0;
1319
+ let totalStreamRequests = 0;
1320
+ let totalNormalRequests = 0;
1321
+ let totalFakeStreamRequests = 0;
1322
+ let totalErrorRequests = 0;
1323
+
1324
+ // 计算总数
1325
+ Object.values(stats).forEach(keyStats => {
1326
+ totalRequests += keyStats.totalRequests;
1327
+ totalStreamRequests += keyStats.streamRequests;
1328
+ totalNormalRequests += keyStats.normalRequests;
1329
+ totalFakeStreamRequests += keyStats.fakeStreamRequests || 0;
1330
+ totalErrorRequests += keyStats.errorRequests;
1331
+ });
1332
+
1333
+ statsGrid.innerHTML = `
1334
+ <div class="stat-card">
1335
+ <div class="stat-value">${totalRequests}</div>
1336
+ <div class="stat-label">总请求数</div>
1337
+ </div>
1338
+ <div class="stat-card">
1339
+ <div class="stat-value">${totalStreamRequests}</div>
1340
+ <div class="stat-label">流式请求</div>
1341
+ </div>
1342
+ <div class="stat-card">
1343
+ <div class="stat-value">${totalNormalRequests}</div>
1344
+ <div class="stat-label">普通请求</div>
1345
+ </div>
1346
+ <div class="stat-card">
1347
+ <div class="stat-value">${totalFakeStreamRequests}</div>
1348
+ <div class="stat-label">假流式请求</div>
1349
+ </div>
1350
+ <div class="stat-card">
1351
+ <div class="stat-value">${totalErrorRequests}</div>
1352
+ <div class="stat-label">错误请求</div>
1353
+ </div>
1354
+ <div class="stat-card">
1355
+ <div class="stat-value">${Object.keys(stats).length}</div>
1356
+ <div class="stat-label">活跃Key数</div>
1357
+ </div>
1358
+ <div class="stat-card">
1359
+ <div class="stat-value">${totalRequests > 0 ? ((totalRequests - totalErrorRequests) / totalRequests * 100).toFixed(1) : 0}%</div>
1360
+ <div class="stat-label">成功率</div>
1361
+ </div>
1362
+ `;
1363
+ } catch (error) {
1364
+ console.error('加载统计信息失败:', error);
1365
+ }
1366
+ }
1367
+
1368
+ // 加载认证Keys
1369
+ async function loadAuthKeys() {
1370
+ try {
1371
+ const response = await fetch('/admin/keys');
1372
+ const keys = await response.json();
1373
+ authKeys = keys;
1374
+
1375
+ // 更新分组选择器
1376
+ const groupFilter = document.getElementById('groupFilter');
1377
+ groupFilter.innerHTML = '<option value="">全部</option>';
1378
+
1379
+ keys.forEach(key => {
1380
+ const option = document.createElement('option');
1381
+ option.value = key.token;
1382
+ option.textContent = key.alias;
1383
+ groupFilter.appendChild(option);
1384
+ });
1385
+
1386
+ // 更新Keys管理表格
1387
+ updateKeysTable();
1388
+
1389
+ } catch (error) {
1390
+ console.error('加载认证Keys失败:', error);
1391
+ }
1392
+ }
1393
+
1394
+ // 更新Keys管理表格
1395
+ function updateKeysTable() {
1396
+ const tbody = document.getElementById('keysTableBody');
1397
+ if (authKeys.length === 0) {
1398
+ tbody.innerHTML = `
1399
+ <tr>
1400
+ <td colspan="5" class="empty-state">
1401
+ <h3>暂无认证Key</h3>
1402
+ <p>点击"添加Key"按钮创建第一个认证Key</p>
1403
+ </td>
1404
+ </tr>
1405
+ `;
1406
+ } else {
1407
+ tbody.innerHTML = authKeys.map(key => `
1408
+ <tr>
1409
+ <td>
1410
+ <div class="alias-edit" id="alias-${key.token}">
1411
+ <span class="alias-display" onclick="startEditAlias('${key.token}', '${key.alias}')" title="点击编辑别名">
1412
+ <strong>${key.alias}</strong> ✏️
1413
+ </span>
1414
+ </div>
1415
+ </td>
1416
+ <td><code>${key.token.substring(0, 15)}...${key.token.substring(key.token.length - 5)}</code></td>
1417
+ <td>
1418
+ <span class="status-badge ${key.enabled ? 'status-enabled' : 'status-disabled'}">
1419
+ ${key.enabled ? '✅ 启用' : '❌ 禁用'}
1420
+ </span>
1421
+ </td>
1422
+ <td>${new Date(key.createdAt).toLocaleString()}</td>
1423
+ <td>
1424
+ <button onclick="toggleKeyStatus('${key.token}', ${!key.enabled})"
1425
+ class="btn ${key.enabled ? 'btn-warning' : 'btn-success'}"
1426
+ style="margin-right: 5px;">
1427
+ ${key.enabled ? '禁用' : '启用'}
1428
+ </button>
1429
+ <button onclick="deleteKey('${key.token}')" class="btn btn-danger">删除</button>
1430
+ </td>
1431
+ </tr>
1432
+ `).join('');
1433
+ }
1434
+ }
1435
+
1436
+ // 开始编辑别名
1437
+ function startEditAlias(token, currentAlias) {
1438
+ currentEditToken = token;
1439
+ document.getElementById('editAlias').value = currentAlias;
1440
+ document.getElementById('editAliasModal').style.display = 'block';
1441
+ }
1442
+
1443
+ // 保存别名
1444
+ async function saveAlias(event) {
1445
+ event.preventDefault();
1446
+
1447
+ const newAlias = document.getElementById('editAlias').value.trim();
1448
+ if (!newAlias) {
1449
+ alert('别名不能为空');
1450
+ return;
1451
+ }
1452
+
1453
+ try {
1454
+ const response = await fetch(`/admin/keys/${encodeURIComponent(currentEditToken)}/alias`, {
1455
+ method: 'PUT',
1456
+ headers: { 'Content-Type': 'application/json' },
1457
+ body: JSON.stringify({ alias: newAlias })
1458
+ });
1459
+
1460
+ const result = await response.json();
1461
+
1462
+ if (result.success) {
1463
+ showSuccess('别名更新成功');
1464
+ closeModal('editAliasModal');
1465
+ await loadAuthKeys();
1466
+ // 如果当前在日志页面,也需要重新加载日志以更新显示
1467
+ if (document.getElementById('logs-tab').classList.contains('active')) {
1468
+ await loadAllLogs();
1469
+ applyFilters();
1470
+ }
1471
+ } else {
1472
+ alert('更新失败: ' + (result.message || '未知错误'));
1473
+ }
1474
+
1475
+ } catch (error) {
1476
+ alert('更新失败: ' + error.message);
1477
+ }
1478
+ }
1479
+
1480
+ // 加载所有日志
1481
+ async function loadAllLogs() {
1482
+ try {
1483
+ const response = await fetch('/admin/all-logs');
1484
+ allLogs = await response.json();
1485
+
1486
+ console.log(`加载了 ${allLogs.length} 条日志`);
1487
+
1488
+ } catch (error) {
1489
+ console.error('加载所有日志失败:', error);
1490
+ }
1491
+ }
1492
+
1493
+ // 应用筛选器
1494
+ function applyFilters() {
1495
+ // 获取筛选条件
1496
+ filters.startTime = document.getElementById('startTime').value;
1497
+ filters.endTime = document.getElementById('endTime').value;
1498
+ filters.tokenFilter = document.getElementById('tokenFilter').value.toLowerCase();
1499
+ filters.modelFilter = document.getElementById('modelFilter').value.toLowerCase();
1500
+ filters.groupFilter = document.getElementById('groupFilter').value;
1501
+
1502
+ // 筛选日志
1503
+ filteredLogs = allLogs.filter(log => {
1504
+ // 时间筛选
1505
+ if (filters.startTime) {
1506
+ const logTime = new Date(log.timestamp || log.requestTime);
1507
+ const startTime = new Date(filters.startTime);
1508
+ if (logTime < startTime) return false;
1509
+ }
1510
+
1511
+ if (filters.endTime) {
1512
+ const logTime = new Date(log.timestamp || log.requestTime);
1513
+ const endTime = new Date(filters.endTime);
1514
+ if (logTime > endTime) return false;
1515
+ }
1516
+
1517
+ // 令牌筛选
1518
+ if (filters.tokenFilter && !(log.keyAlias || '').toLowerCase().includes(filters.tokenFilter)) {
1519
+ return false;
1520
+ }
1521
+
1522
+ // 模型筛选
1523
+ if (filters.modelFilter && !(log.model || '').toLowerCase().includes(filters.modelFilter)) {
1524
+ return false;
1525
+ }
1526
+
1527
+ // 分组筛选
1528
+ if (filters.groupFilter && log.authKey !== filters.groupFilter) {
1529
+ return false;
1530
+ }
1531
+
1532
+ return true;
1533
+ });
1534
+
1535
+ // 重置到第一页
1536
+ currentPage = 1;
1537
+
1538
+ // 渲染筛选后的日志
1539
+ renderLogs();
1540
+ }
1541
+
1542
+ // 重置筛选器
1543
+ function resetFilters() {
1544
+ document.getElementById('startTime').value = '';
1545
+ document.getElementById('endTime').value = '';
1546
+ document.getElementById('tokenFilter').value = '';
1547
+ document.getElementById('modelFilter').value = '';
1548
+ document.getElementById('groupFilter').value = '';
1549
+
1550
+ // 重新设置默认时间范围
1551
+ const now = new Date();
1552
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
1553
+
1554
+ document.getElementById('startTime').value = formatDateForInput(weekAgo);
1555
+ document.getElementById('endTime').value = formatDateForInput(now);
1556
+
1557
+ applyFilters();
1558
+ }
1559
+
1560
+ // 渲染日志
1561
+ function renderLogs() {
1562
+ const container = document.getElementById('logsContainer');
1563
+ const pagination = document.getElementById('logsPagination');
1564
+
1565
+ if (filteredLogs.length === 0) {
1566
+ container.innerHTML = `
1567
+ <div class="empty-state">
1568
+ <h3>暂无符合条件的日志</h3>
1569
+ <p>尝试调整筛选条件或重新加载数据</p>
1570
+ </div>
1571
+ `;
1572
+ pagination.style.display = 'none';
1573
+ return;
1574
+ }
1575
+
1576
+ // 计算分页
1577
+ totalPages = Math.ceil(filteredLogs.length / pageSize);
1578
+ const startIndex = (currentPage - 1) * pageSize;
1579
+ const endIndex = Math.min(startIndex + pageSize, filteredLogs.length);
1580
+ const currentLogs = filteredLogs.slice(startIndex, endIndex);
1581
+
1582
+ // 渲染日志表格
1583
+ container.innerHTML = `
1584
+ <div class="table-container">
1585
+ <table class="logs-table">
1586
+ <thead>
1587
+ <tr>
1588
+ <th>时间</th>
1589
+ <th>用户</th>
1590
+ <th>类型</th>
1591
+ <th>模型</th>
1592
+ <th>用时</th>
1593
+ <th>提示</th>
1594
+ <th>补全</th>
1595
+ <th>详情</th>
1596
+ </tr>
1597
+ </thead>
1598
+ <tbody>
1599
+ ${currentLogs.map((log, index) => `
1600
+ <tr>
1601
+ <td class="log-time">${formatLogTime(log.timestamp || log.requestTime)}</td>
1602
+ <td class="log-user">
1603
+ <div class="user-avatar">${log.keyAlias ? log.keyAlias.charAt(0).toUpperCase() : 'U'}</div>
1604
+ ${log.keyAlias || 'Unknown'}
1605
+ </td>
1606
+ <td>
1607
+ <span class="log-type-badge ${getTypeClass(log.requestType)}">
1608
+ ${getTypeText(log.requestType)}
1609
+ </span>
1610
+ </td>
1611
+ <td>
1612
+ <span class="log-model">${log.model || 'unknown'}</span>
1613
+ </td>
1614
+ <td class="log-timing">
1615
+ ${log.duration ? log.duration + ' ms' : '-'}
1616
+ </td>
1617
+ <td class="log-status ${log.status === 'success' ? 'status-success' : 'status-error'}">
1618
+ ${log.status === 'success' ? '✓' : '✗'}
1619
+ </td>
1620
+ <td class="log-tokens">
1621
+ ${log.responseTokens || 0}
1622
+ </td>
1623
+ <td class="log-detail" title="${log.responseContent || log.error || '无内容'}">
1624
+ ${getDetailText(log)}
1625
+ </td>
1626
+ </tr>
1627
+ `).join('')}
1628
+ </tbody>
1629
+ </table>
1630
+ </div>
1631
+ `;
1632
+
1633
+ // 更新分页信息
1634
+ updatePagination();
1635
+ pagination.style.display = 'flex';
1636
+ }
1637
+
1638
+ // 格式化日志时间
1639
+ function formatLogTime(timeString) {
1640
+ const date = new Date(timeString);
1641
+ const month = String(date.getMonth() + 1).padStart(2, '0');
1642
+ const day = String(date.getDate()).padStart(2, '0');
1643
+ const hours = String(date.getHours()).padStart(2, '0');
1644
+ const minutes = String(date.getMinutes()).padStart(2, '0');
1645
+ const seconds = String(date.getSeconds()).padStart(2, '0');
1646
+
1647
+ return `2025-${month}-${day} ${hours}:${minutes}:${seconds}`;
1648
+ }
1649
+
1650
+ // 获取类型样式类
1651
+ function getTypeClass(requestType) {
1652
+ if (requestType === 'stream') return 'type-stream';
1653
+ if (requestType === 'fake-stream') return 'type-fake-stream';
1654
+ return 'type-normal';
1655
+ }
1656
+
1657
+ // 获取类型文本
1658
+ function getTypeText(requestType) {
1659
+ if (requestType === 'stream') return '流';
1660
+ if (requestType === 'fake-stream') return '假流';
1661
+ return '普通';
1662
+ }
1663
+
1664
+ // 获取详情文本
1665
+ function getDetailText(log) {
1666
+ if (log.status === 'error' && log.error) {
1667
+ return `错误: ${log.error}`;
1668
+ }
1669
+ if (log.responseContent) {
1670
+ return log.responseContent;
1671
+ }
1672
+ return '无内容';
1673
+ }
1674
+
1675
+ // 更新分页信息
1676
+ function updatePagination() {
1677
+ const info = document.getElementById('paginationInfo');
1678
+ const startRecord = (currentPage - 1) * pageSize + 1;
1679
+ const endRecord = Math.min(currentPage * pageSize, filteredLogs.length);
1680
+
1681
+ info.textContent = `显示第 ${startRecord} 条 - 第 ${endRecord} 条,共 ${filteredLogs.length} 条`;
1682
+
1683
+ // 更新按钮状态
1684
+ document.getElementById('firstPageBtn').disabled = currentPage <= 1;
1685
+ document.getElementById('prevPageBtn').disabled = currentPage <= 1;
1686
+ document.getElementById('nextPageBtn').disabled = currentPage >= totalPages;
1687
+
1688
+ // 更新页码输入框
1689
+ document.getElementById('pageInput').value = currentPage;
1690
+ document.getElementById('pageInput').max = totalPages;
1691
+ }
1692
+
1693
+ // 跳转到指定页
1694
+ function goToPage(page) {
1695
+ if (page < 1 || page > totalPages) return;
1696
+ currentPage = page;
1697
+ renderLogs();
1698
+ }
1699
+
1700
+ // 跳转到输入的页码
1701
+ function goToInputPage() {
1702
+ const inputPage = parseInt(document.getElementById('pageInput').value);
1703
+ goToPage(inputPage);
1704
+ }
1705
+
1706
+ // 改变每页大小
1707
+ function changePageSize() {
1708
+ pageSize = parseInt(document.getElementById('pageSizeSelect').value);
1709
+ currentPage = 1;
1710
+ renderLogs();
1711
+ }
1712
+
1713
+ // 刷新日志
1714
+ async function refreshLogs() {
1715
+ document.getElementById('logsContainer').innerHTML = `
1716
+ <div class="loading show">
1717
+ <div class="spinner"></div>
1718
+ <p>重新加载日志中...</p>
1719
+ </div>
1720
+ `;
1721
+
1722
+ await loadAllLogs();
1723
+ applyFilters();
1724
+ }
1725
+
1726
+ // 清除所有日志
1727
+ async function clearAllLogs() {
1728
+ if (!confirm('确定要清除所有Key的日志吗?此操作不可撤销!')) {
1729
+ return;
1730
+ }
1731
+
1732
+ try {
1733
+ const deletePromises = authKeys.map(key =>
1734
+ fetch(`/admin/logs/${key.token}`, { method: 'DELETE' })
1735
+ );
1736
+
1737
+ await Promise.all(deletePromises);
1738
+
1739
+ showSuccess('所有日志已清除');
1740
+ await loadAllLogs();
1741
+ applyFilters();
1742
+ await loadStatistics();
1743
+ await loadRecentLogs();
1744
+ } catch (error) {
1745
+ alert('清除日志失败: ' + error.message);
1746
+ }
1747
+ }
1748
+
1749
+ // 加载最近日志
1750
+ async function loadRecentLogs() {
1751
+ try {
1752
+ const recentLogs = allLogs.slice(0, 10);
1753
+
1754
+ const recentLogsContainer = document.getElementById('recentLogs');
1755
+
1756
+ if (recentLogs.length === 0) {
1757
+ recentLogsContainer.innerHTML = `
1758
+ <div class="empty-state">
1759
+ <h3>暂无请求日志</h3>
1760
+ <p>当有API请求时,日志将在这里显示</p>
1761
+ </div>
1762
+ `;
1763
+ } else {
1764
+ recentLogsContainer.innerHTML = `
1765
+ <div class="table-container">
1766
+ <table class="table">
1767
+ <thead>
1768
+ <tr>
1769
+ <th>Key别名</th>
1770
+ <th>时间</th>
1771
+ <th>类型</th>
1772
+ <th>模型</th>
1773
+ <th>状态</th>
1774
+ <th>Token数</th>
1775
+ <th>响应内容</th>
1776
+ </tr>
1777
+ </thead>
1778
+ <tbody>
1779
+ ${recentLogs.map(log => `
1780
+ <tr>
1781
+ <td><code>${log.keyAlias || 'Unknown'}</code></td>
1782
+ <td>${new Date(log.timestamp || log.requestTime).toLocaleString()}</td>
1783
+ <td>
1784
+ <span class="status-badge ${log.requestType === 'stream' ? 'status-success' : 'status-secondary'}">
1785
+ ${log.requestType === 'stream' ? '🌊 流式' : log.requestType === 'fake-stream' ? '🎭 假流式' : '📄 普通'}
1786
+ </span>
1787
+ </td>
1788
+ <td>${log.model || '未知'}</td>
1789
+ <td>
1790
+ <span class="status-badge ${log.status === 'success' ? 'status-success' : 'status-error'}">
1791
+ ${log.status === 'success' ? '✅ 成功' : '❌ 失败'}
1792
+ </span>
1793
+ </td>
1794
+ <td>${log.responseTokens || 0}</td>
1795
+ <td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
1796
+ ${log.responseContent || (log.error ? '错误: ' + log.error : '无内容')}
1797
+ </td>
1798
+ </tr>
1799
+ `).join('')}
1800
+ </tbody>
1801
+ </table>
1802
+ </div>
1803
+ `;
1804
+ }
1805
+ } catch (error) {
1806
+ console.error('加载最近日志失败:', error);
1807
+ document.getElementById('recentLogs').innerHTML = `
1808
+ <div class="alert alert-error">加载日志失败,请稍后重试</div>
1809
+ `;
1810
+ }
1811
+ }
1812
+
1813
+ // 刷新Keys
1814
+ function refreshKeys() {
1815
+ loadAuthKeys();
1816
+ }
1817
+
1818
+ // 显示添加Key模态框
1819
+ function showAddKeyModal() {
1820
+ document.getElementById('addKeyModal').style.display = 'block';
1821
+ }
1822
+
1823
+ // 关闭模态框
1824
+ function closeModal(modalId) {
1825
+ document.getElementById(modalId).style.display = 'none';
1826
+ // 清空表单
1827
+ if (modalId === 'addKeyModal') {
1828
+ document.getElementById('keyAlias').value = '';
1829
+ document.getElementById('keyToken').value = '';
1830
+ } else if (modalId === 'editAliasModal') {
1831
+ document.getElementById('editAlias').value = '';
1832
+ currentEditToken = null;
1833
+ }
1834
+ }
1835
+
1836
+ // 添加Key
1837
+ async function addKey(event) {
1838
+ event.preventDefault();
1839
+
1840
+ const alias = document.getElementById('keyAlias').value;
1841
+ const token = document.getElementById('keyToken').value;
1842
+
1843
+ try {
1844
+ const response = await fetch('/admin/keys', {
1845
+ method: 'POST',
1846
+ headers: { 'Content-Type': 'application/json' },
1847
+ body: JSON.stringify({ alias, token })
1848
+ });
1849
+
1850
+ const result = await response.json();
1851
+
1852
+ if (result.success) {
1853
+ showSuccess('认证Key添加成功');
1854
+ closeModal('addKeyModal');
1855
+ await loadAuthKeys();
1856
+ } else {
1857
+ alert('添加失败: ' + (result.message || '未知错误'));
1858
+ }
1859
+ } catch (error) {
1860
+ alert('添加失败: ' + error.message);
1861
+ }
1862
+ }
1863
+
1864
+ // 切换Key状态
1865
+ async function toggleKeyStatus(token, enabled) {
1866
+ try {
1867
+ const response = await fetch(`/admin/keys/${encodeURIComponent(token)}/status`, {
1868
+ method: 'PUT',
1869
+ headers: { 'Content-Type': 'application/json' },
1870
+ body: JSON.stringify({ enabled })
1871
+ });
1872
+
1873
+ const result = await response.json();
1874
+
1875
+ if (result.success) {
1876
+ showSuccess(result.message);
1877
+ await loadAuthKeys();
1878
+ } else {
1879
+ alert('操作失败: ' + (result.message || '未知错误'));
1880
+ }
1881
+ } catch (error) {
1882
+ alert('操作失败: ' + error.message);
1883
+ }
1884
+ }
1885
+
1886
+ // 删除Key
1887
+ async function deleteKey(token) {
1888
+ if (!confirm('确定要删除此认证Key吗?此操作不可撤销!')) {
1889
+ return;
1890
+ }
1891
+
1892
+ try {
1893
+ const response = await fetch(`/admin/keys/${encodeURIComponent(token)}`, {
1894
+ method: 'DELETE'
1895
+ });
1896
+
1897
+ const result = await response.json();
1898
+
1899
+ if (result.success) {
1900
+ showSuccess('认证Key删除成功');
1901
+ await loadAuthKeys();
1902
+ } else {
1903
+ alert('删除失败: ' + (result.message || '未知错误'));
1904
+ }
1905
+ } catch (error) {
1906
+ alert('删除失败: ' + error.message);
1907
+ }
1908
+ }
1909
+
1910
+ // 导出Keys配置
1911
+ async function exportKeys() {
1912
+ try {
1913
+ const response = await fetch('/admin/export');
1914
+ const data = await response.json();
1915
+
1916
+ document.getElementById('exportContent').textContent = data.envString;
1917
+ document.getElementById('exportArea').style.display = 'block';
1918
+
1919
+ // 滚动到导出区域
1920
+ document.getElementById('exportArea').scrollIntoView({ behavior: 'smooth' });
1921
+ } catch (error) {
1922
+ alert('导出失败: ' + error.message);
1923
+ }
1924
+ }
1925
+
1926
+ // 加载图表
1927
+ async function loadCharts() {
1928
+ try {
1929
+ const response = await fetch('/admin/statistics');
1930
+ const stats = await response.json();
1931
+
1932
+ // 清除旧图表
1933
+ Object.values(chartInstances).forEach(chart => chart.destroy());
1934
+ chartInstances = {};
1935
+
1936
+ // 准备数据
1937
+ const keyLabels = [];
1938
+ const requestTypeCounts = { stream: 0, normal: 0, 'fake-stream': 0 };
1939
+ const modelCounts = {};
1940
+ const statusCounts = { success: 0, error: 0 };
1941
+ const keyRequestCounts = [];
1942
+
1943
+ Object.entries(stats).forEach(([key, keyStats]) => {
1944
+ const alias = authKeys.find(k => k.token === key)?.alias || key.substring(0, 10) + '...';
1945
+ keyLabels.push(alias);
1946
+ keyRequestCounts.push(keyStats.totalRequests);
1947
+
1948
+ requestTypeCounts.stream += keyStats.streamRequests;
1949
+ requestTypeCounts.normal += keyStats.normalRequests;
1950
+ requestTypeCounts['fake-stream'] += keyStats.fakeStreamRequests || 0;
1951
+
1952
+ statusCounts.success += (keyStats.totalRequests - keyStats.errorRequests);
1953
+ statusCounts.error += keyStats.errorRequests;
1954
+
1955
+ Object.entries(keyStats.modelUsage || {}).forEach(([model, count]) => {
1956
+ modelCounts[model] = (modelCounts[model] || 0) + count;
1957
+ });
1958
+ });
1959
+
1960
+ // 请求类型分布图
1961
+ chartInstances.requestType = new Chart(document.getElementById('requestTypeChart'), {
1962
+ type: 'doughnut',
1963
+ data: {
1964
+ labels: ['流式请求', '普通请求', '假流式请求'],
1965
+ datasets: [{
1966
+ data: [requestTypeCounts.stream, requestTypeCounts.normal, requestTypeCounts['fake-stream']],
1967
+ backgroundColor: ['#42a5f5', '#66bb6a', '#ffb74d'],
1968
+ borderWidth: 2,
1969
+ borderColor: '#fff'
1970
+ }]
1971
+ },
1972
+ options: {
1973
+ responsive: true,
1974
+ plugins: {
1975
+ legend: {
1976
+ position: 'bottom'
1977
+ }
1978
+ }
1979
+ }
1980
+ });
1981
+
1982
+ // 模型使用统计
1983
+ const modelLabels = Object.keys(modelCounts).slice(0, 8);
1984
+ const modelData = modelLabels.map(model => modelCounts[model]);
1985
+
1986
+ chartInstances.modelUsage = new Chart(document.getElementById('modelUsageChart'), {
1987
+ type: 'bar',
1988
+ data: {
1989
+ labels: modelLabels,
1990
+ datasets: [{
1991
+ label: '请求次数',
1992
+ data: modelData,
1993
+ backgroundColor: '#42a5f5',
1994
+ borderColor: '#1976d2',
1995
+ borderWidth: 1
1996
+ }]
1997
+ },
1998
+ options: {
1999
+ responsive: true,
2000
+ scales: {
2001
+ y: {
2002
+ beginAtZero: true
2003
+ }
2004
+ }
2005
+ }
2006
+ });
2007
+
2008
+ // 请求状态分布
2009
+ chartInstances.status = new Chart(document.getElementById('statusChart'), {
2010
+ type: 'pie',
2011
+ data: {
2012
+ labels: ['成功', '失败'],
2013
+ datasets: [{
2014
+ data: [statusCounts.success, statusCounts.error],
2015
+ backgroundColor: ['#4caf50', '#f44336'],
2016
+ borderWidth: 2,
2017
+ borderColor: '#fff'
2018
+ }]
2019
+ },
2020
+ options: {
2021
+ responsive: true,
2022
+ plugins: {
2023
+ legend: {
2024
+ position: 'bottom'
2025
+ }
2026
+ }
2027
+ }
2028
+ });
2029
+
2030
+ // Key使用对比
2031
+ chartInstances.keyUsage = new Chart(document.getElementById('keyUsageChart'), {
2032
+ type: 'bar',
2033
+ data: {
2034
+ labels: keyLabels,
2035
+ datasets: [{
2036
+ label: '请求次数',
2037
+ data: keyRequestCounts,
2038
+ backgroundColor: '#66bb6a',
2039
+ borderColor: '#4caf50',
2040
+ borderWidth: 1
2041
+ }]
2042
+ },
2043
+ options: {
2044
+ responsive: true,
2045
+ scales: {
2046
+ y: {
2047
+ beginAtZero: true
2048
+ }
2049
+ }
2050
+ }
2051
+ });
2052
+
2053
+ } catch (error) {
2054
+ console.error('加载图表失败:', error);
2055
+ }
2056
+ }
2057
+
2058
+ // 页面点击事件 - 关闭模态框
2059
+ window.addEventListener('click', function(event) {
2060
+ const modals = document.querySelectorAll('.modal');
2061
+ modals.forEach(modal => {
2062
+ if (event.target === modal) {
2063
+ modal.style.display = 'none';
2064
+ }
2065
+ });
2066
+ });
2067
+
2068
+ // 回车登录
2069
+ document.getElementById('password').addEventListener('keypress', function(event) {
2070
+ if (event.key === 'Enter') {
2071
+ login();
2072
+ }
2073
+ });
2074
+
2075
+ // 页面加载完成后初始化
2076
+ document.addEventListener('DOMContentLoaded', function() {
2077
+ // 页面加载时显示正确的日志标签
2078
+ setTimeout(() => {
2079
+ if (document.querySelector('.nav-tab[onclick*="logs"]')) {
2080
+ applyFilters();
2081
+ }
2082
+ }, 1000);
2083
+ });
2084
+ </script>
2085
+ </body>
2086
+ </html>