Ant Design List 栅格模式集成 dnd-kit 的网格拖拽排序实战
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
本文围绕仓库中 grid-drag-sorting.md 及其配套示例 grid-drag-sorting.tsx 展开。官方示例给出了一种通用解法:利用
List的自定义渲染能力,将第三方拖拽库 dnd-kit 的能力注入List.Item,从而为多列栅格卡片实现自由拖拽排序。读完本文,你将掌握自定义List.Item的完整封装手法、DndContext/SortableContext/useSortable的正确组合方式,以及拖拽结束后的数据重排逻辑,并能在自己的项目中直接复用这套模式。
一、示例文档与适用场景
1.1 文档定位
grid-drag-sorting.md 是 antd List 组件官方 Demo 目录(components/list/demo/)下的一个示例说明文件,原文核心只有一句:
使用自定义元素,我们可以集成 dnd-kit 来实现网格布局的拖拽排序。 / By using custom components, we can integrate List with dnd-kit to implement drag sorting function for grid layout.
这句话点出了本示例的两条核心路线:
- antd 侧保持开放:
List提供grid属性负责多列栅格排版,renderItem负责每一项的自定义内容,List.Item则透传ref与样式,为第三方能力留出挂载点; - 拖拽能力完全交给 dnd-kit:排序状态、拖拽位移、放置判定均由 dnd-kit 计算,antd 只负责最终渲染结果。
1.2 为什么需要“网格版”拖拽排序
List 的栅格列表(详见 grid.md 与 grid.tsx)会生成类似 Dashboard 卡片墙的多列布局,相比纵向列表,它在两个维度上都需要感知“拖到哪个位置”的能力。dnd-kit 的SortableContext默认按矩形区域(rect)测算放置位,天然支持二维网格;配合arrayMove重排数据源后,React 重新渲染即可完成卡片换位。
二、完整可运行示例
grid-drag-sorting.tsx 是本文档对应的完整实现,下面原样继承其代码:
import React, { useState } from 'react'; import type { DragEndEvent } from '@dnd-kit/core'; import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Card, List } from 'antd'; import type { GetProps } from 'antd'; const SortableListItem: React.FC<GetProps<typeof List.Item> & { itemKey: number }> = (props) => { const { itemKey, style, ...rest } = props; const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: itemKey, }); const listStyle: React.CSSProperties = { ...style, transform: CSS.Translate.toString(transform), transition, cursor: 'move', ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}), }; return <List.Item {...rest} ref={setNodeRef} style={listStyle} {...attributes} {...listeners} />; }; const App: React.FC = () => { const [data, setData] = useState([ { key: 1, title: 'Title 1' }, { key: 2, title: 'Title 2' }, { key: 3, title: 'Title 3' }, { key: 4, title: 'Title 4' }, { key: 5, title: 'Title 5' }, { key: 6, title: 'Title 6' }, ]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 1, }, }), ); const onDragEnd = ({ active, over }: DragEndEvent) => { if (!active || !over) { return; } if (active.id !== over.id) { setData((prev) => { const activeIndex = prev.findIndex((i) => i.key === active.id); const overIndex = prev.findIndex((i) => i.key === over.id); return arrayMove(prev, activeIndex, overIndex); }); } }; return ( <DndContext sensors={sensors} onDragEnd={onDragEnd} id="list-grid-drag-sorting"> <SortableContext items={data.map((item) => item.key)}> <List grid={{ gutter: 16, column: 4 }} dataSource={data} renderItem={(item) => ( <SortableListItem key={item.key} itemKey={item.key}> <Card title={item.title}>Card content</Card> </SortableListItem> )} /> </SortableContext> </DndContext> ); }; export default App;示例运行效果:一屏四列、带 16px 间距的卡片栅格,按住任意卡片即可将其拖至任意网格位置,松手后其余卡片自动让位、数据顺序持久化。
三、实现逐段拆解
3.1 核心思想:把“可排序能力”包装进自定义List.Item
整个方案的枢纽是自封装组件SortableListItem,它做了三件事:
- 通过
GetProps<typeof List.Item>继承 antdList.Item的完整属性类型,并扩展业务所需的itemKey(作为 dnd-kit 的唯一标识):const SortableListItem: React.FC<GetProps<typeof List.Item> & { itemKey: number }> = (props) => { ... }; - 解构出
style后调用useSortable({ id: itemKey }),拿到拖拽必需的setNodeRef、attributes、listeners,以及排序过程计算出的transform/transition与拖拽态标志isDragging; - 将所有拖拽属性原样透传给原生
List.Item:return <List.Item {...rest} ref={setNodeRef} style={listStyle} {...attributes} {...listeners} />;
这样整张卡片(含其内部 Card)都可作为拖拽手柄,用户按住任意区域即开始拖动。
3.2 为何能对List.Item挂ref与样式
这依赖 antdList.Item的底层实现支持。查看 components/list/Item.tsx:InternalItem使用React.forwardRef<HTMLDivElement, ListItemProps>实现,且其接口包含style?: CSSProperties;尤其关键的是,当处于grid模式时(见 components/list/index.tsx 相关逻辑,grid ? <Col ref={ref} flex={1} style={colStyle}>),内部渲染元素从li切换为Col,并始终把外部传入的ref挂到 DOM 节点上。这保证了setNodeRef、style(含 transform 位移)、attributes等都能正确作用到每个栅格子元素,是“拖得动”的前提。
3.3 位移样式处理:transform、transition 与拖拽置顶
dnd-kit 返回的transform是结构化的Transform对象,需要借助@dnd-kit/utilities的CSS.Translate.toString转成 CSS transform 字符串:
const listStyle: React.CSSProperties = { ...style, // 保留 antd 栅格下发的列宽等样式 transform: CSS.Translate.toString(transform),// 拖拽位移 transition, // 让位动画 cursor: 'move', // 明确可拖拽光标 ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}), // 拖拽中置顶,避免被同行卡片遮挡 };其中isDragging置顶是网格场景的关键细节:多列布局中卡片可能横向交叠,不提升zIndex会出现“拖到一半被盖住”的视觉问题。
3.4 传感器配置:PointerSensor + 激活距离
const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 1 }, }), );PointerSensor将鼠标/触摸指针事件统一映射为拖拽;activationConstraint.distance: 1表示指针位移超过 1px 才判定为拖拽,避免把普通点击误判为拖动,同时保留了点击/触摸的原始语义(示例注释也指向 dnd-kit 官方关于激活约束的文档说明)。
3.5 把数据与上下文接起来:DndContext + SortableContext
DndContext是拖拽的全局状态容器,示例显式传入id="list-grid-drag-sorting"。当页面存在多个拖拽区域时,稳定且唯一的 id 可以避免上下文冲突;SortableContext通过items={data.map((item) => item.key)}声明“哪些 id 参与排序”,其内部注册的矩形测算逻辑负责计算网格中的目标槽位。值得注意的是:本网格示例没有显式传入strategy,使用 dnd-kit 的默认矩形排序策略即可满足二维栅格场景;而纵向列表的孪生示例 drag-sorting.tsx 则显式传入了verticalListSortingStrategy来强化垂直方向判定,两者对照可见策略参数的取舍差异。
3.6 拖拽结束的数据重排:onDragEnd + arrayMove
const onDragEnd = ({ active, over }: DragEndEvent) => { if (!active || !over) return; // 无目标时不做处理 if (active.id !== over.id) { setData((prev) => { const activeIndex = prev.findIndex((i) => i.key === active.id); const overIndex = prev.findIndex((i) => i.key === over.id); return arrayMove(prev, activeIndex, overIndex); // 返回新数组触发重渲染 }); } };要点:
- dnd-kit 的
active.id/over.id就是传入useSortable({ id })的itemKey,因此用findIndex((i) => i.key === ...)反查数组下标; arrayMove(来自@dnd-kit/sortable)返回新的数组引用,配合 ReactuseState触发List按新顺序重渲染;active.id === over.id时直接返回,可避免无意义的数组拷贝。
数据与渲染之间通过两处 key 对齐:renderItem中的key={item.key}保证 React 复用 DOM,itemKey={item.key}保证 dnd-kit 索引一致。因此示例数据源中的key字段必须稳定且唯一。
四、底层佐证:List 栅格布局如何与拖拽共存
要从原理上理解这套方案,可以回到 List 组件对grid的实现(components/list/index.tsx 定义了ListGridType):
export interface ListGridType { gutter?: RowProps['gutter']; column?: ColumnCount; xs?: ColumnCount; sm?: ColumnCount; md?: ColumnCount; lg?: ColumnCount; xl?: ColumnCount; xxl?: ColumnCount; xxxl?: ColumnCount; }在渲染阶段(components/list/index.tsx):
- 设置
grid后,列表内容会包进栅格系统的Row,并应用gutter; - 列数优先取“当前激活断点”对应的
xs/sm/md/lg/xl/xxl列数,否则回退到column;每列的colStyle由100 / columnCount计算百分比宽度,因此响应式断点下的列数变化由 List 内部完成,不会与 dnd-kit 冲突。
也就是说,antd 负责“分列排布与间距”,dnd-kit 只负责“谁先谁后”,二者职责正交、互不干扰——这正是示例里SortableContext直接包住整个<List>即可生效的原因。
五、同目录系列示例:从纵向列表到拖拽手柄
components/list/demo/目录围绕“List + dnd-kit”提供了一组循序渐进的示例,可与本文网格版对照阅读:
| 示例 | 布局 | 差异点 |
|---|---|---|
| drag-sorting.md / drag-sorting.tsx | 纵向列表 | 传入verticalListSortingStrategy,并加restrictToVerticalAxis修饰符限定纵向拖拽 |
| drag-sorting-handler.tsx | 纵向列表 | 不把listeners放在整行上,而是挂到行内一个div手柄上 |
| grid-drag-sorting.md / grid-drag-sorting.tsx | 多列网格 | 本文主题,默认矩形排序策略,整卡可拖 |
| grid-drag-sorting-handler.tsx | 多列网格 | 网格版的手柄式拖拽,适合不希望整卡触发拖动的场景 |
对比 drag-sorting.tsx 可以发现一个可复用的演进规律:纵向版把children显式解构出来、将{...attributes} {...listeners}放到一个内部div上,即得到“拖拽手柄”交互;而网格版由于List.Item内部渲染为栅格Col,直接把属性和监听放在List.Item层上让整卡可拖,实现最简洁。若业务需要“仅手柄可拖 + 网格布局”,把 grid-drag-sorting 与 drag-sorting-handler 两种写法合并即可。
六、落地时的注意事项
- dnd-kit 属于第三方运行时依赖:antd 本身并不内置拖拽能力。在本仓库中,
@dnd-kit/core@^6.3.1、@dnd-kit/modifiers@^9.0.0、@dnd-kit/sortable@^10.0.0、@dnd-kit/utilities@^3.2.2出现在 package.json 的devDependencies中,仅用于文档站点示例。你在自己的项目中使用时,需要显式安装对应依赖(建议版本不低于示例所用版本)。 - 组件 API 选择:List 组件在开发模式下会输出一条弃用提示(见 components/list/index.tsx),提示 6.6.0 及以后版本推荐使用位于 components/listy 的新版列表组件。示例仍然基于
List,但“自定义渲染项包装第三方拖拽能力”的思路对Listy等组件同样适用。 - 稳定唯一 key:
itemKey与 Reactkey共用同一数据字段,删除/新增数据时要保证其稳定性,否则拖拽索引会错位。 - 触摸与点按:
PointerSensor的激活距离建议结合实际(如列表项内还有可点击按钮、链接)调整,必要时可换用MouseSensor/TouchSensor组合或更大的distance。 - 空值与边界:
onDragEnd中先判断active/over是否存在再处理,能避免快速拖拽、提前松手等边界态下的空指针问题。
七、小结
从 grid-drag-sorting.md 一句简短的说明出发,结合 grid-drag-sorting.tsx 的完整实现可以看到:antd List 的网格能力 + dnd-kit 的可排序算法是一对互补的组合。核心套路只有四步——①用useSortable封装自定义List.Item并把setNodeRef/attributes/listeners/transform挂上去;②用DndContext管理拖拽上下文与传感器;③用SortableContext声明参与排序的 id;④在onDragEnd中基于active.id/over.id借助arrayMove更新数据源。这套模式不依赖 antd 内部任何拖拽实现,属于纯 UI 结构层面的组合,因此可以平移到卡片墙、作品集栅格、应用宫格等任意基于 List 网格布局的场景。
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考