diff --git a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx index 20ddc3ae..51dbc372 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx @@ -279,6 +279,7 @@ test('uses a compact bottom sheet for mileage source strategy on mobile', async prepareData(); renderPage('/statistics?vins=LTEST000000000001&dateFrom=2026-07-13&dateTo=2026-07-14'); + fireEvent.click(screen.getByRole('button', { name: /修改里程范围/ })); fireEvent.click(screen.getByRole('button', { name: /数据源/ })); expect(screen.getByRole('dialog', { name: '数据源策略配置' })).toBeInTheDocument(); @@ -286,6 +287,33 @@ test('uses a compact bottom sheet for mileage source strategy on mobile', async expect(document.querySelector('.v2-mileage-source-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(58dvh, 500px)' }); }); +test('opens mobile mileage filters in a bottom task sheet without displacing the data canvas', async () => { + layout.mobile = true; + prepareData(); + const view = renderPage('/statistics?vins=LTEST000000000001&dateFrom=2026-07-13&dateTo=2026-07-14'); + + expect(await screen.findByText('车辆每日里程')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-mileage-mobile-discovery')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-mileage-query-panel')).not.toBeInTheDocument(); + expect(screen.queryByRole('dialog', { name: '里程范围筛选' })).not.toBeInTheDocument(); + expect(screen.queryByRole('textbox', { name: '搜索车牌' })).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /修改里程范围/ })); + + expect(await screen.findByRole('dialog', { name: '里程范围筛选' })).toBeInTheDocument(); + expect(document.querySelector('.v2-mileage-filter-sidesheet')).toHaveClass('semi-sidesheet-bottom'); + expect(document.querySelector('.v2-mileage-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 690px)' }); + expect(screen.getByRole('textbox', { name: '搜索车牌' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '应用并查询' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '关闭里程范围筛选' })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: '关闭里程范围筛选' })); + + expect(screen.getByRole('dialog', { name: '里程范围筛选' })).toHaveClass('semi-sidesheet-animation-content_hide_bottom'); + expect(screen.getByText('车辆每日里程')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /修改里程范围/ })).toHaveAttribute('aria-expanded', 'false'); +}); + test('paginates all unique vehicles when no license plate is selected', async () => { prepareData(); const firstPage = Array.from({ length: 20 }, (_, index) => ({ vin: `VIN${String(index + 1).padStart(14, '0')}`, plate: `粤A${String(index + 1).padStart(5, '0')}` })); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx index 225b3465..0459cbd4 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx @@ -9,6 +9,7 @@ import { createMileageExportStream, type MileageExportStream } from '../domain/m import { formatZhNumber } from '../domain/formatters'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; +import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { TablePagination } from '../shared/TablePagination'; import { VehicleCandidateList } from '../shared/VehicleCandidateList'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; @@ -354,6 +355,7 @@ const MileageTable = memo(function MileageTable({ rows, dates, scrollRef }: { ro export default function StatisticsPage() { const [searchParams, setSearchParams] = useSearchParams(); const monitorReturn = monitorReturnFromParams(searchParams); + const mobileLayout = useMobileLayout(); const [draft, setDraft] = useState(() => initialCriteria(searchParams)); const [criteria, setCriteria] = useState(() => initialCriteria(searchParams)); const [page, setPage] = useState(1); @@ -362,10 +364,12 @@ export default function StatisticsPage() { const [exportProgress, setExportProgress] = useState(); const [validationError, setValidationError] = useState(''); const [filtersCollapsed, setFiltersCollapsed] = useState(true); + const mobileFiltersOpen = mobileLayout && !filtersCollapsed; const exportControllerRef = useRef(null); const pageRef = useRef(null); const tableScrollRef = useRef(null); const mountedRef = useRef(true); + useSideSheetA11y(mobileFiltersOpen, '.v2-mileage-filter-sidesheet', 'v2-mileage-filter-panel', '里程范围筛选', '关闭里程范围筛选'); useEffect(() => { mountedRef.current = true; return () => { @@ -435,14 +439,32 @@ export default function StatisticsPage() { }), [displayVehicles, mileageByVin, rankingByVin]); const totalVehicles = hasVehicles ? criteria.vehicles.length : fleetVehicles.data?.total ?? 0; const totalPages = Math.max(1, Math.ceil(totalVehicles / PAGE_SIZE)); - const submit = (event: FormEvent) => { - event.preventDefault(); + const applyDraft = () => { const error = mileageDateRangeError(draft); setValidationError(error); if (error) return; setPage(1); setExportFeedback(''); setCriteria(draft); setSearchParams(preserveMonitorReturn(mileageParams(draft, -1), monitorReturn), { replace: true }); setFiltersCollapsed(true); }; - const setRange = (range: { dateFrom: string; dateTo: string }) => { const next = { ...draft, ...range }; setPage(1); setExportFeedback(''); setValidationError(''); setDraft(next); setCriteria(next); setSearchParams(preserveMonitorReturn(mileageParams(next, -1), monitorReturn), { replace: true }); }; + const submit = (event: FormEvent) => { event.preventDefault(); applyDraft(); }; + const setRange = (range: { dateFrom: string; dateTo: string }) => { const next = { ...draft, ...range }; setPage(1); setExportFeedback(''); setValidationError(''); setDraft(next); setCriteria(next); setSearchParams(preserveMonitorReturn(mileageParams(next, -1), monitorReturn), { replace: true }); setFiltersCollapsed(true); }; + const closeMobileFilters = () => { + setDraft(criteria); + setValidationError(''); + setFiltersCollapsed(true); + }; + const toggleFilters = () => { + if (!mobileLayout) { + setFiltersCollapsed((value) => !value); + return; + } + if (mobileFiltersOpen) { + closeMobileFilters(); + return; + } + setDraft(criteria); + setValidationError(''); + setFiltersCollapsed(false); + }; const todayRange = defaultWindow(1); const yesterdayRange = defaultWindow(1, 1); const sevenDayRange = defaultWindow(7); @@ -533,6 +555,17 @@ export default function StatisticsPage() { exportControllerRef.current.abort(); }; + const rangeSummary = criteria.vehicles.length ? `已选 ${criteria.vehicles.length} 辆 · ${inclusiveDays(criteria.dateFrom, criteria.dateTo)} 天` : `全部车辆 · ${inclusiveDays(criteria.dateFrom, criteria.dateTo)} 天`; + const filterForm =
+ setDraft((current) => ({ ...current, vehicles }))} /> + + + + setDraft((current) => ({ ...current, sources }))} /> +
快捷范围
+ ; + const filterValidation = validationError || criteriaError ?

{validationError || criteriaError}

: null; + return
@@ -545,28 +578,44 @@ export default function StatisticsPage() { statusColor={criteria.vehicles.length ? 'blue' : 'grey'} meta={{criteria.sources.find((source) => source.enabled)?.protocol ?? '未配置'} 优先} /> - + + + 里程范围选择车辆、自然日与数据源
} + onCancel={closeMobileFilters} + footer={
应用后刷新里程统计与车辆矩阵
} + > + {filterForm} + {filterValidation} + +
: setFiltersCollapsed((value) => !value)} + onToggle={toggleFilters} > -
- setDraft((current) => ({ ...current, vehicles }))} /> - - - - setDraft((current) => ({ ...current, sources }))} /> -
快捷范围
- - {validationError || criteriaError ?

{validationError || criteriaError}

: null} + {filterForm} + {filterValidation} -
+ } {statistics.isError || mileage.isError || fleetVehicles.isError ? { statistics.refetch(); mileage.refetch(); if (!hasVehicles) fleetVehicles.refetch(); }} /> : null} diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts index 42846d72..e736d474 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -365,6 +365,9 @@ describe('V2 production entry', () => { expect(corePageSources.StatisticsPage).toContain(' span { + color: #7b899b; + font-size: 10px; +} +.v2-mileage-filter-footer > .semi-button { + min-width: 118px; + min-height: 38px; + border-radius: 8px; + font-weight: 700; +} + +@media (max-width: 680px) { + .v2-mileage-mobile-discovery { + display: grid; + min-width: 0; + flex: 0 0 auto; + gap: 7px; + } + .v2-mileage-mobile-discovery > .v2-mobile-filter-toggle.semi-button { + min-height: 58px; + margin: 0; + } + .v2-mileage-filter-sidesheet .semi-sidesheet-inner { + width: 100vw !important; + overflow: hidden; + border-radius: 18px 18px 0 0; + background: #f4f7fb; + box-shadow: 0 -20px 56px rgba(25, 45, 72, .2); + } + .v2-mileage-filter-sidesheet .semi-sidesheet-header { + min-height: 68px; + border-bottom: 1px solid #dfe7f0; + background: rgba(255, 255, 255, .98); + padding: 11px 14px; + } + .v2-mileage-filter-sidesheet .semi-sidesheet-body { + overflow: auto; + background: #f4f7fb; + padding: 12px; + overscroll-behavior: contain; + } + .v2-mileage-filter-sidesheet .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-mileage-filter-sidesheet .v2-mileage-filter { + display: grid !important; + min-height: 0; + grid-template-columns: 1fr; + gap: 12px; + border: 1px solid #dfe7f0; + border-radius: 12px; + background: #fff; + padding: 12px; + box-shadow: 0 8px 24px rgba(31, 53, 80, .06); + } + .v2-mileage-filter-sidesheet .v2-mileage-filter > label { + gap: 6px; + color: #5f7188; + font-size: 11px; + } + .v2-mileage-filter-sidesheet .v2-mileage-filter > label > .semi-input-wrapper, + .v2-mileage-filter-sidesheet .v2-mileage-source-trigger.semi-button { + height: 44px; + min-height: 44px; + border-radius: 9px; + } + .v2-mileage-filter-sidesheet .v2-mileage-selection > .semi-input-wrapper .semi-input, + .v2-mileage-filter-sidesheet .v2-mileage-filter > label > .semi-input-wrapper .semi-input { + font-size: 14px; + } + .v2-mileage-filter-sidesheet .v2-mileage-inline-submit { + display: none; + } + .v2-mileage-filter-sidesheet .v2-mileage-source-strategy { + width: 100%; + } + .v2-mileage-filter-sidesheet .v2-mileage-ranges { + height: 44px; + order: 5; + border: 1px solid #e0e7f0; + border-radius: 9px; + background: #f4f7fb; + padding: 4px; + } + .v2-mileage-filter-sidesheet .v2-mileage-ranges .semi-button { + height: 34px; + border-radius: 7px; + font-size: 10px; + } + .v2-mileage-filter-sidesheet .v2-mileage-validation { + margin: 9px 2px 0; + } + .v2-mileage-filter-footer > span { + max-width: 150px; + line-height: 1.4; + } +}