From ebaf67a134b9403602a39283f21ddd13ed836054 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 05:46:12 +0800 Subject: [PATCH] feat(platform): add mileage report delivery journey --- .../apps/web/src/pages/Mileage.tsx | 105 ++++++++++++++++++ .../apps/web/src/styles/global.css | 74 ++++++++++++ .../apps/web/src/test/App.test.tsx | 14 +++ 3 files changed, 193 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index 1940390e..84a818ba 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -1482,6 +1482,76 @@ export function Mileage({ '在线影响评估' ); }; + const copyMileageReportJourneyPackage = () => { + const vehicleURL = currentVehicleKeyword + ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) + : undefined; + const reportPeriod = mileageRangeText; + const trajectoryProof = peakMileage + ? `${peakMileage.plate || peakMileage.vin} / ${peakMileage.date} / ${formatKm(peakMileage.dailyMileageKm)} km` + : '暂无轨迹证明样本'; + copyText([ + '【里程报表交付旅程包】', + `报表周期:${reportPeriod}`, + `车辆范围:${currentVehicleKeyword || '全部车辆'}`, + `数据通道:${currentProtocol || '全部数据通道'}`, + `区间里程:${formatKm(summary.totalMileageKm)} km`, + `区间闭合:${closureStatus}`, + `记录覆盖:${formatPercent(pageCoverageRate)}`, + `异常记录:${anomalyRows.length.toLocaleString()} 条`, + `轨迹证明:${trajectoryProof}`, + `导出发布:${mileageDeliveryState} / ${publishAuditConclusion}`, + `在线影响:${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(onlineRatePercent)}`, + `里程统计:${appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol }))}`, + `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: { ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) } }))}`, + `历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: { ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`, + ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) + ].join('\n'), '里程报表交付旅程包'); + }; + const mileageReportJourneyItems = [ + { + step: '1', + title: '报表周期', + value: mileageRangeText, + detail: `${currentVehicleKeyword || '全部车辆'} / ${currentProtocol || '全部数据通道'},先确认客户要看的车辆和时间段。`, + color: currentVehicleKeyword ? 'blue' as const : 'orange' as const, + disabled: false, + onClick: copyStatisticsSummary + }, + { + step: '2', + title: '区间闭合', + value: closureStatus, + detail: closureDeltaKm == null ? `当前明细覆盖 ${formatPercent(pageCoverageRate)},发布前需要拉齐全量。` : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, + color: closureStatusColor, + disabled: false, + onClick: copyPublishAudit + }, + { + step: '3', + title: '轨迹证明', + value: peakMileage?.plate || peakMileage?.vin || '待选车辆', + detail: peakMileage ? `${peakMileage.date} 最大单日 ${formatKm(peakMileage.dailyMileageKm)} km,建议回放轨迹证明。` : '选择车辆后可进入同一时间窗轨迹回放。', + color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, + disabled: !currentVehicleKeyword || !onOpenHistory, + onClick: () => { + if (peakMileage) { + openMileageEvidence(peakMileage); + return; + } + onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }); + } + }, + { + step: '4', + title: '导出发布', + value: mileageDeliveryState, + detail: `${publishAuditConclusion},导出明细和交付包给客户或 BI 复核。`, + color: mileageDeliveryColor, + disabled: false, + onClick: copyMileageReportJourneyPackage + } + ]; const mileageDeliveryConsoleItems = [ { label: '交付状态', @@ -1889,6 +1959,41 @@ export function Mileage({ ))} +
+
+ + 里程报表交付旅程 + {mileageDeliveryState} + {publishAuditConclusion} + + + 按客户报表习惯组织:先确认报表周期和车辆范围,再检查区间闭合、轨迹证明,最后导出或暂缓发布。 + + + 报表交付不再从协议解释开始,而是围绕车辆、时间、里程闭合和可导出证据完成客户复核。 + + +
+
+ {mileageReportJourneyItems.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 85cf5b2f..73b28532 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -10206,6 +10206,80 @@ button.vp-realtime-command-item:focus-visible { align-self: end; } +.vp-mileage-report-journey { + margin-top: 16px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #f8fbff; + display: grid; + grid-template-columns: minmax(280px, 0.68fr) minmax(0, 1.32fr); + gap: 14px; + padding: 16px; +} + +.vp-mileage-report-journey-copy { + display: grid; + align-content: start; + gap: 10px; +} + +.vp-mileage-report-journey-copy .semi-button { + width: fit-content; +} + +.vp-mileage-report-journey-steps { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-mileage-report-journey-step { + min-height: 138px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + align-content: start; + gap: 8px; + transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; +} + +.vp-mileage-report-journey-step:hover, +.vp-mileage-report-journey-step:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + box-shadow: 0 10px 24px rgba(24, 39, 75, 0.08); + outline: none; + transform: translateY(-1px); +} + +.vp-mileage-report-journey-step:disabled { + cursor: not-allowed; + opacity: 0.62; + box-shadow: none; + transform: none; +} + +.vp-mileage-report-journey-step strong { + color: var(--vp-text); + font-size: 17px; + line-height: 23px; + font-weight: 700; + word-break: break-word; +} + +.vp-mileage-report-journey-step span, +.vp-mileage-report-journey-step small { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + .vp-mileage-customer-query-board { margin-top: 16px; overflow: hidden; 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 898f5a96..13c8ccc4 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -9900,6 +9900,13 @@ test('shows vehicle-first statistics domains for one vehicle service', async () expect(await screen.findByText('客户统计查询')).toBeInTheDocument(); expect(screen.getByText('客户里程对账总控')).toBeInTheDocument(); expect(screen.getByText('把区间里程、每日闭合、轨迹证明、异常通知和报表导出收敛成客户可验收的对账链路。')).toBeInTheDocument(); + expect(screen.getByText('里程报表交付旅程')).toBeInTheDocument(); + expect(screen.getByText('按客户报表习惯组织:先确认报表周期和车辆范围,再检查区间闭合、轨迹证明,最后导出或暂缓发布。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程报表交付旅程 1 报表周期 全部日期' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程报表交付旅程 2 区间闭合 分页抽样' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程报表交付旅程 3 轨迹证明 粤A统计服务2' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程报表交付旅程 4 导出发布 复核后交付' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /复制报表旅程包/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程对账总控 区间里程 210 km 复制摘要' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程对账总控 每日闭合 分页抽样 闭合审计' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程对账总控 轨迹证明 2026-07-02 轨迹复核' })).toBeInTheDocument(); @@ -10038,6 +10045,13 @@ test('shows vehicle-first statistics domains for one vehicle service', async () expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间里程:210 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('闭合状态:分页抽样')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('报表导出:http://localhost:3000/#/mileage?keyword=VIN-STATS-SERVICE&protocol=JT808')); + fireEvent.click(screen.getByRole('button', { name: /复制报表旅程包/ })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程报表交付旅程包】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('报表周期:全部日期')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间闭合:分页抽样')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证明:粤A统计服务2 / 2026-07-02 / 80 km')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('导出发布:复核后交付 / 复核后发布')); fireEvent.click(screen.getAllByRole('button', { name: '复制决策说明' })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户里程决策说明】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('决策结论:复核后交付'));