useUserList.ts
5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// 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;
// 卸载守卫:避免异步取数 resolve/reject 在组件卸载后 setState(防内存泄漏 / 测试环境拆除后报错)
const mountedRef = useRef(true);
/** 以给定 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);
if (!mountedRef.current) return;
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) {
if (!mountedRef.current) return;
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(() => {
mountedRef.current = true;
void runFetch({ ...DEFAULT_QUERY });
return () => {
mountedRef.current = false;
};
// 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);
if (mountedRef.current) messageRef.current.success(TEXT_EXPORT_SUCCESS);
} catch {
if (mountedRef.current) messageRef.current.error(TEXT_EXPORT_FAIL);
} finally {
if (mountedRef.current) setExporting(false);
}
}, [total]);
return {
list,
total,
loading,
error,
query,
exporting,
search,
refresh,
clear,
setQueryField,
setMatchType,
setQueryValue,
changePage,
exportExcel,
};
}