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();