当前位置: 首页 > 工具软件 > React-Total > 使用案例 >

二次封装 ant-pro table + react-resizable实现表头控制拉拽

卫弘义
2023-12-01

二次封装 ant-pro table + react-resizable实现表头控制拉拽

封装了 ant-pro table + react-resizable+form 可拖动表格。 拓展了一些功能,如格式化,支持保存用户表格宽度习惯,接口分页字段配置,可编辑表格等。

1 react-resizable 组件

import './index.less';
import { Resizable } from 'react-resizable';

npm install --save react-resizable
npm install --save @types/react-resizable

2 类型声明文件源

import { useRef, useState, useEffect } from 'react';
import type {
  ListToolBarProps,
  ProColumns,
  ProTableProps,
  TableRowEditable,
} from '@ant-design/pro-table';
import { EditableProTable } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import type { TablePaginationConfig } from 'antd';
import './index.less';
import { Resizable } from 'react-resizable';
import type { EditableProTableProps } from '@ant-design/pro-table/lib/components/EditableTable';
// import { cloneDeep } from 'lodash';
// import { getTableColumnsWidth, saveTableColumnsWidth } from '@/services/index';   // 用以保存用户使用表格宽度习惯

3 代码


/**
 *
 * 注意
 * 拖动表格 columns render 中的div 要自己处理 div中的宽度不能写死
 * 设置宽度时要考虑拖拽图标的20px宽度
 * 如果需要动态表头并且需要拖拽 需要传key值 每次更新刷新列表 key值不能是随机数 否则每次render 都会被执行
 *
 */

// 可拖动表格如果不需要拖拽 该列上添加属性isResizable=false  或者不设置dataIndex
type IProColumns = (ProColumns<any, any> & { isResizable?: boolean; titleName?: string })[];
interface UseTableProps {
  // 接口分页字段可能不统一  分页字段名
  paginationfield?: {
    page?: string;
    pageSize?: string;
  };
  // 其他的查询参数
  searchOtherParams?: any;
  // 设置x轴的滚动条 拖动属性为true有效
  scrollX?: number;
  // 接口名称
  api?: (params: any, params2?: any) => Promise<any>;
  // 是否拖动
  isResizable?: boolean;
  // 在fileProps中的属性会覆盖默认的属性
  fileProps?: {
    columns: IProColumns;
  } & ProTableProps<any, any>;

  // 会与默认分页拼接
  pagination?: false | TablePaginationConfig | undefined;
  // 会与默认工具栏拼接
  toolbar?: ListToolBarProps | undefined;
  // 编辑属性
  editable?: TableRowEditable<any>;
  // 编辑表格传入的属性
  editFileProps?: EditableProTableProps<any, any>;
  // 请求数据后转化的数据格式方法
  formatData?: (parasms: any) => any;
  tableName?: string; // 表格名称
}
// 拖拽方法
const ResizeableTitle = (resizeProps: { [x: string]: any; cresize: any; width: any }) => {
  const { cresize, width, isResizable, ...restProps } = resizeProps;
  delete restProps.cresize;
  if (!width) {
    return <th {...restProps} />;
  }
  // 如果 isResizable 为false 不添加拖拽
  if (isResizable === false) {
    return <th {...restProps} />;
  }

  return (
    <Resizable
      width={width}
      height={0}
      onResize={cresize}
      draggableOpts={{ enableUserSelectHack: false }}
    >
      <th {...restProps} />
    </Resizable>
  );
};

const MyProTable = (props: UseTableProps) => {
  // 判断当前页面的表格
  const [pathName] = useState<string>(location.pathname);
  const tableDom = useRef<HTMLDivElement>(null);
  const columnsWidthObj = useRef<Record<string, number | string>>({});
  const isDragegringRef = useRef<boolean>(false); // 是否正在拖动表头
  // 初始列
  const [defaultColumns, setDdefaultColumns] = useState<IProColumns>(
    props.fileProps?.columns || [],
  );
  // 滚动条 拖拽必需设置
  const [scrollX, setScrollX] = useState(0);
  // 覆盖默认的 table 元素
  const [components, setComponents] = useState<any>({
    cell: ResizeableTitle,
  });

  // 设置表头宽度为接接口返回的宽度
  const setColumnsWidth = (data: Record<string, string | number | undefined>) => {
    defaultColumns.forEach((item) => {
      if (typeof item.title === 'string') {
        if (data[item.title]) {
          item.width = data[item.title];
        }
      } else if (typeof item.titleName === 'string') {
        if (data[item.titleName]) {
          item.width = data[item.titleName];
        }
      }
    });
    setDdefaultColumns([...defaultColumns]);
  };

  useEffect(() => {
    if (props.isResizable) {
      setComponents({
        header: {
          cell: ResizeableTitle,
        },
      });
    }
  }, []);

  useEffect(() => {
    // 获取当前Table的所有TH
    let eles: any = null;
    const fn = (e: { stopPropagation: () => void }) => {
      e.stopPropagation();
    };
    setTimeout(() => {
      // location.pathname === pathName  是防止切换页面执行以下方法
      // 只有当前页面改变才会触发 切换页面会触发props.fileProps?.columns 依赖的useEffect 导致列的宽度不准确
      if (tableDom.current && location.pathname === pathName) {
        const theadThs = tableDom.current.getElementsByTagName('TH');
        // 记录隐藏列的个数    theadThs获取不到隐藏列的dom
        let count = 0;
        // 如果带复选框 不计算复选框的宽度
        if (props.fileProps?.rowSelection) {
          count -= 1;
        }
        // const columnsCopy = cloneDeep(props.fileProps?.columns);
        const columnsCopy = props.fileProps?.columns;

        // 获取当天所有TH的宽度总和 赋值给x轴的滚动条
        const total =
          columnsCopy?.reduce((num, item, index) => {
            if (item.hideInTable === true) {
              count += 1;
              return num;
            }
            // 修改默认列的宽度  (可能列设置了百分比或者没有设置宽度)
            if (theadThs[index - count]?.clientWidth) {
              Object.assign(item, {
                //  100 是防止报错
                width: theadThs[index - count]?.clientWidth + 0 || 100,
              });
            }
            // 如果没有dataIndex 给拖拽属性设置false 不添加拖拽功能
            if (!item.dataIndex) {
              Object.assign(item, {
                isResizable: false,
              });
            }
            return Number(item.width) + num;
          }, 0) || scrollX;
        // 表头宽度总和小于 传入的 scrollX 则用 计算宽度 否则用传入的 scrollX
        const num = total < (props?.scrollX || 0) ? total : props?.scrollX || 0;
        setScrollX(num);
        if (props.fileProps) {
          setDdefaultColumns(columnsCopy as IProColumns);
          Object.assign(props.fileProps, {
            columns: columnsCopy,
          });
        }
      }
      // 查找所有拖拽图标添加阻止冒泡 (拖拽和排序会冲突)
      eles = document.querySelectorAll('.react-resizable-handle');
      if (eles && eles.length > 0) {
        eles.forEach((item: any) => {
          item.addEventListener('click', fn);
        });
      }
    }, 500);
    return () => {
      if (eles && eles.length > 0) {
        eles.forEach((item: any) => {
          item.removeEventListener('click', fn);
        });
      }
    };
    // 接口获取值更新columns后刷新 比如有数据字典的数据走接口 请求完成之前已经执行 导致请求后列上没有数据 所以添加依赖
    // 但是会导致切换页面也会执行 所以在上方通过路由判断只有当前页面触发才会执行
  }, [props.fileProps?.columns]);

  // 监听鼠标弹起
  useEffect(() => {
    // 获取表格宽度接口
    if (props.tableName) {
      // 获取用户使用表格宽度习惯
      // getTableColumnsWidth({ tableName: props.tableName }).then((res) => {
      //   try {
      //     const data = JSON.parse(res.data.headerLength);
      //     columnsWidthObj.current = data;
      //     setColumnsWidth(data);
      //   } catch (error) {
      //     console.error(error);
      //   }
      // });
    }

    const fn = () => {
      if (isDragegringRef.current && props.tableName) {
        setDdefaultColumns((columns) => {
          const obj: Record<string, any> = {};
          columns.forEach((item) => {
            if (typeof item.title === 'string') {
              obj[item.title] = item.width;
            } else if (typeof item.titleName === 'string') {
              obj[item.titleName] = item.width;
            } else {
              console.error(
                '因为表头需要保存宽度,请在columns里面的加上title或者titleName为string类型',
              );
            }
          });
          // 保存表格宽度使用习惯
          // saveTableColumnsWidth({ tableName: props.tableName, headerLength: JSON.stringify(obj) });
          return columns;
        });
        isDragegringRef.current = false;
      }
    };
    window.addEventListener('mouseup', fn);
    return () => {
      window.removeEventListener('mouseup', fn);
    };
  }, []);

  // 查询数据
  const queryData = async (params: any) => {
    if (props.api) {
      const { pageSize, current } = params;
      // 接口的参数 页码和页数可能不统一
      delete params.current;
      delete params.pageSize;
      const res = await props.api({
        ...params,
        [props.paginationfield?.pageSize || 'pageSize']: pageSize,
        [props.paginationfield?.page || 'page']: current,
        ...props.searchOtherParams,
      });
      const data = props.formatData
        ? props.formatData(res.data.records || res.data)
        : res.data.records || res.data;

      return {
        data,
        success: res.code === 200,
        total: res.data.records ? res.data.total : res.data.length,
      };
    }
    return {
      data: [],
      success: false,
      total: 0,
    };
  };

  // 表格所有属性
  const tableAllProps: ProTableProps<any, any> = {
    rowKey: 'id',
    size: 'small',
    pagination: {
      pageSize: 10,
      ...props.pagination,
    },
    toolbar: {
      ...props.toolbar,
    },

    request: async (params = {}) => queryData(params),
    ...props.fileProps,
  };

  // 拖动传true 会添加自定义表头和拖拽方法
  if (props.isResizable && defaultColumns) {
    const columns = defaultColumns.map((col, index) => ({
      ...col,
      onHeaderCell: (): any => {
        return {
          width: col.width || 100,
          isResizable: col.isResizable,
          cresize: (_e: any, { size }: { size: any }) => {
            // console.log(11111);
            isDragegringRef.current = true;
            const nextColumns = [...columns];
            nextColumns[index] = {
              ...nextColumns[index],
              width: size.width,
            };
            // console.log(nextColumns)
            const total = nextColumns.reduce((num, item) => {
              return Number(item.width) + num;
            }, 0);
            // 如果宽度总和小于传入的宽度 使用计算的宽度
            if (total < scrollX) {
              setScrollX(total);
            }
            setDdefaultColumns(nextColumns);
          },
        };
      },
    }));
    Object.assign(tableAllProps, {
      columns,
    });
    tableAllProps.scroll = props.fileProps?.scroll
      ? { ...props.fileProps?.scroll, x: scrollX }
      : { x: scrollX };

    tableAllProps.components = components;
    tableAllProps.tableLayout = 'fixed';
  }

  const editFileProps: EditableProTableProps<any, any> = {
    rowKey: 'id',

    editable: {
      type: 'multiple',
      ...props.editable,
    },
    ...props.editFileProps,
  };

  return (
    <div ref={tableDom}>
      {props.editFileProps ? (
        <EditableProTable {...editFileProps} />
      ) : (
        <ProTable {...tableAllProps} />
      )}
    </div>
  );
};

// 设置组件默认props
MyProTable.defaultProps = { isResizable: true };

export default MyProTable;

 类似资料: