From 1de268942c93d4cff93e95e3d77dd4bd7fbc0557 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 03:13:08 +0800 Subject: [PATCH] feat(platform): add time window service audit --- .../apps/web/src/pages/Dashboard.tsx | 58 +++++++++++++++++++ .../apps/web/src/styles/global.css | 55 ++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 9 +++ 3 files changed, 122 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index c30549e4..450ebca3 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -125,6 +125,16 @@ function previousDateString(value: string) { return `${year}-${month}-${day}`; } +function dayRangeText(dateFrom?: string, dateTo?: string) { + if (!dateFrom && !dateTo) return '未限定时间'; + if (!dateFrom || !dateTo) return '单边时间窗'; + const start = new Date(`${dateFrom}T00:00:00+08:00`).getTime(); + const end = new Date(`${dateTo}T00:00:00+08:00`).getTime(); + if (!Number.isFinite(start) || !Number.isFinite(end)) return '时间格式待确认'; + const days = Math.max(0, Math.round((end - start) / 86400000)); + return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`; +} + function priorityIssueVehicleLabel(row: QualityIssueRow) { const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim(); return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-'; @@ -806,12 +816,41 @@ export function Dashboard({ color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const } ]; + const timeMonitorScopeValue = timeMonitorScope(); + const timeMonitorWindowLabel = dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo); + const timeMonitorChecklist = [ + { + label: '范围可解释', + value: timeMonitorScopeValue.keyword ? '单车聚焦' : '全域巡检', + detail: `${timeMonitorScopeValue.protocol || '全部数据通道'} / ${timeMonitorWindowLabel}`, + color: timeMonitorScopeValue.dateFrom && timeMonitorScopeValue.dateTo ? 'green' as const : 'orange' as const + }, + { + label: '轨迹可复盘', + value: `${formatCount(summary?.activeToday)} 活跃车辆`, + detail: '可进入轨迹页核对位置、速度、里程断点和停驶片段。', + color: (summary?.activeToday ?? 0) > 0 ? 'green' as const : 'orange' as const + }, + { + label: '数据可导出', + value: `${formatCount(summary?.frameToday)} 今日数据`, + detail: '历史查询默认带 RAW 与解析字段,支持按字段缩小导出体积。', + color: (summary?.frameToday ?? 0) > 0 ? 'blue' as const : 'orange' as const + }, + { + label: '异常可闭环', + value: `${formatCount(summary?.issueVehicles)} 告警车辆`, + detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有高优先级告警。', + color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const + } + ]; const copyTimeMonitorReviewPackage = () => { const scope = timeMonitorScope(); const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' }; const lines = [ '【客户时间窗复盘包】', `范围:${timeMonitorSummary}`, + `时间窗判定:${dayRangeText(scope.dateFrom, scope.dateTo)}`, '平台能力:实时地图 / 轨迹回放 / 里程统计 / 历史数据查询 / 告警通知', `车辆范围:${scope.keyword || '全部车辆'}`, `数据通道:${scope.protocol || '全部数据通道'}`, @@ -820,6 +859,8 @@ export function Dashboard({ `统计证据:车辆口径 ${formatCount(serviceSummary?.totalVehicles)},用于区间里程和日统计闭合`, `历史证据:今日数据 ${formatCount(summary?.frameToday)},用于 RAW 与解析字段复核`, `告警事件:${formatCount(summary?.issueVehicles)} 辆车存在告警`, + '交付核对:', + ...timeMonitorChecklist.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 }))}`, @@ -1390,6 +1431,23 @@ export function Dashboard({ ))} +
+
+ 时间窗服务核对 + + 交付给客户前,先确认范围、轨迹、导出和异常闭环四件事都能讲清楚。 + +
+
+ {timeMonitorChecklist.map((item) => ( +
+ {item.label} + {item.value} + {item.detail} +
+ ))} +
+
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index a3d0fcfc..39b9927c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -580,6 +580,59 @@ body { line-height: 20px; } +.vp-time-audit-strip { + margin-top: 14px; + padding: 14px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: 8px; + background: #fbfcff; + display: grid; + grid-template-columns: minmax(240px, 0.56fr) minmax(0, 1.44fr); + gap: 12px; +} + +.vp-time-audit-head { + min-width: 0; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-time-audit-head .semi-typography { + line-height: 21px; +} + +.vp-time-audit-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-time-audit-item { + min-height: 122px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #ffffff; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-time-audit-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 700; + word-break: break-word; +} + +.vp-time-audit-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + .vp-time-monitor-workbench { margin-top: 14px; display: grid; @@ -3971,6 +4024,8 @@ button.vp-realtime-command-item:focus-visible { .vp-vehicle-decision-grid, .vp-time-review-package, .vp-time-review-grid, + .vp-time-audit-strip, + .vp-time-audit-grid, .vp-realtime-command-board, .vp-realtime-command-grid, .vp-realtime-impact-board, 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 92d255ea..73e374ea 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -818,6 +818,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.getByText('交付给客户前,先确认范围、轨迹、导出和异常闭环四件事都能讲清楚。')).toBeInTheDocument(); + expect(screen.getByText('范围可解释')).toBeInTheDocument(); + expect(screen.getByText('轨迹可复盘')).toBeInTheDocument(); + expect(screen.getByText('数据可导出')).toBeInTheDocument(); + expect(screen.getByText('异常可闭环')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制复盘包' })).toBeInTheDocument(); expect(screen.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument(); expect(screen.getByText('查看轨迹')).toBeInTheDocument(); @@ -928,6 +934,9 @@ 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('时间窗判定:1 天窗口')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付核对:')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 范围可解释')); cleanup(); await renderDashboard();