From e40eabac92027f0915f373e91b575fa46a813ea6 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 21:53:35 +0800 Subject: [PATCH] refine Semi UI monitor inspector --- .../web/src/v2/pages/MonitorPage.test.tsx | 21 ++- .../apps/web/src/v2/pages/MonitorPage.tsx | 143 +++--------------- .../src/v2/pages/MonitorVehicleDetailCard.tsx | 142 +++++++++++++++++ .../apps/web/src/v2/productionEntry.test.ts | 6 +- .../apps/web/src/v2/styles/v2.css | 67 ++++++-- 5 files changed, 231 insertions(+), 148 deletions(-) create mode 100644 vehicle-data-platform/apps/web/src/v2/pages/MonitorVehicleDetailCard.tsx 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 07e91207..a7da6faf 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 @@ -87,7 +87,7 @@ afterEach(() => { vi.restoreAllMocks(); }); -test('never labels the latest historical mileage row as today when today has no mileage evidence', () => { +test('never labels the latest historical mileage row as today when today has no mileage evidence', async () => { vehicleCardFixture.detail = { sources: ['JT808'], sourceStatus: [], @@ -103,12 +103,12 @@ test('never labels the latest historical mileage row as today when today has no fireEvent.click(screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ })); - const todayMetric = screen.getByText('今日里程').parentElement; + const todayMetric = (await screen.findByText('今日里程')).parentElement; expect(todayMetric).toHaveTextContent('今日里程—'); expect(todayMetric).not.toHaveTextContent('134'); }); -test('starts without a selection and supports expand, collapse, reselection, and clear', () => { +test('starts without a selection and supports expand, collapse, reselection, and clear', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); const workspace = view.container.querySelector('.v2-monitor-workspace')!; @@ -147,6 +147,7 @@ test('starts without a selection and supports expand, collapse, reselection, and expect(screen.queryByText('接入车辆')).not.toBeInTheDocument(); fireEvent.click(firstVehicle); + expect(await screen.findByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument(); expect(workspace).toHaveClass('is-detail-open'); expect(firstVehicle).toHaveClass('is-selected'); expect(screen.getByRole('button', { name: '取消选择车辆' })).toBeInTheDocument(); @@ -159,7 +160,13 @@ test('starts without a selection and supports expand, collapse, reselection, and expect(screen.getByText('身份、车型与来源覆盖')).toBeInTheDocument(); expect(screen.getByText('1/1 在线').closest('.semi-tag')).toBeInTheDocument(); expect(view.container.querySelector('.v2-report-summary')).toHaveTextContent('平台接收时间'); - expect(view.container.querySelector('.v2-report-summary')).toHaveTextContent('主协议JT808'); + expect(view.container.querySelector('.v2-report-summary')).toHaveTextContent('主协议JT/T 808'); + expect(view.container.querySelector('.v2-report-summary .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light'); + expect(view.container.querySelectorAll('.v2-detail-actions a')).toHaveLength(4); + expect(Array.from(view.container.querySelectorAll('.v2-detail-actions a')).map((item) => item.textContent)).toEqual([ + '单车详情', '轨迹回放', '历史数据', '里程查询' + ]); + expect(view.container.querySelector('.v2-detail-protocols .v2-protocol-tag')).toHaveTextContent('JT/T 808'); expect(view.container.querySelector('.v2-metric-grid > div:first-child')).toHaveTextContent('速度42'); expect(view.container.querySelector('.v2-metric-grid > div:nth-child(4)')).toHaveTextContent('今日里程18.6km'); expect(view.container.querySelector('.v2-status-text')).toHaveClass('semi-tag'); @@ -196,7 +203,7 @@ test('starts without a selection and supports expand, collapse, reselection, and expect(secondVehicle).toHaveClass('is-selected'); }); -test('restores URL-backed monitor context and carries it into every vehicle workflow', () => { +test('restores URL-backed monitor context and carries it into every vehicle workflow', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const entry = '/monitor?keyword=%E7%B2%A4A12345&protocol=JT808&status=online&selectedVin=LTEST000000000001&detail=collapsed&zoom=13&bounds=113.100000%2C23.000000%2C113.400000%2C23.300000'; render(); @@ -209,6 +216,7 @@ test('restores URL-backed monitor context and carries it into every vehicle work expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-initial-bounds', '113.100000,23.000000,113.400000,23.300000'); fireEvent.click(screen.getByRole('button', { name: '展开车辆详情' })); + expect(await screen.findByRole('navigation', { name: '车辆快捷操作' })).toBeInTheDocument(); for (const name of ['单车详情', '轨迹回放', '历史数据', '里程查询']) { const target = new URL(screen.getByRole('link', { name }).getAttribute('href')!, 'https://vehicle-platform.invalid'); const monitorReturn = target.searchParams.get('monitorReturn'); @@ -261,7 +269,8 @@ test('switches to a lightweight realtime list and resolves addresses only on dem expect(screen.getAllByText('42')).toHaveLength(1); expect(screen.getAllByText(/18\.6/)).toHaveLength(1); expect(screen.getAllByText(/1,234/)).toHaveLength(1); - expect(screen.getByText('JT808')).toBeInTheDocument(); + expect(screen.getByText('JT/T 808')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-monitor-protocol .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light'); expect(screen.getAllByText(/113\.260000/)).toHaveLength(1); expect(screen.queryByText('推荐来源')).not.toBeInTheDocument(); expect(screen.getByText('当日里程')).toBeInTheDocument(); 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 fcd895b0..f8bba80e 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -1,25 +1,24 @@ import { - IconChevronLeft, IconChevronRight, IconClose, IconFilter, IconList, IconMapPin, + IconChevronLeft, IconClose, IconFilter, IconList, IconMapPin, IconQrCode, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; 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'; +import { lazy, memo, Suspense, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; 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 { ProtocolTag } from '../shared/ProtocolTag'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; -import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor'; -import { formatTelemetryValue } from '../domain/telemetry'; +import { formatNumber, statusLabel, vehicleStatus } from '../domain/monitor'; import { useMobileLayout } from '../hooks/useMobileLayout'; -import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData'; +import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, type MonitorViewport } from '../hooks/useMonitorData'; import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; -import { buildMonitorPath, parseMonitorRouteContext, withMonitorReturn } from '../routing/monitorContext'; +import { buildMonitorPath, parseMonitorRouteContext } from '../routing/monitorContext'; const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT']; const statuses = ['', 'online', 'offline', 'driving', 'idle', 'no_location']; @@ -28,6 +27,7 @@ const MONITOR_VIEW_ITEMS = [ { key: 'map', label: '地图', icon: