Open-Pencil Vue SDK 进阶:用 useVariables() 直接控制变量集合、模式与 CRUD

原创2026-09-26 10:06:481,431 阅读
文章标签:前端桌面应用AI 应用MCP 服务

Open-Pencil Vue SDK 进阶:用 useVariables() 直接控制变量集合、模式与 CRUD

useVariables() 是 Open-Pencil Vue SDK 中位于 @open-pencil/vue 包底层的变量编辑组合式函数,它把「变量集合(collections)、活跃模式(modes)、过滤(filtering)与增删改查(CRUD)」这一整套状态和动作直接暴露给开发者,让你无需依赖预制好的表格或对话框 UI,就能为编辑器构建自定义的变量面板。读完本文,你将掌握 useVariables() 的完整返回值与用法、其背后的源码实现与撤销/重做机制,并能自行组装出符合业务需求的变量编辑界面。

useVariables 是什么:底层变量编辑状态

useVariables() 是 Open-Pencil 变量编辑体系中最底层的组合式函数。官方文档(法语版关联文档)如此定位:它「提供变量编辑器的低层级状态与动作」,适用于「希望在无需完整 table/dialog 抽象的情况下,直接控制 collections、active modes、filtering 以及 CRUD 操作」的场景。与之相对,上层还提供了 useVariablesEditor、useVariablesDialogState、useVariablesTable 三个更完整的抽象,后文会说明它们与 useVariables() 的层级关系。

在 Open-Pencil 的 Vue 组件树中,useVariables() 依赖 useEditor() 从编辑器上下文读取 Editor 实例,再通过 useSceneComputed() 将场景数据计算为响应式引用——也就是说,必须先在组件树上层调用 provideEditor() 注入编辑器上下文,useVariables() 才能正常工作。从 packages/vue/src/variables/use.ts 的实现可以看到,它从 useEditor() 开始,串联了 useSceneComputed 与两个 helpers 工厂:

// packages/vue/src/variables/use.ts
import { ref, computed, watch } from 'vue'
import { useEditor } from '#vue/editor/context'
import { useSceneComputed } from '#vue/internal/scene-computed/use'
import { createVariableCollectionActions, createVariableValueActions } from '#vue/variables/helpers'

export function useVariables() {
  const editor = useEditor()
  const searchTerm = ref('')
  function setSearchTerm(term: string) { searchTerm.value = term }
  const collections = useSceneComputed(() => editor.getCollections())
  const activeCollectionId = ref(collections.value[0]?.id ?? '')
  watch(collections, (cols) => {
    if (!activeCollectionId.value && cols[0]) activeCollectionId.value = cols[0].id
  })
  const activeCollection = computed(() => editor.getCollection(activeCollectionId.value) ?? null)
  const activeModes = computed(() => activeCollection.value?.modes ?? [])
  const variables = useSceneComputed(() => {
    if (!activeCollectionId.value) return [] as Variable[]
    const all = editor.getVariablesForCollection(activeCollectionId.value)
    if (!searchTerm.value) return all
    const q = searchTerm.value.toLowerCase()
    return all.filter((v) => v.name.toLowerCase().includes(q))
  })
  const collectionActions = createVariableCollectionActions(editor, activeCollectionId)
  const variableActions = createVariableValueActions(editor, () => activeCollection.value)
  return {
    editor, collections, activeCollectionId, activeCollection,
    activeModes, variables, searchTerm, setSearchTerm,
    ...collectionActions, ...variableActions
  }
}

一个关键实现细节是:activeCollectionId 默认取第一个集合的 id(若存在),并通过 watch(collections) 在集合列表变化时自动补位——当集合被删除或场景初始为空时,它会回退到 '' 或重新指向第一个集合。这说明 useVariables() 是「跟随当前活跃集合」设计的,所有变量级操作都作用于 activeCollection。

返回值总览:状态与动作一览

useVariables() 的返回对象包含只读状态、过滤状态与大量 CRUD 动作。以下表格汇总全部返回值(依据官方英文文档 packages/docs/programmable/sdk/api/advanced/use-variables.md 与源码实现):

返回值 类型 说明
editor Editor 底层编辑器实例,可继续调用其他编辑器 API
collections 响应式数组 全部变量集合(VariableCollection[]),基于 editor.getCollections()
activeCollectionId Ref<string> 当前活跃集合 id,默认取第一个集合
activeCollection 计算属性 当前活跃集合对象(VariableCollection | null)
activeModes 计算属性 活跃集合下的全部模式(VariableCollectionMode[])
variables 响应式数组 活跃集合内的变量,受 searchTerm 过滤
searchTerm / setSearchTerm() Ref<string> / (term: string) => void 名称过滤状态及其写入函数
setActiveCollection(id) 函数 切换活跃集合
addCollection() 函数 新增集合并自动切换为活跃集合
renameCollection(id, newName) 函数 重命名集合
removeCollection(id) 函数 删除集合并回退活跃选择
addMode() 函数 在活跃集合中新增模式,返回新模式 id
removeMode(modeId) 函数 删除模式(集合至少保留一个)
renameMode(modeId, newName) 函数 重命名模式
setDefaultMode(modeId) 函数 设置默认模式
duplicateMode(modeId) 函数 复制模式(含各变量的值)
setActiveMode(modeId) 函数 设置当前活跃模式
addVariable(type?) 函数 新增变量(默认 'COLOR'),为每个模式填充默认值
removeVariable(id) / renameVariable(id, newName) 函数 删除 / 重命名变量
updateVariableValue(id, modeId, value) 函数 写入某个变量在某个模式下的值
formatModeValue(variable, modeId) 函数 把模式值格式化为显示字符串
parseVariableValue(variable, raw) 函数 把用户输入字符串解析为类型化值
shortName(variable) 函数 提取变量名的最后一段(用于层级名展示)

集合与模式操作详解

集合操作定义在 packages/vue/src/variables/helpers.ts 的 createVariableCollectionActions 中,全部基于编辑器实例执行并同步响应式状态:

  • addCollection() 内部构造一个 VariableCollection,id 采用 col:${randomHex(8)} 的格式,初始自带一个 Mode 1 模式(modeId: 'default')并把它设为默认模式,随后调用 editor.addCollection(collection) 并立即把 activeCollectionId 指向新集合——因此新增后界面会直接切换到新集合。
  • removeCollection(id) 删除集合后,会从 editor.getCollections() 重新取列表,把活跃集合回退到第一个(或 ''),保证后续操作不会指向不存在的集合。
  • 模式操作(addMode / removeMode / renameMode / setDefaultMode / duplicateMode / setActiveMode)都会在 helpers.ts 中先校验活跃集合是否存在,再委托给编辑器;removeMode 在 packages/core/src/editor/variables.ts 中还会做「至少保留一个模式」的保护,避免把集合删成空模式列表。

变量 CRUD 与类型化取值

变量操作定义在 packages/vue/src/variables/helpers.ts 的 createVariableValueActions 中。Open-Pencil 的变量类型由 VariableType 定义(packages/scene-graph/src/types.ts):

export type VariableType = 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN'
export type VariableValue = Color | number | string | boolean | { aliasId: string }

addVariable(type) 是其中最关键的工厂函数:它先读取活跃集合的全部模式,为每个模式生成一份默认值(COLOR → 黑色 { ...BLACK },FLOAT → 0,BOOLEAN → false,STRING → ''),再调用 editor.addVariable() 写入场景图:

function addVariable(type: VariableType = 'COLOR') {
  const col = getActiveCollection()
  if (!col) return
  const id = `var:${randomHex(8)}`
  const valuesByMode: Record<string, VariableValue> = {}
  for (const mode of col.modes) {
    valuesByMode[mode.modeId] = defaultVariableValue(type)
  }
  editor.addVariable({
    id, name: defaultVariableName(type), type,
    collectionId: col.id, valuesByMode,
    description: '', hiddenFromPublishing: false
  })
}

默认名称也按类型区分:New color / New number / New boolean / New text。Variable 数据模型(packages/scene-graph/src/types.ts)除了 id、name、type、collectionId、valuesByMode 外,还包含 description、hiddenFromPublishing 以及可选的 key / version(用于发布库的 assetRef 解析)。

formatModeValue() 与 parseVariableValue() 是编辑界面的「显示 ↔ 解析」双向桥:

  • formatModeValue:颜色值转成 hex 字符串(colorToHexRaw),别名引用显示为 → 别名变量名(找不到目标则显示 → ?),其余类型 String(value) 化;
  • parseVariableValue:COLOR 自动补 # 前缀并 parseColor,FLOAT 用 Number.parseFloat 并在 NaN 时返回 undefined,BOOLEAN 按 'true' 大小写不敏感解析,STRING 原样返回。

快速上手:最小示例

在自定义变量面板组件中使用 useVariables() 的最简方式如下(导入路径以 @open-pencil/vue 公共入口为准,见 packages/vue/src/index.ts 的导出声明):

import { useVariables } from '@open-pencil/vue'

const variables = useVariables()

// 读取状态
variables.collections.value            // 全部集合
variables.activeCollection.value       // 当前集合
variables.activeModes.value            // 当前集合的模式列表
variables.variables.value              // 当前集合的变量(受搜索词过滤)

// 过滤
variables.setSearchTerm('primary')

// 集合 CRUD
variables.addCollection()
variables.renameCollection(colId, 'Design tokens')
variables.removeCollection(colId)

// 变量 CRUD
variables.addVariable('COLOR')
variables.renameVariable(varId, 'Brand/primary')
variables.updateVariableValue(varId, modeId, { r: 0.2, g: 0.4, b: 0.8, a: 1 })

// 模式操作
variables.addMode()
variables.setActiveMode(modeId)
variables.duplicateMode(modeId)

注意模板中使用响应式状态时需经过 .value 或解包;而 activeCollection 与 activeModes 是 computed,在模板中可直接使用。

实战:构建一个无 UI 依赖的变量编辑面板

「无表格/对话框抽象」意味着 UI 完全由你掌控。一个典型的面板结构可以是这样:

<script setup lang="ts">
import { useVariables } from '@open-pencil/vue'

const v = useVariables()

function onAddColor() {
  const id = v.addVariable('COLOR')
  // id 形如 var:xxxxxxxx,可用于聚焦新行
}
</script>

<template>
  <div>
    <select :value="v.activeCollectionId" @change="v.setActiveCollection(($event.target as HTMLSelectElement).value)">
      <option v-for="c in v.collections" :key="c.id" :value="c.id">{{ c.name }}</option>
    </select>

    <div v-for="mode in v.activeModes" :key="mode.modeId">
      {{ mode.name }}
      <button @click="v.setActiveMode(mode.modeId)">激活</button>
    </div>

    <input :value="v.searchTerm" @input="v.setSearchTerm(($event.target as HTMLInputElement).value)" placeholder="搜索变量" />

    <table>
      <tr v-for="variable in v.variables" :key="variable.id">
        <td>{{ v.shortName(variable) }}</td>
        <td v-for="mode in v.activeModes" :key="mode.modeId">
          <input
            :value="v.formatModeValue(variable, mode.modeId)"
            @change="v.updateVariableValue(variable.id, mode.modeId, v.parseVariableValue(variable, ($event.target as HTMLInputElement).value) ?? '')"
          />
        </td>
        <td><button @click="v.renameVariable(variable.id, prompt('新名称') ?? variable.name)">重命名</button></td>
        <td><button @click="v.removeVariable(variable.id)">删除</button></td>
      </tr>
    </table>

    <button @click="onAddColor">新增颜色变量</button>
    <button @click="v.addMode()">新增模式</button>
  </div>
</template>

这个示例完整演示了「集合下拉 → 模式切换 → 搜索过滤 → 按模式编辑值 → 增删改」的闭环:所有交互都只调用 useVariables() 暴露的底层动作,不依赖任何预制组件。编辑场景下,颜色输入建议用 formatModeValue 展示 hex、parseVariableValue 回写;而浮点/布尔类型则可用受控输入配合 parseVariableValue 的类型化解析。

底层原理:editor 动作、场景图与撤销/重做

useVariables() 本身不直接修改场景图,所有写操作都经由 Editor 的变量动作层完成,该层实现在 packages/core/src/editor/variables.ts(createVariableActions)。核心写路径遵循同一模式:修改 ctx.graph 中的集合/变量数据 → 向 ctx.undo.push 注册带 forward / inverse 的可撤销动作 → 调用 ctx.requestRender() 触发重渲染。

以 updateVariableValue 为例(packages/core/src/editor/variables.ts):

function updateVariableValue(id: string, modeId: string, value: VariableValue) {
  const variable = ctx.graph.variables.get(id)
  if (!variable) return
  const prevValue = structuredClone(variable.valuesByMode[modeId])
  const newValue = structuredClone(value)
  variable.valuesByMode[modeId] = newValue
  ctx.undo.push({
    label: 'Update variable value',
    forward: () => { /* 重放 newValue */ },
    inverse: () => { /* 恢复 prevValue */ }
  })
  ctx.requestRender()
}

可以看出:所有变量编辑操作都自动进入编辑器的撤销/重做历史,且 forward/inverse 在重放时各自克隆值,避免历史栈与当前场景共享同一对象引用。removeCollection 会在删除前 structuredClone 整个集合及其下属变量,以便 inverse 完整还原;removeMode 也会快照每个变量在该模式下的值。

场景图层的模式/变量语义由 packages/scene-graph/src/variables.ts 提供,其中两点值得注意:

  • resolveVariable(同文件 L190-L219)按「活跃模式 → 默认模式 → 第一个可用值」的优先级取值,遇到 { aliasId } 会递归解析别名变量(带 visited 防环保护),这正是 activeModes 与 setActiveMode 影响实际渲染值的根本原因;
  • addMode(同文件 L140-L153)在新增模式时会从 sourceMode ?? defaultModeId 克隆每个变量的值,这与 duplicateMode 的「复制模式」语义一致。

因此,通过 useVariables().setActiveMode() 切换模式后,场景中引用该变量集合的颜色/数值会自动更新——这一响应链路由 useSceneComputed 保证,开发者无需手动订阅事件。

与上层 API 的关系:选择哪一层

useVariables() 是变量编辑能力的最底层入口,Open-Pencil 在其之上还有三个渐进封装(详见 advanced/index.md 的「Variables et langue」分组):

选择建议:需要完全定制 UI 与交互时用 useVariables();需要「表格 + 内联重命名」但想自绘单元格时用 useVariablesTable / useVariablesDialogState;需要直接嵌入一个完整变量编辑器(含颜色输入组件、类型图标)时用 useVariablesEditor。四层 API 都已在 packages/vue/src/index.ts 中从 @open-pencil/vue 公共导出。

小结

useVariables() 是 Open-Pencil Vue SDK 变量能力的地基:它把集合、模式、过滤和 CRUD 完整地暴露为响应式状态与动作,所有写操作均落入编辑器撤销历史,并通过 useSceneComputed 与场景图保持同步。掌握了它的返回值与底层实现(packages/vue/src/variables/use.ts、helpers.ts、packages/core/src/editor/variables.ts),你就可以在 provideEditor 上下文中自由搭建面向业务的自定义变量面板,或在需要预制表格/对话框能力时平滑升级到 useVariablesDialogState、useVariablesTable 与 useVariablesEditor。

登录后查看全文
open-pencil