import { useQuery } from '@tanstack/react-query'; import { Button, Card, Empty, Input, Spin, Tag } from '@douyinfe/semi-ui'; import { useMemo, useState } from 'react'; import { api } from '../../api/client'; import type { VehicleLocationSourceEvidence, VehicleMileageSourceEvidence } from '../../api/types'; import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols'; import { QUERY_MEMORY } from '../queryPolicy'; function today() { const now = new Date(); const offset = now.getTimezoneOffset() * 60_000; return new Date(now.getTime() - offset).toISOString().slice(0, 10); } function number(value?: number, digits = 1) { return value == null || !Number.isFinite(value) ? '—' : value.toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits }); } function coordinate(source: VehicleLocationSourceEvidence) { if (source.longitude == null || source.latitude == null) return '—'; return `${source.longitude.toFixed(6)}, ${source.latitude.toFixed(6)}`; } function evidenceTone(source: Pick) { if (!source.enabled) return 'disabled'; if (source.qualityStatus !== 'OK') return 'warning'; if (source.recommended) return 'recommended'; return source.online ? 'ready' : 'offline'; } function sourceBadges(source: Pick) { return <> {source.recommended ? 当前推荐 : null} {!source.recommended && source.selectedWithinProtocol ? 协议内选中 : null} {!source.enabled ? 已禁用 : null} {source.enabled && !source.online ? 离线 : null} {source.qualityStatus !== 'OK' ? {source.qualityStatus} : null} ; } function evidenceSourceLabel(sourceLabel: string | undefined, protocol: string) { const normalized = sourceLabel?.trim(); if (!normalized || normalized === protocol) return protocolDisplayLabel(protocol); return protocolSourceLabel(normalized); } function LocationSourceCard({ source }: { source: VehicleLocationSourceEvidence }) { return
{evidenceSourceLabel(source.sourceLabel, source.protocol)}{protocolDisplayLabel(source.protocol)}{source.terminalLabel ? ` · ${source.terminalLabel}` : ''}
坐标
{coordinate(source)}
速度
{source.speedKmh == null ? '—' : `${number(source.speedKmh)} km/h`}
总里程
{source.totalMileageKm == null ? '—' : `${number(source.totalMileageKm)} km`}
SOC
{source.socPercent == null ? '—' : `${number(source.socPercent)}%`}
设备时间
{source.eventTime || '—'}
接收时间
{source.receivedAt || '—'}
{source.qualityReason ?

质量说明:{source.qualityReason}

: null}
; } function MileageSourceCard({ source }: { source: VehicleMileageSourceEvidence }) { const comparable = { ...source, online: true }; return
{evidenceSourceLabel(source.sourceLabel, source.protocol)}{protocolDisplayLabel(source.protocol)}{source.terminalLabel ? ` · ${source.terminalLabel}` : ''}
当日里程
{source.dailyMileageKm == null ? '—' : `${number(source.dailyMileageKm)} km`}
最新总里程
{source.latestTotalMileageKm == null ? '—' : `${number(source.latestTotalMileageKm)} km`}
起始总里程
{source.firstTotalMileageKm == null ? '—' : `${number(source.firstTotalMileageKm)} km`}
有效样本
{source.sampleCount.toLocaleString('zh-CN')}
首条时间
{source.firstEventTime || '—'}
末条时间
{source.latestEventTime || '—'}
{source.qualityReason ?

选举说明:{source.qualityReason}

: null}
; } export function VehicleSourceEvidencePanel({ vin, compact = false, open, onOpenChange }: { vin: string; compact?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; }) { const [internalOpen, setInternalOpen] = useState(false); const expanded = open ?? internalOpen; const setExpanded = (value: boolean) => { if (open == null) setInternalOpen(value); onOpenChange?.(value); }; const [date, setDate] = useState(today); const query = useQuery({ queryKey: ['vehicle-source-evidence', vin, date], queryFn: ({ signal }) => api.vehicleSourceEvidence(vin, date, signal), enabled: expanded && Boolean(vin), staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchOnWindowFocus: false }); const sourceCount = (query.data?.locationSources.length ?? 0) + (query.data?.mileageSources.length ?? 0); const description = useMemo(() => { if (!query.data) return '按需读取,不影响实时刷新'; if (sourceCount <= 2) return '当前车辆来源较少,仅展示实际存在的证据'; return `已读取 ${query.data.locationSources.length} 个位置来源、${query.data.mileageSources.length} 个里程来源`; }, [query.data, sourceCount]); return
位置与里程来源{description}
{expanded ?
终端已脱敏;推荐结果来自后台选举,不改动原始证据。
{query.isPending ?
正在读取全部来源证据…
: null} {query.isError ?
{query.error instanceof Error ? query.error.message : '来源证据读取失败'}
: null} {query.data ? <>
推荐位置来源{evidenceSourceLabel(query.data.recommendedLocationLabel, query.data.recommendedLocationProtocol)}
最大位置差{number(query.data.comparison.locationMaxDistanceM)}m
总里程差{number(query.data.comparison.totalMileageDeltaKm)}km
上报时间差{number(query.data.comparison.reportTimeDeltaSeconds, 0)}s
{query.data.locationSources.length ?
当前位置来源{query.data.locationConflict ? `后台检测到位置冲突${query.data.conflictDistanceM == null ? '' : ` · ${number(query.data.conflictDistanceM)} m`}` : '推荐来源与备用来源并列展示'}
{query.data.locationSources.map((source, index) => )}
: null} {query.data.mileageSources.length ?
{query.data.mileageDate} 里程来源日里程差 {number(query.data.comparison.dailyMileageDeltaKm)} km
{query.data.mileageSources.map((source, index) => )}
: null} {!query.data.locationSources.length && !query.data.mileageSources.length ? : null} : null}
: null}
; }