feat(platform): add alert handling command queue

This commit is contained in:
lingniu
2026-07-04 19:30:55 +08:00
parent 41c5e0b338
commit 0b433c2592
3 changed files with 114 additions and 2 deletions

View File

@@ -76,6 +76,19 @@ function priorityIssueDigest(plan: QualityNotificationPlan | null, release?: str
].join('\n');
}
function countEvidenceLinks(issue: QualityPriorityIssue) {
return [issue.vehicleHash, issue.realtimeHash, issue.historyHash, issue.rawHash].filter(Boolean).length;
}
function evidenceSummary(issue: QualityPriorityIssue) {
const count = countEvidenceLinks(issue);
return count >= 4 ? '证据完整' : `${4 - count} 类证据`;
}
function evidenceColor(issue: QualityPriorityIssue): 'green' | 'orange' {
return countEvidenceLinks(issue) >= 4 ? 'green' : 'orange';
}
function navigateHash(hash: string) {
if (!hash) return;
window.location.hash = hash;
@@ -119,6 +132,10 @@ export function NotificationRules() {
const activeRuleCount = plan?.activeRuleCount ?? rules.filter((rule) => rule.count > 0).length;
const p0RuleCount = plan?.p0RuleCount ?? rules.filter((rule) => rule.count > 0 && rule.level === 'P0').length;
const release = health?.runtime?.platformRelease ?? '';
const p0IssueCount = priorityIssues.filter((issue) => issue.priority === 'P0').length;
const evidenceCompleteCount = priorityIssues.filter((issue) => countEvidenceLinks(issue) >= 4).length;
const primaryOwner = rules.find((rule) => rule.count > 0)?.owner || policies[0]?.target || '-';
const nextPriorityIssue = priorityIssues[0];
return (
<div className="vp-page">
@@ -146,6 +163,51 @@ export function NotificationRules() {
</Card>
</div>
<Card
bordered
loading={loading}
title={<Space><span></span><Button size="small" aria-label="复制待通知汇总" disabled={priorityIssues.length === 0} icon={<IconCopy />} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-alert-command-board">
<div className="vp-alert-command-grid">
{[
{ label: '待通知告警', value: priorityIssues.length.toLocaleString(), color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const },
{ label: 'P0优先', value: p0IssueCount.toLocaleString(), color: p0IssueCount > 0 ? 'red' as const : 'green' as const },
{ label: '证据完整', value: `${evidenceCompleteCount}/${priorityIssues.length || 0}`, color: evidenceCompleteCount === priorityIssues.length ? 'green' as const : 'orange' as const },
{ label: '主责任方', value: primaryOwner, color: 'blue' as const }
].map((item) => (
<div key={item.label} className="vp-alert-command-item">
<Tag color={item.color}>{item.label}</Tag>
<div>{item.value}</div>
</div>
))}
</div>
<div className="vp-alert-next-action">
<div className="vp-current-service-title"></div>
{nextPriorityIssue ? (
<>
<Typography.Text strong>{nextPriorityIssue.vehicleLabel}</Typography.Text>
<Typography.Text type="secondary">{nextPriorityIssue.actionDetail || nextPriorityIssue.actionLabel}</Typography.Text>
<Space wrap>
<Tag color={nextPriorityIssue.priority === 'P0' ? 'red' : 'orange'}>{nextPriorityIssue.priority}</Tag>
<Tag color={evidenceColor(nextPriorityIssue)}>{evidenceSummary(nextPriorityIssue)}</Tag>
<Tag color="blue">{nextPriorityIssue.sla}</Tag>
</Space>
<Space wrap>
<Button size="small" aria-label="下一步车辆服务" disabled={!nextPriorityIssue.vehicleHash} onClick={() => navigateHash(nextPriorityIssue.vehicleHash)}></Button>
<Button size="small" aria-label="下一步实时证据" disabled={!nextPriorityIssue.realtimeHash} onClick={() => navigateHash(nextPriorityIssue.realtimeHash)}></Button>
<Button size="small" aria-label="下一步轨迹证据" disabled={!nextPriorityIssue.historyHash} onClick={() => navigateHash(nextPriorityIssue.historyHash)}></Button>
<Button size="small" aria-label="下一步RAW证据" disabled={!nextPriorityIssue.rawHash} onClick={() => navigateHash(nextPriorityIssue.rawHash)}>RAW证据</Button>
</Space>
</>
) : (
<Tag color="green"></Tag>
)}
</div>
</div>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" aria-label="复制通知规则Runbook" icon={<IconCopy />} onClick={() => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook')}>Runbook</Button><Button size="small" loading={loading} onClick={load}></Button></Space>}
@@ -185,7 +247,7 @@ export function NotificationRules() {
<Card
bordered
loading={loading}
title={<Space><span></span><Button size="small" aria-label="复制待通知汇总" disabled={priorityIssues.length === 0} icon={<IconCopy />} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}></Button></Space>}
title="当前待通知告警"
style={{ marginTop: 16 }}
>
<Table<QualityPriorityIssue>
@@ -197,6 +259,7 @@ export function NotificationRules() {
{ title: '车辆', width: 210, dataIndex: 'vehicleLabel' },
{ title: '问题', width: 130, render: (_: unknown, row: QualityPriorityIssue) => qualityIssueLabel(row.issueType) },
{ title: 'SLA', width: 120, dataIndex: 'sla' },
{ title: '证据完整度', width: 120, render: (_: unknown, row: QualityPriorityIssue) => <Tag color={evidenceColor(row)}>{evidenceSummary(row)}</Tag> },
{ title: '建议动作', width: 160, dataIndex: 'actionLabel' },
{ title: '最后时间', width: 170, dataIndex: 'lastSeen' },
{

View File

@@ -681,6 +681,48 @@ button.vp-realtime-command-item:focus-visible {
line-height: 28px;
}
.vp-alert-command-board {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 16px;
}
.vp-alert-command-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.vp-alert-command-item {
min-height: 92px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
align-content: space-between;
gap: 10px;
}
.vp-alert-command-item > div {
color: var(--vp-text);
font-size: 22px;
font-weight: 700;
line-height: 28px;
word-break: break-word;
}
.vp-alert-next-action {
min-height: 132px;
padding: 12px;
border: 1px solid rgba(22, 100, 255, 0.28);
border-radius: var(--vp-radius);
background: #f5f9ff;
display: grid;
align-content: start;
gap: 10px;
}
.vp-risk-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1184,6 +1226,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-conclusion-grid,
.vp-monitor-layout,
.vp-alert-flow,
.vp-alert-command-board,
.vp-alert-command-grid,
.vp-risk-grid,
.vp-alert-ops-grid,
.vp-stat-workspace,

View File

@@ -3349,8 +3349,13 @@ test('renders notification rules as a standalone operations page', async () => {
expect(screen.getByText('车辆无任何来源证据')).toBeInTheDocument();
expect(screen.getByText('P0 实时中断')).toBeInTheDocument();
expect(screen.getByText('接入运维 + 业务责任人')).toBeInTheDocument();
expect(screen.getByText('告警处置队列')).toBeInTheDocument();
expect(screen.getByText('下一步处置')).toBeInTheDocument();
expect(screen.getByText('主责任方')).toBeInTheDocument();
expect(screen.getAllByText('证据完整').length).toBeGreaterThan(0);
expect(screen.getByText('核对平台转发配置')).toBeInTheDocument();
expect(screen.getByText('当前待通知告警')).toBeInTheDocument();
expect(screen.getByText('粤A规则1 / VIN-RULES-001')).toBeInTheDocument();
expect(screen.getAllByText('粤A规则1 / VIN-RULES-001').length).toBeGreaterThan(0);
expect(screen.getByText('确认平台转发')).toBeInTheDocument();
expect(screen.getByText('通知规则页待通知告警')).toBeInTheDocument();
expect(screen.getByText('platform-rules-test')).toBeInTheDocument();