import { useEffect, useRef, useState } from "react"; import { Copy, KeyRound, Plus, RotateCcw, ShieldCheck, UserRound, UserRoundCheck, UserRoundX } from "lucide-react"; import { Button, Dialog, Drawer, Form, Space, Table, Tooltip } from "tdesign-react"; import { api } from "../../services/api"; import AccessibleInput from "../../components/AccessibleInput"; import { apiFieldErrors } from "../../services/formErrors"; import type { Admin } from "../../types/auth"; import { dateText } from "../expenses/date"; import { AsyncState, BusyIcon, ErrorBanner, Page, StatusTag } from "../common"; import type { Notify } from "../expenses/types"; import { useUnsavedActions, useUnsavedChanges } from "../../contexts/UnsavedChanges"; import { useDrawerFocus } from "../../hooks/useDrawerFocus"; export default function AdminsPage({ currentAdmin, timezone = "Asia/Shanghai", notify }: { currentAdmin: Admin; timezone?: string; notify?: Notify }) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [showCreate, setShowCreate] = useState(false); const [form, setForm] = useState({ username: "", displayName: "" }); const [formError, setFormError] = useState(""); const [formFields, setFormFields] = useState<{ username?: string; displayName?: string }>({}); const [action, setAction] = useState<{ kind: "toggle" | "reset"; admin: Admin } | null>(null); const [secret, setSecret] = useState(""); const loadSequence = useRef(0); const createInFlight = useRef(false); const { requestDiscard } = useUnsavedActions(); useDrawerFocus(showCreate, ".tn-admin-drawer"); useUnsavedChanges(showCreate && Boolean(form.username || form.displayName)); const load = async () => { const sequence = ++loadSequence.current; setLoading(true); setError(""); try { const result = await api<{ items: Admin[] }>("/api/admins"); if (sequence === loadSequence.current) setItems(result.items); } catch (e) { if (sequence === loadSequence.current) setError((e as Error).message); } finally { if (sequence === loadSequence.current) setLoading(false); } }; useEffect(() => { void load(); }, []); const create = async (event?: React.FormEvent) => { if (event && typeof event.preventDefault === "function") event.preventDefault(); if (busy || createInFlight.current) return; setFormError(""); setFormFields({}); const username = form.username.normalize("NFKC").trim(); const displayName = form.displayName.trim(); const nextFields: typeof formFields = {}; if ([...username].length < 3) nextFields.username = "用户名至少需要 3 个字符"; if (!displayName) nextFields.displayName = "请输入显示名"; if (Object.keys(nextFields).length) { setFormFields(nextFields); focusAdminField(nextFields); return; } createInFlight.current = true; setBusy(true); try { const result = await api<{ temporaryPassword: string }>("/api/admins", { method: "POST", body: JSON.stringify({ username, displayName }) }); setShowCreate(false); setForm({ username: "", displayName: "" }); setSecret(result.temporaryPassword); notify?.("管理员已创建", "success"); await load(); } catch (caught) { const mapped = apiFieldErrors(caught); if (Object.keys(mapped).length) { const fields = { username: mapped.username, displayName: mapped.displayName }; setFormFields(fields); focusAdminField(fields); } else setFormError((caught as Error).message); } finally { createInFlight.current = false; setBusy(false); } }; const toggle = async () => { if (!action) return; setBusy(true); try { const next = action.admin.status === "active" ? "disabled" : "active"; await api(`/api/admins/${action.admin.id}/status`, { method: "PUT", body: JSON.stringify({ status: next, version: action.admin.version }) }); setAction(null); notify?.(next === "active" ? "管理员已启用" : "管理员已停用", "success"); await load(); } catch (e) { setError((e as Error).message); setAction(null); } finally { setBusy(false); } }; const reset = async () => { if (!action) return; setBusy(true); try { const result = await api<{ temporaryPassword: string }>(`/api/admins/${action.admin.id}/reset-password`, { method: "POST", body: JSON.stringify({ version: action.admin.version }) }); setAction(null); setSecret(result.temporaryPassword); notify?.("密码已重置,现有会话已失效", "success"); await load(); } catch (e) { setError((e as Error).message); setAction(null); } finally { setBusy(false); } }; const columns = [ { colKey: "username", title: "用户名", cell: ({ row }: any) => {row.username} }, { colKey: "displayName", title: "显示名" }, { colKey: "status", title: "状态", cell: ({ row }: any) => }, { colKey: "lastLoginAt", title: "最近登录", cell: ({ row }: any) => row.lastLoginAt ? dateText(row.lastLoginAt, timezone) : "从未登录" }, { colKey: "version", title: "版本", cell: ({ row }: any) => v{row.version} }, { colKey: "actions", title: "操作", width: 250, cell: ({ row }: any) => }, ]; return }>

暂无管理员

: undefined} onRetry={() => void load()}>
{ if (!busy) requestDiscard(() => setShowCreate(false)); }} footer={}>
{ void create(); }}> { setForm({ ...form, username: value }); setFormFields(current => ({ ...current, username: undefined })); setFormError(""); }} onEnter={(_, context) => { context.e.preventDefault(); void create(); }} maxlength={64} autocomplete="off" /> { setForm({ ...form, displayName: value }); setFormFields(current => ({ ...current, displayName: undefined })); setFormError(""); }} onEnter={(_, context) => { context.e.preventDefault(); void create(); }} maxlength={80} />{formError &&
{formError}
}
{ if (!busy) setAction(null); }} onConfirm={() => void (action?.kind === "reset" ? reset() : toggle())} onCancel={() => { if (!busy) setAction(null); }}> {action?.kind === "reset" ? <>将生成一次性临时密码,同时让管理员“{action.admin.displayName}”已登录的会话安全退出。 : action?.admin.status === "active" ? "停用后该管理员将无法访问系统,已登录的会话会立即注销。" : "启用后该管理员可恢复系统访问并正常登录。"} setSecret("")} onConfirm={() => setSecret("")} onCancel={() => setSecret("")}>
{secret}

请妥善保管并将临时密码交付给管理员,该密码在首次登录时会被强制更新。

; async function copySecret(value: string) { try { if (!navigator.clipboard) throw new Error("clipboard unavailable"); await navigator.clipboard.writeText(value); notify?.("临时密码已复制", "info"); } catch { notify?.("复制失败,请手动复制临时密码", "error"); } } } function focusAdminField(fields: { username?: string; displayName?: string }) { const selector = fields.username ? "[aria-invalid='true'], [aria-invalid='true'] input" : fields.displayName ? "[aria-invalid='true'], [aria-invalid='true'] input" : ""; if (!selector) return; const focus = () => { const node = document.querySelector(selector); const target = node?.matches("input") ? node : node?.querySelector("input"); target?.focus(); }; requestAnimationFrame(focus); window.setTimeout(focus, 60); }