feat(web): add vehicle directory views

This commit is contained in:
lingniu
2026-07-20 07:58:45 +08:00
parent 3c9eea527c
commit 19b187d942
4 changed files with 185 additions and 21 deletions

View File

@@ -14,6 +14,7 @@ import { PanelEmpty, PanelLoading } from '../shared/AsyncState';
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
import { ProtocolTag } from '../shared/ProtocolTag';
import { SegmentedTabs } from '../shared/SegmentedTabs';
import { TablePagination } from '../shared/TablePagination';
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
@@ -23,6 +24,20 @@ import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../share
const VehicleProfileSyncPanel = lazy(() => import('./VehicleProfileSyncPanel'));
type VehicleDirectoryView = 'all' | 'online' | 'offline' | 'multi';
const vehicleDirectoryViews = [
{ key: 'all', label: '全部车辆' },
{ key: 'online', label: '当前在线' },
{ key: 'offline', label: '当前离线' },
{ key: 'multi', label: '多源车辆' }
] satisfies { key: VehicleDirectoryView; label: string }[];
function applyDirectoryView(params: URLSearchParams, view: VehicleDirectoryView) {
if (view === 'online' || view === 'offline') params.set('online', view);
if (view === 'multi') params.set('coverage', 'multi');
}
function resetWorkspaceScroll() {
const targets = [
document.scrollingElement,
@@ -56,17 +71,19 @@ export default function VehicleSearchWorkspace() {
const [syncOpen, setSyncOpen] = useState(false);
const [candidatesOpen, setCandidatesOpen] = useState(false);
const [filtersCollapsed, setFiltersCollapsed] = useState(mobileLayout);
const [directoryView, setDirectoryView] = useState<VehicleDirectoryView>('all');
const closeTimerRef = useRef<number>();
const deferredKeyword = useDeferredValue(keyword.trim());
const deferredDraftKeyword = useDeferredValue(draftKeyword.trim());
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
const pageSize = mobileLayout ? 8 : 10;
const directoryScope = useMemo(() => queryScopeKey({ keyword: deferredKeyword }), [deferredKeyword]);
const directoryScope = useMemo(() => queryScopeKey({ keyword: deferredKeyword, view: directoryView }), [deferredKeyword, directoryView]);
const candidateParams = useMemo(() => {
const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) });
if (deferredKeyword) params.set('keyword', deferredKeyword);
applyDirectoryView(params, directoryView);
return params;
}, [deferredKeyword, page, pageSize]);
}, [deferredKeyword, directoryView, page, pageSize]);
const candidates = useQuery<Page<VehicleCoverageRow>>({
queryKey: ['vehicle-directory', directoryScope, pageSize, page],
queryFn: ({ signal }) => api.vehicleCoverage(candidateParams, signal),
@@ -77,10 +94,11 @@ export default function VehicleSearchWorkspace() {
const mobileCandidateParams = useMemo(() => {
const params = new URLSearchParams({ limit: '6', offset: '0' });
if (deferredDraftKeyword) params.set('keyword', deferredDraftKeyword);
applyDirectoryView(params, directoryView);
return params;
}, [deferredDraftKeyword]);
}, [deferredDraftKeyword, directoryView]);
const mobileCandidates = useQuery<Page<VehicleCoverageRow>>({
queryKey: ['vehicle-mobile-candidates', queryScopeKey({ keyword: deferredDraftKeyword })],
queryKey: ['vehicle-mobile-candidates', queryScopeKey({ keyword: deferredDraftKeyword, view: directoryView })],
queryFn: ({ signal }) => api.vehicleCoverage(mobileCandidateParams, signal),
enabled: mobileFiltersOpen && Boolean(deferredDraftKeyword),
staleTime: 30_000,
@@ -147,15 +165,23 @@ export default function VehicleSearchWorkspace() {
event.preventDefault();
applyMobileFilters();
};
const selectDirectoryView = (nextView: VehicleDirectoryView) => {
setDirectoryView(nextView);
setPage(1);
setCandidatesOpen(false);
};
const directoryViewLabel = vehicleDirectoryViews.find((item) => item.key === directoryView)?.label ?? '全部车辆';
const vehicleScope = candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权` : '授权范围';
const mobileFilterSummary = deferredKeyword
? `${deferredKeyword} · ${candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆匹配` : '正在查找'}`
: candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权车辆` : '正在读取授权范围';
? `${deferredKeyword}${directoryView === 'all' ? '' : ` · ${directoryViewLabel}`} · ${candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆匹配` : '正在查找'}`
: candidates.data
? directoryView === 'all' ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权车辆` : `${directoryViewLabel} · ${candidates.data.total.toLocaleString('zh-CN')}`
: '正在读取授权范围';
const directoryMetrics: WorkspaceQueueMetricRailItem[] = [
{
label: deferredKeyword ? '匹配车辆' : '授权车辆',
label: deferredKeyword ? '匹配车辆' : directoryView === 'all' ? '授权车辆' : directoryViewLabel,
value: <>{candidates.data ? formatZhNumber(total) : '—'}<i className="v2-vehicle-directory-unit"></i></>,
note: deferredKeyword ? '当前条件' : '账号范围',
note: deferredKeyword || directoryView !== 'all' ? '当前视图' : '账号范围',
tone: 'primary',
emphasis: 'primary'
},
@@ -167,29 +193,45 @@ export default function VehicleSearchWorkspace() {
emphasis: 'primary'
},
{
label: '多源车辆',
label: '本页多源',
value: <>{candidates.data ? formatZhNumber(multiSourceOnPage) : '—'}<i className="v2-vehicle-directory-unit"></i></>,
note: '2+ 协议来源',
tone: 'primary',
emphasis: 'secondary'
}
];
const directoryTitle = deferredKeyword ? '匹配车辆' : mobileLayout ? '授权车辆' : '授权车辆目录';
const directoryTitle = deferredKeyword
? '匹配车辆'
: directoryView === 'all'
? mobileLayout ? '授权车辆' : '授权车辆目录'
: directoryViewLabel;
const directoryDescription = deferredKeyword
? '按当前搜索条件展示,可直接进入车辆数字档案'
: '按照最新上报时间排列,分页浏览全部授权车辆';
: directoryView === 'all'
? '按照最新上报时间排列,分页浏览全部授权车辆'
: `仅展示${directoryViewLabel},按最新上报时间排列`;
return <section className={`v2-vehicle-search-page v2-vehicle-directory-v3${syncOpen ? ' has-sync-panel' : ''}${!mobileLayout && candidatesOpen ? ' has-candidates' : ''}`}>
<div className="v2-vehicle-discovery-shell">
{!mobileLayout || administrator ? <WorkspaceCommandBar
className={`v2-vehicle-command-bar${mobileLayout ? ' is-mobile-admin' : ''}`}
ariaLabel={mobileLayout ? '车辆主档操作' : '查车操作'}
eyebrow={mobileLayout ? '车辆治理' : '车辆目录'}
eyebrow={mobileLayout ? '车辆治理' : undefined}
icon={mobileLayout ? <IconBox /> : <IconSearch />}
title={mobileLayout ? '车辆主档' : '车辆快速定位'}
title={mobileLayout ? '车辆主档' : '查找车辆'}
description={mobileLayout ? '批量维护品牌、车型与运营属性' : '车牌、VIN 或手机号快速查车'}
status={mobileLayout ? undefined : candidates.isPending ? '正在读取授权范围' : vehicleScope}
statusColor={candidates.isError ? 'red' : 'blue'}
actions={administrator ? <Button theme="light" type="tertiary" icon={mobileLayout ? <IconBox /> : undefined} onClick={() => setSyncOpen((value) => !value)}>{syncOpen ? '收起同步' : mobileLayout ? '主档同步' : '批量同步主档'}</Button> : null}
actions={<div className="v2-vehicle-command-actions">
{!mobileLayout ? <SegmentedTabs
className="v2-vehicle-directory-view-tabs"
ariaLabel="车辆目录视图"
value={directoryView}
items={vehicleDirectoryViews}
onChange={selectDirectoryView}
variant="filled"
/> : null}
{administrator ? <Button theme="light" type="tertiary" icon={mobileLayout ? <IconBox /> : undefined} onClick={() => setSyncOpen((value) => !value)}>{syncOpen ? '收起同步' : mobileLayout ? '主档同步' : '批量同步主档'}</Button> : null}
</div>}
/> : null}
{mobileLayout ? <div className="v2-vehicle-mobile-discovery">
<MobileFilterToggle
@@ -313,6 +355,14 @@ export default function VehicleSearchWorkspace() {
/> : null}
</form>
</WorkspaceFilterPanel>}
{mobileLayout ? <SegmentedTabs
className="v2-vehicle-directory-view-tabs"
ariaLabel="车辆目录视图"
value={directoryView}
items={vehicleDirectoryViews}
onChange={selectDirectoryView}
variant="filled"
/> : null}
</div>
<Card className="v2-vehicle-recent-card" bodyStyle={{ padding: 0 }}>
<WorkspaceMetricRail