From 483d92719b9038e3737a9de23c2a47c9e00400b0 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 18:04:45 +0800 Subject: [PATCH] feat(platform): link trajectory playback selection --- .../apps/web/src/pages/History.tsx | 42 +++++++++++++++++-- .../apps/web/src/styles/global.css | 22 ++++++++++ .../apps/web/src/test/App.test.tsx | 9 ++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 759e42da..e9f3dc35 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -109,6 +109,10 @@ function timeValue(row?: HistoryLocationRow) { return Number.isFinite(ms) ? ms : undefined; } +function playbackPointId(row: HistoryLocationRow, index: number) { + return `${row.vin || 'unknown'}-${row.deviceTime || row.serverTime || row.lastSeen || index}-${index}`; +} + function formatDurationMinutes(value?: number, suffix = '') { if (!isFiniteNumber(value)) return '-'; if (value < 1) return `${Math.round(value * 60)} 秒${suffix}`; @@ -444,9 +448,10 @@ export function History({ 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 currentPlaybackPointId = currentPlayback ? playbackPointId(currentPlayback, currentPlaybackIndex) : undefined; const playbackAtEnd = playbackRows.length > 0 && currentPlaybackIndex >= playbackRows.length - 1; const playbackPoints: VehicleMapPoint[] = validLocations.map((row, index) => ({ - id: `${row.vin}-${row.deviceTime || row.serverTime || index}`, + id: playbackPointId(row, index), label: row.plate || row.vin || `点 ${index + 1}`, longitude: row.longitude, latitude: row.latitude, @@ -539,6 +544,17 @@ export function History({ setPlaybackPlaying(false); setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1)); }; + const selectPlaybackPoint = (nextIndex: number) => { + if (playbackRows.length === 0) return; + setPlaybackPlaying(false); + setPlaybackIndex(Math.min(Math.max(nextIndex, 0), playbackRows.length - 1)); + }; + const selectPlaybackMapPoint = (point: VehicleMapPoint) => { + const index = playbackRows.findIndex((row, rowIndex) => playbackPointId(row, rowIndex) === point.id); + if (index >= 0) { + selectPlaybackPoint(index); + } + }; const openPlaybackVehicle = () => { if (!currentPlayback || !canOpenVehicle(currentPlayback.vin)) return; onOpenVehicle(currentPlayback.vin, currentPlayback.protocol); @@ -648,6 +664,8 @@ export function History({ @@ -663,7 +681,7 @@ export function History({
{item.value}
))} -
+
当前回放点
{currentPlayback ? ( <> @@ -703,6 +721,16 @@ export function History({ + selectPlaybackPoint(Number(event.currentTarget.value))} + /> ) : ( 暂无可回放位置点 @@ -712,7 +740,13 @@ export function History({
{(validLocations.length > 0 ? validLocations : locationItems).slice(0, 8).map((row, index) => ( -
+
+ ))} {locationItems.length === 0 ? ( 当前筛选范围暂无轨迹点,请调整车辆、来源或时间范围。 diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 0b2eead5..e2495d4a 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -687,11 +687,33 @@ body { } .vp-playback-step { + text-align: left; min-height: 112px; padding: 12px; border: 1px solid var(--vp-border); border-radius: var(--vp-radius); background: #fbfcff; + color: inherit; + cursor: pointer; + transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease; +} + +.vp-playback-step:hover, +.vp-playback-step:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + box-shadow: 0 0 0 3px rgba(22, 100, 255, 0.08); + outline: none; +} + +.vp-playback-step-active { + border-color: rgba(22, 100, 255, 0.72); + background: #eef5ff; + box-shadow: inset 3px 0 0 #1664ff; +} + +.vp-playback-range { + width: 100%; + accent-color: #1664ff; } .vp-json { 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 6d908443..3909e019 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -5843,6 +5843,15 @@ test('controls trajectory playback current point from history locations', async 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: '选择轨迹点 3' })); + expect(screen.getByText('点 3 / 3')).toBeInTheDocument(); + fireEvent.change(screen.getByRole('slider', { name: '轨迹回放进度' }), { target: { value: '0' } }); + expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); + const mapPointButtons = screen.getAllByRole('button', { name: '选择地图车辆 粤A回放1' }); + fireEvent.click(mapPointButtons[0]); + expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); + fireEvent.click(mapPointButtons[1]); + expect(screen.getByText('点 2 / 3')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '上一点' })); expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '回放点车辆服务' }));