From 33f1ed24e4c293bc8ae430f7b50b2ce7db19842c Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 18:59:10 +0800 Subject: [PATCH] feat(platform): add alert recovery acceptance --- .../apps/web/src/pages/Quality.tsx | 148 ++++++++++++++++++ .../apps/web/src/styles/global.css | 79 ++++++++++ .../apps/web/src/test/App.test.tsx | 12 +- 3 files changed, 238 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 30fc5367..269635e4 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -718,6 +718,51 @@ function alertCustomerDecisionText({ ].join('\n'); } +function alertRecoveryReceiptText({ + summary, + impactLabel, + priorityRows, + primaryPolicy, + filters, + platformRelease +}: { + summary: QualitySummary; + impactLabel: string; + priorityRows: PriorityIssueRow[]; + primaryPolicy?: QualityNotificationPolicy; + filters: Record; + platformRelease?: string; +}) { + const release = platformRelease?.trim(); + const filterLines = [ + filters.keyword ? `关键词=${filters.keyword}` : '', + filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '', + filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : '' + ].filter(Boolean); + const focusRows = priorityRows.slice(0, 5).map((row, index) => { + const lookup = qualityIssueVehicleLookup(row); + return `${index + 1}. ${row.vehicleLabel} / ${qualityProtocolLabel(row.protocol)} / ${lookup.key || '-'} / ${qualityIssueLabel(row.issueType)}`; + }); + return [ + '【客户告警恢复验收回执】', + ...(release ? [`运行版本:${release}`] : []), + `当前筛选:${filterLines.length > 0 ? filterLines.join(';') : '全部告警'}`, + `当前影响:${Number(summary.issueVehicleCount ?? 0).toLocaleString()} 辆车 / ${Number(summary.issueRecordCount ?? 0).toLocaleString()} 条告警 / ${impactLabel}`, + '恢复标准:实时可看 / 轨迹可回放 / 里程可核对 / 历史证据可导出', + `通知策略:${primaryPolicy ? [primaryPolicy.name, primaryPolicy.target, primaryPolicy.channel].filter(Boolean).join(' / ') : '-'}`, + `验收口径:${primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。'}`, + '', + '待复核车辆:', + ...(focusRows.length > 0 ? focusRows : ['当前没有优先车辆']), + '', + `告警事件:${qualityShareURL()}`, + `实时监控:${appURL(buildAppHash({ page: 'realtime', filters }))}`, + `轨迹回放:${appURL(buildAppHash({ page: 'history', filters }))}`, + `里程统计:${appURL(buildAppHash({ page: 'mileage', filters }))}`, + `历史导出:${appURL(buildAppHash({ page: 'history-query', filters: { ...filters, tab: 'raw', includeFields: 'true' } }))}` + ].join('\n'); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -1016,6 +1061,72 @@ export function Quality({ platformRelease: health?.runtime?.platformRelease }), '客户告警决策说明'); }; + const copyAlertRecoveryReceipt = () => { + copyText(alertRecoveryReceiptText({ + summary: notificationPlan?.summary ?? summary, + impactLabel: impactSeverity.label, + priorityRows, + primaryPolicy, + filters, + platformRelease: health?.runtime?.platformRelease + }), '客户告警恢复验收回执'); + }; + const alertRecoveryAcceptanceItems = [ + { + label: '实时恢复', + value: storageWritable ? '可验证' : '先修复存储', + detail: '确认客户能看到车辆实时状态、在线状态和最新协议字段。', + color: storageWritable ? 'green' as const : 'red' as const, + action: '实时确认', + displayAction: '实时验收', + onClick: () => { + if (onOpenRealtime) { + onOpenRealtime(filters); + } else { + window.location.hash = buildAppHash({ page: 'realtime', filters }); + } + } + }, + { + label: '轨迹恢复', + value: issueVehicleCount > 0 ? '需复核' : '正常', + detail: '用受影响车辆回放轨迹,确认定位、速度和时间连续可查。', + color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, + action: '轨迹复核', + displayAction: '轨迹验收', + onClick: () => { + if (onOpenHistory) { + onOpenHistory(filters); + } else { + window.location.hash = buildAppHash({ page: 'history', filters }); + } + } + }, + { + label: '里程恢复', + value: primaryIssueType ? qualityIssueLabel(primaryIssueType) : '可核对', + detail: '用区间里程和历史证据确认统计结果能解释、能导出、能复盘。', + color: primaryIssueType ? 'orange' as const : 'green' as const, + action: '里程核对', + displayAction: '里程验收', + onClick: () => { + if (onOpenMileage) { + onOpenMileage(filters); + } else { + window.location.hash = buildAppHash({ page: 'mileage', filters }); + } + } + }, + { + label: '客户回执', + value: primaryPolicy?.target || '责任人待配置', + detail: '复制一份包含影响范围、验收标准、证据入口和待复核车辆的恢复回执。', + color: primaryPolicy ? 'blue' as const : 'orange' as const, + action: '复制回执', + displayAction: '回执复制', + onClick: copyAlertRecoveryReceipt + } + ]; return (
@@ -1183,6 +1294,43 @@ export function Quality({
+ +
+
+ + {impactSeverity.label} + {storageWritable ? '存储可写' : '存储异常'} + {primaryPolicy?.target || '责任人待配置'} + + 告警恢复不是把状态改绿,而是要证明客户能重新看实时、查轨迹、核里程、导出证据,并留下一份恢复回执。 + + 车辆服务恢复必须用客户能验证的页面闭环:实时状态、轨迹回放、里程统计、历史导出都可打开,才算完成恢复验收。 + + + + + +
+
+ {alertRecoveryAcceptanceItems.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 08bc2cb5..993dc348 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -4554,6 +4554,83 @@ button.vp-realtime-command-item:focus-visible { line-height: 18px; } +.vp-alert-recovery-board { + display: grid; + grid-template-columns: minmax(280px, 0.76fr) minmax(0, 1.24fr); + gap: 16px; +} + +.vp-alert-recovery-summary { + min-height: 168px; + padding: 12px; + border: 1px solid rgba(25, 164, 96, 0.24); + border-radius: var(--vp-radius); + background: #f4fbf7; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-alert-recovery-summary strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + word-break: break-word; +} + +.vp-alert-recovery-summary span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + +.vp-alert-recovery-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-alert-recovery-item { + min-height: 168px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-alert-recovery-item:hover, +.vp-alert-recovery-item:focus-visible { + border-color: rgba(25, 164, 96, 0.42); + background: #f4fbf7; + outline: none; +} + +.vp-alert-recovery-item > div { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.vp-alert-recovery-item strong { + color: var(--vp-text); + font-size: 17px; + line-height: 23px; + word-break: break-word; +} + +.vp-alert-recovery-item span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + .vp-alert-center-board { display: grid; grid-template-columns: minmax(280px, 0.7fr) minmax(0, 1.3fr); @@ -8852,6 +8929,8 @@ button.vp-realtime-command-item:focus-visible { .vp-alert-sla-grid, .vp-alert-channel-board, .vp-alert-channel-grid, + .vp-alert-recovery-board, + .vp-alert-recovery-grid, .vp-alert-impact-board, .vp-alert-impact-grid, .vp-alert-dispatch-grid, 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 c295a98f..c5caf8af 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3810,6 +3810,12 @@ test('shows alert rule and notification policy workspace on quality page', async expect(screen.getByRole('button', { name: '通知渠道闭环 企业微信 复制交接包' })).toBeInTheDocument(); expect(screen.getAllByText('接入运维 + 业务责任人').length).toBeGreaterThan(1); expect(screen.getAllByText('无来源、VIN 缺失、存储不可写').length).toBeGreaterThanOrEqual(1); + 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: '客户恢复验收台 客户回执 复制回执' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制业务影响报告' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警业务影响报告】')); @@ -3825,6 +3831,10 @@ test('shows alert rule and notification policy workspace on quality page', async expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0 实时中断 / 接入运维 + 业务责任人')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级:30 分钟升级')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')); + fireEvent.click(screen.getByRole('button', { name: '客户恢复验收台 客户回执 复制回执' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户告警恢复验收回执】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('恢复标准:实时可看 / 轨迹可回放 / 里程可核对 / 历史证据可导出')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前影响:3 辆车 / 5 条告警 / 高风险')); fireEvent.click(screen.getByRole('button', { name: '分派团队 车辆档案' })); expect(window.location.hash).toBe('#/alert-events?issueType=VIN_MISSING'); }); @@ -3935,7 +3945,7 @@ test('shows actionable priority queue on quality page', async () => { expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); expect(screen.getByText('车辆风险处置任务板')).toBeInTheDocument(); expect(screen.getAllByText('粤A优先1 / VIN-P0-001').length).toBeGreaterThan(0); - expect(screen.getByText('实时确认')).toBeInTheDocument(); + expect(screen.getAllByText('实时确认').length).toBeGreaterThan(0); expect(screen.getAllByText('轨迹证据').length).toBeGreaterThan(0); expect(screen.getByText('里程复核')).toBeInTheDocument(); expect(screen.getAllByText('原始证据').length).toBeGreaterThan(0);