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

@@ -270,23 +270,28 @@ test('uses the shared Semi editor SideSheet for mobile track criteria and eviden
return element;
});
expect(sheet).toHaveClass('semi-sidesheet-inner');
expect(sheet).toHaveAttribute('id', 'v2-track-mobile-workbench');
expect(document.querySelector('.v2-track-detail-sidesheet')).toHaveClass('v2-workspace-editor-sidesheet', 'semi-sidesheet-bottom');
expect(document.querySelector('.v2-track-detail-sidesheet .v2-track-rail')).toBeInTheDocument();
expect(document.querySelector('.v2-track-detail-sidesheet .v2-track-query-summary')).not.toBeInTheDocument();
expect(screen.getByLabelText('轨迹查询任务栏')).toHaveTextContent('粤A12345');
expect(screen.queryByLabelText('轨迹查询任务栏')).not.toBeInTheDocument();
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
expect(screen.getByText('07-15 00:00 07-15 23:59 · 自动来源')).toBeInTheDocument();
expect(screen.getByText('8 km · 1 个停留 · 3 个事件')).toBeInTheDocument();
expect(sheet).toHaveStyle({ height: 'min(82dvh, 326px)' });
expect(screen.getAllByText('07-15 00:00 07-15 23:59 · 自动来源')).toHaveLength(2);
expect(screen.getByRole('list', { name: '轨迹查询与明细摘要' })).toHaveTextContent('行程里程8 km00:20:00停留 / 事件1 / 3地图证据点数据点3时间窗完整');
expect(screen.getByRole('button', { name: '修改轨迹查询' })).toHaveTextContent('修改查询');
expect(screen.getByRole('button', { name: /查看地图$/ })).toBeInTheDocument();
expect(sheet).toHaveStyle({ height: 'min(88dvh, 394px)' });
fireEvent.click(screen.getByRole('tab', { name: /事件点/ }));
await waitFor(() => expect(sheet).toHaveStyle({ height: 'min(82dvh, 450px)' }));
await waitFor(() => expect(sheet).toHaveStyle({ height: 'min(88dvh, 518px)' }));
fireEvent.click(screen.getByRole('tab', { name: /停留点/ }));
fireEvent.click(screen.getByRole('button', { name: /停留 00:05:00/ }));
expect(view.container.querySelector('.v2-track-page')).toHaveClass('is-rail-collapsed');
fireEvent.click(screen.getByRole('button', { name: '打开轨迹详情' }));
await screen.findByRole('dialog', { name: '轨迹查询与明细' });
fireEvent.click(screen.getByRole('button', { name: '修改条件' }));
fireEvent.click(screen.getByRole('button', { name: '修改轨迹查询' }));
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
expect(screen.getByRole('button', { name: '收起轨迹查询' })).toHaveTextContent('收起查询');
expect(sheet).toHaveStyle({ height: 'min(88dvh, 780px)' });
expect(screen.getByRole('button', { name: '关闭轨迹查询与明细' })).toBeInTheDocument();
});
@@ -299,6 +304,7 @@ test('keeps one clear mobile query entry before a vehicle is selected', async ()
expect(screen.queryByRole('button', { name: '打开轨迹详情' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /选择车辆/ }));
expect(await screen.findByRole('dialog', { name: '轨迹查询与明细' })).toBeInTheDocument();
expect(screen.getByRole('list', { name: '轨迹查询与明细摘要' })).toHaveTextContent('查询状态待查询请先选择车辆');
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toBeInTheDocument();
});

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">

View File

@@ -384,6 +384,9 @@ describe('V2 production entry', () => {
expect(corePageSources.TrackPage).toContain("from '../shared/WorkspaceSideSheet'");
expect(corePageSources.TrackPage).toContain('<WorkspaceSideSheet');
expect(corePageSources.TrackPage).toContain('variant="editor"');
expect(corePageSources.TrackPage).toContain('dialogId="v2-track-mobile-workbench"');
expect(corePageSources.TrackPage).toContain('summaryItems={mobileSheetSummary}');
expect(corePageSources.TrackPage).toContain("primaryAction={{ label: '查看地图'");
expect(corePageSources.TrackPage).not.toContain('useSideSheetA11y(mobileLayout');
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('<section className="v2-track-overview-section v2-track-source-card">');

View File

@@ -1416,6 +1416,10 @@
background: #fff;
}
.v2-track-query-panel.is-command-hidden.is-collapsed {
display: none;
}
.v2-track-query-panel .v2-track-query-header {
min-height: 58px;
border-bottom: 0;
@@ -1966,8 +1970,8 @@
}
.v2-track-detail-sidesheet .v2-track-date-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
grid-template-columns: minmax(0, 1fr);
gap: 10px;
}
.v2-track-stage-tools {
@@ -20761,7 +20765,7 @@
scrollbar-gutter: stable;
}
:is(.v2-workspace-config-sidesheet, .v2-workspace-filter-sidesheet, .v2-workspace-task-sidesheet) .semi-sidesheet-footer {
:is(.v2-workspace-config-sidesheet, .v2-workspace-filter-sidesheet, .v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet) .semi-sidesheet-footer {
min-height: 64px;
border-top: 1px solid #e1e8f1;
background: rgba(255, 255, 255, .97);
@@ -21371,6 +21375,23 @@
padding: 7px 9px calc(7px + env(safe-area-inset-bottom));
}
.v2-workspace-editor-sidesheet.semi-sidesheet-bottom .semi-sidesheet-footer {
min-height: 58px;
padding: 7px 9px calc(7px + env(safe-area-inset-bottom));
}
.v2-track-detail-sidesheet .v2-workspace-config-summary {
min-height: 68px;
}
.v2-track-detail-sidesheet .v2-workspace-config-summary strong {
font-size: 11px;
}
.v2-track-detail-sidesheet .v2-workspace-config-summary em {
font-size: 8px;
}
.v2-workspace-task-sidesheet .v2-workspace-config-content {
padding: 8px;
}