需求:用户可以拖拽表格行来调整条款顺序。
为什么选 @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,因为:
- 体积小(tree-shakeable)
- 性能好(用 CSS transform 而不是修改 DOM)
- 支持触摸屏
- 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 />,
},
// ...
];
这样只有点击拖拽手柄时才能拖拽,整行点击不会误触。
最后的效果
拖拽流畅,性能很好,而且:
- 拖拽时有视觉反馈(半透明 + 提升层级)
- 只能垂直拖拽(
restrictToVerticalAxis) - 防止误触(
distance: 1+ 拖拽手柄)
几个踩坑总结
- Table 的行要自定义 :用
components.body.row - transform 要转成 CSS :用
CSS.Translate.toString() - 拖拽手柄要用 Context:避免整行都能拖
- 限制拖拽方向 :用
restrictToVerticalAxis - 防误触不能少 :
distance: 1+ 拖拽手柄