diff --git a/src/components/NodeDetail.tsx b/src/components/NodeDetail.tsx index a3fbe85..21d3d02 100644 --- a/src/components/NodeDetail.tsx +++ b/src/components/NodeDetail.tsx @@ -9,7 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton" import { Country, Status } from "@/components/NodeCard" import { api, type Node } from "@/lib/api" import { - axisBytes, axisTop, bytes, clockFor, despike, quarters, cpuName, CYCLES, FOREVER, money, osName, rate, + axisBytes, axisTop, bytes, clockFor, cycle, despike, quarters, cpuName, FOREVER, money, osName, rate, timeTicks, } from "@/lib/format" @@ -365,7 +365,7 @@ export function NodeDetail({ node }: { node: Node }) { label="续费" value={[ node.price > 0 - ? `${money(node.price, node.currency)} / ${CYCLES[node.billing_cycle] ?? node.billing_cycle}` + ? `${money(node.price, node.currency)} / ${cycle(node.billing_cycle)}` : "免费", node.expires_at ? `${node.expires_at} 到期` : FOREVER, ].join(" · ")} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index b55b7a3..2a39a0f 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -4,7 +4,7 @@ // // Nothing imports it, so the bundle never includes it. import { - axisBytes, axisTop, bytes, cpuName, daysUntil, despike, osName, pair, quarters, timeTicks, uptime, + axisBytes, axisTop, bytes, cpuName, cycle, daysUntil, despike, osName, pair, quarters, timeTicks, uptime, } from "./format.ts" let failed = 0 @@ -117,6 +117,9 @@ eq(uptime(2 * 86400 + 5 * 3600), "2 天 5 小时", "超过一天不再写分钟" eq(despike([180, 181, 180, 180, 181, 180, 180]), [180, 181, 180, 180, 181, 180, 180], "1 ms 抖动原样保留") } +// 旧 hub 存名称,新 hub 把其余长度存成 `m`;两种写法同一个长度读法一致。 +eq(["yearly", "12m", "60m", "18m", "once", "weekly"].map(cycle), ["年付", "年付", "5 年付", "18 个月付", "一次性", "weekly"], "付款周期") + eq(osName("Debian GNU/Linux 12 (bookworm)"), "Debian 12", "发行版名去掉代号") eq(cpuName("Intel(R) Xeon(R) CPU E5-2680 8-Core Processor"), "Intel Xeon E5-2680", "CPU 名去掉商标和核数") diff --git a/src/lib/format.ts b/src/lib/format.ts index ea2953c..1cb013c 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -84,14 +84,17 @@ export function money(amount: number, currency: string): string { return `${SYMBOLS[currency] ?? ""}${amount.toFixed(2)}${SYMBOLS[currency] ? "" : ` ${currency}`}` } -export const CYCLES: Record = { - monthly: "月付", - quarterly: "季付", - semiannual: "半年付", - yearly: "年付", - biennial: "两年付", - triennial: "三年付", - once: "一次性", +// Hub 1.3.0 and earlier store only these names; later hubs store any other +// length as `m`. +const NAMED_CYCLES: Record = { monthly: 1, quarterly: 3, semiannual: 6, yearly: 12, biennial: 24, triennial: 36 } +const CYCLE_WORDS: Record = { 1: "月付", 3: "季付", 6: "半年付", 12: "年付" } + +/** How a billing cycle reads: 月付, 5 年付, 18 个月付, 一次性. */ +export function cycle(billing: string): string { + if (billing === "once") return "一次性" + const months = NAMED_CYCLES[billing] ?? Number(/^(\d+)m$/.exec(billing)?.[1]) + if (!months) return billing + return CYCLE_WORDS[months] ?? (months % 12 ? `${months} 个月付` : `${months / 12} 年付`) } // Hoisted out of `clock`: recharts calls a tickFormatter for every sample when