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 + 拖拽手柄
相关推荐
一tiao咸鱼17 小时前
我用 Claude 做了一个 AI 面试刷题系统,支持 DeepSeek / 阿里 / GPT 帮你打分
前端
掘金一周18 小时前
对车完全小白,不知买油买电还是买混动,求建议| 沸点周刊 7.2
前端·人工智能·后端
妙码生花18 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十六):目录结构更新、完善 token 系统(AI 表示 token 入库无需加密?)
前端·后端·ai编程
程序me18 小时前
Prompt、Context、Harness、Loop 之后是什么? AI工程下一个半年的关键词
前端·后端·ai编程
飞天狗19 小时前
线上Bug一直复现不了?我用Sentry把错误追踪效率提升了10倍
前端
Slice_cy19 小时前
对前端工程化的理解
前端
Slice_cy19 小时前
状态机设计理念与实现
前端
星栈19 小时前
LiveView 的生命周期:mount、handle_event 和 Socket 到底怎么运转
前端·前端框架·elixir
yingyima19 小时前
JWT Token 解析与安全实践速查:5 问 5 答直击要害
前端
kyriewen20 小时前
我用 Codex 重写了同事维护三年的代码,他没说谢谢——而是找了领导
前端·javascript·ai编程