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;
}