You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ant Design表格列Resize行为定制开发咨询

Ant Design表格列Resize行为定制开发咨询

嘿,我完全get到你的需求了:要让Ant Design Table结合react-resizable实现列宽拖拽调整,而且最后一列的调整行为要特殊定制——

  • 当表格没有左侧固定列时,拖拽最后一列右边缘,列宽增加的同时,表格右侧边缘要牢牢贴住容器/视口右侧,通过横向滚动来容纳加宽的列
  • 当表格有左侧固定列时,拖拽最后一列右边缘,列宽增加时,最后一列的右边缘要始终贴合左侧固定列的右边缘,保持对齐

下面我结合你提供的现有代码,梳理实现思路和具体调整方案:

一、核心逻辑拆解

要实现这个特殊的resize效果,关键要抓住三个要点:

  1. 能识别当前调整的列是否是最后一列
  2. 能判断表格是否存在左侧固定列,并计算固定列的总宽度
  3. 调整最后一列宽度时,同步修改表格的滚动配置,强制右侧边缘贴合目标边界

二、升级ResizableTitle组件

我们需要给现有组件新增几个props,用来传递列的位置和表格的固定列状态,然后在resize回调中处理特殊逻辑:

import React, { useState } from 'react';
import { Resizable, ResizeCallbackData } from 'react-resizable';
import 'react-resizable/css/styles.css';

// 扩展props类型,添加列位置与固定列相关信息
interface ResizableTitleProps extends React.HTMLAttributes<any> {
  onResize: (e: React.SyntheticEvent<Element>, data: ResizeCallbackData) => void;
  onResizeStop: (e: React.SyntheticEvent<Element>, data: ResizeCallbackData) => void;
  width: number;
  minWidth: number;
  isResize: boolean;
  isLastColumn: boolean; // 标记是否为最后一列
  hasLeftFixedColumns: boolean; // 标记是否存在左侧固定列
  fixedColumnsTotalWidth?: number; // 左侧固定列的总宽度
  tableScrollWidth?: number; // 表格滚动容器的宽度
}

const ResizableTitle = (props: ResizableTitleProps) => {
  const { 
    onResize, width, onResizeStop, minWidth, isResize, 
    isLastColumn, hasLeftFixedColumns, fixedColumnsTotalWidth, tableScrollWidth,
    ...restProps 
  } = props;
  const [isFieldResizing, setIsFieldResizing] = useState(false);

  if (!width || !isResize) {
    return <th {...restProps} />;
  }

  // 处理最后一列的特殊resize逻辑
  const handleResize = (e: React.SyntheticEvent<Element>, data: ResizeCallbackData) => {
    if (isLastColumn) {
      let targetWidth = data.size.width;
      // 有左侧固定列时,强制最后一列宽度 = 滚动容器宽度 - 固定列总宽度
      if (hasLeftFixedColumns && fixedColumnsTotalWidth && tableScrollWidth) {
        targetWidth = tableScrollWidth - fixedColumnsTotalWidth;
      }
      // 传递处理后的宽度给父组件
      const customData = { ...data, size: { width: targetWidth, height: data.size.height } };
      onResize(e, customData);
    } else {
      // 普通列直接调用原回调
      onResize(e, data);
    }
  };

  const handleResizeStart = (e: React.SyntheticEvent<Element>) => {
    e.stopPropagation();
    setIsFieldResizing(true);
    document.body.classList.add('no-select');
  };

  const handleResizeStop = (e: React.SyntheticEvent<Element>, data: ResizeCallbackData) => {
    setIsFieldResizing(false);
    // 最后一列的停止回调同样处理宽度
    if (isLastColumn) {
      let targetWidth = data.size.width;
      if (hasLeftFixedColumns && fixedColumnsTotalWidth && tableScrollWidth) {
        targetWidth = tableScrollWidth - fixedColumnsTotalWidth;
      }
      const customData = { ...data, size: { width: targetWidth, height: data.size.height } };
      onResizeStop(e, customData);
    } else {
      onResizeStop(e, data);
    }
    document.body.classList.remove('no-select');
  };

  return (
    <Resizable
      width={width}
      height={0}
      handle={
        <span 
          className="react-resizable-handle" 
          onClick={(e) => e.stopPropagation()}
        />
      }
      onResize={handleResize}
      draggableOpts={{ enableUserSelectHack: false }}
      onResizeStart={handleResizeStart}
      onResizeStop={handleResizeStop}
      minConstraints={[minWidth, 0]}
    >
      <th 
        {...restProps} 
        className={
          isFieldResizing 
            ? `${restProps.className || ''} is-resizing` 
            : restProps.className
        } 
      />
    </Resizable>
  );
};

export default ResizableTitle;

三、父组件(表格使用处)调整

在Table所在的组件中,我们需要维护列宽、滚动配置等状态,同时给每个ResizableTitle传递对应的判断参数:

import { Table, ColumnType } from 'antd';
import ResizableTitle from './components/resizable-title';
import { useState, useEffect } from 'react';

interface IListForms {
  formsRef?: React.RefObject<HTMLDivElement>;
  // 其他业务props...
}

const ListForms = ({ formsRef }: IListForms) => {
  const [resizableColumns, setResizableColumns] = useState<ColumnType<any>[]>([]);
  const [tableScroll, setTableScroll] = useState({ x: '1050', y: '400' });
  const [hasLeftFixedColumns, setHasLeftFixedColumns] = useState(false);
  const [fixedColumnsTotalWidth, setFixedColumnsTotalWidth] = useState(0);

  // 模拟初始列配置(实际可从接口/全局配置获取)
  const initialColumns = [
    {
      title: '选择',
      fixed: 'left',
      width: 50,
      isResize: false,
    },
    {
      title: '表单名称',
      dataIndex: 'name',
      width: 200,
      minWidth: 120,
      isResize: true,
    },
    {
      title: '创建时间',
      dataIndex: 'createTime',
      width: 180,
      minWidth: 140,
      isResize: true,
    },
    {
      title: '操作',
      width: 150,
      minWidth: 120,
      isResize: true,
    },
  ];

  // 初始化列配置,判断固定列并计算总宽度
  useEffect(() => {
    const cols = initialColumns.map(col => ({ ...col }));
    setResizableColumns(cols);
    // 检查是否有左侧固定列
    const hasFixed = cols.some(col => col.fixed === 'left');
    setHasLeftFixedColumns(hasFixed);
    // 计算左侧固定列总宽度
    if (hasFixed) {
      const totalWidth = cols.filter(col => col.fixed === 'left').reduce((sum, col) => sum + (col.width || 0), 0);
      setFixedColumnsTotalWidth(totalWidth);
    }
  }, []);

  // 处理列宽更新
  const handleColumnWidthChange = (index: number, newWidth: number) => {
    const newColumns = [...resizableColumns];
    newColumns[index] = { ...newColumns[index], width: newWidth };
    setResizableColumns(newColumns);

    // 最后一列调整时,同步更新表格滚动宽度
    if (index === resizableColumns.length - 1) {
      if (hasLeftFixedColumns) {
        // 有固定列时,滚动宽度 = 固定列总宽度 + 最后一列新宽度
        setTableScroll(prev => ({
          ...prev,
          x: fixedColumnsTotalWidth + newWidth,
        }));
      } else {
        // 无固定列时,滚动宽度 = 所有列总宽度
        const totalWidth = newColumns.reduce((sum, col) => sum + (col.width || 0), 0);
        setTableScroll(prev => ({ ...prev, x: totalWidth }));
      }
    }
  };

  // 生成resize回调
  const generateResizeHandler = (index: number) => (
    e: React.SyntheticEvent<Element>,
    data: ResizeCallbackData
  ) => {
    handleColumnWidthChange(index, data.size.width);
  };

  // 包装列,传递ResizableTitle所需props
  const columns = resizableColumns.map((col, index) => ({
    ...col,
    title: (
      <ResizableTitle
        width={col.width || 0}
        minWidth={col.minWidth || 100}
        isResize={col.isResize || false}
        onResize={generateResizeHandler(index)}
        onResizeStop={generateResizeHandler(index)}
        isLastColumn={index === resizableColumns.length - 1}
        hasLeftFixedColumns={hasLeftFixedColumns}
        fixedColumnsTotalWidth={fixedColumnsTotalWidth}
        tableScrollWidth={typeof tableScroll.x === 'string' ? parseInt(tableScroll.x) : tableScroll.x}
      >
        {col.title}
      </ResizableTitle>
    ),
  }));

  // 业务状态模拟(实际替换为你的真实状态)
  const formsList = [];
  const listFormsIsLoading = false;
  const pagination = {};
  const deleteFormsIds = [];
  const isAllFormsSelected = false;

  return (
    <div className="table-section table-section-with-search" ref={formsRef}>
      <Table
        className="tableCellPadding-9"
        columns={columns}
        onChange={() => {}} // 替换为你的表格onChange逻辑
        showSorterTooltip={false}
        loading={listFormsIsLoading}
        components={{
          header: { cell: ResizableTitle },
        }}
        dataSource={formsList}
        pagination={pagination}
        locale={{
          emptyText: (
            <div>No data found</div> // 替换为你的空状态组件
          ),
        }}
        rowSelection={{
          type: 'checkbox',
          selectedRowKeys: deleteFormsIds.map(item => item),
          onChange: () => {}, // 替换为你的选择逻辑
          onSelectAll: isAllFormsSelected ? () => {} : () => {},
          fixed: 'left',
          columnWidth: '50px',
        }}
        rowKey="id"
        scroll={tableScroll}
        tableLayout={formsList.length > 0 ? 'fixed' : 'auto'}
      />
    </div>
  );
};

export default ListForms;

三、保留原有样式(补充滚动容器样式)

你的原有SCSS已经处理了resize手柄的交互效果,这里再补充一点容器样式,确保右侧边缘固定:

.table-section {
  width: 100%;
  overflow: hidden; // 限制容器不超出父元素
}

.ant-table-thead {
  > tr > th {
    .react-resizable-handle {
      opacity: 0;
      background: none;
      transition: all 0.2s ease;
      right: 0;
      top: 0;
      z-index: 9;
      height: 100%;
      width: 12px;
      padding: 0;

      &:after {
        content: "";
        position: absolute;
        top: 5px;
        bottom: 5px;
        left: 3px;
        z-index: 1;
        width: 1px;
        background-color: #e8e8e8; // 替换为你的项目分割线颜色
      }

      &:hover {
        opacity: 1;
      }
    }

    &.is-resizing {
      .react-resizable-handle {
        opacity: 1;
        transition: all 0.2s ease;
        cursor: col-resize;
      }
    }

    &:hover {
      .react-resizable-handle {
        opacity: 1;
        transition: all 0.2s ease;
        cursor: col-resize;
      }

      .table-sort-icon {
        width: 16px;
        opacity: 1;
        transition: all 0.2s ease;
      }
    }
  }
}

.no-select {
  user-select: none;
}

四、关键细节说明

  1. 列宽状态维护:必须通过state实时保存每一列的宽度,否则组件重新渲染后会丢失调整结果
  2. 滚动配置同步:最后一列调整时,必须同步修改表格的scroll.x值,才能保证右侧边缘贴合目标边界
  3. 固定列宽度计算:如果有多个左侧固定列,要实时计算它们的总宽度,确保最后一列的对齐精度
  4. 边界限制:给每列设置minWidth,避免列宽被调整到过小导致内容溢出

如果实际测试中还有细节偏差,可以根据你的容器布局和业务场景,微调宽度计算的逻辑哦~

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 14:38:03