feat: 网络出口列表改为表格展示

This commit is contained in:
2026-09-03 18:39:29 +08:00
parent de91d7a882
commit fdbb37ad48
+91 -57
View File
@@ -15,6 +15,7 @@ import {
PageState, PageState,
Select, Select,
StatusPill, StatusPill,
Table,
conflictMessage, conflictMessage,
} from "./lib/ui.jsx"; } from "./lib/ui.jsx";
import { useTitle } from "./lib/hooks.js"; import { useTitle } from "./lib/hooks.js";
@@ -239,50 +240,90 @@ function ExitCreateModal({ open, onClose, onSubmit, busy, error }) {
); );
} }
function ExitCard({ exit, boundAccounts, bindingsError, busy, onAction }) { function ExitTable({ exits, accountsByExit, bindingsError, busy, onAction }) {
return ( const columns = [
<Card> {
<CardContent className="space-y-3"> header: "出口",
<div className="flex min-w-0 items-start justify-between gap-3"> width: "20%",
<div className="min-w-0"> render: (exit) => (
<Link <div className="min-w-0">
to={`/network-exits/${encodeURIComponent(exit.id)}`} <Link
className="anywhere font-semibold text-ink hover:text-primary" to={`/network-exits/${encodeURIComponent(exit.id)}`}
> className="anywhere font-semibold text-ink hover:text-primary"
{exit.protocol}://{exit.host}:{exit.port} >
</Link> {exit.protocol}://{exit.host}:{exit.port}
<p className="anywhere text-xs text-muted"> </Link>
{exit.id} · 用户名 {exit.username || "无"} · 密码{" "} <p className="anywhere text-xs text-muted">{exit.id}</p>
{exit.password || "无"}
</p>
</div>
<ExitHealthPill exit={exit} />
</div> </div>
<div className="text-sm"> ),
<span className="text-muted">出口IP:</span> },
{
header: "状态",
width: "9%",
render: (exit) => <ExitHealthPill exit={exit} />,
},
{
header: "用户名",
width: "11%",
render: (exit) => (
<span className="anywhere">{exit.username || "无"}</span>
),
},
{
header: "密码",
width: "11%",
render: (exit) => (
<span className="anywhere">{exit.password || "无"}</span>
),
},
{
header: "出口IP",
width: "13%",
render: (exit) => (
<span className="anywhere">{exit.observed_public_ip || "尚无观测"}</span>
),
},
{
header: "最近检测",
width: "15%",
render: (exit) =>
exit.last_checked_at
? new Date(exit.last_checked_at).toLocaleString("zh-CN")
: "未检测",
},
{
header: "绑定账号",
width: "13%",
render: (exit) => {
if (bindingsError) return <span className="text-muted">状态未知</span>;
const accounts = accountsByExit.get(exit.id);
return (
<span className="anywhere"> <span className="anywhere">
{exit.observed_public_ip || "尚无观测"} {accounts?.length ? accounts.join("、") : "未绑定"}
</span> </span>
</div> );
<p className="text-xs text-muted"> },
最近检测: },
{exit.last_checked_at {
? new Date(exit.last_checked_at).toLocaleString("zh-CN") header: "操作",
: "未检测"} width: "8%",
</p> align: "right",
<div className="text-sm"> render: (exit) => (
<span className="text-muted">绑定账号:</span> <ExitActions
{bindingsError ? ( exit={exit}
"状态未知" busy={busy === exit.id}
) : ( onAction={onAction}
<span className="anywhere"> />
{boundAccounts?.length ? boundAccounts.join("、") : "未绑定"} ),
</span> },
)} ];
</div> return (
<ExitActions exit={exit} busy={busy} onAction={onAction} /> <Table
</CardContent> columns={columns}
</Card> rows={exits}
rowKey={(exit) => exit.id}
minWidth="1100px"
/>
); );
} }
@@ -402,22 +443,15 @@ export function NetworkExitList() {
empty={exits.length === 0} empty={exits.length === 0}
emptyText="暂无网络出口。创建并检测健康后,才能绑定运行环境。" emptyText="暂无网络出口。创建并检测健康后,才能绑定运行环境。"
> >
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <ExitTable
{exits.map((exit) => ( exits={exits}
<ExitCard accountsByExit={accountsByExit}
key={exit.id} bindingsError={!!bindingsError}
exit={exit} busy={busy}
boundAccounts={accountsByExit.get(exit.id)} onAction={(exit, action) =>
bindingsError={!!bindingsError} action === "disable" ? setDisabling(exit) : runAction(exit, action)
busy={busy === exit.id} }
onAction={(exit, action) => />
action === "disable"
? setDisabling(exit)
: runAction(exit, action)
}
/>
))}
</div>
</PageState> </PageState>
<ExitCreateModal <ExitCreateModal
open={createOpen} open={createOpen}