• React 可拖拽列宽 + 点击行选中 ProTable 封装笔记


    平时做后台管理系统,Ant Design Pro 的 ProTable 用得特别多,但原生组件缺少列宽拖拽调整和点击行选中这两个高频功能,每次都要重复写逻辑。

    所以我把这两个功能封装成了通用组件,后续项目直接复用,这里整理成笔记方便自己查阅。

    一、整体思路

    把功能拆成两部分解耦:

    1. 列宽拖拽核心逻辑:独立封装可调整表头组件,无业务侵入
    2. ProTable 业务封装:集成列宽拖拽 + 点击行选中 + 选中状态受控/非受控 + 暴露清空选中方法

    两个文件配合使用,开箱即用,支持 TypeScript,兼容 ProTable 所有原生属性。


    二、列宽拖拽表头封装(ResizableTitle.tsx)

    这是列宽拖拽的核心,基于原生 th 实现鼠标按下、移动、抬起的完整拖拽逻辑,最小宽度限制 80px,右侧有拖拽触发区,体验接近 Excel。

    import React, { useState, useCallback } from 'react';
    
    // 表格列配置类型
    export interface TableColumnType {
      width?: number;
      title?: React.ReactNode;
      dataIndex?: string;
      key?: string;
      [key: string]: any;
    }
    
    // 表头组件
    interface ResizableTitleProps {
      width?: number;
      onResize?: (width: number) => void;
      [key: string]: any;
    }
    
    const ResizableTitle: React.FC<ResizableTitleProps> = (props) => {
      const { width, onResize, ...restProps } = props;
      const [isResizing, setIsResizing] = useState(false);
    
      // 鼠标按下开始拖拽
      const handleMouseDown = useCallback((e: React.MouseEvent) => {
        const thRect = e.currentTarget.getBoundingClientRect();
        // 只在右侧 10px 区域触发拖拽
        const isOnEdge = e.clientX > thRect.right - 10;
        if (!isOnEdge) return;
    
        e.preventDefault();
        setIsResizing(true);
    
        const startX = e.clientX;
        const startWidth = thRect.width;
    
        // 拖拽中实时更新宽度
        const handleMouseMove = (moveEvent: MouseEvent) => {
          const diff = moveEvent.clientX - startX;
          const newWidth = Math.max(80, startWidth + diff);
          onResize?.(newWidth);
        };
    
        // 松开鼠标结束拖拽
        const handleMouseUp = () => {
          setIsResizing(false);
          document.removeEventListener('mousemove', handleMouseMove);
          document.removeEventListener('mouseup', handleMouseUp);
        };
    
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
      }, [onResize]);
    
      return (
        <th
          {...restProps}
          onMouseDown={handleMouseDown}
          style={{
            width,
            position: 'relative',
            paddingRight: '10px',
            cursor: isResizing ? 'col-resize' : undefined,
            userSelect: 'none',
          }}
        >
          {/* 拖拽触发区域 */}
          <span
            style={{
              position: 'absolute',
              right: 0,
              top: 0,
              bottom: 0,
              width: '10px',
              cursor: 'col-resize',
              backgroundColor: 'transparent',
            }}
            onMouseEnter={(e) => {
              e.currentTarget.style.backgroundColor = 'rgba(22, 119, 255, 0.1)';
            }}
            onMouseLeave={(e) => {
              if (!isResizing) {
                e.currentTarget.style.backgroundColor = 'transparent';
              }
            }}
          />
          {props.children}
        th>
      );
    };
    
    // 注入到 ProTable 表头
    export const components = {
      header: {
        cell: ResizableTitle,
      },
    };
    
    // 处理列配置,绑定拖拽回调
    export const getMergeColumns = (
      columns: TableColumnType[],
      setColumns: React.Dispatch>
    ) => {
      return columns.map((col, index) => ({
        ...col,
        onHeaderCell: (column: TableColumnType) => ({
          width: column.width,
          onResize: (newWidth: number) => {
            setColumns((prev: TableColumnType[]) => {
              const next = [...prev];
              next[index] = {
                ...next[index],
                width: newWidth,
              };
              return next;
            });
          },
        }),
      }));
    };
    
    export default ResizableTitle;
    

    核心要点

    • 拖拽只触发在表头右侧 10px 区域,不影响正常点击
    • 最小宽度 80px,防止列被缩没
    • 鼠标悬浮拖拽区有淡蓝色提示,体验更好
    • 对外暴露 components 和 getMergeColumns 供 ProTable 集成

    三、ProTable 业务封装(MyProTable.tsx)

    在 ProTable 基础上集成:

    • 列宽拖拽
    • 点击行选中(支持单选/多选)
    • 选中状态支持外部受控 / 内部非受控
    • 暴露 clearSelected 方法清空选中
    • 搜索栏按钮顺序调整(查询在前,重置在后)
    • 完全兼容 ProTable 原有属性
    import { ProTable, type ProTableProps } from '@ant-design/pro-components';
    import React, { forwardRef, useImperativeHandle, useState } from 'react';
    import { components, getMergeColumns } from '../ResizableTitle';
    
    // 暴露给父组件的方法
    export interface MyProTableRef {
      clearSelected: () => void;
    }
    
    // 扩展 ProTable 属性
    export type MyProTableProps<
      T extends Record<string, any>,
      U extends Record<string, any> = Record<string, any>,
      ValueType = 'text'
    > = ProTablePropsValueType> & {
      enableRowSelect?: boolean; // 是否开启点击选中
      selectedRowKeys?: React.Key[]; // 外部受控选中key
      onSelectedChange?: (keys: React.Key[], rows: T[]) => void; // 选中变化回调
      multiple?: boolean; // 是否多选
    };
    
    const MyProTableInner = <
      T extends Record<string, any>,
      U extends Record<string, any> = Record<string, any>,
      ValueType = 'text'
    >(
      props: MyProTableProps,
      ref: React.ForwardedRef
    ) => {
      const {
        enableRowSelect = true,
        selectedRowKeys,
        onSelectedChange,
        multiple = false,
        rowKey = 'id' as keyof T,
        columns = [],
        ...restProps
      } = props;
    
      // 内部选中状态(非受控模式)
      const [innerKeys, setInnerKeys] = useState<React.Key[]>([]);
      const finalKeys = selectedRowKeys ?? innerKeys;
    
      // 列宽拖拽状态
      const [renderColumns, setRenderColumns] = useState<any[]>(columns);
      const resizeColumns = getMergeColumns(renderColumns, setRenderColumns as any);
    
      // 选中变化统一处理
      const handleChange = (keys: React.Key[], rows: T[]) => {
        if (selectedRowKeys === undefined) setInnerKeys(keys);
        onSelectedChange?.(keys, rows);
      };
    
      // 获取行唯一 key
      const getRowKey = (record: T): React.Key => {
        if (typeof rowKey === 'function') return rowKey(record);
        return record[rowKey] as React.Key;
      };
    
      // 点击行触发选中
      const handleClick = (record: T) => {
        if (!enableRowSelect) return;
        const key = getRowKey(record);
    
        let newKeys: React.Key[];
        if (multiple) {
          // 多选:切换当前行选中状态
          newKeys = finalKeys.includes(key)
            ? finalKeys.filter((k) => k !== key)
            : [...finalKeys, key];
        } else {
          // 单选:只保留当前行或清空
          newKeys = finalKeys.includes(key) ? [] : [key];
        }
    
        // 匹配选中行数据
        const selectedRows = newKeys
          .map((k) => restProps.dataSource?.find((item) => getRowKey(item) === k))
          .filter((item): item is T => !!item);
    
        handleChange(newKeys, selectedRows);
      };
    
      // 暴露方法给父组件
      useImperativeHandle(ref, () => ({
        clearSelected: () => handleChange([], []),
      }));
    
      return (
        <ProTableValueType>
          {...restProps}
          rowKey={rowKey}
          columns={resizeColumns as any}
          components={components} // 注入可拖拽表头
          onRow={(record) => ({
            ...restProps.onRow?.(record),
            onClick: () => handleClick(record), // 绑定点击行事件
          })}
          rowClassName={(record, index, indent) => {
            const key = getRowKey(record);
            const isSelected = finalKeys.includes(key);
            let customClass = '';
    
            // 兼容外部传入的 className
            if (typeof restProps.rowClassName === 'function') {
              customClass = restProps.rowClassName(record, index, indent);
            } else if (typeof restProps.rowClassName === 'string') {
              customClass = restProps.rowClassName;
            }
    
            return isSelected ? `table-row-selected ${customClass}` : customClass;
          }}
          // 搜索栏:查询按钮在前,重置按钮在后
          search={{
            ...restProps.search,
            optionRender: (_searchConfig, _formProps, dom) => {
              if (!dom || dom.length < 2) return dom;
              const [resetBtn, submitBtn] = dom;
              return [submitBtn, resetBtn];
            },
          }}
        />
      );
    };
    
    // 转发 ref,支持泛型
    const MyProTable = forwardRef(MyProTableInner) as <
      T extends Record<string, any>,
      U extends Record<string, any> = Record<string, any>,
      ValueType = 'text'
    >(
      props: MyProTableProps & { ref?: React.ForwardedRef }
    ) => React.ReactElement;
    
    export default MyProTable;
    

    样式补充(全局加一行即可)

    选中行高亮样式,在全局 global.less 中添加:

    .table-row-selected {
      background-color: rgba(22, 119, 255, 0.1) !important;
    }
    

    四、使用示例

    封装完后使用非常简单,和普通 ProTable 用法几乎一致:

    import React, { useRef } from 'react';
    import MyProTable, { MyProTableRef } from './MyProTable';
    
    const DemoPage = () => {
      const tableRef = useRef<MyProTableRef>(null);
    
      const columns = [
        { title: 'ID', dataIndex: 'id', width: 100 },
        { title: '姓名', dataIndex: 'name', width: 150 },
        { title: '年龄', dataIndex: 'age', width: 120 },
        { title: '地址', dataIndex: 'address' },
      ];
    
      // 选中回调
      const handleSelect = (keys, rows) => {
        console.log('选中keys:', keys);
        console.log('选中数据:', rows);
      };
    
      return (
        <div>
          <button onClick={() => tableRef.current?.clearSelected()}>
            清空选中
          button>
    
          <MyProTable
            ref={tableRef}
            columns={columns}
            rowKey="id"
            multiple={true} // 开启多选
            onSelectedChange={handleSelect}
            request={async () => {
              // 模拟接口
              return {
                data: [
                  { id: 1, name: '张三', age: 20, address: '北京' },
                  { id: 2, name: '李四', age: 25, address: '上海' },
                ],
                total: 2,
              };
            }}
          />
        div>
      );
    };
    
    export default DemoPage;
    

    五、使用心得

    1. 列宽拖拽和业务完全解耦,后续想用到普通 Table 也能快速抽离
    2. 点击行选中支持单选/多选,满足大部分列表场景
    3. 选中状态支持受控/非受控,适配表单联动或独立使用
    4. 完全兼容 ProTable 原生属性,老项目改造成本极低
    5. 封装后项目里只需要引入 MyProTable,不用再写重复逻辑

    这个封装基本覆盖了后台系统表格 90% 的常用场景,后续直接复制这两个文件就能用。

  • 相关阅读:
    TCP传输的三次握手、四次挥手策略是什么
    java对zip、rar、7z文件带密码解压实例
    vue的use和extend的理解
    广西南宁新能源汽车电机定子三维扫描3D尺寸测量检测-CASAIM中科广电
    121 买卖股票的最佳时机
    声明式调用 —— SpringCloud OpenFeign
    Linux:安装软件的两种方式rpm和yum
    matlab数据处理: cell table array+datetime
    电脑查看WiFi使用密码
    论文阅读:Rethinking Range View Representation for LiDAR Segmentation
  • 原文地址:https://www.cnblogs.com/minjh/p/20122089