174 lines
7.8 KiB
TypeScript
174 lines
7.8 KiB
TypeScript
import { Button, Card, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui';
|
|
import { useEffect, useState } from 'react';
|
|
import { api } from '../api/client';
|
|
import type { VehicleRealtimeRow } from '../api/types';
|
|
import { DataEmpty } from '../components/DataEmpty';
|
|
import { PageHeader } from '../components/PageHeader';
|
|
import { StatusTag } from '../components/StatusTag';
|
|
|
|
function canOpenVehicle(vin?: string) {
|
|
const value = vin?.trim();
|
|
return Boolean(value && value !== 'unknown');
|
|
}
|
|
|
|
function vehicleServiceStatus(row: VehicleRealtimeRow) {
|
|
if (row.serviceStatus) {
|
|
return {
|
|
label: row.serviceStatus.title,
|
|
color: row.serviceStatus.severity === 'ok' ? 'green' as const : row.serviceStatus.severity === 'error' ? 'red' as const : 'orange' as const
|
|
};
|
|
}
|
|
if (row.onlineSourceCount <= 0) {
|
|
return { label: '车辆离线', color: 'red' as const };
|
|
}
|
|
if (row.onlineSourceCount < row.sourceCount) {
|
|
return { label: '来源异常', color: 'orange' as const };
|
|
}
|
|
return { label: '服务正常', color: 'green' as const };
|
|
}
|
|
|
|
function sourceEvidenceText(row: VehicleRealtimeRow) {
|
|
return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`;
|
|
}
|
|
|
|
export function Realtime({ onOpenVehicle }: { onOpenVehicle: (vin: string, protocol?: string) => void }) {
|
|
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 });
|
|
|
|
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
|
setLoading(true);
|
|
const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) });
|
|
if (values?.keyword) params.set('keyword', values.keyword);
|
|
if (values?.protocol) params.set('protocol', values.protocol);
|
|
if (values?.online) params.set('online', values.online);
|
|
if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus);
|
|
api.vehicleRealtime(params)
|
|
.then((nextPage) => {
|
|
setRows(nextPage.items);
|
|
setPagination({ currentPage: page, pageSize, total: nextPage.total });
|
|
})
|
|
.catch((error: Error) => Toast.error(error.message))
|
|
.finally(() => setLoading(false));
|
|
};
|
|
|
|
useEffect(() => {
|
|
load({}, 1, pagination.pageSize);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="vp-page">
|
|
<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="keyword" 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>
|
|
<Form.Select field="serviceStatus" label="服务状态" placeholder="全部" style={{ width: 150 }}>
|
|
<Select.Option value="healthy">服务正常</Select.Option>
|
|
<Select.Option value="degraded">来源异常</Select.Option>
|
|
<Select.Option value="offline">车辆离线</Select.Option>
|
|
<Select.Option value="identity_required">身份未绑定</Select.Option>
|
|
</Form.Select>
|
|
<Space>
|
|
<Button htmlType="submit" theme="solid" type="primary">查询</Button>
|
|
<Button onClick={() => {
|
|
setFilters({});
|
|
load({}, 1, pagination.pageSize);
|
|
}}>重置</Button>
|
|
</Space>
|
|
</Form>
|
|
<Tabs type="line">
|
|
<Tabs.TabPane tab="表格视图" itemKey="table">
|
|
{rows.length === 0 && !loading ? (
|
|
<DataEmpty />
|
|
) : (
|
|
<Table
|
|
loading={loading}
|
|
rowKey="vin"
|
|
dataSource={rows}
|
|
pagination={{
|
|
currentPage: pagination.currentPage,
|
|
pageSize: pagination.pageSize,
|
|
total: pagination.total,
|
|
showSizeChanger: true,
|
|
onPageChange: (page) => load(filters, page, pagination.pageSize),
|
|
onPageSizeChange: (pageSize) => load(filters, 1, pageSize)
|
|
}}
|
|
columns={[
|
|
{ title: '车牌', dataIndex: 'plate', width: 120 },
|
|
{ title: 'VIN', dataIndex: 'vin', width: 190 },
|
|
{
|
|
title: '车辆服务状态',
|
|
width: 130,
|
|
render: (_: unknown, row: VehicleRealtimeRow) => {
|
|
const status = vehicleServiceStatus(row);
|
|
return <Tag color={status.color}>{status.label}</Tag>;
|
|
}
|
|
},
|
|
{
|
|
title: '车辆核心数据',
|
|
width: 230,
|
|
render: (_: unknown, row: VehicleRealtimeRow) => (
|
|
<Space spacing={4} wrap>
|
|
<Tag color="blue">{row.speedKmh ?? '-'} km/h</Tag>
|
|
<Tag color="green">SOC {row.socPercent ?? '-'}%</Tag>
|
|
<Typography.Text type="tertiary">{row.totalMileageKm ?? '-'} km</Typography.Text>
|
|
</Space>
|
|
)
|
|
},
|
|
{
|
|
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: 120, render: (_: unknown, row: VehicleRealtimeRow) => sourceEvidenceText(row) },
|
|
{ title: '在线', width: 90, render: (_: unknown, row: VehicleRealtimeRow) => <StatusTag status={row.online ? 'ok' : 'offline'} /> },
|
|
{ title: '最后时间', dataIndex: 'lastSeen', width: 170 },
|
|
{
|
|
title: '操作',
|
|
width: 110,
|
|
render: (_: unknown, row: VehicleRealtimeRow) => (
|
|
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin)}>车辆服务</Button>
|
|
)
|
|
}
|
|
]}
|
|
/>
|
|
)}
|
|
</Tabs.TabPane>
|
|
<Tabs.TabPane tab="地图视图" itemKey="map">
|
|
<div className="vp-map">
|
|
{rows.map((row, index) => (
|
|
<span
|
|
key={row.vin}
|
|
className="vp-map-dot"
|
|
title={`${row.plate} ${row.primaryProtocol}`}
|
|
style={{ left: `${18 + index * 24}%`, top: `${26 + (index % 3) * 18}%` }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</Tabs.TabPane>
|
|
</Tabs>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|