refine Semi UI responsive application shell

This commit is contained in:
lingniu
2026-07-18 05:50:04 +08:00
parent 32d97a0ddc
commit 018ec20256
4 changed files with 88 additions and 6 deletions

View File

@@ -172,6 +172,8 @@ test('uses the compact mobile navigation and opens secondary modules in a Semi S
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
expect(screen.getByRole('heading', { name: '全局监控' })).toHaveClass('v2-topbar-page-title');
expect(screen.getByRole('navigation', { name: '主导航' })).toHaveStyle({ '--v2-mobile-nav-count': '5' });
expect(screen.getByRole('link', { name: '全局监控' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '车辆查询' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '轨迹回放' })).toBeInTheDocument();
@@ -193,6 +195,29 @@ test('uses the compact mobile navigation and opens secondary modules in a Semi S
expect(document.body.style.overflow).not.toBe('hidden');
});
test('lets customer mobile navigation use the full width when no more menu is needed', () => {
auth.session = { name: '客户甲', role: 'customer', userType: 'customer', menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'] };
vi.spyOn(window, 'matchMedia').mockReturnValue({
matches: true,
media: '(max-width: 680px)',
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
});
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/statistics']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
expect(screen.getByRole('heading', { name: '里程查询' })).toHaveClass('v2-topbar-page-title');
expect(screen.getByRole('navigation', { name: '主导航' })).toHaveStyle({ '--v2-mobile-nav-count': '4' });
expect(screen.queryByRole('button', { name: '更多功能' })).not.toBeInTheDocument();
expect(screen.getByRole('link', { name: '里程查询' })).toHaveAttribute('aria-current', 'page');
});
test('automatically collapses the Semi sidebar at tablet width', () => {
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
matches: query === '(max-width: 900px)',

View File

@@ -16,7 +16,7 @@ import {
IconExit
} from '@douyinfe/semi-icons';
import { Avatar, Banner, Button, Card, Dropdown, Input, Layout, Modal, Nav, SideSheet, Tag, Typography } from '@douyinfe/semi-ui';
import { FormEvent, type MouseEvent, useEffect, useLayoutEffect, useState } from 'react';
import { type CSSProperties, FormEvent, type MouseEvent, useEffect, useLayoutEffect, useState } from 'react';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { api } from '../../api/client';
import { usePlatformSession } from '../auth/AuthGate';
@@ -150,7 +150,7 @@ export function AppShell() {
{mobileLayout ? <MobileNavigation /> : <Sidebar activePath={activeRoutePath} />}
<Layout className="v2-main">
<Header className="v2-topbar">
<div className="v2-topbar-title"><Text type="tertiary" size="small"></Text><Title heading={4}>{pageNames[section] ?? '车辆数据中台'}</Title></div>
<div className="v2-topbar-title"><Text className="v2-topbar-product" type="tertiary" size="small"></Text><Title className="v2-topbar-page-title" heading={4}>{pageNames[section] ?? '车辆数据中台'}</Title></div>
<div className="v2-topbar-actions">
<Button className="v2-help-trigger" theme="light" type="tertiary" icon={<IconHelpCircle />} aria-label="帮助" aria-expanded={helpOpen} aria-controls="v2-context-help" onClick={() => setHelpOpen(true)}><span className="v2-help-trigger-label"></span></Button>
<Dropdown
@@ -254,6 +254,7 @@ function MobileNavigation() {
const primary = mobilePrimaryNavigation.filter((item) => hasMenu(session, item.menu));
const more = mobileMoreNavigation.filter((item) => hasMenu(session, item.menu));
const moreActive = more.some((item) => location.pathname.startsWith(item.to));
const itemCount = primary.length + (more.length ? 1 : 0);
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
useEffect(() => setMoreOpen(false), [location.pathname]);
useSideSheetA11y(moreOpen, '.v2-mobile-more-sidesheet', 'v2-mobile-more', '更多功能', '关闭更多功能');
@@ -261,7 +262,7 @@ function MobileNavigation() {
const link = ({ to, label, icon: Icon }: (typeof navigation)[number]) => <NavLink key={to} to={to} aria-label={label} onPointerDown={() => warmRoute(to)} className={({ isActive }) => `v2-mobile-nav-item ${isActive ? 'is-active' : ''}`}><Icon size="large" /><span>{label}</span></NavLink>;
return <>
{more.length ? <SideSheet className="v2-mobile-more-sidesheet" visible={moreOpen} placement="bottom" height="auto" title={<div><strong></strong><span></span></div>} aria-label="更多功能" footer={null} onCancel={() => setMoreOpen(false)}><nav>{more.map(link)}</nav></SideSheet> : null}
<nav className="v2-mobile-navigation" aria-label="主导航">
<nav className="v2-mobile-navigation" aria-label="主导航" style={{ '--v2-mobile-nav-count': Math.max(itemCount, 1) } as CSSProperties}>
{primary.map(link)}
{more.length ? <Button theme="borderless" className={`v2-mobile-nav-item${moreActive ? ' is-active' : ''}`} aria-label="更多功能" aria-expanded={moreOpen} aria-controls="v2-mobile-more" icon={<IconMore size="large" />} onClick={() => setMoreOpen((value) => !value)}><span></span></Button> : null}
</nav>