diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx index 71a43287..7e76eed5 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx @@ -194,6 +194,38 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument()); }); +test('uses one focused Semi bottom SideSheet for the complete mobile history query', async () => { + layout.mobile = true; + mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] }); + mocks.historyData.mockResolvedValue(historyData('MOBILEVIN', '粤A移动历史', 'mobile-as-of')); + mocks.historySeries.mockResolvedValue(historySeries('MOBILEVIN', '粤A移动历史', 'mobile-as-of')); + mocks.historyExports.mockResolvedValue([]); + renderPage('/history'); + + await screen.findByText('等待查询历史数据'); + const trigger = screen.getByRole('button', { name: '修改查询范围:请选择车辆' }); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(trigger); + + const dialog = await screen.findByRole('dialog', { name: '历史查询范围' }); + expect(document.querySelector('.v2-history-filter-sidesheet')).toHaveClass('semi-sidesheet-bottom'); + expect(document.querySelector('.v2-history-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 700px)' }); + expect(within(dialog).getByPlaceholderText('车牌 / VIN,多台用逗号分隔')).toBeInTheDocument(); + expect(within(dialog).getByLabelText('开始时间')).toBeInTheDocument(); + expect(within(dialog).getByLabelText('结束时间')).toBeInTheDocument(); + expect(within(dialog).getByRole('combobox', { name: '数据类型' })).toBeInTheDocument(); + expect(within(dialog).getByRole('combobox', { name: '数据来源' })).toBeInTheDocument(); + fireEvent.change(within(dialog).getByPlaceholderText('车牌 / VIN,多台用逗号分隔'), { target: { value: 'MOBILEVIN' } }); + fireEvent.click(within(dialog).getByRole('button', { name: '应用并查询' })); + + await waitFor(() => { + const calls = mocks.historyData.mock.calls; + expect((calls[calls.length - 1]?.[0] as URLSearchParams | undefined)?.get('keywords')).toBe('MOBILEVIN'); + }); + expect(screen.getByRole('button', { name: '修改查询范围:1 辆 · 位置数据' })).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByRole('dialog', { name: '历史查询范围' })).toHaveClass('semi-sidesheet-animation-content_hide_bottom'); +}); + test('renders only selectable Semi history cards on mobile', async () => { layout.mobile = true; mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx index 0683ef1a..61c6fd17 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -10,6 +10,7 @@ import { downloadBlob } from '../domain/download'; import { formatShanghaiDateTime } from '../domain/formatters'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; +import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { PlatformTime } from '../shared/PlatformTime'; import { ProtocolTag } from '../shared/ProtocolTag'; import { TablePagination } from '../shared/TablePagination'; @@ -339,11 +340,12 @@ export default function HistoryPage() { const closeSelectedRow = useCallback(() => { setSelectedRowRef({ scope: dataScope, id: '' }); }, [dataScope]); + const mobileFiltersOpen = mobileLayout && !filtersCollapsed; useSideSheetA11y(mobileLayout && Boolean(selectedRow), '.v2-history-detail-sidesheet', 'v2-history-detail', '历史数据详情', '关闭历史数据详情'); useSideSheetA11y(exportAllowed && exportJobsOpen, '.v2-history-export-sidesheet', 'v2-history-export-jobs', '历史数据导出任务', '关闭历史数据导出任务'); + useSideSheetA11y(mobileFiltersOpen, '.v2-history-filter-sidesheet', 'v2-history-filter-sheet', '历史查询范围', '关闭历史查询范围'); - const submit = (event: FormEvent) => { - event.preventDefault(); + const applyDraft = () => { const parsed = parseHistoryKeywords(draft.keywords); if (!parsed.length) return; const next = { ...draft, keywords: parsed.join(',') }; @@ -355,7 +357,8 @@ export default function HistoryPage() { if (next.protocol) url.set('protocol', next.protocol); setSearchParams(preserveMonitorReturn(url, monitorReturn), { replace: true }); }; - const reset = () => { const next = { keywords: '', ...currentHistoryWindow(), category: 'location', protocol: '' }; setDraft(next); setCriteria(next); setOffset(0); setSearchParams(preserveMonitorReturn(new URLSearchParams(), monitorReturn), { replace: true }); }; + const submit = (event: FormEvent) => { event.preventDefault(); applyDraft(); }; + const reset = () => { const next = { keywords: '', ...currentHistoryWindow(), category: 'location', protocol: '' }; setDraft(next); setCriteria(next); setOffset(0); setFiltersCollapsed(true); setSearchParams(preserveMonitorReturn(new URLSearchParams(), monitorReturn), { replace: true }); }; const toggleMetric = (key: string) => setVisibleByCategory((current) => { const baseline = current[criteria.category] ?? allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key); const next = baseline.includes(key) ? baseline.filter((item) => item !== key) : [...baseline, key]; @@ -398,6 +401,13 @@ export default function HistoryPage() { className: 'is-duration' } ]; + const historyFilterFields = <> + +
+ + + >; + const exportRequest: HistoryExportRequest = { keywords, category: criteria.category, protocol: criteria.protocol || undefined, dateFrom: criteria.dateFrom, dateTo: criteria.dateTo, metrics: visibleKeys, format: 'csv' }; return