From 5bbd110743772db98faea3e864abe2bba38056c6 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 15:24:03 +0800 Subject: [PATCH] feat(platform): show mileage closure check --- vehicle-data-platform/apps/web/src/pages/Mileage.tsx | 11 ++++++++++- vehicle-data-platform/apps/web/src/test/App.test.tsx | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index 5041b21c..518d9b32 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -205,6 +205,9 @@ export function Mileage({ const anomalyRate = rows.length > 0 ? (anomalyRows.length / rows.length) * 100 : 0; const peakMileageShare = pageMileageTotal > 0 && peakMileage ? (peakMileage.dailyMileageKm / pageMileageTotal) * 100 : 0; const pageCoverageRate = summary.recordCount > 0 ? (rows.length / summary.recordCount) * 100 : 0; + const hasFullMileagePage = summary.recordCount > 0 && rows.length === summary.recordCount; + const closureDeltaKm = hasFullMileagePage ? summary.totalMileageKm - pageMileageTotal : undefined; + const closurePassed = closureDeltaKm != null && Math.abs(closureDeltaKm) < 0.01; const confidenceStatus = anomalyRows.length > 0 || pageCoverageRate < 100 ? '需复核' : '可用于 BI'; const confidenceColor = confidenceStatus === '可用于 BI' ? 'green' as const : 'orange' as const; const maxDateMileage = Math.max(...dateSeries.map((item) => item.value), 0); @@ -400,7 +403,13 @@ export function Mileage({ { label: '当前页里程', value: `${formatKm(pageMileageTotal)} km`, color: 'blue' as const, detail: '用于快速核对当前分页明细合计。' }, { label: '异常记录', value: anomalyRows.length.toLocaleString(), color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, detail: '来自异常标记,优先核对首末总里程和断链。' }, { label: '最大单日', value: peakMileage ? `${formatKm(peakMileage.dailyMileageKm)} km` : '-', color: 'blue' as const, detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} / ${peakMileage.date}` : '暂无明细。' }, - { label: '分页覆盖率', value: formatPercent(pageCoverageRate), color: pageCoverageRate >= 100 ? 'green' as const : 'grey' as const, detail: '当前分页记录数 / 全量统计记录数。' } + { label: '分页覆盖率', value: formatPercent(pageCoverageRate), color: pageCoverageRate >= 100 ? 'green' as const : 'grey' as const, detail: '当前分页记录数 / 全量统计记录数。' }, + { + label: '闭合校验', + value: closureDeltaKm == null ? '需全量核验' : `${formatKm(Math.abs(closureDeltaKm))} km`, + color: closurePassed ? 'green' as const : closureDeltaKm == null ? 'grey' as const : 'orange' as const, + detail: closureDeltaKm == null ? '当前分页未覆盖全量记录,需翻页或导出全量后核验。' : '汇总总里程与当前明细合计的差值。' + } ].map((item) => ( {item.label} 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 adb9120f..7e1935f2 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -6266,6 +6266,8 @@ test('shows mileage statistics workspace with trend source and definition', asyn expect(screen.getByText('42.9%')).toBeInTheDocument(); expect(screen.getByText('分页覆盖率')).toBeInTheDocument(); expect(screen.getByText('100%')).toBeInTheDocument(); + expect(screen.getByText('闭合校验')).toBeInTheDocument(); + expect(screen.getAllByText('0 km').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('统计口径')).toBeInTheDocument(); expect(screen.getByText((content) => content.includes('区间总值应等于各日里程之和'))).toBeInTheDocument(); });