fix: QA 第二轮修复 — PatientDetail 重构/测试覆盖/id_number 列宽/小程序 URL 规范化
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 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:
iven
2026-04-25 10:22:44 +08:00
parent 55a3fd32d0
commit 0bf1822fa9
34 changed files with 1110 additions and 641 deletions

View File

@@ -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 };
}