refine Semi UI track replay workspace
This commit is contained in:
@@ -172,11 +172,15 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
|
|||||||
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||||
|
|
||||||
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
||||||
expect(view.container.querySelector('.v2-track-query-card.semi-card')).toHaveClass('is-collapsed');
|
expect(view.container.querySelector('.v2-track-rail-shell.semi-card')).toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-track-rail-result.semi-card')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-track-query-panel')).toHaveClass('is-collapsed');
|
||||||
|
expect(view.container.querySelector('.v2-track-rail-result')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');
|
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');
|
||||||
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-track-current-card.semi-card')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-track-current-card.semi-card')).toBeInTheDocument();
|
||||||
|
expect(view.container.querySelector('.v2-track-current-coverage')).toBeInTheDocument();
|
||||||
|
expect(view.container.querySelector('.v2-track-coverage-float')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('toolbar', { name: '地图轨迹工具' })).toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-track-playback-dock.semi-card')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-track-playback-dock.semi-card')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '0');
|
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '0');
|
||||||
expect(screen.getByText('停留 00:05:00 · 1 个点')).toBeInTheDocument();
|
expect(screen.getByText('停留 00:05:00 · 1 个点')).toBeInTheDocument();
|
||||||
@@ -198,7 +202,8 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
|
|||||||
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1));
|
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1));
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('tab', { name: '概览' }));
|
fireEvent.click(screen.getByRole('tab', { name: '概览' }));
|
||||||
expect(view.container.querySelectorAll('.v2-track-overview-card.semi-card')).toHaveLength(3);
|
expect(view.container.querySelectorAll('.v2-track-overview-section')).toHaveLength(3);
|
||||||
|
expect(view.container.querySelectorAll('.v2-track-overview-section.semi-card')).toHaveLength(0);
|
||||||
expect(view.container.querySelector('.v2-track-overview-descriptions.semi-descriptions')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-track-overview-descriptions.semi-descriptions')).toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-track-source-list.semi-list')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-track-source-list.semi-list')).toBeInTheDocument();
|
||||||
expect(view.container.querySelectorAll('.v2-track-source-item.semi-list-item')).toHaveLength(1);
|
expect(view.container.querySelectorAll('.v2-track-source-item.semi-list-item')).toHaveLength(1);
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function VehiclePicker({ value, onChange, onSelect }: { value: string; onChange:
|
|||||||
|
|
||||||
function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
|
function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
|
||||||
return <div className="v2-track-overview-panel">
|
return <div className="v2-track-overview-panel">
|
||||||
<Card className="v2-track-overview-card" bodyStyle={{ padding: 0 }}>
|
<section className="v2-track-overview-section">
|
||||||
<WorkspacePanelHeader variant="compact" title="行程概览" meta={<Tag color={track.sampled ? 'orange' : 'green'} type="light" size="small">{track.sampled ? '地图已抽稀' : '完整点集'}</Tag>} />
|
<WorkspacePanelHeader variant="compact" title="行程概览" meta={<Tag color={track.sampled ? 'orange' : 'green'} type="light" size="small">{track.sampled ? '地图已抽稀' : '完整点集'}</Tag>} />
|
||||||
<div className="v2-track-overview-card-body"><Descriptions className="v2-track-overview-descriptions" align="left" size="small" data={[
|
<div className="v2-track-overview-card-body"><Descriptions className="v2-track-overview-descriptions" align="left" size="small" data={[
|
||||||
{ key: '开始时间', value: dateTime(track.summary.startTime) },
|
{ key: '开始时间', value: dateTime(track.summary.startTime) },
|
||||||
@@ -163,15 +163,15 @@ function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
|
|||||||
{ key: '平均 / 最高速度', value: `${number(track.summary.averageSpeedKmh)} / ${number(track.summary.maximumSpeedKmh)} km/h` },
|
{ key: '平均 / 最高速度', value: `${number(track.summary.averageSpeedKmh)} / ${number(track.summary.maximumSpeedKmh)} km/h` },
|
||||||
{ key: '停车 / 分段', value: `${track.summary.stopCount} / ${track.summary.segmentCount}` }
|
{ key: '停车 / 分段', value: `${track.summary.stopCount} / ${track.summary.segmentCount}` }
|
||||||
]} /></div>
|
]} /></div>
|
||||||
</Card>
|
</section>
|
||||||
<Card className="v2-track-overview-card v2-track-source-card" bodyStyle={{ padding: 0 }}>
|
<section className="v2-track-overview-section v2-track-source-card">
|
||||||
<WorkspacePanelHeader variant="compact" title="数据来源" meta={<Tag color="blue" type="light" size="small">{track.coverage.totalPoints.toLocaleString('zh-CN')} 个源点</Tag>} />
|
<WorkspacePanelHeader variant="compact" title="数据来源" meta={<Tag color="blue" type="light" size="small">{track.coverage.totalPoints.toLocaleString('zh-CN')} 个源点</Tag>} />
|
||||||
<div className="v2-track-overview-card-body"><List className="v2-track-source-list">{track.sources.map((source) => <List.Item className="v2-track-source-item" key={source.protocol}><Tag color="blue" type="light" size="small">{source.protocol}</Tag><strong>{source.pointCount.toLocaleString('zh-CN')} 点</strong><small>{time(source.startTime)}–{time(source.endTime)}</small></List.Item>)}</List></div>
|
<div className="v2-track-overview-card-body"><List className="v2-track-source-list">{track.sources.map((source) => <List.Item className="v2-track-source-item" key={source.protocol}><Tag color="blue" type="light" size="small">{source.protocol}</Tag><strong>{source.pointCount.toLocaleString('zh-CN')} 点</strong><small>{time(source.startTime)}–{time(source.endTime)}</small></List.Item>)}</List></div>
|
||||||
</Card>
|
</section>
|
||||||
<Card className={`v2-track-overview-card v2-track-quality-card is-${track.quality.status}`} bodyStyle={{ padding: 0 }}>
|
<section className={`v2-track-overview-section v2-track-quality-card is-${track.quality.status}`}>
|
||||||
<WorkspacePanelHeader variant="compact" title="轨迹质量" meta={<Tag color={track.quality.status === 'good' ? 'green' : 'orange'} type="light" size="small">{track.quality.status === 'good' ? '通过' : '需关注'}</Tag>} />
|
<WorkspacePanelHeader variant="compact" title="轨迹质量" meta={<Tag color={track.quality.status === 'good' ? 'green' : 'orange'} type="light" size="small">{track.quality.status === 'good' ? '通过' : '需关注'}</Tag>} />
|
||||||
<div className="v2-track-overview-card-body"><p>{track.quality.evidence}</p><small>无效 {track.quality.invalidCoordinatePoints} · 重复 {track.quality.duplicatePoints} · 漂移 {track.quality.driftPoints} · 大间隔 {track.quality.largeGapCount}</small></div>
|
<div className="v2-track-overview-card-body"><p>{track.quality.evidence}</p><small>无效 {track.quality.invalidCoordinatePoints} · 重复 {track.quality.duplicatePoints} · 漂移 {track.quality.driftPoints} · 大间隔 {track.quality.largeGapCount}</small></div>
|
||||||
</Card>
|
</section>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,11 +193,12 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
|
|||||||
const activePreset = trackPreset(draft);
|
const activePreset = trackPreset(draft);
|
||||||
const choosePreset = (daysBack: number) => onDraft({ ...draft, ...trackWindow(daysBack, daysBack > 0) });
|
const choosePreset = (daysBack: number) => onDraft({ ...draft, ...trackWindow(daysBack, daysBack > 0) });
|
||||||
return <aside className="v2-track-rail">
|
return <aside className="v2-track-rail">
|
||||||
<Card className={`v2-track-query-card${queryCollapsed ? ' is-collapsed' : ''}`} bodyStyle={{ padding: 0 }}>
|
<Card className="v2-track-rail-shell" bodyStyle={{ padding: 0 }}>
|
||||||
|
<section className={`v2-track-query-panel${queryCollapsed ? ' is-collapsed' : ''}`}>
|
||||||
<WorkspacePanelHeader
|
<WorkspacePanelHeader
|
||||||
className="v2-track-query-header"
|
className="v2-track-query-header"
|
||||||
title="轨迹查询"
|
title="轨迹查询"
|
||||||
description={queryCollapsed ? '查询条件已应用' : '最长支持连续 7 天'}
|
description={queryCollapsed ? '条件已应用,可随时修改' : '最长支持连续 7 天'}
|
||||||
actions={<>
|
actions={<>
|
||||||
<Button
|
<Button
|
||||||
className="v2-track-query-toggle"
|
className="v2-track-query-toggle"
|
||||||
@@ -226,14 +227,14 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
|
|||||||
<label><span id="track-source-label">数据来源</span><Select aria-labelledby="track-source-label" value={draft.protocol} onChange={(value) => onDraft({ ...draft, protocol: String(value) })} optionList={[{ value: '', label: '自动选择最佳来源' }, { value: 'GB32960', label: 'GB32960 · 仪表盘里程' }, { value: 'JT808', label: 'JT808 · GPS 里程' }, { value: 'YUTONG_MQTT', label: 'YUTONG · 仪表盘里程' }]} /></label>
|
<label><span id="track-source-label">数据来源</span><Select aria-labelledby="track-source-label" value={draft.protocol} onChange={(value) => onDraft({ ...draft, protocol: String(value) })} optionList={[{ value: '', label: '自动选择最佳来源' }, { value: 'GB32960', label: 'GB32960 · 仪表盘里程' }, { value: 'JT808', label: 'JT808 · GPS 里程' }, { value: 'YUTONG_MQTT', label: 'YUTONG · 仪表盘里程' }]} /></label>
|
||||||
<Button className="v2-track-query-button" theme="solid" htmlType="submit" icon={<IconSearch />} loading={loading} disabled={!draft.keyword.trim()}>查询轨迹</Button>
|
<Button className="v2-track-query-button" theme="solid" htmlType="submit" icon={<IconSearch />} loading={loading} disabled={!draft.keyword.trim()}>查询轨迹</Button>
|
||||||
</form>}
|
</form>}
|
||||||
</Card>
|
</section>
|
||||||
|
|
||||||
<Card className="v2-track-rail-result" bodyStyle={{ padding: 0 }}>
|
<section className="v2-track-rail-result">
|
||||||
<WorkspacePanelHeader
|
<WorkspacePanelHeader
|
||||||
variant="compact"
|
variant="compact"
|
||||||
className="v2-track-result-header"
|
className="v2-track-result-header"
|
||||||
title={track ? track.plate || track.vin : '轨迹明细'}
|
title={track ? track.plate || track.vin : '轨迹证据'}
|
||||||
description={track ? track.vin : '停留、事件与行程证据'}
|
description={track ? track.vin : '停留、事件与来源统一呈现'}
|
||||||
meta={<Tag color={track ? 'blue' : 'grey'} type="light" size="small">{track ? `${number(track.summary.distanceKm)} km` : '等待查询'}</Tag>}
|
meta={<Tag color={track ? 'blue' : 'grey'} type="light" size="small">{track ? `${number(track.summary.distanceKm)} km` : '等待查询'}</Tag>}
|
||||||
/>
|
/>
|
||||||
<SegmentedTabs className="v2-track-rail-tabs" ariaLabel="轨迹明细分类" value={tab} onChange={onTab} items={[{ key: 'stops', label: '停留点', count: track?.stops.length ?? 0 }, { key: 'events', label: '事件点', count: track?.events.length ?? 0 }, { key: 'overview', label: '概览' }]} />
|
<SegmentedTabs className="v2-track-rail-tabs" ariaLabel="轨迹明细分类" value={tab} onChange={onTab} items={[{ key: 'stops', label: '停留点', count: track?.stops.length ?? 0 }, { key: 'events', label: '事件点', count: track?.events.length ?? 0 }, { key: 'overview', label: '概览' }]} />
|
||||||
@@ -243,6 +244,7 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
|
|||||||
{track && tab === 'events' ? <List className="v2-track-evidence-list v2-track-event-list">{track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return <List.Item className="v2-track-evidence-item" key={`${event.type}-${event.time}-${index}`}><Button theme="borderless" type="tertiary" className={`v2-track-list-action${activeEventIndexes.includes(index) ? ' is-active' : ''}`} onClick={() => onSelectIndex(sampled)}><i className={`is-${eventTone(event.type)}`}>{index + 1}</i><span><strong>{event.title}</strong><small>{dateTime(event.time)}</small></span><em>{number(event.speedKmh, 0)} km/h</em></Button></List.Item>; })}{!track.events.length ? <Empty className="v2-track-list-empty" image={<IconMapPin />} title="当前没有事件点" description="时间窗内没有识别到启停、急加速或异常间隔。" /> : null}</List> : null}
|
{track && tab === 'events' ? <List className="v2-track-evidence-list v2-track-event-list">{track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return <List.Item className="v2-track-evidence-item" key={`${event.type}-${event.time}-${index}`}><Button theme="borderless" type="tertiary" className={`v2-track-list-action${activeEventIndexes.includes(index) ? ' is-active' : ''}`} onClick={() => onSelectIndex(sampled)}><i className={`is-${eventTone(event.type)}`}>{index + 1}</i><span><strong>{event.title}</strong><small>{dateTime(event.time)}</small></span><em>{number(event.speedKmh, 0)} km/h</em></Button></List.Item>; })}{!track.events.length ? <Empty className="v2-track-list-empty" image={<IconMapPin />} title="当前没有事件点" description="时间窗内没有识别到启停、急加速或异常间隔。" /> : null}</List> : null}
|
||||||
{track && tab === 'overview' ? <OverviewPanel track={track} /> : null}
|
{track && tab === 'overview' ? <OverviewPanel track={track} /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</Card>
|
</Card>
|
||||||
</aside>;
|
</aside>;
|
||||||
});
|
});
|
||||||
@@ -280,7 +282,7 @@ export default function TrackPage() {
|
|||||||
const [follow, setFollow] = useState(true);
|
const [follow, setFollow] = useState(true);
|
||||||
const [showStops, setShowStops] = useState(true);
|
const [showStops, setShowStops] = useState(true);
|
||||||
const [panelTab, setPanelTab] = useState<PanelTab>('stops');
|
const [panelTab, setPanelTab] = useState<PanelTab>('stops');
|
||||||
const [railCollapsed, setRailCollapsed] = useState(() => typeof window !== 'undefined' && window.matchMedia('(max-width: 700px)').matches);
|
const [railCollapsed, setRailCollapsed] = useState(() => typeof window !== 'undefined' && window.matchMedia('(max-width: 960px)').matches);
|
||||||
const [queryCollapsed, setQueryCollapsed] = useState(() => Boolean(criteria.keyword));
|
const [queryCollapsed, setQueryCollapsed] = useState(() => Boolean(criteria.keyword));
|
||||||
const animationRef = useRef<number>();
|
const animationRef = useRef<number>();
|
||||||
const lastFrameRef = useRef(0);
|
const lastFrameRef = useRef(0);
|
||||||
@@ -321,7 +323,7 @@ export default function TrackPage() {
|
|||||||
});
|
});
|
||||||
const addressSettling = Boolean(!playing && currentAddressPoint && currentAddressPoint.key !== addressPoint?.key);
|
const addressSettling = Boolean(!playing && currentAddressPoint && currentAddressPoint.key !== addressPoint?.key);
|
||||||
|
|
||||||
useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); setAddressPoint(undefined); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 700px)').matches); }, [track?.asOf]);
|
useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); setAddressPoint(undefined); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 960px)').matches); }, [track?.asOf]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!playing || points.length < 2) return;
|
if (!playing || points.length < 2) return;
|
||||||
lastFrameRef.current = 0;
|
lastFrameRef.current = 0;
|
||||||
@@ -382,20 +384,21 @@ export default function TrackPage() {
|
|||||||
<TrackMap points={points} stops={track?.stops ?? []} activeIndex={boundedIndex} showStops={showStops} follow={follow} followDurationMs={playing ? Math.max(40, trackPlaybackInterval(playbackSpeed) - 10) : 180} onSelectIndex={selectIndex} onFollowChange={setFollow} />
|
<TrackMap points={points} stops={track?.stops ?? []} activeIndex={boundedIndex} showStops={showStops} follow={follow} followDurationMs={playing ? Math.max(40, trackPlaybackInterval(playbackSpeed) - 10) : 180} onSelectIndex={selectIndex} onFollowChange={setFollow} />
|
||||||
|
|
||||||
{railCollapsed ? <Button className="v2-track-rail-expand" theme="light" aria-label="展开查询与明细" icon={<IconList />} onClick={() => setRailCollapsed(false)}>查询与明细</Button> : null}
|
{railCollapsed ? <Button className="v2-track-rail-expand" theme="light" aria-label="展开查询与明细" icon={<IconList />} onClick={() => setRailCollapsed(false)}>查询与明细</Button> : null}
|
||||||
<div className="v2-track-stage-tools">
|
<div className="v2-track-stage-tools" role="toolbar" aria-label="地图轨迹工具">
|
||||||
<Button theme={follow ? 'solid' : 'light'} aria-label={follow ? '关闭车辆跟随' : '开启车辆跟随'} className={follow ? 'is-active' : ''} icon={<IconMapPin />} disabled={!points.length} onClick={() => setFollow((value) => !value)}>{follow ? '跟随车辆' : '自由浏览'}</Button>
|
<Button theme="borderless" aria-label={follow ? '关闭车辆跟随' : '开启车辆跟随'} className={follow ? 'is-active' : ''} icon={<IconMapPin />} disabled={!points.length} onClick={() => setFollow((value) => !value)}>{follow ? '跟随车辆' : '自由浏览'}</Button>
|
||||||
<Button theme={showStops ? 'solid' : 'light'} aria-label={showStops ? '隐藏停留点' : '显示停留点'} className={showStops ? 'is-active' : ''} icon={showStops ? <IconEyeOpened /> : <IconEyeClosed />} disabled={!track?.stops.length} onClick={() => setShowStops((value) => !value)}>停留点</Button>
|
<Button theme="borderless" aria-label={showStops ? '隐藏停留点' : '显示停留点'} className={showStops ? 'is-active' : ''} icon={showStops ? <IconEyeOpened /> : <IconEyeClosed />} disabled={!track?.stops.length} onClick={() => setShowStops((value) => !value)}>停留点</Button>
|
||||||
<Button theme="light" aria-label="导出轨迹 CSV" icon={<IconDownload />} disabled={!track?.points.length} onClick={() => track && downloadTrackCsv(track)}>导出</Button>
|
<Button theme="borderless" aria-label="导出轨迹 CSV" icon={<IconDownload />} disabled={!track?.points.length} onClick={() => track && downloadTrackCsv(track)}>导出</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{track?.points.length ? <>
|
{track?.points.length ? <>
|
||||||
<div className={`v2-track-coverage-float${track.coverage.complete ? '' : ' is-warning'}`}><i />
|
|
||||||
<span><strong>{track.coverage.complete ? '时间窗完整' : '仅展示最新切片'}</strong><small title={track.coverage.evidence}>{track.coverage.evidence}</small></span>
|
|
||||||
<em>{track.coverage.processedPoints.toLocaleString('zh-CN')} 有效点 → {track.coverage.returnedPoints.toLocaleString('zh-CN')} 地图点</em>
|
|
||||||
</div>
|
|
||||||
<Card className="v2-track-current-card" bodyStyle={{ padding: 0 }}>
|
<Card className="v2-track-current-card" bodyStyle={{ padding: 0 }}>
|
||||||
<WorkspacePanelHeader variant="compact" title={track.plate || track.vin} description={dateTime(current?.deviceTime)} meta={`${number(progress, 0)}%`} />
|
<WorkspacePanelHeader variant="compact" title={track.plate || track.vin} description={dateTime(current?.deviceTime)} meta={<Tag color="blue" type="light" size="small">{number(progress, 0)}%</Tag>} />
|
||||||
<div><span><small>速度</small><strong>{number(current?.speedKmh ?? 0)}<em> km/h</em></strong></span><span><small>方向</small><strong>{direction(current?.directionDeg)}</strong></span><span><small>SOC</small><strong>{current?.socAvailable ? `${number(current.socPercent)}%` : '—'}</strong></span></div>
|
<div className="v2-track-current-metrics"><span><small>速度</small><strong>{number(current?.speedKmh ?? 0)}<em> km/h</em></strong></span><span><small>方向</small><strong>{direction(current?.directionDeg)}</strong></span><span><small>SOC</small><strong>{current?.socAvailable ? `${number(current.socPercent)}%` : '—'}</strong></span></div>
|
||||||
|
<div className="v2-track-current-source"><Tag color="blue" type="light" size="small">{current?.protocol || '未知来源'}</Tag><span>总里程 {number(current?.totalMileageKm ?? 0)} km</span><span>{alarm(current?.alarmFlag)}</span></div>
|
||||||
|
<div className={`v2-track-current-coverage${track.coverage.complete ? '' : ' is-warning'}`}><i />
|
||||||
|
<span><strong>{track.coverage.complete ? '时间窗完整' : '仅展示最新切片'}</strong><small title={track.coverage.evidence}>{track.coverage.evidence}</small></span>
|
||||||
|
<em>{track.coverage.processedPoints.toLocaleString('zh-CN')} → {track.coverage.returnedPoints.toLocaleString('zh-CN')} 点</em>
|
||||||
|
</div>
|
||||||
<p title={addressSettling ? undefined : addressQuery.data?.formattedAddress}>{playing ? '播放中,暂停地址解析' : addressSettling ? '位置已变化,等待地址解析…' : addressQuery.isFetching ? '地址解析中…' : addressQuery.data?.formattedAddress || `${current?.longitude.toFixed(6)}, ${current?.latitude.toFixed(6)}`}</p>
|
<p title={addressSettling ? undefined : addressQuery.data?.formattedAddress}>{playing ? '播放中,暂停地址解析' : addressSettling ? '位置已变化,等待地址解析…' : addressQuery.isFetching ? '地址解析中…' : addressQuery.data?.formattedAddress || `${current?.longitude.toFixed(6)}, ${current?.latitude.toFixed(6)}`}</p>
|
||||||
</Card>
|
</Card>
|
||||||
</> : <Card className="v2-track-empty-state" bodyStyle={{ padding: 0 }}><Empty image={<IconMapPin />} title="先选择车辆,再开始轨迹回放" description="地图会显示完整路径、停留点、事件点和播放位置。"><Button theme="solid" icon={<IconSearch />} onClick={() => setRailCollapsed(false)}>选择车辆</Button></Empty></Card>}
|
</> : <Card className="v2-track-empty-state" bodyStyle={{ padding: 0 }}><Empty image={<IconMapPin />} title="先选择车辆,再开始轨迹回放" description="地图会显示完整路径、停留点、事件点和播放位置。"><Button theme="solid" icon={<IconSearch />} onClick={() => setRailCollapsed(false)}>选择车辆</Button></Empty></Card>}
|
||||||
@@ -417,7 +420,6 @@ export default function TrackPage() {
|
|||||||
<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} />
|
||||||
</div>
|
</div>
|
||||||
<div className="v2-track-dock-metrics"><span><small>总里程</small><strong>{number(current?.totalMileageKm ?? 0)} km</strong></span><span><small>数据来源</small><strong>{current?.protocol || '—'}</strong></span><span><small>车辆状态</small><strong>{alarm(current?.alarmFlag)}</strong></span></div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -287,12 +287,13 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.TrackPage).toContain('<Input');
|
expect(corePageSources.TrackPage).toContain('<Input');
|
||||||
expect(corePageSources.TrackPage).toContain('<Select');
|
expect(corePageSources.TrackPage).toContain('<Select');
|
||||||
expect(corePageSources.TrackPage).toContain('<Slider');
|
expect(corePageSources.TrackPage).toContain('<Slider');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className={`v2-track-query-card${queryCollapsed');
|
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-rail-shell"');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-rail-result"');
|
expect(corePageSources.TrackPage).toContain('<section className={`v2-track-query-panel${queryCollapsed');
|
||||||
|
expect(corePageSources.TrackPage).toContain('<section className="v2-track-rail-result">');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-current-card"');
|
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-current-card"');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-empty-state"');
|
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-empty-state"');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-playback-dock"');
|
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-playback-dock"');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-overview-card"');
|
expect(corePageSources.TrackPage).toContain('<section className="v2-track-overview-section">');
|
||||||
expect(corePageSources.TrackPage).toContain('<Descriptions className="v2-track-overview-descriptions"');
|
expect(corePageSources.TrackPage).toContain('<Descriptions className="v2-track-overview-descriptions"');
|
||||||
expect(corePageSources.TrackPage).toContain('<Empty className="v2-track-rail-empty"');
|
expect(corePageSources.TrackPage).toContain('<Empty className="v2-track-rail-empty"');
|
||||||
expect(corePageSources.TrackPage).toContain('<List className="v2-track-evidence-list v2-track-stop-list"');
|
expect(corePageSources.TrackPage).toContain('<List className="v2-track-evidence-list v2-track-stop-list"');
|
||||||
@@ -300,15 +301,16 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.TrackPage).toContain('<List.Item className="v2-track-evidence-item"');
|
expect(corePageSources.TrackPage).toContain('<List.Item className="v2-track-evidence-item"');
|
||||||
expect(corePageSources.TrackPage).toContain('<Empty image={<IconMapPin />} title="先选择车辆,再开始轨迹回放"');
|
expect(corePageSources.TrackPage).toContain('<Empty image={<IconMapPin />} title="先选择车辆,再开始轨迹回放"');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<Card className="v2-track-overview-card"><header><strong>行程概览</strong><Tag color={track.sampled ? \'orange\' : \'green\'} type="light" size="small">{track.sampled ? \'地图已抽稀\' : \'完整点集\'}</Tag></header><dl>');
|
expect(corePageSources.TrackPage).not.toContain('<Card className="v2-track-overview-card"><header><strong>行程概览</strong><Tag color={track.sampled ? \'orange\' : \'green\'} type="light" size="small">{track.sampled ? \'地图已抽稀\' : \'完整点集\'}</Tag></header><dl>');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-overview-card v2-track-source-card"');
|
expect(corePageSources.TrackPage).toContain('<section className="v2-track-overview-section v2-track-source-card">');
|
||||||
expect(corePageSources.TrackPage).toContain('<List className="v2-track-source-list"');
|
expect(corePageSources.TrackPage).toContain('<List className="v2-track-source-list"');
|
||||||
expect(corePageSources.TrackPage).toContain('<List.Item className="v2-track-source-item"');
|
expect(corePageSources.TrackPage).toContain('<List.Item className="v2-track-source-item"');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<Card className="v2-track-source-item"');
|
expect(corePageSources.TrackPage).not.toContain('<Card className="v2-track-source-item"');
|
||||||
expect(corePageSources.TrackPage).toContain('<Card className={`v2-track-overview-card v2-track-quality-card');
|
expect(corePageSources.TrackPage).toContain('<section className={`v2-track-overview-section v2-track-quality-card');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<section className={`v2-track-quality-card');
|
expect(corePageSources.TrackPage).not.toContain('<section className={`v2-track-quality-card');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<article key={source.protocol}');
|
expect(corePageSources.TrackPage).not.toContain('<article key={source.protocol}');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<article className="v2-track-current-card"');
|
expect(corePageSources.TrackPage).not.toContain('<article className="v2-track-current-card"');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<footer className="v2-track-playback-dock"');
|
expect(corePageSources.TrackPage).not.toContain('<footer className="v2-track-playback-dock"');
|
||||||
|
expect(corePageSources.TrackPage).not.toContain('v2-track-coverage-float');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<select');
|
expect(corePageSources.TrackPage).not.toContain('<select');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<input');
|
expect(corePageSources.TrackPage).not.toContain('<input');
|
||||||
expect(corePageSources.TrackPage).not.toContain('<button');
|
expect(corePageSources.TrackPage).not.toContain('<button');
|
||||||
@@ -430,8 +432,10 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.OperationsPage).toContain('<Tag className={`v2-ops-health-tag');
|
expect(corePageSources.OperationsPage).toContain('<Tag className={`v2-ops-health-tag');
|
||||||
expect(reconciliationSource).toContain('<Tag className={`v2-reconcile-severity');
|
expect(reconciliationSource).toContain('<Tag className={`v2-reconcile-severity');
|
||||||
expect(reconciliationSource).toContain('<Tag className={`v2-reconcile-status');
|
expect(reconciliationSource).toContain('<Tag className={`v2-reconcile-status');
|
||||||
expect(v2Styles).toContain('.v2-track-rail-expand { z-index: 21; top: auto; bottom: calc(132px + env(safe-area-inset-bottom));');
|
expect(workspaceStyles).toContain('.v2-track-rail-expand {');
|
||||||
expect(v2Styles).toContain('.v2-track-stage:has(.v2-track-current-card) .v2-track-rail-expand { bottom: calc(248px + env(safe-area-inset-bottom));');
|
expect(workspaceStyles).toContain('bottom: calc(112px + env(safe-area-inset-bottom));');
|
||||||
|
expect(workspaceStyles).toContain('.v2-track-stage:has(.v2-track-current-card) .v2-track-rail-expand {');
|
||||||
|
expect(workspaceStyles).toContain('bottom: calc(252px + env(safe-area-inset-bottom));');
|
||||||
expect(v2Styles).toContain('grid-template-rows: minmax(0, 1fr);');
|
expect(v2Styles).toContain('grid-template-rows: minmax(0, 1fr);');
|
||||||
expect(appShellSource).toContain("section === 'tracks' ? ' is-track-workspace' : ''");
|
expect(appShellSource).toContain("section === 'tracks' ? ' is-track-workspace' : ''");
|
||||||
expect(corePageSources.TrackPage).toContain("monitorReturn ? ' has-monitor-return' : ''");
|
expect(corePageSources.TrackPage).toContain("monitorReturn ? ' has-monitor-return' : ''");
|
||||||
@@ -439,8 +443,9 @@ describe('V2 production entry', () => {
|
|||||||
expect(v2Styles).toContain('.v2-content.is-track-workspace { min-height: 0; overflow: hidden; }');
|
expect(v2Styles).toContain('.v2-content.is-track-workspace { min-height: 0; overflow: hidden; }');
|
||||||
expect(workspaceStyles).toContain('.v2-workspace-panel-header.is-inverted > .v2-workspace-panel-copy > h5.semi-typography');
|
expect(workspaceStyles).toContain('.v2-workspace-panel-header.is-inverted > .v2-workspace-panel-copy > h5.semi-typography');
|
||||||
expect(workspaceStyles).toContain('color: #f8fbff;');
|
expect(workspaceStyles).toContain('color: #f8fbff;');
|
||||||
expect(v2Styles).toContain('.v2-track-current-card > .semi-card-body > div strong { margin-top: 4px; color: var(--track-overlay-text);');
|
expect(workspaceStyles).toContain('.v2-track-current-card > .semi-card-body > .v2-track-current-metrics strong {');
|
||||||
expect(v2Styles).toContain('.v2-track-current-card.semi-card { top: auto; right: 8px; bottom: 126px; left: 8px; width: auto; height: auto;');
|
expect(workspaceStyles).toContain('.v2-track-current-card.semi-card,');
|
||||||
|
expect(workspaceStyles).toContain('bottom: 110px;');
|
||||||
expect(workspaceFilterPanelSource).toContain("from './MobileFilterToggle'");
|
expect(workspaceFilterPanelSource).toContain("from './MobileFilterToggle'");
|
||||||
expect(workspaceFilterPanelSource).toContain('<MobileFilterToggle');
|
expect(workspaceFilterPanelSource).toContain('<MobileFilterToggle');
|
||||||
for (const name of ['VehiclePage', 'HistoryPage', 'AlertsPage', 'AccessPage', 'StatisticsPage', 'UsersPage', 'OperationsPage']) {
|
for (const name of ['VehiclePage', 'HistoryPage', 'AlertsPage', 'AccessPage', 'StatisticsPage', 'UsersPage', 'OperationsPage']) {
|
||||||
|
|||||||
@@ -18,6 +18,571 @@
|
|||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Semi UI trajectory replay workspace: one evidence rail, one map overlay and
|
||||||
|
* one compact playback surface. */
|
||||||
|
.v2-track-page {
|
||||||
|
grid-template-columns: 326px minmax(0, 1fr);
|
||||||
|
background: #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail {
|
||||||
|
border-right: 0;
|
||||||
|
background: #f4f7fb;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: inset -1px 0 #dfe6ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail-shell.semi-card {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dfe6ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(27, 46, 72, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail-shell > .semi-card-body {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: visible;
|
||||||
|
border-bottom: 1px solid #e3e9f1;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-query-header {
|
||||||
|
min-height: 58px;
|
||||||
|
border-bottom: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-workspace-panel-copy > h5.semi-typography {
|
||||||
|
color: #26384f;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-workspace-panel-copy > .semi-typography {
|
||||||
|
color: #8491a2;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-workspace-panel-actions {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-query-toggle.semi-button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 78px;
|
||||||
|
height: 32px;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #526780;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel:not(.is-collapsed) .v2-track-query-toggle .semi-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-rail-hide.semi-button {
|
||||||
|
width: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 7px;
|
||||||
|
color: #76869a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-query-summary {
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 7px 12px 11px;
|
||||||
|
background: linear-gradient(180deg, #fff 0%, #fbfcfe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-query-summary strong {
|
||||||
|
color: #30445d;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-query-summary small,
|
||||||
|
.v2-track-query-panel .v2-track-query-summary .semi-tag {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-query {
|
||||||
|
border-bottom: 0;
|
||||||
|
padding: 10px 12px 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail-result {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-result-header {
|
||||||
|
min-height: 62px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
padding: 9px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-result-header .v2-workspace-panel-copy > h5.semi-typography {
|
||||||
|
color: #25384f;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-result-header .v2-workspace-panel-copy > .semi-typography {
|
||||||
|
color: #8592a3;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-result-header .v2-workspace-panel-meta .semi-tag {
|
||||||
|
border-radius: 999px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail-tabs {
|
||||||
|
height: 42px;
|
||||||
|
flex-basis: 42px;
|
||||||
|
border-block: 1px solid #e3e9f1;
|
||||||
|
border-bottom-color: #dfe6ef;
|
||||||
|
background: #fff;
|
||||||
|
padding-inline: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail-tabs > .semi-button {
|
||||||
|
height: 40px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail-scroll {
|
||||||
|
background: #f6f8fb;
|
||||||
|
scrollbar-color: #c8d3e0 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-overview-panel {
|
||||||
|
gap: 7px;
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-overview-section {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(32, 54, 82, .035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-overview-section .v2-workspace-panel-header {
|
||||||
|
border-bottom: 1px solid #edf1f5;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-overview-section .v2-workspace-panel-actions {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-overview-section .v2-workspace-panel-header .semi-tag {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-quality-card.is-good {
|
||||||
|
background: #f7fcf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-quality-card.is-warning {
|
||||||
|
background: #fffaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-quality-card.is-good .v2-workspace-panel-header {
|
||||||
|
background: #f3fbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-quality-card.is-warning .v2-workspace-panel-header {
|
||||||
|
background: #fff8ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage {
|
||||||
|
background: #e7edf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage-tools {
|
||||||
|
top: 14px;
|
||||||
|
right: 14px;
|
||||||
|
gap: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(211, 221, 233, .95);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, .95);
|
||||||
|
padding: 3px;
|
||||||
|
box-shadow: 0 10px 28px rgba(27, 45, 69, .12);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage-tools > .semi-button {
|
||||||
|
height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: transparent;
|
||||||
|
padding-inline: 10px;
|
||||||
|
color: #5f7187;
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage-tools > .semi-button:hover {
|
||||||
|
background: #f2f6fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage-tools > .semi-button.is-active {
|
||||||
|
border: 0;
|
||||||
|
background: #eaf2ff;
|
||||||
|
color: var(--v2-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card.semi-card {
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
width: min(360px, calc(100% - 330px));
|
||||||
|
min-width: 310px;
|
||||||
|
border: 1px solid rgba(210, 221, 234, .97);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, .97);
|
||||||
|
box-shadow: 0 14px 36px rgba(27, 46, 72, .15);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card .v2-workspace-panel-header {
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 8px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card .v2-workspace-panel-meta .semi-tag {
|
||||||
|
min-width: 46px;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
border-block: 1px solid #e5ebf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 7px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span + span {
|
||||||
|
border-left: 1px solid #e5ebf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics small {
|
||||||
|
color: #8491a2;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics strong {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #25384f;
|
||||||
|
font-size: 12px;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-source {
|
||||||
|
display: flex;
|
||||||
|
min-height: 34px;
|
||||||
|
grid-template-columns: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #e8edf3;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: #66778c;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-source .semi-tag {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-source > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-source > span:last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-coverage {
|
||||||
|
display: grid;
|
||||||
|
min-height: 42px;
|
||||||
|
grid-template-columns: 7px minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #e8edf3;
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-coverage > i {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #18a86b;
|
||||||
|
box-shadow: 0 0 0 4px rgba(24, 168, 107, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-coverage.is-warning > i {
|
||||||
|
background: #eea33a;
|
||||||
|
box-shadow: 0 0 0 4px rgba(238, 163, 58, .11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-coverage > span {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-coverage strong,
|
||||||
|
.v2-track-current-coverage small {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-coverage strong {
|
||||||
|
color: #34475e;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-coverage small {
|
||||||
|
color: #8795a8;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-coverage em {
|
||||||
|
color: #6d7e92;
|
||||||
|
font-size: 8px;
|
||||||
|
font-style: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > p {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: #52657b;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-playback-dock.semi-card {
|
||||||
|
min-height: 96px;
|
||||||
|
border-top-color: rgba(205, 216, 229, .98);
|
||||||
|
background: rgba(255, 255, 255, .97);
|
||||||
|
box-shadow: 0 -10px 30px rgba(24, 41, 63, .12);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-playback-dock > .semi-card-body {
|
||||||
|
min-height: 96px;
|
||||||
|
grid-template-columns: 136px minmax(220px, 1fr) auto;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 10px 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-dock-summary strong {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-dock-summary span {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-segment-rail,
|
||||||
|
.v2-track-segment-placeholder {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-progress-slider.semi-slider-wrapper {
|
||||||
|
margin: 8px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-progress-meta {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-dock-controls {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-dock-controls > .semi-button {
|
||||||
|
width: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-dock-controls > .semi-button.is-primary {
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.v2-track-page {
|
||||||
|
grid-template-columns: 304px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-page.is-rail-collapsed {
|
||||||
|
grid-template-columns: 0 minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card.semi-card {
|
||||||
|
top: 62px;
|
||||||
|
width: min(360px, calc(100% - 28px));
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-page.is-rail-collapsed .v2-track-current-card.semi-card {
|
||||||
|
top: 14px;
|
||||||
|
width: min(360px, calc(100% - 330px));
|
||||||
|
min-width: 310px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-playback-dock > .semi-card-body {
|
||||||
|
grid-template-columns: minmax(220px, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-dock-summary {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.v2-track-detail-sidesheet .v2-track-rail {
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-detail-sidesheet .v2-track-rail-shell.semi-card {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-query-panel .v2-track-query-header {
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-result-header {
|
||||||
|
min-height: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage-tools {
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage-tools > .semi-button {
|
||||||
|
width: 38px;
|
||||||
|
min-width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card.semi-card,
|
||||||
|
.v2-track-page.is-rail-collapsed .v2-track-current-card.semi-card {
|
||||||
|
top: auto;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 110px;
|
||||||
|
left: 8px;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
border-radius: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card .v2-workspace-panel-header {
|
||||||
|
min-height: 46px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span {
|
||||||
|
padding: 6px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-source {
|
||||||
|
min-height: 31px;
|
||||||
|
padding: 4px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > .v2-track-current-coverage {
|
||||||
|
min-height: 34px;
|
||||||
|
grid-template-columns: 7px minmax(0, 1fr) auto;
|
||||||
|
padding: 5px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-coverage small {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-current-card > .semi-card-body > p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-playback-dock.semi-card,
|
||||||
|
.v2-track-playback-dock > .semi-card-body {
|
||||||
|
min-height: 102px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-playback-dock > .semi-card-body {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-rail-expand {
|
||||||
|
bottom: calc(112px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-track-stage:has(.v2-track-current-card) .v2-track-rail-expand {
|
||||||
|
bottom: calc(252px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Semi UI admin workspace scale.
|
* Semi UI admin workspace scale.
|
||||||
* Access management is the visual baseline: history and alerts use the same
|
* Access management is the visual baseline: history and alerts use the same
|
||||||
|
|||||||
Reference in New Issue
Block a user