// REQ-USR-003: 用户列表查询 hook(状态机 initialLoading/loading/success/empty/error/exporting) import { useCallback, useEffect, useRef, useState } from 'react'; import { App as AntdApp } from 'antd'; import { listUsers } from '../../../api/usrApi'; import { ApiError } from '../../../api/request'; import type { UserVO, UserListQuery } from '../../../api/types'; import { buildUserCsv, downloadCsv } from './exportUtils'; import { DEFAULT_QUERY, ERR_PAGE_INVALID, ERR_QUERY_INVALID, EXPORT_FILENAME, TEXT_EXPORT_FAIL, TEXT_EXPORT_SUCCESS, TEXT_MSG_NETWORK, TEXT_MSG_PAGE_INVALID, TEXT_MSG_QUERY_INVALID, } from './constants'; export interface UseUserListReturn { list: UserVO[]; total: number; loading: boolean; error: ApiError | null; query: UserListQuery; exporting: boolean; search(): void; refresh(): void; clear(): void; setQueryField(v: string): void; setMatchType(v: string): void; setQueryValue(v: string): void; changePage(pageNum: number, pageSize: number): void; exportExcel(): Promise; } export function useUserList(): UseUserListReturn { const { message } = AntdApp.useApp(); const [query, setQuery] = useState({ ...DEFAULT_QUERY }); const [list, setList] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); // initialLoading const [error, setError] = useState(null); const [exporting, setExporting] = useState(false); // 最新 query 镜像,避免 refresh / 闭包读到旧值 const queryRef = useRef(query); queryRef.current = query; const messageRef = useRef(message); messageRef.current = message; /** 以给定 query 取数;同步 total/pageNum/pageSize 回显(BR15);错误码分流(spec § 4) */ const runFetch = useCallback(async (q: UserListQuery) => { setQuery(q); queryRef.current = q; setLoading(true); setError(null); try { const pageData = await listUsers(q); setList(pageData.records); setTotal(pageData.total); // 信任后端回显(越界回退最后一页等),同步分页当前页/页大小(BR15) setQuery((prev) => { const next = { ...prev, pageNum: pageData.pageNum, pageSize: pageData.pageSize }; queryRef.current = next; return next; }); setLoading(false); } catch (err) { const apiErr = err instanceof ApiError ? err : new ApiError(-1, TEXT_MSG_NETWORK); setLoading(false); if (apiErr.code === ERR_PAGE_INVALID) { // 42201:兜底重置分页为合法值后重查(pageNum=1,pageSize 收敛 ≤100) messageRef.current.warning(TEXT_MSG_PAGE_INVALID); const safePageSize = Math.min(q.pageSize, 100); void runFetch({ ...q, pageNum: 1, pageSize: safePageSize }); return; } if (apiErr.code === ERR_QUERY_INVALID) { // 40001:保留条件不自动重查 messageRef.current.error(TEXT_MSG_QUERY_INVALID); setError(apiErr); return; } // 网络 / 超时 / 5xx 兜底 messageRef.current.error(TEXT_MSG_NETWORK); setError(apiErr); } }, []); // 挂载即以默认条件取数(initialLoading,BR2) useEffect(() => { void runFetch({ ...DEFAULT_QUERY }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const setQueryField = useCallback((v: string) => { setQuery((prev) => ({ ...prev, queryField: v })); }, []); const setMatchType = useCallback((v: string) => { setQuery((prev) => ({ ...prev, matchType: v })); }, []); const setQueryValue = useCallback((v: string) => { setQuery((prev) => ({ ...prev, queryValue: v })); }, []); /** 搜索:以当前条件回第 1 页取数(BR7) */ const search = useCallback(() => { void runFetch({ ...queryRef.current, pageNum: 1 }); }, [runFetch]); /** 刷新:保持当前 query(含 pageNum)重取(BR8) */ const refresh = useCallback(() => { void runFetch({ ...queryRef.current }); }, [runFetch]); /** 清空:重置为默认条件并全量查询(BR10) */ const clear = useCallback(() => { void runFetch({ ...DEFAULT_QUERY }); }, [runFetch]); /** 切页 / 改页大小:改 pageSize 回第 1 页(BR11),仅切页保留页码 */ const changePage = useCallback( (pageNum: number, pageSize: number) => { const cur = queryRef.current; const sizeChanged = pageSize !== cur.pageSize; void runFetch({ ...cur, pageNum: sizeChanged ? 1 : pageNum, pageSize }); }, [runFetch], ); /** 导出:拉当前条件命中结果(pageSize 收敛至上限 100 一次取)→ CSV 下载(BR9 / D-PLAN-1) */ const exportExcel = useCallback(async () => { setExporting(true); try { const cur = queryRef.current; const fetchSize = Math.min(Math.max(total, cur.pageSize, 1), 100); const pageData = await listUsers({ ...cur, pageNum: 1, pageSize: fetchSize }); const csv = buildUserCsv(pageData.records); downloadCsv(EXPORT_FILENAME, csv); messageRef.current.success(TEXT_EXPORT_SUCCESS); } catch { messageRef.current.error(TEXT_EXPORT_FAIL); } finally { setExporting(false); } }, [total]); return { list, total, loading, error, query, exporting, search, refresh, clear, setQueryField, setMatchType, setQueryValue, changePage, exportExcel, }; }