从零实现一个React+Antd5.0后台管理系统-管理页面通用模块实现

前言

完成了Layout模块页面布局后,我们就可以着手内容页面的构建。对于后台管理系统而言,通用的模块可以分为以下几块

1.搜索查询模块

2.功能按钮模块

3.列表模块

我们对以上三个模块分开进行介绍。

管理页面通用模块

搜索查询模块

搜索模块由一或多个form表单搜索项和搜索、重置按钮组成。

当表单项超出三项时,点击高级搜索查看编辑剩余的搜索项。

我们可以额外封装一个搜索栏组件来处理,总共有以下几个步骤

1.父组件传入搜索项数组

我们传递一个数组到搜索栏组件中,数组每项传递

  • formItemProps:为对象,里面包含

    • 表单项的字段名name(必填)
    • 表单项的标签名label(必填)
    • 其它你想要透传给Form.item组件的属性
  • valueCompProps:为对象,里面包含(皆非必填)

    • 输入框组件的类型type,不填为input
    • select组件的选项数组selectValues
    • 输入类组件onChange事件的回调函数callback
    • 其它你想透传给输入类组件的属性

例如用户管理需要用户名、昵称、邮箱、状态的搜索项。除了状态为Select下拉框组件,其它皆为Input输入框,数组便如下所示

src/pages/System/User/index.jsx

css 复制代码
// 导入获取字典hook
import useDict from '@/hooks/useDict'
...
// 状态字典
const statusDict = useDict('status')
// 搜索栏表单项数组
const formItemList = [
{ formItemProps: { name: 'username', label: '用户名' }, valueCompProps: {} },
{ formItemProps: { name: 'nickname', label: '昵称' }, valueCompProps: {} },
{ formItemProps: { name: 'email', label: '邮箱' }, valueCompProps: {} },
{
  formItemProps: { name: 'status', label: '状态' },
  valueCompProps: { type: 'select', selectValues: statusDict }
}]

我们可以发现用到了useDict这个自定义hook,其作用就是返回指定字典名称的字典值。

2.自定义hook获取字典值

自定义hook其实就是一个处理函数,往里传入参数,返回需要的结果。我们在src下新加一个hook文件夹,并且新建useDict.js文件。之后的步骤如以下:

  • 获取传入的字典名称
  • 调用接口获取此字典名称的字典值
  • 将字典值转换为需要的{label,value}格式返回

src/hook/useDict.js

javascript 复制代码
import { useState, useEffect } from 'react'
import dictApi from '@/api/dict'
// 自定义Hook,用于获取字典数据
const useDict = (dictName) => {
  const [dictionary, setDictionary] = useState([])
​
  useEffect(() => {
    const fetchDictionary = async () => {
      // 调用接口获取字典值
      const { data } = await dictApi.manage.queryByName(dictName)
      const options = data.map((item) => {
        return { label: item.item_text, value: item.item_value }
      })
      setDictionary(options)
    }
​
    fetchDictionary()
  }, [dictName])
​
  return dictionary
}
export default useDict

3.封装搜索栏组件

(1)构建基本框架

外围框架直接用antd的Card组件,内部为表单组件。表单项采用Row、Col构建栅格布局

src/components/SearchBar/index.jsx

javascript 复制代码
import React from 'react'
import { Card, Form, Row } from 'antd'
​
const SearchBar = ({ formItemList }) => {
  // form 表单实例
  const [form] = Form.useForm()
  const onFinish = (values) => {
    console.log(values)
  }
​
  return (
    <Card>
      <Form
        form={form}
        name="queryForm"
        labelCol={{ span: 6 }}
        wrapperCol={{ span: 18 }}
        layout="inline"
        onFinish={onFinish}>
        <Row justify="start" gutter={[20, 20]} style={{ width: '100%' }}></Row>
      </Form>
    </Card>
  )
}
​
export default SearchBar

(2)搜索项数量大于等于三和小于三的处理

搜索查询模块最开始的图片所展示的即是经过处理的效果。

  • 当搜索项数量小于等于三,直接展示全部搜索项
  • 当搜索项数量大于3,默认先展示前三项,点击高级搜索后展示剩余项

我们获取数组后直接将数组用slice方法分块,[0,3)和[3,length-1]。我们先来处理默认展示的[0,3)

为了实现栅格布局,在Row标签内每个子项都得带有Col组件。然后Col组件包裹着Form.Item组件,Form.Item组件包裹输入类组件。输入类组件的类型为父组件传入的,得根据传入的类型返回不同的组件。由此我们构建一个对象,每个属性的键为输入类组件的类型;值为函数,函数参数为外部传入,返回值为输入类组件。具体如下代码(目前只封装三个组件,大家可以依此扩展)

javascript 复制代码
// 表单输入式组件
const formComponents = {
  select: ({ selectValues = [], callback = () => {},   ...restProps }) =>
  createElement(
    Select,
    { onChange: (v) => callback(v), ...restProps },
    selectValues.map((v) => createElement(Select.Option, { key: v.value, value: v.value }, v.label))
  ),
  input: (props) => <Input {...props} />,
  datePicker: (props) => <DatePicker format="YYYY-MM-DD" {...props} />
}

依据类型封装完组件后,将外部参数参数传入后面再添加上搜索和重置按钮即可。

typescript 复制代码
...
<Row justify="start" gutter={[20, 20]} style={{ width: '100%' }}>
   {formItemList &&
    formItemList.slice(0, 3).map((item, index) => {
      // 取出输入类组件的类型,无则默认input
      const { type = 'input' } = item.valueCompProps
      // 依据类型返回对应的组件
      const C = formComponents[type]
      // 输出不用的type属性以便传入输入类组件
      delete item.valueCompProps.type
      return (
        <Col span={6} key={index}>
          <Form.Item {...item.formItemProps}>{C(item.valueCompProps)}</Form.Item>
        </Col>
      )
    })}
  {formItemList && (
    <Col span={6}>
      <Form.Item wrapperCol={{ span: 12, offset: 6 }}>
        <Space>
          <Button type="primary" htmlType="submit">
            查询
          </Button>
          <Button onClick={onReset}>重置</Button>
        </Space>
      </Form.Item>
    </Col>
  )}
</Row>
...

然后当搜索项数量大于三的时候,我们在搜索和重置按钮后多展示一个高级搜索按钮,点击后展示剩余的搜索项(即数组的[3,length-1])。

增加的按钮

ini 复制代码
   // 是否展开高级搜索
   const [advancedSearch, setAdvancedSearch] = useState(false)
   ...
   <Button onClick={onReset}>重置</Button>
   {formItemList.length > 3 && (
        <Button
          type="link"
          icon={<PlusCircleOutlined />}
          onClick={() => setAdvancedSearch((value) => !value)}>
          高级搜索
        </Button>
   )}
 </Space>

剩余的数组项

typescript 复制代码
{advancedSearch &&
    formItemList.length > 3 &&
    formItemList.slice(3).map((item, index) => {
      const { type = 'input' } = item.valueCompProps
      const C = formComponents[type]
      delete item.valueCompProps.type
      return (
        <Col span={6} key={3 + index}>
          <Form.Item {...item.formItemProps}>{C(item.valueCompProps)}</Form.Item>
        </Col>
      )
})}

最后我们编写搜索重置按钮的事件

scss 复制代码
// form 表单实例
const [form] = Form.useForm()
const [advancedSearch, setAdvancedSearch] = useState(false)
const onFinish = (values) => {
  // 接收并调用父组件的获取表单值方法
  getSearchParams(values)
}
const onReset = () => {
  form.resetFields()
  getSearchParams({})
}

4.父组件引用搜索栏组件

src/pages/System/User/index.jsx

javascript 复制代码
// 搜索请求参数
const [searchParams, setSearchParams] = useState([])
// 获取搜索栏组件的表单参数
const getSearchParams = (searchParams) => {
  setSearchParams(searchParams)
}
...
return(
  <>
    <SearchBar formItemList={formItemList} getSearchParams={getSearchParams} />
  </>
)

这个模块就封装完了,具体效果参考模块顶部图。

功能按钮模块

功能按钮模块比较简单,做一个按钮栏放置功能按钮即可。

但需要注意的是,可能有的角色并没有操作一些按钮的权限。关于这个我们在每个按钮上都可以定义一个权限标识 字段,当该用户的权限按钮数组有按钮的权限标识 ,我们就展示按钮。权限按钮数组之前我们在Reduxuser切片已经存储了,我们再封装一个权限按钮组件来判断是否角色拥有权限即可。

1.封装权限按钮组件

1.获取Redux Store中的权限标识数组

src/components/AuthComponent/index.jsx

typescript 复制代码
import React from 'react'
import { useSelector } from 'react-redux'
​
const AuthComponent = ({ permission, children, type = 'primary', ...props }) => {
  // 获取权限标识数组
  const rolePermission = useSelector((state) => state.user.userinfo.buttons)
}
export default AuthComponent

权限标识数组如下图所示,数组每项大概就是父模块名 +当前模块名 +按钮功能

2.遍历数组判断是否存在传入的权限标识

javascript 复制代码
// 遍历数组判断是否存在此权限标识
if (rolePermission.includes(permission)) {
return (
  <Button type={type} {...props}>
    {children}
  </Button>
)
}
return null

2.在组件直接编写结构

由于结构比较简单,我们直接在管理页面的组件编写结构。

src/pages/System/User/index.jsx

javascript 复制代码
const addRow = () => {
  console.log('新增')
}
const deleteRow = () => {
  console.log('删除');
}
return (
 <>
  <SearchBar formItemList={formItemList} getSearchParams={getSearchParams} />
  <Card>
    <Space>
      <AuthComponent permission="system:user:add" onClick={addRow}>
        新增
      </AuthComponent>
      <Popconfirm title="删除用户" description="确定要删除吗?" onConfirm={deleteRow}>
        <AuthComponent permission="system:user:del" danger>
          批量删除
        </AuthComponent>
      </Popconfirm>
    </Space>
  </Card>
 </>
}

表格模块

表格我们用到Ant Design的Table组件,由于分页、loading、接口请求占了很大的代码量,又是重复率很高的,所以封装是有必要的。

封装Table组件

首先我们先有个大概的雏形,请求表格的接口方法fetchMethod我们肯定是要传入的

src/components/CustomTable/index.jsx

javascript 复制代码
import React from 'react'
import { Table } from 'antd'
const CustomTable = ({ fetchMethod, ...resetTableProps }) => {
  return (
    <Table
      {...resetTableProps}
    />
  )
}
export default CustomTable

然后我们看一下Table的配置项

其中最重要的就是columns表格列配置,这个我们在父组件中传入【包含展示列、操作列】,它的配置比较重要的有

  • dataIndex:列数据在数据项中对应的路径,支持通过数组查询嵌套路径
  • render:生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,function(text, record, index) {},可以用它来自定义渲染效果
  • title:列头显示标题

用户管理表格列配置示例如下,其中rowkey字段必须传入为数组中每列表格项唯一标识字段

src/pages/System/User/index.jsx

javascript 复制代码
// 导入需要在表格列展示的组件
import {Tag} from 'Antd'
// 导入api
import userApi from '@/api/user'
...
// 表格配置项
const columns = [
{
  title: '用户编号',
  dataIndex: 'user_id',
  align: 'center'
},
{
  title: '用户名',
  dataIndex: 'username',
  align: 'center'
},
{
  title: '角色',
  dataIndex: 'roles',
  render: (roles) => (
    <span>
      {roles.map((item) => {
        let color = ''
        if (item.role_name === '管理员') color = 'geekblue'
        else color = 'green'
        return (
          <Tag color={color} key={item.role_id}>
            {item.role_name}
          </Tag>
        )
      })}
    </span>
  ),
  align: 'center'
},
{
  title: '状态',
  dataIndex: 'status',
  render: (status) => {
    let color = status === '1' ? 'green' : 'red'
    const statusItem = statusDict.find((item) => item.value === status)
    return statusItem ? <Tag color={color}>{statusItem.label}</Tag> : ''
  },
  align: 'center'
},
{
  title: '创建时间',
  dataIndex: 'create_time',
  align: 'center'
}]
...
return (
    <>
      ...
      <CustomTable
        columns={columns}
        rowKey="user_id"
        bordered
        fetchMethod={userApi.manage.query}
      />
    </>
  )

其次就是pagination分页的配置,我们同样在父组件传入分页的数据,再传入分页数据或搜索参数动态变化的函数onParamChange

ini 复制代码
// 表格请求参数
const [requestParam, setRequestParam] = useState({
  pageSize: 5,
  current: 1
})
  <CustomTable
    columns={columns}
    rowKey="user_id"
    bordered
    fetchMethod={userApi.manage.query}
    requestParam={requestParam}
    onParamChange={(data) => setRequestParam({ ...requestParam, ...data })}
  />

然后,我们就在自定义表格组件里接收参数,完成两个步骤。

1.请求接口

这里我们用一个自定义hook来完成,主要就是传入请求接口的方法和请求参数,传出数据数组

src/hooks/useFetchTableData.js

scss 复制代码
import { useEffect, useState } from 'react'
const useFetchTableData = (fetchMethod, params, onParamChange) => {
  const [loading, setLoading] = useState(false)
  const [tableData, setTableData] = useState([])
​
  // 获取表格数据
  async function fetchTableData() {
    try {
      setLoading(true)
      const {
        data: { count, rows }
      } = await fetchMethod({
        pageSize: params.pageSize,
        currentPage: params.current
      })
​
      setTableData({
        tableData: rows,
        total: count
      })
      // 如果删除页面最后一个元素且不是第一页,当前页数减去1
      if (!rows.length && params.current !== 1) {
        onParamChange((params) => ({ current: params.current - 1 }))
      }
    } finally {
      setLoading(false)
    }
  }
​
  useEffect(() => {
    fetchTableData()
  }, [params])
  // 导出数组数组和loading
  return { loading, tableData }
}
export default useFetchTableData
2.赋值给自定义Table组件

自定义Table组件接收hooks导出的数据数组并赋值给组件

src/components/CustomTable/index.jsx

javascript 复制代码
import React from 'react'
import { Table } from 'antd'
// 导入自定义hook
import useFetchTableData from '@/hooks/useFetchTableData'
const CustomTable = ({ fetchMethod, columns, requestParam, onParamChange, ...resetTableProps }) => {
  // 请求表格数据
  const { loading, tableData } = useFetchTableData(fetchMethod, requestParam, onParamChange)
​
  // 翻页重设参数
  const onTableChange = (page) => {
    onParamChange(page)
  }
​
  return (
    <Table
      {...resetTableProps}
      onChange={onTableChange}
      loading={loading}
      dataSource={tableData.tableData}
      columns={columns}
      pagination={{
        pageSize: requestParam.pageSize ?? 5,
        current: requestParam.current ?? 1,
        total: tableData.total,
        showTotal: (t) => <span style={{ color: '#333' }}>共{t}条</span>
      }}
    />
  )
}
export default CustomTable

到这里表格就封装完了,由于进行了属性透传,大家可以再传一些所需要的参数

相关推荐
wakangda4 小时前
React Native 集成原生Android功能
javascript·react native·react.js
秃头女孩y10 小时前
【React中最优雅的异步请求】
javascript·vue.js·react.js
前端小小王16 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发16 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
不是鱼21 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
飞翔的渴望1 天前
antd3升级antd5总结
前端·react.js·ant design
╰つ゛木槿1 天前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
用户30587584891251 天前
Connected-react-router核心思路实现
react.js
哑巴语天雨2 天前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情2 天前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js