feat(platform): add vehicle service overview

This commit is contained in:
lingniu
2026-07-04 02:49:06 +08:00
parent 131f59360c
commit 38e1bf422a
6 changed files with 267 additions and 12 deletions

View File

@@ -81,6 +81,7 @@ export interface VehicleDetail {
identity?: VehicleRow;
realtimeSummary?: VehicleRealtimeRow;
serviceStatus?: VehicleServiceStatus;
serviceOverview?: VehicleServiceOverview;
sources: string[];
sourceStatus: VehicleSourceStatus[];
realtime: RealtimeLocationRow[];
@@ -90,6 +91,24 @@ export interface VehicleDetail {
quality: Page<QualityIssueRow>;
}
export interface VehicleServiceOverview {
vin: string;
plate: string;
phone: string;
oem: string;
protocols: string[];
primaryProtocol: string;
coverageStatus: string;
sourceCount: number;
onlineSourceCount: number;
lastSeen: string;
realtimeCount: number;
historyCount: number;
rawCount: number;
mileageCount: number;
qualityIssueCount: number;
}
export interface VehicleServiceStatus {
status: string;
severity: string;

View File

@@ -19,6 +19,13 @@ const sourceCapabilities: Array<{ key: keyof Pick<VehicleSourceStatus, 'hasRealt
{ key: 'hasMileage', label: '里程' }
];
const coverageStatusText: Record<string, string> = {
online: '全部在线',
partial: '部分在线',
offline: '全部离线',
no_data: '暂无来源'
};
export function VehicleDetail({
vin,
protocol,
@@ -78,6 +85,7 @@ export function VehicleDetail({
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 : '-';
@@ -156,6 +164,24 @@ export function VehicleDetail({
</div>
) : null}
{overview ? (
<Card bordered title="车辆服务概览" style={{ marginTop: 16 }}>
<Descriptions
row
data={[
{ key: '服务车辆', value: [overview.plate, overview.vin].filter(Boolean).join(' / ') || '-' },
{ key: '在线来源', value: `${overview.onlineSourceCount}/${overview.sourceCount}` },
{ key: '覆盖状态', value: <Tag color={overview.coverageStatus === 'online' ? 'green' : overview.coverageStatus === 'partial' ? 'orange' : 'grey'}>{coverageStatusText[overview.coverageStatus] ?? overview.coverageStatus}</Tag> },
{ key: '主来源', value: overview.primaryProtocol || '-' },
{ key: '最后上报', value: overview.lastSeen || '-' },
{ key: '数据规模', value: `历史 ${overview.historyCount} / RAW ${overview.rawCount} / 里程 ${overview.mileageCount}` },
{ key: '实时来源', value: overview.realtimeCount },
{ key: '质量问题', value: <Tag color={overview.qualityIssueCount > 0 ? 'orange' : 'green'}>{overview.qualityIssueCount}</Tag> }
]}
/>
</Card>
) : null}
<Card bordered style={{ marginTop: 16 }}>
<div className="vp-vehicle-summary">
<Descriptions

View File

@@ -1056,6 +1056,75 @@ test('switches vehicle detail to a source from the coverage cards', async () =>
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service?keyword=VIN001&protocol=JT808'), undefined);
});
test('shows unified service overview on vehicle detail', async () => {
window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/vehicle-service')) {
return {
ok: true,
json: async () => ({
data: {
vin: 'VIN001',
lookupKey: 'VIN001',
lookupResolved: true,
serviceOverview: {
vin: 'VIN001',
plate: '粤AG18312',
phone: '13307795425',
oem: 'G7s',
protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'],
primaryProtocol: 'GB32960',
coverageStatus: 'partial',
sourceCount: 3,
onlineSourceCount: 2,
lastSeen: '2026-07-03 20:12:10',
realtimeCount: 3,
historyCount: 12,
rawCount: 34,
mileageCount: 7,
qualityIssueCount: 1
},
serviceStatus: {
status: 'degraded',
severity: 'warning',
title: '部分来源离线',
detail: '2/3 个来源在线',
sourceCount: 3,
onlineSourceCount: 2
},
sources: ['GB32960', 'JT808', 'YUTONG_MQTT'],
sourceStatus: [],
realtime: [],
history: { items: [], total: 12, limit: 20, offset: 0 },
raw: { items: [], total: 34, limit: 10, offset: 0 },
mileage: { items: [], total: 7, limit: 20, offset: 0 },
quality: { items: [], total: 1, limit: 20, offset: 0 }
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
return {
ok: true,
json: async () => ({
data: path.includes('/api/ops/health')
? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }
: { items: [], total: 0, limit: 10, offset: 0 },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
});
render(<App />);
expect(await screen.findByText('车辆服务概览')).toBeInTheDocument();
expect(screen.getByText('2/3')).toBeInTheDocument();
expect(screen.getByText('历史 12 / RAW 34 / 里程 7')).toBeInTheDocument();
});
test('opens history with the currently selected vehicle detail source', async () => {
window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {