基于antd protable/table的自定义表格列宽拖动

1,426 阅读4分钟

前言

一开始遇到给表格增加列宽拖动的需求,所以引用了网上现有的‘react-resizable’插件,但是在实际的使用过程中,发现还没有element table自带的列宽拖动好用,‘react-resizable’是实时拖动的变化的,所以也没法快速拖动,搜索了一下网上现有的,目前也只有一个‘react-resizable’,索性自己封装了一个组件,还稍显粗糙,发出来沟流沟流,

废话不多说,先上代码:

组件部分代码:

/**
 * @description:1:需要每一列默认有宽度,2:目前不支持多表头列的拖拽
 *
 */
import React, { useEffect, useRef, useState } from 'react';
import './index.less';

export const ResizableHeaderCell = (props: any) => {
  const { onResize, width, children, ...restProps } = props;
  const [dragging, setDragging] = useState(false);
  const dragLineRef = useRef<HTMLDivElement | null>(null);
  const draggingRef = useRef(false); // 新增
  const startX = useRef(0);
  const startWidth = useRef(0);
  const cellRef = useRef<HTMLDivElement | null>(null);

  // 鼠标按下时触发
  const handleMouseDown = (e: React.MouseEvent) => {
    e.preventDefault();
    startX.current = e.clientX;
    setDragging(true);
    draggingRef.current = true; // 同时更新 useRef
    startWidth.current = width;
    const lineHeight = (document.querySelector('.ant-table-body') as HTMLElement)?.offsetHeight;
    const dragLine = document.createElement('div');
    dragLine.className = 'drag-line';
    dragLine.style.height = `${80}px`;
    dragLine.style.width = '2px';
    dragLine.style.position = 'absolute';
    dragLine.style.left = `${e.clientX}px`;
    document.body.appendChild(dragLine);
    dragLineRef.current = dragLine;

    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  //鼠标放开时触发
  const handleMouseUp = (e: MouseEvent) => {
    setDragging(false);
    draggingRef.current = false; // 同时更新 useRef
    const newWidth = startWidth.current + (e.clientX - startX.current);
    
    // console.log(width, 'startWidth.current = width--原本的宽度');
    // console.log(e.clientX - startX.current, '移动的宽度');
    // console.log(newWidth, 'newWidth---移动后的宽度');
    if (onResize) {
      onResize(newWidth);
    }
    if (dragLineRef.current) {
      dragLineRef.current.parentElement?.removeChild(dragLineRef.current);
      dragLineRef.current = null;
    }


    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('mouseup', handleMouseUp);
  };

  //鼠标移动时触发
  const handleMouseMove = (e: MouseEvent) => {
    if (draggingRef.current && dragLineRef.current) {
      dragLineRef.current.style.left = `${e.clientX}px`;
    }
  };

  useEffect(() => {
    // 组件卸载时移除事件监听器
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, []);

  return (
    <th ref={cellRef} className="resize-th" style={{ width }} {...restProps}>
      <div className="resize-container">{children}</div>
      <div className="resize-handle" onMouseDown={handleMouseDown}></div>
    </th>
  );
};

less

.resize-th::before {
  display: flex;
  cursor: pointer !important;
}

.resize-container {
  position: relative;
  height: 100%;
}

.resize-handle {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 5px !important;
  height: 100%;
  cursor: col-resize;
  z-index: 1;
}

.drag-line {
  top: 220px;
  z-index:1000;
  background: #f0a76f;
}

首先需要说一下,光看组件,使用起来还是有点问题,因为还需要在tsx文件中,对数据进行一些处理,下面是使用举例:

import { ResizableHeaderCell } from '@/components/MultilistHeadResizable';

const initColumns: ProColumns<StoreInfoTableListItem>[] = [...]
 const [columns, setColumns] = useState<ProColumns<StoreInfoTableListItem>[]>(initColumns);

  const handleResize =
    (index: number) =>
    (newWidth: any) => {
      setColumns((prevColumns) => {
        const nextColumns = [...prevColumns];
        nextColumns[index] = {
          ...nextColumns[index],
          width:newWidth,
        };
        localStorage.setItem('Store-Info-tableColumns', JSON.stringify(nextColumns));
        return nextColumns;
      });
    };

  const mergedColumns = columns.map((col, index) => ({
    ...col,
    onHeaderCell: (column: ProColumns<any>) => ({
      width: column.width,
      onResize: handleResize(index),
    }),
  }));
  
  return (
    <>
      <ProTable<StoreInfoTableListItem>
        className="proTable_style noTitle_Select_Setting"
        rowKey="dimBInfoId"
        actionRef={actionRef}
        columns={mergedColumns as ProColumns<StoreInfoTableListItem>[]}
        components={{
          header: {
            cell: ResizableHeaderCell,
          },
        }}
        request={getListData}
        rowSelection={{
          alwaysShowAlert: true, 
          selectedRowKeys,
          onChange: onSelectChange,
        }}
        columnsState={{
          persistenceKey: 'StoreInfoShipNewSetting',
          persistenceType: 'localStorage',
        }}
        tableAlertRender={false}/>

实现思路

主要是根据ProTable的components自定义了一个表头,另外这里还是有坑存在的,比如你columns使用了render,renderFormItem等等,在缓存的时候是要出问题的,需要自己处理一下,如果不缓存就没问题,另外mergedColumns方法,handleResize方法是可以根据实际情况使用做改变的。我举个双表头的例子:


  const handleResize = (key: string) => {
    return (newWidth: number) => {
      setColumns((prevColumns) => {
        const updateColumns = (
          cols: ProColumns<any>[],
        ): ProColumns<any>[] => {
          return cols.map((col) => {
            if (col.key === key) {
              return {
                ...col,
                width: newWidth,
              };
            }
            if (col.children) {
              return {
                ...col,
                children: updateColumns(col.children),
              };
            }
            return col;
          });
        };
        const nextColumns = updateColumns([...prevColumns]);
        localStorage.setItem('tableColumns', JSON.stringify(nextColumns));
        return nextColumns;
      });
    };
  };

  // 递归遍历 columns 并生成 mergedColumns
  const getMergedColumns = (
    cols: ProColumns<any>[],
  ): ProColumns<any>[] => {
    return cols.map((col: any) => {
      if (col.children) {
        return {
          ...col,
          children: getMergedColumns(col.children),
          onHeaderCell: (column: ProColumns<any>) => ({
            width: column.width,
            onResize: handleResize(col.key as string) as any,
          }),
        };
      }
      return {
        ...col,
        onHeaderCell: (column: ProColumns<any>) => ({
          width: column.width,
          onResize: handleResize(col.key as string) as any,
        }),
      };
    });
  };

  const mergedColumns: ProColumns<any>[] = getMergedColumns(columns);

总结

虽然实现了需求,但是代码上看起来挺臃肿的,如果有更好,更简洁的处理方法,欢迎交流~好了散会

补充: 上一篇介绍了自定义的列宽拖动方法,后面嫌弃每个页面都写,挺麻烦的,所以抽离了部分公共代码,单独封装成了hooks,看此方法之前,请先阅读我前一篇文章。

好久没弄了,代码有点乱,我先上图片,代码可以直接复制下面代码:

image.png

image.png

image.png

`import { useState, useEffect } from 'react';

const useResizableColumns = <T extends any[]>( initialColumns: T, storageKey: string, renderFunctions: Record<string, Function>, ) => { // 状态:列配置 const [columns, setColumns] = useState(initialColumns);

// 初始化:从 localStorage 中读取列配置,并重新绑定渲染逻辑 useEffect(() => { const storedColumns = localStorage.getItem(storageKey); if (storedColumns) { try { const parsedColumns = JSON.parse(storedColumns); // 重新绑定渲染逻辑 const columnsWithMethods = injectRenderFunctions(parsedColumns, renderFunctions); setColumns(columnsWithMethods as T); // 确保类型一致 } catch (error) { console.error('Failed to parse stored columns:', error); } } }, [storageKey, renderFunctions]);

const injectRenderFunctions = (cols: any[], renderFunctions: Record<string, Function>): any[] => { return cols.map((col) => { // 如果当前列有 children,递归处理子列 if (col.children) { return { ...col, children: injectRenderFunctions(col.children, renderFunctions), }; } // 如果当前列有对应的渲染逻辑,动态注入 if (renderFunctions[col.dataIndex || col.key]) { return { ...col, ...renderFunctionscol.dataIndex || col.key, }; } return col; }); };

const handleResize = (key: string) => (newWidth: number) => { setColumns((prevColumns) => { const updateColumns = (cols: any[]): any[] => { return cols.map((col) => { // 如果当前列的 key 匹配,更新宽度 if (col.key === key) { return { ...col, width: newWidth, }; } // 如果当前列有 children,递归处理子列 if (col.children) { return { ...col, children: updateColumns(col.children), }; } return col; }); }; const nextColumns = updateColumns([...prevColumns]); // 将更新后的列配置保存到 localStorage localStorage.setItem(storageKey, JSON.stringify(nextColumns)); return nextColumns as T; // 确保类型一致 }); };

const getMergedColumns = (cols: any[]): any[] => { return cols.map((col) => { // 如果当前列有 children,递归处理子列 if (col.children) { return { ...col, children: getMergedColumns(col.children), onHeaderCell: (column: any) => ({ width: column.width, onResize: handleResize(col.key) as any, }), }; } return { ...col, onHeaderCell: (column: any) => ({ width: column.width, onResize: handleResize(col.key) as any, }), }; }); };

// 返回处理后的列配置 return getMergedColumns(columns); };

export default useResizableColumns;`