React基于antd Table实现可拖拽调整列宽的表格

实现功能
1:表格列宽初始自动分配、列宽总和不能超过容器宽度(无横向滚动条,公司项目特殊需求)
2:当容器宽度变化时,保持当前列宽的分配比例,等比缩小
3:拖动过程中不进行列宽的调整,只有释放之后再进行列宽调整
效果图见在这里插入图片描述
目录结构:
在这里插入代码片在这里插入图片描述
useTableCol.tsx: 处理表格列的宽度计算等相关逻辑

import { useMemoizedFn, useSafeState } from 'ahooks';
import type { ColumnType } from 'antd/es/table/interface';
import { useEffect, useRef, useCallback } from 'react';

const useTableCol = (wrapperWidth: number | undefined, columns: ColumnType<any>[]) => {
  const [isInit, setInit] = useSafeState<boolean>(false);
  // 保存每一列宽度的百分比,用来当容器的宽度变化时,计算每列的宽度
  const titleWidthMapRef = useRef<{ titleWidthMap: Record<string, number> | undefined }>({ titleWidthMap: undefined });
  // 每一列的宽度转换成数字之后的列配置
  const [tableShowColumns, setTableShowColumns] = useSafeState<ColumnType<any>[]>([]);

  // 初始时,将传入的表格配置数据进行转换
  // 将百分比、字符串格式的宽度配置转换成对应的数字宽度
  // 并根据容器的宽度做自适应
  const getTableNumberWidthCol = useMemoizedFn(() => {
    let resultTableColumesList: ColumnType<any>[] = [];
    if (wrapperWidth && columns) {
      // TODO: 筛选出所有显示的列
      const showCols = columns.filter((col) => col);
      const newColumesList = showCols.map((col) => {
        const { width } = col;
        const newCol = { ...col };
        // 当配置了width属性,且width为字符串类型时,计算具体的宽度值
        if (width && typeof width === 'string') {
          newCol.width = width.endsWith('%') ? (wrapperWidth * parseFloat(width)) / 100 : parseFloat(width);
        }
        return newCol;
      });
      // 表格总的宽度
      const totalWidth = newColumesList
        .filter((item) => typeof item.width === 'number')
        .reduce((sum, current) => sum + Number(current.width), 0);
      // 查找出未配置宽度的列
      const noWidthColumes = newColumesList.filter((col) => !col.width);
      // 如果存在未配置宽度的列,则将容器未分配的宽度,等分给未分配宽度的列
      if (noWidthColumes.length > 0) {
        const otherWidth = wrapperWidth - totalWidth;
        if (otherWidth > 0) {
          // 为了简单,向下取整,并将差值放到最后一列
          const commonWidth = Math.floor(otherWidth / noWidthColumes.length);
          const resultColumes = newColumesList.map((col) => {
            if (!col.width) {
              // 最后一个未配置宽度的列宽取差值
              if (col.title === noWidthColumes[noWidthColumes.length - 1].title) {
                col.width = otherWidth - commonWidth * (noWidthColumes.length - 1);
              } else {
                // 非最后一个未配置宽度的列,则取均值的向下取整值
                col.width = commonWidth;
              }
            }
            return col;
          });
          resultTableColumesList = resultColumes;
        } else {
          // 存在未分配宽度的列,但是列的已分配宽度大于容器宽度,此处正常情况下不应出现
          // 若出现了此情况,则给无列宽的列都分配60px的宽度,其他有列宽的需要同等缩小
          const needWidth = 60 * noWidthColumes.length + Math.abs(otherWidth);
          const showColWithWidth = newColumesList.length - noWidthColumes.length;
          if (showColWithWidth > 0) {
            const averageWidth = Math.floor(needWidth / showColWithWidth);
            const lastWidth = needWidth - averageWidth * (showColWithWidth - 1);
            const resultColumes = newColumesList.map((col) => {
              if (!col.width) {
                // 最后一个未配置宽度的列宽取差值
                if (col.title === noWidthColumes[noWidthColumes.length - 1].title) {
                  col.width = lastWidth;
                } else {
                  // 非最后一个未配置宽度的列,则取均值的向下取整值
                  col.width = averageWidth;
                }
              }
              return col;
            });
            resultTableColumesList = resultColumes;
          }
        }
      } else {
        const otherWidth = totalWidth - wrapperWidth;
        const averageWidth = Math.floor(otherWidth / newColumesList.length);
        const lastWidth = otherWidth - averageWidth * (newColumesList.length - 1);
        const resultColumes = newColumesList.map((col, index) => {
          if (index !== newColumesList.length - 1) {
            return { ...col, width: Number(col.width) - averageWidth };
          }
          return { ...col, width: Number(col.width) - lastWidth };
        });
        resultTableColumesList = resultColumes;
      }
    }
    return resultTableColumesList;
  });
  // 更新列宽占容器百分比的方法,若表格列支持拖拽,则需提供给拖拽方法,每次拖拽结束后,更新值
  const updateTitleWidthMap = useCallback(
    (result: Record<string, number>) => {
      titleWidthMapRef.current.titleWidthMap = result;
    },
    [titleWidthMapRef],
  );

  // 将数字列宽所占百分比保存下来,用以当容器的宽度变更时,做自适应处理
  const setTitleWidthMapMethod = useMemoizedFn((colList: ColumnType<any>[], allWidth?: number) => {
    if (allWidth) {
      const result: Record<string, number> = {};
      colList.forEach(({ width }, index) => {
        result[`_${index}`] = parseFloat(((width as number) / allWidth).toFixed(2));
      });
      updateTitleWidthMap(result);
    }
  });
  // 此useEffect为第一次执行表格渲染时,生成对应的列配置
  useEffect(() => {
    // 初始化时,根据配置项,设置表格列的宽度,并记录对应百分比
    if (wrapperWidth && !isInit) {
      const resultTableCol = getTableNumberWidthCol();
      setTitleWidthMapMethod(resultTableCol, wrapperWidth);
      setTableShowColumns(resultTableCol);
      setInit(true);
    }
  }, [
    isInit,
    wrapperWidth,
    tableShowColumns,
    setInit,
    setTableShowColumns,
    getTableNumberWidthCol,
    setTitleWidthMapMethod,
  ]);

  // 当容器宽度变化时,根据每列所占的比例,重新结算列宽
  useEffect(() => {
    if (wrapperWidth && isInit) {
      setTableShowColumns((oldColumns) => {
        const result: ColumnType<any>[] = [];
        const titleWidthMap = titleWidthMapRef?.current?.titleWidthMap;
        oldColumns.forEach((col, index) => {
          const pervent = titleWidthMap?.[`_${index}`];
          result.push({
            ...col,
            width: wrapperWidth * pervent!,
          });
        });
        const totalWidth = result.reduce((sum, cur) => sum + parseFloat(`${cur.width!}`), 0);
        result[result.length - 1].width = wrapperWidth + parseFloat(`${result[result.length - 1].width!}`) - totalWidth;
        return result;
      });
    }
  }, [isInit, wrapperWidth, titleWidthMapRef, setTableShowColumns]);

  return {
    tableShowColumns,
    isInit,
    setTitleWidthMapMethod,
  } as const;
};

export default useTableCol;


useResizeTableCol.tsx:将表格的列转成可拖拽列,增加相关方法

import { useMemoizedFn, useSafeState } from 'ahooks';
import type { ColumnType } from 'antd/lib/table';
import { useState, useEffect } from 'react';
import useTableCol from './useTableCol';

const useResizeTableCol = (wrapperWidth: number | undefined, tableRef: any, columns: ColumnType<any>[]) => {
  const [colIsInit, setColInit] = useSafeState<boolean>(false);
  const [tableColumns, setTableColumns] = useState<ColumnType<any>[]>(columns);
  const { tableShowColumns, isInit, setTitleWidthMapMethod } = useTableCol(wrapperWidth, columns);

  const handleResize = useMemoizedFn((index: number) => (e: any, { size }: any) => {
    e.stopImmediatePropagation();
    if (tableRef.current) {
      const widthList = [
        ...(tableRef.current as HTMLElement).querySelectorAll('.ant-table-thead th.react-resizable'),
      ].map((th) => {
        return (th as HTMLElement).getBoundingClientRect().width;
      });
      setTableColumns((col) => {
        const nextColumns = [...col];
        const { width: oldWidth } = nextColumns[index];
        // 此次平移的宽度
        const offsetWidth = size.width - Number(oldWidth || 0);
        // 当前列得宽度
        const currentWidth = widthList[index] + offsetWidth;
        const nextWidth = widthList[index + 1] - offsetWidth;
        // 左移,当前宽度小于42
        if (currentWidth < 42) {
          widthList[index] = 42;
          widthList[index + 1] = nextWidth - 42 + currentWidth;
        } else if (nextWidth < 42) {
          // 右移,下一列得宽度小于42
          widthList[index] = currentWidth - 42 + nextWidth;
          widthList[index + 1] = 42;
        } else {
          widthList[index] = currentWidth;
          widthList[index + 1] = nextWidth;
        }
        console.log(widthList);
        const resultColumns = nextColumns.map((nextCol, _index) => ({
          ...nextCol,
          width: widthList[_index],
          onHeaderCell:
            _index !== nextColumns.length - 1
              ? () => ({
                  width: widthList[_index],
                  onResize: handleResize(_index),
                })
              : undefined,
        }));
        setTitleWidthMapMethod(resultColumns, wrapperWidth);
        return resultColumns;
      });
    }
  });

  useEffect(() => {
    if (isInit) {
      setTableColumns(
        tableShowColumns.map((col, index) => ({
          ...col,
          onHeaderCell:
            index !== tableShowColumns.length - 1
              ? () => ({
                  width: col.width,
                  onResize: handleResize(index),
                })
              : undefined,
        })),
      );
      setColInit(true);
    }
  }, [tableShowColumns, isInit, setTableColumns, handleResize, setColInit]);

  return {
    colIsInit,
    tableColumns,
  } as const;
};

export default useResizeTableCol;

ResizeableTitle.tsx:自定义可拖动的表头th

import React, { useMemo, useState } from 'react';
import type { ResizeCallbackData } from 'react-resizable';
import { Resizable } from 'react-resizable';

const ResizeableTitle: React.FC<any> = (props) => {
  const { width, className, children, onResize, style = {}, ...resetProps } = props;
  const [offset, setOffset] = useState<number>(0);
  const [nextWidth, setNextWidth] = useState<number>(58);

  const getTranslateX = useMemo(() => {
    if (offset >= nextWidth + 42) {
      return nextWidth - 42;
    }
    return offset;
  }, [offset, nextWidth]);
  if (className?.includes('ant-table-selection-column')) {
    return (
      <th className={className} {...resetProps}>
        {children}
      </th>
    );
  }
  // console.log(props);
  if (onResize) {
    return (
      <Resizable
        width={width + offset}
        height={0}
        handle={
          <span
            className={`react-resizable-handle ${offset ? 'active' : ''}`}
            style={{ transform: `translateX(${getTranslateX}px)` }}
            onClick={(e) => {
              e.stopPropagation();
              e.preventDefault();
            }}
          />
        }
        // onResizeStart={() => (this.resizing = true)}
        onResizeStop={(...arg: any[]) => {
          setOffset(0);
          onResize(...arg);
        }}
        onResizeStart={(e: any) => {
          const _nextWidth = e.target.parentNode.nextSibling.getBoundingClientRect().width;
          setNextWidth(_nextWidth);
        }}
        onResize={(e: any, { size }: ResizeCallbackData) => {
          const currentOffset = size.width - width;
          if (currentOffset > nextWidth - 42) {
            setOffset(nextWidth - 42);
          } else {
            setOffset(currentOffset);
          }
        }}
        draggableOpts={{
          enableUserSelectHack: true,
          minConstraints: [width - 42, 0],
          maxConstraints: [width + nextWidth, 0],
        }}
      >
        <th className={className} style={{ ...style, width: width + 'px' }} {...resetProps}>
          <div
            style={{ width: width + 'px' }}
            className="ofs-table-cell-wrapper"
            title={typeof children.join('') === 'string' ? children.join('') : ''}
          >
            <div className="ofs-table-cell">
              {children}
            </div>
          </div>
        </th>
      </Resizable>
    );
  }
  return (
    <th className={className} style={{ ...style, width: width + 'px' }}>
      <div
        style={{ width: width + 'px' }}
        className="ofs-table-cell-wrapper"
        title={typeof children.join('') === 'string' ? children.join('') : ''}
      >
        <div {...resetProps} className="ofs-table-cell">
          {children}
        </div>
      </div>
    </th>
  );
};

export default ResizeableTitle;


index.less:表格样式

.react-resizable {
  position: relative;
}

.react-resizable-handle {
  position: absolute;
  z-index: 999;
  bottom: 0;
  right: 0;
  width: 2px;
  height: 100%;
  cursor: col-resize;
  &.active::before {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    height: 1000px;
    border-left: 2px solid #d0d0d0;
    content: '';
  }
}
.ant-table-wrapper {
  position: relative;
  overflow: hidden;
}
.ofs-table-row {
  display: flex;
  flex-direction: row;
  width: 100%;
  overflow: hidden;
}
.ofs-table-cell-wrapper {
  width: 100%;
  overflow: hidden;
}
.ofs-table-cell {
  padding: 0 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td,
.ant-table tfoot > tr > th,
.ant-table tfoot > tr > td {
  padding: 16px 0px;
}
.ant-table-thead > tr > th:last-child span.react-resizable-handle {
  display: none;
}
.ant-table-thead {
  .ant-table-cell-ellipsis {
    overflow: visible;
    & > div {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      word-break: keep-all;
    }
  }
}


index.tsx:表格实现

import React, { useEffect, useRef, useState } from 'react';
import { Table } from 'antd';
import ResizeableTitle from './components/ResizeableTitle';
import type { ColumnType } from 'antd/lib/table/interface';
import './index.less';
import useResizeTableCol from './hooks/useResizeTableCol';
import { useSize } from 'ahooks';
const columes = [
  {
    title: 'Full Name',
    width: '10%',
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: 'Age',
    width: '10%',
    dataIndex: 'age',
    key: 'age',
  },
  { title: 'Column 1', dataIndex: 'address', ellipsis: true, width: '10%', key: '1' },
  { title: 'Column 2', dataIndex: 'address', ellipsis: true, width: '10%', key: '2' },
  { title: 'Column 3', dataIndex: 'address', ellipsis: true, width: '10%', key: '3' },
  { title: 'Column 4', dataIndex: 'address', ellipsis: true, width: '10%', key: '4' },
  { title: 'Column 5', dataIndex: 'address', ellipsis: true, width: '20%', key: '5' },
  { title: 'Column 6', dataIndex: 'address', ellipsis: true, width: '20%', key: '6' },
  { title: 'Column 7', dataIndex: 'address', ellipsis: true, width: 100, key: '7' },
  { title: 'Column 8', dataIndex: 'address', ellipsis: true, width: 100, key: '8' },
  {
    title: 'aa',
    key: 'operation',
    ellipsis: true,
    width: 100,
    // fixed: 'right',
    render: () => <a>action</a>,
  },
];
const data = [
  {
    key: '1',
    name: 'John Brown',
    age: 32,
    address: 'New York Park',
  },
  {
    key: '2',
    name: 'Jim Green',
    age: 40,
    address: 'London Park',
  },
];
const AntdTableTest: React.FC = () => {
  const tableRef = useRef(null);
  const tableWrapperSize = useSize(tableRef);
  const [wrapperWidth, setWrapperWidth] = useState<number>();
  const { colIsInit, tableColumns } = useResizeTableCol(wrapperWidth, tableRef, columes);

  useEffect(() => {
    console.log(tableWrapperSize);
    if (tableWrapperSize) {
      setWrapperWidth(tableWrapperSize.width);
    }
  }, [tableRef, tableWrapperSize]);

  return (
    <>
      <div ref={tableRef}>
        {colIsInit ? (
          <Table
            rowSelection={{
              type: 'checkbox',
            }}
            columns={tableColumns as ColumnType<any>[]}
            components={{
              header: {
                cell: ResizeableTitle,
              },
            }}
            dataSource={data}
          />
        ) : null}
      </div>
    </>
  );
};

export default AntdTableTest;


  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱技术的大仙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值