refine Semi UI responsive application shell
This commit is contained in:
@@ -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>
|
<Routes><Route element={<AppShell />}><Route path="*" element={<span>页面内容</span>} /></Route></Routes>
|
||||||
</MemoryRouter>);
|
</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();
|
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');
|
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', () => {
|
test('automatically collapses the Semi sidebar at tablet width', () => {
|
||||||
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
||||||
matches: query === '(max-width: 900px)',
|
matches: query === '(max-width: 900px)',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
IconExit
|
IconExit
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { Avatar, Banner, Button, Card, Dropdown, Input, Layout, Modal, Nav, SideSheet, Tag, Typography } from '@douyinfe/semi-ui';
|
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 { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import { usePlatformSession } from '../auth/AuthGate';
|
import { usePlatformSession } from '../auth/AuthGate';
|
||||||
@@ -150,7 +150,7 @@ export function AppShell() {
|
|||||||
{mobileLayout ? <MobileNavigation /> : <Sidebar activePath={activeRoutePath} />}
|
{mobileLayout ? <MobileNavigation /> : <Sidebar activePath={activeRoutePath} />}
|
||||||
<Layout className="v2-main">
|
<Layout className="v2-main">
|
||||||
<Header className="v2-topbar">
|
<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">
|
<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>
|
<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
|
<Dropdown
|
||||||
@@ -254,6 +254,7 @@ function MobileNavigation() {
|
|||||||
const primary = mobilePrimaryNavigation.filter((item) => hasMenu(session, item.menu));
|
const primary = mobilePrimaryNavigation.filter((item) => hasMenu(session, item.menu));
|
||||||
const more = mobileMoreNavigation.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 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); };
|
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
|
||||||
useEffect(() => setMoreOpen(false), [location.pathname]);
|
useEffect(() => setMoreOpen(false), [location.pathname]);
|
||||||
useSideSheetA11y(moreOpen, '.v2-mobile-more-sidesheet', 'v2-mobile-more', '更多功能', '关闭更多功能');
|
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>;
|
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 <>
|
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}
|
{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)}
|
{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}
|
{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>
|
</nav>
|
||||||
|
|||||||
@@ -2215,7 +2215,7 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.v2-history-mobile-list { display: none; }
|
.v2-history-mobile-list { display: none; }
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (max-width: 680px) {
|
||||||
.v2-mobile-navigation { position: fixed; z-index: 90; right: 0; bottom: 0; left: 0; display: grid; height: calc(66px + env(safe-area-inset-bottom)); grid-template-columns: repeat(5, minmax(0, 1fr)); border-top: 1px solid #dce4ee; background: rgba(255,255,255,.97); padding: 5px 6px calc(5px + env(safe-area-inset-bottom)); box-shadow: 0 -10px 30px rgba(20,35,55,.10); backdrop-filter: blur(16px); }
|
.v2-mobile-navigation { position: fixed; z-index: 90; right: 0; bottom: 0; left: 0; display: grid; height: calc(66px + env(safe-area-inset-bottom)); grid-template-columns: repeat(var(--v2-mobile-nav-count, 5), minmax(0, 1fr)); border-top: 1px solid #dce4ee; background: rgba(255,255,255,.97); padding: 5px 6px calc(5px + env(safe-area-inset-bottom)); box-shadow: 0 -10px 30px rgba(20,35,55,.10); backdrop-filter: blur(16px); }
|
||||||
.v2-mobile-nav-item { position: relative; display: flex; min-width: 0; min-height: 54px; align-items: center; justify-content: center; flex-direction: column; gap: 4px; border: 0; border-radius: 10px; background: transparent; color: #718096; text-decoration: none; cursor: pointer; }
|
.v2-mobile-nav-item { position: relative; display: flex; min-width: 0; min-height: 54px; align-items: center; justify-content: center; flex-direction: column; gap: 4px; border: 0; border-radius: 10px; background: transparent; color: #718096; text-decoration: none; cursor: pointer; }
|
||||||
.v2-mobile-nav-item > svg { font-size: 20px; }
|
.v2-mobile-nav-item > svg { font-size: 20px; }
|
||||||
.v2-mobile-nav-item > span { max-width: 100%; overflow: hidden; font-size: 10px; font-weight: 700; line-height: 1.15; text-overflow: ellipsis; white-space: nowrap; }
|
.v2-mobile-nav-item > span { max-width: 100%; overflow: hidden; font-size: 10px; font-weight: 700; line-height: 1.15; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|||||||
@@ -4837,15 +4837,25 @@
|
|||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (max-width: 680px) {
|
||||||
.v2-topbar {
|
.v2-topbar {
|
||||||
|
height: 56px;
|
||||||
|
flex-basis: 56px;
|
||||||
padding-inline: 12px 8px;
|
padding-inline: 12px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v2-topbar-title > .semi-typography {
|
.v2-topbar-title {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-topbar-title > .v2-topbar-product.semi-typography {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v2-topbar-title > h4.semi-typography {
|
.v2-topbar-title > .v2-topbar-page-title.semi-typography {
|
||||||
|
display: block;
|
||||||
|
max-width: min(58vw, 230px);
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v2-topbar-actions {
|
.v2-topbar-actions {
|
||||||
@@ -4872,6 +4882,52 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-mobile-navigation {
|
||||||
|
border-top-color: #e1e8f1;
|
||||||
|
background: rgba(255, 255, 255, .985);
|
||||||
|
box-shadow: 0 -8px 26px rgba(25, 44, 70, .085);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-navigation .v2-mobile-nav-item {
|
||||||
|
gap: 2px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-navigation .v2-mobile-nav-item > .semi-icon,
|
||||||
|
.v2-mobile-navigation > .semi-button.v2-mobile-nav-item .semi-icon {
|
||||||
|
display: inline-grid;
|
||||||
|
width: 32px;
|
||||||
|
height: 26px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: background-color .16s ease, color .16s ease, box-shadow .16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-navigation .v2-mobile-nav-item.is-active {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--v2-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-navigation .v2-mobile-nav-item.is-active > .semi-icon,
|
||||||
|
.v2-mobile-navigation > .semi-button.v2-mobile-nav-item.is-active .semi-icon {
|
||||||
|
background: #eaf3ff;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(18, 104, 243, .07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-navigation .v2-mobile-nav-item.is-active::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-navigation > .semi-button.v2-mobile-nav-item .semi-button-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-help-sheet .semi-sidesheet-inner {
|
.v2-help-sheet .semi-sidesheet-inner {
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user