前言
我在做后台管理系统的时候发现,我们的系统有大量的表格查询模块,几乎每个页面都有一个表格查询功能,所以我利用react hooks的特性以及查阅网上的文章,封装了一个适合业务的通用hooks表格组件,供大家参考。
组件目录结构
index.tsx 就是我们的组件主文件,index.less 用于自定义表格样式, type.ts 用于定义接口类型
组件实现
1. 组件的功能
- 支持自定义表头配置
- 支持自定义查询方法
- 查询时自动loading
- 支持翻页功能
- 支持配置是否初始化
2. 组件 Props
首先我们看一下组件接收哪些参数
也就是我们定义的接口 ArgTableProps
baseProps 用于接收Table组件的自带属性
owncolumns 是一个函数,用来生成 table组件需要的表头配置
queryAction 查询操作方法,返回一个异步查询方法
params 表格的查询参数
defaultCurrent 表格默认页码
noInit 初始化不直接查询操作
// types.ts
import { TableProps } from 'antd/lib/table/Table';
export interface Columns {
[propName: string]: any;
}
// 查询方法
interface queryActionType {
(arg: any): Promise<any>;
}
// 生成表头方法
export interface ColumnFunc {
(updateMethod?: queryActionType): Array<Columns>;
}
// 表格组件Props
export interface ArgTableProps {
baseProps?: TableProps<any>;
owncolumns: ColumnFunc;
queryAction: queryActionType;
params?: any;
defaultCurrent?: number;
noInit?: boolean;
}
// 页码状态
export interface paginationInitialType {
current: number;
pageSize: number;
total: number;
}
// 表格初始化状态
export interface initialStateType {
loading: boolean;
pagination: paginationInitialType;
dataSource: Array<any>;
}
// 操作类型
export interface actionType {
type: string;
payload?: any;
}
3. 使用useReducer集中管理组件状态
// 分页数据
const paginationInitial: paginationInitialType = {
current: defaultCurrent || 1,
pageSize: 10,
total: 0,
};
// table组件全量数据
const initialState: initialStateType = {
loading: false, // 加载状态
pagination: paginationInitial, // 分页状态
dataSource: [], // 表格数据
};
const reducer = useCallback((state: initialStateType, action: actionType) => {
const { payload } = action;
switch (action.type) {
case 'TOGGLE_LOADING':
return { ...state, loading: !state.loading };
case 'SET_PAGINATION':
return { ...state, pagination: payload.pagination };
case 'SET_DATA_SOURCE':
return { ...state, dataSource: payload.dataSource };
default:
return state;
}
}, []);
const [state, dispatch] = useReducer(reducer, initialState);
4. 表格的核心查询方法
我们封装的表格一个最主要的功能就是我们只需要传入一个定义好的接口方法,表格组件就可以自动查询数据,查询方法如下
async function fetchData(currentPage?: number | undefined) {
dispatch({
type: 'TOGGLE_LOADING',
});
// 分页字段名称转换
const { current: pageNum, pageSize } = state.pagination;
const concatParams = { pageNum: currentPage || pageNum, pageSize, ...params };
const res = await queryAction(concatParams).catch(err => {
console.log('请求表格数据出错了');
dispatch({ type: 'TOGGLE_LOADING' });
return { err };
});
// 关闭loading
dispatch({
type: 'TOGGLE_LOADING',
});
if (res && res.code && res.code === 1) { // 这里根据具体后台接口返回判断
const { body } = res;
const list = body && body.records ? body.records : body;
// 根据后端接口返回设置总页码数和数据总数
dispatch({
type: 'SET_PAGINATION',
payload: {
pagination: {
...state.pagination,
current: currentPage || pageNum,
total: (body && body.total) || 0,
},
},
});
// 回填list数据
dispatch({
type: 'SET_DATA_SOURCE',
payload: {
dataSource: list,
},
});
}
}
5. 组件结构
然后我们分析下组件结构,这里我们依赖 antd 的 table 组件和 paination 组件
这里 table 组件本身也有自带的页码组件,但是我们为了更好调整表格和页码布局自己引入了页码组件
return (
<Fragment>
<Table
className={styles.table_wrap}
columns={owncolumns(fetchData)}
rowKey={(record: any) => record.id}
pagination={false}
dataSource={state.dataSource}
loading={state.loading}
{...baseProps}
/>
<Row style={{ marginTop: 20 }} justify="end">
<Col>
<Pagination
defaultCurrent={1}
current={state.pagination.current}
total={state.pagination.total}
showTotal={total => `总共 ${total} 条数据`}
showSizeChanger
onChange={handleTableChange}
/>
</Col>
</Row>
</Fragment>
);
6. 执行逻辑
我们使用 useCallback 得到两个函数 fetchDataWarp 和 paramsChangeFetch
fetchDataWarp 作用是翻页状态变化时调用查询方法
paramsChangeFetch 作用是查询条件参数变化时调用查询方法
const [isInit, setIsInit] = useState(true); // 是否是第一次加载数据
const [mount, setMount] = useState(false); // 表格组件是否已挂载
// useCallback包装请求,缓存依赖,优化组件性能
const fetchDataWarp = useCallback(fetchData, [
state.pagination.current,
state.pagination.pageSize,
]);
const paramsChangeFetch = useCallback(fetchData, [params]);
useEffect(() => {
if (!(noInit && isInit)) { // 如果是第一次加载且noInit参数为true则不执行查询方法
fetchDataWarp();
}
if (isInit) {
setIsInit(false);
}
}, [fetchDataWarp]);
// 查询参数变化重置页码
useEffect(() => {
if (mount) { // 只在第一次挂载后参数变化才执行查询方法
paramsChangeFetch(1);
}
setMount(true);
}, [paramsChangeFetch]);
7. 全部代码
// index.tsx
import { Table, Pagination, Row, Col } from 'antd';
import React, { useEffect, useReducer, useCallback, Fragment, useState } from 'react';
import { ArgTableProps, paginationInitialType, initialStateType, actionType } from './type';
import styles from './index.less';
const useAsyncTable: React.FC<ArgTableProps> = props => {
const { owncolumns, queryAction, params, baseProps, defaultCurrent, noInit = false } = props;
// 分页数据
const paginationInitial: paginationInitialType = {
current: defaultCurrent || 1,
pageSize: 10,
total: 0,
};
// table组件全量数据
const initialState: initialStateType = {
loading: false,
pagination: paginationInitial,
dataSource: [],
};
const reducer = useCallback((state: initialStateType, action: actionType) => {
const { payload } = action;
switch (action.type) {
case 'TOGGLE_LOADING':
return { ...state, loading: !state.loading };
case 'SET_PAGINATION':
console.log('reducer更新', payload);
return { ...state, pagination: payload.pagination };
case 'SET_DATA_SOURCE':
return { ...state, dataSource: payload.dataSource };
default:
return state;
}
}, []);
const [state, dispatch] = useReducer(reducer, initialState);
const [isInit, setIsInit] = useState(true);
const [mount, setMount] = useState(false);
async function fetchData(currentPage?: number | undefined) {
dispatch({
type: 'TOGGLE_LOADING',
});
// 分页字段名称转换
const { current: pageNum, pageSize } = state.pagination;
const concatParams = { pageNum: currentPage || pageNum, pageSize, ...params };
const res = await queryAction(concatParams).catch(err => {
console.log('请求表格数据出错了');
dispatch({ type: 'TOGGLE_LOADING' });
return { err };
});
// 关闭loading
dispatch({
type: 'TOGGLE_LOADING',
});
if (res && res.code && res.code === 1) {
const { body } = res;
const list = body && body.records ? body.records : body;
dispatch({
type: 'SET_PAGINATION',
payload: {
pagination: {
...state.pagination,
current: currentPage || pageNum,
total: (body && body.total) || 0,
},
},
});
// 回填list数据
dispatch({
type: 'SET_DATA_SOURCE',
payload: {
dataSource: list,
},
});
}
}
// 改变页码
function handleTableChange(current: number, pageSize?: number) {
console.log('页码改变', current, pageSize);
dispatch({
type: 'SET_PAGINATION',
payload: {
pagination: {
...state.pagination,
current,
pageSize,
},
},
});
}
// useCallback包装请求,缓存依赖,优化组件性能
const fetchDataWarp = useCallback(fetchData, [
state.pagination.current,
state.pagination.pageSize,
]);
const paramsChangeFetch = useCallback(fetchData, [params]);
useEffect(() => {
console.log('页码改变,请求方法更新', `noInit: ${noInit}, isInit: ${isInit}`);
if (!(noInit && isInit)) {
console.log('执行数据列表更新');
fetchDataWarp();
}
if (isInit) {
setIsInit(false);
}
}, [fetchDataWarp]);
// 查询参数变化重置页码
useEffect(() => {
if (mount) {
paramsChangeFetch(1);
}
setMount(true);
}, [paramsChangeFetch]);
return (
<Fragment>
<Table
className={styles.table_wrap}
columns={owncolumns(fetchData)}
rowKey={(record: any) => record.id}
pagination={false}
dataSource={state.dataSource}
loading={state.loading}
{...baseProps}
/>
<Row style={{ marginTop: 20 }} justify="end">
<Col>
<Pagination
defaultCurrent={1}
current={state.pagination.current}
total={state.pagination.total}
showTotal={total => `总共 ${total} 条数据`}
showSizeChanger
onChange={handleTableChange}
/>
</Col>
</Row>
</Fragment>
);
};
export default useAsyncTable;
组件使用方法
1. 代码示例
import React, { useCallback } from 'react';
import ArgTable from '@/components/Hooks/useAsyncTable';
import getColumnFunc from '@/utils/ColumnFunc';
import { getLogTableData } from '@/server/serverList'
const Page: React.FC<any> = () => {
const [params, setParams] = useState({});
// 查询方法
const fetchData = (params: any) => getLogTableData(params);
// 获取表头方法
const getColumn = getColumnFunc.educationManage.operationLog();
// 获取表头字段(缓存数据) updateMethod 这里是刷新表格方法,用于表格内部操作调用
const cbGetColumn = useCallback(updateMethod => getColumn(updateMethod), []);
return (
<ArgTable params={params} owncolumns={cbGetColumn} queryAction={fetchData} />
)
}
简单说明一下,我们封装的表格组件主要接收三个参数 params 查询参数, owncolumns 生成表头函数方法, queryAction 查询表格接口
2. 分模块处理表格表头配置
因为我们有大量的表格表头配置,所以我选择分模块集中配置表头,方便后期维护,然后通过 getColumnFunc 方法来导出表头配置。
这里重点提一下 getColumnFunc 函数,主要是获取生成表头配置的函数,先看一下目录结构
代码实现如下:
// 生成column函数 index.ts
import educationManage, { IEducationManage } from './modules/educationManage';
import systemManage, { ISystemManage } from './modules/systemManage';
import operationManage, { IOperationManage } from './modules/operationManage';
import educationWorkbench, { IEducationWworkbench } from './modules/educationWorkbench';
import marketingWorkbench, { IMarketingWorkbench } from './modules/marketingWorkbench';
import systemClassManage, { ISystemClassManage } from './modules/systemClassManage';
import financialWorkbench, { IFinancialWorkbench } from './modules/financialWorkbench';
interface IColumnFunc {
educationManage: IEducationManage;
systemManage: ISystemManage;
operationManage: IOperationManage;
educationWorkbench: IEducationWworkbench;
marketingWorkbench: IMarketingWorkbench;
systemClassManage: ISystemClassManage;
financialWorkbench: IFinancialWorkbench;
}
const columnFunc: IColumnFunc = {
educationManage,
educationWorkbench,
systemManage,
operationManage,
marketingWorkbench,
systemClassManage,
financialWorkbench,
};
export default columnFunc;
其中一个模块的写法
// 生成column函数 xxx模块
import { ColumnProps } from 'antd/lib/table';
import React, { Fragment } from 'react';
import { formatTime } from '@/utils/utils';
export interface IEducationManage {
operationLog: () => (updata?: any) => ColumnProps<any>[];
}
const operationLog = () => (): ColumnProps<any>[] => [
{
title: '操作类型',
width: '20%',
dataIndex: 'actionType',
},
{
title: '操作说明',
dataIndex: 'msg',
width: '30%',
},
{
title: '操作人员',
width: '30%',
dataIndex: 'userName',
},
{
title: '操作时间',
dataIndex: 'createDate',
render: (text: any) => <span>{formatTime(text)}</span>,
},
];
const educationManage: IEducationManage = {
operationLog,
};
export default educationManage;
这样我们就基本上实现了一个react hooks表格组件,如果有其他业务需求也可以在此基础上进行修改,如有大佬有更好的实现建议或者问题都可以留言,感谢阅读。