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

455 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 低码移动端组件开发指南
## 概述
本文档介绍低码平台的移动端组件开发指南。移动端组件基于 Vue 进行开发,以 H5 形式运行,组件库采用 Vant。
**阅读前须知:** 在阅读本文档前,请先阅读《字段类型与物料组件开发指南.md》。移动端开发与浏览器端在整体设计、开发流程上保持一致,主要差异在于底层规划、文件目录、运行和初始化等差异。对于组件开发者,只需了按移动端的目录标准开发组件、配置组件即可。
本文档主要介绍移动端开发中的差异部分。关于功能开发的详细说明、开发标准与规范,请参考《字段类型与物料组件开发指南.md》。
## 目录结构
移动端组件的相关配置文件位于 `packages/widget-mobile/src/config` 目录下,主要包含以下文件:
```text
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` 文件中添加组件类型枚举,枚举值需要与后端定义的控件类型保持一致。
```typescript
export enum WidgetType {
// 测试组件
TestWidget = "TEST_WIDGET",
// 你的新组件
// YourWidget = 'YOUR_WIDGET',
}
```
### 步骤 2:创建 Vue 组件
`widget/` 目录下创建组件的 Vue 文件,例如 `YourWidget.vue`
```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 控制器方法
参考示例:
```typescript
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` 对象中添加组件映射。
```typescript
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 的导入和注册。
```typescript
import TestWidgetHelper from "./widget-helper/TestWidgetHelper";
export const SYS_MOBILE_WIDGET_HELPERS = [
TestWidgetHelper,
// 你的新组件
// YourWidgetHelper,
];
```
### 步骤 6:添加到物料面板
`Material.ts` 文件的 `getMaterialPanelConfig()` 函数中添加组件到物料面板配置。
```typescript
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,零插槽开发)
**示例代码:**
```vue
<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 + 自定义插槽)
**示例代码:**
```vue
<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
**示例代码:**
```vue
<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 扩展时优先考虑 **模式二(插槽扩展模式)**
- 仅在前两种模式无法满足需求时,再采用 **模式三(完全自定义模式)**,以控制维护成本。