Files
hms/apps/web/src/pages/health/CriticalValueThresholdList.tsx
iven 15b5781dbb
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(health): 危急值告警全链路修复 — 消费者生命周期 + payload 映射 + 阈值优先级
1. CRITICAL: 修复 SubscriptionHandle 提前 drop 导致所有事件消费者失效
   - register_handlers_with_state 中所有 handle 在函数返回时被 drop
   - cancel channel 关闭导致 subscribe_filtered 的过滤任务退出
   - 方案: 收集所有 handle 并 std::mem::forget,生命周期与进程一致

2. HIGH: 修复 critical_alert 消费者 payload 字段映射不匹配
   - 消费者读取 alert_type/metric_name 等顶层字段,但实际在 alert 嵌套对象中
   - 更新消费者从 alert 对象提取 indicator/value/threshold/level
   - handle_critical_alert_event 增加 severity 参数

3. MEDIUM: 修复 check_indicator 优先匹配最高严重级别
   - 原实现返回第一个匹配的阈值(可能匹配 warning 而非 critical)
   - 改为遍历所有匹配阈值,选择 severity 最高的(critical > warning)

4. MEDIUM: 修复危急值阈值页面不自动加载数据
   - CriticalValueThresholdList 添加 useEffect 初始化加载
2026-05-05 10:11:06 +08:00

241 lines
7.1 KiB
TypeScript

import { useState, useCallback, useEffect } 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);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
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>
);
}