feat: refine global shell for mobile landscape

This commit is contained in:
lingniu
2026-07-19 04:29:58 +08:00
parent 712721bc73
commit d3eb77e2c7
4 changed files with 420 additions and 5 deletions

View File

@@ -187,6 +187,10 @@ test('uses the compact mobile navigation and opens secondary modules in a Semi S
const moreSheet = screen.getByRole('dialog', { name: '更多功能' });
expect(moreSheet).toHaveTextContent('数据分析与系统管理');
expect(screen.getByRole('navigation', { name: '分析与处置' })).toBeInTheDocument();
expect(screen.getByRole('navigation', { name: '平台治理' })).toBeInTheDocument();
expect(moreSheet).toHaveTextContent('历史证据与业务告警');
expect(moreSheet).toHaveTextContent('接入、账号与运维质量');
expect(screen.getByRole('link', { name: '历史数据' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '告警中心' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '接入管理' })).toBeInTheDocument();

View File

@@ -251,23 +251,60 @@ function PasswordDialog({ onClose, onChanged }: { onClose: () => void; onChanged
}
const mobilePrimaryNavigation = [navigation[0], navigation[1], navigation[2], navigation[4]];
const mobileMoreNavigation = [navigation[3], navigation[5], navigation[6], navigation[7], { to: '/operations', menu: 'operations', label: '运维质量', icon: IconSetting }];
const operationsNavigation = { to: '/operations', menu: 'operations', label: '运维质量', icon: IconSetting };
const mobileMoreGroups = [
{
key: 'insight',
title: '分析与处置',
description: '历史证据与业务告警',
items: [navigation[3], navigation[5]]
},
{
key: 'governance',
title: '平台治理',
description: '接入、账号与运维质量',
items: [navigation[6], navigation[7], operationsNavigation]
}
];
const mobileMoreNavigation = mobileMoreGroups.flatMap((group) => group.items);
function MobileNavigation() {
const location = useLocation();
const { session } = usePlatformSession();
const [moreOpen, setMoreOpen] = useState(false);
const primary = mobilePrimaryNavigation.filter((item) => hasMenu(session, item.menu));
const more = mobileMoreNavigation.filter((item) => hasMenu(session, item.menu));
const moreGroups = mobileMoreGroups
.map((group) => ({ ...group, items: group.items.filter((item) => hasMenu(session, item.menu)) }))
.filter((group) => group.items.length > 0);
const more = moreGroups.flatMap((group) => group.items);
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', '更多功能', '关闭更多功能');
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 mobileMoreNavigation)[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}
{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)}
>
<div className="v2-mobile-more-groups">
{moreGroups.map((group) => <section className="v2-mobile-more-section" key={group.key} aria-labelledby={`v2-mobile-more-${group.key}`}>
<header>
<div><strong id={`v2-mobile-more-${group.key}`}>{group.title}</strong><span>{group.description}</span></div>
<Tag size="small" color="blue">{group.items.length} </Tag>
</header>
<nav aria-label={group.title}>{group.items.map(link)}</nav>
</section>)}
</div>
</SideSheet> : null}
<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}

View File

@@ -532,7 +532,7 @@ describe('V2 production entry', () => {
test('keeps the shared desktop and mobile application shell on Semi UI navigation primitives', () => {
expect(appShellSource).toContain('<Layout className="v2-shell">');
expect(appShellSource).toContain('<Nav className="v2-navigation"');
expect(appShellSource).toContain('<SideSheet className="v2-mobile-more-sidesheet"');
expect(appShellSource).toContain('className="v2-mobile-more-sidesheet"');
expect(appShellSource).toContain('className="v2-password-modal"');
expect(appShellSource.match(/<PasswordInput/g)).toHaveLength(3);
expect(authGateSource.match(/<PasswordInput/g)).toHaveLength(2);
@@ -541,6 +541,12 @@ describe('V2 production entry', () => {
expect(passwordInputSource).toContain("`${visible ? '隐藏' : '显示'}${visibilityLabel}`");
expect(workspaceStyles).toContain('.v2-password-modal .semi-modal {');
expect(workspaceStyles).not.toContain('.v2-password-modal.semi-modal {');
expect(appShellSource).toContain('className="v2-mobile-more-groups"');
expect(appShellSource).toContain("title: '分析与处置'");
expect(appShellSource).toContain("title: '平台治理'");
expect(workspaceStyles).toContain('.v2-mobile-more-section > header {');
expect(workspaceStyles).toContain('.v2-auth-form-heading {');
expect(workspaceStyles).toContain('grid-template-columns: minmax(190px, .58fr) minmax(0, 1fr);');
expect(appShellSource).not.toContain('v2-mobile-more-backdrop');
expect(appShellSource).not.toContain('v2-mobile-more-sheet');
});

View File

@@ -18,6 +18,374 @@
padding: 8px 14px;
}
/*
* Global short-landscape entry and navigation.
* Keep every authentication action in the first viewport, preserve practical
* touch targets, and group secondary destinations by the task they support.
*/
@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {
:root .v2-auth-screen:not(.is-session-loading) {
display: grid;
min-height: 100dvh;
grid-template-columns: minmax(0, 1fr);
place-items: center;
overflow: hidden;
padding: 8px;
}
:root .v2-auth-screen:not(.is-session-loading) > .v2-auth-card.semi-card {
width: min(650px, calc(100vw - 16px));
height: min(359px, calc(100dvh - 16px));
max-height: calc(100dvh - 16px);
justify-self: center;
overflow: hidden;
border-radius: 16px;
}
:root .v2-auth-screen:not(.is-session-loading) > .v2-auth-card.semi-card > .semi-card-body {
height: 100%;
}
:root .v2-auth-form {
display: grid;
height: 100%;
min-height: 0;
grid-template-columns: minmax(190px, .58fr) minmax(0, 1fr);
grid-template-rows: auto auto auto auto;
align-content: center;
column-gap: 18px;
padding: 12px 18px;
}
:root .v2-auth-form-heading {
min-width: 0;
grid-column: 1;
grid-row: 1 / -1;
align-content: center;
justify-items: start;
border-right: 1px solid #e7edf5;
padding-right: 18px;
}
:root .v2-auth-logo {
width: 118px;
margin: 0 0 13px;
}
:root .v2-auth-form-heading > .semi-tag {
height: 22px;
margin: 0 0 9px;
padding-inline: 7px;
font-size: 8px;
}
:root .v2-auth-form-heading > h3.semi-typography {
margin: 0;
text-align: left;
font-size: 18px;
line-height: 1.25;
}
:root .v2-auth-form-heading > h3 + .semi-typography {
max-width: 170px;
margin: 7px 0 0;
text-align: left;
font-size: 9px;
line-height: 1.55;
}
:root .v2-auth-mode-tabs {
min-width: 0;
grid-column: 2;
grid-row: 1;
align-self: center;
margin-top: 0;
}
:root .v2-auth-mode-tabs > .semi-tabs-bar {
padding: 3px;
}
:root .v2-auth-mode-tabs > .semi-tabs-bar .semi-tabs-tab {
height: 32px;
font-size: 10px;
}
:root .v2-auth-fields {
min-height: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
:root .v2-auth-fields > label {
min-width: 0;
gap: 4px;
margin-top: 9px;
font-size: 9px;
}
:root .v2-auth-fields > label .semi-input-wrapper {
height: 40px;
border-radius: 9px;
}
:root .v2-auth-token-note.semi-banner {
min-width: 0;
margin-top: 9px;
padding: 6px 7px;
}
:root .v2-auth-token-note .semi-banner-description {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
:root .v2-auth-error {
min-width: 0;
grid-column: 2;
grid-row: 2;
margin-top: 6px;
}
:root .v2-auth-error .semi-banner {
padding: 6px 8px;
}
:root .v2-auth-error .semi-banner-description {
margin-top: 1px;
line-height: 1.35;
}
:root .v2-auth-form > .semi-button-primary,
:root .v2-auth-form > .v2-auth-error + .semi-button-primary {
height: 40px;
grid-column: 2;
grid-row: 3;
margin-top: 8px;
border-radius: 9px;
font-size: 11px;
}
:root .v2-auth-trust-note {
min-height: 18px;
grid-column: 2;
grid-row: 4;
margin-top: 3px;
}
:root .v2-auth-trust-note > .semi-typography {
font-size: 8px;
}
:root .v2-auth-screen.is-session-loading {
min-height: 100dvh;
place-items: center;
overflow: hidden;
padding: 8px;
}
:root .v2-auth-loading.semi-card {
width: min(650px, calc(100vw - 16px));
max-height: calc(100dvh - 16px);
}
:root .v2-auth-loading-inner {
grid-template-columns: minmax(180px, .58fr) minmax(0, 1fr);
align-items: center;
gap: 10px 18px;
padding: 16px 18px;
}
:root .v2-auth-loading-header {
height: 100%;
grid-column: 1;
grid-row: 1 / 4;
align-content: center;
align-items: flex-start;
justify-content: center;
flex-direction: column;
border-right: 1px solid #e7edf5;
border-bottom: 0;
padding: 0 18px 0 0;
}
:root .v2-auth-loading-header > img {
max-width: 128px;
}
:root .v2-auth-loading-status,
:root .v2-auth-loading-skeleton,
:root .v2-auth-loading-inner > footer {
grid-column: 2;
}
:root .v2-auth-loading-skeleton {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
:root .v2-auth-loading-skeleton > i,
:root .v2-auth-loading-skeleton > i:last-child {
min-height: 38px;
grid-column: auto;
}
:root .v2-auth-loading-inner > footer {
padding-top: 10px;
}
:root .v2-topbar {
height: 48px;
flex-basis: 48px;
padding-inline: 10px 6px;
}
:root .v2-topbar-title > .v2-topbar-page-title.semi-typography {
font-size: 15px;
}
:root .v2-topbar-actions > .v2-help-trigger.semi-button,
:root .v2-topbar-actions > .v2-current-user.semi-button {
width: 36px;
min-width: 36px;
height: 36px;
}
:root .v2-main,
:root .v2-sidebar.is-collapsed + .v2-main {
padding-bottom: calc(56px + env(safe-area-inset-bottom));
}
:root .v2-mobile-navigation {
height: calc(56px + env(safe-area-inset-bottom));
padding: 3px 5px calc(3px + env(safe-area-inset-bottom));
}
:root .v2-mobile-navigation .v2-mobile-nav-item,
:root .v2-mobile-navigation > .semi-button.v2-mobile-nav-item {
min-height: 48px;
gap: 1px;
}
:root .v2-mobile-navigation .v2-mobile-nav-item > .semi-icon,
:root .v2-mobile-navigation > .semi-button.v2-mobile-nav-item .semi-icon {
width: 30px;
height: 24px;
font-size: 17px;
}
:root .v2-mobile-navigation .v2-mobile-nav-item > span,
:root .v2-mobile-navigation > .semi-button.v2-mobile-nav-item .semi-button-content {
font-size: 9px;
}
:root .v2-mobile-more-sidesheet .semi-sidesheet-header {
min-height: 54px;
padding: 9px 14px 8px;
}
:root .v2-mobile-more-sidesheet .semi-sidesheet-body {
padding: 8px 10px 10px;
}
:root .v2-mobile-more-sidesheet .semi-sidesheet-title strong {
font-size: 15px;
}
:root .v2-mobile-more-sidesheet .semi-sidesheet-title span {
font-size: 9px;
}
:root .v2-mobile-more-groups {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
:root .v2-mobile-more-section > header {
min-height: 40px;
padding: 7px 8px 6px;
}
:root .v2-mobile-more-section > nav {
min-height: 0;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 5px;
padding: 6px;
}
:root .v2-mobile-more-section:first-child > nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
:root .v2-mobile-more-sidesheet .v2-mobile-more-section .v2-mobile-nav-item {
min-height: 60px;
}
}
@media (max-width: 680px) {
.v2-mobile-more-groups {
display: grid;
gap: 10px;
}
.v2-mobile-more-section {
min-width: 0;
overflow: hidden;
border: 1px solid #e4eaf2;
border-radius: 13px;
background: #fbfcfe;
}
.v2-mobile-more-section > header {
display: flex;
min-height: 48px;
align-items: center;
justify-content: space-between;
gap: 10px;
border-bottom: 1px solid #e8edf4;
padding: 9px 10px 8px;
}
.v2-mobile-more-section > header > div {
display: grid;
min-width: 0;
gap: 2px;
}
.v2-mobile-more-section > header strong {
color: #2a3c53;
font-size: 11px;
font-weight: 750;
}
.v2-mobile-more-section > header span {
overflow: hidden;
color: #8a97a9;
font-size: 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-mobile-more-section > header > .semi-tag {
flex: 0 0 auto;
border-radius: 999px;
font-size: 8px;
}
.v2-mobile-more-section > nav {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
padding: 8px;
}
.v2-mobile-more-sidesheet .v2-mobile-more-section .v2-mobile-nav-item {
min-height: 68px;
border-color: #e7ecf3;
background: #fff;
}
}
/*
* Short landscape operations cockpit.
* Treat the command bar and workspace switcher as one compact control rail,