- PatientList: handleCreateOrEdit/handleDelete/openEditModal 使用 execute - AppointmentList: handleStatusChange(2处)/handleSubmit 使用 execute - FollowUpTaskList: handleCreate/handleRecordSubmit/handleAssign/handleDelete 使用 execute - 移除不再需要的 message 导入(PatientList/FollowUpTaskList)
487 lines
14 KiB
TypeScript
487 lines
14 KiB
TypeScript
import { useState, useCallback, useRef } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
Table,
|
|
Button,
|
|
Space,
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Popconfirm,
|
|
DatePicker,
|
|
} from 'antd';
|
|
import {
|
|
PlusOutlined,
|
|
EditOutlined,
|
|
DeleteOutlined,
|
|
} from '@ant-design/icons';
|
|
import { patientApi } from '../../api/health/patients';
|
|
import type {
|
|
PatientListItem,
|
|
PatientDetail,
|
|
CreatePatientReq,
|
|
UpdatePatientReq,
|
|
} from '../../api/health/patients';
|
|
import { StatusTag } from './components/StatusTag';
|
|
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS, STATUS_OPTIONS } from '../../constants/health';
|
|
import { AuthButton } from '../../components/AuthButton';
|
|
import { PageContainer } from '../../components/PageContainer';
|
|
import { DrawerForm } from '../../components/DrawerForm';
|
|
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
|
import { useApiRequest } from '../../hooks/useApiRequest';
|
|
import { calcAge, formatDateTime } from '../../utils/format';
|
|
import { dayjs } from '../../utils/dayjs';
|
|
|
|
/** 筛选器结构 */
|
|
interface PatientFilters {
|
|
search: string;
|
|
status: string;
|
|
gender: string;
|
|
dateRange: [string, string] | null;
|
|
}
|
|
|
|
const DEFAULT_FILTERS: PatientFilters = {
|
|
search: '',
|
|
status: '',
|
|
gender: '',
|
|
dateRange: null,
|
|
};
|
|
|
|
export default function PatientList() {
|
|
const navigate = useNavigate();
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editingPatient, setEditingPatient] = useState<PatientDetail | null>(null);
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
|
|
|
// ---- API 请求 Hook ----
|
|
const { execute } = useApiRequest();
|
|
|
|
// ---- 分页数据 Hook ----
|
|
const {
|
|
data: patients,
|
|
total,
|
|
page,
|
|
loading,
|
|
filters,
|
|
setFilters,
|
|
refresh,
|
|
} = usePaginatedData<PatientListItem, PatientFilters>(
|
|
async (p, pageSize, f) => {
|
|
const result = await patientApi.list({
|
|
page: p,
|
|
page_size: pageSize,
|
|
search: f.search || undefined,
|
|
status: f.status || undefined,
|
|
});
|
|
return result;
|
|
},
|
|
{ pageSize: 20, defaultFilters: { ...DEFAULT_FILTERS } },
|
|
);
|
|
|
|
// ---- 筛选回调 ----
|
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const handleSearchChange = useCallback(
|
|
(value: string) => {
|
|
setFilters((prev) => ({ ...prev, search: value }));
|
|
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
debounceTimer.current = setTimeout(() => {
|
|
refresh(1);
|
|
}, 300);
|
|
},
|
|
[setFilters, refresh],
|
|
);
|
|
|
|
const handleFilterChange = useCallback(
|
|
(key: keyof PatientFilters, value: string | [string, string] | null) => {
|
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
|
refresh(1);
|
|
},
|
|
[setFilters, refresh],
|
|
);
|
|
|
|
const handleResetFilters = useCallback(() => {
|
|
setFilters({ ...DEFAULT_FILTERS });
|
|
refresh(1);
|
|
}, [setFilters, refresh]);
|
|
|
|
// ---- CRUD 操作 ----
|
|
|
|
const handleCreateOrEdit = async (values: Record<string, unknown>) => {
|
|
const birthDate = values.birth_date;
|
|
const formattedBirthDate =
|
|
birthDate && typeof birthDate === 'object' && 'format' in (birthDate as object)
|
|
? (birthDate as { format: (f: string) => string }).format('YYYY-MM-DD')
|
|
: (birthDate as string | undefined);
|
|
|
|
const payload = {
|
|
name: values.name as string,
|
|
gender: values.gender as string | undefined,
|
|
birth_date: formattedBirthDate,
|
|
blood_type: values.blood_type as string | undefined,
|
|
id_number: values.id_number as string | undefined,
|
|
allergy_history: values.allergy_history as string | undefined,
|
|
medical_history_summary: values.medical_history_summary as string | undefined,
|
|
emergency_contact_name: values.emergency_contact_name as string | undefined,
|
|
emergency_contact_phone: values.emergency_contact_phone as string | undefined,
|
|
source: values.source as string | undefined,
|
|
notes: values.notes as string | undefined,
|
|
};
|
|
|
|
const successMsg = editingPatient ? '患者信息更新成功' : '患者创建成功';
|
|
const result = await execute(
|
|
async () => {
|
|
if (editingPatient) {
|
|
const req: UpdatePatientReq & { version: number } = {
|
|
...payload,
|
|
version: editingPatient.version,
|
|
};
|
|
return patientApi.update(editingPatient.id, req);
|
|
}
|
|
const req: CreatePatientReq = payload;
|
|
return patientApi.create(req);
|
|
},
|
|
successMsg,
|
|
'操作失败',
|
|
);
|
|
if (result !== null) {
|
|
closeModal();
|
|
refresh();
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
const patient = patients.find((p) => p.id === id);
|
|
const version = patient?.version ?? 0;
|
|
const result = await execute(
|
|
() => patientApi.delete(id, version),
|
|
'患者已删除',
|
|
);
|
|
if (result !== null) refresh();
|
|
};
|
|
|
|
const openCreateModal = () => {
|
|
setEditingPatient(null);
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const openEditModal = async (record: PatientListItem) => {
|
|
const detail = await execute(
|
|
() => patientApi.get(record.id),
|
|
undefined,
|
|
'获取患者详情失败',
|
|
);
|
|
if (detail) {
|
|
setEditingPatient(detail);
|
|
setModalOpen(true);
|
|
}
|
|
};
|
|
|
|
const closeModal = () => {
|
|
setModalOpen(false);
|
|
setEditingPatient(null);
|
|
};
|
|
|
|
// ---- 列定义 ----
|
|
|
|
const columns = [
|
|
{
|
|
title: '姓名',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
render: (name: string, record: PatientListItem) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<div
|
|
style={{
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 8,
|
|
background: 'linear-gradient(135deg, #0ea5e9, #38bdf8)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{(name?.[0] || 'P').toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontWeight: 500, fontSize: 14 }}>{name}</div>
|
|
<div style={{ fontSize: 12, color: '#94a3b8' }}>
|
|
{record.source && <span>来源: {record.source}</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '性别',
|
|
dataIndex: 'gender',
|
|
key: 'gender',
|
|
width: 80,
|
|
render: (v?: string) => {
|
|
if (!v) return '-';
|
|
const map: Record<string, string> = {
|
|
male: '男',
|
|
female: '女',
|
|
other: '其他',
|
|
};
|
|
return map[v] || v;
|
|
},
|
|
},
|
|
{
|
|
title: '年龄',
|
|
dataIndex: 'birth_date',
|
|
key: 'birth_date',
|
|
width: 100,
|
|
render: (v?: string) => calcAge(v),
|
|
},
|
|
{
|
|
title: '血型',
|
|
dataIndex: 'blood_type',
|
|
key: 'blood_type',
|
|
width: 80,
|
|
render: (v?: string) => v || '-',
|
|
},
|
|
{
|
|
title: '状态',
|
|
key: 'status',
|
|
width: 140,
|
|
render: (_: unknown, record: PatientListItem) => (
|
|
<Space size={4}>
|
|
<StatusTag status={record.status} />
|
|
<StatusTag status={record.verification_status} />
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'created_at',
|
|
key: 'created_at',
|
|
width: 150,
|
|
render: (v: string) => formatDateTime(v),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 140,
|
|
render: (_: unknown, record: PatientListItem) => (
|
|
<AuthButton code="health.patient.manage">
|
|
<Space size={4}>
|
|
<Button
|
|
size="small"
|
|
type="text"
|
|
icon={<EditOutlined />}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
openEditModal(record);
|
|
}}
|
|
/>
|
|
<Popconfirm
|
|
title="确定删除此患者?"
|
|
onConfirm={(e) => {
|
|
e?.stopPropagation();
|
|
handleDelete(record.id);
|
|
}}
|
|
>
|
|
<Button
|
|
size="small"
|
|
type="text"
|
|
icon={<DeleteOutlined />}
|
|
danger
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
</Popconfirm>
|
|
</Space>
|
|
</AuthButton>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PageContainer
|
|
title="患者管理"
|
|
subtitle="管理患者档案、基本信息和认证状态"
|
|
filters={
|
|
<>
|
|
<Input
|
|
placeholder="搜索患者姓名..."
|
|
value={filters.search}
|
|
onChange={(e) => handleSearchChange(e.target.value)}
|
|
allowClear
|
|
style={{ width: 200 }}
|
|
/>
|
|
<Select
|
|
placeholder="状态"
|
|
value={filters.status || undefined}
|
|
onChange={(v) => handleFilterChange('status', v ?? '')}
|
|
options={STATUS_OPTIONS}
|
|
allowClear
|
|
style={{ width: 130 }}
|
|
/>
|
|
<Select
|
|
placeholder="性别"
|
|
value={filters.gender || undefined}
|
|
onChange={(v) => handleFilterChange('gender', v ?? '')}
|
|
options={GENDER_OPTIONS}
|
|
allowClear
|
|
style={{ width: 120 }}
|
|
/>
|
|
<DatePicker.RangePicker
|
|
onChange={(dates) => {
|
|
if (dates && dates[0] && dates[1]) {
|
|
handleFilterChange('dateRange', [
|
|
dates[0].format('YYYY-MM-DD'),
|
|
dates[1].format('YYYY-MM-DD'),
|
|
]);
|
|
} else {
|
|
handleFilterChange('dateRange', null);
|
|
}
|
|
}}
|
|
placeholder={['开始日期', '结束日期']}
|
|
/>
|
|
</>
|
|
}
|
|
onResetFilters={handleResetFilters}
|
|
actions={
|
|
<AuthButton code="health.patient.manage">
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
|
|
新建患者
|
|
</Button>
|
|
</AuthButton>
|
|
}
|
|
selectedCount={selectedRowKeys.length}
|
|
onClearSelection={() => setSelectedRowKeys([])}
|
|
batchActions={
|
|
<span style={{ fontSize: 13, color: '#94a3b8' }}>
|
|
已选择 {selectedRowKeys.length} 项
|
|
</span>
|
|
}
|
|
loading={loading}
|
|
>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={patients}
|
|
rowKey="id"
|
|
loading={loading}
|
|
rowSelection={{
|
|
selectedRowKeys,
|
|
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
|
}}
|
|
onRow={(record) => ({
|
|
onClick: () => navigate(`/health/patients/${record.id}`),
|
|
style: { cursor: 'pointer' },
|
|
})}
|
|
pagination={{
|
|
current: page,
|
|
total,
|
|
pageSize: 20,
|
|
onChange: (p) => refresh(p),
|
|
showTotal: (t) => `共 ${t} 条记录`,
|
|
style: { padding: '12px 16px', margin: 0 },
|
|
}}
|
|
/>
|
|
|
|
{/* 新建/编辑患者抽屉 */}
|
|
<DrawerForm
|
|
title={editingPatient ? '编辑患者' : '新建患者'}
|
|
open={modalOpen}
|
|
onClose={closeModal}
|
|
onSubmit={handleCreateOrEdit}
|
|
initialValues={
|
|
editingPatient
|
|
? {
|
|
name: editingPatient.name,
|
|
gender: editingPatient.gender,
|
|
birth_date: editingPatient.birth_date ? dayjs(editingPatient.birth_date) : undefined,
|
|
blood_type: editingPatient.blood_type,
|
|
id_number: editingPatient.id_number,
|
|
allergy_history: editingPatient.allergy_history,
|
|
medical_history_summary: editingPatient.medical_history_summary,
|
|
emergency_contact_name: editingPatient.emergency_contact_name,
|
|
emergency_contact_phone: editingPatient.emergency_contact_phone,
|
|
source: editingPatient.source,
|
|
notes: editingPatient.notes,
|
|
}
|
|
: undefined
|
|
}
|
|
width={640}
|
|
columns={2}
|
|
sections={[
|
|
{
|
|
title: '基本信息',
|
|
fields: (
|
|
<>
|
|
<Form.Item
|
|
name="name"
|
|
label="姓名"
|
|
rules={[{ required: true, message: '请输入患者姓名' }]}
|
|
>
|
|
<Input placeholder="请输入姓名" />
|
|
</Form.Item>
|
|
<Form.Item name="gender" label="性别">
|
|
<Select options={GENDER_OPTIONS} placeholder="请选择性别" allowClear />
|
|
</Form.Item>
|
|
<Form.Item name="birth_date" label="出生日期">
|
|
<DatePicker style={{ width: '100%' }} placeholder="请选择出生日期" />
|
|
</Form.Item>
|
|
<Form.Item name="blood_type" label="血型">
|
|
<Select
|
|
options={BLOOD_TYPE_OPTIONS}
|
|
placeholder="请选择血型"
|
|
allowClear
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
title: '联系方式',
|
|
fields: (
|
|
<>
|
|
<Form.Item name="id_number" label="身份证号">
|
|
<Input placeholder="请输入身份证号" />
|
|
</Form.Item>
|
|
<Form.Item name="source" label="来源">
|
|
<Input placeholder="请输入患者来源" />
|
|
</Form.Item>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
title: '医疗信息',
|
|
fields: (
|
|
<>
|
|
<Form.Item name="allergy_history" label="过敏史">
|
|
<Input.TextArea rows={2} placeholder="请输入过敏史" />
|
|
</Form.Item>
|
|
<Form.Item name="medical_history_summary" label="病史摘要">
|
|
<Input.TextArea rows={2} placeholder="请输入病史摘要" />
|
|
</Form.Item>
|
|
<Form.Item name="notes" label="备注">
|
|
<Input.TextArea rows={2} placeholder="请输入备注" />
|
|
</Form.Item>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
title: '紧急联系人',
|
|
fields: (
|
|
<>
|
|
<Form.Item name="emergency_contact_name" label="联系人姓名">
|
|
<Input placeholder="请输入紧急联系人姓名" />
|
|
</Form.Item>
|
|
<Form.Item name="emergency_contact_phone" label="联系电话">
|
|
<Input placeholder="请输入紧急联系人电话" />
|
|
</Form.Item>
|
|
</>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</PageContainer>
|
|
);
|
|
}
|