diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index e7b4b601..4fa74d58 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -1215,6 +1215,56 @@ export function Mileage({ onClick: exportMileage } ]; + const mileageReconciliationItems = [ + { + label: '区间里程', + value: `${formatKm(summary.totalMileageKm)} km`, + detail: `车辆 ${summary.vehicleCount.toLocaleString()} 辆,时间窗 ${mileageRangeText}。`, + color: 'blue' as const, + action: '复制摘要', + onClick: copyStatisticsSummary + }, + { + label: '日报闭合', + value: closureStatus, + detail: closureDeltaKm == null ? `当前页覆盖 ${formatPercent(pageCoverageRate)},交付前需要拉齐明细。` : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, + color: closureStatusColor, + action: '闭合审计', + onClick: copyPublishAudit + }, + { + label: '轨迹证据', + value: peakMileage?.date || mileageRangeText, + detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} 最大单日 ${formatKm(peakMileage.dailyMileageKm)} km。` : '选择车辆后可按同一时间窗回放轨迹。', + color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, + action: '轨迹回放', + disabled: !currentVehicleKeyword || !onOpenHistory, + onClick: () => { + if (peakMileage) { + openMileageEvidence(peakMileage); + return; + } + onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }); + } + }, + { + label: '告警通知', + value: `${offlineVehicleCount.toLocaleString()} 离线`, + detail: offlineVehicleCount > 0 ? '离线车辆会影响当日里程、定位和客户通知。' : '当前在线状态未形成主要统计风险。', + color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const, + action: '在线态势', + onClick: copyOnlineOperationsReport + }, + { + label: '报表导出', + value: `${rows.length.toLocaleString()} 条`, + detail: '导出当前明细,作为客户对账、BI核对和运营日报附件。', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + action: '导出CSV', + disabled: rows.length === 0, + onClick: exportMileage + } + ]; const mileageServiceTasks = [ { title: '发布判断', @@ -1379,6 +1429,36 @@ export function Mileage({ )} /> +
+
+ + 客户里程对账台 + {mileageDeliveryState} + {publishAuditConclusion} + + 用同一辆车和同一时间窗完成区间里程、日报闭合、轨迹证据、告警通知和报表导出。 + + 面向客户的统计页只围绕车辆服务展开:先看里程结论,再用轨迹和在线态势解释风险,最后交付 CSV 或复核说明。 + +
+
+ {mileageReconciliationItems.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 e88d3d18..a52dd17c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -5073,6 +5073,82 @@ button.vp-realtime-command-item:focus-visible { gap: 16px; } +.vp-mileage-reconciliation-desk { + margin-top: 16px; + border: 1px solid rgba(11, 159, 119, 0.2); + border-radius: var(--vp-radius); + background: #fff; + display: grid; + grid-template-columns: minmax(280px, 0.56fr) minmax(0, 1.44fr); + overflow: hidden; +} + +.vp-mileage-reconciliation-summary { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f5fbf9; + display: grid; + gap: 11px; + align-content: start; +} + +.vp-mileage-reconciliation-grid { + padding: 16px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-mileage-reconciliation-item { + min-height: 148px; + padding: 12px; + border: 1px solid rgba(11, 159, 119, 0.18); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-mileage-reconciliation-item:hover, +.vp-mileage-reconciliation-item:focus-visible { + border-color: rgba(11, 159, 119, 0.46); + background: #f5fbf9; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-mileage-reconciliation-item:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-mileage-reconciliation-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + word-break: break-word; +} + +.vp-mileage-reconciliation-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-mileage-reconciliation-item em { + color: var(--vp-primary); + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 18px; +} + .vp-mileage-delivery-console { margin-top: 16px; margin-bottom: 16px; @@ -8582,6 +8658,8 @@ button.vp-realtime-command-item:focus-visible { .vp-online-ops-board, .vp-online-ops-grid, .vp-stat-insight-grid, + .vp-mileage-reconciliation-desk, + .vp-mileage-reconciliation-grid, .vp-mileage-delivery-console, .vp-mileage-delivery-console-grid, .vp-mileage-next-actions, 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 a41471a3..46e6d8fe 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -8228,6 +8228,13 @@ test('shows vehicle-first statistics domains for one vehicle service', async () expect(screen.getByRole('button', { name: '客户统计查询 发布判断 发布审计' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户统计查询 复核证据 轨迹复核' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户统计查询 明细导出 导出明细' })).toBeInTheDocument(); + expect(screen.getByText('客户里程对账台')).toBeInTheDocument(); + expect(screen.getByText('用同一辆车和同一时间窗完成区间里程、日报闭合、轨迹证据、告警通知和报表导出。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程对账台 区间里程 210 km 复制摘要' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程对账台 日报闭合 分页抽样 闭合审计' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程对账台 轨迹证据 2026-07-02 轨迹回放' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程对账台 告警通知 1 离线 在线态势' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程对账台 报表导出 3 条 导出CSV' })).toBeInTheDocument(); expect(screen.getByText('客户里程交付导航')).toBeInTheDocument(); expect(screen.getByText('把里程统计变成客户能看懂的交付路径:先确认口径,再回放轨迹,最后导出明细和结论。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程交付导航 确认统计口径 复制统计摘要' })).toBeInTheDocument();