import { useCallback, useEffect, useRef, useState } from "react"; import { useConfirm } from "../../shared/ui/ConfirmDialog"; import { Edit2, Info, Plus, RefreshCw, Trash2, X } from "lucide-react"; import type { PageResult } from "../../shared/api/types"; import { ipEntryApi, type IpEntryApi } from "./ipEntryApi"; import type { IpEntryCreateRequest, IpEntrySummary, IpEntryUpdateRequest, } from "./ipEntryTypes"; import { B, DANGER, N, PBG, W, type ApiResultState, describeError, initialResult, LabeledInput, ModalFooter, ModalShell, smallButton, } from "./directoryUi"; const EMPTY_IP_PAGE: PageResult = { items: [], page: 1, pageSize: 20, total: 0, totalPages: 0, }; interface IpEntryForm { ipValue: string; remark: string; } interface IpEntryEditState { mode: "create" | "edit"; target: IpEntrySummary | null; form: IpEntryForm; rowVersion: number; } function buildIpEntryForm( mode: "create" | "edit", target: IpEntrySummary | null, ): IpEntryForm { if (mode === "edit" && target) { return { ipValue: target.ipValue, remark: target.remark }; } return { ipValue: "", remark: "" }; } /** IP地址池维护:全局唯一IP池(精确IP或CIDR网段),供机构维护中为部门分配。 */ export function IpEntryMaintenancePanel({ api = ipEntryApi, }: { api?: IpEntryApi; }) { const { confirm: confirmDialog, dialog: confirmDialogNode } = useConfirm(); const [keyword, setKeyword] = useState(""); const [page, setPage] = useState(1); const pageSize = 20; const [retry, setRetry] = useState(0); const [data, setData] = useState>(EMPTY_IP_PAGE); const [listStatus, setListStatus] = useState<"loading" | "success" | "error">( "loading", ); const [listError, setListError] = useState(null); const [editState, setEditState] = useState(null); const [mutation, setMutation] = useState(initialResult); // 列表:搜索防抖 + 分页(与 RoleMaintenancePanel 相同的竞态防护模式) const listVersion = useRef(0); useEffect(() => { const controller = new AbortController(); const current = ++listVersion.current; const timer = window.setTimeout(async () => { setListStatus("loading"); setListError(null); try { const result = await api.getIpEntries( { keyword: keyword.trim() || undefined, page, pageSize, }, controller.signal, ); if (controller.signal.aborted || current !== listVersion.current) { return; } setData(result); setListStatus("success"); } catch (loadError) { if (controller.signal.aborted || current !== listVersion.current) { return; } setListError(describeError(loadError)); setListStatus("error"); } }, keyword.trim() ? 300 : 0); return () => { window.clearTimeout(timer); controller.abort(); }; }, [api, keyword, page, retry]); const refresh = useCallback(() => setRetry((v) => v + 1), []); const openCreate = () => { setEditState({ mode: "create", target: null, form: buildIpEntryForm("create", null), rowVersion: 0, }); setMutation(initialResult); }; const openEdit = (entry: IpEntrySummary) => { setEditState({ mode: "edit", target: entry, form: buildIpEntryForm("edit", entry), rowVersion: entry.rowVersion, }); setMutation(initialResult); }; const closeEditor = () => { setEditState(null); setMutation(initialResult); }; const handleSubmit = async () => { if (!editState) return; const form = editState.form; if (!form.ipValue.trim()) { setMutation({ submitting: false, error: "IP地址或网段不能为空" }); return; } setMutation({ submitting: true, error: null }); try { if (editState.mode === "create") { const payload: IpEntryCreateRequest = { ipValue: form.ipValue.trim(), remark: form.remark.trim() || null, }; await api.create(payload); } else if (editState.target) { const payload: IpEntryUpdateRequest = { ipValue: form.ipValue.trim(), remark: form.remark.trim() || null, rowVersion: editState.rowVersion, }; await api.update(editState.target.id, payload); } closeEditor(); refresh(); } catch (error) { setMutation({ submitting: false, error: describeError(error) }); } }; const handleDelete = async (entry: IpEntrySummary) => { if (!(await confirmDialog({ message: `确认删除IP「${entry.ipValue}」?`, danger: true }))) return; setMutation({ submitting: true, error: null }); try { await api.delete(entry.id, entry.rowVersion); setMutation(initialResult); refresh(); } catch (error) { setMutation({ submitting: false, error: describeError(error) }); } }; return (
{confirmDialogNode}
{ setKeyword(e.target.value); setPage(1); }} placeholder="按IP或备注筛选" className="flex-1 px-3 py-1.5 text-base border rounded-sm outline-none" style={{ borderColor: "rgba(var(--dms-bright-rgb),0.4)", color: N }} /> {keyword && ( )}
{smallButton(false, { label: "刷新", icon: , onClick: refresh, })} {smallButton(true, { label: "新建IP", icon: , onClick: openCreate, })}
全局IP池:维护精确IP或CIDR网段;在「机构管理」中为部门分配后,该部门用户仅可从分配的IP登录(ADMIN不受限制)。
{listStatus === "loading" && (
正在加载IP条目…
)} {listStatus === "error" && (
{listError ?? "加载失败"}
)} {listStatus === "success" && data.items.length === 0 && (
暂无IP条目,点击右上角"新建IP"开始维护。
)} {data.items.length > 0 && ( {["IP地址/网段", "备注", "操作"].map((h) => ( ))} {data.items.map((entry) => ( ))}
{h}
{entry.ipValue} {entry.remark || "—"}
{smallButton(false, { label: "编辑", icon: , onClick: () => openEdit(entry), })} {smallButton(false, { label: "删除", icon: , onClick: () => handleDelete(entry), danger: true, })}
)}
共 {data.total} 条 · 第 {data.page} / {Math.max(data.totalPages, 1)}{" "} 页
{mutation.error && (
{mutation.error}
)} {editState && ( setEditState((current) => current ? { ...current, form: { ...current.form, ...patch } } : current, ) } onClose={closeEditor} onSubmit={handleSubmit} submitting={mutation.submitting} /> )}
); } function IpEntryEditorModal({ mode, target, form, onChange, onClose, onSubmit, submitting, }: { mode: "create" | "edit"; target: IpEntrySummary | null; form: IpEntryForm; onChange: (patch: Partial) => void; onClose: () => void; onSubmit: () => void; submitting: boolean; }) { const title = mode === "create" ? "新建IP条目" : `编辑IP条目:${target?.ipValue ?? ""}`; return (
onChange({ ipValue: v })} placeholder="如:192.168.1.5 或 10.0.0.0/24" />
支持精确IP(IPv4/IPv6)或无主机位的CIDR网段;输入会被自动规范化, 网段不允许携带主机位(如 10.0.0.1/24 将被拒绝)。
onChange({ remark: v })} placeholder="选填,如:总部办公网段" />
); }