使用vite+react+ts+Ant Design开发后台管理项目(五)
前言
本文将引导开发者从零基础开始,运用vite、react、react-router、react-redux、Ant Design、less、tailwindcss、axios等前沿技术栈,构建一个高效、响应式的后台管理系统。通过详细的步骤和实践指导,文章旨在为开发者揭示如何利用这些技术工具,从项目构思到最终实现的全过程,提供清晰的开发思路和实用的技术应用技巧。
项目gitee地址:lbking666666/enqi-admin
本系列文章:
- 使用vite+react+ts+Ant Design开发后台管理项目(一)
- 使用vite+react+ts+Ant Design开发后台管理项目(二)
- 使用vite+react+ts+Ant Design开发后台管理项目(三)
- 使用vite+react+ts+Ant Design开发后台管理项目(四)
- 使用vite+react+ts+Ant Design开发后台管理项目(五)
最近比较忙更新比较慢本章节添加面包屑和tab标签页及一些优化,下一章节系统管理下的用户管理和角色管理
状态管理
在store文件夹下的reducers下新增menu.ts文件,记录左侧菜单展开和点击及所有已经打开的状态
代码如下:
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类型定义代码如下:
// 菜单项属性
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
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代码如下:
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代码如下:
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的方法封装到这里代码如下:
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所需数据格式的组装
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组件根据状态中存储的所有打开的页面显示和添加关闭操作
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文件
export interface color {
name:string;
value:string;
}
在src文件夹下新增utils文件夹,创建文件color.ts
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文件
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文件修改
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组件文件修改
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组件文件修改
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文件修改
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;