当前位置: 首页 > news >正文

使用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: stringpath: stringtitle: 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();//修改菜单展开的keyconst 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"><Buttontype="text"icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}style={{fontSize: "16px",width: 64,height: 64,}}onClick={stateHandleCollapsed}/><Breadcrumbitems={menuList}itemRender={(item) => <span onClick={handleLink(item)}>{item.title}</span>}/></Flex><Buttontype="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 }} /><Flexgap="0"justify="flex-start"className="bg-white  pl-5 pr-5"align="center"><><Tagbordered={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 (<Tagbordered={false}onClick={() => {handleTabClick(item);}}key={item.key}closablestyle={{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><Contentstyle={{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 (<divclassName="w-6 h-6 flex justify-center items-center  rounded cursor-pointer items"style={{ background: color }}><CheckOutlined style={{ color: "#fff" }} /></div>);} else {return (<divclassName="w-6 h-6 flex justify-center items-center  rounded cursor-pointer items"style={{ background: color }}onClick={() => stateHandleColorPrimary(color)}></div>);}};return (<Drawertitle="设置"width={300}closeIcon={false}open={showPoup}extra={<Space><Buttontype="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) => (<ColorItemkey={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"><SwitchdefaultCheckedchecked={isSelectdDark}onChange={stateHandleIsDark}/></div></div><div className="flex justify-between"><div className="flex gap-2"><span>开启圆角主题</span></div><div className="flex gap-2"><SwitchdefaultCheckedchecked={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 (<ConfigProvidertheme={{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 }} /><Flexgap="0"justify="flex-start"className="bg-white  pl-5 pr-5"align="center"><><Tagbordered={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 (<Tagbordered={false}onClick={() => {handleTabClick(item);}}key={item.key}closablestyle={{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><Contentstyle={{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 (<MenuonClick={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;

相关文章:

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

前言 本文将引导开发者从零基础开始&#xff0c;运用vite、react、react-router、react-redux、Ant Design、less、tailwindcss、axios等前沿技术栈&#xff0c;构建一个高效、响应式的后台管理系统。通过详细的步骤和实践指导&#xff0c;文章旨在为开发者揭示如何利用这些技术…...

Spring Boot实现多数据源连接和切换

文章目录 前言一、多数据源配置与切换方案二、实现步骤1. 创建多个 DataSource 配置类2. 创建 DataSource 配置类3. 创建动态数据源路由类4. 实现 DynamicDataSource 类5. 创建 DataSourceContextHolder 来存储当前的数据源标识6. AOP 方式切换数据源7. 自定义注解来指定数据源…...

发布 VectorTraits v3.0(支持 X86架构的Avx512系列指令集,支持 Wasm架构及PackedSimd指令集等)

文章目录 支持 X86架构的Avx512系列指令集支持Avx512时的输出信息 支持 Wasm架构及PackedSimd指令集支持PackedSimd时的输出信息VectorTraits.Benchmarks.Wasm 使用说明 新增了向量方法支持 .NET 8.0 新增的向量方法提供交织与解交织的向量方法YGroup3Unzip的范例代码 提供重新…...

详解如何创建SpringBoot项目

目录 点击New Project 选择依赖 简单使用SpringBoot 前面已经讲解了如何获取IDEA专业版&#xff0c;下面将以此为基础来讲解如何创建SpringBoot项目。 点击New Project 选择依赖 注意&#xff0c;在选择SpringBoot版本时&#xff0c;不要选择带SNAPSHOT的版本。 这样&#…...

IT架构管理

目录 总则 IT架构管理目的 明确组织与职责 IT架构管理旨在桥接技术实施与业务需求之间的鸿沟&#xff0c;通过深入理解业务战略和技术能力&#xff0c;推动技术创新以支持业务增长&#xff0c;实现技术投资的最大价值。 设定目标与范围 IT架构管理的首要目的是确立清晰的组织…...

Feign入门实践

引言 随着微服务架构的兴起&#xff0c;服务间的通信变得越来越频繁和复杂。为了简化服务之间的调用过程&#xff0c;提高开发效率和系统的可维护性&#xff0c;Spring Cloud 生态系统提供了多种解决方案&#xff0c;其中 OpenFeign 是一种声明式的 HTTP 客户端&#xff0c;它使…...

Leetcode 买卖股票的最佳时机 Ⅱ

使用贪心算法来解决此问题&#xff0c;通过在价格上涨的每一天买入并在第二天卖出的方式&#xff0c;累计所有上涨的利润&#xff0c;以实现最大收益。关键点是从第二天开始遍历&#xff0c;并且只要当前比前一天价格高&#xff0c;我们就在前一天买入然后第二天卖出去。下面是…...

书生大模型实战营-玩转HF/魔搭社区闯关任务

通过Github Codespace下载InternLM模型并运行 本篇博客是记录《书生大模型实战营第四期-玩转HF/魔搭/魔乐》章节的闯关任务从HF上下载模型文件&#xff0c;对实战营感兴趣的小伙伴也可以扫码报名哦。 一、通过模版创建Codespace环境 访问codespace 点击Jupyter Notebook 模版…...

混响(Reverb):原理、应用与发展趋势的深度解析

目录 引言1. 混响的基本原理2. 混响的应用3. 混响的技术实现4. 混响的未来发展趋势5. 总结 引言 混响&#xff08;Reverb&#xff09;是音频信号处理中的重要概念之一&#xff0c;在自然界和音频工程中都扮演着关键角色。从音乐制作到语音识别&#xff0c;从电影音效到虚拟现实…...

Java学习教程,从入门到精通,Java修饰符语法知识点及案例代码(23)

1.Java修饰符语法知识点及案例代码 Java修饰符用于改变类、方法、变量、接口等元素的行为和可见性。主要分为两大类&#xff1a;访问修饰符和非访问修饰符。 访问修饰符&#xff08;Access Modifiers&#xff09; public 提供最大的访问权限&#xff0c;任何类都可以访问。使…...

钉钉小程序使用getApp实现类型provide inject的功能 应用场景:解决页面同步子组件弹窗的滚动问题

前言:在开发钉钉小程序的时候 组件内部的弹窗滚动会带着视图同步滚动 所以需要在组件内部弹窗显示的时候禁用视图的scroll滚动 由于我组件封装的比较深 不可能逐级传递 dd也么有provide的语法 所以我使用的getApp 完成控制的效果 最终完美运行 觉得有帮助相互关注一下 后续会持…...

标准化 Git 提交信息的约定

在使用 Git 进行版本控制时&#xff0c;良好的提交信息可以帮助团队成员更好地理解每次提交的目的和影响。为了规范化提交信息&#xff0c;一些团队采用了特定的格式或约定&#xff0c;比如 Angular 团队提出的 Commit Message Conventions。这种规范有助于自动化工具的使用&am…...

React教程(详细版)

React教程&#xff08;详细版&#xff09; 1&#xff0c;简介 1.1 概念 react是一个渲染html界面的一个js库&#xff0c;类似于vue&#xff0c;但是更加灵活&#xff0c;写法也比较像原生js&#xff0c;之前我们写出一个完成的是分为html&#xff0c;js&#xff0c;css&…...

Perfect Forwarding(完美转发)

文章目录 1. 引用折叠2. 万能引用3. 完美转发3.1对比&#xff1a;std::move and std::forward比较 3.2使用时机3.3 返回值优化&#xff08;RVO)两个前提条件注意事项 4. 完美转发失败情况完美转发失败五种情况 完美转发的实现要依赖于模版类型推导和引用折叠和万能引用。 1. 引…...

PHP露营地管理平台小程序系统源码

⛺️【露营新风尚】露营地管理平台系统全攻略⛺️ &#x1f3d5;️一、露营热潮下的管理难题&#xff1a;如何高效运营露营地&#xff1f;&#x1f914; 随着露营文化的兴起&#xff0c;越来越多的人选择在大自然中享受宁静与自由。然而&#xff0c;露营地的管理却面临着诸多…...

速盾:vue的cdn是干嘛的?

CDN&#xff0c;即内容分发网络&#xff08;Content Delivery Network&#xff09;&#xff0c;是一种将网站的静态资源分发到全球各个节点并缓存起来的技术。它可以帮助网站提供更快的加载速度&#xff0c;更好的用户体验&#xff0c;并且可以减轻源服务器的负载压力。 Vue.j…...

线性代数:Matrix2x2和Matrix3x3

今天整理自己的框架代码&#xff0c;将Matrix2x2和Matrix3x3给扩展了一下&#xff0c;发现网上unity数学计算相关挺少的&#xff0c;所以记录一下。 首先扩展Matrix2x2&#xff1a; using System.Collections; using System.Collections.Generic; using Unity.Mathemati…...

Windows 中 Electron 项目实现运行时权限提升以杀掉特定进程

#Windows 中 Electron 项目实现运行时权限提升以杀掉特定进程 一、引言 在 Windows 操作系统中&#xff0c;有时我们需要以管理员权限来执行某些操作&#xff0c;特别是当需要杀掉由管理员启动的进程时。Electron 是一个开源的框架&#xff0c;用于使用 JavaScript、HTML 和 C…...

赠你一只金色的眼 - 富集分析和表达数据可视化

GOplot包介绍 GOplot包用于生物数据的可视化。更确切地说&#xff0c;该包将表达数据与功能分析的结果整合并进行可视化。但是要注意该包不能用于执行这些分析&#xff0c;只能把分析结果进行可视化。在所有科学领域&#xff0c;由于空间限制和结果所需的简洁性&#xff0c;切…...

鸿蒙的进击之路

1. 题记&#xff1a; 为什么要写鸿蒙&#xff0c;因为她是华为的&#xff0c;为什么是华为就要写&#xff0c;因为华为背负了国人太多太多的包袱&#xff0c;或点赞或抨击。 我是强烈支持华为的&#xff0c;但我会客观公正地去评价华为的产品&#xff0c;就比如这篇博文&#…...

stm32G473的flash模式是单bank还是双bank?

今天突然有人stm32G473的flash模式是单bank还是双bank&#xff1f;由于时间太久&#xff0c;我真忘记了。搜搜发现&#xff0c;还真有人和我一样。见下面的链接&#xff1a;https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

Psychopy音频的使用

Psychopy音频的使用 本文主要解决以下问题&#xff1a; 指定音频引擎与设备&#xff1b;播放音频文件 本文所使用的环境&#xff1a; Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

华硕a豆14 Air香氛版,美学与科技的馨香融合

在快节奏的现代生活中&#xff0c;我们渴望一个能激发创想、愉悦感官的工作与生活伙伴&#xff0c;它不仅是冰冷的科技工具&#xff0c;更能触动我们内心深处的细腻情感。正是在这样的期许下&#xff0c;华硕a豆14 Air香氛版翩然而至&#xff0c;它以一种前所未有的方式&#x…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

计算机基础知识解析:从应用到架构的全面拆解

目录 前言 1、 计算机的应用领域&#xff1a;无处不在的数字助手 2、 计算机的进化史&#xff1a;从算盘到量子计算 3、计算机的分类&#xff1a;不止 “台式机和笔记本” 4、计算机的组件&#xff1a;硬件与软件的协同 4.1 硬件&#xff1a;五大核心部件 4.2 软件&#…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)

一、OpenBCI_GUI 项目概述 &#xff08;一&#xff09;项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台&#xff0c;其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言&#xff0c;首次接触 OpenBCI 设备时&#xff0c;往…...

「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案

在移动互联网营销竞争白热化的当下&#xff0c;推客小程序系统凭借其裂变传播、精准营销等特性&#xff0c;成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径&#xff0c;助力开发者打造具有市场竞争力的营销工具。​ 一、系统核心功能架构&…...

Xela矩阵三轴触觉传感器的工作原理解析与应用场景

Xela矩阵三轴触觉传感器通过先进技术模拟人类触觉感知&#xff0c;帮助设备实现精确的力测量与位移监测。其核心功能基于磁性三维力测量与空间位移测量&#xff0c;能够捕捉多维触觉信息。该传感器的设计不仅提升了触觉感知的精度&#xff0c;还为机器人、医疗设备和制造业的智…...