react动态路由内容修改

创建指令: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>
  );
};
相关推荐
起床学FPGA3 小时前
AI回答问题之后,会自动跳到最下面的问题
开发语言·javascript·ecmascript
Fluxart.ai3 小时前
电商商品图审核怎么自动化?规则引擎、人工复核与发布门禁
java·前端·自动化
why技术3 小时前
AI 写的文章,可能都带着手敲一遍都去不掉的“隐形水印”。
前端·人工智能·后端
愚公搬代码4 小时前
【愚公系列】《Android应用案例开发大全》016-LBS类应用掌上杭州(辅助工具类的开发)
android·前端
CodeSheep4 小时前
又一个华为天才少年,离职了!
前端·后端·程序员
kyriewen4 小时前
面试官说"打开你的AI工具"——我才发现,他考的根本不是写代码
前端·人工智能·面试
IT_陈寒5 小时前
Vue的双向绑定把我坑惨了,原来这个场景不能用
前端·人工智能·后端
hunterandroid5 小时前
[Android 从零到一] Compose LazyColumn 性能优化:key、稳定性与重组治理
android·前端
lichenyang4536 小时前
从一次团队邀请开始:用 React、NestJS 与 Socket.IO 做可靠的实时通知
前端
vtian6 小时前
一篇文章吃透 Monorepo:pnpm + Turborepo + Changesets 全流程实战(含 8 个踩坑)
前端