封装form表单

在后台项目里,表单是最容易产生重复代码的区域:

同样的输入框、同样的校验、同样的抽屉弹窗,不同页面反复写,改一处要改一片。

这次我在 daotu-cloud-vue3 里做了一个轻量级表单封装:src/components/FormControl。目标很直接:统一控件行为 + 配置化渲染 + 保留业务灵活性。


1. 组件全景:一层分发 + 一层容器 + 一组原子控件

index.js 统一导出所有能力,调用方不关心内部细节:

index.jsLines 1-23

/**

* 常用表单控件封装(Element Plus),全局注册见 registerGlobal.js

*

* FormField 统一入口,as 选择 input | password | textarea | select | switch | date | daterange | ...

* ...

*/

export { default as FormField } from './FormField.vue'

export { default as FormInput } from './FormInput.vue'

export { default as FormPasswordInput } from './FormPasswordInput.vue'

export { default as FormTextarea } from './FormTextarea.vue'

export { default as FormSelect } from './FormSelect.vue'

export { default as FormSwitch } from './FormSwitch.vue'

export { default as FormDatePicker } from './FormDatePicker.vue'

export { default as FormStatic } from './FormStatic.vue'

export { default as FormSchemaDrawer } from './FormSchemaDrawer.vue'

并且在全局注册入口里直接注册成全局组件:

registerGlobal.jsLines 56-64

import FormField from '@/components/FormControl/FormField.vue'

import FormInput from '@/components/FormControl/FormInput.vue'

import FormPasswordInput from '@/components/FormControl/FormPasswordInput.vue'

import FormTextarea from '@/components/FormControl/FormTextarea.vue'

import FormSelect from '@/components/FormControl/FormSelect.vue'

import FormSwitch from '@/components/FormControl/FormSwitch.vue'

import FormDatePicker from '@/components/FormControl/FormDatePicker.vue'

import FormSchemaDrawer from '@/components/FormControl/FormSchemaDrawer.vue'

app.component('FormField', FormField)

app.component('FormInput', FormInput)

app.component('FormPasswordInput', FormPasswordInput)

app.component('FormTextarea', FormTextarea)

app.component('FormSelect', FormSelect)

app.component('FormSwitch', FormSwitch)

app.component('FormDatePicker', FormDatePicker)

app.component('FormSchemaDrawer', FormSchemaDrawer)


2. 核心思想:FormField 做统一分发

FormField 的价值是"页面只写类型,不写实现"。

比如 as="select"、as="date",它自动映射到底层组件。

复制代码
const TAG_BY_AS = {
	input: 'FormInput',
	forminput: 'FormInput',
	password: 'FormPasswordInput',
	formpasswordinput: 'FormPasswordInput',
	textarea: 'FormTextarea',
	formtextarea: 'FormTextarea',
	select: 'FormSelect',
	formselect: 'FormSelect',
	switch: 'FormSwitch',
	formswitch: 'FormSwitch',
	date: 'FormDatePicker',
	formdatepicker: 'FormDatePicker',
	datetime: 'FormDatePicker',
	daterange: 'FormDatePicker',
	datetimerange: 'FormDatePicker',
	month: 'FormDatePicker',
	year: 'FormDatePicker',
	week: 'FormDatePicker'
}

它还支持日期类型的自动补全(没传 type 时根据 as 推断):

复制代码
mergedBind() {
	const attrs = { ...this.$attrs }
	if (this.resolvedTag !== 'FormDatePicker' || attrs.type) {
		return attrs
	}
	const a = normalizeKey(this.as).toLowerCase()
	const fromAs = {
		date: 'date',
		datetime: 'datetime',
		daterange: 'daterange',
		datetimerange: 'datetimerange',
		month: 'month',
		year: 'year',
		week: 'week',
		formdatepicker: 'date'
	}
	if (fromAs[a]) attrs.type = fromAs[a]
	return attrs
}

3. 容器层:FormSchemaDrawer 负责"规则与骨架"

FormSchemaDrawer 统一了抽屉、表单、校验和底部按钮,并按 formlist 渲染字段:

复制代码
<template v-for="(item, index) in formlist" :key="item.prop || item.slot || index">
	<el-form-item
		v-if="item.component === 'slot' && item.slot"
		:label="item.label"
		:prop="item.prop"
		:required="item.required"
	>
		<slot :name="item.slot" :item="item" />
	</el-form-item>
	<el-form-item
		v-else-if="isFormStaticItem(item)"
		:label="item.label"
	>
		<FormStatic
			:content="item.content"
			:content-class="item.contentClass"
			:wrapper-class="item.wrapperClass"
		/>
	</el-form-item>
	<el-form-item
		v-else
		:label="item.label"
		:prop="item.prop"
		:required="item.required"
	>
		<FormField
			:as="item.component || item.as || 'input'"
			:model-value="formModel[item.prop]"
			v-bind="item.props || {}"
			@update:model-value="(v) => onSchemaFieldInput(item.prop, v)"
		/>
	</el-form-item>
</template>

提交时由容器做统一校验拦截:

复制代码
handleConfirm() {
	this.$refs.schemaFormRef?.validate((valid) => {
		if (!valid) return
		this.$emit('confirm', this.formModel)
	})
}

4. 业务页怎么用:动态表单是关键收益点

在 orgVisitor 的新增/编辑弹窗中,visitorFormList 用 computed 动态组装,避免写大量模板分支:

复制代码
			visitorFormList() {
				const read = this.isRead;
				const spec = this.specification;
				const t = this.type;
				const blurUser = () => this.setGraduationUser();
				const fixedPropsMap = {
					visitorName: { placeholder: "请输入姓名", class: inputClass, readonly: read, onBlur: blurUser },
					sex: {
						placeholder: "请选择性别",
						disabled: read,
						class: inputClass,
						options: this.sexOptions,
						labelKey: "dictLabel",
						valueKey: "dictValue",
						onChange: blurUser,
					},
					birthday: {
						disabled: read,
						type: "date",
						format: "YYYY/MM/DD",
						valueFormat: "YYYY-MM-DD",
						placeholder: "请选择出生日期",
						class: inputClass,
						disabledDate: this.disabledDate,
					},
					nation: {
						placeholder: "请选择民族",
						disabled: read,
						class: inputClass,
						options: this.nationOptions,
						labelKey: "dictLabel",
						valueKey: "dictValue",
					},
					visType: {
						placeholder: "请选择人员类型",
						disabled: read,
						class: inputClass,
						options: this.visTypeOptions,
						labelKey: "typeName",
						valueKey: "typeValue",
						onChange: blurUser,
					},
					health: {
						placeholder: "请选择健康状态",
						disabled: read,
						class: inputClass,
						options: this.healthOptions,
						labelKey: "dictLabel",
						valueKey: "dictValue",
					},
				};

				const list = this.fixedFormList.map((item) => ({
					...item,
					props: fixedPropsMap[item.key] || item.props,
				}));

				const dynamicAccountField =
					spec === "2"
						? {
								label: "手机号 : ",
								prop: "phonenumber",
								component: "FormInput",
								props: {
									type: "number",
									disabled: t == 1,
									placeholder: "请输入手机号",
									class: inputClass,
									readonly: read,
									onBlur: blurUser,
								},
						  }
						: spec === "3"
						? {
								label: "身份证号 : ",
								prop: "identity",
								component: "FormInput",
								props: {
									disabled: t == 1,
									placeholder: "请输入身份证号",
									class: inputClass,
									maxlength: 50,
									readonly: read,
									onBlur: blurUser,
								},
						  }
						: spec === "4" || spec === "5"
						? t == 0 && spec === "5"
							? {
									label: "人员账号 : ",
									prop: "nickName",
									component: "slot",
									slot: "visitorNickNameComposite",
									required: true,
							  }
							: {
									label: "人员账号 : ",
									prop: "nickName",
									component: "FormInput",
									required: true,
									props: {
										disabled: t == 1,
										placeholder: "请输入人员账号",
										class: inputClass,
										maxlength: 20,
										readonly: read,
										onBlur: blurUser,
										onInput: (v) => this.handleNickNameInput(v),
									},
							  }
						: null;

				if (dynamicAccountField) {
					list.splice(1, 0, dynamicAccountField);
				}

				if (this.showJoin) {
					list.push({
						label: "",
						prop: "joinTask",
						component: "slot",
						slot: "visitorJoinTask",
					});
				}

				return list;
			},

根据不同规则插入不同账号字段:

复制代码
				const dynamicAccountField =
					spec === "2"
						? {
								label: "手机号 : ",
								prop: "phonenumber",
								component: "FormInput",
								props: {
									type: "number",
									disabled: t == 1,
									placeholder: "请输入手机号",
									class: inputClass,
									readonly: read,
									onBlur: blurUser,
								},
						  }
						: spec === "3"
						? {
								label: "身份证号 : ",
								prop: "identity",
								component: "FormInput",
								props: {
									disabled: t == 1,
									placeholder: "请输入身份证号",
									class: inputClass,
									maxlength: 50,
									readonly: read,
									onBlur: blurUser,
								},
						  }

最后把 formModel + rules + formlist 统一交给容器:

复制代码
<FormSchemaDrawer

ref="schemaDrawerRef"

v-model="isShow"

direction="rtl"

:modal="true"

class="el-drawer-pr"

size="25%"

:title="title"

label-width="120px"

form-class="el-form-height"

body-class="form-schema-drawer-body org-visitor-add-body"

:form-model="formVal"

:formlist="visitorFormList"

:rules="rules"

:show-footer="type != 2"/>

5. 这套封装带来的实际价值

  • 降低样板代码:同类字段复用同一套行为和事件模型。
  • 提升变更效率:字段改动主要集中在配置,不再大面积改模板。
  • 控件行为一致:输入、选择、日期、开关都遵循同一 v-model 和 attrs 透传模式。
  • 复杂场景可扩展:保留 slot 机制,不把业务灵活性封死。
相关推荐
陈随易2 小时前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
Dream-Y.ocean4 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn5 小时前
性能监控
前端
Highcharts.js5 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn5 小时前
构建工具
前端
FRing036 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷7 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen8 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒8 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫9 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库