Files
hms/apps/web/src/pages/health/components/AlertDetailPanel.tsx
iven 27c32e5561
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
feat(web): 实时告警仪表盘页面 + SSE Hook + 告警详情面板
- 新增 AlertDashboard 页面:实时告警列表 + 统计摘要 + 详情面板
- 新增 useAlertSSE Hook:封装 SSE 连接、自动重连、事件分发
- 新增 AlertDetailPanel 组件:告警详情展示 + 确认/忽略/恢复操作
- alertApi.list 添加 doctor_id 参数支持
- 注册 /health/alert-dashboard 路由 + 面包屑映射
2026-04-28 19:59:51 +08:00

172 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Descriptions, Tag, Typography, Space, Button, Popconfirm, Tooltip } from 'antd';
import {
CheckOutlined,
StopOutlined,
SafetyCertificateOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import type { Alert } from '../../../api/health/alerts';
const SEVERITY_CONFIG: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
info: { color: 'default', label: '提示', icon: <ExclamationCircleOutlined /> },
warning: { color: 'orange', label: '警告', icon: <ExclamationCircleOutlined /> },
critical: { color: 'red', label: '严重', icon: <ExclamationCircleOutlined /> },
urgent: { color: 'magenta', label: '紧急', icon: <ExclamationCircleOutlined /> },
};
const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
pending: { color: 'orange', label: '待处理' },
acknowledged: { color: 'blue', label: '已确认' },
resolved: { color: 'green', label: '已恢复' },
dismissed: { color: 'default', label: '已忽略' },
};
interface AlertDetailPanelProps {
alert: Alert;
onAcknowledge?: (id: string, version: number) => Promise<void>;
onDismiss?: (id: string, version: number) => Promise<void>;
onResolve?: (id: string, version: number) => Promise<void>;
loading?: boolean;
}
/**
* 告警详情面板 — 展示告警完整信息及操作按钮。
*/
export function AlertDetailPanel({
alert,
onAcknowledge,
onDismiss,
onResolve,
loading = false,
}: AlertDetailPanelProps) {
const severity = SEVERITY_CONFIG[alert.severity] ?? SEVERITY_CONFIG.info;
const status = STATUS_CONFIG[alert.status] ?? STATUS_CONFIG.pending;
const isPending = alert.status === 'pending';
const isAcknowledged = alert.status === 'acknowledged';
return (
<div style={{ padding: 16 }}>
{/* 顶部摘要 */}
<div style={{ marginBottom: 16 }}>
<Space align="center" size="middle">
<Tag color={severity.color} icon={severity.icon} style={{ fontSize: 14, padding: '4px 12px' }}>
{severity.label}
</Tag>
<Tag color={status.color}>{status.label}</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
<ClockCircleOutlined style={{ marginRight: 4 }} />
{new Date(alert.created_at).toLocaleString('zh-CN')}
</Typography.Text>
</Space>
</div>
{/* 详情 */}
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="告警 ID" span={2}>
<Typography.Text copyable style={{ fontSize: 12 }}>{alert.id}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="患者 ID">
<Typography.Text copyable style={{ fontSize: 12 }}>{alert.patient_id}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="规则 ID">
<Typography.Text copyable style={{ fontSize: 12 }}>{alert.rule_id}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="严重程度">
<Tag color={severity.color}>{severity.label}</Tag>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={status.color}>{status.label}</Tag>
</Descriptions.Item>
{alert.acknowledged_by && (
<Descriptions.Item label="处理人" span={2}>
<Typography.Text style={{ fontSize: 12 }}>{alert.acknowledged_by}</Typography.Text>
</Descriptions.Item>
)}
{alert.acknowledged_at && (
<Descriptions.Item label="确认时间">
{new Date(alert.acknowledged_at).toLocaleString('zh-CN')}
</Descriptions.Item>
)}
{alert.resolved_at && (
<Descriptions.Item label="恢复时间">
{new Date(alert.resolved_at).toLocaleString('zh-CN')}
</Descriptions.Item>
)}
</Descriptions>
{/* 告警详情 JSON */}
{alert.detail && (
<div style={{ marginTop: 12 }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}></Typography.Text>
<pre style={{
fontSize: 12,
background: 'var(--ant-color-bg-layout)',
padding: 8,
borderRadius: 6,
maxHeight: 200,
overflow: 'auto',
margin: '4px 0 0',
}}>
{JSON.stringify(alert.detail, null, 2)}
</pre>
</div>
)}
{/* 操作按钮 */}
<div style={{ marginTop: 16, borderTop: '1px solid var(--ant-color-border)', paddingTop: 12 }}>
<Space>
{isPending && onAcknowledge && (
<Tooltip title="确认已知晓此告警">
<Popconfirm
title="确认此告警?"
description="确认后将标记为已确认状态"
onConfirm={() => onAcknowledge(alert.id, alert.version)}
>
<Button
type="primary"
icon={<CheckOutlined />}
loading={loading}
>
</Button>
</Popconfirm>
</Tooltip>
)}
{isPending && onDismiss && (
<Tooltip title="忽略此告警">
<Popconfirm
title="忽略此告警?"
description="告警将被标记为已忽略"
onConfirm={() => onDismiss(alert.id, alert.version)}
>
<Button icon={<StopOutlined />} loading={loading}>
</Button>
</Popconfirm>
</Tooltip>
)}
{(isPending || isAcknowledged) && onResolve && (
<Tooltip title="标记告警已恢复">
<Popconfirm
title="标记为已恢复?"
description="告警将标记为已恢复状态"
onConfirm={() => onResolve(alert.id, alert.version)}
>
<Button
type="primary"
ghost
icon={<SafetyCertificateOutlined />}
loading={loading}
>
</Button>
</Popconfirm>
</Tooltip>
)}
</Space>
</div>
</div>
);
}