mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix: polish usage statistics ui (#3426)
* fix: improve usage statistics ui * chore: remove unused token suffix translation --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -80,53 +80,51 @@ export function UsageDashboard() {
|
||||
transition={{ duration: 0.4 }}
|
||||
className="space-y-8 pb-8"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-2xl font-bold">{t("usage.title")}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("usage.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:items-end justify-between gap-4 mb-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
{t("usage.title")}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">{t("usage.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border/50 bg-card/40 backdrop-blur-sm p-4">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center p-1 bg-muted/30 rounded-lg border border-border/50">
|
||||
{APP_FILTER_OPTIONS.map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
onClick={() => setAppType(type)}
|
||||
className={cn(
|
||||
"px-4 py-1.5 rounded-lg text-sm font-medium transition-all",
|
||||
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
|
||||
appType === type
|
||||
? "bg-primary/10 text-primary shadow-sm border border-primary/20"
|
||||
: "text-muted-foreground hover:text-primary hover:bg-muted/50 border border-transparent",
|
||||
? "bg-background text-primary shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50",
|
||||
)}
|
||||
>
|
||||
{t(`usage.appFilter.${type}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-xs text-muted-foreground"
|
||||
title={t("common.refresh", "刷新")}
|
||||
onClick={changeRefreshInterval}
|
||||
>
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
{refreshIntervalMs > 0 ? `${refreshIntervalMs / 1000}s` : "--"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 ml-auto lg:ml-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs"
|
||||
title={t("common.refresh", "刷新")}
|
||||
onClick={changeRefreshInterval}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-3.5 w-3.5" />
|
||||
{refreshIntervalMs > 0 ? `${refreshIntervalMs / 1000}s` : "--"}
|
||||
</Button>
|
||||
|
||||
<UsageDateRangePicker
|
||||
selection={range}
|
||||
triggerLabel={rangeLabel}
|
||||
onApply={(nextRange) => setRange(nextRange)}
|
||||
/>
|
||||
</div>
|
||||
<UsageDateRangePicker
|
||||
selection={range}
|
||||
triggerLabel={rangeLabel}
|
||||
onApply={(nextRange) => setRange(nextRange)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+111
-102
@@ -201,112 +201,121 @@ export function UsageHero({
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-gradient-to-br from-primary/5 via-card/50 to-background/50 backdrop-blur-xl shadow-sm">
|
||||
<CardContent className="p-6 md:p-8">
|
||||
{/* Header: title + cost */}
|
||||
<div className="flex flex-wrap items-start justify-between gap-4 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn("p-2 rounded-lg", titleTheme.iconBg)}>
|
||||
<Zap className={cn("h-4 w-4", titleTheme.accent)} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{appLabel && (
|
||||
<>
|
||||
<span className={cn("font-semibold", titleTheme.accent)}>
|
||||
{appLabel}
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-card/60 backdrop-blur-xl shadow-sm">
|
||||
<CardContent className="p-4 md:p-5">
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Top row: Main Token Count, Requests, Cost */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"p-2.5 rounded-xl bg-gradient-to-br shadow-sm",
|
||||
titleTheme.iconBg,
|
||||
)}
|
||||
>
|
||||
<Zap className={cn("h-5 w-5", titleTheme.accent)} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground flex items-center gap-1.5 mb-0.5">
|
||||
{appLabel && (
|
||||
<>
|
||||
<span
|
||||
className={cn("font-semibold", titleTheme.accent)}
|
||||
>
|
||||
{appLabel}
|
||||
</span>
|
||||
<span className="text-muted-foreground/30">•</span>
|
||||
</>
|
||||
)}
|
||||
{t("usage.realTotal", "真实消耗 Tokens")}
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span
|
||||
className="text-2xl md:text-3xl font-bold tabular-nums tracking-tight leading-none"
|
||||
title={realTotal.toLocaleString()}
|
||||
>
|
||||
{realTotal.toLocaleString()}
|
||||
</span>
|
||||
<span className="mx-1.5 text-muted-foreground/40">·</span>
|
||||
</>
|
||||
)}
|
||||
{t("usage.realTotal", "真实消耗 Tokens")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-right">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("usage.totalRequests")}
|
||||
</span>
|
||||
<span className="text-sm font-semibold flex items-center gap-1 justify-end">
|
||||
<Activity className="h-3.5 w-3.5 text-blue-500" />
|
||||
{requests.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-medium bg-muted/40 px-1.5 py-0.5 rounded-md">
|
||||
≈ {formatTokensShort(realTotal, lang, 2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("usage.totalCost")}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-green-500">
|
||||
{totalCost == null ? "--" : fmtUsd(totalCost, 4)}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-5 bg-background/50 px-4 py-2.5 rounded-xl border border-border/40 shadow-sm">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-medium">
|
||||
{t("usage.totalRequests")}
|
||||
</span>
|
||||
<span className="font-semibold flex items-center gap-1.5 text-sm tabular-nums">
|
||||
<Activity className="h-3.5 w-3.5 text-blue-500" />
|
||||
{requests.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-px h-8 bg-border/60" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-medium">
|
||||
{t("usage.totalCost")}
|
||||
</span>
|
||||
<span className="font-semibold text-green-500 text-sm tabular-nums">
|
||||
{totalCost == null ? "--" : fmtUsd(totalCost, 4)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero number */}
|
||||
<div className="flex flex-col items-start mb-6">
|
||||
<div
|
||||
className="text-4xl md:text-5xl font-bold tracking-tight tabular-nums leading-tight"
|
||||
title={realTotal.toLocaleString()}
|
||||
>
|
||||
{realTotal.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
≈ {formatTokensShort(realTotal, lang, 2)}{" "}
|
||||
{t("usage.tokensSuffix", "tokens")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breakdown row: 4 mini stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-5">
|
||||
<MiniStat
|
||||
icon={<ArrowDownToLine className="h-3.5 w-3.5" />}
|
||||
label={t("usage.freshInput", "新增输入")}
|
||||
value={formatTokensShort(input, lang)}
|
||||
accent="text-blue-500"
|
||||
/>
|
||||
<MiniStat
|
||||
icon={<ArrowUpFromLine className="h-3.5 w-3.5" />}
|
||||
label={t("usage.output")}
|
||||
value={formatTokensShort(output, lang)}
|
||||
accent="text-purple-500"
|
||||
/>
|
||||
<MiniStat
|
||||
icon={<Database className="h-3.5 w-3.5" />}
|
||||
label={t("usage.cacheWrite", "缓存写入")}
|
||||
value={cacheWriteDisplay.value}
|
||||
accent="text-amber-500"
|
||||
muted={cacheWriteDisplay.muted}
|
||||
tooltip={cacheWriteDisplay.tooltip}
|
||||
/>
|
||||
<MiniStat
|
||||
icon={<Sparkles className="h-3.5 w-3.5" />}
|
||||
label={t("usage.cacheRead", "缓存命中")}
|
||||
value={formatTokensShort(cacheRead, lang)}
|
||||
accent="text-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Hit rate progress */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{t("usage.cacheHitRate", "缓存命中率")}
|
||||
</span>
|
||||
<span className="font-semibold text-emerald-500 tabular-nums">
|
||||
{hitPercentLabel}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-2 rounded-full bg-muted/50 overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-emerald-500/80 to-emerald-400 rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${hitPercent}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
{/* Bottom row: Breakdown and Hit Rate */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
|
||||
<MiniStat
|
||||
icon={<ArrowDownToLine className="h-3.5 w-3.5" />}
|
||||
label={t("usage.freshInput", "新增输入")}
|
||||
value={formatTokensShort(input, lang)}
|
||||
accent="text-blue-500"
|
||||
/>
|
||||
<MiniStat
|
||||
icon={<ArrowUpFromLine className="h-3.5 w-3.5" />}
|
||||
label={t("usage.output")}
|
||||
value={formatTokensShort(output, lang)}
|
||||
accent="text-purple-500"
|
||||
/>
|
||||
<MiniStat
|
||||
icon={<Database className="h-3.5 w-3.5" />}
|
||||
label={t("usage.cacheWrite", "缓存写入")}
|
||||
value={cacheWriteDisplay.value}
|
||||
accent="text-amber-500"
|
||||
muted={cacheWriteDisplay.muted}
|
||||
tooltip={cacheWriteDisplay.tooltip}
|
||||
/>
|
||||
<MiniStat
|
||||
icon={<Sparkles className="h-3.5 w-3.5" />}
|
||||
label={t("usage.cacheRead", "缓存命中")}
|
||||
value={formatTokensShort(cacheRead, lang)}
|
||||
accent="text-emerald-500"
|
||||
/>
|
||||
|
||||
<div className="col-span-2 lg:col-span-1 flex flex-col justify-center rounded-xl border border-border/40 bg-background/40 p-3 shadow-sm">
|
||||
<div className="flex items-center justify-between text-[11px] mb-2">
|
||||
<span className="text-muted-foreground font-medium">
|
||||
{t("usage.cacheHitRate", "缓存命中率")}
|
||||
</span>
|
||||
<span className="font-bold text-emerald-500 tabular-nums">
|
||||
{hitPercentLabel}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-1.5 rounded-full bg-muted/60 overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute inset-y-0 left-0 bg-emerald-500 rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${hitPercent}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -336,21 +345,21 @@ function MiniStat({
|
||||
}: MiniStatProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-1 rounded-lg border border-border/40 bg-background/40 px-3 py-2.5"
|
||||
className="flex flex-col gap-1 rounded-xl border border-border/40 bg-background/40 p-3 shadow-sm"
|
||||
title={tooltip}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-1.5 text-xs text-muted-foreground ${accent}`}
|
||||
className={`flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground ${accent}`}
|
||||
>
|
||||
{icon}
|
||||
<span className="text-foreground/70">{label}</span>
|
||||
<span className="text-foreground/70 tracking-wide">{label}</span>
|
||||
{tooltip && (
|
||||
<Info className="h-3 w-3 text-muted-foreground/60 shrink-0" />
|
||||
<Info className="h-3 w-3 text-muted-foreground/60 shrink-0 ml-auto" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-base font-semibold tabular-nums",
|
||||
"text-sm font-semibold tabular-nums",
|
||||
muted && "text-muted-foreground/70",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1345,7 +1345,6 @@
|
||||
"cacheTokens": "Cache Tokens",
|
||||
"realTotal": "Tokens Processed",
|
||||
"realTotalHint": "Input + Output + Cache Write + Cache Read",
|
||||
"tokensSuffix": "tokens",
|
||||
"freshInput": "Fresh Input",
|
||||
"cacheHitRate": "Cache Hit Rate",
|
||||
"requestLogs": "Request Logs",
|
||||
|
||||
@@ -1345,7 +1345,6 @@
|
||||
"cacheTokens": "キャッシュトークン",
|
||||
"realTotal": "実消費トークン",
|
||||
"realTotalHint": "Input + Output + キャッシュ書込 + キャッシュ命中",
|
||||
"tokensSuffix": "tokens",
|
||||
"freshInput": "新規入力",
|
||||
"cacheHitRate": "キャッシュヒット率",
|
||||
"requestLogs": "リクエストログ",
|
||||
|
||||
@@ -1291,7 +1291,6 @@
|
||||
"cacheTokens": "快取 Token",
|
||||
"realTotal": "真實消耗 Tokens",
|
||||
"realTotalHint": "Input + Output + 快取建立 + 快取命中",
|
||||
"tokensSuffix": "Tokens",
|
||||
"freshInput": "新增輸入",
|
||||
"cacheHitRate": "快取命中率",
|
||||
"requestLogs": "請求日誌",
|
||||
|
||||
@@ -1345,7 +1345,6 @@
|
||||
"cacheTokens": "缓存 Token",
|
||||
"realTotal": "真实消耗 Tokens",
|
||||
"realTotalHint": "Input + Output + 缓存写入 + 缓存命中",
|
||||
"tokensSuffix": "tokens",
|
||||
"freshInput": "新增输入",
|
||||
"cacheHitRate": "缓存命中率",
|
||||
"requestLogs": "请求日志",
|
||||
|
||||
Reference in New Issue
Block a user