- refactor(web): PatientDetail.tsx 拆分为 4 个子组件(737→334行) - refactor(web): 提取 usePaginatedData hook 消除重复分页状态 - feat(db): patient.id_number varchar(20)→varchar(255) 容纳加密值 - test(health): 添加预约模块集成测试(创建/列表/租户隔离) - test(plugin): 添加 6 个 SQL 注入 sanitize 测试 - fix(miniprogram): 7 个 service 文件 URL 构建规范化(params 对象) - fix(miniprogram): 跨平台字段名对齐(birth_date/start_time/end_time)
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from 'react';
|
||
import { message } from 'antd';
|
||
|
||
interface PaginatedState<T> {
|
||
data: T[];
|
||
total: number;
|
||
page: number;
|
||
loading: boolean;
|
||
}
|
||
|
||
/**
|
||
* 通用分页数据 Hook,封装 data / total / page / loading / fetch 逻辑。
|
||
*
|
||
* 支持两种签名:
|
||
* 1. 三参数 (page, pageSize, search) — 带搜索的列表页
|
||
* 2. 两参数 (page, pageSize) — 纯分页,不含搜索
|
||
*
|
||
* @param fetchFn - 数据获取函数
|
||
* @param pageSize - 每页条数,默认 20
|
||
* @param autoFetch - 是否在 mount / fetchFn 变化时自动请求第一页,默认 true
|
||
*/
|
||
export function usePaginatedData<T>(
|
||
fetchFn:
|
||
| ((page: number, pageSize: number, search: string) => Promise<{ data: T[]; total: number }>)
|
||
| ((page: number, pageSize: number) => Promise<{ data: T[]; total: number }>),
|
||
pageSize = 20,
|
||
autoFetch = true,
|
||
) {
|
||
const [state, setState] = useState<PaginatedState<T>>({
|
||
data: [],
|
||
total: 0,
|
||
page: 1,
|
||
loading: false,
|
||
});
|
||
const [searchText, setSearchText] = useState('');
|
||
|
||
// 用 ref 保存最新 fetchFn,避免 refresh 因闭包引用过期 fetchFn 而频繁重建
|
||
const fetchFnRef = useRef(fetchFn);
|
||
fetchFnRef.current = fetchFn;
|
||
|
||
// 用 ref 保存最新 searchText,同理
|
||
const searchTextRef = useRef(searchText);
|
||
searchTextRef.current = searchText;
|
||
|
||
const refresh = useCallback(
|
||
async (p?: number) => {
|
||
const targetPage = p ?? state.page;
|
||
setState((s) => ({ ...s, loading: true }));
|
||
try {
|
||
// 统一按三参数调用;若 fetchFn 只接受两参数,第三个参数会被忽略
|
||
const result = await (fetchFnRef.current as (
|
||
page: number,
|
||
pageSize: number,
|
||
search: string,
|
||
) => Promise<{ data: T[]; total: number }>)(
|
||
targetPage,
|
||
pageSize,
|
||
searchTextRef.current,
|
||
);
|
||
setState({ data: result.data, total: result.total, page: targetPage, loading: false });
|
||
} catch {
|
||
message.error('加载数据失败');
|
||
setState((s) => ({ ...s, loading: false }));
|
||
}
|
||
},
|
||
[pageSize, state.page],
|
||
);
|
||
|
||
// mount 或 fetchFn 变化时自动请求
|
||
useEffect(() => {
|
||
if (autoFetch) {
|
||
refresh(1);
|
||
}
|
||
}, [autoFetch, refresh]);
|
||
|
||
return { ...state, searchText, setSearchText, refresh };
|
||
}
|