/** * DeepSeek2API Neo · Material Design 3 Dashboard */ const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); // ======== 格式化工具 ======== const fmtNum = (n) => { if (n == null || isNaN(n)) return '--'; if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B'; if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; if (n >= 1e4) return (n / 1e3).toFixed(1) + 'K'; return String(n); }; const fmtTime = (ts) => { const d = new Date(ts * 1000); return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); }; // ======== 主题色 ======== function themeColors() { const s = getComputedStyle(document.documentElement); return { primary: s.getPropertyValue('--md-sys-color-primary').trim(), secondary: s.getPropertyValue('--md-sys-color-secondary').trim(), error: s.getPropertyValue('--md-sys-color-error').trim(), surfaceVar: s.getPropertyValue('--md-sys-color-surface-variant').trim(), onSurface: s.getPropertyValue('--md-sys-color-on-surface').trim(), }; } // Input/output 专用色 const COLOR_IN_FLASH = '#1565C0'; // 蓝色系 - 输入 const COLOR_OUT_FLASH = '#42A5F5'; // 浅蓝系 - 输出 const COLOR_IN_PRO = '#C62828'; // 红色系 - 输入 const COLOR_OUT_PRO = '#EF5350'; // 浅红系 - 输出 // ======== 全局状态 ======== let period = 0; let chartTokens = null; let chartRequests = null; // ======== API ======== async function fetchJSON(path) { const resp = await fetch(path); if (!resp.ok) throw new Error(`${path} returned ${resp.status}`); return resp.json(); } // ======== 概览卡片 ======== async function updateOverview() { try { const [ov, models] = await Promise.all([ fetchJSON(`/api/stats/overview?days=${period}`), fetchJSON(`/api/stats/models?days=${period}`), ]); $('#ovRequests').textContent = fmtNum(ov.total_requests); $('#ovTokens').textContent = fmtNum(ov.total_tokens); // 总体输入/输出 const totalIn = models.reduce((s, m) => s + (m.prompt_tokens || 0), 0); const totalOut = models.reduce((s, m) => s + (m.completion_tokens || 0), 0); $('#ovInOut').innerHTML = totalIn + totalOut > 0 ? `入 ${fmtNum(totalIn)}出 ${fmtNum(totalOut)}` : ''; const flash = models.find(m => m.model_display === 'DeepSeek-V4-Flash') || { request_count:0, prompt_tokens:0, completion_tokens:0, total_tokens:0 }; const pro = models.find(m => m.model_display === 'DeepSeek-V4-Pro') || { request_count:0, prompt_tokens:0, completion_tokens:0, total_tokens:0 }; $('#ovFlashReq').textContent = fmtNum(flash.request_count); $('#ovFlashInOut').innerHTML = (flash.prompt_tokens + flash.completion_tokens) > 0 ? `入 ${fmtNum(flash.prompt_tokens)}出 ${fmtNum(flash.completion_tokens)}` : ''; $('#ovProReq').textContent = fmtNum(pro.request_count); $('#ovProInOut').innerHTML = (pro.prompt_tokens + pro.completion_tokens) > 0 ? `入 ${fmtNum(pro.prompt_tokens)}出 ${fmtNum(pro.completion_tokens)}` : ''; } catch (e) { console.error('[updateOverview]', e); } } // ======== 图表 ======== async function updateCharts() { try { const dailyData = await fetchJSON('/api/stats/daily?days=30'); // 始终生成最近30天(含今天),无数据则填0 const allDates = []; for (let i = 29; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); allDates.push(d.toISOString().slice(0, 10)); } const dateMap = {}; for (const d of allDates) dateMap[d] = {}; for (const row of dailyData) { if (dateMap[row.date]) dateMap[row.date][row.model_display] = row; } const sortedDates = allDates; const labels = sortedDates.map(d => d.slice(5)); // 输入/输出拆分 const flashIn = sortedDates.map(d => (dateMap[d]['DeepSeek-V4-Flash'] || {}).prompt_tokens || 0); const flashOut = sortedDates.map(d => (dateMap[d]['DeepSeek-V4-Flash'] || {}).completion_tokens || 0); const proIn = sortedDates.map(d => (dateMap[d]['DeepSeek-V4-Pro'] || {}).prompt_tokens || 0); const proOut = sortedDates.map(d => (dateMap[d]['DeepSeek-V4-Pro'] || {}).completion_tokens || 0); const flashReqs = sortedDates.map(d => (dateMap[d]['DeepSeek-V4-Flash'] || {}).request_count || 0); const proReqs = sortedDates.map(d => (dateMap[d]['DeepSeek-V4-Pro'] || {}).request_count || 0); const tc = themeColors(); const chartOpts = (yLabel) => ({ responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { x: { grid: { color: tc.surfaceVar + '80' }, ticks: { color: tc.onSurface, maxTicksLimit: 10, font: { size: 11 } }, }, y: { beginAtZero: true, stacked: true, title: { display: true, text: yLabel, color: tc.onSurface }, grid: { color: tc.surfaceVar + '80' }, ticks: { color: tc.onSurface, callback: v => fmtNum(v), font: { size: 11 } }, }, }, plugins: { legend: { position: 'bottom', labels: { color: tc.onSurface, usePointStyle: true, padding: 16, font: { size: 11 } }, }, tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${fmtNum(ctx.raw)}`, }, }, }, }); // Token 图表 —— 堆叠柱状图:4 系列(输入/输出 × Flash/Pro) if (chartTokens) chartTokens.destroy(); chartTokens = new Chart($('#chartTokens'), { type: 'bar', data: { labels, datasets: [ { label: 'Flash 输入', data: flashIn, backgroundColor: COLOR_IN_FLASH, stack: 'flash', borderWidth: 0, borderRadius: 0 }, { label: 'Flash 输出', data: flashOut, backgroundColor: COLOR_OUT_FLASH, stack: 'flash', borderWidth: 0, borderRadius: 4 }, { label: 'Pro 输入', data: proIn, backgroundColor: COLOR_IN_PRO, stack: 'pro', borderWidth: 0, borderRadius: 0 }, { label: 'Pro 输出', data: proOut, backgroundColor: COLOR_OUT_PRO, stack: 'pro', borderWidth: 0, borderRadius: 4 }, ], }, options: chartOpts('Token'), }); // 请求数图表 if (chartRequests) chartRequests.destroy(); chartRequests = new Chart($('#chartRequests'), { type: 'line', data: { labels, datasets: [ { label: 'V4-Flash', data: flashReqs, borderColor: COLOR_IN_FLASH, backgroundColor: COLOR_IN_FLASH + '33', fill: true, tension: 0.3, pointRadius: 2, pointHoverRadius: 5, }, { label: 'V4-Pro', data: proReqs, borderColor: COLOR_IN_PRO, backgroundColor: COLOR_IN_PRO + '33', fill: true, tension: 0.3, pointRadius: 2, pointHoverRadius: 5, }, ], }, options: { ...chartOpts('请求数'), scales: { ...chartOpts('请求数').scales, y: { ...chartOpts('请求数').scales.y, stacked: false } } }, }); } catch (e) { console.error('[updateCharts]', e); } } // ======== 账号表格 — 按模型分开展示 ======== function renderAccountTable(tbodyId, modelDisplay, data) { const tbody = $(`#${tbodyId}`); const filtered = data.filter(r => r.model_display === modelDisplay); if (!filtered.length) { tbody.innerHTML = `