refactor: use original dashboard UI primitives

This commit is contained in:
2026-09-20 17:14:13 +08:00
parent a25e84ac14
commit 59f170c191
32 changed files with 1981 additions and 2179 deletions
@@ -1,23 +1,14 @@
import { useEffect, useMemo, useState } from "react";
import { Link, useParams } from "../../shared/router.jsx";
import { useDataProvider, useList, useOne } from "../../shared/hooks/dataHooks.js";
import {
Alert,
Button,
Card,
CardContent,
ConfirmDialog,
DetailList,
Field,
Input,
Modal,
PageHeader,
PageState,
Select,
StatusPill,
Table,
conflictMessage,
} from "../../shared/ui/ui.jsx";
import { Alert } from "../../components/ui/alert";
import { Button } from "../../components/ui/button";
import { Card, CardContent } from "../../components/ui/card";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../components/ui/table";
import { ConfirmDialog, DetailList, Field, PageHeader, PageState, StatusPill, conflictMessage } from "../../shared/ui/ui.jsx";
import { useTitle } from "../../shared/hooks/hooks.js";
const createInitial = {
@@ -63,31 +54,18 @@ function ExitActions({ exit, busy, onAction, onEdit, onDelete }) {
const disabled = busy;
return (
<div className="flex flex-wrap gap-2">
<Button
size="sm"
icon="ri-refresh-line"
disabled={disabled}
onClick={() => onAction(exit, "check")}
>
<Button size="sm" disabled={disabled} onClick={() => onAction(exit, "check")}>
<i className="ri-refresh-line" aria-hidden="true" />
检测
</Button>
{exit.health_status === "disabled" ? (
<Button
size="sm"
icon="ri-checkbox-circle-line"
disabled={disabled}
onClick={() => onAction(exit, "enable")}
>
<Button size="sm" disabled={disabled} onClick={() => onAction(exit, "enable")}>
<i className="ri-checkbox-circle-line" aria-hidden="true" />
启用
</Button>
) : (
<Button
size="sm"
variant="danger"
icon="ri-forbid-line"
disabled={disabled}
onClick={() => onAction(exit, "disable")}
>
<Button size="sm" variant="destructive" disabled={disabled} onClick={() => onAction(exit, "disable")}>
<i className="ri-forbid-line" aria-hidden="true" />
停用
</Button>
)}
@@ -160,31 +138,10 @@ function ExitCreateModal({
}
return (
<Modal
open={open}
onClose={onClose}
title={title}
labelledBy="exit-create-title"
size="lg"
footer={
<>
<Button onClick={onClose} disabled={busy}>
取消
</Button>
<Button
variant="primary"
type="submit"
form="exit-create-form"
busy={busy}
busyText="创建中…"
disabled={!valid}
>
{submitLabel}
</Button>
</>
}
>
<form id="exit-create-form" onSubmit={submit} noValidate>
<Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader><DialogTitle>{title}</DialogTitle></DialogHeader>
<form id="exit-create-form" onSubmit={submit} noValidate>
{error ? (
<Alert variant="destructive" className="mb-4">
{conflictMessage(
@@ -195,12 +152,12 @@ function ExitCreateModal({
) : null}
<div className="grid gap-4 sm:grid-cols-3">
<Field id="exit-protocol" label="协议" required helper="代理协议">
<Select
id="exit-protocol"
value={form.protocol}
onChange={(event) => update("protocol", event.target.value)}
options={protocolOptions}
/>
<Select value={form.protocol} onValueChange={(value) => update("protocol", value)}>
<SelectTrigger id="exit-protocol"><SelectValue /></SelectTrigger>
<SelectContent>
{protocolOptions.map((option) => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}
</SelectContent>
</Select>
</Field>
<Field id="exit-host" label="主机" required helper="代理服务器地址">
<Input
@@ -248,7 +205,7 @@ function ExitCreateModal({
maxLength={255}
value={form.password}
onChange={(event) => update("password", event.target.value)}
invalid={!credentialsValid}
aria-invalid={!credentialsValid || undefined}
/>
</Field>
<Field
@@ -279,8 +236,15 @@ function ExitCreateModal({
/>
</Field>
</div>
</form>
</Modal>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose} disabled={busy}>取消</Button>
<Button variant="default" type="submit" form="exit-create-form" disabled={!valid || busy}>
{busy ? "保存中…" : submitLabel}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -293,95 +257,47 @@ function ExitTable({
onEdit,
onDelete,
}) {
const columns = [
{
header: "出口",
width: "22%",
render: (exit) => (
<div className="min-w-0">
<Link
to={`/network-exits/${encodeURIComponent(exit.id)}`}
className="anywhere font-semibold text-ink hover:text-primary"
>
{exit.id}
</Link>
<p className="anywhere text-xs text-muted">
{exit.protocol}://{exit.host}:{exit.port}
</p>
</div>
),
},
{
header: "状态",
width: "9%",
render: (exit) => <ExitHealthPill exit={exit} />,
},
{
header: "用户名",
width: "10%",
render: (exit) => (
<span className="anywhere">{exit.username || "无"}</span>
),
},
{
header: "密码",
width: "10%",
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">
{accounts?.length ? accounts.join("、") : "未绑定"}
</span>
);
},
},
{
header: "操作",
width: "8%",
align: "right",
render: (exit) => (
<ExitActions
exit={exit}
busy={busy === exit.id}
onAction={onAction}
onEdit={onEdit}
onDelete={onDelete}
/>
),
},
];
return (
<Table
columns={columns}
rows={exits}
rowKey={(exit) => exit.id}
minWidth="1100px"
/>
<Table>
<TableHeader>
<TableRow>
<TableHead>出口</TableHead>
<TableHead>状态</TableHead>
<TableHead>用户名</TableHead>
<TableHead>密码</TableHead>
<TableHead>出口IP</TableHead>
<TableHead>最近检测</TableHead>
<TableHead>绑定账号</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{exits.map((exit) => {
const accounts = accountsByExit.get(exit.id);
return (
<TableRow key={exit.id}>
<TableCell>
<div className="min-w-0">
<Link to={`/network-exits/${encodeURIComponent(exit.id)}`} className="anywhere font-semibold text-foreground hover:text-primary">{exit.id}</Link>
<p className="anywhere text-xs text-muted-foreground">{exit.protocol}://{exit.host}:{exit.port}</p>
</div>
</TableCell>
<TableCell><ExitHealthPill exit={exit} /></TableCell>
<TableCell><span className="anywhere">{exit.username || "无"}</span></TableCell>
<TableCell><span className="anywhere">{exit.password || "无"}</span></TableCell>
<TableCell><span className="anywhere">{exit.observed_public_ip || "尚无观测"}</span></TableCell>
<TableCell>{exit.last_checked_at ? new Date(exit.last_checked_at).toLocaleString("zh-CN") : "未检测"}</TableCell>
<TableCell>
{bindingsError ? <span className="text-muted-foreground">状态未知</span> : <span className="anywhere">{accounts?.length ? accounts.join("、") : "未绑定"}</span>}
</TableCell>
<TableCell>
<ExitActions exit={exit} busy={busy === exit.id} onAction={onAction} onEdit={onEdit} onDelete={onDelete} />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}
@@ -425,7 +341,7 @@ export function NetworkExitList() {
await dataProvider.create({ resource: "network-exits", variables: data });
await query.refetch();
setNotice({
variant: "success",
variant: "default",
text: "网络出口已创建;检测通过后才可用于账号绑定。",
});
return true;
@@ -449,7 +365,7 @@ export function NetworkExitList() {
await query.refetch();
setEditing(null);
setNotice({
variant: "success",
variant: "default",
text: "网络出口已更新,需重新检测后才能使用。",
});
return true;
@@ -471,7 +387,7 @@ export function NetworkExitList() {
await dataProvider.delete({ resource: "network-exits", id: exit.id });
await query.refetch();
setDeleting(null);
setNotice({ variant: "success", text: "网络出口已删除。" });
setNotice({ variant: "default", text: "网络出口已删除。" });
} catch (reason) {
setNotice({
variant: "destructive",
@@ -489,7 +405,7 @@ export function NetworkExitList() {
await dataProvider.networkExitAction(exit.id, action);
await Promise.all([query.refetch(), refetchBrowsers()]);
setNotice({
variant: "success",
variant: "default",
text:
action === "check"
? "出口检测完成。"
@@ -517,16 +433,13 @@ export function NetworkExitList() {
title="网络出口"
description="维护固定出口、健康观测与账号绑定"
>
<Button
variant="primary"
icon="ri-add-line"
onClick={() => setCreateOpen(true)}
>
<Button variant="default" onClick={() => setCreateOpen(true)}>
<i className="ri-add-line" aria-hidden="true" />
创建网络出口
</Button>
</PageHeader>
{notice ? (
<Alert variant={notice.variant} className="mb-4">
<Alert variant={notice.variant === "destructive" ? "destructive" : "default"} className="mb-4">
{notice.text}
</Alert>
) : null}
@@ -633,8 +546,11 @@ export function NetworkExitDetail() {
</span>
}
>
<Button icon="ri-arrow-left-line" as={Link} to="/network-exits">
返回出口列表
<Button asChild>
<Link to="/network-exits">
<i className="ri-arrow-left-line" aria-hidden="true" />
返回出口列表
</Link>
</Button>
</PageHeader>
<Card>
@@ -673,11 +589,8 @@ export function NetworkExitDetail() {
]}
/>
<div className="flex flex-wrap gap-3">
<Button
as={Link}
to={`/audit?network_exit_id=${encodeURIComponent(exit.id)}`}
>
查看审计
<Button asChild>
<Link to={`/audit?network_exit_id=${encodeURIComponent(exit.id)}`}>查看审计</Link>
</Button>
</div>
</CardContent>