diff --git a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx
index aa502ccc..818f8074 100644
--- a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx
@@ -551,6 +551,53 @@ export function NotificationRules() {
const ownerRows = coverageOwnerRows(rules, policies);
const totalActiveHits = ownerRows.reduce((total, row) => total + row.hitCount, 0);
const unhealthyLinkCount = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok').length;
+ const notificationCustomerSteps = [
+ {
+ step: '01',
+ title: '发现问题',
+ value: `${priorityIssues.length.toLocaleString()} 待通知`,
+ detail: '断链、无来源、VIN缺失、字段缺失等问题先进入车辆服务告警队列。',
+ action: '告警队列',
+ color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const,
+ onClick: () => navigateHash('#/alert-events')
+ },
+ {
+ step: '02',
+ title: '补齐证据',
+ value: `${evidenceCompleteCount}/${priorityIssues.length || 0} 完整`,
+ detail: '通知前必须带上车辆服务、实时监控、轨迹回放和RAW证据链接。',
+ action: '证据矩阵',
+ color: evidenceCompleteCount === priorityIssues.length ? 'green' as const : 'orange' as const,
+ onClick: () => nextPriorityIssue?.rawHash ? navigateHash(nextPriorityIssue.rawHash) : undefined
+ },
+ {
+ step: '03',
+ title: '通知责任人',
+ value: `${ownerRows.length.toLocaleString()} 责任方`,
+ detail: '规则命中后按责任团队、目标人群和通知渠道形成可复制通知。',
+ action: '复制通知',
+ color: ownerRows.length > 0 ? 'blue' as const : 'orange' as const,
+ onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警')
+ },
+ {
+ step: '04',
+ title: '超时升级',
+ value: `${overdueCount}/${dueSoonCount}`,
+ detail: '按P0/P1策略监控已超时和即将升级的告警,避免断链无人跟进。',
+ action: 'SLA报告',
+ color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const,
+ onClick: () => copyText(slaEscalationReport(slaRows, release), 'SLA升级报告')
+ },
+ {
+ step: '05',
+ title: '验收恢复',
+ value: release || '-',
+ detail: '恢复后按验收口径复核,不再命中规则后关闭本次告警闭环。',
+ action: '运行手册',
+ color: 'blue' as const,
+ onClick: () => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook')
+ }
+ ];
const exportNotificationRules = () => {
const rows = notificationExportRows(plan, release);
if (rows.length === 0) {
@@ -564,6 +611,44 @@ export function NotificationRules() {
return (
+
+
+
+ 客户告警闭环
+ 0 ? 'red' : priorityIssues.length > 0 ? 'orange' : 'green'}>
+ {p0IssueCount > 0 ? `P0 ${p0IssueCount.toLocaleString()}` : priorityIssues.length > 0 ? `${priorityIssues.length.toLocaleString()} 待通知` : '暂无待通知'}
+
+ 0 ? 'red' : dueSoonCount > 0 ? 'orange' : 'green'}>{overdueCount.toLocaleString()} 超时 / {dueSoonCount.toLocaleString()} 临近
+
+ 从告警触发到通知升级,再到验收恢复
+
+ 客户需要知道哪些车受影响、证据是否完整、通知给谁、多久升级、恢复后如何验收;规则和策略只是支撑这个闭环的配置。
+
+
+
+
+
+
+
+
+
+ {notificationCustomerSteps.map((item) => (
+
+ ))}
+
+
{activeRuleCount.toLocaleString()}
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css
index 94a1e72e..aa55f8e0 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -2550,6 +2550,87 @@ button.vp-realtime-command-item:focus-visible {
gap: 10px;
}
+.vp-notification-customer-board {
+ margin-bottom: 16px;
+ overflow: hidden;
+}
+
+.vp-notification-customer-board .semi-card-body {
+ display: grid;
+ grid-template-columns: minmax(280px, 0.56fr) minmax(0, 1fr);
+}
+
+.vp-notification-customer-summary {
+ padding: 18px;
+ border-right: 1px solid var(--vp-border);
+ background: #f8fbff;
+ display: grid;
+ gap: 12px;
+ align-content: start;
+}
+
+.vp-notification-customer-steps {
+ padding: 16px;
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 10px;
+ background: #fff;
+}
+
+.vp-notification-customer-step {
+ min-height: 158px;
+ padding: 12px;
+ border: 1px solid var(--vp-border);
+ border-radius: 8px;
+ background: #fbfcff;
+ text-align: left;
+ cursor: pointer;
+ font: inherit;
+ display: grid;
+ gap: 8px;
+ align-content: start;
+}
+
+.vp-notification-customer-step:hover,
+.vp-notification-customer-step:focus-visible {
+ border-color: rgba(22, 100, 255, 0.45);
+ box-shadow: var(--vp-shadow-sm);
+ outline: none;
+}
+
+.vp-notification-customer-step > span {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: #1664ff;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.vp-notification-customer-step strong {
+ color: var(--vp-text);
+ font-size: 18px;
+ line-height: 24px;
+}
+
+.vp-notification-customer-step small {
+ color: var(--vp-text-muted);
+ font-size: 12px;
+ line-height: 18px;
+}
+
+.vp-notification-customer-step em {
+ color: #1664ff;
+ font-style: normal;
+ font-size: 12px;
+ font-weight: 600;
+ align-self: end;
+}
+
.vp-notification-coverage-board {
display: grid;
grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr);
@@ -4655,6 +4736,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-alert-vehicle-task-grid,
.vp-alert-command-board,
.vp-alert-command-grid,
+ .vp-notification-customer-board .semi-card-body,
+ .vp-notification-customer-steps,
.vp-notification-coverage-board,
.vp-notification-coverage-grid,
.vp-notification-owner-item,
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 130f8ac0..363d8991 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -4006,6 +4006,14 @@ test('renders notification rules as a standalone operations page', async () => {
render();
expect(await screen.findByRole('heading', { name: '通知规则' })).toBeInTheDocument();
+ expect(screen.getByText('客户告警闭环')).toBeInTheDocument();
+ expect(screen.getByText('从告警触发到通知升级,再到验收恢复')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户告警闭环 发现问题 告警队列' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户告警闭环 补齐证据 证据矩阵' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户告警闭环 通知责任人 复制通知' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户告警闭环 超时升级 SLA报告' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户告警闭环 验收恢复 运行手册' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '复制值班清单' })).toBeInTheDocument();
expect(screen.getByText('无来源规则')).toBeInTheDocument();
expect(screen.getByText('车辆无任何来源证据')).toBeInTheDocument();
expect(screen.getAllByText('P0 实时中断').length).toBeGreaterThan(0);
@@ -4027,7 +4035,7 @@ test('renders notification rules as a standalone operations page', async () => {
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();
+ expect(screen.getAllByText('platform-rules-test').length).toBeGreaterThan(0);
expect(screen.getByText('通知覆盖与升级风险')).toBeInTheDocument();
expect(screen.getByText('责任覆盖')).toBeInTheDocument();
expect(screen.getByText('活跃命中')).toBeInTheDocument();