封装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 的新增/编辑弹窗中,visitorFormListcomputed 动态组装,避免写大量模板分支:

复制代码
			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 小时前
Unity类似博物馆场景
前端·unity·游戏引擎·贴图·模型
喜欢吃鱿鱼2 小时前
vue 数字转千分位js
前端·javascript·vue.js
吴声子夜歌2 小时前
Vue3——组件进阶
前端·javascript·vue.js
鸽芷咕2 小时前
KingbaseES NFS部署实战:环境变量缺失与权限报错排查指南
前端·chrome
Fighting_p2 小时前
【FileShowCom 组件】文件预览:图片预览 el-image,其余文件预览打开新窗口或者下载
开发语言·前端·javascript
Ting.~2 小时前
从 0 到 1 搭建 Vue 项目
vue.js·前端框架
a1117762 小时前
Web3D 在线3D模型骨骼动画编辑器(开源 Reze Studio)
前端·3d·开源·html
柳杉2 小时前
有了大屏设计稿还不够,我又用 gpt-image-2把里面的素材扒了出来
前端·three.js·数据可视化
朝阳392 小时前
react 实战【svg 图片】插件 vite-plugin-svgr 的使用
前端·javascript·react.js