使用React + Antd4.x + React Router 6.x 封装菜单(多级菜单)和动态面包屑
1. 总览
1.1 效果图:
1.2 实现功能
2. 具体实现
2.1 根据路由表自动生成菜单
2.1.1 配置路由表
React Router V6
引入useRoutes
这个hook
来解析路由表,路由表的参数必须有path
和element
这两个,其他的根据项目可进行自定义。
下面是我定义的路由表接口:
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 = /一级路径/二级路径 以此类推
}
以下截图是路由表一部分↓
你可能会发现有重复项,那是路由重定向的功能~
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 时,需要给非第一项和最后一项加上点击跳转功能
- 面包屑标题来源 可以用之前
hidden
为true
的路由项(因为他们是父级重定向的,可以单独筛选出来)以下称为routesParents
。- 拿到路由表里没有被隐藏的项(
hidden
为false
),需要用到扁平化数组的方法。- 当每次页面路径变化时,拿到当前路径最后一段(最后一个
/
后的内容),然后和扁平化之后的路由项(以下称为item
)进行比较
- 当
item.parentpath === /
时,说明是一级菜单。直接往面包屑数组里push
当前item
的title
属性- 当
item.parentpath !== /
时,说明是多级菜单,那么我们就要拿到item
的parentpath
,并用/
分割成数组,好用来与routesParents
进行遍历比较 拿到当前父级菜单的title.- 然后就是双重循环进行添加
title
和path
代码:
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>)
}