feat: densify mobile monitor workspace

This commit is contained in:
lingniu
2026-07-19 02:15:10 +08:00
parent dcfd78f73b
commit f4f8bf9f4b
3 changed files with 252 additions and 3 deletions

View File

@@ -112,6 +112,7 @@ test('starts without a selection and supports expand, collapse, reselection, and
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
const workspace = view.container.querySelector('.v2-monitor-workspace')!;
const monitorPage = view.container.querySelector('.v2-monitor-page')!;
const firstVehicle = screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ });
const secondVehicle = screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ });
@@ -136,6 +137,7 @@ test('starts without a selection and supports expand, collapse, reselection, and
expect(liveStatus).toHaveTextContent('最近同步等待数据');
expect(workspace).not.toHaveClass('is-detail-open');
expect(workspace).not.toHaveClass('is-detail-collapsed');
expect(monitorPage).toHaveClass('is-map-mode');
expect(firstVehicle).not.toHaveClass('is-selected');
expect(screen.queryByRole('button', { name: '取消选择车辆' })).not.toBeInTheDocument();
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', '');
@@ -149,6 +151,7 @@ test('starts without a selection and supports expand, collapse, reselection, and
fireEvent.click(firstVehicle);
expect(await screen.findByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument();
expect(workspace).toHaveClass('is-detail-open');
expect(monitorPage).toHaveClass('is-map-mode');
expect(firstVehicle).toHaveClass('is-selected');
expect(screen.getByRole('button', { name: '取消选择车辆' })).toBeInTheDocument();
expect(view.container.querySelector('.v2-vehicle-detail')).toHaveClass('semi-card');
@@ -203,6 +206,28 @@ test('starts without a selection and supports expand, collapse, reselection, and
expect(secondVehicle).toHaveClass('is-selected');
});
test('returns the monitor scroll owner to the command bar when changing views', async () => {
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render(
<main className="v2-content" data-testid="monitor-scroll-owner">
<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>
</main>
);
const scrollOwner = screen.getByTestId('monitor-scroll-owner');
const monitorPage = view.container.querySelector('.v2-monitor-page')!;
scrollOwner.scrollTop = 180;
fireEvent.click(screen.getByRole('tab', { name: /列表/ }));
expect(scrollOwner.scrollTop).toBe(0);
expect(monitorPage).toHaveClass('is-list-mode');
scrollOwner.scrollTop = 120;
fireEvent.click(screen.getByRole('tab', { name: /地图/ }));
expect(scrollOwner.scrollTop).toBe(0);
expect(monitorPage).toHaveClass('is-map-mode');
});
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';

View File

@@ -204,6 +204,7 @@ const MemoFleetMap = memo(FleetMap);
export default function MonitorPage() {
const [routeParams, setRouteParams] = useSearchParams();
const pageRef = useRef<HTMLDivElement>(null);
const initialContextRef = useRef(parseMonitorRouteContext(routeParams));
const initialContext = initialContextRef.current;
const mobileLayout = useMobileLayout(760);
@@ -280,11 +281,16 @@ export default function MonitorPage() {
const selected = selectedVin
? rows.find((vehicle) => vehicle.vin === selectedVin) ?? selectedVehicle.data?.items[0]
: undefined;
const resetMonitorScroll = useCallback(() => {
const scrollOwner = pageRef.current?.closest<HTMLElement>('.v2-content');
if (scrollOwner) scrollOwner.scrollTop = 0;
}, []);
const selectVehicle = useCallback((vin: string) => {
resetMonitorScroll();
setSelectedVin(vin);
setDetailOpen(true);
setMode('map');
}, []);
}, [resetMonitorScroll]);
const clearSelection = useCallback(() => {
setSelectedVin('');
setDetailOpen(false);
@@ -293,9 +299,10 @@ export default function MonitorPage() {
const collapseDetail = useCallback(() => setDetailOpen(false), []);
const expandDetail = useCallback(() => setDetailOpen(true), []);
const changeMonitorMode = useCallback((nextMode: 'map' | 'list') => {
resetMonitorScroll();
setMode(nextMode);
if (nextMode === 'list') setDetailOpen(false);
}, []);
}, [resetMonitorScroll]);
const clearFilters = useCallback(() => {
setKeyword('');
setProtocol('');
@@ -314,7 +321,7 @@ export default function MonitorPage() {
const lastSyncAt = vehicles.dataUpdatedAt ? new Date(vehicles.dataUpdatedAt) : undefined;
return (
<div className="v2-monitor-page">
<div ref={pageRef} className={`v2-monitor-page is-${mode}-mode`}>
<Card className="v2-filterbar" bodyStyle={{ padding: 0 }} aria-label="车辆筛选">
<div id="monitor-filter-fields" className={`v2-monitor-filter-fields${filtersCollapsed ? ' is-mobile-collapsed' : ''}`} aria-label="筛选条件">
<div className={`v2-search-field${searchTerms.length > 1 ? ' is-batch' : ''}`}>