封装适合自己项目的 ProTable

540 阅读5分钟

公司后台使用 antd-ProTable 搭建,几乎每个页面有长得有相似的地方,于是想进一步封装ProTable,最小程度的增加新页面,减少复制。

use_common_page.gif

通用的地方:

  • 重置和查询,(查询使用的通用项提取可以看这里
  • 表头左侧有共xx个数据
  • 表头右侧有规则说明和导出,旁边还捎带刷新和设置按钮
  • 点击规则说明,弹框显示规则介绍
  • 点击导出,先进行弹框提示,确定之后,调用导出接口

说说逻辑:

  • ProTable 就能实现查询和表格,这是基调,以下属性单独处理,其他属性直接透传
  • 规则说明,单独一个组件抽离,然后如果有规则文字,就显示规则说明
  • 导出按钮,单独一个组件抽离,然后如果有导出接口,就显示导出。注意,导出只有数量大于 0 的时候才可用
  • 预制其他的按钮的位置
  • 查询条件外围传进来,统一在表格里隐藏
  • 列表展示外围传进来,统一在搜索里隐藏,并居中
  • 右侧功能较多的时候,提供自定义的设置
  • 查询参数和导出参数进行处理,外部使用方便
  • 暴露ref,外部能自己获取方法

思维导图: PageTable.png

整体其实没什么难度,以下是实现:

实现代码

/**
 * @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;

抛砖引玉,如果有类似需求的,可以参考(^▽^)