Spaces:
Sleeping
Sleeping
<html lang="zh-CN"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta name="description" content="Abacus Chat代理仪表板 - 监控系统状态、Token使用情况和API端点"> | |
<meta name="theme-color" content="#6366f1"> | |
<title>Abacus Chat代理仪表板</title> | |
<link rel="icon" href="/static/favicon.ico" type="image/x-icon"> | |
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |
<style> | |
:root { | |
--primary-color: #6366f1; | |
--primary-dark: #4f46e5; | |
--accent-color: #8b5cf6; | |
--background: #f8fafc; | |
--card-bg: #ffffff; | |
--text-color: #1e293b; | |
--text-light: #64748b; | |
--error: #ef4444; | |
--success: #10b981; | |
--warning: #f59e0b; | |
--surface-1: rgba(255, 255, 255, 0.05); | |
--surface-2: rgba(255, 255, 255, 0.1); | |
--blur-bg: rgba(15, 23, 42, 0.6); | |
} | |
@media (prefers-color-scheme: dark) { | |
:root { | |
--primary-color: #818cf8; | |
--primary-dark: #6366f1; | |
--accent-color: #a78bfa; | |
--background: #0f172a; | |
--card-bg: #1e293b; | |
--text-color: #f1f5f9; | |
--text-light: #94a3b8; | |
} | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; | |
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s; | |
} | |
body { | |
min-height: 100vh; | |
background: var(--background); | |
color: var(--text-color); | |
line-height: 1.6; | |
overflow-x: hidden; | |
} | |
/* 动态背景 */ | |
.background-animation { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: -1; | |
background: radial-gradient( | |
circle at 50% 50%, | |
rgba(99, 102, 241, 0.15), | |
rgba(139, 92, 246, 0.15), | |
transparent 60% | |
); | |
filter: blur(80px); | |
opacity: 0.5; | |
animation: pulse 8s ease-in-out infinite alternate; | |
} | |
@keyframes pulse { | |
0% { | |
transform: scale(1); | |
opacity: 0.5; | |
} | |
100% { | |
transform: scale(1.2); | |
opacity: 0.3; | |
} | |
} | |
/* 顶部导航栏 */ | |
.navbar { | |
position: sticky; | |
top: 0; | |
z-index: 100; | |
background: var(--blur-bg); | |
backdrop-filter: blur(12px); | |
-webkit-backdrop-filter: blur(12px); | |
border-bottom: 1px solid var(--surface-1); | |
padding: 1rem 2rem; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.navbar-brand { | |
display: flex; | |
align-items: center; | |
gap: 1rem; | |
text-decoration: none; | |
color: var(--text-color); | |
} | |
.navbar-logo { | |
font-size: 1.75rem; | |
background: linear-gradient(45deg, var(--primary-color), var(--accent-color)); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
animation: float 3s ease-in-out infinite; | |
} | |
@keyframes float { | |
0%, 100% { transform: translateY(0); } | |
50% { transform: translateY(-5px); } | |
} | |
.navbar-title { | |
font-size: 1.25rem; | |
font-weight: 600; | |
background: linear-gradient(45deg, var(--primary-color), var(--accent-color)); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
} | |
.navbar-actions { | |
display: flex; | |
gap: 1rem; | |
} | |
.btn-logout { | |
padding: 0.5rem 1.25rem; | |
border: none; | |
border-radius: 8px; | |
background: var(--surface-1); | |
color: var(--text-color); | |
font-weight: 500; | |
cursor: pointer; | |
transition: all 0.2s; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
text-decoration: none; | |
backdrop-filter: blur(4px); | |
} | |
.btn-logout:hover { | |
background: var(--surface-2); | |
transform: translateY(-1px); | |
} | |
/* 主容器 */ | |
.container { | |
max-width: 1400px; | |
margin: 0 auto; | |
padding: 2rem; | |
} | |
/* 网格布局 */ | |
.grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
gap: 1.5rem; | |
margin-bottom: 2rem; | |
} | |
/* 卡片样式 */ | |
.card { | |
background: var(--card-bg); | |
border-radius: 16px; | |
padding: 1.5rem; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
border: 1px solid var(--surface-1); | |
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
position: relative; | |
overflow: hidden; | |
transform: translateY(0) scale(1); | |
opacity: 0; | |
transform: translateY(20px); | |
transition: opacity 0.5s, transform 0.5s; | |
} | |
.card:hover { | |
transform: translateY(-4px) scale(1.005); | |
} | |
.card::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 3px; | |
background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
opacity: 0; | |
transition: opacity 0.3s; | |
} | |
.card:hover::before { | |
opacity: 1; | |
} | |
.card-header { | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
margin-bottom: 1.5rem; | |
} | |
.card-title { | |
font-size: 1.25rem; | |
font-weight: 600; | |
color: var(--text-color); | |
display: flex; | |
align-items: center; | |
gap: 0.75rem; | |
} | |
.card-icon { | |
width: 32px; | |
height: 32px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background: linear-gradient(45deg, var(--primary-color), var(--accent-color)); | |
border-radius: 8px; | |
font-size: 1.25rem; | |
color: white; | |
} | |
/* 状态项样式 */ | |
.status-item { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 0.75rem 0; | |
border-bottom: 1px solid var(--surface-1); | |
position: relative; | |
transition: all 0.2s; | |
overflow: hidden; | |
} | |
.status-item:last-child { | |
border-bottom: none; | |
} | |
.status-item:hover { | |
background: var(--surface-1); | |
border-radius: 8px; | |
padding-left: 0.5rem; | |
padding-right: 0.5rem; | |
} | |
.status-label { | |
color: var(--text-light); | |
font-weight: 500; | |
} | |
.status-value { | |
font-weight: 600; | |
color: var(--text-color); | |
} | |
.status-value.success { | |
color: var(--success); | |
} | |
.status-value.warning { | |
color: var(--warning); | |
} | |
.status-value.danger { | |
color: var(--error); | |
} | |
/* 模型标签 */ | |
.models-list { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 0.5rem; | |
} | |
.model-tag { | |
padding: 0.25rem 0.75rem; | |
background: var(--surface-1); | |
border-radius: 16px; | |
font-size: 0.875rem; | |
color: var(--text-color); | |
border: 1px solid var(--surface-2); | |
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
transform: translateY(0); | |
} | |
.model-tag:hover { | |
background: var(--surface-2); | |
transform: translateY(-2px); | |
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); | |
} | |
/* 进度条 */ | |
.progress-container { | |
width: 100%; | |
height: 8px; | |
background: var(--surface-1); | |
border-radius: 4px; | |
overflow: hidden; | |
margin-top: 0.5rem; | |
} | |
.progress-bar { | |
height: 100%; | |
background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
border-radius: 4px; | |
position: relative; | |
overflow: hidden; | |
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1); | |
} | |
.progress-bar::after { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: -100%; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
animation: shimmer 2s infinite; | |
} | |
@keyframes shimmer { | |
100% { transform: translateX(200%); } | |
} | |
.progress-bar.warning { | |
background: linear-gradient(90deg, var(--warning), #fbbf24); | |
} | |
.progress-bar.danger { | |
background: linear-gradient(90deg, var(--error), #dc2626); | |
} | |
/* 响应式设计 */ | |
@media (max-width: 1024px) { | |
.grid { | |
grid-template-columns: repeat(2, 1fr); | |
} | |
.container { | |
padding: 1.5rem; | |
} | |
} | |
@media (max-width: 768px) { | |
.grid { | |
grid-template-columns: 1fr; | |
} | |
.container { | |
padding: 1rem; | |
} | |
.navbar { | |
padding: 0.75rem 1rem; | |
} | |
.navbar-title { | |
font-size: 1rem; | |
} | |
.card { | |
padding: 1rem; | |
} | |
.card-title { | |
font-size: 1.1rem; | |
} | |
.table-container { | |
margin: -1rem; | |
width: calc(100% + 2rem); | |
border-radius: 0; | |
} | |
.data-table th, | |
.data-table td { | |
padding: 0.75rem; | |
font-size: 0.875rem; | |
} | |
.token-count, | |
.call-count, | |
.compute-points { | |
font-size: 0.875rem; | |
padding: 0.2rem 0.4rem; | |
} | |
.model-tag { | |
font-size: 0.75rem; | |
padding: 0.2rem 0.5rem; | |
} | |
.btn-toggle { | |
font-size: 0.75rem; | |
padding: 0.4rem 0.75rem; | |
} | |
.endpoint-url { | |
font-size: 0.875rem; | |
padding: 0.5rem 0.75rem; | |
} | |
.back-to-top { | |
width: 40px; | |
height: 40px; | |
font-size: 1rem; | |
right: 1rem; | |
bottom: 1rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.navbar-logo { | |
font-size: 1.25rem; | |
} | |
.btn-logout { | |
padding: 0.4rem 0.75rem; | |
font-size: 0.875rem; | |
} | |
.card-header { | |
flex-direction: column; | |
align-items: flex-start; | |
gap: 0.75rem; | |
} | |
.btn-toggle { | |
margin-left: 0; | |
width: 100%; | |
justify-content: center; | |
} | |
.status-item { | |
flex-direction: column; | |
align-items: flex-start; | |
gap: 0.5rem; | |
} | |
.progress-container { | |
margin-top: 0.5rem; | |
width: 100%; | |
} | |
} | |
/* 表格样式 */ | |
.table-container { | |
overflow-x: auto; | |
margin-top: 1rem; | |
border-radius: 12px; | |
border: 1px solid var(--surface-1); | |
background: var(--card-bg); | |
position: relative; | |
overflow: hidden; | |
} | |
.data-table { | |
width: 100%; | |
border-collapse: collapse; | |
text-align: left; | |
} | |
.data-table th { | |
background: var(--surface-1); | |
padding: 1rem; | |
font-weight: 600; | |
color: var(--text-color); | |
position: relative; | |
overflow: hidden; | |
} | |
.data-table th::after { | |
content: ''; | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
transform: scaleX(0); | |
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
transform-origin: left; | |
} | |
.data-table th:hover::after { | |
transform: scaleX(1); | |
} | |
.data-table td { | |
padding: 1rem; | |
border-bottom: 1px solid var(--surface-1); | |
} | |
.data-table tbody tr { | |
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
} | |
.data-table tbody tr:hover { | |
transform: scale(1.01); | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
z-index: 1; | |
} | |
.data-table tbody tr:last-child td { | |
border-bottom: none; | |
} | |
/* 特殊值样式 */ | |
.token-count { | |
font-family: 'JetBrains Mono', 'Fira Code', monospace; | |
color: var(--primary-color); | |
font-weight: 600; | |
position: relative; | |
padding: 0.25rem 0.5rem; | |
border-radius: 4px; | |
background: var(--surface-1); | |
transition: all 0.2s; | |
} | |
.token-count:hover { | |
background: var(--surface-2); | |
transform: scale(1.1); | |
} | |
.call-count { | |
font-family: 'JetBrains Mono', 'Fira Code', monospace; | |
color: var(--success); | |
font-weight: 600; | |
position: relative; | |
padding: 0.25rem 0.5rem; | |
border-radius: 4px; | |
background: var(--surface-1); | |
transition: all 0.2s; | |
} | |
.call-count:hover { | |
background: var(--surface-2); | |
transform: scale(1.1); | |
} | |
.compute-points { | |
font-family: 'JetBrains Mono', 'Fira Code', monospace; | |
color: var(--accent-color); | |
font-weight: 600; | |
position: relative; | |
padding: 0.25rem 0.5rem; | |
border-radius: 4px; | |
background: var(--surface-1); | |
transition: all 0.2s; | |
} | |
.compute-points:hover { | |
background: var(--surface-2); | |
transform: scale(1.1); | |
} | |
/* API端点卡片 */ | |
.endpoint-item { | |
background: var(--surface-1); | |
padding: 1.25rem; | |
border-radius: 12px; | |
margin-bottom: 1rem; | |
border-left: 3px solid var(--primary-color); | |
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
transform: translateX(0); | |
} | |
.endpoint-item:hover { | |
background: var(--surface-2); | |
transform: translateX(8px); | |
} | |
.endpoint-url { | |
font-family: 'JetBrains Mono', 'Fira Code', monospace; | |
background: var(--card-bg); | |
padding: 0.75rem 1rem; | |
border-radius: 8px; | |
margin-top: 0.5rem; | |
display: inline-block; | |
border: 1px solid var(--surface-1); | |
transition: all 0.2s; | |
cursor: pointer; | |
} | |
.endpoint-url:hover { | |
border-color: var(--primary-color); | |
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1); | |
} | |
/* 页脚 */ | |
.footer { | |
text-align: center; | |
padding: 2rem 0; | |
color: var(--text-light); | |
font-size: 0.9rem; | |
border-top: 1px solid var(--surface-1); | |
margin-top: 2rem; | |
background: var(--card-bg); | |
position: relative; | |
overflow: hidden; | |
} | |
.footer::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 1px; | |
background: linear-gradient( | |
90deg, | |
transparent, | |
var(--primary-color), | |
var(--accent-color), | |
transparent | |
); | |
animation: footerGlow 3s infinite; | |
} | |
@keyframes footerGlow { | |
0%, 100% { | |
opacity: 0.3; | |
} | |
50% { | |
opacity: 0.7; | |
} | |
} | |
/* 返回顶部按钮 */ | |
.back-to-top { | |
position: fixed; | |
bottom: 2rem; | |
right: 2rem; | |
width: 48px; | |
height: 48px; | |
border-radius: 50%; | |
background: var(--card-bg); | |
border: none; | |
color: var(--text-color); | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transition: all 0.3s; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
opacity: 0; | |
visibility: hidden; | |
transform: translateY(20px); | |
} | |
.back-to-top.visible { | |
opacity: 1; | |
visibility: visible; | |
transform: translateY(0); | |
} | |
.back-to-top:hover { | |
background: var(--primary-color); | |
color: white; | |
transform: translateY(-5px); | |
} | |
/* 滚动条美化 */ | |
::-webkit-scrollbar { | |
width: 8px; | |
height: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: var(--surface-1); | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb { | |
background: var(--primary-color); | |
border-radius: 4px; | |
border: 2px solid var(--surface-1); | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: var(--primary-dark); | |
} | |
/* 模型统计折叠样式 */ | |
.model-stats { | |
position: relative; | |
} | |
.hidden-model { | |
display: none; | |
animation: fadeOut 0.3s ease; | |
} | |
.hidden-model.show { | |
display: block; | |
animation: fadeIn 0.3s ease; | |
} | |
@keyframes fadeIn { | |
from { | |
opacity: 0; | |
transform: translateY(-10px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
@keyframes fadeOut { | |
from { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
to { | |
opacity: 0; | |
transform: translateY(-10px); | |
} | |
} | |
.btn-toggle { | |
background: var(--surface-1); | |
border: 1px solid var(--surface-2); | |
border-radius: 8px; | |
padding: 0.5rem 1rem; | |
color: var(--text-color); | |
cursor: pointer; | |
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
font-size: 0.875rem; | |
font-weight: 500; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
margin-left: auto; | |
} | |
.btn-toggle:hover { | |
background: var(--surface-2); | |
transform: translateY(-2px); | |
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); | |
} | |
.btn-toggle .icon { | |
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
} | |
.btn-toggle.expanded .icon { | |
transform: rotate(180deg); | |
} | |
/* Token注释样式 */ | |
.token-note { | |
margin-top: 1rem; | |
color: var(--text-light); | |
font-style: italic; | |
line-height: 1.6; | |
padding: 1rem; | |
border-radius: 8px; | |
background: var(--surface-1); | |
border: 1px solid var(--surface-2); | |
position: relative; | |
transition: all 0.3s; | |
} | |
.token-note::before { | |
content: '💡'; | |
position: absolute; | |
top: -12px; | |
left: 1rem; | |
background: var(--card-bg); | |
padding: 0 0.5rem; | |
transition: all 0.3s; | |
} | |
.token-note:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); | |
} | |
.token-note:hover::before { | |
transform: scale(1.2); | |
} | |
.token-model-table { | |
margin-top: 1.5rem; | |
} | |
/* 健康检查状态 */ | |
.health-status { | |
display: inline-flex; | |
align-items: center; | |
gap: 0.5rem; | |
padding: 0.25rem 0.75rem; | |
border-radius: 16px; | |
font-size: 0.875rem; | |
font-weight: 500; | |
background: var(--success); | |
color: white; | |
animation: pulse 2s infinite; | |
position: relative; | |
overflow: hidden; | |
} | |
.health-status.warning { | |
background: var(--warning); | |
} | |
.health-status.error { | |
background: var(--error); | |
} | |
.health-status::before { | |
content: ''; | |
position: absolute; | |
top: -50%; | |
left: -50%; | |
width: 200%; | |
height: 200%; | |
background: linear-gradient( | |
45deg, | |
transparent, | |
rgba(255, 255, 255, 0.1), | |
transparent | |
); | |
transform: rotate(45deg); | |
animation: shine 3s infinite; | |
} | |
@keyframes shine { | |
0% { | |
transform: translateX(-100%) rotate(45deg); | |
} | |
100% { | |
transform: translateX(100%) rotate(45deg); | |
} | |
} | |
@keyframes pulse { | |
0% { | |
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); | |
} | |
70% { | |
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); | |
} | |
100% { | |
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); | |
} | |
} | |
/* 加载动画 */ | |
@keyframes shimmer { | |
0% { | |
background-position: -1000px 0; | |
} | |
100% { | |
background-position: 1000px 0; | |
} | |
} | |
.loading { | |
position: relative; | |
overflow: hidden; | |
} | |
.loading::after { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient( | |
90deg, | |
transparent, | |
var(--surface-2), | |
transparent | |
); | |
animation: shimmer 2s infinite linear; | |
background-size: 1000px 100%; | |
} | |
/* 页面加载动画 */ | |
.page-loader { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: var(--background); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
z-index: 9999; | |
opacity: 1; | |
visibility: visible; | |
transition: all 0.5s; | |
} | |
.page-loader.hidden { | |
opacity: 0; | |
visibility: hidden; | |
} | |
.loader { | |
width: 48px; | |
height: 48px; | |
border: 3px solid var(--surface-1); | |
border-radius: 50%; | |
display: inline-block; | |
position: relative; | |
box-sizing: border-box; | |
animation: rotation 1s linear infinite; | |
} | |
.loader::after { | |
content: ''; | |
box-sizing: border-box; | |
position: absolute; | |
left: 50%; | |
top: 50%; | |
transform: translate(-50%, -50%); | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
border: 3px solid transparent; | |
border-bottom-color: var(--primary-color); | |
} | |
@keyframes rotation { | |
0% { | |
transform: rotate(0deg); | |
} | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
/* 数据加载中状态 */ | |
.loading-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: var(--background); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 9999; | |
transition: opacity 0.3s, visibility 0.3s; | |
} | |
.loading .loading-overlay { | |
opacity: 1; | |
visibility: visible; | |
} | |
body:not(.loading) .loading-overlay { | |
opacity: 0; | |
visibility: hidden; | |
} | |
.loader { | |
width: 48px; | |
height: 48px; | |
border: 3px solid var(--primary-color); | |
border-radius: 50%; | |
display: inline-block; | |
position: relative; | |
box-sizing: border-box; | |
animation: rotation 1s linear infinite; | |
} | |
.loader::after { | |
content: ''; | |
box-sizing: border-box; | |
position: absolute; | |
left: 50%; | |
top: 50%; | |
transform: translate(-50%, -50%); | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
border: 3px solid transparent; | |
border-bottom-color: var(--primary-dark); | |
} | |
@keyframes rotation { | |
0% { | |
transform: rotate(0deg); | |
} | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
/* 卡片动画 */ | |
.card { | |
opacity: 0; | |
transform: translateY(20px); | |
transition: opacity 0.5s, transform 0.5s; | |
} | |
.card.animate { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
/* 状态项动画 */ | |
.status-item { | |
position: relative; | |
overflow: hidden; | |
} | |
.status-item::before { | |
content: ''; | |
position: absolute; | |
top: -50%; | |
left: -50%; | |
width: 200%; | |
height: 200%; | |
background: radial-gradient( | |
circle, | |
rgba(255, 255, 255, 0.1) 0%, | |
transparent 70% | |
); | |
transform: rotate(0deg); | |
animation: rotate 10s linear infinite; | |
} | |
@keyframes rotate { | |
0% { | |
transform: rotate(0deg); | |
} | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
/* 健康状态动画 */ | |
.health-status { | |
position: relative; | |
display: inline-flex; | |
align-items: center; | |
gap: 0.5rem; | |
padding: 0.5rem 1rem; | |
border-radius: 20px; | |
background: var(--surface-1); | |
transition: all 0.3s; | |
} | |
.health-status .status-indicator { | |
width: 10px; | |
height: 10px; | |
border-radius: 50%; | |
background: var(--success); | |
position: relative; | |
} | |
.health-status[data-status="healthy"] .status-indicator { | |
background: var(--success); | |
} | |
.health-status[data-status="warning"] .status-indicator { | |
background: var(--warning); | |
} | |
.health-status[data-status="error"] .status-indicator { | |
background: var(--error); | |
} | |
.health-status .status-indicator::after { | |
content: ''; | |
position: absolute; | |
top: -2px; | |
left: -2px; | |
right: -2px; | |
bottom: -2px; | |
border-radius: 50%; | |
border: 2px solid currentColor; | |
opacity: 0; | |
animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; | |
} | |
@keyframes ping { | |
75%, 100% { | |
transform: scale(2); | |
opacity: 0; | |
} | |
} | |
/* 表格动画 */ | |
.table-container { | |
position: relative; | |
overflow: hidden; | |
} | |
.table-container::after { | |
content: ''; | |
position: absolute; | |
top: 0; | |
right: 0; | |
width: 40px; | |
height: 100%; | |
background: linear-gradient( | |
to right, | |
transparent, | |
var(--card-bg) 50% | |
); | |
pointer-events: none; | |
} | |
tbody tr { | |
transition: all 0.2s; | |
} | |
tbody tr:hover { | |
background: var(--surface-1); | |
transform: scale(1.01); | |
} | |
/* API端点动画 */ | |
.endpoint-card { | |
position: relative; | |
overflow: hidden; | |
} | |
.endpoint-card::after { | |
content: ''; | |
position: absolute; | |
top: -50%; | |
left: -50%; | |
width: 200%; | |
height: 200%; | |
background: linear-gradient( | |
45deg, | |
transparent, | |
rgba(255, 255, 255, 0.1), | |
transparent | |
); | |
transform: translateX(-100%); | |
transition: transform 0.3s; | |
} | |
.endpoint-card:hover::after { | |
transform: translateX(100%); | |
} | |
/* 主题切换按钮动画 */ | |
.theme-toggle { | |
position: fixed; | |
bottom: 2rem; | |
left: 2rem; | |
width: 48px; | |
height: 48px; | |
border-radius: 50%; | |
background: var(--card-bg); | |
border: none; | |
color: var(--text-color); | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transition: all 0.3s; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
} | |
.theme-toggle:hover { | |
transform: rotate(180deg); | |
background: var(--primary-color); | |
color: white; | |
} | |
/* 返回顶部按钮动画 */ | |
.back-to-top { | |
position: fixed; | |
bottom: 2rem; | |
right: 2rem; | |
width: 48px; | |
height: 48px; | |
border-radius: 50%; | |
background: var(--card-bg); | |
border: none; | |
color: var(--text-color); | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transition: all 0.3s; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
opacity: 0; | |
visibility: hidden; | |
transform: translateY(20px); | |
} | |
.back-to-top.visible { | |
opacity: 1; | |
visibility: visible; | |
transform: translateY(0); | |
} | |
.back-to-top:hover { | |
background: var(--primary-color); | |
color: white; | |
transform: translateY(-5px); | |
} | |
/* 响应式优化 */ | |
@media (max-width: 768px) { | |
.status-grid { | |
grid-template-columns: 1fr; | |
} | |
.endpoints-grid { | |
grid-template-columns: 1fr; | |
} | |
.navbar { | |
padding: 1rem; | |
} | |
.navbar .brand h1 { | |
font-size: 1.2rem; | |
} | |
.card { | |
margin: 1rem; | |
padding: 1rem; | |
} | |
.theme-toggle, | |
.back-to-top { | |
width: 40px; | |
height: 40px; | |
} | |
} | |
@media (max-width: 480px) { | |
.token-overview, | |
.points-overview { | |
flex-direction: column; | |
} | |
.table-container { | |
margin: 0 -1rem; | |
width: calc(100% + 2rem); | |
} | |
.health-status { | |
width: 100%; | |
justify-content: center; | |
} | |
} | |
/* 打印优化 */ | |
@media print { | |
.background-animation, | |
.loading-overlay, | |
.theme-toggle, | |
.back-to-top { | |
display: none ; | |
} | |
.card { | |
break-inside: avoid; | |
page-break-inside: avoid; | |
border: 1px solid #ddd; | |
margin: 1rem 0; | |
padding: 1rem; | |
} | |
.status-grid, | |
.endpoints-grid { | |
grid-template-columns: 1fr ; | |
} | |
.token-overview, | |
.points-overview { | |
flex-direction: column; | |
} | |
.table-container::after { | |
display: none; | |
} | |
} | |
</style> | |
</head> | |
<body class="loading"> | |
<!-- 跳转到主要内容的链接 --> | |
<a href="#main-content" class="skip-link">跳转到主要内容</a> | |
<!-- 页面加载动画 --> | |
<div class="loading-overlay" role="progressbar" aria-label="页面加载中"> | |
<div class="loader" aria-hidden="true"></div> | |
<p>加载中...</p> | |
</div> | |
<!-- 导航栏 --> | |
<nav class="navbar" role="navigation" aria-label="主导航"> | |
<div class="brand"> | |
<img src="/static/logo.png" alt="Abacus Chat Logo" class="logo" width="32" height="32"> | |
<h1>Abacus Chat代理仪表板</h1> | |
</div> | |
<div class="nav-actions"> | |
<button class="btn btn-secondary" onclick="location.href='/logout'" aria-label="退出登录"> | |
<span class="material-icons" aria-hidden="true">logout</span> | |
<span>退出登录</span> | |
</button> | |
</div> | |
</nav> | |
<!-- 主要内容 --> | |
<main id="main-content" class="dashboard-content" role="main"> | |
<!-- 系统状态卡片 --> | |
<section class="card" id="system-status" aria-labelledby="status-title"> | |
<div class="card-header"> | |
<h2 id="status-title">系统状态</h2> | |
<button class="btn-toggle" aria-expanded="true" aria-controls="status-content"> | |
<span class="sr-only">折叠系统状态</span> | |
<span class="toggle-icon" aria-hidden="true">▼</span> | |
</button> | |
</div> | |
<div id="status-content" class="card-body"> | |
<div class="status-grid" role="list"> | |
<div class="status-item" role="listitem"> | |
<span class="material-icons" aria-hidden="true">timer</span> | |
<div class="status-info"> | |
<h3>运行时间</h3> | |
<p class="uptime">{{ uptime }}</p> | |
</div> | |
</div> | |
<div class="status-item" role="listitem"> | |
<span class="material-icons" aria-hidden="true">health_and_safety</span> | |
<div class="status-info"> | |
<h3>健康状态</h3> | |
<div class="health-status" data-status="{{ health_status }}" role="status"> | |
<span class="status-indicator" aria-hidden="true"></span> | |
<span class="status-text">{{ health_status }}</span> | |
</div> | |
</div> | |
</div> | |
<div class="status-item" role="listitem"> | |
<span class="material-icons" aria-hidden="true">group</span> | |
<div class="status-info"> | |
<h3>用户数量</h3> | |
<p class="user-count" data-value="{{ user_count }}" aria-live="polite">{{ user_count }}</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</section> | |
<!-- Token使用统计卡片 --> | |
<section class="card" id="token-stats" aria-labelledby="token-title"> | |
<div class="card-header"> | |
<h2 id="token-title">Token使用统计</h2> | |
<button class="btn-toggle" aria-expanded="true" aria-controls="token-content"> | |
<span class="sr-only">折叠Token统计</span> | |
<span class="toggle-icon" aria-hidden="true">▼</span> | |
</button> | |
</div> | |
<div id="token-content" class="card-body"> | |
<div class="token-overview"> | |
<div class="token-total"> | |
<h3>总Token使用量</h3> | |
<p class="token-count" data-value="{{ total_tokens }}" aria-live="polite">{{ total_tokens }}</p> | |
<small class="token-note" role="note">*此数据仅包含代理使用的token,不包含Abacus网站使用的token。数据为粗略估计。</small> | |
</div> | |
<div class="token-breakdown"> | |
<h3>按模型统计</h3> | |
<div class="table-container"> | |
<table class="token-table" aria-label="Token使用明细"> | |
<thead> | |
<tr> | |
<th scope="col">模型</th> | |
<th scope="col">Token使用量</th> | |
<th scope="col">占比</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for model in token_stats %} | |
<tr> | |
<th scope="row">{{ model.name }}</th> | |
<td class="token-count" data-value="{{ model.tokens }}">{{ model.tokens }}</td> | |
<td>{{ model.percentage }}%</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
</div> | |
</section> | |
<!-- 计算点使用统计卡片 --> | |
<section class="card" id="compute-points" aria-labelledby="points-title"> | |
<div class="card-header"> | |
<h2 id="points-title">计算点使用统计</h2> | |
<button class="btn-toggle" aria-expanded="true" aria-controls="points-content"> | |
<span class="sr-only">折叠计算点统计</span> | |
<span class="toggle-icon" aria-hidden="true">▼</span> | |
</button> | |
</div> | |
<div id="points-content" class="card-body"> | |
<div class="points-overview"> | |
<div class="points-total"> | |
<h3>总计算点使用量</h3> | |
<p class="compute-points" data-value="{{ total_compute_points }}" aria-live="polite">{{ total_compute_points }}</p> | |
</div> | |
<div class="points-breakdown"> | |
<h3>使用记录</h3> | |
<div class="table-container"> | |
<table class="points-table" aria-label="计算点使用记录"> | |
<thead> | |
<tr> | |
<th scope="col">时间</th> | |
<th scope="col">计算点</th> | |
<th scope="col">模型</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for entry in compute_points_log %} | |
<tr> | |
<td>{{ entry.timestamp }}</td> | |
<td class="compute-points">{{ entry.points }}</td> | |
<td>{{ entry.model }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
<small class="points-note" role="note">*每小时更新一次</small> | |
</div> | |
</div> | |
</div> | |
</section> | |
<!-- API端点卡片 --> | |
<section class="card" id="api-endpoints" aria-labelledby="endpoints-title"> | |
<div class="card-header"> | |
<h2 id="endpoints-title">API端点</h2> | |
<button class="btn-toggle" aria-expanded="true" aria-controls="endpoints-content"> | |
<span class="sr-only">折叠API端点</span> | |
<span class="toggle-icon" aria-hidden="true">▼</span> | |
</button> | |
</div> | |
<div id="endpoints-content" class="card-body"> | |
<div class="endpoints-grid" role="list"> | |
{% for endpoint in api_endpoints %} | |
<div class="endpoint-card" role="listitem"> | |
<h3>{{ endpoint.name }}</h3> | |
<p class="api-endpoint" role="button" tabindex="0" aria-label="复制API端点: {{ endpoint.url }}">{{ endpoint.url }}</p> | |
<small class="endpoint-note" aria-hidden="true">点击复制</small> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</section> | |
</main> | |
<!-- 返回顶部按钮 --> | |
<button class="back-to-top" aria-label="返回页面顶部"> | |
<span class="material-icons" aria-hidden="true">arrow_upward</span> | |
</button> | |
<!-- 主题切换按钮 --> | |
<button class="theme-toggle" aria-label="切换深色/浅色主题"> | |
<!-- 添加页面加载器到body --> | |
<div class="page-loader"> | |
<div class="loader"></div> | |
</div> | |
<!-- 添加加载覆盖层到每个卡片 --> | |
<div class="loading-overlay"> | |
<div class="loading-spinner"></div> | |
</div> | |
<script> | |
// 复制到剪贴板 | |
function copyToClipboard(element) { | |
const text = element.textContent; | |
navigator.clipboard.writeText(text).then(() => { | |
const originalText = element.textContent; | |
element.textContent = '已复制!'; | |
element.style.color = 'var(--success)'; | |
setTimeout(() => { | |
element.textContent = originalText; | |
element.style.color = ''; | |
}, 1500); | |
}); | |
} | |
// 切换模型显示 | |
function toggleModels(button) { | |
const hiddenModels = document.querySelectorAll('.hidden-model'); | |
const textSpan = button.querySelector('.text'); | |
const iconSpan = button.querySelector('.icon'); | |
hiddenModels.forEach(model => { | |
model.classList.toggle('show'); | |
}); | |
button.classList.toggle('expanded'); | |
textSpan.textContent = button.classList.contains('expanded') ? '隐藏部分' : '显示全部'; | |
} | |
// 返回顶部按钮 | |
const backToTopButton = document.querySelector('.back-to-top'); | |
window.onscroll = function() { | |
if (document.body.scrollTop > 500 || document.documentElement.scrollTop > 500) { | |
backToTopButton.classList.add('visible'); | |
} else { | |
backToTopButton.classList.remove('visible'); | |
} | |
}; | |
function scrollToTop() { | |
window.scrollTo({ | |
top: 0, | |
behavior: 'smooth' | |
}); | |
} | |
// 健康状态动画 | |
const healthStatus = document.querySelector('.health-status'); | |
if (healthStatus) { | |
setInterval(() => { | |
healthStatus.style.animation = 'none'; | |
healthStatus.offsetHeight; // 触发重排 | |
healthStatus.style.animation = 'pulse 2s infinite'; | |
}, 2000); | |
} | |
// 页面加载完成后隐藏加载器 | |
window.addEventListener('load', () => { | |
const pageLoader = document.querySelector('.page-loader'); | |
pageLoader.classList.add('hidden'); | |
}); | |
// 数据加载状态模拟 | |
function showLoading(element) { | |
const overlay = element.querySelector('.loading-overlay'); | |
if (overlay) { | |
overlay.classList.add('visible'); | |
} | |
} | |
function hideLoading(element) { | |
const overlay = element.querySelector('.loading-overlay'); | |
if (overlay) { | |
overlay.classList.remove('visible'); | |
} | |
} | |
// 示例:模拟数据加载 | |
document.querySelectorAll('.card').forEach(card => { | |
showLoading(card); | |
setTimeout(() => { | |
hideLoading(card); | |
}, Math.random() * 1000 + 500); // 随机延迟以模拟不同加载时间 | |
}); | |
// 表格搜索功能 | |
function initTableSearch() { | |
document.querySelectorAll('table').forEach(table => { | |
const wrapper = document.createElement('div'); | |
wrapper.className = 'table-container'; | |
table.parentNode.insertBefore(wrapper, table); | |
wrapper.appendChild(table); | |
const search = document.createElement('input'); | |
search.type = 'text'; | |
search.className = 'table-search'; | |
search.placeholder = '搜索表格内容...'; | |
wrapper.insertBefore(search, table); | |
search.addEventListener('input', e => { | |
const searchText = e.target.value.toLowerCase(); | |
const rows = table.querySelectorAll('tbody tr'); | |
rows.forEach(row => { | |
const text = row.textContent.toLowerCase(); | |
row.style.display = text.includes(searchText) ? '' : 'none'; | |
}); | |
}); | |
}); | |
} | |
// 卡片折叠功能 | |
function initCardCollapse() { | |
document.querySelectorAll('.card .card-header').forEach(header => { | |
if (!header.querySelector('.btn-toggle')) { | |
const btn = document.createElement('button'); | |
btn.className = 'btn-toggle'; | |
btn.innerHTML = '<span class="sr-only">折叠卡片</span>▼'; | |
header.appendChild(btn); | |
btn.addEventListener('click', () => { | |
const card = header.closest('.card'); | |
card.classList.toggle('collapsed'); | |
btn.innerHTML = card.classList.contains('collapsed') ? | |
'<span class="sr-only">展开卡片</span>▶' : | |
'<span class="sr-only">折叠卡片</span>▼'; | |
}); | |
} | |
}); | |
} | |
// API端点点击复制 | |
function initApiEndpointCopy() { | |
document.querySelectorAll('.api-endpoint').forEach(endpoint => { | |
endpoint.style.cursor = 'pointer'; | |
endpoint.setAttribute('role', 'button'); | |
endpoint.setAttribute('tabindex', '0'); | |
endpoint.addEventListener('click', async () => { | |
try { | |
await navigator.clipboard.writeText(endpoint.textContent); | |
showNotification('API端点已复制到剪贴板', 'success'); | |
} catch (err) { | |
showNotification('复制失败,请手动复制', 'error'); | |
} | |
}); | |
endpoint.addEventListener('keydown', e => { | |
if (e.key === 'Enter' || e.key === ' ') { | |
e.preventDefault(); | |
endpoint.click(); | |
} | |
}); | |
}); | |
} | |
// 健康检查状态动画 | |
function updateHealthStatus(status) { | |
const healthIndicator = document.querySelector('.health-status'); | |
if (!healthIndicator) return; | |
const oldStatus = healthIndicator.getAttribute('data-status'); | |
if (oldStatus === status) return; | |
healthIndicator.setAttribute('data-status', status); | |
healthIndicator.classList.add('animate'); | |
setTimeout(() => { | |
healthIndicator.classList.remove('animate'); | |
}, 1000); | |
} | |
// 数字动画 | |
function animateValue(element, start, end, duration) { | |
if (start === end) return; | |
const range = end - start; | |
const startTime = performance.now(); | |
function update(currentTime) { | |
const elapsed = currentTime - startTime; | |
const progress = Math.min(elapsed / duration, 1); | |
const value = Math.floor(start + range * progress); | |
element.textContent = new Intl.NumberFormat().format(value); | |
if (progress < 1) { | |
requestAnimationFrame(update); | |
} | |
} | |
requestAnimationFrame(update); | |
} | |
// 初始化所有功能 | |
document.addEventListener('DOMContentLoaded', () => { | |
initTableSearch(); | |
initCardCollapse(); | |
initApiEndpointCopy(); | |
// 初始化数字动画 | |
document.querySelectorAll('[data-value]').forEach(element => { | |
const value = parseInt(element.getAttribute('data-value')); | |
if (!isNaN(value)) { | |
animateValue(element, 0, value, 1000); | |
} | |
}); | |
// 初始化加载状态 | |
document.body.classList.remove('loading'); | |
// 显示欢迎通知 | |
setTimeout(() => { | |
showNotification('欢迎使用Abacus Chat代理仪表板', 'success'); | |
}, 1000); | |
}); | |
// 定期更新健康状态 | |
setInterval(async () => { | |
try { | |
const response = await fetch('/health'); | |
const data = await response.json(); | |
updateHealthStatus(data.status); | |
} catch (err) { | |
console.error('健康检查更新失败:', err); | |
} | |
}, 60000); | |
// 自动隐藏通知 | |
document.addEventListener('click', e => { | |
if (e.target.closest('.notification')) { | |
e.target.closest('.notification').classList.remove('show'); | |
} | |
}); | |
// 键盘导航支持 | |
document.addEventListener('keydown', e => { | |
if (e.key === 'Escape') { | |
const modal = document.querySelector('.modal.show'); | |
if (modal) { | |
modal.classList.remove('show'); | |
} | |
const notifications = document.querySelectorAll('.notification.show'); | |
notifications.forEach(notification => { | |
notification.classList.remove('show'); | |
}); | |
} | |
}); | |
// 无障碍支持 | |
class A11yManager { | |
constructor() { | |
this.focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; | |
this.init(); | |
} | |
init() { | |
this.setupFocusTrap(); | |
this.setupKeyboardNavigation(); | |
this.setupSkipLink(); | |
this.setupAnnouncer(); | |
} | |
setupFocusTrap() { | |
document.addEventListener('keydown', (e) => { | |
if (e.key === 'Tab') { | |
const modal = document.querySelector('.modal.show'); | |
if (!modal) return; | |
const focusableElements = modal.querySelectorAll(this.focusableElements); | |
const firstFocusable = focusableElements[0]; | |
const lastFocusable = focusableElements[focusableElements.length - 1]; | |
if (e.shiftKey && document.activeElement === firstFocusable) { | |
e.preventDefault(); | |
lastFocusable.focus(); | |
} else if (!e.shiftKey && document.activeElement === lastFocusable) { | |
e.preventDefault(); | |
firstFocusable.focus(); | |
} | |
} | |
}); | |
} | |
setupKeyboardNavigation() { | |
document.addEventListener('keydown', (e) => { | |
if (e.key === 'Escape') { | |
const modal = document.querySelector('.modal.show'); | |
if (modal) { | |
this.closeModal(modal); | |
} | |
const notifications = document.querySelectorAll('.notification.show'); | |
notifications.forEach(notification => { | |
this.closeNotification(notification); | |
}); | |
} | |
}); | |
} | |
setupSkipLink() { | |
const skipLink = document.querySelector('.skip-link'); | |
if (!skipLink) return; | |
skipLink.addEventListener('click', (e) => { | |
e.preventDefault(); | |
const target = document.querySelector(skipLink.getAttribute('href')); | |
if (target) { | |
target.setAttribute('tabindex', '-1'); | |
target.focus(); | |
} | |
}); | |
} | |
setupAnnouncer() { | |
const announcer = document.createElement('div'); | |
announcer.setAttribute('aria-live', 'polite'); | |
announcer.setAttribute('aria-atomic', 'true'); | |
announcer.classList.add('sr-only'); | |
document.body.appendChild(announcer); | |
this.announcer = announcer; | |
} | |
announce(message) { | |
if (!this.announcer) return; | |
this.announcer.textContent = message; | |
} | |
closeModal(modal) { | |
modal.classList.remove('show'); | |
this.announce('模态框已关闭'); | |
} | |
closeNotification(notification) { | |
notification.classList.remove('show'); | |
this.announce('通知已关闭'); | |
} | |
} | |
// 主题管理 | |
class ThemeManager { | |
constructor() { | |
this.init(); | |
} | |
init() { | |
this.setupThemeToggle(); | |
this.loadSavedTheme(); | |
this.setupSystemThemeListener(); | |
} | |
setupThemeToggle() { | |
const themeToggle = document.querySelector('.theme-toggle'); | |
if (!themeToggle) return; | |
themeToggle.addEventListener('click', () => { | |
const currentTheme = document.documentElement.getAttribute('data-theme'); | |
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; | |
this.setTheme(newTheme); | |
this.announce(`已切换到${newTheme === 'dark' ? '深色' : '浅色'}主题`); | |
}); | |
} | |
loadSavedTheme() { | |
const savedTheme = localStorage.getItem('theme'); | |
if (savedTheme) { | |
this.setTheme(savedTheme); | |
} else { | |
this.setTheme(this.getSystemTheme()); | |
} | |
} | |
setupSystemThemeListener() { | |
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); | |
mediaQuery.addListener((e) => { | |
if (!localStorage.getItem('theme')) { | |
this.setTheme(e.matches ? 'dark' : 'light'); | |
} | |
}); | |
} | |
getSystemTheme() { | |
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; | |
} | |
setTheme(theme) { | |
document.documentElement.setAttribute('data-theme', theme); | |
localStorage.setItem('theme', theme); | |
} | |
announce(message) { | |
const announcer = document.querySelector('[aria-live="polite"]'); | |
if (announcer) { | |
announcer.textContent = message; | |
} | |
} | |
} | |
// 数据管理 | |
class DataManager { | |
constructor() { | |
this.init(); | |
} | |
init() { | |
this.setupTableSearch(); | |
this.setupTableSort(); | |
this.setupDataRefresh(); | |
} | |
setupTableSearch() { | |
document.querySelectorAll('.table-container').forEach(container => { | |
const table = container.querySelector('table'); | |
if (!table) return; | |
const search = document.createElement('input'); | |
search.type = 'text'; | |
search.className = 'table-search'; | |
search.placeholder = '搜索表格内容...'; | |
search.setAttribute('aria-label', '搜索表格内容'); | |
container.insertBefore(search, table); | |
search.addEventListener('input', (e) => { | |
const searchText = e.target.value.toLowerCase(); | |
const rows = table.querySelectorAll('tbody tr'); | |
rows.forEach(row => { | |
const text = row.textContent.toLowerCase(); | |
const display = text.includes(searchText) ? '' : 'none'; | |
row.style.display = display; | |
row.setAttribute('aria-hidden', display === 'none'); | |
}); | |
this.announce(`找到 ${Array.from(rows).filter(row => row.style.display !== 'none').length} 条匹配记录`); | |
}); | |
}); | |
} | |
setupTableSort() { | |
document.querySelectorAll('table th').forEach(th => { | |
if (th.getAttribute('data-sortable') === 'false') return; | |
th.style.cursor = 'pointer'; | |
th.setAttribute('role', 'button'); | |
th.setAttribute('aria-sort', 'none'); | |
th.addEventListener('click', () => { | |
const table = th.closest('table'); | |
const tbody = table.querySelector('tbody'); | |
const rows = Array.from(tbody.querySelectorAll('tr')); | |
const index = Array.from(th.parentNode.children).indexOf(th); | |
const direction = th.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending'; | |
// 重置其他列的排序状态 | |
th.parentNode.querySelectorAll('th').forEach(header => { | |
header.setAttribute('aria-sort', 'none'); | |
}); | |
th.setAttribute('aria-sort', direction); | |
const sortedRows = rows.sort((a, b) => { | |
const aValue = a.children[index].textContent; | |
const bValue = b.children[index].textContent; | |
if (this.isNumeric(aValue) && this.isNumeric(bValue)) { | |
return direction === 'ascending' ? | |
this.parseNumber(aValue) - this.parseNumber(bValue) : | |
this.parseNumber(bValue) - this.parseNumber(aValue); | |
} | |
return direction === 'ascending' ? | |
aValue.localeCompare(bValue) : | |
bValue.localeCompare(aValue); | |
}); | |
tbody.append(...sortedRows); | |
this.announce(`表格已按${th.textContent}${direction === 'ascending' ? '升序' : '降序'}排序`); | |
}); | |
}); | |
} | |
setupDataRefresh() { | |
setInterval(async () => { | |
try { | |
const response = await fetch('/api/dashboard/data'); | |
const data = await response.json(); | |
this.updateDashboard(data); | |
} catch (err) { | |
console.error('数据刷新失败:', err); | |
} | |
}, 60000); // 每分钟更新一次 | |
} | |
updateDashboard(data) { | |
// 更新系统状态 | |
if (data.uptime) { | |
document.querySelector('.uptime').textContent = data.uptime; | |
} | |
if (data.health_status) { | |
const healthStatus = document.querySelector('.health-status'); | |
healthStatus.setAttribute('data-status', data.health_status); | |
healthStatus.querySelector('.status-text').textContent = data.health_status; | |
} | |
if (data.user_count) { | |
const userCount = document.querySelector('.user-count'); | |
this.animateNumber(userCount, parseInt(userCount.textContent), data.user_count); | |
} | |
// 更新Token统计 | |
if (data.total_tokens) { | |
const tokenCount = document.querySelector('.token-count'); | |
this.animateNumber(tokenCount, parseInt(tokenCount.textContent), data.total_tokens); | |
} | |
// 更新计算点统计 | |
if (data.compute_points) { | |
const computePoints = document.querySelector('.compute-points'); | |
this.animateNumber(computePoints, parseInt(computePoints.textContent), data.compute_points); | |
} | |
this.announce('仪表板数据已更新'); | |
} | |
animateNumber(element, start, end) { | |
if (start === end) return; | |
const duration = 1000; | |
const startTime = performance.now(); | |
const range = end - start; | |
const update = (currentTime) => { | |
const elapsed = currentTime - startTime; | |
const progress = Math.min(elapsed / duration, 1); | |
const current = Math.floor(start + range * progress); | |
element.textContent = new Intl.NumberFormat().format(current); | |
if (progress < 1) { | |
requestAnimationFrame(update); | |
} | |
}; | |
requestAnimationFrame(update); | |
} | |
isNumeric(value) { | |
return !isNaN(this.parseNumber(value)); | |
} | |
parseNumber(value) { | |
return parseFloat(value.replace(/[^0-9.-]+/g, '')); | |
} | |
announce(message) { | |
const announcer = document.querySelector('[aria-live="polite"]'); | |
if (announcer) { | |
announcer.textContent = message; | |
} | |
} | |
} | |
// 初始化 | |
document.addEventListener('DOMContentLoaded', () => { | |
const a11y = new A11yManager(); | |
const theme = new ThemeManager(); | |
const data = new DataManager(); | |
// 移除加载状态 | |
document.body.classList.remove('loading'); | |
}); | |
</script> | |
</body> | |
</html> |