From 6d6c9ce5344de489dadae239ac6bedaac81369ae Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 16 Jul 2026 13:07:43 +0800 Subject: [PATCH] refactor(mileage): use fixed-column table on mobile --- .../web/src/v2/pages/StatisticsPage.test.tsx | 27 +++++-------------- .../apps/web/src/v2/pages/StatisticsPage.tsx | 15 ----------- .../apps/web/src/v2/styles/v2.css | 27 +++++++++---------- 3 files changed, 20 insertions(+), 49 deletions(-) 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 a575a637..c5607ac9 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 @@ -115,30 +115,17 @@ test('removes the previous mileage scope while a new date range is loading', asy expect(screen.getAllByText('2026-07-10 至 2026-07-16').length).toBeGreaterThan(0); }); -test('mounts only the mobile mileage cards and removes the viewport listener', async () => { +test('uses one responsive mileage matrix without viewport listeners', async () => { prepareData(); - const addEventListener = vi.fn(); - const removeEventListener = vi.fn(); - vi.spyOn(window, 'matchMedia').mockReturnValue({ - matches: true, - media: '(max-width: 680px)', - onchange: null, - addEventListener, - removeEventListener, - addListener: vi.fn(), - removeListener: vi.fn(), - dispatchEvent: vi.fn(() => false) - } as unknown as MediaQueryList); const view = renderPage('/statistics?vins=LTEST000000000001&dateFrom=2026-07-13&dateTo=2026-07-14'); expect(await screen.findByText('车辆每日里程')).toBeInTheDocument(); - await waitFor(() => expect(view.container.querySelectorAll('.v2-mileage-mobile-list article')).toHaveLength(1)); - expect(view.container.querySelector('.v2-mileage-table-wrap')).not.toBeInTheDocument(); - expect(view.container.querySelectorAll('.v2-mileage-mobile-days > div')).toHaveLength(2); - expect(addEventListener).toHaveBeenCalledWith('change', expect.any(Function)); - - view.unmount(); - expect(removeEventListener).toHaveBeenCalledWith('change', expect.any(Function)); + await waitFor(() => expect(view.container.querySelectorAll('.v2-mileage-table tbody tr')).toHaveLength(1)); + expect(view.container.querySelector('.v2-mileage-table-wrap')).toBeInTheDocument(); + expect(view.container.querySelectorAll('.v2-mileage-table th.is-date')).toHaveLength(2); + expect(view.container.querySelector('.v2-mileage-table th.is-plate')).toHaveTextContent('车牌'); + expect(view.container.querySelector('.v2-mileage-table th.is-total')).toHaveTextContent('区间总里程'); + expect(view.container.querySelector('.v2-mileage-mobile-list')).not.toBeInTheDocument(); }); test('supports searching and selecting license plates before querying exact VINs', async () => { 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 073bc20a..c9294dcc 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx @@ -15,7 +15,6 @@ const MAX_SELECTED_VEHICLES = 20; const PAGE_SIZE = 20; const EXPORT_VEHICLE_PAGE_SIZE = 2_000; const EXPORT_VIN_BATCH_SIZE = 50; -const MOBILE_MILEAGE_QUERY = '(max-width: 680px)'; const MAX_MILEAGE_RANGE_DAYS = 366; type VehicleOption = Pick; @@ -223,18 +222,6 @@ function SummaryRail({ data, criteria, fleetTotal, loading }: { data?: MileageSt type VehicleMileageMatrix = VehicleOption & { days: Map; sources: Map; totalMileageKm: number }; -function useMobileMileageLayout() { - const [mobile, setMobile] = useState(() => window.matchMedia(MOBILE_MILEAGE_QUERY).matches); - useEffect(() => { - const media = window.matchMedia(MOBILE_MILEAGE_QUERY); - const update = () => setMobile(media.matches); - media.addEventListener('change', update); - update(); - return () => media.removeEventListener('change', update); - }, []); - return mobile; -} - function rangeDates(dateFrom: string, dateTo: string) { const dates: string[] = []; const cursor = new Date(`${dateFrom}T00:00:00Z`); @@ -252,8 +239,6 @@ function dateLabel(date: string) { } function MileageTable({ rows, dates }: { rows: VehicleMileageMatrix[]; dates: string[] }) { - const mobile = useMobileMileageLayout(); - if (mobile) return
{rows.map((row) =>
{row.plate || '未绑定'}{row.vin}
{formatKm(row.totalMileageKm)} km区间总里程
{dates.map((date) => { const source = row.sources.get(date); return
{row.days.has(date) ? `${formatKm(row.days.get(date))} km` : '—'}{source ? {source === 'YUTONG_MQTT' ? 'YUTONG' : source} : null}
; })}
)}
; let maxDailyMileage = 1; for (const row of rows) { for (const mileage of row.days.values()) maxDailyMileage = Math.max(maxDailyMileage, mileage); diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css index f65df468..bd4bdb27 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css @@ -1332,8 +1332,8 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-mileage-result-actions > button { height: 34px; flex: 0 0 auto; } .v2-mileage-export-progress { align-items: stretch; flex-direction: column; gap: 7px; padding: 9px 12px; } .v2-mileage-export-progress > span { min-width: 0; } - .v2-mileage-table-wrap { display: none; } - .v2-mileage-mobile-list { display: flex; flex-direction: column; } + .v2-mileage-table-wrap { display: block; } + .v2-mileage-mobile-list { display: none; } .v2-mileage-mobile-list article { border-bottom: 1px solid #e9eef5; padding: 14px 12px; } .v2-mileage-mobile-list article > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; } .v2-mileage-mobile-list article > header div { min-width: 0; } @@ -1934,16 +1934,15 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-mileage-results > header .v2-mileage-result-actions { gap: 6px; } .v2-mileage-results > header .v2-mileage-result-actions em { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .v2-mileage-result-actions > button { height: 32px; padding: 0 9px; font-size: 10px; } - .v2-mileage-mobile-list { gap: 5px; background: #f4f7fb; padding: 5px; } - .v2-mileage-mobile-list article { overflow: hidden; border: 1px solid #dfe6ef; border-radius: 8px; background: #fff; padding: 9px 10px 8px; box-shadow: 0 1px 4px rgba(23,39,62,.03); } - .v2-mileage-mobile-list article > header { align-items: center; } - .v2-mileage-mobile-list article > header strong { font-size: 14px; } - .v2-mileage-mobile-list article > header span { margin-top: 1px; font-size: 8px; } - .v2-mileage-mobile-list article > header b { color: #135fcb; font-size: 17px; letter-spacing: -.025em; } - .v2-mileage-mobile-list article > header b small { margin-top: 1px; font-size: 8px; } - .v2-mileage-mobile-days { grid-auto-columns: 70px; gap: 4px; margin-top: 7px; border-top: 1px solid #edf1f5; padding-top: 6px; padding-bottom: 1px; scroll-snap-type: x proximity; } - .v2-mileage-mobile-days > div { min-height: 38px; border: 0; border-radius: 6px; background: #f6f8fb; padding: 5px 7px; scroll-snap-align: start; } - .v2-mileage-mobile-days time { font-size: 8px; } - .v2-mileage-mobile-days strong { margin-top: 3px; color: #26364b; font-size: 10px; } - .v2-mileage-mobile-days small { display: none; } + .v2-mileage-table-wrap { display: block; width: 100%; max-width: 100%; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; } + .v2-mileage-table { width: max-content; min-width: 100%; font-size: 10px; } + .v2-mileage-table th { height: 38px; padding: 0 8px; font-size: 9px; } + .v2-mileage-table td { height: 44px; padding: 0 8px; font-size: 10px; } + .v2-mileage-table .is-vin { display: none; } + .v2-mileage-table .is-plate { left: 0; width: 92px; min-width: 92px; max-width: 92px; border-right: 1px solid #d8e0ea; box-shadow: 5px 0 10px rgba(15,23,42,.035); } + .v2-mileage-table td.is-plate strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .v2-mileage-table .is-date { width: 76px; min-width: 76px; } + .v2-mileage-table .is-total { width: 104px; min-width: 104px; padding-right: 10px; } + .v2-mileage-table .is-daily { font-size: 10px; } + .v2-mileage-table .is-period { font-size: 11px; } }