likes
comments
collection
share

从零实现一个React+Antd5.0后台管理系统-多页签及面包屑实现页面框架和内容区域都构建完毕,接下来准备实现一些细

作者站长头像
站长
· 阅读数 36

前言

页面框架和内容区域都构建完毕,接下来准备实现一些细节的东西。

  • 多页签,在本系统中,当点击导航菜单时会切换路由,一般是在内容区域的Outlet路由视图中直接显示对应路由的页面组件,利用React Router配合Ant Designtab组件实现多页签功能,打开一个新的菜单路由时变成弹出一个新的tab页签,并且随时可以切换到之前的页面,保留住之前的组件状态。
  • 面包屑,当系统拥有超过两级以上的层级结构时或需要告知用户『你在哪里』时,并且需要向上导航的功能时就需要面包屑。利用React Router配合Ant DesignBreadcrumb组件实现面包屑功能。当打开一个菜单路由,会在页面上显示当前路由的层级结构,并且能返回上层。

多页签实现

为了实现多页签,我们需要一个数组记录路由的路径、标题信息等。但数组信息不止限定在多页签组件中使用,其它组件也可能用到,所以我们设置一个全局状态来存储。

全局缓存多页签数组

在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组件,找一个功能差不多的,样式如下

从零实现一个React+Antd5.0后台管理系统-多页签及面包屑实现页面框架和内容区域都构建完毕,接下来准备实现一些细

所用到的配置项,其中destroyInactiveTabPane默认为false不销毁隐藏时DOM的结构即可实现缓存。

配置项类型说明
typestring页签的基本样式,可选 linecard editable-card 类型
activeKeystring当前激活选项卡的 key
hideAddboolean是否隐藏加号图标,在 type="editable-card" 时有效
destroyInactiveTabPaneboolean隐藏时是否销毁DOM结构,默认为false不销毁
items{label:选项卡显示标题,key:选项卡对应key,children:选项卡显示内容,closable:是否显示选项卡的关闭按钮,在 type="editable-card" 时有效}[]配置选项卡内容
onChangefunction(activeKey) {}切换选项卡的回调,activeKey为选中选项卡的key
onEditfunction(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
}

然后我们用useMemohook缓存返回值赋值给一个变量,注意添加上首页路由,将其赋值给上一步创建的TabsView组件。我们再额外传递切换选项卡跳转路由的方法(注意传给React.memo包裹的子组件函数要用useCallbackhook包裹缓存,不然会报错

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和actionremove。随后有两个步骤

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))
}

效果图

从零实现一个React+Antd5.0后台管理系统-多页签及面包屑实现页面框架和内容区域都构建完毕,接下来准备实现一些细

面包屑实现

面包屑我们直接用Ant Design提供的Breadcrumb组件,我们用到其中的items来配置路由栈信息,items的配置项如下所示

从零实现一个React+Antd5.0后台管理系统-多页签及面包屑实现页面框架和内容区域都构建完毕,接下来准备实现一些细

我们只用到其中的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组件中展示

从零实现一个React+Antd5.0后台管理系统-多页签及面包屑实现页面框架和内容区域都构建完毕,接下来准备实现一些细

最终效果

从零实现一个React+Antd5.0后台管理系统-多页签及面包屑实现页面框架和内容区域都构建完毕,接下来准备实现一些细

代码

转载自:https://juejin.cn/post/7308996393397059636
评论
请登录