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 + 拖拽手柄
相关推荐
海石12 小时前
微信小程序开发01:XR-FRAME的快速上手
前端·增强现实·trae
叶梅树15 小时前
DocsJS npmjs 自动化发布复盘(Trusted Publisher)
前端·npm
我命由我1234515 小时前
Element Plus - Form 的 resetField 方法观察记录
开发语言·前端·javascript·vue.js·html·html5·js
清空mega16 小时前
《Vue3 项目结构详解:components、views、assets、router、stores 到底该怎么理解?》
前端·javascript·vue.js
雨雨雨雨雨别下啦16 小时前
Vue——小白也能学!Day6
前端·javascript·vue.js
XPoet16 小时前
AI 编程工程化:Hook——AI 每次操作前后的自动检查站
前端·后端·ai编程
難釋懷17 小时前
RedisTemplate配置读写分离
前端·bootstrap·html
冰暮流星17 小时前
javascript如何实现删除数组里面的重复元素
开发语言·前端·javascript
网络点点滴18 小时前
透传属性$attrs
前端·javascript·vue.js
90后的晨仔19 小时前
OpenClaw macOS 完整安装指南
前端