perf(web): ConsultationList/FollowUpTaskList 移除 N+1 nameCache
后端已内联 patient_name/doctor_name,前端移除逐条查询。 Session/FollowUpTask 接口添加 name 可选字段。 FollowUpTaskList 保留 assignee 的 getUser 查询(users 表未内联)。
This commit is contained in:
@@ -6,6 +6,8 @@ export interface Session {
|
|||||||
id: string;
|
id: string;
|
||||||
patient_id: string;
|
patient_id: string;
|
||||||
doctor_id?: string;
|
doctor_id?: string;
|
||||||
|
patient_name?: string;
|
||||||
|
doctor_name?: string;
|
||||||
consultation_type: string;
|
consultation_type: string;
|
||||||
status: string;
|
status: string;
|
||||||
last_message_at?: string;
|
last_message_at?: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface FollowUpTask {
|
|||||||
id: string;
|
id: string;
|
||||||
patient_id: string;
|
patient_id: string;
|
||||||
assigned_to?: string;
|
assigned_to?: string;
|
||||||
|
patient_name?: string;
|
||||||
follow_up_type: string;
|
follow_up_type: string;
|
||||||
planned_date: string;
|
planned_date: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|||||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { consultationApi, type Session, type CreateSessionReq } from '../../api/health/consultations';
|
import { consultationApi, type Session, type CreateSessionReq } from '../../api/health/consultations';
|
||||||
import { patientApi } from '../../api/health/patients';
|
|
||||||
import { doctorApi } from '../../api/health/doctors';
|
|
||||||
import { StatusTag } from './components/StatusTag';
|
import { StatusTag } from './components/StatusTag';
|
||||||
import { PatientSelect } from './components/PatientSelect';
|
import { PatientSelect } from './components/PatientSelect';
|
||||||
import { DoctorSelect } from './components/DoctorSelect';
|
import { DoctorSelect } from './components/DoctorSelect';
|
||||||
@@ -69,10 +67,6 @@ export default function ConsultationList() {
|
|||||||
// Close session
|
// Close session
|
||||||
const [closingId, setClosingId] = useState<string | null>(null);
|
const [closingId, setClosingId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Label caches
|
|
||||||
const [patientLabels, setPatientLabels] = useState<Record<string, string>>({});
|
|
||||||
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
const isDark = useThemeMode();
|
const isDark = useThemeMode();
|
||||||
|
|
||||||
// --- Data fetching ---
|
// --- Data fetching ---
|
||||||
@@ -82,48 +76,12 @@ export default function ConsultationList() {
|
|||||||
const result = await consultationApi.listSessions(params);
|
const result = await consultationApi.listSessions(params);
|
||||||
setSessions(result.data);
|
setSessions(result.data);
|
||||||
setTotal(result.total);
|
setTotal(result.total);
|
||||||
|
|
||||||
// 批量解析患者名称
|
|
||||||
const patientIds = [...new Set(result.data.map((s) => s.patient_id))];
|
|
||||||
const missingPatientIds = patientIds.filter((id) => !patientLabels[id]);
|
|
||||||
if (missingPatientIds.length > 0) {
|
|
||||||
const newLabels: Record<string, string> = {};
|
|
||||||
await Promise.all(
|
|
||||||
missingPatientIds.map(async (id) => {
|
|
||||||
try {
|
|
||||||
const detail = await patientApi.get(id);
|
|
||||||
newLabels[id] = detail.name;
|
|
||||||
} catch {
|
|
||||||
newLabels[id] = id.slice(0, 8);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setPatientLabels((prev) => ({ ...prev, ...newLabels }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量解析医生名称
|
|
||||||
const doctorIds = [...new Set(result.data.map((s) => s.doctor_id).filter(Boolean))] as string[];
|
|
||||||
const missingDoctorIds = doctorIds.filter((id) => !doctorLabels[id]);
|
|
||||||
if (missingDoctorIds.length > 0) {
|
|
||||||
const newLabels: Record<string, string> = {};
|
|
||||||
await Promise.all(
|
|
||||||
missingDoctorIds.map(async (id) => {
|
|
||||||
try {
|
|
||||||
const detail = await doctorApi.get(id);
|
|
||||||
newLabels[id] = detail.name;
|
|
||||||
} catch {
|
|
||||||
newLabels[id] = id.slice(0, 8);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setDoctorLabels((prev) => ({ ...prev, ...newLabels }));
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载咨询列表失败');
|
message.error('加载咨询列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [patientLabels, doctorLabels]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSessions(query);
|
fetchSessions(query);
|
||||||
@@ -142,14 +100,6 @@ export default function ConsultationList() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePatientLabel = (id: string, label: string) => {
|
|
||||||
setPatientLabels((prev) => ({ ...prev, [id]: label }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDoctorLabel = (id: string, label: string) => {
|
|
||||||
setDoctorLabels((prev) => ({ ...prev, [id]: label }));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -195,18 +145,19 @@ export default function ConsultationList() {
|
|||||||
const columns: ColumnsType<Session> = [
|
const columns: ColumnsType<Session> = [
|
||||||
{
|
{
|
||||||
title: '患者',
|
title: '患者',
|
||||||
dataIndex: 'patient_id',
|
dataIndex: 'patient_name',
|
||||||
key: 'patient_id',
|
key: 'patient_name',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (id: string) => patientLabels[id] || id.slice(0, 8),
|
render: (_: unknown, record: Session) =>
|
||||||
|
record.patient_name ?? record.patient_id.slice(0, 8),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '医护',
|
title: '医护',
|
||||||
dataIndex: 'doctor_id',
|
dataIndex: 'doctor_name',
|
||||||
key: 'doctor_id',
|
key: 'doctor_name',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (id: string | undefined) =>
|
render: (_: unknown, record: Session) =>
|
||||||
id ? doctorLabels[id] || id.slice(0, 8) : '-',
|
record.doctor_name ?? record.doctor_id?.slice(0, 8) ?? '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '咨询类型',
|
title: '咨询类型',
|
||||||
@@ -383,14 +334,10 @@ export default function ConsultationList() {
|
|||||||
label="患者"
|
label="患者"
|
||||||
rules={[{ required: true, message: '请选择患者' }]}
|
rules={[{ required: true, message: '请选择患者' }]}
|
||||||
>
|
>
|
||||||
<PatientSelect
|
<PatientSelect />
|
||||||
onChange={(_val, label) => handlePatientLabel(_val, label)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="doctor_id" label="医护">
|
<Form.Item name="doctor_id" label="医护">
|
||||||
<DoctorSelect
|
<DoctorSelect />
|
||||||
onChange={(_val, label) => handleDoctorLabel(_val, label)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="consultation_type" label="咨询类型">
|
<Form.Item name="consultation_type" label="咨询类型">
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-d
|
|||||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
|
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
|
||||||
import { patientApi } from '../../api/health/patients';
|
|
||||||
import { getUser } from '../../api/users';
|
import { getUser } from '../../api/users';
|
||||||
import { StatusTag } from './components/StatusTag';
|
import { StatusTag } from './components/StatusTag';
|
||||||
import { PatientSelect } from './components/PatientSelect';
|
import { PatientSelect } from './components/PatientSelect';
|
||||||
@@ -93,8 +92,7 @@ export default function FollowUpTaskList() {
|
|||||||
const [assignForm] = Form.useForm<AssignFormValues>();
|
const [assignForm] = Form.useForm<AssignFormValues>();
|
||||||
const [assignTask, setAssignTask] = useState<FollowUpTask | null>(null);
|
const [assignTask, setAssignTask] = useState<FollowUpTask | null>(null);
|
||||||
|
|
||||||
// Patient/doctor label cache for display
|
// Doctor label cache (for assignee display from users table)
|
||||||
const [patientLabels, setPatientLabels] = useState<Record<string, string>>({});
|
|
||||||
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
|
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const isDark = useThemeMode();
|
const isDark = useThemeMode();
|
||||||
@@ -107,41 +105,29 @@ export default function FollowUpTaskList() {
|
|||||||
setTasks(result.data);
|
setTasks(result.data);
|
||||||
setTotal(result.total);
|
setTotal(result.total);
|
||||||
|
|
||||||
// Batch resolve patient names
|
// Batch resolve assignee names (from users table, not inlined by backend)
|
||||||
const patientIds = [...new Set(result.data.map((t: FollowUpTask) => t.patient_id).filter(Boolean))];
|
|
||||||
const newLabels: Record<string, string> = {};
|
|
||||||
await Promise.allSettled(
|
|
||||||
patientIds.map(async (id: string) => {
|
|
||||||
try {
|
|
||||||
const p = await patientApi.get(id);
|
|
||||||
newLabels[id] = p.name;
|
|
||||||
} catch { /* skip */ }
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (Object.keys(newLabels).length > 0) {
|
|
||||||
setPatientLabels((prev) => ({ ...prev, ...newLabels }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch resolve assignee names
|
|
||||||
const assigneeIds = [...new Set(result.data.map((t: FollowUpTask) => t.assigned_to).filter((x): x is string => !!x))];
|
const assigneeIds = [...new Set(result.data.map((t: FollowUpTask) => t.assigned_to).filter((x): x is string => !!x))];
|
||||||
const newDoctorLabels: Record<string, string> = {};
|
const missingIds = assigneeIds.filter((id) => !doctorLabels[id]);
|
||||||
await Promise.allSettled(
|
if (missingIds.length > 0) {
|
||||||
assigneeIds.map(async (id) => {
|
const newDoctorLabels: Record<string, string> = {};
|
||||||
try {
|
await Promise.allSettled(
|
||||||
const u = await getUser(id);
|
missingIds.map(async (id) => {
|
||||||
newDoctorLabels[id] = u.display_name || u.username;
|
try {
|
||||||
} catch { /* skip */ }
|
const u = await getUser(id);
|
||||||
}),
|
newDoctorLabels[id] = u.display_name || u.username;
|
||||||
);
|
} catch { /* skip */ }
|
||||||
if (Object.keys(newDoctorLabels).length > 0) {
|
}),
|
||||||
setDoctorLabels((prev) => ({ ...prev, ...newDoctorLabels }));
|
);
|
||||||
|
if (Object.keys(newDoctorLabels).length > 0) {
|
||||||
|
setDoctorLabels((prev) => ({ ...prev, ...newDoctorLabels }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载随访任务失败');
|
message.error('加载随访任务失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [doctorLabels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTasks(query);
|
fetchTasks(query);
|
||||||
@@ -260,10 +246,6 @@ export default function FollowUpTaskList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Store labels from selects
|
// Store labels from selects
|
||||||
const handlePatientLabel = (id: string, label: string) => {
|
|
||||||
setPatientLabels((prev) => ({ ...prev, [id]: label }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDoctorLabel = (id: string, label: string) => {
|
const handleDoctorLabel = (id: string, label: string) => {
|
||||||
setDoctorLabels((prev) => ({ ...prev, [id]: label }));
|
setDoctorLabels((prev) => ({ ...prev, [id]: label }));
|
||||||
};
|
};
|
||||||
@@ -272,10 +254,11 @@ export default function FollowUpTaskList() {
|
|||||||
const columns: ColumnsType<FollowUpTask> = [
|
const columns: ColumnsType<FollowUpTask> = [
|
||||||
{
|
{
|
||||||
title: '患者',
|
title: '患者',
|
||||||
dataIndex: 'patient_id',
|
dataIndex: 'patient_name',
|
||||||
key: 'patient_id',
|
key: 'patient_name',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (id: string) => patientLabels[id] || id.slice(0, 8),
|
render: (_: unknown, record: FollowUpTask) =>
|
||||||
|
record.patient_name ?? record.patient_id.slice(0, 8),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '随访类型',
|
title: '随访类型',
|
||||||
@@ -445,9 +428,7 @@ export default function FollowUpTaskList() {
|
|||||||
label="患者"
|
label="患者"
|
||||||
rules={[{ required: true, message: '请选择患者' }]}
|
rules={[{ required: true, message: '请选择患者' }]}
|
||||||
>
|
>
|
||||||
<PatientSelect
|
<PatientSelect />
|
||||||
onChange={(_val, label) => handlePatientLabel(_val, label)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="follow_up_type"
|
name="follow_up_type"
|
||||||
|
|||||||
Reference in New Issue
Block a user