feat(platform): add vehicle coverage view
This commit is contained in:
@@ -22,7 +22,7 @@ export default function App() {
|
||||
};
|
||||
|
||||
const pages: Record<PageKey, JSX.Element> = {
|
||||
dashboard: <Dashboard />,
|
||||
dashboard: <Dashboard onOpenVehicle={openVehicle} />,
|
||||
vehicles: <Vehicles onOpenVehicle={openVehicle} />,
|
||||
realtime: <Realtime />,
|
||||
detail: <VehicleDetail vin={activeVin} />,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
QualityIssueRow,
|
||||
RawFrameRow,
|
||||
RealtimeLocationRow,
|
||||
VehicleCoverageRow,
|
||||
VehicleDetail,
|
||||
VehicleRow
|
||||
} from './types';
|
||||
@@ -24,6 +25,7 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
export const api = {
|
||||
dashboardSummary: () => request<DashboardSummary>('/api/dashboard/summary'),
|
||||
vehicles: (params = new URLSearchParams()) => request<Page<VehicleRow>>(`/api/vehicles?${params.toString()}`),
|
||||
vehicleCoverage: (params = new URLSearchParams()) => request<Page<VehicleCoverageRow>>(`/api/vehicles/coverage?${params.toString()}`),
|
||||
vehicleDetail: (params = new URLSearchParams()) => request<VehicleDetail>(`/api/vehicles/detail?${params.toString()}`),
|
||||
realtimeLocations: (params = new URLSearchParams()) => request<Page<RealtimeLocationRow>>(`/api/realtime/locations?${params.toString()}`),
|
||||
historyLocations: (params = new URLSearchParams()) => request<Page<HistoryLocationRow>>(`/api/history/locations?${params.toString()}`),
|
||||
|
||||
@@ -38,6 +38,19 @@ export interface VehicleRow {
|
||||
bindingScore: number;
|
||||
}
|
||||
|
||||
export interface VehicleCoverageRow {
|
||||
vin: string;
|
||||
plate: string;
|
||||
phone: string;
|
||||
oem: string;
|
||||
protocols: string[];
|
||||
sourceCount: number;
|
||||
onlineSourceCount: number;
|
||||
online: boolean;
|
||||
lastSeen: string;
|
||||
bindingStatus: string;
|
||||
}
|
||||
|
||||
export interface VehicleDetail {
|
||||
vin: string;
|
||||
identity?: VehicleRow;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Card, Col, Row, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, Col, Row, Space, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { DashboardSummary, LinkHealth, ProtocolStat, RealtimeLocationRow, VehicleRow } from '../api/types';
|
||||
import type { DashboardSummary, LinkHealth, ProtocolStat, RealtimeLocationRow, VehicleCoverageRow, VehicleRow } from '../api/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { StatusTag } from '../components/StatusTag';
|
||||
|
||||
const statusColor: Record<string, 'green' | 'orange' | 'red' | 'grey'> = {
|
||||
ok: 'green',
|
||||
@@ -10,9 +11,10 @@ const statusColor: Record<string, 'green' | 'orange' | 'red' | 'grey'> = {
|
||||
error: 'red'
|
||||
};
|
||||
|
||||
export function Dashboard() {
|
||||
export function Dashboard({ onOpenVehicle }: { onOpenVehicle: (vin: string) => void }) {
|
||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||
const [vehicles, setVehicles] = useState<VehicleRow[]>([]);
|
||||
const [coverage, setCoverage] = useState<VehicleCoverageRow[]>([]);
|
||||
const [locations, setLocations] = useState<RealtimeLocationRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -20,11 +22,13 @@ export function Dashboard() {
|
||||
Promise.all([
|
||||
api.dashboardSummary(),
|
||||
api.vehicles(new URLSearchParams({ limit: '5' })),
|
||||
api.vehicleCoverage(new URLSearchParams({ limit: '8' })),
|
||||
api.realtimeLocations(new URLSearchParams({ limit: '8' }))
|
||||
])
|
||||
.then(([nextSummary, vehiclePage, locationPage]) => {
|
||||
.then(([nextSummary, vehiclePage, coveragePage, locationPage]) => {
|
||||
setSummary(nextSummary);
|
||||
setVehicles(vehiclePage.items);
|
||||
setCoverage(coveragePage.items);
|
||||
setLocations(locationPage.items);
|
||||
})
|
||||
.catch((error: Error) => Toast.error(error.message))
|
||||
@@ -88,6 +92,35 @@ export function Dashboard() {
|
||||
<Card title="实时积压" bordered style={{ marginTop: 16 }}>
|
||||
<Typography.Text>Kafka 当前消费积压:{summary?.kafkaLag ?? 0}</Typography.Text>
|
||||
</Card>
|
||||
<Card title="车辆服务覆盖" bordered style={{ marginTop: 16 }}>
|
||||
<Table
|
||||
pagination={false}
|
||||
rowKey="vin"
|
||||
dataSource={coverage}
|
||||
columns={[
|
||||
{ title: '车牌', dataIndex: 'plate', width: 110 },
|
||||
{ title: 'VIN', dataIndex: 'vin', width: 190 },
|
||||
{
|
||||
title: '来源',
|
||||
width: 240,
|
||||
render: (_: unknown, row: VehicleCoverageRow) => (
|
||||
<Space spacing={4} wrap>
|
||||
{row.protocols.map((protocol) => <Tag key={protocol} color="blue">{protocol}</Tag>)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '覆盖',
|
||||
width: 110,
|
||||
render: (_: unknown, row: VehicleCoverageRow) => `${row.onlineSourceCount}/${row.sourceCount}`
|
||||
},
|
||||
{ title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => <StatusTag status={row.online ? 'ok' : 'offline'} /> },
|
||||
{ title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => <Tag color={row.bindingStatus === 'bound' ? 'green' : 'orange'}>{row.bindingStatus === 'bound' ? '已绑定' : '未绑定'}</Tag> },
|
||||
{ title: '最后时间', dataIndex: 'lastSeen', width: 170 },
|
||||
{ title: '操作', width: 110, render: (_: unknown, row: VehicleCoverageRow) => <Button onClick={() => onOpenVehicle(row.vin)}>车辆服务</Button> }
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Row gutter={16} style={{ marginTop: 16 }}>
|
||||
<Col span={12}>
|
||||
<Card title="实时位置预览" bordered>
|
||||
|
||||
Reference in New Issue
Block a user