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>
);
};