feat: refine global shell for mobile landscape
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user