diff --git a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx index a1c4feb7..bf37eaec 100644 --- a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx @@ -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(); diff --git a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx index fba01eb1..da1b79b7 100644 --- a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx @@ -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]) => warmRoute(to)} className={({ isActive }) => `v2-mobile-nav-item ${isActive ? 'is-active' : ''}`}>{label}; + const link = ({ to, label, icon: Icon }: (typeof mobileMoreNavigation)[number]) => warmRoute(to)} className={({ isActive }) => `v2-mobile-nav-item ${isActive ? 'is-active' : ''}`}>{label}; return <> - {more.length ? 更多功能数据分析与系统管理} aria-label="更多功能" footer={null} onCancel={() => setMoreOpen(false)}>{more.map(link)} : null} + {more.length ? 更多功能数据分析与系统管理} + aria-label="更多功能" + footer={null} + onCancel={() => setMoreOpen(false)} + > + + {moreGroups.map((group) => + + {group.title}{group.description} + {group.items.length} 项 + + {group.items.map(link)} + )} + + : null} {primary.map(link)} {more.length ? } onClick={() => setMoreOpen((value) => !value)}>更多 : null} diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts index bb3b5b8e..cc981285 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -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(''); expect(appShellSource).toContain(' { 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'); }); diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index 0dcaee7d..d07a740d 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -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,