From 4877c3fe8f27f09f7f8f2c7a0e085e29b6a01a53 Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 16 Jul 2026 12:53:32 +0800 Subject: [PATCH] feat(mileage): compact mobile results and add date presets --- .../web/src/v2/pages/StatisticsPage.test.tsx | 27 ++++++++++ .../apps/web/src/v2/pages/StatisticsPage.tsx | 21 +++++--- .../apps/web/src/v2/styles/v2.css | 52 ++++++++++--------- 3 files changed, 69 insertions(+), 31 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 7b71f8d5..a575a637 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 @@ -58,6 +58,33 @@ test('renders only the desktop matrix with dates as columns and a period total', expect(mocks.dailyMileage.mock.calls[0][0].get('protocols')).toBe('GB32960,JT808,YUTONG_MQTT'); }); +test('defaults to seven days and supports today and yesterday shortcuts', async () => { + prepareData(); + renderPage(); + const localDate = (value: Date) => new Date(value.getTime() - value.getTimezoneOffset() * 60_000).toISOString().slice(0, 10); + const today = localDate(new Date()); + const yesterday = localDate(new Date(Date.now() - 86_400_000)); + const sevenDaysAgo = localDate(new Date(Date.now() - 6 * 86_400_000)); + + await waitFor(() => expect(mocks.mileageStatistics).toHaveBeenCalled()); + expect(mocks.mileageStatistics.mock.calls[0][0].get('dateFrom')).toBe(sevenDaysAgo); + expect(mocks.mileageStatistics.mock.calls[0][0].get('dateTo')).toBe(today); + + fireEvent.click(screen.getByRole('button', { name: '昨天' })); + await waitFor(() => { + const params = mocks.mileageStatistics.mock.calls[mocks.mileageStatistics.mock.calls.length - 1]?.[0]; + expect(params.get('dateFrom')).toBe(yesterday); + expect(params.get('dateTo')).toBe(yesterday); + }); + + fireEvent.click(screen.getByRole('button', { name: '今天' })); + await waitFor(() => { + const params = mocks.mileageStatistics.mock.calls[mocks.mileageStatistics.mock.calls.length - 1]?.[0]; + expect(params.get('dateFrom')).toBe(today); + expect(params.get('dateTo')).toBe(today); + }); +}); + test('rejects overlong mileage ranges before querying or rendering a huge matrix', async () => { prepareData(); const view = renderPage('/statistics?vins=LTEST000000000001&dateFrom=2025-01-01&dateTo=2026-07-14'); 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 4affa28f..073bc20a 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx @@ -36,11 +36,15 @@ function localDate(value = new Date()) { return new Date(value.getTime() - offset).toISOString().slice(0, 10); } -function defaultWindow(days = 30) { - const end = new Date(); +function defaultWindow(days = 7, endOffsetDays = 0) { + const end = new Date(Date.now() - endOffsetDays * DAY); return { dateFrom: localDate(new Date(end.getTime() - (days - 1) * DAY)), dateTo: localDate(end) }; } +function isSameRange(criteria: Pick, range: { dateFrom: string; dateTo: string }) { + return criteria.dateFrom === range.dateFrom && criteria.dateTo === range.dateTo; +} + function formatKm(value?: number) { if (value == null || !Number.isFinite(value)) return '—'; return formatZhNumber(value, 1); @@ -74,7 +78,7 @@ function mileageParams(criteria: Criteria, offset = 0) { } function initialCriteria(searchParams: URLSearchParams): Criteria { - const defaults = defaultWindow(30); + const defaults = defaultWindow(); const vins = (searchParams.get('vins') ?? '').split(',').map((vin) => vin.trim()).filter(Boolean).slice(0, MAX_SELECTED_VEHICLES); const requestedProtocols = (searchParams.get('protocols') ?? '').split(',').filter((protocol): protocol is MileageProtocol => DEFAULT_SOURCES.some((source) => source.protocol === protocol)); let sources = DEFAULT_SOURCES.map((source) => ({ ...source })); @@ -249,7 +253,7 @@ 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.length} 天
{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}
; })}
)}
; + 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); @@ -337,7 +341,12 @@ export default function StatisticsPage() { if (error) return; setPage(1); setExportFeedback(''); setCriteria(draft); setSearchParams(mileageParams(draft, -1), { replace: true }); setFiltersCollapsed(true); }; - const setDays = (days: number) => { const range = defaultWindow(days); const next = { ...draft, ...range }; setPage(1); setExportFeedback(''); setValidationError(''); setDraft(next); setCriteria(next); setSearchParams(mileageParams(next, -1), { replace: true }); }; + const setRange = (range: { dateFrom: string; dateTo: string }) => { const next = { ...draft, ...range }; setPage(1); setExportFeedback(''); setValidationError(''); setDraft(next); setCriteria(next); setSearchParams(mileageParams(next, -1), { replace: true }); }; + const todayRange = defaultWindow(1); + const yesterdayRange = defaultWindow(1, 1); + const sevenDayRange = defaultWindow(7); + const thirtyDayRange = defaultWindow(30); + const ninetyDayRange = defaultWindow(90); const refreshing = statistics.isFetching || mileage.isFetching || fleetVehicles.isFetching; const resultsLoading = fleetVehicles.isLoading || mileage.isLoading || mileage.isPlaceholderData; const exportPercent = exportProgress?.total @@ -432,7 +441,7 @@ export default function StatisticsPage() { setDraft((current) => ({ ...current, sources }))} /> -
快捷范围
+
快捷范围
{validationError || criteriaError ?

{validationError || criteriaError}

: null} 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 62b29ec0..f65df468 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css @@ -1914,34 +1914,36 @@ button, a { -webkit-tap-highlight-color: transparent; } :is(.v2-alert-filter,.v2-access-filter-v3,.v2-history-toolbar,.v2-mileage-filter).is-mobile-collapsed { display: none !important; } .v2-track-page.is-rail-collapsed { display: grid; grid-template-columns: minmax(0,1fr); } - .v2-mileage-page { gap: 10px; padding: 8px 8px 18px; } - .v2-mileage-query-panel { padding: 8px; border-radius: 12px; } - .v2-mileage-query-panel > .v2-mobile-filter-toggle { margin: 0 0 8px; } - .v2-mileage-summary { display: grid; grid-template-columns: 1fr; gap: 8px; border: 0; background: transparent; } - .v2-mileage-summary > .is-primary { min-height: 112px; border-radius: 10px; padding: 16px 17px 17px; } - .v2-mileage-summary > .is-primary strong { margin: 8px 0 5px; font-size: 27px; } + .v2-mileage-page { gap: 7px; padding: 7px 7px 14px; } + .v2-mileage-query-panel { padding: 7px; border-radius: 11px; } + .v2-mileage-query-panel > .v2-mobile-filter-toggle { min-height: 48px; margin: 0 0 7px; padding: 7px 10px; } + .v2-mileage-ranges { grid-template-columns: repeat(5,minmax(0,1fr)); } + .v2-mileage-ranges button { height: 36px; padding: 0 3px; font-size: 10px; } + .v2-mileage-summary { display: grid; grid-template-columns: 1fr; gap: 6px; border: 0; background: transparent; } + .v2-mileage-summary > .is-primary { min-height: 82px; border-radius: 9px; padding: 11px 14px 12px; } + .v2-mileage-summary > .is-primary strong { margin: 4px 0 2px; font-size: 25px; } .v2-mileage-summary-secondary { grid-template-columns: repeat(3,minmax(0,1fr)); overflow: hidden; border: 1px solid #e1e7ef; border-radius: 10px; } - .v2-mileage-summary-secondary article { min-height: 94px; justify-content: flex-start; border-bottom: 0 !important; padding: 13px 11px 12px; } - .v2-mileage-summary-secondary article + article::before { inset: 13px auto 13px 0; } - .v2-mileage-summary-secondary strong { margin: 7px 0 4px; font-size: 17px; } + .v2-mileage-summary-secondary article { min-height: 70px; justify-content: flex-start; border-bottom: 0 !important; padding: 9px 9px 8px; } + .v2-mileage-summary-secondary article + article::before { inset: 9px auto 9px 0; } + .v2-mileage-summary-secondary strong { margin: 4px 0 2px; font-size: 16px; } .v2-mileage-summary-secondary span { overflow: hidden; font-size: 9px; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; } .v2-mileage-results { min-height: 430px; border-radius: 12px; } - .v2-mileage-results > header { gap: 8px; padding: 12px; } - .v2-mileage-results > header strong { font-size: 15px; } + .v2-mileage-results > header { gap: 5px; padding: 9px 10px; } + .v2-mileage-results > header div:first-child { width: 100%; flex-direction: row; align-items: baseline; justify-content: space-between; gap: 8px; } + .v2-mileage-results > header strong { font-size: 14px; } .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: 36px; padding: 0 10px; font-size: 10px; } - .v2-mileage-mobile-list { gap: 9px; background: #f4f7fb; padding: 9px; } - .v2-mileage-mobile-list article { overflow: hidden; border: 1px solid #dfe6ef; border-radius: 10px; background: #fff; padding: 14px 13px 13px; box-shadow: 0 2px 8px rgba(23,39,62,.035); } - .v2-mileage-mobile-list article > header strong { font-size: 15px; } - .v2-mileage-mobile-list article > header b { color: #135fcb; font-size: 19px; letter-spacing: -.025em; } - .v2-mileage-mobile-list article > header b small { margin-top: 4px; font-size: 9px; } - .v2-mileage-mobile-day-heading { display: flex; align-items: center; justify-content: space-between; margin-top: 13px; border-top: 1px solid #edf1f5; padding-top: 11px; } - .v2-mileage-mobile-day-heading span { color: #526176; font-size: 11px; font-weight: 700; } - .v2-mileage-mobile-day-heading em { color: #8b97a8; font-size: 9px; font-style: normal; } - .v2-mileage-mobile-days { grid-auto-columns: 88px; gap: 6px; margin-top: 8px; padding-bottom: 2px; scroll-snap-type: x proximity; } - .v2-mileage-mobile-days > div { min-height: 66px; border-color: #e3e9f1; border-radius: 8px; background: #f8fafc; padding: 8px 9px; scroll-snap-align: start; } - .v2-mileage-mobile-days time { font-size: 9px; } - .v2-mileage-mobile-days strong { margin-top: 6px; color: #26364b; font-size: 11px; } - .v2-mileage-mobile-days small { display: block; margin-top: 4px; overflow: hidden; color: #7d8ba0; font-size: 8px; 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; } }