From 2d987fd1859c04aa70ae837974461d83d6c94d35 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 19 Jul 2026 14:19:43 +0800 Subject: [PATCH] refactor: polish semi mileage workspace --- .../web/src/v2/pages/StatisticsPage.test.tsx | 13 +- .../apps/web/src/v2/pages/StatisticsPage.tsx | 73 +++++- .../apps/web/src/v2/styles/workspace.css | 208 ++++++++++++++---- 3 files changed, 245 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 d0b5693d..ec6e5b15 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 @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, expect, test, vi } from 'vitest'; import { MemoryRouter } from 'react-router-dom'; -import StatisticsPage from './StatisticsPage'; +import StatisticsPage, { mileageRangeContainsDate } from './StatisticsPage'; import { buildMonitorPath, withMonitorReturn } from '../routing/monitorContext'; import { ROUTER_FUTURE } from '../routing/routerConfig'; @@ -37,6 +37,13 @@ function prepareData() { mocks.vehicleCoverage.mockResolvedValue({ items: [{ vin: 'LTEST000000000001', plate: '粤A12345' }], total: 1, limit: 20, offset: 0 }); } +test('refreshes only ranges that include the current business date', () => { + expect(mileageRangeContainsDate({ dateFrom: '2026-07-13', dateTo: '2026-07-19' }, '2026-07-19')).toBe(true); + expect(mileageRangeContainsDate({ dateFrom: '2026-07-19', dateTo: '2026-07-19' }, '2026-07-19')).toBe(true); + expect(mileageRangeContainsDate({ dateFrom: '2026-07-13', dateTo: '2026-07-18' }, '2026-07-19')).toBe(false); + expect(mileageRangeContainsDate({ dateFrom: '2026-07-20', dateTo: '2026-07-20' }, '2026-07-19')).toBe(false); +}); + test('renders only the desktop matrix with dates as columns and a period total', async () => { prepareData(); const view = renderPage('/statistics?vins=LTEST000000000001&dateFrom=2026-07-13&dateTo=2026-07-14'); @@ -53,6 +60,10 @@ test('renders only the desktop matrix with dates as columns and a period total', expect(screen.getByLabelText('里程查询工作区')).toHaveTextContent('GB32960 优先'); expect(screen.getByRole('heading', { name: '里程范围', level: 5 })).toBeInTheDocument(); expect(screen.getByText('车牌、自然日与里程数据源策略')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-mileage-date-range.semi-datepicker')).toBeInTheDocument(); + expect(screen.getByRole('group', { name: '快捷日期范围' })).toHaveClass('semi-button-group'); + expect(screen.getByPlaceholderText('开始日期')).toHaveValue('2026-07-13'); + expect(screen.getByPlaceholderText('结束日期')).toHaveValue('2026-07-14'); expect(screen.getAllByText('1 辆 · 2 天').some((element) => element.closest('.semi-tag'))).toBe(true); expect(screen.queryByText('当日起始里程')).not.toBeInTheDocument(); expect(screen.queryByText('数据来源')).not.toBeInTheDocument(); 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 8bb7a3cf..d5597c91 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx @@ -1,5 +1,5 @@ import { IconArrowDown, IconArrowUp, IconClose, IconDownload, IconInfoCircle, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons'; -import { Button, Card, Empty, Input, Spin, Switch, Table, Tag } from '@douyinfe/semi-ui'; +import { Button, ButtonGroup, Card, DatePicker, Empty, Input, Progress, Spin, Switch, Table, Tag } from '@douyinfe/semi-ui'; import { useQuery } from '@tanstack/react-query'; import { type CSSProperties, FormEvent, type KeyboardEvent, memo, type RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; @@ -30,6 +30,7 @@ const PAGE_SIZE = 20; const EXPORT_VEHICLE_PAGE_SIZE = 2_000; const EXPORT_VIN_BATCH_SIZE = 50; const MAX_MILEAGE_RANGE_DAYS = 366; +const CURRENT_DAY_REFRESH_MS = 60_000; type VehicleOption = Pick; type MileageProtocol = 'GB32960' | 'JT808' | 'YUTONG_MQTT'; @@ -58,6 +59,13 @@ function isSameRange(criteria: Pick, range: { d return criteria.dateFrom === range.dateFrom && criteria.dateTo === range.dateTo; } +export function mileageRangeContainsDate( + range: { dateFrom: string; dateTo: string }, + date = localDate() +) { + return Boolean(range.dateFrom && range.dateTo && range.dateFrom <= date && range.dateTo >= date); +} + function formatKm(value?: number) { if (value == null || !Number.isFinite(value)) return '—'; return formatZhNumber(value, 1); @@ -77,6 +85,20 @@ function mileageDateRangeError(criteria: Pick) return ''; } +function datePickerRange(dateValue?: Date | Date[] | string | string[], formattedValue?: string | string[] | Date | Date[]) { + const values = Array.isArray(formattedValue) && formattedValue.length === 2 + ? formattedValue + : Array.isArray(dateValue) && dateValue.length === 2 ? dateValue : []; + if (values.length !== 2) return; + const normalized = values.map((value) => { + if (value instanceof Date) return localDate(value); + if (typeof value === 'number') return localDate(new Date(value)); + return String(value).slice(0, 10); + }); + if (normalized.some((value) => !/^\d{4}-\d{2}-\d{2}$/.test(value))) return; + return { dateFrom: normalized[0], dateTo: normalized[1] }; +} + function mileageParams(criteria: Criteria, offset = 0) { const params = new URLSearchParams({ dateFrom: criteria.dateFrom, dateTo: criteria.dateTo }); if (criteria.vehicles.length) params.set('vins', criteria.vehicles.map((vehicle) => vehicle.vin).join(',')); @@ -441,8 +463,27 @@ export default function StatisticsPage() { const statisticsScope = statisticsParams.toString(); const rowsCriteria = useMemo(() => ({ ...criteria, vehicles: displayVehicles }), [criteria, displayVehicles]); const rowsParams = useMemo(() => mileageParams(rowsCriteria, 0), [rowsCriteria]); - const statistics = useQuery({ queryKey: ['mileage-statistics', statisticsScope], queryFn: ({ signal }) => api.mileageStatistics(statisticsParams, signal), enabled: !criteriaError, staleTime: 60_000, gcTime: QUERY_MEMORY.summaryGcTime }); - const mileage = useQuery>({ queryKey: ['daily-mileage-query', statisticsScope, rowsParams.toString()], queryFn: ({ signal }) => api.dailyMileage(rowsParams, signal), enabled: !criteriaError && displayVehicles.length > 0, staleTime: 60_000, gcTime: QUERY_MEMORY.highVolumeGcTime, placeholderData: retainPreviousPageWithinScope>(statisticsScope, 2) }); + const currentDayRange = !criteriaError && mileageRangeContainsDate(criteria); + const currentDayRefreshInterval = currentDayRange ? CURRENT_DAY_REFRESH_MS : false; + const statistics = useQuery({ + queryKey: ['mileage-statistics', statisticsScope], + queryFn: ({ signal }) => api.mileageStatistics(statisticsParams, signal), + enabled: !criteriaError, + staleTime: CURRENT_DAY_REFRESH_MS, + gcTime: QUERY_MEMORY.summaryGcTime, + refetchInterval: currentDayRefreshInterval, + refetchIntervalInBackground: false + }); + const mileage = useQuery>({ + queryKey: ['daily-mileage-query', statisticsScope, rowsParams.toString()], + queryFn: ({ signal }) => api.dailyMileage(rowsParams, signal), + enabled: !criteriaError && displayVehicles.length > 0, + staleTime: CURRENT_DAY_REFRESH_MS, + gcTime: QUERY_MEMORY.highVolumeGcTime, + placeholderData: retainPreviousPageWithinScope>(statisticsScope, 2), + refetchInterval: currentDayRefreshInterval, + refetchIntervalInBackground: false + }); const dates = useMemo(() => criteriaError ? [] : rangeDates(criteria.dateFrom, criteria.dateTo), [criteria.dateFrom, criteria.dateTo, criteriaError]); const mileageByVin = useMemo(() => { const index = new Map; sources: Map }>(); @@ -592,11 +633,27 @@ export default function StatisticsPage() { 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; @@ -668,7 +725,9 @@ export default function StatisticsPage() { /> {exportProgress ?
{exportProgress.label}{exportPercent == null ? '处理中' : `${exportPercent}%`} - + {exportPercent == null + ? + : }
: null} {!resultsLoading && displayVehicles.length ? : null} {resultsLoading ? : displayVehicles.length ? : null} diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index b7a57623..7c5aaef1 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -14,7 +14,7 @@ justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--v2-border); - background: linear-gradient(180deg, #fff 0%, #fbfcfe 100%); + background: #fff; padding: 8px 14px; } @@ -6978,7 +6978,7 @@ gap: 9px; border-bottom: 1px solid #e6ebf2; border-radius: 12px 12px 0 0; - background: linear-gradient(100deg, #f8fbff 0%, #f5f8fc 58%, #fbfcfe 100%); + background: #f8fafc; padding: 7px 12px; } @@ -9982,7 +9982,7 @@ display: flex; width: 100%; max-width: 1480px; - min-height: 52px; + min-height: 48px; flex: 0 0 auto; align-items: center; justify-content: space-between; @@ -9992,7 +9992,7 @@ border-radius: 10px; background: #fff; padding: 7px 9px 7px 14px; - box-shadow: 0 6px 20px rgba(31, 50, 76, .035); + box-shadow: 0 4px 14px rgba(31, 50, 76, .03); } .v2-workspace-command-copy { @@ -10642,8 +10642,8 @@ /* Final cascade layer for the shared command hierarchy. */ .v2-workspace-command-bar { box-sizing: border-box; - height: 52px; - min-height: 52px; + height: 48px; + min-height: 48px; padding-block: 5px; } @@ -14541,8 +14541,8 @@ * operations workspaces. */ .v2-mileage-page { - gap: 10px; - padding: 14px 18px 16px; + gap: 8px; + padding: 12px 16px 14px; } .v2-mileage-discovery-shell { @@ -14552,9 +14552,9 @@ gap: 0; overflow: visible; border: 1px solid #d8e3ef; - border-radius: 14px; + border-radius: 12px; background: #fff; - box-shadow: 0 9px 28px rgba(31, 53, 80, .055); + box-shadow: 0 7px 22px rgba(31, 53, 80, .045); } .v2-mileage-discovery-shell > .v2-mileage-command-bar { @@ -14564,7 +14564,7 @@ min-height: 46px; border: 0; border-bottom: 1px solid #e3eaf2; - border-radius: 14px 14px 0 0; + border-radius: 12px 12px 0 0; padding-inline: 13px 10px; box-shadow: none; } @@ -14579,7 +14579,7 @@ width: 100%; overflow: visible; border: 0; - border-radius: 0 0 14px 14px; + border-radius: 0 0 12px 12px; box-shadow: none; } @@ -14590,23 +14590,67 @@ .v2-mileage-query-panel .v2-workspace-filter-heading { min-height: 44px; border-bottom-color: #e3eaf2; - background: linear-gradient(100deg, #f7faff 0%, #f9fbfd 58%, #fff 100%); + background: #f8fafc; padding-inline: 13px; } .v2-mileage-query-panel .v2-mileage-filter { - gap: 10px; + grid-template-columns: minmax(220px, 1.15fr) minmax(224px, .95fr) auto auto minmax(228px, auto); + align-items: end; + gap: 8px; background: #fff; - padding: 11px 12px 12px; + padding: 8px 10px; } .v2-mileage-filter > label { - gap: 5px; + gap: 4px; color: #607187; - font-size: 12px; + font-size: 11px; font-weight: 650; } +.v2-mileage-filter > label > span:first-child { + min-height: 14px; + font-size: 10px; + line-height: 14px; +} + +.v2-mileage-query-panel .v2-mileage-vehicle-field > small { + display: none; +} + +.v2-mileage-date-range.semi-datepicker { + width: 100%; + min-height: 38px; +} + +.v2-mileage-date-range .semi-datepicker-input { + width: 100%; +} + +.v2-mileage-date-range .semi-input-wrapper { + min-height: 38px; + border-color: #d8e2ed; + border-radius: 8px; + background: #fbfcfe; +} + +.v2-mileage-date-range .semi-input-wrapper:hover { + border-color: #b9c9dc; + background: #fff; +} + +.v2-mileage-date-range .semi-input-wrapper-focus { + border-color: #75a5ed; + background: #fff; + box-shadow: 0 0 0 3px rgba(37, 99, 235, .09); +} + +.v2-mileage-date-range .semi-input { + font-size: 12px; + font-variant-numeric: tabular-nums; +} + .v2-mileage-multiselect, .v2-mileage-filter > label > .semi-input-wrapper { min-height: 38px; @@ -14644,6 +14688,11 @@ box-shadow: 0 4px 12px rgba(18, 104, 223, .16); } +.v2-mileage-inline-submit.semi-button, +.v2-mileage-source-strategy { + margin-top: 0; +} + .v2-mileage-source-trigger.semi-button { min-height: 38px; border-color: #d8e3ef; @@ -14674,6 +14723,12 @@ padding: 3px; } +.v2-mileage-ranges > .semi-button-group { + display: flex; + min-width: 0; + gap: 2px; +} + .v2-mileage-ranges .semi-button { min-height: 30px; border-radius: 6px; @@ -14741,9 +14796,9 @@ } .v2-mileage-results > .semi-card-body > .v2-workspace-panel-header { - min-height: 50px; + min-height: 46px; border-bottom-color: #e2e9f1; - background: linear-gradient(90deg, #fff 0%, #fbfcfe 100%); + background: #fff; padding-inline: 14px; } @@ -14772,7 +14827,7 @@ align-items: center; gap: 14px; border-bottom: 1px solid #e2e9f1; - background: linear-gradient(90deg, #f9fbfe 0%, #fff 100%); + background: #f8fafc; padding: 5px 14px; color: #6c7d92; font-size: 11px; @@ -14897,11 +14952,25 @@ } .v2-mileage-results > .semi-card-body > footer { - min-height: 40px; + min-height: 36px; border-top-color: #e3eaf2; background: #fbfcfe; } +.v2-mileage-export-progress { + min-height: 40px; + padding: 7px 14px; +} + +.v2-mileage-export-progress > .v2-mileage-export-bar { + min-width: 160px; + flex: 1 1 auto; +} + +.v2-mileage-export-progress > .v2-mileage-export-spinner { + margin-left: auto; +} + .v2-mileage-evidence { min-height: 20px; align-items: center; @@ -14936,11 +15005,31 @@ @media (min-width: 681px) and (max-width: 1050px) { .v2-mileage-query-panel .v2-mileage-filter { + grid-template-columns: minmax(200px, 1.25fr) minmax(220px, 1fr) auto; + grid-template-areas: + "vehicle date submit" + "source ranges ranges"; row-gap: 10px; } - .v2-mileage-summary > .semi-card-body { - grid-template-columns: minmax(285px, 1fr) minmax(0, 2fr); + .v2-mileage-query-panel .v2-mileage-vehicle-field { + grid-area: vehicle; + } + + .v2-mileage-query-panel .v2-mileage-date-field { + grid-area: date; + } + + .v2-mileage-query-panel .v2-mileage-filter > .v2-primary-button { + grid-area: submit; + } + + .v2-mileage-query-panel .v2-mileage-source-strategy { + grid-area: source; + } + + .v2-mileage-query-panel .v2-mileage-ranges { + grid-area: ranges; } } @@ -14970,6 +15059,7 @@ } .v2-mileage-filter > label > .semi-input-wrapper, + .v2-mileage-date-range .semi-input-wrapper, .v2-mileage-multiselect, .v2-mileage-source-trigger.semi-button, .v2-mileage-filter > .v2-primary-button.semi-button { @@ -14981,6 +15071,30 @@ border-radius: 10px; } + .v2-mileage-query-panel .v2-mileage-vehicle-field > small { + display: block; + } + + .v2-mileage-date-range.semi-datepicker, + .v2-mileage-date-range .semi-datepicker-input, + .v2-mileage-date-range .semi-input-wrapper { + width: 100%; + } + + .v2-mileage-ranges > .semi-button-group { + width: 100%; + overflow-x: auto; + scrollbar-width: none; + } + + .v2-mileage-ranges > .semi-button-group::-webkit-scrollbar { + display: none; + } + + .v2-mileage-ranges > .semi-button-group > .semi-button { + flex: 1 0 auto; + } + .v2-mileage-summary-secondary .v2-mileage-summary-card strong { font-size: 16px; } @@ -15645,7 +15759,7 @@ .v2-workspace-metric-rail.is-queue > .semi-card-body { display: grid; - min-height: 58px; + min-height: 54px; grid-template-columns: minmax(0, 1fr); } @@ -15662,9 +15776,9 @@ .v2-workspace-metric-list > span { display: grid; min-width: 0; - min-height: 58px; + min-height: 54px; grid-template-columns: auto minmax(0, 1fr); - grid-template-rows: auto auto; + grid-template-rows: 14px 24px; align-content: center; column-gap: 10px; padding: 7px 13px; @@ -15681,7 +15795,7 @@ .v2-workspace-metric-action.semi-button { width: 100%; height: 100%; - min-height: 58px; + min-height: 54px; border-radius: 0; padding: 0; color: inherit; @@ -15700,9 +15814,9 @@ display: grid; width: 100%; min-width: 0; - min-height: 58px; + min-height: 54px; grid-template-columns: auto minmax(0, 1fr); - grid-template-rows: auto auto; + grid-template-rows: 14px 24px; align-content: center; column-gap: 10px; padding: 7px 13px; @@ -15717,6 +15831,7 @@ font-weight: 650; line-height: 14px; white-space: nowrap; + margin: 0; } .v2-workspace-metric-list > span > strong, @@ -15727,6 +15842,7 @@ font-variant-numeric: tabular-nums; line-height: 24px; white-space: nowrap; + margin: 0; } .v2-workspace-metric-list > span > em, @@ -15741,6 +15857,7 @@ line-height: 14px; text-overflow: ellipsis; white-space: nowrap; + margin: 0; } .v2-workspace-metric-list > span.is-secondary > strong, @@ -15749,8 +15866,8 @@ } .v2-workspace-metric-list > span.is-primary { - background: linear-gradient(145deg, #edf5ff 0%, #f8fbff 100%); - box-shadow: inset 0 -3px #1f6feb; + background: #f5f9ff; + box-shadow: inset 3px 0 #3b82f6; } .v2-workspace-metric-list > span.is-primary > :is(small, strong), @@ -15759,8 +15876,8 @@ } .v2-workspace-metric-list > span.is-danger { - background: linear-gradient(145deg, #fff 0%, #fff8f7 100%); - box-shadow: inset 0 -3px #e2554f; + background: #fff8f7; + box-shadow: inset 3px 0 #e2554f; } .v2-workspace-metric-list > span.is-danger > :is(small, strong), @@ -15784,21 +15901,21 @@ } .v2-workspace-metric-list > span.is-action.is-danger { - background: linear-gradient(145deg, #fff 0%, #fff8f7 100%); + background: #fff8f7; } .v2-workspace-metric-list > span.is-action.is-warning { - background: linear-gradient(145deg, #fff 0%, #fffcf5 100%); + background: #fffcf5; } .v2-workspace-metric-list > span.is-action.is-active { - background: linear-gradient(145deg, #edf5ff 0%, #f8fbff 100%); - box-shadow: inset 0 -3px #1f6feb; + background: #f5f9ff; + box-shadow: inset 3px 0 #3b82f6; } .v2-workspace-metric-list > span.is-action.is-active.is-danger { - background: linear-gradient(145deg, #fff1ef 0%, #fff8f7 100%); - box-shadow: inset 0 -3px #e2554f; + background: #fff4f2; + box-shadow: inset 3px 0 #e2554f; } .v2-workspace-metric-context { @@ -23245,7 +23362,7 @@ .v2-mileage-summary.v2-workspace-metric-rail.is-queue > .semi-card-body { display: grid; height: auto; - min-height: 58px; + min-height: 54px; grid-template-columns: minmax(0, 1fr); gap: 0; } @@ -23256,7 +23373,12 @@ .v2-mileage-summary .v2-workspace-metric-list > span { display: grid; - min-height: 58px; + min-height: 54px; +} + +.v2-mileage-summary .v2-workspace-metric-list > span > :is(small, strong, em), +.v2-mileage-summary .v2-workspace-metric-action .semi-button-content > :is(small, strong, em) { + margin: 0; } .v2-mileage-summary-unit { @@ -23290,6 +23412,10 @@ .v2-mileage-summary .v2-workspace-metric-list > span { min-height: 56px; } + + .v2-mileage-summary .v2-workspace-metric-list > span.is-secondary { + display: none; + } } /*