diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index 3f405273..49a05f31 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -838,6 +838,59 @@ export function Mileage({ detail: `${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线` } ]; + const mileageCustomerWorkbenchItems = [ + { + label: '区间里程', + value: `${formatKm(summary.totalMileageKm)} km`, + detail: `${summary.vehicleCount.toLocaleString()} 辆车,${mileageRangeText}。`, + color: 'blue' as const, + action: '复制摘要', + disabled: false, + onClick: () => copyStatisticsSummary() + }, + { + label: '每日闭合', + value: closureStatus, + detail: closureDeltaKm == null ? `当前页覆盖 ${formatPercent(pageCoverageRate)},发布前需要拉齐全量。` : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, + color: closureStatusColor, + action: '发布审计', + disabled: false, + 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: `${rows.length.toLocaleString()} 条`, + detail: `当前页覆盖 ${formatPercent(pageCoverageRate)},用于客户对账和 BI 复核。`, + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + action: '导出明细', + disabled: rows.length === 0, + onClick: () => exportMileage() + }, + { + label: '告警通知', + value: `${offlineVehicleCount.toLocaleString()} 离线`, + detail: offlineVehicleCount > 0 ? '离线会影响定位、里程解释和客户通知。' : '当前在线状态未形成主要统计风险。', + color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const, + action: '在线态势', + disabled: false, + onClick: () => copyOnlineOperationsReport() + } + ]; const mileageCustomerActions = [ { label: '导出当前明细', @@ -1446,6 +1499,40 @@ export function Mileage({ )} /> + + + + 客户里程统计工作台 + {mileageDeliveryState} + {publishAuditConclusion} + + + 统计页先回答客户这段时间跑了多少、每日是否闭合、异常在哪里、能否导出。 + + + 车辆范围:{currentVehicleKeyword || '全部车辆'} + 时间范围:{mileageRangeText} + 来源证据只用于统计可信度:{currentProtocol || '全部数据通道'} + + + + {mileageCustomerWorkbenchItems.map((item) => ( + + {item.label} + {item.value} + {item.detail} + {item.action} + + ))} + + diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 8e069ae6..dc8dbd2c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -5646,6 +5646,90 @@ button.vp-realtime-command-item:focus-visible { gap: 16px; } +.vp-mileage-customer-workbench { + margin-top: 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.62fr) minmax(0, 1.38fr); + overflow: hidden; +} + +.vp-mileage-customer-workbench-main { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f7fbff; + display: grid; + gap: 12px; + align-content: start; +} + +.vp-mileage-customer-workbench-meta { + display: grid; + gap: 6px; + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + +.vp-mileage-customer-workbench-grid { + padding: 16px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-mileage-customer-workbench-item { + min-height: 144px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.14); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-mileage-customer-workbench-item:hover, +.vp-mileage-customer-workbench-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-mileage-customer-workbench-item:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-mileage-customer-workbench-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + word-break: break-word; +} + +.vp-mileage-customer-workbench-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-mileage-customer-workbench-item em { + color: var(--vp-primary); + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 18px; +} + .vp-mileage-reconciliation-desk { margin-top: 16px; border: 1px solid rgba(11, 159, 119, 0.2); @@ -9311,6 +9395,8 @@ button.vp-realtime-command-item:focus-visible { .vp-online-ops-board, .vp-online-ops-grid, .vp-stat-insight-grid, + .vp-mileage-customer-workbench, + .vp-mileage-customer-workbench-grid, .vp-mileage-reconciliation-desk, .vp-mileage-reconciliation-grid, .vp-mileage-delivery-console, 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 2c0c0b7e..bb015447 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -8650,6 +8650,14 @@ test('shows vehicle-first statistics domains for one vehicle service', async () render(); expect(await screen.findByText('客户统计查询')).toBeInTheDocument(); + expect(screen.getByText('客户里程统计工作台')).toBeInTheDocument(); + expect(screen.getByText('统计页先回答客户这段时间跑了多少、每日是否闭合、异常在哪里、能否导出。')).toBeInTheDocument(); + expect(screen.getByText('来源证据只用于统计可信度:JT808')).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: '客户里程统计工作台 历史导出 3 条 导出明细' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程统计工作台 告警通知 1 离线 在线态势' })).toBeInTheDocument(); expect(screen.getByText('先定车辆和时间范围,再判断里程能否交付')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户统计查询 选择范围 调整筛选' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户统计查询 区间里程 复制摘要' })).toBeInTheDocument();