From 5a8eec16f35fc0dfbc68ae0744fc30eeb4a3a74c Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 02:10:11 +0800 Subject: [PATCH] refine Semi UI vehicle discovery workspace --- .../web/src/v2/pages/VehiclePage.test.tsx | 39 ++++- .../apps/web/src/v2/pages/VehiclePage.tsx | 49 +++--- .../apps/web/src/v2/productionEntry.test.ts | 4 +- .../apps/web/src/v2/styles/workspace.css | 139 +++++++++++------- 4 files changed, 152 insertions(+), 79 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx index 8078f86d..a5357144 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx @@ -137,7 +137,7 @@ test('shows deduplicated Semi vehicle candidates and opens the selected VIN', as const vehicles = vi.spyOn(api, 'vehicles').mockResolvedValue({ items: [candidate, { ...candidate }], total: 2, - limit: 8, + limit: 12, offset: 0 }); vi.spyOn(api, 'vehicleDetail').mockResolvedValue(detail); @@ -147,15 +147,16 @@ test('shows deduplicated Semi vehicle candidates and opens the selected VIN', as const view = render(} />} />); - const capabilitySummary = screen.getByLabelText('可查询的数据范围'); - expect(capabilitySummary).toHaveTextContent('统一身份'); - expect(capabilitySummary).toHaveTextContent('实时状态'); - expect(capabilitySummary).toHaveTextContent('来源证据'); + expect(screen.getByRole('heading', { name: '车辆查询', level: 2 })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '车辆定位', level: 5 }).closest('.semi-card')).toHaveClass('v2-vehicle-query-panel', 'v2-workspace-filter-panel'); const input = screen.getByRole('textbox', { name: '搜索车辆' }); expect(input).toHaveAttribute('aria-expanded', 'false'); expect(input).toHaveAttribute('aria-controls', 'v2-vehicle-search-options'); expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument(); await waitFor(() => expect(vehicles).toHaveBeenCalledTimes(1)); + const initialQuery = vehicles.mock.calls[0]?.[0]; + expect(initialQuery).toBeDefined(); + expect(initialQuery!.get('limit')).toBe('12'); expect(screen.getByRole('heading', { name: '最近上报车辆', level: 5 })).toBeInTheDocument(); expect(await screen.findByText('当前显示 1 辆')).toBeInTheDocument(); expect(screen.getAllByRole('listitem', { name: `打开 ${initialRealtime.plate} 车辆档案` })).toHaveLength(1); @@ -180,6 +181,34 @@ test('shows deduplicated Semi vehicle candidates and opens the selected VIN', as workspace.remove(); }); +test('defaults to a compact mobile vehicle directory and keeps eight recent vehicles', async () => { + layout.mobile = true; + const items = Array.from({ length: 10 }, (_, index) => ({ + vin: `LTEST${String(index).padStart(12, '0')}`, + plate: `粤A${String(index).padStart(5, '0')}`, + phone: '', + oem: '测试品牌', + protocol: index % 2 ? 'JT808' : 'GB32960', + online: index < 6, + lastSeen: `2026-07-16 10:00:${String(index).padStart(2, '0')}`, + locationText: '广东省广州市', + bindingScore: 100 + })); + vi.spyOn(api, 'vehicles').mockResolvedValue({ items, total: 10, limit: 12, offset: 0 }); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + const view = render(} />); + + expect(await screen.findByText('当前显示 8 辆')).toBeInTheDocument(); + expect(screen.getAllByRole('listitem', { name: /打开 .* 车辆档案/ })).toHaveLength(8); + expect(view.container.querySelector('.v2-vehicle-query-panel')).toHaveClass('is-mobile-collapsed'); + const toggle = screen.getByRole('button', { name: '修改车辆定位:10 辆授权车辆' }); + expect(toggle).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(toggle); + expect(view.container.querySelector('.v2-vehicle-query-panel')).not.toHaveClass('is-mobile-collapsed'); + expect(screen.getByRole('textbox', { name: '搜索车辆' })).toBeVisible(); +}); + test('keeps the monitor return on the vehicle page and its nested investigation actions', async () => { vi.spyOn(api, 'vehicleDetail').mockResolvedValue(detail); vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [initialRealtime], total: 1, limit: 1, offset: 0 }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index 9cc329d3..5f458f38 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -1,9 +1,9 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { IconAlarm, IconArrowRight, IconBox, IconCalendar, IconClock, IconCopy, - IconChevronRight, IconMapPin, IconSearch, IconTickCircle + IconChevronRight, IconMapPin, IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons'; -import { Button, Card, Descriptions, Empty, Input, List, Select, Table, Tag, Typography } from '@douyinfe/semi-ui'; +import { Button, Card, Descriptions, Empty, Input, List, Select, Table, Tag } from '@douyinfe/semi-ui'; import { FormEvent, lazy, Suspense, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; @@ -17,10 +17,12 @@ import { isValidAMapCoordinate } from '../../integrations/amap'; import { FleetMap } from '../map/FleetMap'; import { InlineError, PageLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; +import { PageHeader } from '../shared/PageHeader'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { VehicleCandidateList } from '../shared/VehicleCandidateList'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; +import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext'; import { useMobileLayout } from '../hooks/useMobileLayout'; @@ -56,10 +58,11 @@ function VehicleSearch() { const [keyword, setKeyword] = useState(''); const [syncOpen, setSyncOpen] = useState(false); const [candidatesOpen, setCandidatesOpen] = useState(false); + const [filtersCollapsed, setFiltersCollapsed] = useState(mobileLayout); const closeTimerRef = useRef(); const deferredKeyword = useDeferredValue(keyword.trim()); const candidateParams = useMemo(() => { - const params = new URLSearchParams({ limit: '8', offset: '0' }); + const params = new URLSearchParams({ limit: '12', offset: '0' }); if (deferredKeyword) params.set('keyword', deferredKeyword); return params; }, [deferredKeyword]); @@ -70,7 +73,7 @@ function VehicleSearch() { gcTime: QUERY_MEMORY.optionGcTime }); const options = useMemo(() => mergeVehicleCandidates(candidates.data?.items ?? []), [candidates.data?.items]); - const quickVehicles = useMemo(() => options.slice(0, mobileLayout ? 5 : 6), [mobileLayout, options]); + const quickVehicles = useMemo(() => options.slice(0, mobileLayout ? 8 : 12), [mobileLayout, options]); useEffect(() => () => window.clearTimeout(closeTimerRef.current), []); const openCandidates = () => { window.clearTimeout(closeTimerRef.current); @@ -94,16 +97,27 @@ function VehicleSearch() { event.preventDefault(); openVehicle(keyword); }; + const vehicleScope = candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权` : '授权范围'; return
- -
- -
- 查询单车数字档案 - 通过车牌、VIN 或终端手机号定位车辆,并查看统一身份、实时状态和来源证据。 -
-
-
+ setSyncOpen((value) => !value)}>{syncOpen ? '收起主档同步' : '批量同步主档'} : null} + /> + setFiltersCollapsed((value) => !value)} + > +
openVehicle(vehicle.vin)} /> : null} -
- 统一身份车牌、VIN 与车辆主档 - 实时状态位置、里程与最新上报 - 来源证据协议质量与字段明细 -
- {canAdminister(session) ? : null} - + {candidates.data ? `当前显示 ${formatZhNumber(quickVehicles.length)} 辆` : '授权范围'}} + actions={} /> {candidates.isFetching && !candidates.data ?
正在读取授权车辆…
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 bbcd9c35..d324114f 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -55,7 +55,7 @@ describe('V2 production entry', () => { expect(source).toContain("from '@douyinfe/semi-ui'"); expect(source).toContain(' { expect(v2Styles).toContain('.v2-track-current-card.semi-card { top: auto; right: 8px; bottom: 126px; left: 8px; width: auto; height: auto;'); expect(workspaceFilterPanelSource).toContain("from './MobileFilterToggle'"); expect(workspaceFilterPanelSource).toContain(' span { - display: grid; - min-width: 0; - grid-template-columns: 30px minmax(0, 1fr); - grid-template-rows: auto auto; - align-items: center; - gap: 2px 9px; - border: 1px solid #e2e9f2; - border-radius: 10px; +.v2-vehicle-search-page > .v2-page-heading, +.v2-vehicle-search-page > .v2-mobile-filter-toggle, +.v2-vehicle-search-page > .v2-vehicle-query-panel, +.v2-vehicle-search-page > .v2-vehicle-recent-card, +.v2-vehicle-search-page > .v2-profile-sync-panel { + box-sizing: border-box; + width: min(1280px, 100%); +} + +.v2-vehicle-query-panel.v2-workspace-filter-panel.semi-card { + overflow: visible; +} + +.v2-vehicle-query-panel .v2-vehicle-search-form { + height: auto; + min-height: 60px; + margin: 0; + border: 0; + border-top: 1px solid #e7edf4; + border-radius: 0 0 12px 12px; background: #fbfcfe; - padding: 10px 12px; + padding: 8px 10px; } -.v2-vehicle-search-capabilities svg { - grid-row: 1 / span 2; - color: var(--v2-blue); - font-size: 18px; +.v2-vehicle-query-panel .v2-vehicle-search-form > .v2-vehicle-search-picker { + min-height: 42px; + border: 1px solid #d7e1ed; + border-radius: 9px; + background: #fff; + padding-inline: 3px; } -.v2-vehicle-search-capabilities b { - grid-column: 2; - overflow: hidden; - color: #33475e; - font-size: 11px; - text-overflow: ellipsis; - white-space: nowrap; +.v2-vehicle-query-panel .v2-vehicle-search-form > .semi-button { + height: 42px; + margin: 0; } -.v2-vehicle-search-capabilities small { - grid-column: 2; - overflow: hidden; - color: #8794a5; - font-size: 9px; - text-overflow: ellipsis; - white-space: nowrap; +.v2-vehicle-query-panel .v2-vehicle-search-options.v2-vehicle-candidate-list { + top: calc(100% + 8px); + left: 10px; + width: calc(100% - 20px); +} + +.v2-vehicle-recent-card.semi-card { + width: min(1280px, 100%); +} + +.v2-vehicle-recent-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.v2-vehicle-recent-item.semi-button { + min-height: 78px; +} + +@media (max-width: 1100px) { + .v2-vehicle-recent-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } } .v2-vehicle-record-page { @@ -1553,32 +1578,42 @@ } @media (max-width: 700px) { - .v2-vehicle-search-capabilities { - gap: 6px; - margin-top: 13px; + .v2-vehicle-search-page { + gap: 9px; + padding: 9px 8px 92px; } - .v2-vehicle-search-capabilities > span { - display: flex; - min-height: 44px; - justify-content: center; - gap: 6px; - padding: 7px 5px; + .v2-vehicle-query-panel .v2-vehicle-search-form { + min-height: 0; + border-top: 0; + padding: 9px; } - .v2-vehicle-search-capabilities svg { - flex: 0 0 auto; - font-size: 15px; - } - - .v2-vehicle-search-capabilities b { - font-size: 10px; - } - - .v2-vehicle-search-capabilities small { + .v2-vehicle-query-panel .v2-vehicle-search-form.is-mobile-collapsed { display: none; } + .v2-vehicle-query-panel .v2-vehicle-search-form > .v2-vehicle-search-picker { + width: 100%; + min-height: 46px; + flex: 1 1 100%; + } + + .v2-vehicle-query-panel .v2-vehicle-search-form > .semi-button { + width: 100%; + height: 44px; + justify-content: center; + } + + .v2-vehicle-query-panel .v2-vehicle-search-options.v2-vehicle-candidate-list { + width: 100%; + margin-top: 8px; + } + + .v2-vehicle-recent-grid { + grid-template-columns: 1fr; + } + .v2-track-detail-sidesheet .semi-sidesheet-inner { overflow: hidden; border-radius: 18px 18px 0 0;