从零实现一个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

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

相关推荐
September_ning几秒前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人11 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱00112 分钟前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking2 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫3 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull8 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet19 小时前
那总结下来,react就是落后了
前端·react.js
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router