Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/NodeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -266,7 +266,7 @@ function Price({ node }: { node: Node }) {
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
{icon}
<span className="tnum truncate">
{money(node.price, node.currency)} / {CYCLES[node.billing_cycle] ?? node.billing_cycle}
{money(node.price, node.currency)} / {cycle(node.billing_cycle)}
</span>
</span>
)
Expand Down
4 changes: 2 additions & 2 deletions src/components/NodeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(" · ")}
Expand Down
4 changes: 2 additions & 2 deletions src/components/NodeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -107,7 +107,7 @@ const COLUMNS: {
className: "hidden lg:table-cell",
cell: (n) => (
<span className="block truncate text-xs text-muted-foreground">
{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)}` : "免费"}
</span>
),
},
Expand Down
10 changes: 9 additions & 1 deletion src/lib/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 把其余长度存成 `<n>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 校验未通过")
Expand Down
20 changes: 12 additions & 8 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<n>m`, which reads as `5 年` or `18 个月` in the same style.
*/
export const CYCLES: Record<string, string> = {
monthly: "月",
quarterly: "季",
semiannual: "半年",
yearly: "年",
biennial: "两年",
triennial: "三年",
once: "一次性",
const NAMED_CYCLES: Record<string, number> = { monthly: 1, quarterly: 3, semiannual: 6, yearly: 12, biennial: 24, triennial: 36 }
const CYCLE_WORDS: Record<number, string> = { 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
Expand Down
Loading