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 } from '../api/types'; import { PageHeader } from '../components/PageHeader'; const emptySummary: MileageSummary = { vehicleCount: 0, recordCount: 0, sourceCount: 0, totalMileageKm: 0, averageMileagePerVin: 0 }; function mileageParams(values: Record, pageSize?: number, offset?: number) { const params = new URLSearchParams(); if (pageSize != null) params.set('limit', String(pageSize)); if (offset != null) params.set('offset', String(offset)); if (values?.keyword) params.set('keyword', values.keyword); if (values?.protocol) params.set('protocol', values.protocol); if (values?.dateFrom) params.set('dateFrom', values.dateFrom); if (values?.dateTo) params.set('dateTo', values.dateTo); return params; } function formatKm(value: number) { return Number.isFinite(value) ? value.toLocaleString(undefined, { maximumFractionDigits: 1 }) : '0'; } function canOpenVehicle(vin?: string) { const value = vin?.trim(); return Boolean(value && value !== 'unknown'); } export function Mileage({ initialVin, initialProtocol, onOpenVehicle }: { initialVin: string; initialProtocol?: string; onOpenVehicle: (vin: string, protocol?: string) => void }) { const [rows, setRows] = useState([]); const [summary, setSummary] = useState(emptySummary); const [loading, setLoading] = useState(true); const [summaryLoading, setSummaryLoading] = useState(true); const [filters, setFilters] = useState>({ keyword: initialVin, protocol: initialProtocol ?? '' }); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 }); const currentVehicleKeyword = filters.keyword?.trim() ?? ''; const currentProtocol = filters.protocol?.trim() ?? ''; const loadSummary = (values: Record = filters) => { setSummaryLoading(true); api.mileageSummary(mileageParams(values)) .then(setSummary) .catch((error: Error) => Toast.error(error.message)) .finally(() => setSummaryLoading(false)); }; const load = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoading(true); api.dailyMileage(mileageParams(values, pageSize, (page - 1) * pageSize)) .then((nextPage) => { setRows(nextPage.items); setPagination({ currentPage: page, pageSize, total: nextPage.total }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); }; useEffect(() => { const nextFilters = { keyword: initialVin, protocol: initialProtocol ?? '' }; setFilters(nextFilters); loadSummary(nextFilters); load(nextFilters, 1, pagination.pageSize); }, [initialVin, initialProtocol]); return (
onOpenVehicle(currentVehicleKeyword)}> 当前车辆服务 )} />
当前车辆:{currentVehicleKeyword || '-'} 当前来源:{currentProtocol || '全部来源'} 里程统计按当前车辆与来源范围汇总。
{ const nextFilters = values as Record; setFilters(nextFilters); loadSummary(nextFilters); load(nextFilters, 1, pagination.pageSize); }}> GB32960 JT808 YUTONG_MQTT
{[ { label: '车辆数', value: summary.vehicleCount.toLocaleString() }, { label: '累计里程 km', value: formatKm(summary.totalMileageKm) }, { label: '单车平均 km', value: formatKm(summary.averageMileagePerVin) }, { label: '来源 / 记录', value: `${summary.sourceCount}/${summary.recordCount.toLocaleString()}` } ].map((item) => (
{item.value}
{item.label}
))}
`${row?.date ?? ''}-${row?.vin ?? ''}-${row?.source ?? ''}`} 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: 'date' }, { title: 'VIN', dataIndex: 'vin' }, { title: '车牌', dataIndex: 'plate' }, { title: '起始里程', dataIndex: 'startMileageKm' }, { title: '结束里程', dataIndex: 'endMileageKm' }, { title: '日里程', dataIndex: 'dailyMileageKm' }, { title: '来源', dataIndex: 'source' }, { title: '异常', render: (_: unknown, row: DailyMileageRow) => row.anomalySeverity ? {row.anomalySeverity} : 正常 }, { title: '操作', width: 110, render: (_: unknown, row: DailyMileageRow) => ( ) } ]} /> ); }