ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenPencil 外观控件无头根组件 AppearanceControlsRoot 深度解析:自定义属性面板的插槽契约与实现原理

OpenPencil 外观控件无头根组件 AppearanceControlsRoot 深度解析:自定义属性面板的插槽契约与实现原理 前端桌面应用AI 应用MCP 服务【免费下载链接】open-pencilAI-native design editor. Open-source Figma alternative.项目地址https://gitcode.com/gh_mirrors/op/open-pencil点击查看免费下载AppearanceControlsRoot是 OpenPencil Vue SDK 中面向外观Appearance属性面板的无头Headless根组件它将useAppearance()组合式函数返回的插槽契约以结构化原语的形式暴露出来让开发者可以在不依赖任何现成 UI 皮肤的前提下自行渲染可见性、不透明度、混合模式、圆角半径与圆角平滑等外观控件。读完本文你将掌握该组件的完整插槽 API、多选与 MIXED 状态的处理方式、独立圆角independent corners的判定逻辑以及如何基于它搭建一套可复用、绑定感知binding-aware的自定义外观属性面板。组件定位为什么需要它在 Property Panels 指南 中OpenPencil 的open-pencil/vue采用组合式函数优先composable-first的设计哲学如果面板主要需要基于选区推导出的值 更新动作优先使用usePosition()、useLayout()、useAppearance()、useTypography()、useExport()这类组合式函数只有当面板需要可复用的数组/列表结构时才使用PropertyListRoot之类的无头列表原语。AppearanceControlsRoot恰好处于两者的交汇点它不提供任何渲染结果而是把useAppearance()内部状态和动作统一成一个插槽契约slot contract作为结构化原语structural primitive暴露给消费者。正如英文原版文档所述AppearanceControlsRootexposes the slot contract returned byuseAppearance()as a structural primitive. Use it when you want reusable appearance controls with custom presentation.也就是说当你想复用一套外观控件的逻辑选区推导、多选合并、撤销分组但又想完全自定义呈现形式时就该使用该组件——逻辑归组件管样式归你的模板管。从 AppearanceControls/index.ts 可以看到SDK 同时导出了AppearanceControlsRoot组件和AppearanceControlsActions、AppearanceControlsRootSlotProps、AppearanceControlsRootSlots三个类型便于消费者在 TypeScript 下获得完整的类型推导。组件内部一行调用全量透传AppearanceControlsRoot的源码实现非常精简见 AppearanceControlsRoot.vuescript setup langts import { useAppearance } from #vue/controls/appearance/use import type { AppearanceControlsRootSlots } from #vue/primitives/AppearanceControls/types const ctx useAppearance() defineSlotsAppearanceControlsRootSlots() const actions { updateProp: ctx.updateProp, commitProp: ctx.commitProp, setBlendMode: ctx.setBlendMode, toggleVisibility: ctx.toggleVisibility, toggleIndependentCorners: ctx.toggleIndependentCorners, updateUniformRadius: ctx.updateUniformRadius, commitUniformRadius: ctx.commitUniformRadius, updateCornerProp: ctx.updateCornerProp, commitCornerProp: ctx.commitCornerProp } /script template slot :nodectx.node.value :is-multictx.isMulti.value :activectx.active.value :has-corner-radiusctx.hasCornerRadius.value :independent-cornersctx.independentCorners.value :show-independent-cornersctx.showIndependentCorners.value :corner-radius-valuectx.cornerRadiusValue.value :corner-radius-binding-pathsctx.cornerRadiusBindingPaths.value :corner-smoothing-percentctx.cornerSmoothingPercent.value :opacity-percentctx.opacityPercent.value :blend-mode-valuectx.blendModeValue.value :visibility-statectx.visibilityState.value :actionsactions / /template组件只做三件事调用useAppearance()获取当前选区的完整外观状态与动作通过defineSlotsAppearanceControlsRootSlots()声明默认插槽在默认插槽上把全部状态以.value展开和actions逐项透传。这里有一个关键实现细节组件拿到的是useAppearance()的返回值而非在插槽里让消费者自行调用。这意味着组件的存在价值正是封装一次选区订阅——消费者不需要理解useAppearance()内部如何订阅编辑器上下文只需声明式地接收插槽属性即可。而useAppearance()本身见 use.ts由三部分构成useEditor()获取编辑器上下文useNodeProps()提供当前选区推导node、nodes、active、isMulti、merged与通用的updateProp/commitProp动作createAppearanceState(options)与createAppearanceActions({ editor, ...options })分别负责推导状态与构造动作。其中expandedCornerNodeId是一个refstring | null用于记录哪一个节点的独立圆角是否处于展开编辑态它会被传入状态与动作工厂是showIndependentCorners判定的一部分。插槽契约完整的状态与动作清单插槽的全部属性与动作在 types.ts 中做了严格的类型声明。状态Slot Props属性类型说明nodeSceneNode \| null当前选中的单个节点多选时为 nullisMultiboolean是否多选activeboolean当前是否有激活的选区hasCornerRadiusboolean选中节点是否支持圆角矩形、圆角矩形、画板、组件、实例independentCornersMixedValueboolean是否使用独立圆角多选时可能是MIXEDshowIndependentCornersboolean是否展示独立圆角编辑区判定逻辑见下文cornerRadiusValueMixedValuenumber统一圆角半径值多选时可能是MIXEDcornerRadiusBindingPathsArrayCornerRadiusKey \| cornerRadius圆角字段可绑定的路径列表cornerSmoothingPercentMixedValuenumber圆角平滑度归一化为0…100或MIXEDopacityPercentMixedValuenumber不透明度归一化为0…100或MIXEDblendModeValueMixedValueBlendMode混合模式多选不一致时为MIXEDvisibilityStatevisible \| hidden \| mixed可见性三态actionsAppearanceControlsActions全部更新动作见下表动作Actions动作签名说明updateProp(key: string, value: number) void拖拽/输入过程中实时更新通用数值属性不写撤销记录commitProp(key: string, value: number, previous: number) void提交通用数值属性的变更写撤销记录setBlendMode(value: BlendMode) void设置混合模式支持多选批量toggleVisibility() void切换可见性多选时统一翻转toggleIndependentCorners() void在统一圆角与独立圆角之间切换updateUniformRadius(value: number) void实时更新统一圆角半径commitUniformRadius() void提交统一圆角半径变更updateCornerProp(key: CornerGeometryKey, value: number) void实时更新某个角半径或平滑度commitCornerProp(key: CornerGeometryKey, value: number, previous: number) void提交某个角半径或平滑度变更其中CornerGeometryKey CornerRadiusKey | cornerSmoothingCornerRadiusKey是topLeftRadius | topRightRadius | bottomRightRadius | bottomLeftRadius见 types.ts。注cornerSmoothingPercent对外暴露的是归一化后的0…100数值或MIXED而通过角动作更新它时必须使用归一化的0…1值。例如updateCornerProp(cornerSmoothing, 0.75)表示 75%。这一约定在文档和 AppearanceSection.vue 的$event / 100转换中保持一致。选区推导状态从哪来AppearanceControlsRoot透传的所有状态都由createAppearanceState见 helpers.ts基于当前选区计算值得逐项理解hasCornerRadius判断选中的节点类型是否属于RECTANGLE、ROUNDED_RECTANGLE、FRAME、COMPONENT、INSTANCE这五类支持圆角的节点。多选时要求所有节点都支持圆角才为 true。independentCorners单选时读节点的independentCorners字段多选时通过merged(independentCorners)得到MIXED或统一值。showIndependentCorners这是整个组件最值得注意的展示决策状态。单选时只要满足以下任一条件即为true该节点正处于展开编辑态expandedCornerNodeId.value selected.id该节点的四个角半径值不相等hasUnequalCorners节点标记了independentCorners但四个角的变量绑定并不一致independentCorners !cornersHaveEquivalentBindings。第三种情况正是文档中强调的导入节点场景一个从外部文件导入的节点其四角数值不相等但independentCorners标志仍是旧的统一值stale uniform flag此时也应展示独立圆角编辑区。消费者应当直接根据这个状态决定渲染而不是自己维护一个并行的本地展开 ref——这正是该组件作为展示决策所有者the root owns selection-derived presentation decisions的核心价值。多选时showIndependentCorners恒为false。cornerRadiusValue当四角变量绑定一致且四角数值相等时返回topLeftRadius否则返回节点的cornerRadius字段统一圆角值。多选时走merged(cornerRadius)。cornerRadiusBindingPaths与上述条件联动——四角一致时返回[topLeftRadius,topRightRadius,bottomRightRadius,bottomLeftRadius]四个绑定路径否则只返回[cornerRadius]。这告诉绑定感知的字段如VariableNumberField该把变量绑定到哪个/哪些属性上。cornerSmoothingPercent/opacityPercent底层场景值cornerSmoothing、opacity是0…1的浮点数这里统一做Math.round(v * 100)转成百分制多选不一致时返回MIXED哨兵值。blendModeValue/visibilityState分别对blendMode与visible做合并可见性被映射为visible | hidden | mixed三态方便 UI 直接渲染眼睛图标。MIXED哨兵值来自 node-props/use.ts其 JSDoc 说明是当某属性在多个选中节点之间不一致时返回的哨兵值Sentinel value returned when a property differs across multiple selected nodes。MixedValueT类型即T | typeof MIXED。动作实现撤销分组与多选语义动作层由createAppearanceActions实现它接受了editor、节点选择状态以及expandedCornerNodeId。其实现体现了 OpenPencil 外观编辑的几个重要设计决策可见性与混合模式批量 单次撤销toggleVisibility()在单选中通过editor.updateNodeWithUndo(liveNode.id, { visible: !liveNode.visible }, Toggle visibility)写入撤销记录多选时先判断当前是否全部可见再统一翻转并将所有节点的变更包进一次editor.undo.runBatch(Toggle visibility, ...)。setBlendMode同理过滤出值确实变化的节点统一放进一个Change blend mode批处理避免多选时产生一串撤销步骤。独立圆角切换三种路径toggleIndependentCorners()的逻辑最复杂分三种情况单选 四角绑定一致 四角数值相等此时只是展开/收起独立圆角编辑区仅翻转expandedCornerNodeId不修改任何场景数据切到独立圆角makeIndependent把每个节点的independentCorners置为true并用其当前cornerRadius值填充四个角字段整体包进Independent corner radii撤销批切回统一圆角取topLeftRadius作为统一值写回cornerRadius并清除independentCorners包进Uniform corner radius撤销批。判定makeIndependent的依据是是否所有目标节点都已经独立或四角不等——若全部已独立则反向切回统一。圆角编辑update/commit 分离 快照回滚updateCornerProp/commitCornerProp与updateUniformRadius/commitUniformRadius遵循 OpenPencil 标准的scrub拖拽实时更新→ commit提交撤销两段式模式拖拽过程中updateCornerProp用editor.updateNode直接改场景无撤销记录同时在previousCornerValues这个MapCornerGeometryKey, Mapstring, number中按节点 id 记住每个目标的原始值首次触碰时快照松开鼠标/失焦时commitCornerProp调用editor.commitNodeUpdate把从原始值变到当前值作为一次Change ${key}撤销条目写入多选时再包一层editor.undo.runBatch保证一次多选圆角编辑只产生一个撤销条目updateUniformRadius还内置了回到原值即回滚的逻辑如果新值等于快照中的原始值直接用editor.updateNode(target.id, previous)把节点恢复原状相当于拖拽往返后不产生任何变更。这套机制正是文档所说多节点独立圆角切换、平滑度编辑与逐角提交会被分组为一次撤销条目同时保留每个节点的原始值的代码级印证。实战基于插槽构建自定义外观面板官方 SDK 文档useAppearance页面与 Property Panels 指南给出组合式函数风格的最小用法import { useAppearance } from open-pencil/vue const { visibilityState, opacityPercent, cornerRadiusValue, cornerSmoothingPercent, showIndependentCorners, toggleVisibility, toggleIndependentCorners, } useAppearance()而在需要以插槽契约方式复用时应改用AppearanceControlsRoot。下面是一个完全自定义、无任何内置皮肤依赖的示例骨架script setup langts import { AppearanceControlsRoot, MIXED } from open-pencil/vue /script template AppearanceControlsRoot v-slot{ node, isMulti, active, hasCornerRadius, showIndependentCorners, cornerRadiusValue, cornerSmoothingPercent, opacityPercent, blendModeValue, visibilityState, actions } section v-ifactive classappearance-panel !-- 可见性 -- button :aria-pressedvisibilityState hidden clickactions.toggleVisibility {{ visibilityState }} /button !-- 不透明度百分制 ⇄ 0..1 -- label Opacity input typerange :min0 :max100 :valueopacityPercent MIXED ? 0 : opacityPercent inputactions.updateProp(opacity, Number(($event.target as HTMLInputElement).value) / 100) change($event) { const v Number(($event.target as HTMLInputElement).value) / 100 actions.commitProp(opacity, v, (opacityPercent MIXED ? 0 : opacityPercent) / 100) } / /label !-- 混合模式 -- select :valueblendModeValue MIXED ? : blendModeValue changeactions.setBlendMode(($event.target as HTMLSelectElement).value as never) option v-ifblendModeValue MIXED valueMixed/option option valueNORMALNormal/option option valueMULTIPLYMultiply/option option valueSCREENScreen/option /select !-- 统一圆角 / 独立圆角切换 -- template v-ifhasCornerRadius template v-if!showIndependentCorners label Radius input typenumber :min0 :valuecornerRadiusValue MIXED ? : cornerRadiusValue inputactions.updateUniformRadius(Number(($event.target as HTMLInputElement).value)) changeactions.commitUniformRadius / /label button clickactions.toggleIndependentCornersIndependent corners/button /template template v-else-ifnode !isMulti !-- 四个角分别编辑 -- input typenumber :min0 :valuenode.topLeftRadius inputactions.updateCornerProp(topLeftRadius, Number(($event.target as HTMLInputElement).value)) change(e) { const v Number(($event.target as HTMLInputElement).value) actions.commitCornerProp(topLeftRadius, v, node.topLeftRadius) } / input typenumber :min0 :valuenode.topRightRadius inputactions.updateCornerProp(topRightRadius, Number(($event.target as HTMLInputElement).value)) change(e) { const v Number(($event.target as HTMLInputElement).value) actions.commitCornerProp(topRightRadius, v, node.topRightRadius) } / button clickactions.toggleIndependentCornersBack to uniform/button /template !-- 圆角平滑度0..100 ⇄ 0..1 -- label Smoothing input typerange :min0 :max100 :valuecornerSmoothingPercent MIXED ? 0 : cornerSmoothingPercent inputactions.updateCornerProp(cornerSmoothing, Number(($event.target as HTMLInputElement).value) / 100) change(e) { const v Number(($event.target as HTMLInputElement).value) / 100 const p (cornerSmoothingPercent MIXED ? 0 : cornerSmoothingPercent) / 100 actions.commitCornerProp(cornerSmoothing, v, p) } / /label /template /section /AppearanceControlsRoot /template使用要点用v-slot解构出状态与actions所有交互事件全部走actions不要在模板里直接改场景数据数值型字段坚持百分制显示、归一化提交的约定opacityPercent/cornerSmoothingPercent除以 100 后传给updateProp/updateCornerProp拖拽过程中只调用update*实时、无撤销松手/失焦时调用commit*写入一次撤销条目是否渲染四角编辑区直接读showIndependentCorners不要再维护本地展开状态。参考官方应用皮肤的实现方式OpenPencil 应用本体src/components/properties/AppearanceSection.vue正是AppearanceControlsRoot最完整的落地范例可以作为自定义实现的参照通过v-slot解构全部插槽属性用VariableNumberField支持变量绑定渲染不透明度与统一圆角用NumberField渲染多选场景下的普通数值输入update:model-value走actions.updateProp/actions.updateUniformRadiuscommit走actions.commitProp/actions.commitUniformRadius圆角平滑度通过actions.updateCornerProp(cornerSmoothing, $event / 100)与actions.commitCornerProp(cornerSmoothing, v / 100, p / 100)完成百分制 ⇄ 归一化换算混合模式用AppSelect渲染blendModeValue MIXED时额外注入一个 Mixed 选项并禁用修改可见性按钮根据visibilityState三态切换eye/eye-off图标四个角TL/TR/BL/BR在showIndependentCorners为真、且单选时渲染每个角都是独立的VariableNumberField绑定路径分别为topLeftRadius、topRightRadius、bottomLeftRadius、bottomRightRadius。该文件还展示了cornerRadiusBindingPaths的消费方式把它传给VariableNumberField的binding-paths使变量绑定在四角一致时落到四个角字段、四角不一致时落到cornerRadius——这与createAppearanceState中cornerRadiusBindingPaths的推导完全对应。绑定感知Binding-aware与相关 APIAppearanceControlsRoot本身是呈现无关presentation-agnostic的但 OpenPencil 的属性面板体系中存在绑定感知字段。Property Panels 指南给出了明确的交互约束字段闲置时应显示变量身份应用皮肤用紫色变量名胶囊解析值在悬浮提示等辅助 UI 中展示聚焦或打开选择器不得解绑detach变量仅在用户真正修改值时才应用detach-on-edit、readonly-when-bound或edit-variable显式的解绑动作应放在选择器中而不是放在字段上的破坏性一键图标里绑定替换、编辑时解绑、多目标变更应合并为一次 provider 批次。结合AppearanceControlsRoot的插槽属性开发者可以自由选择字段级绑定交给BindableValueRoot之类组件面板级逻辑选区订阅、多选合并、撤销分组交给本组件两者互不干扰。相关文档与源码路径继续深入可参考以下仓库资源英文原版文档appearance-controls-root.md及对应的西班牙语版 es 版组合式函数文档use-appearance.md含可见性切换、逐角编辑、平滑度编辑的最小代码示例属性面板体系指南property-panels.md组件源码AppearanceControlsRoot.vue类型声明types.ts公共导出见 index.ts状态与动作实现helpers.ts、use.ts、types.ts底层选区工具node-props/use.tsMIXED哨兵值来源官方应用落地范例AppearanceSection.vue组件 API 元数据加载器appearance-controls-root.data.ts赞分享前端桌面应用AI 应用MCP 服务【免费下载链接】open-pencilAI-native design editor. Open-source Figma alternative.项目地址https://gitcode.com/gh_mirrors/op/open-pencil点击查看免费下载相关推荐OpenPencil Vue SDK 无样式外观控件AppearanceControlsRoot 插槽 API 与属性面板实战OpenPencil Vue SDK 无样式外观控件AppearanceControlsRoot 插槽 API 与属性面板实战 AppearanceContr前端桌面应用AI 应用MCP 服务OpenPencil PositionControlsRoot 无头组件详解从插槽状态到自定义位置属性面板OpenPencil PositionControlsRoot 无头组件详解从插槽状态到自定义位置属性面板 PositionControlsRoot 是 o前端桌面应用AI 应用MCP 服务OpenPencil Vue SDK使用 AppearanceControlsRoot 构建自定义外观控制面板OpenPencil Vue SDK使用 AppearanceControlsRoot 构建自定义外观控制面板 本文以 OpenPencil 官方文档中的 A前端桌面应用AI 应用MCP 服务创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表