From 45820daa466ecae128b8675ef89b23f1999b5021 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 05:31:24 +0800 Subject: [PATCH] refine Semi UI monitor workspace --- .../web/src/v2/pages/MonitorPage.test.tsx | 48 ++- .../apps/web/src/v2/pages/MonitorPage.tsx | 91 +++-- .../apps/web/src/v2/productionEntry.test.ts | 8 +- .../apps/web/src/v2/styles/workspace.css | 337 ++++++++++++++++++ 4 files changed, 436 insertions(+), 48 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx index 70e29684..f982fb69 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx @@ -135,7 +135,8 @@ test('starts without a selection and supports expand, collapse, reselection, and expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', ''); expect(screen.getByText('车辆总数')).toBeInTheDocument(); expect(screen.getByText('无实时位置')).toBeInTheDocument(); - expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效' })).toHaveTextContent('实时筛选'); + expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效,已启用 0 个条件' })).toHaveTextContent('自动生效'); + expect(screen.getByRole('button', { name: /清空/ })).toBeDisabled(); expect(screen.queryByRole('button', { name: '筛选' })).not.toBeInTheDocument(); expect(screen.queryByText('接入车辆')).not.toBeInTheDocument(); @@ -210,21 +211,38 @@ test('restores URL-backed monitor context and carries it into every vehicle work } }); +test('shows active filter count and clears all filter fields in one action', () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render(); + + expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效,已启用 3 个条件' })).toHaveTextContent('自动生效3'); + const clear = screen.getByRole('button', { name: /清空/ }); + expect(clear).toBeEnabled(); + + fireEvent.click(clear); + + expect(screen.getByRole('textbox', { name: '搜索车辆' })).toHaveValue(''); + expect(screen.getByRole('combobox', { name: '协议' })).toHaveTextContent('全部协议'); + expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveTextContent('全部状态'); + expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效,已启用 0 个条件' })).toHaveTextContent('自动生效'); + expect(clear).toBeDisabled(); +}); + test('switches to a lightweight realtime list and resolves addresses only on demand', async () => { const row = vehicles[0]; vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [row], total: 1, limit: 50, offset: 0 }); const reverseGeocode = vi.spyOn(api, 'reverseGeocode').mockResolvedValue({ provider: 'AMap', longitude: 113.26, latitude: 23.13, formattedAddress: '广东省广州市天河区测试路' }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); - const modeGroup = screen.getByRole('group', { name: '监控视图' }); - const mapMode = within(modeGroup).getByRole('button', { name: /地图/ }); - const listMode = within(modeGroup).getByRole('button', { name: /列表/ }); - expect(mapMode).toHaveAttribute('aria-pressed', 'true'); + const modeGroup = screen.getByRole('tablist', { name: '监控视图' }); + const mapMode = within(modeGroup).getByRole('tab', { name: /地图/ }); + const listMode = within(modeGroup).getByRole('tab', { name: /列表/ }); + expect(mapMode).toHaveAttribute('aria-selected', 'true'); expect(mapMode).toHaveClass('is-active'); - expect(listMode).toHaveAttribute('aria-pressed', 'false'); + expect(listMode).toHaveAttribute('aria-selected', 'false'); fireEvent.click(listMode); - expect(mapMode).toHaveAttribute('aria-pressed', 'false'); - expect(listMode).toHaveAttribute('aria-pressed', 'true'); + expect(mapMode).toHaveAttribute('aria-selected', 'false'); + expect(listMode).toHaveAttribute('aria-selected', 'true'); expect(listMode).toHaveClass('is-active'); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '车辆实时列表', level: 5 })).toBeInTheDocument(); @@ -254,7 +272,7 @@ test('switches to a lightweight realtime list and resolves addresses only on dem expect(screen.queryByText('三路正常')).not.toBeInTheDocument(); expect(screen.queryByTestId('fleet-map')).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: /地图/ })); + fireEvent.click(screen.getByRole('tab', { name: /地图/ })); await waitFor(() => expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(0)); view.unmount(); }); @@ -285,7 +303,7 @@ test('keeps authorized vehicles without realtime coordinates in the list without const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); - fireEvent.click(screen.getByRole('button', { name: /列表/ })); + fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(await screen.findByText('粤A无定位')).toBeInTheDocument(); expect(screen.getByText('暂无实时位置')).toBeInTheDocument(); @@ -301,10 +319,10 @@ test('pauses selected-vehicle polling in list mode and resumes it when returning fireEvent.click(screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ })); expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), 'LTEST000000000001', true, true); - fireEvent.click(screen.getByRole('button', { name: /列表/ })); + fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), '', false, false); - fireEvent.click(screen.getByRole('button', { name: /地图/ })); + fireEvent.click(screen.getByRole('tab', { name: /地图/ })); expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), 'LTEST000000000001', true, true); }); @@ -371,7 +389,7 @@ test('mounts only the mobile list representation and removes its viewport listen const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); - fireEvent.click(screen.getByRole('button', { name: /列表/ })); + fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-cards .v2-monitor-mobile-card.semi-card')).toHaveLength(1)); expect(view.container.querySelector('.v2-monitor-table-scroll')).not.toBeInTheDocument(); @@ -391,7 +409,7 @@ test('pastes, deduplicates, and submits multiple plates as one batch search', as expect(input).toHaveValue('粤A12345,粤B67890'); expect(screen.getAllByText('已找到 2/2')).toHaveLength(3); - fireEvent.click(screen.getByRole('button', { name: /列表/ })); + fireEvent.click(screen.getByRole('tab', { name: /列表/ })); await waitFor(() => { const params = vehicleRealtime.mock.calls[vehicleRealtime.mock.calls.length - 1]?.[0]; expect(params?.get('keywords')).toBe('粤A12345,粤B67890'); @@ -412,7 +430,7 @@ test('opens an explicit batch editor and applies copied plate rows', async () => expect(screen.queryByRole('dialog', { name: '批量搜索车辆' })).not.toBeInTheDocument(); expect(screen.getByRole('textbox', { name: '搜索车辆' })).toHaveValue('粤A12345,粤B67890'); - fireEvent.click(screen.getByRole('button', { name: /列表/ })); + fireEvent.click(screen.getByRole('tab', { name: /列表/ })); await waitFor(() => { const params = vehicleRealtime.mock.calls[vehicleRealtime.mock.calls.length - 1]?.[0]; expect(params?.get('keywords')).toBe('粤A12345,粤B67890'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index ac6e7508..e96ddbf0 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -2,7 +2,7 @@ import { IconChevronLeft, IconChevronRight, IconClose, IconFilter, IconList, IconMapPin, IconQrCode, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; -import { Button, ButtonGroup, Card, Empty, Input, Modal, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; +import { Button, Card, Empty, Input, Modal, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; import { useQuery } from '@tanstack/react-query'; import { memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; @@ -10,6 +10,7 @@ import { api } from '../../api/client'; import type { Page, VehicleRealtimeRow } from '../../api/types'; import { FleetMap } from '../map/FleetMap'; import { EmptyState, InlineError } from '../shared/AsyncState'; +import { SegmentedTabs } from '../shared/SegmentedTabs'; import { TablePagination } from '../shared/TablePagination'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; @@ -23,6 +24,10 @@ const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT']; const statuses = ['', 'online', 'offline', 'driving', 'idle', 'no_location']; const EMPTY_VEHICLES: VehicleRealtimeRow[] = []; const MOBILE_MONITOR_QUERY = '(max-width: 760px)'; +const MONITOR_VIEW_ITEMS = [ + { key: 'map', label: '地图', icon: