diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 65fa2f6d..735835c3 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -1,5 +1,5 @@ import { Button, Card, Form, Select, SideSheet, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui'; -import { IconRefresh, IconSearch } from '@douyinfe/semi-icons'; +import { IconCopy, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; import { useEffect, useMemo, useState } from 'react'; import { api, type RawFrameQuery } from '../api/client'; import type { HistoryLocationRow, Page, RawFrameRow } from '../api/types'; @@ -125,6 +125,58 @@ function exportFileName(prefix: string, filters: HistoryFilters) { return `${prefix}-${keyword}-${protocol}.csv`; } +function trajectorySummaryText({ + filters, + totalPoints, + validPointCount, + mileageDelta, + maxSpeed, + firstLocation, + lastLocation +}: { + filters: HistoryFilters; + totalPoints: number; + validPointCount: number; + mileageDelta?: number; + maxSpeed?: number; + firstLocation?: HistoryLocationRow; + lastLocation?: HistoryLocationRow; +}) { + const vehicle = filters.keyword?.trim() || '全部车辆'; + const protocol = filters.protocol?.trim() || '全部来源'; + const dateFrom = filters.dateFrom?.trim(); + const dateTo = filters.dateTo?.trim(); + const range = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部时间'; + const startTime = firstLocation?.deviceTime || firstLocation?.serverTime || '-'; + const endTime = lastLocation?.deviceTime || lastLocation?.serverTime || '-'; + return [ + '【轨迹回放摘要】', + `车辆:${vehicle}`, + `数据来源:${protocol}`, + `查询范围:${range}`, + `轨迹点:${totalPoints.toLocaleString()},有效定位:${validPointCount.toLocaleString()}`, + `区间里程:${formatNumber(mileageDelta, ' km')}`, + `最高速度:${formatNumber(maxSpeed, ' km/h')}`, + `起点:${startTime}`, + `终点:${endTime}`, + `当前位置服务:${window.location.origin}${window.location.pathname}${window.location.hash}` + ].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 History({ initialVin, initialProtocol, @@ -299,6 +351,20 @@ export function History({ downloadCsv(exportFileName('raw-frames', filters), buildCsv(rawExportColumns, rawFrames.items)); Toast.success(`已导出 ${rawFrames.items.length} 条 RAW 帧`); }; + const copyTrajectorySummary = () => { + copyText( + trajectorySummaryText({ + filters, + totalPoints: locations.total, + validPointCount: validLocations.length, + mileageDelta, + maxSpeed, + firstLocation, + lastLocation + }), + '轨迹摘要' + ); + }; const movePlayback = (delta: number) => { if (playbackRows.length === 0) return; setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1)); @@ -388,7 +454,16 @@ export function History({ ) : null} - + + 轨迹回放作业台 + + + )} + style={{ marginTop: 16 }} + >
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 fdb9ae30..533bfb79 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3380,6 +3380,99 @@ test('opens same-day raw evidence from quality issue row', async () => { expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); }); +test('copies trajectory playback summary from history page', async () => { + window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); + 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, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/history/locations')) { + return { + ok: true, + json: async () => ({ + data: { + items: [ + { + vin: 'VIN-HISTORY-SUMMARY', + plate: '粤A轨迹1', + protocol: 'JT808', + longitude: 113.201, + latitude: 23.122, + speedKmh: 12.5, + totalMileageKm: 1000, + deviceTime: '2026-07-03 10:00:00', + serverTime: '2026-07-03 10:00:02' + }, + { + vin: 'VIN-HISTORY-SUMMARY', + plate: '粤A轨迹1', + protocol: 'JT808', + longitude: 113.301, + latitude: 23.222, + speedKmh: 58.8, + totalMileageKm: 1022.6, + deviceTime: '2026-07-03 11:00:00', + serverTime: '2026-07-03 11:00:02' + } + ], + total: 2, + limit: 10, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/history/raw-frames/query')) { + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, 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.findAllByText('VIN-HISTORY-SUMMARY')).length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole('button', { name: /复制轨迹摘要/ })); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹回放摘要】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源:JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('查询范围:2026-07-03 至 2026-07-04')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹点:2,有效定位:2')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间里程:22.6 km')); + 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')); +}); + test('opens same-day mileage statistics from quality issue row', async () => { window.history.replaceState(null, '', '/#/quality'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {