mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
polish: redesign node admin UI
This commit is contained in:
@@ -1,20 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { StatusBadge } from "@/components/shared/status-badge";
|
||||
import type { NodeDetail } from "../node-detail-data";
|
||||
import { InboundsTab } from "./tabs/inbounds-tab";
|
||||
|
||||
export function NodeDetailTabs({ node }: { node: NodeDetail }) {
|
||||
return (
|
||||
<Tabs defaultValue="inbounds">
|
||||
<TabsList variant="line" className="w-full overflow-x-auto">
|
||||
<TabsTrigger value="inbounds">
|
||||
3x-ui 入站 ({node.inbounds.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="inbounds">
|
||||
<InboundsTab node={node} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<section className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium tracking-wide text-muted-foreground">线路入口</p>
|
||||
<h2 className="text-lg font-semibold tracking-tight">3x-ui 入站</h2>
|
||||
</div>
|
||||
<StatusBadge tone={node.inbounds.length > 0 ? "info" : "neutral"}>
|
||||
{node.inbounds.length} 个
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<InboundsTab node={node} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user