feat(platform): link trajectory playback selection
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: '回放点车辆服务' }));
|
||||
|
||||
Reference in New Issue
Block a user