feat(web): unify guided workspace states

This commit is contained in:
lingniu
2026-07-20 07:15:07 +08:00
parent 1a73b8bbd4
commit 3d4e1fc01b
3 changed files with 58 additions and 7 deletions

View File

@@ -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(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks']}><TrackPage /></MemoryRouter></QueryClientProvider>);
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 } } });

View File

@@ -357,6 +357,7 @@ export default function TrackPage() {
const [queryCollapsed, setQueryCollapsed] = useState(() => Boolean(criteria.keyword));
const animationRef = useRef<number>();
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;

View File

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