feat(web): 实时告警仪表盘页面 + SSE Hook + 告警详情面板
- 新增 AlertDashboard 页面:实时告警列表 + 统计摘要 + 详情面板 - 新增 useAlertSSE Hook:封装 SSE 连接、自动重连、事件分发 - 新增 AlertDetailPanel 组件:告警详情展示 + 确认/忽略/恢复操作 - alertApi.list 添加 doctor_id 参数支持 - 注册 /health/alert-dashboard 路由 + 面包屑映射
This commit is contained in:
129
apps/web/src/hooks/useAlertSSE.ts
Normal file
129
apps/web/src/hooks/useAlertSSE.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
|
||||
/** SSE 事件数据结构 — alert.triggered 事件 */
|
||||
export interface AlertSSEEvent {
|
||||
alert_id: string;
|
||||
patient_id: string;
|
||||
rule_name: string;
|
||||
severity: string;
|
||||
detail?: Record<string, unknown>;
|
||||
schema_version?: string;
|
||||
occurred_at?: string;
|
||||
}
|
||||
|
||||
/** SSE 事件数据结构 — device.readings.synced 事件 */
|
||||
export interface VitalUpdateSSEEvent {
|
||||
patient_id: string;
|
||||
count: number;
|
||||
device_model?: string;
|
||||
date_range?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
};
|
||||
schema_version?: string;
|
||||
occurred_at?: string;
|
||||
}
|
||||
|
||||
interface UseAlertSSEOptions {
|
||||
/** 是否启用 SSE 连接(默认 true) */
|
||||
enabled?: boolean;
|
||||
/** 收到 alert 事件的回调 */
|
||||
onAlert?: (data: AlertSSEEvent) => void;
|
||||
/** 收到 vital_update 事件的回调 */
|
||||
onVitalUpdate?: (data: VitalUpdateSSEEvent) => void;
|
||||
}
|
||||
|
||||
interface UseAlertSSEReturn {
|
||||
/** 连接状态 */
|
||||
connected: boolean;
|
||||
/** 最近收到的 alert 事件列表(最多保留 100 条) */
|
||||
recentAlerts: AlertSSEEvent[];
|
||||
/** 手动重连 */
|
||||
reconnect: () => void;
|
||||
}
|
||||
|
||||
const MAX_RECENT_ALERTS = 100;
|
||||
|
||||
/**
|
||||
* SSE 实时告警订阅 Hook。
|
||||
*
|
||||
* 封装 EventSource 连接管理,支持:
|
||||
* - 自动重连(EventSource 内置)
|
||||
* - 事件分发(alert / vital_update)
|
||||
* - 连接状态追踪
|
||||
* - 最近告警缓存
|
||||
*/
|
||||
export function useAlertSSE(options: UseAlertSSEOptions = {}): UseAlertSSEReturn {
|
||||
const { enabled = true, onAlert, onVitalUpdate } = options;
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const reconnectKeyRef = useRef(0);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [recentAlerts, setRecentAlerts] = useState<AlertSSEEvent[]>([]);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
// 关闭旧连接
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
setConnected(false);
|
||||
|
||||
if (!enabled) return;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) return;
|
||||
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api/v1';
|
||||
const url = `${baseUrl}/messages/stream?token=${encodeURIComponent(token)}`;
|
||||
const es = new EventSource(url);
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.onopen = () => {
|
||||
setConnected(true);
|
||||
};
|
||||
|
||||
es.addEventListener('alert', (e: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data) as AlertSSEEvent;
|
||||
onAlert?.(data);
|
||||
setRecentAlerts((prev) => {
|
||||
const next = [data, ...prev];
|
||||
return next.length > MAX_RECENT_ALERTS ? next.slice(0, MAX_RECENT_ALERTS) : next;
|
||||
});
|
||||
} catch {
|
||||
// 忽略解析失败的事件
|
||||
}
|
||||
});
|
||||
|
||||
es.addEventListener('vital_update', (e: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data) as VitalUpdateSSEEvent;
|
||||
onVitalUpdate?.(data);
|
||||
} catch {
|
||||
// 忽略解析失败的事件
|
||||
}
|
||||
});
|
||||
|
||||
es.onerror = () => {
|
||||
setConnected(false);
|
||||
// EventSource 会自动重连,无需手动处理
|
||||
};
|
||||
}, [enabled, onAlert, onVitalUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
setConnected(false);
|
||||
};
|
||||
}, [connect, reconnectKeyRef.current]);
|
||||
|
||||
const reconnect = useCallback(() => {
|
||||
reconnectKeyRef.current += 1;
|
||||
}, []);
|
||||
|
||||
return { connected, recentAlerts, reconnect };
|
||||
}
|
||||
Reference in New Issue
Block a user