P0 阻塞修复:
- 修复 PrivateRoute 权限旁路: p.startsWith('auth.') 匹配不到任何权限码,
改为基于实际权限码的路由级检查 (user.manage/role.manage/organization.manage)
- 修复 deviceReadings API 路径: /patients/{id}/device-readings/daily 改为
/vital-signs/daily?patient_id=, 消除 404
P1 重要修复:
- 补全事件注册表: 新增 auth(11) + config(8) + workflow(4) + plugin(2) = 25 条
- article_article_tag 联表新增 tenant_id + deleted_at + 审计列 (迁移 107)
- vital_signs_hourly 新增 deleted_at 支持软删除过滤 (迁移 108)
- 6 个页面添加权限守卫 (AlertDashboard/AlertRuleList/DeviceManage/
AiAnalysisList/AiUsageDashboard)
- DialysisModule 声明 auth 依赖
224 lines
7.0 KiB
TypeScript
224 lines
7.0 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { Button, Form, Input, InputNumber, message, Modal, Result, Select, Space, Switch, Table, Tag } from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
import {
|
|
alertRuleApi,
|
|
type AlertRule,
|
|
type CreateAlertRuleReq,
|
|
type UpdateAlertRuleReq,
|
|
} from '../../api/health/alerts';
|
|
import { SEVERITY_COLOR, SEVERITY_OPTIONS, DEVICE_TYPE_OPTIONS, CONDITION_TYPE_OPTIONS } from '../../constants/health';
|
|
import { usePermission } from '../../hooks/usePermission';
|
|
|
|
export default function AlertRuleList() {
|
|
const { hasPermission } = usePermission('health.alerts.list');
|
|
if (!hasPermission) return <Result status="403" title="权限不足" subTitle="您没有查看告警规则的权限" />;
|
|
const [data, setData] = useState<AlertRule[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editingRule, setEditingRule] = useState<AlertRule | null>(null);
|
|
const [form] = Form.useForm();
|
|
|
|
const fetchRules = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await alertRuleApi.list({ page, page_size: 20 });
|
|
setData(res.data);
|
|
setTotal(res.total);
|
|
} catch {
|
|
message.error('加载规则列表失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [page]);
|
|
|
|
useEffect(() => {
|
|
fetchRules();
|
|
}, [fetchRules]);
|
|
|
|
const openCreateModal = () => {
|
|
setEditingRule(null);
|
|
form.resetFields();
|
|
form.setFieldsValue({
|
|
severity: 'warning',
|
|
cooldown_minutes: 60,
|
|
});
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const openEditModal = (rule: AlertRule) => {
|
|
setEditingRule(rule);
|
|
form.setFieldsValue({
|
|
name: rule.name,
|
|
description: rule.description,
|
|
device_type: rule.device_type,
|
|
condition_type: rule.condition_type,
|
|
condition_params: JSON.stringify(rule.condition_params, null, 2),
|
|
severity: rule.severity,
|
|
cooldown_minutes: rule.cooldown_minutes,
|
|
});
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
const conditionParams = JSON.parse(values.condition_params);
|
|
|
|
if (editingRule) {
|
|
const req: UpdateAlertRuleReq = {
|
|
name: values.name,
|
|
description: values.description,
|
|
condition_params: conditionParams,
|
|
severity: values.severity,
|
|
cooldown_minutes: values.cooldown_minutes,
|
|
version: editingRule.version,
|
|
};
|
|
await alertRuleApi.update(editingRule.id, req);
|
|
message.success('规则已更新');
|
|
} else {
|
|
const req: CreateAlertRuleReq = {
|
|
name: values.name,
|
|
description: values.description,
|
|
device_type: values.device_type,
|
|
condition_type: values.condition_type,
|
|
condition_params: conditionParams,
|
|
severity: values.severity,
|
|
cooldown_minutes: values.cooldown_minutes,
|
|
};
|
|
await alertRuleApi.create(req);
|
|
message.success('规则已创建');
|
|
}
|
|
setModalOpen(false);
|
|
fetchRules();
|
|
} catch (e) {
|
|
if (e instanceof SyntaxError) {
|
|
message.error('条件参数 JSON 格式无效');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleToggle = async (rule: AlertRule, active: boolean) => {
|
|
try {
|
|
if (!active) {
|
|
await alertRuleApi.deactivate(rule.id, rule.version);
|
|
message.success('规则已禁用');
|
|
}
|
|
fetchRules();
|
|
} catch {
|
|
message.error('操作失败');
|
|
}
|
|
};
|
|
|
|
const columns: ColumnsType<AlertRule> = [
|
|
{ title: '规则名称', dataIndex: 'name', width: 180 },
|
|
{
|
|
title: '指标类型',
|
|
dataIndex: 'device_type',
|
|
width: 100,
|
|
render: (v: string) => DEVICE_TYPE_OPTIONS.find((d) => d.value === v)?.label || v,
|
|
},
|
|
{
|
|
title: '条件类型',
|
|
dataIndex: 'condition_type',
|
|
width: 120,
|
|
render: (v: string) => CONDITION_TYPE_OPTIONS.find((c) => c.value === v)?.label || v,
|
|
},
|
|
{
|
|
title: '严重程度',
|
|
dataIndex: 'severity',
|
|
width: 90,
|
|
render: (v: string) => <Tag color={SEVERITY_COLOR[v] || 'default'}>{v}</Tag>,
|
|
},
|
|
{
|
|
title: '启用',
|
|
dataIndex: 'is_active',
|
|
width: 80,
|
|
render: (v: boolean, record) => (
|
|
<Switch checked={v} onChange={(checked) => handleToggle(record, checked)} />
|
|
),
|
|
},
|
|
{
|
|
title: '冷却(分)',
|
|
dataIndex: 'cooldown_minutes',
|
|
width: 90,
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 80,
|
|
render: (_, record) => (
|
|
<Button size="small" type="link" onClick={() => openEditModal(record)}>
|
|
编辑
|
|
</Button>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
|
<h2 style={{ margin: 0 }}>告警规则</h2>
|
|
<Button type="primary" onClick={openCreateModal}>新建规则</Button>
|
|
</div>
|
|
|
|
<Table<AlertRule>
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data}
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
total,
|
|
pageSize: 20,
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
onChange: (p) => setPage(p),
|
|
}}
|
|
/>
|
|
|
|
<Modal
|
|
title={editingRule ? '编辑规则' : '新建规则'}
|
|
open={modalOpen}
|
|
onOk={handleSubmit}
|
|
onCancel={() => setModalOpen(false)}
|
|
width={560}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="name" label="规则名称" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="描述">
|
|
<Input.TextArea rows={2} />
|
|
</Form.Item>
|
|
<Space style={{ width: '100%' }} size="large">
|
|
<Form.Item name="device_type" label="指标类型" rules={[{ required: true }]}>
|
|
<Select style={{ width: 160 }} options={DEVICE_TYPE_OPTIONS} disabled={!!editingRule} />
|
|
</Form.Item>
|
|
<Form.Item name="condition_type" label="条件类型" rules={[{ required: true }]}>
|
|
<Select style={{ width: 160 }} options={CONDITION_TYPE_OPTIONS} disabled={!!editingRule} />
|
|
</Form.Item>
|
|
</Space>
|
|
<Form.Item
|
|
name="condition_params"
|
|
label="条件参数 (JSON)"
|
|
rules={[{ required: true }]}
|
|
extra='例如: {"direction":"above","value":100}'
|
|
>
|
|
<Input.TextArea rows={4} style={{ fontFamily: 'monospace' }} />
|
|
</Form.Item>
|
|
<Space style={{ width: '100%' }} size="large">
|
|
<Form.Item name="severity" label="严重程度">
|
|
<Select style={{ width: 140 }} options={SEVERITY_OPTIONS} />
|
|
</Form.Item>
|
|
<Form.Item name="cooldown_minutes" label="冷却时间(分钟)">
|
|
<InputNumber min={1} max={1440} style={{ width: 140 }} />
|
|
</Form.Item>
|
|
</Space>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|