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

Upload 6 files

Browse files
Files changed (1) hide show
  1. templates/dashboard.html +477 -205
templates/dashboard.html CHANGED
@@ -3,7 +3,11 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
 
6
  <title>Abacus Chat代理仪表板</title>
 
 
7
  <style>
8
  :root {
9
  --primary-color: #6366f1;
@@ -1296,242 +1300,186 @@
1296
  </style>
1297
  </head>
1298
  <body class="loading">
1299
- <div class="background-animation"></div>
1300
-
1301
- <nav class="navbar">
 
 
 
 
 
 
 
 
1302
  <div class="brand">
1303
- <img src="/static/logo.png" alt="Abacus Chat Logo" class="logo">
1304
  <h1>Abacus Chat代理仪表板</h1>
1305
  </div>
1306
  <div class="nav-actions">
1307
- <button class="btn btn-secondary" onclick="location.href='/logout'">
1308
- <span class="material-icons">logout</span>
1309
- 退出登录
1310
  </button>
1311
  </div>
1312
  </nav>
1313
-
1314
- <div class="container">
1315
- <div class="grid">
1316
- <!-- 系统状态卡片 -->
1317
- <div class="card">
1318
- <div class="card-header">
1319
- <h2 class="card-title">
1320
- <div class="card-icon">📊</div>
1321
- 系统状态
1322
- </h2>
1323
- </div>
1324
- <div class="status-item">
1325
- <span class="status-label">运行时间</span>
1326
- <span class="status-value">{{ uptime }}</span>
1327
- </div>
1328
- <div class="status-item">
1329
- <span class="status-label">健康检查</span>
1330
- <span class="health-status">
1331
- <span>✓</span>
1332
- <span>{{ health_checks }}次</span>
1333
- </span>
1334
- </div>
1335
- <div class="status-item">
1336
- <span class="status-label">用户数量</span>
1337
- <span class="status-value">{{ user_count }}</span>
1338
- </div>
1339
- </div>
1340
 
1341
- <!-- Token使用统计卡片 -->
1342
- <div class="card">
1343
- <div class="card-header">
1344
- <h2 class="card-title">
1345
- <div class="card-icon">🎯</div>
1346
- Token使用统计
1347
- </h2>
1348
- </div>
1349
- <div class="status-item">
1350
- <span class="status-label">输入Token</span>
1351
- <span class="token-count">{{ "{:,}".format(total_tokens["prompt"]) }}</span>
1352
- </div>
1353
- <div class="status-item">
1354
- <span class="status-label">输出Token</span>
1355
- <span class="token-count">{{ "{:,}".format(total_tokens["completion"]) }}</span>
1356
- </div>
1357
- <div class="status-item">
1358
- <span class="status-label">总Token</span>
1359
- <span class="token-count">{{ "{:,}".format(total_tokens["total"]) }}</span>
1360
- </div>
1361
- <div class="token-note">
1362
- 注意:此处显示的Token数量仅包含通过代理使用的Token,不包含在Abacus网站上直接使用的Token。这是一个粗略估计,可能与实际使用量有所偏差。
1363
- </div>
1364
  </div>
1365
-
1366
- <!-- 计算点使用情况卡片 -->
1367
- <div class="card">
1368
- <div class="card-header">
1369
- <h2 class="card-title">
1370
- <div class="card-icon">💎</div>
1371
- 计算点使用情况
1372
- </h2>
1373
- </div>
1374
- <div class="status-item">
1375
- <span class="status-label">剩余计算点</span>
1376
- <span class="compute-points">{{ "{:,}".format(compute_points["left"]) }}</span>
1377
- </div>
1378
- <div class="status-item">
1379
- <span class="status-label">已用计算点</span>
1380
- <span class="compute-points">{{ "{:,}".format(compute_points["used"]) }}</span>
1381
- </div>
1382
- <div class="status-item">
1383
- <span class="status-label">总计算点</span>
1384
- <span class="compute-points">{{ "{:,}".format(compute_points["total"]) }}</span>
1385
- </div>
1386
- <div class="status-item">
1387
- <span class="status-label">使用百分比</span>
1388
- <div class="progress-container">
1389
- <div class="progress-bar{% if compute_points['percentage'] > 80 %} danger{% elif compute_points['percentage'] > 60 %} warning{% endif %}" style="width: {{ compute_points['percentage'] }}%"></div>
1390
  </div>
1391
- <span class="status-value{% if compute_points['percentage'] > 80 %} danger{% elif compute_points['percentage'] > 60 %} warning{% endif %}">{{ compute_points["percentage"] }}%</span>
1392
- </div>
1393
- {% if compute_points["last_update"] %}
1394
- <div class="status-item">
1395
- <span class="status-label">最后更新</span>
1396
- <span class="status-value">{{ compute_points["last_update"].strftime('%Y-%m-%d %H:%M:%S') }}</span>
1397
  </div>
1398
- {% endif %}
1399
- </div>
1400
- </div>
1401
-
1402
- <!-- 可用模型卡片 -->
1403
- <div class="card">
1404
- <div class="card-header">
1405
- <h2 class="card-title">
1406
- <div class="card-icon">🤖</div>
1407
- 可用模型
1408
- </h2>
1409
- </div>
1410
- <div class="models-list">
1411
- {% for model in models %}
1412
- <span class="model-tag">{{ model }}</span>
1413
- {% endfor %}
1414
  </div>
1415
- </div>
1416
 
1417
- <!-- 模型使用统计卡片 -->
1418
- <div class="card">
1419
  <div class="card-header">
1420
- <h2 class="card-title">
1421
- <div class="card-icon">📈</div>
1422
- 模型使用统计
1423
- </h2>
1424
- <button class="btn-toggle" onclick="toggleModels(this)">
1425
- <span class="text">显示全部</span>
1426
- <span class="icon">▼</span>
1427
  </button>
1428
  </div>
1429
- <div class="model-stats">
1430
- <div class="table-container">
1431
- <table class="data-table">
1432
- <thead>
1433
- <tr>
1434
- <th>模型名称</th>
1435
- <th>调用次数</th>
1436
- <th>输入Token</th>
1437
- <th>输出Token</th>
1438
- <th>总Token</th>
1439
- </tr>
1440
- </thead>
1441
- <tbody>
1442
- {% for model, stats in model_stats.items() %}
1443
- <tr class="{{ 'hidden-model' if loop.index > 5 }}">
1444
- <td>{{ model }}</td>
1445
- <td><span class="call-count">{{ stats["count"] }}</span></td>
1446
- <td><span class="token-count">{{ "{:,}".format(stats["prompt_tokens"]) }}</span></td>
1447
- <td><span class="token-count">{{ "{:,}".format(stats["completion_tokens"]) }}</span></td>
1448
- <td><span class="token-count">{{ "{:,}".format(stats["total_tokens"]) }}</span></td>
1449
- </tr>
1450
- {% endfor %}
1451
- </tbody>
1452
- </table>
1453
- </div>
1454
- <div class="token-note">
1455
- 💡 Token计算说明:输入Token包括用户发送的所有消息,输出Token包括AI助手的所有回复。总Token为两者之和。这些数据是使用tiktoken库计算的估算值,可能与实际计费有所差异。
 
 
 
1456
  </div>
1457
  </div>
1458
- </div>
1459
 
1460
- <!-- 计算点使用日志卡片 -->
1461
- <div class="card">
1462
  <div class="card-header">
1463
- <h2 class="card-title">
1464
- <div class="card-icon">📊</div>
1465
- 计算点使用日志
1466
- </h2>
1467
- </div>
1468
- <div class="table-container">
1469
- <table class="data-table">
1470
- <thead>
1471
- <tr>
1472
- {% for key, value in compute_points_log.columns.items() %}
1473
- <th>{{ value }}</th>
1474
- {% endfor %}
1475
- </tr>
1476
- </thead>
1477
- <tbody>
1478
- {% for entry in compute_points_log.log %}
1479
- <tr>
1480
- {% for key, value in compute_points_log.columns.items() %}
1481
- <td>
1482
- {% if key == 'timestamp' %}
1483
- {{ entry.get(key, '').split('T')[0] }}
1484
- {% elif key == 'computePoints' %}
1485
- <span class="compute-points">{{ "{:,}".format(entry.get(key, 0)) }}</span>
1486
- {% elif key == 'llmName' %}
1487
- <span class="model-tag">{{ entry.get(key, 'Unknown') }}</span>
1488
- {% else %}
1489
- {{ entry.get(key, 0) }}
1490
- {% endif %}
1491
- </td>
1492
- {% endfor %}
1493
- </tr>
1494
- {% endfor %}
1495
- </tbody>
1496
- </table>
1497
  </div>
1498
- <div class="token-note">
1499
- 💡 此日志显示了计算点的详细使用记录,包括使用时间、消耗的计算点数量以及对应的模型。数据每小时更新一次。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1500
  </div>
1501
- </div>
1502
 
1503
  <!-- API端点卡片 -->
1504
- <div class="card">
1505
  <div class="card-header">
1506
- <h2 class="card-title">
1507
- <div class="card-icon">🔌</div>
1508
- API端点
1509
- </h2>
1510
- </div>
1511
- <div class="endpoint-item">
1512
- <div>获取可用模型列表</div>
1513
- <div class="endpoint-url" onclick="copyToClipboard(this)">GET /v1/models</div>
1514
- </div>
1515
- <div class="endpoint-item">
1516
- <div>发送聊天请求</div>
1517
- <div class="endpoint-url" onclick="copyToClipboard(this)">POST /v1/chat/completions</div>
1518
  </div>
1519
- <div class="endpoint-item">
1520
- <div>健康检查</div>
1521
- <div class="endpoint-url" onclick="copyToClipboard(this)">GET /health</div>
 
 
 
 
 
 
 
1522
  </div>
1523
- </div>
1524
-
1525
- <footer class="footer">
1526
- <p>© {{ year }} Abacus Chat Proxy. All rights reserved.</p>
1527
- </footer>
1528
- </div>
1529
 
1530
  <!-- 返回顶部按钮 -->
1531
- <button class="back-to-top" onclick="scrollToTop()" title="返回顶部">
1532
-
1533
  </button>
1534
 
 
 
1535
  <!-- 添加页面加载器到body -->
1536
  <div class="page-loader">
1537
  <div class="loader"></div>
@@ -1792,6 +1740,330 @@
1792
  });
1793
  }
1794
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1795
  </script>
1796
  </body>
1797
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Abacus Chat代理仪表板 - 监控系统状态、Token使用情况和API端点">
7
+ <meta name="theme-color" content="#6366f1">
8
  <title>Abacus Chat代理仪表板</title>
9
+ <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
10
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
11
  <style>
12
  :root {
13
  --primary-color: #6366f1;
 
1300
  </style>
1301
  </head>
1302
  <body class="loading">
1303
+ <!-- 跳转到主要内容的链接 -->
1304
+ <a href="#main-content" class="skip-link">跳转到主要内容</a>
1305
+
1306
+ <!-- 页面加载动画 -->
1307
+ <div class="loading-overlay" role="progressbar" aria-label="页面加载中">
1308
+ <div class="loader" aria-hidden="true"></div>
1309
+ <p>加载中...</p>
1310
+ </div>
1311
+
1312
+ <!-- 导航栏 -->
1313
+ <nav class="navbar" role="navigation" aria-label="主导航">
1314
  <div class="brand">
1315
+ <img src="/static/logo.png" alt="Abacus Chat Logo" class="logo" width="32" height="32">
1316
  <h1>Abacus Chat代理仪表板</h1>
1317
  </div>
1318
  <div class="nav-actions">
1319
+ <button class="btn btn-secondary" onclick="location.href='/logout'" aria-label="退出登录">
1320
+ <span class="material-icons" aria-hidden="true">logout</span>
1321
+ <span>退出登录</span>
1322
  </button>
1323
  </div>
1324
  </nav>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1325
 
1326
+ <!-- 主要内容 -->
1327
+ <main id="main-content" class="dashboard-content" role="main">
1328
+ <!-- 系统状态卡片 -->
1329
+ <section class="card" id="system-status" aria-labelledby="status-title">
1330
+ <div class="card-header">
1331
+ <h2 id="status-title">系统状态</h2>
1332
+ <button class="btn-toggle" aria-expanded="true" aria-controls="status-content">
1333
+ <span class="sr-only">折叠系统状态</span>
1334
+ <span class="toggle-icon" aria-hidden="true">▼</span>
1335
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
1336
  </div>
1337
+ <div id="status-content" class="card-body">
1338
+ <div class="status-grid" role="list">
1339
+ <div class="status-item" role="listitem">
1340
+ <span class="material-icons" aria-hidden="true">timer</span>
1341
+ <div class="status-info">
1342
+ <h3>运行时间</h3>
1343
+ <p class="uptime">{{ uptime }}</p>
1344
+ </div>
1345
+ </div>
1346
+ <div class="status-item" role="listitem">
1347
+ <span class="material-icons" aria-hidden="true">health_and_safety</span>
1348
+ <div class="status-info">
1349
+ <h3>健康状态</h3>
1350
+ <div class="health-status" data-status="{{ health_status }}" role="status">
1351
+ <span class="status-indicator" aria-hidden="true"></span>
1352
+ <span class="status-text">{{ health_status }}</span>
1353
+ </div>
1354
+ </div>
1355
+ </div>
1356
+ <div class="status-item" role="listitem">
1357
+ <span class="material-icons" aria-hidden="true">group</span>
1358
+ <div class="status-info">
1359
+ <h3>用户数量</h3>
1360
+ <p class="user-count" data-value="{{ user_count }}" aria-live="polite">{{ user_count }}</p>
1361
+ </div>
1362
  </div>
 
 
 
 
 
 
1363
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1364
  </div>
1365
+ </section>
1366
 
1367
+ <!-- Token使用统计卡片 -->
1368
+ <section class="card" id="token-stats" aria-labelledby="token-title">
1369
  <div class="card-header">
1370
+ <h2 id="token-title">Token使用统计</h2>
1371
+ <button class="btn-toggle" aria-expanded="true" aria-controls="token-content">
1372
+ <span class="sr-only">折叠Token统计</span>
1373
+ <span class="toggle-icon" aria-hidden="true">▼</span>
 
 
 
1374
  </button>
1375
  </div>
1376
+ <div id="token-content" class="card-body">
1377
+ <div class="token-overview">
1378
+ <div class="token-total">
1379
+ <h3>总Token使用量</h3>
1380
+ <p class="token-count" data-value="{{ total_tokens }}" aria-live="polite">{{ total_tokens }}</p>
1381
+ <small class="token-note" role="note">*此数据仅包含代理使用的token,不包含Abacus网站使用的token。数据为粗略估计。</small>
1382
+ </div>
1383
+ <div class="token-breakdown">
1384
+ <h3>按模型统计</h3>
1385
+ <div class="table-container">
1386
+ <table class="token-table" aria-label="Token使用明细">
1387
+ <thead>
1388
+ <tr>
1389
+ <th scope="col">模型</th>
1390
+ <th scope="col">Token使用量</th>
1391
+ <th scope="col">占比</th>
1392
+ </tr>
1393
+ </thead>
1394
+ <tbody>
1395
+ {% for model in token_stats %}
1396
+ <tr>
1397
+ <th scope="row">{{ model.name }}</th>
1398
+ <td class="token-count" data-value="{{ model.tokens }}">{{ model.tokens }}</td>
1399
+ <td>{{ model.percentage }}%</td>
1400
+ </tr>
1401
+ {% endfor %}
1402
+ </tbody>
1403
+ </table>
1404
+ </div>
1405
+ </div>
1406
  </div>
1407
  </div>
1408
+ </section>
1409
 
1410
+ <!-- 计算点使用统计卡片 -->
1411
+ <section class="card" id="compute-points" aria-labelledby="points-title">
1412
  <div class="card-header">
1413
+ <h2 id="points-title">计算点使用统计</h2>
1414
+ <button class="btn-toggle" aria-expanded="true" aria-controls="points-content">
1415
+ <span class="sr-only">折叠计算点统计</span>
1416
+ <span class="toggle-icon" aria-hidden="true">▼</span>
1417
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1418
  </div>
1419
+ <div id="points-content" class="card-body">
1420
+ <div class="points-overview">
1421
+ <div class="points-total">
1422
+ <h3>总计算点使用量</h3>
1423
+ <p class="compute-points" data-value="{{ total_compute_points }}" aria-live="polite">{{ total_compute_points }}</p>
1424
+ </div>
1425
+ <div class="points-breakdown">
1426
+ <h3>使用记录</h3>
1427
+ <div class="table-container">
1428
+ <table class="points-table" aria-label="计算点使用记录">
1429
+ <thead>
1430
+ <tr>
1431
+ <th scope="col">时间</th>
1432
+ <th scope="col">计算点</th>
1433
+ <th scope="col">模型</th>
1434
+ </tr>
1435
+ </thead>
1436
+ <tbody>
1437
+ {% for entry in compute_points_log %}
1438
+ <tr>
1439
+ <td>{{ entry.timestamp }}</td>
1440
+ <td class="compute-points">{{ entry.points }}</td>
1441
+ <td>{{ entry.model }}</td>
1442
+ </tr>
1443
+ {% endfor %}
1444
+ </tbody>
1445
+ </table>
1446
+ </div>
1447
+ <small class="points-note" role="note">*每小时更新一次</small>
1448
+ </div>
1449
+ </div>
1450
  </div>
1451
+ </section>
1452
 
1453
  <!-- API端点卡片 -->
1454
+ <section class="card" id="api-endpoints" aria-labelledby="endpoints-title">
1455
  <div class="card-header">
1456
+ <h2 id="endpoints-title">API端点</h2>
1457
+ <button class="btn-toggle" aria-expanded="true" aria-controls="endpoints-content">
1458
+ <span class="sr-only">折叠API端点</span>
1459
+ <span class="toggle-icon" aria-hidden="true">▼</span>
1460
+ </button>
 
 
 
 
 
 
 
1461
  </div>
1462
+ <div id="endpoints-content" class="card-body">
1463
+ <div class="endpoints-grid" role="list">
1464
+ {% for endpoint in api_endpoints %}
1465
+ <div class="endpoint-card" role="listitem">
1466
+ <h3>{{ endpoint.name }}</h3>
1467
+ <p class="api-endpoint" role="button" tabindex="0" aria-label="复制API端点: {{ endpoint.url }}">{{ endpoint.url }}</p>
1468
+ <small class="endpoint-note" aria-hidden="true">点击复制</small>
1469
+ </div>
1470
+ {% endfor %}
1471
+ </div>
1472
  </div>
1473
+ </section>
1474
+ </main>
 
 
 
 
1475
 
1476
  <!-- 返回顶部按钮 -->
1477
+ <button class="back-to-top" aria-label="返回页面顶部">
1478
+ <span class="material-icons" aria-hidden="true">arrow_upward</span>
1479
  </button>
1480
 
1481
+ <!-- 主题切换按钮 -->
1482
+ <button class="theme-toggle" aria-label="切换深色/浅色主题">
1483
  <!-- 添加页面加载器到body -->
1484
  <div class="page-loader">
1485
  <div class="loader"></div>
 
1740
  });
1741
  }
1742
  });
1743
+
1744
+ // 无障碍支持
1745
+ class A11yManager {
1746
+ constructor() {
1747
+ this.focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
1748
+ this.init();
1749
+ }
1750
+
1751
+ init() {
1752
+ this.setupFocusTrap();
1753
+ this.setupKeyboardNavigation();
1754
+ this.setupSkipLink();
1755
+ this.setupAnnouncer();
1756
+ }
1757
+
1758
+ setupFocusTrap() {
1759
+ document.addEventListener('keydown', (e) => {
1760
+ if (e.key === 'Tab') {
1761
+ const modal = document.querySelector('.modal.show');
1762
+ if (!modal) return;
1763
+
1764
+ const focusableElements = modal.querySelectorAll(this.focusableElements);
1765
+ const firstFocusable = focusableElements[0];
1766
+ const lastFocusable = focusableElements[focusableElements.length - 1];
1767
+
1768
+ if (e.shiftKey && document.activeElement === firstFocusable) {
1769
+ e.preventDefault();
1770
+ lastFocusable.focus();
1771
+ } else if (!e.shiftKey && document.activeElement === lastFocusable) {
1772
+ e.preventDefault();
1773
+ firstFocusable.focus();
1774
+ }
1775
+ }
1776
+ });
1777
+ }
1778
+
1779
+ setupKeyboardNavigation() {
1780
+ document.addEventListener('keydown', (e) => {
1781
+ if (e.key === 'Escape') {
1782
+ const modal = document.querySelector('.modal.show');
1783
+ if (modal) {
1784
+ this.closeModal(modal);
1785
+ }
1786
+
1787
+ const notifications = document.querySelectorAll('.notification.show');
1788
+ notifications.forEach(notification => {
1789
+ this.closeNotification(notification);
1790
+ });
1791
+ }
1792
+ });
1793
+ }
1794
+
1795
+ setupSkipLink() {
1796
+ const skipLink = document.querySelector('.skip-link');
1797
+ if (!skipLink) return;
1798
+
1799
+ skipLink.addEventListener('click', (e) => {
1800
+ e.preventDefault();
1801
+ const target = document.querySelector(skipLink.getAttribute('href'));
1802
+ if (target) {
1803
+ target.setAttribute('tabindex', '-1');
1804
+ target.focus();
1805
+ }
1806
+ });
1807
+ }
1808
+
1809
+ setupAnnouncer() {
1810
+ const announcer = document.createElement('div');
1811
+ announcer.setAttribute('aria-live', 'polite');
1812
+ announcer.setAttribute('aria-atomic', 'true');
1813
+ announcer.classList.add('sr-only');
1814
+ document.body.appendChild(announcer);
1815
+ this.announcer = announcer;
1816
+ }
1817
+
1818
+ announce(message) {
1819
+ if (!this.announcer) return;
1820
+ this.announcer.textContent = message;
1821
+ }
1822
+
1823
+ closeModal(modal) {
1824
+ modal.classList.remove('show');
1825
+ this.announce('模态框已关闭');
1826
+ }
1827
+
1828
+ closeNotification(notification) {
1829
+ notification.classList.remove('show');
1830
+ this.announce('通知已关闭');
1831
+ }
1832
+ }
1833
+
1834
+ // 主题管理
1835
+ class ThemeManager {
1836
+ constructor() {
1837
+ this.init();
1838
+ }
1839
+
1840
+ init() {
1841
+ this.setupThemeToggle();
1842
+ this.loadSavedTheme();
1843
+ this.setupSystemThemeListener();
1844
+ }
1845
+
1846
+ setupThemeToggle() {
1847
+ const themeToggle = document.querySelector('.theme-toggle');
1848
+ if (!themeToggle) return;
1849
+
1850
+ themeToggle.addEventListener('click', () => {
1851
+ const currentTheme = document.documentElement.getAttribute('data-theme');
1852
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
1853
+ this.setTheme(newTheme);
1854
+ this.announce(`已切换到${newTheme === 'dark' ? '深色' : '浅色'}主题`);
1855
+ });
1856
+ }
1857
+
1858
+ loadSavedTheme() {
1859
+ const savedTheme = localStorage.getItem('theme');
1860
+ if (savedTheme) {
1861
+ this.setTheme(savedTheme);
1862
+ } else {
1863
+ this.setTheme(this.getSystemTheme());
1864
+ }
1865
+ }
1866
+
1867
+ setupSystemThemeListener() {
1868
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1869
+ mediaQuery.addListener((e) => {
1870
+ if (!localStorage.getItem('theme')) {
1871
+ this.setTheme(e.matches ? 'dark' : 'light');
1872
+ }
1873
+ });
1874
+ }
1875
+
1876
+ getSystemTheme() {
1877
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1878
+ }
1879
+
1880
+ setTheme(theme) {
1881
+ document.documentElement.setAttribute('data-theme', theme);
1882
+ localStorage.setItem('theme', theme);
1883
+ }
1884
+
1885
+ announce(message) {
1886
+ const announcer = document.querySelector('[aria-live="polite"]');
1887
+ if (announcer) {
1888
+ announcer.textContent = message;
1889
+ }
1890
+ }
1891
+ }
1892
+
1893
+ // 数据管理
1894
+ class DataManager {
1895
+ constructor() {
1896
+ this.init();
1897
+ }
1898
+
1899
+ init() {
1900
+ this.setupTableSearch();
1901
+ this.setupTableSort();
1902
+ this.setupDataRefresh();
1903
+ }
1904
+
1905
+ setupTableSearch() {
1906
+ document.querySelectorAll('.table-container').forEach(container => {
1907
+ const table = container.querySelector('table');
1908
+ if (!table) return;
1909
+
1910
+ const search = document.createElement('input');
1911
+ search.type = 'text';
1912
+ search.className = 'table-search';
1913
+ search.placeholder = '搜索表格内容...';
1914
+ search.setAttribute('aria-label', '搜索表格内容');
1915
+ container.insertBefore(search, table);
1916
+
1917
+ search.addEventListener('input', (e) => {
1918
+ const searchText = e.target.value.toLowerCase();
1919
+ const rows = table.querySelectorAll('tbody tr');
1920
+
1921
+ rows.forEach(row => {
1922
+ const text = row.textContent.toLowerCase();
1923
+ const display = text.includes(searchText) ? '' : 'none';
1924
+ row.style.display = display;
1925
+ row.setAttribute('aria-hidden', display === 'none');
1926
+ });
1927
+
1928
+ this.announce(`找到 ${Array.from(rows).filter(row => row.style.display !== 'none').length} 条匹配记录`);
1929
+ });
1930
+ });
1931
+ }
1932
+
1933
+ setupTableSort() {
1934
+ document.querySelectorAll('table th').forEach(th => {
1935
+ if (th.getAttribute('data-sortable') === 'false') return;
1936
+
1937
+ th.style.cursor = 'pointer';
1938
+ th.setAttribute('role', 'button');
1939
+ th.setAttribute('aria-sort', 'none');
1940
+
1941
+ th.addEventListener('click', () => {
1942
+ const table = th.closest('table');
1943
+ const tbody = table.querySelector('tbody');
1944
+ const rows = Array.from(tbody.querySelectorAll('tr'));
1945
+ const index = Array.from(th.parentNode.children).indexOf(th);
1946
+ const direction = th.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending';
1947
+
1948
+ // 重置其他列的排序状态
1949
+ th.parentNode.querySelectorAll('th').forEach(header => {
1950
+ header.setAttribute('aria-sort', 'none');
1951
+ });
1952
+
1953
+ th.setAttribute('aria-sort', direction);
1954
+
1955
+ const sortedRows = rows.sort((a, b) => {
1956
+ const aValue = a.children[index].textContent;
1957
+ const bValue = b.children[index].textContent;
1958
+
1959
+ if (this.isNumeric(aValue) && this.isNumeric(bValue)) {
1960
+ return direction === 'ascending' ?
1961
+ this.parseNumber(aValue) - this.parseNumber(bValue) :
1962
+ this.parseNumber(bValue) - this.parseNumber(aValue);
1963
+ }
1964
+
1965
+ return direction === 'ascending' ?
1966
+ aValue.localeCompare(bValue) :
1967
+ bValue.localeCompare(aValue);
1968
+ });
1969
+
1970
+ tbody.append(...sortedRows);
1971
+ this.announce(`表格已按${th.textContent}${direction === 'ascending' ? '升序' : '降序'}排序`);
1972
+ });
1973
+ });
1974
+ }
1975
+
1976
+ setupDataRefresh() {
1977
+ setInterval(async () => {
1978
+ try {
1979
+ const response = await fetch('/api/dashboard/data');
1980
+ const data = await response.json();
1981
+ this.updateDashboard(data);
1982
+ } catch (err) {
1983
+ console.error('数据刷新失败:', err);
1984
+ }
1985
+ }, 60000); // 每分钟更新一次
1986
+ }
1987
+
1988
+ updateDashboard(data) {
1989
+ // 更新系统状态
1990
+ if (data.uptime) {
1991
+ document.querySelector('.uptime').textContent = data.uptime;
1992
+ }
1993
+
1994
+ if (data.health_status) {
1995
+ const healthStatus = document.querySelector('.health-status');
1996
+ healthStatus.setAttribute('data-status', data.health_status);
1997
+ healthStatus.querySelector('.status-text').textContent = data.health_status;
1998
+ }
1999
+
2000
+ if (data.user_count) {
2001
+ const userCount = document.querySelector('.user-count');
2002
+ this.animateNumber(userCount, parseInt(userCount.textContent), data.user_count);
2003
+ }
2004
+
2005
+ // 更新Token统计
2006
+ if (data.total_tokens) {
2007
+ const tokenCount = document.querySelector('.token-count');
2008
+ this.animateNumber(tokenCount, parseInt(tokenCount.textContent), data.total_tokens);
2009
+ }
2010
+
2011
+ // 更新计算点统计
2012
+ if (data.compute_points) {
2013
+ const computePoints = document.querySelector('.compute-points');
2014
+ this.animateNumber(computePoints, parseInt(computePoints.textContent), data.compute_points);
2015
+ }
2016
+
2017
+ this.announce('仪表板数据已更新');
2018
+ }
2019
+
2020
+ animateNumber(element, start, end) {
2021
+ if (start === end) return;
2022
+
2023
+ const duration = 1000;
2024
+ const startTime = performance.now();
2025
+ const range = end - start;
2026
+
2027
+ const update = (currentTime) => {
2028
+ const elapsed = currentTime - startTime;
2029
+ const progress = Math.min(elapsed / duration, 1);
2030
+
2031
+ const current = Math.floor(start + range * progress);
2032
+ element.textContent = new Intl.NumberFormat().format(current);
2033
+
2034
+ if (progress < 1) {
2035
+ requestAnimationFrame(update);
2036
+ }
2037
+ };
2038
+
2039
+ requestAnimationFrame(update);
2040
+ }
2041
+
2042
+ isNumeric(value) {
2043
+ return !isNaN(this.parseNumber(value));
2044
+ }
2045
+
2046
+ parseNumber(value) {
2047
+ return parseFloat(value.replace(/[^0-9.-]+/g, ''));
2048
+ }
2049
+
2050
+ announce(message) {
2051
+ const announcer = document.querySelector('[aria-live="polite"]');
2052
+ if (announcer) {
2053
+ announcer.textContent = message;
2054
+ }
2055
+ }
2056
+ }
2057
+
2058
+ // 初始化
2059
+ document.addEventListener('DOMContentLoaded', () => {
2060
+ const a11y = new A11yManager();
2061
+ const theme = new ThemeManager();
2062
+ const data = new DataManager();
2063
+
2064
+ // 移除加载状态
2065
+ document.body.classList.remove('loading');
2066
+ });
2067
  </script>
2068
  </body>
2069
  </html>