diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 735835c3..e32016cc 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -163,6 +163,29 @@ function trajectorySummaryText({ ].join('\n'); } +function amapLocationName(row: HistoryLocationRow, fallback: string) { + return encodeURIComponent(row.plate || row.vin || fallback); +} + +function amapTrajectoryURL(points: HistoryLocationRow[]) { + const validPoints = points.filter(hasValidCoordinate); + if (validPoints.length === 0) return ''; + if (validPoints.length === 1) { + const point = validPoints[0]; + return `https://uri.amap.com/marker?position=${point.longitude},${point.latitude}&name=${amapLocationName(point, '车辆位置')}&src=lingniu-vehicle-platform`; + } + const start = validPoints[0]; + const end = validPoints[validPoints.length - 1]; + return [ + 'https://uri.amap.com/navigation?', + `from=${start.longitude},${start.latitude},${amapLocationName(start, '轨迹起点')}`, + `&to=${end.longitude},${end.latitude},${amapLocationName(end, '轨迹终点')}`, + '&mode=car', + '&policy=1', + '&src=lingniu-vehicle-platform' + ].join(''); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -365,6 +388,14 @@ export function History({ '轨迹摘要' ); }; + const openAmapTrajectory = () => { + const url = amapTrajectoryURL(validLocations); + if (!url) { + Toast.warning('当前轨迹没有有效坐标'); + return; + } + window.open(url, '_blank', 'noopener,noreferrer'); + }; const movePlayback = (delta: number) => { if (playbackRows.length === 0) return; setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1)); @@ -460,6 +491,7 @@ export function History({ 轨迹回放作业台 + )} 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 1c40aac5..358875c9 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4499,6 +4499,116 @@ test('shows trajectory playback workspace from history locations', async () => { expect(screen.getByText('终点')).toBeInTheDocument(); }); +test('opens amap route from trajectory playback workspace', async () => { + window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-AMAP&protocol=JT808'); + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { + 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/history/locations')) { + return { + ok: true, + json: async () => ({ + data: { + items: [ + { vin: 'VIN-TRACK-AMAP', 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-AMAP', 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' } + ], + total: 2, + limit: 10, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/history/raw-frames/query')) { + expect(init?.method).toBe('POST'); + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '高德线路' })); + + expect(openSpy).toHaveBeenCalledWith( + expect.stringContaining('https://uri.amap.com/navigation?from=113.2,23.1'), + '_blank', + 'noopener,noreferrer' + ); + expect(openSpy).toHaveBeenCalledWith( + expect.stringContaining('to=113.3,23.2'), + '_blank', + 'noopener,noreferrer' + ); +}); + +test('opens amap marker when trajectory has one valid point', async () => { + window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-AMAP-ONE&protocol=JT808'); + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); + 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-AMAP-ONE', plate: '粤A单点1', protocol: 'JT808', longitude: 113.28, latitude: 23.18, 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' } + ], + total: 1, + limit: 10, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/history/raw-frames/query')) { + expect(init?.method).toBe('POST'); + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '高德线路' })); + + expect(openSpy).toHaveBeenCalledWith( + expect.stringContaining('https://uri.amap.com/marker?position=113.28,23.18'), + '_blank', + 'noopener,noreferrer' + ); +}); + 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) => {