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 + 拖拽手柄
相关推荐
csdn2015_9 小时前
vue 前端运行命令
前端·javascript·vue.js
swipe9 小时前
02|从 `pnpm dev` 到 Spring Boot 启动:后端服务到底怎么跑起来?
前端·后端·全栈
阿懂在掘金9 小时前
做命令式弹窗工具应该考虑什么——我为什么把 vue-layerx 设计成这样
前端·前端框架·函数式编程
太平洋月光9 小时前
AI 快捷指令:Cursor Rules · Commands · Skills
前端·ai编程
默_笙9 小时前
😁 LLM 不知道的事它不会说"不知道",而是会"编"——RAG 就是让它闭嘴先查资料
前端·javascript
stringwu9 小时前
用 LangGraph 落地飞书 Bug 自动修复 Agent 实践
前端
Revolution619 小时前
变量提升到底提升了什么:为什么 var 是 undefined,let 却直接报错
前端·javascript
Revolution6110 小时前
本地明明正常,为什么 CI 又挂了:一次前端构建失败的排查过程
前端·前端工程化
太平洋月光10 小时前
stagewise如何结合cursor开发
前端·ai编程
前端Hardy10 小时前
JavaScript 终于又进化了!ES2026 正式发布,这些新特性你必须知道!
前端