feat(platform): enrich vehicle source evidence matrix
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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' }));
|
||||
|
||||
Reference in New Issue
Block a user