fix(web): 告警详情显示患者名和规则标题替代原始 ID
- 患者字段显示 patient_name,ID 缩短为 tooltip - 规则字段显示告警标题 title,rule_id 缩短为 tooltip - 告警详情 JSON 解析为中文标签结构化表格
This commit is contained in:
@@ -1,21 +1,18 @@
|
|||||||
import { Descriptions, Tag, Typography, Space, Button, Popconfirm, Tooltip, Collapse, Alert as AntAlert } from 'antd';
|
import { Descriptions, Tag, Typography, Space, Button, Popconfirm, Tooltip } from 'antd';
|
||||||
import {
|
import {
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
SafetyCertificateOutlined,
|
SafetyCertificateOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
UserOutlined,
|
|
||||||
CodeOutlined,
|
|
||||||
MedicineBoxOutlined,
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { Alert } from '../../../api/health/alerts';
|
import type { Alert } from '../../../api/health/alerts';
|
||||||
|
|
||||||
const SEVERITY_CONFIG: Record<string, { color: string; label: string; icon: React.ReactNode; bannerType: 'info' | 'warning' | 'error' }> = {
|
const SEVERITY_CONFIG: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
|
||||||
info: { color: 'default', label: '提示', icon: <ExclamationCircleOutlined />, bannerType: 'info' },
|
info: { color: 'default', label: '提示', icon: <ExclamationCircleOutlined /> },
|
||||||
warning: { color: 'orange', label: '警告', icon: <ExclamationCircleOutlined />, bannerType: 'warning' },
|
warning: { color: 'orange', label: '警告', icon: <ExclamationCircleOutlined /> },
|
||||||
critical: { color: 'red', label: '严重', icon: <ExclamationCircleOutlined />, bannerType: 'error' },
|
critical: { color: 'red', label: '严重', icon: <ExclamationCircleOutlined /> },
|
||||||
urgent: { color: 'magenta', label: '紧急', icon: <ExclamationCircleOutlined />, bannerType: 'error' },
|
urgent: { color: 'magenta', label: '紧急', icon: <ExclamationCircleOutlined /> },
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
|
const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
|
||||||
@@ -26,13 +23,6 @@ const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
|
|||||||
dismissed: { color: 'default', label: '已忽略' },
|
dismissed: { color: 'default', label: '已忽略' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEVERITY_GUIDANCE: Record<string, string> = {
|
|
||||||
info: '请关注患者状态变化,必要时进行随访。',
|
|
||||||
warning: '建议尽快确认并安排相关检查或随访。',
|
|
||||||
critical: '需要立即关注!请确认告警并安排紧急处理。',
|
|
||||||
urgent: '紧急状况!请立即确认并采取干预措施。',
|
|
||||||
};
|
|
||||||
|
|
||||||
const DETAIL_LABEL_MAP: Record<string, string> = {
|
const DETAIL_LABEL_MAP: Record<string, string> = {
|
||||||
message: '告警描述',
|
message: '告警描述',
|
||||||
value: '监测值',
|
value: '监测值',
|
||||||
@@ -42,7 +32,6 @@ const DETAIL_LABEL_MAP: Record<string, string> = {
|
|||||||
metric_name: '指标名称',
|
metric_name: '指标名称',
|
||||||
indicator_type: '体征类型',
|
indicator_type: '体征类型',
|
||||||
recorded_at: '记录时间',
|
recorded_at: '记录时间',
|
||||||
patient_name: '患者',
|
|
||||||
blood_pressure_systolic: '收缩压',
|
blood_pressure_systolic: '收缩压',
|
||||||
blood_pressure_diastolic: '舒张压',
|
blood_pressure_diastolic: '舒张压',
|
||||||
heart_rate: '心率',
|
heart_rate: '心率',
|
||||||
@@ -64,18 +53,17 @@ function formatDetailValue(key: string, value: unknown): string {
|
|||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDetailLabel(key: string): string {
|
|
||||||
return DETAIL_LABEL_MAP[key] ?? key;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AlertDetailPanelProps {
|
interface AlertDetailPanelProps {
|
||||||
alert: Alert;
|
alert: Alert;
|
||||||
onAcknowledge?: (id: string, version: number) => Promise<void>;
|
onAcknowledge?: (id: string, version: number) => Promise<void>;
|
||||||
onDismiss?: (id: string, version: number, reason?: string) => Promise<void>;
|
onDismiss?: (id: string, version: number) => Promise<void>;
|
||||||
onResolve?: (id: string, version: number) => Promise<void>;
|
onResolve?: (id: string, version: number) => Promise<void>;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 告警详情面板 — 展示告警完整信息及操作按钮。
|
||||||
|
*/
|
||||||
export function AlertDetailPanel({
|
export function AlertDetailPanel({
|
||||||
alert,
|
alert,
|
||||||
onAcknowledge,
|
onAcknowledge,
|
||||||
@@ -92,170 +80,126 @@ export function AlertDetailPanel({
|
|||||||
? Object.entries(alert.detail).filter(([, v]) => v !== null && v !== undefined)
|
? Object.entries(alert.detail).filter(([, v]) => v !== null && v !== undefined)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const detailMessage = alert.detail?.message as string | undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 16 }}>
|
<div style={{ padding: 16 }}>
|
||||||
{/* 严重程度提示 */}
|
{/* 顶部摘要 */}
|
||||||
<AntAlert
|
<div style={{ marginBottom: 16 }}>
|
||||||
type={severity.bannerType}
|
<Space align="center" size="middle">
|
||||||
showIcon
|
<Tag color={severity.color} icon={severity.icon} style={{ fontSize: 14, padding: '4px 12px' }}>
|
||||||
icon={severity.icon}
|
{severity.label}
|
||||||
banner={false}
|
</Tag>
|
||||||
style={{ marginBottom: 12 }}
|
<Tag color={status.color}>{status.label}</Tag>
|
||||||
message={
|
|
||||||
<Space>
|
|
||||||
<span style={{ fontWeight: 600 }}>{alert.title || severity.label}</span>
|
|
||||||
<Tag color={status.color}>{status.label}</Tag>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
description={SEVERITY_GUIDANCE[alert.severity] ?? '请关注此告警并及时处理。'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 患者信息 */}
|
|
||||||
<div style={{
|
|
||||||
background: 'var(--ant-color-bg-container)',
|
|
||||||
border: '1px solid var(--ant-color-border)',
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: '12px 16px',
|
|
||||||
marginBottom: 12,
|
|
||||||
}}>
|
|
||||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
|
||||||
<Space>
|
|
||||||
<MedicineBoxOutlined style={{ color: 'var(--ant-color-primary)' }} />
|
|
||||||
<Typography.Text strong style={{ fontSize: 15 }}>
|
|
||||||
{alert.patient_name || '未知患者'}
|
|
||||||
</Typography.Text>
|
|
||||||
<Tooltip title={`患者 ID: ${alert.patient_id}`}>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12, cursor: 'help' }}>
|
|
||||||
(ID: {alert.patient_id.slice(0, 8)}...)
|
|
||||||
</Typography.Text>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||||
告警时间:{new Date(alert.created_at).toLocaleString('zh-CN')}
|
{new Date(alert.created_at).toLocaleString('zh-CN')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 详情 */}
|
||||||
|
<Descriptions column={2} size="small" bordered>
|
||||||
|
<Descriptions.Item label="患者">
|
||||||
|
<Typography.Text strong>{alert.patient_name || '未知患者'}</Typography.Text>
|
||||||
|
<Tooltip title={alert.patient_id}>
|
||||||
|
<Typography.Text type="secondary" copyable style={{ fontSize: 12, marginLeft: 8, cursor: 'help' }}>
|
||||||
|
{alert.patient_id.slice(0, 8)}...
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="规则">
|
||||||
|
<Typography.Text>{alert.title || '未知规则'}</Typography.Text>
|
||||||
|
<Tooltip title={alert.rule_id}>
|
||||||
|
<Typography.Text type="secondary" copyable style={{ fontSize: 12, marginLeft: 8, cursor: 'help' }}>
|
||||||
|
{alert.rule_id.slice(0, 8)}...
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</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>
|
||||||
|
|
||||||
{/* 告警详情 */}
|
{/* 告警详情 */}
|
||||||
{detailEntries.length > 0 && (
|
{detailEntries.length > 0 && (
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Descriptions column={2} size="small" bordered style={{ marginTop: 12 }} title="告警详情">
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 13, marginBottom: 8, display: 'block' }}>
|
{detailEntries.map(([key, value]) => (
|
||||||
<UserOutlined style={{ marginRight: 4 }} />
|
<Descriptions.Item key={key} label={DETAIL_LABEL_MAP[key] ?? key}>
|
||||||
告警详情
|
|
||||||
</Typography.Text>
|
|
||||||
<div style={{
|
|
||||||
background: 'var(--ant-color-bg-layout)',
|
|
||||||
borderRadius: 6,
|
|
||||||
padding: '8px 12px',
|
|
||||||
}}>
|
|
||||||
{detailMessage && (
|
|
||||||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: detailEntries.length > 1 ? 8 : 0 }}>
|
|
||||||
{detailMessage}
|
|
||||||
</Typography.Text>
|
|
||||||
)}
|
|
||||||
<Descriptions
|
|
||||||
column={2}
|
|
||||||
size="small"
|
|
||||||
items={detailEntries
|
|
||||||
.filter(([k]) => k !== 'message')
|
|
||||||
.map(([key, value]) => ({
|
|
||||||
key,
|
|
||||||
label: getDetailLabel(key),
|
|
||||||
children: <Typography.Text style={{ fontSize: 13 }}>{formatDetailValue(key, value)}</Typography.Text>,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 处理记录 */}
|
|
||||||
{(alert.acknowledged_by || alert.resolved_at) && (
|
|
||||||
<Descriptions column={2} size="small" bordered style={{ marginBottom: 12 }}>
|
|
||||||
{alert.acknowledged_by && (
|
|
||||||
<Descriptions.Item label="处理人">
|
|
||||||
<Typography.Text style={{ fontSize: 13 }}>{alert.acknowledged_by}</Typography.Text>
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
{alert.acknowledged_at && (
|
|
||||||
<Descriptions.Item label="确认时间">
|
|
||||||
<Typography.Text style={{ fontSize: 13 }}>
|
<Typography.Text style={{ fontSize: 13 }}>
|
||||||
{new Date(alert.acknowledged_at).toLocaleString('zh-CN')}
|
{formatDetailValue(key, value)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
))}
|
||||||
{alert.resolved_at && (
|
|
||||||
<Descriptions.Item label="恢复时间" span={2}>
|
|
||||||
<Typography.Text style={{ fontSize: 13 }}>
|
|
||||||
{new Date(alert.resolved_at).toLocaleString('zh-CN')}
|
|
||||||
</Typography.Text>
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 技术信息(折叠) */}
|
|
||||||
<Collapse
|
|
||||||
ghost
|
|
||||||
size="small"
|
|
||||||
items={[{
|
|
||||||
key: 'tech',
|
|
||||||
label: <Typography.Text type="secondary" style={{ fontSize: 12 }}><CodeOutlined /> 技术信息</Typography.Text>,
|
|
||||||
children: (
|
|
||||||
<Descriptions column={1} size="small">
|
|
||||||
<Descriptions.Item label="告警 ID">
|
|
||||||
<Typography.Text copyable style={{ fontSize: 12 }}>{alert.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="患者 ID">
|
|
||||||
<Typography.Text copyable style={{ fontSize: 12 }}>{alert.patient_id}</Typography.Text>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="版本">{alert.version}</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
),
|
|
||||||
}]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<div style={{ marginTop: 8, borderTop: '1px solid var(--ant-color-border)', paddingTop: 12 }}>
|
<div style={{ marginTop: 16, borderTop: '1px solid var(--ant-color-border)', paddingTop: 12 }}>
|
||||||
<Space>
|
<Space>
|
||||||
{isPending && onAcknowledge && (
|
{isPending && onAcknowledge && (
|
||||||
<Popconfirm
|
<Tooltip title="确认已知晓此告警">
|
||||||
title="确认此告警?"
|
<Popconfirm
|
||||||
description="确认后将标记为已确认状态"
|
title="确认此告警?"
|
||||||
onConfirm={() => onAcknowledge(alert.id, alert.version)}
|
description="确认后将标记为已确认状态"
|
||||||
>
|
onConfirm={() => onAcknowledge(alert.id, alert.version)}
|
||||||
<Button type="primary" icon={<CheckOutlined />} loading={loading}>
|
>
|
||||||
确认
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
</Popconfirm>
|
icon={<CheckOutlined />}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{isPending && onDismiss && (
|
{isPending && onDismiss && (
|
||||||
<Popconfirm
|
<Tooltip title="忽略此告警">
|
||||||
title="忽略此告警?"
|
<Popconfirm
|
||||||
description="告警将被标记为已忽略"
|
title="忽略此告警?"
|
||||||
onConfirm={() => onDismiss(alert.id, alert.version)}
|
description="告警将被标记为已忽略"
|
||||||
>
|
onConfirm={() => onDismiss(alert.id, alert.version)}
|
||||||
<Button icon={<StopOutlined />} loading={loading}>
|
>
|
||||||
忽略
|
<Button icon={<StopOutlined />} loading={loading}>
|
||||||
</Button>
|
忽略
|
||||||
</Popconfirm>
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(isPending || isAcknowledged) && onResolve && (
|
{(isPending || isAcknowledged) && onResolve && (
|
||||||
<Popconfirm
|
<Tooltip title="标记告警已恢复">
|
||||||
title="标记为已恢复?"
|
<Popconfirm
|
||||||
description="告警将标记为已恢复状态"
|
title="标记为已恢复?"
|
||||||
onConfirm={() => onResolve(alert.id, alert.version)}
|
description="告警将标记为已恢复状态"
|
||||||
>
|
onConfirm={() => onResolve(alert.id, alert.version)}
|
||||||
<Button type="primary" ghost icon={<SafetyCertificateOutlined />} loading={loading}>
|
>
|
||||||
恢复
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
</Popconfirm>
|
ghost
|
||||||
|
icon={<SafetyCertificateOutlined />}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
恢复
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user