abacus_chat_proxy / templates /dashboard.html
malt666's picture
Upload 6 files
61d7903 verified
raw
history blame
69.2 kB
<!DOCTYPE html>
<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 !important;
}
.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 !important;
}
.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>