feat(platform): add online vehicle statistics detail
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Button, Card, Form, Select, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { DailyMileageRow, MileageSummary, OnlineStatisticsSummary } from '../api/types';
|
||||
import type { DailyMileageRow, MileageSummary, OnlineStatisticsSummary, OnlineVehicleStatusRow } from '../api/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { buildAppHash } from '../domain/appRoute';
|
||||
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
|
||||
@@ -43,6 +43,15 @@ function formatPercent(value: number) {
|
||||
return Number.isFinite(value) ? `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}%` : '0%';
|
||||
}
|
||||
|
||||
function formatOfflineDuration(minutes?: number | null) {
|
||||
if (minutes == null || !Number.isFinite(Number(minutes))) return '-';
|
||||
const value = Math.max(0, Number(minutes));
|
||||
if (value < 60) return `${value.toLocaleString()} 分钟`;
|
||||
const hours = Math.floor(value / 60);
|
||||
const rest = Math.round(value % 60);
|
||||
return rest > 0 ? `${hours.toLocaleString()} 小时 ${rest} 分钟` : `${hours.toLocaleString()} 小时`;
|
||||
}
|
||||
|
||||
function canOpenVehicle(vin?: string) {
|
||||
const value = vin?.trim();
|
||||
return Boolean(value && value !== 'unknown');
|
||||
@@ -200,12 +209,15 @@ export function Mileage({
|
||||
onOpenRaw?: (filters: Record<string, string>) => void;
|
||||
}) {
|
||||
const [rows, setRows] = useState<DailyMileageRow[]>([]);
|
||||
const [onlineRows, setOnlineRows] = useState<OnlineVehicleStatusRow[]>([]);
|
||||
const [summary, setSummary] = useState<MileageSummary>(emptySummary);
|
||||
const [onlineSummary, setOnlineSummary] = useState<OnlineStatisticsSummary>(emptyOnlineSummary);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [onlineLoading, setOnlineLoading] = useState(true);
|
||||
const [summaryLoading, setSummaryLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<Record<string, string>>(mergeInitialFilters(initialVin, initialProtocol, initialFilters));
|
||||
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 });
|
||||
const [onlinePagination, setOnlinePagination] = useState({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
const currentVehicleKeyword = filters.keyword?.trim() ?? '';
|
||||
const currentProtocol = filters.protocol?.trim() ?? '';
|
||||
const dateSeries = groupMileageByDate(rows);
|
||||
@@ -264,11 +276,23 @@ export function Mileage({
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const loadOnlineRows = (values: Record<string, string> = filters, page = onlinePagination.currentPage, pageSize = onlinePagination.pageSize) => {
|
||||
setOnlineLoading(true);
|
||||
api.onlineVehicleStatuses(mileageParams(values, pageSize, (page - 1) * pageSize))
|
||||
.then((nextPage) => {
|
||||
setOnlineRows(nextPage.items);
|
||||
setOnlinePagination({ currentPage: page, pageSize, total: nextPage.total });
|
||||
})
|
||||
.catch((error: Error) => Toast.error(error.message))
|
||||
.finally(() => setOnlineLoading(false));
|
||||
};
|
||||
|
||||
const applyFilters = (nextFilters: Record<string, string>) => {
|
||||
setFilters(nextFilters);
|
||||
onFiltersChange?.(nextFilters);
|
||||
loadSummary(nextFilters);
|
||||
load(nextFilters, 1, pagination.pageSize);
|
||||
loadOnlineRows(nextFilters, 1, onlinePagination.pageSize);
|
||||
};
|
||||
|
||||
const clearRangeFilters = () => {
|
||||
@@ -326,6 +350,7 @@ export function Mileage({
|
||||
setFilters(nextFilters);
|
||||
loadSummary(nextFilters);
|
||||
load(nextFilters, 1, pagination.pageSize);
|
||||
loadOnlineRows(nextFilters, 1, onlinePagination.pageSize);
|
||||
}, [initialVin, initialProtocol, JSON.stringify(initialFilters)]);
|
||||
|
||||
return (
|
||||
@@ -487,6 +512,66 @@ export function Mileage({
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card bordered title="在线状态明细" style={{ marginTop: 16 }}>
|
||||
<div className="vp-table-toolbar">
|
||||
<Space wrap>
|
||||
<Tag color="green">在线 {onlineVehicleCount.toLocaleString()} 车</Tag>
|
||||
<Tag color="orange">离线 {offlineVehicleCount.toLocaleString()} 车</Tag>
|
||||
<Typography.Text type="secondary">按车辆服务归并 VIN,展示最近上报时间、离线时长和来源覆盖。</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
loading={onlineLoading}
|
||||
rowKey={(row?: OnlineVehicleStatusRow) => row?.vin ?? ''}
|
||||
dataSource={onlineRows}
|
||||
pagination={{
|
||||
currentPage: onlinePagination.currentPage,
|
||||
pageSize: onlinePagination.pageSize,
|
||||
total: onlinePagination.total,
|
||||
showSizeChanger: true,
|
||||
onPageChange: (page) => loadOnlineRows(filters, page, onlinePagination.pageSize),
|
||||
onPageSizeChange: (pageSize) => loadOnlineRows(filters, 1, pageSize)
|
||||
}}
|
||||
columns={[
|
||||
{ title: 'VIN', dataIndex: 'vin', width: 190 },
|
||||
{ title: '车牌', dataIndex: 'plate', width: 120 },
|
||||
{ title: 'OEM', dataIndex: 'oem', width: 120 },
|
||||
{
|
||||
title: '在线',
|
||||
width: 90,
|
||||
render: (_: unknown, row: OnlineVehicleStatusRow) => <Tag color={row.online ? 'green' : 'orange'}>{row.online ? '在线' : '离线'}</Tag>
|
||||
},
|
||||
{ title: '最近上报', dataIndex: 'lastSeen', width: 180 },
|
||||
{
|
||||
title: '离线时长',
|
||||
width: 130,
|
||||
render: (_: unknown, row: OnlineVehicleStatusRow) => formatOfflineDuration(row.offlineDurationMinutes)
|
||||
},
|
||||
{
|
||||
title: '来源覆盖',
|
||||
width: 150,
|
||||
render: (_: unknown, row: OnlineVehicleStatusRow) => `${row.onlineSourceCount}/${row.sourceCount}`
|
||||
},
|
||||
{
|
||||
title: '协议',
|
||||
width: 240,
|
||||
render: (_: unknown, row: OnlineVehicleStatusRow) => (
|
||||
<Space spacing={4} wrap>
|
||||
{(row.protocols ?? []).map((protocol) => <Tag key={protocol} color="blue">{protocol}</Tag>)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{ title: '绑定状态', dataIndex: 'bindingStatus', width: 130 },
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
render: (_: unknown, row: OnlineVehicleStatusRow) => (
|
||||
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, currentProtocol)}>车辆服务</Button>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card bordered title="统计口径" style={{ marginTop: 16 }}>
|
||||
<div className="vp-table-toolbar">
|
||||
<Space wrap>
|
||||
|
||||
Reference in New Issue
Block a user