diff --git a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx
index 1adc8991..1a95a5cf 100644
--- a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx
@@ -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 (
@@ -146,6 +163,51 @@ export function NotificationRules() {
+ 告警处置队列} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}>复制待通知汇总}
+ style={{ marginTop: 16 }}
+ >
+
+
+ {[
+ { 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) => (
+
+
{item.label}
+
{item.value}
+
+ ))}
+
+
+
下一步处置
+ {nextPriorityIssue ? (
+ <>
+
{nextPriorityIssue.vehicleLabel}
+
{nextPriorityIssue.actionDetail || nextPriorityIssue.actionLabel}
+
+ {nextPriorityIssue.priority}
+ {evidenceSummary(nextPriorityIssue)}
+ {nextPriorityIssue.sla}
+
+
+
+
+
+
+
+ >
+ ) : (
+
暂无待通知告警
+ )}
+
+
+
+
规则运行手册} onClick={() => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook')}>复制通知规则Runbook}
@@ -185,7 +247,7 @@ export function NotificationRules() {
当前待通知告警} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}>复制待通知汇总}
+ title="当前待通知告警"
style={{ marginTop: 16 }}
>
@@ -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) => {evidenceSummary(row)} },
{ title: '建议动作', width: 160, dataIndex: 'actionLabel' },
{ title: '最后时间', width: 170, dataIndex: 'lastSeen' },
{
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css
index a6b8976d..5a2133b0 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -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,
diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx
index 7973cf69..4be9f4da 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -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();