feat: unify mobile track workspace
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user