feat(platform): add realtime map service queue
This commit is contained in:
@@ -17,12 +17,12 @@ export function SourceStatusTags({
|
||||
lastSeen
|
||||
}: {
|
||||
sourceStatus?: VehicleSourceStatus[];
|
||||
protocols: string[];
|
||||
protocols?: string[];
|
||||
lastSeen: string;
|
||||
}) {
|
||||
const sources = sourceStatus?.length
|
||||
? sourceStatus
|
||||
: protocols.map((protocol) => ({ protocol, online: false, hasRealtime: true, lastSeen }));
|
||||
: (protocols ?? []).map((protocol) => ({ protocol, online: false, hasRealtime: true, lastSeen }));
|
||||
|
||||
return (
|
||||
<Space spacing={4} wrap>
|
||||
|
||||
@@ -34,6 +34,15 @@ function sourceEvidenceText(row: VehicleRealtimeRow) {
|
||||
return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`;
|
||||
}
|
||||
|
||||
function serviceStatusWeight(row: VehicleRealtimeRow) {
|
||||
const severity = row.serviceStatus?.severity;
|
||||
if (severity === 'error') return 4;
|
||||
if (severity === 'warning') return 3;
|
||||
if (row.onlineSourceCount <= 0) return 4;
|
||||
if (row.onlineSourceCount < row.sourceCount) return 3;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function isValidCoordinate(row: VehicleRealtimeRow) {
|
||||
return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
|
||||
}
|
||||
@@ -101,13 +110,21 @@ export function Realtime({
|
||||
const locatedCount = rows.filter(isValidCoordinate).length;
|
||||
const degradedCount = rows.filter((row) => row.onlineSourceCount < row.sourceCount).length;
|
||||
const primaryProtocols = new Set(rows.map((row) => row.primaryProtocol).filter(Boolean));
|
||||
const mapServiceRows = rows
|
||||
.filter((row) => isValidCoordinate(row) && canOpenVehicle(row.vin))
|
||||
.sort((a, b) => {
|
||||
const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a);
|
||||
if (statusDelta !== 0) return statusDelta;
|
||||
return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? ''));
|
||||
})
|
||||
.slice(0, 5);
|
||||
const mapPoints: VehicleMapPoint[] = rows.map((row, index) => ({
|
||||
id: row.vin || `${row.primaryProtocol}-${index}`,
|
||||
label: row.plate || row.vin || 'unknown',
|
||||
longitude: row.longitude,
|
||||
latitude: row.latitude,
|
||||
online: row.online,
|
||||
title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}`
|
||||
title: `${row.plate || row.vin || '-'} ${vehicleServiceStatus(row).label} ${row.primaryProtocol || ''} ${row.lastSeen || ''}`
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -184,6 +201,34 @@ export function Realtime({
|
||||
<Typography.Text type="secondary">
|
||||
高德 Web JS Key 和安全密钥通过运行环境配置,前端只读取公开 Key;安全密钥后续走服务端代理,避免明文下发。
|
||||
</Typography.Text>
|
||||
<div className="vp-map-service-queue">
|
||||
<div className="vp-map-service-queue-title">地图车辆服务队列</div>
|
||||
{mapServiceRows.length === 0 ? (
|
||||
<Typography.Text type="tertiary">暂无可定位车辆</Typography.Text>
|
||||
) : (
|
||||
mapServiceRows.map((row) => {
|
||||
const status = vehicleServiceStatus(row);
|
||||
return (
|
||||
<div key={`${row.vin}-${row.primaryProtocol}`} className="vp-map-service-item">
|
||||
<div className="vp-map-service-item-main">
|
||||
<div>
|
||||
<Typography.Text strong>{row.plate || row.vin}</Typography.Text>
|
||||
<Typography.Text type="tertiary" size="small">{row.vin}</Typography.Text>
|
||||
</div>
|
||||
<Tag color={status.color}>{status.label}</Tag>
|
||||
</div>
|
||||
<Typography.Text type="secondary" size="small">
|
||||
{row.serviceStatus?.detail || sourceEvidenceText(row)}
|
||||
</Typography.Text>
|
||||
<div className="vp-map-service-item-footer">
|
||||
<Typography.Text type="tertiary" size="small">{row.lastSeen || '-'}</Typography.Text>
|
||||
<Button size="small" onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}>地图车辆服务</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs type="line">
|
||||
|
||||
@@ -377,6 +377,42 @@ body {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.vp-map-service-queue {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--vp-border);
|
||||
}
|
||||
|
||||
.vp-map-service-queue-title {
|
||||
color: var(--vp-text);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.vp-map-service-item {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--vp-border);
|
||||
border-radius: var(--vp-radius);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.vp-map-service-item-main,
|
||||
.vp-map-service-item-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vp-map-service-item-main > div {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vp-alert-flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
||||
@@ -4287,7 +4287,7 @@ test('opens vehicle service from realtime vehicles with primary source evidence'
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('VIN-MQTT-001')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('VIN-MQTT-001')).length).toBeGreaterThan(0);
|
||||
fireEvent.click(screen.getByRole('button', { name: '车辆服务' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -4370,7 +4370,7 @@ test('opens vehicle service from realtime vehicles with current source filter',
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('VIN-RT-FILTER')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('VIN-RT-FILTER')).length).toBeGreaterThan(0);
|
||||
fireEvent.click(screen.getByRole('button', { name: '车辆服务' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -4435,8 +4435,8 @@ test('shows canonical service status in realtime vehicles', async () => {
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('VIN-RT-DEGRADED')).toBeInTheDocument();
|
||||
expect(screen.getByText('来源不完整')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('VIN-RT-DEGRADED')).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('来源不完整').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('frames realtime page as one vehicle service with source evidence', async () => {
|
||||
@@ -4508,7 +4508,99 @@ test('frames realtime page as one vehicle service with source evidence', async (
|
||||
expect(screen.getByText('GB32960 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('JT808 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument();
|
||||
expect(screen.getByText('2/2 来源在线')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('2/2 来源在线').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('opens vehicle service from realtime map service queue', async () => {
|
||||
window.history.replaceState(null, '', '/#/realtime');
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const path = String(input);
|
||||
if (path.includes('/api/realtime/vehicles')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
items: [{
|
||||
vin: 'VIN-MAP-001',
|
||||
plate: '粤AMAP01',
|
||||
phone: '',
|
||||
oem: 'G7s',
|
||||
protocols: ['GB32960', 'JT808'],
|
||||
sourceStatus: [
|
||||
{ protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false },
|
||||
{ protocol: 'JT808', online: false, lastSeen: '2026-07-03 19:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }
|
||||
],
|
||||
sourceCount: 2,
|
||||
onlineSourceCount: 1,
|
||||
online: true,
|
||||
primaryProtocol: 'GB32960',
|
||||
longitude: 113.2,
|
||||
latitude: 23.1,
|
||||
speedKmh: 30,
|
||||
socPercent: 78,
|
||||
totalMileageKm: 119925,
|
||||
lastSeen: '2026-07-03 20:12:10',
|
||||
bindingStatus: 'bound',
|
||||
serviceStatus: {
|
||||
status: 'degraded',
|
||||
severity: 'warning',
|
||||
title: '来源不完整',
|
||||
detail: 'JT808 离线,GB32960 仍可支撑车辆服务',
|
||||
sourceCount: 2,
|
||||
onlineSourceCount: 1
|
||||
}
|
||||
}],
|
||||
total: 1,
|
||||
limit: 50,
|
||||
offset: 0
|
||||
},
|
||||
traceId: 'trace-test',
|
||||
timestamp: 1783094400000
|
||||
})
|
||||
} as Response;
|
||||
}
|
||||
if (path.includes('/api/vehicle-service/overview')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
vin: 'VIN-MAP-001',
|
||||
plate: '粤AMAP01',
|
||||
sourceCount: 2,
|
||||
onlineSourceCount: 1,
|
||||
coverageStatus: 'degraded',
|
||||
primaryProtocol: 'GB32960',
|
||||
lastSeen: '2026-07-03 20:12:10',
|
||||
historyCount: 0,
|
||||
rawCount: 0,
|
||||
mileageCount: 0,
|
||||
qualityIssueCount: 0
|
||||
},
|
||||
traceId: 'trace-test',
|
||||
timestamp: 1783094400000
|
||||
})
|
||||
} as Response;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: { items: [], total: 0, limit: 10, offset: 0 },
|
||||
traceId: 'trace-test',
|
||||
timestamp: 1783094400000
|
||||
})
|
||||
} as Response;
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('地图车辆服务队列')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('粤AMAP01').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('JT808 离线,GB32960 仍可支撑车辆服务')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '地图车辆服务' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe('#/detail?keyword=VIN-MAP-001&protocol=GB32960');
|
||||
});
|
||||
});
|
||||
|
||||
test('loads realtime vehicles from shareable source filter hash', async () => {
|
||||
@@ -4559,7 +4651,7 @@ test('loads realtime vehicles from shareable source filter hash', async () => {
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('VIN-RT-FILTERED')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('VIN-RT-FILTERED')).length).toBeGreaterThan(0);
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/realtime/vehicles?limit=50&offset=0&keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded'), undefined);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user