From 09e9cc937dfaf19cfcf4908a70fdf98d39a5f0c6 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 20:16:58 +0800 Subject: [PATCH] feat(platform): add history evidence package --- .../apps/web/src/pages/History.tsx | 88 +++++++++++++++++-- .../apps/web/src/test/App.test.tsx | 34 ++++++- 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 18fc1044..37677f31 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -6,6 +6,7 @@ import type { HistoryLocationRow, Page, RawFrameRow } from '../api/types'; import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap'; import { PageHeader } from '../components/PageHeader'; import { isAMapConfigured } from '../config/appConfig'; +import { buildAppHash } from '../domain/appRoute'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; type HistoryFilters = { @@ -281,6 +282,59 @@ function trajectorySummaryText({ ].join('\n'); } +function appURL(hash: string) { + return `${window.location.origin}${window.location.pathname}${hash}`; +} + +function historyEvidencePackageText({ + filters, + locationCount, + rawCount, + fieldCount, + validPointCount, + mileageDelta, + maxSpeed, + firstLocation, + lastLocation, + anomalySummary +}: { + filters: HistoryFilters; + locationCount: number; + rawCount: number; + fieldCount: number; + validPointCount: number; + mileageDelta?: number; + maxSpeed?: number; + firstLocation?: HistoryLocationRow; + lastLocation?: HistoryLocationRow; + anomalySummary: TrajectoryAnomalySummary; +}) { + const vehicle = filters.keyword?.trim() || ''; + const protocol = filters.protocol?.trim() || ''; + const evidenceFilters = { + ...(filters.dateFrom?.trim() ? { dateFrom: filters.dateFrom.trim() } : {}), + ...(filters.dateTo?.trim() ? { dateTo: filters.dateTo.trim() } : {}) + }; + return [ + '【历史证据包】', + `车辆:${vehicle || '全部车辆'}`, + `数据来源:${protocol || '全部来源'}`, + `查询范围:${filters.dateFrom?.trim() || '-'} 至 ${filters.dateTo?.trim() || '-'}`, + `位置记录:${locationCount.toLocaleString()},有效定位:${validPointCount.toLocaleString()}`, + `RAW帧:${rawCount.toLocaleString()},解析字段:${fieldCount.toLocaleString()}`, + `区间里程:${formatNumber(mileageDelta, ' km')},最高速度:${formatNumber(maxSpeed, ' km/h')}`, + `起点:${firstLocation?.deviceTime || firstLocation?.serverTime || '-'}`, + `终点:${lastLocation?.deviceTime || lastLocation?.serverTime || '-'}`, + `异常:断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`, + `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: vehicle, protocol, filters: evidenceFilters }))}`, + `历史位置:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'location' } }))}`, + `RAW证据:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`, + `解析字段:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'fields', includeFields: 'true', ...(filters.fields?.trim() ? { fields: filters.fields.trim() } : {}) } }))}`, + `里程复核:${appURL(buildAppHash({ page: 'mileage', keyword: vehicle, protocol, filters: evidenceFilters }))}`, + `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: vehicle, protocol }))}` + ].join('\n'); +} + function amapLocationName(row: HistoryLocationRow, fallback: string) { return encodeURIComponent(row.plate || row.vin || fallback); } @@ -454,7 +508,8 @@ export function History({ }; const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]); - const rawFieldRows = useMemo(() => rawFrames.items.flatMap((row) => { + const rawItems = rawFrames.items ?? []; + const rawFieldRows = useMemo(() => rawItems.flatMap((row) => { const fields = flattenParsedFields(row.parsedFields ?? {}); return fields.map((field, index) => ({ id: `${row.id}-${field.path}-${index}`, @@ -468,8 +523,8 @@ export function History({ fieldPath: field.path, fieldValue: field.value })); - }), [rawFrames.items]); - const locationItems = locations.items; + }), [rawItems]); + const locationItems = locations.items ?? []; const validLocations = locationItems.filter(hasValidCoordinate); const mileageValues = locationItems.map((item) => item.totalMileageKm).filter(isFiniteNumber); const mileageDelta = mileageValues.length > 1 ? Math.max(...mileageValues) - Math.min(...mileageValues) : undefined; @@ -556,6 +611,23 @@ export function History({ '轨迹摘要' ); }; + const copyHistoryEvidencePackage = () => { + copyText( + historyEvidencePackageText({ + filters, + locationCount: locations.total ?? 0, + rawCount: rawFrames.total ?? 0, + fieldCount: rawFieldRows.length, + validPointCount: validLocations.length, + mileageDelta, + maxSpeed, + firstLocation, + lastLocation, + anomalySummary: trajectoryAnomalies + }), + '历史证据包' + ); + }; const openAmapTrajectory = () => { const url = amapTrajectoryURL(validLocations); if (!url) { @@ -643,9 +715,12 @@ export function History({ title={pageTitle} description={pageDescription} actions={( - + + + + )} />
@@ -695,6 +770,7 @@ export function History({ 轨迹回放作业台 + )} 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 e7baa451..5e83af27 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4651,7 +4651,25 @@ test('copies trajectory playback summary from history page', async () => { return { ok: true, json: async () => ({ - data: { items: [], total: 0, limit: 10, offset: 0 }, + data: { + items: [{ + id: 'raw-history-evidence-001', + vin: 'VIN-HISTORY-SUMMARY', + plate: '粤A轨迹1', + protocol: 'JT808', + frameType: '0x0200', + rawSizeBytes: 96, + deviceTime: '2026-07-03 10:00:00', + serverTime: '2026-07-03 10:00:02', + parsedFields: { + 'jt808.location.longitude': 113.201, + 'jt808.location.total_mileage_km': 1000 + } + }], + total: 1, + limit: 10, + offset: 0 + }, traceId: 'trace-test', timestamp: 1783094400000 }) @@ -4681,6 +4699,20 @@ test('copies trajectory playback summary from history page', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最高速度:58.8 km/h')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('起点:2026-07-03 10:00:00')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('终点:2026-07-03 11:00:00')); + + fireEvent.click(screen.getAllByRole('button', { name: /复制历史证据包/ })[0]); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【历史证据包】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('位置记录:2,有效定位:2')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW帧:1,解析字段:2')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常:断点 1 / 里程回退 0 / 超速 0')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史位置:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=location&dateFrom=2026-07-03&dateTo=2026-07-04')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('解析字段:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=fields&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程复核:http://localhost:3000/#/mileage?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-HISTORY-SUMMARY&protocol=JT808')); }); test('opens same-day mileage statistics from quality issue row', async () => {