From 9d35f65be6f4784f1a365019be83b1d30d684949 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 13:19:08 +0800 Subject: [PATCH] feat(platform): copy mileage statistics summary --- .../apps/web/src/pages/Mileage.tsx | 68 +++++++++++++++++ .../apps/web/src/test/App.test.tsx | 73 +++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index 0933fb72..42004247 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -111,6 +111,56 @@ function exportFileName(filters: Record) { return `daily-mileage-${keyword}-${protocol}.csv`; } +function mileageSummaryText({ + filters, + summary, + pageMileageTotal, + anomalyCount, + peakMileage +}: { + filters: Record; + summary: MileageSummary; + pageMileageTotal: number; + anomalyCount: number; + peakMileage?: DailyMileageRow; +}) { + const keyword = filters.keyword?.trim() || '全部车辆'; + const protocol = filters.protocol?.trim() || '全部来源'; + const dateFrom = filters.dateFrom?.trim(); + const dateTo = filters.dateTo?.trim(); + const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; + const peak = peakMileage + ? `${peakMileage.plate || peakMileage.vin} / ${peakMileage.date} / ${formatKm(peakMileage.dailyMileageKm)} km` + : '-'; + return [ + '【里程统计摘要】', + `车辆范围:${keyword}`, + `数据来源:${protocol}`, + `日期范围:${dateRange}`, + `统计车辆:${summary.vehicleCount.toLocaleString()},记录:${summary.recordCount.toLocaleString()},来源:${summary.sourceCount.toLocaleString()}`, + `累计里程:${formatKm(summary.totalMileageKm)} km`, + `单车平均:${formatKm(summary.averageMileagePerVin)} km`, + `当前页里程:${formatKm(pageMileageTotal)} km`, + `异常记录:${anomalyCount.toLocaleString()}`, + `最大单日:${peak}`, + '统计口径:区间总值等于各日里程之和' + ].join('\n'); +} + +async function copyText(value: string, label: string) { + const text = value.trim(); + if (!text) { + Toast.warning(`${label}为空`); + return; + } + try { + await navigator.clipboard.writeText(text); + Toast.success(`已复制${label}`); + } catch { + Toast.error(`复制${label}失败`); + } +} + export function Mileage({ initialVin, initialProtocol, @@ -208,6 +258,18 @@ export function Mileage({ downloadCsv(exportFileName(filters), buildCsv(mileageExportColumns, rows)); Toast.success(`已导出 ${rows.length} 条里程明细`); }; + const copyStatisticsSummary = () => { + copyText( + mileageSummaryText({ + filters, + summary, + pageMileageTotal, + anomalyCount: anomalyRows.length, + peakMileage + }), + '统计摘要' + ); + }; useEffect(() => { const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters); @@ -324,6 +386,12 @@ export function Mileage({ ))} +
+ + 可复制日报口径 + + +
日里程 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 9493f9ec..01a9681e 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -5059,6 +5059,79 @@ test('shows mileage statistics workspace with trend source and definition', asyn expect(screen.getByText((content) => content.includes('区间总值应等于各日里程之和'))).toBeInTheDocument(); }); +test('copies mileage statistics summary for operations reporting', async () => { + window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-COPY&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03'); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/mileage/summary')) { + return { + ok: true, + json: async () => ({ + data: { vehicleCount: 2, recordCount: 4, sourceCount: 2, totalMileageKm: 140, averageMileagePerVin: 70 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/mileage/daily')) { + return { + ok: true, + json: async () => ({ + data: { + items: [ + { vin: 'VIN-MILEAGE-COPY', plate: '粤A统计1', source: 'JT808', date: '2026-07-01', startMileageKm: 100, endMileageKm: 140, dailyMileageKm: 40, anomalySeverity: '' }, + { vin: 'VIN-MILEAGE-COPY-2', plate: '粤A统计2', source: 'JT808', date: '2026-07-02', startMileageKm: 300, endMileageKm: 400, dailyMileageKm: 100, anomalySeverity: 'warning' } + ], + total: 2, + limit: 20, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('区间趋势')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '复制统计摘要' })); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程统计摘要】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-MILEAGE-COPY')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源:JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('日期范围:2026-07-01 至 2026-07-03')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('累计里程:140 km')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页里程:140 km')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常记录:1')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最大单日:粤A统计2 / 2026-07-02 / 100 km')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计口径:区间总值等于各日里程之和')); +}); + test('exports current mileage page as CSV', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-EXPORT&protocol=JT808'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mileage-export');