Browse Source

feat: extract header actions from avatar dropdown and add all locale options (#11733)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
pull/11736/head
Alex Zhu 4 weeks ago
committed by GitHub
parent
commit
55aeab79d6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      config/routes.ts
  2. 13
      src/app.tsx
  3. 90
      src/components/RightContent/AvatarDropdown.tsx
  4. 132
      src/components/RightContent/index.tsx
  5. 4
      src/components/index.ts
  6. 22
      src/pages/404.tsx
  7. 38
      src/pages/exception/404/index.tsx

4
config/routes.ts

@ -38,7 +38,7 @@ export default [
},
{
name: '404',
component: './404',
component: './exception/404',
path: '/user/*',
},
],
@ -292,7 +292,7 @@ export default [
},
{
name: '404',
component: './404',
component: './exception/404',
path: '/*',
},
];

13
src/app.tsx

@ -10,7 +10,13 @@ import React from 'react';
// Initialize dayjs plugins globally
dayjs.extend(relativeTime);
import { AvatarDropdown, Footer } from '@/components';
import {
AvatarDropdown,
DocLink,
Footer,
LangDropdown,
VersionDropdown,
} from '@/components';
import { currentUser as queryCurrentUser } from '@/services/ant-design-pro/api';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
@ -80,6 +86,11 @@ export const layout: RunTimeLayoutConfig = ({
}
return dom;
},
actionsRender: () => [
<DocLink key="doc" />,
<VersionDropdown key="version" />,
<LangDropdown key="lang" />,
],
avatarProps: {
src: initialState?.currentUser?.avatar,
title: 'ProUser',

90
src/components/RightContent/AvatarDropdown.tsx

@ -1,20 +1,9 @@
import {
BookOutlined,
CheckOutlined,
GlobalOutlined,
HistoryOutlined,
LogoutOutlined,
SettingOutlined,
SkinOutlined,
} from '@ant-design/icons';
import {
getAllLocales,
getLocale,
history,
setLocale,
useIntl,
useModel,
} from '@umijs/max';
import { history, useModel } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Spin } from 'antd';
import React from 'react';
@ -26,14 +15,6 @@ export type GlobalHeaderRightProps = {
children?: React.ReactNode;
};
const localeLabelMap: Record<string, { emoji: string; label: string }> = {
'zh-CN': { emoji: '🇨🇳', label: '简体中文' },
'zh-TW': { emoji: '🇭🇰', label: '繁体中文' },
'en-US': { emoji: '🇺🇸', label: 'English' },
};
const supportLocaleKeys = Object.keys(localeLabelMap);
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
children,
}) => {
@ -53,7 +34,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
}
};
const { initialState, setInitialState } = useModel('@@initialState');
const intl = useIntl();
const onMenuClick: MenuProps['onClick'] = (event) => {
const { key } = event;
@ -68,19 +48,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
setInitialState((s) => ({ ...s, settingDrawerOpen: true }));
return;
}
if (key === 'doc') {
history.push('/welcome');
return;
}
if (key.startsWith('lang-')) {
setLocale(key.replace('lang-', ''), false);
return;
}
if (key.startsWith('version-')) {
const url = key.replace('version-', '');
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
history.push(`/account/${key}`);
};
@ -94,12 +61,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
return <Spin size="small" />;
}
const allLocales = getAllLocales();
const currentLocale = getLocale();
const supportLocales = allLocales.filter((l) =>
supportLocaleKeys.includes(l),
);
const menuItems: MenuProps['items'] = [
{
key: 'settings',
@ -111,55 +72,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
icon: <SkinOutlined />,
label: '主题设置',
},
{
key: 'doc',
icon: <BookOutlined />,
label: '使用文档',
},
{
key: 'version',
icon: <HistoryOutlined />,
label: intl.formatMessage({
id: 'component.globalHeader.historyVersion',
}),
children: [
{
key: 'version-https://v5.pro.ant.design',
label: 'v5',
},
{
key: 'version-https://v4.pro.ant.design',
label: 'v4',
},
{
key: 'version-https://v2.pro.ant.design',
label: 'v2',
},
{
key: 'version-https://v1.pro.ant.design',
label: 'v1',
},
],
},
...(supportLocales.length > 1
? [
{
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,
},

132
src/components/RightContent/index.tsx

@ -1,20 +1,130 @@
import { QuestionCircleOutlined } from '@ant-design/icons';
import { history, SelectLang as UmiSelectLang } from '@umijs/max';
import {
BookOutlined,
CheckOutlined,
ForkOutlined,
GlobalOutlined,
} from '@ant-design/icons';
import { getAllLocales, getLocale, history, setLocale } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Button, Tooltip } from 'antd';
import { createStyles } from 'antd-style';
import React, { useMemo } from 'react';
import HeaderDropdown from '../HeaderDropdown';
export type SiderTheme = 'light' | 'dark';
export const localeLabelMap: Record<string, { emoji: string; label: string }> =
{
'zh-CN': { emoji: '🇨🇳', label: '简体中文' },
'zh-TW': { emoji: '🇭🇰', label: '繁體中文' },
'en-US': { emoji: '🇺🇸', label: 'English' },
'ja-JP': { emoji: '🇯🇵', label: '日本語' },
'pt-BR': { emoji: '🇧🇷', label: 'Português' },
'id-ID': { emoji: '🇮🇩', label: 'Bahasa Indonesia' },
'fa-IR': { emoji: '🇮🇷', label: 'فارسی' },
'bn-BD': { emoji: '🇧🇩', label: 'বাংলা' },
};
export const SelectLang: React.FC = () => {
return <UmiSelectLang />;
const useStyles = createStyles(({ token, css }) => ({
action: css`
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 36px !important;
min-width: 36px;
padding-inline: 8px !important;
padding-block: 0 !important;
border-radius: ${token.borderRadius}px !important;
`,
}));
export const DocLink: React.FC = () => {
const { styles } = useStyles();
return (
<Tooltip title="使用文档">
<Button
type="text"
className={styles.action}
icon={<BookOutlined />}
aria-label="使用文档"
onClick={() => {
history.push('/welcome');
}}
/>
</Tooltip>
);
};
const versionItems: MenuProps['items'] = [
{ key: 'https://v5.pro.ant.design', label: 'v5' },
{ key: 'https://v4.pro.ant.design', label: 'v4' },
{ key: 'https://v2.pro.ant.design', label: 'v2' },
{ key: 'https://v1.pro.ant.design', label: 'v1' },
];
const onVersionClick: MenuProps['onClick'] = ({ key }) => {
window.open(key, '_blank', 'noopener,noreferrer');
};
export const VersionDropdown: React.FC = () => {
const { styles } = useStyles();
return (
<HeaderDropdown
placement="bottomRight"
arrow
menu={{
selectedKeys: [],
onClick: onVersionClick,
items: versionItems,
style: { minWidth: 100 },
}}
>
<Button type="text" className={styles.action} aria-label="历史版本">
<ForkOutlined />
</Button>
</HeaderDropdown>
);
};
export const Question: React.FC = () => {
export const LangDropdown: React.FC = () => {
const { styles } = useStyles();
const allLocales = useMemo(() => getAllLocales(), []);
const currentLocale = getLocale();
const supportLocales = allLocales.filter((l) => l in localeLabelMap);
if (supportLocales.length <= 1) {
return null;
}
const langItems: MenuProps['items'] = 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}`,
}));
const onLangClick: MenuProps['onClick'] = ({ key }) => {
if (key.startsWith('lang-')) {
setLocale(key.replace('lang-', ''), false);
}
};
return (
<span
onClick={() => {
history.push('/welcome');
<HeaderDropdown
placement="bottomRight"
arrow
menu={{
selectedKeys: [`lang-${currentLocale}`],
onClick: onLangClick,
items: langItems,
style: { minWidth: 180 },
}}
>
<QuestionCircleOutlined />
</span>
<Button type="text" className={styles.action} aria-label="语言切换">
<GlobalOutlined />
</Button>
</HeaderDropdown>
);
};

4
src/components/index.ts

@ -6,7 +6,7 @@
*
*/
import Footer from './Footer';
import { Question, SelectLang } from './RightContent';
import { DocLink, LangDropdown, VersionDropdown } from './RightContent';
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, Footer, Question, SelectLang };
export { AvatarDropdown, DocLink, Footer, LangDropdown, VersionDropdown };

22
src/pages/404.tsx

@ -1,22 +0,0 @@
import { Link, useIntl } from '@umijs/max';
import { Button, Card, Result } from 'antd';
import React from 'react';
const NoFoundPage: React.FC = () => (
<Card variant="borderless">
<Result
status="404"
title="404"
subTitle={useIntl().formatMessage({ id: 'pages.404.subTitle' })}
extra={
<Link to="/" prefetch>
<Button type="primary">
{useIntl().formatMessage({ id: 'pages.404.buttonText' })}
</Button>
</Link>
}
/>
</Card>
);
export default NoFoundPage;

38
src/pages/exception/404/index.tsx

@ -1,17 +1,25 @@
import { Link } from '@umijs/max';
import { Link, useIntl } from '@umijs/max';
import { Button, Card, Result } from 'antd';
import React from 'react';
export default () => (
<Card variant="borderless">
<Result
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Link to="/" prefetch>
<Button type="primary">Back Home</Button>
</Link>
}
/>
</Card>
);
const Exception404: React.FC = () => {
const intl = useIntl();
return (
<Card variant="borderless">
<Result
status="404"
title="404"
subTitle={intl.formatMessage({ id: 'pages.404.subTitle' })}
extra={
<Link to="/" prefetch>
<Button type="primary">
{intl.formatMessage({ id: 'pages.404.buttonText' })}
</Button>
</Link>
}
/>
</Card>
);
};
export default Exception404;

Loading…
Cancel
Save