Beracles commited on
Commit
7416023
·
1 Parent(s): 81053f9
Files changed (2) hide show
  1. page.py +0 -39
  2. static/index.html +566 -0
page.py DELETED
@@ -1,39 +0,0 @@
1
- import panel as pn
2
- import logging
3
- from pathlib import Path
4
-
5
-
6
- tab_console_names = ["APP", "Game"]
7
- pn.extension("terminal") # type: ignore
8
- tabs = []
9
- for name in tab_console_names:
10
- terminal = pn.widgets.Terminal(height=600, sizing_mode="stretch_width")
11
-
12
- logger = logging.getLogger(name)
13
- logger.setLevel(logging.DEBUG)
14
- stream_handler = logging.StreamHandler(terminal)
15
- stream_handler.terminator = " \n"
16
- formatter = logging.Formatter("%(asctime)s [%(levelname)s]: %(message)s")
17
- stream_handler.setFormatter(formatter)
18
- stream_handler.setLevel(logging.DEBUG)
19
- logger.addHandler(stream_handler)
20
-
21
- log_path = Path(f"data/logs/{name.lower()}.log")
22
- log_path.parent.mkdir(parents=True, exist_ok=True)
23
- file_handler = logging.FileHandler(log_path)
24
- file_handler.setFormatter(formatter)
25
- logger.addHandler(file_handler)
26
- downloader = pn.widgets.FileDownload(log_path)
27
- tabs.append(
28
- (
29
- name,
30
- pn.Column(
31
- pn.Row(terminal, align="center"),
32
- pn.Row(downloader, align="center"),
33
- ),
34
- )
35
- )
36
-
37
-
38
- def page():
39
- return pn.Column(pn.Tabs(*tabs))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/index.html ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>日志管理</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 15px;
26
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 2.5rem;
39
+ margin-bottom: 10px;
40
+ font-weight: 300;
41
+ }
42
+
43
+ .header p {
44
+ opacity: 0.9;
45
+ font-size: 1.1rem;
46
+ }
47
+
48
+ .controls {
49
+ padding: 30px;
50
+ background: #f8f9fa;
51
+ border-bottom: 1px solid #e9ecef;
52
+ }
53
+
54
+ .search-container {
55
+ display: flex;
56
+ gap: 15px;
57
+ align-items: center;
58
+ flex-wrap: wrap;
59
+ }
60
+
61
+ .search-input {
62
+ flex: 1;
63
+ min-width: 250px;
64
+ padding: 12px 20px;
65
+ border: 2px solid #e9ecef;
66
+ border-radius: 25px;
67
+ font-size: 16px;
68
+ transition: all 0.3s ease;
69
+ }
70
+
71
+ .search-input:focus {
72
+ outline: none;
73
+ border-color: #667eea;
74
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
75
+ }
76
+
77
+ .btn {
78
+ padding: 12px 25px;
79
+ border: none;
80
+ border-radius: 25px;
81
+ cursor: pointer;
82
+ font-size: 16px;
83
+ font-weight: 500;
84
+ transition: all 0.3s ease;
85
+ text-decoration: none;
86
+ display: inline-block;
87
+ }
88
+
89
+ .btn-primary {
90
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
91
+ color: white;
92
+ }
93
+
94
+ .btn-primary:hover {
95
+ transform: translateY(-2px);
96
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
97
+ }
98
+
99
+ .btn-secondary {
100
+ background: #6c757d;
101
+ color: white;
102
+ }
103
+
104
+ .btn-secondary:hover {
105
+ background: #5a6268;
106
+ transform: translateY(-2px);
107
+ }
108
+
109
+ .page-size-container {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 10px;
113
+ margin-top: 20px;
114
+ }
115
+
116
+ .page-size-container label {
117
+ font-weight: 500;
118
+ color: #495057;
119
+ }
120
+
121
+ .page-size-container select {
122
+ padding: 8px 15px;
123
+ border: 2px solid #e9ecef;
124
+ border-radius: 8px;
125
+ font-size: 14px;
126
+ background: white;
127
+ cursor: pointer;
128
+ transition: all 0.3s ease;
129
+ }
130
+
131
+ .page-size-container select:focus {
132
+ outline: none;
133
+ border-color: #667eea;
134
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
135
+ }
136
+
137
+ .content {
138
+ padding: 30px;
139
+ }
140
+
141
+ .log-table {
142
+ width: 100%;
143
+ border-collapse: collapse;
144
+ margin-bottom: 30px;
145
+ background: white;
146
+ border-radius: 10px;
147
+ overflow: hidden;
148
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
149
+ }
150
+
151
+ .log-table th {
152
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
153
+ color: white;
154
+ padding: 15px;
155
+ text-align: left;
156
+ font-weight: 500;
157
+ }
158
+
159
+ .log-table td {
160
+ padding: 15px;
161
+ border-bottom: 1px solid #e9ecef;
162
+ vertical-align: middle;
163
+ }
164
+
165
+ .log-table tr:hover {
166
+ background: #f8f9fa;
167
+ }
168
+
169
+ .log-table tr:last-child td {
170
+ border-bottom: none;
171
+ }
172
+
173
+ .username {
174
+ font-weight: 600;
175
+ color: #495057;
176
+ }
177
+
178
+ .token {
179
+ font-family: 'Courier New', monospace;
180
+ background: #f8f9fa;
181
+ padding: 8px 12px;
182
+ border-radius: 4px;
183
+ font-size: 0.85rem;
184
+ color: #6c757d;
185
+ word-break: break-all;
186
+ cursor: pointer;
187
+ transition: all 0.3s ease;
188
+ position: relative;
189
+ border: 1px solid #e9ecef;
190
+ }
191
+
192
+ .token:hover {
193
+ background: #e9ecef;
194
+ border-color: #667eea;
195
+ }
196
+
197
+ .token::after {
198
+ content: "点击复制";
199
+ position: absolute;
200
+ top: -30px;
201
+ left: 50%;
202
+ transform: translateX(-50%);
203
+ background: #333;
204
+ color: white;
205
+ padding: 4px 8px;
206
+ border-radius: 4px;
207
+ font-size: 12px;
208
+ opacity: 0;
209
+ pointer-events: none;
210
+ transition: opacity 0.3s ease;
211
+ white-space: nowrap;
212
+ }
213
+
214
+ .token:hover::after {
215
+ opacity: 1;
216
+ }
217
+
218
+ .copy-success {
219
+ background: #d4edda !important;
220
+ border-color: #28a745 !important;
221
+ color: #155724 !important;
222
+ }
223
+
224
+ .content {
225
+ word-wrap: break-word;
226
+ line-height: 1.4;
227
+ }
228
+
229
+
230
+
231
+ .timestamp {
232
+ color: #6c757d;
233
+ font-size: 0.9rem;
234
+ }
235
+
236
+ .pagination {
237
+ display: flex;
238
+ justify-content: center;
239
+ align-items: center;
240
+ gap: 20px;
241
+ margin-top: 30px;
242
+ flex-wrap: wrap;
243
+ }
244
+
245
+ .pagination .page-size-container {
246
+ margin: 0;
247
+ }
248
+
249
+ .pagination-controls {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 10px;
253
+ }
254
+
255
+ .pagination button {
256
+ padding: 8px 12px;
257
+ border: 1px solid #dee2e6;
258
+ background: white;
259
+ cursor: pointer;
260
+ border-radius: 5px;
261
+ transition: all 0.3s ease;
262
+ }
263
+
264
+ .pagination button:hover:not(:disabled) {
265
+ background: #667eea;
266
+ color: white;
267
+ border-color: #667eea;
268
+ }
269
+
270
+ .pagination button:disabled {
271
+ opacity: 0.5;
272
+ cursor: not-allowed;
273
+ }
274
+
275
+ .pagination .active {
276
+ background: #667eea;
277
+ color: white;
278
+ border-color: #667eea;
279
+ }
280
+
281
+ .page-info {
282
+ color: #6c757d;
283
+ margin: 0 15px;
284
+ }
285
+
286
+ .no-data {
287
+ text-align: center;
288
+ padding: 60px 20px;
289
+ color: #6c757d;
290
+ }
291
+
292
+ .no-data i {
293
+ font-size: 4rem;
294
+ margin-bottom: 20px;
295
+ opacity: 0.5;
296
+ }
297
+
298
+ @media (max-width: 768px) {
299
+ .container {
300
+ margin: 10px;
301
+ border-radius: 10px;
302
+ }
303
+
304
+ .header {
305
+ padding: 20px;
306
+ }
307
+
308
+ .header h1 {
309
+ font-size: 2rem;
310
+ }
311
+
312
+ .controls {
313
+ padding: 20px;
314
+ }
315
+
316
+ .search-container {
317
+ flex-direction: column;
318
+ align-items: stretch;
319
+ }
320
+
321
+ .search-input {
322
+ min-width: auto;
323
+ }
324
+
325
+ .stats {
326
+ justify-content: center;
327
+ }
328
+
329
+ .content {
330
+ padding: 20px;
331
+ }
332
+
333
+ .log-table {
334
+ font-size: 0.9rem;
335
+ }
336
+
337
+ .log-table th,
338
+ .log-table td {
339
+ padding: 10px 8px;
340
+ }
341
+
342
+ .error-content {
343
+ max-width: 200px;
344
+ }
345
+ }
346
+ </style>
347
+ </head>
348
+ <body>
349
+ <div class="container">
350
+ <div class="header">
351
+ <h1>日志管理系统</h1>
352
+ </div>
353
+
354
+ <div class="controls">
355
+ <div class="search-container">
356
+ <input type="text" id="searchInput" class="search-input" placeholder="搜索用户名...">
357
+ <button class="btn btn-primary" onclick="searchLogs()">搜索</button>
358
+ <button class="btn btn-secondary" onclick="refreshLogs()">刷新</button>
359
+ </div>
360
+
361
+
362
+ </div>
363
+
364
+ <div class="content">
365
+ <table class="log-table" id="logTable">
366
+ <thead>
367
+ <tr>
368
+ <th>Type</th>
369
+ <th>Source</th>
370
+ <th>UID</th>
371
+ <th>Username</th>
372
+ <th>Token</th>
373
+ <th>Content</th>
374
+ <th>Timestamp</th>
375
+ </tr>
376
+ </thead>
377
+ <tbody id="logTableBody">
378
+ </tbody>
379
+ </table>
380
+
381
+ <div class="no-data" id="noData" style="display: none;">
382
+ <div>📋</div>
383
+ <h3>暂无数据</h3>
384
+ <p>没有找到匹配的错误日志</p>
385
+ </div>
386
+
387
+ <div class="pagination" id="pagination">
388
+ <div class="page-size-container">
389
+ <label for="pageSize">每页显示:</label>
390
+ <select id="pageSize" onchange="changePageSize()">
391
+ <option value="20" selected>20条</option>
392
+ <option value="40">40条</option>
393
+ <option value="60">60条</option>
394
+ </select>
395
+ </div>
396
+ <div class="pagination-controls">
397
+ <button onclick="goToPage(1)" id="firstBtn">首页</button>
398
+ <button onclick="goToPage(currentPage - 1)" id="prevBtn">上一页</button>
399
+ <span class="page-info" id="pageInfo">第 1 页,共 1 页</span>
400
+ <button onclick="goToPage(currentPage + 1)" id="nextBtn">下一页</button>
401
+ <button onclick="goToPage(totalPages)" id="lastBtn">末页</button>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+
407
+ <script>
408
+ const data = {{ data | tojson}};
409
+ // 分页配置
410
+ let currentPage = 1;
411
+ let pageSize = 20;
412
+ let totalPages = 1;
413
+ let filteredLogs = [...data];
414
+ let searchTerm = '';
415
+
416
+ // 初始化页面
417
+ function initPage() {
418
+ renderLogs();
419
+ updatePagination();
420
+ }
421
+
422
+ // 改变每页显示数量
423
+ function changePageSize() {
424
+ const select = document.getElementById('pageSize');
425
+ pageSize = parseInt(select.value);
426
+ currentPage = 1; // 重置到第一页
427
+ renderLogs();
428
+ updatePagination();
429
+ }
430
+
431
+ // 渲染日志表格
432
+ function renderLogs() {
433
+ const tbody = document.getElementById('logTableBody');
434
+ const noData = document.getElementById('noData');
435
+ const table = document.getElementById('logTable');
436
+
437
+ if (filteredLogs.length === 0) {
438
+ table.style.display = 'none';
439
+ noData.style.display = 'block';
440
+ return;
441
+ }
442
+
443
+ table.style.display = 'table';
444
+ noData.style.display = 'none';
445
+
446
+ totalPages = Math.ceil(filteredLogs.length / pageSize);
447
+ const startIndex = (currentPage - 1) * pageSize;
448
+ const endIndex = startIndex + pageSize;
449
+ const currentLogs = filteredLogs.slice(startIndex, endIndex);
450
+
451
+ tbody.innerHTML = '';
452
+
453
+ currentLogs.forEach(log => {
454
+ const row = document.createElement('tr');
455
+ row.innerHTML = `
456
+ <td>
457
+ <div class="type">${log.type}</div>
458
+ </td>
459
+ <td>
460
+ <div class="uid">${log.source}</div>
461
+ </td>
462
+ <td>
463
+ <div class="uid">${log.uid}</div>
464
+ </td>
465
+ <td>
466
+ <div class="username">${log.username}</div>
467
+ </td>
468
+ <td>
469
+ <div class="token" onclick="copyToken('${log.token}')" title="点击复制">${log.token}</div>
470
+ </td>
471
+ <td>
472
+ <div class="content">${log.content}</div>
473
+ </td>
474
+ <td>
475
+ <div class="timestamp">${log.timestamp}</div>
476
+ </td>
477
+ `;
478
+ tbody.appendChild(row);
479
+ });
480
+ }
481
+
482
+ // 复制token到剪贴板
483
+ function copyToken(token) {
484
+ navigator.clipboard.writeText(token).then(function() {
485
+ // 显示复制成功的视觉反馈
486
+ const tokenElements = document.querySelectorAll('.token');
487
+ tokenElements.forEach(el => {
488
+ if (el.textContent === token) {
489
+ el.classList.add('copy-success');
490
+ setTimeout(() => {
491
+ el.classList.remove('copy-success');
492
+ }, 1000);
493
+ }
494
+ });
495
+ }).catch(function(err) {
496
+ console.error('复制失败: ', err);
497
+ // 降级方案:使用旧的复制方法
498
+ const textArea = document.createElement('textarea');
499
+ textArea.value = token;
500
+ document.body.appendChild(textArea);
501
+ textArea.select();
502
+ document.execCommand('copy');
503
+ document.body.removeChild(textArea);
504
+ });
505
+ }
506
+
507
+ // 更新分页控件
508
+ function updatePagination() {
509
+ const pageInfo = document.getElementById('pageInfo');
510
+ const firstBtn = document.getElementById('firstBtn');
511
+ const prevBtn = document.getElementById('prevBtn');
512
+ const nextBtn = document.getElementById('nextBtn');
513
+ const lastBtn = document.getElementById('lastBtn');
514
+
515
+ pageInfo.textContent = `第 ${currentPage} 页,共 ${totalPages} 页`;
516
+
517
+ firstBtn.disabled = currentPage === 1;
518
+ prevBtn.disabled = currentPage === 1;
519
+ nextBtn.disabled = currentPage === totalPages;
520
+ lastBtn.disabled = currentPage === totalPages;
521
+ }
522
+
523
+ // 跳转到指定页面
524
+ function goToPage(page) {
525
+ if (page < 1 || page > totalPages) return;
526
+ currentPage = page;
527
+ renderLogs();
528
+ updatePagination();
529
+ }
530
+
531
+ // 搜索日志
532
+ function searchLogs() {
533
+ searchTerm = document.getElementById('searchInput').value.trim();
534
+
535
+ if (searchTerm === '') {
536
+ filteredLogs = [...data];
537
+ } else {
538
+ filteredLogs = data.filter(log =>
539
+ log.username.toLowerCase().includes(searchTerm.toLowerCase())
540
+ );
541
+ }
542
+
543
+ currentPage = 1;
544
+ renderLogs();
545
+ updatePagination();
546
+ }
547
+
548
+ // 刷新日志
549
+ function refreshLogs() {
550
+ // 模拟刷新数据
551
+ console.log('刷新日志数据...');
552
+ initPage();
553
+ }
554
+
555
+ // 监听回车键搜索
556
+ document.getElementById('searchInput').addEventListener('keypress', function(e) {
557
+ if (e.key === 'Enter') {
558
+ searchLogs();
559
+ }
560
+ });
561
+
562
+ // 页面加载完成后初始化
563
+ document.addEventListener('DOMContentLoaded', initPage);
564
+ </script>
565
+ </body>
566
+ </html>