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