前言
完成了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} />
</>
)
这个模块就封装完了,具体效果参考模块顶部图。
功能按钮模块
功能按钮模块比较简单,做一个按钮栏放置功能按钮即可。
但需要注意的是,可能有的角色并没有操作一些按钮的权限。关于这个我们在每个按钮上都可以定义一个权限标识 字段,当该用户的权限按钮数组有按钮的权限标识 ,我们就展示按钮。权限按钮数组之前我们在Redux
的user
切片已经存储了,我们再封装一个权限按钮组件来判断是否角色拥有权限即可。
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
到这里表格就封装完了,由于进行了属性透传,大家可以再传一些所需要的参数