Files
wukuang/低码移动端组件开发指南.md
T
2026-05-23 14:05:22 +08:00

15 KiB
Raw Blame History

低码移动端组件开发指南

概述

本文档介绍低码平台的移动端组件开发指南。移动端组件基于 Vue 进行开发,以 H5 形式运行,组件库采用 Vant。

阅读前须知: 在阅读本文档前,请先阅读《字段类型与物料组件开发指南.md》。移动端开发与浏览器端在整体设计、开发流程上保持一致,主要差异在于底层规划、文件目录、运行和初始化等差异。对于组件开发者,只需了按移动端的目录标准开发组件、配置组件即可。

本文档主要介绍移动端开发中的差异部分。关于功能开发的详细说明、开发标准与规范,请参考《字段类型与物料组件开发指南.md》。

目录结构

移动端组件的相关配置文件位于 packages/widget-mobile/src/config 目录下,主要包含以下文件:

packages/widget-mobile/src/config/
├── index.ts                    # 配置入口文件,注册移动端设计器和预览器配置
├── Material.ts                 # 物料面板配置,定义组件在物料面板中的分类和展示
├── RegisterWidget.ts           # 组件注册配置,注册移动端组件的实现
├── RegisterWidgetHelper.ts     # 组件辅助工具注册配置
├── RegisterSettings.ts         # 组件设置配置
├── WidgetType.ts               # 组件类型枚举定义
├── widget/                     # 组件实现目录
│   ├── Root.vue                # 页面根容器组件
│   └── TestWidget.vue          # 测试组件示例
└── widget-helper/              # 组件辅助工具目录
    └── TestWidgetHelper.ts     # 测试组件辅助工具示例

主要文件说明

  • index.ts: 配置入口,提供 setMobileConfigAll() 函数,用于注册设计器和预览器的配置。
  • Material.ts: 定义组件在物料面板中的分类组织,通过 getMaterialPanelConfig() 返回物料配置数组。
  • RegisterWidget.ts: 注册移动端组件的实现,将组件类型与对应的 Vue 组件进行映射。
  • WidgetType.ts: 定义所有移动端组件的类型枚举,需要与后端定义的控件类型保持一致。

开发流程

添加一个新的移动端物料组件,需要完成以下 6 个步骤的配置。下面以 TestWidget 组件为例,详细说明每个步骤的操作。

步骤 1:添加组件类型枚举

WidgetType.ts 文件中添加组件类型枚举,枚举值需要与后端定义的控件类型保持一致。

export enum WidgetType {
  // 测试组件
  TestWidget = "TEST_WIDGET",

  // 你的新组件
  // YourWidget = 'YOUR_WIDGET',
}

步骤 2:创建 Vue 组件

widget/ 目录下创建组件的 Vue 文件,例如 YourWidget.vue

<script setup lang="ts">
import { Rate } from "vant";
import { ref } from "vue";

const value = ref(3);
</script>

<template>
  <div class="your-widget-container">
    <!-- 组件内容 -->
    <Rate v-model="value" />
  </div>
</template>

<style lang="css" scoped>
.your-widget-container {
  /* 组件样式 */
}
</style>

注意事项:

  • 组件使用 Vant 作为 UI 组件库
  • 组件应支持响应式设计,适配移动端屏幕

步骤 3:创建 WidgetHelper 配置

widget-helper/ 目录下创建 Helper 配置文件,例如 YourWidgetHelper.ts

Helper 文件需要实现 WidgetHelper 接口,主要包含以下方法:

  • buildMaterialConfig(): 定义组件在物料面板中的展示信息(图标、名称等)
  • buildSettings(): 定义组件的设置面板配置(属性、样式、事件等)
  • buildWidget(): 定义组件的默认 Schema 结构
  • buildSchemaController(): 定义组件的 Schema 控制器方法

参考示例:

import type { WidgetHelper } from "~@/types/WidgetHelper";
import { WidgetType } from "../WidgetType";

const config: WidgetHelper = {
  type: WidgetType.TestWidget,
  buildMaterialConfig() {
    // 返回物料配置
  },
  buildSettings(params) {
    // 返回设置面板配置
  },
  buildWidget(params) {
    // 返回组件默认 Schema
  },
  buildSchemaController() {
    // 返回 Schema 控制器方法
  },
  // 其他可选方法...
};

export default config;

详细说明请参考: widget-helper/TestWidgetHelper.ts 文件的完整实现。

步骤 4:注册组件

RegisterWidget.ts 文件的 SYS_COMPS_MAP 对象中添加组件映射。

import TestWidget from "./widget/TestWidget.vue";
import { WidgetType } from "./WidgetType";

export const SYS_COMPS_MAP: Recordable<RegisterFormComponentOptions> = {
  [WidgetType.TestWidget]: {
    instance: TestWidget,
  },

  // 你的新组件
  // [WidgetType.YourWidget]: {
  //   instance: YourWidget,
  // },
};

步骤 5:注册 Helper

RegisterWidgetHelper.ts 文件的 SYS_MOBILE_WIDGET_HELPERS 数组中添加 Helper 的导入和注册。

import TestWidgetHelper from "./widget-helper/TestWidgetHelper";

export const SYS_MOBILE_WIDGET_HELPERS = [
  TestWidgetHelper,

  // 你的新组件
  // YourWidgetHelper,
];

步骤 6:添加到物料面板

Material.ts 文件的 getMaterialPanelConfig() 函数中添加组件到物料面板配置。

import { MaterialCollapseTypeEnum } from "~@/types/MaterialPanelType";
import { WidgetType } from "./WidgetType";

export function getMaterialPanelConfig() {
  return [
    {
      name: MaterialCollapseTypeEnum.Common, // 通用组件分类
      widgetTypeList: [
        WidgetType.TestWidget,
        // WidgetType.YourWidget, // 你的新组件
      ],
    },
  ];
}

分类说明:

  • MaterialCollapseTypeEnum.Common: 通用组件
  • 其他分类可根据组件特性选择合适的分类

完整开发清单

开发新组件时,请按以下清单逐项完成:

  • 步骤 1:在 WidgetType.ts 中添加枚举
  • 步骤 2:创建 widget/YourWidget.vue 组件文件
  • 步骤 3:创建 widget-helper/YourWidgetHelper.ts Helper 文件
  • 步骤 4:在 RegisterWidget.ts 中注册组件
  • 步骤 5:在 RegisterWidgetHelper.ts 中注册 Helper
  • 步骤 6:在 Material.ts 中添加到物料面板配置

参考示例

完整示例可参考 TestWidget 组件:

  • 组件实现:widget/TestWidget.vue
  • Helper 配置:widget-helper/TestWidgetHelper.ts
  • 类型定义:WidgetType.TestWidget
  • 组件注册:RegisterWidget.ts 中的 SYS_COMPS_MAP[WidgetType.TestWidget]
  • Helper 注册:RegisterWidgetHelper.ts 中的 SYS_MOBILE_WIDGET_HELPERS
  • 物料配置:Material.ts 中的物料面板配置

BaseField / Field 三种使用方式说明

移动端字段容器在实际业务中有三种典型用法,本节重点说明 BaseField 与 Vant Field 的配置与使用模式

整体上可以分为三种模式:

  • 模式一:基础模式(只用 BaseField,完全由 Schema 驱动)
  • 模式二:插槽扩展模式(在 BaseField 上增加自定义插槽)
  • 模式三:完全自定义模式(基于 useField + Vant Field 自行开发)

下面分别说明三种模式的使用方式与适用场景。

模式一:基础模式(只用 BaseField,零插槽开发)

示例代码:

<script setup lang="ts">
import type { WidgetProps, WidgetSchema } from "~@/types/WidgetSchema";
import { computed, toRefs, useAttrs } from "vue";
import { usInitValueHook } from "~@/config/widget/utils/UseInitValueHook";
import { useCompEvent } from "~@/config/user-script/hooks/UseComp";
import BaseField from "../components/BaseField.vue";
import { useField } from "./hook/useField";

const props = defineProps<
  WidgetProps<WidgetSchema<any>> & {
    placeholder?: string;
  }
>();

const { schema, fieldPath, placeholder } = toRefs(props);
const attrs = useAttrs();
const { fieldProps, wrapperStyle, fieldSlots } = useField({
  schema,
  attrs,
  placeholder,
});

type ModelValue = string | undefined;
const value = defineModel<ModelValue>();
usInitValueHook(value, { schema, fieldPath });

const { handleBlur, handleFocus, handleValueChange } = useCompEvent(
  schema as any,
);

function setValue(nv: ModelValue) {
  value.value = nv;
  handleValueChange(nv);
}
</script>

<template>
  <BaseField
    v-bind="fieldProps"
    :style="wrapperStyle"
    :field-slots="fieldSlots"
    :model-value="value"
    @update:model-value="setValue"
    @focus="handleFocus"
    @blur="handleBlur"
  />
</template>
  • 使用方式说明:
    • 模板中直接使用 BaseField,传入由 useField 计算出的 fieldProps / wrapperStyle / fieldSlots
    • 通过 v-model 和事件处理器完成双向绑定和事件派发;
    • fieldProps / wrapperStyle / fieldSlots 均由 useField(schema, attrs, placeholder) 等 Hook 计算而来。
  • 字段配置来源:
    • 标题、占位符、字数限制、校验规则、必填标记等,全部从 Schema(低码配置)中读取;
    • 开发者只需要通过 Helper 的 buildSettings / buildWidget 暴露字段配置即可。
  • 适合场景:
    • 只想快速用一个“通用输入组件”,只需要提供标准样式;

总结: 模式一中,BaseField 被看作“通用字段容器”,开发者只需要准备好 fieldProps / fieldSlots,绝大多数展示和行为由平台内置。

模式二:插槽扩展模式(BaseField + 自定义插槽)

示例代码:

<script setup lang="ts">
// ... script 部分与模式一相同,这里省略
</script>

<template>
  <BaseField
    v-bind="fieldProps"
    :style="wrapperStyle"
    :field-slots="fieldSlots"
    :model-value="value"
    @update:model-value="setValue"
    @focus="handleFocus"
    @blur="handleBlur"
  >
    <!-- 自定义标签区域 -->
    <template #label>
      <div class="custom-label">
        <span>自定义标签</span>
        <i class="help-icon" />
      </div>
    </template>

    <!-- 自定义左侧图标 -->
    <template #left-icon>
      <van-icon name="search" />
    </template>

    <!-- 自定义右侧图标 -->
    <template #right-icon>
      <van-icon name="clear" @click="handleClear" />
    </template>

    <!-- 自定义输入区域 -->
    <template #input>
      <input
        :value="value"
        @input="setValue(($event.target as HTMLInputElement).value)"
        class="custom-input"
      />
    </template>
  </BaseField>
</template>
  • 使用方式说明:
    • 继续使用 BaseField 作为外层容器,并保留基础属性和事件绑定;
    • 同时在 <BaseField> 内部编写插槽:
      • #label:自定义标签区域(可加入图标、说明、换行布局等);
      • #left-icon / #right-icon:替换为自定义图标/按钮(如搜索图标、清空按钮等);
      • #input:替换内部输入区域(可以是 <input>,也可以是更复杂的组合,如“区号 + 手机号”)。
  • 与基础模式的关系:
    • 仍然完全复用 BaseField 的:
      • 表单布局(label 区、内容区、错误提示等);
      • 事件派发(focus / blur / valueChange);
      • Schema 驱动的属性(fieldProps / fieldSlots)。
    • 只是把“局部渲染区域”交给业务组件来自定义。
  • 适合人群/场景:
    • 需要在标签上插入说明/图标,或改变标签的布局;
    • 需要在左右两侧放置单位、按钮、icon 等;
    • 需要对输入 DOM 做轻量改造,但又不希望重写整个 Field 逻辑。

总结: 模式二是在 “不放弃 BaseField 能力” 的前提下,通过插槽对部分区域进行增强,兼顾 低码配置少量编码定制

模式三:完全自定义模式(useField + Vant Field

示例代码:

<script setup lang="ts">
import type { WidgetProps, WidgetSchema } from "~@/types/WidgetSchema";
import { Field } from "vant-cisdi";
import { toRefs, useAttrs } from "vue";
import { usInitValueHook } from "~@/config/widget/utils/UseInitValueHook";
import { useCompEvent } from "~@/config/user-script/hooks/UseComp";
import { useField } from "./hook/useField";

const props = defineProps<
  WidgetProps<WidgetSchema<any>> & {
    placeholder?: string;
  }
>();

const { schema, fieldPath, placeholder } = toRefs(props);
const attrs = useAttrs();
const { fieldProps, wrapperStyle, fieldSlots } = useField({
  schema,
  attrs,
  placeholder,
});

type ModelValue = string | undefined;
const value = defineModel<ModelValue>();
usInitValueHook(value, { schema, fieldPath });

const { handleBlur, handleFocus, handleValueChange } = useCompEvent(
  schema as any,
);
</script>

<template>
  <Field
    v-model="value"
    v-bind="fieldProps"
    :style="wrapperStyle"
    @update:model-value="handleValueChange"
    @focus="handleFocus"
    @blur="handleBlur"
  >
    <!-- 根据 fieldSlots 数据动态渲染标签 -->
    <template v-if="fieldSlots.hasLabelSlot" #label>
      <div
        v-if="fieldSlots.labelData?.value?.showLabel"
        class="van-field__label-wrapper"
      >
        <label :style="{ width: fieldSlots.labelData?.value?.labelWidth }">
          <span :style="fieldSlots.labelData?.value?.customLabelStyle">
            自定义 + {{ fieldSlots.labelData?.value?.label }}
          </span>
          <i
            v-if="fieldSlots.labelData?.value?.required"
            class="van-badge__wrapper van-icon__wrap mbicon mbicon-Required van-field__required-mark"
          />
        </label>
      </div>
    </template>

    <!-- 根据 fieldSlots 数据动态渲染左侧图标 -->
    <template v-if="fieldSlots.hasPrepend" #left-icon>
      {{ fieldSlots.prependContent }}
    </template>

    <!-- 根据 fieldSlots 数据动态渲染右侧图标 -->
    <template v-if="fieldSlots.hasAppend" #right-icon>
      {{ fieldSlots.appendContent }}
    </template>
  </Field>
</template>
  • 使用方式说明:
    • 不再使用 BaseField,直接使用 Vant 的 Field 组件:
      • v-model="value":字段双向绑定;
      • v-bind="fieldProps":透传由 useField 计算出的属性(如 placeholder、校验相关 props 等);
      • :style="wrapperStyle":应用统一样式;
      • @update:model-value="handleValueChange"@focus="handleFocus"@blur="handleBlur":自行接入事件。
    • 使用 fieldSlots 提供的数据决定是否渲染 label / icon 等:
      • fieldSlots.hasLabelSlot + fieldSlots.labelData?.value 控制标签显隐、宽度、样式、必填星号等;
      • fieldSlots.hasPrepend / fieldSlots.hasAppend + 内容字段控制左右自定义区域。
  • 与前两种模式的区别:
    • 布局与 DOM 结构完全由业务组件掌控;
    • 只复用数据与配置层能力(fieldProps / fieldSlots / 事件 Hook),不复用 BaseField 的 UI 结构。
  • 适合人群/场景:
    • 对 DOM 结构、动画、样式有强定制需求;
    • 由前端开发者主导开发,低码配置主要提供字段元数据与校验规则;

总结: 模式三是 “只拿 Schema & Hook,不拿默认 UI” 的高级用法,保留低码平台的数据/事件体系,同时对视觉与交互做彻底自定义。


建议实践:

  • 通用表单场景优先使用 模式一(基础模式)
  • 需要少量 UI 扩展时优先考虑 模式二(插槽扩展模式)
  • 仅在前两种模式无法满足需求时,再采用 模式三(完全自定义模式),以控制维护成本。