dnd-kit 实现表格拖拽排序

需求:用户可以拖拽表格行来调整条款顺序。

为什么选 @dnd-kit?

市面上常见的拖拽库:

  • react-dnd:太老了,API 很复杂
  • react-beautiful-dnd:已经不维护了,React 19 不兼容
  • react-sortable-hoc:API 过时,不支持 React 18+ 并发特性
  • react-grid-layout:太重了,功能太多
  • @dnd-kit:轻量、现代、React 19 完美兼容

我最后选了 @dnd-kit,因为:

  1. 体积小(tree-shakeable)
  2. 性能好(用 CSS transform 而不是修改 DOM)
  3. 支持触摸屏
  4. TypeScript 支持好
基础实现

首先安装依赖:

javascript 复制代码
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

然后用 DndContext 包裹表格:

javascript 复制代码
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { CSS } from '@dnd-kit/utilities';

// 拖拽传感器
const sensors = useSensors(
  useSensor(PointerSensor, {
    activationConstraint: {
      distance: 1, // 移动 1px 后才开始拖拽,防止误触
    },
  }),
);

// 拖拽结束
const onDragEnd = ({ active, over }) => {
  if (active.id !== over?.id) {
    const activeIndex = clauseList.findIndex(i => i.key === active.id);
    const overIndex = clauseList.findIndex(i => i.key === over?.id);
    
    const newClauseList = arrayMove(clauseList, activeIndex, overIndex);
    setClauseList(newClauseList);
  }
};

return (
  <DndContext sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>
    <SortableContext
      items={clauseList.map(i => i.key)}
      strategy={verticalListSortingStrategy}
    >
      <Table ... />
    </SortableContext>
  </DndContext>
);
但有个坑:Ant Design Table 的行怎么变成可拖拽的?

Ant Design Table 的行不是普通的 <tr>,是内部封装的组件。我要自定义 body.row

javascript 复制代码
const Row = props => {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging, setActivatorNodeRef } = useSortable({
    id: props['data-row-key'],
  });
  
  const style = {
    ...props.style,
    transform: CSS.Translate.toString(transform), // 关键:把 transform 转成 CSS
    transition,
    ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}), // 拖拽时提升层级
  };
  
  const contextValue = useMemo(
    () => ({ setActivatorNodeRef, listeners }),
    [setActivatorNodeRef, listeners],
  );
  
  return (
    <RowContext.Provider value={contextValue}>
      <tr {...props} ref={setNodeRef} style={style} {...attributes} />
    </RowContext.Provider>
  );
};

// 使用
<Table
  components={{
    body: { row: Row },
  }}
  ...
/>

这里有个坑:transform 不是字符串,是对象,要用 CSS.Translate.toString() 转换。

还有个坑:拖拽手柄怎么实现?

我不希望整行都能拖拽,只希望点击某个按钮才能拖。这样可以防止误操作。

RowContext 传递 listeners:

javascript 复制代码
const RowContext = React.createContext({});

const Row = props => {
  // ...
  const contextValue = useMemo(
    () => ({ setActivatorNodeRef, listeners }),
    [setActivatorNodeRef, listeners],
  );
  
  return (
    <RowContext.Provider value={contextValue}>
      <tr {...props} ref={setNodeRef} style={style} {...attributes} />
    </RowContext.Provider>
  );
};

// 拖拽手柄组件
const DragHandle = () => {
  const { setActivatorNodeRef, listeners } = useContext(RowContext);
  return (
    <MenuOutlined
      ref={setActivatorNodeRef}
      style={{ cursor: 'move' }}
      {...listeners} // 只在手柄上绑定 listeners
    />
  );
};

// 在 columns 里使用
const columns = [
  {
    key: 'sort',
    render: () => <DragHandle />,
  },
  // ...
];

这样只有点击拖拽手柄时才能拖拽,整行点击不会误触。

最后的效果

拖拽流畅,性能很好,而且:

  1. 拖拽时有视觉反馈(半透明 + 提升层级)
  2. 只能垂直拖拽(restrictToVerticalAxis
  3. 防止误触(distance: 1 + 拖拽手柄)
几个踩坑总结
  1. Table 的行要自定义 :用 components.body.row
  2. transform 要转成 CSS :用 CSS.Translate.toString()
  3. 拖拽手柄要用 Context:避免整行都能拖
  4. 限制拖拽方向 :用 restrictToVerticalAxis
  5. 防误触不能少distance: 1 + 拖拽手柄
相关推荐
dotnet90几秒前
PDF 页面尺寸上限是 14400。iText 直接加载成功的大图可能超过这个限制,需要在 setPageSize 之前等比缩放。
前端·javascript·html
threelab几秒前
Three.js 几何图形变换 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
道友可好2 分钟前
写给 AI 的入职手册,AGENTS.md
前端·人工智能·后端
吠品10 分钟前
处理 Python 类继承中那些变来变去的初始化参数
linux·前端·python
云水一下13 分钟前
TypeScript 从零基础到精通(七):从配置到全栈项目落地
前端·javascript·typescript
秋天的一阵风31 分钟前
✨ 代码秒跳转、自动补全?全靠 LSP 和 AST!
前端·后端·ai编程
如果超人不会飞42 分钟前
TinyVue Checkbox复选框组件使用指南
前端·vue.js
程序员小淞43 分钟前
写一个行政区划下拉选组件(异步+搜索)
前端
星栈1 小时前
用 Rust + Makepad 做一个 JSON 查看器:从零到能用的全过程
前端·rust
yijianace1 小时前
Python爬虫实战:分页爬取 + 详情页采集 + CSV存储
前端·爬虫·python