diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index f5cf4d44..a73b6039 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -1117,6 +1117,71 @@ export function Mileage({ onClick: () => copyMileageDeliveryPackage() } ]; + const mileageAcceptanceItems = [ + { + label: '范围确认', + value: currentVehicleKeyword || '全部车辆', + detail: `${mileageRangeText} / ${currentProtocol || '全部数据通道'}`, + color: currentVehicleKeyword ? 'blue' as const : 'orange' as const, + disabled: false, + onClick: () => copyStatisticsSummary() + }, + { + label: '区间闭合', + value: closureStatus, + detail: closureDeltaKm == null ? '当前为分页抽样,交付前需要拉齐全量明细。' : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, + color: closureStatusColor, + disabled: false, + onClick: () => copyPublishAudit() + }, + { + label: '明细覆盖', + value: formatPercent(pageCoverageRate), + detail: `${rows.length.toLocaleString()} / ${summary.recordCount.toLocaleString()} 条记录,覆盖全量后再交付更稳。`, + color: pageCoverageRate >= 100 ? 'green' as const : 'orange' as const, + disabled: rows.length === 0, + onClick: () => exportMileage() + }, + { + label: '异常复核', + value: `${anomalyRows.length.toLocaleString()} 条`, + detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 优先回放。` : '当前明细未发现异常日里程。', + color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, + disabled: false, + onClick: () => anomalyRows[0] ? openMileageEvidence(anomalyRows[0]) : copyMileageDecision() + }, + { + label: '在线影响', + value: `${offlineVehicleCount.toLocaleString()} 离线`, + detail: offlineVehicleCount > 0 ? '离线车辆会影响当日解释和客户通知。' : '在线状态未形成主要统计风险。', + color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const, + disabled: false, + onClick: () => copyOnlineOperationsReport() + } + ]; + const mileageAcceptanceActions = [ + { + label: '复制验收', + action: '交付说明', + color: mileageDeliveryColor, + disabled: false, + onClick: () => copyMileageDeliveryPackage() + }, + { + label: '复核轨迹', + action: '轨迹', + color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, + disabled: !currentVehicleKeyword || !onOpenHistory, + onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }) + }, + { + label: '导出明细', + action: 'CSV', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + disabled: rows.length === 0, + onClick: () => exportMileage() + } + ]; const loadSummary = (values: Record = filters) => { setSummaryLoading(true); @@ -1953,6 +2018,52 @@ export function Mileage({ +
+
+ + 里程客户验收清单 + {mileageDeliveryState} + {publishAuditConclusion} + + + 客户交付前只看五个门禁:范围、闭合、覆盖、异常、在线影响;任何一项不通过都能直接进入复核动作。 + + + 这份清单把里程统计从技术查询变成客户验收口径,确保区间总值、每日明细、异常解释和导出证据能讲清楚。 + +
+
+ {mileageAcceptanceItems.map((item) => ( + + ))} +
+
+ {mileageAcceptanceActions.map((item) => ( + + ))} +
+
{mileageCustomerQuestions.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 094a5f7c..4e5b93b3 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -9973,6 +9973,109 @@ button.vp-realtime-command-item:focus-visible { line-height: 18px; } +.vp-mileage-acceptance-strip { + 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(260px, 0.72fr) minmax(0, 1.28fr) minmax(168px, 0.42fr); + overflow: hidden; +} + +.vp-mileage-acceptance-copy { + padding: 16px; + border-right: 1px solid var(--vp-border); + background: #f7fbff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-mileage-acceptance-grid { + padding: 12px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-mileage-acceptance-item { + min-height: 118px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-mileage-acceptance-item:hover, +.vp-mileage-acceptance-item:focus-visible, +.vp-mileage-acceptance-action:hover, +.vp-mileage-acceptance-action:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-mileage-acceptance-item:disabled, +.vp-mileage-acceptance-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-mileage-acceptance-item strong { + color: var(--vp-text); + font-size: 17px; + line-height: 23px; + word-break: break-word; +} + +.vp-mileage-acceptance-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-mileage-acceptance-actions { + padding: 12px; + border-left: 1px solid var(--vp-border); + background: #fafcff; + display: grid; + gap: 8px; +} + +.vp-mileage-acceptance-action { + min-height: 44px; + padding: 8px 10px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-mileage-acceptance-action span { + color: var(--vp-primary); + font-size: 12px; + font-weight: 700; + line-height: 18px; + white-space: nowrap; +} + .vp-mileage-delivery-board { display: grid; grid-template-columns: minmax(280px, 0.64fr) minmax(0, 1.36fr); @@ -14338,6 +14441,8 @@ button.vp-realtime-command-item:focus-visible { .vp-mileage-next-grid, .vp-mileage-question-grid, .vp-mileage-delivery-nav-grid, + .vp-mileage-acceptance-strip, + .vp-mileage-acceptance-grid, .vp-mileage-decision-board, .vp-mileage-decision-grid, .vp-stat-definition-grid, @@ -14478,7 +14583,9 @@ button.vp-realtime-command-item:focus-visible { .vp-mileage-conclusion-copy, .vp-mileage-conclusion-actions, .vp-mileage-definition-copy, - .vp-mileage-definition-actions { + .vp-mileage-definition-actions, + .vp-mileage-acceptance-copy, + .vp-mileage-acceptance-actions { border-left: 0; border-right: 0; border-bottom: 1px solid var(--vp-border); 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 6120cedc..4457c7e8 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -9380,6 +9380,16 @@ test('shows vehicle-first statistics domains for one vehicle service', async () expect(screen.getByRole('button', { name: '客户里程交付导航 回放里程轨迹 轨迹复核' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程交付导航 导出明细证据 导出CSV' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程交付导航 发送交付结论 复制交付包' })).toBeInTheDocument(); + expect(screen.getByText('里程客户验收清单')).toBeInTheDocument(); + expect(screen.getByText('客户交付前只看五个门禁:范围、闭合、覆盖、异常、在线影响;任何一项不通过都能直接进入复核动作。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程客户验收 范围确认 VIN-STATS-SERVICE' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程客户验收 区间闭合 分页抽样' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程客户验收 明细覆盖 50%' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程客户验收 异常复核 1 条' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程客户验收 在线影响 1 离线' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程验收动作 复制验收 交付说明' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程验收动作 复核轨迹 轨迹' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程验收动作 导出明细 CSV' })).toBeInTheDocument(); expect(await screen.findByText('车辆统计服务总览')).toBeInTheDocument(); expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('在线率与离线时长')).toBeInTheDocument();