From c6875e12a5bc06a94dceee411cf34dc1a6a051e3 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 13:03:29 +0800 Subject: [PATCH] feat(platform): export mileage statistics page --- .../apps/web/src/pages/Mileage.tsx | 32 ++++++++++ .../apps/web/src/test/App.test.tsx | 62 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index 6b9d5dd1..0933fb72 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { DailyMileageRow, MileageSummary } from '../api/types'; import { PageHeader } from '../components/PageHeader'; +import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; const emptySummary: MileageSummary = { vehicleCount: 0, @@ -93,6 +94,23 @@ function maxMileageRow(rows: DailyMileageRow[]) { }, undefined); } +const mileageExportColumns: CsvColumn[] = [ + { title: '日期', value: (row) => row.date }, + { title: 'VIN', value: (row) => row.vin }, + { title: '车牌', value: (row) => row.plate }, + { title: '来源', value: (row) => row.source }, + { title: '起始里程km', value: (row) => row.startMileageKm }, + { title: '结束里程km', value: (row) => row.endMileageKm }, + { title: '日里程km', value: (row) => row.dailyMileageKm }, + { title: '异常', value: (row) => row.anomalySeverity || '正常' } +]; + +function exportFileName(filters: Record) { + const keyword = filters.keyword?.trim() || 'all'; + const protocol = filters.protocol?.trim() || 'all-source'; + return `daily-mileage-${keyword}-${protocol}.csv`; +} + export function Mileage({ initialVin, initialProtocol, @@ -182,6 +200,14 @@ export function Mileage({ ...(dateTo ? { dateTo } : {}) }); }; + const exportMileage = () => { + if (rows.length === 0) { + Toast.warning('当前没有可导出的里程明细'); + return; + } + downloadCsv(exportFileName(filters), buildCsv(mileageExportColumns, rows)); + Toast.success(`已导出 ${rows.length} 条里程明细`); + }; useEffect(() => { const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters); @@ -314,6 +340,12 @@ export function Mileage({ +
+ + 当前页 {rows.length.toLocaleString()} 条 + + +
`${row?.date ?? ''}-${row?.vin ?? ''}-${row?.source ?? ''}`} 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 af881548..3dd1dcb2 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4927,6 +4927,68 @@ test('shows mileage statistics workspace with trend source and definition', asyn expect(screen.getByText((content) => content.includes('区间总值应等于各日里程之和'))).toBeInTheDocument(); }); +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'); + const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); + 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: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 42.5, averageMileagePerVin: 42.5 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/mileage/daily')) { + return { + ok: true, + json: async () => ({ + data: { + items: [ + { vin: 'VIN-MILEAGE-EXPORT', plate: '粤A导出1', source: 'JT808', date: '2026-07-03', startMileageKm: 100, endMileageKm: 142.5, dailyMileageKm: 42.5, anomalySeverity: '' } + ], + total: 1, + 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('VIN-MILEAGE-EXPORT')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '导出里程当前页 CSV' })); + + expect(createObjectURL).toHaveBeenCalled(); + expect(revokeObjectURL).toHaveBeenCalledWith('blob:mileage-export'); +}); + test('opens vehicle service from history results with row source evidence', async () => { window.history.replaceState(null, '', '/#/history?keyword=%E7%B2%A4AG18312&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {