feat(platform): link trajectory playback selection

This commit is contained in:
lingniu
2026-07-04 18:04:45 +08:00
parent 89e954cad9
commit 483d92719b
3 changed files with 69 additions and 4 deletions

View File

@@ -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({
<VehicleMap
points={playbackPoints}
mode="track"
selectedId={currentPlaybackPointId}
onPointSelect={selectPlaybackMapPoint}
fallbackLabel="高德地图未配置,显示轨迹坐标预览"
/>
</div>
@@ -663,7 +681,7 @@ export function History({
<div className="vp-monitor-metric-value">{item.value}</div>
</div>
))}
<div className="vp-playback-current">
<div className="vp-playback-current">
<div className="vp-map-service-queue-title"></div>
{currentPlayback ? (
<>
@@ -703,6 +721,16 @@ export function History({
<Button size="small" disabled={currentPlaybackIndex >= playbackRows.length - 1} onClick={() => movePlayback(1)}></Button>
<Button size="small" disabled={!canOpenVehicle(currentPlayback.vin)} onClick={openPlaybackVehicle}></Button>
</Space>
<input
className="vp-playback-range"
aria-label="轨迹回放进度"
type="range"
min={0}
max={Math.max(playbackRows.length - 1, 0)}
step={1}
value={currentPlaybackIndex}
onChange={(event) => selectPlaybackPoint(Number(event.currentTarget.value))}
/>
</>
) : (
<Typography.Text type="tertiary"></Typography.Text>
@@ -712,7 +740,13 @@ export function History({
</div>
<div className="vp-playback-timeline">
{(validLocations.length > 0 ? validLocations : locationItems).slice(0, 8).map((row, index) => (
<div key={`${row.deviceTime}-${index}`} className="vp-playback-step">
<button
key={`${row.deviceTime}-${index}`}
type="button"
className={`vp-playback-step ${index === currentPlaybackIndex ? 'vp-playback-step-active' : ''}`}
aria-label={`选择轨迹点 ${index + 1}`}
onClick={() => selectPlaybackPoint(index)}
>
<Tag color={index === 0 ? 'green' : index === Math.min((validLocations.length || locationItems.length), 8) - 1 ? 'orange' : 'blue'}>
{index === 0 ? '起点' : index === Math.min((validLocations.length || locationItems.length), 8) - 1 ? '终点' : `${index + 1}`}
</Tag>
@@ -720,7 +754,7 @@ export function History({
<Typography.Text type="secondary">
{formatNumber(row.speedKmh, ' km/h')} / {formatNumber(row.totalMileageKm, ' km')}
</Typography.Text>
</div>
</button>
))}
{locationItems.length === 0 ? (
<Typography.Text type="secondary"></Typography.Text>

View File

@@ -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 {

View File

@@ -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: '回放点车辆服务' }));