公司后台使用 antd-ProTable 搭建,几乎每个页面有长得有相似的地方,于是想进一步封装ProTable,最小程度的增加新页面,减少复制。
通用的地方:
- 重置和查询,(查询使用的通用项提取可以看这里)
- 表头左侧有
共xx个数据 - 表头右侧有
规则说明和导出,旁边还捎带刷新和设置按钮 - 点击
规则说明,弹框显示规则介绍 - 点击
导出,先进行弹框提示,确定之后,调用导出接口
说说逻辑:
- ProTable 就能实现查询和表格,这是基调,以下属性单独处理,其他属性直接透传
- 规则说明,单独一个组件抽离,然后如果有规则文字,就显示规则说明
- 导出按钮,单独一个组件抽离,然后如果有导出接口,就显示导出。注意,导出只有数量大于 0 的时候才可用
- 预制其他的按钮的位置
- 查询条件外围传进来,统一在表格里隐藏
- 列表展示外围传进来,统一在搜索里隐藏,并居中
- 右侧功能较多的时候,提供自定义的设置
- 查询参数和导出参数进行处理,外部使用方便
- 暴露ref,外部能自己获取方法
思维导图:
整体其实没什么难度,以下是实现:
实现代码
/**
* @description: 通用表格组件
* @param {function} apiQueryList 查询接口
* @param {function} apiExportList 导出接口
* @param {array} searchColumns 查询配置
* @param {array} tableColumns 表格配置
* @param {string} rowKey 表格的key
* @param {any} formRef 查询表单的ref
* @param {ReactNode} ruleHtml 规则说明
* @param {string} ruleLink 规则说明链接
* @param {ReactNode | ReactNode[]} ButtonsElse 其他按钮
* @param {function} toolBarRender 工具栏
* @return {ReactNode}
*/
import React, { forwardRef, useImperativeHandle, useRef, useState } from "react"
import ProTable from "@ant-design/pro-components'"
import TableCount from "@/components/TableCount"
import ButtonExport from '@/components/ButtonExport'
import ButtonRule from '@/components/ButtonRule'
import { Button } from 'antd'
import { formatParams, formatSorter, formatApiQueryList } from "./utils"
type PageTableProps = {
apiQueryList: (params: any) => Promise<any>
apiExportList?: (params: any) => Promise<any>
searchColumns: any[]
tableColumns: any[]
rowKey: string
formRef: any
ruleHtml?: React.ReactNode
ruleLink?: string
ButtonsElse?: React.ReactNode[] | React.ReactNode
toolBarRender?: () => React.ReactNode[],
// 其他属性,直接透传给ProTable
[key: string]: any
}
export type PageTableRef = {
reload: () => void
getParams: () => any
}
const PageTable = forwardRef<PageTableRef, PageTableProps>(({ apiQueryList, apiExportList, searchColumns, tableColumns, rowKey, formRef, ruleHtml, ruleLink, toolBarRender, ButtonsElse, ...otherProTableProps }, ref) => {
const tableActionRef = useRef<any>(undefined)
/** 这里导出需要知道总数,所以抛出来 */
const [total, setTotal] = useState(0)
/** 这里查询的请求参数需要保存,因为导出需要,也需要向外暴露 */
const [params, setParams] = useState<any>(null)
useImperativeHandle(ref, () => ({
reload: () => {
tableActionRef.current?.reload()
},
getParams: () => ({ ...params })
}))
const columns = (() => {
// 查询条件隐藏在表格中
searchColumns?.forEach(item => {
item.hideInTable = true
})
// 表格列居中,不显示在查询条件中
tableColumns?.forEach((item: any) => {
item.hideInSearch = true
item.align = 'center'
})
return [...searchColumns, ...tableColumns]
})();
const apiList = async (params: any, sorter: any) => {
// 先验证表单,部分需要必填值
if (formRef?.current?.validateFields) {
try {
await formRef.current.validateFields()
}
catch (error) {
console.error("没有必填值", error);
return { data: [], success: false, total: 0 };
}
}
params = formatParams({ ...params, ...formatSorter(sorter) })
const { total, success, data } = await formatApiQueryList(apiQueryList, params)
setParams(params)
setTotal(total)
return { data, success, total }
}
const apiExport = (params: any) => formatApiQueryList(apiExportList, params)
toolBarRender = toolBarRender || (() => {
const res: React.ReactNode[] = []
if (ruleLink) {
res.push(<Button type="link" href={ruleLink} target="_blank" rel="noopener noreferrer">规则说明</Button>)
}
if (ruleHtml) {
res.push(<ButtonRule > {ruleHtml}</ButtonRule>)
}
if (ButtonsElse) {
Array.isArray(ButtonsElse) ? res.push(...ButtonsElse) : res.push(ButtonsElse)
}
if (apiExportList) {
res.push(<ButtonExport listLength={total} apiExport={() => { return apiExport(params) }} />)
}
return () => res
})();
return (
<>
<ProTable
toolBarRender={toolBarRender}
actionRef={tableActionRef}
formRef={formRef}
columns={columns}
rowKey={rowKey || "uuid"}
request={apiList}
search={{
defaultCollapsed: false,
collapseRender: undefined,
defaultColsNumber: 4,
}}
headerTitle={<TableCount length={total} />}
options={{ density: false }}
dateFormatter={false}
form={{ span: 6, ignoreRules: false }}
pagination={{ hideOnSinglePage: false }}
{...otherProTableProps}
/>
</>
)
})
PageTable.displayName = "PageTable"
export default PageTable
参数的通用处理utils
处理是param,sorter,统一处理参数。
import { message } from 'antd';
import uuid from 'react-uuid';
export function formatSorter(sorter: any) {
let orderColumn = null
let orderType = null
if (sorter && Object.keys(sorter).length) {
orderColumn = Object.keys(sorter)[0]
orderType = sorter[orderColumn] === 'ascend' ? 2 : 1
}
return {
orderColumn,
orderType
}
}
export function formatParams(params: any) {
const { current: pageNum, pageSize, ...otherParams } = params;
delete params.current;
Object.keys(otherParams).forEach((key) => {
// 过滤掉值为 ''、null、undefined 的字段
if (otherParams[key] === '' || otherParams[key] === null || otherParams[key] === undefined) {
delete otherParams[key];
}
// 如果是数组且长度为0,则删除该字段
if (Array.isArray(otherParams[key]) && otherParams[key].length === 0) {
delete otherParams[key];
}
});
return {
pageNum,
pageSize,
...otherParams
}
}
export function formatApiQueryList(apiMethod: any, params: any) {
return apiMethod(params)
.then((res: any) => {
const { success, message: msg, data } = res
if (!success) {
message.error(msg || '服务开小差了,请稍后再试')
return {
data: [],
success: true,
total: 0
}
}
const { list, total } = data || { list: [], total: 0 }
if (!list || !Array.isArray(list)) {
return {
data: [],
success: true,
total: 0
}
}
list?.forEach((item: any, index: number) => {
if (item.idx === undefined) {
item.idx = params.pageSize * (params.pageNum - 1) + index + 1
}
if (item.uuid === undefined) {
item.uuid = uuid()
}
})
return {
data: list,
success: true,
total
}
})
}
export function formatApiExportList(apiMethod: any, params: any) {
if (params.pageSize) delete params.pageSize
if (params.pageNum) delete params.pageNum
return apiMethod(params)
.then((res: any) => {
const { success, message: msg } = res
if (!success) {
message.error(msg || '服务开小差了,请稍后再试')
return
}
return res
})
}
使用的时候,就狠狠方便了,只关注业务层面即可!
使用的时候,就狠狠方便了,只关注业务层面即可!
使用的时候,就狠狠方便了,只关注业务层面即可!
import PageTable from '@/components/PageTableNew';
import { apiQuery, apiExportList } from './service';
import useColumns from './hooks/useColumns';
const SchoolData: React.FC = () => {
const formActionRef = useRef<any>(null); // 表单操作引用
const { formRef, searchColumns, tableColumns } = useColumns();
// 可以在这里处理参数逻辑
const apiQueryList = (params: any) => {
params.activityType = tabType;
return apiQuery(params);
};
const reload = () => {
formActionRef.current?.reload();
};
const getFormValues = () => {
return formRef.current?.getFieldsValue() || {};
};
return <PageTable
apiQueryList={apiQueryList} // 查询API
apiExportList={apiExportList} // 导出API
searchColumns={searchColumns} // 查询列配置
tableColumns={tableColumns} // 表格列配置
formRef={formRef} // 表单引用
actionRef={formActionRef} // 表单操作引用
ButtonsElse={[
<Button type="primary">新建</Button>
]}
/>
}
对于service.ts
import { BASE_URL } from '@/utils/define';
import { request } from '@umijs/max';
// 查询
export const apiQuery = (data: any) => {
return request(`${BASE_URL}/npad-operation/npad/experience/activity/list`, {
method: 'POST',
data,
});
};
// 导出
export const apiExportList = (params: any) => {
return request(`${BASE_URL}/npad-operation/npad/experience/activity/list/export`, {
method: 'POST',
data,
});
}
对应useColumns
export function useColumns() {
const formRef = useRef<any>(null)
const searchColumns = [
{
title: '活动编码',
dataIndex: 'uniActivityId',
},
]
const tableColumns = [
{
title: '创建时间',
dataIndex: 'activityTime',
width: 180,
},
]
return {searchColumns,tableColumns,formRef }
}
导出按钮的封装
常用的配置也可以放在一个组件单独封装
import { message } from 'antd';
import { CloudDownloadOutlined } from '@ant-design/icons';
import { Button, Modal } from 'antd';
import { useRequest } from 'ahooks';
const ButtonExport = ({ listLength, apiExport }: any) => {
const { run: exportList, loading } = useRequest(apiExport, {
manual: true,
onSuccess: (res) => {
res.success ? message.success('导出成功') : message.error(res.message);
},
});
return (
<Button
key="export"
type="primary"
onClick={() => {
Modal.confirm({
title: '导出提示',
content:
'您下载的数据报表会自动发送到您的企业邮箱,登陆企业邮箱后在链接里查看数据报表。是否确认下载报表吗?',
maskClosable: true,
onOk: exportList,
});
}}
loading={loading}
disabled={!listLength}
>
<CloudDownloadOutlined /> 导出
</Button>
);
};
export default ButtonExport;
规则说明的封装
import React, { useRef } from 'react';
import { Button } from 'antd';
import ModalRule, { ModalRuleRef } from '@/components/ModalRule';
type ButtonRuleProps = {
children: React.ReactNode;
};
const ButtonRule: React.FC<ButtonRuleProps> = ({ children }) => {
const ruleModalRef = useRef<ModalRuleRef | null>(null);
return (
<>
<Button
key="rule"
type="link"
style={{ marginRight: 10 }}
onClick={() => {
ruleModalRef.current?.open();
}}
>
规则说明
</Button>
<ModalRule ref={ruleModalRef}> {children} </ModalRule>
</>
);
};
ButtonRule.displayName = 'ButtonRule';
export default ButtonRule;
因为弹框规则说明,有大段 html,所以单独组件放出去了
import React, { useImperativeHandle, useState, forwardRef } from 'react';
import { Modal } from 'antd';
type RuleProps = {
children: React.ReactNode;
};
export type ModalRuleRef = {
open: () => void;
close: () => void;
};
const Rule = forwardRef<ModalRuleRef, RuleProps>((props, ref) => {
const [visible, setVisible] = useState(false);
const close = () => {
setVisible(false);
};
// 只暴露想暴露的方法
useImperativeHandle(ref, () => ({
open: () => {
setVisible(true);
},
close,
}));
const modalProps = {
title: '规则说明',
visible: visible,
footer: null,
width: 600,
onCancel: close,
};
return <Modal {...modalProps}> {props.children} </Modal>;
});
export default Rule;
抛砖引玉,如果有类似需求的,可以参考(^▽^)