From f3c6f1df7c7d6fcd78c7f5baed1527c7964dd4a7 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 14:41:29 +0800 Subject: [PATCH] feat(platform): add time window delivery decision --- .../apps/web/src/pages/Dashboard.tsx | 66 +++++++++++++++++ .../apps/web/src/styles/global.css | 70 +++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 9 +++ 3 files changed, 145 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 9c289dc2..c2d1ac24 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -881,12 +881,50 @@ export function Dashboard({ color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const } ]; + const timeMonitorHasRange = Boolean(timeMonitorScopeValue.dateFrom && timeMonitorScopeValue.dateTo); + const timeMonitorHasTrajectory = (summary?.activeToday ?? 0) > 0; + const timeMonitorHasHistory = (summary?.frameToday ?? 0) > 0; + const timeMonitorHasAlerts = (summary?.issueVehicles ?? 0) > 0; + const timeMonitorDeliveryStatus = !timeMonitorHasRange + ? { label: '待补时间', color: 'orange' as const, detail: '开始和结束时间不完整,交付前先锁定复盘窗口。' } + : !timeMonitorHasHistory + ? { label: '证据不足', color: 'red' as const, detail: '当前没有历史证据,无法支撑客户复盘或导出。' } + : timeMonitorHasAlerts + ? { label: '带风险交付', color: 'orange' as const, detail: '存在告警车辆,交付时需要附带告警闭环和原始证据。' } + : { label: '可交付', color: 'green' as const, detail: '时间窗、轨迹、历史证据和统计入口已准备好。' }; + const timeMonitorDeliveryActions = [ + { + label: '客户结论', + value: timeMonitorDeliveryStatus.label, + detail: timeMonitorDeliveryStatus.detail, + action: '复制复盘包', + color: timeMonitorDeliveryStatus.color, + onClick: () => copyTimeMonitorReviewPackage() + }, + { + label: '证据准备', + value: timeMonitorHasTrajectory && timeMonitorHasHistory ? '证据可用' : '需要补证', + detail: `轨迹 ${formatCount(summary?.activeToday)} 活跃 / 历史 ${formatCount(summary?.frameToday)} 帧。`, + action: timeMonitorHasHistory ? '历史导出' : '查历史', + color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const, + onClick: openTimeMonitorRaw + }, + { + label: '异常说明', + value: timeMonitorHasAlerts ? `${formatCount(summary?.issueVehicles)} 告警` : '无高风险', + detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有需要额外说明的高优先级告警。', + action: '告警复盘', + color: timeMonitorHasAlerts ? 'orange' as const : 'green' as const, + onClick: openTimeMonitorAlerts + } + ]; const copyTimeMonitorReviewPackage = () => { const scope = timeMonitorScope(); const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' }; const lines = [ '【客户时间窗复盘包】', `范围:${timeMonitorSummary}`, + `交付结论:${timeMonitorDeliveryStatus.label};${timeMonitorDeliveryStatus.detail}`, `时间窗判定:${dayRangeText(scope.dateFrom, scope.dateTo)}`, '平台能力:实时地图 / 轨迹回放 / 里程统计 / 历史数据查询 / 告警通知', `车辆范围:${scope.keyword || '全部车辆'}`, @@ -898,6 +936,8 @@ export function Dashboard({ `告警事件:${formatCount(summary?.issueVehicles)} 辆车存在告警`, '交付核对:', ...timeMonitorChecklist.map((item, index) => `${index + 1}. ${item.label}:${item.value};${item.detail}`), + '下一步动作:', + ...timeMonitorDeliveryActions.map((item, index) => `${index + 1}. ${item.label}:${item.value};${item.detail}`), `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`, `里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`, `历史数据查询:${appURL(buildAppHash({ page: 'history-query', keyword: scope.keyword, protocol: scope.protocol, filters: rawFilters }))}`, @@ -1618,6 +1658,32 @@ export function Dashboard({ ))} +
+
+ + {timeMonitorDeliveryStatus.label} + {timeMonitorScopeValue.keyword ? '单车时间窗' : '车辆池时间窗'} + + 时间窗交付决策 + {timeMonitorDeliveryStatus.detail} +
+
+ {timeMonitorDeliveryActions.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 eda199c8..8253f390 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1334,6 +1334,74 @@ body { line-height: 20px; } +.vp-time-delivery-strip { + margin-top: 14px; + display: grid; + grid-template-columns: minmax(260px, 0.58fr) minmax(0, 1.42fr); + gap: 12px; +} + +.vp-time-delivery-summary { + min-height: 148px; + padding: 14px; + border: 1px solid rgba(10, 168, 107, 0.26); + border-radius: 8px; + background: #f6fbf8; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-time-delivery-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.vp-time-delivery-item { + min-height: 148px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #fbfcff; + display: grid; + gap: 10px; + align-content: start; + text-align: left; + font: inherit; + cursor: pointer; +} + +.vp-time-delivery-item:hover, +.vp-time-delivery-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f7fbff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-time-delivery-item strong { + color: var(--vp-text); + font-size: 19px; + line-height: 24px; + word-break: break-word; +} + +.vp-time-delivery-item span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + +.vp-time-delivery-item em { + color: var(--vp-primary); + font-style: normal; + font-size: 13px; + font-weight: 600; + line-height: 20px; + align-self: end; +} + .vp-result-summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -6285,6 +6353,8 @@ button.vp-realtime-command-item:focus-visible { .vp-time-review-grid, .vp-time-audit-strip, .vp-time-audit-grid, + .vp-time-delivery-strip, + .vp-time-delivery-grid, .vp-realtime-customer-board .semi-card-body, .vp-realtime-next-actions, .vp-realtime-customer-steps, 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 2ea705ed..023cb721 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -859,6 +859,12 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(screen.getByText('轨迹可复盘')).toBeInTheDocument(); expect(screen.getByText('数据可导出')).toBeInTheDocument(); expect(screen.getByText('异常可闭环')).toBeInTheDocument(); + expect(screen.getByText('时间窗交付决策')).toBeInTheDocument(); + expect(screen.getAllByText('带风险交付').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('存在告警车辆,交付时需要附带告警闭环和原始证据。').length).toBeGreaterThanOrEqual(1); + expect(screen.getByRole('button', { name: '时间窗交付 客户结论 复制复盘包' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '时间窗交付 证据准备 历史导出' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '时间窗交付 异常说明 告警复盘' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制复盘包' })).toBeInTheDocument(); expect(screen.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument(); expect(screen.getByText('查看轨迹')).toBeInTheDocument(); @@ -971,9 +977,12 @@ test('dashboard exposes vehicle data center capability matrix', async () => { fireEvent.click(screen.getByRole('button', { name: '复制复盘包' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('平台能力')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付结论:带风险交付;存在告警车辆,交付时需要附带告警闭环和原始证据。')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('时间窗判定:1 天窗口')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付核对:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 范围可解释')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('下一步动作:')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 客户结论:带风险交付')); cleanup(); await renderDashboard();