feat(web): add vehicle directory views
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user