Files

455 lines
15 KiB
Markdown
Raw Permalink Normal View History

2026-05-23 14:05:22 +08:00
# 低码移动端组件开发指南
## 概述
本文档介绍低码平台的移动端组件开发指南。移动端组件基于 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 扩展时优先考虑 **模式二(插槽扩展模式)**
- 仅在前两种模式无法满足需求时,再采用 **模式三(完全自定义模式)**,以控制维护成本。