自定义设置器与属性编辑器
自定义 Setter 和属性编辑器构成了 VTJ 可扩展属性配置系统的基础,使开发者能够为物料组件属性创建专门的输入控件。该系统提供了基于插件的架构,与设计器环境无缝集成,同时为属性编辑场景提供最大的灵活性。
架构概览
Setter 系统通过集中管理模式运行,支持动态注册、基于类型的发现和组件生命周期管理。该架构由三个核心层组成:Setter 管理层、Wrapper 集成层和组件渲染层。
graph TD
A[物料组件] --> B[物料 Schema]
B --> C[Setter 配置]
C --> D[SetterManager]
D --> E[Setter 注册表]
D --> F[SetterWrapper]
F --> G[SetterView]
G --> H[自定义 Setter 组件]
H --> I[值发送]
I --> J[PropModel 更新]
D --> K[自定义 Setter 注册]
K -.->|注册| D
D -.->|根据类型获取| K
D -.->|获取| F
Setter 管理系统
SetterManager
SetterManager 作为所有属性编辑器的中央注册表,管理 Setter 生命周期并提供注册接口。
class SetterManager {
private setters: Record<string, Setter> = {};
public defaultSetter: Setter = defaultSetter;
// 核心方法
register(setter: Setter);
get(name: string): Setter;
set(name: string, setter: Partial<Setter>);
getByType(type: BlockPropDataType): string[];
}
主要职责:
- 注册:通过
register()方法添加自定义 setter - 检索:使用
get()方法按名称获取 setter - 修改:使用
set()方法更新现有 setter 配置 - 类型发现:使用
getByType()查询与特定数据类型兼容的 setter
💡 请始终在物料初始化之前注册自定义 setter,以确保在组件渲染期间可用。当需要自定义而不完全替换时,使用
set()覆盖内置 setter 配置。
Setter 接口
Setter 协议定义了所有属性编辑器的契约:
interface Setter {
name: string;
component: VueComponent;
type: BlockPropDataType;
props?: Record<string, any>;
}
属性:
name:Setter 的唯一标识符component:实现编辑器 UI 的 Vue 组件type:Setter 处理的数据类型(String, Boolean, Number, Object, Array, Function)props:传递给组件的默认配置
内置 Setters
VTJ 提供了一套全面的预配置 setter,涵盖常见的属性编辑场景:
| Setter 名称 | 组件 | 类型 | 描述 |
|---|---|---|---|
| StringSetter | ElInput | String | 标准文本输入字段 |
| BooleanSetter | - | Boolean | 布尔值的复选框/切换 |
| NumberSetter | ElInputNumber | Number | 带步控的数值输入 |
| ColorSetter | ElColorPicker | String | 颜色选择器 |
| SelectSetter | ElSelect | String | 选项下拉选择 |
| ExpressionSetter | 自定义输入 | Object | 带 {{ }} 语法的表达式编辑器 |
| JsonSetter | 自定义编辑器 | Object/Array | 用于复杂数据结构的 JSON 编辑器 |
| FunctionSetter | 自定义编辑器 | Function | 函数体编辑器 |
| IconSetter | 自定义选择器 | String | 图标选择界面 |
| ImageSetter | 自定义上传器 | String | 图片上传/选择 |
| FileSetter | 自定义上传器 | String | 文件上传控件 |
| SliderSetter | ElSlider | Number | 范围滑块输入 |
| RadioSetter | ElRadio | String | 单选按钮组 |
| TagSetter | 自定义输入 | String | 标签管理界面 |
| SizeSetter | 自定义选择 | String | 尺寸预设选择器 |
| SectionSetter | 自定义 UI | String | 可视化部分分隔符 |
| CssSetter | 自定义编辑器 | String | CSS 属性编辑器 |
| VanIconSetter | 自定义选择器 | String | Vant 图标选择 |
💡 内置 setter 默认使用 Element Plus 组件,确保视觉一致性。利用
props配置自定义行为,而无需创建新的 setter 类型。
创建自定义 Setters
基本 Setter 实现
通过实现一个 Vue 组件来创建自定义 setter,该组件通过 v-model 或显式 props 接收值,并通过事件发出更改。
示例:日期 Setter
<template>
<ElDatePicker
v-model="localValue"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleChange"
/>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { ElDatePicker } from "element-plus";
import { isJSExpression } from "@vtj/renderer";
export interface Props {
modelValue?: string | any;
placeholder?: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
placeholder: "Select date",
disabled: false,
});
const emit = defineEmits<{
change: [value: string];
}>();
const localValue = ref<string | undefined>(
isJSExpression(props.modelValue) ? undefined : props.modelValue,
);
watch(
() => props.modelValue,
(v) => {
localValue.value = isJSExpression(v) ? undefined : (v as string);
},
);
const handleChange = (value: string) => {
emit("change", value);
};
defineOptions({
name: "DateSetter",
});
</script>
示例:富文本 Setter
<template>
<div class="rich-text-setter">
<ElButton v-if="!isEditing" @click="startEdit" :disabled="disabled">
Edit Content
</ElButton>
<div v-else class="editor-container">
<textarea v-model="editorValue" :disabled="disabled" rows="10" />
<div class="actions">
<ElButton @click="cancelEdit">Cancel</ElButton>
<ElButton type="primary" @click="saveEdit">Save</ElButton>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { ElButton } from "element-plus";
import { isJSExpression } from "@vtj/renderer";
export interface Props {
modelValue?: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
});
const emit = defineEmits<{
change: [value: string];
}>();
const isEditing = ref(false);
const editorValue = ref("");
const startEdit = () => {
const value = props.modelValue;
editorValue.value = isJSExpression(value) ? "" : (value as string) || "";
isEditing.value = true;
};
const saveEdit = () => {
emit("change", editorValue.value);
isEditing.value = false;
};
const cancelEdit = () => {
isEditing.value = false;
};
defineOptions({
name: "RichTextSetter",
});
</script>
具有上下文访问的高级 Setter
高级 setter 可以访问渲染上下文以提供变量绑定、表达式求值和项目元数据等功能。
<template>
<div class="advanced-expression-setter">
<ElInput v-model="textValue" @change="handleChange" :disabled="disabled">
<template #prefix>{{ prefix }}</template>
<template #suffix>{{ suffix }}</template>
</ElInput>
<ElButton
v-if="context && showBindButton"
@click="showVariablePicker"
size="small"
text
>
Bind Variable
</ElButton>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from "vue";
import { ElInput, ElButton } from "element-plus";
import { type JSExpression } from "@vtj/core";
import { isJSExpression } from "@vtj/renderer";
import { type Context } from "@vtj/renderer";
import { expressionValidate } from "../../utils";
export interface Props {
modelValue?: JSExpression | string;
context?: Context;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
});
const emit = defineEmits<{
change: [value: JSExpression];
}>();
const prefix = `{{`;
const suffix = `}}`;
const showBindButton = computed(() => {
return isJSExpression(props.modelValue) || !props.modelValue;
});
const createValue = (value: JSExpression | string = "") => {
return {
type: "JSExpression",
value: isJSExpression(value) ? value.value : value,
} as JSExpression;
};
const textValue = ref(createValue(props.modelValue).value);
watch(
() => props.modelValue,
(v) => {
textValue.value = createValue(v).value;
},
{ immediate: true },
);
const validate = (value: JSExpression) => {
return expressionValidate(value, props.context, true);
};
const handleChange = (value: string) => {
const expression: JSExpression = {
type: "JSExpression",
value,
};
if (validate(expression)) {
emit("change", expression);
}
};
const showVariablePicker = () => {
// 实现变量选择器对话框
console.log("Show variable picker", props.context);
};
defineOptions({
name: "AdvancedExpressionSetter",
});
</script>
Setter 注册
注册方法
方法 1:通过 SetterManager 直接注册
import { setterManager } from "@vtj/designer";
import DateSetter from "./setters/DateSetter.vue";
setterManager.register({
name: "DateSetter",
component: DateSetter,
type: "String",
});
方法 2:基于插件的注册
import type { Plugin } from "@vtj/designer";
import { setterManager } from "@vtj/designer";
import DateSetter from "./setters/DateSetter.vue";
const customSettersPlugin: Plugin = {
name: "CustomSettersPlugin",
setup(ctx) {
setterManager.register({
name: "DateSetter",
component: DateSetter,
type: "String",
props: {
format: "YYYY-MM-DD",
},
});
return {
// 插件返回值
};
},
};
export default customSettersPlugin;
动态 Setter 配置
使用 set() 方法在运行时修改现有 setter 配置:
setterManager.set("DateSetter", {
props: {
format: "DD/MM/YYYY",
disabled: false,
},
});
物料 Schema 集成
在物料 Schema 中配置 Setters
Setter 在物料组件属性 schemas 中使用 setter 字段进行配置:
{
componentName: 'MyCustomComponent',
props: [
{
name: 'title',
title: 'Title',
type: 'String',
defaultValue: 'Default Title',
setter: {
name: 'StringSetter',
props: {
maxlength: 50,
showWordLimit: true
}
}
},
{
name: 'themeColor',
title: 'Theme Color',
type: 'String',
setter: 'ColorSetter'
},
{
name: 'publishDate',
title: 'Publish Date',
type: 'String',
setter: {
name: 'DateSetter',
label: 'Select Date'
}
},
{
name: 'content',
title: 'Content',
type: 'Object',
setter: [
'StringSetter',
{
name: 'RichTextSetter',
label: 'Rich Text'
},
'ExpressionSetter'
]
}
]
}
多 Setter 支持
属性可以支持多种 setter 类型,允许用户在不同的编辑模式之间切换:
{
name: 'dataSource',
title: 'Data Source',
type: 'String',
setter: [
'SelectSetter',
{
name: 'JsonSetter',
props: {
type: 'Object'
}
},
'ExpressionSetter'
],
options: ['static', 'api', 'local']
}
SetterWrapper 集成
SetterWrapper 组件连接物料 schemas 和 setter 实现,提供自动类型检测、上下文注入和值规范化。
组件属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| context | Context | null | 用于表达式求值的渲染上下文 |
| current | BlockModel | null | 当前正在编辑的组件 |
| name | string | - | 属性名称 |
| label | string | - | 显示标签 |
| title | string | - | 工具提示/描述 |
| value | any | undefined | 当前属性值 |
| setters | string|MaterialSetter|Array | 必填 | Setter 配置 |
| options | Array | [] | SelectSetter/RadioSetter 的选项 |
| variable | boolean | true | 启用表达式绑定 |
| removable | boolean | false | 允许属性移除 |
| disabled | boolean | false | 禁用编辑 |
值流
sequenceDiagram
participant 物料Schema
participant SetterWrapper
participant SetterView
participant Setter组件
participant PropModel
物料Schema->>SetterWrapper: 属性配置
SetterWrapper->>SetterWrapper: 计算Setters
SetterWrapper->>SetterWrapper: 根据类型计算默认Setter
SetterWrapper->>SetterView: 使用上下文渲染
SetterView->>Setter组件: 传递Props和值
Setter组件->>Setter组件: 用户交互
Setter组件->>Setter组件: 验证值
Setter组件->>SetterView: 发出更改事件
SetterView->>SetterWrapper: 发出更改
SetterWrapper->>PropModel: 更新PropModel
PropModel->>PropModel: 标记为已设置/未设置
最佳实践
性能优化
<script lang="ts" setup>
import { markRaw, defineComponent } from "vue";
// 使用 markRaw 防止不必要的响应式
const setterComponent = markRaw(DateSetter);
// 对于复杂组件,避免深层响应式包装
const props = defineProps<{
modelValue: any;
options?: Record<string, any>[];
}>();
</script>
表达式处理
始终检查 JSExpression 类型,以防止当值包含绑定表达式时出现运行时错误:
import { isJSExpression } from "@vtj/renderer";
const computedValue = computed(() => {
if (isJSExpression(props.modelValue)) {
return undefined; // 或显示表达式指示器
}
return props.modelValue;
});
类型安全
导出 setter props 的 TypeScript 接口以启用类型检查:
export interface DateSetterProps {
modelValue?: string;
placeholder?: string;
format?: string;
disabled?: boolean;
minDate?: Date;
maxDate?: Date;
}
const props = withDefaults(defineProps<DateSetterProps>(), {
placeholder: "Select date",
format: "YYYY-MM-DD",
});
可访问性
确保自定义 setter 遵循可访问性指南:
<template>
<label :for="inputId" class="setter-label">
{{ label }}
<input
:id="inputId"
v-model="localValue"
:aria-label="label"
:aria-invalid="hasError"
@change="handleChange"
/>
<span v-if="errorMessage" class="error-message" role="alert">
{{ errorMessage }}
</span>
</label>
</template>
高级场景
条件 Setters
实现根据上下文显示不同 setter 的逻辑:
<template>
<component
:is="currentSetter.component"
v-bind="currentSetter.props"
:model-value="value"
@change="handleChange"
/>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { setterManager } from "@vtj/designer";
const props = defineProps<{
value: any;
context: any;
}>();
const currentSetter = computed(() => {
if (props.context?.isComplex) {
return setterManager.get("JsonSetter");
}
return setterManager.get("StringSetter");
});
</script>
异步数据加载
为 SelectSetter 动态加载选项:
<template>
<ElSelect v-model="selectedValue" :loading="loading" @change="handleChange">
<ElOption
v-for="item in dynamicOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { ElSelect, ElOption } from "element-plus";
const loading = ref(false);
const dynamicOptions = ref<Array<{ label: string; value: any }>>([]);
const selectedValue = ref();
const emit = defineEmits<{
change: [value: any];
}>();
const loadOptions = async () => {
loading.value = true;
try {
const response = await fetch("/api/options");
dynamicOptions.value = await response.json();
} finally {
loading.value = false;
}
};
onMounted(loadOptions);
const handleChange = (value: any) => {
emit("change", value);
};
</script>
故障排除
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Setter 不显示 | 注册时机 | 在设计器初始化之前注册 setter |
| 值未更新 | 缺少 emit | 确保 @change 事件发出新值 |
| 表达式错误 | 未传递上下文 | 通过 SetterWrapper 提供有效上下文 |
| 类型不匹配 | 类型声明不正确 | 验证 type 是否与实际数据类型匹配 |
| Props 未传递 | 组件名称不匹配 | 确保 setter 名称与注册匹配 |
调试模式
为 setter 操作启用详细日志记录:
import { logger } from "@vtj/utils";
// 记录 setter 注册
setterManager.register({
name: "CustomSetter",
component: CustomSetterComponent,
type: "String",
});
logger.info("CustomSetter registered");
// 记录 setter 检索
const setter = setterManager.get("CustomSetter");
logger.debug("Setter retrieved:", setter);
后续步骤
掌握自定义 setter 后,探索以下相关主题:
- 物料 Schema 配置:学习如何使用 setter 规范配置物料组件
- 自定义小部件和设计器面板:使用自定义面板和小部件扩展设计器 UI
- 插件系统开发:将自定义 setter 和扩展打包到插件中
- 创建自定义物料组件:开发具有属性编辑器的完整物料组件
- Provider API 参考:了解用于上下文管理的 provider 系统
参考资料
- 开源代码仓库:gitee.com/newgateway/…