diff --git a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx index 719d6876..aaf5075c 100644 --- a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx +++ b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx @@ -761,6 +761,74 @@ export function NotificationRules() { onClick: () => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook') } ]; + const primaryPolicy = policies.find((policy) => policy.name.startsWith('P0')) ?? policies[0]; + const notificationOrchestrationItems = [ + { + label: '告警触发', + value: `${activeRuleCount.toLocaleString()} 类活跃规则`, + detail: `${p0RuleCount.toLocaleString()} 类 P0,当前命中 ${totalActiveHits.toLocaleString()} 次。`, + color: p0RuleCount > 0 ? 'red' as const : activeRuleCount > 0 ? 'orange' as const : 'green' as const, + onClick: () => navigateHash('#/alert-events') + }, + { + label: '受影响车辆', + value: `${priorityIssues.length.toLocaleString()} 待通知`, + detail: p0IssueCount > 0 ? `P0 ${p0IssueCount.toLocaleString()} 条,优先通知业务责任人。` : `${evidenceCompleteCount}/${priorityIssues.length || 0} 证据完整。`, + color: p0IssueCount > 0 ? 'red' as const : priorityIssues.length > 0 ? 'orange' as const : 'green' as const, + onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') + }, + { + label: '通知对象', + value: primaryPolicy?.target || primaryOwner, + detail: `渠道:${primaryPolicy?.channel || policies[0]?.channel || '待配置渠道'}。`, + color: primaryPolicy?.target ? 'blue' as const : 'orange' as const, + onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') + }, + { + label: '升级时钟', + value: overdueCount > 0 ? `已超时 ${overdueCount.toLocaleString()}` : dueSoonCount > 0 ? `临近 ${dueSoonCount.toLocaleString()}` : '正常跟进', + detail: primaryPolicy ? formatEscalationMinutes(primaryPolicy.escalationMinutes) : '缺少升级策略。', + color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const, + onClick: () => copyText(slaEscalationReport(slaRows, release), 'SLA升级报告') + }, + { + label: '恢复验收', + value: primaryPolicy?.acceptanceCriteria || '按车辆服务证据验收', + detail: '恢复后必须复核实时、轨迹、原始记录和规则命中状态。', + color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const, + onClick: () => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook') + } + ]; + const notificationOrchestrationActions = [ + { + label: '查看告警', + action: '告警队列', + color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const, + disabled: false, + onClick: () => navigateHash('#/alert-events') + }, + { + label: '复制通知', + action: '待通知', + color: priorityIssues.length > 0 ? 'blue' as const : 'grey' as const, + disabled: priorityIssues.length === 0, + onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') + }, + { + label: '复制SLA', + action: 'SLA报告', + color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const, + disabled: slaRows.length === 0, + onClick: () => copyText(slaEscalationReport(slaRows, release), 'SLA升级报告') + }, + { + label: '导出演练', + action: '演练包', + color: 'blue' as const, + disabled: false, + onClick: () => copyText(alertNotificationDrillPackage(plan, health, release), '通知演练包') + } + ]; const exportNotificationRules = () => { const rows = notificationExportRows(plan, release); if (rows.length === 0) { @@ -812,6 +880,53 @@ export function NotificationRules() { ))} +
+
+ + 客户通知编排条 + 0 || overdueCount > 0 ? 'red' : priorityIssues.length > 0 ? 'orange' : 'green'}> + {priorityIssues.length > 0 ? `${priorityIssues.length.toLocaleString()} 待通知` : '当前稳定'} + + {primaryPolicy?.channel || '待配置渠道'} + + + 先把规则命中转成可执行的通知编排:触发条件、受影响车辆、责任方、渠道、SLA 和验收口径在一行里完成确认。 + + + 客户需要的是谁受影响、谁处理、多久升级和如何证明恢复;规则表保留为后面的运行证据。 + +
+
+ {notificationOrchestrationItems.map((item) => ( + + ))} +
+
+ {notificationOrchestrationActions.map((item) => ( + + ))} +
+
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index ab8e6364..48e9965a 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -8751,6 +8751,109 @@ button.vp-realtime-command-item:focus-visible { align-self: end; } +.vp-notification-orchestration-strip { + margin-bottom: 16px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #fff; + display: grid; + grid-template-columns: minmax(280px, 0.74fr) minmax(0, 1.34fr) minmax(190px, 0.52fr); + overflow: hidden; + box-shadow: var(--vp-shadow-sm); +} + +.vp-notification-orchestration-copy { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f7fbff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-notification-orchestration-grid { + padding: 14px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-notification-orchestration-item { + min-height: 124px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-notification-orchestration-item:hover, +.vp-notification-orchestration-item:focus-visible, +.vp-notification-orchestration-action:hover, +.vp-notification-orchestration-action:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-notification-orchestration-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + word-break: break-word; +} + +.vp-notification-orchestration-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-notification-orchestration-actions { + padding: 14px; + border-left: 1px solid var(--vp-border); + background: #fafcff; + display: grid; + gap: 8px; +} + +.vp-notification-orchestration-action { + min-height: 46px; + padding: 8px 10px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-notification-orchestration-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-notification-orchestration-action span { + color: var(--vp-primary); + font-size: 12px; + font-weight: 700; + line-height: 18px; + white-space: nowrap; +} + .vp-notification-question-board { display: grid; grid-template-columns: minmax(260px, 0.58fr) minmax(0, 1.42fr); @@ -13995,6 +14098,8 @@ button.vp-realtime-command-item:focus-visible { .vp-alert-command-grid, .vp-notification-customer-board .semi-card-body, .vp-notification-customer-steps, + .vp-notification-orchestration-strip, + .vp-notification-orchestration-grid, .vp-notification-question-board, .vp-notification-question-grid, .vp-notification-coverage-board, @@ -14181,7 +14286,9 @@ button.vp-realtime-command-item:focus-visible { .vp-history-delivery-conclusion-copy, .vp-history-delivery-conclusion-actions, .vp-history-next-export-copy, - .vp-history-next-export-actions { + .vp-history-next-export-actions, + .vp-notification-orchestration-copy, + .vp-notification-orchestration-actions { border-left: 0; border-right: 0; border-bottom: 1px solid var(--vp-border); 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 a6f331c8..8f93983d 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4839,6 +4839,17 @@ test('renders notification rules as a standalone operations page', async () => { 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('先把规则命中转成可执行的通知编排:触发条件、受影响车辆、责任方、渠道、SLA 和验收口径在一行里完成确认。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排 告警触发 2 类活跃规则' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排 受影响车辆 1 待通知' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排 通知对象 接入运维 + 业务责任人' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排 升级时钟 已超时 1' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排 恢复验收 来源恢复并持续 10 分钟' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排动作 查看告警 告警队列' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排动作 复制通知 待通知' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排动作 复制SLA SLA报告' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知编排动作 导出演练 演练包' })).toBeInTheDocument(); expect(screen.getByText('客户通知常问')).toBeInTheDocument(); expect(screen.getByText('把通知规则翻译成客户能理解的影响、责任、升级和恢复口径')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户通知常问 哪些车受影响? 影响摘要' })).toBeInTheDocument();