mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
Polish admin list UI for lite
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Waypoints } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { EmptyState } from "@/components/shared/page-shell";
|
||||
import { InboundDeleteButton } from "../../../inbound-delete-button";
|
||||
@@ -32,41 +31,35 @@ export function InboundsTab({ node }: { node: NodeDetail }) {
|
||||
<p className="rounded-lg border border-border bg-muted/30 px-4 py-3 text-xs text-muted-foreground">
|
||||
入站配置由 3x-ui 面板维护;本页仅展示已同步的线路,并允许调整前台展示名称。
|
||||
</p>
|
||||
<div className="grid gap-3">
|
||||
<div className="surface-card divide-y divide-border/60 overflow-hidden rounded-xl">
|
||||
{node.inbounds.map((inbound) => (
|
||||
<Card key={inbound.id}>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-3 pb-2">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<Waypoints className="size-4 shrink-0 text-primary" />
|
||||
<CardTitle className="text-sm">
|
||||
<InboundDisplayNameForm
|
||||
inboundId={inbound.id}
|
||||
defaultValue={getDisplayName(inbound)}
|
||||
/>
|
||||
</CardTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{inbound.protocol}</Badge>
|
||||
<Badge variant="outline">:{inbound.port}</Badge>
|
||||
<InboundDeleteButton inboundId={inbound.id} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-x-6 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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
<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>
|
||||
<div className="flex items-center gap-2 lg:justify-end">
|
||||
<Badge variant="secondary">{inbound.protocol}</Badge>
|
||||
<Badge variant="outline">:{inbound.port}</Badge>
|
||||
<InboundDeleteButton inboundId={inbound.id} />
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { batchTestNodeConnections } from "@/actions/admin/nodes";
|
||||
import { BatchActionBar, BatchActionButton } from "@/components/admin/batch-action-bar";
|
||||
import { EmptyState } from "@/components/shared/page-shell";
|
||||
import { StatusBadge } from "@/components/shared/status-badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getNodeStatusLabel } from "@/lib/domain-labels";
|
||||
import { InboundDeleteButton } from "../inbound-delete-button";
|
||||
import { InboundDisplayNameForm } from "../inbound-display-name-form";
|
||||
@@ -16,7 +15,7 @@ const NODE_BATCH_FORM_ID = "node-batch-form";
|
||||
|
||||
function PanelInfoBar({ node }: { node: NodeServerRow }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-muted/25 px-4 py-3 text-xs text-muted-foreground">
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">3x-ui</span>
|
||||
<span>{node.panelUrl || "未配置面板"}</span>
|
||||
{node.agentToken && <span>探测 Token: 已启用</span>}
|
||||
@@ -26,75 +25,78 @@ function PanelInfoBar({ node }: { node: NodeServerRow }) {
|
||||
|
||||
function NodeCard({ node, siteUrl }: { node: NodeServerRow; siteUrl: string | null }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-4 pb-2 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<input
|
||||
form={NODE_BATCH_FORM_ID}
|
||||
type="checkbox"
|
||||
name="nodeIds"
|
||||
value={node.id}
|
||||
aria-label={`选择节点 ${node.name}`}
|
||||
className="mt-3 size-5 rounded-lg border-border accent-primary shadow-sm"
|
||||
/>
|
||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
|
||||
<Server className="size-5" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="text-lg">
|
||||
<section className="grid gap-4 px-4 py-4 xl:grid-cols-[minmax(0,1fr)_minmax(14rem,0.55fr)_minmax(20rem,0.95fr)_auto] xl:items-start">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<input
|
||||
form={NODE_BATCH_FORM_ID}
|
||||
type="checkbox"
|
||||
name="nodeIds"
|
||||
value={node.id}
|
||||
aria-label={`选择节点 ${node.name}`}
|
||||
className="mt-2 size-5 rounded-lg border-border accent-primary shadow-sm"
|
||||
/>
|
||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
|
||||
<Server className="size-4" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="truncate text-base font-semibold tracking-tight">
|
||||
<Link href={`/admin/nodes/${node.id}`} className="hover:underline">
|
||||
{node.name}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{node.panelUrl || "未配置面板"} · {node._count.inbounds} 个入站
|
||||
</p>
|
||||
</h3>
|
||||
<StatusBadge tone={node.status === "active" ? "success" : "neutral"}>
|
||||
{getNodeStatusLabel(node.status)}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-sm leading-6 text-muted-foreground">
|
||||
{node.panelUrl || "未配置面板"} · {node._count.inbounds} 个入站
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={node.status === "active" ? "success" : "neutral"}>
|
||||
{getNodeStatusLabel(node.status)}
|
||||
</StatusBadge>
|
||||
<NodeForm
|
||||
node={{
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
panelUrl: node.panelUrl,
|
||||
panelUsername: node.panelUsername,
|
||||
}}
|
||||
triggerLabel="编辑"
|
||||
triggerVariant="outline"
|
||||
/>
|
||||
<NodeActions
|
||||
node={{ id: node.id, name: node.name, agentToken: node.agentToken }}
|
||||
siteUrl={siteUrl}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<PanelInfoBar node={node} />
|
||||
{node.inbounds.length > 0 ? (
|
||||
<div className="grid gap-2 rounded-lg bg-muted/20 p-3">
|
||||
{node.inbounds.map((inbound) => (
|
||||
<div
|
||||
key={inbound.id}
|
||||
className="flex min-w-0 flex-wrap items-center gap-2 border-b border-border/50 pb-2 text-xs font-medium last:border-b-0 last:pb-0"
|
||||
>
|
||||
<Waypoints className="size-3.5 shrink-0 text-primary" />
|
||||
<span className="shrink-0 text-muted-foreground">{inbound.protocol} · {inbound.port}</span>
|
||||
<InboundDisplayNameForm
|
||||
inboundId={inbound.id}
|
||||
defaultValue={getInboundDisplayName(inbound)}
|
||||
/>
|
||||
<InboundDeleteButton inboundId={inbound.id} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-lg border border-dashed border-border bg-muted/20 px-4 py-3 text-xs text-muted-foreground">暂无已同步入站,请在 3x-ui 创建入站后点击同步</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{node.inbounds.length > 0 ? (
|
||||
<div className="grid gap-2 rounded-lg bg-muted/20 p-3">
|
||||
{node.inbounds.map((inbound) => (
|
||||
<div
|
||||
key={inbound.id}
|
||||
className="flex min-w-0 flex-wrap items-center gap-2 border-b border-border/50 pb-2 text-xs font-medium last:border-b-0 last:pb-0"
|
||||
>
|
||||
<Waypoints className="size-3.5 shrink-0 text-primary" />
|
||||
<span className="shrink-0 text-muted-foreground">{inbound.protocol} · {inbound.port}</span>
|
||||
<InboundDisplayNameForm
|
||||
inboundId={inbound.id}
|
||||
defaultValue={getInboundDisplayName(inbound)}
|
||||
/>
|
||||
<InboundDeleteButton inboundId={inbound.id} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-lg border border-dashed border-border bg-muted/20 px-4 py-3 text-xs text-muted-foreground">暂无已同步入站,请在 3x-ui 创建入站后点击同步</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 xl:justify-end">
|
||||
<NodeForm
|
||||
node={{
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
panelUrl: node.panelUrl,
|
||||
panelUsername: node.panelUsername,
|
||||
}}
|
||||
triggerLabel="编辑"
|
||||
triggerVariant="outline"
|
||||
/>
|
||||
<NodeActions
|
||||
node={{ id: node.id, name: node.name, agentToken: node.agentToken }}
|
||||
siteUrl={siteUrl}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,16 +106,18 @@ export function NodeCardList({ nodes, siteUrl }: { nodes: NodeServerRow[]; siteU
|
||||
<BatchActionBar id={NODE_BATCH_FORM_ID} action={batchTestNodeConnections}>
|
||||
<BatchActionButton>批量同步入站</BatchActionButton>
|
||||
</BatchActionBar>
|
||||
<div className="grid gap-5">
|
||||
<div className="surface-card divide-y divide-border/60 overflow-hidden rounded-xl">
|
||||
{nodes.map((node) => (
|
||||
<NodeCard key={node.id} node={node} siteUrl={siteUrl} />
|
||||
))}
|
||||
{nodes.length === 0 && (
|
||||
<EmptyState
|
||||
title="暂无节点"
|
||||
description="添加 3x-ui 节点后,可以同步入站并绑定到代理套餐。"
|
||||
action={<NodeForm triggerLabel="添加节点" />}
|
||||
/>
|
||||
<div className="p-5">
|
||||
<EmptyState
|
||||
title="暂无节点"
|
||||
description="添加 3x-ui 节点后,可以同步入站并绑定到代理套餐。"
|
||||
action={<NodeForm triggerLabel="添加节点" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -27,7 +27,7 @@ interface NodeActionValue {
|
||||
agentToken: string | null;
|
||||
}
|
||||
|
||||
const INSTALL_SCRIPT_URL = "https://raw.githubusercontent.com/JetSprow/J-Board/lite/scripts/install-jboard-agent.sh";
|
||||
const INSTALL_SCRIPT_URL = "https://raw.githubusercontent.com/JetSprow/J-Board-Lite/main/scripts/install-jboard-agent.sh";
|
||||
|
||||
function shellQuote(value: string) {
|
||||
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
||||
|
||||
Reference in New Issue
Block a user