// REQ-USR-001 / REQ-USR-002: 用户单据 hook(状态机 initialLoading/editing/submitting/submitError/submitSuccess/loadError) import { useCallback, useEffect, useRef, useState } from 'react'; import { App as AntdApp } from 'antd'; import { createUser, updateUser, getUserDetail, listEmployees, listPermissions, } from '../../../api/usrApi'; import { ApiError } from '../../../api/request'; import type { EmployeeOption, PermissionItem, UserVO, UserDetailMode, } from '../../../api/types'; import { CREATE_DEFAULTS, ERR_USERNAME_EXISTS, ERR_USER_NOT_FOUND, ERR_NO_PERMISSION, ERR_VALIDATION, MSG_ERR_USERNAME_EXISTS, MSG_ERR_USER_NOT_FOUND, MSG_ERR_NO_PERMISSION, MSG_ERR_VALIDATION, MSG_ERR_NETWORK, MSG_ERR_LOAD_EMPLOYEES, MSG_ERR_LOAD_PERMISSIONS, MSG_LOAD_DETAIL_FAIL, toCreateReq, toUpdateReq, userVoToFormValues, type UserFormValues, } from './constants'; export interface UseUserDetailArgs { mode: UserDetailMode; userId?: number; presetUser?: UserVO | null; } export interface SubmitFieldError { field: keyof UserFormValues; message: string; } export interface SubmitResult { ok: boolean; id?: number; fieldError?: SubmitFieldError; } export interface UseUserDetailReturn { mode: UserDetailMode; formValues: UserFormValues; employees: EmployeeOption[]; permissions: PermissionItem[]; checkedPermissionIds: number[]; readonlyCreator: string; readonlyCreateTime: string; loading: boolean; submitting: boolean; error: ApiError | null; loadFailed: boolean; notFound: boolean; setField(name: keyof UserFormValues, value: unknown): void; selectEmployee(value: number | null): void; togglePermission(id: number, checked: boolean): void; toggleAll(checked: boolean): void; submit(values: UserFormValues): Promise; reload(): void; } export function useUserDetail(args: UseUserDetailArgs): UseUserDetailReturn { const { mode, userId, presetUser } = args; const { message } = AntdApp.useApp(); const [formValues, setFormValues] = useState({ ...CREATE_DEFAULTS }); const [employees, setEmployees] = useState([]); const [permissions, setPermissions] = useState([]); const [checkedPermissionIds, setCheckedPermissionIds] = useState([]); const [readonlyCreator, setReadonlyCreator] = useState(''); const [readonlyCreateTime, setReadonlyCreateTime] = useState(''); const [loading, setLoading] = useState(true); // initialLoading const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [loadFailed, setLoadFailed] = useState(false); const [notFound, setNotFound] = useState(false); const employeesRef = useRef(employees); employeesRef.current = employees; const checkedRef = useRef(checkedPermissionIds); checkedRef.current = checkedPermissionIds; const messageRef = useRef(message); messageRef.current = message; const mountedRef = useRef(true); /** 把后端权限分类回勾:UserVO 不暴露已授权 id,故 edit 预填仅用 detail 的权限字段(无则空集) */ const initFromVo = useCallback((vo: UserVO) => { setFormValues(userVoToFormValues(vo)); setReadonlyCreator(vo.sCreator ?? ''); setReadonlyCreateTime(vo.tCreateDate ?? ''); // UserVO 不含已授权权限 id(FE-03 列表 VO),按空集初始化;后端补详情端点后可回勾 setCheckedPermissionIds([]); }, []); /** 挂载预取(员工/权限)+ edit 详情回填(initialLoading→editing / loadError) */ const runLoad = useCallback(async () => { setLoading(true); setLoadFailed(false); setNotFound(false); try { const [emps, perms] = await Promise.all([listEmployees(), listPermissions()]); if (!mountedRef.current) return; setEmployees(emps); setPermissions(perms); if (mode === 'edit') { if (presetUser) { initFromVo(presetUser); } else if (userId != null) { const vo = await getUserDetail({ queryField: '用户号', queryValue: String(userId), }); if (!mountedRef.current) return; if (vo) { initFromVo(vo); } else { // 详情不存在:交由页面按 40401 路径处理(返回列表入口) setNotFound(true); messageRef.current.error(MSG_ERR_USER_NOT_FOUND); setLoading(false); return; } } } else { setFormValues({ ...CREATE_DEFAULTS }); setCheckedPermissionIds([]); } setLoading(false); } catch (err) { if (!mountedRef.current) return; setLoading(false); setLoadFailed(true); // 区分员工/权限/详情失败文案:以 reject 顺序无法精确分辨,按权限优先(最常见空源) const apiErr = err instanceof ApiError ? err : new ApiError(-1, MSG_ERR_NETWORK); if (mode === 'edit' && employeesRef.current.length === 0 && permissions.length === 0) { messageRef.current.error(MSG_LOAD_DETAIL_FAIL); } else { messageRef.current.error( apiErr.message === MSG_ERR_LOAD_EMPLOYEES ? MSG_ERR_LOAD_EMPLOYEES : MSG_ERR_LOAD_PERMISSIONS, ); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mode, userId, presetUser, initFromVo]); useEffect(() => { mountedRef.current = true; void runLoad(); return () => { mountedRef.current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const setField = useCallback((name: keyof UserFormValues, value: unknown) => { setFormValues((prev) => ({ ...prev, [name]: value })); }, []); /** 选择员工:带出用户名(create)/ 用户号(BR5,用户仍可改) */ const selectEmployee = useCallback((value: number | null) => { setFormValues((prev) => { const emp = employeesRef.current.find((e) => e.value === value); if (!emp) return { ...prev, iEmployeeId: value }; return { ...prev, iEmployeeId: value, sUserName: emp.label, sUserNo: emp.sEmployeeNo ?? prev.sUserNo, }; }); }, []); const togglePermission = useCallback((id: number, checked: boolean) => { setCheckedPermissionIds((prev) => { if (checked) return prev.includes(id) ? prev : [...prev, id]; return prev.filter((p) => p !== id); }); }, []); const toggleAll = useCallback((checked: boolean) => { setCheckedPermissionIds(checked ? permissions.map((p) => p.id) : []); }, [permissions]); /** 提交:create→POST / edit→PUT;错误码分流(spec § 4) */ const submit = useCallback( async (values: UserFormValues): Promise => { setSubmitting(true); setError(null); try { const ids = checkedRef.current; let id: number; if (mode === 'edit' && userId != null) { const ret = await updateUser(userId, toUpdateReq(values, ids)); id = ret.id; } else { const ret = await createUser(toCreateReq(values, ids)); id = ret.id; } if (mountedRef.current) setSubmitting(false); return { ok: true, id }; } catch (err) { const apiErr = err instanceof ApiError ? err : new ApiError(-1, MSG_ERR_NETWORK); if (mountedRef.current) { setSubmitting(false); setError(apiErr); } if (apiErr.code === ERR_USERNAME_EXISTS) { messageRef.current.error(MSG_ERR_USERNAME_EXISTS); return { ok: false, fieldError: { field: 'sUserName', message: MSG_ERR_USERNAME_EXISTS }, }; } if (apiErr.code === ERR_USER_NOT_FOUND) { messageRef.current.error(MSG_ERR_USER_NOT_FOUND); } else if (apiErr.code === ERR_NO_PERMISSION) { messageRef.current.error(MSG_ERR_NO_PERMISSION); } else if (apiErr.code === ERR_VALIDATION) { messageRef.current.error(MSG_ERR_VALIDATION); } else { messageRef.current.error(MSG_ERR_NETWORK); } return { ok: false }; } }, [mode, userId], ); const reload = useCallback(() => { void runLoad(); }, [runLoad]); return { mode, formValues, employees, permissions, checkedPermissionIds, readonlyCreator, readonlyCreateTime, loading, submitting, error, loadFailed, notFound, setField, selectEmployee, togglePermission, toggleAll, submit, reload, }; }