AI驱动的 Vue3应用开发平台深入探究(十五):扩展与定制之自定义设置器与属性编辑器

自定义设置器与属性编辑器

自定义 Setter 和属性编辑器构成了 VTJ 可扩展属性配置系统的基础,使开发者能够为物料组件属性创建专门的输入控件。该系统提供了基于插件的架构,与设计器环境无缝集成,同时为属性编辑场景提供最大的灵活性。

架构概览

Setter 系统通过集中管理模式运行,支持动态注册、基于类型的发现和组件生命周期管理。该架构由三个核心层组成:Setter 管理层、Wrapper 集成层和组件渲染层。

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 禁用编辑

值流

最佳实践

性能优化

复制代码
<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 系统

参考资料

相关推荐
天地沧海1 分钟前
Encoder-only、Decoder-only、Encoder-Decoder 到底长什么样
人工智能
Flying pigs~~2 分钟前
Dify平台入门指南:开源LLM应用开发平台深度解析
人工智能·开源·大模型·agent·dify·rag
PD我是你的真爱粉2 分钟前
Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解
人工智能·python·架构
林深时见鹿v5 分钟前
《后端开发全栈工具安装踩坑指南 & 经验沉淀手册》
java·人工智能·python·oracle
扬帆破浪5 分钟前
察元 WPS AI助手技术手记:从源码构建到各平台安装与上手
人工智能·wps
zero.cyx5 分钟前
更换Live2D模型具体步骤
人工智能·计算机视觉·语音识别
阿星AI工作室5 分钟前
Codex登录又崩了?零基础用CCSwitch秒连教程
人工智能
扬帆破浪5 分钟前
察元 WPS AI插件:工程边界与阅读地图
人工智能·开源·wps
相信神话20217 分钟前
第六章:迷你项目:「投壶」单关卡小游戏
前端
晴天丨10 分钟前
🔔 如何实现一个优雅的通知中心?(Vue 3 + 消息队列实战)
前端·vue.js