fix(billing): 页面文字溢出卡片 + 数字千分位

用户报 billing 页"文字超出展示框"。真因是三处叠加,都不是文案问题:

1. KPI 卡是 CSS grid,grid item 默认 min-width:auto。2e8 级的 prompt
   tokens(实测 270,149,209)无法收缩,于是把 grid 轨道撑出容器 → 整页横向
   溢出。修法是 min-width:0 + overflow:hidden(不给 min-width:0 的话,
   grid 子项永远不肯收缩,这是 grid 最常见的溢出坑)。
2. 表格写死 min-width:560px,窄视口下必然溢出。改 width:100% +
   table-layout:fixed,列宽由布局分配而不是由内容撑开。
3. 长名称(deepseek-v4.1-flash 这类模型 id)撑宽单元格。名称列改 ellipsis +
   title 悬停看全名;数字列 word-break:break-all 在列宽内换行。

顺手:所有 token/请求数走 toLocaleString 千分位。原始 9 位数字读起来要数
零位数,分组后 270,149,209 一眼可读,也顺带缩短了字符串宽度。

CDP 实测(820px 窄视口,逼出溢出条件):
  main/body 横向溢出 = no
  table=338 < card=366(修复前 min-width:560 必然 > 366)
  KPI 输入 tokens = 270,149,209
  billing 页无控制台错误

残留(不影响布局):表头 TH 在固定布局下 48>42 轻微超出自身格,因为
word-break 不拆单个长词;表格整体仍在容器内,未产生页面滚动。

全量测试全绿(8 包)。
This commit is contained in:
JianFeeeee
2026-10-02 12:16:40 +08:00
parent aa10ee8c27
commit d1da40e493

View File

@ -547,21 +547,30 @@ plugin.ui = {
if (!b.cache_reported_reqs) return "n/r"; if (!b.cache_reported_reqs) return "n/r";
return ((hit / prompt) * 100).toFixed(1) + "%"; return ((hit / prompt) * 100).toFixed(1) + "%";
} }
// fmtInt 千分位分组:2e8 级 token 总数可读、也更短,降低撑宽风险。
function fmtInt(n) {
return (Number(n) || 0).toLocaleString("en-US");
}
function row(name, b, cur) { function row(name, b, cur) {
var fresh = (b.cache_fresh_tokens === undefined) ? (b.prompt_tokens || 0) : b.cache_fresh_tokens; var fresh = (b.cache_fresh_tokens === undefined) ? (b.prompt_tokens || 0) : b.cache_fresh_tokens;
return "<tr><td><b>" + esc(name) + "</b></td><td>" + money(b.cost, cur) + // 名称列 ellipsis(title 悬停看全名);数字列 break-all 在列宽内换行而不是
"</td><td>" + (b.requests || 0) + "</td><td>" + (b.prompt_tokens || 0) + // 把表格撑出卡片。单元格结构与列数不变,列数判据不受影响。
"</td><td>" + fresh + return "<tr><td style='overflow:hidden'><b style='display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis' title='" +
"</td><td>" + (b.cache_hit_tokens || 0) + esc(String(name).replace(/'/g, "&#39;")) + "'>" + esc(name) + "</b></td>" +
"</td><td>" + esc(cacheRate(b)) + "<td style='word-break:break-all'>" + money(b.cost, cur) + "</td>" +
"</td><td>" + (b.completion_tokens || 0) + "</td></tr>"; "<td style='word-break:break-all'>" + fmtInt(b.requests || 0) + "</td>" +
"<td style='word-break:break-all'>" + fmtInt(b.prompt_tokens || 0) + "</td>" +
"<td style='word-break:break-all'>" + fmtInt(fresh) + "</td>" +
"<td style='word-break:break-all'>" + fmtInt(b.cache_hit_tokens || 0) + "</td>" +
"<td style='word-break:break-all'>" + esc(cacheRate(b)) + "</td>" +
"<td style='word-break:break-all'>" + fmtInt(b.completion_tokens || 0) + "</td></tr>";
} }
function tableFor(el, obj, cur, empty) { function tableFor(el, obj, cur, empty) {
var keys = Object.keys(obj || {}); var keys = Object.keys(obj || {});
if (!keys.length) { el.innerHTML = '<div class="muted">' + empty + "</div>"; return; } if (!keys.length) { el.innerHTML = '<div class="muted">' + empty + "</div>"; return; }
keys.sort(function (a, b) { return (obj[b].cost || 0) - (obj[a].cost || 0); }); keys.sort(function (a, b) { return (obj[b].cost || 0) - (obj[a].cost || 0); });
var TH = L(); var TH = L();
var h = "<table style='width:100%;border-collapse:collapse;font-size:13px;min-width:560px'>" + var h = "<table style='width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;word-break:break-word'>" +
"<tr style='text-align:left;opacity:.65'><th>" + TH.thName + "</th><th>" + TH.thCost + "<tr style='text-align:left;opacity:.65'><th>" + TH.thName + "</th><th>" + TH.thCost +
"</th><th>" + TH.thReqs + "</th><th>" + TH.thPrompt + "</th><th>" + TH.thReqs + "</th><th>" + TH.thPrompt +
"</th><th>" + TH.thFresh + "</th><th>" + TH.thCache + "</th><th>" + TH.thCachePct + "</th><th>" + TH.thFresh + "</th><th>" + TH.thCache + "</th><th>" + TH.thCachePct +
@ -589,20 +598,23 @@ plugin.ui = {
var t = st.total || {}; var t = st.total || {};
document.getElementById("billing-kpis").innerHTML = [ document.getElementById("billing-kpis").innerHTML = [
[T.total, money(t.cost, cur)], [T.total, money(t.cost, cur)],
[T.requests, t.requests || 0], [T.requests, fmtInt(t.requests || 0)],
[T.degraded, st.degraded_reqs || 0], [T.degraded, fmtInt(st.degraded_reqs || 0)],
[T.unpriced, st.unpriced_reqs || 0], [T.unpriced, fmtInt(st.unpriced_reqs || 0)],
[T.prompt, t.prompt_tokens || 0], [T.prompt, fmtInt(t.prompt_tokens || 0)],
[T.completion, t.completion_tokens || 0], [T.completion, fmtInt(t.completion_tokens || 0)],
[T.failures, t.failures || 0], [T.failures, t.failures || 0],
// Cache KPIs: last session added the table columns but the KPI cards // Cache KPIs: last session added the table columns but the KPI cards
// were left out — the edit's assert failed and the retry only re-did the // were left out — the edit's assert failed and the retry only re-did the
// tables. The numbers existed in state and nowhere in the UI. // tables. The numbers existed in state and nowhere in the UI.
[T.cacheRate, cacheRate(t)], [T.cacheRate, cacheRate(t)],
[T.cacheTokens, t.cache_hit_tokens || 0] [T.cacheTokens, fmtInt(t.cache_hit_tokens || 0)]
].map(function (kv) { ].map(function (kv) {
return "<div class='card' style='padding:12px'><div style='font-size:11px;opacity:.65'>" + // min-width:0:grid item 默认 min-width:auto,2e8 级长数字会把轨道撑出
kv[0] + "</div><div style='font-size:19px;font-weight:600;margin-top:4px'>" + // 容器造成横向溢出。标签 nowrap 截断,数值 break-all 换行。
return "<div class='card' style='padding:12px;min-width:0;overflow:hidden'>" +
"<div style='font-size:11px;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'>" +
esc(kv[0]) + "</div><div style='font-size:19px;font-weight:600;margin-top:4px;word-break:break-all;line-height:1.2'>" +
esc(kv[1]) + "</div></div>"; esc(kv[1]) + "</div></div>";
}).join(""); }).join("");
var TD = L(); var TD = L();