From 3d4e1fc01b9509aef8c7eaba22fd356125a74661 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 20 Jul 2026 07:15:07 +0800 Subject: [PATCH] feat(web): unify guided workspace states --- .../apps/web/src/v2/pages/TrackPage.test.tsx | 31 ++++++++++++++++++- .../apps/web/src/v2/pages/TrackPage.tsx | 7 +++++ .../apps/web/src/v2/styles/workspace.css | 27 ++++++++++++---- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx index 8fa77675..2d444961 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, expect, test, vi } from 'vitest'; import { MemoryRouter, useNavigate } from 'react-router-dom'; import type { TrackPlaybackResponse } from '../../api/types'; @@ -316,6 +316,35 @@ test('uses the shared Semi editor SideSheet for mobile track criteria and eviden expect(screen.getByRole('button', { name: '关闭轨迹查询与明细' })).toBeInTheDocument(); }); +test('keeps the mobile track canvas focused when the viewport crosses the responsive breakpoint', async () => { + let mobile = false; + let mobileListener: (() => void) | undefined; + Object.defineProperty(window, 'matchMedia', { + configurable: true, + value: vi.fn((query: string) => ({ + get matches() { return mobile; }, + addEventListener: (_type: string, listener: () => void) => { + if (query.includes('680px')) mobileListener = listener; + }, + removeEventListener: vi.fn() + })) + }); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const view = render(); + + expect(view.container.querySelector('.v2-track-page')).not.toHaveClass('is-mobile-layout', 'is-rail-collapsed'); + expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toBeInTheDocument(); + + await act(async () => { + mobile = true; + mobileListener?.(); + }); + + expect(view.container.querySelector('.v2-track-page')).toHaveClass('is-mobile-layout', 'is-rail-collapsed'); + expect(document.querySelector('.v2-track-detail-sidesheet .semi-sidesheet-animation-content_hide_bottom')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /选择车辆/ })).toBeInTheDocument(); +}); + test('keeps one clear mobile query entry before a vehicle is selected', async () => { Object.defineProperty(window, 'matchMedia', { configurable: true, value: vi.fn(() => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() })) }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx index 51dfca86..e1aa1296 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx @@ -357,6 +357,7 @@ export default function TrackPage() { const [queryCollapsed, setQueryCollapsed] = useState(() => Boolean(criteria.keyword)); const animationRef = useRef(); const lastFrameRef = useRef(0); + const previousMobileLayoutRef = useRef(mobileLayout); const params = useMemo(() => { const next = new URLSearchParams({ keyword: criteria.keyword, maxPoints: '1600' }); @@ -393,6 +394,12 @@ export default function TrackPage() { }); const addressSettling = Boolean(!playing && currentAddressPoint && currentAddressPoint.key !== addressPoint?.key); + useEffect(() => { + const previousMobileLayout = previousMobileLayoutRef.current; + previousMobileLayoutRef.current = mobileLayout; + if (previousMobileLayout === mobileLayout || track?.points.length) return; + setRailCollapsed(mobileLayout); + }, [mobileLayout, track?.points.length]); useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); setAddressPoint(undefined); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 960px)').matches); }, [track?.asOf]); useEffect(() => { if (!playing || points.length < 2) return; diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index 790d4a47..ce0d8f71 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -7096,6 +7096,13 @@ display: none; } + .v2-mobile-navigation .v2-mobile-nav-item:focus-visible, + .v2-mobile-navigation > .semi-button.v2-mobile-nav-item:focus-visible { + outline: 2px solid rgba(18, 104, 243, .42); + outline-offset: -3px; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .7); + } + .v2-mobile-navigation > .semi-button.v2-mobile-nav-item .semi-button-content { display: flex; align-items: center; @@ -25294,17 +25301,26 @@ display: grid; width: 100%; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; + gap: 0; + overflow: hidden; + border: 1px solid #e0e7f0; + border-radius: 12px; + background: linear-gradient(180deg, #fff 0%, #fbfcfe 100%); margin-top: 21px; + box-shadow: 0 8px 24px rgba(31, 48, 70, .045); } .v2-workspace-empty-guide-step.semi-card { min-width: 0; overflow: hidden; - border: 1px solid #e0e7f0; - border-radius: 10px; - background: #fff; - box-shadow: 0 3px 12px rgba(31, 48, 70, .04); + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.v2-workspace-empty-guide-step.semi-card + .v2-workspace-empty-guide-step.semi-card { + border-left: 1px solid #e7edf4; } .v2-workspace-empty-guide-step > .semi-card-body { @@ -25420,7 +25436,6 @@ } .v2-workspace-empty-guide-steps.semi-card-group { - gap: 6px; margin-top: 16px; }