useUserList.ts 5.4 KB
// 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<void>;
}

export function useUserList(): UseUserListReturn {
  const { message } = AntdApp.useApp();

  const [query, setQuery] = useState<UserListQuery>({ ...DEFAULT_QUERY });
  const [list, setList] = useState<UserVO[]>([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(true); // initialLoading
  const [error, setError] = useState<ApiError | null>(null);
  const [exporting, setExporting] = useState(false);

  // 最新 query 镜像,避免 refresh / 闭包读到旧值
  const queryRef = useRef<UserListQuery>(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,
  };
}