import { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Building2, ChevronDown, ChevronRight, Edit2, Key, Lock, Network, Plus, RefreshCw, Shield, Trash2, Users, X, } from "lucide-react"; import { EnabledStatus, RoleCode, SecurityLevel, type EnabledStatus as EnabledStatusCode, type RoleCode as RoleCodeValue, type SecurityLevel as SecurityLevelValue, } from "../../shared/api/enums"; import { useConfirm } from "../../shared/ui/ConfirmDialog"; import { useDictionaries } from "../dictionaries/dictionaryContext"; import type { UserSummary } from "../auth/authTypes"; import { flattenOrganizations, useOrganizationTree, useUserDirectory, } from "./directoryState"; import { organizationApi, type OrganizationApi, } from "./organizationApi"; import { userApi, type UserApi } from "./userApi"; import { roleApi, type RoleApi } from "./roleApi"; import type { RoleSummary } from "./roleTypes"; import { RoleMaintenancePanel } from "./RoleMaintenancePanel"; import { IpEntryMaintenancePanel } from "./IpEntryMaintenancePanel"; import { OrgIpAssignmentModal } from "./OrgIpAssignmentModal"; import type { OrganizationCreateRequest, OrganizationNode, OrganizationUpdateRequest, UserCreateRequest, UserQuery, UserResetPasswordRequest, UserUpdateRequest, } from "./directoryTypes"; import { B, DANGER, MAX_TREE_DEPTH, N, PBG, W, type ApiResultState, describeError, initialResult, LabeledInput, LabeledNumberInput, type OptionEntry, LabeledSelect, ModalFooter, ModalShell, roleOptions, securityLevelOptions, smallButton, statusOptions, } from "./directoryUi"; type DirectoryTab = | "organizations" | "users" | "roles" | "ipEntries"; // ===== 主页面 ===== export function DirectoryPage() { const [tab, setTab] = useState("organizations"); const dictionaries = useDictionaries(); const tabLabel = (key: DirectoryTab) => key === "organizations" ? dictionaries.label("allowedModules", "ORGANIZATION_USER_MANAGEMENT") || "机构维护" : key === "users" ? "用户维护" : key === "roles" ? "角色管理" : "IP地址列表"; return (
{tab === "organizations" ? ( ) : tab === "users" ? ( ) : tab === "roles" ? ( ) : ( )}
); } // ===== 机构维护面板 ===== interface OrganizationForm { orgCode: string; orgName: string; parentId: string | null; sortNo: number; status: EnabledStatusCode; } interface EditState { mode: "create" | "edit" | null; target: OrganizationNode | null; form: OrganizationForm | null; rowVersion: number; } function buildInitialForm( mode: "create" | "edit", target: OrganizationNode | null, defaultParentId: string | null, ): OrganizationForm { if (mode === "edit" && target) { return { orgCode: target.orgCode, orgName: target.orgName, parentId: target.parentId, sortNo: target.sortNo, status: target.status, }; } return { orgCode: "", orgName: "", parentId: defaultParentId, sortNo: 0, status: EnabledStatus.ENABLED, }; } function OrganizationMaintenancePanel({ api = organizationApi, }: { api?: OrganizationApi; }) { const dictionaries = useDictionaries(); const { confirm: confirmDialog, dialog: confirmDialogNode } = useConfirm(); const [keyword, setKeyword] = useState(""); const [retryVersion, setRetryVersion] = useState(0); const { tree, status, error } = useOrganizationTree( keyword, retryVersion, api, ); const [expanded, setExpanded] = useState>(() => new Set()); const [selectedId, setSelectedId] = useState(null); const [ipAssignTarget, setIpAssignTarget] = useState( null, ); const [editState, setEditState] = useState({ mode: null, target: null, form: null, rowVersion: 0, }); const [mutation, setMutation] = useState(initialResult); // 初次加载自动展开所有节点(用 useRef 避免依赖 expanded.size 反复触发) const initialExpandDone = useRef(false); useEffect(() => { if ( !initialExpandDone.current && status === "success" && tree.length > 0 ) { initialExpandDone.current = true; setExpanded(new Set(flattenOrganizations(tree).map((n) => n.id))); } }, [status, tree]); const flatNodes = useMemo(() => flattenOrganizations(tree), [tree]); const nodeMap = useMemo(() => { const map = new Map(); flatNodes.forEach((node) => map.set(node.id, node)); return map; }, [flatNodes]); const selectedNode = selectedId ? nodeMap.get(selectedId) ?? null : null; const refresh = useCallback(() => { setRetryVersion((v) => v + 1); }, []); const toggleExpand = (id: string) => { setExpanded((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const openCreate = (parent: OrganizationNode | null) => { setEditState({ mode: "create", target: null, form: buildInitialForm("create", null, parent?.id ?? null), rowVersion: 0, }); setMutation(initialResult); }; const openEdit = (node: OrganizationNode) => { setEditState({ mode: "edit", target: node, form: buildInitialForm("edit", node, null), rowVersion: node.rowVersion, }); setMutation(initialResult); }; const closeEditor = () => { setEditState({ mode: null, target: null, form: null, rowVersion: 0, }); setMutation(initialResult); }; const handleSubmit = async () => { if (!editState.form || !editState.mode) return; const form = editState.form; if (!form.orgCode.trim() || !form.orgName.trim()) { setMutation({ submitting: false, error: "编码与名称均不能为空" }); return; } setMutation({ submitting: true, error: null }); try { if (editState.mode === "create") { const payload: OrganizationCreateRequest = { orgCode: form.orgCode.trim(), orgName: form.orgName.trim(), parentId: form.parentId, sortNo: form.sortNo, status: form.status, }; const created = await api.create(payload); setExpanded((current) => { const next = new Set(current); if (form.parentId) next.add(form.parentId); next.add(created.id); return next; }); setSelectedId(created.id); } else if (editState.target) { const payload: OrganizationUpdateRequest = { orgCode: form.orgCode.trim(), orgName: form.orgName.trim(), parentId: form.parentId, sortNo: form.sortNo, status: form.status, rowVersion: editState.rowVersion, }; await api.update(editState.target.id, payload); } closeEditor(); refresh(); } catch (error) { setMutation({ submitting: false, error: describeError(error) }); } }; const handleDelete = async (node: OrganizationNode) => { if (!(await confirmDialog({ message: `确认删除机构「${node.orgName}」?`, danger: true }))) return; setMutation({ submitting: true, error: null }); try { await api.delete(node.id, node.rowVersion); if (selectedId === node.id) setSelectedId(null); setMutation(initialResult); refresh(); } catch (error) { setMutation({ submitting: false, error: describeError(error) }); } }; return (
setKeyword(e.target.value)} placeholder="按机构名称或编码筛选" className="flex-1 px-3 py-1.5 text-base border rounded-sm outline-none" style={{ borderColor: "rgba(49,145,235,0.4)", color: N }} /> {keyword && ( )}
{smallButton(false, { label: "刷新", icon: , onClick: refresh, })} {smallButton(true, { label: "新建顶层机构", icon: , onClick: () => openCreate(null), })}
{status === "loading" && (
正在加载机构树…
)} {status === "error" && (
{error?.message ?? "加载失败"}
)} {status !== "loading" && status !== "error" && tree.length === 0 && (
暂无机构数据,点击右上角"新建顶层机构"开始维护。
)} {tree.length > 0 && (
{tree.map((node) => ( openCreate(parent)} onAssignIp={setIpAssignTarget} onEdit={openEdit} onDelete={handleDelete} /> ))}
)}
{mutation.error && (
{mutation.error}
)} {ipAssignTarget && ( setIpAssignTarget(null)} onSaved={() => { setIpAssignTarget(null); refresh(); }} /> )} {editState.form && editState.mode && ( setEditState((current) => current.form ? { ...current, form: { ...current.form, ...patch }, } : current, ) } onClose={closeEditor} onSubmit={handleSubmit} submitting={mutation.submitting} statusLabel={(code) => dictionaries.label("enabledStatuses", code)} /> )} {confirmDialogNode}
); } function OrganizationTreeRow({ node, depth, expanded, onToggle, selectedId, onSelect, onAddChild, onAssignIp, onEdit, onDelete, }: { node: OrganizationNode; depth: number; expanded: ReadonlySet; onToggle: (id: string) => void; selectedId: string | null; onSelect: (id: string) => void; onAddChild: (parent: OrganizationNode) => void; onAssignIp: (node: OrganizationNode) => void; onEdit: (node: OrganizationNode) => void; onDelete: (node: OrganizationNode) => void; }) { const dictionaries = useDictionaries(); // 防御性:避免极端深度时栈溢出(后端 _descendants 已做循环检测,此处为前端二次保护) if (depth >= MAX_TREE_DEPTH) return null; const isOpen = expanded.has(node.id); const isSelected = selectedId === node.id; const hasChildren = node.children.length > 0; return ( <>
onSelect(node.id)} > {hasChildren ? ( ) : ( )} {node.orgName} ({node.orgCode}) {dictionaries.label("enabledStatuses", node.status)} {node.orgPath}
e.stopPropagation()}> {smallButton(false, { label: "新增下级", icon: , onClick: () => onAddChild(node), })} {smallButton(false, { label: "分配IP", icon: , onClick: () => onAssignIp(node), })} {smallButton(false, { label: "编辑", icon: , onClick: () => onEdit(node), })} {smallButton(false, { label: "删除", icon: , onClick: () => onDelete(node), danger: true, })}
{isOpen && node.children.map((child) => ( ))} ); } function OrganizationEditorModal({ mode, target, form, allNodes, onChange, onClose, onSubmit, submitting, statusLabel, }: { mode: "create" | "edit"; target: OrganizationNode | null; form: OrganizationForm; allNodes: OrganizationNode[]; onChange: (patch: Partial) => void; onClose: () => void; onSubmit: () => void; submitting: boolean; statusLabel: (code: EnabledStatusCode) => string; }) { const dictionaries = useDictionaries(); const title = mode === "create" ? "新建机构" : `编辑机构:${target?.orgName ?? ""}`; const isEdit = mode === "edit"; const parentOptions = useMemo(() => { const result: OrganizationNode[] = []; const blockedIds = new Set(); if (mode === "edit" && target) { blockedIds.add(target.id); const collectDescendants = (root: OrganizationNode) => { root.children.forEach((child) => { blockedIds.add(child.id); collectDescendants(child); }); }; collectDescendants(target); } allNodes.forEach((node) => { if (!blockedIds.has(node.id)) result.push(node); }); return result; }, [allNodes, mode, target]); const parentSelectOptions = useMemo[]>( () => parentOptions.map((node) => ({ value: node.id, label: `${node.orgPath} (${node.orgCode})`, })), [parentOptions], ); const statusSelectOptions = useMemo[]>( () => statusOptions().map((code) => ({ value: code, label: statusLabel(code) })), [statusLabel], ); return (
onChange({ orgCode: v })} placeholder="如:ORG_HQ" /> onChange({ orgName: v })} /> label="上级机构" value={form.parentId ?? ""} onChange={(v) => onChange({ parentId: v || null })} options={[ { value: "", label: "— 顶层机构 —" }, ...parentSelectOptions, ]} /> onChange({ sortNo: v })} /> label="状态" value={form.status} onChange={(v) => onChange({ status: v })} options={statusSelectOptions} />
); } // ===== 用户维护面板 ===== interface UserForm { username: string; realName: string; initialPassword: string; organizationId: string | null; roleCode: RoleCodeValue; roleId: string | null; securityLevel: SecurityLevelValue; status: EnabledStatusCode; } interface UserEditState { mode: "create" | "edit" | null; target: UserSummary | null; form: UserForm | null; rowVersion: number; } interface ResetState { target: UserSummary | null; newPassword: string; } function buildInitialUserForm( mode: "create" | "edit", target: UserSummary | null, defaultOrgId: string | null, ): UserForm { if (mode === "edit" && target) { return { username: target.username, realName: target.realName, initialPassword: "", organizationId: target.organizationId, roleCode: target.roleCode, roleId: target.roleId, securityLevel: target.securityLevel, status: target.status, }; } return { username: "", realName: "", initialPassword: "", organizationId: defaultOrgId, roleCode: RoleCode.USER, roleId: null, securityLevel: SecurityLevel.INTERNAL, status: EnabledStatus.ENABLED, }; } function UserMaintenancePanel({ userApiImpl = userApi, orgApiImpl = organizationApi, roleApiImpl = roleApi, }: { userApiImpl?: UserApi; orgApiImpl?: OrganizationApi; roleApiImpl?: RoleApi; }) { const dictionaries = useDictionaries(); const { confirm: confirmDialog, dialog: confirmDialogNode } = useConfirm(); const [orgKeyword, setOrgKeyword] = useState(""); const [orgRetry, setOrgRetry] = useState(0); // 功能角色下拉数据(仅 roleCode=USER 时可选) const [roles, setRoles] = useState([]); useEffect(() => { let cancelled = false; roleApiImpl .getRoles({ pageSize: 100 }) .then((page) => { if (!cancelled) setRoles(page.items); }) .catch(() => { // 角色加载失败不阻塞用户维护,下拉为空即可 }); return () => { cancelled = true; }; }, [roleApiImpl]); const { tree } = useOrganizationTree(orgKeyword, orgRetry, orgApiImpl); const [selectedOrgId, setSelectedOrgId] = useState(null); const [includeDescendants, setIncludeDescendants] = useState(false); const [userKeyword, setUserKeyword] = useState(""); const [userStatus, setUserStatus] = useState(""); const [page, setPage] = useState(1); const pageSize = 20; const [userRetry, setUserRetry] = useState(0); const userQuery: UserQuery = useMemo( () => ({ organizationId: selectedOrgId ?? undefined, includeDescendants, keyword: userKeyword.trim() || undefined, status: userStatus || undefined, page, pageSize, }), [selectedOrgId, includeDescendants, userKeyword, userStatus, page], ); const { data, status, error } = useUserDirectory( userQuery, userRetry, userApiImpl, ); const [expanded, setExpanded] = useState>(() => new Set()); const [userEdit, setUserEdit] = useState({ mode: null, target: null, form: null, rowVersion: 0, }); const [resetState, setResetState] = useState(null); const [mutation, setMutation] = useState(initialResult); // 初次加载自动展开用户筛选机构树(同上 useRef 模式) const initialUserExpandDone = useRef(false); useEffect(() => { if (!initialUserExpandDone.current && tree.length > 0) { initialUserExpandDone.current = true; setExpanded(new Set(flattenOrganizations(tree).map((n) => n.id))); } }, [tree]); const refreshUsers = useCallback(() => setUserRetry((v) => v + 1), []); const refreshTree = useCallback(() => setOrgRetry((v) => v + 1), []); const openCreateUser = () => { setUserEdit({ mode: "create", target: null, form: buildInitialUserForm("create", null, selectedOrgId), rowVersion: 0, }); setMutation(initialResult); }; const openEditUser = (user: UserSummary) => { setUserEdit({ mode: "edit", target: user, form: buildInitialUserForm("edit", user, null), rowVersion: user.rowVersion, }); setMutation(initialResult); }; const closeUserEditor = () => { setUserEdit({ mode: null, target: null, form: null, rowVersion: 0, }); setMutation(initialResult); }; const handleSubmitUser = async () => { if (!userEdit.form || !userEdit.mode) return; const form = userEdit.form; if (!form.username.trim() || !form.realName.trim()) { setMutation({ submitting: false, error: "用户名与姓名不能为空" }); return; } if (userEdit.mode === "create" && form.initialPassword.length < 8) { setMutation({ submitting: false, error: "初始密码至少8位" }); return; } setMutation({ submitting: true, error: null }); try { if (userEdit.mode === "create") { const payload: UserCreateRequest = { username: form.username.trim(), realName: form.realName.trim(), initialPassword: form.initialPassword, organizationId: form.organizationId, roleCode: form.roleCode, roleId: form.roleCode === RoleCode.USER ? form.roleId : null, securityLevel: form.securityLevel, status: form.status, }; await userApiImpl.create(payload); } else if (userEdit.target) { const payload: UserUpdateRequest = { realName: form.realName.trim(), organizationId: form.organizationId, roleCode: form.roleCode, roleId: form.roleCode === RoleCode.USER ? form.roleId : null, securityLevel: form.securityLevel, status: form.status, rowVersion: userEdit.rowVersion, }; await userApiImpl.update(userEdit.target.id, payload); } closeUserEditor(); refreshUsers(); } catch (error) { setMutation({ submitting: false, error: describeError(error) }); } }; const handleDeleteUser = async (user: UserSummary) => { if (!(await confirmDialog({ message: `确认删除用户「${user.realName}」?`, danger: true }))) return; setMutation({ submitting: true, error: null }); try { await userApiImpl.delete(user.id, user.rowVersion); setMutation(initialResult); refreshUsers(); } catch (error) { setMutation({ submitting: false, error: describeError(error) }); } }; const handleResetPassword = async () => { if (!resetState) return; if (resetState.newPassword.length < 8) { setMutation({ submitting: false, error: "新密码至少8位" }); return; } setMutation({ submitting: true, error: null }); try { const payload: UserResetPasswordRequest = { newPassword: resetState.newPassword, rowVersion: resetState.target?.rowVersion ?? 0, }; await userApiImpl.resetPassword(resetState.target!.id, payload); setResetState(null); setMutation(initialResult); refreshUsers(); } catch (error) { setMutation({ submitting: false, error: describeError(error) }); } }; return (
机构筛选
setOrgKeyword(e.target.value)} placeholder="按机构名称或编码筛选" className="w-full px-2.5 py-1.5 text-base border rounded-sm outline-none" style={{ borderColor: "rgba(49,145,235,0.4)", color: N }} />
{tree.map((node) => ( setExpanded((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }) } selectedId={selectedOrgId} onSelect={setSelectedOrgId} /> ))}
{smallButton(false, { label: "刷新", icon: , onClick: refreshTree, })}
{ setUserKeyword(e.target.value); setPage(1); }} placeholder="按用户名或姓名筛选" className="flex-1 px-3 py-1.5 text-base border rounded-sm outline-none" style={{ borderColor: "rgba(49,145,235,0.4)", color: N }} /> {userKeyword && ( )}
{smallButton(false, { label: "刷新", icon: , onClick: refreshUsers, })} {smallButton(true, { label: "新建用户", icon: , onClick: openCreateUser, })}
{status === "loading" && (
正在加载用户列表…
)} {status === "error" && (
{error?.message ?? "加载失败"}
)} {status !== "loading" && status !== "error" && data.items.length === 0 && (
暂无用户,请点击右上角"新建用户"。
)} {data.items.length > 0 && ( {[ "用户名", "姓名", "所属机构", "角色", "密级", "状态", "最近登录", "操作", ].map((h) => ( ))} {data.items.map((user) => ( ))}
{h}
{user.username} {user.realName} {user.organizationName || "—"} {user.roleCode === RoleCode.USER && user.roleName ? user.roleName : dictionaries.label("roleCodes", user.roleCode)} {dictionaries.label("securityLevels", user.securityLevel)} {dictionaries.label("enabledStatuses", user.status)} {user.lastLoginAt ?? "—"}
{smallButton(false, { label: "编辑", icon: , onClick: () => openEditUser(user), })} {smallButton(false, { label: "重置密码", icon: , onClick: () => setResetState({ target: user, newPassword: "", }), })} {smallButton(false, { label: "删除", icon: , onClick: () => handleDeleteUser(user), danger: true, })}
)}
共 {data.total} 条 · 第 {data.page} / {Math.max(data.totalPages, 1)} 页
{mutation.error && (
{mutation.error}
)} {userEdit.form && userEdit.mode && ( setUserEdit((current) => current.form ? { ...current, form: { ...current.form, ...patch } } : current, ) } onClose={closeUserEditor} onSubmit={handleSubmitUser} submitting={mutation.submitting} /> )} {resetState && resetState.target && ( setResetState((current) => current ? { ...current, newPassword: v } : current, ) } onClose={() => { setResetState(null); setMutation(initialResult); }} onSubmit={handleResetPassword} submitting={mutation.submitting} /> )} {confirmDialogNode}
); } function UserOrgTreeRow({ node, depth, expanded, onToggle, selectedId, onSelect, }: { node: OrganizationNode; depth: number; expanded: ReadonlySet; onToggle: (id: string) => void; selectedId: string | null; onSelect: (id: string) => void; }) { // 防御性:避免极端深度时栈溢出(后端 _descendants 已做循环检测,此处为前端二次保护) if (depth >= MAX_TREE_DEPTH) return null; const isOpen = expanded.has(node.id); const isSelected = selectedId === node.id; const hasChildren = node.children.length > 0; return ( <>
onSelect(node.id)} > {hasChildren ? ( ) : ( )} {node.orgName} {isSelected && ( 已选 )}
{isOpen && node.children.map((child) => ( ))} ); } function UserEditorModal({ mode, target, form, organizations, roles, onChange, onClose, onSubmit, submitting, }: { mode: "create" | "edit"; target: UserSummary | null; form: UserForm; organizations: OrganizationNode[]; roles: RoleSummary[]; onChange: (patch: Partial) => void; onClose: () => void; onSubmit: () => void; submitting: boolean; }) { const dictionaries = useDictionaries(); const title = mode === "create" ? "新建用户" : `编辑用户:${target?.realName ?? ""}`; const isEdit = mode === "edit"; const orgSelectOptions = useMemo[]>( () => organizations.map((node) => ({ value: node.id, label: `${node.orgPath} (${node.orgCode})`, })), [organizations], ); const roleSelectOptions = useMemo[]>( () => roleOptions().map((code) => ({ value: code, label: dictionaries.label("roleCodes", code), })), [dictionaries], ); const customRoleOptions = useMemo[]>( () => roles.map((role) => ({ value: role.id, label: role.status === EnabledStatus.ENABLED ? role.roleName : `${role.roleName}(已停用)`, })), [roles], ); const securitySelectOptions = useMemo[]>( () => securityLevelOptions().map((code) => ({ value: code, label: dictionaries.label("securityLevels", code), })), [dictionaries], ); const statusSelectOptions = useMemo[]>( () => statusOptions().map((code) => ({ value: code, label: dictionaries.label("enabledStatuses", code), })), [dictionaries], ); return (
onChange({ username: v })} disabled={isEdit} placeholder="登录用户名" /> onChange({ realName: v })} /> {mode === "create" && ( onChange({ initialPassword: v })} type="password" autoComplete="new-password" /> )} label="所属机构" value={form.organizationId ?? ""} onChange={(v) => onChange({ organizationId: v || null })} options={[ { value: "", label: "— 未指定 —" }, ...orgSelectOptions, ]} /> label="用户类型" value={form.roleCode} onChange={(v) => onChange( v === RoleCode.USER ? { roleCode: v } : { roleCode: v, roleId: null }, ) } options={roleSelectOptions} /> {form.roleCode === RoleCode.USER && ( label="角色" value={form.roleId ?? ""} onChange={(v) => onChange({ roleId: v || null })} options={[ { value: "", label: "— 未指定(跟随默认角色) —" }, ...customRoleOptions, ]} /> )} label="密级" value={form.securityLevel} onChange={(v) => onChange({ securityLevel: v })} options={securitySelectOptions} /> label="状态" value={form.status} onChange={(v) => onChange({ status: v })} options={statusSelectOptions} />
); } function ResetPasswordModal({ target, value, onChange, onClose, onSubmit, submitting, }: { target: UserSummary; value: string; onChange: (v: string) => void; onClose: () => void; onSubmit: () => void; submitting: boolean; }) { return (
新密码生效后该用户原有登录凭证立即失效。
); }