refine Semi UI vehicle directory
This commit is contained in:
@@ -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(view.container.querySelector('.v2-vehicle-discovery-shell')).toContainElement(screen.getByLabelText('查车操作'));
|
||||||
expect(screen.getByRole('button', { name: `打开 ${initialRealtime.vin} 车辆档案` })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: `打开 ${initialRealtime.vin} 车辆档案` })).toBeInTheDocument();
|
||||||
fireEvent.focus(input);
|
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 } });
|
fireEvent.change(input, { target: { value: initialRealtime.plate } });
|
||||||
await waitFor(() => expect(vehicles).toHaveBeenCalledTimes(2));
|
await waitFor(() => expect(vehicles).toHaveBeenCalledTimes(2));
|
||||||
|
expect(input).toHaveAttribute('aria-expanded', 'true');
|
||||||
expect(screen.getByRole('heading', { name: '匹配车辆', level: 5 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '匹配车辆', level: 5 })).toBeInTheDocument();
|
||||||
const option = await screen.findByRole('option', { name: `${initialRealtime.plate} ${initialRealtime.vin} JT808 选择` });
|
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();
|
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(input).toBeVisible();
|
||||||
expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument();
|
expect(view.container.querySelector('.v2-vehicle-search-options')).not.toBeInTheDocument();
|
||||||
fireEvent.focus(input);
|
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 () => {
|
test('paginates the complete desktop authorized vehicle directory', async () => {
|
||||||
|
|||||||
@@ -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 { type FormEvent, lazy, Suspense, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import type { VehicleCoverageRow } from '../../api/types';
|
import type { Page, VehicleCoverageRow } from '../../api/types';
|
||||||
import { usePlatformSession } from '../auth/AuthGate';
|
import { usePlatformSession } from '../auth/AuthGate';
|
||||||
import { canAdminister } from '../auth/session';
|
import { canAdminister } from '../auth/session';
|
||||||
import { formatZhNumber } from '../domain/formatters';
|
import { formatZhNumber } from '../domain/formatters';
|
||||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { QUERY_MEMORY } from '../queryPolicy';
|
import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||||
import { TablePagination } from '../shared/TablePagination';
|
import { TablePagination } from '../shared/TablePagination';
|
||||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||||
@@ -54,15 +54,16 @@ export default function VehicleSearchWorkspace() {
|
|||||||
const closeTimerRef = useRef<number>();
|
const closeTimerRef = useRef<number>();
|
||||||
const deferredKeyword = useDeferredValue(keyword.trim());
|
const deferredKeyword = useDeferredValue(keyword.trim());
|
||||||
const pageSize = mobileLayout ? 8 : 10;
|
const pageSize = mobileLayout ? 8 : 10;
|
||||||
|
const directoryScope = useMemo(() => queryScopeKey({ keyword: deferredKeyword }), [deferredKeyword]);
|
||||||
const candidateParams = useMemo(() => {
|
const candidateParams = useMemo(() => {
|
||||||
const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) });
|
const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) });
|
||||||
if (deferredKeyword) params.set('keyword', deferredKeyword);
|
if (deferredKeyword) params.set('keyword', deferredKeyword);
|
||||||
return params;
|
return params;
|
||||||
}, [deferredKeyword, page, pageSize]);
|
}, [deferredKeyword, page, pageSize]);
|
||||||
const candidates = useQuery({
|
const candidates = useQuery<Page<VehicleCoverageRow>>({
|
||||||
queryKey: ['vehicle-directory', candidateParams.toString()],
|
queryKey: ['vehicle-directory', directoryScope, pageSize, page],
|
||||||
queryFn: ({ signal }) => api.vehicleCoverage(candidateParams, signal),
|
queryFn: ({ signal }) => api.vehicleCoverage(candidateParams, signal),
|
||||||
placeholderData: (previous) => previous,
|
placeholderData: retainPreviousPageWithinScope<Page<VehicleCoverageRow>>(directoryScope),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
gcTime: QUERY_MEMORY.optionGcTime
|
gcTime: QUERY_MEMORY.optionGcTime
|
||||||
});
|
});
|
||||||
@@ -80,7 +81,7 @@ export default function VehicleSearchWorkspace() {
|
|||||||
}, [mobileLayout]);
|
}, [mobileLayout]);
|
||||||
const openCandidates = () => {
|
const openCandidates = () => {
|
||||||
window.clearTimeout(closeTimerRef.current);
|
window.clearTimeout(closeTimerRef.current);
|
||||||
setCandidatesOpen(true);
|
setCandidatesOpen(Boolean(keyword.trim()));
|
||||||
};
|
};
|
||||||
const closeCandidates = () => {
|
const closeCandidates = () => {
|
||||||
window.clearTimeout(closeTimerRef.current);
|
window.clearTimeout(closeTimerRef.current);
|
||||||
@@ -99,7 +100,8 @@ export default function VehicleSearchWorkspace() {
|
|||||||
};
|
};
|
||||||
const submit = (event: FormEvent) => {
|
const submit = (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openVehicle(keyword);
|
setCandidatesOpen(false);
|
||||||
|
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
||||||
};
|
};
|
||||||
const vehicleScope = candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权` : '授权范围';
|
const vehicleScope = candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权` : '授权范围';
|
||||||
return <section className={`v2-vehicle-search-page${syncOpen ? ' has-sync-panel' : ''}${candidatesOpen ? ' has-candidates' : ''}`}>
|
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}
|
aria-expanded={candidatesOpen}
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={(value) => { setKeyword(value); setPage(1); setCandidatesOpen(true); }}
|
onChange={(value) => { setKeyword(value); setPage(1); setCandidatesOpen(Boolean(value.trim())); }}
|
||||||
onFocus={openCandidates}
|
onFocus={openCandidates}
|
||||||
onBlur={closeCandidates}
|
onBlur={closeCandidates}
|
||||||
placeholder="输入车牌 / VIN / 终端手机号"
|
placeholder="输入车牌 / VIN / 终端手机号"
|
||||||
@@ -140,7 +142,7 @@ export default function VehicleSearchWorkspace() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button theme="solid" htmlType="submit" icon={<IconArrowRight />} iconPosition="right">查询车辆</Button>
|
<Button theme="solid" htmlType="submit" icon={<IconArrowRight />} iconPosition="right">查询车辆</Button>
|
||||||
{candidatesOpen ? <VehicleCandidateList
|
{candidatesOpen && deferredKeyword ? <VehicleCandidateList
|
||||||
id="v2-vehicle-search-options"
|
id="v2-vehicle-search-options"
|
||||||
className="v2-vehicle-search-options"
|
className="v2-vehicle-search-options"
|
||||||
items={options}
|
items={options}
|
||||||
|
|||||||
@@ -2356,6 +2356,10 @@
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-search-page.has-candidates > .v2-vehicle-recent-card {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-vehicle-recent-grid {
|
.v2-vehicle-recent-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user