- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
@open-pencil/vue的属性面板(Property Panels)是设计工具中最复杂的 UI 区域之一:它既要实时反映当前选区的计算结果,又要安全地写回场景图,还要支持变量绑定与多对象同步编辑。本篇指南以@open-pencil/vue官方文档为核心,围绕“composable 优先、无头原语辅助”的架构哲学,讲解如何用usePosition、useFillControls等 composable 构建面板状态,用PropertyListRoot这类无外观(headless)原语组织可复用的列表结构,并深入BindableValueRoot的绑定语义,最终让你能独立搭建一套专业、可维护且尊重设计数据的属性面板。
一、面板架构哲学:Composable 优先,原语补位
@open-pencil/vue的属性面板设计有两个明确分工(见 property-panels.md):
- 面板主要需要“选区计算值 + 修改动作”时,优先使用 composable。因为面板的核心工作是把
x、opacity、fills这类选中节点的属性投影成响应式状态,再把用户的输入写回节点——这正是 composable 的职责。 - 面板需要可复用的数组/列表结构时,使用无头原语(headless primitive),例如
PropertyListRoot。当难点在于协调重复的列表、树或插槽结构时,由原语负责结构编排,外观完全交给调用方。
这一原则在 packages/vue/src/index.ts 的导出结构中可以得到印证:controls/目录集中存放usePosition、useLayout等控制 composable,primitives/目录则存放PropertyList、BindableValue等无头组件。
二、常用控制 Composables:标准属性区块的起点
2.1 单值属性区块
文档列出的五个标准 composable 对应面板中最常见的五个分区:
| Composable | 用途 | 源码位置 |
|---|---|---|
usePosition() | 位置与尺寸(x/y、宽高、旋转、对齐、翻转) | controls/position/use.ts |
useLayout() | 自动布局(方向、间距、内边距、网格轨道、尺寸策略) | controls/layout/use.ts |
useAppearance() | 可见性、不透明度、圆角半径 | controls/appearance/use.ts |
useTypography() | 文本排版(字体族、字重、字号等格式化控制) | controls/typography/use.ts |
useExport() | 导出相关属性 | controls/下的导出控制 |
以 usePosition 为例,源码显示它基于useNodeProps()获取当前选区的nodes、node、active、isMulti状态,然后派生出一组计算属性:
x、y、width、height:直接映射到选中(活动)节点,无节点时回退为0;rotation:对旋转角做了Math.round取整,适合直接渲染进数字输入框;- 动作层:
updateProp(key, value)(即时更新)、commitProp(key, value, previous)(提交到撤销栈)、cancelProp(key)(取消预览); - 多选能力:
align(axis, pos)调用editor.alignNodes,flip(axis)调用editor.flipNodes,rotate(degrees)调用editor.rotateNodes,全部基于ids(选中节点 id 列表)批量执行。
也就是说,usePosition不只是返回四个数字,它把“单选编辑、多选对齐/翻转/旋转、撤销提交”整套交互都封装好了。类似的,useLayout 内部由createLayoutSelectionState、createPaddingActions、createLayoutActions、createGridTrackActions几个工厂函数组合而成,覆盖自动布局方向、统一/对称/独立内边距、网格轨道增删等能力;useAppearance 则通过createAppearanceState+createAppearanceActions提供可见性与圆角编辑,并带有expandedCornerNodeId状态用于展开独立圆角输入。
2.2 列表型属性区块
对于填充(fills)、描边(strokes)、效果(effects)这类“一组对象”的属性,文档推荐三个 composable:
useFillControls()useStrokeControls()useEffectsControls()
以 useFillControls 为例,源码极其简洁但信息量很大:它继承useColorVariableBinding('fills')的全部行为,再额外暴露一个defaultFill——即DEFAULT_SHAPE_FILL(来自@open-pencil/core/constants)。这个默认值正是“添加填充”按钮要用到的素材:用户在面板里点“Add fill”,得到的就是一个符合 SDK 约定的默认填充对象,而不是调用方临时拼出来的结构。
三、示例实战一:位置与尺寸面板
文档给出的位置面板示例完整复刻如下:
<script setup lang="ts"> import { usePosition } from '@open-pencil/vue' const { x, y, width, height, updateProp, commitProp } = usePosition() </script> <template> <div class="grid grid-cols-2 gap-2"> <input :value="x" @input="updateProp('x', Number(($event.target as HTMLInputElement).value))" /> <input :value="y" @input="updateProp('y', Number(($event.target as HTMLInputElement).value))" /> <input :value="width" @input="updateProp('width', Number(($event.target as HTMLInputElement).value))" /> <input :value="height" @input="updateProp('height', Number(($event.target as HTMLInputElement).value))" /> </div> </template>结合源码可以把这个示例的每个细节讲透:
updateProp与commitProp的分工。在 usePosition 中,两者都来自usePropScrub(editor)。updateProp是即时写入选区节点(适合@input拖拽/连续输入时使用),commitProp则把“新值 + 旧值”一起提交给撤销系统(适合输入结束、失焦、@change时调用)。正确的做法是:拖动过程中持续updateProp,松手/失焦时commitProp,这样用户按Ctrl/Cmd+Z能整段回退。- 数字安全。示例里用
Number(...)把输入框字符串转成数值。updateProp的key类型是NumericNodeProperty(来自@open-pencil/scene-graph),只接受数字,所以在绑定到input事件时必须自己做转换与过滤。 - 多选行为。当
isMulti为真时,updateProp/commitProp作用于nodes.value全部节点——这就是多对象同步编辑的实现路径。若选中多个尺寸不同的节点,width/height等计算属性取值自“活动节点”(node.value),面板需要结合prop(属性合并状态)自行呈现“mixed(混合)”提示。
四、示例实战二:填充列表面板
文档给出的填充面板示例是“composable + 无头原语”协同的完整范本:
<script setup lang="ts"> import { PropertyListRoot, useEditorPropertyList, useFillControls } from '@open-pencil/vue' const fillControls = useFillControls() const fills = useEditorPropertyList('fills') </script> <template> <PropertyListRoot prop-key="fills" :items="fills.items.value" :mixed="fills.isMixed.value" @add="fills.actions.add" @remove="fills.actions.remove" v-slot="{ items, actions }" > <div v-for="(fill, index) in items" :key="index"> {{ fill.type }} <button @click="actions.remove(index)">Supprimer</button> </div> <button @click="actions.add(fillControls.defaultFill)">Ajouter un remplissage</button> </PropertyListRoot> </template>(示例按钮文案按法语原文保留为 “Supprimer / Ajouter un remplissage”,英文版对应 “Remove / Add fill”。)
这里有三层值得展开:
useEditorPropertyList('fills')是列表数据的来源。查看 controls/property-list/use.ts 的源码,它返回items(当前活动节点的fills数组)、isMixed(多选时数组属性是否混合)、isMulti、active,以及一整套actions:add(item):单对象时向节点数组追加(深拷贝structuredClone防共享引用);多对象时通过editor.undo.runBatch(label, apply)把对每个节点的写入包进一个批量撤销操作;remove(index):过滤掉指定下标,多对象同样走批量;update(index, item)/patch(index, changes):通过useUndoBatch的batch.ensure合并连续编辑,patch只合并部分字段变化;toggleVisibility(index):切换单项visible,且每次读取editor.getNode(node.id)获取最新节点,避免闭包里的过期引用;reorder(fromIndex, toIndex):用moveItem在数组内移动元素。
也就是说,你在模板里拿到的
fills.actions.add/remove,底层已经在处理“多选批量写入 + 撤销分组 + 引用隔离”这些脏活。PropertyListRoot是纯结构协调者。查看 PropertyListRoot.vue:它只负责接收prop-key、items、mixed、disabled,把add/remove/update/patch/toggleVisibility/reorder六个事件转发成 slot 里的actions对象,并通过providePropertyList提供给子组件(PropertyListItem、PropertyListAdd、PropertyListRemove、PropertyListVisibility等配套原语共享同一上下文,见 primitives/PropertyList)。它不渲染任何样式,连默认的disabled保护也只在actions里拦截事件转发。useFillControls().defaultFill是“添加”按钮的素材。没有它,你得自己拼一个合法的 fill 对象;有了它,actions.add(fillControls.defaultFill)一行就完成了。
五、变量绑定字段:BindableValueRoot 的语义与最佳实践
当面板字段可以引用变量(variable)或外部设计令牌(design token)时,文档要求用BindableValueRoot包住该字段。它在 primitives/BindableValue 下实现,BindableValueRoot.vue的源码展示了完整的“绑定感知”状态机(BindableValueRoot.vue):
- 它解析
provider(providerProp或注入的provideBindingProvider),缺少时直接抛错,保证误用能在开发期暴露; state通过provider.getState(targets)计算,取值包括unbound、bound、unresolved、mixed,并渲染成data-unbound、data-bound、data-unresolved、data-mixed、data-picker-open、data-policy等属性(stateAttrs),让外部样式可以纯靠 CSS 属性选择器区分状态;policy默认值为'detach-on-edit',其余可选值包括readonly-when-bound、edit-variable(见BindableValueRootProps定义)。
文档同时给出了五条绑定交互的硬性规范,这是构建“尊重用户数据”的面板必须遵守的:
- 空闲时显示变量身份,解析值放在辅助 UI:字段非编辑态应显示变量名(OpenPencil 应用皮肤里是紫色的变量名胶囊),解析出的计算值通过 tooltip 等支持性 UI 暴露;
- 聚焦或打开变量选择器绝不能破坏绑定:把焦点移入字段不等于用户要编辑,因此“聚焦即分离”是禁止的;
- 只在真正发生修改时才应用
detach-on-edit/readonly-when-bound/edit-variable:这三种策略都绑定在“用户实际改动值”这一事件上,而不是“字段获得焦点”上; - 显式的解除绑定动作放在选择器内部,而不是放在字段旁边一个容易误触的一次性图标按钮上;
- 把“替换绑定、编辑时分离、多对象更新”放进同一次 provider 批量操作:源码里
beginProviderBatch/commitProviderBatch/rollbackProviderBatch的交互批处理(supportsInteractionBatch)正是为此设计——一次交互要么整体生效,要么整体回滚,配合batchLabel(默认'Edit bound value')进入撤销栈。
关于第 1 条的“紫色胶囊”,文档特别注明这是 OpenPencil 应用皮肤的实现选择:字段空闲时显示紫色变量名,NumberField进入编辑模式时才揭示解析后的数字值。BindableValueRoot本身是无外观的,自定义编辑器外壳可以完全用不同的方式呈现同一套 headless 状态。
六、如何选择 API:一句经验法则
文档最后给出了一条可操作的判断标准:
- 需要直接控制逻辑(状态 + 动作)→ 用 composable。典型场景:位置、尺寸、透明度、圆角、排版、导出等标准分区,直接用
usePosition、useAppearance、useTypography等; - 难点在重复的列表/树/插槽协调 → 用结构原语。典型场景:填充、描边、效果这类可增删、可排序、可见性可切换的数组属性,用
PropertyListRoot配合useEditorPropertyList; - 两者不是互斥的:填充面板示例就是“composable(
useFillControls、useEditorPropertyList)提供数据与动作,原语(PropertyListRoot)负责结构”的组合用法。实践中,一个复杂面板通常是若干 composable 加上若干原语的混合体。
七、相关 API 速查
围绕本文主题,@open-pencil/vue还提供以下 API 供深入阅读:
- usePosition
- useLayout
- useAppearance
- useTypography
- useFillControls
- useStrokeControls
- useEffectsControls
- PropertyListRoot
如需了解这些 composable 与编辑器实例的关系(如useEditor、useNodeProps、useUndoBatch),可进一步阅读 SDK 架构文档 与 入门指南。
结语
OpenPencil 的属性面板 API 把“设计工具的脏活”封装成了清晰的层次:composable 承载选区状态与写入动作,无头原语承载列表/绑定结构,BindableValueRoot则用一套严谨的交互规范保护变量绑定不被误伤。掌握“先看状态来自哪个 composable、再看结构该用哪个原语”的判断方法,你就能在自定义编辑器外壳中快速复刻出专业级属性面板,同时保持代码的可测试性与可复用性。
- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
相关推荐
OpenMetadata 邮件配置完整指南:SMTP 参数解析、端口策略与源码实现原理
OpenMetadata 邮件配置完整指南:SMTP 参数解析、端口策略与源码实现原理 OpenMetadata 在用户注册、忘记密码、密码重置以及数据资产变更
前端桌面应用AI 应用MCP 服务OpenPencil SDK 组合式 API 实战:useStrokeControls 描边属性面板的完整指南
OpenPencil SDK 组合式 API 实战:useStrokeControls 描边属性面板的完整指南 useStrokeControls 是 Open
前端桌面应用AI 应用MCP 服务OpenPencil Vue SDK 无样式外观控件:AppearanceControlsRoot 插槽 API 与属性面板实战
OpenPencil Vue SDK 无样式外观控件:AppearanceControlsRoot 插槽 API 与属性面板实战 AppearanceContr
前端桌面应用AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考