feat(platform): add trajectory autoplay controls

This commit is contained in:
lingniu
2026-07-04 16:40:10 +08:00
parent 318beccaaf
commit bdb2ebc723
4 changed files with 106 additions and 5 deletions

View File

@@ -247,6 +247,8 @@ export function History({
const [locationPagination, setLocationPagination] = useState({ currentPage: 1, pageSize: 10 });
const [rawPagination, setRawPagination] = useState({ currentPage: 1, pageSize: 10 });
const [playbackIndex, setPlaybackIndex] = useState(0);
const [playbackPlaying, setPlaybackPlaying] = useState(false);
const [playbackSpeedMs, setPlaybackSpeedMs] = useState(1200);
const buildParams = (nextFilters: HistoryFilters, limit: number, offset: number, raw: boolean) => {
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
@@ -278,6 +280,7 @@ export function History({
setLocations(nextPage);
setLocationPagination({ currentPage: page, pageSize });
setPlaybackIndex(0);
setPlaybackPlaying(false);
})
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoadingLocations(false));
@@ -361,6 +364,7 @@ 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 playbackAtEnd = playbackRows.length > 0 && currentPlaybackIndex >= playbackRows.length - 1;
const playbackPoints: VehicleMapPoint[] = validLocations.map((row, index) => ({
id: `${row.vin}-${row.deviceTime || row.serverTime || index}`,
label: row.plate || row.vin || `${index + 1}`,
@@ -418,8 +422,33 @@ export function History({
}
window.open(url, '_blank', 'noopener,noreferrer');
};
useEffect(() => {
if (!playbackPlaying || playbackRows.length <= 1) return undefined;
const timer = window.setInterval(() => {
setPlaybackIndex((current) => {
if (current >= playbackRows.length - 1) {
setPlaybackPlaying(false);
return current;
}
const next = current + 1;
if (next >= playbackRows.length - 1) setPlaybackPlaying(false);
return next;
});
}, playbackSpeedMs);
return () => window.clearInterval(timer);
}, [playbackPlaying, playbackRows.length, playbackSpeedMs]);
const togglePlayback = () => {
if (playbackPlaying) {
setPlaybackPlaying(false);
return;
}
if (playbackRows.length <= 1) return;
if (playbackAtEnd) setPlaybackIndex(0);
setPlaybackPlaying(true);
};
const movePlayback = (delta: number) => {
if (playbackRows.length === 0) return;
setPlaybackPlaying(false);
setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1));
};
const openPlaybackVehicle = () => {
@@ -560,6 +589,27 @@ export function History({
<Tag color="green">{formatNumber(currentPlayback.speedKmh, ' km/h')}</Tag>
<Tag color="blue">{formatNumber(currentPlayback.totalMileageKm, ' km')}</Tag>
</Space>
<Space wrap>
<Button
size="small"
aria-label={playbackPlaying ? '暂停轨迹' : '播放轨迹'}
disabled={playbackRows.length <= 1}
onClick={togglePlayback}
>
{playbackPlaying ? '暂停' : playbackAtEnd ? '重播' : '播放'}
</Button>
<Typography.Text type="secondary" size="small"></Typography.Text>
<Select
size="small"
value={String(playbackSpeedMs)}
style={{ width: 108 }}
onChange={(value) => setPlaybackSpeedMs(Number(value))}
>
<Select.Option value="1800"></Select.Option>
<Select.Option value="1200"></Select.Option>
<Select.Option value="600"></Select.Option>
</Select>
</Space>
<Space wrap>
<Button size="small" disabled={currentPlaybackIndex <= 0} onClick={() => movePlayback(-1)}></Button>
<Button size="small" disabled={currentPlaybackIndex >= playbackRows.length - 1} onClick={() => movePlayback(1)}></Button>

View File

@@ -5683,6 +5683,57 @@ test('controls trajectory playback current point from history locations', async
});
});
test('auto plays trajectory playback points with selectable speed', async () => {
window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-AUTO&protocol=JT808');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/history/locations')) {
return {
ok: true,
json: async () => ({
data: {
items: [
{ vin: 'VIN-TRACK-AUTO', 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-AUTO', 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-AUTO', 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;
}
return {
ok: true,
json: async () => ({
data: { items: [], total: 0, limit: 10, offset: 0 },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
});
render(<App />);
expect(await screen.findByText('播放速度')).toBeInTheDocument();
vi.useFakeTimers();
expect(screen.getByText('点 1 / 3')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '播放轨迹' }));
expect(screen.getByRole('button', { name: '暂停轨迹' })).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(1200);
expect(screen.getByText('点 2 / 3')).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(2400);
expect(screen.getByText('点 3 / 3')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '播放轨迹' })).toBeInTheDocument();
vi.useRealTimers();
});
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');

View File

@@ -6,7 +6,7 @@ The UI is a professional vehicle data operations console. It uses Semi UI as the
## Product North Star
The platform is a vehicle service center. GB32960, JT808, and Yutong MQTT are ingestion evidence for the same vehicle, not three separate product surfaces. A user should be able to search one VIN, plate, or phone number and understand whether that vehicle is online, where it is, what its latest state is, how reliable its mileage is, and which source produced each piece of evidence.
The platform is a vehicle service center. GB32960, JT808, and Yutong MQTT are ingestion evidence for the same vehicle, not three separate product surfaces. These three data sources ultimately exist to support one vehicle service. A user should be able to search one VIN, plate, or phone number and understand whether that vehicle is online, where it is, what its latest state is, how reliable its mileage is, and which source produced each piece of evidence.
Every primary workflow should answer a vehicle-level question first, then allow protocol-level drill-down only when diagnosis or raw evidence is needed.
@@ -18,7 +18,7 @@ The platform navigation should use vehicle-service language rather than protocol
2. Live Monitor: map-first current vehicle monitoring with online/offline state, abnormal highlights, source freshness, and a dense vehicle table.
3. Vehicle Center: table-first vehicle registry, binding state, source coverage, service status, and missing-source governance.
4. Vehicle Detail: one vehicle service file with identity, latest realtime state, source evidence, history, raw, mileage, alerts, quality evidence, and copyable diagnostics.
5. Trajectory Replay: historical location playback with AMap polyline, playback marker, controls, and table evidence.
5. Trajectory Replay: historical location playback with AMap polyline, playback marker, auto play/pause, speed controls, and table evidence.
6. History Query: historical locations, raw frames, and flattened parsed fields with pagination and field trimming.
7. Alert Events: alert trigger records, affected vehicles, rule evidence, manual state, notification state, and escalation readiness.
8. Notification Rules: alert trigger rules, notification targets, channels, escalation windows, acceptance criteria, and copyable runbooks.
@@ -64,7 +64,7 @@ The product uses AMap as the map provider for production map rendering.
- API/server key is kept in backend or ECS environment configuration.
- The AMap security code must stay server-side in production. The browser should receive `AMAP_SECURITY_SERVICE_HOST`, not the raw security code.
- Realtime monitor shows current vehicle points, clustering or bounded point rendering, online/offline color, abnormal highlights, selected vehicle highlight, and source freshness.
- Trajectory playback shows historical polyline, start/end markers, current playback marker, playback controls, and the table row that backs the selected point.
- Trajectory playback shows historical polyline, start/end markers, current playback marker, auto play/pause, speed controls, step controls, and the table row that backs the selected point.
- Alert events can highlight affected vehicle locations when the alert has valid coordinates.
- If AMap is unavailable or not configured, pages must fall back to coordinate preview instead of blocking the workflow.

View File

@@ -2,7 +2,7 @@
## Goal
Build one vehicle data management platform around a single vehicle service. GB32960, JT808, and Yutong MQTT are source evidence for the same vehicle, not separate applications. The platform should help operations users answer six questions quickly:
Build one vehicle data management platform around a single vehicle service. GB32960, JT808, and Yutong MQTT are source evidence for the same vehicle, not separate applications. The three sources are only acquisition paths; the product outcome is one searchable, monitorable, and diagnosable vehicle service. The platform should help operations users answer six questions quickly:
1. Is this vehicle online now?
2. Where is it and what is its latest state?
@@ -125,7 +125,7 @@ Purpose: historical route verification and operations playback.
Primary modules:
- AMap route panel: polyline, start/end markers, playback marker, optional speed color ramp.
- Playback controls: play, pause, speed, step, seek.
- Playback controls: play, pause, speed, step, seek, and auto-stop at the last point.
- Evidence table: time, longitude, latitude, speed, mileage, protocol, raw frame link.
- Quality strip: missing points, time gaps, mileage jumps.