From 442ca3a275c27b8bbdec6f070e68331601181af7 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 11:32:14 +0800 Subject: [PATCH] feat(platform): add trajectory playback controls --- .../apps/web/src/pages/History.tsx | 37 +++++++++ .../apps/web/src/styles/global.css | 9 +++ .../apps/web/src/test/App.test.tsx | 75 +++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 39fea037..786b5298 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -124,6 +124,7 @@ export function History({ const [loadingRaw, setLoadingRaw] = useState(false); const [locationPagination, setLocationPagination] = useState({ currentPage: 1, pageSize: 10 }); const [rawPagination, setRawPagination] = useState({ currentPage: 1, pageSize: 10 }); + const [playbackIndex, setPlaybackIndex] = useState(0); const buildParams = (nextFilters: HistoryFilters, limit: number, offset: number, raw: boolean) => { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }); @@ -154,6 +155,7 @@ export function History({ .then((nextPage) => { setLocations(nextPage); setLocationPagination({ currentPage: page, pageSize }); + setPlaybackIndex(0); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoadingLocations(false)); @@ -223,6 +225,9 @@ export function History({ const currentProtocol = filters.protocol?.trim() ?? ''; const amapConfigured = isAMapConfigured(); const selectedFieldCount = splitFields(filters.fields).length; + const playbackRows = validLocations.length > 0 ? validLocations : locationItems; + const currentPlaybackIndex = playbackRows.length === 0 ? -1 : Math.min(playbackIndex, playbackRows.length - 1); + const currentPlayback = currentPlaybackIndex >= 0 ? playbackRows[currentPlaybackIndex] : undefined; const playbackPoints: VehicleMapPoint[] = validLocations.map((row, index) => ({ id: `${row.vin}-${row.deviceTime || row.serverTime || index}`, label: row.plate || row.vin || `点 ${index + 1}`, @@ -258,6 +263,14 @@ export function History({ downloadCsv(exportFileName('raw-frames', filters), buildCsv(rawExportColumns, rawFrames.items)); Toast.success(`已导出 ${rawFrames.items.length} 条 RAW 帧`); }; + const movePlayback = (delta: number) => { + if (playbackRows.length === 0) return; + setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1)); + }; + const openPlaybackVehicle = () => { + if (!currentPlayback || !canOpenVehicle(currentPlayback.vin)) return; + onOpenVehicle(currentPlayback.vin, currentPlayback.protocol); + }; return (
@@ -340,6 +353,30 @@ export function History({
{item.value}
))} +
+
当前回放点
+ {currentPlayback ? ( + <> + + 点 {currentPlaybackIndex + 1} / {playbackRows.length} + {currentPlayback.protocol || '-'} + + {currentPlayback.plate || currentPlayback.vin} + {currentPlayback.deviceTime || currentPlayback.serverTime || '-'} + + {formatNumber(currentPlayback.speedKmh, ' km/h')} + {formatNumber(currentPlayback.totalMileageKm, ' km')} + + + + + + + + ) : ( + 暂无可回放位置点 + )} +
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index c9ac620b..f8b1480a 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -565,6 +565,15 @@ body { align-content: start; } +.vp-playback-current { + display: grid; + gap: 10px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fff; +} + .vp-playback-timeline { margin-top: 16px; display: grid; 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 ebc717c4..e3bb1cd3 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3381,6 +3381,81 @@ test('shows trajectory playback workspace from history locations', async () => { expect(screen.getByText('终点')).toBeInTheDocument(); }); +test('controls trajectory playback current point from history locations', async () => { + window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-CONTROL&protocol=JT808'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { + const path = String(input); + if (path.includes('/api/history/locations')) { + return { + ok: true, + json: async () => ({ + data: { + items: [ + { vin: 'VIN-TRACK-CONTROL', plate: '粤A回放1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, + { vin: 'VIN-TRACK-CONTROL', plate: '粤A回放1', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' }, + { vin: 'VIN-TRACK-CONTROL', plate: '粤A回放1', protocol: 'JT808', longitude: 113.4, latitude: 23.3, speedKmh: 28, socPercent: 0, totalMileageKm: 119.9, lastSeen: '2026-07-03 10:20:00', deviceTime: '2026-07-03 10:20:00', serverTime: '2026-07-03 10:20:01' } + ], + total: 3, + limit: 10, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/history/raw-frames/query')) { + expect(init?.method).toBe('POST'); + } + if (path.includes('/api/vehicle-service/overview')) { + return { + ok: true, + json: async () => ({ + data: { + vin: 'VIN-TRACK-CONTROL', + plate: '粤A回放1', + sourceCount: 1, + onlineSourceCount: 1, + coverageStatus: 'healthy', + primaryProtocol: 'JT808', + lastSeen: '2026-07-03 10:20:00', + historyCount: 3, + rawCount: 0, + mileageCount: 1, + qualityIssueCount: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('当前回放点')).toBeInTheDocument(); + expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); + expect(screen.getAllByText('2026-07-03 10:00:00').length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole('button', { name: '下一点' })); + expect(screen.getByText('点 2 / 3')).toBeInTheDocument(); + expect(screen.getAllByText('2026-07-03 10:10:00').length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole('button', { name: '上一点' })); + expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '回放点车辆服务' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/detail?keyword=VIN-TRACK-CONTROL&protocol=JT808'); + }); +}); + test('exports current history location page as csv', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-EXPORT-LOC&protocol=JT808'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:history-location');