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 516801f5..e851cfac 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 @@ -181,9 +181,11 @@ test('shows deduplicated Semi vehicle candidates and opens the selected VIN', as expect(view.container.querySelector('.v2-vehicle-discovery-shell')).toContainElement(screen.getByLabelText('查车操作')); expect(screen.getByRole('button', { name: `打开 ${initialRealtime.vin} 车辆档案` })).toBeInTheDocument(); fireEvent.focus(input); - expect(input).toHaveAttribute('aria-expanded', 'true'); + expect(input).toHaveAttribute('aria-expanded', 'false'); + expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument(); fireEvent.change(input, { target: { value: initialRealtime.plate } }); await waitFor(() => expect(vehicles).toHaveBeenCalledTimes(2)); + expect(input).toHaveAttribute('aria-expanded', 'true'); expect(screen.getByRole('heading', { name: '匹配车辆', level: 5 })).toBeInTheDocument(); const option = await screen.findByRole('option', { name: `${initialRealtime.plate} ${initialRealtime.vin} JT808 选择` }); expect(view.container.querySelector('#v2-vehicle-search-options.v2-vehicle-candidate-list')).toBeInTheDocument(); @@ -237,7 +239,49 @@ test('defaults to a compact mobile vehicle directory and keeps eight recent vehi expect(input).toBeVisible(); expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument(); fireEvent.focus(input); - expect(view.container.querySelector('.v2-vehicle-search-options')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument(); + fireEvent.change(input, { target: { value: items[0].plate } }); + await waitFor(() => expect(view.container.querySelector('.v2-vehicle-search-options')).toBeInTheDocument()); + expect(view.container.querySelector('.v2-vehicle-search-page')).toHaveClass('has-candidates'); + fireEvent.click(screen.getByRole('button', { name: /查询车辆/ })); + expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument(); + expect(view.container.querySelector('.v2-vehicle-search-page')).not.toHaveClass('has-candidates'); + expect(screen.getByRole('heading', { name: '匹配车辆', level: 5 })).toBeInTheDocument(); +}); + +test('keeps a partial vehicle query in the directory instead of navigating to an invalid detail route', async () => { + const candidate = { + vin: initialRealtime.vin, + plate: initialRealtime.plate, + phone: '13800000000', + oem: '测试品牌', + protocols: ['JT808'], + missingProtocols: [], + sourceStatus: [], + sourceCount: 1, + onlineSourceCount: 1, + online: true, + lastSeen: initialRealtime.lastSeen, + locationText: '广东省广州市', + bindingScore: 100, + bindingStatus: 'bound' + }; + vi.spyOn(api, 'vehicleCoverage').mockResolvedValue({ items: [candidate], total: 1, limit: 10, offset: 0 }); + const vehicleDetail = vi.spyOn(api, 'vehicleDetail'); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + const view = render(} />} />); + + await screen.findByRole('heading', { name: '授权车辆目录', level: 5 }); + const input = screen.getByRole('textbox', { name: '搜索车辆' }); + fireEvent.change(input, { target: { value: '粤A' } }); + expect(await screen.findByRole('listbox')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /查询车辆/ })); + + expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '匹配车辆', level: 5 })).toBeInTheDocument(); + expect(vehicleDetail).not.toHaveBeenCalled(); }); test('paginates the complete desktop authorized vehicle directory', async () => { diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx index 99f0d604..5372f326 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx @@ -4,12 +4,12 @@ import { Button, Card, Empty, Input, Table, Tag } from '@douyinfe/semi-ui'; import { type FormEvent, lazy, Suspense, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { api } from '../../api/client'; -import type { VehicleCoverageRow } from '../../api/types'; +import type { Page, VehicleCoverageRow } from '../../api/types'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister } from '../auth/session'; import { formatZhNumber } from '../domain/formatters'; import { useMobileLayout } from '../hooks/useMobileLayout'; -import { QUERY_MEMORY } from '../queryPolicy'; +import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy'; import { TablePagination } from '../shared/TablePagination'; import { VehicleCandidateList } from '../shared/VehicleCandidateList'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; @@ -54,15 +54,16 @@ export default function VehicleSearchWorkspace() { const closeTimerRef = useRef(); const deferredKeyword = useDeferredValue(keyword.trim()); const pageSize = mobileLayout ? 8 : 10; + const directoryScope = useMemo(() => queryScopeKey({ keyword: deferredKeyword }), [deferredKeyword]); const candidateParams = useMemo(() => { const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) }); if (deferredKeyword) params.set('keyword', deferredKeyword); return params; }, [deferredKeyword, page, pageSize]); - const candidates = useQuery({ - queryKey: ['vehicle-directory', candidateParams.toString()], + const candidates = useQuery>({ + queryKey: ['vehicle-directory', directoryScope, pageSize, page], queryFn: ({ signal }) => api.vehicleCoverage(candidateParams, signal), - placeholderData: (previous) => previous, + placeholderData: retainPreviousPageWithinScope>(directoryScope), staleTime: 30_000, gcTime: QUERY_MEMORY.optionGcTime }); @@ -80,7 +81,7 @@ export default function VehicleSearchWorkspace() { }, [mobileLayout]); const openCandidates = () => { window.clearTimeout(closeTimerRef.current); - setCandidatesOpen(true); + setCandidatesOpen(Boolean(keyword.trim())); }; const closeCandidates = () => { window.clearTimeout(closeTimerRef.current); @@ -99,7 +100,8 @@ export default function VehicleSearchWorkspace() { }; const submit = (event: FormEvent) => { event.preventDefault(); - openVehicle(keyword); + setCandidatesOpen(false); + if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); }; const vehicleScope = candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权` : '授权范围'; return
@@ -132,7 +134,7 @@ export default function VehicleSearchWorkspace() { aria-expanded={candidatesOpen} prefix={} value={keyword} - onChange={(value) => { setKeyword(value); setPage(1); setCandidatesOpen(true); }} + onChange={(value) => { setKeyword(value); setPage(1); setCandidatesOpen(Boolean(value.trim())); }} onFocus={openCandidates} onBlur={closeCandidates} placeholder="输入车牌 / VIN / 终端手机号" @@ -140,7 +142,7 @@ export default function VehicleSearchWorkspace() { /> - {candidatesOpen ? .v2-vehicle-recent-card { + display: none; + } + .v2-vehicle-recent-grid { grid-template-columns: 1fr; }