Initial commit

This commit is contained in:
JetSprow
2026-04-29 05:12:39 +10:00
commit 27dbca9cbf
379 changed files with 43486 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
"use client";
import { Activity, Clock3, RefreshCw, Route } from "lucide-react";
import { useLatencyRefreshMeta, type LatencyItem } from "./latency-loader";
import type { TraceItem } from "./trace-loader";
import { cn } from "@/lib/utils";
const carrierLabels: Record<string, string> = {
telecom: "电信",
unicom: "联通",
mobile: "移动",
};
const CARRIER_ORDER: string[] = ["telecom", "unicom", "mobile"];
function sortByCarrier<T extends { carrier: string }>(items: T[]): T[] {
return [...items].sort(
(a, b) => (CARRIER_ORDER.indexOf(a.carrier) >>> 0) - (CARRIER_ORDER.indexOf(b.carrier) >>> 0),
);
}
export function getCarrierLabel(carrier: string) {
return carrierLabels[carrier] ?? carrier.replace("中国", "");
}
function formatRefreshLabel(meta: ReturnType<typeof useLatencyRefreshMeta>) {
if (meta.loading) return "正在刷新";
if (!meta.updatedAt) return "约 1 分钟更新";
const updatedAt = new Date(meta.updatedAt);
const nextRefreshAt = meta.nextRefreshAt ? new Date(meta.nextRefreshAt) : null;
const updated = updatedAt.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
const next = nextRefreshAt?.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
return next ? `${updated} 更新 · ${next} 再刷` : `${updated} 已更新`;
}
export function ProxySignalPanel({
latencyItems,
traceItems,
onTraceSelect,
onLatencyClick,
}: {
latencyItems: LatencyItem[];
traceItems: TraceItem[];
onTraceSelect: (item: TraceItem) => void;
onLatencyClick?: () => void;
}) {
const refreshMeta = useLatencyRefreshMeta();
if (latencyItems.length === 0 && traceItems.length === 0) {
return (
<div className="rounded-lg border border-dashed border-border bg-muted/20 p-4 text-sm leading-6 text-muted-foreground">
线
</div>
);
}
return (
<div className="space-y-4 rounded-xl border border-border bg-muted/30 p-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="inline-flex items-center gap-2 text-sm font-semibold">
<Activity className="size-4 text-primary" /> 线
</p>
<p className="mt-1 text-xs text-muted-foreground">访线</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="inline-flex items-center gap-1.5 rounded-full border border-primary/15 bg-primary/10 px-2.5 py-1 text-[0.68rem] font-semibold text-primary">
{refreshMeta.loading ? <RefreshCw className="size-3 animate-spin" /> : <Clock3 className="size-3" />}
{formatRefreshLabel(refreshMeta)}
</div>
{refreshMeta.error && (
<div className="rounded-full border border-amber-500/20 bg-amber-500/10 px-2.5 py-1 text-[0.68rem] font-semibold text-amber-700 dark:text-amber-300">
</div>
)}
</div>
</div>
{latencyItems.length > 0 && <ProxyLatencyGrid items={latencyItems} onClick={onLatencyClick} />}
{traceItems.length > 0 && <ProxyTraceGrid items={traceItems} onTraceSelect={onTraceSelect} />}
</div>
);
}
export function ProxyLatencyGrid({ items, onClick }: { items: LatencyItem[]; onClick?: () => void }) {
if (items.length === 0) return null;
const sorted = sortByCarrier(items);
const bestLatency = Math.min(...sorted.map((item) => item.latencyMs));
return (
<div className="space-y-2">
<p className="inline-flex items-center gap-2 text-xs font-semibold tracking-[0.14em] text-muted-foreground">
<Activity className="size-3.5" /> {onClick && <span className="font-normal">· </span>}
</p>
<div className={cn("grid grid-cols-3 gap-2", onClick && "cursor-pointer")} onClick={onClick}>
{sorted.map((item) => {
const strong = item.latencyMs === bestLatency;
return (
<div
key={item.carrier}
className={cn(
"rounded-lg border px-3 py-3 text-center transition-colors duration-200",
strong ? "border-primary/20 bg-primary/10 text-primary" : "border-border bg-background",
onClick && "hover:border-primary/25 hover:bg-primary/7",
)}
>
<p className="text-[11px] font-semibold leading-tight text-muted-foreground">
{getCarrierLabel(item.carrier)}
</p>
<p className="mt-1 text-xl font-semibold tracking-tight tabular-nums">
{item.latencyMs}
<span className="ml-0.5 text-xs font-normal text-muted-foreground">ms</span>
</p>
{strong && <p className="mt-1 text-[10px] font-semibold tracking-[0.14em]">BEST</p>}
</div>
);
})}
</div>
</div>
);
}
export function ProxyTraceGrid({
items,
onTraceSelect,
}: {
items: TraceItem[];
onTraceSelect: (item: TraceItem) => void;
}) {
if (items.length === 0) return null;
const sorted = sortByCarrier(items);
return (
<div className="space-y-2">
<p className="inline-flex items-center gap-2 text-xs font-semibold tracking-[0.14em] text-muted-foreground">
<Route className="size-3.5" /> 访
</p>
<div className="grid gap-2 sm:grid-cols-3">
{sorted.map((item) => (
<button
key={item.carrier}
type="button"
className="group rounded-lg border border-border bg-background px-3 py-3 text-left transition-colors duration-200 hover:border-primary/25 hover:bg-primary/7 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20"
onClick={(event) => {
event.stopPropagation();
onTraceSelect(item);
}}
>
<p className="text-[11px] font-semibold text-muted-foreground">
{getCarrierLabel(item.carrier)} · {item.hopCount}
</p>
<p className="mt-1 truncate text-xs font-semibold tracking-tight group-hover:text-primary">
{item.summary}
</p>
</button>
))}
</div>
</div>
);
}