创建指令:git clone --depth 1 https://gitee.com/ant-design/ant-design-pro.git my-project
模板地址:https://pro.ant.design/zh-CN/
一动态路由
1. 修改代理
位置:my-project/config/proxy.ts
修改:配置其接口地址
export default {
dev: {
'/api': {
target: '接口地址', // 正确的后端域名
changeOrigin: true,
pathRewrite: { '^/api': '' },
timeout: 30000,
},
'/pc': {
target: '接口地址', // 正确的后端域名
changeOrigin: true,
timeout: 30000,
},
},
};
2. 修改登录接口
位置:src/services/ant-design-pro/api.ts
修改:修改登录接口请求路径
/** 登录接口 */
export async function adminLogin(body: { username: string; password: string }) {
return request('接口路径', {
method: 'POST',
data: body,
requestType: 'form', // 适配后端的表单提交格式
});
}
3. 登录页内容修改
位置:src/pages/user/login/index.tsx
修改:登录页修改的完整内容(该接口参数为username和password)
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { history } from '@umijs/max';
import { App, Button, Form, Input } from 'antd';
import { createStyles } from 'antd-style';
import React, { useEffect } from 'react';
import { adminLogin } from '@/services/ant-design-pro/api';
import { getFirstMenuPath, getMenuListFromStorage } from '@/utils/menu';
const useStyles = createStyles(() => {
return {
container: {
height: '100vh',
display: 'flex',
alignItems: 'center',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
},
wrapper: {
position: 'relative',
},
title: {
fontSize: '1.8rem',
textAlign: 'center',
fontWeight: '700',
color: '#505050',
marginTop: 10,
},
loginBox: {
height: '500px',
marginLeft: '180px',
width: 400,
background: '#fff',
padding: '40px 35px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
},
boxHeadTitle: {
fontSize: '1.2rem',
color: '#505050',
textAlign: 'center',
marginBottom: 8,
padding: '30px 0px',
},
inputBox: {
backgroundColor: '#fff',
borderRadius: 6,
border: '1px solid #d9d9d9',
},
formItem: {
marginBottom: 20,
},
loginButton: {
width: '100%',
height: 38,
borderRadius: '25px',
background: '#4096ff',
borderColor: '#4096ff',
fontSize: 13,
marginTop: 50,
},
};
});
/**
* 从 URL 的 redirect 参数取登录后的跳转目标(即登录前正要访问的页面)。
* 仅允许站内路径(以 / 开头且不是 // 开头),防止开放重定向。
*/
function getSafeRedirect(defaultPath: string): string {
const redirect = new URLSearchParams(window.location.search).get('redirect');
if (redirect?.startsWith('/') && !redirect.startsWith('//')) {
return redirect;
}
return defaultPath;
}
const Login: React.FC = () => {
const { styles } = useStyles();
const { message } = App.useApp();
const [form] = Form.useForm();
// 防回退拦截:若已登录,跳到登录前要访问的页面(redirect 参数),否则跳首页
useEffect(() => {
const token = localStorage.getItem('usertoken');
if (token) {
const firstPath = getFirstMenuPath(getMenuListFromStorage()) || '/home';
history.replace(getSafeRedirect(firstPath));
}
}, []);
const handleLogin = async (values: {
username: string;
password: string;
}) => {
try {
console.log('提交登录:', values);
const res: any = await adminLogin(values);
if (res.code === 1) {
message.success('登录成功!');
// 持久化保存 Token 与 数据
const userInfo = res.data?.userinfo || {};
const token = res.data?.token || '';
const menus = res.data?.menus || [];
localStorage.setItem('usertoken', token);
localStorage.setItem('userInfo', JSON.stringify(userInfo));
localStorage.setItem('menuList', JSON.stringify(menus));
// 动态路由由 app.tsx 的 patchRoutes 在应用启动时读取 menuList 注册,
// 登录后需整页刷新,让动态路由与左侧树形菜单重新生效。
// 优先跳回登录前要访问的页面(redirect 参数),否则跳动态菜单首页。
const firstPath = getFirstMenuPath(menus) || '/home';
window.location.href = getSafeRedirect(firstPath);
return;
}
message.error(res.msg || '登录失败,请检查账号密码');
} catch (error) {
console.error('登录错误:', error);
message.error('后端服务未启动或连接失败,请检查后端!');
}
};
return (
<div className={styles.container}>
<div className={styles.wrapper}>
<div className={styles.loginBox}>
<div className={styles.boxHeadTitle}>欢迎登录</div>
<Form form={form} onFinish={handleLogin}>
<Form.Item
name="username"
className={styles.formItem}
rules={[
{ required: true, message: '请输入账号' },
() => ({
validator(_, value) {
if (!value) return Promise.resolve();
if (value.includes(' ')) {
return Promise.reject('账号不能包含空格');
}
if (/[\u4e00-\u9fa5]/.test(value)) {
return Promise.reject('账号不能包含中文');
}
return Promise.resolve();
},
}),
]}
>
<Input
size="large"
prefix={<UserOutlined style={{ color: '#999' }} />}
placeholder="请输入账号"
className={styles.inputBox}
/>
</Form.Item>
<Form.Item
name="password"
className={styles.formItem}
rules={[
{ required: true, message: '请输入密码' },
() => ({
validator(_, value) {
if (!value) return Promise.resolve();
if (value.includes(' ')) {
return Promise.reject('密码不能包含空格');
}
return Promise.resolve();
},
}),
]}
>
<Input.Password
size="large"
prefix={<LockOutlined style={{ color: '#999' }} />}
placeholder="请输入密码"
className={styles.inputBox}
autoComplete="current-password"
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className={styles.loginButton}
size="large"
>
立即登录
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
};
export default Login;
4. 修改全局状态
位置:src/app.ts
修改:修改全局状态,引入import {buildDynamicRoutes,getFirstMenuPath,getMenuListFromStorage,} from '@/utils/menu'; 动态路由页面效果
import {
AppstoreOutlined,
AuditOutlined,
CreditCardOutlined,
EditOutlined,
FolderOutlined,
GiftOutlined,
HomeOutlined,
KeyOutlined,
LinkOutlined,
MailOutlined,
TeamOutlined,
UserOutlined,
} from '@ant-design/icons';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { SettingDrawer } from '@ant-design/pro-components';
import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
import { history, Link, Outlet } from '@umijs/max';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import React from 'react';
import DynamicMenu from '@/pages/DynamicMenu';
import {
buildDynamicRoutes,
getFirstMenuPath,
getMenuListFromStorage,
} from '@/utils/menu';
dayjs.extend(relativeTime);
import {
AvatarDropdown,
DocLink,
ErrorBoundary,
Footer,
LangDropdown,
OfflineBanner,
VersionDropdown,
} from '@/components';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
const isDev = process.env.NODE_ENV === 'development';
const loginPath = '/user/login';
/** 渲染子路由的容器组件(目录/redirect 路由使用) */
const EmptyRoute: React.FC = () => <Outlet />;
const EmptyRouteLazy = React.lazy(() =>
Promise.resolve({ default: EmptyRoute }),
);
const DynamicMenuLazy = React.lazy(() =>
Promise.resolve({ default: DynamicMenu }),
);
/**
* 后端菜单图标是 iconfont 类名
* 这里把常见图标名映射为内置的 antd 图标,未匹配到的使用默认图标。
*/
const MENU_ICON_MAP: Record<string, React.ReactNode> = {
'iconfont icon-barcode-qr': <HomeOutlined />,
'iconfont icon-gerenzhongxin': <UserOutlined />,
'iconfont icon-juxingkaobei': <AppstoreOutlined />,
'iconfont icon-zhongduancanshuchaxun': <AuditOutlined />,
'iconfont icon-icon-': <TeamOutlined />,
'iconfont icon-quanjushezhi_o': <KeyOutlined />,
'ele-EditPen': <EditOutlined />,
'ele-Tickets': <MailOutlined />,
'ele-CreditCard': <CreditCardOutlined />,
'ele-Apple': <GiftOutlined />,
'ele-Present': <GiftOutlined />,
};
function resolveMenuIcon(icon?: string): React.ReactNode {
if (!icon) return undefined;
return MENU_ICON_MAP[icon] ?? <FolderOutlined />;
}
/**
* 动态路由注册
*/
export function patchRoutes({
routes,
routeComponents,
}: {
routes: Record<string, any>;
routeComponents: Record<string, any>;
}) {
const menuList = getMenuListFromStorage();
if (menuList.length === 0) return;
// 记录静态路由(config/routes.ts 已声明的路径,如 /home、/user)
const staticPaths = new Set(
Object.values(routes)
.filter((route: any) => route.path && !route.isLayout)
.map((route: any) => route.path),
);
// 判断某路径是否为「已声明真实页面的静态路由」
// (有 component 且非 layout:false,如 /home → ./Home)
const hasStaticPage = (path: string) =>
Object.values(routes).some(
(route: any) =>
route.path === path &&
route.component &&
!route.isLayout &&
route.layout !== false &&
!route.redirect,
);
const defs = buildDynamicRoutes(menuList);
defs.forEach((def) => {
let routePath = def.path;
// 已声明真实页面的静态路由(如 /home)→ 跳过动态注册,由静态路由渲染
if (hasStaticPage(routePath)) return;
// 与静态路由同名但未声明真实页面(如后端的「用户列表 /user」与认证 /user)
// → 动态侧改用 /user-list,避免被静态 /user 拦截而跳去登录页
if (staticPaths.has(routePath)) {
routePath = `${routePath}-list`;
}
routes[def.id] = {
path: routePath,
name: def.name,
...(def.icon ? { icon: resolveMenuIcon(def.icon) } : {}),
...(def.redirect ? { redirect: def.redirect } : {}),
parentId: def.parentId,
id: def.id,
};
// 未声明真实页面的菜单路径渲染 DynamicMenu(404 占位页)
routeComponents[def.id] =
def.isDirectory || def.redirect ? EmptyRouteLazy : DynamicMenuLazy;
});
const firstPath = getFirstMenuPath(menuList);
if (firstPath) {
const rootRoute = Object.values(routes).find(
(route: any) => route.path === '/' && route.redirect,
);
if (rootRoute) {
rootRoute.redirect = firstPath;
}
}
}
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: any;
loading?: boolean;
fetchUserInfo?: () => Promise<any>;
settingDrawerOpen?: boolean;
}> {
// 定义从本地存储读取用户信息的函数(替代请求后端)
const fetchUserInfo = async () => {
try {
const userInfoStr = localStorage.getItem('userInfo');
if (userInfoStr) {
return JSON.parse(userInfoStr);
}
} catch (e) {
console.error('获取本地用户信息失败:', e);
}
return undefined;
};
const { location } = history;
const token = localStorage.getItem('usertoken');
// 如果不在公开页面(登录/注册页)
if (
![loginPath, '/user/register', '/user/register-result'].includes(
location.pathname,
)
) {
// 没 Token 则重定向去登录页
if (!token) {
history.replace(
`${loginPath}?redirect=${encodeURIComponent(location.pathname + location.search + location.hash)}`,
);
return {
fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>,
settingDrawerOpen: false,
};
}
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
settingDrawerOpen: false,
};
}
return {
fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>,
settingDrawerOpen: false,
};
}
// ProLayout 全局布局配置
export const layout: RunTimeLayoutConfig = ({
initialState,
setInitialState,
}) => {
// 获取已初始化的当前用户信息(来源于本地缓存)
const currentUser = initialState?.currentUser;
// 处理头像地址拼接:如果是相对路径,则拼接上真实后端域名
const avatarSrc = currentUser?.avatar
? currentUser.avatar.startsWith('http')
? currentUser.avatar
: `接口地址${currentUser.avatar}`
: '默认头像';
return {
menuItemRender: (item, dom) => {
if (item.path) {
return (
<Link to={item.path} prefetch>
{dom}
</Link>
);
}
return dom;
},
actionsRender: () => [
<DocLink key="doc" />,
<VersionDropdown key="version" />,
<LangDropdown key="lang" />,
],
avatarProps: {
// 使用处理好的完整头像路径
src: avatarSrc,
// 优先显示 nickname,其次是 username,最后兜底 '管理员'
title: currentUser?.nickname || currentUser?.username || '管理员',
render: (_, avatarChildren) => (
<AvatarDropdown>{avatarChildren}</AvatarDropdown>
),
},
footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
const token = localStorage.getItem('usertoken');
// 路由切换时拦截:未登录且不在登录页则强行跳转
if (!token && location.pathname !== loginPath) {
history.replace(
`${loginPath}?redirect=${encodeURIComponent(location.pathname + location.search + location.hash)}`,
);
}
},
links: isDev
? [
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
<LinkOutlined />
<span>OpenAPI 文档</span>
</Link>,
]
: [],
ErrorBoundary,
childrenRender: (children) => {
return (
<>
{children}
<SettingDrawer
disableUrlParams
enableDarkTheme
collapse={initialState?.settingDrawerOpen}
onCollapseChange={(open) => {
setInitialState((s) => ({
...s,
settingDrawerOpen: open,
}));
}}
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((s) => ({
...s,
settings,
}));
}}
/>
</>
);
},
...initialState?.settings,
};
};
export const request: RequestConfig = {
baseURL: isDev ? '' : 'https://pro-api.ant-design-demo.workers.dev', // 如果要发布生产环境,请修改为你的后端域名
...errorConfig,
};
export function rootContainer(container: React.ReactNode) {
return (
<>
<OfflineBanner />
<ErrorBoundary>{container}</ErrorBoundary>
</>
);
}
5. 创建动态路由页面
位置:src/utils/menu.ts
修改:
/**
* 动态菜单工具
*
* 后端登录接口返回的 menus 是一份扁平列表(通过 pid 关联父子关系):
* {
* id: 11,
* pid: 10, // 父级 id,0 表示顶级菜单
* title: '菜单管理',
* path: 'menu', // 相对路径段,需拼接父级路径得到完整路径 /public/menu
* icon: 'iconfont icon-xxx',
* sort: 0,
* }
*
* 本模块负责:
* 1. 把扁平列表转换成树形结构
* 2. 由树形结构生成 Umi 动态路由配置(供 app.tsx 的 patchRoutes 使用)
* 3. 提供按路径查找菜单节点等辅助函数
*/
/** 后端菜单原始结构 */
export interface BackendMenu {
id: number;
pid: number;
title: string;
path: string;
icon?: string;
sort?: number;
[key: string]: any;
}
/** 树形菜单节点 */
export interface MenuTreeNode extends BackendMenu {
/** 完整路由路径,如 /public/menu */
fullPath: string;
children: MenuTreeNode[];
}
/** 动态路由定义(写入 routes / routeComponents 前的中间结构) */
export interface DynamicRouteDef {
/** 路由 id,唯一,如 menu-11 */
id: string;
/** 完整路径 */
path: string;
/** 菜单标题 */
name: string;
icon?: string;
/** 父级路由 id,顶级菜单为布局路由 id */
parentId: string;
/** 目录节点:自身路径重定向到第一个可点击子页 */
redirect?: string;
/** 是否为目录(有子菜单,需要 Outlet 渲染子路由) */
isDirectory?: boolean;
}
/** 布局路由 id(Umi plugin-layout 自动生成,动态路由挂在它下面才会出现在侧边菜单) */
export const LAYOUT_ROUTE_ID = 'ant-design-pro-layout';
/** 菜单数据在 localStorage 中的存储键 */
export const MENU_LIST_KEY = 'menuList';
/**
* 将扁平菜单列表转换为树形结构,并计算每个节点的完整路径
*/
export function buildMenuTree(menus: BackendMenu[]): MenuTreeNode[] {
// 只按 sort 稳定排序;sort 相同时保留后端返回顺序(Array.sort 是稳定的),
// 避免按 id 二次排序打乱后端配置好的子菜单顺序
const sorted = [...menus].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
const map = new Map<number, MenuTreeNode>();
sorted.forEach((menu) => {
map.set(menu.id, { ...menu, fullPath: '', children: [] });
});
const roots: MenuTreeNode[] = [];
// Map 按插入顺序迭代,即上面按 sort/id 排序后的顺序
map.forEach((node) => {
if (node.pid === 0) {
roots.push(node);
return;
}
const parent = map.get(node.pid);
if (parent) {
parent.children.push(node);
} else {
// 父级不存在(脏数据),降级为顶级节点
roots.push(node);
}
});
const assignPath = (node: MenuTreeNode, parentPath: string) => {
node.fullPath = `${parentPath}/${node.path}`.replace(/\/+/g, '/');
node.children.forEach((child) => {
assignPath(child, node.fullPath);
});
};
roots.forEach((node) => {
assignPath(node, '');
});
return roots;
}
/**
* 读取登录成功后存储到 localStorage 的菜单列表
*/
export function getMenuListFromStorage(): BackendMenu[] {
try {
const raw = localStorage.getItem(MENU_LIST_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
console.error('读取本地菜单数据失败:', error);
return [];
}
}
/**
* 获取第一个可点击(叶子)菜单的完整路径,作为登录后默认跳转目标
*/
export function getFirstMenuPath(menus: BackendMenu[]): string | null {
const tree = buildMenuTree(menus);
const findFirstLeaf = (nodes: MenuTreeNode[]): string | null => {
for (const node of nodes) {
if (node.children.length > 0) {
const childPath = findFirstLeaf(node.children);
if (childPath) return childPath;
} else {
return node.fullPath;
}
}
return null;
};
return findFirstLeaf(tree);
}
/**
* 由菜单树生成动态路由定义列表
*
* 目录节点(有 children)生成两条路由:
* 1. 目录本身,作为侧边菜单的子菜单分组
* 2. 一条与目录同路径的 redirect,保证直接访问目录路径时能跳转到第一个子页面
* 叶子节点生成一条普通路由,渲染 DynamicMenu 占位页面。
*/
export function buildDynamicRoutes(menus: BackendMenu[]): DynamicRouteDef[] {
const tree = buildMenuTree(menus);
const defs: DynamicRouteDef[] = [];
const firstLeafPath = (node: MenuTreeNode): string | null => {
if (node.children.length === 0) return node.fullPath;
return firstLeafPath(node.children[0]);
};
const walk = (nodes: MenuTreeNode[], parentRouteId: string) => {
nodes.forEach((node) => {
const routeId = `menu-${node.id}`;
if (node.children.length > 0) {
defs.push({
id: routeId,
path: node.fullPath,
name: node.title,
icon: node.icon,
parentId: parentRouteId,
isDirectory: true,
});
const firstPath = firstLeafPath(node);
if (firstPath) {
// redirect 路由不设置 name,ProLayout 会把它从菜单中过滤掉,
// 避免与目录本身在侧边菜单中重复展示
defs.push({
id: `${routeId}-redirect`,
path: node.fullPath,
name: '',
icon: undefined,
parentId: routeId,
redirect: firstPath,
isDirectory: true,
});
}
walk(node.children, routeId);
} else {
defs.push({
id: routeId,
path: node.fullPath,
name: node.title,
icon: node.icon,
parentId: parentRouteId,
});
}
});
};
walk(tree, LAYOUT_ROUTE_ID);
return defs;
}
/** 按完整路径查找菜单节点,返回节点与父级链(从根到直接父级) */
export function findMenuNodeByPath(
nodes: MenuTreeNode[],
targetPath: string,
): { node: MenuTreeNode; parents: MenuTreeNode[] } | null {
for (const node of nodes) {
if (node.fullPath === targetPath) {
return { node, parents: [] };
}
if (node.children.length > 0) {
const found = findMenuNodeByPath(node.children, targetPath);
if (found) {
return { node: found.node, parents: [node, ...found.parents] };
}
}
}
return null;
}
6.修改动态路由效果
位置:my-project/config/routes.ts
修改:
/**
* @name umi 的路由配置
* @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
* @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
* @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。
* @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。
* @param redirect 配置路由跳转
* @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验
* @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题
* @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
* @doc https://umijs.org/docs/guides/routes
*
* 说明:
* 1. 左侧树形菜单由后端登录接口返回的 menus 动态注册(见 app.tsx 的 patchRoutes),
* 菜单中没有真实页面的路径会渲染 404 占位页(DynamicMenu)。
* 2. 要给某个菜单添加真实页面:在这里声明一条与菜单完整路径一致的路由即可
* (如「首页」菜单路径为 /home,下面已声明),patchRoutes 会自动跳过动态注册,
* 让这条静态路由渲染真实页面并出现在侧边菜单对应位置。
* 3. 后端的「用户列表」菜单路径也是 /user,与认证路由同名;patchRoutes 会
* 自动把它映射到 /user-list,避免与认证路由冲突。
*/
export default [
{
path: '/user',
layout: false,
routes: [
{
path: '/user/login',
name: 'login',
component: './user/login',
},
{
path: '/user',
redirect: '/user/login',
},
{
name: 'register-result',
icon: 'checkCircle',
path: '/user/register-result',
component: './user/register-result',
},
{
name: 'register',
icon: 'userAdd',
path: '/user/register',
component: './user/register',
},
{
name: '404',
component: './exception/404',
path: '/user/*',
},
],
},
{
path: '/home',
name: '首页',
icon: 'home',
component: './Home',
},
{
path: '/',
redirect: '/home',
},
];
7.退出登录修改效果
位置:src/RightContent/AvatarDropdown.tsx
修改:
import {
LogoutOutlined,
SettingOutlined,
SkinOutlined,
} from '@ant-design/icons';
import { history, useModel } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Spin } from 'antd';
import React from 'react';
import HeaderDropdown from '../HeaderDropdown';
export type GlobalHeaderRightProps = {
children?: React.ReactNode;
};
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
children,
}) => {
const { initialState, setInitialState } = useModel('@@initialState');
// 退出登录逻辑:清理接口、所有本地缓存,并强制跳转登录页
const loginOut = () => {
// 1. 清空所有的本地持久化存储和会话存储数据
localStorage.clear();
sessionStorage.clear();
// 2. 构建包含当前页面重定向参数的跳转地址
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
const searchParams = new URLSearchParams({
redirect: pathname + search,
});
const redirect = urlParams.get('redirect');
// 3. 重定向去登录页
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: searchParams.toString(),
});
}
};
const onMenuClick: MenuProps['onClick'] = (event) => {
const { key } = event;
if (key === 'logout') {
// 清空 Umi 全局 currentUser 状态
setInitialState((s) => ({ ...s, currentUser: undefined }));
// 执行退出并清理存储
loginOut();
return;
}
if (key === 'theme') {
setInitialState((s) => ({ ...s, settingDrawerOpen: true }));
return;
}
history.push(`/account/${key}`);
};
if (!initialState) {
return <Spin size="small" />;
}
const { currentUser } = initialState;
if (!currentUser) {
return <Spin size="small" />;
}
const menuItems: MenuProps['items'] = [
{
key: 'settings',
icon: <SettingOutlined />,
label: '个人设置',
},
{
key: 'theme',
icon: <SkinOutlined />,
label: '主题设置',
},
{
type: 'divider' as const,
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
},
];
return (
<HeaderDropdown
placement="bottomRight"
menu={{
selectedKeys: [],
onClick: onMenuClick,
items: menuItems,
}}
arrow
>
{children}
</HeaderDropdown>
);
};