feat(web): refine track replay controls
This commit is contained in:
@@ -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(view.container.querySelector('.v2-track-dock-summary')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-track-progress-meta')).toHaveTextContent('已暂停 · 数据点 1 / 3');
|
||||
expect(screen.getByRole('button', { name: '开始轨迹播放' })).toHaveTextContent('播放');
|
||||
expect(screen.queryByRole('button', { name: '暂停轨迹播放' })).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(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.click(screen.getByRole('button', { name: '开始轨迹播放' }));
|
||||
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');
|
||||
fireEvent.click(screen.getByRole('button', { name: '暂停轨迹播放' }));
|
||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '180');
|
||||
|
||||
@@ -545,11 +545,13 @@ export default function TrackPage() {
|
||||
<div className="v2-track-dock-progress">
|
||||
{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} 个数据点`} />
|
||||
<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 className="v2-track-dock-controls">
|
||||
<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} />
|
||||
<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} />
|
||||
|
||||
@@ -28053,3 +28053,211 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user