refine Semi UI vehicle directory

This commit is contained in:
lingniu
2026-07-18 13:29:46 +08:00
parent c7123f6fbd
commit 3b06dd86a8
3 changed files with 61 additions and 11 deletions

View File

@@ -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(<QueryClientProvider client={client}><MemoryRouter initialEntries={['/vehicles']} future={ROUTER_FUTURE}><Routes><Route path="/vehicles" element={<VehiclePage />} /><Route path="/vehicles/:vin" element={<VehiclePage />} /></Routes></MemoryRouter></QueryClientProvider>);
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 () => {

View File

@@ -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<number>();
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<Page<VehicleCoverageRow>>({
queryKey: ['vehicle-directory', directoryScope, pageSize, page],
queryFn: ({ signal }) => api.vehicleCoverage(candidateParams, signal),
placeholderData: (previous) => previous,
placeholderData: retainPreviousPageWithinScope<Page<VehicleCoverageRow>>(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 <section className={`v2-vehicle-search-page${syncOpen ? ' has-sync-panel' : ''}${candidatesOpen ? ' has-candidates' : ''}`}>
@@ -132,7 +134,7 @@ export default function VehicleSearchWorkspace() {
aria-expanded={candidatesOpen}
prefix={<IconSearch />}
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() {
/>
</div>
<Button theme="solid" htmlType="submit" icon={<IconArrowRight />} iconPosition="right"></Button>
{candidatesOpen ? <VehicleCandidateList
{candidatesOpen && deferredKeyword ? <VehicleCandidateList
id="v2-vehicle-search-options"
className="v2-vehicle-search-options"
items={options}

View File

@@ -2356,6 +2356,10 @@
margin-top: 8px;
}
.v2-vehicle-search-page.has-candidates > .v2-vehicle-recent-card {
display: none;
}
.v2-vehicle-recent-grid {
grid-template-columns: 1fr;
}