使用vite+react+ts+Ant Design开发后台管理项目(五)

前言

本文将引导开发者从零基础开始,运用vite、react、react-router、react-redux、Ant Design、less、tailwindcss、axios等前沿技术栈,构建一个高效、响应式的后台管理系统。通过详细的步骤和实践指导,文章旨在为开发者揭示如何利用这些技术工具,从项目构思到最终实现的全过程,提供清晰的开发思路和实用的技术应用技巧。

项目gitee地址:lbking666666/enqi-admin

本系列文章:

最近比较忙更新比较慢本章节添加面包屑和tab标签页及一些优化,下一章节系统管理下的用户管理和角色管理

状态管理

在store文件夹下的reducers下新增menu.ts文件,记录左侧菜单展开和点击及所有已经打开的状态

代码如下:

javascript 复制代码
import { createSlice } from "@reduxjs/toolkit";
import type { RootState } from "@/store/index.ts";
import type { MenuState, OpenedMenu } from "@/types/menu.ts";

const initialState: MenuState = {
  openMenuKey: [], // 展开的菜单栏的key  用于侧边栏
  selectMenuKey: [], // 选中菜单栏的key  用户侧边栏
  openedMenu: [], // 保存已经打开的菜单栏 用于顶部导航
  currentPath: "", // 页面当前路径
};
export const menuSlice = createSlice({
  name: "menu",
  initialState,
  reducers: {
    setOpenKey(state, action) {
      const oldKeys = state.openMenuKey;
      const keys = action.payload;
      const isSame = keys.every(
        (item: string, index: number) => item === oldKeys[index]
      );
      const flag = keys.length === oldKeys.length && isSame;
      if (flag) {
        return state;
      }
      return { ...state, openMenuKey: keys };
    },
    setCurrent(state, action) {
      const keys = action.payload;
      if (state.selectMenuKey[0] === keys[0]) {
        return state;
      }
      const openedMenu = [...state.openedMenu];
      const useCurrentPath = openedMenu.find(
        (item: OpenedMenu) => item.key === keys[0]
      );
      return {
        ...state,
        selectMenuKey: keys,
        currentPath: useCurrentPath?.path || "/",
      };
    },

    addMenu(state, action) {
      const menuItem = action.payload;
      if (state.openedMenu.find((item) => item.path === menuItem.path)) {
        return state;
      } else {
        const openedMenu = [...state.openedMenu];
        const currentPath = menuItem.path;
        openedMenu.push(menuItem);
        return { ...state, openedMenu, currentPath };
      }
    },
    removeMenu(state, action) {
      const keys = action.payload;
      const openedMenu = state.openedMenu.filter((i) => !keys.includes(i.key));
      const currentPath =
        openedMenu.length > 0 ? openedMenu[openedMenu.length - 1].path : "/";
      if (state.openedMenu.length === openedMenu.length) {
        return state;
      }
      return { ...state, openedMenu, currentPath };
    },
    clearMenu(state) {
      const currentPath = "";
      const openedMenu: OpenedMenu[] = [];
      return { ...state, openedMenu, currentPath };
    },
  },
});

export const { setCurrent, setOpenKey, addMenu, removeMenu, clearMenu } =
  menuSlice.actions;
export const selectOpenKey = (state: RootState) => state.menu.openMenuKey;
export const selectMenu = (state: RootState) => state.menu.selectMenuKey;
export const selectOpenedMenu = (state: RootState) => state.menu.openedMenu;
export const selectCurrentPath = (state: RootState) => state.menu.currentPath;
export default menuSlice.reducer;

types文件夹下新增menu.d.ts类型定义代码如下:

javascript 复制代码
// 菜单项属性
export interface MenuItemProps {
  id?: string;
  key: string;
  icon?: string;
  label: string;
  children?: MenuItemProps[];
}
export interface OpenedMenu {
  key: string
  path: string
  title: string
}
// 菜单状态属性
export interface MenuState {
  openedMenu: OpenedMenu[]
  openMenuKey: string[]
  selectMenuKey: string[]
  currentPath: string
}
export interface MenuItem {
  [MENU_ICON]: string | null
  [MENU_KEEPALIVE]: string
  [MENU_KEY]: string | number
  [MENU_ORDER]?: number
  [MENU_PARENTKEY]: number | null
  [MENU_PATH]: string
  [MENU_TITLE]: string
  [MENU_CHILDREN]?: MenuList
  [MENU_PARENTPATH]?: string
  [MENU_SHOW]?: boolean | string
  [key: string]: any
}

在store文件夹中的index.ts中引入menu

javascript 复制代码
import { configureStore } from "@reduxjs/toolkit";
import globalReducer from "./reducers/global";
import menuReducer from "./reducers/menu";
//处理eslint报错
/* eslint-disable @typescript-eslint/no-unused-vars */
const store = configureStore({
  reducer: {
    global: globalReducer,
    menu: menuReducer,
  },
});

// 从 store 本身推断 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>;
// 推断类型:{posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

export default store;

hook管理

因为之前的使用的文件名称是UseGlobal.hooks.ts这个并不是设置全局配置的有点歧义,把原来UseGlobal.hooks.ts的内容复制一份放入到新建一个UseStore.hooks.ts文件

把之前组件中使用到UseGlobal.hooks.ts的地方抽离到hook中

UseGlobal.hooks.ts代码如下:

javascript 复制代码
import { useAppSelector, useAppDispatch } from "@/hooks/UseStore.hooks";
import { useCallback } from "react";
import {
  setCollapsed,
  selectCollapsed,
  selectShowSetting,
  setShowSetting,
  selectColorPrimary,
  selectIsDark,
  selectIsRadius,
  setIsDark,
  setColorPrimary,
  setIsRadius,
} from "@/store/reducers/global";

//获取当前菜单栏是否折叠状态
export const useIsCollapsed = () => useAppSelector(selectCollapsed);
//获取当前弹窗是否显示状态
export const useShowPoup = () => useAppSelector(selectShowSetting);
//获取当前主题颜色的值
export const useCurColor = () => useAppSelector(selectColorPrimary);
//获取当前主题是否是暗黑模式
export const useIsSelectdDark = () => useAppSelector(selectIsDark);
//获取当前主题是否是圆角
export const useIsSelectdRadius = () => useAppSelector(selectIsRadius);

export const useDispatchGlobal = () => {
  const dispatch = useAppDispatch();

  // 更改菜单栏的折叠状态
  const stateHandleCollapsed = useCallback(() => {
    dispatch(setCollapsed());
  }, [dispatch]);

  // 更新主题颜色
  const stateHandleColorPrimary = useCallback(
    (color: string) => {
      dispatch(setColorPrimary(color));
    },
    [dispatch]
  );

  // 切换主题是否是暗黑模式
  const stateHandleIsDark = useCallback(
    () => {
      dispatch(setIsDark());
    },
    [dispatch]
  );

  // 切换主题是否是圆角
  const stateHandleIsRadius = useCallback(
    () => {
      dispatch(setIsRadius());
    },
    [dispatch]
  );

  // 更新是否显示设置弹窗
  const stateHandleShowPopup = useCallback(
    (isShow: boolean) => {
      dispatch(setShowSetting(isShow));
    },
    [dispatch]
  );

  return {
    stateHandleCollapsed,
    stateHandleColorPrimary,
    stateHandleIsDark,
    stateHandleIsRadius,
    stateHandleShowPopup,
  };
};

UseStore.hooks.ts代码如下:

javascript 复制代码
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '@/store/index';

// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

面包屑和顶部tab的hook

hooks文件夹下新增UseMenu.hooks.ts文件把状态及hook的方法封装到这里代码如下:

javascript 复制代码
import { useAppSelector, useAppDispatch } from "@/hooks/UseStore.hooks";
import { useCallback } from "react";
import type { OpenedMenu } from "@/types/menu.ts";
import {
  selectOpenKey,
  selectMenu,
  selectOpenedMenu,
  selectCurrentPath,
  setOpenKey,
  setCurrent,
  addMenu,
  removeMenu,
  clearMenu,
} from "@/store/reducers/menu";

//获取当前菜单展开的key
export const useOpenKey = () => useAppSelector(selectOpenKey);

//获取当前菜单
export const useMenu = () => useAppSelector(selectMenu);

//获取当前菜单列表
export const useOpenedMenu = () => useAppSelector(selectOpenedMenu);

//获取当前路径
export const useCurrentPath = () => useAppSelector(selectCurrentPath);

export const useDispatchMenu = () => {
  const dispatch = useAppDispatch();
  //修改菜单展开的key
  const stateChangeOpenKey = useCallback(
    (menu: string[]) => {
      dispatch(setOpenKey(menu));
    },
    [dispatch]
  );
  //修改当前菜单
  const stateChangeCurrent = useCallback(
    (menu: string[]) => {
      dispatch(setCurrent(menu));
    },
    [dispatch]
  );
  //添加菜单
  const stateAddMenu = useCallback(
    (menu: OpenedMenu) => {
      dispatch(addMenu(menu));
    },
    [dispatch]
  );
  //删除菜单
  const stateRemoveMenu = useCallback(
    (menu: string) => {
      dispatch(removeMenu(menu));
    },
    [dispatch]
  );
  //清空菜单
  const stateClearMenu = useCallback(() => {
    dispatch(clearMenu());
  }, [dispatch]);
  return {
    stateChangeOpenKey,
    stateChangeCurrent,
    stateAddMenu,
    stateRemoveMenu,
    stateClearMenu,
  };
};

面包屑和顶部tab标签

面包屑

修改header.tsx文件使用antd的组件Breadcurmb,根据当前useCurrentPath获取到当前url的路由地址进行分隔和组装Breadcurmb所需数据格式的组装

javascript 复制代码
import React from "react";
import { Button, Layout, theme, Flex, Breadcrumb } from "antd";
import {
  HomeOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  SettingOutlined,
} from "@ant-design/icons";
import { useShowPoup, useDispatchGlobal } from "@/hooks/UseGlobal.hooks";
import { useCurrentPath } from "@/hooks/UseMenu.hooks";
import Setting from "./setting";
import { MenuItemProps } from "@/types/menu";
import { NavigateFunction } from "react-router-dom";
const { Header } = Layout;
interface AppSiderProps {
  menu: MenuItemProps[];
  collapsed: boolean;
  navigate:NavigateFunction;
}
const setMenuData = (arr: MenuItemProps[], keys: string[]) => {
  const menuList:Array<{title:React.ReactNode,href?:string}> = [{ title: <HomeOutlined />, href: "/" }];
  const subKey = keys[0];
  const key = keys[1];
  arr.forEach((item) => {
    if (item.key === subKey) {
      menuList.push({ title: <> {item.label} </> });
      if (item.children) {
        item.children.forEach((child) => {
          if (child.key === key) {
            menuList.push({ title: <> {child.label} </> });
          }
        });
      }
    }
  });
  return menuList;
};
const AppHeader: React.FC<AppSiderProps> = ({ menu, collapsed,navigate }) => {
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const showPoup: boolean = useShowPoup();
  const { stateHandleShowPopup, stateHandleCollapsed } = useDispatchGlobal();
  const currentMenu = useCurrentPath();
  const menuList = setMenuData(
    JSON.parse(JSON.stringify(menu)),
    currentMenu.split("/")
  );
  const handleLink = (item: { href?: string }) => () => {
    if (item.href) {
      navigate(item.href)
    }
  };
  console.log(menu, currentMenu, menuList, "menu");
  //设置按钮点击事件
  return (
    <Header style={{ padding: 0, background: colorBgContainer }}>
      <Flex gap="middle" justify="space-between" align="center">
        <Flex justify="space-between" align="center">
          <Button
            type="text"
            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
            style={{
              fontSize: "16px",
              width: 64,
              height: 64,
            }}
            onClick={stateHandleCollapsed}
          />
          <Breadcrumb
            items={menuList}
            itemRender={(item) => <span onClick={handleLink(item)}>{item.title}</span>}
          />
        </Flex>

        <Button
          type="primary"
          className="mr-4"
          icon={<SettingOutlined />}
          onClick={() => {
            stateHandleShowPopup(true);
          }}
        />
      </Flex>
      <Setting showPoup={showPoup} />
    </Header>
  );
};
export default AppHeader;

tab标签

修改main.tsx文件,使用antd的Tag组件根据状态中存储的所有打开的页面显示和添加关闭操作

javascript 复制代码
import { Layout, theme, Flex, Divider, Tag } from "antd";
import { useCallback, useEffect } from "react";
import {
  useOpenedMenu,
  useCurrentPath,
  useDispatchMenu,
} from "@/hooks/UseMenu.hooks";
import { useIsCollapsed } from "@/hooks/UseGlobal.hooks";
import { NavigateFunction, Outlet } from "react-router-dom";
import type { OpenedMenu } from "@/types/menu.ts";
import { HomeOutlined } from "@ant-design/icons";
const { Content } = Layout;
interface AppMainProps {
  pathname: string;
  navigate: NavigateFunction;
}
const homeTag: OpenedMenu = {
  key: "home",
  path: "/",
  title: "首页",
};
//获取路径的层级
const getPathParts = (path: string): string[] =>
  path.replace("/", "").split("/");
const AppMain: React.FC<AppMainProps> = ({ pathname, navigate }) => {
  const {
    token: { colorBgContainer, borderRadiusLG,colorPrimaryBg, colorPrimary },
  } = theme.useToken();

  const tabList = useOpenedMenu();
  const currentMenu = useCurrentPath();
  const isCIsCollapsed = useIsCollapsed();
  const { stateChangeOpenKey, stateChangeCurrent, stateRemoveMenu } =
    useDispatchMenu();

  // 点击tab时,更新路径状态

  const handleTabClick = (item: OpenedMenu) => {
    navigate(item.path || "/");
    stateChangeCurrent([item.key]);
  };
  const handleTabClose = (key: string) => {
    stateRemoveMenu(key);
    // 关闭当前tab,并打开上一个
    const tabMenu = tabList.filter((i) => !key.includes(i.key));

    if (tabMenu.length === 0) {
      navigate("/");
      stateChangeCurrent(["home"]);
      return;
    }
    const item = tabMenu[tabMenu.length - 1];
    navigate(item.path || "/");
    stateChangeCurrent([item.key]);
  };

  // 路径变化时,更新菜单状态
  const onPathChange = useCallback(() => {
    const parts = getPathParts(pathname);
    stateChangeOpenKey([parts[0]]);
    stateChangeCurrent([parts[1] || "home"]);
  }, [pathname, stateChangeOpenKey, stateChangeCurrent]);

  // 菜单展开/收起时,更新路径状态
  useEffect(() => {
    onPathChange();
  }, [pathname, isCIsCollapsed, onPathChange]);
  return (
    <>
      <Divider style={{ margin: 0 }} />
      <Flex
        gap="0"
        justify="flex-start"
        className="bg-white  pl-5 pr-5"
        align="center"
      >
        <>
          <Tag
            bordered={false}
            icon={<HomeOutlined />}
            onClick={() => handleTabClick(homeTag)}
            style={{
              background: "/" == currentMenu ? colorPrimaryBg : "transparent",
              color: "/" == currentMenu ? colorPrimary : "rgba(0, 0, 0, 0.88)",
            }}
            className="cursor-pointer flex items-center pt-2  pb-2 pl-4 pr-4 text-base rounded-b-none"
          >
            <span className="mr-1">{homeTag.title}</span>
          </Tag>
        </>

        {tabList.map<React.ReactNode>((item) => {
          return (
            <Tag
              bordered={false}
              onClick={() => {
                handleTabClick(item);
              }}
              key={item.key}
              closable
              style={{
                background: item.path == currentMenu ? colorPrimaryBg : "transparent",
                color:
                  item.path == currentMenu ? colorPrimary : "rgba(0, 0, 0, 0.88)",
              }}
              onClose={() => handleTabClose(item.key)}
              className="cursor-pointer flex items-center pt-2 pb-2 pl-4 text-base rounded-b-none"
            >
              <span className="mr-1">{item.title}</span>
            </Tag>
          );
        })}
      </Flex>
      <Content
        style={{
          margin: "24px 16px",
          padding: 24,
          minHeight: 280,
          background: colorBgContainer,
          borderRadius: borderRadiusLG,
        }}
      >
        <Outlet />
      </Content>
    </>
  );
};
export default AppMain;

效果如下

优化

对已经完成的部分做一些代码的抽离和封装

1.主题颜色抽离及设置hook方法

types文件夹下新增color.d.ts文件

javascript 复制代码
export interface color {
    name:string;
    value:string;
}

在src文件夹下新增utils文件夹,创建文件color.ts

javascript 复制代码
export const colors = [
  {
    name: "拂晓蓝",
    value: "#1677ff",
  },
  {
    name: "薄暮",
    value: "#5f80c7",
  },
  {
    name: "日暮",
    value: "#faad14",
  },
  {
    name: "火山",
    value: "#f5686f",
  },
  {
    name: "酱紫",
    value: "#9266f9",
  },
  {
    name: "极光绿",
    value: "#3c9",
  },
  {
    name: "极客蓝",
    value: "#32a2d4",
  },
];

修改setting.tsx文件

javascript 复制代码
import React from "react";
import { Button, Flex, Drawer, Space, Switch } from "antd";
import { CloseOutlined, CheckOutlined } from "@ant-design/icons";
import {
  useCurColor,
  useIsSelectdDark,
  useIsSelectdRadius,
  useDispatchGlobal,
} from "@/hooks/UseGlobal.hooks";
import { colors } from "@/utils/color";
type AppSiderProps = {
  showPoup: boolean;
};
const Setting: React.FC<AppSiderProps> = ({ showPoup }) => {
  //主题颜色
  const curColor: string = useCurColor();
  //暗黑模式
  const isSelectdDark: boolean = useIsSelectdDark();
  //圆角模式
  const isSelectdRadius: boolean = useIsSelectdRadius();
  const {
    stateHandleColorPrimary,
    stateHandleIsDark,
    stateHandleIsRadius,
    stateHandleShowPopup,
  } = useDispatchGlobal();
  const ColorItem: React.FC<{ color: string; isSelectd: boolean }> = ({
    color,
    isSelectd,
  }) => {
    if (isSelectd) {
      return (
        <div
          className="w-6 h-6 flex justify-center items-center  rounded cursor-pointer items"
          style={{ background: color }}
        >
          <CheckOutlined style={{ color: "#fff" }} />
        </div>
      );
    } else {
      return (
        <div
          className="w-6 h-6 flex justify-center items-center  rounded cursor-pointer items"
          style={{ background: color }}
          onClick={() => stateHandleColorPrimary(color)}
        ></div>
      );
    }
  };
  return (
    <Drawer
      title="设置"
      width={300}
      closeIcon={false}
      open={showPoup}
      extra={
        <Space>
          <Button
            type="text"
            onClick={() => {
              stateHandleShowPopup(false);
            }}
            icon={<CloseOutlined />}
          ></Button>
        </Space>
      }
    >
      <div className="mb-3 font-bold">主题颜色</div>
      <Flex gap="middle" justify="space-between" align="center">
        {colors.map((item) => (
          <ColorItem
            key={item.value}
            color={item.value}
            isSelectd={curColor == item.value}
          />
        ))}
      </Flex>
      <div className="mb-3 mt-3 font-bold">主题模式</div>
      <div className="flex justify-between mb-3">
        <div className="flex gap-2">
          <span>开启暗黑模式</span>
        </div>
        <div className="flex gap-2">
          <Switch
            defaultChecked
            checked={isSelectdDark}
            onChange={stateHandleIsDark}
          />
        </div>
      </div>
      <div className="flex justify-between">
        <div className="flex gap-2">
          <span>开启圆角主题</span>
        </div>
        <div className="flex gap-2">
          <Switch
            defaultChecked
            checked={isSelectdRadius}
            onChange={stateHandleIsRadius}
          />
        </div>
      </div>
    </Drawer>
  );
};

export default Setting;

2.布局组件hooks抽离和封装

  • layout文件夹下的index.tsx文件修改
javascript 复制代码
import React, { useEffect, useState } from "react";
import { Layout, ConfigProvider, theme } from "antd";
import { useNavigate, useLocation } from "react-router-dom";
import {
  useIsCollapsed,
  useCurColor,
  useIsSelectdDark,
  useIsSelectdRadius,
} from "@/hooks/UseGlobal.hooks";
import AppHeader from "./header";
import AppSider from "./sider";
import AppMain from "./main";
import { MenuItemProps } from "@/types/menu";
import { getMenu } from "@/api/menu";
const App: React.FC = () => {
  const collapsed: boolean = useIsCollapsed();
  const isDark: boolean = useIsSelectdDark();
  const isRadius: boolean = useIsSelectdRadius();
  const themeColor: string = useCurColor();
  // 菜单数据
  const [menu, setMenu] = useState([] as MenuItemProps[]);
  const { pathname } = useLocation();
  const navigate = useNavigate();

  // 获取菜单数据
  useEffect(() => {
    // 获取菜单数据
    const getData = async () => {
      const res = await getMenu();
      const menuData = res?.data as MenuItemProps[];
      // 设置菜单数据
      setMenu([...menuData]);
    };
    getData();
  }, []);
  // 简化返回内容的嵌套
  const appLayout = (
    <Layout className="app-layout">
      <AppSider menu={menu} pathname={pathname} navigate={navigate}  collapsed={collapsed} />
      <Layout>
        <AppHeader menu={menu} collapsed={collapsed} navigate={navigate} />
        <AppMain pathname={pathname} navigate={navigate} />
      </Layout>
    </Layout>
  );

  return (
    <ConfigProvider
      theme={{
        token: {
          colorPrimary: themeColor,
          borderRadius: isRadius ? 6 : 0,
          motion: true,
        },
        algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
      }}
    >
      {appLayout}
    </ConfigProvider>
  );
};

export default App;
  • layout文件夹下的main.tsx组件文件修改
javascript 复制代码
import { Layout, theme, Flex, Divider, Tag } from "antd";
import { useCallback, useEffect } from "react";
import {
  useOpenedMenu,
  useCurrentPath,
  useDispatchMenu,
} from "@/hooks/UseMenu.hooks";
import { useIsCollapsed } from "@/hooks/UseGlobal.hooks";
import { NavigateFunction, Outlet } from "react-router-dom";
import type { OpenedMenu } from "@/types/menu.ts";
import { HomeOutlined } from "@ant-design/icons";
const { Content } = Layout;
interface AppMainProps {
  pathname: string;
  navigate: NavigateFunction;
}
const homeTag: OpenedMenu = {
  key: "home",
  path: "/",
  title: "首页",
};
//获取路径的层级
const getPathParts = (path: string): string[] =>
  path.replace("/", "").split("/");
const AppMain: React.FC<AppMainProps> = ({ pathname, navigate }) => {
  const {
    token: { colorBgContainer, borderRadiusLG,colorPrimaryBg, colorPrimary },
  } = theme.useToken();

  const tabList = useOpenedMenu();
  const currentMenu = useCurrentPath();
  const isCIsCollapsed = useIsCollapsed();
  const { stateChangeOpenKey, stateChangeCurrent, stateRemoveMenu } =
    useDispatchMenu();

  // 点击tab时,更新路径状态

  const handleTabClick = (item: OpenedMenu) => {
    navigate(item.path || "/");
    stateChangeCurrent([item.key]);
  };
  const handleTabClose = (key: string) => {
    stateRemoveMenu(key);
    // 关闭当前tab,并打开上一个
    const tabMenu = tabList.filter((i) => !key.includes(i.key));

    if (tabMenu.length === 0) {
      navigate("/");
      stateChangeCurrent(["home"]);
      return;
    }
    const item = tabMenu[tabMenu.length - 1];
    navigate(item.path || "/");
    stateChangeCurrent([item.key]);
  };

  // 路径变化时,更新菜单状态
  const onPathChange = useCallback(() => {
    const parts = getPathParts(pathname);
    stateChangeOpenKey([parts[0]]);
    stateChangeCurrent([parts[1] || "home"]);
  }, [pathname, stateChangeOpenKey, stateChangeCurrent]);

  // 菜单展开/收起时,更新路径状态
  useEffect(() => {
    onPathChange();
  }, [pathname, isCIsCollapsed, onPathChange]);
  return (
    <>
      <Divider style={{ margin: 0 }} />
      <Flex
        gap="0"
        justify="flex-start"
        className="bg-white  pl-5 pr-5"
        align="center"
      >
        <>
          <Tag
            bordered={false}
            icon={<HomeOutlined />}
            onClick={() => handleTabClick(homeTag)}
            style={{
              background: "/" == currentMenu ? colorPrimaryBg : "transparent",
              color: "/" == currentMenu ? colorPrimary : "rgba(0, 0, 0, 0.88)",
            }}
            className="cursor-pointer flex items-center pt-2  pb-2 pl-4 pr-4 text-base rounded-b-none"
          >
            <span className="mr-1">{homeTag.title}</span>
          </Tag>
        </>

        {tabList.map<React.ReactNode>((item) => {
          return (
            <Tag
              bordered={false}
              onClick={() => {
                handleTabClick(item);
              }}
              key={item.key}
              closable
              style={{
                background: item.path == currentMenu ? colorPrimaryBg : "transparent",
                color:
                  item.path == currentMenu ? colorPrimary : "rgba(0, 0, 0, 0.88)",
              }}
              onClose={() => handleTabClose(item.key)}
              className="cursor-pointer flex items-center pt-2 pb-2 pl-4 text-base rounded-b-none"
            >
              <span className="mr-1">{item.title}</span>
            </Tag>
          );
        })}
      </Flex>
      <Content
        style={{
          margin: "24px 16px",
          padding: 24,
          minHeight: 280,
          background: colorBgContainer,
          borderRadius: borderRadiusLG,
        }}
      >
        <Outlet />
      </Content>
    </>
  );
};
export default AppMain;
  • layout文件夹下的menu.tsx组件文件修改
javascript 复制代码
import React, { useCallback, useEffect } from "react";
import { HomeOutlined, SettingOutlined, ShopOutlined } from "@ant-design/icons";
import { Menu } from "antd";
import { MenuItemProps } from "@/types/menu";
import { NavigateFunction } from "react-router-dom";
import { useOpenKey, useMenu, useDispatchMenu } from "@/hooks/UseMenu.hooks";
// 图标映射
const Icons = {
  home: HomeOutlined,
  setting: SettingOutlined,
  shop: ShopOutlined,
};

interface AppMenuProps {
  pathname:string;
  menu:MenuItemProps[];
  navigate:NavigateFunction;
}
// 获取图标组件
const IconByName: React.FC<{ iconName: string }> = ({ iconName }) => {
  // 获取图标组件
  const IconComponent = Icons[iconName as keyof typeof Icons];
  // 返回图标组件
  return IconComponent ? <IconComponent /> : null;
};
// 查找菜单项
const findMenuByKey = (
  arr: MenuItemProps[],
  key: string
): MenuItemProps | undefined => {
  for (const item of arr) {
    if (item.key === key) {
      return item;
    }
    if (item.children) {
      const found = findMenuByKey(item.children, key);
      if (found) {
        return found;
      }
    }
  }
  return undefined;
};
// 获取路径
const getPathParts = (path: string): string[] =>
  path.replace("/", "").split("/");
// 侧边栏
const AppMenu: React.FC<AppMenuProps> = ({ menu,pathname,navigate }) => {
  const openKeys = useOpenKey();
  const currentMenu = useMenu();
  const { stateChangeOpenKey: onOpenChange, stateAddMenu } = useDispatchMenu();
  // 设置当前菜单
  const setTabMenu = useCallback(
    (keyPath: string[]) => {
      const itemMenu: MenuItemProps | undefined = findMenuByKey(
        menu,
        keyPath[1] as string
      );
      if (itemMenu) {
        stateAddMenu({
          key: itemMenu?.key,
          path: keyPath.join("/"),
          title: itemMenu?.label,
        });
      }
    },
    [menu, stateAddMenu]
  );
  // 路由地址变化后设置当前菜单
  useEffect(() => {
    const keyPath = getPathParts(pathname);
    setTabMenu(keyPath);
  }, [pathname, setTabMenu]);

  // 点击菜单项
  const handleMenu = ({ keyPath }: { keyPath: string[] }) => {
    const routerPath: string = keyPath.reverse().join("/");
    setTabMenu(keyPath);
    navigate(routerPath);
  };
  // 使用递归查找匹配的菜单项
  const menuData = menu.map((item: MenuItemProps) => {
    return {
      key: item.key,
      label: item.label,
      icon: item.icon ? <IconByName iconName={item.icon} /> : undefined,
      children: item.children?.map((child) => ({
        key: child.key,
        label: child.label,
      })),
    };
  });
  return (
    <Menu
      onClick={handleMenu}
      theme="dark"
      selectedKeys={currentMenu}
      onOpenChange={onOpenChange}
      openKeys={openKeys}
      mode="inline"
      items={menuData}
    />
  );
};

export default AppMenu;
  • layout文件夹下的sider.tsx文件修改
javascript 复制代码
import React from "react";
import { Layout } from "antd";
import AppMenu from "./menu";
import { MenuItemProps } from "@/types/menu";
import { NavigateFunction } from "react-router-dom";
const { Sider } = Layout;

interface AppSiderProps {
  pathname: string;
  menu: MenuItemProps[];
  navigate: NavigateFunction;
  collapsed: boolean;
}
// 侧边栏
const AppSider: React.FC<AppSiderProps> = ({
  menu,
  collapsed,
  navigate,
  pathname,
}) => {
  // 返回侧边栏
  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="demo-logo-vertical" />
      <AppMenu menu={menu} pathname={pathname} navigate={navigate} />
    </Sider>
  );
};

export default AppSider;
相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-1 小时前
验证码机制
前端·后端
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235243 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试