feat: unify track replay summary
This commit is contained in:
@@ -18,6 +18,7 @@ import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||
import { QUERY_MEMORY } from '../queryPolicy';
|
||||
@@ -127,6 +128,39 @@ function mobileTrackSheetHeight(track: TrackPlaybackResponse | undefined, queryC
|
||||
return `min(88dvh, ${contentHeight}px)`;
|
||||
}
|
||||
|
||||
function trackSummaryItems(track?: TrackPlaybackResponse): WorkspaceQueueMetricRailItem[] {
|
||||
return [
|
||||
{
|
||||
label: '行程里程',
|
||||
value: track ? `${number(track.summary.distanceKm)} km` : '—',
|
||||
note: track ? formatDuration(track.summary.durationSeconds) : '等待查询',
|
||||
tone: 'primary',
|
||||
emphasis: 'primary'
|
||||
},
|
||||
{
|
||||
label: '停留点',
|
||||
value: track ? track.stops.length.toLocaleString('zh-CN') : '—',
|
||||
note: track ? '超过 3 分钟' : '等待查询',
|
||||
tone: track?.stops.length ? 'warning' : 'neutral',
|
||||
emphasis: 'secondary'
|
||||
},
|
||||
{
|
||||
label: '事件点',
|
||||
value: track ? track.events.length.toLocaleString('zh-CN') : '—',
|
||||
note: track ? '轨迹事件' : '等待查询',
|
||||
tone: 'neutral',
|
||||
emphasis: 'secondary'
|
||||
},
|
||||
{
|
||||
label: '数据点',
|
||||
value: track ? track.points.length.toLocaleString('zh-CN') : '—',
|
||||
note: track ? track.coverage.complete ? '时间窗完整' : '已返回切片' : '等待查询',
|
||||
tone: track?.coverage.complete ? 'success' : track ? 'warning' : 'neutral',
|
||||
emphasis: 'secondary'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function VehiclePicker({ value, onChange, onSelect }: { value: string; onChange: (value: string) => void; onSelect: (vehicle: VehicleRow) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [debounced, setDebounced] = useState(value.trim());
|
||||
@@ -195,9 +229,10 @@ function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
|
||||
</div>;
|
||||
}
|
||||
|
||||
const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopIndexes, activeEventIndexes, tab, queryCollapsed, showCommandBar, onDraft, onSubmit, onTab, onSelectIndex, onToggleQuery, onCollapse }: {
|
||||
const TrackRail = memo(function TrackRail({ draft, track, summaryItems, loading, activeStopIndexes, activeEventIndexes, tab, queryCollapsed, showCommandBar, onDraft, onSubmit, onTab, onSelectIndex, onToggleQuery, onCollapse }: {
|
||||
draft: Draft;
|
||||
track?: TrackPlaybackResponse;
|
||||
summaryItems: WorkspaceQueueMetricRailItem[];
|
||||
loading: boolean;
|
||||
activeStopIndexes: readonly number[];
|
||||
activeEventIndexes: readonly number[];
|
||||
@@ -254,12 +289,11 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
|
||||
</section>
|
||||
|
||||
<section className="v2-track-rail-result">
|
||||
<WorkspacePanelHeader
|
||||
variant="compact"
|
||||
className="v2-track-result-header"
|
||||
title="轨迹证据"
|
||||
description={track ? track.plate ? `${track.plate} · ${track.vin}` : track.vin : '停留、事件与来源统一呈现'}
|
||||
meta={<Tag color={track ? 'blue' : 'grey'} type="light" size="small">{track ? `${number(track.summary.distanceKm)} km` : '等待查询'}</Tag>}
|
||||
<WorkspaceMetricRail
|
||||
variant="queue"
|
||||
ariaLabel="轨迹行程摘要"
|
||||
className="v2-track-result-metric-rail"
|
||||
items={summaryItems}
|
||||
/>
|
||||
<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: '概览' }]} />
|
||||
<div className="v2-track-rail-scroll">
|
||||
@@ -386,6 +420,7 @@ export default function TrackPage() {
|
||||
if (mobileLayout) setRailCollapsed(true);
|
||||
}, [mobileLayout, selectIndex]);
|
||||
const collapseRail = useCallback(() => setRailCollapsed(true), []);
|
||||
const toggleQuery = useCallback(() => setQueryCollapsed((value) => !value), []);
|
||||
const togglePlayback = () => {
|
||||
if (points.length < 2) return;
|
||||
if (!playing && boundedIndex >= points.length - 1) setActiveIndex(0);
|
||||
@@ -393,7 +428,8 @@ export default function TrackPage() {
|
||||
setPlaying((value) => !value);
|
||||
};
|
||||
const progress = points.length > 1 ? boundedIndex / (points.length - 1) * 100 : 0;
|
||||
const trackRail = <TrackRail draft={draft} track={track} loading={query.isFetching} activeStopIndexes={activeStopIndexes} activeEventIndexes={activeEventIndexes} tab={panelTab} queryCollapsed={queryCollapsed} showCommandBar={!mobileLayout} onDraft={setDraft} onSubmit={submit} onTab={setPanelTab} onSelectIndex={selectRailIndex} onToggleQuery={() => setQueryCollapsed((value) => !value)} onCollapse={collapseRail} />;
|
||||
const tripSummaryItems = useMemo(() => trackSummaryItems(track), [track]);
|
||||
const trackRail = <TrackRail draft={draft} track={track} summaryItems={tripSummaryItems} loading={query.isFetching} activeStopIndexes={activeStopIndexes} activeEventIndexes={activeEventIndexes} tab={panelTab} queryCollapsed={queryCollapsed} showCommandBar={!mobileLayout} onDraft={setDraft} onSubmit={submit} onTab={setPanelTab} onSelectIndex={selectRailIndex} onToggleQuery={toggleQuery} onCollapse={collapseRail} />;
|
||||
const appliedWindow = `${draft.dateFrom.replace('T', ' ').slice(5)} – ${draft.dateTo.replace('T', ' ').slice(5)}`;
|
||||
const appliedSource = draft.protocol ? protocolDisplayLabel(draft.protocol) : '自动来源';
|
||||
const mobileSheetTitle = track?.plate || draft.keyword.trim() || '轨迹查询与明细';
|
||||
@@ -405,29 +441,16 @@ export default function TrackPage() {
|
||||
: track
|
||||
? track.coverage.complete ? '时间窗完整' : '最新切片'
|
||||
: '待查询';
|
||||
const mobileSheetSummary = track ? [
|
||||
{
|
||||
label: '行程里程',
|
||||
value: `${number(track.summary.distanceKm)} km`,
|
||||
detail: formatDuration(track.summary.durationSeconds),
|
||||
tone: 'primary' as const
|
||||
},
|
||||
{
|
||||
label: '停留 / 事件',
|
||||
value: `${track.stops.length} / ${track.events.length}`,
|
||||
detail: '地图证据点'
|
||||
},
|
||||
{
|
||||
label: '数据点',
|
||||
value: track.points.length.toLocaleString('zh-CN'),
|
||||
detail: track.coverage.complete ? '时间窗完整' : '已返回切片',
|
||||
tone: track.coverage.complete ? 'success' as const : 'warning' as const
|
||||
}
|
||||
] : [
|
||||
const mobileSheetSummary = useMemo(() => track ? tripSummaryItems.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
detail: item.note,
|
||||
tone: item.tone
|
||||
})) : [
|
||||
{ label: '查询状态', value: '待查询', detail: '请先选择车辆', tone: 'primary' as const },
|
||||
{ label: '时间范围', value: appliedWindow, detail: '最长连续 7 天' },
|
||||
{ label: '数据来源', value: appliedSource, detail: '支持自动优选' }
|
||||
];
|
||||
], [appliedSource, appliedWindow, track, tripSummaryItems]);
|
||||
|
||||
return <div className={`v2-track-page${mobileLayout ? ' is-mobile-layout' : ''}${railCollapsed ? ' is-rail-collapsed' : ''}${monitorReturn ? ' has-monitor-return' : ''}`}>
|
||||
<MonitorReturnBar />
|
||||
@@ -451,7 +474,7 @@ export default function TrackPage() {
|
||||
label: queryCollapsed ? '修改查询' : '收起查询',
|
||||
ariaLabel: queryCollapsed ? '修改轨迹查询' : '收起轨迹查询',
|
||||
icon: <IconSearch />,
|
||||
onClick: () => setQueryCollapsed((value) => !value)
|
||||
onClick: toggleQuery
|
||||
}]}
|
||||
primaryAction={{ label: '查看地图', icon: <IconMapPin />, onClick: collapseRail }}
|
||||
onCancel={collapseRail}
|
||||
@@ -494,7 +517,6 @@ export default function TrackPage() {
|
||||
{query.isError ? <div className="v2-track-error"><InlineError message={query.error instanceof Error ? query.error.message : '轨迹查询失败'} onRetry={() => query.refetch()} /></div> : null}
|
||||
|
||||
<Card className="v2-track-playback-dock" bodyStyle={{ padding: 0 }}>
|
||||
<div className="v2-track-dock-summary"><small>行程里程</small><strong>{track ? `${number(track.summary.distanceKm)} km` : '等待查询'}</strong><span>{track ? `${formatDuration(track.summary.durationSeconds)} · ${track.summary.stopCount} 次停留` : '查询后可播放完整轨迹'}</span></div>
|
||||
<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} 个数据点`} />
|
||||
|
||||
Reference in New Issue
Block a user