import { useQuery } from '@tanstack/react-query'; import { IconArrowRight, IconBox, IconChevronRight, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; import { Button, Card, 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 { 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, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy'; 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'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail'; 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, document.documentElement, document.body, document.querySelector('.v2-shell'), document.querySelector('.v2-main'), document.querySelector('.v2-content') ]; for (const target of targets) { if (!(target instanceof HTMLElement)) continue; target.scrollTop = 0; target.scrollLeft = 0; target.scrollTo?.({ top: 0, left: 0, behavior: 'auto' }); } } function vehicleLastSeenLabel(value?: string) { if (!value) return '暂无上报时间'; const normalized = value.replace('T', ' '); return normalized.length >= 16 ? `${normalized.slice(5, 16)} 更新` : normalized; } export default function VehicleSearchWorkspace() { const navigate = useNavigate(); const { session } = usePlatformSession(); const mobileLayout = useMobileLayout(); const [keyword, setKeyword] = useState(''); const [draftKeyword, setDraftKeyword] = useState(''); const [page, setPage] = useState(1); const [syncOpen, setSyncOpen] = useState(false); const [candidatesOpen, setCandidatesOpen] = useState(false); const [filtersCollapsed, setFiltersCollapsed] = useState(mobileLayout); const [directoryView, setDirectoryView] = useState('all'); const closeTimerRef = useRef(); 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, 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, directoryView, page, pageSize]); const candidates = useQuery>({ queryKey: ['vehicle-directory', directoryScope, pageSize, page], queryFn: ({ signal }) => api.vehicleCoverage(candidateParams, signal), placeholderData: retainPreviousPageWithinScope>(directoryScope), staleTime: 30_000, gcTime: QUERY_MEMORY.optionGcTime }); const mobileCandidateParams = useMemo(() => { const params = new URLSearchParams({ limit: '6', offset: '0' }); if (deferredDraftKeyword) params.set('keyword', deferredDraftKeyword); applyDirectoryView(params, directoryView); return params; }, [deferredDraftKeyword, directoryView]); const mobileCandidates = useQuery>({ queryKey: ['vehicle-mobile-candidates', queryScopeKey({ keyword: deferredDraftKeyword, view: directoryView })], queryFn: ({ signal }) => api.vehicleCoverage(mobileCandidateParams, signal), enabled: mobileFiltersOpen && Boolean(deferredDraftKeyword), staleTime: 30_000, gcTime: QUERY_MEMORY.optionGcTime }); const options = useMemo(() => mergeVehicleCandidates(candidates.data?.items ?? []), [candidates.data?.items]); const mobileOptions = useMemo(() => mergeVehicleCandidates(mobileCandidates.data?.items ?? []).slice(0, 6), [mobileCandidates.data?.items]); const pageVehicles = useMemo(() => options.slice(0, pageSize), [options, pageSize]); const total = candidates.data?.total ?? 0; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const onlineOnPage = useMemo(() => pageVehicles.filter((vehicle) => vehicle.online).length, [pageVehicles]); const multiSourceOnPage = useMemo(() => pageVehicles.filter((vehicle) => vehicle.protocols.length > 1).length, [pageVehicles]); const administrator = canAdminister(session); useEffect(() => () => window.clearTimeout(closeTimerRef.current), []); useEffect(() => { setFiltersCollapsed(mobileLayout); setCandidatesOpen(false); }, [mobileLayout]); const openCandidates = () => { window.clearTimeout(closeTimerRef.current); if (!mobileLayout) setCandidatesOpen(Boolean(keyword.trim())); }; const closeCandidates = () => { window.clearTimeout(closeTimerRef.current); closeTimerRef.current = window.setTimeout(() => setCandidatesOpen(false), 140); }; const openVehicle = (value: string) => { const normalized = value.trim(); if (!normalized) return; setCandidatesOpen(false); if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); resetWorkspaceScroll(); navigate(`/vehicles/${encodeURIComponent(normalized)}`); window.queueMicrotask(resetWorkspaceScroll); window.requestAnimationFrame(resetWorkspaceScroll); window.setTimeout(resetWorkspaceScroll, 120); }; const openMobileFilters = () => { setDraftKeyword(keyword); setCandidatesOpen(false); setFiltersCollapsed(false); }; const closeMobileFilters = () => { setDraftKeyword(keyword); setCandidatesOpen(false); setFiltersCollapsed(true); }; const applyMobileFilters = () => { setKeyword(draftKeyword.trim()); setPage(1); setCandidatesOpen(false); setFiltersCollapsed(true); }; const resetMobileDraft = () => { setDraftKeyword(''); setCandidatesOpen(false); }; const submit = (event: FormEvent) => { event.preventDefault(); setCandidatesOpen(false); if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); }; const submitMobileFilters = (event: FormEvent) => { 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}${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 ? '匹配车辆' : directoryView === 'all' ? '授权车辆' : directoryViewLabel, value: <>{candidates.data ? formatZhNumber(total) : '—'}, note: deferredKeyword || directoryView !== 'all' ? '当前视图' : '账号范围', tone: 'primary', emphasis: 'primary' }, { label: '本页在线', value: <>{candidates.data ? formatZhNumber(onlineOnPage) : '—'}, note: candidates.data ? `本页 ${formatZhNumber(pageVehicles.length)} 辆` : '正在读取', tone: 'success', emphasis: 'primary' }, { label: '本页多源', value: <>{candidates.data ? formatZhNumber(multiSourceOnPage) : '—'}, note: '2+ 协议来源', tone: 'primary', emphasis: 'secondary' } ]; const directoryTitle = deferredKeyword ? '匹配车辆' : directoryView === 'all' ? mobileLayout ? '授权车辆' : '授权车辆目录' : directoryViewLabel; const directoryDescription = deferredKeyword ? '按当前搜索条件展示,可直接进入车辆数字档案' : directoryView === 'all' ? '按照最新上报时间排列,分页浏览全部授权车辆' : `仅展示${directoryViewLabel},按最新上报时间排列`; return
{!mobileLayout || administrator ? : } title={mobileLayout ? '车辆主档' : '查找车辆'} description={mobileLayout ? '批量维护品牌、车型与运营属性' : '车牌、VIN 或手机号快速查车'} status={mobileLayout ? undefined : candidates.isPending ? '正在读取授权范围' : vehicleScope} statusColor={candidates.isError ? 'red' : 'blue'} actions={
{!mobileLayout ? : null} {administrator ? : null}
} /> : null} {mobileLayout ?
{deferredDraftKeyword ? mobileCandidates.refetch()} emptyText="没有匹配的授权车辆" header="快速结果" meta={mobileCandidates.data ? `${mobileCandidates.data.total.toLocaleString('zh-CN')} 辆匹配` : '授权范围'} showProtocols layout="list" actionLabel="打开" onSelect={(vehicle) => openVehicle(vehicle.vin)} /> :
快速定位车辆输入完整或部分车牌即可查看候选。
}
: setFiltersCollapsed((value) => !value)} >
} value={keyword} onChange={(value) => { setKeyword(value); setPage(1); setCandidatesOpen(Boolean(value.trim())); }} onFocus={openCandidates} onBlur={closeCandidates} placeholder="输入车牌 / VIN / 终端手机号" autoComplete="off" />
{candidatesOpen && deferredKeyword ? candidates.refetch()} emptyText="没有匹配的授权车辆" header="车辆候选" meta="车牌优先 · VIN 辅助" showProtocols layout={mobileLayout ? 'list' : 'grid'} onSelect={(vehicle) => openVehicle(vehicle.vin)} /> : null}
} {mobileLayout ? : null}
{directoryTitle}
{directoryDescription}
第 {page} / {totalPages} 页 } /> {candidates.isFetching && !candidates.data ? : candidates.isError ?
{candidates.error instanceof Error ? candidates.error.message : '授权车辆加载失败'}
: pageVehicles.length ? mobileLayout ?
{pageVehicles.map((vehicle) =>
)}
: {vehicle.plate || '未绑定车牌'}{vehicle.vin} }, { title: '品牌 / 终端', dataIndex: 'oem', width: 260, render: (_value: string, vehicle: VehicleCoverageRow) => {vehicle.oem || '品牌待维护'}{vehicle.phone || '暂无终端手机号'} }, { title: '实时状态', dataIndex: 'online', width: 180, render: (_value: boolean, vehicle: VehicleCoverageRow) => {vehicle.online ? '在线' : '离线'}{vehicleLastSeenLabel(vehicle.lastSeen)} }, { title: '协议来源', dataIndex: 'protocols', render: (protocols: string[]) => {protocols.length ? protocols.slice(0, 3).map((protocol) => ) : 暂无来源} }, { title: '', dataIndex: 'vin', width: 116, align: 'right', render: () => } ]} dataSource={pageVehicles} rowKey="vin" onRow={(vehicle) => vehicle ? ({ className: vehicle.online ? 'is-online' : 'is-offline', role: 'button', tabIndex: 0, 'aria-label': `打开 ${vehicle.plate || vehicle.vin} 车辆档案`, 'data-testid': `vehicle-directory-row-${vehicle.vin}`, onClick: () => openVehicle(vehicle.vin), onKeyDown: (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); openVehicle(vehicle.vin); } }) : ({})} pagination={false} loading={candidates.isFetching} empty={null} /> : } title={deferredKeyword ? '没有匹配车辆' : '暂无授权车辆'} description={deferredKeyword ? '当前范围没有结果,可清除搜索返回完整车辆目录。' : '管理员分配车辆权限后会显示在这里。'} action={deferredKeyword ? : } />} {total > 0 ?
{ setPage(next); setCandidatesOpen(false); }} />
: null} {syncOpen ? }> setSyncOpen(false)} /> : null} ; }