v3 是对 v2 的重写。模型保持不变:一个 provider,加上在你自己的元素上调用的组合式函数。但大多数名称和选项都变了,排序移进了库里,你为每个列表写的放置处理函数也不再需要。本页把 v2 的每个部分对应到 v3,然后展示常见场景的前后对比。
改了什么
- 排序是内置的。在 v2 中,列表是一个放置区,它的行是带
[index, items]载荷的可拖拽元素,你在onDrop中用e.helpers.suggestSort()排序。在 v3 中,由makeSortableList和makeSortableItem完成:列表渲染你的数组的一份草稿,它在拖拽时重新排序,放置时把新数组写进你的 ref。大多数 v2 的放置处理函数直接删除,而不是移植。 - 东西会自己动。行滑到新位置(FLIP),浮层在放置时落到它的位置上,取消时飞回原处。
<TransitionGroup>不再需要。 - 仍然只有一个包。
@vue-dnd-kit/core包含列表、树、多项拖拽、修饰器、传感器和碰撞检测函数。Nuxt 模块保留;@vue-dnd-kit/actions(程序化拖拽)和@vue-dnd-kit/devtools是新增的可选包。 - 真正的浮层。它被传送到
body,显示一个深度克隆,尺寸与它将落入的位置一致。浮层组件以 props 的形式获得拖拽信息,而不是读取 provider 的内部状态。 - 用 id,而不是元素。选中集合、事件和拖拽状态都以你数据中的 id 为键。不再需要读取 DOM 元素的映射表。
- 类型和 accept 规则替代了普通可拖拽元素和放置目标上的
groups。列表按group名称互相交换项。 - 放置之前做决定。
onBeforeDrop接受、拒绝、让拖拽继续,或只接收部分项。onDrop只负责应用。 - 传感器。指针和键盘输入都是传感器。激活按指针类型分别设置,触摸拖拽通过长按开始,所以列表在手指下仍能滚动。
- 区域。
makeDragArea取代了makeConstraintArea和makeAutoScroll,自动滚动无需代码就能在每个滚动容器中工作。 - 无障碍默认开启:ARIA 属性、放置后焦点会回来,以及屏幕阅读器播报。
升级
npm i @vue-dnd-kit/core@^3
yarn add @vue-dnd-kit/core@^3
pnpm add @vue-dnd-kit/core@^3
bun add @vue-dnd-kit/core@^3
| 包 | v2 | v3 |
|---|---|---|
@vue-dnd-kit/core | 2.4.x | 3.x |
@vue-dnd-kit/nuxt | 1.x | 3.x |
@vue-dnd-kit/actions | — | 1.x,可选 |
@vue-dnd-kit/devtools | — | 3.x,可选 |
package.json中写着"@vue-dnd-kit/core": "^2…"的应用会一直停留在 v2,直到版本范围改为^3。- Vue 3.5 或更新版本。v2 没有依赖;v3 有一个:
@vueuse/core(14 或 15)。 - 浏览器:Chrome 和 Edge 104+、Safari 15.4+、Firefox 104+。
- v2 和 v3 共用同一个包名,所以应用要一次性迁移到 v3。没有兼容层。v2 的文档仍可从页面顶部的版本菜单访问。
推荐的工作顺序:
- 升级包,并重命名 provider 的 props(见下文)。
- 把每个可排序列表改成
makeSortableList和makeSortableItem,并删除它的放置处理函数。 - 移植普通的可拖拽元素和放置区。
- 把自定义预览移植成浮层组件,或 provider 的
#overlay插槽。 - 用
makeDragArea替换makeConstraintArea和makeAutoScroll,或者什么都不用。 - 删除 v2 时代的变通写法:可拖拽元素上的
tabindex和aria-grabbed、整个项上的touch-action: none、行外面的<TransitionGroup>、预览组件外面的markRaw。
新旧对照
表格中的无对应项表示 v3 中没有这个名称的东西;单元格里写明了改用什么。
一览
| v2 | v3 |
|---|---|
DnDProvider | DnDProvider,带有新的 props 和一个 #overlay 插槽。 |
makeDraggable | 自由拖拽用 makeDraggable;列表的行用 makeSortableItem。 |
makeDroppable | 普通放置目标用 makeDroppable;列表和树用 makeSortableList 或 makeSortableTree。 |
makeSelectionArea | makeSelectionArea。 |
makeConstraintArea | makeDragArea。 |
makeAutoScroll | 不需要:自动滚动是自动的。用 makeDragArea 或列表的 autoScroll 调整它。 |
useDnDProvider | 状态用 useDnD,事件用 useDnDMonitor。 |
injectionKey | useDnDContext() 和 provideDnDContext(app, context)。 |
DragPreview | DragPreview:现在是单个被拖拽项的克隆,用于你自己的浮层。 |
makeSnappedOverlayPosition、<DragPreview :grid> | snapToGrid 修饰器。 |
e.helpers.suggestSort、suggestSwap、suggestCopy、suggestRemove | 内置在列表中:见排序。 |
Provider
| v2 | v3 |
|---|---|
autoScrollViewport,不设置就关闭 | autoScroll:默认开启,作用于窗口和每个滚动容器。{ threshold, maxSpeed, acceleration } 或 false。 |
previewTo,默认渲染在 provider 内部 | overlayTo:选择器或元素,默认为 'body'。 |
#preview="{ preview }" 插槽 | #overlay 插槽,拖拽信息作为 props:items、active、over、accepted、status、mode、to。见浮层。 |
keyboard.keys.forDrag、forDrop、forCancel(在 useDnDProvider() 上设置) | :keyboard="{ keys: { start, drop, cancel } }"。 |
keyboard.step | :keyboard="{ step }":默认 25 px,用于自由拖拽。 |
keyboard.keys.forMove、forMoveFaster、keyboard.moveFaster | 无对应项。方向键负责移动;:kbd{value="Shift"} 让自由拖拽的步长变为四倍。 |
collision.throttle | 无对应项。collision 决定如何找到目标:pointerFirst(默认)、pointerWithin、rectIntersection、closestCenter、closestCorners,或你自己的函数。 |
| 每页一个 provider,从不嵌套 | 可以嵌套:最近的 provider 掌管一个项。整个应用一个 provider 仍然是目标。 |
在 v2 中没有对应项的新 props:activation、modifiers、sensors、animation、dropAnimation、animateChanges、haptics、copyModifier、overlayFit、overlayClass、overlayTransition、overlayPreviews、announcements、screenReaderInstructions 和 defaultStyles。provider 还会触发 drag-start、drag-move、drag-over、drag-end、drag-cancel、drag-resume 和 drag-settle。它们全都在 provider 指南中。
读取状态:useDnD
useDnDProvider() | v3 |
|---|---|
state:'dragging'、'activating'、'selecting'、'pending' 或 null | useDnD().status:'idle'、'activating'、'dragging'、'dropping' 或 'selecting'。'pending'(异步放置)对应 'dropping';null 对应 'idle'。 |
pointer.value.current | useDnD().pointer,经过修饰器之后的值。每个事件还有 delta,即相对起点的偏移。 |
entities.draggingMap | useDnD().active:id、ids、items、data、type、element、rect。 |
entities.selectedSet(元素) | useDnD().selection(id):has、add、delete、toggle、set、clear、ids。 |
hovered.droppable、hovered.draggable | useDnD().over:id、data、element、rect、accepted。 |
delay.progress、distanceProgress | useDnD().activation.progress,从 0 到 1。 |
preview.position、preview.size、preview.render、preview.to | 无对应项。库负责浮层的位置和尺寸。使用 overlayTo、#overlay 插槽或浮层组件。 |
entities.draggableMap、droppableMap、各个 visible…Set、scrollPosition | 无对应项。 |
| — | 新增:useDnD().cancel()、position、isAutoScrolling。 |
事件移到了 useDnDMonitor:见事件与放置决定。完整结构见 useDnD API。
makeDraggable
| v2 | v3 |
|---|---|
makeDraggable(ref, options, payload) | makeDraggable(ref, options):没有第三个参数。列表的行使用 makeSortableItem(ref, { item })。 |
id,虚拟列表中需要 | id:从你的数据中以 getter 传入。默认值来自 useId(),每个组件实例都不同。 |
data: () => card | data:值、ref 或 getter。通过 active.data 和 active.items[i].data 读回。 |
groups: ['dev'] | type: 'dev',由目标的 accept 匹配。见分组。 |
dragHandle: '.handle' | handle:选择器或元素 ref。 |
disabled | disabled。 |
modifier: { keys, method } | canDrag: (event) => event.altKey:每次按下时的检查。 |
activation: { distance, delay } | 按指针类型设置的 activation:{ mouse: { distance }, touch: { delay, tolerance }, pen }。同时设置 distance 和 delay 时,哪个先满足就从哪个开始拖拽。 |
activation.distance: { x, y, condition } | 无对应项。每种指针类型一个 distance。 |
render: markRaw(Preview) | overlay: Preview:组件以 props 的形式获得拖拽信息。见浮层。 |
placementMargins | 无对应项。可排序项上的放置目标以它的中间部分(25% 到 75%)计算;树用 nesting: 'hover' 嵌套。见放进可排序项。 |
events.onSelfDragStart、onSelfDragMove、onSelfDragEnd、onSelfDragCancel | 可拖拽元素上无对应项:使用 useDnDMonitor 或 watch(isDragging)。见事件。 |
events.onHover、onLeave,以及经过它上方的拖拽的 onDragStart… | 在同一个元素上用 makeDroppable:onDragEnter、onDragLeave。 |
返回 isDragging、selected | 相同。 |
返回 isDragOver | 可排序项返回 indicator(配合 placeholder: 'indicator')和 isPlaceholder;放置目标返回 placement。 |
返回 isAllowed | 可拖拽元素上无对应项。放置目标返回 isAccepting。 |
| — | 新增:label、axis、modifiers、keyboardMode、overlay: false 配合返回的 transform、dropAnimation、attributes、native。 |
makeDroppable
| v2 | v3 |
|---|---|
makeDroppable(ref, options, () => items) | makeDroppable(ref, options)。承载列表的区域就是一个 makeSortableList。 |
groups | accept:'*'、一个类型、类型列表或函数。 |
groupMatch: 'every'、'some' | acceptMode: 'all'、'some'。 |
events.onEnter、events.onLeave | onDragEnter、onDragLeave:独立的选项,没有 events 对象。 |
events.onDrop,可以返回 false 或 promise | onBeforeDrop 做决定,onDrop 负责应用。见放置决定。 |
events.onValidate | onBeforeDrop,返回要保留的 id。 |
data | data,通过 over.data 读回。 |
disabled | disabled。 |
返回 isAllowed | isAccepting。 |
返回 isDragOver:{ top, right, bottom, left, center } | isOver(布尔值),以及 placement:{ top, right, bottom, left },即指针所在的各个半边。没有 center。 |
| — | 新增:id、label、priority、overOverlay、overDropAnimation、external(来自电脑的文件)。 |
放置事件
v2 IDragEvent | v3 |
|---|---|
e.draggedItems | e.active.items:每一项都是 { id, item, data, label, type },抓起的项在最前。列表还会得到 e.items 和 e.keys。 |
e.draggedItems[0].data | e.active.data。 |
e.draggedItems[i].index、.items | 列表:e.from,{ list, index }。 |
e.dropZone:items、placement、data | e.over:id、data、element、rect、accepted。列表:e.to,{ list, index }。 |
e.hoveredDraggable | 无对应项:列表会自己算出 e.to.index。 |
e.helpers.suggestSort() | 列表负责写入放置结果;e.result 包含它改变的每个列表的新数组。 |
e.helpers.suggestSwap() | 在列表上设置 behavior: 'swap';e.partner 是与之交换的项。 |
e.helpers.suggestCopy() | group: { name, pull: 'clone' } 或复制键(:kbd{value="Alt"}),配合列表的 clone;e.mode 为 'copy'。 |
e.helpers.suggestRemove() | 在放置目标中调用 e.source.remove(),或使用列表的 dropOutside: 'remove'。 |
result.sameList | e.sameList。 |
e.helpers.insertAt、removeAt、removeIndexes、swapAt、getInsertSide、isAtZoneStart、isAtZoneEnd | 无对应项。列表不需要它们;对普通放置目标,按你的需要修改数据。 |
e.provider | useDnD()。 |
| — | 新增:e.pointer、e.delta、e.position(放到画板上的项该放在哪里)、e.mode。 |
排序
| v2 | v3 |
|---|---|
一个带 () => items 的 makeDroppable 列表 | makeSortableList(listRef, { items })。 |
一个带 () => [index, items] 的 makeDraggable 行 | makeSortableItem(el, { item: () => props.task })。 |
| 渲染你的数组 | 渲染返回的 items:拖拽时重新排序的草稿。 |
suggestSort('vertical')、suggestSort('horizontal') | strategy: 'vertical'(默认)、'horizontal' 或 'grid'。 |
用引用比较找出源数组和目标数组,再赋值 sourceItems 和 targetItems | 不需要:放置会写入它改变的每个列表。 |
<TransitionGroup> | 内置。animateChanges: true 为不经拖拽的变化(添加、删除、打乱)添加动画。 |
根据 isDragOver.top、.bottom 画插入线 | placeholder: 'indicator' 和项的 indicator.side。见插入线。 |
suggestSwap() | behavior: 'swap'。 |
只复制的面板(suggestCopy() 加一个 copyOnly 标志) | group: { name, pull: 'clone', put: false }、sort: false 和 clone。 |
用 suggestRemove() 的回收区 | makeDroppable(trash, { onDrop: (e) => e.source?.remove() }),或 dropOutside: 'remove'。 |
| Pinia 或不可变数据:自己赋值结果 | 把 items 写成 getter,然后在 onDrop 中调用 e.preventDefault() 并应用 e.result。见受控列表。 |
递归树:每个文件夹一个 droppable,再加 applyToTree | makeSortableTree:一个容器,行带着各自的深度扁平渲染。见树。 |
用 v-model 的嵌套分组(每个分组一个 droppable) | 列表套列表,全部在同一个 group 中,每个都用 items: defineModel()。 |
虚拟列表:每个可拖拽元素都要 id | 把草稿交给虚拟化器,在行上设置 id: () => props.item.id。见虚拟列表。 |
前后代码见下文。关于列表的一切都在可排序列表和共享项的列表中。
分组
v2 用一个 groups 选项处理所有情况。v3 把它拆成两个:
| v2 | v3 |
|---|---|
makeDraggable(el, { groups: ['dev'] }) | makeDraggable(el, { type: 'dev' })。 |
makeDroppable(el, { groups: ['dev'] }) | makeDroppable(el, { accept: 'dev' })。 |
放置区上的 groups: ['dev', 'ops'] | accept: ['dev', 'ops'],或一个函数:({ active, items, types }) => boolean。 |
groups: [],或不设置 | 省略 type 或 accept。v2 的规则仍然成立:没有类型的可拖拽元素符合任何规则,没有 accept 的放置目标接受一切。 |
groupMatch: 'every'、'some' | acceptMode: 'all'、'some'。 |
列表放置区及其行上的 groups | makeSortableList 上的 group: 'board'。group: { name, pull, put } 说明什么可以离开、什么可以进入。 |
makeSelectionArea 上的 groups | accept。 |
没有 group 的列表自成一组,不与任何其他列表交换项。在 v2 中,没有 groups 的放置区接受一切。见目标接收哪些拖拽。
浮层
| v2 | v3 |
|---|---|
在 provider 中渲染、通过 previewTo 传送的预览 | 总是被传送,到 body 或 overlayTo。 |
一个克隆:相同的标签、它的 innerHTML 和尺寸 | 深度克隆:表单的值、<canvas> 的像素、继承的样式和表格单元格都会带上。用 data-dnd-no-clone 标记不需要带上的部分。 |
render: markRaw(Preview),读取 useDnDProvider().entities | overlay: Preview,以 props 的形式获得 items、active、over、accepted、status、mode 和 to。不需要 markRaw;切换它时用 shallowRef。 |
#preview 插槽,根据 preview.position 定位 | #overlay 插槽:库负责盒子的位置和尺寸。什么都不渲染时退回到克隆。 |
useDnDProvider().preview.render.value = … | #overlay 插槽,或作为 overlay 传入的 shallowRef。 |
在放置区上方换一种外观,通过 onEnter 和 onLeave | makeDroppable 或列表上的 overOverlay,或在浮层组件中读取 over。见改变浮层的目标。 |
<DragPreview v-slot="{ draggingMap }"> | 浮层 props 中的 items;<DragPreview :id="item.id" /> 渲染一个克隆。 |
<DragPreview :grid>、makeSnappedOverlayPosition | snapToGrid 修饰器。它吸附的不只是画面,还有查找目标的位置和放置的 position。 |
DragPreview 外面的 <Transition> 或 motion 的 AnimatePresence | 拿起用 overlay-class 配合 @starting-style,落位用 dropAnimation,组件之间用 overlayTransition。见抬起、倾斜、落定。 |
.dnd-kit-preview | 盒子上的 [data-dnd-overlay]("dragging"、"dropping"),它内部的 [data-dnd-overlay-content]。 |
preview.size,需要手动更新 | 盒子取它将落入的位置的尺寸(overlayFit)。 |
| 预览在放置时消失 | 它落到自己的位置上,或在取消时飞回原处。 |
更多内容见浮层指南。
选择与多项拖拽
| v2 | v3 |
|---|---|
selected,由 makeDraggable 返回 | selected,由 makeDraggable 和 makeSortableItem 返回。 |
entities.selectedSet,元素的集合 | useDnD().selection,以 id 为键。 |
makeSelectionArea:默认需要按住 Ctrl(ControlLeft) | 默认不需要按键:在区域的空白处按下就会画出选框。modifier: { keys: ['Control'], method: 'every' } 可以恢复 Ctrl。 |
modifier.keys 使用 KeyboardEvent.code | KeyboardEvent.key('Control'、'Shift')或 .code('ControlLeft')。 |
strategy: 'toggle'、'select' | 相同。 |
groups | accept。 |
events.onSelected(elements) | onSelected(ids)。 |
disabled | 相同。 |
用 suggestSort() 批量移动选中集合 | 由列表移动它。multiple: 'group'(默认)、'gaps' 或 'stack' 决定其余项在途中如何表现。 |
混合选中时用 onValidate | acceptMode: 'some' 加上返回 id 的 onBeforeDrop。见只接受一部分。 |
更多内容见多项拖拽与选择。
轴、边界与吸附
| v2 | v3 |
|---|---|
makeConstraintArea(el, { axis: 'x' }) | makeDragArea(el, { axis: 'x' }),作用于在它内部开始的拖拽。axis 也可用于可拖拽元素、列表、树或项。 |
axis: 'both' | 省略 axis。 |
restrictToArea: true | makeDragArea(el, { restrictToArea: true }),或修饰器 restrictToParent 和 restrictToElement(el)。 |
| 每个区域一个 provider,以隔开规则 | 一个 provider,每个区域一个 makeDragArea。区域里的项仍然可以移动到任何地方。 |
<DragPreview :grid="20">、makeSnappedOverlayPosition | snapToGrid(20) 或 snapToGrid({ x, y, origin, smooth }),放在 provider、区域、列表或可拖拽元素的 modifiers 中。 |
| — | 新增:restrictToWindowEdges、restrictToFirstScrollableAncestor、snapCenterToCursor,以及你自己的修饰器。 |
更多内容见轴、边界与网格。
自动滚动
| v2 | v3 |
|---|---|
<DnDProvider :autoScrollViewport="true">,默认关闭 | 默认开启,作用于窗口和每个滚动容器。:auto-scroll="false" 关闭它。 |
makeAutoScroll(el, { threshold, speed }) | 不需要:任何 overflow: auto 或 scroll 的元素都会滚动。要调整某一个:makeDragArea(el, { autoScroll: { … } }) 或列表的 autoScroll。 |
threshold,单位 px,或按边设置 | threshold:容器尺寸的比例(0.2),介于 24 到 96 px 之间。按边设置的阈值无对应项。 |
speed,一个倍数 | maxSpeed(每帧 25 px)和 acceleration(10)。 |
disabled: true | autoScroll: false。 |
isScrolling | isAutoScrolling,来自 makeDragArea 或 useDnD()。 |
键盘与无障碍
| v2 | v3 |
|---|---|
每个可拖拽元素上的 tabindex="0" | 自动设置,同时设置 role、aria-roledescription、aria-describedby、aria-disabled 和 aria-pressed。你自己的属性优先。 |
手动写 :aria-grabbed="isDragging" | 删掉它:aria-pressed 会跟随拖拽。 |
在 useDnDProvider().keyboard.keys 上设置按键 | provider 的 keyboard prop。绑定匹配 event.code 或 event.key。 |
方向键按 keyboard.step 移动预览 | 自由拖拽:每次 keyboard.step px。列表:按位置移动,并能移进分组中的下一个列表。keyboardMode: 'droppables' 在放置目标之间跳转。 |
重新绑定方向键(forMove、forMoveFaster) | 无对应项。 |
| 没有播报 | 实时区域播报每一次拿起、移动、放置和取消。label 为事物命名;announcements 和 screenReaderInstructions 改写措辞。 |
更多内容见无障碍。
事件与放置决定
| v2 | v3 |
|---|---|
可拖拽元素的 events.onSelfDragStart | useDnDMonitor({ onDragStart }),检查 active.ids。如果只是为了加类名,isDragging 就够了。 |
onSelfDragMove | 监听器的 onDragMove。 |
onSelfDragEnd,放置或取消时 | 监听器的 onDragEnd(放置)和 onDragCancel。onDragSettle 两者都能收到,在浮层落位之后。 |
onSelfDragCancel | 监听器的 onDragCancel。 |
可拖拽元素的 onHover、onLeave,以及经过它上方的拖拽的 onDragStart… | 在该元素上用 makeDroppable:onDragEnter、onDragLeave。列表的行什么都不需要。 |
droppable 的 events.onEnter、onLeave | onDragEnter、onDragLeave。 |
droppable 的 events.onDrop | onBeforeDrop 做决定,然后 onDrop 负责应用。onDrop 是同步的,它的返回值会被忽略。 |
droppable 的 events.onValidate | onBeforeDrop,配合 acceptMode: 'some'。 |
| — | 新增:provider 的 @drag-start … @drag-settle,以及监听器的 onDragOver、onBeforeDrop 和 onDragResume。 |
v2 的 onDrop 可以返回一个值或一个值的 promise 来决定放置。在 v3 中,这是 onBeforeDrop 的工作,可用于列表、树、放置目标和监听器:
v2 onDrop 返回 | v3 onBeforeDrop 返回 |
|---|---|
什么都不返回,或 true:放置生效 | 'accept'、true 或什么都不返回。 |
false:这次放置被拒,拖拽保持进行 | 'continue'。现在 false 会拒绝放置。 |
| 被拒绝的 promise 或抛出异常:拖拽被取消 | 'reject' 或 false。抛出异常也会拒绝。 |
一个 promise;期间 state 为 'pending' | 一个 promise;期间 status 为 'dropping',浮层带着 data-dnd-overlay="dropping" 等待。 |
onValidate 返回 IDragItem[] | 一个 id 数组:只有这些项会放置,其他的飞回原处。 |
监听器先回答,然后是目标。接着目标的 onDrop 运行,列表写入它的新数组,随后是 onDragEnd。更多内容见决定放置和事件 API。
类型
| v2 | v3 |
|---|---|
IDragEvent | DnDEvent(放置目标、监听器)、SortableDropEvent(列表)、DnDEndEvent(onDragEnd)。 |
IDragItem | DragItem。 |
IDropZoneContext | Over。 |
IHoveredDraggableContext | 无对应项。 |
ISuggestSortResult、ISuggestSwapResult、ISuggestCopyResult、ISuggestRemoveResult | SortableDropEvent:result、to、mode、partner。 |
IDropValidateEvent | DnDEvent,在 onBeforeDrop 中。 |
IPlacement | Placement,没有 center。 |
ICoordinates | Coordinates。 |
IDragActivationOptions | ActivationOptions。 |
IAutoScrollOptions | AutoScrollOptions。 |
TDnDState | DnDStatus。 |
IDnDProviderExternal | DnDState,即 useDnD() 的返回值。 |
IDraggableEvents、IDroppableEvents | DraggableOptions、DroppableOptions 和 DnDMonitor 中的回调。 |
Nuxt
npm i @vue-dnd-kit/nuxt@^3 @vue-dnd-kit/core@^3
v2(@vue-dnd-kit/nuxt 1.x) | v3(@vue-dnd-kit/nuxt 3.x) |
|---|---|
自动导入 makeDraggable、makeDroppable、makeSelectionArea、makeConstraintArea、makeAutoScroll、useDnDProvider | 自动导入 makeDraggable、makeDroppable、makeSortableList、makeSortableItem、makeSortableTree、makeSelectionArea、makeDragArea、useDnD、useDnDMonitor。 |
| 不自动导入类型 | 主要类型会自动导入(DnDEvent、SortableDropEvent、OverlayProps…)。通用类型(Key、Active、Over)需手动导入。 |
| — | 修饰器、碰撞检测函数和传感器按需开启:vueDndKit: { autoImports: { modifiers: true, collision: true, sensors: true } }。 |
DnDProvider 和 DragPreview 全局注册 | 相同,并多了一个 prefix 选项。 |
布局中、provider 内部的 <DragPreview /> | 删掉它:浮层由 provider 渲染。DragPreview 接收一个 id,应放在浮层组件内部。 |
peer 依赖 @vue-dnd-kit/core 2 | peer 依赖 @vue-dnd-kit/core 3;Nuxt 4。 |
nuxt.config.ts 中的 modules: ['@vue-dnd-kit/nuxt'] 保持不变。见安装。
前后对比
一个可拖拽元素和一个放置区
v2
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
groups: ['task'],
dragHandle: '.handle',
data: () => props.card,
});
</script>
<template>
<div ref="el" tabindex="0" :class="{ dragging: isDragging }">
<span class="handle">⋮⋮</span>
{{ card.title }}
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';
const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragOver, isAllowed } = makeDroppable(el, {
groups: ['task'],
events: {
onDrop: (e) => emit('drop', e.draggedItems.map((item) => item.data as Card)),
},
});
</script>
<template>
<div ref="el" :class="{ over: isDragOver && isAllowed }"><slot /></div>
</template>
v3
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
id: () => props.card.id, // read back as `active.id`
type: 'task', // matched by the targets' `accept`
handle: '.handle',
data: () => props.card,
});
</script>
<template>
<div ref="el" :class="{ dragging: isDragging }">
<button type="button" class="handle" :aria-label="`Move ${card.title}`">⋮⋮</button>
{{ card.title }}
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';
const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isOver, isAccepting } = makeDroppable(el, {
accept: 'task',
onDrop: (e) => emit('drop', e.active.items.map((item) => item.data as Card)),
});
</script>
<template>
<div ref="el" :class="{ over: isOver && isAccepting }"><slot /></div>
</template>
- 卡片会自动获得
tabindex、role和aria-*属性。<button>手柄可以获得焦点,键盘拖拽从它开始。 groups在可拖拽元素上变成了type,在放置目标上变成了accept。回调离开了events对象。- 有
isOver而没有isAccepting,说明这个目标拒绝这次拖拽:在那里放置会飞回原处。
一个可排序列表
v2
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type IDragEvent } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';
const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');
makeDroppable(
el,
{
events: {
onDrop(e: IDragEvent) {
const r = e.helpers.suggestSort('vertical');
if (r) tasks.value = r.targetItems as Task[];
},
},
},
() => tasks.value,
);
</script>
<template>
<div ref="el">
<TransitionGroup name="list">
<TaskItem v-for="(task, index) in tasks" :key="task.id" :task="task" :index="index" :tasks="tasks" />
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ task: Task; index: number; tasks: Task[] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, { id: props.task.id }, () => [props.index, props.tasks]);
</script>
<template>
<div ref="el" :class="{ 'is-dragging': isDragging }">{{ task.title }}</div>
</template>
v3
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';
const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');
const { items } = makeSortableList(el, { items: tasks });
</script>
<template>
<ul ref="el">
<TaskItem v-for="task in items" :key="task.id" :task="task" />
</ul>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableItem } from '@vue-dnd-kit/core';
const props = defineProps<{ task: Task }>();
const el = useTemplateRef<HTMLElement>('el');
const { isPlaceholder } = makeSortableItem(el, { item: () => props.task });
</script>
<template>
<li ref="el" :class="{ slot: isPlaceholder }">{{ task.title }}</li>
</template>
- 渲染
items,也就是草稿:它在拖拽时重新排序。放置时tasks得到新的顺序,defineModel触发update:modelValue。 - 没有索引、没有载荷、没有
onDrop、没有<TransitionGroup>:行会自己滑动。 - 被拖拽的行作为它将落入的位置留在列表中。用
isPlaceholder或[data-dnd-placeholder]给它加样式,而不是把它隐藏。
共享项的列表
v2
// Each column's makeDroppable calls this from its onDrop, with its array as the payload.
function applySort(e: IDragEvent) {
const r = e.helpers.suggestSort('vertical');
if (!r) return;
const src = e.draggedItems[0]?.items;
const tgt = e.hoveredDraggable?.items ?? e.dropZone?.items;
if (src === todo.value) todo.value = r.sourceItems as Task[];
else if (src === done.value) done.value = r.sourceItems as Task[];
if (!r.sameList) {
if (tgt === todo.value) todo.value = r.targetItems as Task[];
else if (tgt === done.value) done.value = r.targetItems as Task[];
}
}
v3
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';
const props = defineProps<{ column: { id: string; tasks: Task[] } }>();
const el = useTemplateRef<HTMLElement>('el');
const { items } = makeSortableList(el, {
id: props.column.id, // `from.list` and `to.list` in drop events
items: toRef(props.column, 'tasks'), // writable: the parent passes a reactive column
group: 'board', // same name: tasks move between these lists
});
</script>
<template>
<ul ref="el">
<TaskItem v-for="task in items" :key="task.id" :task="task" />
</ul>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';
const columns = reactive([
{ id: 'todo', tasks: [{ id: 't1', title: 'Write the docs' }] },
{ id: 'done', tasks: [] },
]);
</script>
<template>
<Column v-for="column in columns" :key="column.id" :column="column" />
</template>
放置会写入两个数组:任务离开的那个和它进入的那个。TaskItem.vue 就是上面可排序列表中的那个。分发副本的面板过去靠 suggestCopy() 和一个 copyOnly 标志;现在它就是一个列表:
makeSortableList(el, {
items: blocks,
group: { name: 'board', pull: 'clone', put: false }, // copies go out, nothing comes in
sort: false, // its own order stays
clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});
对于 Pinia store 或其他你不直接写入的状态,把 items 写成 getter,在 onDrop 中调用 e.preventDefault() 并应用 e.result:见受控列表。
自定义浮层
v2
makeDraggable(el, {
render: markRaw(TaskOverlay),
data: () => props.task,
});
<script setup lang="ts">
import { computed } from 'vue';
import { useDnDProvider } from '@vue-dnd-kit/core';
const { entities } = useDnDProvider();
const task = computed(() => {
const [node] = entities.draggingMap.keys();
return node ? (entities.draggableMap.get(node)?.data?.() as Task) : null;
});
</script>
<template>
<div class="task-preview">{{ task?.title }}</div>
</template>
v3
makeDraggable(el, {
id: () => props.task.id,
overlay: TaskOverlay,
data: () => props.task,
});
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null; accepted: boolean }>();
const task = computed(() => props.items[0]!.data as Task);
</script>
<template>
<!-- Fills the overlay box, which takes the size of the slot. -->
<div class="task-preview" :class="{ refused: over && !accepted }">
{{ task.title }}
<span v-if="items.length > 1" class="count">{{ items.length }}</span>
</div>
</template>
<style scoped>
.task-preview { width: 100%; height: 100%; }
</style>
- 组件以 props 的形式获得拖拽信息:
items(每一个被拖拽的项,抓起的项在最前)、over、accepted、status、mode和to。只声明你要读取的那些。对于列表的行,列表项是items[0].item。 - 原样传入它。用
shallowRef切换它,绝不要用深层ref。
provider 的插槽也以同样的方式改变,你的组件不再需要自己定位:
v2
<DnDProvider>
<RouterView />
<template #preview>
<MyOverlay /> <!-- moved itself with useDnDProvider().preview.position -->
</template>
</DnDProvider>
v3
<DnDProvider>
<RouterView />
<template #overlay="{ items, over, accepted }">
<MyOverlay :items="items" :over="over" :accepted="accepted" />
</template>
</DnDProvider>
把 DragPreview 包在 <Transition> 中实现的弹出效果,改为 overlay-class 配合 @starting-style:见抬起、倾斜、落定。
需要确认的放置
v2
makeDroppable(el, {
events: {
onDrop: (e) =>
new Promise<void | boolean>((resolve) => {
if (!window.confirm('Drop here?')) return resolve(false); // keep dragging
place(e.draggedItems);
resolve();
}),
},
});
v3
makeDroppable(el, {
onBeforeDrop: () => (window.confirm('Drop here?') ? 'accept' : 'continue'),
onDrop: (e) => place(e.active.items),
});
列表也用同样的方式询问,而 promise 让你可以打开自己的对话框。在 promise 完成之前,浮层停在松开的地方等待:
makeSortableList(el, {
items: toRef(props.column, 'tasks'),
group: 'board',
// `ask` opens a dialog and resolves 'accept', 'continue' or 'reject'.
onBeforeDrop: (e) => (e.sameList ? 'accept' : ask(e)),
});
一个只接收混合选中集合中匹配部分的放置区(groupMatch: 'some'):
v2
makeDroppable(el, {
groups: ['frontend'],
groupMatch: 'some',
events: {
onDrop: (e) => place(e.draggedItems), // only the items of the group arrive
},
});
v3
import type { DragItem } from '@vue-dnd-kit/core';
const fits = (item: DragItem) => item.type.length === 0 || item.type.includes('frontend');
makeDroppable(el, {
accept: 'frontend',
acceptMode: 'some',
onBeforeDrop: (e) => e.active.items.filter(fits).map((item) => item.id), // the others fly back
onDrop: (e) => place(e.active.items.filter(fits)), // `active.items` still lists every dragged item
});
列表不用写代码就能做到这一点:设置 acceptMode: 'some' 后,它们的 group.put 接受的项会落位,其他的飞回原处。
升级时的坑
- 用版本范围安装 v3。在
package.json中写^3:npm update会让^2停留在 v2。 - 渲染草稿。列表渲染
makeSortableList返回的items,而不是你的数组。如果渲染你的数组,在放置之前什么都不会动。 - 不要排序两次。删掉列表中的
suggestSort代码:列表会在它的onDrop之后把每次放置写进可写的itemsref。要自己应用,就调用e.preventDefault()并使用e.result。 false会拒绝放置。在 v2 中,resolve 为false的onDrop会让拖拽继续。在 v3 中,从onBeforeDrop返回'continue'。从onDrop返回的 promise 会被忽略。- 列表只在同一个
group内共享。没有group的列表不与任何其他列表交换项。 - 用 id 作为行的 key。
:key="index"会让错误的行产生动画。也要从你的数据中传入 id:默认 id 对每个组件实例都不同。 - 不要用
<TransitionGroup>,也不要在transform上加transition。库会自己移动行,否则移动会被动画两次。 - 撤掉触摸的变通写法。整个项上的
touch-action: none会让页面无法在手指下滚动;v3 改为通过长按开始触摸拖拽。在需要立即拖动的手柄上保留它,并配合activation: { touch: { distance: 4 } }。 - 移除过时的属性。库会设置
tabindex、role和它需要的aria-*,并且从不覆盖你的属性,所以遗留的role或aria-grabbed会保持你留下的样子。 - 重新设置浮层样式。
.dnd-kit-preview已经没有了:给[data-dnd-overlay='dragging']内部的内容加样式,绝不要碰盒子的transform。给浮层组件的根元素设置width: 100%; height: 100%,并把<DragPreview />从你的布局中移除。 - 自动滚动是开启的。在 v2 从不滚动的地方传入
:auto-scroll="false"。 - 选框不需要按键。
makeSelectionArea在它的空白处普通按下就会开始画框。添加一个modifier可以保留 v2 的 Ctrl。 - 一个 provider,区域放在里面。项不能跨越 provider。过去用每个区域一个 provider 来隔开规则的页面,需要改为一个 provider 加每个区域一个
makeDragArea。 - 两个参数。
makeDraggable和makeDroppable接收一个 ref 和一个选项对象,没有载荷。