import { Banner, Button, Card, Descriptions, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconRefresh, IconSearch } from '@douyinfe/semi-icons'; import { useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../api/client'; import type { QualityIssueRow, VehicleDetail as VehicleDetailData, VehicleSourceStatus } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { StatusTag } from '../components/StatusTag'; import { isLikelyVIN } from '../domain/vehicleLookup'; type VehicleQuery = { keyword: string; protocol?: string; }; const sourceCapabilities: Array<{ key: keyof Pick; label: string }> = [ { key: 'hasRealtime', label: '实时' }, { key: 'hasHistory', label: '历史' }, { key: 'hasRaw', label: 'RAW' }, { key: 'hasMileage', label: '里程' } ]; const coverageStatusText: Record = { online: '全部在线', partial: '部分在线', offline: '全部离线', no_data: '暂无来源' }; export function VehicleDetail({ vin, protocol, onOpenHistory, onOpenMileage, onQueryChange }: { vin: string; protocol?: string; onOpenHistory: (vin: string, protocol?: string) => void; onOpenMileage: (vin: string, protocol?: string) => void; onQueryChange?: (keyword: string, protocol?: string) => void; }) { const [query, setQuery] = useState({ keyword: vin, protocol }); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const requestSeq = useRef(0); const load = (nextQuery = query) => { const keyword = nextQuery.keyword.trim(); if (!keyword) { Toast.warning('请输入 VIN / 车牌 / 手机号'); return; } setLoading(true); const params = new URLSearchParams({ keyword }); if (nextQuery.protocol?.trim()) { params.set('protocol', nextQuery.protocol.trim()); } const seq = requestSeq.current + 1; requestSeq.current = seq; api.vehicleDetail(params) .then((nextDetail) => { if (requestSeq.current === seq) { setDetail(nextDetail); } }) .catch((error: Error) => { if (requestSeq.current === seq) { Toast.error(error.message); } }) .finally(() => { if (requestSeq.current === seq) { setLoading(false); } }); }; useEffect(() => { const nextQuery = { keyword: vin, protocol }; setQuery(nextQuery); load(nextQuery); }, [vin, protocol]); const identity = detail?.identity; const summary = detail?.realtimeSummary; const latest = detail?.realtime?.[0]; const resolution = detail?.resolution; const overview = detail?.serviceOverview; const resolvedVIN = resolution?.vin || detail?.vin || summary?.vin || identity?.vin || latest?.vin || query.keyword; const hasResolvedVIN = resolution?.resolved ?? detail?.lookupResolved ?? isLikelyVIN(resolvedVIN); const displayVIN = hasResolvedVIN ? resolvedVIN : '-'; const displayLookupKey = resolution?.lookupKey || detail?.lookupKey || query.keyword; const protocols = useMemo(() => resolution?.protocols?.length ? resolution.protocols : detail?.sources ?? [], [detail?.sources, resolution?.protocols]); const latestRaw = detail?.raw?.items?.[0]; const qualityCount = detail?.quality?.total ?? 0; const online = resolution?.online || summary?.online || identity?.online || false; const lastSeen = resolution?.lastSeen || summary?.lastSeen || latest?.lastSeen || '-'; const serviceStatus = detail?.serviceStatus; const activeProtocol = query.protocol?.trim() ?? ''; const scopeText = activeProtocol ? `单一来源:${activeProtocol}` : '全部来源聚合'; const formKey = `${query.keyword}-${query.protocol ?? ''}`; const switchSource = (nextProtocol = '') => { const nextQuery = { keyword: resolvedVIN || query.keyword, protocol: nextProtocol }; setQuery(nextQuery); onQueryChange?.(nextQuery.keyword, nextQuery.protocol); load(nextQuery); }; const qualityTable = ( `${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.lastSeen ?? ''}`} dataSource={detail?.quality?.items ?? []} columns={[ { title: '来源', dataIndex: 'protocol', width: 130 }, { title: '问题', dataIndex: 'issueType', width: 150 }, { title: '级别', width: 110, render: (_: unknown, row: QualityIssueRow) => {row.severity} }, { title: '最后时间', dataIndex: 'lastSeen', width: 190 }, { title: '说明', dataIndex: 'detail' } ]} /> ); return (
{ const nextQuery = { keyword: String(values.keyword ?? ''), protocol: String(values.protocol ?? '') }; setQuery(nextQuery); onQueryChange?.(nextQuery.keyword, nextQuery.protocol); load(nextQuery); }}> GB32960 JT808 YUTONG_MQTT
当前查看范围 {scopeText} {activeProtocol ? '下方实时、历史、RAW 和里程仅展示该来源返回的数据。' : '下方数据按车辆聚合展示全部可用来源。'}
{serviceStatus ? (
车辆服务状态 {serviceStatus.title} {serviceStatus.detail}
) : null} {overview ? ( {coverageStatusText[overview.coverageStatus] ?? overview.coverageStatus} }, { key: '主来源', value: overview.primaryProtocol || '-' }, { key: '最后上报', value: overview.lastSeen || '-' }, { key: '数据规模', value: `历史 ${overview.historyCount} / RAW ${overview.rawCount} / 里程 ${overview.mileageCount}` }, { key: '实时来源', value: overview.realtimeCount }, { key: '质量问题', value: 0 ? 'orange' : 'green'}>{overview.qualityIssueCount} } ]} /> ) : null}
{hasResolvedVIN ? '已解析 VIN' : '未解析到 VIN'} }, { key: '车牌', value: resolution?.plate || summary?.plate || identity?.plate || latest?.plate || '-' }, { key: '手机号', value: resolution?.phone || summary?.phone || identity?.phone || '-' }, { key: 'OEM', value: resolution?.oem || summary?.oem || identity?.oem || '-' }, { key: '在线', value: }, { key: '数据来源', value: protocols.length > 0 ? {protocols.map((item) => {item})} : '-' }, { key: '在线来源', value: summary ? `${summary.onlineSourceCount}/${summary.sourceCount}` : '-' }, { key: '最后位置时间', value: lastSeen }, { key: '最新 RAW 时间', value: latestRaw?.serverTime ?? '-' }, { key: '质量问题', value: 0 ? 'orange' : 'green'}>{qualityCount > 0 ? `${qualityCount} 项` : '无'} } ]} />
{!hasResolvedVIN ? ( ) : null}
{hasResolvedVIN ? ( <> {detail?.sourceStatus?.length ? ( <>
{activeProtocol ? `当前 ${activeProtocol}` : '当前 全部来源'}
{detail.sourceStatus.map((source) => (
{source.protocol} {source.lastSeen || '无上报'}
{sourceCapabilities.map((item) => ( {item.label}{source[item.key] ? '有' : '无'} ))}
))}
) : ( 暂未查询到该车辆的数据来源覆盖。 )}
}, { title: '最后时间', dataIndex: 'lastSeen', width: 190 }, { title: '实时', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasRealtime ? '有' : '无'} }, { title: '历史', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasHistory ? '有' : '无'} }, { title: 'RAW', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasRaw ? '有' : '无'} }, { title: '里程', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasMileage ? '有' : '无'} } ]} /> ) : null} {!hasResolvedVIN ? ( {qualityTable} ) : (
{[ { label: '最新来源', value: summary?.primaryProtocol || '-' }, { label: '速度 km/h', value: summary ? summary.speedKmh : '-' }, { label: 'SOC %', value: summary ? summary.socPercent : '-' }, { label: '总里程 km', value: summary ? summary.totalMileageKm : '-' }, { label: '经纬度', value: summary ? `${summary.longitude}, ${summary.latitude}` : '-' }, { label: '最后时间', value: lastSeen } ].map((item) => (
{item.label}
{item.value}
))}
最新 RAW 解析字段
{JSON.stringify(latestRaw?.parsedFields ?? {}, null, 2)}
{qualityTable} )} ); }