mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 17:07:59 +00:00
fix(ui): status block = pie chart (no labels) with color legend below
- rewrite drawStatusChart to a Canvas pie (no per-slice labels), center shows total requests - status card: title + total caption + tall pie area (110px) + color legend row below (swatch + code + count), each item has title tooltip with ok/err - store status swatch palette and reuse across pie/legend for color consistency - deployed (bak .bak.20260811m), server active
This commit is contained in:
@ -110,6 +110,12 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
|
|||||||
.kpi .k-sub { font-size:11.5px; color:var(--muted); margin-top:4px; }
|
.kpi .k-sub { font-size:11.5px; color:var(--muted); margin-top:4px; }
|
||||||
.kpi .k-chart { margin-top:8px; position:relative; height:86px; }
|
.kpi .k-chart { margin-top:8px; position:relative; height:86px; }
|
||||||
.kpi canvas { width:100%; height:86px; display:block; }
|
.kpi canvas { width:100%; height:86px; display:block; }
|
||||||
|
.kpi.kpi-status .k-chart-pie { height:110px; }
|
||||||
|
.kpi.kpi-status canvas { height:110px; }
|
||||||
|
.status-legend { display:flex; flex-wrap:wrap; gap:5px 9px; margin-top:6px; justify-content:center; }
|
||||||
|
.status-legend .lg { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--muted); white-space:nowrap; }
|
||||||
|
.status-legend .lg i { width:9px; height:9px; border-radius:2px; flex:0 0 auto; }
|
||||||
|
.status-legend .lg b { color:var(--fg); font-variant-numeric:tabular-nums; }
|
||||||
.kpi.kpi-exit { cursor:pointer; border-color:var(--err); }
|
.kpi.kpi-exit { cursor:pointer; border-color:var(--err); }
|
||||||
.kpi.kpi-exit:hover { border-color:var(--err); box-shadow:0 0 0 3px rgba(244,67,54,.15); }
|
.kpi.kpi-exit:hover { border-color:var(--err); box-shadow:0 0 0 3px rgba(244,67,54,.15); }
|
||||||
.kpi.kpi-exit .k-lab::after { content:'×'; color:var(--err); font-weight:800; font-size:15px; margin-left:auto; }
|
.kpi.kpi-exit .k-lab::after { content:'×'; color:var(--err); font-weight:800; font-size:15px; margin-left:auto; }
|
||||||
@ -783,10 +789,9 @@ async function paintStats() {
|
|||||||
<div class="k-chart"><canvas id="cv-lat"></canvas></div>
|
<div class="k-chart"><canvas id="cv-lat"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="kpi kpi-status">
|
<div class="kpi kpi-status">
|
||||||
<div class="k-hdr"><span class="k-lab">${t('dashStatus')}</span></div>
|
<div class="k-hdr"><span class="k-lab">${t('dashStatus')}</span><span class="k-sub" id="tb-status-total"></span></div>
|
||||||
<div class="k-val">${fmtN((st.by_status||[]).reduce((a,x)=>a+x.reqs,0))}</div>
|
<div class="k-chart k-chart-pie"><canvas id="cv-status"></canvas><div class="donut-note"></div></div>
|
||||||
<div class="k-sub" id="tb-status"></div>
|
<div class="status-legend" id="tb-status"></div>
|
||||||
<div class="k-chart"><canvas id="cv-status"></canvas></div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
paintModelTable(st.by_model || []);
|
paintModelTable(st.by_model || []);
|
||||||
paintSrcTable(st.by_source || []);
|
paintSrcTable(st.by_source || []);
|
||||||
@ -832,11 +837,13 @@ function paintSrcTable(rows) {
|
|||||||
<td class="num">${fmtMs(fmtLat(r.latency_sum_ms, r.reqs))}</td><td class="num">${fmtMs(r.latency_max_ms)}</td></tr>`).join('') + '</table></div>';
|
<td class="num">${fmtMs(fmtLat(r.latency_sum_ms, r.reqs))}</td><td class="num">${fmtMs(r.latency_max_ms)}</td></tr>`).join('') + '</table></div>';
|
||||||
}
|
}
|
||||||
function paintStatusTable(rows) {
|
function paintStatusTable(rows) {
|
||||||
|
const total = rows.reduce((m, x) => m + x.reqs, 0) || 1;
|
||||||
|
const totalEl = $('#tb-status-total');
|
||||||
|
if (totalEl) totalEl.textContent = fmtN(total) + ' ' + t('thReqs');
|
||||||
const el = $('#tb-status'); if (!el) return;
|
const el = $('#tb-status'); if (!el) return;
|
||||||
if (!rows.length) { el.innerHTML = ''; return; }
|
if (!rows.length) { el.innerHTML = ''; return; }
|
||||||
const total = rows.reduce((m, x) => m + x.reqs, 0) || 1;
|
const palette = ['#3fb27f', '#4fa3d8', '#f0a45c', '#e06c6c', '#b06ce0', '#8a9bb5', '#6fbfa0', '#d0a45c'];
|
||||||
const palette = ['#3fb27f', '#4fa3d8', '#f0a45c', '#e06c6c', '#b06ce0', '#8a9bb5'];
|
el.innerHTML = rows.map((r, i) => `<span class="lg" title="${esc(r.name)}: ${fmtN(r.reqs)} (${(r.reqs/total*100).toFixed(1)}%) · ${t('thOk')} ${fmtN(r.ok)} · ${t('thErr')} ${fmtN(r.err)}">
|
||||||
el.innerHTML = rows.map((r, i) => `<span class="sc" title="${esc(r.name)}: ${fmtN(r.reqs)} (${(r.reqs/total*100).toFixed(1)}%) · ${t('thOk')} ${fmtN(r.ok)} · ${t('thErr')} ${fmtN(r.err)}">
|
|
||||||
<i style="background:${palette[i%palette.length]}"></i>${esc(r.name)}<b>${fmtN(r.reqs)}</b></span>`).join('');
|
<i style="background:${palette[i%palette.length]}"></i>${esc(r.name)}<b>${fmtN(r.reqs)}</b></span>`).join('');
|
||||||
}
|
}
|
||||||
// sparkline: turn a value array into a compact polyline (area fill under accent)
|
// sparkline: turn a value array into a compact polyline (area fill under accent)
|
||||||
@ -931,18 +938,34 @@ function drawStatusChart(rows) {
|
|||||||
const ctx = cv.getContext('2d');
|
const ctx = cv.getContext('2d');
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
|
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
|
||||||
cv.width = rect.width * dpr; cv.height = 86 * dpr; ctx.scale(dpr, dpr);
|
const H = rect.height || 110;
|
||||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
|
||||||
// stacked bar of request counts by status code
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
const total = rows.reduce((a,x)=>a+x.reqs,0) || 1;
|
const total = (rows||[]).reduce((a,x)=>a+x.reqs,0);
|
||||||
const palette = ['#3fb27f','#4fa3d8','#f0a45c','#e06c6c','#b06ce0','#8a9bb5'];
|
if (!total) return;
|
||||||
let x = 0;
|
const palette = ['#3fb27f', '#4fa3d8', '#f0a45c', '#e06c6c', '#b06ce0', '#8a9bb5', '#6fbfa0', '#d0a45c'];
|
||||||
rows.forEach((r,i) => {
|
const r = Math.min(rect.width, H) / 2 - 2;
|
||||||
const w = Math.max(2, r.reqs / total * rect.width);
|
const cx = rect.width / 2, cy = H / 2;
|
||||||
ctx.fillStyle = palette[i%palette.length];
|
let a0 = -Math.PI / 2;
|
||||||
ctx.fillRect(x, 0, w, 86);
|
rows.forEach((row, i) => {
|
||||||
x += w;
|
const sweep = row.reqs / total * Math.PI * 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cx, cy);
|
||||||
|
ctx.arc(cx, cy, r, a0, a0 + sweep);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = palette[i % palette.length];
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = 'var(--card)'; ctx.lineWidth = 1.5; ctx.stroke();
|
||||||
|
a0 += sweep;
|
||||||
});
|
});
|
||||||
|
// center label = total
|
||||||
|
ctx.fillStyle = 'var(--fg)';
|
||||||
|
ctx.font = '700 16px ui-monospace,Menlo,Consolas,monospace';
|
||||||
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(fmtN(total), cx, cy - 7);
|
||||||
|
ctx.fillStyle = 'var(--muted)';
|
||||||
|
ctx.font = '10px ui-monospace,Menlo,Consolas,monospace';
|
||||||
|
ctx.fillText(t('thReqs'), cx, cy + 9);
|
||||||
}
|
}
|
||||||
function paintKeyTable(rows, keyNames) {
|
function paintKeyTable(rows, keyNames) {
|
||||||
const el = $('#tb-key'); if (!el) return;
|
const el = $('#tb-key'); if (!el) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user