refactor: page layout

This commit is contained in:
jialin
2025-11-05 10:02:25 +08:00
parent 219f9871a7
commit 4c22d08da5
43 changed files with 784 additions and 513 deletions
+298
View File
@@ -0,0 +1,298 @@
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
import DropDownActions from '@/components/drop-down-actions';
import IconFont from '@/components/icon-font';
import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links';
import useBodyScroll from '@/hooks/use-body-scroll';
import { logout } from '@/pages/login/apis';
import { useModel } from '@@/plugin-model';
import {
DiscordOutlined,
GithubOutlined,
HomeOutlined,
InfoCircleOutlined,
LogoutOutlined,
ReadOutlined,
SettingOutlined
} from '@ant-design/icons';
import { history, useIntl, useNavigate } from '@umijs/max';
import { Avatar, Divider, Modal } from 'antd';
import { useAtom } from 'jotai';
import { useMemo } from 'react';
import styled from 'styled-components';
const IconWrapper = styled.span`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--ant-color-text-secondary);
`;
const Wrapper = styled.div`
display: flex;
align-items: center;
gap: 24px;
height: 40px;
`;
const DropdownWrapper = styled.div`
min-width: 160px;
box-shadow: var(--ant-box-shadow-secondary);
background-color: var(--ant-color-bg-elevated);
border-radius: var(--ant-border-radius-lg);
padding: var(--ant-padding-xs);
.ant-dropdown-menu {
padding: 0;
box-shadow: none;
background-color: transparent;
border-radius: 0;
a {
color: var(--ant-color-text);
}
}
`;
const CustomItem = styled.div`
display: flex;
align-items: center;
gap: 8px;
height: 32px;
justify-content: flex-start;
height: 32px;
padding: 0 var(--ant-padding-xs);
cursor: pointer;
&.user-info {
cursor: default;
justify-content: space-between;
.user-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100px;
}
}
&:not(.user-info):hover {
background-color: var(--ant-control-item-bg-hover);
}
background-color: var(--ant-color-bg-elevated);
`;
export const ExtraContent = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [modal, contextHolder] = Modal.useModal();
const [version] = useAtom(GPUStackVersionAtom);
const [updateCheck] = useAtom(UpdateCheckAtom);
const intl = useIntl();
const initialInfo = useModel('@@initialState') || {
initialState: undefined,
loading: false,
setInitialState: null
};
const { initialState } = initialInfo;
const navigate = useNavigate();
const loginPath = '/login';
const showUpgrade = useMemo(() => {
return (
initialState?.currentUser?.is_admin &&
updateCheck.latest_version &&
updateCheck.latest_version !== version?.version &&
updateCheck.latest_version?.indexOf('0.0.0') === -1 &&
updateCheck.latest_version?.indexOf('rc') === -1
);
}, [
updateCheck.latest_version,
version.version,
initialState?.currentUser?.is_admin
]);
const showVersion = () => {
saveScrollHeight();
modal.info({
...modalConfig,
width: 460,
content: <VersionInfo intl={intl} />,
onCancel: restoreScrollHeight
});
};
const handleLogout = async () => {
await logout();
navigate(loginPath);
};
const helpList = [
{
key: 'site',
icon: <HomeOutlined />,
label: 'GPUStack',
url: externalLinks.site
},
{
key: 'github',
icon: <GithubOutlined />,
label: intl.formatMessage({ id: 'common.issue.report' }),
url: externalLinks.reportIssue
},
{
key: 'faq',
icon: <IconFont type="icon-fankuifaqs"></IconFont>,
label: intl.formatMessage({ id: 'common.button.faq' }),
url: externalLinks.faq
},
{
key: 'Discord',
icon: <DiscordOutlined />,
label: 'Discord',
url: externalLinks.discord
},
{
key: 'docs',
icon: <ReadOutlined />,
label: intl.formatMessage({ id: 'common.button.docs' }),
url: externalLinks.documentation
},
{
key: 'version',
icon: <InfoCircleOutlined />,
label: intl.formatMessage({ id: 'common.button.version' })
}
];
const helpMenu = {
items: helpList.map((item) => ({
key: item.key,
label: (
<span className="flex flex-center">
{item.icon}
{item.key === 'version' ? (
<>
<a className="m-l-8">{item.label}</a>
{showUpgrade && (
<span className="new-icon">
<span className="text">
{intl.formatMessage({ id: 'common.text.new' })}
</span>
</span>
)}
</>
) : (
<a
className="m-l-8 "
href={item.url}
target="_blank"
rel="noreferrer"
>
{item.label}
</a>
)}
</span>
),
onClick() {
if (item.key === 'version') {
showVersion();
}
}
}))
};
const userMenu = {
items: [
{
key: 'apikeys',
label: (
<span className="flex flex-center">
<IconFont type="icon-key" />
<span className="m-l-8" style={{ marginLeft: 8 }}>
{intl?.formatMessage?.({ id: 'menu.apikeys' })}
</span>
</span>
),
onClick: () => {
history.push('/api-keys');
}
},
{
key: 'settings',
label: (
<span className="flex flex-center">
<SettingOutlined />
<span className="m-l-8" style={{ marginLeft: 8 }}>
{intl?.formatMessage?.({ id: 'common.button.settings' })}
</span>
</span>
),
onClick: () => {
history.push('/profile');
}
}
]
};
const userPopupRender = (originNode: React.ReactNode) => {
return (
<DropdownWrapper>
<CustomItem className="user-info border-bottom">
<span className="flex-center gap-8">
<Avatar
size={24}
src={initialState?.currentUser?.avatar}
icon={
<IconFont type="icon-user-filled" className="font-size-24" />
}
/>
<span className="user-name">
{initialState?.currentUser?.username}
</span>
</span>
</CustomItem>
<Divider style={{ marginBlock: 4 }} />
{originNode}
<Divider style={{ marginBlock: 4 }} />
<CustomItem onClick={handleLogout} className="border-top">
<LogoutOutlined />
<span>{intl?.formatMessage?.({ id: 'common.button.logout' })}</span>
</CustomItem>
</DropdownWrapper>
);
};
const helpPopupRender = (originNode: React.ReactNode) => {
return <DropdownWrapper>{originNode}</DropdownWrapper>;
};
return (
<Wrapper>
{contextHolder}
<DropDownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
<IconWrapper>
<IconFont type="icon-help" className="font-size-20" />
{showUpgrade && (
<span
className="m-l-5"
style={{
display: 'flex',
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: 'var(--ant-orange-5)'
}}
></span>
)}
</IconWrapper>
</DropDownActions>
<DropDownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
<IconWrapper>
<Avatar
size={24}
src={initialState?.currentUser?.avatar}
icon={<IconFont type="icon-user-filled" className="font-size-24" />}
/>
</IconWrapper>
</DropDownActions>
</Wrapper>
);
};
+33 -32
View File
@@ -33,14 +33,26 @@ import 'driver.js/dist/driver.css';
import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css';
import { useEffect, useMemo, useState } from 'react';
import { PageContainerInner } from '../pages/_components/page-box';
import Exception from './Exception';
import './Layout.css';
import { LogoIcon, SLogoIcon } from './Logo';
import ErrorBoundary from './error-boundary';
import { ExtraContent } from './extraRender';
import { getRightRenderContent } from './rightRender';
import { patchRoutes } from './runtime';
import SiderMenu from './sider-menu';
const NO_CONTAINER_PAGES = [
'chat',
'rerank',
'embedding',
'speech',
'image',
'clusterDetail',
'clusterCreate'
];
const loginPath = '/login';
type InitialStateType = {
@@ -205,8 +217,6 @@ export default (props: any) => {
const role = initialState?.currentUser?.is_admin ? 'admin' : 'user';
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role));
console.log('route============', route);
patchRoutes({
routes: route.children,
initialState: initialInfo.initialState
@@ -217,6 +227,12 @@ export default (props: any) => {
[location.pathname]
);
const isNoContainerPage = useMemo(() => {
return NO_CONTAINER_PAGES.includes(matchedRoute?.name);
}, [matchedRoute]);
console.log('matchedRoute=========', matchedRoute, route);
const allRouteKeys = useMemo(() => {
const keys = new Set<string>();
const childrenRoutes = route?.children || [];
@@ -257,27 +273,6 @@ export default (props: any) => {
}
}, [initialize]);
useEffect(() => {
const checkAndInitialize = () => {
const menuWrap = window.document.querySelector(
'.ant-menu.ant-menu-root'
)?.parentElement;
if (menuWrap) {
try {
initializeMenu(menuWrap);
} catch (error) {
console.error('Failed to initialize menu:', error);
}
} else {
console.warn('Menu wrapper not found.');
}
};
const timeout = setTimeout(checkAndInitialize, 500);
return () => clearTimeout(timeout);
}, [initializeMenu, matchedRoute, location]);
const collapsed = useMemo(() => {
return userSettings.collapsed || false;
}, [userSettings.collapsed]);
@@ -326,7 +321,6 @@ export default (props: any) => {
};
const actionRender = (layoutProps) => {
console.log('actionRender', layoutProps);
const dom = getRightRenderContent({
runtimeConfig,
loading,
@@ -334,8 +328,8 @@ export default (props: any) => {
setInitialState,
intl,
isDarkTheme: userSettings.isDarkTheme,
siderWidth: layoutProps.siderWidth,
collapsed: layoutProps.collapsed,
siderWidth: layoutProps?.siderWidth,
collapsed: layoutProps?.collapsed,
showUpgrade
});
@@ -479,6 +473,12 @@ export default (props: any) => {
return data;
}, [userSettings.isDarkTheme, themeData]);
const outlet = runtimeConfig.childrenRender ? (
runtimeConfig.childrenRender(<Outlet />, props)
) : (
<Outlet />
);
return (
<ConfigProvider
componentSize="large"
@@ -492,6 +492,9 @@ export default (props: any) => {
>
<DarkMask></DarkMask>
<ProLayout
fixSiderbar
fixedHeader
breadcrumbRender={false}
route={route}
location={location}
title={userConfig.title}
@@ -510,6 +513,7 @@ export default (props: any) => {
onCollapse={onCollapse}
onMenuHeaderClick={onMenuHeaderClick}
menuHeaderRender={renderMenuHeader}
extra={[<ExtraContent key="extra-content" />]}
collapsed={userSettings.collapsed}
onPageChange={onPageChange}
formatMessage={formatMessage}
@@ -521,10 +525,7 @@ export default (props: any) => {
logo={collapsed ? SLogoIcon : LogoIcon}
menuContentRender={menuContentRender}
disableContentMargin
fixSiderbar
fixedHeader
{...runtimeConfig}
actionsRender={actionRender}
ErrorBoundary={ErrorBoundary}
>
<Exception
@@ -534,10 +535,10 @@ export default (props: any) => {
unAccessible={runtimeConfig?.unAccessible}
noAccessible={runtimeConfig?.noAccessible}
>
{runtimeConfig.childrenRender ? (
runtimeConfig.childrenRender(<Outlet />, props)
{isNoContainerPage ? (
outlet
) : (
<Outlet />
<PageContainerInner>{outlet}</PageContainerInner>
)}
</Exception>
</ProLayout>
+2 -8
View File
@@ -280,14 +280,8 @@ export const getRightRenderContent = (opts: {
return (
<>
<Menu {...helpMenu} style={getMenuStyle(collapsed, siderWidth)} />
<Menu
{...userMenu}
style={getMenuStyle(collapsed, siderWidth, {
marginTop: 8,
marginBottom: 0
})}
/>
<Menu {...helpMenu} mode="vertical" />
<Menu {...userMenu} mode="vertical" />
</>
);
};