feat(web): refine track replay controls

This commit is contained in:
lingniu
2026-07-20 06:19:49 +08:00
parent a8bbbbc5c4
commit 8e54e08dba
3 changed files with 214 additions and 2 deletions

View File

@@ -212,6 +212,7 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
expect(screen.getByRole('slider', { name: '轨迹播放进度' })).toBeInTheDocument(); expect(screen.getByRole('slider', { name: '轨迹播放进度' })).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-dock-summary')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-track-dock-summary')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-track-progress-meta')).toHaveTextContent('已暂停 · 数据点 1 / 3'); expect(view.container.querySelector('.v2-track-progress-meta')).toHaveTextContent('已暂停 · 数据点 1 / 3');
expect(screen.getByRole('button', { name: '开始轨迹播放' })).toHaveTextContent('播放');
expect(screen.queryByRole('button', { name: '暂停轨迹播放' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: '暂停轨迹播放' })).not.toBeInTheDocument();
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1)); await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1));
expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1); expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1);
@@ -239,6 +240,7 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
fireEvent.change(screen.getByRole('combobox', { name: '速度' }), { target: { value: '4' } }); fireEvent.change(screen.getByRole('combobox', { name: '速度' }), { target: { value: '4' } });
fireEvent.click(screen.getByRole('button', { name: '开始轨迹播放' })); fireEvent.click(screen.getByRole('button', { name: '开始轨迹播放' }));
expect(view.container.querySelector('.v2-track-progress-meta')).toHaveTextContent(/播放中 · 数据点 [12] \/ 3/); expect(view.container.querySelector('.v2-track-progress-meta')).toHaveTextContent(/播放中 · 数据点 [12] \/ 3/);
expect(screen.getByRole('button', { name: '暂停轨迹播放' })).toHaveTextContent('暂停');
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '65'); expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '65');
fireEvent.click(screen.getByRole('button', { name: '暂停轨迹播放' })); fireEvent.click(screen.getByRole('button', { name: '暂停轨迹播放' }));
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '180'); expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '180');

View File

@@ -545,11 +545,13 @@ export default function TrackPage() {
<div className="v2-track-dock-progress"> <div className="v2-track-dock-progress">
{track ? <SegmentRail track={track} /> : <div className="v2-track-segment-placeholder" />} {track ? <SegmentRail track={track} /> : <div className="v2-track-segment-placeholder" />}
<Slider key={`track-progress-${points.length}`} className="v2-track-progress-slider" aria-label="轨迹播放进度" min={0} max={Math.max(0, points.length - 1)} step={1} value={boundedIndex} onChange={(value) => selectIndex(Number(value))} disabled={!points.length} showBoundary={false} tipFormatter={(value) => `${Number(value) + 1} / ${points.length} 个数据点`} /> <Slider key={`track-progress-${points.length}`} className="v2-track-progress-slider" aria-label="轨迹播放进度" min={0} max={Math.max(0, points.length - 1)} step={1} value={boundedIndex} onChange={(value) => selectIndex(Number(value))} disabled={!points.length} showBoundary={false} tipFormatter={(value) => `${Number(value) + 1} / ${points.length} 个数据点`} />
<div className="v2-track-progress-meta"><time>{time(current?.deviceTime)}</time><span>{playing ? '播放中' : '已暂停'} · {points.length ? boundedIndex + 1 : 0} / {points.length}</span><time>{time(track?.summary.endTime)}</time></div> <div className="v2-track-progress-meta"><time>{time(current?.deviceTime)}</time><span className="v2-track-progress-status">{playing ? '播放中' : '已暂停'} · {points.length ? boundedIndex + 1 : 0} / {points.length}</span><time>{time(track?.summary.endTime)}</time></div>
</div> </div>
<div className="v2-track-dock-controls"> <div className="v2-track-dock-controls">
<Button theme="borderless" aria-label="上一个轨迹点" icon={<IconChevronLeft />} onClick={() => selectIndex(boundedIndex - 1)} disabled={!boundedIndex} /> <Button theme="borderless" aria-label="上一个轨迹点" icon={<IconChevronLeft />} onClick={() => selectIndex(boundedIndex - 1)} disabled={!boundedIndex} />
<Button theme="solid" className="is-primary" aria-label={playing ? '暂停轨迹播放' : '开始轨迹播放'} icon={playing ? <IconPause /> : <IconPlay />} onClick={togglePlayback} disabled={points.length < 2} /> <Button theme="solid" className="is-primary" aria-label={playing ? '暂停轨迹播放' : '开始轨迹播放'} icon={playing ? <IconPause /> : <IconPlay />} onClick={togglePlayback} disabled={points.length < 2}>
<span className="v2-track-playback-primary-label">{playing ? '暂停' : '播放'}</span>
</Button>
<Button theme="borderless" aria-label="下一个轨迹点" icon={<IconChevronRight />} onClick={() => selectIndex(boundedIndex + 1)} disabled={!points.length || boundedIndex >= points.length - 1} /> <Button theme="borderless" aria-label="下一个轨迹点" icon={<IconChevronRight />} onClick={() => selectIndex(boundedIndex + 1)} disabled={!points.length || boundedIndex >= points.length - 1} />
<label><span id="track-playback-speed-label"></span><Select aria-labelledby="track-playback-speed-label" value={playbackSpeed} onChange={(value) => setPlaybackSpeed(Number(value) as PlaybackSpeed)} optionList={speedOptions.map((speed) => ({ value: speed, label: `${speed}×` }))} /></label> <label><span id="track-playback-speed-label"></span><Select aria-labelledby="track-playback-speed-label" value={playbackSpeed} onChange={(value) => setPlaybackSpeed(Number(value) as PlaybackSpeed)} optionList={speedOptions.map((speed) => ({ value: speed, label: `${speed}×` }))} /></label>
<Button theme="borderless" aria-label="回到起点" icon={<IconRefresh />} onClick={() => selectIndex(0)} disabled={!boundedIndex} /> <Button theme="borderless" aria-label="回到起点" icon={<IconRefresh />} onClick={() => selectIndex(0)} disabled={!boundedIndex} />

View File

@@ -28053,3 +28053,211 @@
min-height: 320px; min-height: 320px;
} }
} }
/*
* Semi UI trajectory evidence polish.
* Preserve the dense map-first workspace while raising the active vehicle,
* playback state and evidence labels above the microcopy layer. The transport
* control now has one explicit primary action instead of relying on an icon.
*/
.v2-track-query-header.has-identity .v2-workspace-command-eyebrow {
font-size: 9px;
line-height: 11px;
}
.v2-track-query-header.has-identity .v2-workspace-command-title {
font-size: 16px;
line-height: 20px;
}
.v2-track-query-header.has-identity .v2-workspace-command-description {
font-size: 10px;
line-height: 13px;
}
.v2-track-result-metric-rail .v2-workspace-metric-list > span > small {
font-size: 9px;
line-height: 13px;
}
.v2-track-result-metric-rail .v2-workspace-metric-list > span > strong,
.v2-track-result-metric-rail .v2-workspace-metric-list > span.is-secondary > strong {
font-weight: 760;
}
.v2-track-list-action.semi-button {
min-height: 56px;
border-radius: 9px;
}
.v2-track-list-action .semi-button-content > span > strong {
font-size: 12px;
line-height: 17px;
}
.v2-track-list-action .semi-button-content > span > small,
.v2-track-list-action .semi-button-content > em {
font-size: 9px;
line-height: 14px;
}
.v2-track-current-card .v2-workspace-panel-copy > h5.semi-typography {
font-size: 15px;
line-height: 20px;
}
.v2-track-current-card .v2-workspace-panel-copy > .semi-typography {
font-size: 10px;
line-height: 14px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics small {
font-size: 11px;
line-height: 15px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics strong {
font-weight: 740;
}
.v2-track-current-evidence,
.v2-track-current-evidence .semi-tag,
.v2-track-current-evidence strong {
font-size: 10px;
}
.v2-track-progress-meta {
min-height: 18px;
}
.v2-track-progress-meta > time {
color: #718197;
font-size: 10px;
font-variant-numeric: tabular-nums;
}
.v2-track-progress-status {
display: inline-flex;
min-height: 22px;
align-items: center;
border: 1px solid #e0e8f2;
border-radius: 999px;
background: #f7faff;
padding: 2px 9px;
color: #455d78;
font-size: 10px;
font-weight: 700;
line-height: 16px;
}
.v2-track-dock-controls > .semi-button.is-primary {
width: auto;
min-width: 76px;
gap: 5px;
padding-inline: 13px;
}
.v2-track-playback-primary-label {
font-size: 11px;
font-weight: 750;
line-height: 16px;
}
@media (max-width: 700px) {
.v2-track-detail-sidesheet .v2-workspace-config-title strong {
font-size: 16px;
line-height: 21px;
}
.v2-track-detail-sidesheet .v2-workspace-config-title small {
font-size: 10px;
line-height: 14px;
}
.v2-track-detail-sidesheet .v2-workspace-config-summary > span {
min-height: 68px;
padding: 8px 9px;
}
.v2-track-detail-sidesheet .v2-workspace-config-summary small {
font-size: 9px;
line-height: 13px;
}
.v2-track-detail-sidesheet .v2-workspace-config-summary strong {
font-size: 14px;
line-height: 18px;
}
.v2-track-detail-sidesheet .v2-workspace-config-summary em {
font-size: 8px;
line-height: 11px;
}
.v2-track-detail-sidesheet .v2-track-rail-tabs > .semi-button {
font-size: 12px;
font-weight: 680;
}
.v2-track-detail-sidesheet .v2-track-list-action.semi-button {
min-height: 62px;
padding: 8px 10px;
}
.v2-track-detail-sidesheet .v2-track-list-action .semi-button-content > span > strong {
font-size: 13px;
line-height: 18px;
}
.v2-track-detail-sidesheet .v2-track-list-action .semi-button-content > span > small,
.v2-track-detail-sidesheet .v2-track-list-action .semi-button-content > em {
font-size: 10px;
line-height: 15px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics small {
font-size: 9px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics strong {
font-size: 14px;
}
.v2-track-playback-dock > .semi-card-body {
grid-template-columns: minmax(0, 1fr) auto;
}
.v2-track-progress-status {
max-width: 142px;
min-height: 20px;
overflow: hidden;
padding-inline: 7px;
font-size: 9px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-track-dock-controls > .semi-button.is-primary {
min-width: 72px;
height: 44px;
padding-inline: 11px;
}
.v2-track-playback-primary-label {
font-size: 11px;
}
}
@media (max-width: 460px) {
.v2-track-dock-controls > .semi-button[aria-label="回到起点"] {
display: none;
}
.v2-track-dock-controls {
gap: 3px;
}
.v2-track-dock-controls > label {
min-width: 70px;
}
}