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 系统

参考资料

相关推荐
恋猫de小郭2 小时前
Flutter 3.41.6 版本很重要,你大概率需要更新一下
android·前端·flutter
Surmon8 小时前
彻底搞懂大模型 Temperature、Top-p、Top-k 的区别!
前端·人工智能
见行AGV机器人10 小时前
无人机脉动线中的AGV小车
人工智能·无人机·agv·非标定制agv
廋到被风吹走10 小时前
【AI】从 OpenAI Codex 到 GitHub Copilot:AI 编程助手的技术演进脉络
人工智能·github·copilot
木斯佳10 小时前
前端八股文面经大全:bilibili生态技术方向二面 (2026-03-25)·面经深度解析
前端·ai·ssd·sse·rag
不会写DN10 小时前
Gin 日志体系详解
前端·javascript·gin
newsxun10 小时前
DHA之后,大脑营养进入GPC时代?
人工智能
程序员Better10 小时前
2026年AI大模型选择指南:8大主流模型深度对比,小白秒懂如何选!
人工智能
冬夜戏雪10 小时前
实习面经记录(十)
java·前端·javascript