feat(web): 危急值阈值 + 诊断记录 Web UI — Phase 2b-2/2b-3
危急值阈值:CRUD 列表页(指标/方向/阈值/级别/科室/年龄范围) 诊断记录:患者范围 CRUD 列表页(ICD编码/类型/状态/确诊日期)
This commit is contained in:
236
apps/web/src/pages/health/CriticalValueThresholdList.tsx
Normal file
236
apps/web/src/pages/health/CriticalValueThresholdList.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
Button, Form, Input, InputNumber, message, Modal, Popconfirm, Result, Select, Space, Switch, Table, Tag,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
import {
|
||||
criticalValueThresholdApi,
|
||||
type CriticalValueThreshold,
|
||||
type CreateThresholdReq,
|
||||
type UpdateThresholdReq,
|
||||
INDICATOR_OPTIONS,
|
||||
DIRECTION_OPTIONS,
|
||||
LEVEL_OPTIONS,
|
||||
LEVEL_COLOR,
|
||||
INDICATOR_LABEL,
|
||||
DIRECTION_LABEL,
|
||||
LEVEL_LABEL,
|
||||
} from '../../api/health/criticalValueThresholds';
|
||||
import { PageContainer } from '../../components/PageContainer';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
|
||||
export default function CriticalValueThresholdList() {
|
||||
const { hasPermission } = usePermission('health.critical-value-thresholds.list');
|
||||
|
||||
const [data, setData] = useState<CriticalValueThreshold[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editRecord, setEditRecord] = useState<CriticalValueThreshold | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = await criticalValueThresholdApi.list();
|
||||
setData(list);
|
||||
setLoaded(true);
|
||||
} catch {
|
||||
message.error('加载危急值阈值失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditRecord(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ level: 'critical' });
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: CriticalValueThreshold) => {
|
||||
setEditRecord(record);
|
||||
form.setFieldsValue({
|
||||
indicator: record.indicator,
|
||||
direction: record.direction,
|
||||
threshold_value: record.threshold_value,
|
||||
level: record.level,
|
||||
department: record.department,
|
||||
age_min: record.age_min,
|
||||
age_max: record.age_max,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
|
||||
if (editRecord) {
|
||||
const req: UpdateThresholdReq = {
|
||||
threshold_value: values.threshold_value,
|
||||
level: values.level,
|
||||
department: values.department,
|
||||
age_min: values.age_min,
|
||||
age_max: values.age_max,
|
||||
version: editRecord.version,
|
||||
};
|
||||
await criticalValueThresholdApi.update(editRecord.id, req);
|
||||
message.success('阈值已更新');
|
||||
} else {
|
||||
const req: CreateThresholdReq = values;
|
||||
await criticalValueThresholdApi.create(req);
|
||||
message.success('阈值已创建');
|
||||
}
|
||||
|
||||
setModalOpen(false);
|
||||
fetchData();
|
||||
} catch {
|
||||
// validation
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (record: CriticalValueThreshold) => {
|
||||
try {
|
||||
await criticalValueThresholdApi.delete(record.id);
|
||||
message.success('阈值已删除');
|
||||
fetchData();
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<CriticalValueThreshold> = [
|
||||
{
|
||||
title: '指标',
|
||||
dataIndex: 'indicator',
|
||||
width: 120,
|
||||
render: (v: string) => INDICATOR_LABEL[v] ?? v,
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
dataIndex: 'direction',
|
||||
width: 80,
|
||||
render: (v: string) => DIRECTION_LABEL[v] ?? v,
|
||||
},
|
||||
{
|
||||
title: '阈值',
|
||||
dataIndex: 'threshold_value',
|
||||
width: 100,
|
||||
render: (v: number) => v,
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
width: 80,
|
||||
render: (v: string) => (
|
||||
<Tag color={LEVEL_COLOR[v] ?? 'default'}>{LEVEL_LABEL[v] ?? v}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '科室',
|
||||
dataIndex: 'department',
|
||||
width: 100,
|
||||
render: (v: string) => v ?? '通用',
|
||||
},
|
||||
{
|
||||
title: '年龄范围',
|
||||
width: 120,
|
||||
render: (_, record) => {
|
||||
if (record.age_min == null && record.age_max == null) return '不限';
|
||||
return `${record.age_min ?? 0} - ${record.age_max ?? '∞'}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'is_active',
|
||||
width: 80,
|
||||
render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Popconfirm title="确定删除此阈值规则?" onConfirm={() => handleDelete(record)}>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (!hasPermission) {
|
||||
return <Result status="403" title="权限不足" subTitle="您没有查看危急值阈值的权限" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="危急值阈值"
|
||||
actions={<Button type="primary" onClick={handleCreate}>添加阈值</Button>}
|
||||
>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={fetchData} loading={loading}>
|
||||
{loaded ? '刷新' : '加载阈值'}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{loaded && (
|
||||
<Table<CriticalValueThreshold>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={editRecord ? '编辑阈值' : '添加阈值'}
|
||||
open={modalOpen}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
confirmLoading={submitting}
|
||||
width={520}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{!editRecord && (
|
||||
<>
|
||||
<Form.Item name="indicator" label="指标" rules={[{ required: true, message: '请选择指标' }]}>
|
||||
<Select options={INDICATOR_OPTIONS} placeholder="选择指标" />
|
||||
</Form.Item>
|
||||
<Form.Item name="direction" label="方向" rules={[{ required: true, message: '请选择方向' }]}>
|
||||
<Select options={DIRECTION_OPTIONS} placeholder="偏高/偏低" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item name="threshold_value" label="阈值" rules={[{ required: true, message: '请输入阈值' }]}>
|
||||
<InputNumber style={{ width: '100%' }} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item name="level" label="级别">
|
||||
<Select options={LEVEL_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="department" label="限定科室">
|
||||
<Input placeholder="留空表示通用规则" />
|
||||
</Form.Item>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="age_min" label="最小年龄">
|
||||
<InputNumber min={0} max={150} placeholder="不限" style={{ width: 200 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="age_max" label="最大年龄">
|
||||
<InputNumber min={0} max={150} placeholder="不限" style={{ width: 200 }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
271
apps/web/src/pages/health/DiagnosisList.tsx
Normal file
271
apps/web/src/pages/health/DiagnosisList.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Button, DatePicker, Form, Input, message, Modal, Popconfirm,
|
||||
Result, Select, Space, Table, Tag,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
diagnosisApi,
|
||||
type Diagnosis,
|
||||
type CreateDiagnosisReq,
|
||||
type UpdateDiagnosisReq,
|
||||
DIAGNOSIS_TYPE_OPTIONS,
|
||||
DIAGNOSIS_STATUS_OPTIONS,
|
||||
DIAGNOSIS_TYPE_COLOR,
|
||||
DIAGNOSIS_STATUS_COLOR,
|
||||
DIAGNOSIS_TYPE_LABEL,
|
||||
DIAGNOSIS_STATUS_LABEL,
|
||||
} from '../../api/health/diagnoses';
|
||||
import { PageContainer } from '../../components/PageContainer';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
|
||||
export default function DiagnosisList() {
|
||||
const { hasPermission } = usePermission('health.health-data.list');
|
||||
|
||||
const [data, setData] = useState<Diagnosis[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [patientId, setPatientId] = useState('');
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editRecord, setEditRecord] = useState<Diagnosis | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
const fetchData = useCallback(async (pid: string, p: number) => {
|
||||
if (!pid) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await diagnosisApi.list(pid, { page: p, page_size: pageSize });
|
||||
setData(resp.data);
|
||||
setTotal(resp.total);
|
||||
setPage(p);
|
||||
} catch {
|
||||
message.error('加载诊断记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
const pid = searchInput.trim();
|
||||
if (!pid) {
|
||||
message.warning('请输入患者 ID');
|
||||
return;
|
||||
}
|
||||
setPatientId(pid);
|
||||
fetchData(pid, 1);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditRecord(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ patient_id: patientId, diagnosis_type: 'primary', status: 'active' });
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: Diagnosis) => {
|
||||
setEditRecord(record);
|
||||
form.setFieldsValue({
|
||||
icd_code: record.icd_code,
|
||||
diagnosis_name: record.diagnosis_name,
|
||||
diagnosis_type: record.diagnosis_type,
|
||||
diagnosed_date: record.diagnosed_date ? dayjs(record.diagnosed_date) : undefined,
|
||||
status: record.status,
|
||||
diagnosed_by: record.diagnosed_by,
|
||||
notes: record.notes,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const req = {
|
||||
...values,
|
||||
diagnosed_date: values.diagnosed_date?.format('YYYY-MM-DD'),
|
||||
};
|
||||
|
||||
setSubmitting(true);
|
||||
if (editRecord) {
|
||||
await diagnosisApi.update(editRecord.id, {
|
||||
...req,
|
||||
version: editRecord.version,
|
||||
} as UpdateDiagnosisReq & { version: number });
|
||||
message.success('诊断记录已更新');
|
||||
} else {
|
||||
await diagnosisApi.create(req.patient_id, req as CreateDiagnosisReq);
|
||||
message.success('诊断记录已创建');
|
||||
}
|
||||
setModalOpen(false);
|
||||
fetchData(patientId, page);
|
||||
} catch {
|
||||
// validation
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (record: Diagnosis) => {
|
||||
try {
|
||||
await diagnosisApi.delete(record.id, record.version);
|
||||
message.success('诊断记录已删除');
|
||||
fetchData(patientId, page);
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<Diagnosis> = useMemo(() => [
|
||||
{
|
||||
title: 'ICD 编码',
|
||||
dataIndex: 'icd_code',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
title: '诊断名称',
|
||||
dataIndex: 'diagnosis_name',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'diagnosis_type',
|
||||
width: 100,
|
||||
render: (v: string) => (
|
||||
<Tag color={DIAGNOSIS_TYPE_COLOR[v] ?? 'default'}>
|
||||
{DIAGNOSIS_TYPE_LABEL[v] ?? v}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '确诊日期',
|
||||
dataIndex: 'diagnosed_date',
|
||||
width: 110,
|
||||
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 80,
|
||||
render: (v: string) => (
|
||||
<Tag color={DIAGNOSIS_STATUS_COLOR[v] ?? 'default'}>
|
||||
{DIAGNOSIS_STATUS_LABEL[v] ?? v}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'notes',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
render: (v: string) => v ?? '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Popconfirm title="确定删除此诊断记录?" onConfirm={() => handleDelete(record)}>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
], [patientId, page]);
|
||||
|
||||
if (!hasPermission) {
|
||||
return <Result status="403" title="权限不足" subTitle="您没有查看诊断记录的权限" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="诊断记录"
|
||||
actions={patientId ? <Button type="primary" onClick={handleCreate}>添加诊断</Button> : undefined}
|
||||
>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Input.Search
|
||||
placeholder="输入患者 ID 搜索"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: 360 }}
|
||||
enterButton="查询"
|
||||
/>
|
||||
{patientId && (
|
||||
<Tag color="blue">当前患者: {patientId}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{!patientId ? (
|
||||
<Result title="请输入患者 ID 查询诊断记录" />
|
||||
) : (
|
||||
<Table<Diagnosis>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p) => fetchData(patientId, p),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={editRecord ? '编辑诊断' : '添加诊断'}
|
||||
open={modalOpen}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
confirmLoading={submitting}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{!editRecord && (
|
||||
<Form.Item name="patient_id" label="患者 ID" rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="icd_code" label="ICD 编码" rules={[{ required: true, message: '请输入 ICD 编码' }]} style={{ width: 200 }}>
|
||||
<Input placeholder="如:N18.9" />
|
||||
</Form.Item>
|
||||
<Form.Item name="diagnosis_name" label="诊断名称" rules={[{ required: true, message: '请输入诊断名称' }]} style={{ width: 320 }}>
|
||||
<Input placeholder="如:慢性肾脏病" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="diagnosis_type" label="类型" style={{ width: 200 }}>
|
||||
<Select options={DIAGNOSIS_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="diagnosed_date" label="确诊日期" rules={[{ required: true, message: '请选择确诊日期' }]} style={{ width: 260 }}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="status" label="状态" style={{ width: 200 }}>
|
||||
<Select options={DIAGNOSIS_STATUS_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="diagnosed_by" label="确诊医生 ID" style={{ width: 320 }}>
|
||||
<Input placeholder="医生 UUID(可选)" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="notes" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user