feat(platform-web): use lightweight overview for vehicle context
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Toast } from '@douyinfe/semi-ui';
|
import { Toast } from '@douyinfe/semi-ui';
|
||||||
import { api } from './api/client';
|
import { api } from './api/client';
|
||||||
import type { VehicleServiceStatus } from './api/types';
|
import type { VehicleServiceOverview, VehicleServiceStatus } from './api/types';
|
||||||
import { buildAppHash, parseAppHash } from './domain/appRoute';
|
import { buildAppHash, parseAppHash } from './domain/appRoute';
|
||||||
import { AppShell, type PageKey } from './layout/AppShell';
|
import { AppShell, type PageKey } from './layout/AppShell';
|
||||||
import { Dashboard } from './pages/Dashboard';
|
import { Dashboard } from './pages/Dashboard';
|
||||||
@@ -28,11 +28,12 @@ export default function App() {
|
|||||||
if (!lookupKey) {
|
if (!lookupKey) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const resolved = await api.vehicleResolve(new URLSearchParams({ keyword: lookupKey }));
|
const overview = await api.vehicleServiceOverview(new URLSearchParams({ keyword: lookupKey }));
|
||||||
const nextKey = resolved.resolved && resolved.vin ? resolved.vin : lookupKey;
|
const nextKey = overview.vin || lookupKey;
|
||||||
setCurrentVehicleStatus(resolved.serviceStatus);
|
const resolved = Boolean(overview.vin);
|
||||||
setCurrentVehicleLabel(resolved.resolved ? [resolved.plate, resolved.vin || nextKey].filter(Boolean).join(' / ') : lookupKey);
|
setCurrentVehicleStatus(serviceStatusFromOverview(overview));
|
||||||
return { resolved, nextKey };
|
setCurrentVehicleLabel(resolved ? [overview.plate, overview.vin || nextKey].filter(Boolean).join(' / ') : lookupKey);
|
||||||
|
return { resolved, nextKey, overview };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshOpsHealth = useCallback((showError = true) => {
|
const refreshOpsHealth = useCallback((showError = true) => {
|
||||||
@@ -120,7 +121,7 @@ export default function App() {
|
|||||||
setActiveProtocol(nextProtocol);
|
setActiveProtocol(nextProtocol);
|
||||||
setActivePage('detail');
|
setActivePage('detail');
|
||||||
replaceHash('detail', nextKey, nextProtocol);
|
replaceHash('detail', nextKey, nextProtocol);
|
||||||
if (!resolved?.resolved) {
|
if (!resolved) {
|
||||||
Toast.warning('未匹配到车辆身份,已打开问题排查视图');
|
Toast.warning('未匹配到车辆身份,已打开问题排查视图');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -179,3 +180,56 @@ export default function App() {
|
|||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serviceStatusFromOverview(overview: VehicleServiceOverview): VehicleServiceStatus {
|
||||||
|
const sourceCount = overview.sourceCount;
|
||||||
|
const onlineSourceCount = overview.onlineSourceCount;
|
||||||
|
if (!overview.vin) {
|
||||||
|
return {
|
||||||
|
status: 'identity_required',
|
||||||
|
severity: 'warning',
|
||||||
|
title: '身份未绑定',
|
||||||
|
detail: '车辆关键词暂未解析到 VIN,需先维护身份绑定后才能形成完整车辆服务。',
|
||||||
|
sourceCount,
|
||||||
|
onlineSourceCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sourceCount <= 0) {
|
||||||
|
return {
|
||||||
|
status: 'no_data',
|
||||||
|
severity: 'warning',
|
||||||
|
title: '暂无数据来源',
|
||||||
|
detail: '车辆已解析,但暂未查询到 32960、808 或 MQTT 数据来源。',
|
||||||
|
sourceCount,
|
||||||
|
onlineSourceCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (onlineSourceCount <= 0) {
|
||||||
|
return {
|
||||||
|
status: 'offline',
|
||||||
|
severity: 'error',
|
||||||
|
title: '车辆离线',
|
||||||
|
detail: '所有已知数据来源均未在线,需要检查平台转发、终端上报或链路状态。',
|
||||||
|
sourceCount,
|
||||||
|
onlineSourceCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (onlineSourceCount < sourceCount) {
|
||||||
|
return {
|
||||||
|
status: 'degraded',
|
||||||
|
severity: 'warning',
|
||||||
|
title: '部分来源离线',
|
||||||
|
detail: `${onlineSourceCount}/${sourceCount} 个来源在线,车辆服务可用但需要关注离线来源。`,
|
||||||
|
sourceCount,
|
||||||
|
onlineSourceCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 'healthy',
|
||||||
|
severity: 'ok',
|
||||||
|
title: '服务正常',
|
||||||
|
detail: `${onlineSourceCount}/${sourceCount} 个来源在线,全部已知来源在线。`,
|
||||||
|
sourceCount,
|
||||||
|
onlineSourceCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ test('shows vehicle service status distribution on dashboard', async () => {
|
|||||||
|
|
||||||
test('shows resolved vehicle service status after topbar search', async () => {
|
test('shows resolved vehicle service status after topbar search', async () => {
|
||||||
window.history.replaceState(null, '', '/#/dashboard');
|
window.history.replaceState(null, '', '/#/dashboard');
|
||||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/ops/health')) {
|
if (path.includes('/api/ops/health')) {
|
||||||
return {
|
return {
|
||||||
@@ -81,28 +81,26 @@ test('shows resolved vehicle service status after topbar search', async () => {
|
|||||||
})
|
})
|
||||||
} as Response;
|
} as Response;
|
||||||
}
|
}
|
||||||
if (path.includes('/api/vehicles/resolve')) {
|
if (path.includes('/api/vehicle-service/overview')) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
data: {
|
data: {
|
||||||
lookupKey: '粤AG18312',
|
|
||||||
resolved: true,
|
|
||||||
vin: 'LB9A32A24R0LS1426',
|
vin: 'LB9A32A24R0LS1426',
|
||||||
plate: '粤AG18312',
|
plate: '粤AG18312',
|
||||||
phone: '13307795425',
|
phone: '13307795425',
|
||||||
oem: 'G7s',
|
oem: 'G7s',
|
||||||
protocols: ['GB32960', 'JT808'],
|
protocols: ['GB32960', 'JT808'],
|
||||||
online: true,
|
primaryProtocol: 'JT808',
|
||||||
lastSeen: '2026-07-03 20:12:10',
|
coverageStatus: 'partial',
|
||||||
serviceStatus: {
|
|
||||||
status: 'degraded',
|
|
||||||
severity: 'warning',
|
|
||||||
title: '部分来源离线',
|
|
||||||
detail: '1/2 个来源在线',
|
|
||||||
sourceCount: 2,
|
sourceCount: 2,
|
||||||
onlineSourceCount: 1
|
onlineSourceCount: 1,
|
||||||
}
|
lastSeen: '2026-07-03 20:12:10',
|
||||||
|
realtimeCount: 2,
|
||||||
|
historyCount: 0,
|
||||||
|
rawCount: 0,
|
||||||
|
mileageCount: 0,
|
||||||
|
qualityIssueCount: 0
|
||||||
},
|
},
|
||||||
traceId: 'trace-test',
|
traceId: 'trace-test',
|
||||||
timestamp: 1783094400000
|
timestamp: 1783094400000
|
||||||
@@ -147,6 +145,8 @@ test('shows resolved vehicle service status after topbar search', async () => {
|
|||||||
|
|
||||||
expect(await screen.findByText('当前车辆:部分来源离线')).toBeInTheDocument();
|
expect(await screen.findByText('当前车辆:部分来源离线')).toBeInTheDocument();
|
||||||
expect(screen.getByText('粤AG18312 / LB9A32A24R0LS1426')).toBeInTheDocument();
|
expect(screen.getByText('粤AG18312 / LB9A32A24R0LS1426')).toBeInTheDocument();
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service/overview?keyword=%E7%B2%A4AG18312'), undefined);
|
||||||
|
expect(fetchMock).not.toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/resolve'), undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows row service status in dashboard vehicle previews', async () => {
|
test('shows row service status in dashboard vehicle previews', async () => {
|
||||||
@@ -397,28 +397,26 @@ test('shows vehicle context when opening detail from shareable hash', async () =
|
|||||||
window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
|
window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
|
||||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/vehicles/resolve')) {
|
if (path.includes('/api/vehicle-service/overview')) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
data: {
|
data: {
|
||||||
lookupKey: 'VIN001',
|
|
||||||
resolved: true,
|
|
||||||
vin: 'VIN001',
|
vin: 'VIN001',
|
||||||
plate: '粤AG18312',
|
plate: '粤AG18312',
|
||||||
phone: '13307795425',
|
phone: '13307795425',
|
||||||
oem: 'G7s',
|
oem: 'G7s',
|
||||||
protocols: ['GB32960', 'JT808'],
|
protocols: ['GB32960', 'JT808'],
|
||||||
online: true,
|
primaryProtocol: 'JT808',
|
||||||
lastSeen: '2026-07-03 20:12:10',
|
coverageStatus: 'partial',
|
||||||
serviceStatus: {
|
|
||||||
status: 'degraded',
|
|
||||||
severity: 'warning',
|
|
||||||
title: '部分来源离线',
|
|
||||||
detail: '1/2 个来源在线',
|
|
||||||
sourceCount: 2,
|
sourceCount: 2,
|
||||||
onlineSourceCount: 1
|
onlineSourceCount: 1,
|
||||||
}
|
lastSeen: '2026-07-03 20:12:10',
|
||||||
|
realtimeCount: 2,
|
||||||
|
historyCount: 0,
|
||||||
|
rawCount: 0,
|
||||||
|
mileageCount: 0,
|
||||||
|
qualityIssueCount: 0
|
||||||
},
|
},
|
||||||
traceId: 'trace-test',
|
traceId: 'trace-test',
|
||||||
timestamp: 1783094400000
|
timestamp: 1783094400000
|
||||||
@@ -468,28 +466,26 @@ test('refreshes vehicle context when hash changes to another detail vehicle', as
|
|||||||
window.history.replaceState(null, '', '/#/dashboard');
|
window.history.replaceState(null, '', '/#/dashboard');
|
||||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/vehicles/resolve')) {
|
if (path.includes('/api/vehicle-service/overview')) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
data: {
|
data: {
|
||||||
lookupKey: 'VIN002',
|
|
||||||
resolved: true,
|
|
||||||
vin: 'VIN002',
|
vin: 'VIN002',
|
||||||
plate: '粤B20002',
|
plate: '粤B20002',
|
||||||
phone: '',
|
phone: '',
|
||||||
oem: 'G7s',
|
oem: 'G7s',
|
||||||
protocols: ['JT808'],
|
protocols: ['JT808'],
|
||||||
online: false,
|
primaryProtocol: 'JT808',
|
||||||
lastSeen: '2026-07-03 20:10:10',
|
coverageStatus: 'offline',
|
||||||
serviceStatus: {
|
|
||||||
status: 'offline',
|
|
||||||
severity: 'error',
|
|
||||||
title: '车辆离线',
|
|
||||||
detail: '所有已知数据来源均未在线',
|
|
||||||
sourceCount: 1,
|
sourceCount: 1,
|
||||||
onlineSourceCount: 0
|
onlineSourceCount: 0,
|
||||||
}
|
lastSeen: '2026-07-03 20:10:10',
|
||||||
|
realtimeCount: 1,
|
||||||
|
historyCount: 0,
|
||||||
|
rawCount: 0,
|
||||||
|
mileageCount: 0,
|
||||||
|
qualityIssueCount: 0
|
||||||
},
|
},
|
||||||
traceId: 'trace-test',
|
traceId: 'trace-test',
|
||||||
timestamp: 1783094400000
|
timestamp: 1783094400000
|
||||||
@@ -552,28 +548,26 @@ test('shows vehicle context when opening detail from sidebar navigation', async
|
|||||||
window.history.replaceState(null, '', '/#/dashboard');
|
window.history.replaceState(null, '', '/#/dashboard');
|
||||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/vehicles/resolve')) {
|
if (path.includes('/api/vehicle-service/overview')) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
data: {
|
data: {
|
||||||
lookupKey: 'LB9A32A24R0LS1426',
|
|
||||||
resolved: true,
|
|
||||||
vin: 'LB9A32A24R0LS1426',
|
vin: 'LB9A32A24R0LS1426',
|
||||||
plate: '粤AGQ8398',
|
plate: '粤AGQ8398',
|
||||||
phone: '',
|
phone: '',
|
||||||
oem: 'G7s',
|
oem: 'G7s',
|
||||||
protocols: ['JT808'],
|
protocols: ['JT808'],
|
||||||
online: false,
|
primaryProtocol: 'JT808',
|
||||||
lastSeen: '2026-07-03 20:10:10',
|
coverageStatus: 'offline',
|
||||||
serviceStatus: {
|
|
||||||
status: 'offline',
|
|
||||||
severity: 'error',
|
|
||||||
title: '车辆离线',
|
|
||||||
detail: '所有已知数据来源均未在线',
|
|
||||||
sourceCount: 1,
|
sourceCount: 1,
|
||||||
onlineSourceCount: 0
|
onlineSourceCount: 0,
|
||||||
}
|
lastSeen: '2026-07-03 20:10:10',
|
||||||
|
realtimeCount: 1,
|
||||||
|
historyCount: 0,
|
||||||
|
rawCount: 0,
|
||||||
|
mileageCount: 0,
|
||||||
|
qualityIssueCount: 0
|
||||||
},
|
},
|
||||||
traceId: 'trace-test',
|
traceId: 'trace-test',
|
||||||
timestamp: 1783094400000
|
timestamp: 1783094400000
|
||||||
|
|||||||
Reference in New Issue
Block a user