fix: QA 第二轮修复 — PatientDetail 重构/测试覆盖/id_number 列宽/小程序 URL 规范化
- 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)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { message } from 'antd';
|
||||
|
||||
interface PaginatedState<T> {
|
||||
@@ -8,9 +8,23 @@ interface PaginatedState<T> {
|
||||
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 }>,
|
||||
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: [],
|
||||
@@ -20,17 +34,44 @@ export function usePaginatedData<T>(
|
||||
});
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const refresh = useCallback(async (p?: number) => {
|
||||
const targetPage = p ?? state.page;
|
||||
setState(s => ({ ...s, loading: true }));
|
||||
try {
|
||||
const result = await fetchFn(targetPage, pageSize, searchText);
|
||||
setState({ data: result.data, total: result.total, page: targetPage, loading: false });
|
||||
} catch {
|
||||
message.error('加载数据失败');
|
||||
setState(s => ({ ...s, loading: false }));
|
||||
// 用 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);
|
||||
}
|
||||
}, [fetchFn, pageSize, searchText, state.page]);
|
||||
}, [autoFetch, refresh]);
|
||||
|
||||
return { ...state, searchText, setSearchText, refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user