From ab485fa2572d832647145e91dd309072fd5799ff Mon Sep 17 00:00:00 2001 From: jlthzy Date: Tue, 29 Sep 2026 23:45:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=AD=E8=B4=B9=E4=B8=80=E6=A0=8F?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E4=BB=BB=E6=84=8F=E4=BB=98=E6=AC=BE=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=EF=BC=885=20=E5=B9=B4=E3=80=8118=20=E4=B8=AA=E6=9C=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hub 现在把没有名字的周期存成 `m`,而主题只会认 `monthly`…`triennial` 那六个名字,于是 18 个月会原样显示成「18m」。改成先按名字或 `m` 解析成 月数,再按同一个风格写出来:`5 年`、`18 个月`。 措辞保持我们原来的**纯周期**风格(`/ 年`、`/ 月`,不带「付」),没有采用上游 主题的「年付/月付」:那两行注释写着「付」字正是价格放不下被省略号截掉的原因。 已命名的长度一个字都不变(`两年`、`三年` 仍在表里),只有新长度是新写法。 三处调用(卡片、表格、详情页)一起改;上游只改了详情页,我们的卡片和表格也 渲染周期。测试补一条:同一个长度的名字写法与 `m` 写法读出来一致,认不出的 周期原样显示、不吞掉。 --- src/components/NodeCard.tsx | 4 ++-- src/components/NodeDetail.tsx | 4 ++-- src/components/NodeTable.tsx | 4 ++-- src/lib/format.test.ts | 10 +++++++++- src/lib/format.ts | 20 ++++++++++++-------- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx index 35c2210..6273bcf 100644 --- a/src/components/NodeCard.tsx +++ b/src/components/NodeCard.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge" import { Card } from "@/components/ui/card" import { Meter } from "@/components/Meter" import type { Node } from "@/lib/api" -import { bytes, CYCLES, daysUntil, FOREVER, money, osName, pair, percent, rate, uptime } from "@/lib/format" +import { bytes, cycle, daysUntil, FOREVER, money, osName, pair, percent, rate, uptime } from "@/lib/format" import { cn } from "@/lib/utils" // Emitted as files and fetched on first use, so a page carries only the flags its @@ -266,7 +266,7 @@ function Price({ node }: { node: Node }) { {icon} - {money(node.price, node.currency)} / {CYCLES[node.billing_cycle] ?? node.billing_cycle} + {money(node.price, node.currency)} / {cycle(node.billing_cycle)} ) diff --git a/src/components/NodeDetail.tsx b/src/components/NodeDetail.tsx index d8be1fc..68e7df4 100644 --- a/src/components/NodeDetail.tsx +++ b/src/components/NodeDetail.tsx @@ -15,7 +15,7 @@ import { ChartTooltip, Crosshair, Swatch } from "@/components/ChartTooltip" 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, despike, quarters, cpuName, cycle, FOREVER, money, osName, rate, stamp, timeTicks, } from "@/lib/format" import type { Availability } from "@/lib/uptime" @@ -556,7 +556,7 @@ export function NodeDetail({ node, showPeak, onBack }: { 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/components/NodeTable.tsx b/src/components/NodeTable.tsx index 4950d69..d5e502c 100644 --- a/src/components/NodeTable.tsx +++ b/src/components/NodeTable.tsx @@ -3,7 +3,7 @@ import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react" import { Country, StatusDot, monthUsage, statusLabel } from "@/components/NodeCard" import type { Node } from "@/lib/api" -import { CYCLES, FOREVER, bytes, daysUntil, money, osName, pair, percent, rate } from "@/lib/format" +import { cycle, FOREVER, bytes, daysUntil, money, osName, pair, percent, rate } from "@/lib/format" import { cn } from "@/lib/utils" type SortKey = "name" | "cpu" | "mem" | "disk" | "traffic" | "expiry" @@ -107,7 +107,7 @@ const COLUMNS: { className: "hidden lg:table-cell", cell: (n) => ( - {n.price > 0 ? `${money(n.price, n.currency)} / ${CYCLES[n.billing_cycle] ?? n.billing_cycle}` : "免费"} + {n.price > 0 ? `${money(n.price, n.currency)} / ${cycle(n.billing_cycle)}` : "免费"} ), }, diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 3ce3920..5510f1a 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 @@ -121,6 +121,14 @@ eq(uptime(3 * 3600), "3 小时", "整小时的零头不写") 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 名去掉商标和核数") +// 旧 hub 存名称,新 hub 把其余长度存成 `m`;同一个长度两种写法读出来一样, +// 而认不出的周期原样显示,不吞掉。 +eq( + ["monthly", "yearly", "12m", "60m", "18m", "biennial", "36m", "once", "weekly"].map(cycle), + ["月", "年", "年", "5 年", "18 个月", "两年", "三年", "一次性", "weekly"], + "付款周期", +) + if (failed) { console.error(`\n${failed} 项不通过`) throw new Error("format 校验未通过") diff --git a/src/lib/format.ts b/src/lib/format.ts index 8e3bc5a..b9f5407 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -94,15 +94,19 @@ export function money(amount: number, currency: string): string { * prints it after a slash -- `$39.90 / 年` is per year, and the "付" that used to * follow it was the difference between the price fitting its column and being cut * off with an ellipsis. + * + * Older hubs store only the named lengths below; later ones store any other + * length as `m`, which reads as `5 年` or `18 个月` in the same style. */ -export const CYCLES: Record = { - monthly: "月", - quarterly: "季", - semiannual: "半年", - yearly: "年", - biennial: "两年", - triennial: "三年", - once: "一次性", +const NAMED_CYCLES: Record = { monthly: 1, quarterly: 3, semiannual: 6, yearly: 12, biennial: 24, triennial: 36 } +const CYCLE_WORDS: Record = { 1: "月", 3: "季", 6: "半年", 12: "年", 24: "两年", 36: "三年" } + +/** How a billing cycle reads after the slash: 月, 年, 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