diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 907b9def..56e48bc0 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -189,10 +189,13 @@ export default function App() { }; const updateHistoryFilters = (filters: Record = {}, tab = historyTab) => { + const hasKeywordInput = Object.prototype.hasOwnProperty.call(filters, 'keyword'); const nextFilters = normalizeHistoryFilterValues(filters); setHistoryFilters(nextFilters); if (nextFilters.keyword) { setAnalysisVin(nextFilters.keyword); + } else if (hasKeywordInput) { + setAnalysisVin(''); } setActiveProtocol(nextFilters.protocol ?? ''); setHistoryTab(tab); @@ -201,7 +204,7 @@ export default function App() { if (tab && tab !== 'location') { routeFilters.tab = tab; } - replaceHash('history', keyword ?? analysisVin, protocol, routeFilters); + replaceHash('history', hasKeywordInput ? keyword : (keyword ?? analysisVin), protocol, routeFilters); }; const replaceHistoryHash = (filters: Record = {}, tab = historyTab) => { diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index b258cda1..65fa2f6d 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -58,6 +58,19 @@ function isIncludeFieldsEnabled(value?: boolean | string) { return value === true || value === 'true'; } +function hasRawFieldQueryScope(filters: HistoryFilters) { + return Boolean( + filters.keyword?.trim() || + filters.dateFrom?.trim() || + filters.dateTo?.trim() || + splitFields(filters.fields).length > 0 + ); +} + +function shouldBlockRawFieldQuery(filters: HistoryFilters) { + return isIncludeFieldsEnabled(filters.includeFields) && !hasRawFieldQueryScope(filters); +} + function isFiniteNumber(value: unknown): value is number { return typeof value === 'number' && Number.isFinite(value); } @@ -72,9 +85,10 @@ function formatNumber(value?: number, suffix = '') { } function mergeInitialFilters(initialVin: string, initialProtocol?: string, initialFilters: Record = {}): HistoryFilters { + const hasExplicitFilters = Object.keys(initialFilters).length > 0; return { ...defaultFilters, - keyword: initialVin || defaultFilters.keyword, + keyword: initialVin || (hasExplicitFilters ? '' : defaultFilters.keyword), protocol: initialProtocol, ...initialFilters, includeFields: isIncludeFieldsEnabled(initialFilters.includeFields) @@ -178,6 +192,12 @@ export function History({ }; const loadRawFrames = (nextFilters = filters, page = rawPagination.currentPage, pageSize = rawPagination.pageSize) => { + if (shouldBlockRawFieldQuery(nextFilters)) { + setRawFrames({ items: [], total: 0, limit: pageSize, offset: (page - 1) * pageSize }); + setRawPagination({ currentPage: page, pageSize }); + Toast.warning('RAW 解析字段查询需要车辆、时间范围或字段裁剪'); + return; + } setLoadingRaw(true); api.rawFramesQuery(buildRawQuery(nextFilters, pageSize, (page - 1) * pageSize)) .then((nextPage) => { diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index b8f5aba6..6ff3171c 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3892,6 +3892,54 @@ test('updates history hash when vehicle history filters are submitted', async () })); }); +test('prevents unscoped raw parsed-field query from history form', async () => { + window.history.replaceState(null, '', '/#/history?keyword=VIN-SAFE-RAW'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + await screen.findByRole('heading', { name: '轨迹回放' }); + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({ + body: expect.stringContaining('VIN-SAFE-RAW') + })); + }); + fetchMock.mockClear(); + + const keywordInput = screen.getByPlaceholderText('VIN / 车牌 / 手机号'); + fireEvent.change(keywordInput, { target: { value: '' } }); + fireEvent.input(keywordInput, { target: { value: '' } }); + await waitFor(() => { + expect(keywordInput).toHaveValue(''); + }); + fireEvent.click(screen.getByRole('checkbox', { name: '返回解析字段' })); + fireEvent.click(screen.getByRole('button', { name: 'search 查询' })); + + expect((await screen.findAllByText('RAW 解析字段查询需要车辆、时间范围或字段裁剪')).length).toBeGreaterThanOrEqual(1); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?'), undefined); + expect(fetchMock).not.toHaveBeenCalledWith('/api/history/raw-frames/query', expect.anything()); +}); + test('shows trajectory playback workspace from history locations', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-001&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {