diff --git a/src/components/ServerTable.tsx b/src/components/ServerTable.tsx index 84a0fea..7ae5078 100644 --- a/src/components/ServerTable.tsx +++ b/src/components/ServerTable.tsx @@ -8,7 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { groupsOf, type Node } from "@/lib/api" import { - bytes, compact, CYCLES, distro, duration, expiresIn, FOREVER, money, monthUsage, osName, cpuName, pair, + bytes, compact, cycle, distro, duration, expiresIn, FOREVER, money, monthUsage, osName, cpuName, pair, percent, rate, uptime, } from "@/lib/format" import { Link } from "@/lib/route" @@ -191,7 +191,7 @@ function Details({ node }: { node: Node }) { {node.online ? (m ? uptime(m.uptime) : "等待上报") : away >= 60 ? uptime(away) : "刚刚"} - {node.price > 0 ? `${money(node.price, node.currency)} / ${CYCLES[node.billing_cycle] ?? node.billing_cycle}` : "免费"} + {node.price > 0 ? `${money(node.price, node.currency)} / ${cycle(node.billing_cycle)}` : "免费"} {node.expires_at diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index b1f3f20..2627a39 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, compact, cpuName, daysUntil, despike, distro, duration, expiresIn, monthUsage, osName, pair, + axisBytes, axisTop, bytes, compact, cpuName, cycle, daysUntil, despike, distro, duration, expiresIn, monthUsage, osName, pair, quarters, timeTicks, uptime, } from "./format.ts" @@ -152,6 +152,9 @@ eq(duration(76 * 86400 + 5), "76 天", "超过一天只写天数") eq(despike([20, 900, 20, 20, 20], 3)[1], 20, "窗口可以调小") } +// 旧 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 2639e3a..c222bbe 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -138,14 +138,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