feat(web): 告警管理前端页面 + 路由注册 + bugfix
新增: - AlertList 告警列表页: 状态筛选/确认/忽略操作 - AlertRuleList 告警规则页: 创建/编辑/启停管理 - alerts + deviceReadings 前端 API 层 - App.tsx 路由注册 + MainLayout 标题 fallback - wiki/frontend.md 更新页面清单 修复: - ArticleEditor: 修复 unused variable 构建错误 - FollowUpTaskList: 修复 filter(Boolean) 类型窄化问题
This commit is contained in:
251
apps/web/src/pages/health/AlertRuleList.tsx
Normal file
251
apps/web/src/pages/health/AlertRuleList.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Form, Input, InputNumber, message, Modal, Select, Space, Switch, Table, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
import {
|
||||
createAlertRule,
|
||||
deactivateAlertRule,
|
||||
listAlertRules,
|
||||
updateAlertRule,
|
||||
type AlertRule,
|
||||
type CreateAlertRuleReq,
|
||||
type UpdateAlertRuleReq,
|
||||
} from '../../api/health/alerts';
|
||||
|
||||
const DEVICE_TYPES = [
|
||||
{ label: '心率', value: 'heart_rate' },
|
||||
{ label: '血氧', value: 'blood_oxygen' },
|
||||
{ label: '体温', value: 'temperature' },
|
||||
{ label: '步数', value: 'steps' },
|
||||
{ label: '睡眠', value: 'sleep' },
|
||||
{ label: '压力', value: 'stress' },
|
||||
];
|
||||
|
||||
const CONDITION_TYPES = [
|
||||
{ label: '单次阈值', value: 'single_threshold' },
|
||||
{ label: '连续触发', value: 'consecutive' },
|
||||
{ label: '趋势变化', value: 'trend' },
|
||||
];
|
||||
|
||||
const SEVERITY_OPTIONS = [
|
||||
{ label: '提示', value: 'info' },
|
||||
{ label: '警告', value: 'warning' },
|
||||
{ label: '严重', value: 'critical' },
|
||||
{ label: '紧急', value: 'urgent' },
|
||||
];
|
||||
|
||||
const SEVERITY_COLOR: Record<string, string> = {
|
||||
info: 'default',
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
urgent: 'magenta',
|
||||
};
|
||||
|
||||
export default function AlertRuleList() {
|
||||
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 listAlertRules({ 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 updateAlertRule(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 createAlertRule(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 deactivateAlertRule(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_TYPES.find((d) => d.value === v)?.label || v,
|
||||
},
|
||||
{
|
||||
title: '条件类型',
|
||||
dataIndex: 'condition_type',
|
||||
width: 120,
|
||||
render: (v: string) => CONDITION_TYPES.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_TYPES} disabled={!!editingRule} />
|
||||
</Form.Item>
|
||||
<Form.Item name="condition_type" label="条件类型" rules={[{ required: true }]}>
|
||||
<Select style={{ width: 160 }} options={CONDITION_TYPES} 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user