import { useMutation, useQuery } from '@tanstack/react-query'; import { IconAlarm, IconBox, IconCalendar, IconClock, IconCopy, IconChevronRight, IconMapPin, IconSearch, IconTickCircle } from '@douyinfe/semi-icons'; import { Button, Card, Descriptions, Empty, Input, List, Select, Table, Tag } from '@douyinfe/semi-ui'; import { FormEvent, lazy, Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { LatestTelemetryResponse, LatestTelemetryValue, QualityIssueRow, VehicleDetail, VehicleRealtimeRow } from '../../api/types'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister, hasMenu } from '../auth/session'; import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy'; import { formatTelemetryTime, formatTelemetryValue, telemetryQualityColor, telemetryQualityLabel, telemetryValueTypeLabel } from '../domain/telemetry'; import { formatZhNumber, vehicleOperationStatusLabels } from '../domain/formatters'; import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols'; import { isValidAMapCoordinate } from '../../integrations/amap'; import { FleetMap } from '../map/FleetMap'; import { InlineError, PageLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { PlatformTime } from '../shared/PlatformTime'; import { ProtocolTag } from '../shared/ProtocolTag'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet'; import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext'; import { useMobileLayout } from '../hooks/useMobileLayout'; function fmt(value?: string) { return value?.trim() || '—'; } function metric(value: number | undefined) { return Number.isFinite(value as number) ? formatZhNumber(value!, 1) : '—'; } function availableMetric(value: number | undefined, available?: boolean) { return available === false ? '—' : metric(value); } function issueTone(issue: QualityIssueRow) { return issue.severity === 'error' ? 'error' : 'warning'; } function durationHours(seconds?: number | null) { return seconds == null ? '—' : `${formatZhNumber(seconds / 3600, 1)} 小时`; } function localDateTime(value?: string) { return value ? value.slice(0, 16) : ''; } const SINGLE_VEHICLE_REFRESH_MS = 10_000; const VehicleSearch = lazy(() => import('./VehicleSearchWorkspace')); const VehicleActions = lazy(() => import('./VehicleActions')); function resetWorkspaceScroll(routeRoot?: HTMLElement | null) { const targets = new Set([ routeRoot ?? null, routeRoot?.closest('.v2-content') ?? null, document.scrollingElement, document.documentElement, document.body, document.querySelector('.v2-shell'), document.querySelector('.v2-main') ]); for (const content of document.querySelectorAll('.v2-content')) targets.add(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 revealVehicleIdentity(root: HTMLElement | null) { resetWorkspaceScroll(root); if (!root) return; root.scrollTop = 0; root.scrollLeft = 0; root.focus({ preventScroll: true }); } function eventTimeLabel(value?: string) { if (!value) return '—'; const normalized = value.replace('T', ' '); return normalized.length >= 16 ? normalized.slice(5, 16) : normalized; } function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; editable: boolean; onUpdated: () => void }) { const profile = detail.profile; const [editing, setEditing] = useState(false); const [draft, setDraft] = useState({ brandName: '', modelName: '', vehicleType: '', companyName: '', operationStatus: 'unknown', accessProvider: '', firstAccessAt: '', runtimeHours: '' }); const save = useMutation({ mutationFn: () => api.updateVehicleProfile(detail.vin, { brandName: draft.brandName.trim(), modelName: draft.modelName.trim(), vehicleType: draft.vehicleType.trim(), companyName: draft.companyName.trim(), operationStatus: draft.operationStatus as NonNullable['operationStatus'], accessProvider: draft.accessProvider.trim(), firstAccessAt: draft.firstAccessAt, runtimeSeconds: draft.runtimeHours.trim() === '' ? null : Math.round(Number(draft.runtimeHours) * 3600), version: profile?.version ?? 0 }), onSuccess: () => { setEditing(false); onUpdated(); } }); const startEditing = () => { setDraft({ brandName: profile?.brandName ?? '', modelName: profile?.modelName ?? '', vehicleType: profile?.vehicleType ?? '', companyName: profile?.companyName ?? '', operationStatus: profile?.operationStatus ?? 'unknown', accessProvider: profile?.accessProvider ?? '', firstAccessAt: localDateTime(profile?.firstAccessAt), runtimeHours: profile?.runtimeSeconds == null ? '' : String(profile.runtimeSeconds / 3600) }); save.reset(); setEditing(true); }; const submit = (event: FormEvent) => { event.preventDefault(); if (draft.runtimeHours === '' || Number.isFinite(Number(draft.runtimeHours))) save.mutate(); }; return 维护档案 : null} /> {editing ?
{save.isError ?

{save.error.message}

: null}
: <>{vehicleOperationStatusLabels[profile?.operationStatus ?? 'unknown']} }, { key: '接入服务商', value: fmt(profile?.accessProvider) }, { key: '首次接入', value: fmt(profile?.firstAccessAt) }, { key: '累计运行', value: durationHours(profile?.runtimeSeconds) } ]} />

身份字段来自网关;补充主档来源 {profile?.sourceSystem || '未配置'}{profile?.updatedAt ? ` · v${profile.version} · ${profile.updatedBy} 更新` : ''}

}
; } function Events({ detail }: { detail: VehicleDetail }) { const events = [ ...detail.quality.items.slice(0, 3).map((item) => ({ tone: issueTone(item), title: item.severity === 'error' ? '质量异常' : '质量提醒', detail: item.detail, time: item.lastSeen })), ...detail.sourceStatus.slice(0, 3).map((item) => ({ tone: item.online ? 'success' : 'muted', title: item.online ? '数据上报' : '来源离线', detail: `${protocolDisplayLabel(item.protocol)} · ${item.online ? '当前在线' : '暂无在线数据'}`, time: item.lastSeen })) ].slice(0, 5); return } /> } renderItem={(event, index) => {event.tone === 'success' ? : }
{event.title}

{event.detail}

} />
; } function TelemetryFieldCell({ item }: { item: LatestTelemetryValue }) { return
{item.label}{item.sourceField}
; } type TelemetryValuePresentation = { complex: boolean; preview: string; summary: string; }; function tryParseTelemetryJSON(value: string) { const trimmed = value.trim(); if (!(trimmed.startsWith('[') && trimmed.endsWith(']')) && !(trimmed.startsWith('{') && trimmed.endsWith('}'))) return undefined; try { return JSON.parse(trimmed) as unknown; } catch { return undefined; } } function telemetryValuePresentation(item: LatestTelemetryValue): TelemetryValuePresentation { const formatted = formatTelemetryValue(item.value, item.displayValue); const parsed = typeof item.value === 'string' ? tryParseTelemetryJSON(item.value) : typeof item.displayValue === 'string' ? tryParseTelemetryJSON(item.displayValue) : item.value; if (Array.isArray(parsed)) { return { complex: true, preview: parsed.slice(0, 3).map((value) => String(value)).join(',') || '空数组', summary: `${formatZhNumber(parsed.length)} 个值` }; } if (parsed && typeof parsed === 'object') { const entries = Object.entries(parsed); return { complex: true, preview: entries.slice(0, 2).map(([key]) => key).join(',') || '空对象', summary: `${formatZhNumber(entries.length)} 个字段` }; } const detail = String(formatted); return { complex: detail.length > 42, preview: detail.length > 54 ? `${detail.slice(0, 54)}…` : detail, summary: detail.length > 42 ? `${formatZhNumber(detail.length)} 个字符` : detail }; } function telemetryValueDetail(item: LatestTelemetryValue) { const parsed = typeof item.value === 'string' ? tryParseTelemetryJSON(item.value) : typeof item.displayValue === 'string' ? tryParseTelemetryJSON(item.displayValue) : item.value; if (parsed && typeof parsed === 'object') return JSON.stringify(parsed, null, 2); return String(formatTelemetryValue(item.value, item.displayValue)); } function TelemetryValueCell({ item, onInspect }: { item: LatestTelemetryValue; onInspect?: (item: LatestTelemetryValue) => void }) { const presentation = telemetryValuePresentation(item); if (presentation.complex) { if (!onInspect) { return
{presentation.summary}{presentation.preview}
; } return
; } return
{presentation.summary}{item.unit ? {item.unit} : null}
; } function TelemetryQualityCell({ item }: { item: LatestTelemetryValue }) { return
{telemetryQualityLabel(item.quality)}{item.qualityReason || '未提供质量说明'} · {formatZhNumber(item.freshnessSeconds, 0)}s
; } function TelemetryTimeCell({ item }: { item: LatestTelemetryValue }) { return
设备{formatTelemetryTime(item.deviceTime)}接收{formatTelemetryTime(item.serverTime)}
; } function TelemetrySourceCell({ item }: { item: LatestTelemetryValue }) { return
{item.sourceEndpoint || '协议默认来源'}
; } function telemetryRowKey(item?: LatestTelemetryValue) { return [ item?.protocol ?? '', item?.sourceEndpoint ?? '', item?.key || `${item?.category ?? ''}:${item?.sourceField ?? ''}` ].join('\u0000'); } function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryResponse; pending: boolean; error?: string }) { const mobileLayout = useMobileLayout(); const [selectedProtocol, setSelectedProtocol] = useState(''); const [selectedCategory, setSelectedCategory] = useState('vehicle'); const [inspectedValue, setInspectedValue] = useState(); const [copiedValue, setCopiedValue] = useState(false); const inspectedPresentation = useMemo(() => inspectedValue ? telemetryValuePresentation(inspectedValue) : undefined, [inspectedValue]); const inspectedDetail = useMemo(() => inspectedValue ? telemetryValueDetail(inspectedValue) : '', [inspectedValue]); const closeInspection = () => { setInspectedValue(undefined); setCopiedValue(false); }; const indexed = useMemo(() => { const valuesByProtocolCategory = new Map(); const sources = new Map(); const protocols: string[] = []; for (const value of data?.values ?? []) { if (!protocols.includes(value.protocol)) protocols.push(value.protocol); const categoryKey = `${value.protocol}\u0000${value.category}`; const values = valuesByProtocolCategory.get(categoryKey); if (values) values.push(value); else valuesByProtocolCategory.set(categoryKey, [value]); const sourceKey = `${value.protocol}\u0000${value.sourceEndpoint ?? ''}`; if (!sources.has(sourceKey)) sources.set(sourceKey, { protocol: value.protocol, endpoint: value.sourceEndpoint }); } const order = ['GB32960', 'JT808', 'YUTONG_MQTT']; protocols.sort((left, right) => order.indexOf(left) - order.indexOf(right)); return { valuesByProtocolCategory, protocols, sources: [...sources.values()] }; }, [data]); const activeProtocol = indexed.protocols.includes(selectedProtocol) ? selectedProtocol : indexed.protocols[0] ?? ''; const categoryLabels = new Map((data?.categories ?? []).map((category) => [category.key, category.label])); const categories = [...new Set((data?.values ?? []).filter((value) => value.protocol === activeProtocol).map((value) => value.category))] .map((key) => ({ key, label: categoryLabels.get(key) ?? key, count: indexed.valuesByProtocolCategory.get(`${activeProtocol}\u0000${key}`)?.length ?? 0 })); const activeCategory = indexed.valuesByProtocolCategory.has(`${activeProtocol}\u0000${selectedCategory}`) ? selectedCategory : categories[0]?.key ?? ''; const visibleMetrics = indexed.valuesByProtocolCategory.get(`${activeProtocol}\u0000${activeCategory}`) ?? []; const columns = useMemo(() => [ { title: '字段 / 协议映射', dataIndex: 'label', width: 260, render: (_: unknown, item: LatestTelemetryValue) => }, { title: '当前值', dataIndex: 'value', width: 170, render: (_: unknown, item: LatestTelemetryValue) => }, { title: '质量 / 新鲜度', dataIndex: 'quality', width: 170, render: (_: unknown, item: LatestTelemetryValue) => }, { title: '设备 / 接收时间', dataIndex: 'deviceTime', width: 230, render: (_: unknown, item: LatestTelemetryValue) => }, { title: '数据来源', dataIndex: 'protocol', width: 180, render: (_: unknown, item: LatestTelemetryValue) => } ], []); const state = pending ?
正在读取最新遥测按协议整理最新标量证据…
: error ?
最新遥测不可用{error}
: visibleMetrics.length === 0 ? : mobileLayout ?
} /> :
; return
{ setSelectedProtocol(protocol); setSelectedCategory('vehicle'); }} items={indexed.protocols.map((protocol) => ({ key: protocol, label: protocolDisplayLabel(protocol), count: (data?.values ?? []).filter((value) => value.protocol === protocol).length }))} />
{state}
来源{indexed.sources.map((source) => {protocolDisplayLabel(source.protocol)})}扫描 {data?.scannedFrames ?? 0} 帧 · 上海时间 {formatTelemetryTime(data?.asOf)}
, detail: inspectedValue.sourceEndpoint || '协议默认来源', tone: 'primary' }, { label: '完整值', value: inspectedPresentation?.summary || '—', detail: inspectedValue.unit ? `单位 ${inspectedValue.unit}` : '无单位' }, { label: '数据质量', value: telemetryQualityLabel(inspectedValue.quality), detail: `${formatZhNumber(inspectedValue.freshnessSeconds, 0)}s · ${inspectedValue.qualityReason || '未提供说明'}`, tone: inspectedValue.quality === 'good' ? 'success' : inspectedValue.quality === 'stale' ? 'warning' : 'danger' } ] : []} footerNote="完整值按需渲染,不影响实时遥测刷新。" primaryAction={{ label: '完成', onClick: closeInspection }} onCancel={closeInspection} > {inspectedValue ?
完整数据{inspectedPresentation?.summary}
{inspectedDetail}
: null}
; } function CurrentVehicleAddress({ vehicle, fallback }: { vehicle?: VehicleRealtimeRow; fallback?: string }) { const coordinate = vehicle && vehicle.locationAvailable !== false && isValidAMapCoordinate(vehicle.longitude, vehicle.latitude) ? { longitude: Number(vehicle.longitude.toFixed(4)), latitude: Number(vehicle.latitude.toFixed(4)) } : undefined; const coordinateKey = coordinate ? `${coordinate.longitude.toFixed(4)},${coordinate.latitude.toFixed(4)}` : ''; const [requestedKey, setRequestedKey] = useState(''); const requested = requestedKey ? requestedKey.split(',').map(Number) : []; const address = useQuery({ queryKey: ['vehicle-detail-address', requestedKey], enabled: requested.length === 2, queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({ longitude: requested[0].toFixed(4), latitude: requested[1].toFixed(4) }), signal), staleTime: 6 * 60 * 60_000, gcTime: QUERY_MEMORY.highVolumeGcTime, refetchOnWindowFocus: false, retry: 1 }); const moved = !!(coordinateKey && requestedKey && coordinateKey !== requestedKey); return
当前位置{coordinate ? `${vehicle!.longitude.toFixed(6)}, ${vehicle!.latitude.toFixed(6)}` : '—'} 地理位置{!coordinate ? 暂无有效实时坐标 : !requestedKey ? : address.isFetching && !address.data ? 地址解析中… : address.isError ? : {address.data?.formattedAddress || fallback || '暂无地址结果'}} {moved ? : null}
; } const vehicleSectionIDs = { location: 'vehicle-location-panel', events: 'vehicle-events-panel', telemetry: 'vehicle-telemetry-panel', archive: 'vehicle-archive-panel' } as const; type VehicleSection = keyof typeof vehicleSectionIDs; function VehicleRecordNavigation() { const jumpToSection = (section: VehicleSection) => { const target = document.getElementById(vehicleSectionIDs[section]); if (!target) return; target.scrollIntoView({ behavior: 'smooth', block: 'start' }); target.focus({ preventScroll: true }); }; const navigationItems = [ ['location', '实时位置', ], ['events', '最近事件', ], ['telemetry', '实时遥测', ], ['archive', '车辆主档', ] ] as const; return {navigationItems.map(([key, label, icon]) => )} } /> ; } function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, telemetryError, monitorReturn, onUpdated }: { detail: VehicleDetail; liveRealtime?: VehicleRealtimeRow; telemetry?: LatestTelemetryResponse; telemetryPending: boolean; telemetryError?: string; monitorReturn: string; onUpdated: () => void }) { const { session } = usePlatformSession(); const navigate = useNavigate(); const mobileLayout = useMobileLayout(); const recordRootRef = useRef(null); const [sourceEvidenceOpen, setSourceEvidenceOpen] = useState(false); const realtime = liveRealtime ?? detail.realtimeSummary; const identity = detail.identity; const hasLocation = !!(realtime && realtime.locationAvailable !== false && isValidAMapCoordinate(realtime.longitude, realtime.latitude)); const mapVehicles = hasLocation && realtime ? [realtime] : []; const actions = [ { key: 'switch', label: '切换车辆', icon: , to: '/vehicles', type: 'primary' as const }, ...(hasMenu(session, 'tracks') ? [{ key: 'tracks', label: '轨迹回放', icon: , to: withMonitorReturn(`/tracks?vin=${encodeURIComponent(detail.vin)}`, monitorReturn), type: 'tertiary' as const }] : []), ...(hasMenu(session, 'history') ? [{ key: 'history', label: '历史数据', icon: , to: withMonitorReturn(`/history?vin=${encodeURIComponent(detail.vin)}`, monitorReturn), type: 'tertiary' as const }] : []), ...(hasMenu(session, 'statistics') ? [{ key: 'statistics', label: '里程查询', icon: , to: withMonitorReturn(`/statistics?vins=${encodeURIComponent(detail.vin)}`, monitorReturn), type: 'tertiary' as const }] : []), ...(hasMenu(session, 'alerts') ? [{ key: 'alerts', label: '告警事件', icon: , to: `/alerts?vin=${encodeURIComponent(detail.vin)}`, type: 'tertiary' as const }] : []) ]; const liveMetrics: WorkspaceQueueMetricRailItem[] = [ { label: '速度', value: <>{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}km/h, note: '实时车速', tone: 'primary', emphasis: 'primary' }, { label: 'SOC', value: <>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}%, note: '剩余电量', tone: 'success', emphasis: 'secondary' }, { label: '总里程', value: <>{availableMetric(realtime?.totalMileageKm, realtime?.mileageAvailable)}km, note: 推荐口径, emphasis: 'secondary', ariaLabel: '查看总里程全部来源', onClick: () => setSourceEvidenceOpen(true) }, { label: '当日里程', value: <>{availableMetric(realtime?.todayMileageKm, realtime?.todayMileageAvailable)}km, note: 今日累计, tone: 'primary', emphasis: 'primary', ariaLabel: '查看当日里程全部来源', onClick: () => setSourceEvidenceOpen(true) }, { label: '来源状态', value: <>{realtime?.onlineSourceCount ?? 0}/ {detail.sourceStatus.length}, note: '在线来源 / 全部', tone: (realtime?.onlineSourceCount ?? 0) > 0 ? 'success' : 'neutral', emphasis: 'secondary' } ]; useLayoutEffect(() => { const root = recordRootRef.current; if (!root) return; const scrollOwner = root.closest('.v2-content') ?? document.querySelector('.v2-content'); let active = true; let resetFrame = 0; const timers: number[] = []; const revealIdentity = () => { if (active) revealVehicleIdentity(root); }; const stopEntryGuard = () => { if (!active) return; active = false; window.cancelAnimationFrame(resetFrame); for (const timer of timers) window.clearTimeout(timer); resizeObserver?.disconnect(); scrollOwner?.removeEventListener('scroll', restoreEntryPosition); for (const eventName of interactionEvents) window.removeEventListener(eventName, stopEntryGuard, true); }; const restoreEntryPosition = () => { if (!active || !scrollOwner || scrollOwner.scrollTop === 0) return; window.cancelAnimationFrame(resetFrame); resetFrame = window.requestAnimationFrame(revealIdentity); }; const interactionEvents = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const; for (const eventName of interactionEvents) window.addEventListener(eventName, stopEntryGuard, { capture: true, passive: true }); scrollOwner?.addEventListener('scroll', restoreEntryPosition, { passive: true }); const resizeObserver = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(revealIdentity); resizeObserver?.observe(root); revealIdentity(); const frame = window.requestAnimationFrame(revealIdentity); // Map tiles, source evidence and live data settle independently. Guard the // first viewport through those late layout changes, but yield immediately // as soon as the user starts scrolling or interacting. for (const delay of [80, 180, 360, 720, 1200, 1800, 2400, 3600, 5200]) timers.push(window.setTimeout(revealIdentity, delay)); return () => { window.cancelAnimationFrame(frame); stopEntryGuard(); }; }, [detail.vin]); return
{fmt(identity?.plate || realtime?.plate)} {realtime?.online ? '在线' : '离线'} } description={VIN{detail.vin}更新时间:
; } export default function VehiclePage() { const { vin } = useParams(); const [searchParams] = useSearchParams(); const monitorReturn = monitorReturnFromParams(searchParams); const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: !!vin, queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime }); const resolvedVin = query.data?.lookupResolved ? query.data.vin : ''; const realtime = useQuery({ queryKey: ['vehicle-detail-realtime', resolvedVin], queryFn: ({ signal }) => api.vehicleRealtime(new URLSearchParams({ keywords: resolvedVin, limit: '1', offset: '0' }), signal), enabled: !!resolvedVin, staleTime: 5_000, gcTime: QUERY_MEMORY.highVolumeGcTime, refetchInterval: resolvedVin ? SINGLE_VEHICLE_REFRESH_MS : false, ...LIVE_QUERY_POLICY }); const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: !!vin, queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY }); useEffect(() => { resetWorkspaceScroll(); let trailingFrame = 0; const frame = window.requestAnimationFrame(() => { resetWorkspaceScroll(); trailingFrame = window.requestAnimationFrame(() => resetWorkspaceScroll()); }); const shortTimer = window.setTimeout(resetWorkspaceScroll, 120); const settledTimer = window.setTimeout(resetWorkspaceScroll, 360); return () => { window.cancelAnimationFrame(frame); window.cancelAnimationFrame(trailingFrame); window.clearTimeout(shortTimer); window.clearTimeout(settledTimer); }; }, [vin, resolvedVin]); if (!vin) return }>; if (query.isPending) return ; if (query.isError) return
query.refetch()} />
; if (!query.data.lookupResolved) return } title="未找到车辆" description={`没有匹配“${vin}”的车牌、VIN 或终端记录。`}>; return { void query.refetch(); void realtime.refetch(); void telemetry.refetch(); }} />; }