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 = <> + +
时间范围
setDraft((current) => ({ ...current, dateFrom: value }))} /> setDraft((current) => ({ ...current, dateTo: value }))} />
+ + ; + const exportRequest: HistoryExportRequest = { keywords, category: criteria.category, protocol: criteria.protocol || undefined, dateFrom: criteria.dateFrom, dateTo: criteria.dateTo, metrics: visibleKeys, format: 'csv' }; return
@@ -411,7 +421,34 @@ export default function HistoryPage() { statusColor={keywords.length ? 'blue' : 'grey'} meta={上海时区 · 最多 5 台车辆} /> - + setFiltersCollapsed((value) => !value)} + /> + 筛选历史数据车辆、时间、数据类型与协议来源
} + onCancel={() => setFiltersCollapsed(true)} + footer={
} + > +
+
+
查询范围最多 5 台车辆,按上海时区检索可追溯数据
+
{historyFilterFields}
+
+ {exportAllowed ?
导出当前结果按已应用范围与当前字段生成 CSV
: 只读} +
+ + : setFiltersCollapsed((value) => !value)} >
- -
时间范围
setDraft((current) => ({ ...current, dateFrom: value }))} /> setDraft((current) => ({ ...current, dateTo: value }))} />
- -
{exportAllowed ? : 只读}
+ {historyFilterFields} +
{exportAllowed ? : 只读}
-
+ } {keywords.length ? .v2-mobile-filter-toggle.semi-button { + min-height: 58px; + margin: 0; + } + + .v2-history-filter-sidesheet .semi-sidesheet-inner { + width: 100vw !important; + max-width: 100%; + overflow: hidden; + border-radius: 18px 18px 0 0; + background: #f4f7fb; + box-shadow: 0 -20px 56px rgba(25, 45, 72, .2); + padding-bottom: env(safe-area-inset-bottom); + } + + .v2-history-filter-sidesheet.semi-sidesheet-bottom .semi-sidesheet-header { + min-height: 68px; + border-bottom: 1px solid #dfe7f0; + background: rgba(255, 255, 255, .98); + padding: 11px 14px; + } + + .v2-history-filter-sidesheet.semi-sidesheet-bottom .semi-sidesheet-body { + min-height: 0; + overflow-y: auto; + background: #f4f7fb; + padding: 12px; + overscroll-behavior: contain; + } + + .v2-history-filter-sidesheet.semi-sidesheet-bottom .semi-sidesheet-footer { + border-top: 1px solid #dfe7f0; + background: rgba(255, 255, 255, .98); + padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); + } + + .v2-history-mobile-filter-form { + display: grid; + gap: 10px; + } + + .v2-history-mobile-filter-section { + display: grid; + gap: 12px; + border: 1px solid #dfe7f0; + border-radius: 12px; + background: #fff; + padding: 12px; + box-shadow: 0 8px 24px rgba(31, 53, 80, .055); + } + + .v2-history-mobile-filter-section > header { + display: flex; + min-width: 0; + flex-direction: column; + gap: 3px; + } + + .v2-history-mobile-filter-section > header > strong { + color: #2b4058; + font-size: 13px; + line-height: 1.3; + } + + .v2-history-mobile-filter-section > header > span { + color: #7c8b9e; + font-size: 10px; + line-height: 1.45; + } + + .v2-history-mobile-filter-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 11px 9px; + } + + .v2-history-mobile-filter-grid > .v2-history-vehicles, + .v2-history-mobile-filter-grid > .v2-history-range { + grid-column: 1 / -1; + } + + .v2-history-mobile-filter-grid > label, + .v2-history-mobile-filter-grid > fieldset { + display: grid; + min-width: 0; + gap: 6px; + margin: 0; + border: 0; + padding: 0; + color: #5f7188; + font-size: 11px; + font-weight: 650; + } + + .v2-history-mobile-filter-grid > fieldset > legend { + margin-bottom: 6px; + padding: 0; + } + + .v2-history-mobile-filter-grid > label > .semi-input-wrapper, + .v2-history-mobile-filter-grid > label > .semi-select { + width: 100%; + min-height: 44px; + border-radius: 9px; + background: #fff; + } + + .v2-history-mobile-filter-grid .v2-history-range > div { + display: grid; + min-width: 0; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + gap: 6px; + } + + .v2-history-mobile-filter-grid .v2-history-range .semi-input-wrapper { + min-width: 0; + min-height: 44px; + border-radius: 9px; + background: #fff; + } + + .v2-history-mobile-filter-grid .v2-history-range > div > span { + color: #8a98aa; + font-size: 10px; + } + + .v2-history-mobile-filter-grid .semi-input, + .v2-history-mobile-filter-grid .semi-select-selection-text { + font-size: 13px; + } + + .v2-history-mobile-export-action { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 10px; + border: 1px solid #dfe7f0; + border-radius: 11px; + background: #fff; + padding: 10px 11px; + } + + .v2-history-mobile-export-action > span { + display: grid; + min-width: 0; + gap: 2px; + } + + .v2-history-mobile-export-action strong { + color: #40536b; + font-size: 11px; + } + + .v2-history-mobile-export-action small { + color: #8794a6; + font-size: 9px; + line-height: 1.4; + } + + .v2-history-mobile-export-action > .semi-button { + min-height: 38px; + flex: 0 0 auto; + border-radius: 8px; + font-size: 11px; + } + + .v2-history-mobile-filter-footer { + display: grid; + width: 100%; + grid-template-columns: minmax(0, .72fr) minmax(0, 1.28fr); + gap: 9px; + } + + .v2-history-mobile-filter-footer > .semi-button { + width: 100%; + min-height: 42px; + border-radius: 9px; + font-weight: 700; + } +}