import { IconChevronLeft, IconChevronRight, IconClose, IconFilter, IconList, IconMapPin, IconQrCode, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; import { useQuery } from '@tanstack/react-query'; import QRCode from 'qrcode'; import { memo, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { api } from '../../api/client'; import type { AlertEvent, MapReverseGeocode, VehicleDetail as VehicleDetailData, VehicleRealtimeRow } from '../../api/types'; import { FleetMap } from '../map/FleetMap'; import { EmptyState, InlineError } from '../shared/AsyncState'; import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor'; import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData'; const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT']; const statuses = ['', 'online', 'offline', 'driving', 'idle']; const EMPTY_VEHICLES: VehicleRealtimeRow[] = []; type AddressCoordinate = { longitude: number; latitude: number; key: string }; function addressCoordinate(vehicle: VehicleRealtimeRow): AddressCoordinate | undefined { if (!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: () => api.reverseGeocode(new URLSearchParams({ longitude: requested!.longitude.toFixed(4), latitude: requested!.latitude.toFixed(4) })), enabled: Boolean(requested), staleTime: 6 * 60 * 60_000, gcTime: 24 * 60 * 60_000, refetchOnWindowFocus: false, retry: 1 }); const moved = Boolean(current && requested && current.key !== requested.key); if (!current) return 无有效坐标; if (!requested) return ; if (addressQuery.isFetching && !addressQuery.data) return 解析中; if (addressQuery.isError) return ; return
{addressQuery.data?.formattedAddress || '暂无地址'}{moved ? : null}
; }); function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, onSelect, onPage, onLimit }: { rows: VehicleRealtimeRow[]; total: number; page: number; totalPages: number; limit: number; loading: boolean; onSelect: (vin: string) => void; onPage: (page: number) => void; onLimit: (limit: number) => void }) { return
车辆实时列表速度、里程和坐标实时刷新,文字地址按需解析
{total.toLocaleString('zh-CN')} 辆车辆
{rows.map((row) => )}
车辆速度总里程经纬度地理位置
{formatNumber(row.speedKmh, 1)}km/h {formatNumber(row.totalMileageKm, 1)}km {row.longitude.toFixed(6)}
{row.latitude.toFixed(6)}
{loading ?
正在更新车辆实时数据…
: null}{!loading && !rows.length ? : null}
{rows.map((row) =>
{row.plate || '未绑定车牌'}{row.vin}
{formatNumber(row.speedKmh, 1)}km/h
总里程
{formatNumber(row.totalMileageKm, 1)} km
经纬度
{row.longitude.toFixed(6)}, {row.latitude.toFixed(6)}
地理位置
)}{loading ?
正在更新车辆实时数据…
: null}{!loading && !rows.length ? : null}
第 {page} / {totalPages} 页
; } function MobileEntry({ onClose }: { onClose: () => void }) { const [qr, setQr] = useState(''); const url = `${window.location.origin}/monitor`; useEffect(() => { let cancelled = false; void QRCode.toDataURL(url, { width: 240, margin: 2, color: { dark: '#122033', light: '#ffffff' } }).then((value) => { if (!cancelled) setQr(value); }); return () => { cancelled = true; }; }, [url]); return

手机端全局监控

扫码后使用现有账号鉴权,不在二维码中保存 Token

{qr ? 全局监控手机端二维码 : }{url}
; } 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); function VehicleDetailCard({ vehicle, detail, activeAlerts, address, onCollapse, onClear }: { vehicle: VehicleRealtimeRow; detail?: VehicleDetailData; activeAlerts?: { items: AlertEvent[]; total: number }; address?: MapReverseGeocode; onCollapse: () => void; onClear: () => void; }) { const status = vehicleStatus(vehicle); const dailyMileage = detail?.mileage.items[0]?.dailyMileageKm; const latestAlert = activeAlerts?.items[0]; return ( ); } export default function MonitorPage() { const [mode, setMode] = useState<'map' | 'list'>('map'); const [mobileEntryOpen, setMobileEntryOpen] = useState(false); const [listOffset, setListOffset] = useState(0); const [listLimit, setListLimit] = useState(50); const [keyword, setKeyword] = useState(''); const deferredKeyword = useDeferredValue(keyword); const searchTerms = useMemo(() => parseMonitorSearchTerms(keyword), [keyword]); const [protocol, setProtocol] = useState(''); const [status, setStatus] = useState(''); const [selectedVin, setSelectedVin] = useState(''); const [detailOpen, setDetailOpen] = useState(false); const [viewport, setViewport] = useState({ zoom: 5, bounds: '' }); 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 listParams = useMemo(() => { const params = monitorQueryParams(filters, listLimit); params.set('offset', String(listOffset)); return params; }, [filters, listLimit, listOffset]); const { summary, vehicles, map, selectedVehicle } = useMonitorData(filters, viewport, selectedVin, mode === 'map', mode === 'map'); const realtimeListQuery = useQuery({ queryKey: ['monitor', 'vehicle-list', listParams.toString()], queryFn: ({ signal }) => api.vehicleRealtime(listParams, signal), enabled: mode === 'list', placeholderData: (previous) => previous, refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false }); 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 batchSearchPending = searchTerms.length > 1 && ( keyword !== deferredKeyword || activeBatchQuery.isLoading || activeBatchQuery.isPlaceholderData ); 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 = batchSearchPending ? [] : 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 selected = selectedVin ? rows.find((vehicle) => vehicle.vin === selectedVin) ?? selectedVehicle.data?.items[0] : undefined; const selectVehicle = useCallback((vin: string) => { setSelectedVin(vin); setDetailOpen(true); setMode('map'); }, []); 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 card = useMonitorVehicleCard(selected?.vin ?? '', selected, Boolean(selectedVin)); 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; return (
{[ ['有实时位置', formatNumber(summary.data?.totalVehicles ?? vehicles.data?.total ?? rows.length), 'fleet'], ['当前在线', formatNumber(summary.data?.onlineVehicles ?? rows.length - offline), 'online'], ['当前离线', formatNumber(summary.data?.offlineVehicles ?? offline), 'offline'], ['行驶车辆', formatNumber(summary.data?.drivingVehicles ?? driving), 'driving'], ['静止车辆', formatNumber(summary.data?.idleVehicles ?? idle), 'idle'], ['告警车辆', summary.data?.alertDataAvailable ? formatNumber(summary.data.alertVehicles) : '—', 'alert'], ['今日上报', formatNumber(summary.data?.frameToday ?? 0), 'today'] ].map(([label, value, tone]) =>
{label}{value}
)}
{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) => )}
{searchTerms.length > 1 && !batchSearchPending ? batchStatus : `当前载入 ${visibleRows.length} / ${vehicles.data?.total ?? visibleRows.length} 辆`}
1 && (batchSearchPending || map.isPlaceholderData) ? undefined : map.data} selectedVin={selectedVin || undefined} onSelect={selectMapVehicle} onSelectVin={selectVehicle} onViewportChange={updateViewport} /> {selected && detailOpen ? ( ) : null} {selected && !detailOpen ? ( ) : null}
: setListOffset((page - 1) * listLimit)} onLimit={(next) => { setListLimit(next); setListOffset(0); }} />}
实时数据状态 {vehicles.isFetching ? '正在刷新' : '实时车辆已同步'} {mode === 'map' ? `列表 ${visibleRows.length} 条 · 地图 ${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} 辆`} 智能刷新 重点车辆 {MONITOR_REFRESH.selected / 1000} 秒 · 车队 {MONITOR_REFRESH.fleet / 1000} 秒 · 统计 {MONITOR_REFRESH.summary / 1000} 秒
{mobileEntryOpen ? setMobileEntryOpen(false)} /> : null}
); }