feat: 网络出口列表改为表格展示
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user