feat(platform): add alert notification center

This commit is contained in:
lingniu
2026-07-05 01:45:20 +08:00
parent f45cf90b70
commit 7642f9c9eb
3 changed files with 161 additions and 4 deletions

View File

@@ -896,6 +896,82 @@ export function Quality({
return ( return (
<div className="vp-page"> <div className="vp-page">
<PageHeader title="告警事件" description="围绕车辆服务沉淀断链、VIN 缺失、字段缺失和链路健康事件,并形成通知闭环" /> <PageHeader title="告警事件" description="围绕车辆服务沉淀断链、VIN 缺失、字段缺失和链路健康事件,并形成通知闭环" />
<Card bordered title="告警事件与通知中心">
<div className="vp-alert-center-board">
<div className="vp-alert-center-summary">
<Space wrap>
<Tag color={impactSeverity.color}>{impactSeverity.label}</Tag>
<Tag color={priorityP0Count > 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()}</Tag>
<Tag color={activeRuleCount > 0 ? 'orange' : 'green'}> {activeRuleCount.toLocaleString()}</Tag>
</Space>
<strong>{issueVehicleCount.toLocaleString()} </strong>
<span>
RAW
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" disabled={priorityRows.length === 0} onClick={copyNotificationHandoff}></Button>
<Button size="small" onClick={copyBusinessImpact}></Button>
{onOpenNotificationRules ? <Button size="small" onClick={onOpenNotificationRules}></Button> : null}
</Space>
</div>
<div className="vp-alert-center-grid">
{[
{
label: '影响车辆',
value: `${issueVehicleCount.toLocaleString()}`,
detail: `${issueRecordCount.toLocaleString()} 条告警记录,优先确认车辆服务是否仍可用。`,
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
onClick: () => undefined,
disabled: true
},
{
label: '优先队列',
value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`,
detail: priorityRows.length > 0 ? '已按优先级生成可复制通知和处置工单。' : '当前没有待通知车辆。',
color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const,
onClick: copyPriorityDigest,
disabled: priorityRows.length === 0
},
{
label: '主要来源',
value: primaryProtocolImpact ? qualityProtocolLabel(primaryProtocolImpact.name) : '-',
detail: primaryProtocolImpact ? `${primaryProtocolImpact.count.toLocaleString()} 条问题,点击筛选该来源。` : '暂无来源问题。',
color: primaryProtocolImpact ? 'blue' as const : 'green' as const,
onClick: () => primaryProtocolImpact && drillProtocol(primaryProtocolImpact.name),
disabled: !primaryProtocolImpact
},
{
label: '主要问题',
value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-',
detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()} 条命中,点击筛选该问题。` : '暂无问题类型。',
color: primaryIssueImpact ? 'orange' as const : 'green' as const,
onClick: () => primaryIssueImpact && drillIssueType(primaryIssueImpact.name),
disabled: !primaryIssueImpact
},
{
label: '链路状态',
value: storageWritable ? '可写' : '异常',
detail: `Kafka Lag ${formatLag(health?.kafkaLag)},容量发现 ${capacityFindingCount.toLocaleString()} 项。`,
color: storageWritable && capacityFindingCount === 0 ? 'green' as const : 'red' as const,
onClick: loadHealth,
disabled: false
}
].map((item) => (
<button
key={item.label}
className="vp-alert-center-item"
type="button"
disabled={item.disabled}
onClick={item.onClick}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</div>
</Card>
<div className="vp-kpi-grid"> <div className="vp-kpi-grid">
{[ {[
{ label: '问题车辆', value: issueVehicleCount.toLocaleString() }, { label: '问题车辆', value: issueVehicleCount.toLocaleString() },

View File

@@ -1237,6 +1237,81 @@ button.vp-realtime-command-item:focus-visible {
gap: 12px; gap: 12px;
} }
.vp-alert-center-board {
display: grid;
grid-template-columns: minmax(280px, 0.7fr) minmax(0, 1.3fr);
gap: 16px;
}
.vp-alert-center-summary {
min-height: 168px;
padding: 12px;
border: 1px solid rgba(245, 158, 11, 0.32);
border-radius: var(--vp-radius);
background: #fff8ed;
display: grid;
gap: 10px;
align-content: start;
}
.vp-alert-center-summary strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
word-break: break-word;
}
.vp-alert-center-summary span {
color: var(--vp-text-muted);
font-size: 13px;
line-height: 20px;
}
.vp-alert-center-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.vp-alert-center-item {
min-height: 168px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 10px;
align-content: start;
text-align: left;
font: inherit;
cursor: pointer;
}
.vp-alert-center-item:disabled {
cursor: not-allowed;
opacity: 0.68;
}
.vp-alert-center-item:not(:disabled):hover,
.vp-alert-center-item:not(:disabled):focus-visible {
border-color: rgba(245, 158, 11, 0.48);
background: #fff8ed;
outline: none;
}
.vp-alert-center-item strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
word-break: break-word;
}
.vp-alert-center-item span {
color: var(--vp-text-muted);
font-size: 13px;
line-height: 20px;
}
.vp-alert-flow-item { .vp-alert-flow-item {
min-height: 96px; min-height: 96px;
padding: 12px; padding: 12px;
@@ -3056,6 +3131,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-alert-impact-board, .vp-alert-impact-board,
.vp-alert-impact-grid, .vp-alert-impact-grid,
.vp-alert-dispatch-grid, .vp-alert-dispatch-grid,
.vp-alert-center-board,
.vp-alert-center-grid,
.vp-alert-command-board, .vp-alert-command-board,
.vp-alert-command-grid, .vp-alert-command-grid,
.vp-notification-coverage-board, .vp-notification-coverage-board,

View File

@@ -3543,15 +3543,19 @@ test('shows alert rule and notification policy workspace on quality page', async
render(<App />); render(<App />);
expect(await screen.findByText('告警事件与通知中心')).toBeInTheDocument();
expect(screen.getByText('客户处置先判断影响车辆和优先级再通知责任团队并用实时、轨迹、RAW、里程证据闭环。', { exact: false })).toBeInTheDocument();
expect(screen.getByText('链路状态')).toBeInTheDocument();
expect(screen.getAllByText('优先队列').length).toBeGreaterThanOrEqual(1);
expect(await screen.findByText('告警触发规则')).toBeInTheDocument(); expect(await screen.findByText('告警触发规则')).toBeInTheDocument();
expect(screen.getByText('业务影响评估')).toBeInTheDocument(); expect(screen.getByText('业务影响评估')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '复制业务影响报告' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制业务影响报告' })).toBeInTheDocument();
expect(screen.getAllByText('高风险').length).toBeGreaterThan(0); expect(screen.getAllByText('高风险').length).toBeGreaterThan(0);
expect(screen.getByText('3 辆受影响')).toBeInTheDocument(); expect(screen.getByText('3 辆受影响')).toBeInTheDocument();
expect(screen.getByText('影响车辆')).toBeInTheDocument(); expect(screen.getAllByText('影响车辆').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('3 辆')).toBeInTheDocument(); expect(screen.getAllByText('3 辆').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('P0 0 / P1 0')).toBeInTheDocument(); expect(screen.getAllByText('P0 0 / P1 0').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('车辆服务').length).toBeGreaterThan(0); expect(screen.getAllByText('车辆服务').length).toBeGreaterThan(0);
expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThan(0); expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThan(0);
expect(screen.getAllByText('异常').length).toBeGreaterThan(0); expect(screen.getAllByText('异常').length).toBeGreaterThan(0);
expect(screen.getByText('4 类活跃')).toBeInTheDocument(); expect(screen.getByText('4 类活跃')).toBeInTheDocument();