从零实现一个React+Antd5.0后台管理系统-多页签及面包屑实现页面框架和内容区域都构建完毕,接下来准备实现一些细
前言
页面框架和内容区域都构建完毕,接下来准备实现一些细节的东西。
- 多页签,在本系统中,当点击导航菜单时会切换路由,一般是在内容区域的
Outlet
路由视图中直接显示对应路由的页面组件,利用React Router
配合Ant Design
的tab
组件实现多页签功能,打开一个新的菜单路由时变成弹出一个新的tab页签,并且随时可以切换到之前的页面,保留住之前的组件状态。 - 面包屑,当系统拥有超过两级以上的层级结构时或需要告知用户『你在哪里』时,并且需要向上导航的功能时就需要面包屑。利用
React Router
配合Ant Design
的Breadcrumb
组件实现面包屑功能。当打开一个菜单路由,会在页面上显示当前路由的层级结构,并且能返回上层。
多页签实现
为了实现多页签,我们需要一个数组记录路由的路径、标题信息等。但数组信息不止限定在多页签组件中使用,其它组件也可能用到,所以我们设置一个全局状态来存储。
全局缓存多页签数组
在store文件夹下的reducers文件夹新建tabSlice
切片,里面存储状态tabs
数组,并且在reducers
配置项中添加新增和删除的方法。
src/store/reducers/tabSlice.js
import { createSlice } from '@reduxjs/toolkit'
const tabSlice = createSlice({
name: 'tabs',
initialState: {
tabs: []
},
reducers: {
// 新增tab
addTab: (state, action) => {
state.tabs.push(action.payload)
},
// 删除tab
removeTab: (state, action) => {
state.tabs = state.tabs.filter((tab) => tab.key !== action.payload)
}
}
})
export const { setActiveKey, addTab, removeTab } = tabSlice.actions
export default tabSlice
然后在store
中新增此切片
src/store/index.js
// 创建store对象
const store = configureStore({
reducer: {
user: userSlice.reducer,
permission: permissionSlice.reducer,
// tabs切片
tabs: tabSlice.reducer
},
...
})
tabs组件展示及新增切换删除功能
Ant Design
提供了Tabs
组件,找一个功能差不多的,样式如下
所用到的配置项,其中destroyInactiveTabPane
默认为false
不销毁隐藏时DOM的结构即可实现缓存。
配置项 | 类型 | 说明 |
---|---|---|
type | string | 页签的基本样式,可选 line 、card editable-card 类型 |
activeKey | string | 当前激活选项卡的 key |
hideAdd | boolean | 是否隐藏加号图标,在 type="editable-card" 时有效 |
destroyInactiveTabPane | boolean | 隐藏时是否销毁DOM结构,默认为false不销毁 |
items | {label :选项卡显示标题,key :选项卡对应key,children :选项卡显示内容,closable :是否显示选项卡的关闭按钮,在 type="editable-card" 时有效}[] | 配置选项卡内容 |
onChange | function(activeKey ) {} | 切换选项卡的回调,activeKey 为选中选项卡的key |
onEdit | function(targetKey :选中选项卡key, action :新增add删除remove){} | 新增和删除页签的回调,在 type="editable-card" 时有效 |
我们直接用它的代码,Tabs组件渲染配置项items
使用全局状态的tabs数组,然后将其封装为一个组件在Layout
组件使用。
src/Layout/components/TabsView.jsx
import React, { useEffect, useMemo, useState } from 'react'
import { Tabs, ConfigProvider } from 'antd'
import { useDispatch, useSelector } from 'react-redux'
import { addTab, removeTab } from '@/store/reducers/tabSlice'
const TabsView = React.memo(({ pathname }) => {
// 获取全局tabs
const tabs = useSelector((state) => state.tabs.tabs)
const dispatch = useDispatch()
// 当前选中tab
const [activeKey, setActiveKey] = useState()
// Tabs渲染所用数组,当长度为1时Tab项不显示关闭
const tabItems = useMemo(() => {
return tabs.map((item) => ({...item,closable: tabs.length > 1}))
}, [tabs])
useEffect(() => {
if (pathname !== '/') {
setActiveKey(pathname)
// 数组中无此项,进行添加
if (!tabs.some((item) => item.key === pathname)) {
onAddTab(pathname)
}
}
}, [pathname])
/** tab操作方法 */
// tab切换事件
const handleTabChange = (activeKey) => {
...
}
// 添加方法
const onAddTab = (pathname) => {
...
}
// 点击关闭
const closeTab = (targetKey) => {
...
}
const handleEdit = (targetKey, action) => {
if (action === 'remove') {
closeTab(targetKey)
}
}
return (
<ConfigProvider
theme={{
components: {
Tabs: {
// 横向标签页标签外间距
horizontalMargin: 0
}
}
}}>
<div style={{ backgroundColor: '#fff' }}>
<Tabs
type="editable-card"
onChange={handleTabChange}
activeKey={activeKey}
onEdit={handleEdit}
items={tabItems}
hideAdd
/>
</div>
</ConfigProvider>
)
})
export default TabsView
获取所有菜单项
Tabs组件大致的框架已经完成了,但是现在还展示不出东西,原因是我们没有新增项到全局数据tabs
中。但在新增之前,我们得要获取最底层的菜单项(无子菜单的菜单项) 的信息,这样对应可以获取当前路由路径的路由信息(标题等)。
在Layout组件中,我们有获取过全局的后端路由,可以通过它做结构转换,获取需要的底层菜单项数组。后端路由结构类似[{title:'xx',children:[xx]},{...}]
,我们只需要把不存在children字段或children字段为空的提取出来即可。
src/Layout/index.jsx
import TabsView from './components/TabsView'
// 提取底层路由方法
const getMenus = (routes) => {
let menus = []
function getMenuItem(route) {
route.forEach((item) => {
if (item.children && item.children.length) getMenuItem(item.children)
else {
// 排除默认路由
if (item.path) menus.push(item)
}
})
}
getMenuItem(routes)
return menus
}
然后我们用useMemo
hook缓存返回值赋值给一个变量,注意添加上首页路由,将其赋值给上一步创建的TabsView
组件。我们再额外传递切换选项卡跳转路由的方法(注意传给React.memo
包裹的子组件函数要用useCallback
hook包裹缓存,不然会报错)
src/Layout/index.jsx
const LayoutApp=()=>{
const {pathname}=useLocation()
// 获取后端权限路由
const permissionRoutes = useSelector((state) => state.permission.permissionRoutes)
// 格式化路由数组
const Home = lazy(() => import('@/pages/Home'))
const formatRoutes = useMemo(() => {
return [{ title: '首页', menuPath: '/home',element:<Home /> }].concat(getMenus(permissionRoutes))
}, [permissionRoutes])
// 选择选项卡以后,跳转对应路由
const selectTab = useCallback(
(key) => {
navigate(key)
},
[navigate]
)
...
return(
...
<Content
style={{
// padding: 24,
minHeight: 280
// background: colorBgContainer
}}>
<TabsView pathname={pathname} formatRoutes={formatRoutes} selectTab={selectTab}/>
</Content>
)
}
新增事件
获取底层路由数组后,我们就可以进行选项卡的添加。
src/Layout/components/TabsView.jsx
const TabsView = React.memo(({ pathname, formatRoutes, selectTab }) => {
// 添加方法
const onAddTab = (pathname) => {
// 找到对应路径的菜单信息
const menu = formatRoutes.find((item) => item.menuPath === pathname)
if (menu) dispatch(addTab({ label: menu.title, key: menu.menuPath,
children: <div style={{ padding: 24, backgroundColor: '#f5f5f5' }}>{menu.element}</div> }))
}
}
切换事件
切换选项卡会调用Tabs组件的onChange
回调函数,传递的参数为切换的选项卡的key。随后调用父组件传来的跳转事件并传递跳转路径即key。
// tab切换事件
const handleTabChange = (activeKey) => {
selectTab(activeKey)
}
删除事件
删除事件是点击选项卡右上角的关闭按钮后会调用Tabs组件的onEdit
回调,传递的参数为选项卡key和action
为remove
。随后有两个步骤
1.关闭当前选项卡后,左边是否有选项卡【tabs数组上一项】,有则选中跳转左边的选项卡,否则选中跳转右边的选项卡【tabs数组下一项】
// 点击选项卡关闭
const closeTab = (targetKey) => {
// 获取删除后的数组
const afterRemoveTabs = tabs.filter((item) => item.key !== targetKey)
// 获取选中跳转的数组下标
const selectIndex = tabs.findIndex((item) => item.key === targetKey) - 1
if (selectIndex >= 0) {
selectTab(afterRemoveTabs[selectIndex].key)
} else {
selectTab(afterRemoveTabs[selectIndex + 1].key)
}
}
2.删除全局状态中的当前项
const closeTab = (targetKey) => {
...
dispatch(removeTab(targetKey))
}
效果图
面包屑实现
面包屑我们直接用Ant Design
提供的Breadcrumb
组件,我们用到其中的items
来配置路由栈信息,items的配置项如下所示
我们只用到其中的title
来实现跳转及展示功能。在这之前,我们得先获得路由的平铺对象(即每一级路由url
与标题一一对应的对象),例如{'/home':'首页','/system':'系统管理'}
,然后我们再获取页面当前路由的路由路径数组例如['system','user']
一个个拼接去与这个对象作对应。
获得路由的平铺对象
我们用全局状态中的后端路由permissonRoutes
拼接上首页后进行递归遍历。
src/utils/common.js
/**
* 面包屑获取路由平铺对象 ,
* @param {*} routes
* @returns object, 例:{"/home":"首页"}
*/
export const getBreadcrumbNameMap = (routes) => {
//首先拼接上首页
const list = [{ path: 'home', menuPath: '/home', title: '首页' }, ...routes]
let breadcrumbNameObj = {}
const getItems = (list) => {
//先遍历数组
list.forEach((item) => {
//遍历数组项的对象
if (item.children && item.children.length) {
const menuPath = item.menuPath ? item.menuPath : '/' + item.path
breadcrumbNameObj[menuPath] = item.title
getItems(item.children)
} else {
breadcrumbNameObj[item.menuPath] = item.title
}
})
}
//调用一下递归函数
getItems(list)
//返回新数组
return breadcrumbNameObj
}
然后在Layout
组件中用useMemo hook
来缓存方法返回值
src/Layout/index.jsx
import {getBreadcrumbNameMap} from '@/utils/common'
...
// 获取全局状态中的后端路由
const permissionRoutes = useSelector((state) => state.permission.permissionRoutes)
// 缓存面包屑的路由平铺对象
const breadcrumbNameMap = useMemo(() => getBreadcrumbNameMap(permissionRoutes), [permissionRoutes])
获取页面路由路径数组
我们通过useLocation hook
获取的pathname
是类似/system/user
这样的,我们需要将其转换为数组。
const { pathname } = useLocation()
// 获取页面路由路径数组
const pathSnippets = pathname.split('/').filter((i) => i)
然后我们遍历这个数组,转换为面包屑items配置项所需格式。
/** 面包屑 */
const breadcrumbNameMap = useMemo(() => getBreadcrumbNameMap(permissionRoutes), [permissionRoutes])
const breadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`
// 如果是最后一项,即当前页面路由,渲染文本不可点击跳转
if (index + 1 === pathSnippets.length)
return {
key: url,
title: breadcrumbNameMap[url]
}
// 其余用link标签可点击跳转(注意:上级路由默认跳转到其定义的重定向路由,例如/system跳转至/system/user)
return {
key: url,
title: <Link to={url}>{breadcrumbNameMap[url]}</Link>
}
})
最后展示面包屑组件,先引入组件
import { Breadcrumb } from 'antd'
Layout组件
中展示
最终效果
代码
转载自:https://juejin.cn/post/7308996393397059636