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 // Initialize dayjs plugins globally
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
import { import { AvatarDropdown, Footer } from '@/components';
AvatarDropdown,
AvatarName,
Footer,
Question,
SelectLang,
} from '@/components';
import { currentUser as queryCurrentUser } from '@/services/ant-design-pro/api'; import { currentUser as queryCurrentUser } from '@/services/ant-design-pro/api';
import defaultSettings from '../config/defaultSettings'; import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig'; import { errorConfig } from './requestErrorConfig';
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
const isDevOrTest = isDev || process.env.CI;
const loginPath = '/user/login'; const loginPath = '/user/login';
/** /**
@ -33,6 +26,7 @@ export async function getInitialState(): Promise<{
currentUser?: API.CurrentUser; currentUser?: API.CurrentUser;
loading?: boolean; loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>; fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
settingDrawerOpen?: boolean;
}> { }> {
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
try { try {
@ -57,11 +51,13 @@ export async function getInitialState(): Promise<{
fetchUserInfo, fetchUserInfo,
currentUser, currentUser,
settings: defaultSettings as Partial<LayoutSettings>, settings: defaultSettings as Partial<LayoutSettings>,
settingDrawerOpen: false,
}; };
} }
return { return {
fetchUserInfo, fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>, settings: defaultSettings as Partial<LayoutSettings>,
settingDrawerOpen: false,
}; };
} }
@ -71,10 +67,6 @@ export const layout: RunTimeLayoutConfig = ({
setInitialState, setInitialState,
}) => { }) => {
return { return {
actionsRender: () => [
<Question key="doc" />,
<SelectLang key="SelectLang" />,
],
menuItemRender: (item, dom) => { menuItemRender: (item, dom) => {
if (item.path) { if (item.path) {
return ( return (
@ -87,7 +79,7 @@ export const layout: RunTimeLayoutConfig = ({
}, },
avatarProps: { avatarProps: {
src: initialState?.currentUser?.avatar, src: initialState?.currentUser?.avatar,
title: <AvatarName />, title: 'ProUser',
render: (_, avatarChildren) => ( render: (_, avatarChildren) => (
<AvatarDropdown>{avatarChildren}</AvatarDropdown> <AvatarDropdown>{avatarChildren}</AvatarDropdown>
), ),
@ -140,19 +132,24 @@ export const layout: RunTimeLayoutConfig = ({
return ( return (
<> <>
{children} {children}
{isDevOrTest && ( <SettingDrawer
<SettingDrawer disableUrlParams
disableUrlParams enableDarkTheme
enableDarkTheme collapse={initialState?.settingDrawerOpen}
settings={initialState?.settings} onCollapseChange={(open) => {
onSettingChange={(settings) => { setInitialState((s) => ({
setInitialState((preInitialState) => ({ ...s,
...preInitialState, settingDrawerOpen: open,
settings, }));
})); }}
}} 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)`]: { [`@media screen and (max-width: ${token.screenXS}px)`]: {
width: '100%', 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 { import {
BookOutlined,
CheckOutlined,
GlobalOutlined,
LogoutOutlined, LogoutOutlined,
SettingOutlined, SettingOutlined,
UserOutlined, SkinOutlined,
} from '@ant-design/icons'; } 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 type { MenuProps } from 'antd';
import { Spin } from 'antd'; import { Spin } from 'antd';
import { createStyles } from 'antd-style';
import React from 'react'; import React from 'react';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
import { outLogin } from '@/services/ant-design-pro/api'; import { outLogin } from '@/services/ant-design-pro/api';
import HeaderDropdown from '../HeaderDropdown'; import HeaderDropdown from '../HeaderDropdown';
export type GlobalHeaderRightProps = { export type GlobalHeaderRightProps = {
menu?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
}; };
export const AvatarName = () => { const localeLabelMap: Record<string, { emoji: string; label: string }> = {
const { initialState } = useModel('@@initialState'); 'zh-CN': { emoji: '🇨🇳', label: '简体中文' },
const { currentUser } = initialState || {}; 'zh-TW': { emoji: '🇭🇰', label: '繁体中文' },
return <span className="anticon">{currentUser?.name}</span>; 'en-US': { emoji: '🇺🇸', label: 'English' },
}; };
const useStyles = createStyles(({ token }) => { const supportLocaleKeys = Object.keys(localeLabelMap);
return {
action: {
display: 'flex',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
alignItems: 'center',
padding: '0 8px',
cursor: 'pointer',
borderRadius: token.borderRadius,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
},
};
});
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
menu,
children, children,
}) => { }) => {
/**
* 退出登录,并且将当前的 url 保存
*/
const loginOut = async () => { const loginOut = async () => {
await outLogin(); await outLogin();
const { search, pathname } = window.location; const { search, pathname } = window.location;
@ -55,9 +42,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
redirect: pathname + search, redirect: pathname + search,
}); });
/** 此方法会跳转到 redirect 参数所在的位置 */
const redirect = urlParams.get('redirect'); const redirect = urlParams.get('redirect');
// Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) { if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({ history.replace({
pathname: '/user/login', pathname: '/user/login',
@ -65,8 +50,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
}); });
} }
}; };
const { styles } = useStyles();
const { initialState, setInitialState } = useModel('@@initialState'); const { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick: MenuProps['onClick'] = (event) => { const onMenuClick: MenuProps['onClick'] = (event) => {
@ -78,49 +61,75 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
loginOut(); loginOut();
return; 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}`); history.push(`/account/${key}`);
}; };
const loading = (
<span className={styles.action}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
if (!initialState) { if (!initialState) {
return loading; return <Spin size="small" />;
} }
const { currentUser } = initialState; const { currentUser } = initialState;
if (!currentUser?.name) { if (!currentUser) {
return loading; return <Spin size="small" />;
} }
const menuItems = [ const allLocales = getAllLocales();
...(menu 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', key: 'lang',
icon: <UserOutlined />, icon: <GlobalOutlined />,
label: '个人中心', label: localeLabelMap[currentLocale]?.label ?? currentLocale,
}, children: supportLocales.map((locale) => ({
{ key: `lang-${locale}`,
key: 'settings', icon:
icon: <SettingOutlined />, locale === currentLocale ? (
label: '个人设置', <CheckOutlined style={{ color: '#52c41a' }} />
}, ) : (
{ <span style={{ display: 'inline-block', width: 14 }} />
type: 'divider' as const, ),
label: `${localeLabelMap[locale]?.emoji ?? ''} ${localeLabelMap[locale]?.label ?? locale}`,
})),
}, },
] ]
: []), : []),
{
type: 'divider' as const,
},
{ {
key: 'logout', key: 'logout',
icon: <LogoutOutlined />, icon: <LogoutOutlined />,
@ -130,11 +139,13 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
return ( return (
<HeaderDropdown <HeaderDropdown
placement="bottomRight"
menu={{ menu={{
selectedKeys: [], selectedKeys: [],
onClick: onMenuClick, onClick: onMenuClick,
items: menuItems, items: menuItems,
}} }}
arrow
> >
{children} {children}
</HeaderDropdown> </HeaderDropdown>

24
src/components/RightContent/index.tsx

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

4
src/components/index.ts

@ -7,7 +7,7 @@
*/ */
import Footer from './Footer'; import Footer from './Footer';
import { Question, SelectLang } from './RightContent'; 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 StandardFormRow } from './StandardFormRow';
export { default as TagSelect } from './TagSelect'; export { default as TagSelect } from './TagSelect';
export { AvatarDropdown, AvatarName, Footer, Question, SelectLang }; export { AvatarDropdown, Footer, Question, SelectLang };

Loading…
Cancel
Save