import { IconClose, IconDownload, IconRefresh, IconSave, IconSearch } from '@douyinfe/semi-icons'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FormEvent, useEffect, useMemo, useState } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { AccessProtocolStatus, AccessQuery, AccessSummary, AccessThresholdConfig, AccessThresholdUpdate, AccessUnresolvedIdentity, AccessVehicleRow, Page } from '../../api/types'; import { accessRowsToCSV, formatAccessTime, formatSeconds, updateProtocolThreshold } from '../domain/access'; import { InlineError } from '../shared/AsyncState'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister } from '../auth/session'; import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy'; const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT'] as const; const EMPTY_FILTERS = { keyword: '', protocol: '', oem: '', connectionState: '', onlineState: '', model: '', provider: '', firstSeenFrom: '', firstSeenTo: '', latestSeenFrom: '', latestSeenTo: '', delayState: '' }; type Filters = typeof EMPTY_FILTERS; const connectionLabels: Record = { healthy: '三协议正常', degraded: '协议离线', incomplete: '接入不完整', offline: '全部离线', not_connected: '尚未接入' }; function statusByProtocol(row: AccessVehicleRow, protocol: string) { return row.protocolStatuses.find((item) => item.protocol === protocol); } function compactTime(value: string) { if (!value) return '—'; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return '—'; return new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(parsed).replace(/\//g, '-'); } function ProtocolState({ status, detailed = false }: { status?: AccessProtocolStatus; detailed?: boolean }) { const state = !status?.connected ? 'missing' : status.onlineState; const label = state === 'missing' ? '未接入' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报'; if (detailed) { return
{status?.protocol}{label}
接入厂家
{status?.provider || '—'}
首次接入
{formatAccessTime(status?.firstSeenAt || '')}
最新上报
{formatAccessTime(status?.latestReceivedAt || '')}
当前离线
{formatSeconds(status?.freshnessSec)}
上报间隔
{formatSeconds(status?.reportIntervalSec)}
数据延迟
{formatSeconds(status?.dataDelaySec)}

{status?.firstSeenEvidence || '应接协议尚未形成实时快照'}

; } return
{label} {status?.connected ? compactTime(status.latestReceivedAt) : '等待接入'} {status?.provider || (status?.connected ? '接入方未维护' : '无实时快照')}
; } function ConnectionState({ row }: { row: AccessVehicleRow }) { return
{connectionLabels[row.connectionState]}{row.actualProtocols.length} / {row.expectedProtocols.length} 已接入
; } function ProtocolCoverage({ summary }: { summary?: AccessSummary }) { return
{PROTOCOLS.map((protocol) => { const actual = summary?.protocols.find((item) => item.name === protocol); const total = summary?.totalVehicles ?? 0; return {protocol}{(actual?.total ?? 0).toLocaleString('zh-CN')} / {total.toLocaleString('zh-CN')}; })}
; } function VehicleInspector({ row, onClose }: { row: AccessVehicleRow; onClose: () => void }) { return ; } function IdentityQueue({ items, total }: { items: AccessUnresolvedIdentity[]; total: number }) { if (!total) return null; return
另有 {total.toLocaleString('zh-CN')} 条来源身份待绑定这些来源不计入主车辆,绑定权威 VIN 后再归档
{items.slice(0, 6).map((item) =>
{item.identifierMasked}{item.protocol} · {item.plate || '车牌待核对'} · {formatAccessTime(item.latestSeenAt)}{item.recommendedAction}
)}
; } function ThresholdSettings({ config, draft, editable, saving, error, onChange, onSave }: { config?: AccessThresholdConfig; draft?: AccessThresholdUpdate; editable: boolean; saving: boolean; error?: string; onChange: (next: AccessThresholdUpdate) => void; onSave: () => void }) { if (!draft) return null; return
在线判定阈值 · v{config?.version ?? '—'}
{PROTOCOLS.map((protocol) => )}{error ?

{error}

: null}{editable ? : 当前账户为只读角色}
; } function downloadRows(rows: AccessVehicleRow[]) { const blob = new Blob([accessRowsToCSV(rows)], { type: 'text/csv;charset=utf-8' }); const href = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = href; anchor.download = `vehicle-access-${new Date().toISOString().slice(0, 10)}.csv`; anchor.click(); URL.revokeObjectURL(href); } export default function AccessPage() { const { session } = usePlatformSession(); const editable = canAdminister(session); const [searchParams, setSearchParams] = useSearchParams(); const initial = Object.fromEntries(Object.keys(EMPTY_FILTERS).map((key) => [key, searchParams.get(key) ?? ''])) as Filters; const [draft, setDraft] = useState(initial); const [criteria, setCriteria] = useState(initial); const [offset, setOffset] = useState(0); const [limit, setLimit] = useState(50); const [selectedVIN, setSelectedVIN] = useState(''); const [thresholdDraft, setThresholdDraft] = useState(); const queryClient = useQueryClient(); const baseQuery = useMemo(() => Object.fromEntries(Object.entries(criteria).filter(([, value]) => value)) as AccessQuery, [criteria]); const vehicleScope = useMemo(() => queryScopeKey(baseQuery), [baseQuery]); const summaryQuery = useQuery({ queryKey: ['access-summary'], queryFn: ({ signal }) => api.accessSummary({}, signal), staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime }); const vehiclesQuery = useQuery>({ queryKey: ['access-vehicles', vehicleScope, limit, offset], queryFn: ({ signal }) => api.accessVehicles({ ...baseQuery, limit, offset }, signal), placeholderData: retainPreviousPageWithinScope>(vehicleScope), gcTime: QUERY_MEMORY.highVolumeGcTime }); const unresolvedQuery = useQuery({ queryKey: ['access-unresolved-identities', criteria.keyword, criteria.protocol], queryFn: ({ signal }) => api.accessUnresolvedIdentities({ keyword: criteria.keyword || undefined, protocol: criteria.protocol || undefined, limit: 20, offset: 0 }, signal), enabled: editable, staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime }); const thresholdQuery = useQuery({ queryKey: ['access-thresholds'], queryFn: ({ signal }) => api.accessThresholds(signal), enabled: editable, staleTime: 60_000, gcTime: QUERY_MEMORY.summaryGcTime }); useEffect(() => { if (thresholdQuery.data && !thresholdDraft) setThresholdDraft({ version: thresholdQuery.data.version, defaultThresholdSec: thresholdQuery.data.defaultThresholdSec, delayThresholdSec: thresholdQuery.data.delayThresholdSec, longOfflineSec: thresholdQuery.data.longOfflineSec, protocols: thresholdQuery.data.protocols }); }, [thresholdDraft, thresholdQuery.data]); const updateThreshold = useMutation({ mutationFn: api.updateAccessThresholds, onSuccess: async (config) => { setThresholdDraft({ version: config.version, defaultThresholdSec: config.defaultThresholdSec, delayThresholdSec: config.delayThresholdSec, longOfflineSec: config.longOfflineSec, protocols: config.protocols }); await Promise.all([queryClient.invalidateQueries({ queryKey: ['access-summary'] }), queryClient.invalidateQueries({ queryKey: ['access-vehicles'] })]); } }); const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN); const syncURL = (filters: Filters) => { const next = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) next.set(key, value); }); setSearchParams(next, { replace: true }); }; const apply = (next: Filters) => { setDraft(next); setCriteria(next); setOffset(0); setSelectedVIN(''); syncURL(next); }; const submit = (event: FormEvent) => { event.preventDefault(); apply(draft); }; const page = Math.floor(offset / limit) + 1; const totalPages = Math.max(1, Math.ceil((vehiclesQuery.data?.total ?? 0) / limit)); const summary = summaryQuery.data; const refresh = () => Promise.all([summaryQuery.refetch(), vehiclesQuery.refetch(), ...(editable ? [unresolvedQuery.refetch(), thresholdQuery.refetch()] : [])]); return

车辆接入管理

以主车辆为对象,对照应接协议、实际接入和各协议最新上报时间

数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}
{summaryQuery.isError ? summaryQuery.refetch()} /> : null}
{[ ['主车辆', summary?.totalVehicles ?? 0, 'all', ''], ['有接入差异', Math.max(0, (summary?.totalVehicles ?? 0) - (summary?.healthyVehicles ?? 0)), 'attention', 'attention'], ['全部离线', summary?.offlineVehicles ?? 0, 'offline', 'offline'], ['尚未接入', summary?.neverReported ?? 0, 'never', 'not_connected'] ].map(([label, value, tone, connectionState]) => )}
{vehiclesQuery.isError ? vehiclesQuery.refetch()} /> : null}
车辆协议接入差异优先展示应接与实接差异;时间为各协议最后接收时间
{PROTOCOLS.map((item) => )}{rows.map((row) => setSelectedVIN(row.vin)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') setSelectedVIN(row.vin); }}>{PROTOCOLS.map((protocol) => )})}
车辆品牌 / 车型应接协议{item}综合状态
{row.plate || '未绑定车牌'}{row.vin}{row.oem || '品牌未维护'}{row.model || row.company || '车型未维护'}{row.expectedProtocols.length} 项{row.expectedProtocols.join(' / ')}
{vehiclesQuery.isFetching ?
正在更新车辆接入状态…
: null}{!vehiclesQuery.isFetching && !rows.length ?
当前筛选条件没有车辆
: null}
第 {page} / {totalPages} 页,共 {(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆
{selected ? setSelectedVIN('')} /> : null}
{editable && unresolvedQuery.isError ? unresolvedQuery.refetch()} /> : null} {editable ? : null} {editable && thresholdQuery.isError ? thresholdQuery.refetch()} /> : null} {editable ? thresholdDraft && updateThreshold.mutate(thresholdDraft)} /> : null}
; }