> 文章列表 > 使用React + Antd4.x + React Router 6.x 封装菜单(多级菜单)和动态面包屑

使用React + Antd4.x + React Router 6.x 封装菜单(多级菜单)和动态面包屑

使用React + Antd4.x + React Router 6.x 封装菜单(多级菜单)和动态面包屑

1. 总览

1.1 效果图:

使用React + Antd4.x + React Router 6.x 封装菜单(多级菜单)和动态面包屑

1.2 实现功能

  • 根据路由表自动生成菜单
  • 刷新页面可回显菜单
  • 动态生成面包屑

2. 具体实现

2.1 根据路由表自动生成菜单

2.1.1 配置路由表

React Router V6引入useRoutes这个hook来解析路由表,路由表的参数必须有pathelement这两个,其他的根据项目可进行自定义。
下面是我定义的路由表接口:

export interface IRouterMap {path: string,auth: number, // 是否需要鉴权title: string,key: string, // 与path保持一致element: any,hidden?: boolean, // 是否显示在菜单上, 默认要显示。加该参数主要是隐藏用于重定向的菜单children? : IChildRouterMap[]
}export interface IChildRouterMap extends IRouterMap {parentpath: string // 父级路径 用于面包屑和回显两个逻辑 如果是三级菜单 那么parentpath = /一级路径/二级路径 以此类推
}

以下截图是路由表一部分↓
使用React + Antd4.x + React Router 6.x 封装菜单(多级菜单)和动态面包屑
你可能会发现有重复项,那是路由重定向的功能~

2.1.2 引入组件 渲染菜单

<Menuitems={menuList}mode="inline"selectedKeys={[selectedKeys]}openKeys={openKeys}onClick={handleMenuClick}onOpenChange={handleOpenChange}
/>

Menu组件里展示的标题是label字段,所以我们需要将路由表里的title赋值给label字段(需要递归处理)

const [menuList, setMenuList] = useState<IChildRouterMap[]>([])React.useEffect(() => {// 菜单文字的处理const items = RouterMapAuth[0].children?.filter(item => !item.hidden)handleMenuLabel(items as IChildRouterMap[])
}, [])const handleMenuLabel = (menuItem: IChildRouterMap[]) => {let list: IChildRouterMap[] = []menuItem?.forEach((item: any) => {item.label = item.titlelist.push(item)if (item.children) handleMenuLabel(item.children)})setMenuList(list)
}

2.2 刷新页面可回显菜单

2.2.1 存取关键字段

  • openKeys当前展开的 SubMenu 菜单项 key 数组
  • selectedKeys 当前选中的菜单项 key 数组
  • onOpenChange SubMenu 展开/关闭的回调

我们会用到以上三个属性以及点击事件去实现该功能。

  • 当点击菜单时,我们可以获取到当前菜单项,菜单key,keyPath(是个数组)
  • 我们首先需要把key存到本地,
  • 然后判断keyPath的长度是否大于1
  • 如果大于1说明点击的是多级菜单,那么我们就需要把数组最后一项添加到给openKeys
  • 如果小于1说明点击的是普通层级的菜单,我们就把openKeys进行清空
  • 接下来就是跳转功能
  • 如果当前菜单项的parentpath === /那么说明是普通层级的菜单直接跳转navigate(item.props.path)
  • 如果不等于 就跳转到navigate(`${item.props.parentpath}/${item.props.path}`)

当点击父级菜单时(触发openChange),我们只需要把回调参数赋值给openKeys即可

直接贴代码:

const navigate = useNavigate()
const { pathname } = useLocation()
const [selectedKeys, setSelectedKeys] = useState<string>('')
const [openKeys, setOpenKeys] = useState<Array<string>>([])
const [menuList, setMenuList] = useState<IChildRouterMap[]>([])
const { t } = useTranslation()React.useEffect(() => {// 从当前url里取出路径 回显对应的菜单SStorage.setItem('selectedKeys', pathname.slice(pathname.lastIndexOf('/') + 1))// 当多级菜单时 还需设置openKey 回显默认展开的嵌套菜单if (pathname.lastIndexOf('/') !== 0) {SStorage.setItem('openKeys', pathname.match(/(?<=\\/).*?(?=\\/)/g))}// 回显默认选中的菜单和展开的嵌套菜单SStorage.getItem('selectedKeys') ? setSelectedKeys(SStorage.getItem('selectedKeys')) : setSelectedKeys('homepage')setOpenKeys(SStorage.getItem('openKeys'))
}, [pathname])const handleMenuClick: MenuProps['onClick'] = ({ item, key, keyPath }: any) => {// 存储key到本地 刷新页面选择的菜单也不会丢失SStorage.setItem('selectedKeys', key)if (keyPath.length > 1) {// 点击多级菜单时 存储被点击的root menu key, 用来回显SStorage.setItem('openKeys', [keyPath[keyPath.length - 1]])} else {// 当点击一级菜单时,清空curNestedKey,这样刷新的时候就不会展开没有被选中的二级菜单SStorage.setItem('openKeys', [])}if (item.props.parentpath === '/') {navigate(item.props.path)} else {navigate(`${item.props.parentpath}/${item.props.path}`)}
}const handleOpenChange: MenuProps['onOpenChange'] = (openKeys: string[]) => {setOpenKeys(openKeys)
}

2.3 动态生成面包屑

2.3.1 分析

  • 面包屑数据需要时数组格式,并且数据量 >= 3 时,需要给非第一项和最后一项加上点击跳转功能
  • 面包屑标题来源 可以用之前hiddentrue的路由项(因为他们是父级重定向的,可以单独筛选出来)以下称为routesParents
  • 拿到路由表里没有被隐藏的项(hiddenfalse),需要用到扁平化数组的方法。
  • 当每次页面路径变化时,拿到当前路径最后一段(最后一个/后的内容),然后和扁平化之后的路由项(以下称为item)进行比较
  • item.parentpath === /时,说明是一级菜单。直接往面包屑数组里push当前itemtitle属性
  • item.parentpath !== /时,说明是多级菜单,那么我们就要拿到itemparentpath,并用/分割成数组,好用来与routesParents进行遍历比较 拿到当前父级菜单的title.
  • 然后就是双重循环进行添加titlepath

代码:

export default function BreadCrumb() {const { pathname } = useLocation()const { t } = useTranslation()const [breadCrumb, setBreadCrumb] = useState<IBreadCrumbs[]>([])const routes = RouterMapAuth[0].children.filter(item => !item.hidden)let routesParents: IChildRouterMap[] = [] // 隐藏起来的路由(可以利用他们的title属性设置面包屑)React.useEffect(() => {routesParents = handleFlattenRoutes(RouterMapAuth[0].children).filter(item => item.hidden)const path = pathname.slice(pathname.lastIndexOf('/') + 1)handleGetBreadcrumb(handleFlattenRoutes(routes), path)}, [pathname])const handleFlattenRoutes = (routes: IChildRouterMap[]) => {return routes.reduce((pre, next) => {return pre.concat(Array.isArray(next.children) ?  handleFlattenRoutes(next.children) : next)},[])}const handleGetBreadcrumb = (routes: IChildRouterMap[], path: string) => {let arr = []let breadPath = [] // 面包屑需要跳转的地址routes.map(item => {if (item.path === path) {if (item.parentpath === '/') {setBreadCrumb([{ title: item.title }])} else {// 当为三级及以上菜单时,需要给面包屑的第二级加上跳转功能const parentpath = item.parentpath.split('/').filter(item => item)parentpath.map((item, index) => {routesParents.map(item2 => {if (item === item2.path) {if (index < parentpath.length - 1) {// 除了最后一项 其他的都需要把path存进去breadPath.push(item2.path)}if (parentpath.length >= 2 && index !== 0) {// 说明时三级及以上的菜单层级// 不给面包屑的第一个层级加跳转功能arr.push({ title: item2.title, path: breadPath[index - 1] })} else {arr.push({ title: item2.title })}}})})arr.push({ title: item.title })setBreadCrumb(arr)}}})}return (<div className={style['breadCrumb']}>{breadCrumb.map(item =><div className={style['breadCrumb-item']} key={item.title}>{item.path ?<NavLink to={item.path} >{t(item.title)}</NavLink> :<div className={style['breadCrumb-item-label']}>{t(item.title)}</div>}</div>)}</div>)
}