import {
IconChevronLeft, IconFilter, IconList, IconMapPin,
IconQrCode, IconRefresh, IconSearch
} from '@douyinfe/semi-icons';
import { Button, Card, Empty, Input, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui';
import { useQuery } from '@tanstack/react-query';
import { lazy, memo, Suspense, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { Page, VehicleRealtimeRow } from '../../api/types';
import { FleetMap } from '../map/FleetMap';
import { EmptyState, InlineError } from '../shared/AsyncState';
import { SegmentedTabs } from '../shared/SegmentedTabs';
import { TablePagination } from '../shared/TablePagination';
import { ProtocolTag } from '../shared/ProtocolTag';
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
import { formatNumber, statusLabel, vehicleStatus } from '../domain/monitor';
import { useMobileLayout } from '../hooks/useMobileLayout';
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, type MonitorViewport } from '../hooks/useMonitorData';
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
import { buildMonitorPath, parseMonitorRouteContext } from '../routing/monitorContext';
const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT'];
const statuses = ['', 'online', 'offline', 'driving', 'idle', 'no_location'];
const EMPTY_VEHICLES: VehicleRealtimeRow[] = [];
const MONITOR_VIEW_ITEMS = [
{ key: 'map', label: '地图', icon: },
{ key: 'list', label: '列表', icon: }
] as const;
const VehicleDetailCard = lazy(() => import('./MonitorVehicleDetailCard'));
function BatchVehicleSearchDialog({ initialValue, mobile, onApply, onClose }: { initialValue: string; mobile: boolean; onApply: (value: string) => void; onClose: () => void }) {
const [draft, setDraft] = useState(initialValue);
const terms = useMemo(() => parseMonitorSearchTerms(draft), [draft]);
return }
badge={`${terms.length} 辆`}
badgeColor={terms.length ? 'blue' : 'grey'}
summaryItems={[
{ label: '已识别', value: terms.length.toLocaleString('zh-CN'), detail: '可直接应用到监控筛选', tone: terms.length ? 'primary' : 'neutral' },
{ label: '重复处理', value: '自动去重', detail: '相同车牌只保留一次', tone: 'success' },
{ label: '单次上限', value: `${MAX_MONITOR_SEARCH_TERMS} 辆`, detail: '超出部分不会进入查询' }
]}
footerNote="支持换行、空格、逗号或分号分隔。"
secondaryActions={[{ label: '取消', onClick: onClose }]}
primaryAction={{ label: `应用搜索(${terms.length})`, ariaLabel: `应用搜索(${terms.length})`, disabled: !terms.length, icon: , onClick: () => onApply(terms.join(',')) }}
onCancel={onClose}
>
已识别 {terms.length} 辆,重复项已自动去除最多 {MAX_MONITOR_SEARCH_TERMS} 辆
;
}
type AddressCoordinate = { longitude: number; latitude: number; key: string };
function hasRealtimeLocation(vehicle: VehicleRealtimeRow) {
if (vehicle.locationAvailable != null) return vehicle.locationAvailable;
return Number.isFinite(vehicle.longitude) && Number.isFinite(vehicle.latitude)
&& Math.abs(vehicle.longitude) <= 180 && Math.abs(vehicle.latitude) <= 90
&& (vehicle.longitude !== 0 || vehicle.latitude !== 0);
}
function hasRealtimeSpeed(vehicle: VehicleRealtimeRow) {
return vehicle.speedAvailable ?? hasRealtimeLocation(vehicle);
}
function hasRealtimeMileage(vehicle: VehicleRealtimeRow) {
return vehicle.mileageAvailable ?? hasRealtimeLocation(vehicle);
}
function hasTodayMileage(vehicle: VehicleRealtimeRow): vehicle is VehicleRealtimeRow & { todayMileageKm: number } {
return vehicle.todayMileageAvailable === true && vehicle.todayMileageKm != null;
}
function hasRealtimeSOC(vehicle: VehicleRealtimeRow) {
return vehicle.socAvailable ?? (vehicle.primaryProtocol === 'GB32960' || vehicle.primaryProtocol === 'YUTONG_MQTT');
}
function addressCoordinate(vehicle: VehicleRealtimeRow): AddressCoordinate | undefined {
if (!hasRealtimeLocation(vehicle) || !Number.isFinite(vehicle.longitude) || !Number.isFinite(vehicle.latitude)
|| Math.abs(vehicle.longitude) > 180 || Math.abs(vehicle.latitude) > 90
|| (vehicle.longitude === 0 && vehicle.latitude === 0)) return undefined;
const longitude = Number(vehicle.longitude.toFixed(4));
const latitude = Number(vehicle.latitude.toFixed(4));
return { longitude, latitude, key: `${longitude.toFixed(4)},${latitude.toFixed(4)}` };
}
const MonitorAddressCell = memo(function MonitorAddressCell({ vehicle }: { vehicle: VehicleRealtimeRow }) {
const current = addressCoordinate(vehicle);
const [requested, setRequested] = useState();
const addressQuery = useQuery({
queryKey: ['monitor', 'list-address', requested?.key],
queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({
longitude: requested!.longitude.toFixed(4),
latitude: requested!.latitude.toFixed(4)
}), signal),
enabled: Boolean(requested),
staleTime: 6 * 60 * 60_000,
gcTime: QUERY_MEMORY.highVolumeGcTime,
refetchOnWindowFocus: false,
retry: 1
});
const moved = Boolean(current && requested && current.key !== requested.key);
if (!current) return 暂无实时位置;
if (!requested) return } aria-label={`解析${vehicle.plate || vehicle.vin}位置`} title="按需调用高德地址解析,不随实时数据刷新" onClick={() => setRequested(current)}>解析位置;
if (addressQuery.isFetching && !addressQuery.data) return 解析中;
if (addressQuery.isError) return ;
return {addressQuery.data?.formattedAddress || '暂无地址'}{addressQuery.dataUpdatedAt ? `解析于 ${new Date(addressQuery.dataUpdatedAt).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })}` : ''}{moved ? : null}
;
});
function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, mobile, onSelect, onPage, onLimit }: { rows: VehicleRealtimeRow[]; total: number; page: number; totalPages: number; limit: number; loading: boolean; mobile: boolean; onSelect: (vin: string) => void; onPage: (page: number) => void; onLimit: (limit: number) => void }) {
const columns = useMemo(() => [
{ title: '车辆', dataIndex: 'plate', width: 210, className: 'v2-monitor-table-vehicle', render: (_value: string, row: VehicleRealtimeRow) => },
{ title: '速度', dataIndex: 'speedKmh', width: 120, render: (_value: number, row: VehicleRealtimeRow) => <>{hasRealtimeSpeed(row) ? formatNumber(row.speedKmh, 1) : '—'}{hasRealtimeSpeed(row) ? km/h : null}> },
{ title: '当日里程', dataIndex: 'todayMileageKm', width: 140, render: (_value: number, row: VehicleRealtimeRow) => <>{hasTodayMileage(row) ? formatNumber(row.todayMileageKm, 1) : '—'}{hasTodayMileage(row) ? km : null}> },
{ title: '总里程', dataIndex: 'totalMileageKm', width: 170, render: (_value: number, row: VehicleRealtimeRow) => <>{hasRealtimeMileage(row) ? formatNumber(row.totalMileageKm, 1) : '—'}{hasRealtimeMileage(row) ? km : null}> },
{ title: '协议来源', dataIndex: 'primaryProtocol', width: 150, render: (_value: string, row: VehicleRealtimeRow) => {row.protocols.length > 1 ?
+{row.protocols.length - 1} 路 : null}
},
{ title: '经纬度', dataIndex: 'longitude', width: 190, render: (_value: number, row: VehicleRealtimeRow) => hasRealtimeLocation(row) ? {row.longitude.toFixed(6)}
{row.latitude.toFixed(6)} : — },
{ title: '地理位置', dataIndex: 'vin', render: (_value: string, row: VehicleRealtimeRow) => }
], [onSelect]);
return
{!mobile ? {loading ?
: null}{!loading && !rows.length ?
: null}
: null}
{mobile ? {rows.map((row) =>
{row.plate || '未绑定车牌'}{row.vin}
{hasRealtimeSpeed(row) ? formatNumber(row.speedKmh, 1) : '—'}{hasRealtimeSpeed(row) ? km/h : null}} aria-label={`在地图中定位${row.plate || row.vin}`} onClick={() => onSelect(row.vin)}>地图
- 当日里程
- {hasTodayMileage(row) ? `${formatNumber(row.todayMileageKm, 1)} km` : '—'}
- 总里程
- {hasRealtimeMileage(row) ? `${formatNumber(row.totalMileageKm, 1)} km` : '—'}
- 协议来源
- {row.protocols.length > 1 ? {row.protocols.length} 路 : null}
- 经纬度
- {hasRealtimeLocation(row) ?
{row.longitude.toFixed(6)}, {row.latitude.toFixed(6)} : '—'} - 地理位置
)}{loading ?
: null}{!loading && !rows.length ?
: null}
: null}
;
}
function MobileEntry({ mobile, onClose }: { mobile: boolean; onClose: () => void }) {
const [qr, setQr] = useState('');
const [error, setError] = useState('');
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle');
const [attempt, setAttempt] = useState(0);
const url = `${window.location.origin}/monitor`;
useEffect(() => {
if (copyState === 'idle') return;
const timer = window.setTimeout(() => setCopyState('idle'), 1_800);
return () => window.clearTimeout(timer);
}, [copyState]);
useEffect(() => {
let cancelled = false;
setQr('');
setError('');
void import('qrcode')
.then(({ default: QRCode }) => QRCode.toDataURL(url, { width: 240, margin: 2, color: { dark: '#122033', light: '#ffffff' } }))
.then((value) => { if (!cancelled) setQr(value); })
.catch(() => { if (!cancelled) setError('二维码生成失败,请检查浏览器环境后重试'); });
return () => { cancelled = true; };
}, [attempt, url]);
const copyURL = async () => {
try {
if (!navigator.clipboard?.writeText) throw new Error('clipboard unavailable');
await navigator.clipboard.writeText(url);
setCopyState('copied');
} catch {
setCopyState('error');
}
};
const qrState = qr ? '已生成' : error ? '生成失败' : '生成中';
const copyLabel = copyState === 'copied' ? '已复制访问地址' : copyState === 'error' ? '复制失败,重试' : '复制访问地址';
return }
badge="账号鉴权"
badgeColor="blue"
summaryItems={[
{ label: '入口页面', value: '全局监控', detail: '移动端响应式布局', tone: 'primary' },
{ label: '凭证安全', value: '不含 Token', detail: '扫码后仍需账号鉴权', tone: 'success' },
{ label: '二维码状态', value: qrState, detail: error ? '可在下方重新生成' : '仅在打开入口时生成', tone: error ? 'danger' : qr ? 'success' : 'neutral' }
]}
footerNote={url}
secondaryActions={[{ label: '关闭', onClick: onClose }]}
primaryAction={{ label: copyLabel, loading: !qr && !error, disabled: !qr && !error, onClick: () => void copyURL() }}
onCancel={onClose}
>
扫码后使用现有账号鉴权,不在二维码中保存 Token。
{qr ?

: error ?
{error}
:
}
{url}
{copyLabel}
;
}
const VehicleRow = memo(function VehicleRow({ vehicle, selected, onSelect }: { vehicle: VehicleRealtimeRow; selected: boolean; onSelect: (vin: string) => void }) {
const status = vehicleStatus(vehicle);
return (
);
});
const MemoFleetMap = memo(FleetMap);
export default function MonitorPage() {
const [routeParams, setRouteParams] = useSearchParams();
const pageRef = useRef(null);
const initialContextRef = useRef(parseMonitorRouteContext(routeParams));
const initialContext = initialContextRef.current;
const mobileLayout = useMobileLayout(760);
const [mode, setMode] = useState<'map' | 'list'>(initialContext.mode);
const [mobileEntryOpen, setMobileEntryOpen] = useState(false);
const [listOffset, setListOffset] = useState(initialContext.listOffset);
const [listLimit, setListLimit] = useState(initialContext.listLimit);
const [keyword, setKeyword] = useState(initialContext.keyword);
const [batchSearchOpen, setBatchSearchOpen] = useState(false);
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
const deferredKeyword = useDeferredValue(keyword);
const searchTerms = useMemo(() => parseMonitorSearchTerms(keyword), [keyword]);
const [protocol, setProtocol] = useState(initialContext.protocol);
const [status, setStatus] = useState(initialContext.status);
const [selectedVin, setSelectedVin] = useState(initialContext.selectedVin);
const [detailOpen, setDetailOpen] = useState(initialContext.detailOpen);
const [viewport, setViewport] = useState(initialContext.viewport);
const updateViewport = useCallback((next: MonitorViewport) => {
setViewport((current) => current.zoom === next.zoom && current.bounds === next.bounds ? current : next);
}, []);
const filters = useMemo(() => ({ keyword: deferredKeyword, protocol, status }), [deferredKeyword, protocol, status]);
const monitorReturn = useMemo(() => buildMonitorPath({
mode, keyword, protocol, status, selectedVin, detailOpen, viewport, listOffset, listLimit
}), [detailOpen, keyword, listLimit, listOffset, mode, protocol, selectedVin, status, viewport]);
useEffect(() => {
const next = new URL(monitorReturn, 'https://vehicle-platform.invalid').searchParams;
if (next.toString() !== routeParams.toString()) setRouteParams(next, { replace: true });
}, [monitorReturn, routeParams, setRouteParams]);
const listScope = useMemo(() => monitorFilterScope(filters), [filters]);
const listParams = useMemo(() => {
const params = monitorQueryParams(filters, listLimit);
params.set('offset', String(listOffset));
return params;
}, [filters, listLimit, listOffset]);
const trackedVin = mode === 'map' ? selectedVin : '';
const { summary, vehicles, map, selectedVehicle } = useMonitorData(filters, viewport, trackedVin, mode === 'map', mode === 'map');
const realtimeListQuery = useQuery>({
queryKey: ['monitor', 'vehicle-list', listScope, listLimit, listOffset],
queryFn: ({ signal }) => api.vehicleRealtime(listParams, signal),
enabled: mode === 'list', placeholderData: retainPreviousPageWithinScope>(listScope, 2), refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false,
gcTime: QUERY_MEMORY.highVolumeGcTime,
...LIVE_QUERY_POLICY
});
const rows = useMemo(() => {
const data = vehicles.data?.items ?? [];
if (status === 'driving' || status === 'idle') return data.filter((vehicle) => vehicleStatus(vehicle) === status);
return data;
}, [status, vehicles.data?.items]);
const listRows = realtimeListQuery.data?.items ?? EMPTY_VEHICLES;
const activeBatchRows = mode === 'map' ? rows : listRows;
const activeBatchQuery = mode === 'map' ? vehicles : realtimeListQuery;
const filterTransitionPending = keyword !== deferredKeyword || activeBatchQuery.isLoading;
const batchSearchPending = searchTerms.length > 1 && filterTransitionPending;
const batchMatch = useMemo(() => {
const identities = new Set();
for (const vehicle of activeBatchRows) {
identities.add(vehicle.vin.toLocaleUpperCase());
if (vehicle.plate) identities.add(vehicle.plate.toLocaleUpperCase());
}
const missing = searchTerms.filter((term) => !identities.has(term));
return { matched: searchTerms.length - missing.length, missing };
}, [activeBatchRows, searchTerms]);
const visibleRows = filterTransitionPending ? [] : rows;
const batchStatus = batchSearchPending
? `正在查找 ${searchTerms.length} 辆`
: batchMatch.missing.length
? `已找到 ${batchMatch.matched}/${searchTerms.length} · 未找到 ${batchMatch.missing.length} 辆`
: `已找到 ${batchMatch.matched}/${searchTerms.length}`;
const batchStatusTitle = batchMatch.missing.length
? `未找到:${batchMatch.missing.join('、')}`
: searchTerms.join('、');
const visibleListRows = filterTransitionPending ? EMPTY_VEHICLES : listRows;
const visibleListTotal = filterTransitionPending ? 0 : (realtimeListQuery.data?.total ?? 0);
const selected = selectedVin
? rows.find((vehicle) => vehicle.vin === selectedVin) ?? selectedVehicle.data?.items[0]
: undefined;
const resetMonitorScroll = useCallback(() => {
const scrollOwner = pageRef.current?.closest('.v2-content');
if (scrollOwner) scrollOwner.scrollTop = 0;
}, []);
const selectVehicle = useCallback((vin: string) => {
resetMonitorScroll();
setSelectedVin(vin);
setDetailOpen(true);
setMode('map');
}, [resetMonitorScroll]);
const clearSelection = useCallback(() => {
setSelectedVin('');
setDetailOpen(false);
}, []);
const selectMapVehicle = useCallback((vehicle: VehicleRealtimeRow) => selectVehicle(vehicle.vin), [selectVehicle]);
const collapseDetail = useCallback(() => setDetailOpen(false), []);
const expandDetail = useCallback(() => setDetailOpen(true), []);
const changeMonitorMode = useCallback((nextMode: 'map' | 'list') => {
resetMonitorScroll();
setMode(nextMode);
if (nextMode === 'list') setDetailOpen(false);
}, [resetMonitorScroll]);
const clearFilters = useCallback(() => {
setKeyword('');
setProtocol('');
setStatus('');
setListOffset(0);
}, []);
const activeFilterCount = Number(Boolean(keyword.trim())) + Number(Boolean(protocol)) + Number(Boolean(status));
const driving = rows.filter((vehicle) => vehicleStatus(vehicle) === 'driving').length;
const idle = rows.filter((vehicle) => vehicleStatus(vehicle) === 'idle').length;
const offline = rows.filter((vehicle) => vehicleStatus(vehicle) === 'offline').length;
const totalVehicleCount = summary.data?.totalVehicles ?? vehicles.data?.total ?? rows.length;
const onlineVehicleCount = summary.data?.onlineVehicles ?? rows.length - offline;
const offlineVehicleCount = summary.data?.offlineVehicles ?? offline;
const drivingVehicleCount = summary.data?.drivingVehicles ?? driving;
const idleVehicleCount = summary.data?.idleVehicles ?? idle;
const alertVehicleCount = summary.data?.alertDataAvailable ? summary.data.alertVehicles : undefined;
const monitorMetrics: WorkspaceQueueMetricRailItem[] = [
{
label: '车辆总数',
value: <>{formatNumber(totalVehicleCount)}辆>,
note: '授权车辆',
tone: 'primary',
emphasis: 'primary'
},
{
label: '当前在线',
value: <>{formatNumber(onlineVehicleCount)}辆>,
note: '当前可用',
tone: 'success',
emphasis: 'primary'
},
{
label: '行驶车辆',
value: <>{formatNumber(drivingVehicleCount)}辆>,
note: '实时行驶',
tone: 'primary',
emphasis: 'primary'
},
{
label: '当前离线',
value: <>{formatNumber(offlineVehicleCount)}辆>,
note: '无在线来源',
tone: 'neutral',
emphasis: 'secondary'
},
{
label: '静止车辆',
value: <>{formatNumber(idleVehicleCount)}辆>,
note: '在线静止',
tone: 'success',
emphasis: 'secondary'
},
{
label: '告警车辆',
value: <>{alertVehicleCount == null ? '—' : formatNumber(alertVehicleCount)}辆>,
note: alertVehicleCount == null ? '数据未开放' : '需要关注',
tone: alertVehicleCount ? 'danger' : 'neutral',
emphasis: 'primary'
}
];
const viewportLoad = mode === 'map'
? (map.data?.clusters.length
? `${map.data.clusters.length} 个聚合 · ${map.data.points.length} 个车辆点`
: `${map.data?.points.length ?? 0} 个车辆点`)
: `${realtimeListQuery.data?.items.length ?? 0} / ${realtimeListQuery.data?.total ?? 0} 辆`;
const lastSyncAt = vehicles.dataUpdatedAt ? new Date(vehicles.dataUpdatedAt) : undefined;
return (
{mobileLayout ? }
aria-controls="monitor-filter-fields"
aria-expanded={!filtersCollapsed}
aria-label={`${filtersCollapsed ? '展开' : '收起'}车辆筛选,当前 ${activeFilterCount} 个条件`}
onClick={() => setFiltersCollapsed((value) => !value)}
>
{filtersCollapsed ? '筛选' : '收起'}{activeFilterCount ? {activeFilterCount} : null}
:
自动生效
{activeFilterCount ? {activeFilterCount} : null}
}
} aria-label="打开手机端入口" onClick={() => setMobileEntryOpen(true)}>手机端
{batchSearchOpen ?
setBatchSearchOpen(false)} onApply={(value) => { setKeyword(value); setListOffset(0); setBatchSearchOpen(false); }} /> : null}
今日上报{formatNumber(summary.data?.frameToday ?? 0)}条数据活跃度
无实时位置{formatNumber(summary.data?.noLocationVehicles ?? 0)}辆辅助排查
}
/>
{vehicles.isError ? vehicles.refetch()} /> : null}
{mode === 'list' && realtimeListQuery.isError ? realtimeListQuery.refetch()} /> : null}
{mode === 'map' ?
车辆列表{batchSearchPending ? '查询中' : `${formatNumber(vehicles.data?.total ?? visibleRows.length)} 辆`}
1 ? batchStatusTitle : undefined}>{searchTerms.length > 1 ? batchStatus : deferredKeyword ? `正在筛选“${deferredKeyword}”` : '按最新上报排序'}
{vehicles.isLoading || batchSearchPending ?
{batchSearchPending ? `正在查找 ${searchTerms.length} 辆车` : '加载车辆'}
: null}
{!vehicles.isLoading && !batchSearchPending && visibleRows.length === 0 ?
: null}
{visibleRows.map((vehicle) =>
)}
{selected && detailOpen ? (
正在加载车辆详情
}>
) : null}
{selected && !detailOpen ? (
) : null}
: setListOffset((page - 1) * listLimit)} onLimit={(next) => { setListLimit(next); setListOffset(0); }} />}
{vehicles.isFetching ? '正在同步' : '数据已同步'}
实时数据{visibleRows.length.toLocaleString('zh-CN')} 辆已载入
{mode === 'map' ? '地图载荷' : '列表载荷'}{viewportLoad}
智能刷新
重点 {MONITOR_REFRESH.selected / 1000}s · 车队 {MONITOR_REFRESH.fleet / 1000}s · 统计 {MONITOR_REFRESH.summary / 1000}s
{mobileEntryOpen ? setMobileEntryOpen(false)} /> : null}
);
}