- 新增 AI 透析分析 API + 药物提醒 API - MediaPicker/ThemeSwitcher/usePaginatedData 优化 - 健康管理页面组件增强(Banner/Consultation/Doctor/MediaLibrary 等) - PluginCRUDPage 导入优化
393 lines
14 KiB
TypeScript
393 lines
14 KiB
TypeScript
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||
import { Table, Tag, Button, Modal, Form, InputNumber, DatePicker, Select, message, Popconfirm, Space, Input } from 'antd';
|
||
import { useSearchParams } from 'react-router-dom';
|
||
import { PlusOutlined, EditOutlined, DeleteOutlined, AuditOutlined } from '@ant-design/icons';
|
||
import { dayjs } from '../../utils/dayjs';
|
||
import type { Dayjs } from 'dayjs';
|
||
import { dialysisApi } from '../../api/health/dialysis';
|
||
import type { DialysisRecord } from '../../api/health/dialysis';
|
||
import { patientApi } from '../../api/health/patients';
|
||
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
||
import { AuthButton } from '../../components/AuthButton';
|
||
import { handleApiError } from '../../api/client';
|
||
|
||
const DIALYSIS_TYPE_MAP: Record<string, { color: string; label: string }> = {
|
||
HD: { color: 'blue', label: 'HD' },
|
||
HDF: { color: 'green', label: 'HDF' },
|
||
HF: { color: 'purple', label: 'HF' },
|
||
};
|
||
|
||
const STATUS_MAP: Record<string, { color: string; label: string }> = {
|
||
pending: { color: 'orange', label: '待审核' },
|
||
reviewed: { color: 'green', label: '已审核' },
|
||
};
|
||
|
||
interface PatientOption {
|
||
id: string;
|
||
name: string;
|
||
}
|
||
|
||
export default function DialysisManageList() {
|
||
const [searchParams] = useSearchParams();
|
||
const urlPatientId = searchParams.get('patient_id');
|
||
const [selectedPatientId, setSelectedPatientId] = useState<string | null>(urlPatientId);
|
||
const [patientOptions, setPatientOptions] = useState<PatientOption[]>([]);
|
||
const [patientSearch, setPatientSearch] = useState('');
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [editingRecord, setEditingRecord] = useState<DialysisRecord | null>(null);
|
||
const [form] = Form.useForm();
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [reviewOpen, setReviewOpen] = useState(false);
|
||
const [reviewRecord, setReviewRecord] = useState<DialysisRecord | null>(null);
|
||
const [reviewSubmitting, setReviewSubmitting] = useState(false);
|
||
|
||
// URL 带了 patient_id 时预加载患者选项
|
||
useEffect(() => {
|
||
if (urlPatientId) {
|
||
patientApi.get(urlPatientId).then((p) => {
|
||
if (p) setPatientOptions([{ id: p.id, name: p.name }]);
|
||
}).catch(() => {});
|
||
}
|
||
}, [urlPatientId]);
|
||
|
||
const searchPatients = async (keyword: string) => {
|
||
if (!keyword.trim()) {
|
||
setPatientOptions([]);
|
||
return;
|
||
}
|
||
try {
|
||
const res = await patientApi.list({ search: keyword, page: 1, page_size: 20 });
|
||
setPatientOptions(
|
||
(res.data || []).map((p) => ({ id: p.id, name: p.name })),
|
||
);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
};
|
||
|
||
const fetcher = useCallback(
|
||
async (page: number, pageSize: number) => {
|
||
if (!selectedPatientId) return { data: [], total: 0, page: 1 };
|
||
return dialysisApi.listRecords(selectedPatientId, { page, page_size: pageSize });
|
||
},
|
||
[selectedPatientId],
|
||
);
|
||
|
||
const { data, total, page, loading, refresh } = usePaginatedData<DialysisRecord>(fetcher, 10);
|
||
|
||
const openCreateModal = () => {
|
||
if (!selectedPatientId) {
|
||
message.warning('请先选择患者');
|
||
return;
|
||
}
|
||
setEditingRecord(null);
|
||
form.resetFields();
|
||
form.setFieldsValue({ dialysis_type: 'HD' });
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const openEditModal = (record: DialysisRecord) => {
|
||
setEditingRecord(record);
|
||
form.setFieldsValue({
|
||
dialysis_date: dayjs(record.dialysis_date),
|
||
start_time: record.start_time,
|
||
end_time: record.end_time,
|
||
dialysis_type: record.dialysis_type,
|
||
pre_weight: record.pre_weight,
|
||
post_weight: record.post_weight,
|
||
dry_weight: record.dry_weight,
|
||
pre_bp_systolic: record.pre_bp_systolic,
|
||
pre_bp_diastolic: record.pre_bp_diastolic,
|
||
post_bp_systolic: record.post_bp_systolic,
|
||
post_bp_diastolic: record.post_bp_diastolic,
|
||
pre_heart_rate: record.pre_heart_rate,
|
||
post_heart_rate: record.post_heart_rate,
|
||
ultrafiltration_volume: record.ultrafiltration_volume,
|
||
dialysis_duration: record.dialysis_duration,
|
||
blood_flow_rate: record.blood_flow_rate,
|
||
complication_notes: record.complication_notes,
|
||
});
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleSubmit = async (values: Record<string, unknown>) => {
|
||
setSubmitting(true);
|
||
try {
|
||
const payload = {
|
||
...values,
|
||
patient_id: selectedPatientId!,
|
||
dialysis_date: (values.dialysis_date as Dayjs).format('YYYY-MM-DD'),
|
||
};
|
||
if (editingRecord) {
|
||
await dialysisApi.updateRecord(editingRecord.id, {
|
||
...payload,
|
||
version: editingRecord.version,
|
||
});
|
||
message.success('透析记录更新成功');
|
||
} else {
|
||
await dialysisApi.createRecord(payload as Parameters<typeof dialysisApi.createRecord>[0]);
|
||
message.success('透析记录添加成功');
|
||
}
|
||
setModalOpen(false);
|
||
setEditingRecord(null);
|
||
form.resetFields();
|
||
refresh();
|
||
} catch (err) {
|
||
handleApiError(err, editingRecord ? '更新失败' : '添加失败');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (record: DialysisRecord) => {
|
||
try {
|
||
await dialysisApi.deleteRecord(record.id, record.version);
|
||
message.success('删除成功');
|
||
refresh();
|
||
} catch (err) {
|
||
handleApiError(err, '删除失败');
|
||
}
|
||
};
|
||
|
||
const handleReview = async () => {
|
||
if (!reviewRecord) return;
|
||
setReviewSubmitting(true);
|
||
try {
|
||
await dialysisApi.reviewRecord(reviewRecord.id, { version: reviewRecord.version });
|
||
message.success('审核完成');
|
||
setReviewOpen(false);
|
||
setReviewRecord(null);
|
||
refresh();
|
||
} catch (err) {
|
||
handleApiError(err, '审核失败');
|
||
} finally {
|
||
setReviewSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const columns = useMemo(
|
||
() => [
|
||
{ title: '透析日期', dataIndex: 'dialysis_date', key: 'dialysis_date', width: 110 },
|
||
{
|
||
title: '类型',
|
||
dataIndex: 'dialysis_type',
|
||
key: 'dialysis_type',
|
||
width: 70,
|
||
render: (v: string) => {
|
||
const m = DIALYSIS_TYPE_MAP[v];
|
||
return m ? <Tag color={m.color}>{m.label}</Tag> : <Tag>{v}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
width: 80,
|
||
render: (v: string) => {
|
||
const m = STATUS_MAP[v];
|
||
return m ? <Tag color={m.color}>{m.label}</Tag> : <Tag>{v}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '透前体重',
|
||
dataIndex: 'pre_weight',
|
||
key: 'pre_weight',
|
||
width: 90,
|
||
render: (v: number | null) => (v != null ? `${v} kg` : '-'),
|
||
},
|
||
{
|
||
title: '透后体重',
|
||
dataIndex: 'post_weight',
|
||
key: 'post_weight',
|
||
width: 90,
|
||
render: (v: number | null) => (v != null ? `${v} kg` : '-'),
|
||
},
|
||
{
|
||
title: '超滤量',
|
||
dataIndex: 'ultrafiltration_volume',
|
||
key: 'ultrafiltration_volume',
|
||
width: 80,
|
||
render: (v: number | null) => (v != null ? `${v} ml` : '-'),
|
||
},
|
||
{
|
||
title: '时长(分)',
|
||
dataIndex: 'dialysis_duration',
|
||
key: 'dialysis_duration',
|
||
width: 80,
|
||
render: (v: number | null) => v ?? '-',
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 200,
|
||
render: (_: unknown, record: DialysisRecord) => (
|
||
<AuthButton code="health.dialysis.manage">
|
||
<Space size={0}>
|
||
{record.status === 'pending' && (
|
||
<Button type="link" size="small" icon={<AuditOutlined />} onClick={() => { setReviewRecord(record); setReviewOpen(true); }}>
|
||
审核
|
||
</Button>
|
||
)}
|
||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEditModal(record)}>
|
||
编辑
|
||
</Button>
|
||
<Popconfirm title="确认删除该记录?" onConfirm={() => handleDelete(record)} okText="确认" cancelText="取消">
|
||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
</AuthButton>
|
||
),
|
||
},
|
||
],
|
||
[openEditModal, handleDelete],
|
||
);
|
||
|
||
return (
|
||
<div>
|
||
<div style={{ marginBottom: 16, display: 'flex', gap: 12, alignItems: 'center' }}>
|
||
<Select
|
||
showSearch
|
||
style={{ width: 280 }}
|
||
placeholder="搜索选择患者"
|
||
filterOption={false}
|
||
onSearch={(v) => {
|
||
setPatientSearch(v);
|
||
searchPatients(v);
|
||
}}
|
||
onChange={(v) => setSelectedPatientId(v)}
|
||
notFoundContent={patientSearch ? '未找到患者' : '输入姓名搜索'}
|
||
options={patientOptions.map((p) => ({ value: p.id, label: p.name }))}
|
||
value={selectedPatientId || undefined}
|
||
/>
|
||
<Button icon={<PlusOutlined />} onClick={openCreateModal} disabled={!selectedPatientId}>
|
||
添加记录
|
||
</Button>
|
||
</div>
|
||
|
||
{selectedPatientId ? (
|
||
<Table
|
||
columns={columns}
|
||
dataSource={data}
|
||
rowKey="id"
|
||
loading={loading}
|
||
size="small"
|
||
pagination={{
|
||
current: page,
|
||
total,
|
||
pageSize: 10,
|
||
onChange: (p) => refresh(p),
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
style: { margin: 0 },
|
||
}}
|
||
/>
|
||
) : (
|
||
<div style={{ textAlign: 'center', padding: 60, color: '#999' }}>
|
||
请先选择患者查看透析记录
|
||
</div>
|
||
)}
|
||
|
||
<Modal
|
||
title={editingRecord ? '编辑透析记录' : '添加透析记录'}
|
||
open={modalOpen}
|
||
onCancel={() => {
|
||
setModalOpen(false);
|
||
setEditingRecord(null);
|
||
}}
|
||
onOk={() => form.submit()}
|
||
confirmLoading={submitting}
|
||
destroyOnHidden
|
||
width={640}
|
||
>
|
||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||
<Form.Item name="dialysis_date" label="透析日期" rules={[{ required: true, message: '请选择日期' }]}>
|
||
<DatePicker style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<div style={{ display: 'flex', gap: 16 }}>
|
||
<Form.Item name="dialysis_type" label="透析方式" style={{ flex: 1 }}>
|
||
<Select
|
||
options={[
|
||
{ value: 'HD', label: 'HD (血液透析)' },
|
||
{ value: 'HDF', label: 'HDF (血液透析滤过)' },
|
||
{ value: 'HF', label: 'HF (血液滤过)' },
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="dialysis_duration" label="时长(分钟)" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={600} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16 }}>
|
||
<Form.Item name="pre_weight" label="透前体重(kg)" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={300} step={0.1} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="post_weight" label="透后体重(kg)" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={300} step={0.1} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="dry_weight" label="干体重(kg)" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={300} step={0.1} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16 }}>
|
||
<Form.Item name="pre_bp_systolic" label="透前收缩压" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={300} placeholder="mmHg" style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="pre_bp_diastolic" label="透前舒张压" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={200} placeholder="mmHg" style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16 }}>
|
||
<Form.Item name="post_bp_systolic" label="透后收缩压" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={300} placeholder="mmHg" style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="post_bp_diastolic" label="透后舒张压" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={200} placeholder="mmHg" style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16 }}>
|
||
<Form.Item name="pre_heart_rate" label="透前心率" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={250} placeholder="bpm" style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="post_heart_rate" label="透后心率" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={250} placeholder="bpm" style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16 }}>
|
||
<Form.Item name="ultrafiltration_volume" label="超滤量(ml)" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={10000} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="blood_flow_rate" label="血流量(ml/min)" style={{ flex: 1 }}>
|
||
<InputNumber min={0} max={600} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</div>
|
||
<Form.Item name="complication_notes" label="并发症备注">
|
||
<Input.TextArea rows={2} placeholder="可选" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title="审核透析记录"
|
||
open={reviewOpen}
|
||
onCancel={() => {
|
||
setReviewOpen(false);
|
||
setReviewRecord(null);
|
||
}}
|
||
onOk={handleReview}
|
||
confirmLoading={reviewSubmitting}
|
||
width={400}
|
||
>
|
||
{reviewRecord && (
|
||
<div>
|
||
<p><strong>透析日期:</strong>{reviewRecord.dialysis_date}</p>
|
||
<p><strong>透析方式:</strong>{reviewRecord.dialysis_type}</p>
|
||
<p><strong>时长:</strong>{reviewRecord.dialysis_duration ?? '-'} 分钟</p>
|
||
<p>确认审核通过该透析记录?</p>
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|