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 + 拖拽手柄
相关推荐
滑雪的企鹅.4 小时前
HTML头部元信息避坑指南大纲
前端·html
一拳不是超人4 小时前
老婆天天吵吵要买塔罗牌,我直接用 AI 2 小时写了个在线塔罗牌
前端·ai编程
excel6 小时前
如何解决 Nuxt DevTools 中关于 unstorage 包的报错
前端
Rust研习社6 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
C澒6 小时前
AI 生码 - API2Code:接口智能匹配与 API 自动化生码全链路设计
前端·低代码·ai编程
浔川python社6 小时前
HTML头部元信息避坑指南技术文章大纲
前端·html
IT_陈寒6 小时前
SpringBoot配置加载顺序把我坑惨了
前端·人工智能·后端
kyriewen6 小时前
Next.js部署:从本地跑得欢,到线上飞得稳
前端·react.js·next.js
Moment6 小时前
面试官:给 llm 传递上下文,有哪几个身份 role ❓❓❓
前端·后端·面试
跨境数据猎手7 小时前
跨境独立站系统技术拆解(附带源码)
服务器·前端·php