feat(platform): aggregate realtime by vehicle
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
QualityIssueRow,
|
||||
RawFrameRow,
|
||||
RealtimeLocationRow,
|
||||
VehicleRealtimeRow,
|
||||
VehicleCoverageRow,
|
||||
VehicleDetail,
|
||||
VehicleRow
|
||||
@@ -27,6 +28,7 @@ export const api = {
|
||||
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()}`),
|
||||
vehicleRealtime: (params = new URLSearchParams()) => request<Page<VehicleRealtimeRow>>(`/api/realtime/vehicles?${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()}`),
|
||||
rawFrames: (params = new URLSearchParams()) => request<Page<RawFrameRow>>(`/api/history/raw-frames?${params.toString()}`),
|
||||
|
||||
@@ -85,6 +85,24 @@ export interface RealtimeLocationRow {
|
||||
lastSeen: string;
|
||||
}
|
||||
|
||||
export interface VehicleRealtimeRow {
|
||||
vin: string;
|
||||
plate: string;
|
||||
phone: string;
|
||||
oem: string;
|
||||
protocols: string[];
|
||||
sourceCount: number;
|
||||
onlineSourceCount: number;
|
||||
online: boolean;
|
||||
primaryProtocol: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
speedKmh: number;
|
||||
socPercent: number;
|
||||
totalMileageKm: number;
|
||||
lastSeen: string;
|
||||
}
|
||||
|
||||
export interface HistoryLocationRow extends RealtimeLocationRow {
|
||||
deviceTime: string;
|
||||
serverTime: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Card, Col, Form, Row, Select, 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, QualityIssueRow, RealtimeLocationRow, VehicleCoverageRow, VehicleRow } from '../api/types';
|
||||
import type { DashboardSummary, LinkHealth, ProtocolStat, QualityIssueRow, VehicleCoverageRow, VehicleRealtimeRow, VehicleRow } from '../api/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { StatusTag } from '../components/StatusTag';
|
||||
|
||||
@@ -19,7 +19,7 @@ export function Dashboard({ onOpenVehicle, onOpenQuality }: { onOpenVehicle: (vi
|
||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||
const [vehicles, setVehicles] = useState<VehicleRow[]>([]);
|
||||
const [coverage, setCoverage] = useState<VehicleCoverageRow[]>([]);
|
||||
const [locations, setLocations] = useState<RealtimeLocationRow[]>([]);
|
||||
const [locations, setLocations] = useState<VehicleRealtimeRow[]>([]);
|
||||
const [qualityIssues, setQualityIssues] = useState<QualityIssueRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [coverageLoading, setCoverageLoading] = useState(false);
|
||||
@@ -42,7 +42,7 @@ export function Dashboard({ onOpenVehicle, onOpenQuality }: { onOpenVehicle: (vi
|
||||
api.dashboardSummary(),
|
||||
api.vehicles(new URLSearchParams({ limit: '5' })),
|
||||
api.vehicleCoverage(new URLSearchParams({ limit: '8' })),
|
||||
api.realtimeLocations(new URLSearchParams({ limit: '8' })),
|
||||
api.vehicleRealtime(new URLSearchParams({ limit: '8' })),
|
||||
api.qualityIssues(new URLSearchParams({ limit: '5' }))
|
||||
])
|
||||
.then(([nextSummary, vehiclePage, coveragePage, locationPage, qualityPage]) => {
|
||||
@@ -190,7 +190,7 @@ export function Dashboard({ onOpenVehicle, onOpenQuality }: { onOpenVehicle: (vi
|
||||
<span
|
||||
key={row.vin}
|
||||
className="vp-map-dot"
|
||||
title={`${row.plate} ${row.protocol}`}
|
||||
title={`${row.plate} ${row.primaryProtocol}`}
|
||||
style={{ left: `${18 + index * 13}%`, top: `${24 + (index % 4) * 15}%` }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Button, Card, Form, Select, Space, Table, Tabs, Toast } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, Form, Select, Space, Table, Tabs, Tag, Toast } from '@douyinfe/semi-ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { RealtimeLocationRow } from '../api/types';
|
||||
import type { VehicleRealtimeRow } from '../api/types';
|
||||
import { DataEmpty } from '../components/DataEmpty';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { StatusTag } from '../components/StatusTag';
|
||||
|
||||
export function Realtime() {
|
||||
const [rows, setRows] = useState<RealtimeLocationRow[]>([]);
|
||||
const [rows, setRows] = useState<VehicleRealtimeRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
|
||||
@@ -16,7 +17,8 @@ export function Realtime() {
|
||||
const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) });
|
||||
if (values?.vin) params.set('vin', values.vin);
|
||||
if (values?.protocol) params.set('protocol', values.protocol);
|
||||
api.realtimeLocations(params)
|
||||
if (values?.online) params.set('online', values.online);
|
||||
api.vehicleRealtime(params)
|
||||
.then((nextPage) => {
|
||||
setRows(nextPage.items);
|
||||
setPagination({ currentPage: page, pageSize, total: nextPage.total });
|
||||
@@ -31,19 +33,23 @@ export function Realtime() {
|
||||
|
||||
return (
|
||||
<div className="vp-page">
|
||||
<PageHeader title="实时状态" description="按协议和车辆查看最新实时位置、在线状态和核心数据" />
|
||||
<PageHeader title="实时状态" description="以车辆为主对象查看最新位置、在线来源和核心实时数据" />
|
||||
<Card bordered>
|
||||
<Form layout="horizontal" onSubmit={(values) => {
|
||||
const nextFilters = values as Record<string, string>;
|
||||
setFilters(nextFilters);
|
||||
load(nextFilters, 1, pagination.pageSize);
|
||||
}} style={{ marginBottom: 12 }}>
|
||||
<Form.Input field="vin" label="关键词" placeholder="VIN / 车牌" style={{ width: 260 }} />
|
||||
<Form.Input field="vin" label="关键词" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
|
||||
<Form.Select field="protocol" label="协议" placeholder="全部协议" style={{ width: 180 }}>
|
||||
<Select.Option value="GB32960">GB32960</Select.Option>
|
||||
<Select.Option value="JT808">JT808</Select.Option>
|
||||
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
|
||||
</Form.Select>
|
||||
<Form.Select field="online" label="在线" placeholder="全部" style={{ width: 130 }}>
|
||||
<Select.Option value="online">在线</Select.Option>
|
||||
<Select.Option value="offline">离线</Select.Option>
|
||||
</Form.Select>
|
||||
<Space>
|
||||
<Button htmlType="submit" theme="solid" type="primary">查询</Button>
|
||||
<Button onClick={() => {
|
||||
@@ -59,7 +65,7 @@ export function Realtime() {
|
||||
) : (
|
||||
<Table
|
||||
loading={loading}
|
||||
rowKey={(row?: RealtimeLocationRow) => `${row?.vin ?? ''}-${row?.protocol ?? ''}`}
|
||||
rowKey="vin"
|
||||
dataSource={rows}
|
||||
pagination={{
|
||||
currentPage: pagination.currentPage,
|
||||
@@ -72,10 +78,23 @@ export function Realtime() {
|
||||
columns={[
|
||||
{ title: 'VIN', dataIndex: 'vin' },
|
||||
{ title: '车牌', dataIndex: 'plate' },
|
||||
{ title: '协议', dataIndex: 'protocol' },
|
||||
{
|
||||
title: '来源',
|
||||
width: 260,
|
||||
render: (_: unknown, row: VehicleRealtimeRow) => (
|
||||
<Space spacing={4} wrap>
|
||||
{row.protocols.map((protocol) => (
|
||||
<Tag key={protocol} color={protocol === row.primaryProtocol ? 'blue' : 'grey'}>{protocol}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{ title: '覆盖', width: 90, render: (_: unknown, row: VehicleRealtimeRow) => `${row.onlineSourceCount}/${row.sourceCount}` },
|
||||
{ title: '在线', width: 90, render: (_: unknown, row: VehicleRealtimeRow) => <StatusTag status={row.online ? 'ok' : 'offline'} /> },
|
||||
{ title: '速度 km/h', dataIndex: 'speedKmh' },
|
||||
{ title: 'SOC %', dataIndex: 'socPercent' },
|
||||
{ title: '总里程 km', dataIndex: 'totalMileageKm' },
|
||||
{ title: '最新来源', dataIndex: 'primaryProtocol' },
|
||||
{ title: '最后时间', dataIndex: 'lastSeen' }
|
||||
]}
|
||||
/>
|
||||
@@ -87,7 +106,7 @@ export function Realtime() {
|
||||
<span
|
||||
key={row.vin}
|
||||
className="vp-map-dot"
|
||||
title={`${row.plate} ${row.protocol}`}
|
||||
title={`${row.plate} ${row.primaryProtocol}`}
|
||||
style={{ left: `${18 + index * 24}%`, top: `${26 + (index % 3) * 18}%` }}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user