feat(platform): enrich vehicle source evidence matrix

This commit is contained in:
lingniu
2026-07-04 10:06:43 +08:00
parent 7925be676b
commit e59e5f7c11
2 changed files with 62 additions and 2 deletions

View File

@@ -2,7 +2,7 @@ import { Banner, Button, Card, Descriptions, Form, Select, Space, Table, Tabs, T
import { IconCopy, 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 type { QualityIssueRow, RealtimeLocationRow, VehicleDetail as VehicleDetailData, VehicleSourceStatus } from '../api/types';
import { PageHeader } from '../components/PageHeader';
import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag';
@@ -36,6 +36,11 @@ function formatCompactNumber(value?: number, suffix = '') {
return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}${suffix}`;
}
function formatLocation(row?: RealtimeLocationRow) {
if (!row || !isFiniteNumber(row.longitude) || !isFiniteNumber(row.latitude)) return '-';
return `${row.longitude}, ${row.latitude}`;
}
function parseTimeMs(value?: string) {
if (!value) return undefined;
const ms = Date.parse(value);
@@ -152,6 +157,14 @@ export function VehicleDetail({
const lastSeen = resolution?.lastSeen || summary?.lastSeen || latest?.lastSeen || '-';
const serviceStatus = detail?.serviceStatus;
const realtimeRows = detail?.realtime ?? [];
const latestRealtimeByProtocol = useMemo(() => {
const next = new Map<string, RealtimeLocationRow>();
for (const row of realtimeRows) {
if (!row.protocol || next.has(row.protocol)) continue;
next.set(row.protocol, row);
}
return next;
}, [realtimeRows]);
const realtimeSources = new Set(realtimeRows.map((row) => row.protocol).filter(Boolean));
const sourceCount = Math.max(detail?.sourceStatus?.length ?? 0, realtimeSources.size, summary?.sourceCount ?? 0, protocols.length);
const onlineSourceCount = detail?.sourceStatus?.length
@@ -488,6 +501,26 @@ export function VehicleDetail({
},
{ title: '在线', width: 100, render: (_: unknown, row: VehicleSourceStatus) => <StatusTag status={row.online ? 'ok' : 'offline'} /> },
{ title: '最后时间', dataIndex: 'lastSeen', width: 190, render: (value?: string) => value || '无上报' },
{
title: '最新位置',
width: 170,
render: (_: unknown, row: VehicleSourceStatus) => formatLocation(latestRealtimeByProtocol.get(row.protocol))
},
{
title: '里程',
width: 110,
render: (_: unknown, row: VehicleSourceStatus) => formatCompactNumber(latestRealtimeByProtocol.get(row.protocol)?.totalMileageKm, ' km')
},
{
title: '速度',
width: 110,
render: (_: unknown, row: VehicleSourceStatus) => formatCompactNumber(latestRealtimeByProtocol.get(row.protocol)?.speedKmh, ' km/h')
},
{
title: 'SOC',
width: 90,
render: (_: unknown, row: VehicleSourceStatus) => formatCompactNumber(latestRealtimeByProtocol.get(row.protocol)?.socPercent, '%')
},
...sourceCapabilities.map((item) => ({
title: item.label,
width: 90,

View File

@@ -4337,7 +4337,30 @@ test('switches vehicle detail to a source from the source matrix', async () => {
{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true },
{ protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false }
],
realtime: [],
realtime: [
{
vin: 'VIN001',
plate: '粤AG18312',
protocol: 'GB32960',
longitude: 113.2,
latitude: 23.1,
speedKmh: 30,
socPercent: 78,
totalMileageKm: 100,
lastSeen: '2026-07-03 20:12:10'
},
{
vin: 'VIN001',
plate: '粤AG18312',
protocol: 'JT808',
longitude: 113.21,
latitude: 23.11,
speedKmh: 28,
socPercent: 0,
totalMileageKm: 99.5,
lastSeen: '2026-07-03 20:12:08'
}
],
history: { items: [], total: 0, limit: 20, offset: 0 },
raw: { items: [], total: 0, limit: 10, offset: 0 },
mileage: { items: [], total: 0, limit: 20, offset: 0 },
@@ -4368,6 +4391,10 @@ test('switches vehicle detail to a source from the source matrix', async () => {
expect(screen.getByText('主来源')).toBeInTheDocument();
expect(screen.getByText('在线证据')).toBeInTheDocument();
expect(screen.getByText('暂无来源')).toBeInTheDocument();
expect(screen.getByText('最新位置')).toBeInTheDocument();
expect(screen.getAllByText('113.2, 23.1').length).toBeGreaterThan(0);
expect(screen.getByText(/100\s*km/)).toBeInTheDocument();
expect(screen.getByText(/30\s*km\/h/)).toBeInTheDocument();
expect(screen.getAllByText('YUTONG_MQTT').length).toBeGreaterThan(0);
expect(screen.getAllByText('无上报').length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole('button', { name: '仅看 JT808' }));