Merge pull request #9 from pikasTech/fix/api-usd-decimal-alignment
Pipelines as Code CI / sub2rank-nc01-53eafa08ab23f64c49644ac5a41b4b00fdd90685 Success

fix: 对齐 API USD 小数展示
This commit is contained in:
Lyon
2026-07-17 08:29:53 +08:00
committed by GitHub
4 changed files with 25 additions and 5 deletions
+15 -3
View File
@@ -12,6 +12,18 @@ function number(value, digits = 0) {
return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: digits, minimumFractionDigits: digits }) return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: digits, minimumFractionDigits: digits })
} }
function usd(value, digits = 3) {
const numeric = Number(value)
if (value === null || value === undefined || !Number.isFinite(numeric)) return '<span class="usd-value is-empty">—</span>'
const formatter = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: digits, minimumFractionDigits: digits })
const parts = formatter.formatToParts(numeric)
const whole = parts.filter(({ type }) => ['minusSign', 'plusSign', 'integer', 'group'].includes(type)).map(({ value: part }) => part).join('')
const point = parts.find(({ type }) => type === 'decimal')?.value ?? '.'
const fraction = parts.find(({ type }) => type === 'fraction')?.value ?? ''.padEnd(digits, '0')
const label = `$${formatter.format(numeric)}`
return `<span class="usd-value" aria-label="${escapeHtml(label)}"><span class="usd-symbol" aria-hidden="true">$</span><span class="usd-whole" aria-hidden="true">${escapeHtml(whole)}</span><span class="usd-point" aria-hidden="true">${escapeHtml(point)}</span><span class="usd-fraction" aria-hidden="true">${escapeHtml(fraction)}</span></span>`
}
function compact(value) { function compact(value) {
if (value === null || value === undefined) return '—' if (value === null || value === undefined) return '—'
return new Intl.NumberFormat('zh-CN', { notation: 'compact', maximumFractionDigits: 2 }).format(Number(value)) return new Intl.NumberFormat('zh-CN', { notation: 'compact', maximumFractionDigits: 2 }).format(Number(value))
@@ -134,7 +146,7 @@ function renderScoreRows() {
<td>${row.ttftP95Ms == null ? '—' : `${number(row.ttftP95Ms)} ms`}</td> <td>${row.ttftP95Ms == null ? '—' : `${number(row.ttftP95Ms)} ms`}</td>
<td>${compact(usage.requestCount)}</td> <td>${compact(usage.requestCount)}</td>
<td>${compact(usage.tokenCount)}</td> <td>${compact(usage.tokenCount)}</td>
<td>${usage.apiAmountUsd == null ? '—' : `$${number(usage.apiAmountUsd, 3)}`}</td> <td class="usd-cell">${usd(usage.apiAmountUsd)}</td>
<td>${number(row.failoverRecovered)} / ${number(row.failoverRequests)}</td> <td>${number(row.failoverRecovered)} / ${number(row.failoverRequests)}</td>
<td><span class="availability ${row.currentlyAvailable ? 'is-up' : 'is-down'}">${row.currentlyAvailable ? '可用' : '不可用'}</span></td> <td><span class="availability ${row.currentlyAvailable ? 'is-up' : 'is-down'}">${row.currentlyAvailable ? '可用' : '不可用'}</span></td>
</tr>` </tr>`
@@ -180,10 +192,10 @@ async function rankingPage() {
const data = await requestJson('/api/ranking') const data = await requestJson('/api/ranking')
const ranking = data.ranking const ranking = data.ranking
$('#ranking-range').textContent = `${ranking.startDate}${ranking.endDate}` $('#ranking-range').textContent = `${ranking.startDate}${ranking.endDate}`
$('#ranking-cost').textContent = `$${number(ranking.totals.actualCost, 3)}` $('#ranking-cost').innerHTML = usd(ranking.totals.actualCost)
$('#ranking-requests').textContent = compact(ranking.totals.requests) $('#ranking-requests').textContent = compact(ranking.totals.requests)
$('#ranking-tokens').textContent = compact(ranking.totals.tokens) $('#ranking-tokens').textContent = compact(ranking.totals.tokens)
$('#ranking-body').innerHTML = ranking.rows.length ? ranking.rows.map((row) => `<tr><td>${String(row.rank).padStart(2, '0')}</td><td class="account-cell"><b>${escapeHtml(row.displayName)}</b></td><td>$${number(row.actualCost, 3)}</td><td>${compact(row.requests)}</td><td>${compact(row.tokens)}</td></tr>`).join('') : '<tr><td colspan="5" class="empty">当前窗口暂无用量</td></tr>' $('#ranking-body').innerHTML = ranking.rows.length ? ranking.rows.map((row) => `<tr><td>${String(row.rank).padStart(2, '0')}</td><td class="account-cell"><b>${escapeHtml(row.displayName)}</b></td><td class="usd-cell">${usd(row.actualCost)}</td><td>${compact(row.requests)}</td><td>${compact(row.tokens)}</td></tr>`).join('') : '<tr><td colspan="5" class="empty">当前窗口暂无用量</td></tr>'
} }
function creditLabel(status) { function creditLabel(status) {
+1 -1
View File
@@ -13,7 +13,7 @@
<main class="workspace"> <main class="workspace">
<header class="page-head"><div><p class="eyebrow">USER FLOW / DAILY</p><h1>用户用量</h1></div><span id="ranking-range" class="state-badge">读取中</span></header> <header class="page-head"><div><p class="eyebrow">USER FLOW / DAILY</p><h1>用户用量</h1></div><span id="ranking-range" class="state-badge">读取中</span></header>
<section class="metric-strip"><div><span>标准 API 用量</span><strong id="ranking-cost"></strong></div><div><span>请求</span><strong id="ranking-requests"></strong></div><div><span>Token</span><strong id="ranking-tokens"></strong></div></section> <section class="metric-strip"><div><span>标准 API 用量</span><strong id="ranking-cost"></strong></div><div><span>请求</span><strong id="ranking-requests"></strong></div><div><span>Token</span><strong id="ranking-tokens"></strong></div></section>
<section class="table-section"><div class="table-wrap"><table class="data-table"><thead><tr><th>名次</th><th>用户</th><th>API USD</th><th>请求</th><th>Token</th></tr></thead><tbody id="ranking-body"><tr><td colspan="5" class="empty">正在读取用量...</td></tr></tbody></table></div></section> <section class="table-section"><div class="table-wrap"><table class="data-table"><thead><tr><th>名次</th><th>用户</th><th class="usd-cell">API USD</th><th>请求</th><th>Token</th></tr></thead><tbody id="ranking-body"><tr><td colspan="5" class="empty">正在读取用量...</td></tr></tbody></table></div></section>
</main> </main>
</body> </body>
</html> </html>
+1 -1
View File
@@ -36,7 +36,7 @@
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table" aria-label="所有分组账号评分"> <table class="data-table" aria-label="所有分组账号评分">
<thead><tr><th>账号</th><th>分组</th><th>优先级</th><th>评分</th><th>等级</th><th>置信度</th><th>尝试</th><th>失败率</th><th>TTFT P95</th><th>请求</th><th>Token</th><th>API USD</th><th>切号恢复</th><th>可用</th></tr></thead> <thead><tr><th>账号</th><th>分组</th><th>优先级</th><th>评分</th><th>等级</th><th>置信度</th><th>尝试</th><th>失败率</th><th>TTFT P95</th><th>请求</th><th>Token</th><th class="usd-cell">API USD</th><th>切号恢复</th><th>可用</th></tr></thead>
<tbody id="score-body"><tr><td colspan="14" class="empty">正在读取评分快照...</td></tr></tbody> <tbody id="score-body"><tr><td colspan="14" class="empty">正在读取评分快照...</td></tr></tbody>
</table> </table>
</div> </div>
+8
View File
@@ -67,6 +67,14 @@ button:disabled { cursor: wait; opacity: .48; }
.data-table th { height: 40px; padding: 0 12px; position: sticky; top: 0; z-index: 1; text-align: left; white-space: nowrap; background: var(--surface-raised); color: var(--muted); font: 10px/1 var(--mono); border-bottom: 1px solid var(--line); } .data-table th { height: 40px; padding: 0 12px; position: sticky; top: 0; z-index: 1; text-align: left; white-space: nowrap; background: var(--surface-raised); color: var(--muted); font: 10px/1 var(--mono); border-bottom: 1px solid var(--line); }
.data-table td { height: 52px; padding: 8px 12px; border-bottom: 1px solid var(--line); white-space: nowrap; } .data-table td { height: 52px; padding: 8px 12px; border-bottom: 1px solid var(--line); white-space: nowrap; }
.data-table tbody tr:hover { background: rgba(185,228,71,.045); } .data-table tbody tr:hover { background: rgba(185,228,71,.045); }
.usd-cell { text-align: right !important; }
.usd-value { display: inline-grid; grid-template-columns: 1ch 9ch .65ch 3ch; justify-content: end; align-items: baseline; font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.usd-symbol { color: var(--muted); text-align: left; }
.usd-whole { color: var(--paper); text-align: right; font-weight: 650; }
.usd-point, .usd-fraction { color: var(--info); }
.usd-point { text-align: center; }
.usd-fraction { text-align: left; font-weight: 550; }
.usd-value.is-empty { display: inline-block; width: 13.65ch; color: var(--muted); text-align: right; }
.account-cell { min-width: 230px; } .account-cell { min-width: 230px; }
.account-cell b { display: block; max-width: 360px; overflow: hidden; text-overflow: ellipsis; } .account-cell b { display: block; max-width: 360px; overflow: hidden; text-overflow: ellipsis; }
.account-cell small { display: block; color: var(--muted); margin-top: 4px; font: 10px/1 var(--mono); } .account-cell small { display: block; color: var(--muted); margin-top: 4px; font: 10px/1 var(--mono); }