• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
阿炜死了
帮助1

1. 总览

1.1 效果图:

学新通

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 = /一级路径/二级路径 以此类推
}

以下截图是路由表一部分↓
学新通
你可能会发现有重复项,那是路由重定向的功能~

2.1.2 引入组件 渲染菜单

<Menu
  	items={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.title
        list.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>
    )
}

学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgfgghk
系列文章
更多 icon
同类精品
更多 icon
继续加载