☰
OpenPencil 导航面板开发指南:用 PageListRoot、LayerTreeRoot 构建页面与图层面板
2026/9/26 23:48:03 网站建设 项目流程
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

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

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

本文围绕 OpenPencil Vue SDK 的无头(headless)导航原语展开,讲解如何用PageListRoot/usePageList()搭建页面列表、用LayerTreeRoot搭建图层树,并结合useSelectionState驱动选择状态。读完本文,你将掌握侧边栏导航面板的完整构建思路:SDK 负责树结构与交互逻辑,应用自由决定样式与行组件,并可参考官方应用的 PagesPanel.vue 与 LayerTree 获得可落地的实现细节。

导航面板:页面导航与图层导航的组合

在设计工具中,侧边栏(sidebar)通常承担两类职责:

  • 页面导航(page navigation):列出文档内的多个页面,支持切换、新建、重命名、删除与排序;
  • 图层导航(layer navigation):以树形结构展示当前页面中的节点层级,支持选中、展开/折叠、可见性/锁定切换与重命名。

OpenPencil 的 Vue SDK(@open-pencil/vue)为这两类职责分别提供了无头原语(headless primitives):PageListRoot与LayerTreeRoot。所谓"无头",是指这些组件不携带任何预设外观——它们只负责维护数据、状态与交互行为,把pages、items、selectedIds等状态通过作用域插槽(v-slot)暴露出来,由应用自行决定 Markup 与样式。这正是 LayerTreeRoot.vue 中 "SDK-managed tree structure but app-owned presentation" 的设计意图。

页面导航:PageListRoot 与 usePageList()

PageListRoot 基础用法

原文档给出的最小示例清晰展示了PageListRoot的用法——通过v-slot解构出pages、currentPageId、switchPage、addPage:

<PageListRoot v-slot="{ pages, currentPageId, switchPage, addPage }"> <div> <button v-for="page in pages" :key="page.id" @click="switchPage(page.id)"> {{ page.name }} </button> <button @click="addPage()">New page</button> </div> </PageListRoot>

PageListRoot本身不产出任何 DOM 结构,v-for与按钮完全由应用编写。页面数据来自当前编辑器场景图(scene graph),因此页面的增删、切换都会立即反映到插槽数据中。

插槽暴露的完整数据与动作

结合 PageListRoot.vue 的插槽实现,PageListRoot实际暴露:

插槽属性类型说明
pages页面数组当前文档的全部页面(id、name、childIds等)
currentPageIdstring当前激活页面 id,可用于高亮当前页
isDivider(page) => boolean判断某页是否为"分割线"页(无子节点且名称匹配分隔符模式)
actions对象页面操作的统一集合:add、switch、rename、delete、move

actions与组件原生emit一一对应:add、switch、rename、delete、move。也就是说,即使不直接调用 composable,也能在插槽内通过actions.add()、actions.switch(pageId)等完成全部页面管理操作,这比原文档示例中的switchPage/addPage覆盖面更完整。

分割线(divider)识别机制

PageListRoot支持一个可选 propdividerPattern?: RegExp,用于识别作为视觉分隔符使用的"页面"。其默认模式为/^[-–—*\s]+$/,即名称仅由连字符、短破折号、长破折号、星号与空白组成的页面被视为分割线;同时要求该页childIds.length === 0(没有子节点)。应用可用isDivider在插槽内决定是否渲染分隔线样式——官方 PagesPanel.vue 中就用isDivider(pg)渲染一条细线并支持双击重命名。

usePageList():页面管理的 composable 形态

当不需要组件封装、只想在逻辑层直接操作页面时,可以使用usePageList()。其实现位于 packages/vue/src/primitives/PageList/usePageList.ts,返回:

const { editor, // Editor 实例 pages, // 响应式页面数组(useSceneComputed(editor.graph.getPages())) currentPageId, // computed,来自 editor.state.currentPageId switchPage, // editor.switchPage addPage, // editor.addPage deletePage, // editor.deletePage movePage, // editor.movePage renamePage // editor.renamePage } = usePageList()

从源码可以看出,pages通过useSceneComputed包装editor.graph.getPages(),因而对场景图变更保持响应式;而switchPage、addPage等动作直接透传自 Editor 实例。也就是说PageListRoot与usePageList()共享同一套底层实现,前者只是多了插槽与 emit 的声明式包装。官方应用在 PagesPanel.vue 中直接消费插槽内的actions(actions.add()、actions.switch(pg.id)、actions.rename、actions.delete、actions.move),并结合useInlineRename实现双击行内重命名、useFlatReorderDrag实现拖拽排序。

图层导航:LayerTreeRoot

基础用法

当希望 SDK 管理树的构建与行为、应用负责渲染与样式时,使用LayerTreeRoot:

<LayerTreeRoot v-slot="{ items, selectedIds, select, toggleExpand, getKey, getChildren }"> <TreeView :items="items" :selected-ids="selectedIds" :get-key="getKey" :get-children="getChildren" @select="select" @toggle-expand="toggleExpand" /> </LayerTreeRoot>

示例中的TreeView是应用自绘的树组件:items提供扁平节点数组,getKey/getChildren用于恢复层级,select与toggleExpand则回传用户交互。整个过程中 SDK 负责"树从哪来、选中怎么算、展开怎么存"。

插槽暴露的完整状态与动作

根据 LayerTreeRoot.vue,插槽实际提供:

插槽属性说明
itemsLayerNode[],树节点数组
flattenItems由底层 reka-uiTreeRoot提供的扁平化条目
visibleRows展开状态下可见的行({ node, level, hasChildren }),配合虚拟滚动
expanded当前展开的节点 id 数组(v-model 语义)
treeVersion树重建版本号,用于强制刷新缓存
selectedIds当前选中的节点 id 集合(Set<string>)
focused树是否聚焦
draggingId/instruction/instructionTargetId拖拽行、拖拽指令与目标 id
actionsselect、toggleExpand、setFocused、setVirtualizer

树节点LayerNode的结构定义在 packages/vue/src/primitives/LayerTree/context.ts:id、name、type、layoutMode、visible、locked以及可选的children——恰好覆盖图层树行渲染所需的全部信息。

树模型构建与增量更新

LayerTreeRoot内部通过buildLayerTreeModel(editor.graph, editor.state.currentPageId)从当前页面构建树模型,并订阅了以下编辑器事件以保持同步(见 LayerTreeRoot.vue):

  • graph:replaced、page:changed→ 整体重建树;
  • node:created、node:deleted、node:reparented、node:reordered→ 调度微任务重建;
  • node:updated→ 只对name、type、layoutMode、visible、locked等可修补字段做局部 patch,避免整树重建;
  • selection:changed→ 自动展开选中节点的祖先,并将选中项滚动到可见区域。

这种"全量重建 + 增量 patch"的组合,保证了图层树在大型文档下仍能保持流畅,也为虚拟滚动提供了稳定的行数据源。

选择行为:additive 与 range

LayerTreeRoot的select动作接收(id, selection),其中selection既可以是布尔值,也可以是{ additive, range }模式对象。官方应用 LayerTree.vue 中把鼠标事件转换为选择模式:按住 Meta/Ctrl 点击为增量选择(additive),按住 Shift 点击为范围选择(range)。底层还会通过layerSelectionForTarget依据可见行、当前选中集合与选择锚点计算最终选中集,并在非增量模式下同步画布容器作用域(syncCanvasScope)。

行渲染、虚拟滚动与拖拽

  • 行组件:SDK 还导出配套的LayerTreeItem行原语,以及useLayerTree()组合式函数用于在子组件中读取树上下文;在LayerTreeRoot之外调用useLayerTree()会抛出[open-pencil] useLayerTree() called outside <LayerTreeRoot>错误(见 context.ts)。
  • 虚拟滚动:LayerTreeRoot接受indentPerLevel(默认16,每层缩进像素)prop,并通过setVirtualizer让应用把 reka-uiTreeVirtualizer注册进来,配合visibleRows实现scrollToIndex定位(选中自动滚动即依赖此机制)。
  • 拖拽:拖拽能力由useLayerDrag提供,指令类型包括reorder-above、reorder-below、make-child,应用可据此渲染拖拽指示器并实现图层重排/嵌套。

官方应用对照

官方 LayerTree/LayerTree.vue 是LayerTreeRoot的生产级用法范例:它组合 reka-ui 的TreeItem、TreeVirtualizer与ContextMenu,用LayerTreeNodeRow.vue/LayerTreeRenameRow.vue作为行组件,并在右键时先选中目标节点再弹出上下文菜单。整个 LayersPanel.vue 再通过垂直 Splitter 把PagesPanel(页面列表)与图层树上下分区,构成了"页面上、图层下"的典型侧边栏。

用 useSelectionState 驱动面板状态

图层树与属性面板都依赖"当前选了什么"这一派生状态。useSelectionState()从当前编辑器读取选择并计算出一组响应式派生值,实现位于 packages/vue/src/editor/selection-state/use.ts:

返回字段说明
selectedIds选中节点 id 集合(Set<string>)
hasSelection是否至少选中一个节点
selectedNode主选中节点(首选节点)
selectedCount选中节点数量
selectedNodeType主选中节点的类型(INSTANCE/COMPONENT/GROUP等)
isInstance是否为实例(类型INSTANCE)
isComponent是否为组件(类型COMPONENT)
isGroup是否为编组(类型GROUP)
canCreateComponentSet选中项 ≥ 2 且全部为COMPONENT时,可创建组件集

基础用法示例:

<script setup lang="ts"> import { useSelectionState } from '@open-pencil/vue' const { hasSelection, selectedCount, isInstance } = useSelectionState() </script> <template> <div class="text-xs text-muted"> <span v-if="!hasSelection">No selection</span> <span v-else> {{ selectedCount }} selected <span v-if="isInstance">· instance</span> </span> </div> </template>

从源码看,selectedIds同样经useSceneComputed包装editor.state.selectedIds;canCreateComponentSet的判定逻辑是遍历选中集合、要求每个节点类型均为COMPONENT且数量不少于 2。这些派生值可以直接用于属性面板的启用/禁用状态、图层树右键菜单项显隐,以及与 useSelectionCapabilities、useEditorCommands 联动实现命令级操作。

典型布局:页面在上、图层在下

综合官方实践与 SDK 设计,一个完整的导航侧边栏通常遵循如下排布:

  • 页面列表位于侧边栏顶部:用PageListRoot渲染页面按钮/行,高亮currentPageId,提供新建、重命名、删除与拖拽排序;
  • 图层树位于其下:用LayerTreeRoot渲染节点层级,行内显示可见性、锁定图标,支持多选、展开折叠与拖拽重排;
  • 详情与行内重命名嵌入行组件:详情信息(如类型、布局模式)与重命名输入框都放在行组件内部,官方应用通过useInlineRename实现双击进入编辑态(参见 PagesPanel.vue 与 LayerTree.vue)。

这一模式的核心收益在于:结构、状态与行为由 SDK 保证一致性,而视觉与交互细节完全由应用掌控——无论是官方编辑器皮肤,还是完全自定义的编辑器外壳(custom editor shell),都可以复用同一套无头原语。

相关 API 与延伸阅读

  • usePageList:页面管理 composable 的完整文档
  • PageListRoot:页面列表无头组件文档
  • LayerTreeRoot:图层树无头组件文档
  • useSelectionState:选择派生状态 composable 文档
  • Property Panels 指南:与导航面板配套的属性面板构建思路
  • Custom Editor Shell 指南:自定义编辑器外壳的组装方式
  • Vue SDK 入口:PageListRoot、LayerTreeRoot、LayerTreeItem、usePageList等公共导出的统一出口
  • Getting Started:SDK 初始化与编辑器上下文注入
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

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

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

相关推荐

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询