Spaces:
Paused
Paused
| <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; | |
| } | |
| } | |
| /* 跳转链接样式 */ | |
| .skip-link { | |
| position: absolute; | |
| top: -40px; | |
| left: 0; | |
| background: var(--primary-color); | |
| color: white; | |
| padding: 8px 16px; | |
| z-index: 100; | |
| transition: top 0.2s; | |
| } | |
| .skip-link:focus { | |
| top: 0; | |
| } | |
| /* 加载动画优化 */ | |
| .loading-overlay { | |
| backdrop-filter: blur(8px); | |
| -webkit-backdrop-filter: blur(8px); | |
| } | |
| .loader { | |
| position: relative; | |
| } | |
| .loader::before { | |
| content: ''; | |
| position: absolute; | |
| inset: -8px; | |
| border-radius: 50%; | |
| background: conic-gradient( | |
| from 0deg, | |
| transparent 0%, | |
| var(--primary-color) 25%, | |
| transparent 100% | |
| ); | |
| animation: pulse 2s linear infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| transform: scale(1); | |
| opacity: 0.5; | |
| } | |
| 50% { | |
| transform: scale(1.2); | |
| opacity: 0.2; | |
| } | |
| 100% { | |
| transform: scale(1); | |
| opacity: 0.5; | |
| } | |
| } | |
| /* 卡片内容动画 */ | |
| .card-body { | |
| transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| overflow: hidden; | |
| } | |
| .card.collapsed .card-body { | |
| max-height: 0; | |
| } | |
| /* 表格优化 */ | |
| .table-container { | |
| position: relative; | |
| border-radius: 8px; | |
| background: var(--surface-1); | |
| padding: 1rem; | |
| margin: 1rem 0; | |
| } | |
| .table-search { | |
| margin: -0.5rem -0.5rem 0.5rem -0.5rem; | |
| width: calc(100% + 1rem); | |
| border-radius: 8px 8px 0 0; | |
| border-bottom: 1px solid var(--surface-2); | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: separate; | |
| border-spacing: 0; | |
| } | |
| th, td { | |
| padding: 0.75rem; | |
| text-align: left; | |
| border-bottom: 1px solid var(--surface-2); | |
| } | |
| th { | |
| font-weight: 600; | |
| color: var(--text-light); | |
| position: relative; | |
| user-select: none; | |
| } | |
| th[aria-sort]:not([aria-sort="none"])::after { | |
| content: ''; | |
| position: absolute; | |
| right: 0.5rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 0; | |
| height: 0; | |
| border-left: 4px solid transparent; | |
| border-right: 4px solid transparent; | |
| } | |
| th[aria-sort="ascending"]::after { | |
| border-bottom: 4px solid var(--primary-color); | |
| } | |
| th[aria-sort="descending"]::after { | |
| border-top: 4px solid var(--primary-color); | |
| } | |
| tbody tr { | |
| transition: all 0.2s; | |
| } | |
| tbody tr:hover { | |
| background: var(--surface-2); | |
| } | |
| /* 状态指示器优化 */ | |
| .status-indicator { | |
| position: relative; | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| margin-right: 0.5rem; | |
| } | |
| .status-indicator::before { | |
| content: ''; | |
| position: absolute; | |
| inset: -4px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| opacity: 0.2; | |
| animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; | |
| } | |
| /* 按钮优化 */ | |
| .btn { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 5px; | |
| height: 5px; | |
| background: rgba(255, 255, 255, 0.5); | |
| opacity: 0; | |
| border-radius: 100%; | |
| transform: scale(1, 1) translate(-50%); | |
| transform-origin: 50% 50%; | |
| } | |
| .btn:focus:not(:active)::after { | |
| animation: ripple 1s ease-out; | |
| } | |
| @keyframes ripple { | |
| 0% { | |
| transform: scale(0, 0); | |
| opacity: 0.5; | |
| } | |
| 20% { | |
| transform: scale(25, 25); | |
| opacity: 0.5; | |
| } | |
| 100% { | |
| opacity: 0; | |
| transform: scale(40, 40); | |
| } | |
| } | |
| /* 主题切换按钮优化 */ | |
| .theme-toggle { | |
| overflow: hidden; | |
| } | |
| .theme-toggle .material-icons { | |
| transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .theme-toggle:hover .material-icons { | |
| transform: rotate(180deg); | |
| } | |
| [data-theme="dark"] .theme-toggle .material-icons { | |
| transform: rotate(180deg); | |
| } | |
| /* 响应式优化 */ | |
| @media (max-width: 768px) { | |
| .card { | |
| margin: 0.5rem; | |
| } | |
| .table-container { | |
| margin: 0.5rem 0; | |
| padding: 0.5rem; | |
| } | |
| th, td { | |
| padding: 0.5rem; | |
| } | |
| .status-grid { | |
| gap: 0.5rem; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .navbar { | |
| flex-direction: column; | |
| text-align: center; | |
| padding: 0.5rem; | |
| } | |
| .nav-actions { | |
| margin-top: 0.5rem; | |
| } | |
| .card-header { | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .btn-toggle { | |
| width: 100%; | |
| } | |
| } | |
| /* 打印优化 */ | |
| @media print { | |
| .card { | |
| break-inside: avoid; | |
| page-break-inside: avoid; | |
| border: 1px solid #ddd; | |
| margin: 1rem 0; | |
| padding: 1rem; | |
| box-shadow: none; | |
| } | |
| .table-container { | |
| overflow: visible; | |
| } | |
| .table-container::after { | |
| display: none; | |
| } | |
| th, td { | |
| border-bottom: 1px solid #ddd; | |
| } | |
| .status-indicator::before { | |
| 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> |