polish: redesign node admin UI

This commit is contained in:
JetSprow
2026-04-30 22:09:21 +10:00
parent c5592621a4
commit 157f3841f6
13 changed files with 399 additions and 195 deletions

View File

@@ -3,6 +3,7 @@
import { Waypoints } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { EmptyState } from "@/components/shared/page-shell";
import { StatusBadge } from "@/components/shared/status-badge";
import { InboundDeleteButton } from "../../../inbound-delete-button";
import { InboundDisplayNameForm } from "../../../inbound-display-name-form";
import type { NodeDetail } from "../../node-detail-data";
@@ -16,52 +17,77 @@ function getDisplayName(inbound: { tag: string; settings: unknown }) {
return inbound.tag;
}
function streamValue(settings: unknown, key: string) {
if (!settings || typeof settings !== "object") return null;
const value = (settings as Record<string, unknown>)[key];
if (typeof value !== "string" || !value.trim()) return null;
return value;
}
function InboundMeta({
label,
value,
}: {
label: string;
value: string | number;
}) {
return (
<span className="inline-flex min-h-8 items-center gap-1.5 rounded-lg border border-border bg-muted/25 px-2.5 text-xs">
<span className="text-muted-foreground">{label}</span>
<span className="font-semibold text-foreground">{value}</span>
</span>
);
}
export function InboundsTab({ node }: { node: NodeDetail }) {
if (node.inbounds.length === 0) {
return (
<EmptyState
title="暂无已同步入站"
description="在 3x-ui 创建后回到节点列表同步。"
/>
<div className="surface-card rounded-xl p-5">
<EmptyState
title="暂无已同步入站"
description="同步节点后会显示可售入口。"
/>
</div>
);
}
return (
<div className="space-y-4 pt-4">
<p className="rounded-lg border border-border bg-muted/30 px-4 py-3 text-xs text-muted-foreground">
3x-ui
</p>
<div className="surface-card divide-y divide-border/60 overflow-hidden rounded-xl">
{node.inbounds.map((inbound) => (
<section key={inbound.id} className="grid gap-3 px-4 py-3 lg:grid-cols-[minmax(0,1fr)_minmax(14rem,0.6fr)_auto] lg:items-center">
<div className="flex min-w-0 items-center gap-2.5">
<Waypoints className="size-4 shrink-0 text-primary" />
<InboundDisplayNameForm
inboundId={inbound.id}
defaultValue={getDisplayName(inbound)}
/>
<div className="surface-card divide-y divide-border/60 overflow-hidden rounded-xl">
{node.inbounds.map((inbound) => {
const network = streamValue(inbound.streamSettings, "network");
const security = streamValue(inbound.streamSettings, "security");
return (
<article key={inbound.id} className="grid gap-4 px-4 py-4 xl:grid-cols-[minmax(0,1fr)_minmax(18rem,0.75fr)_auto] xl:items-center">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
<Waypoints className="size-4" />
</span>
<div className="min-w-0 flex-1 space-y-2">
<div className="flex min-h-6 flex-wrap items-center gap-2">
<StatusBadge tone="info">{inbound.protocol}</StatusBadge>
<Badge variant="outline">:{inbound.port}</Badge>
<span className="min-w-0 truncate text-xs font-medium text-muted-foreground">{inbound.tag}</span>
</div>
<InboundDisplayNameForm
inboundId={inbound.id}
defaultValue={getDisplayName(inbound)}
/>
</div>
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span>: {inbound.clients.length}</span>
{inbound.streamSettings && typeof inbound.streamSettings === "object" && (
<>
{(inbound.streamSettings as Record<string, unknown>).network && (
<span>: {String((inbound.streamSettings as Record<string, unknown>).network)}</span>
)}
{(inbound.streamSettings as Record<string, unknown>).security && (
<span>: {String((inbound.streamSettings as Record<string, unknown>).security)}</span>
)}
</>
)}
<div className="flex flex-wrap gap-2">
<InboundMeta label="客户端" value={inbound.clients.length} />
{network && <InboundMeta label="传输" value={network} />}
{security && <InboundMeta label="安全" value={security} />}
</div>
<div className="flex items-center gap-2 lg:justify-end">
<Badge variant="secondary">{inbound.protocol}</Badge>
<Badge variant="outline">:{inbound.port}</Badge>
<div className="flex justify-start xl:justify-end">
<InboundDeleteButton inboundId={inbound.id} />
</div>
</section>
))}
</div>
</article>
);
})}
</div>
);
}