Skip to content

materials 物料面板

物料面板插件,展示可用的组件库和工作流节点,支持拖拽添加到画布。

拖拽生成节点

拖拽组件到画布时,系统根据物料的 snippet.schema 自动生成节点数据:

json
{
  "component": "Button",
  "props": { "type": "primary", "children": "按钮" }
}

props 的初始值来自物料定义中的 snippet.schema.props,确保每个组件都有合理的默认属性。

节点标签页

节点标签页展示所有可用的工作流节点,来源于 @catpull/built-in-nodesmeta.nodes

  • 分组展示:按节点分组(基础节点、数据管理、流程控制、AI 工具)折叠展示
  • 单列布局:每项显示图标和名称,横向排列
  • 隐藏控制configure.isShowfalse 的节点不在面板中显示(如开始节点)

物料加载机制

物料的加载分为两个阶段:

1. 初始化内置物料

启动时自动加载 @catpull/built-in-materials 中的内置组件:

initMaterial() → 提取 snippets → 注册到 resource 缓存 → 添加到面板分组

2. 拉取远程物料

从配置的物料 URL 远程获取扩展物料包:

fetchMaterial() → 请求物料 URL → 解析 MaterialBundle → 合并到面板分组

远程物料与内置物料按 group 分组合并,相同 group 的物料会合并到同一折叠面板下,不同 group 则新增分组。分组按 priority 降序排列。

物料数据结构

Group(物料分组)

json
{
  "group": "basic",
  "label": "基础组件",
  "priority": 100,
  "children": [
    {
      "snippet": {
        "component": "Button",
        "name": "按钮",
        "icon": "icon-button",
        "schema": { "props": { "type": "primary", "children": "按钮" } }
      },
      "component": {
        "configure": { ... },
        "schema": { "properties": [...] },
        "events": [...]
      }
    }
  ]
}
字段类型说明
groupstring分组标识,相同标识的分组会合并
labelstring分组显示名称,不设置时使用 group
prioritynumber排序优先级,数值越大越靠前
childrenarray分组下的物料项列表

Snippet(物料片段)

物料片段定义了物料在面板中的展示信息和拖拽生成节点时的默认数据。

ts
snippet: {
  name: '自定义导航栏',         // 面板中显示的名称
  component: 'UniNavBar',       // 组件名
  icon: 'icon-biaotilan',       // 面板中显示的图标类名
  schema: {
    props: {                    // 拖拽生成节点时的默认属性模板
      title: '自定义导航栏',
      backgroundColor: '#007AFF',
      fixed: true
    },
    children: []                // 默认子内容(可选)
  }
}
字段类型说明
componentstring | string[]组件名,支持数组形式(一个物料对应多个同类型组件,如:h1-h6)
namestring面板中显示的名称
iconstring面板中显示的图标类名
schema.propsobject拖拽生成节点时的默认属性模板
schema.childrenstring | array默认子内容,字符串为文本,数组为子组件列表(可选)

schema.children 子组件结构

children 为数组时,每个子项的结构如下:

ts
children: [
  {
    component: 'UniFormsItem',   // 子组件名
    props: {                     // 子组件属性
      label: '姓名',
      required: true
    },
    children: [...]              // 嵌套子内容(可选)
  }
]
字段类型说明
componentstring子组件名
propsobject子组件属性
childrenstring | array嵌套子内容(可选)

Component(组件配置)

组件配置定义了属性面板的渲染规则和组件的行为约束。

ts
component: {
  schema: {
    properties: [...]           // 属性分组列表
  },
  events: [                     // 组件事件列表(可选)
    { name: 'play', params: ['event'], description: '播放时触发' },
    { name: 'clickAction', params: ['event'], description: '点击动作' }
  ],
  configure: {                  // 组件行为配置(可选)
    isContainer: true,
    nestingRule: { childWhiteList: [], childBlackList: [] },
    slots: ['default', 'left', 'right']
  }
}

组件事件定义位置:

事件定义在 component.events 中,与 configure 同级,位于组件配置的顶层:

位置说明
component.events组件事件列表,用于定义组件的内置事件和自定义事件(如 Video 的 playpause,Button 的 clickAction

组件内置事件在事件绑定面板中显示在通用事件(click、dblclick 等 DOM 事件)上方。

schema.properties 属性分组

属性面板按分组折叠展示,每个分组包含 labelcontent

ts
properties: [
  {
    label: '基本属性',           // 分组名称
    content: [                  // 分组下的属性项列表
      {
        label: '标题',          // 属性标签
        description: '标题文字', // 属性描述
        property: 'title',      // 属性名(对应组件 prop)
        bindState: true,        // 是否支持绑定状态变量
        widget: {               // 属性编辑器配置
          component: 'InputString',
          props: {}
        },
        labelPosition: 'left'   // 标签位置:'left' | 'top'
      }
    ]
  }
]

属性项字段说明:

字段类型说明
labelstring属性标签,显示在属性面板中
descriptionstring属性描述,鼠标悬停时显示
propertystring属性名,对应组件的 prop 名称
bindStateboolean是否支持绑定状态变量(为 true 时显示绑定按钮)
widgetobject属性编辑器配置
labelPosition'left' | 'top'标签位置,left 为行内左侧,top 为顶部独占一行

widget 编辑器配置

widget 定义了属性在面板中的编辑方式:

ts
widget: {
  component: 'Select',          // 编辑器组件名
  props: {                      // 编辑器属性
    options: [
      { label: '主要', value: 'primary' },
      { label: '默认', value: 'default' }
    ]
  },
  children: [...],              // 子属性(用于复合编辑器,可选)
  config: {                     // 编辑器配置(可选)
    visible: ({ props, value }) => props['enable-proxy'] === true
  }
}

内置编辑器组件:

编辑器说明常用 props
InputString文本输入框placeholder, type: 'textarea', rows
Number数字输入框min, max, step
Switch开关切换
Select下拉选择options: [{ label, value }]
Color颜色选择器
Resource资源选择器type: 'image' | 'icon'
HtmlTextHTML 文本编辑showRadioButton
BindVariable变量绑定type: 'state' | 'cloudFuns' | 'datasource', mode: 'assign'
UnbindVariable未绑定变量
MetaCodeEditor代码编辑器language: 'json', buttonShowContent
Array数组编辑器children 定义数组项的子属性
Condition条件编辑器children 定义条件项的子属性
OutputTips输出端口提示props 为提示项数组
...

widget.config 条件显隐:

config.visible 是一个函数,接收 ({ props, value }) 参数,返回 boolean 控制编辑器的显隐。常用于根据其他属性值动态显示/隐藏当前属性:

ts
config: {
  visible: ({ props }) => props['enable-destructuring'] === true
}

widget.children 复合编辑器

ArrayCondition 编辑器通过 children 定义子属性结构:

ts
widget: {
  component: 'Array',
  props: {},
  children: [
    {
      label: '变量',
      property: 'variable',
      widget: { component: 'BindVariable', props: { type: 'state' } }
    },
    {
      label: '赋值类型',
      property: 'type',
      widget: { component: 'Select', props: { options: [...] } }
    }
  ]
}

configure 组件行为配置

configure 控制组件在画布中的行为约束:

字段类型说明
isContainerboolean是否为容器组件(可拖入子组件)
nestingRuleobject嵌套规则(仅容器组件生效)
nestingRule.childWhiteListstring[]子组件白名单,为空表示不限制
nestingRule.childBlackListstring[]子组件黑名单,为空表示不限制
slotsstring[]插槽列表,定义组件可用的插槽名

事件格式说明:

events 支持两种格式,推荐使用对象数组格式:

ts
// 对象数组格式(推荐)
events: [
  { name: 'change', params: ['event'], description: '选中项发生改变时触发' },
  { name: 'input', params: ['value'], description: '输入内容时触发' }
]

// 字符串数组格式(兼容旧版)
events: ['change', 'input']

对象数组格式提供更丰富的事件信息,事件绑定面板会优先展示组件内置事件(来自 events 定义),然后展示通用事件(click、dblclick 等 DOM 事件)。

组件行为配置

typescript
component: {
	isModal: false,           // 是否为模态框
	isContainer: false,       // 是否为容器
	rootSelector: '.canvas-tab-bar',  // 用于选中框定位的选择器
	disableBehaviors: ['copy', 'drag', 'remove'],  // 禁用的行为
	nestingRule: {
		childWhitelist: [],   // 允许的子组件
		descendantBlacklist: ['TabBar']  // 禁止嵌套的组件
	}
}

完整示例

UniNavBar(自定义导航栏)为例,展示完整的物料元数据定义:

ts
export default {
  snippet: {
    name: '自定义导航栏',
    component: 'UniNavBar',
    icon: 'icon-biaotilan',
    schema: {
      props: {
        title: '自定义导航栏',
        leftText: '返回',
        rightText: '',
        leftIcon: 'left',
        rightIcon: '',
        color: '#ffffff',
        backgroundColor: '#007AFF',
        fixed: true,
        statusBar: false,
        shadow: true,
        border: true,
        height: 44,
        dark: false
      }
    }
  },
  component: {
    schema: {
      properties: [
        {
          label: '基本属性',
          content: [
            {
              label: '标题',
              description: '标题文字',
              property: 'title',
              bindState: true,
              widget: { component: 'InputString', props: {} },
              labelPosition: 'left'
            },
            {
              label: '左侧图标',
              description: '左侧按钮图标',
              property: 'leftIcon',
              bindState: true,
              widget: { component: 'Resource', props: { type: 'icon' } },
              labelPosition: 'left'
            }
          ]
        },
        {
          label: '样式设置',
          content: [
            {
              label: '文字颜色',
              description: '图标和文字颜色',
              property: 'color',
              bindState: true,
              widget: { component: 'Color', props: {} },
              labelPosition: 'left'
            },
            {
              label: '固定顶部',
              description: '是否固定顶部',
              property: 'fixed',
              bindState: true,
              widget: { component: 'Switch', props: {} },
              labelPosition: 'left'
            }
          ]
        }
      ]
    },
    configure: {
      slots: ['default', 'left', 'right']
    },
    events: [
      { name: 'clickLeft', params: ['event'], description: '左侧按钮点击事件' },
      { name: 'clickRight', params: ['event'], description: '右侧按钮点击事件' }
    ]
  }
}

自定义组件开发注意事项

画布环境中的响应式断裂问题

画布渲染器运行在独立的 iframe 中,拥有自己独立的 Vue 实例(从远程加载的 Vue 运行时)。当自定义组件通过 <script setup> + Composition API 使用 refcomputedwatch 时,这些 API 来自主应用打包的 Vue 包,而组件的渲染 effect(Render Effect)由 iframe 中的 Vue 实例管理,两者属于完全独立的响应式系统,导致内部状态变化无法触发 UI 更新。

问题表现

  • 组件内部 ref 的值确实发生了变化(可通过 console.log 确认)
  • 但模板中依赖该 ref 的部分不会重新渲染
  • 纯展示组件(仅依赖 props 派生,无内部可变状态)不受此影响

解决方案:使用 Options API

Options API 的 data()watchcomputed创建组件的 Vue 实例(即 iframe 的 Vue)直接处理,响应式系统天然一致,不存在断裂问题。

❌ 错误写法(<script setup> + Composition API):

vue
<script setup>
import { ref, computed, watch } from 'vue'; // 来自主应用打包的 Vue

const props = defineProps({
  current: { type: Number, default: 0 }
});

const emit = defineEmits(['update:current', 'tabChange']);

// ❌ ref 属于主应用 Vue 的响应式系统,变化无法触发 iframe Vue 的重渲染
const localCurrent = ref(props.current);

watch(() => props.current, (val) => {
  localCurrent.value = val;
});

const currentIndex = computed(() => localCurrent.value);

const handleClick = (index) => {
  localCurrent.value = index; // 值变了,但 UI 不更新!
  emit('update:current', index);
};
</script>

✅ 正确写法(Options API):

vue
<script>
export default {
  props: {
    current: { type: Number, default: 0 }
  },
  emits: ['update:current', 'tabChange'],
  data() {
    return {
      // ✅ data() 由 iframe Vue 的 reactive() 包装,属于 iframe Vue 的响应式系统
      localCurrent: this.current
    };
  },
  watch: {
    // ✅ watch 由 iframe Vue 设置,能正确追踪 props 变化
    current(val) {
      this.localCurrent = val;
    }
  },
  computed: {
    // ✅ computed 由 iframe Vue 创建,能正确派生计算值
    currentIndex() {
      return this.localCurrent;
    }
  },
  methods: {
    handleClick(index) {
      // ✅ this.localCurrent 属于 iframe Vue 的响应式系统,变化能触发重渲染
      this.localCurrent = index;
      this.$emit('update:current', index);
    }
  }
};
</script>

适用范围

组件类型是否受影响推荐写法
纯展示组件(仅依赖 props 派生,无内部可变状态)❌ 不受影响<script setup> 或 Options API 均可
有内部可变状态的组件(如选中、展开、播放状态等)✅ 受影响必须使用 Options API
仅使用 ref 获取 DOM 元素引用(Template Ref)❌ 不受影响<script setup> 或 Options API 均可

核心原则

在画布环境中,自定义组件如果需要维护内部可变状态,必须使用 Options API 而非 <script setup> + Composition API 的 ref/computed/watch 这是因为 Options API 的响应式由运行时 Vue 实例直接管理,不存在跨 Vue 实例的响应式断裂问题。