diff --git a/src/coding/proxy/server/dashboard.py b/src/coding/proxy/server/dashboard.py index dbd46a5..7581986 100644 --- a/src/coding/proxy/server/dashboard.py +++ b/src/coding/proxy/server/dashboard.py @@ -457,6 +457,34 @@ def _build_favicon() -> bytes: margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border-subtle); font-weight: 500; font-size: 12px; color: var(--text-secondary); } + /* ── Tabs ─────────────────────────────────────────────────── */ + .tabs { + display: flex; + gap: 4px; + margin-bottom: 16px; + border-bottom: 1px solid var(--border); + padding: 0 2px; + } + .tab-btn { + appearance: none; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: var(--text-secondary); + cursor: pointer; + font-family: inherit; + font-size: 14px; + font-weight: 500; + padding: 10px 16px; + margin-bottom: -1px; + transition: color .15s ease, border-color .15s ease, background .15s ease; + border-radius: 6px 6px 0 0; + } + .tab-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); } + .tab-btn.active { color: var(--text-primary); border-bottom-color: var(--accent-blue); } + .tab-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; } + .tab-pane { display: none; } + .tab-pane.active { display: block; } @@ -473,6 +501,14 @@ def _build_favicon() -> bytes:
+ + + + +
时间区间 @@ -562,7 +598,10 @@ def _build_favicon() -> bytes:
+
+ +
@@ -590,6 +629,7 @@ def _build_favicon() -> bytes:
+
@@ -1390,48 +1430,119 @@ def _build_favicon() -> bytes: } } -// ── 主刷新逻辑 ──────────────────────────────────────────── +// ── 主刷新逻辑(按 Tab 分发) ────────────────────────────── let refreshing = false; +let currentTab = 'overview'; +const tabLoaded = { overview: false, sessions: false }; +const TAB_LABELS = { overview: 'Overview', sessions: 'Recent Active Sessions' }; + +async function refreshOverview() { + const days = currentDays > 0 ? currentDays : 7; + const [summary, timeline, status] = await Promise.all([ + fetchJSON('/api/dashboard/summary?days=' + days), + fetchJSON('/api/dashboard/timeline?days=' + days), + fetchJSON('/api/status'), + ]); + + if (summary.version) { + document.getElementById('version-badge').textContent = 'v' + summary.version; + } + + updateKPI(summary); + updateVendorStatus(status); + updateChartTitles(days); + + const rows = timeline.rows || []; + const tierOrder = (status.tiers || []).map(t => t.name); + buildTimeline(rows, tierOrder); + buildVendorDist(rows, tierOrder); + buildTokenTimeline(rows, tierOrder); + buildModelTokenTimeline(rows); +} + +async function refreshSessions() { + await updateSessions(); +} + async function refresh() { if (refreshing) return; refreshing = true; - document.getElementById('refresh-time').textContent = '刷新中…'; try { - const days = currentDays > 0 ? currentDays : 7; - const [summary, timeline, status] = await Promise.all([ - fetchJSON('/api/dashboard/summary?days=' + days), - fetchJSON('/api/dashboard/timeline?days=' + days), - fetchJSON('/api/status'), - ]); - - if (summary.version) { - document.getElementById('version-badge').textContent = 'v' + summary.version; + // 循环:若 await 期间用户切到了尚未加载的另一页签,补一次刷新,避免 tabLoaded 错位。 + while (true) { + const tab = currentTab; + document.getElementById('refresh-time').textContent = '刷新中…'; + try { + if (tab === 'sessions') { + await refreshSessions(); + } else { + await refreshOverview(); + } + tabLoaded[tab] = true; + if (tab === currentTab) { + document.getElementById('refresh-time').textContent = + '上次刷新: ' + now() + '(' + TAB_LABELS[tab] + ')'; + } + } catch (e) { + console.error('Dashboard refresh error:', e); + document.getElementById('refresh-time').textContent = '刷新失败 ' + now(); + } + if (currentTab !== tab && !tabLoaded[currentTab]) continue; + break; } + } finally { + refreshing = false; + } +} - updateKPI(summary); - updateVendorStatus(status); - updateChartTitles(days); +// ── 页签切换(懒加载 + URL 同步) ───────────────────────── +function syncTabUrl(name) { + try { + const url = new URL(window.location.href); + if (url.searchParams.get('tab') === name) return; + url.searchParams.set('tab', name); + window.history.replaceState({}, '', url); + } catch (e) { /* no-op */ } +} - const rows = timeline.rows || []; - const tierOrder = (status.tiers || []).map(t => t.name); - buildTimeline(rows, tierOrder); - buildVendorDist(rows, tierOrder); - buildTokenTimeline(rows, tierOrder); - buildModelTokenTimeline(rows); - updateSessions(); +function applyTabState(name) { + document.querySelectorAll('.tab-btn').forEach(function (b) { + const active = b.getAttribute('data-tab') === name; + b.classList.toggle('active', active); + b.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + document.querySelectorAll('.tab-pane').forEach(function (p) { + p.classList.toggle('active', p.getAttribute('data-tab') === name); + }); +} - document.getElementById('refresh-time').textContent = '上次刷新: ' + now(); - } catch (e) { - console.error('Dashboard refresh error:', e); - document.getElementById('refresh-time').textContent = '刷新失败 ' + now(); - } finally { - refreshing = false; +function switchTab(name) { + if (name !== 'overview' && name !== 'sessions') name = 'overview'; + if (name === currentTab) { + syncTabUrl(name); + return; + } + currentTab = name; + applyTabState(name); + syncTabUrl(name); + if (!tabLoaded[name]) { + refresh(); } } -// 页面加载 + 每 30 秒自动刷新 -refresh(); -setInterval(refresh, 600000); +// ── 初始化 ──────────────────────────────────────────────── +(function bootstrap() { + let initial = 'overview'; + try { + const t = new URL(window.location.href).searchParams.get('tab'); + if (t === 'sessions') initial = 'sessions'; + } catch (e) { /* no-op */ } + currentTab = initial; + applyTabState(initial); + syncTabUrl(initial); + refresh(); // 仅加载初始页签的数据 + setInterval(refresh, 600000); // 每 10 分钟刷新当前页签 +})();