diff --git a/vehicle-data-platform/apps/web/src/api/client.test.ts b/vehicle-data-platform/apps/web/src/api/client.test.ts new file mode 100644 index 00000000..22ab764a --- /dev/null +++ b/vehicle-data-platform/apps/web/src/api/client.test.ts @@ -0,0 +1,39 @@ +import { afterEach, expect, test, vi } from 'vitest'; +import { api } from './client'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +test('rawFramesQuery posts structured JSON instead of URL query strings', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response); + + await api.rawFramesQuery({ + keyword: '粤AG18312', + protocol: 'GB32960', + fields: ['gb32960.vehicle.speed_kmh'], + includeFields: true, + limit: 20, + offset: 0 + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + keyword: '粤AG18312', + protocol: 'GB32960', + fields: ['gb32960.vehicle.speed_kmh'], + includeFields: true, + limit: 20, + offset: 0 + }) + }); +}); diff --git a/vehicle-data-platform/apps/web/src/api/client.ts b/vehicle-data-platform/apps/web/src/api/client.ts index 932f4257..620f050b 100644 --- a/vehicle-data-platform/apps/web/src/api/client.ts +++ b/vehicle-data-platform/apps/web/src/api/client.ts @@ -16,6 +16,18 @@ import type { VehicleRow } from './types'; +export type RawFrameQuery = { + keyword?: string; + vin?: string; + protocol?: string; + dateFrom?: string; + dateTo?: string; + fields?: string[]; + includeFields?: boolean; + limit?: number; + offset?: number; +}; + async function request(path: string, init?: RequestInit): Promise { const response = await fetch(path, init); if (!response.ok) { @@ -34,6 +46,11 @@ export const api = { realtimeLocations: (params = new URLSearchParams()) => request>(`/api/realtime/locations?${params.toString()}`), historyLocations: (params = new URLSearchParams()) => request>(`/api/history/locations?${params.toString()}`), rawFrames: (params = new URLSearchParams()) => request>(`/api/history/raw-frames?${params.toString()}`), + rawFramesQuery: (query: RawFrameQuery) => request>('/api/history/raw-frames/query', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(query) + }), mileageSummary: (params = new URLSearchParams()) => request(`/api/mileage/summary?${params.toString()}`), dailyMileage: (params = new URLSearchParams()) => request>(`/api/mileage/daily?${params.toString()}`), qualitySummary: (params = new URLSearchParams()) => request(`/api/quality/summary?${params.toString()}`), diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index e93933b7..aaed864a 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -1,7 +1,7 @@ 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 { api, type RawFrameQuery } from '../api/client'; import type { HistoryLocationRow, Page, RawFrameRow } from '../api/types'; import { PageHeader } from '../components/PageHeader'; @@ -26,6 +26,13 @@ function canOpenVehicle(vin?: string) { return Boolean(value && value !== 'unknown'); } +function splitFields(value?: string) { + return (value ?? '') + .split(/[\n,]/) + .map((item) => item.trim()) + .filter(Boolean); +} + export function History({ initialVin, onOpenVehicle }: { initialVin: string; onOpenVehicle: (vin: string) => void }) { const [filters, setFilters] = useState({ ...defaultFilters, keyword: initialVin || defaultFilters.keyword }); const [locations, setLocations] = useState>(defaultPage); @@ -47,6 +54,18 @@ export function History({ initialVin, onOpenVehicle }: { initialVin: string; onO return params; }; + const buildRawQuery = (nextFilters: HistoryFilters, limit: number, offset: number): RawFrameQuery => { + const query: RawFrameQuery = { limit, offset }; + if (nextFilters.keyword?.trim()) query.keyword = nextFilters.keyword.trim(); + if (nextFilters.protocol?.trim()) query.protocol = nextFilters.protocol.trim(); + if (nextFilters.dateFrom?.trim()) query.dateFrom = nextFilters.dateFrom.trim(); + if (nextFilters.dateTo?.trim()) query.dateTo = nextFilters.dateTo.trim(); + if (nextFilters.includeFields) query.includeFields = true; + const fields = splitFields(nextFilters.fields); + if (fields.length > 0) query.fields = fields; + return query; + }; + const loadLocations = (nextFilters = filters, page = locationPagination.currentPage, pageSize = locationPagination.pageSize) => { setLoadingLocations(true); api.historyLocations(buildParams(nextFilters, pageSize, (page - 1) * pageSize, false)) @@ -60,7 +79,7 @@ export function History({ initialVin, onOpenVehicle }: { initialVin: string; onO const loadRawFrames = (nextFilters = filters, page = rawPagination.currentPage, pageSize = rawPagination.pageSize) => { setLoadingRaw(true); - api.rawFrames(buildParams(nextFilters, pageSize, (page - 1) * pageSize, true)) + api.rawFramesQuery(buildRawQuery(nextFilters, pageSize, (page - 1) * pageSize)) .then((nextPage) => { setRawFrames(nextPage); setRawPagination({ currentPage: page, pageSize });