Files
hms/apps/web/src/pages/health/DialysisManageList.tsx
iven e4e5ef04d4 feat(web): Web 前端功能完善 — API 扩展 + 组件优化
- 新增 AI 透析分析 API + 药物提醒 API
- MediaPicker/ThemeSwitcher/usePaginatedData 优化
- 健康管理页面组件增强(Banner/Consultation/Doctor/MediaLibrary 等)
- PluginCRUDPage 导入优化
2026-05-13 23:28:22 +08:00

393 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}