From a90e02a04c43df8b66ca4cdc3d625e8906a3f2f0 Mon Sep 17 00:00:00 2001 From: lofyer Date: Thu, 2 Jul 2026 09:17:48 +0800 Subject: [PATCH] feat(layout): move navigation menu from sidebar to top header Switch ProLayout to top layout with a custom horizontal HeaderMenu (dropdowns for grouped items) and relocate the user/help/version actions into the header right area. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/layouts/header-menu.tsx | 128 +++++++++++++++++++++++++++++ src/layouts/index.tsx | 78 +++++------------- src/pages/_components/page-box.tsx | 10 +-- 3 files changed, 150 insertions(+), 66 deletions(-) create mode 100644 src/layouts/header-menu.tsx diff --git a/src/layouts/header-menu.tsx b/src/layouts/header-menu.tsx new file mode 100644 index 00000000..fa30fef0 --- /dev/null +++ b/src/layouts/header-menu.tsx @@ -0,0 +1,128 @@ +import { IconFont } from '@gpustack/core-ui'; +import { Link, useLocation, useNavigate } from '@umijs/max'; +import { Menu } from 'antd'; +import { createStyles } from 'antd-style'; +import React, { useMemo } from 'react'; + +interface MenuItem { + icon?: string; + selectedIcon?: string; + defaultIcon?: string; + children?: MenuItem[]; + + [key: string]: any; +} + +interface HeaderMenuProps { + menuData: MenuItem[]; + initialState?: Global.InitialStateType; +} + +const useStyles = createStyles(({ css }) => { + return { + headerMenu: css` + flex: 1; + min-width: 0; + background: transparent; + border-bottom: none; + line-height: inherit; + + &.ant-menu-horizontal { + border-bottom: none; + } + &.ant-menu-horizontal > .ant-menu-item::after, + &.ant-menu-horizontal > .ant-menu-submenu::after { + display: none; + } + .ant-menu-title-content { + display: inline-flex; + align-items: center; + gap: 8px; + } + .anticon { + font-size: 16px; + } + ` + }; +}); + +const isItemSelected = (item: MenuItem, pathname: string) => { + return ( + pathname === item.path || + (Array.isArray(item.subMenu) && item.subMenu.includes(pathname)) + ); +}; + +const HeaderMenu: React.FC = (props) => { + const { menuData } = props; + const { styles } = useStyles(); + const location = useLocation(); + const navigate = useNavigate(); + + const buildLeaf = (item: MenuItem) => { + const selected = isItemSelected(item, location.pathname); + return { + key: item.path as string, + label: ( + + + + {item.name} + + + ) + }; + }; + + const items = useMemo(() => { + return menuData.map((item) => { + if (item.children && item.children.length > 0) { + return { + key: item.key, + label: item.name, + children: item.children.map((child) => buildLeaf(child)) + }; + } + return buildLeaf(item); + }); + }, [menuData, location.pathname]); + + const selectedKeys = useMemo(() => { + const keys: string[] = []; + for (const item of menuData) { + const leaves = + item.children && item.children.length > 0 ? item.children : [item]; + for (const leaf of leaves) { + if (isItemSelected(leaf, location.pathname)) { + keys.push(leaf.path as string); + } + } + } + return keys; + }, [menuData, location.pathname]); + + const handleClick = ({ key }: { key: string }) => { + if (key.startsWith('/')) { + navigate(key.replace('/*', '')); + } + }; + + return ( + + ); +}; + +export default HeaderMenu; diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 2a4c37d2..432d245b 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -22,7 +22,7 @@ import { import { useAccessMarkedRoutes } from '@@/plugin-access'; import { useModel } from '@@/plugin-model'; import { ProLayout } from '@ant-design/pro-components'; -import { CoreUIProvider, IconFont } from '@gpustack/core-ui'; +import { CoreUIProvider } from '@gpustack/core-ui'; import { Access, Outlet, @@ -39,18 +39,18 @@ import { useNavigate, type IRoute } from '@umijs/max'; -import { Button, ConfigProvider, Modal, theme } from 'antd'; +import { ConfigProvider, Modal, theme } from 'antd'; import { useAtom } from 'jotai'; import 'overlayscrollbars/overlayscrollbars.css'; import { useEffect, useMemo, useRef } from 'react'; import { PageContainerInner } from '../pages/_components/page-box'; import Exception from './Exception'; import './Layout.css'; -import { LogoIcon, SLogoIcon } from './Logo'; +import { LogoIcon } from './Logo'; import ErrorBoundary from './error-boundary'; import { ExtraContent } from './extraRender'; +import HeaderMenu from './header-menu'; import { patchRoutes } from './runtime'; -import SiderMenu from './sider-menu'; // Pages that use the page container in the page const NO_CONTAINER_PAGES = [ @@ -133,7 +133,7 @@ const mapRoutes = (routes: IRoute[], role: string) => { export default (props: any) => { const [, contextHolder] = Modal.useModal(); - const { themeData, setUserSettings, userSettings } = useUserSettings(); + const { themeData, userSettings } = useUserSettings(); const [userInfo] = useAtom(userAtom); const [routeCache] = useAtom(routeCacheAtom); const location = useLocation(); @@ -238,13 +238,6 @@ export default (props: any) => { const coreUISlots = useMemo(() => ({ ExtraContent }), []); - const handleToggleCollapse = (e: any) => { - e.stopPropagation(); - setUserSettings({ - ...userSettings, - collapsed: !userSettings.collapsed - }); - }; const newRoutes = filterRoutes( // @ts-ignore clientRoutes.filter((route) => route.id === 'max-tabs'), @@ -274,16 +267,19 @@ export default (props: any) => { return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string); }, [matchedRoute]); - const collapsed = useMemo(() => { - return userSettings.collapsed || false; - }, [userSettings.collapsed]); - const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => { return <>{logo}; }; - const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => { - return ; + const headerContentRender = ( + headerProps: any, + defaultDom: React.ReactNode + ) => { + return ; + }; + + const actionsRender = () => { + return ; }; const onPageChange = async (route: any) => { @@ -337,17 +333,6 @@ export default (props: any) => { navigate(pagepath); }; - const onCollapse = (value: boolean) => { - // only trigger by window resize - if (!value) { - return; - } - setUserSettings({ - ...userSettings, - collapsed: value - }); - }; - return ( { ( - - )} - onCollapse={onCollapse} onMenuHeaderClick={onMenuHeaderClick} - collapsed={userSettings.collapsed} onPageChange={onPageChange} formatMessage={formatMessage} menu={{ - locale: true, - type: 'group' + locale: true }} - splitMenus={true} - logo={userSettings.collapsed ? : } - menuContentRender={menuContentRender} + logo={} + headerContentRender={headerContentRender} + actionsRender={actionsRender} {...runtimeConfig} ErrorBoundary={ErrorBoundary} > @@ -457,7 +421,7 @@ export default (props: any) => { style={{ display: 'flex', flexDirection: 'column', - height: '100vh', + height: '100%', overflow: 'hidden' }} > diff --git a/src/pages/_components/page-box.tsx b/src/pages/_components/page-box.tsx index dd2e7079..6970a5f1 100644 --- a/src/pages/_components/page-box.tsx +++ b/src/pages/_components/page-box.tsx @@ -1,11 +1,9 @@ -import { ExtraContent } from '@/layouts/extraRender'; import { PageContainer, RouteContext, type PageContainerProps } from '@ant-design/pro-components'; import { useOverlayScroller } from '@gpustack/core-ui'; -import { Divider } from 'antd'; import classNames from 'classnames'; import { useContext, useEffect, useRef } from 'react'; import pageBoxCss from './styles/page-box.less'; @@ -53,13 +51,7 @@ export const PageContainerInner: React.FC< {leftContent || pageContext.title}
- {rightContent && ( -
- {rightContent} - -
- )} - + {rightContent &&
{rightContent}
}