mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
fix(ui): eliminate KPI card height jump on load — all five cards fixed at 206px, chart area flex:1
Previously the skeleton was a fixed 186px but the real cards had different natural heights (status pie 110 + legend, tokens two-line sub + legend, etc.), so replacing the skeleton caused a size jump. Now every .kpi is a fixed 206px flex column; .k-chart absorbs the remaining space (flex:1) and the canvas absolutely fills it (draw fns read container height, not a hardcoded 86/110). Skeleton .ksk is exactly 206px with the same layout, so first paint and real cards are identical in size. Deployed (bak .bak.20260811w), server active.
This commit is contained in:
@ -102,9 +102,9 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
|
||||
|
||||
/* ---------- dashboard / usage metrics ---------- */
|
||||
.kpi-skeletons { grid-column:1 / -1; display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; }
|
||||
.ksk { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; min-height:186px; display:flex; flex-direction:column; gap:10px; }
|
||||
.ksk { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; height:206px; display:flex; flex-direction:column; gap:10px; }
|
||||
.ksk i { display:block; height:11px; border-radius:6px; background:linear-gradient(90deg,var(--card2) 25%,var(--line) 37%,var(--card2) 63%); background-size:400% 100%; animation:shimmer 1.2s infinite; }
|
||||
.ksk i:nth-child(1){ width:38%; align-self:flex-start; } .ksk i:nth-child(2){ width:68%; height:24px; } .ksk i:nth-child(3){ width:100%; height:86px; margin-top:4px; }
|
||||
.ksk i:nth-child(1){ width:38%; align-self:flex-start; } .ksk i:nth-child(2){ width:68%; height:24px; } .ksk i:nth-child(3){ width:100%; flex:1 1 auto; margin:6px 0 0; }
|
||||
@keyframes shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
|
||||
/* entry / transition animations */
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
|
||||
@ -114,17 +114,15 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
|
||||
.tab-pane { animation:tabIn .25s ease; }
|
||||
.chat-log .chat-empty { animation:fadeIn .3s ease; }
|
||||
.kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; margin-bottom:18px; }
|
||||
.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; box-shadow:var(--shadow); }
|
||||
.kpi .k-hdr { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; }
|
||||
.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; box-shadow:var(--shadow); display:flex; flex-direction:column; height:206px; }
|
||||
.kpi .k-hdr { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; flex:0 0 auto; }
|
||||
.kpi .k-lab { font-size:12px; color:var(--muted); font-weight:600; white-space:nowrap; }
|
||||
.kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; }
|
||||
.kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; flex:0 0 auto; }
|
||||
.kpi .k-val .u { font-size:13px; font-weight:600; color:var(--muted); margin-left:3px; }
|
||||
.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; flex:0 0 auto; }
|
||||
.kpi .k-sub.k2 { display:flex; flex-direction:column; align-items:flex-start; line-height:1.5; }
|
||||
.kpi .k-chart { margin-top:8px; position:relative; height:86px; }
|
||||
.kpi canvas { width:100%; height:86px; display:block; }
|
||||
.kpi.kpi-status .k-chart-pie { height:110px; }
|
||||
.kpi.kpi-status canvas { height:110px; }
|
||||
.kpi .k-chart { position:relative; flex:1 1 auto; min-height:40px; margin-top:8px; }
|
||||
.kpi canvas { position:absolute; inset:0; width:100%; height:100%; display:block; }
|
||||
.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; }
|
||||
@ -915,7 +913,7 @@ function drawActiveChart(st) {
|
||||
const ctx = cv.getContext('2d');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
|
||||
const H = 86;
|
||||
const H = rect.height || 86;
|
||||
cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
// sample: requests started in the last 3s (approx QPS). Records carry unix-ms time.
|
||||
@ -945,14 +943,15 @@ function drawReqsChart(st) {
|
||||
const ctx = cv.getContext('2d');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
|
||||
cv.width = rect.width * dpr; cv.height = 86 * dpr; ctx.scale(dpr, dpr);
|
||||
const H = rect.height || 86;
|
||||
cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
const recs = st.records || [];
|
||||
// bucket by UTC day -> request counts (last 7 non-empty days)
|
||||
const byDay = new Map();
|
||||
recs.forEach(r => { const d = new Date(r.time).toISOString().slice(5,10); byDay.set(d, (byDay.get(d)||0)+1); });
|
||||
const counts = Array.from(byDay.values()).slice(-7);
|
||||
dailyBars(ctx, counts, rect.width, 86, '#3fb27f');
|
||||
dailyBars(ctx, counts, rect.width, H, '#3fb27f');
|
||||
}
|
||||
const tokPalette = ['#3fb27f','#4fa3d8','#f0a45c','#b06ce0'];
|
||||
function drawTokensChart(models) {
|
||||
@ -960,7 +959,8 @@ function drawTokensChart(models) {
|
||||
const ctx = cv.getContext('2d');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
|
||||
cv.width = rect.width * dpr; cv.height = 86 * dpr; ctx.scale(dpr, dpr);
|
||||
const H = rect.height || 86;
|
||||
cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
const rows = (models||[]).slice().sort((a,b)=>b.tokens-a.tokens).slice(0,4);
|
||||
const tot = rows.reduce((a,x)=>a+x.tokens,0) || 1;
|
||||
@ -968,7 +968,7 @@ function drawTokensChart(models) {
|
||||
rows.forEach((m,i) => {
|
||||
const w = Math.max(2, m.tokens / tot * rect.width);
|
||||
ctx.fillStyle = tokPalette[i%tokPalette.length];
|
||||
ctx.fillRect(x, 0, w, 86);
|
||||
ctx.fillRect(x, 0, w, H);
|
||||
x += w;
|
||||
});
|
||||
// color-swatch legend instead of on-chart text
|
||||
@ -982,12 +982,13 @@ function drawLatencyChart(st) {
|
||||
const ctx = cv.getContext('2d');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
|
||||
cv.width = rect.width * dpr; cv.height = 86 * dpr; ctx.scale(dpr, dpr);
|
||||
const H = rect.height || 86;
|
||||
cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
const recs = st.records || [];
|
||||
// per-record latency sparkline (last 40)
|
||||
const lat = recs.slice(-40).map(r => r.latency_ms || 0);
|
||||
sparkline(ctx, lat, rect.width, 86, '#e06c6c');
|
||||
sparkline(ctx, lat, rect.width, H, '#e06c6c');
|
||||
}
|
||||
function drawStatusChart(rows) {
|
||||
const cv = document.getElementById('cv-status'); if (!cv) return;
|
||||
|
||||
Reference in New Issue
Block a user