feat(web): 告警管理前端页面 + 路由注册 + bugfix
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

新增:
- AlertList 告警列表页: 状态筛选/确认/忽略操作
- AlertRuleList 告警规则页: 创建/编辑/启停管理
- alerts + deviceReadings 前端 API 层
- App.tsx 路由注册 + MainLayout 标题 fallback
- wiki/frontend.md 更新页面清单

修复:
- ArticleEditor: 修复 unused variable 构建错误
- FollowUpTaskList: 修复 filter(Boolean) 类型窄化问题
This commit is contained in:
iven
2026-04-27 07:38:47 +08:00
parent 3424a33b6b
commit 5f83080ab8
9 changed files with 800 additions and 13 deletions

View 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>
);
}