From 84e30f9cfc7a6909350cedaf478e116f6e2475e1 Mon Sep 17 00:00:00 2001 From: umaru Date: Sat, 30 May 2026 15:22:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E7=BB=9F=E4=B8=80=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E9=87=91=E9=A2=9D=E6=A0=BC=E5=BC=8F=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E5=B0=8F=E9=A2=9D=E6=98=BE=E7=A4=BA=E8=BF=87=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仪表盘今日消费卡片与排行榜消费列共用一份 formatCost:小于一分从六位小数改为四位小数,常规金额改为两位小数,千/百万级用 K/M 缩写。将 formatCost 提取到 chart-theme.ts 复用,消除两处重复实现,并补充对应单元测试。 --- src/components/dashboard/chart-theme.ts | 9 ++++++ .../dashboard/leaderboard-section.tsx | 8 +---- src/components/dashboard/stats-cards.tsx | 8 +---- .../dashboard/leaderboard-section.test.tsx | 30 +++++++++++++++++ .../components/dashboard/stats-cards.test.tsx | 32 +++++++++++++++++++ 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/components/dashboard/chart-theme.ts b/src/components/dashboard/chart-theme.ts index 9ad328e9..0fa3162c 100644 --- a/src/components/dashboard/chart-theme.ts +++ b/src/components/dashboard/chart-theme.ts @@ -114,3 +114,12 @@ export function formatDuration(ms: number): string { } return `${Math.round(ms)}ms`; } + +export function formatCost(usd: number): string { + if (usd === 0) return "$0.00"; + if (usd >= 1_000_000) return `$${(usd / 1_000_000).toFixed(2)}M`; + if (usd >= 1_000) return `$${(usd / 1_000).toFixed(2)}K`; + if (usd >= 0.01) return `$${usd.toFixed(2)}`; + // 小于一分时保留四位小数,既能让真实存在的消费显示出来,又不会过长 + return `$${usd.toFixed(4)}`; +} diff --git a/src/components/dashboard/leaderboard-section.tsx b/src/components/dashboard/leaderboard-section.tsx index 3e155842..16fd226f 100644 --- a/src/components/dashboard/leaderboard-section.tsx +++ b/src/components/dashboard/leaderboard-section.tsx @@ -8,7 +8,7 @@ import { Card, CardContent } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import type { StatsLeaderboardResponse, DistributionItem } from "@/types/api"; -import { formatNumber, UPSTREAM_COLORS_DARK } from "./chart-theme"; +import { formatCost, formatNumber, UPSTREAM_COLORS_DARK } from "./chart-theme"; import { DashboardLoadingBlock, DashboardLoadingSurface } from "./dashboard-loading"; interface LeaderboardSectionProps { @@ -40,12 +40,6 @@ function getTtftClass(ttftMs: number): string { return "text-status-success"; } -function formatCost(usd: number): string { - if (usd === 0) return "$0.00"; - if (usd < 0.01) return `$${usd.toFixed(6)}`; - return `$${usd.toFixed(4)}`; -} - const RANK_ROW_BASE = "flex items-center gap-3 rounded-cf-sm border-l-2 px-2.5 py-1.5 transition-colors bg-surface-300/42 hover:bg-surface-400/55"; diff --git a/src/components/dashboard/stats-cards.tsx b/src/components/dashboard/stats-cards.tsx index 30e20b6f..fb5f125f 100644 --- a/src/components/dashboard/stats-cards.tsx +++ b/src/components/dashboard/stats-cards.tsx @@ -15,7 +15,7 @@ import { import { Card, CardContent } from "@/components/ui/card"; import { cn } from "@/lib/utils"; -import { formatDuration, formatNumber } from "./chart-theme"; +import { formatCost, formatDuration, formatNumber } from "./chart-theme"; import { DashboardLoadingBlock, DashboardLoadingSurface } from "./dashboard-loading"; interface StatsCardsProps { @@ -52,12 +52,6 @@ function formatCacheRate(rate: number): string { return `${normalizedRate.toFixed(2)}%`; } -function formatCost(usd: number): string { - if (usd === 0) return "$0.00"; - if (usd < 0.01) return `$${usd.toFixed(6)}`; - return `$${usd.toFixed(4)}`; -} - interface DeltaBadgeProps { today: number; yesterday: number; diff --git a/tests/components/dashboard/leaderboard-section.test.tsx b/tests/components/dashboard/leaderboard-section.test.tsx index 8af07f28..12b0c554 100644 --- a/tests/components/dashboard/leaderboard-section.test.tsx +++ b/tests/components/dashboard/leaderboard-section.test.tsx @@ -197,6 +197,36 @@ describe("LeaderboardSection", () => { expect(screen.getByText("10.0K")).toBeInTheDocument(); }); + it("renders costs with two decimals", () => { + render(); + + expect(screen.getByText("$12.50")).toBeInTheDocument(); + expect(screen.getByText("$8.00")).toBeInTheDocument(); + expect(screen.getByText("$0.50")).toBeInTheDocument(); + }); + + it("renders sub-cent cost with four decimals", () => { + const data: StatsLeaderboardResponse = { + api_keys: [ + { + id: "key-tiny", + name: "Tiny Key", + key_prefix: "sk-tiny", + request_count: 1, + total_tokens: 10, + total_cost_usd: 0.006599, + model_distribution: [], + }, + ], + upstreams: [], + models: [], + }; + + render(); + + expect(screen.getByText("$0.0066")).toBeInTheDocument(); + }); + it("renders distribution pie charts with fixed dimensions", () => { render(); diff --git a/tests/components/dashboard/stats-cards.test.tsx b/tests/components/dashboard/stats-cards.test.tsx index e26dcfeb..ae29abb7 100644 --- a/tests/components/dashboard/stats-cards.test.tsx +++ b/tests/components/dashboard/stats-cards.test.tsx @@ -200,6 +200,38 @@ describe("StatsCards", () => { }); }); + describe("Cost formatting", () => { + it("renders zero cost as $0.00", () => { + render(); + + expect(screen.getByText("$0.00")).toBeInTheDocument(); + }); + + it("renders sub-cent cost with four decimals", () => { + render(); + + expect(screen.getByText("$0.0066")).toBeInTheDocument(); + }); + + it("renders regular cost with two decimals", () => { + render(); + + expect(screen.getByText("$12.50")).toBeInTheDocument(); + }); + + it("abbreviates thousand-dollar cost with K suffix", () => { + render(); + + expect(screen.getByText("$1.23K")).toBeInTheDocument(); + }); + + it("abbreviates million-dollar cost with M suffix", () => { + render(); + + expect(screen.getByText("$2.50M")).toBeInTheDocument(); + }); + }); + describe("Icons", () => { it("renders activity icon for requests", () => { render(