import { useMutation, useQuery } from '@tanstack/react-query'; import { IconAlarm, IconArrowRight, IconBox, IconCalendar, IconClock, IconCopy, IconMapPin, IconSearch, IconTickCircle } from '@douyinfe/semi-icons'; import { FormEvent, useMemo, useState } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { LatestTelemetryResponse, QualityIssueRow, VehicleDetail, VehicleProfileSyncItem, VehicleProfileSyncResult } from '../../api/types'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister } from '../auth/session'; import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy'; import { formatTelemetryTime, formatTelemetryValue, telemetryQualityLabel } from '../domain/telemetry'; import { formatZhNumber } from '../domain/formatters'; import { parseVehicleProfileSyncCSV, vehicleProfileSyncCSVHeader } from '../domain/profileSync'; import { FleetMap } from '../map/FleetMap'; import { InlineError, PageLoading } from '../shared/AsyncState'; function fmt(value?: string) { return value?.trim() || '—'; } function metric(value: number | undefined, fallback = '—') { return typeof value === 'number' && Number.isFinite(value) ? formatZhNumber(value, 1) : fallback; } function timeOnly(value?: string) { if (!value) return '—'; const parts = value.split(' '); return parts[parts.length - 1] || 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 operationStatusLabels = { unknown: '待维护', active: '运营中', inactive: '停运', maintenance: '维保中', retired: '已退役' } as const; function ProfileSyncPanel({ onClose }: { onClose: () => void }) { const [sourceSystem, setSourceSystem] = useState(''); const [sourceVersion, setSourceVersion] = useState(''); const [conflictPolicy, setConflictPolicy] = useState<'preserve' | 'overwrite'>('preserve'); const [items, setItems] = useState([]); const [fileName, setFileName] = useState(''); const [parseError, setParseError] = useState(''); const sync = useMutation({ mutationFn: (dryRun) => api.syncVehicleProfiles({ sourceSystem: sourceSystem.trim(), sourceVersion: sourceVersion.trim(), conflictPolicy, dryRun, items }) }); const readFile = async (file?: File) => { sync.reset(); setItems([]); setFileName(file?.name ?? ''); setParseError(''); if (!file) return; try { setItems(parseVehicleProfileSyncCSV(await file.text())); } catch (error) { setParseError(error instanceof Error ? error.message : 'CSV 解析失败'); } }; const ready = sourceSystem.trim() !== '' && sourceVersion.trim() !== '' && items.length > 0 && !sync.isPending; const issues = sync.data?.items.filter((item) => item.status.startsWith('conflict_') || item.status === 'missing_vehicle').slice(0, 20) ?? []; const applied = sync.data && !sync.data.dryRun; return
批量同步车辆主档

CSV 最多 500 辆;先预演,再写入。默认保留人工档案和其他来源。

表头:{vehicleProfileSyncCSVHeader}

{fileName ?

{fileName} · 已读取 {items.length} 辆

: null} {parseError ?

{parseError}

: null} {sync.isError ?

{sync.error.message}

: null} {sync.data ?
收到{sync.data.received}新增{sync.data.created}更新{sync.data.updated}未变化{sync.data.unchanged}冲突{sync.data.conflicted}身份缺失{sync.data.missing}
{issues.length ?
    {issues.map((item) =>
  • {item.vin}{item.status === 'missing_vehicle' ? '网关身份不存在' : item.status === 'conflict_source_version' ? '同来源版本内容不一致' : `现有来源 ${item.previousSource || '未知'} 已保护`}
  • )}
:

未发现来源冲突或身份缺失。

}
: null} {conflictPolicy === 'overwrite' ?

覆盖模式会接管人工或其他系统维护的补充主档,请先确认预演结果。

: null}
; } function VehicleSearch() { const navigate = useNavigate(); const { session } = usePlatformSession(); const [keyword, setKeyword] = useState(''); const [syncOpen, setSyncOpen] = useState(false); const submit = (event: FormEvent) => { event.preventDefault(); const value = keyword.trim(); if (value) navigate(`/vehicles/${encodeURIComponent(value)}`); }; return

查询单车数字档案

通过车牌、VIN 或终端手机号定位车辆,并查看统一身份、实时状态和来源证据。

setKeyword(event.target.value)} placeholder="输入车牌 / VIN / 终端手机号" autoFocus /> {canAdminister(session) ? : null}
{syncOpen ? setSyncOpen(false)} /> : null}
; } function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; editable: boolean; onUpdated: () => void }) { const profile = detail.profile; const [editing, setEditing] = useState(false); const [draft, setDraft] = useState({ modelName: '', vehicleType: '', companyName: '', operationStatus: 'unknown', accessProvider: '', firstAccessAt: '', runtimeHours: '' }); const save = useMutation({ mutationFn: () => api.updateVehicleProfile(detail.vin, { 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({ 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
车辆主档完整度 {profile?.completeness ?? 0}%{editable && !editing ? : null}
{editing ?
{save.isError ?

{save.error.message}

: null}
: <>
车型 / 类型
{[profile?.modelName, profile?.vehicleType].filter(Boolean).join(' / ') || '—'}
所属企业
{fmt(profile?.companyName)}
运营状态
{operationStatusLabels[profile?.operationStatus ?? 'unknown']}
接入服务商
{fmt(profile?.accessProvider)}
首次接入
{fmt(profile?.firstAccessAt)}
累计运行
{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: `${item.protocol} · ${item.online ? '当前在线' : '暂无在线数据'}`, time: item.lastSeen })) ].slice(0, 5); return
最近事件查看全部
{events.length ? events.map((event, index) =>
{event.tone === 'success' ? : }
{event.title}

{event.detail}

) :
暂无可用事件证据
}
; } function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryResponse; pending: boolean; error?: string }) { const [selectedCategory, setSelectedCategory] = useState('vehicle'); const indexed = useMemo(() => { const valuesByCategory = new Map(); const sources = new Map(); for (const value of data?.values ?? []) { const values = valuesByCategory.get(value.category); if (values) values.push(value); else valuesByCategory.set(value.category, [value]); const sourceKey = `${value.protocol}\u0000${value.sourceEndpoint ?? ''}`; if (!sources.has(sourceKey)) sources.set(sourceKey, { protocol: value.protocol, endpoint: value.sourceEndpoint }); } return { valuesByCategory, sources: [...sources.values()] }; }, [data]); const categories = data?.categories ?? []; const activeCategory = indexed.valuesByCategory.has(selectedCategory) ? selectedCategory : categories[0]?.key ?? ''; const visibleMetrics = indexed.valuesByCategory.get(activeCategory) ?? []; return
{pending ?
正在读取最新遥测…
: error ?
最新遥测不可用:{error}
: visibleMetrics.length ? visibleMetrics.map((item) =>
{item.label}{item.sourceField} · {item.protocol}{item.sourceEndpoint ? ` · ${item.sourceEndpoint}` : ''} {formatTelemetryValue(item.value)} {item.unit}
) :
最近 {data?.scannedFrames ?? 0} 帧没有可展示的标量遥测
}
来源{indexed.sources.map((source) => {source.protocol})}扫描 {data?.scannedFrames ?? 0} 帧 · 截至 {formatTelemetryTime(data?.asOf)}
; } function VehicleRecord({ detail, telemetry, telemetryPending, telemetryError, onUpdated }: { detail: VehicleDetail; telemetry?: LatestTelemetryResponse; telemetryPending: boolean; telemetryError?: string; onUpdated: () => void }) { const { session } = usePlatformSession(); const realtime = detail.realtimeSummary; const identity = detail.identity; const mapVehicles = realtime ? [realtime] : []; const lastMileage = detail.mileage.items[0]; return
{fmt(identity?.plate || realtime?.plate)}{realtime?.online ? '在线' : '离线'}VIN{detail.vin}
接入来源

{detail.sources.map((source) => {source})}

最后上报时间{fmt(realtime?.lastSeen || identity?.lastSeen)}
轨迹回放历史数据告警事件
undefined} />
{realtime ? `实时坐标 ${realtime.longitude.toFixed(6)}, ${realtime.latitude.toFixed(6)}` : '暂无有效实时坐标'}{identity?.locationText ? ` · 档案区域 ${identity.locationText}` : ''}
实时指标{timeOnly(realtime?.lastSeen)}
速度{metric(realtime?.speedKmh)}km/h
SOC{metric(realtime?.socPercent)}%
总里程{metric(realtime?.totalMileageKm)}km
当日里程{metric(lastMileage?.dailyMileageKm)}km
在线来源{realtime?.onlineSourceCount ?? 0}
数据源总数{detail.sourceStatus.length}
; } export default function VehiclePage() { const { vin } = useParams(); const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime }); const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY }); if (!vin) return ; if (query.isPending) return ; if (query.isError) return
query.refetch()} />
; if (!query.data.lookupResolved) return

未找到车辆

没有匹配“{vin}”的车牌、VIN 或终端记录。

重新查询
; return { void query.refetch(); void telemetry.refetch(); }} />; }