import { useEffect, useState } from "react"; import { App as AntApp } from "antd"; import { PlusOutlined, EditOutlined, DeleteOutlined, SaveOutlined, CloseOutlined, AppstoreOutlined, FileExcelOutlined, KeyOutlined, RollbackOutlined, SettingOutlined, } from "@ant-design/icons"; import { Field, PrimInput, PrimSelect, PrimCheckbox, ToolbarBtnDark, } from "@/components/Primitives"; import StaffPicker from "@/components/StaffPicker"; import { createUser, getUser, listPermissionCategories, updateUser, type PermissionCategoryVO, type UserDTO, type UserListVO, } from "@/api/user"; import { USER_TYPES, LANGUAGE_OPTIONS, SCOPE_ITEMS, } from "@/utils/data"; import { useAppDispatch, useAppSelector } from "@/store"; import { closeTab, setActiveTab } from "@/store/tabsSlice"; import { fmtDateTime } from "@/utils/format"; type Mode = "view" | "edit" | "new"; interface Props { userId?: number; mode: Mode; } interface FormState { sUserNo: string; sUserName: string; iStaffId: number | null; staffName: string; sUserType: string; sLanguage: string; bCanModifyDocs: boolean; } const PERM_TABS = [ { id: "groups", label: "权限组" }, { id: "customer", label: "客户查看权限" }, { id: "supplier", label: "供应商查看权限" }, { id: "staff", label: "人员查看权限" }, { id: "process", label: "工序查看权限" }, { id: "driver", label: "司机查看权限" }, ]; export default function UserDetail({ userId, mode: initialMode }: Props) { const [mode, setMode] = useState(initialMode); const [submitting, setSubmitting] = useState(false); const dispatch = useAppDispatch(); const { message } = AntApp.useApp(); const tabs = useAppSelector((s) => s.tabs.tabs); const activeTabId = useAppSelector((s) => s.tabs.activeTabId); const tab = tabs.find((t) => t.id === activeTabId); const snapshot = tab?.meta?.snapshot as UserListVO | undefined; const [detail, setDetail] = useState(snapshot); const currentSnapshot = detail ?? snapshot; const [permissionCategories, setPermissionCategories] = useState([]); // null = 详情尚未加载(仅在新建模式下立即视为 [])。 // 编辑模式必须等到非 null 后才允许保存,否则会把"未知"误当成"清空"。 const [permissionCategoryIds, setPermissionCategoryIds] = useState( initialMode === "new" ? [] : null ); const buildForm = (snap?: UserListVO): FormState => mode === "new" || !snap ? { sUserNo: "", sUserName: "", iStaffId: null, staffName: "", sUserType: "普通用户", sLanguage: "zh", bCanModifyDocs: false, } : { sUserNo: snap.sUserNo, sUserName: snap.sUserName, iStaffId: snap.iStaffId, staffName: snap.staffName ?? "", sUserType: snap.sUserType, sLanguage: snap.sLanguage, bCanModifyDocs: !!snap.bCanModifyDocs, }; const [form, setForm] = useState(() => buildForm(currentSnapshot)); const [permTab, setPermTab] = useState("groups"); const [tabPerms, setTabPerms] = useState>({}); useEffect(() => { setDetail(snapshot); }, [snapshot]); useEffect(() => { void listPermissionCategories() .then(setPermissionCategories) .catch(() => { // interceptor already showed message }); }, []); useEffect(() => { if (mode === "new" || !userId) return; void getUser(userId) .then((u) => { setDetail(u); setPermissionCategoryIds(u.permissionCategoryIds ?? []); }) .catch(() => { // interceptor already showed message; 保持 null 以便"修改/保存"维持 disabled。 }); }, [mode, userId]); useEffect(() => { setForm(buildForm(currentSnapshot)); if (mode === "new") { setPermissionCategoryIds([]); } else if (currentSnapshot?.permissionCategoryIds !== undefined) { // detail() 返回过来的快照里带 ids 才采纳;列表态快照没有该字段时保持 null(仍未加载)。 setPermissionCategoryIds(currentSnapshot.permissionCategoryIds); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentSnapshot, mode]); const set = (k: K, v: FormState[K]) => setForm((s) => ({ ...s, [k]: v })); const disabled = mode === "view"; const checkedCount = permissionCategoryIds?.length ?? 0; const permsLoaded = permissionCategoryIds !== null; const startEdit = () => setMode("edit"); const startNew = () => setMode("new"); const cancel = () => { if (mode === "new") { dispatch(closeTab(activeTabId)); dispatch(setActiveTab("userlist")); } else { setForm(buildForm(currentSnapshot)); if (currentSnapshot?.permissionCategoryIds !== undefined) { setPermissionCategoryIds(currentSnapshot.permissionCategoryIds); } setMode("view"); } }; const save = async () => { if (!form.sUserNo.trim() || !form.sUserName.trim() || !form.sUserType || !form.sLanguage) { message.error("请填写必填项"); return; } const dto: UserDTO = { sUserNo: form.sUserNo, sUserName: form.sUserName, iStaffId: form.iStaffId, sUserType: form.sUserType, sLanguage: form.sLanguage, bCanModifyDocs: form.bCanModifyDocs, }; if (mode === "new") { dto.permissionCategoryIds = permissionCategoryIds ?? []; } else if (permissionCategoryIds !== null) { // 编辑模式:未加载完前不下发该字段,后端按"不动权限"处理。 dto.permissionCategoryIds = permissionCategoryIds; } setSubmitting(true); try { if (mode === "new") { await createUser(dto); message.success("新增成功"); dispatch(closeTab(activeTabId)); dispatch(setActiveTab("userlist")); } else if (userId) { await updateUser(userId, dto); const refreshed = await getUser(userId); setDetail(refreshed); setPermissionCategoryIds(refreshed.permissionCategoryIds ?? []); message.success("保存成功"); setMode("view"); } } catch { // interceptor } finally { setSubmitting(false); } }; return (
} onClick={startNew} disabled={mode !== "view"}> 新增 } onClick={startEdit} disabled={mode !== "view" || !userId || !permsLoaded} > 修改 } disabled={mode !== "view"} danger> 删除 } onClick={save} disabled={mode === "view" || submitting || (mode === "edit" && !permsLoaded)} > 保存 } onClick={cancel} disabled={mode === "view"}> 取消 }>功能 }>作废 }>重置密码 }>取消作废
已选权限: {checkedCount} /{" "} {permissionCategories.length} {mode === "view" ? "只读" : mode === "new" ? "新增中" : "编辑中"}
setForm((s) => ({ ...s, staffName: name, iStaffId: id, // Auto-fill 用户号 / 用户名 to the staff name on selection // (matches prototype behavior — user can still override after). ...(id != null ? { sUserNo: name, sUserName: name } : {}), })) } disabled={disabled} required placeholder="输入员工姓名搜索" /> set("sUserName", v)} disabled={disabled} required /> set("sUserType", v)} disabled={disabled} required allowEmpty={false} options={USER_TYPES} /> set("sUserNo", v)} disabled={disabled} required mono /> set("sLanguage", v)} disabled={disabled} required allowEmpty={false} options={LANGUAGE_OPTIONS} />
set("bCanModifyDocs", v)} disabled={disabled} />
{PERM_TABS.map((t) => { const active = t.id === permTab; return (
setPermTab(t.id)} style={{ padding: "6px 14px", fontSize: 12, cursor: "pointer", color: active ? "var(--accent-strong)" : "var(--text)", background: active ? "var(--accent-soft)" : "transparent", borderTop: active ? "2px solid var(--accent)" : "2px solid transparent", borderRight: "1px solid var(--border)", fontWeight: active ? 500 : 400, }} onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = "var(--bg-row-hover)"; }} onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = "transparent"; }} > {t.label}
); })}
{permTab === "groups" ? ( setPermissionCategoryIds((s) => { const base = s ?? []; return v ? Array.from(new Set([...base, id])) : base.filter((x) => x !== id); }) } disabled={disabled || !permsLoaded} /> ) : ( setTabPerms((s) => ({ ...s, [permTab]: v }))} disabled={disabled} /> )}
); } interface PermissionGridProps { categories: PermissionCategoryVO[]; selectedIds: number[]; setPermission: (id: number, v: boolean) => void; disabled?: boolean; } function PermissionGrid({ categories, selectedIds, setPermission, disabled }: PermissionGridProps) { const [filter, setFilter] = useState(""); const [hovered, setHovered] = useState(null); const selected = new Set(selectedIds); const visibleCategories = categories.filter( (c) => !filter || c.sCategoryName.includes(filter) || c.sCategoryCode.includes(filter) ); // 表头全选只反映/操作"当前可见行",避免过滤态下越权授权或误清。 const allChecked = visibleCategories.length > 0 && visibleCategories.every((c) => selected.has(c.iIncrement)); return ( {visibleCategories.map((c, i) => { const isHover = hovered === c.sCategoryCode; const checked = selected.has(c.iIncrement); return ( setHovered(c.sCategoryCode)} onMouseLeave={() => setHovered(null)} onClick={disabled ? undefined : () => setPermission(c.iIncrement, !checked)} style={{ background: isHover && !disabled ? "var(--bg-row-hover)" : checked ? "var(--accent-soft)" : i % 2 === 0 ? "#fff" : "var(--bg-row-zebra)", cursor: disabled ? "default" : "pointer", height: 24, }} > ); })}
{ visibleCategories.forEach((c) => setPermission(c.iIncrement, v)); }} disabled={disabled} /> 权限分类 setFilter(e.target.value)} placeholder="过滤..." style={{ height: 20, padding: "0 6px", border: "1px solid var(--border-input)", background: "#fff", fontSize: 11, width: 120, fontFamily: "inherit", outline: "none", }} />
setPermission(c.iIncrement, v)} disabled={disabled} /> {c.sCategoryName}
); } interface ScopeTabProps { tabKey: string; enabled: boolean; onToggle: (v: boolean) => void; disabled?: boolean; } function ScopeTab({ tabKey, enabled, onToggle, disabled }: ScopeTabProps) { const cfg = SCOPE_ITEMS[tabKey]; const [filter, setFilter] = useState(""); if (!cfg) return null; const filtered = cfg.items.filter((it) => !filter || it.includes(filter)); return (
setFilter(e.target.value)} placeholder={`筛选${cfg.label}...`} style={{ height: 24, padding: "0 8px", border: "1px solid var(--border-input)", background: "var(--bg-input)", fontSize: 12, width: 200, fontFamily: "inherit", outline: "none", }} /> 共 {cfg.items.length} 个{cfg.label}
{filtered.map((it) => ( ))}
); }