Spaces:
Runtime error
Runtime error
Upload 6 files
Browse files- 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 |
-
|
| 1300 |
-
|
| 1301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 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 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
<
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
|
| 1389 |
-
|
| 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 |
-
</
|
| 1416 |
|
| 1417 |
-
<!--
|
| 1418 |
-
<
|
| 1419 |
<div class="card-header">
|
| 1420 |
-
<h2
|
| 1421 |
-
|
| 1422 |
-
|
| 1423 |
-
|
| 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="
|
| 1430 |
-
<div class="
|
| 1431 |
-
<
|
| 1432 |
-
<
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
|
| 1437 |
-
|
| 1438 |
-
|
| 1439 |
-
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
|
| 1443 |
-
|
| 1444 |
-
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
-
<
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
|
|
|
|
|
|
|
|
|
|
| 1456 |
</div>
|
| 1457 |
</div>
|
| 1458 |
-
</
|
| 1459 |
|
| 1460 |
-
<!--
|
| 1461 |
-
<
|
| 1462 |
<div class="card-header">
|
| 1463 |
-
<h2
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 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="
|
| 1499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1500 |
</div>
|
| 1501 |
-
</
|
| 1502 |
|
| 1503 |
<!-- API端点卡片 -->
|
| 1504 |
-
<
|
| 1505 |
<div class="card-header">
|
| 1506 |
-
<h2
|
| 1507 |
-
|
| 1508 |
-
API
|
| 1509 |
-
|
| 1510 |
-
|
| 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="
|
| 1520 |
-
<div
|
| 1521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1522 |
</div>
|
| 1523 |
-
</
|
| 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"
|
| 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>
|