从0开始的中后台管理系统-5(菜单的路径绑定以及角色页面的实现)

1.菜单和路由页面双向绑定

实现了用户管理以及菜单管理和部门管理以及工作台这些路由组件之后,我们还没有和导航栏绑定这些路径,也就是我们需要在导航栏添加这些组件的名称以及点击后可以跳转到对应的路径。也就是说我们需要访问一个路由,可以返回给我们当前的菜单列表菜单名称以及路径。然后我们需要去定义一个数组,去通过递归的方式获取到里面的路径以及菜单名称还有图表用于跳转以及在导航里面展示。

首先调用这个路由的时机肯定是在访问所有的路由路由之前,也就是我们需要给布局组件一个loader然后绑定一个函数去提前请求路由拿到权限列表,然后把里面包含的菜单列表传递到一个递归获取path的函数里面。

​编辑

typescript 复制代码
import api from "@/api"
import { getMenuPath } from "@/utils"
export default async function AuthLoader(){
  const data = await api.getPermissionList()
  const menuPathList = getMenuPath(data.menuList)
  console.log('menuPathList',menuPathList)
return {
  buttonList:data.buttonList,
  menuList:data.menuList,
  menuPathList:menuPathList
}
}
// 获取页面路径
export const getMenuPath = (list: Menu.MenuItem[]): string[] => {
  return list.reduce((result: string[], item: Menu.MenuItem) => {
    // 先收集当前菜单的 path(如果存在)
    if (item.path) {
      result.push(item.path);
    }
    // 递归 children(无论是否有按钮)
    if (Array.isArray(item.children) && item.children.length > 0) {
      result = result.concat(getMenuPath(item.children));
    }
    return result;
  }, []);
};

这里我们获取到了路由数组,然后我们就可以去导航区域绑定。

用useRouteLoaderData钩子可以获取到对应路由loader返回的数据通过这个拿到路径列表。以及菜单列表还有按钮权限列表。

然后我们通过拿到的菜单列表遍历生成导航需要的菜单列表树,设置列表树每一个需要label和key以及icon和children然后递归往里面传入参数。

typescript 复制代码
import React, { useEffect, useState } from 'react'
import { Menu as IMenu } from 'antd'
import type { Menu } from '@/types/api'
import {
  DesktopOutlined,
  SettingOutlined,
  TeamOutlined
} from '@ant-design/icons'
import styles from './index.module.less'
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
import type { MenuProps } from 'antd'
import * as Icons from '@ant-design/icons'
export default function SideMenu() {
  const [menuList, setMenuList] = useState<MenuItem[]>([])
  const data = useRouteLoaderData('layout')
  const [selectedKeys, setSelectKeys] = useState<string[]>([])
  console.log(' data', data)
  const { pathname } = useLocation()

  type MenuItem = Required<MenuProps>['items'][number]
  const navigite = useNavigate()
  // 生成每一个菜单项
  function getItem(
    label: React.ReactNode,
    key?: React.Key | null,
    icon?: React.ReactNode,
    children?: MenuItem[]
  ): MenuItem {
    return {
      label,
      key,
      icon,
      children
    } as MenuItem
  }
  function createIcon(name?: string) {
    if (!name) return <></>
    const customerIcons: { [key: string]: any } = Icons
    const icon = customerIcons[name]
    if (!icon) return <></>
    return React.createElement(icon)
  }
  //递归生成菜单
  const getTreeMenu = (
    menuList: Menu.MenuItem[],
    treeList: MenuItem[] = []
  ) => {
    menuList.forEach((item, index) => {
      if (item.menuType === 1 && item.menuState === 1) {
        if (item.button) {
          return treeList.push(
            getItem(item.menuName, item.path || index, createIcon(item.icon))
          )
        }
        treeList.push(
          getItem(
            item.menuName,
            item.path || index,
            createIcon(item.icon),
            getTreeMenu(item.children || [])
          )
        )
      }
    })
    return treeList
  }
  useEffect(() => {
    const treeMenuList = getTreeMenu(data.menuList)
    setMenuList(treeMenuList)
    setSelectKeys([pathname])
  }, [])
  const handleClickLogo = () => {
    navigite('/welcome')
  }
  //点击跳转路由
  const handleClickMenu = ({ key }: { key: string }) => {
    setSelectKeys([key])
    navigite(key)
  }
  return (
    <div>
      <div className={styles.logo} onClick={handleClickLogo}>
        <img className={styles.img} src='/images/logo.png' alt='' />
        <span>木木货运</span>
      </div>
      <IMenu
        mode='inline' //mode 模式设置的子导航打开方式 行内行外
        theme='dark'
        items={menuList}
        selectedKeys={selectedKeys}
        onClick={handleClickMenu}
      />
    </div>
  )
}

然后将生成的树结构绑定给表单,然后点击事件就是跳转到对应的key。默认接收的参数就是点击事件发生的那一项。对象结构拿出key跳转即可。

为了保留刷新之后还可以停留在当前路径,设置一个state去存储当前的pathname,然后组件挂载完之后就保存当前的pathname,然后双向绑定到菜单组件上。

2.角色页面

角色页面和其他的页面相同。展示效果图。

​编辑

也是通过点击新增编辑然后展示表单框,通过useImperativeHandle方法保暴露自己的open方法,然后让父组件操作自己的展示列表的state。还有父组件用useAntdTable钩子通过接收Promise形式的请求返回值,以及默认的对象配置来生成对应的表单数据和search搜索方法。一键生成访问请求返回的数据以及search操作表单,search.submit就直接调用方法。

typescript 复制代码
import React, { useRef } from 'react'
import api from '@/api/roleApi'
import { useAntdTable } from 'ahooks'
import { Button, Form, Input, Space, Table } from 'antd'
import { useForm } from 'antd/es/form/Form'
import type { Role } from '@/types/api'
import { toLocalDate } from '@/utils'
import type { IAction } from '@/types/modal'
import CreateRole from './CreateRole'
import type { ColumnsType } from 'antd/es/table'
export default function RoleList() {
  const roleRef = useRef<{
    open: (type: IAction, data?: Role.RoleItem) => void
  }>(null)
  const [form] = useForm()
  const getTableData = (
    {
      current,
      pageSize
    }: {
      current: number
      pageSize: number
    },
    formData: Role.Params
  ) => {
    return api
      .getRoleList({
        ...formData,
        pageNum: current,
        pageSize: pageSize
      })
      .then(data => {
        return {
          total: data.page.total,
          list: data.list
        }
      })
  }
  const { tableProps, search } = useAntdTable(getTableData, {
    form,
    defaultPageSize: 5
  })
  const columns: ColumnsType<Role.RoleItem> = [
    {
      title: '角色名称',
      dataIndex: 'roleName',
      key: 'roleName'
    },
    {
      title: '备注',
      dataIndex: 'remark',
      key: 'remark'
    },
    {
      title: '更新时间',
      dataIndex: 'updateTime',
      key: 'updateTime',
      render(updataTime: string) {
        return toLocalDate(updataTime)
      }
    },
    {
      title: '创建时间',
      dataIndex: 'createTime',
      key: 'createTime',
      render(createTime: string) {
        return toLocalDate(createTime)
      }
    },
    {
      title: '操作',
      key: 'action',
      render(_, record) {
        return (
          <Space>
            <Button
              onClick={() => {
                handleEdit(record)
              }}
            >
              编辑
            </Button>
            <Button>设置权限</Button>
            <Button>删除</Button>
          </Space>
        )
      }
    }
  ]
  //创建角色
  const handleCreate = () => {
    roleRef.current?.open('create')
  }
  //编辑角色
  const handleEdit = (data: Role.RoleItem) => {
    roleRef.current?.open('edit', data)
  }
  return (
    <div className='role-warp'>
      <Form form={form} className='search-form' layout='inline'>
        <Form.Item name='roleName' label='角色名称'>
          <Input placeholder='请输入角色名称' />
        </Form.Item>
        <Form.Item>
          <Space>
            <Button type='primary' onClick={search.submit}>
              搜索
            </Button>
            <Button type='default' onClick={search.reset}>
              重置
            </Button>
          </Space>
        </Form.Item>
      </Form>
      <div className='base-table'>
        <div className='header-wrapper'>
          <div className='title'>角色列表</div>
          <div className='action'>
            <Button type='primary' onClick={handleCreate}>
              新增
            </Button>
          </div>
        </div>
        <Table
          rowKey='userId' // 保证每行有唯一 key
          {...tableProps}
          bordered
          columns={columns}
        />
      </div>
      <CreateRole mRef={roleRef} update={search.submit} />
    </div>
  )
}

就如代码所展示的,然后是子组件表单弹出。

typescript 复制代码
import type { Role } from '@/types/api'
import type { IAction, ImodalProp } from '@/types/modal'
import { Form, Input, message, Modal } from 'antd'
import { useForm } from 'antd/es/form/Form'
import React, { useState } from 'react'
import api from '@/api/roleApi'
import { useImperativeHandle } from 'react'
export default function CreateRole(props: ImodalProp<Role.RoleItem>) {
  const [form] = useForm()
  const [visible, setVisible] = useState(false)
  const [action, setAction] = useState<IAction>('create')
  //暴露子组件open方法
  useImperativeHandle(props.mRef, () => {
    return {
      open
    }
  })
  //调用弹窗显示方法
  const open = (type: IAction, data?: Role.RoleItem) => {
    setAction(type)
    setVisible(true)
    if (data) {
      form.setFieldsValue(data)
    }
  }
  const handleOk = async () => {
    const valid = await form.validateFields()
    if (valid) {
      const params = form.getFieldsValue()
      if (action === 'create') {
        await api.createRole(params)
      } else {
        await api.editRole(params)
      }
      message.success('操作成功')
      handleCancel()
      props.update()
    }
  }
  //取消
  const handleCancel = () => {
    form.resetFields()
    setVisible(false)
  }
  return (
    <Modal
      title={action === 'create' ? '新增角色' : '编辑角色'}
      width={600}
      open={visible}
      okText='确定'
      cancelText='取消'
      onOk={handleOk}
      onCancel={handleCancel}
    >
      <Form form={form} labelAlign='right' labelCol={{ span: 4 }}>
        {/* 隐藏域 */}
        <Form.Item name='_id' hidden>
          <Input />
        </Form.Item>
        <Form.Item
          name='roleName'
          label='角色名称'
          rules={[{ required: true, message: '请输入角色名称' }]}
        >
          <Input placeholder='请输入角色名称' />
        </Form.Item>
        <Form.Item name='remark' label='备注'>
          <Input.TextArea placeholder='请输入备注' />
        </Form.Item>
      </Form>
    </Modal>
  )
}

相关推荐
摸鱼的春哥16 分钟前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
念念不忘 必有回响19 分钟前
viepress:vue组件展示和源码功能
前端·javascript·vue.js
C澒24 分钟前
多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀
前端·架构·系统架构·前端框架
崔庆才丨静觅26 分钟前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘28 分钟前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
恋猫de小郭2 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端