import { Button, Card, Form, Select, SideSheet, Space, Table, Tabs, Toast, Typography } from '@douyinfe/semi-ui'; import { IconRefresh, IconSearch } from '@douyinfe/semi-icons'; import { useEffect, useMemo, useState } from 'react'; import { api } from '../api/client'; import type { HistoryLocationRow, Page, RawFrameRow } from '../api/types'; import { PageHeader } from '../components/PageHeader'; type HistoryFilters = { vin?: string; protocol?: string; dateFrom?: string; dateTo?: string; fields?: string; includeFields?: boolean; }; const defaultFilters: HistoryFilters = { vin: 'LB9A32A24R0LS1426', includeFields: false }; const defaultPage = { items: [], total: 0, limit: 10, offset: 0 }; function canOpenVehicle(vin?: string) { const value = vin?.trim(); return Boolean(value && value !== 'unknown'); } export function History({ onOpenVehicle }: { onOpenVehicle: (vin: string) => void }) { const [filters, setFilters] = useState(defaultFilters); const [locations, setLocations] = useState>(defaultPage); const [rawFrames, setRawFrames] = useState>(defaultPage); const [selectedRaw, setSelectedRaw] = useState(null); const [loadingLocations, setLoadingLocations] = useState(false); const [loadingRaw, setLoadingRaw] = useState(false); const [locationPagination, setLocationPagination] = useState({ currentPage: 1, pageSize: 10 }); const [rawPagination, setRawPagination] = useState({ currentPage: 1, pageSize: 10 }); const buildParams = (nextFilters: HistoryFilters, limit: number, offset: number, raw: boolean) => { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }); if (nextFilters.vin?.trim()) params.set('vin', nextFilters.vin.trim()); if (nextFilters.protocol?.trim()) params.set('protocol', nextFilters.protocol.trim()); if (nextFilters.dateFrom?.trim()) params.set('dateFrom', nextFilters.dateFrom.trim()); if (nextFilters.dateTo?.trim()) params.set('dateTo', nextFilters.dateTo.trim()); if (raw && nextFilters.includeFields) params.set('includeFields', 'true'); if (raw && nextFilters.fields?.trim()) params.set('fields', nextFilters.fields.trim()); return params; }; const loadLocations = (nextFilters = filters, page = locationPagination.currentPage, pageSize = locationPagination.pageSize) => { setLoadingLocations(true); api.historyLocations(buildParams(nextFilters, pageSize, (page - 1) * pageSize, false)) .then((nextPage) => { setLocations(nextPage); setLocationPagination({ currentPage: page, pageSize }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoadingLocations(false)); }; const loadRawFrames = (nextFilters = filters, page = rawPagination.currentPage, pageSize = rawPagination.pageSize) => { setLoadingRaw(true); api.rawFrames(buildParams(nextFilters, pageSize, (page - 1) * pageSize, true)) .then((nextPage) => { setRawFrames(nextPage); setRawPagination({ currentPage: page, pageSize }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoadingRaw(false)); }; const submit = (values: Record) => { const nextFilters: HistoryFilters = { vin: String(values.vin ?? ''), protocol: String(values.protocol ?? ''), dateFrom: String(values.dateFrom ?? ''), dateTo: String(values.dateTo ?? ''), fields: String(values.fields ?? ''), includeFields: Boolean(values.includeFields) }; setFilters(nextFilters); loadLocations(nextFilters, 1, locationPagination.pageSize); loadRawFrames(nextFilters, 1, rawPagination.pageSize); }; const reset = () => { setFilters(defaultFilters); loadLocations(defaultFilters, 1, locationPagination.pageSize); loadRawFrames(defaultFilters, 1, rawPagination.pageSize); }; useEffect(() => { loadLocations(defaultFilters, 1, locationPagination.pageSize); loadRawFrames(defaultFilters, 1, rawPagination.pageSize); }, []); const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]); return (
submit(values)}> GB32960 JT808 YUTONG_MQTT 返回解析字段
loadLocations(filters, page, locationPagination.pageSize), onPageSizeChange: (pageSize) => loadLocations(filters, 1, pageSize) }} columns={[ { title: 'VIN', dataIndex: 'vin', width: 190 }, { title: '协议', dataIndex: 'protocol', width: 120 }, { title: '经度', dataIndex: 'longitude', width: 120 }, { title: '纬度', dataIndex: 'latitude', width: 120 }, { title: '速度 km/h', dataIndex: 'speedKmh', width: 120 }, { title: '总里程 km', dataIndex: 'totalMileageKm', width: 130 }, { title: '设备时间', dataIndex: 'deviceTime', width: 190 }, { title: '入库时间', dataIndex: 'serverTime', width: 190 }, { title: '操作', width: 110, render: (_: unknown, row: HistoryLocationRow) => ( ) } ]} />
loadRawFrames(filters, page, rawPagination.pageSize), onPageSizeChange: (pageSize) => loadRawFrames(filters, 1, pageSize) }} columns={[ { title: 'ID', dataIndex: 'id', width: 260 }, { title: 'VIN', dataIndex: 'vin', width: 190 }, { title: '车牌', dataIndex: 'plate', width: 120 }, { title: '协议', dataIndex: 'protocol', width: 120 }, { title: '帧类型', dataIndex: 'frameType', width: 190 }, { title: '大小 B', dataIndex: 'rawSizeBytes', width: 100 }, { title: '设备时间', dataIndex: 'deviceTime', width: 190 }, { title: '入库时间', dataIndex: 'serverTime', width: 190 }, { title: '操作', width: 200, render: (_: unknown, row: RawFrameRow) => ( ) } ]} /> setSelectedRaw(null)} width={720}> {selectedRaw?.protocol ?? '-'} / {selectedRaw?.plate || selectedRaw?.vin || '-'} / {rawFieldCount} 个字段 {rawFieldCount === 0 ? ( 当前查询未返回解析字段,请勾选“返回解析字段”或配置字段裁剪后重新查询。 ) : null}
{JSON.stringify(selectedRaw?.parsedFields ?? {}, null, 2)}
); }