From 293f7f524aa3d9f35d01e5d28b612908e2f21175 Mon Sep 17 00:00:00 2001 From: m1saka Date: Sun, 25 Oct 2026 15:16:34 +0800 Subject: [PATCH] feat: format token counts in compact k/M/B/T units --- web/src/common.ts | 12 +++++++++++- web/src/components/UsageChart.tsx | 10 ++-------- web/src/pages/Keys.tsx | 4 ++-- web/src/pages/Logs.tsx | 4 ++-- web/src/pages/Overview.tsx | 6 ++++-- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/web/src/common.ts b/web/src/common.ts index e6c0b9d..0c71451 100644 --- a/web/src/common.ts +++ b/web/src/common.ts @@ -25,6 +25,16 @@ export type Action = (fn: () => Promise) => void export const num = (v: number | null | undefined) => (v == null ? '—' : v.toLocaleString('zh-CN')) +/** 紧凑 tokens 换算:1.2k / 3.4M / 5.6B / 7.8T,保留至多一位小数;空值显示 —。 */ +export const compact = (v: number | null | undefined) => { + if (v == null) return '—' + if (v >= 1e12) return (v / 1e12).toFixed(1).replace(/\.0$/, '') + 'T' + if (v >= 1e9) return (v / 1e9).toFixed(1).replace(/\.0$/, '') + 'B' + if (v >= 1e6) return (v / 1e6).toFixed(1).replace(/\.0$/, '') + 'M' + if (v >= 1e3) return (v / 1e3).toFixed(1).replace(/\.0$/, '') + 'k' + return num(v) +} + export function channelName(channels: Channel[], id: string) { return channels.find(c => c.id === id)?.name || id } @@ -35,4 +45,4 @@ export const usageHeads = ['请求数', '输入 tokens', '输出 tokens', '缓 export const cacheRate = (u: Usage) => (u.input_tokens ? num(Math.round((u.cache_read_tokens / u.input_tokens) * 1000) / 10) + '%' : '—') export const usageCells = (u: Usage) => - [u.requests, u.input_tokens, u.output_tokens, u.cache_read_tokens, u.cache_write_tokens].map(num).concat(cacheRate(u)) + [num(u.requests), compact(u.input_tokens), compact(u.output_tokens), compact(u.cache_read_tokens), compact(u.cache_write_tokens), cacheRate(u)] diff --git a/web/src/components/UsageChart.tsx b/web/src/components/UsageChart.tsx index b1351a2..5942498 100644 --- a/web/src/components/UsageChart.tsx +++ b/web/src/components/UsageChart.tsx @@ -1,6 +1,6 @@ import { useLayoutEffect, useRef, useState } from 'react' import type { Usage } from '../api' -import { num } from '../common' +import { compact, num } from '../common' /** * 每日用量堆叠柱状图(纯 CSS 实现,无第三方依赖)。 @@ -42,7 +42,7 @@ export function UsageChart({ data }: { data: (Usage & { day: string })[] }) { {data.map((d, i) => { const total = totals[i] return ( -
+
@@ -60,9 +60,3 @@ export function UsageChart({ data }: { data: (Usage & { day: string })[] }) {
) } - -function compact(v: number): string { - if (v >= 1e6) return (v / 1e6).toFixed(1).replace(/\.0$/, '') + 'M' - if (v >= 1e3) return (v / 1e3).toFixed(1).replace(/\.0$/, '') + 'k' - return String(v) -} diff --git a/web/src/pages/Keys.tsx b/web/src/pages/Keys.tsx index 1d35a88..3939018 100644 --- a/web/src/pages/Keys.tsx +++ b/web/src/pages/Keys.tsx @@ -1,6 +1,6 @@ import { useState, type FormEvent } from 'react' import { api, type Channel, type Key, type Stats } from '../api' -import { channelName, num, type Action } from '../common' +import { channelName, compact, num, type Action } from '../common' import { formatTime, keyExpiry } from '../time' import { IconPlus } from '../components/icons' import { Badge, Dialog, Feedback, Table } from '../components/ui' @@ -104,7 +104,7 @@ export function KeysPage({ keys, stats, channels, timezone, busy, error, notice, <>{k.prefix}…, k.all_channels ? '全部渠道' : k.channels?.map(id => channelName(channels, id)).join('、') || '无渠道权限', {k.revoked ? '已撤销' : k.enabled ? '启用' : '停用'}, - <>{num(u?.requests ?? 0)} 次输入 {num(u?.input_tokens ?? 0)} / 输出 {num(u?.output_tokens ?? 0)}, + <>{num(u?.requests ?? 0)} 次输入 {compact(u?.input_tokens ?? 0)} / 输出 {compact(u?.output_tokens ?? 0)}, <>{time(k.expires_at) === '—' ? '永不过期' : time(k.expires_at)}创建于 {time(k.created_at)},
diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index c5d3be9..ee956a3 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -1,5 +1,5 @@ import type { Channel, Log } from '../api' -import { channelName, num } from '../common' +import { channelName, compact, num } from '../common' import { formatTime } from '../time' import { Badge, Table } from '../components/ui' @@ -18,7 +18,7 @@ export function LogsPage({ logs, channels }: { logs: Log[]; channels: Channel[] <>{l.downstream} → {l.upstream}{l.stream ? '流式' : '非流式'}, <>= 200 && l.status < 400 ? 'good' : 'danger'}>{l.status || '失败'}{num(l.duration_ms)} ms, l.usage_known - ? <>输入 {num(l.input_tokens)} / 输出 {num(l.output_tokens)}缓存读 {num(l.cache_read_tokens)} / 写 {num(l.cache_write_tokens)} + ? <>输入 {compact(l.input_tokens)} / 输出 {compact(l.output_tokens)}缓存读 {compact(l.cache_read_tokens)} / 写 {compact(l.cache_write_tokens)} : '用量未知',
展开 diff --git a/web/src/pages/Overview.tsx b/web/src/pages/Overview.tsx index ddd73f5..84f51fa 100644 --- a/web/src/pages/Overview.tsx +++ b/web/src/pages/Overview.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import type { Channel, Stats } from '../api' -import { channelName, num, usageCells, usageHeads } from '../common' +import { channelName, compact, num, usageCells, usageHeads } from '../common' import { Table } from '../components/ui' import { UsageChart } from '../components/UsageChart' import { IconActivity, IconArrowDown, IconArrowUp, IconCheckCircle, IconDatabase, IconHelpCircle, IconXCircle, IconZap } from '../components/icons' @@ -16,6 +16,8 @@ const METRICS: { key: keyof Stats['summary']; label: string; icon: ReactNode; to { key: 'cache_write_tokens', label: '缓存写入', icon: }, ] +const TOKEN_KEYS: (keyof Stats['summary'])[] = ['input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_write_tokens'] + export function Overview({ stats, channels }: { stats: Stats | null; channels: Channel[] }) { if (!stats) return
暂无统计数据,请刷新重试。
@@ -29,7 +31,7 @@ export function Overview({ stats, channels }: { stats: Stats | null; channels: C
{m.label} - {num(value)} + {TOKEN_KEYS.includes(m.key) ? compact(value) : num(value)}
) })}