feat: unify mobile track workspace

This commit is contained in:
lingniu
2026-07-19 07:58:51 +08:00
parent 6795a8ea6d
commit af71468e63
4 changed files with 89 additions and 21 deletions

View File

@@ -39,8 +39,8 @@ const TRACK_SOURCE_OPTIONS = [
const numberFormatters = new Map<number, Intl.NumberFormat>();
const timeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
const dateTimeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
const TRACK_SHEET_MAX_HEIGHT_PX = 720;
const TRACK_SHEET_CHROME_HEIGHT_PX = 264;
const TRACK_SHEET_MAX_HEIGHT_PX = 780;
const TRACK_SHEET_CHROME_HEIGHT_PX = 332;
const TRACK_SHEET_ROW_HEIGHT_PX = 62;
function number(value: number, digits = 1) {
@@ -120,11 +120,11 @@ function eventTone(type: string) {
}
function mobileTrackSheetHeight(track: TrackPlaybackResponse | undefined, queryCollapsed: boolean, tab: PanelTab) {
if (!track || !queryCollapsed || tab === 'overview') return `min(82dvh, ${TRACK_SHEET_MAX_HEIGHT_PX}px)`;
if (!track || !queryCollapsed || tab === 'overview') return `min(88dvh, ${TRACK_SHEET_MAX_HEIGHT_PX}px)`;
const itemCount = tab === 'stops' ? track.stops.length : track.events.length;
const visibleRows = itemCount ? Math.min(itemCount, 7) : 2;
const contentHeight = Math.min(TRACK_SHEET_MAX_HEIGHT_PX, TRACK_SHEET_CHROME_HEIGHT_PX + visibleRows * TRACK_SHEET_ROW_HEIGHT_PX);
return `min(82dvh, ${contentHeight}px)`;
return `min(88dvh, ${contentHeight}px)`;
}
function VehiclePicker({ value, onChange, onSelect }: { value: string; onChange: (value: string) => void; onSelect: (vehicle: VehicleRow) => void }) {
@@ -195,7 +195,7 @@ function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
</div>;
}
const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopIndexes, activeEventIndexes, tab, queryCollapsed, onDraft, onSubmit, onTab, onSelectIndex, onToggleQuery, onCollapse }: {
const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopIndexes, activeEventIndexes, tab, queryCollapsed, showCommandBar, onDraft, onSubmit, onTab, onSelectIndex, onToggleQuery, onCollapse }: {
draft: Draft;
track?: TrackPlaybackResponse;
loading: boolean;
@@ -203,6 +203,7 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
activeEventIndexes: readonly number[];
tab: PanelTab;
queryCollapsed: boolean;
showCommandBar: boolean;
onDraft: (draft: Draft) => void;
onSubmit: (event: FormEvent) => void;
onTab: (tab: PanelTab) => void;
@@ -217,8 +218,8 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
const contextTitle = queryCollapsed ? track?.plate || draft.keyword || '轨迹查询' : '轨迹查询';
return <aside className="v2-track-rail">
<Card className="v2-track-rail-shell" bodyStyle={{ padding: 0 }}>
<section className={`v2-track-query-panel${queryCollapsed ? ' is-collapsed' : ''}`}>
<WorkspaceCommandBar
<section className={`v2-track-query-panel${queryCollapsed ? ' is-collapsed' : ''}${showCommandBar ? '' : ' is-command-hidden'}`}>
{showCommandBar ? <WorkspaceCommandBar
className="v2-track-query-header"
ariaLabel="轨迹查询任务栏"
title={contextTitle}
@@ -238,7 +239,7 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
>{queryCollapsed ? '修改条件' : '收起条件'}</Button>
<Button className="v2-track-rail-hide" theme="borderless" aria-label="隐藏查询与明细" icon={<IconChevronLeft />} onClick={onCollapse}><span className="v2-track-rail-hide-label"></span></Button>
</>}
/>
/> : null}
{!queryCollapsed ? <form className="v2-track-query" onSubmit={onSubmit}>
<label><span></span><VehiclePicker value={draft.keyword} onChange={(keyword) => onDraft({ ...draft, keyword })} onSelect={(vehicle) => onDraft({ ...draft, keyword: vehicle.plate || vehicle.vin })} /></label>
<div className="v2-track-presets">
@@ -392,16 +393,41 @@ 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} onDraft={setDraft} onSubmit={submit} onTab={setPanelTab} onSelectIndex={selectRailIndex} onToggleQuery={() => setQueryCollapsed((value) => !value)} onCollapse={collapseRail} />;
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 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() || '轨迹查询与明细';
const mobileSheetDescription = track
? `${number(track.summary.distanceKm)} km · ${track.stops.length} 个停留 · ${track.events.length} 个事件`
: '选择车辆时间范围';
? `${appliedWindow} · ${appliedSource}`
: '选择车辆时间范围与数据来源';
const mobileSheetBadge = query.isFetching
? '读取中'
: track
? `${track.points.length.toLocaleString('zh-CN')}`
? 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
}
] : [
{ label: '查询状态', value: '待查询', detail: '请先选择车辆', tone: 'primary' as const },
{ label: '时间范围', value: appliedWindow, detail: '最长连续 7 天' },
{ label: '数据来源', value: appliedSource, detail: '支持自动优选' }
];
return <div className={`v2-track-page${mobileLayout ? ' is-mobile-layout' : ''}${railCollapsed ? ' is-rail-collapsed' : ''}${monitorReturn ? ' has-monitor-return' : ''}`}>
<MonitorReturnBar />
@@ -410,12 +436,24 @@ export default function TrackPage() {
variant="editor"
visible={!railCollapsed}
ariaLabel="轨迹查询与明细"
closeLabel="关闭轨迹查询与明细"
dialogId="v2-track-mobile-workbench"
placement="bottom"
height={mobileTrackSheetHeight(track, queryCollapsed, panelTab)}
title={mobileSheetTitle}
description={mobileSheetDescription}
icon={<IconMapPin />}
badge={mobileSheetBadge}
badgeColor={query.isFetching ? 'blue' : track ? 'green' : 'grey'}
summaryItems={mobileSheetSummary}
footerNote={`${appliedWindow} · ${appliedSource}`}
secondaryActions={[{
label: queryCollapsed ? '修改查询' : '收起查询',
ariaLabel: queryCollapsed ? '修改轨迹查询' : '收起轨迹查询',
icon: <IconSearch />,
onClick: () => setQueryCollapsed((value) => !value)
}]}
primaryAction={{ label: '查看地图', icon: <IconMapPin />, onClick: collapseRail }}
onCancel={collapseRail}
>{trackRail}</WorkspaceSideSheet> : trackRail}
<section className="v2-track-stage">