Browse Source

refactor: 统一导航栏右上角样式,更新头像菜单 (#11721)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
pull/11724/head
Alex Zhu 5 months ago
committed by GitHub
parent
commit
ab5213f082
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 49
      src/app.tsx
  2. 8
      src/components/HeaderDropdown/index.tsx
  3. 133
      src/components/RightContent/AvatarDropdown.tsx
  4. 24
      src/components/RightContent/index.tsx
  5. 4
      src/components/index.ts

49
src/app.tsx

@ -10,19 +10,12 @@ import React from 'react';
// Initialize dayjs plugins globally
dayjs.extend(relativeTime);
import {
AvatarDropdown,
AvatarName,
Footer,
Question,
SelectLang,
} from '@/components';
import { AvatarDropdown, Footer } from '@/components';
import { currentUser as queryCurrentUser } from '@/services/ant-design-pro/api';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
const isDev = process.env.NODE_ENV === 'development';
const isDevOrTest = isDev || process.env.CI;
const loginPath = '/user/login';
/**
@ -33,6 +26,7 @@ export async function getInitialState(): Promise<{
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
settingDrawerOpen?: boolean;
}> {
const fetchUserInfo = async () => {
try {
@ -57,11 +51,13 @@ export async function getInitialState(): Promise<{
fetchUserInfo,
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
settingDrawerOpen: false,
};
}
return {
fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>,
settingDrawerOpen: false,
};
}
@ -71,10 +67,6 @@ export const layout: RunTimeLayoutConfig = ({
setInitialState,
}) => {
return {
actionsRender: () => [
<Question key="doc" />,
<SelectLang key="SelectLang" />,
],
menuItemRender: (item, dom) => {
if (item.path) {
return (
@ -87,7 +79,7 @@ export const layout: RunTimeLayoutConfig = ({
},
avatarProps: {
src: initialState?.currentUser?.avatar,
title: <AvatarName />,
title: 'ProUser',
render: (_, avatarChildren) => (
<AvatarDropdown>{avatarChildren}</AvatarDropdown>
),
@ -140,19 +132,24 @@ export const layout: RunTimeLayoutConfig = ({
return (
<>
{children}
{isDevOrTest && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
)}
<SettingDrawer
disableUrlParams
enableDarkTheme
collapse={initialState?.settingDrawerOpen}
onCollapseChange={(open) => {
setInitialState((s) => ({
...s,
settingDrawerOpen: open,
}));
}}
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((s) => ({
...s,
settings,
}));
}}
/>
</>
);
},

8
src/components/HeaderDropdown/index.tsx

@ -10,6 +10,14 @@ const useStyles = createStyles(({ token }) => {
[`@media screen and (max-width: ${token.screenXS}px)`]: {
width: '100%',
},
'.ant-dropdown-menu-item .anticon, .ant-dropdown-menu-submenu-title .anticon':
{
display: 'inline-flex',
alignItems: 'center',
},
'.ant-dropdown-menu-submenu-title .anticon': {
color: token.colorTextSecondary,
},
},
};
});

133
src/components/RightContent/AvatarDropdown.tsx

@ -1,53 +1,40 @@
import {
BookOutlined,
CheckOutlined,
GlobalOutlined,
LogoutOutlined,
SettingOutlined,
UserOutlined,
SkinOutlined,
} from '@ant-design/icons';
import { history, useModel } from '@umijs/max';
import {
getAllLocales,
getLocale,
history,
setLocale,
useModel,
} from '@umijs/max';
import type { MenuProps } from 'antd';
import { Spin } from 'antd';
import { createStyles } from 'antd-style';
import React from 'react';
import { flushSync } from 'react-dom';
import { outLogin } from '@/services/ant-design-pro/api';
import HeaderDropdown from '../HeaderDropdown';
export type GlobalHeaderRightProps = {
menu?: boolean;
children?: React.ReactNode;
};
export const AvatarName = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
return <span className="anticon">{currentUser?.name}</span>;
const localeLabelMap: Record<string, { emoji: string; label: string }> = {
'zh-CN': { emoji: '🇨🇳', label: '简体中文' },
'zh-TW': { emoji: '🇭🇰', label: '繁体中文' },
'en-US': { emoji: '🇺🇸', label: 'English' },
};
const useStyles = createStyles(({ token }) => {
return {
action: {
display: 'flex',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
alignItems: 'center',
padding: '0 8px',
cursor: 'pointer',
borderRadius: token.borderRadius,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
},
};
});
const supportLocaleKeys = Object.keys(localeLabelMap);
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
menu,
children,
}) => {
/**
* 退出登录,并且将当前的 url 保存
*/
const loginOut = async () => {
await outLogin();
const { search, pathname } = window.location;
@ -55,9 +42,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
const searchParams = new URLSearchParams({
redirect: pathname + search,
});
/** 此方法会跳转到 redirect 参数所在的位置 */
const redirect = urlParams.get('redirect');
// Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
@ -65,8 +50,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
});
}
};
const { styles } = useStyles();
const { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick: MenuProps['onClick'] = (event) => {
@ -78,49 +61,75 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
loginOut();
return;
}
if (key === 'theme') {
setInitialState((s) => ({ ...s, settingDrawerOpen: true }));
return;
}
if (key === 'doc') {
history.push('/welcome');
return;
}
if (key.startsWith('lang-')) {
setLocale(key.replace('lang-', ''), false);
return;
}
history.push(`/account/${key}`);
};
const loading = (
<span className={styles.action}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
if (!initialState) {
return loading;
return <Spin size="small" />;
}
const { currentUser } = initialState;
if (!currentUser?.name) {
return loading;
if (!currentUser) {
return <Spin size="small" />;
}
const menuItems = [
...(menu
const allLocales = getAllLocales();
const currentLocale = getLocale();
const supportLocales = allLocales.filter((l) =>
supportLocaleKeys.includes(l),
);
const menuItems: MenuProps['items'] = [
{
key: 'settings',
icon: <SettingOutlined />,
label: '个人设置',
},
{
key: 'theme',
icon: <SkinOutlined />,
label: '主题设置',
},
{
key: 'doc',
icon: <BookOutlined />,
label: '使用文档',
},
...(supportLocales.length > 1
? [
{
key: 'center',
icon: <UserOutlined />,
label: '个人中心',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '个人设置',
},
{
type: 'divider' as const,
key: 'lang',
icon: <GlobalOutlined />,
label: localeLabelMap[currentLocale]?.label ?? currentLocale,
children: supportLocales.map((locale) => ({
key: `lang-${locale}`,
icon:
locale === currentLocale ? (
<CheckOutlined style={{ color: '#52c41a' }} />
) : (
<span style={{ display: 'inline-block', width: 14 }} />
),
label: `${localeLabelMap[locale]?.emoji ?? ''} ${localeLabelMap[locale]?.label ?? locale}`,
})),
},
]
: []),
{
type: 'divider' as const,
},
{
key: 'logout',
icon: <LogoutOutlined />,
@ -130,11 +139,13 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
return (
<HeaderDropdown
placement="bottomRight"
menu={{
selectedKeys: [],
onClick: onMenuClick,
items: menuItems,
}}
arrow
>
{children}
</HeaderDropdown>

24
src/components/RightContent/index.tsx

@ -1,32 +1,20 @@
import { QuestionCircleOutlined } from '@ant-design/icons';
import { SelectLang as UmiSelectLang } from '@umijs/max';
import { history, SelectLang as UmiSelectLang } from '@umijs/max';
export type SiderTheme = 'light' | 'dark';
export const SelectLang: React.FC = () => {
return (
<UmiSelectLang
style={{
padding: 4,
}}
/>
);
return <UmiSelectLang />;
};
export const Question: React.FC = () => {
return (
<a
href="https://pro.ant.design/docs/getting-started"
target="_blank"
rel="noreferrer"
style={{
display: 'inline-flex',
padding: '4px',
fontSize: '18px',
color: 'inherit',
<span
onClick={() => {
history.push('/welcome');
}}
>
<QuestionCircleOutlined />
</a>
</span>
);
};

4
src/components/index.ts

@ -7,7 +7,7 @@
*/
import Footer from './Footer';
import { Question, SelectLang } from './RightContent';
import { AvatarDropdown, AvatarName } from './RightContent/AvatarDropdown';
import { AvatarDropdown } from './RightContent/AvatarDropdown';
/**
* 业务组件
@ -17,4 +17,4 @@ export { default as AvatarList } from './AvatarList';
export { default as StandardFormRow } from './StandardFormRow';
export { default as TagSelect } from './TagSelect';
export { AvatarDropdown, AvatarName, Footer, Question, SelectLang };
export { AvatarDropdown, Footer, Question, SelectLang };

Loading…
Cancel
Save