news 2026/9/27 10:12:18

open-pencil SDK 进阶:useLayerDrag 实现图层树拖拽重排与跨容器移动的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
open-pencil SDK 进阶:useLayerDrag 实现图层树拖拽重排与跨容器移动的完整指南
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

AI-native design editor. Open-source Figma alternative.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载

useLayerDrag是 open-pencil Vue SDK 中负责图层树(Layer Tree)拖拽交互的底层接线工具,它基于 pragmatic-drag-and-drop 把"拖到哪、怎么放"解析为具体的编辑器重排指令。本文以 packages/docs/fr/programmable/sdk/api/advanced/use-layer-drag.md 为骨架,结合 useLayerDrag.ts 源码与核心编辑器重排实现,带你掌握如何在自定义图层树中接入拖拽重排、跨容器移动和自动展开等能力。

一、useLayerDrag 是什么

useLayerDrag(options)为图层树的每一行提供拖拽所需的状态与动作。它计算三个关键信息:

  • 拖放目标(drop target):指针当前悬停在哪一行上;
  • 插入位置(position):落在目标行的"之前 / 内部 / 之后"(before / inside / after);
  • 变更有效性(validity):该次拖放是否合法,例如不能把容器拖入自身的后代。

同时它负责把拖拽结果转换为editor的重排操作。值得注意的是,当一个对象跨容器移动时,其视觉位置会被保留——这是由底层reorderChildWithUndo实现的,稍后详述。

在 API 层级上,useLayerDrag属于 "Navigation & editing" 一组的进阶 API(见 packages/docs/fr/programmable/sdk/api/advanced/index.md),服务于那些"标准组件无法满足定制需求"的场景。

二、签名与返回值

函数签名

export function useLayerDrag( editor: Editor, indentPerLevel = 16, onMakeChildDrop?: (targetId: string) => void )

参数说明:

参数类型默认值说明
editorEditor—来自@open-pencil/core/editor的编辑器实例,提供graph、state与重排操作
indentPerLevelnumber16每一层缩进的像素数,用于计算命中区域的宽度,决定"上方 / 内部 / 下方"判定
onMakeChildDrop(targetId: string) => void—当元素被放成某容器子节点时回调(可用来展开该容器)

返回值

{ draggingId: Ref<string | null> // 当前正在拖拽的行 id instruction: Ref<TreeInstruction | null> // 当前的放置指令 instructionTargetId: Ref<string | null> // 指令作用的目标行 id setupItem: (el: Ref<HTMLElement | null>, item: () => DragItem) => void // 把某行接上拖拽/放置能力 }

其中TreeInstruction(即LayerDragInstruction)只有三种取值,定义于 packages/vue/src/primitives/LayerTree/context.ts:

export interface LayerDragInstruction { type: 'reorder-above' | 'reorder-below' | 'make-child' }

DragItem是行数据的形状:

interface DragItem { id: string level: number // 当前层级,供缩进命中计算 hasChildren: boolean // 是否有子节点,决定 expanded / standard 模式 parentId: string | null }

三、内部工作流:从拖拽到重排

整个流程由setupItem注册的若干 pragmatic-drag-and-drop 适配器,以及一个全局monitorForElements共同完成。

1. 行级接线:setupItem

setupItem(el, item)接收目标 DOM 元素与一个返回行数据的函数,通过watchEffect在元素可用时自动接线(useLayerDrag.ts):

function setupItem(el: Ref<HTMLElement | null>, item: () => DragItem) { watchEffect((onCleanup) => { const element = el.value if (!element) return const data = item() const isContainer = editor.graph.isContainer(data.id) const mode: ItemMode = data.hasChildren ? 'expanded' : 'standard' // ... draggable + dropTargetForElements onCleanup(cleanup) }) }
  • draggable:让行可被拖起,getInitialData提供源节点 id,拖起/落下时维护draggingId;
  • dropTargetForElements:让行可作为放置目标,其getData用attachInstruction根据指针位置、缩进宽度(indentPerLevel)、当前层级与模式计算放置指令。

放置模式与合法性由block数组控制,这是本工具最关键的一处策略:

block: isContainer ? ['reparent'] : ['make-child', 'reparent']

含义是:

  • 目标是容器时,允许把它作为"父容器"放子节点进去,但禁止把别的节点"重定位"到它之上/之下(reparent被 block);
  • 目标是普通叶子节点时,既不允许做成它的子节点,也不允许跨级重定位,只能"插到它上方或下方"。

同时canDrop禁止拖到自身:

canDrop: ({ source }) => source.data.id !== data.id

onDrag中通过extractInstruction取出指令,若为'instruction-blocked'(被 block 的非法操作)则清空指令与目标。getIsSticky: () => true保证拖拽悬停时目标行保持粘性。

2. 全局落点解析:monitorForElements

拖放结束时,全局 monitor 统一处理(useLayerDrag.ts):

const cleanupMonitor = monitorForElements({ onDrop: ({ source, location }) => { const target = location.current.dropTargets.at(0) if (!target) return // 解析 sourceId / targetId / rawInstruction ... if (editor.graph.isDescendant(targetId, sourceId)) return // 防拖入自身后代 const targetParentId = targetNode.parentId ?? editor.state.currentPageId const targetIndex = targetParent.childIds.indexOf(targetId) if (inst.type === 'reorder-above') { editor.reorderChildWithUndo(sourceId, targetParentId, targetIndex) } else if (inst.type === 'reorder-below') { editor.reorderChildWithUndo(sourceId, targetParentId, targetIndex + 1) } else { const container = editor.graph.getNode(targetId) if (!container || !editor.graph.isContainer(targetId)) return editor.reorderChildWithUndo(sourceId, targetId, container.childIds.length) onMakeChildDrop?.(targetId) } } }) onScopeDispose(cleanupMonitor)

三种指令的落点换算:

指令换算后的重排调用
reorder-above插入到目标行在其父节点的当前位置(targetIndex)
reorder-below插入到目标行之后(targetIndex + 1)
make-child插入到目标容器的末尾(childIds.length),并触发onMakeChildDrop回调

两条安全校验值得注意:

  1. editor.graph.isDescendant(targetId, sourceId)直接拦截"把节点拖进自己或自己后代"的非法操作;
  2. make-child分支再次用isContainer复核目标确实是容器。

四、视觉位置保留的底层原理

文档强调"对象更换容器时视觉位置被保留",这是核心编辑器层reorderChildWithUndo的职责,实现在 packages/core/src/editor/structure/reorder.ts:

function reorderChildWithUndo(nodeId: string, newParentId: string, insertIndex: number) { assertNodeEditable(ctx.graph, nodeId) assertNodeEditable(ctx.graph, newParentId) const node = ctx.graph.getNode(nodeId) if (!node) return const origParentId = node.parentId ?? ctx.state.currentPageId const origIndex = ctx.graph.getNode(origParentId)?.childIds.indexOf(nodeId) ?? 0 const origX = node.x const origY = node.y ctx.graph.reorderChild(nodeId, newParentId, insertIndex) ctx.runLayoutForNode(newParentId) if (origParentId !== newParentId) ctx.runLayoutForNode(origParentId) // ... 压入 undo 栈,forward/inverse 分别记录重做与撤销 }

关键点:

  • 它记录origParentId、origIndex、origX、origY,并连同forward/inverse一起压入撤销栈,因此每次拖拽重排都可撤销;
  • 同时注意同文件中的doReorderChild使用了getAbsolutePosition差值换算坐标,而reorderInAutoLayout专门处理自动布局(layoutMode !== 'NONE')父容器内的重排。useLayerDrag统一走reorderChildWithUndo,由内部runLayoutForNode负责新旧父容器的布局重算,从而维持画布上视觉位置的连续性与稳定性。

五、与 LayerTreeRoot / useLayerTree 的配合

useLayerDrag不是孤立 API,它被 LayerTreeRoot.vue 直接消费,并与上下文 API 深度绑定。

1. LayerTreeRoot 中的使用

LayerTreeRoot是 SDK 的无头结构原语(headless structural primitive),负责渲染图层树并提供树模型、展开状态、选择/可见性/锁定/重命名等交互接线,同时把useLayerDrag的结果注入上下文:

const { draggingId, instruction, instructionTargetId, setupItem } = useLayerDrag( editor, indentPerLevel, expandNode // 作为 onMakeChildDrop:拖成子节点时自动展开容器 )

注意这里第三个参数传的是expandNode——当节点被拖成某容器的子节点时,该容器会被自动展开,这正是文档所说onMakeChildDrop回调的典型用途。

随后整个树在编辑器事件驱动下增量重建:

const unsubscribe = [ editor.onEditorEvent('graph:replaced', rebuildTree), editor.onEditorEvent('page:changed', rebuildTree), editor.onEditorEvent('node:created', scheduleTreeRebuild), editor.onEditorEvent('node:deleted', scheduleTreeRebuild), editor.onEditorEvent('node:reparented', scheduleTreeRebuild), editor.onEditorEvent('node:reordered', scheduleTreeRebuild), editor.onEditorEvent('node:updated', patchTreeNode), editor.onEditorEvent('selection:changed', onSelectionChanged) ]

node:reparented与node:reordered都会触发树的微任务级重建(scheduleTreeRebuild合并同一轮改动),保证拖拽导致的层级变化立即反映在界面上。

2. 通过上下文提供

LayerTreeRoot把setupDrag: setupItem、draggingId、instruction、instructionTargetId等一并provideLayerTree(...)进上下文(见 context.ts 中的LayerTreeContext接口)。自定义行组件内部可用useLayerTree()(对应 use-layer-tree 文档)读取这些状态,并调用ctx.setupDrag(el, item)完成行级接线:

const ctx = useLayerTree() ctx.setupDrag(rowEl, () => ({ id: node.id, level: row.level, hasChildren: row.hasChildren, parentId: node.parentId }))

3. 与 LayerTreeItem 的分工

  • LayerTreeRoot:提供树结构与交互接线的无头根组件;
  • LayerTreeItem:渲染单行并暴露选择、展开、可见性、锁定、重命名处理器(通过默认插槽);
  • useLayerDrag:提供拖拽状态与指令解析;
  • useLayerTree:读取根组件注入的上下文。

因此,自定义拖拽 UI 的推荐做法是:在LayerTreeRoot的默认插槽内渲染自有行结构,行上调用setupItem(或上下文setupDrag)接线,拖拽状态从draggingId/instruction/instructionTargetId读取以驱动高亮与占位样式。

六、在自定义图层树中接入拖拽:最小示例

假设你要替换默认图层树行的拖拽样式,自定义行组件可以这样写:

<script setup lang="ts"> import { useLayerTree } from '#vue/primitives/LayerTree/context' const props = defineProps<{ nodeId: string; level: number; hasChildren: boolean }>() const ctx = useLayerTree() const rowEl = ref<HTMLElement | null>(null) ctx.setupDrag(rowEl, () => ({ id: props.nodeId, level: props.level, hasChildren: props.hasChildren, parentId: ctx.editor.graph.getNode(props.nodeId)?.parentId ?? null })) </script> <template> <div ref="rowEl" :class="{ 'drag-source': ctx.draggingId === nodeId, 'drop-above': ctx.instruction?.type === 'reorder-above' && ctx.instructionTargetId === nodeId, 'drop-below': ctx.instruction?.type === 'reorder-below' && ctx.instructionTargetId === nodeId, 'drop-inside': ctx.instruction?.type === 'make-child' && ctx.instructionTargetId === nodeId }" > <slot /> </div> </template>

要点:

  1. 必须处于LayerTreeRoot内部(否则useLayerTree会抛出'[open-pencil] useLayerTree() called outside <LayerTreeRoot>',见 context.ts);
  2. setupDrag的item回调应返回实时的id / level / hasChildren / parentId,拖拽命中计算依赖level与缩进宽度;
  3. 用draggingId标记正在拖拽的源行,用instruction+instructionTargetId组合渲染"上方 / 下方 / 内部"三种放置指示器;
  4. 若希望在拖成子节点时展开目标容器,把展开函数作为useLayerDrag的onMakeChildDrop传入(LayerTreeRoot正是这样做的)。

七、注意事项与最佳实践

  • 合法性与自防呆:block策略与canDrop(禁止拖到自身)、isDescendant(禁止拖入自身后代)三重防护共同保证了非法放置不会落库;
  • 缩进宽度影响判定:indentPerLevel决定"above / inside / below"三种区域划分的宽度,默认 16px,若你的行高或缩进不同,应显式传入匹配值;
  • 可撤销:所有落点最终都进入reorderChildWithUndo的撤销栈(reorder.ts),无需在 UI 层额外处理 undo;
  • 事件驱动刷新:拖拽引起的结构变化会触发node:reparented/node:reordered事件,由LayerTreeRoot增量重建树,无需手动刷新;
  • 生命周期:setupItem在元素卸载时通过onCleanup(cleanup)释放监听,全局 monitor 通过onScopeDispose清理,避免内存泄漏。

八、相关 API 导航

  • LayerTreeRoot:无头树结构原语,负责上下文注入与模型构建;
  • LayerTreeItem:单行渲染原语,暴露选择/展开/可见性/锁定/重命名处理器;
  • useLayerTree:读取LayerTreeRoot注入的上下文;
  • 源码:useLayerDrag.ts、LayerTreeRoot.vue、context.ts、reorder.ts。
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

AI-native design editor. Open-source Figma alternative.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载
上一篇:3分钟掌握B站会员购抢票神器:免费开源工具完整指南
下一篇:PotPlayer字幕翻译插件:让外语视频瞬间变中文的神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 10:12:16

出口电商平台怎么选?3个维度避开域名服务器坑

出口电商平台怎么选?3个维度避开域名服务器坑 域名服务器搞不懂,选出口电商平台时最容易踩坑。很多新手觉得买个服务器就行,结果备案卡住,或者服务器性能撑不住海外访问,最后网站打开像蜗牛。别慌,今天把怎么选出口电商平台这件事拆透。…

作者头像 李华
网站建设 2026/9/27 10:12:09

怎么搭建自己的网页:5个避坑注意事项

怎么搭建自己的网页:5个避坑注意事项 网站做好了没人访问,是不是因为你连域名和服务器都没配好? 很多老板觉得只要页面漂亮就行,结果上线后打开速度慢得像蜗牛,甚至直接打不开。 这时候再谈SEO优化就是扯淡,地基没打好,楼盖得再高也是危房。 今天不聊虚的,专门讲讲 怎么搭建自己的网页 里最容易被忽略的…

作者头像 李华
网站建设 2026/9/27 10:11:22

视觉网站建设哪家好?3步搞定被黑挂马的救命方案

视觉网站建设哪家好?3步搞定被黑挂马的救命方案 网站突然打开变成赌博页面,后台登录不进去,客户投诉链接带毒。这种时候,别急着重启服务器,先深呼吸。 我见过太多做视觉设计的老板,因为不懂技术,网站被黑后只能干瞪眼。其实, 网站被黑挂马不知道怎么办 ,是建站行业最痛的点。很多人找外包做 视觉网站建设…

作者头像 李华
网站建设 2026/9/27 10:10:54

网站建设完成后如何备案?3步搞定,服务器怎么选不踩坑

网站建设完成后如何备案?3步搞定,服务器怎么选不踩坑 刚把网站代码跑通,兴冲冲准备上线,结果卡在“备案”这一关?看着工信部那密密麻麻的条款,是不是脑子一团浆糊,完全不知道第一步该点哪里?别慌,这就是典型的“备案流程一头雾水”。很多设计师转前端的朋友,写代码没问题,但面对服务器选购和合规流程就抓瞎。今…

作者头像 李华
网站建设 2026/9/27 10:10:24

2026最新三合一网站管理系统实操指南:告别没人访问的尴尬

2026最新三合一网站管理系统实操指南:告别没人访问的尴尬 网站上线三个月,百度收录量只有两位数,后台数据显示日均UV不足5个。这种“建好即沉默”的痛点,在2026年的数字营销环境中愈发刺眼。很多站长以为流量低是内容问题,实则根源往往出在底层架构的冗余与低效上。传统的CMS系统往往将内容管理、SEO…

作者头像 李华
网站建设 2026/9/27 10:10:14

网站换程序多少钱?搞懂备案坑点与SEO实操

网站换程序多少钱?搞懂备案坑点与SEO实操 备案流程一头雾水,是不是让你在面对“网站换程序”时,心里没底,甚至不敢问报价?别慌,很多老板以为换个程序就是点几下鼠标,其实背后牵扯着服务器迁移、数据库清洗、甚至重新备案的麻烦事。…

作者头像 李华