【前端】基于vite的react项目工程化(4)

176 阅读25分钟

1.前言

2.用户管理

2.1 查询用户

前面已经实现了增删改,接着来实现查询用户的操作:

// ...
import SearchFormFC from '@/components/SearchForm'
// 搜索请求
  const handleSearch = async (params: UserSearchParamType) => {}
const UserFC: React.FC = () => {
  // ...
  return (
    <div>
      {/* 检索框 */}
      <SearchFormFC onSearch={handleSearch} />
      {/* 用户新增/批量删除 */}
      {/* ... */}
      {/* 表格数据 */}
      {/* ... */}
      {/* 创建/更新用户 */}
      {/* ... */}
    </div>
  )
}
export default UserFC

这里将搜索相关的内容封装到了SearchFormFC组件,当组件中点击搜索时就会触发handleSearch函数通知父组件使用搜索参数进行后端请求并渲染,具体代码如下:

import { Space, Button, Form, Input, Select } from 'antd'
import styles from './index.module.css'
import { useForm } from 'antd/es/form/Form'
import { useMenuStore } from '@/store'
import { UserSearchParamType } from '@/types'

interface SearchFormProps {
  onSearch: (params: UserSearchParamType) => void
}

export default function SearchFormFC({ onSearch }: SearchFormProps) {
  const [form] = useForm()
  const { roleList } = useMenuStore()
  // 搜索触发
  const handleSearchClick = async () => {
    const searchParams = form.getFieldsValue()
    await onSearch(searchParams as UserSearchParamType)
  }
  // 重置触发
  const handleResetClick = () => {
    form.resetFields()
  }

  const handleSelectChange = () => {}
  return (
    <Form className={styles.searchform} form={form} layout='inline'>
      <Form.Item name='username' label='用户名称'>
        <Input placeholder='请输入用户名称' />
      </Form.Item>
      <Form.Item label='系统角色' name='roleList'>
        <Select placeholder='请选择角色' onChange={handleSelectChange}>
          {roleList.map(item => {
            return (
              <Select.Option value={item._id} key={item.roleName}>
                {item.roleName}
              </Select.Option>
            )
          })}
        </Select>
      </Form.Item>
      <Form.Item>
        <Space>
          <Button type='primary' onClick={handleSearchClick}>
            搜索
          </Button>
          <Button type='default' onClick={handleResetClick}>
            重置
          </Button>
        </Space>
      </Form.Item>
    </Form>
  )
}

这里也用到了角色列表const { roleList } = useMenuStore(),所以把它放在了zustand中:

type UpdateRoleList = (roleList: SelectParamType) => void

// State 接口
interface State extends Action {
  // 角色列表
  roleList: SelectParamType
}
// 添加更新菜单列表的动作
type Action = {
  updateRoleList: UpdateRoleList
}
// 左侧菜单 zustand 存储
export const useMenuStore = create<State & Action>((set, get) => ({
  // 所有角色列表
  roleList: [],
  updateRoleList: roleList =>
    set(state => ({
      ...state,
      roleList: roleList
    })),
  // ...
})

CreateUser.tsx中将数据存起来:

// 获取所有角色
  const getAllRoles = async () => {
    const allRolesResult = await getAllRolesApi()
    // 将数据转换成所需要的结构
    // ...
    // 将其存到zustand供搜索时使用
    updateRoleList(newRoleList)
  }

此时如果输入内容进行检索,可能是复合条件检测,也可能是单一条件检测,需要调用后端接口进行获取,具体内容如下:

export const SEARCH_USER_WITH_CONDITIONS = '/user/searchUserWithConditions' //复合条件检索用户
// 复合条件检索用户
export function searchUserWithConditionsApi(params: UserSearchParamType): Promise<Result<userInfoResponseType[]>> {
  return request.post(SEARCH_USER_WITH_CONDITIONS, params)
}

// 复合查询用户时的请求参数类型
export interface UserSearchParamType {
  username: string
  roleList: string
}

在组件中进行调用:

// ...
// 搜索请求
  const handleSearch = async (params: UserSearchParamType) => {
    if (!params.roleList && !params.username) {
      message.error('检索条件不能为空')
    } else {
      let transformData: TableDataType[] | undefined = []
      const searchResult = await searchUserWithConditionsApi(params)
      console.log(searchResult.data.length)
      if (searchResult.data.length > 0) {
        // 转换数据
        transformData = transformUserPageToTableData(searchResult.data)
        // 设置数据
        setTableData(transformData as TableDataType[])
      } else {
        // 设置数据
        setTableData(transformData as TableDataType[])
      }
    }
  }
// ...

具体效果如下: 234234423.gif

3.角色管理

3.1 展示角色列表

前面已经实现了用户相关的增删改查,接下来实现角色相关的操作,首先来处理角色的展示:

import AuthButton from '@/components/AuthButton'
import styles from './index.module.css'
import { Space, Table, TableColumnsType } from 'antd'
import { useState } from 'react'

export type RoleTableItemDataType = {
  key: string
  rolename: string
  createTime: string
  updateTime: string
}
const RoleFC: React.FC = () => {
  const [tableData] = useState<RoleTableItemDataType[]>([
    {
      key: '1',
      rolename: '超级管理员',
      createTime: '2024-04-18 19:45',
      updateTime: '2024-04-18 19:45'
    }
  ])
  // 显示新增角色表单
  const handleShowAdd = () => {}
  // 批量删除角色
  const handleBatchdel = () => {}
  // checkbox选择时触发
  const rowSelection = {
    onChange: (selectedRowKeys: React.Key[]) => {
      console.log(selectedRowKeys)
    }
  }
  // 表头名称
  const columns: TableColumnsType<RoleTableItemDataType> = [
    {
      title: '角色ID',
      dataIndex: 'key',
      width: 100,
      align: 'center'
    },
    {
      title: '角色名',
      dataIndex: 'rolename',
      width: 150,
      align: 'center'
    },
    {
      title: '创建时间',
      dataIndex: 'createTime',
      width: 150,
      align: 'center'
    },
    {
      title: '更新时间',
      dataIndex: 'updateTime',
      width: 150,
      align: 'center'
    },
    {
      title: '操作',
      key: 'action',
      width: 150,
      align: 'center',
      render: (_, record) => (
        <Space size='small'>
          <AuthButton auth='role@update' type='link' onClick={() => handleEdit(record)}>
            编辑
          </AuthButton>
          <AuthButton auth='role@assign' type='link' onClick={() => handleAssign(record)}>
            分配
          </AuthButton>
          <AuthButton auth='role@delete' type='text' danger={true} onClick={() => handleDel(record)}>
            删除
          </AuthButton>
        </Space>
      )
    }
  ]
  // 分配角色
  const handleAssign = (record: any): void => {
    console.log(record)
  }
  // 更新角色
  const handleEdit = (record: any): void => {
    console.log(record)
  }
  // 删除角色
  const handleDel = (record: any): void => {
    console.log(record)
  }
  return (
    <div>
      {/* 角色新增/批量删除 */}
      <div className={styles.headerwrapper}>
        <div className='title'>角色列表</div>
        <div className='action'>
          <AuthButton auth='role@add' type='primary' onClick={handleShowAdd}>
            新增
          </AuthButton>
          <AuthButton auth='role@batchdelete' type='primary' onClick={handleBatchdel}>
            批量删除
          </AuthButton>
        </div>
      </div>
      {/* 表格数据 */}
      <Table
        rowSelection={{
          ...rowSelection
        }}
        columns={columns}
        dataSource={tableData}
      />
    </div>
  )
}
export default RoleFC

搜索相关的组件放到后面实现,此时需要在组件加载时分页获取角色数据展示:

const RoleFC: React.FC = () => {
  const [tableData, setTableData] = useState<RoleTableItemDataType[]>([
    {
      key: '1',
      rolename: '超级管理员',
      createTime: '2024-04-18 19:45',
      updateTime: '2024-04-18 19:45'
    }
  ])
  // 初始化时获取要分页展示的用户数据
  useEffect(() => {
    getRoleByPagination()
  }, [])
  const getRoleByPagination = async (pageSize: number = 10, pageNumber: number = 0) => {
    // 获取数据
    const rawData = await getRoleByPaginationApi({ pageSize, pageNumber })
    // 转换数据
    const transformData = transformRolePageToTableData(rawData.data.roleList)
    // 设置数据
    setTableData(transformData as RoleTableItemDataType[])
  }
  return (
    {/* ... */}
  )
}

现在效果如下: 12345678765432.gif 现在将工具函数和类型文件抽离出去:

import AuthButton from '@/components/AuthButton'
import styles from './index.module.css'
import { Space, Table, TableColumnsType } from 'antd'
import { useEffect, useState } from 'react'
import { getRoleByPaginationApi } from '@/api'
import { transformRolePageToTableData } from '@/utils/transformRolePageToTableData'
import { RoleTableItemDataType } from '@/types'

const RoleFC: React.FC = () => {
  const [tableData, setTableData] = useState<RoleTableItemDataType[]>([])
  // 初始化时获取要分页展示的用户数据
  useEffect(() => {
    getRoleByPagination()
  }, [])
  const getRoleByPagination = async (pageSize: number = 10, pageNumber: number = 0) => {
    // 获取数据
    const rawData = await getRoleByPaginationApi({ pageSize, pageNumber })
    // 转换数据
    const transformData = transformRolePageToTableData(rawData.data.roleList)
    // 设置数据
    setTableData(transformData as RoleTableItemDataType[])
  }
  // 显示新增角色表单
  const handleShowAdd = () => {}
  // 批量删除角色
  const handleBatchdel = () => {}
  // checkbox选择时触发
  const rowSelection = {
    onChange: (selectedRowKeys: React.Key[]) => {
      console.log(selectedRowKeys)
    }
  }
  // 表头名称
  const columns: TableColumnsType<RoleTableItemDataType> = [
    {
      title: '角色ID',
      dataIndex: 'key',
      width: 100,
      align: 'center'
    },
    {
      title: '角色名',
      dataIndex: 'rolename',
      width: 150,
      align: 'center'
    },
    {
      title: '创建时间',
      dataIndex: 'createTime',
      width: 150,
      align: 'center'
    },
    {
      title: '更新时间',
      dataIndex: 'updateTime',
      width: 150,
      align: 'center'
    },
    {
      title: '操作',
      key: 'action',
      width: 150,
      align: 'center',
      render: (_, record) => (
        <Space size='small'>
          <AuthButton auth='role@update' type='link' onClick={() => handleEdit(record)}>
            编辑
          </AuthButton>
          <AuthButton auth='role@assign' type='link' onClick={() => handleAssign(record)}>
            分配
          </AuthButton>
          <AuthButton auth='role@delete' type='text' danger={true} onClick={() => handleDel(record)}>
            删除
          </AuthButton>
        </Space>
      )
    }
  ]
  // 分配角色
  const handleAssign = (record: any): void => {
    console.log(record)
  }
  // 更新角色
  const handleEdit = (record: any): void => {
    console.log(record)
  }
  // 删除角色
  const handleDel = (record: any): void => {
    console.log(record)
  }
  return (
    <div>
      {/* 角色新增/批量删除 */}
      <div className={styles.headerwrapper}>
        <div className='title'>角色列表</div>
        <div className='action'>
          <AuthButton auth='role@add' type='primary' onClick={handleShowAdd}>
            新增
          </AuthButton>
          <AuthButton auth='role@batchdelete' type='primary' onClick={handleBatchdel}>
            批量删除
          </AuthButton>
        </div>
      </div>
      {/* 表格数据 */}
      <Table
        rowSelection={{
          ...rowSelection
        }}
        columns={columns}
        dataSource={tableData}
      />
    </div>
  )
}
export default RoleFC

3.2 新增角色

接着来处理一下新增逻辑:

// ...
import CreateRole from './CreateRole'

const RoleFC: React.FC = () => {
  const [tableData, setTableData] = useState<RoleTableItemDataType[]>([]) // 角色表单数据
  const [createRoleVisible, setCreateRoleVisible] = useState(false) // 是否显示创建角色的组件
  const [type] = useState<'update' | 'add'>('add') // 是否显示创建角色的组件
  const [updateRoleKey] = useState<string>('') // 选中的行数据的key

  // 初始化时获取要分页展示的用户数据
  // ...
  // 显示新增角色表单
  const handleShowAdd = () => {
    setCreateRoleVisible(true)
  }
  // 批量删除角色
  const handleBatchdel = () => {}
  // checkbox选择时触发
  // ...
  // 表头名称
  // ...
  // 分配角色
  // ...
  // 更新角色
  // ...
  // 删除角色
  // ...
  // 取消弹框
  const handleCreateRoleCancel = () => {
    setCreateRoleVisible(false)
  }
  // 确认新增角色/更新角色
  const handleCreateUserConfirm = async (createRoleParams: any) => {
    console.log(createRoleParams)
  }
  return (
    <div>
      {/* 角色新增/批量删除 */}
      {/* ... */}
      {/* 表格数据 */}
      {/* ... */}
      {/* 创建/更新角色 */}
      <CreateRole
        visible={createRoleVisible}
        onCancel={handleCreateRoleCancel}
        onOk={handleCreateUserConfirm}
        type={type}
        updateRoleKey={updateRoleKey}
      />
    </div>
  )
}
export default RoleFC

import { CreateRoleProps } from '@/types'
import { Form, Input, Modal } from 'antd'
import { useEffect } from 'react'

const CreateRole: React.FC<CreateRoleProps> = ({ visible, onCancel, onOk, type = 'add', updateRoleKey }) => {
  const [form] = Form.useForm()

  useEffect(() => {
    console.log(type)
    console.log(updateRoleKey)
  }, [])
  // 点击确认时触发
  const getCreateRoleInfo = () => {
    // 获取表单数据
    const formData = form.getFieldsValue()
    console.log(formData)
    // 传递数据到父组件
    return onOk(formData)
  }
  return (
    <Modal open={visible} onCancel={onCancel} onOk={getCreateRoleInfo} closeIcon={null} cancelText='取消' okText='确认'>
      <Form form={form} labelCol={{ span: 4 }} labelAlign='right'>
        {/* 角色名称 */}
        <Form.Item
          label='用户名称'
          name='rolename'
          rules={[
            { required: true, message: '请输入角色名称' },
            { min: 5, max: 10, message: '用户名称最小5个字符最大10个字符' }
          ]}
        >
          <Input placeholder='请输入角色名称'></Input>
        </Form.Item>
      </Form>
    </Modal>
  )
}
export default CreateRole

此时效果如下: 8888888.gif

接着来实现一下新增:

// 新增角色
export function addRoleApi(params: roleAddParamType): Promise<Result<RoleAddResponseType>> {
  return request.post(ROLE_ADD_URL, params)
}
// 新增角色时的参数类型
export interface roleAddParamType {
  rolename: string
}
// 确认新增角色/更新角色
  const handleCreateUserConfirm = async (createRoleParams: any) => {
    console.log('-----', createRoleParams)
    setCreateRoleVisible(false)
    if (type === 'update') {
      // 更新角色
    } else {
      // 新增角色
      // 将数据传到后端进行新增
      const result = await addRoleApi(createRoleParams)
      if (result) {
        message.success(result.message)
        // 重新请求数据并渲染
        getRoleByPagination()
      }
    }
  }

此时效果如下: 11111111.gif

3.3 更新角色

接着实现角色更新的逻辑,首先要找到需要更新的key:

  const [createRoleVisible, setCreateRoleVisible] = useState(false) // 是否显示创建角色的组件
  const [updateRoleKey, setUpdateRoleKey] = useState<string>('') // 选中的行数据的key

// 更新角色
  const handleEdit = (record: any): void => {
    // 显示创建/更新的组件
    setCreateRoleVisible(true)
    // 设置为更新类型而不是添加类型
    setType('update')
    // 将当前行数据传给组件获取数据回显
    setUpdateRoleKey(record.key)
  }

接着获取数据并回显:

// 根据type类型判断是否需要获取角色信息
  const getRoleByRoleId = async (roleId: number) => {
    const result = await getRoleByRoleIdApi(roleId)
    if (result.data.length > 0) {
      // 回显数据
      form.setFieldValue('rolename', result.data[0].rolename)
    }
  }
// 根据id获取角色
export function getRoleByRoleIdApi(roleId: number): Promise<Result<roleInfoResponseType[]>> {
  return request.get(GET_USER_BY_ROLE_ID_URL + '/?roleid=' + roleId)
}
// 角色权限类型
export interface userPermissionType {
  id: number
  permissionName: string
  parentId: number
  symbol: string
  menuPath: string
  menuIcon: string
  permissionType: string
  createTime: string
  updateTime: string
}
// 请求角色信息返回的结果类型
export interface roleInfoResponseType {
  id: number
  rolename: string
  createTime: string
  updateTime: string
  permissions: userPermissionType[] | []
}

接下来就是用户修改数据后点击确认时将数据交给后端进行更新:

import { getRoleByRoleIdApi } from '@/api'
import { CreateRoleProps } from '@/types'
import { Form, Input, Modal } from 'antd'
import { useEffect } from 'react'

const CreateRole: React.FC<CreateRoleProps> = ({ visible, onCancel, onOk, type = 'add', updateRoleKey }) => {
  const [form] = Form.useForm()

  // 根据type类型判断是否需要获取角色信息
  const getRoleByRoleId = async (roleId: number) => {
    const result = await getRoleByRoleIdApi(roleId)
    if (result.data.length > 0) {
      // 回显数据
      form.setFieldValue('rolename', result.data[0].rolename)
    }
  }
  useEffect(() => {
    if (type === 'update') {
      // 回显数据
      getRoleByRoleId(Number(updateRoleKey))
      // 用户修改数据后点击确定/点击取消
    } else {
      return () => {
        form.setFieldsValue({
          rolename: undefined
        })
      }
    }
  }, [type, updateRoleKey])
  // 点击确认时触发
  const getCreateRoleInfo = () => {
    // 获取表单数据
    const formData = form.getFieldsValue()
    // 清空数据避免下一次点开时看到上一次数据
    form.setFieldsValue({
      rolename: undefined
    })
    // 传递数据到父组件
    return onOk(formData)
  }
  return (
    <Modal open={visible} onCancel={onCancel} onOk={getCreateRoleInfo} closeIcon={null} cancelText='取消' okText='确认'>
      <Form form={form} labelCol={{ span: 4 }} labelAlign='right'>
        {/* 角色名称 */}
        <Form.Item
          label='用户名称'
          name='rolename'
          rules={[
            { required: true, message: '请输入角色名称' },
            { min: 5, max: 10, message: '用户名称最小5个字符最大10个字符' }
          ]}
        >
          <Input placeholder='请输入角色名称'></Input>
        </Form.Item>
      </Form>
    </Modal>
  )
}
export default CreateRole
// 确认新增角色/更新角色
  const handleCreateUserConfirm = async (createRoleParams: any) => {
    setCreateRoleVisible(false)
    if (type === 'update') {
      // 更新角色
      createRoleParams.rawRoleId = Number(updateRoleKey) // 角色id
      const roleResult = await updateRoleApi(createRoleParams)
      // 重新渲染数据
      if (roleResult) {
        message.success(roleResult.message)
        setType('add')
        // 重新请求数据并渲染
        getRoleByPagination()
      }
    } else {
      // 新增角色
      // 将数据传到后端进行新增
      const result = await addRoleApi(createRoleParams)
      if (result) {
        message.success(result.message)
        // 重新请求数据并渲染
        getRoleByPagination()
      }
    }
  }
// 更新角色
export function updateRoleApi(params: roleUpdateParamType): Promise<Result<RoleAddResponseType>> {
  return request.put(ROLE_UPDATE_URL, params)
}

此时效果如下: 1234553212345.gif

3.4 删除角色

接着实现删除角色,先来实现访问的api,这里将用id删除和批量删除都实现了:

// 根据角色id删除角色
export function deleteRoleByRoleIdApi(roleid: number): Promise<Result<[]>> {
  return request.delete(DELETE_ROLE_BY_ROLE_ID_URL + '/?roleid=' + roleid)
}

// 批量删除角色
export function batchDeleteRoleApi(batchDeleteUserParams: batchDeleteRoleParamsType): Promise<Result<[]>> {
  return request.post(BATCH_DELETE_ROLE_URL, batchDeleteUserParams)
}
export const DELETE_ROLE_BY_ROLE_ID_URL = '/role/deleteRole' //根据id删除角色
export const BATCH_DELETE_ROLE_URL = '/role/batchDeleteRole' //批量删除角色

接着在组件中进行调用:

  const [selectedRowKeys, SetSelectedRowKeys] = useState<React.Key[]>([]) // 当前多选的行key数组

// 删除角色
  const handleDel = async (record: any) => {
    console.log()
    const deleteResult = await deleteRoleByRoleIdApi(Number(record.key))
    if (deleteResult) {
      getRoleByPagination()
      message.success(deleteResult.message)
    }
  }
  // 批量删除角色
  const handleBatchdel = async () => {
    // 获取选中的行数据
    if (selectedRowKeys.length > 0) {
      const ids = selectedRowKeys.map(Number)
      const result = await batchDeleteRoleApi({ roleIds: ids })
      if (result) {
        message.success(result.message)
        // 重新请求数据
        getRoleByPagination()
      } else {
        message.error('批量删除角色失败')
      }
    } else {
      message.error('当前并未选中任何数据')
    }
  }
  // checkbox选择时触发
  const rowSelection = {
    onChange: (selectedRowKeys: React.Key[]) => {
      SetSelectedRowKeys(selectedRowKeys)
    }
  }

具体效果如下:

2222222222.gif

3.5 搜索角色

接着处理搜索角色

import { Space, Button, Form, Input } from 'antd'
import styles from './index.module.css'
import { useForm } from 'antd/es/form/Form'
import { RoleSearchParamType } from '@/types'

interface RoleSearchFormType {
  onSearch: (params: RoleSearchParamType) => void
}

export default function RoleSearchFormFC({ onSearch }: RoleSearchFormType) {
  const [form] = useForm()
  // 搜索触发
  const handleSearchClick = async () => {
    const searchParams = form.getFieldsValue()
    await onSearch(searchParams as RoleSearchParamType)
  }
  // 重置触发
  const handleResetClick = () => {
    form.resetFields()
  }
  return (
    <Form className={styles.searchform} form={form} layout='inline'>
      <Form.Item name='rolename' label='角色名称'>
        <Input placeholder='请输入角色名称' />
      </Form.Item>
      <Form.Item>
        <Space>
          <Button type='primary' onClick={handleSearchClick}>
            搜索
          </Button>
          <Button type='default' onClick={handleResetClick}>
            重置
          </Button>
        </Space>
      </Form.Item>
    </Form>
  )
}
// ...
import RoleSearchFormFC from './RoleSearchForm'

const RoleFC: React.FC = () => {
  // ...

  // ...
  // 取消弹框
  // ...
  // 确认新增角色/更新角色
  // ...
  // 角色搜索
  const handleSearch = async (params: RoleSearchParamType) => {
    if (!params.rolename) {
      message.error('检索条件不能为空')
    } else {
      let transformData: RoleTableItemDataType[] | undefined = []
      const searchResult = await fuzzySearchRoleByRolenameApi(params)
      if (searchResult.data.length > 0) {
        // 转换数据
        transformData = transformRolePageToTableData(searchResult.data)
        // 设置数据
        setTableData(transformData as RoleTableItemDataType[])
      } else {
        // 设置数据
        setTableData(transformData as RoleTableItemDataType[])
      }
    }
  }
  return (
    <div>
      {/* 检索框 */}
      <RoleSearchFormFC onSearch={handleSearch} />
      {/* 角色新增/批量删除 */}
      {/* ... */}
      {/* 表格数据 */}
      {/* ... */}
      {/* 创建/更新角色 */}
      {/* ... */}
    </div>
  )
}
export default RoleFC

其请求如下:

// 根据角色名称查询角色:模糊查询
export function fuzzySearchRoleByRolenameApi(params: RoleSearchParamType): Promise<Result<roleInfoResponseType[]>> {
  return request.post(FUZZY_SEARCH_ROLE_BY_ROLENAME_URL, params)
}
export const FUZZY_SEARCH_ROLE_BY_ROLENAME_URL = '/role/fuzzySearchRoleByRolename' //根据角色名称模糊搜索用户
// 角色查询时请求的参数类型
export interface RoleSearchParamType {
  rolename: string
}

其效果如下: 3454655432345y.gif

3.6 角色分配

前面的逻辑和用户管理的逻辑差不多,接下来处理角色分配权限, 使用的组件参考 tree组件

import { Form, Modal, Tree } from 'antd'
import { useState } from 'react'

export interface SetPermissionsType {
  visible: boolean
  onCancel: () => void
  onOk: () => void
  updateRoleKey: string
}

export interface MenuItem {
  key: string
  menuName: string
  children?: MenuItem[]
}

const SetPermissions: React.FC<SetPermissionsType> = ({ visible, onCancel, onOk, updateRoleKey }) => {
  const [checkedKeys, setCheckedKeys] = useState<string[]>([]) // 当前选择的权限

  const [menuList] = useState([
    {
      menuName: '一级标题1',
      key: '0',
      children: [
        {
          menuName: '二级标题1',
          key: '0-0',
          children: [
            {
              menuName: '三级标题1',
              key: '0-0-1',
              children: []
            }
          ]
        },
        {
          menuName: '二级标题2',
          key: '0-1',
          children: []
        },
        {
          menuName: '二级标题3',
          key: '0-2',
          children: []
        }
      ]
    }
  ]) // tree结构所需要的数据

  const handleCancel = () => {
    return onCancel()
  }
  const handleOk = () => {
    console.log(updateRoleKey)
    console.log(onOk)
    return onOk()
  }
  // 复选框被选中触发
  const onCheck = (record: any) => {
    console.log('复选框被选中', record)
    setCheckedKeys(record)
  }
  return (
    <Modal
      title='设置权限'
      width={600}
      open={visible}
      okText='确定'
      cancelText='取消'
      onOk={handleOk}
      onCancel={handleCancel}
    >
      <Form labelAlign='right' labelCol={{ span: 4 }}>
        <Form.Item>
          <Tree
            checkable
            defaultExpandAll
            fieldNames={{
              title: 'menuName',
              key: 'key',
              children: 'children'
            }}
            onCheck={onCheck}
            checkedKeys={checkedKeys}
            treeData={menuList}
          />
        </Form.Item>
      </Form>
    </Modal>
  )
}
export default SetPermissions
// ...
import SetPermissions from './SetPermissions'

const RoleFC: React.FC = () => {
  // ...
  const [assignRoleVisible, setAssignRoleVisible] = useState(false) // 是否显示角色赋权的组件
  const [type, setType] = useState<'update' | 'add'>('add') // 是否显示创建角色的组件
  const [updateRoleKey, setUpdateRoleKey] = useState<string>('') // 选中的行数据的key
  // ...

  // 初始化时获取要分页展示的用户数据
  // ...
  // 批量删除角色
  // ...
  // checkbox选择时触发
  // ...
  // 表头名称
  const columns: TableColumnsType<RoleTableItemDataType> = [
    // ...
    {
      title: '操作',
      key: 'action',
      width: 150,
      align: 'center',
      render: (_, record) => (
        <Space size='small'>
          // ...
          <AuthButton auth='role@assign' type='link' onClick={() => handleAssign(record)}>
            分配
          </AuthButton>
          // ...
        </Space>
      )
    }
  ]
  // 分配角色
  const handleAssign = (record: any): void => {
    console.log(record)
    setAssignRoleVisible(true)
  }
  // 更新角色
  // ...
  // 删除角色
  // ...
  // 取消弹框
  // ...
  // 确认新增角色/更新角色
  // ...
  // 角色搜索
  // ...
  // 取消角色赋权
  const handleAssignCabcel = () => {
    setAssignRoleVisible(false)
  }
  // 确定角色赋权
  const handleAssignOk = () => {
    setAssignRoleVisible(true)
  }
  return (
    <div>
      {/* 检索框 */}
      {/* ... */}
      {/* 角色新增/批量删除 */}
      {/* ... */}
      {/* 表格数据 */}
      {/* ... */}
      {/* 创建/更新角色 */}
      {/* ... */}
      {/* 角色赋予权限 */}
      <SetPermissions
        visible={assignRoleVisible}
        updateRoleKey={updateRoleKey}
        onCancel={handleAssignCabcel}
        onOk={handleAssignOk}
      />
    </div>
  )
}
export default RoleFC

此时效果如下: 4444444.gif 可以发现需要将当前用户的权限转换成tree组件需要的数据结构,转换逻辑如下:

import { MenuItem } from '@/pages/system/role/SetPermissions'

// 给的的数据类型
export interface PermissionItem {
  id: number
  permissionName: string
  parentId: number
  symbol: string
  menuPath: string
  menuIcon: string
  permissionType: string
  createTime: string
  updateTime: string
  children?: PermissionItem[]
}
export function transformToMenuTree(permissions: PermissionItem[]): MenuItem[] {
  const result: MenuItem[] = []

  permissions.forEach(item => {
    if (item.parentId === 0) {
      const menuItem: MenuItem = {
        menuName: item.permissionName,
        key: `${item.id}`,
        children: item.children ? transformToSubMenu(item.children) : []
      }
      result.push(menuItem)
    }
  })

  return result

  function transformToSubMenu(childrenItems: PermissionItem[]): MenuItem[] {
    return childrenItems.map(child => ({
      menuName: child.permissionName,
      key: `${child.id}`,
      children: child.children ? transformToSubMenu(child.children) : []
    }))
  }
}

然后在分配权限的组件中调用:

import { Form, Modal, Tree } from 'antd'
import { useEffect, useState } from 'react'
import storage from '@/utils/localStorage'
import { PermissionItem, transformToMenuTree } from '@/utils/transformToMenuTree'

export interface SetPermissionsType {
  visible: boolean
  onCancel: () => void
  onOk: () => void
  updateRoleKey: string
}

export interface MenuItem {
  key: string
  menuName: string
  children?: MenuItem[]
}

const SetPermissions: React.FC<SetPermissionsType> = ({ visible, onCancel, onOk, updateRoleKey }) => {
  const [checkedKeys, setCheckedKeys] = useState<string[]>([]) // 当前选择的权限

  const [menuList, setMenuList] = useState<MenuItem[]>([]) // tree结构所需要的数据

  useEffect(() => {
    // 获取权限信息
    const permissionData = storage.get('permissionData')
    // 将其转化成tree结构
    const data = transformToMenuTree(permissionData as unknown as PermissionItem[])
    // 设置显示tree数据
    setMenuList(data)
  }, [])
  // ...
  // 复选框被选中触发
  // ...
  return (
    {/* ... */}
  )
}
export default SetPermissions

此时效果如下: 444565434563.gif 此时还需要去将当前角色已经设定的权限获取并显示在页面上,因此先来封装请求角色对应权限的api:

// 根据角色id获取其对应的权限
export function getPermissionByRoleIdApi(roleid: number): Promise<Result<userPermissionType[]>> {
  return request.get(GET_PERMISSION_BY_ROLEID_URL + '/?roleid=' + roleid)
}
export const GET_PERMISSION_BY_ROLEID_URL = '/permission/getPermissionByRoleId' //根据角色id查找权限

接着就可以在组件中调用了:

import { Form, Modal, Tree } from 'antd'
import { useEffect, useState } from 'react'
import storage from '@/utils/localStorage'
import { PermissionItem, transformToMenuTree } from '@/utils/transformToMenuTree'
import { getPermissionByRoleIdApi } from '@/api'

export interface SetPermissionsType {
  visible: boolean
  onCancel: () => void
  onOk: () => void
  updateRoleKey: string
}

export interface MenuItem {
  key: string
  menuName: string
  children?: MenuItem[]
}

const SetPermissions: React.FC<SetPermissionsType> = ({ visible, onCancel, onOk, updateRoleKey }) => {
  const [checkedKeys, setCheckedKeys] = useState<string[]>([]) // 当前选择的权限

  const [menuList, setMenuList] = useState<MenuItem[]>([
    {
      menuName: '一级标题1',
      key: '0',
      children: [
        {
          menuName: '二级标题1',
          key: '0-0',
          children: [
            {
              menuName: '三级标题1',
              key: '0-0-1',
              children: []
            }
          ]
        },
        {
          menuName: '二级标题2',
          key: '0-1',
          children: []
        },
        {
          menuName: '二级标题3',
          key: '0-2',
          children: []
        }
      ]
    }
  ]) // tree结构所需要的数据

  const getPermissionByRoleId = async (updateRoleKey: number) => {
    // 当前角色已经被赋予的权限
    const result = await getPermissionByRoleIdApi(updateRoleKey)
    // 将已被赋予的权限状态变为选中
    setCheckedKeys(result.data.map(item => item.id + ''))
  }
  useEffect(() => {
    if (updateRoleKey) {
      // 1.获取当前角色已经被赋予的权限
      getPermissionByRoleId(Number(updateRoleKey))

      // 2.获取当前用户可分配的权限
      // 获取权限信息
      const permissionData = storage.get('permissionData')
      // 将其转化成tree结构
      const data = transformToMenuTree(permissionData as unknown as PermissionItem[])
      console.log(data)
      // 设置显示tree数据
      setMenuList(data)
    }
  }, [updateRoleKey])
  const handleCancel = () => {
    return onCancel()
  }
  const handleOk = () => {
    console.log(updateRoleKey)
    console.log(onOk)
    return onOk()
  }
  // 复选框被选中触发
  const onCheck = (record: any) => {
    console.log('复选框被选中', record)
    setCheckedKeys(record)
  }
  return (
    {/* ... */}
  )
}
export default SetPermissions

此时效果如下: 444565434563.gif 接着当用户选择了对应权限就需要提交到后端进行更新了,此时需要写更新的请求了:

// 根据权限id数组为角色赋予权限
export function assignPermissionsByPidsApi(params: AssignPermissionsByPidsParamsType) {
  return request.post(ASSIGN_PERMISSIONS_BY_ROLEID_URL, params)
}
export const ASSIGN_PERMISSIONS_BY_ROLEID_URL = '/permission/assignPermissionsByPids' //根据权限id为角色赋权
// 赋权限时的传参类型
export type AssignPermissionsByPidsParamsType = {
  roleid: number
  permissionIds: string[]
}

写好了以后就可以调用了:

// ...

const RoleFC: React.FC = () => {
  // ...
  // 确定角色赋权
  const handleAssignOk = async (permissionIds: string[]) => {
    setAssignRoleVisible(false)
    // 将当前角色的权限ids传给后端进行赋权
    const param: AssignPermissionsByPidsParamsType = {
      roleid: Number(updateRoleKey),
      permissionIds: permissionIds
    }
    const result = await assignPermissionsByPidsApi(param)
    if (result) {
      message.success(result.message)
    }
  }
  return (
    {/* ... */}
  )
}
export default RoleFC

此时效果如下: 999999.gif

4.权限管理

4.1 查询权限

前面已经实现了用户和角色管理,接下来实现权限相关的管理,首先还是需要PermissionFC组件加载时显示对应的权限,先把数据写死,看看该组件需要什么结构的数据:

import AuthButton from '@/components/AuthButton'
import styles from './index.module.css'
import { Space, Table, TableColumnsType } from 'antd'
import { useState } from 'react'

interface TableDataType {
  key: string
  permissionName: string
  permissionType: string
  symbol: string
  menuPath: string
  menuIcon: string
  children?: TableDataType[]
}

const PermissionFC: React.FC = () => {
  const [tableData] = useState<TableDataType[]>([
    {
      key: '1',
      permissionName: '工作台',
      permissionType: 'menu',
      symbol: '',
      menuPath: '/welcome',
      menuIcon: 'DesktopOutlined',
      children: [
        {
          key: '1-1',
          permissionName: '显示',
          permissionType: 'btn',
          symbol: 'dashboard@search',
          menuPath: '',
          menuIcon: ''
        }
      ]
    },
    {
      key: '2',
      permissionName: '系统管理',
      permissionType: 'menu',
      symbol: '',
      menuPath: '',
      menuIcon: 'UnorderedListOutlined',
      children: [
        {
          key: '2-1',
          permissionName: '用户管理',
          permissionType: 'menu',
          symbol: '',
          menuPath: '/userlist',
          menuIcon: 'UserOutlined',
          children: [
            {
              key: '2-1-1',
              permissionName: '新增用户',
              permissionType: 'btn',
              symbol: 'user@add',
              menuPath: '',
              menuIcon: ''
            },
            {
              key: '2-1-2',
              permissionName: ' 删除用户',
              permissionType: 'btn',
              symbol: 'user@delete',
              menuPath: '',
              menuIcon: ''
            }
          ]
        },
        {
          key: '4-1',
          permissionName: '角色管理',
          permissionType: 'menu',
          symbol: '',
          menuPath: '/rolelist',
          menuIcon: 'UserOutlined',
          children: [
            {
              key: '4-1-1',
              permissionName: '新增角色',
              permissionType: 'btn',
              symbol: 'role@add',
              menuPath: '',
              menuIcon: ''
            },
            {
              key: '4-1-2',
              permissionName: ' 删除角色',
              permissionType: 'btn',
              symbol: 'role@delete',
              menuPath: '',
              menuIcon: ''
            }
          ]
        }
      ]
    },
    {
      key: '3',
      permissionName: '资源管理',
      permissionType: 'menu',
      symbol: '',
      menuPath: '',
      menuIcon: 'AppstoreAddOutlined',
      children: [
        {
          key: '3-1',
          permissionName: '课程管理',
          permissionType: 'menu',
          symbol: '',
          menuPath: '/courselist',
          menuIcon: 'ContainerOutlined',
          children: [
            {
              key: '3-1-1',
              permissionName: '新增课程',
              permissionType: 'btn',
              symbol: 'coure@add',
              menuPath: '',
              menuIcon: ''
            },
            {
              key: '3-1-2',
              permissionName: ' 删除课程',
              permissionType: 'btn',
              symbol: 'coure@delete',
              menuPath: '',
              menuIcon: ''
            }
          ]
        }
      ]
    }
  ]) // 表格所需要的数据

  // 显示新增/更新权限表单
  const handleShowAdd = () => {}
  // 显示批量删除权限表单
  const handleBatchdel = () => {}
  // 编辑权限
  const handleEdit = (record: any) => {
    console.log(record)
  }
  // 删除权限
  const handleDel = (record: any) => {
    console.log(record)
  }
  // 表头名称
  const columns: TableColumnsType<TableDataType> = [
    {
      title: '权限名',
      dataIndex: 'permissionName',
      width: 230,
      align: 'center'
    },
    {
      title: '权限类型',
      dataIndex: 'permissionType',
      width: 250,
      align: 'center'
    },
    {
      title: '权限标识',
      dataIndex: 'symbol',
      width: 150,
      align: 'center'
    },
    {
      title: '菜单路径',
      dataIndex: 'menuPath',
      width: 250,
      align: 'center'
    },
    {
      title: '菜单Icon',
      dataIndex: 'menuIcon',
      width: 150,
      align: 'center'
    },
    {
      title: '操作',
      key: 'action',
      width: 150,
      align: 'center',
      render: (_, record) => (
        <Space size='small'>
          <AuthButton auth='user@update' type='link' onClick={() => handleEdit(record)}>
            编辑
          </AuthButton>
          <AuthButton auth='user@delete' type='text' danger={true} onClick={() => handleDel(record)}>
            删除
          </AuthButton>
        </Space>
      )
    }
  ]
  // checkbox选择时触发
  const rowSelection = {
    onChange: (selectedRowKeys: React.Key[]) => {
      console.log(selectedRowKeys)
    }
  }
  return (
    <div>
      {/* 检索框 */}
      {/* 权限新增/批量删除 */}
      <div className={styles.headerwrapper}>
        <div className='title'>权限列表</div>
        <div className='action'>
          <AuthButton auth='permission@add' type='primary' onClick={handleShowAdd}>
            新增
          </AuthButton>
          <AuthButton auth='permission@batchdelete' type='primary' onClick={handleBatchdel}>
            批量删除
          </AuthButton>
        </div>
      </div>
      {/* 表格数据 */}
      <Table
        rowSelection={{
          ...rowSelection
        }}
        columns={columns}
        dataSource={tableData}
      />
      {/* 创建/更新权限 */}
    </div>
  )
}
export default PermissionFC

此时效果如下: 33333333.gif

可以看到当tableData中包含children时,表格会自动添加展开项,因此需要将用户权限数据转换成所需要的格式:

interface TableDataType {
  key: string
  permissionName: string
  permissionType: string
  symbol: string
  menuPath: string
  menuIcon: string
  children?: TableDataType[]
}
// 将权限数据转为table所需要的结构
function convertToTableDataType(data: userPermissionType[]): TableDataType[] {
  return data.map(item => {
    const newItem: TableDataType = {
      key: String(item.id),
      permissionName: item.permissionName,
      permissionType: item.permissionType,
      symbol: item.symbol,
      menuPath: item.menuPath,
      menuIcon: item.menuIcon
    }

    if (item.children && item.children.length > 0) {
      newItem.children = convertToTableDataType(item.children)
    }

    return newItem
  })
}

然后在useEffect中调用:

useEffect(() => {
    const permissionData = storage.get('permissionData') as unknown as userPermissionType[]
    setTableData(convertToTableDataType(permissionData))
  }, [])

此时效果如下: 2222222222.gif

到这儿已经实现了从存储的权限数据中转化成对应的table所需结构,其实这里应该实现请求获取最新的权限数据,因为当新增权限时,如果还是取默认的localstorage中的权限数据,更新就不及时,具体实现如下:

// 获取所有权限
export function getAllPermissionApi(): Promise<Result<PermissionTree>> {
  return request.get(GET_ALL_PERMISSION_URL)
}
export const GET_ALL_PERMISSION_URL = '/permission/getAllPermission' //获取所有权限
// 获取用户权限列表的数据类型
export type PermissionItem = {
  id: number
  permissionName: string
  parentId: number
  symbol: string
  menuIcon: string
  menuPath: string
  permissionType: string
  createTime: string
  updateTime: string
  children?: PermissionItem[]
}

export type PermissionTree = PermissionItem[] | null

此时就可以在PermissionFC组件加载时调用了:

// ...
// 转换权限信息为table格式
  useEffect(() => {
    getAllPermission()
    const permissionData = storage.get('permissionData') as unknown as userPermissionType[]
    setTableData(convertToTableDataType(permissionData))
  }, [])

  // 获取所有权限
  const getAllPermission = async () => {
    const allPermissionData = await getAllPermissionApi()
    storage.set('permissionData', allPermissionData.data)
  }
// ...

此时的数据就是从后端获取的最新数据了

4.2 新增权限

前面已经实现了从后端获取的最新数据后转化成对应的table所需结构,接着来处理新增时的逻辑,当点击新增按钮时需要将表单弹出,填写好数据后将数据发回父组件进行提交:

import { CreatePermissionProps, PermissionTree, PermissionTreeDataType } from '@/types'
import { Button, Form, Input, message, Modal, Radio, TreeSelect } from 'antd'
import { useForm } from 'antd/es/form/Form'
import { useEffect, useState } from 'react'
import storage from '@/utils/localStorage'
import { transformPermissionsToTreeData } from '@/utils/transformPermissionsToTreeData'
const CreatePermissionFC: React.FC<CreatePermissionProps> = ({
  visible,
  onCancel,
  onOk,
  type = 'add',
  updatePermissionKey
}) => {
  const [form] = useForm()
  const [treeData, setTreeData] = useState<PermissionTreeDataType[]>([]) // 表格所需要的数据
  // 判断当前是添加按钮还是菜单权限,true是菜单false是按钮
  const [createMenuOrBtnPermission, setCreateMenuOrBtnPermission] = useState<boolean>(true)
  useEffect(() => {
    const newTreeData = transformPermissionsToTreeData(storage.get('permissionData') as PermissionTree, true)
    const newData = [
      {
        value: '0',
        title: '权限',
        children: newTreeData
      }
    ]
    setTreeData(newData)
    if (updatePermissionKey) {
      console.log(updatePermissionKey)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  // 点击确认时触发
  const getCreateUserInfo = () => {
    if (type === 'add') {
      // 获取表单数据
      const formData = form.getFieldsValue()
      if (formData.permissionType === 2) {
        // btn类型
        if (!formData.parentId || !formData.permissionName || !formData.permissionType || !formData.symbol) {
          message.error('必填项不能为空!')
        } else {
          formData.permissionType = 'btn'
          formData.parentId = Number(formData.parentId)
          formData.menuIcon = ''
          formData.menuPath = ''
          return onOk(formData)
        }
      } else {
        // menu类型
        if (
          !formData.parentId ||
          !formData.permissionName ||
          !formData.permissionType ||
          !formData.menuIcon ||
          !formData.menuPath
        ) {
          message.error('必填项不能为空!')
        } else {
          formData.permissionType = 'menu'
          formData.symbol = ''
          formData.parentId = Number(formData.parentId)
          return onOk(formData)
        }
      }
    }
  }
  // 点击取消时触发
  const handleCancel = () => {
    return onCancel()
  }
  // radio变化时触发
  const handleRadioChange = (record: any) => {
    if (record.target.value === 1) {
      setCreateMenuOrBtnPermission(true)
    } else {
      setCreateMenuOrBtnPermission(false)
    }
  }
  return (
    <Modal
      open={visible}
      onCancel={handleCancel}
      onOk={getCreateUserInfo}
      closeIcon={null}
      cancelText='取消'
      okText='确认'
    >
      <Form form={form} labelCol={{ span: 4 }} labelAlign='right'>
        {/* 父级菜单 */}
        <Form.Item label='父级菜单' name='parentId' rules={[{ required: true, message: '请输入权限名称' }]}>
          <TreeSelect placeholder='请选择父级菜单' allowClear treeDefaultExpandAll={false} treeData={treeData} />
        </Form.Item>
        {/* 权限名称 */}
        <Form.Item
          label='权限名称'
          name='permissionName'
          rules={[
            { required: true, message: '请输入权限名称' },
            { min: 2, max: 30, message: '权限名称最小2个字符最大12个字符' }
          ]}
        >
          <Input placeholder='请输入权限名称'></Input>
        </Form.Item>
        {/* 权限类型 */}
        <Form.Item label='权限类型' name='permissionType' rules={[{ required: true, message: '请输入权限标识' }]}>
          <Radio.Group onChange={handleRadioChange} value={createMenuOrBtnPermission ? 1 : 2}>
            <Radio value={1}>菜单</Radio>
            <Radio value={2}>按钮</Radio>
          </Radio.Group>
        </Form.Item>
        {createMenuOrBtnPermission ? (
          <>
            {/* 权限菜单路径 */}
            <Form.Item
              label='菜单路径'
              name='menuPath'
              rules={[
                { min: 5, max: 30, message: '菜单路径最小5个字符最大12个字符' },
                { required: true, message: '请输入权限标识' }
              ]}
            >
              <Input placeholder='请输入菜单路径'></Input>
            </Form.Item>
            {/* 权限菜单icon */}
            <Form.Item
              label='菜单icon'
              name='menuIcon'
              rules={[
                { min: 5, max: 40, message: '菜单icon最小5个字符最大12个字符' },
                { required: true, message: '请输入权限标识' }
              ]}
            >
              <Input placeholder='请输入菜单icon'></Input>
            </Form.Item>
            <Button type='link' href='https://ant-design.antgroup.com/components/icon-cn'>
              icon参考
            </Button>
          </>
        ) : (
          <>
            {/* 权限标识 */}
            <Form.Item
              label='权限标识'
              name='symbol'
              rules={[
                { min: 5, max: 30, message: '权限标识最小5个字符最大12个字符' },
                { required: true, message: '请输入权限标识' }
              ]}
            >
              <Input placeholder='请输入权限标识'></Input>
            </Form.Item>
          </>
        )}
      </Form>
    </Modal>
  )
}
export default CreatePermissionFC

此时显示的父级菜单会发现并没有btn类型的权限,因为该类型的权限默认不存在子权限,所以将其去掉了,更改一下工具函数:

import { PermissionItem, PermissionTree, PermissionTreeDataType } from '@/types'

// 将权限信息转为所需的tree结构
export function transformPermissionsToTreeData(
  permissions: PermissionTree,
  needBtn: boolean = false
): PermissionTreeDataType[] {
  const treeData: PermissionTreeDataType[] = []

  permissions?.forEach((item: PermissionItem) => {
    if (!needBtn && item.permissionType === 'btn') {
      // 如果不需要btn类型且当前项是btn类型,则跳过
      return
    }
    const transformedItem: PermissionTreeDataType = {
      value: item.id.toString(), // 将id转换为字符串作为value
      title: item.permissionName,
      children: []
    }

    if (item.children) {
      transformedItem.children = transformPermissionsToTreeData(item.children)
    }

    treeData.push(transformedItem)
  })

  return treeData
}

此时效果如下:

222222.gif

可以看到,当加载创建权限的组件时候,需要先把用户可用的父级权限检索并展示为tree结构,然后填写对应的权限内容后进行添加,但是可以看到它是直接从一级菜单开始的,如果想要添加一级菜单那就实现不了,所以需要再最外层添加根权限,具体如下:

// ...
useEffect(() => {
    const newTreeData = transformPermissionsToTreeData(storage.get('permissionData') as PermissionTree, true)
    const newData = [
      {
        value: '0',
        title: '权限',
        children: newTreeData
      }
    ]
    console.log(newData)
    setTreeData(newData)
    if (updatePermissionKey) {
      console.log(updatePermissionKey)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])
// ...

此时数据还是从localstorage中获取的,但在权限组件一加载的时候已经获取过一次了,存入localstorage的数据是最新的,可以直接用,只需要转化一下即可:

import { PermissionItem, PermissionTree, PermissionTreeDataType } from '@/types'

// 将权限信息转为所需的tree结构
export function transformPermissionsToTreeData(permissions: PermissionTree): PermissionTreeDataType[] {
  const treeData: PermissionTreeDataType[] = []

  permissions?.forEach((item: PermissionItem) => {
    const transformedItem: PermissionTreeDataType = {
      value: item.id.toString(), // 将id转换为字符串作为value
      title: item.permissionName,
      children: []
    }

    if (item.children) {
      transformedItem.children = transformPermissionsToTreeData(item.children)
    }

    treeData.push(transformedItem)
  })

  return treeData
}

以上是转换逻辑,此时效果如下:

111111.gif

接着写新增逻辑,当点击确认时收集转化表单数据后返回:

// ...
const CreatePermissionFC: React.FC<CreatePermissionProps> = ({
  visible,
  onCancel,
  onOk,
  type = 'add',
  updatePermissionKey
}) => {
  // ...

  // 点击确认时触发
  const getCreateUserInfo = () => {
    if (type === 'add') {
      // 获取表单数据
      const formData = form.getFieldsValue()
      if (formData.permissionType === 2) {
        // btn类型
        if (!formData.parentId || !formData.permissionName || !formData.permissionType || !formData.symbol) {
          message.error('必填项不能为空!')
        } else {
          formData.permissionType = 'btn'
          formData.parentId = Number(formData.parentId)
          formData.menuIcon = ''
          formData.menuPath = ''
          return onOk(formData)
        }
      } else {
        // menu类型
        if (
          !formData.parentId ||
          !formData.permissionName ||
          !formData.permissionType ||
          !formData.menuIcon ||
          !formData.menuPath
        ) {
          message.error('必填项不能为空!')
        } else {
          formData.permissionType = 'menu'
          formData.symbol = ''
          formData.parentId = Number(formData.parentId)
          return onOk(formData)
        }
      }
    }
  }
  // 点击取消时触发
  // ...
  // radio变化时触发
  const handleRadioChange = (record: any) => {
    if (record.target.value === 1) {
      setCreateMenuOrBtnPermission(true)
    } else {
      setCreateMenuOrBtnPermission(false)
    }
  }
  return (
   {/* ... */}
  )
}
export default CreatePermissionFC

此时效果如下:

22222.gif 接着就可以在PermissionFC组件调用:

// ...
import { addPermissionApi } from '@/api'

const PermissionFC: React.FC = () => {
  // ...
  // 确认新增/更新权限
  const handleCreatepPermissipnConfirm = async (record: any) => {
    setCreatepPermissipnVisible(false)
    console.log(record)
    if (type === 'add') {
      record.symbol = ''
      const result = await addPermissionApi(record)
      console.log(result)
    }
  }
  return (
    <div>
      {/* ... */}
      {/* 创建/更新权限 */}
      <CreatePermissionFC
        visible={createpPermissipnVisible}
        onCancel={handleCreatepPermissipnCancel}
        onOk={handleCreatepPermissipnConfirm}
        type={type}
        updatePermissionKey={updatePermissionKey}
      />
    </div>
  )
}
export default PermissionFC

addPermissionApi实现如下:

// 新增权限
export function addPermissionApi(params: addPermissionType): Promise<Result<[]>> {
  return request.post(PERMISSION_ADD_URL, params)
}
export const PERMISSION_ADD_URL = '/permission/addPermission' //新增权限
// 新增权限时参数类型
export interface addPermissionType {
  permissionId?: number //更新时需要
  parentId: number
  permissionName: string
  permissionType: 'menu' | 'btn'
  symbol?: string
  menuIcon?: string
  menuPath?: string
}

具体效果如下:

2222222222.gif

此时发现虽然权限添加成功了,但是在permission的展示页里它并没有出现,这是因为它在组件最初加载时是直接从localStorage中获取的权限然后转换生成的展示数据,因此新增权限后localStorage数据没变,它自然也不会发生改变,而localStorage数据应该在角色页面中对角色赋权后才需要改变,在这里直接把所有权限重新请求后展示:

import AuthButton from '@/components/AuthButton'
import styles from './index.module.css'
import { Space, Table, TableColumnsType } from 'antd'
import { useEffect, useState } from 'react'
import { userPermissionType } from '@/types'
import storage from '@/utils/localStorage'
import CreatePermissionFC from './CreatePermission'
import { convertToTableDataType, PermissionTableDataType } from '@/utils/convertToTableDataType'
import { addPermissionApi, getAllPermissionApi } from '@/api'

const PermissionFC: React.FC = () => {
  // ...
  // 确认新增/更新权限
  const handleCreatepPermissipnConfirm = async (record: any) => {
    setCreatepPermissipnVisible(false)
    if (type === 'add') {
      record.symbol = ''
      // 新增权限
      const result = await addPermissionApi(record)
      console.log(result)
      // 获取所有权限展示
      const rawData = await getAllPermissionApi()
      const permissionData = rawData.data as unknown as userPermissionType[]
      setTableData(convertToTableDataType(permissionData))
    }
  }
  return (
    <div>
      {/* 检索框 */}
      {/* ... */}
  )
}
export default PermissionFC

此时新增权限后就可以直接展示了,具体效果如下: 2222222222.gif

接着来处理分配权限相关的问题,

还有个问题是此时回到角色权限分配时会发现此时虽然已经添加了新的权限,但在分配时却并不显示,这是因为此时的权限是从localstorage中直接取出来渲染的,它并没有更新,这里其实应该是获取所有的权限然后展示,前面因为还没有涉及到权限的内容,所以就先拿本地存储的内容替代了,先来解决第二个问题,此时需要给它更改:

import { Form, Modal, Tree } from 'antd'
import { useEffect, useState } from 'react'
import { PermissionItem, transformToMenuTree } from '@/utils/transformToMenuTree'
import { getAllPermissionApi, getPermissionByRoleIdApi } from '@/api'

export interface SetPermissionsType {
  visible: boolean
  onCancel: () => void
  onOk: (permissionIds: string[]) => void
  updateRoleKey: string
}

export interface MenuItem {
  key: string
  menuName: string
  children?: MenuItem[]
}

const SetPermissions: React.FC<SetPermissionsType> = ({ visible, onCancel, onOk, updateRoleKey }) => {
  const [checkedKeys, setCheckedKeys] = useState<string[]>([]) // 当前选择的权限

  const [menuList, setMenuList] = useState<MenuItem[]>([]) // tree结构所需要的数据
  // 获取所有权限
  const getPermission = async (updateRoleKey: number) => {
    // 获取所有权限显示
    const result = await getAllPermissionApi()
    const data = transformToMenuTree(result.data as unknown as PermissionItem[])
    setMenuList(data)

    // 获取用户可分配的权限后状态变为选中
    const result2 = await getPermissionByRoleIdApi(updateRoleKey)
    setCheckedKeys(result2.data.map(item => item.id + ''))
  }
  useEffect(() => {
    if (updateRoleKey) {
      // 获取所有权限显示-获取用户可分配的权限勾选
      getPermission(Number(updateRoleKey))
    }
  }, [updateRoleKey])
  const handleCancel = () => {
    return onCancel()
  }
  // ...
  // 复选框被选中触发
  const onCheck = (record: any) => {
    console.log(record)
    setCheckedKeys(record.checked)
  }
  return (
    <Modal
      {/* ... */}
    >
      <Form labelAlign='right' labelCol={{ span: 4 }}>
        <Form.Item>
          <Tree
            {/* ... */}
            checkStrictly={true} // 添加这一行禁用父子节点联动选中
          />
        </Form.Item>
      </Form>
    </Modal>
  )
}
export default SetPermissions

可以看到添加了checkStrictly={true},它是为了禁用父子节点联动选中,比如系统管理的id是2,它下面有用户/角色/权限/部门管理等,分别是3456,如果选择了2,则3456都会被选中,所以要将其关联取消,然后api实现如下:

// 获取所有权限
export function getAllPermissionApi(): Promise<Result<PermissionTree>> {
  return request.get(GET_ALL_PERMISSION_URL)
}
export const GET_ALL_PERMISSION_URL = '/permission/getAllPermission' //获取所有权限

此时效果如下: 3333333333.gif

此时发现,新分配的权限在加载时没有更新,这还是因为storage.get('permissionData')所以也要在确认新增角色/更新角色后重新更新:

// ...
import storage from '@/utils/localStorage'

const RoleFC: React.FC = () => {
  // ...
  // 确认新增角色/更新角色
  const handleCreateUserConfirm = async (createRoleParams: any) => {
    // ...
    // 重新获取用户权限更新
    const permissionData = await getPermissionListApi()
    storage.set('permissionData', permissionData.data)
  }
  // 角色搜索
  // ...
  // 取消角色赋权
  // ...
  // 确定角色赋权
  // ...
  return (
      {/* ... */}
  )
}
export default RoleFC

现在加完逻辑再去测试,此时已经加上了新的菜单和路由,正常应该左侧菜单里就能显示对应的路由了,但此时并没有显示,需要重新登陆才能显示

并且重新登陆时,由于并没有指定其图标映射,所以还会报错,所以还需要解决这些问题: 截屏2024-04-23 21.34.26.png 先手动加一下图标:

import {
  // ...
  CoffeeOutlined
} from '@ant-design/icons'
import { CustomMenuItem } from '@/types'

// 图标组件映射,实际应用中请按需添加更多
const iconMap: Record<string, () => ReactElement> = {
  // ...
  CoffeeOutlined: () => <CoffeeOutlined />
}

此时效果如下: 截屏2024-04-23 21.35.47.png 但只能保证用户用的是@ant-design/icons的图标,但是不知道是哪个,需要动态创建:

// transformMenuTree
import { CustomMenuItem } from '@/types'
import React from 'react'
import * as Icons from '@ant-design/icons'

// 动态创建icon
function createIcon(name?: string) {
  if (!name) return <></>
  const customerIcons: { [key: string]: any } = Icons
  const icon = customerIcons[name]
  if (!icon) return <></>
  return React.createElement(icon)
}

// 将用户权限数据转成左侧菜单所需要的结构
export function transformMenuTree(menuTree: any[], randomKeyForEmptyPath: boolean): CustomMenuItem[] {
  return menuTree.flatMap(item => {
    if (item.permissionType === 'menu') {
      const customMenuItem: CustomMenuItem = {
        // ...
        icon: item.menuIcon ? createIcon(item.menuIcon) : createIcon('CopyFilled')
      }

      // 如果子项存在并且有至少一个子菜单(permissionType为menu)
      // ...

        // 只有当有筛选出的子菜单时才设置children属性
        // ...
      }

      // 返回符合条件的菜单项
      // ...
    }

    return [] // 当前项不符合条件,则返回空数组
  })
}

// 用于生成随机key的辅助函数
export function generateRandomKey(): string {
  // ...
}

此时也能正常加载后渲染,接着来处理每次新增后不会实时显示在左侧菜单的问题,这是因为新增权限后进行分配时只是将权限分给了角色,但是并没有: