diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 37677f31..7d5cece3 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -335,6 +335,75 @@ function historyEvidencePackageText({ ].join('\n'); } +function trajectoryReviewPackageText({ + filters, + locationCount, + rawCount, + fieldCount, + validPointCount, + coverageRate, + playbackSpanMinutes, + playbackIntervalMinutes, + mileageDelta, + maxSpeed, + firstLocation, + lastLocation, + currentPlayback, + currentPlaybackIndex, + playbackCount, + anomalySummary +}: { + filters: HistoryFilters; + locationCount: number; + rawCount: number; + fieldCount: number; + validPointCount: number; + coverageRate?: number; + playbackSpanMinutes?: number; + playbackIntervalMinutes?: number; + mileageDelta?: number; + maxSpeed?: number; + firstLocation?: HistoryLocationRow; + lastLocation?: HistoryLocationRow; + currentPlayback?: HistoryLocationRow; + currentPlaybackIndex: number; + playbackCount: number; + 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() } : {}) + }; + const anomalyAction = anomalySummary.gapCount > 0 || anomalySummary.mileageRollbackCount > 0 || anomalySummary.overspeedCount > 0 + ? '建议核对 RAW 帧、解析字段、当日里程统计,并确认平台是否存在断链或补发。' + : '当前页未发现明显断点、里程回退或速度异常,可作为轨迹证据使用。'; + return [ + '【轨迹复盘交接包】', + `车辆:${vehicle || '全部车辆'}`, + `数据来源:${protocol || '全部来源'}`, + `查询范围:${filters.dateFrom?.trim() || '-'} 至 ${filters.dateTo?.trim() || '-'}`, + `轨迹规模:位置 ${locationCount.toLocaleString()} / 有效定位 ${validPointCount.toLocaleString()} / RAW ${rawCount.toLocaleString()} / 解析字段 ${fieldCount.toLocaleString()}`, + `轨迹质量:定位覆盖率 ${formatPercent(coverageRate)} / 回放跨度 ${formatDurationMinutes(playbackSpanMinutes)} / 采样间隔 ${formatDurationMinutes(playbackIntervalMinutes, '/点')}`, + `里程速度:区间里程 ${formatNumber(mileageDelta, ' km')} / 最高速度 ${formatNumber(maxSpeed, ' km/h')}`, + `起点:${firstLocation?.deviceTime || firstLocation?.serverTime || '-'}`, + `终点:${lastLocation?.deviceTime || lastLocation?.serverTime || '-'}`, + `当前回放点:${currentPlayback ? `点 ${currentPlaybackIndex + 1}/${playbackCount},${currentPlayback.deviceTime || currentPlayback.serverTime || '-'},${formatNumber(currentPlayback.speedKmh, ' km/h')},${formatNumber(currentPlayback.totalMileageKm, ' km')}` : '-'}`, + `异常判读:断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`, + `最大断点:${formatDurationMinutes(anomalySummary.maxGapMinutes)}`, + `最大里程回退:${formatNumber(anomalySummary.maxMileageRollbackKm, ' km')}`, + `最高异常速度:${formatNumber(anomalySummary.maxSpeedKmh, ' km/h')}`, + `下一步:${anomalyAction}`, + `轨迹回放:${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); } @@ -628,6 +697,29 @@ export function History({ '历史证据包' ); }; + const copyTrajectoryReviewPackage = () => { + copyText( + trajectoryReviewPackageText({ + filters, + locationCount: locations.total ?? 0, + rawCount: rawFrames.total ?? 0, + fieldCount: rawFieldRows.length, + validPointCount: validLocations.length, + coverageRate: trajectoryCoverageRate, + playbackSpanMinutes, + playbackIntervalMinutes, + mileageDelta, + maxSpeed, + firstLocation, + lastLocation, + currentPlayback, + currentPlaybackIndex, + playbackCount: playbackRows.length, + anomalySummary: trajectoryAnomalies + }), + '轨迹复盘交接包' + ); + }; const openAmapTrajectory = () => { const url = amapTrajectoryURL(validLocations); if (!url) { @@ -771,6 +863,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 e5b4dd5f..c50ccb6e 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4798,6 +4798,18 @@ test('copies trajectory playback summary from history page', async () => { 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')); + + fireEvent.click(screen.getByRole('button', { name: /复制轨迹复盘包/ })); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹复盘交接包】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹规模:位置 2 / 有效定位 2 / RAW 1 / 解析字段 2')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹质量:定位覆盖率 100% / 回放跨度 60 分钟 / 采样间隔 60 分钟/点')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程速度:区间里程 22.6 km / 最高速度 58.8 km/h')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前回放点:点 1/2,2026-07-03 10:00:00,12.5 km/h,1,000 km')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常判读:断点 1 / 里程回退 0 / 超速 0')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('下一步:建议核对 RAW 帧、解析字段、当日里程统计,并确认平台是否存在断链或补发。')); + 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')); }); test('opens same-day mileage statistics from quality issue row', async () => {