Vue2 + Element UI 通用表单组件封装:高效开发中后台表单

84 阅读5分钟

在 Vue2 中后台管理系统开发中,表单是贯穿始终的核心交互组件 —— 数据查询、新增编辑、参数配置等场景都离不开它。但重复编写表单结构、复制粘贴验证逻辑、维护零散的表单状态,不仅效率低下,还会导致代码冗余、风格不统一。本文基于 Vue2 + Element UI,封装一个配置化、高复用、高灵活的通用表单组件,让表单开发从 “重复编码” 变成 “配置化组装”,大幅提升开发效率。

QQ20251104-173647.png

一、封装目标与核心价值

1. 封装核心目标

  • 支持 Element UI 常用表单控件(输入框、选择器、日期选择器、开关等)
  • 配置化生成表单,无需重复编写 HTML 结构
  • 统一表单验证、重置、清除验证等基础方法
  • 兼容 Element UI 表单核心特性(行内布局、标签位置、尺寸、禁用状态等)
  • 支持自定义插槽,满足特殊业务场景(如自定义控件、联动逻辑)

2. 相比原生开发的优势

对比维度原生开发方式通用表单封装方式
开发效率重复编写 HTML,效率低下配置化生成,一行配置一个表单项
代码维护零散分布,修改需改多处配置集中管理,修改更便捷
风格统一性易出现样式 / 交互不一致统一封装,风格完全一致
学习成本需熟悉每个控件的使用细节基于 Element UI,上手即会
扩展性需手动扩展,兼容性难保证支持插槽扩展,兼容特殊场景

二、技术选型

  • 核心框架:Vue2(Options API)
  • UI 组件库:Element UI(2.x 版本,表单控件核心依赖)
  • 样式预处理:SCSS(结构化编写样式,支持组件样式隔离)

三、通用表单组件完整实现(BaseForm)

1. 组件核心思路

通过 props 接收表单数据表单项配置验证规则等核心参数,内部通过 v-for 遍历表单项配置,动态渲染对应 Element UI 控件,同时封装统一的表单方法(验证、重置等),对外暴露调用接口。

2. 完整组件代码(BaseForm.vue)

<template>
  <div class="base-form">
    <!-- Element UI 表单容器,绑定核心属性 -->
    <el-form
      ref="form"
      :model="formData"
      :rules="rules"
      :label-width="labelWidth"
      :label-position="labelPosition"
      :inline="inline"
      :size="size"
      :disabled="disabled"
    >
      <!-- 遍历表单项配置,动态渲染控件 -->
      <template v-for="(item, index) in formItems">
        <el-form-item
          :key="index"
          :label="item.label"
          :prop="item.prop"
          :rules="item.rules"  <!-- 支持单个表单项独立验证规则 -->
          :hidden="item.hidden" <!-- 支持动态隐藏表单项 -->
        >
          <!-- 1. 输入框(input) -->
          <el-input
            v-if="item.type === 'input'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"  <!-- 透传控件原生属性 -->
            :placeholder="item.placeholder || `请输入${item.label}`"
          />

          <!-- 2. 下拉选择器(select) -->
          <el-select
            v-else-if="item.type === 'select'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"
            :placeholder="item.placeholder || `请选择${item.label}`"
          >
            <el-option
              v-for="option in item.options"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            />
          </el-select>

          <!-- 3. 日期选择器(date) -->
          <el-date-picker
            v-else-if="item.type === 'date'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"
            :placeholder="item.placeholder || `请选择${item.label}`"
          />

          <!-- 4. 时间选择器(time) -->
          <el-time-picker
            v-else-if="item.type === 'time'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"
            :placeholder="item.placeholder || `请选择${item.label}`"
          />

          <!-- 5. 日期时间选择器(datetime) -->
          <el-date-picker
            v-else-if="item.type === 'datetime'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"
            type="datetime"
            :placeholder="item.placeholder || `请选择${item.label}`"
          />

          <!-- 6. 开关(switch) -->
          <el-switch
            v-else-if="item.type === 'switch'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"
            :active-text="item.activeText"
            :inactive-text="item.inactiveText"
          />

          <!-- 7. 单选框组(radio) -->
          <el-radio-group
            v-else-if="item.type === 'radio'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"
          >
            <el-radio
              v-for="option in item.options"
              :key="option.value"
              :label="option.value"
              :disabled="option.disabled"
            >
              {{ option.label }}
            </el-radio>
          </el-radio-group>

          <!-- 8. 复选框组(checkbox) -->
          <el-checkbox-group
            v-else-if="item.type === 'checkbox'"
            v-model="formData[item.prop]"
            v-bind="item.props || {}"
          >
            <el-checkbox
              v-for="option in item.options"
              :key="option.value"
              :label="option.value"
              :disabled="option.disabled"
            >
              {{ option.label }}
            </el-checkbox>
          </el-checkbox-group>

          <!-- 9. 文本域(textarea) -->
          <el-input
            v-else-if="item.type === 'textarea'"
            v-model="formData[item.prop]"
            type="textarea"
            v-bind="item.props || {}"
            :placeholder="item.placeholder || `请输入${item.label}`"
          />

          <!-- 10. 自定义插槽(支持特殊业务场景) -->
          <slot
            v-else-if="item.type === 'slot'"
            :name="item.slotName"
            :formData="formData"
            :item="item"
          ></slot>
        </el-form-item>
      </template>
    </el-form>
  </div>
</template>

<script>
export default {
  name: 'BaseForm', // 组件名称,便于调试和复用
  props: {
    // 1. 表单核心数据(双向绑定的表单值)
    formData: {
      type: Object,
      required: true,
      validator: (val) => {
        // 验证 formData 是纯对象(避免数组/null 等)
        return Object.prototype.toString.call(val) === '[object Object]'
      }
    },

    // 2. 表单项配置(数组,每个元素对应一个表单项)
    formItems: {
      type: Array,
      default: () => [],
      validator: (val) => {
        // 验证每个表单项必须包含 type 和 prop
        return val.every(item => item.type && item.prop)
      }
    },

    // 3. 全局表单验证规则(Element UI 原生规则格式)
    rules: {
      type: Object,
      default: () => {}
    },

    // 4. 标签宽度(支持 px/百分比,同 Element UI)
    labelWidth: {
      type: String,
      default: '100px'
    },

    // 5. 标签位置(left/right/top,同 Element UI)
    labelPosition: {
      type: String,
      default: 'right',
      validator: (val) => ['left', 'right', 'top'].includes(val)
    },

    // 6. 是否行内表单(适用于查询表单)
    inline: {
      type: Boolean,
      default: false
    },

    // 7. 表单尺寸(large/medium/small,同 Element UI)
    size: {
      type: String,
      default: 'medium',
      validator: (val) => ['large', 'medium', 'small'].includes(val)
    },

    // 8. 是否禁用整个表单
    disabled: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    /**
     * 表单验证(对外暴露,返回 Promise 便于异步处理)
     * @returns {Promise} 验证成功 resolve(formData),失败 reject(error)
     */
    validate() {
      return new Promise((resolve, reject) => {
        this.$refs.form.validate((valid, error) => {
          if (valid) {
            resolve({ ...this.formData }) // 深拷贝返回,避免外部修改原数据
          } else {
            reject(error) // 返回验证失败信息
          }
        })
      })
    },

    /**
     * 重置表单(恢复初始值 + 清除验证状态)
     */
    resetForm() {
      this.$refs.form.resetFields()
    },

    /**
     * 清除指定表单项的验证状态(支持单个或多个)
     * @param {String/Array} props - 表单项 prop 名称(单个字符串或数组)
     */
    clearValidate(props) {
      this.$refs.form.clearValidate(props)
    },

    /**
     * 手动设置表单项值(支持外部动态修改表单数据)
     * @param {Object} data - { prop: 值 } 格式的对象
     */
    setFormData(data) {
      Object.keys(data).forEach(prop => {
        if (this.formData.hasOwnProperty(prop)) {
          this.$set(this.formData, prop, data[prop]) // 响应式设置
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.base-form {
  width: 100%;
  // 表单项间距优化(避免原生 Element UI 间距过窄)
  .el-form-item {
    margin-bottom: 20px;

    // 行内表单间距调整
    &.el-form-item--inline {
      margin-right: 30px;
      margin-bottom: 15px;
    }
  }

  // 适配文本域自动高度
  .el-textarea {
    resize: vertical;
  }
}
</style>

四、组件核心特性详解

1. 配置化表单项(formItems)

formItems 是组件的核心配置,每个元素对应一个表单项,支持以下关键属性:

属性名类型说明必传
typeString表单控件类型(input/select/date 等)
propString表单数据绑定的 key(与 formData 对应)
labelString表单项标签文本
placeholderString输入提示文本(默认 “请输入 / 选择 XX”)
rulesArray单个表单项的验证规则(覆盖全局 rules)
propsObject透传控件原生属性(如 maxlengthmultiple
optionsArray选择器 / 单选框 / 复选框的选项([{label, value}])
hiddenBoolean是否隐藏该表单项(动态控制显示)
activeTextStringswitch 组件激活状态文本
inactiveTextStringswitch 组件未激活状态文本
slotNameString自定义插槽名称(type 为 slot 时必传)

2. 统一表单方法

组件封装了 Element UI 表单常用方法,对外暴露统一接口,无需手动操作 $refs

  • validate():表单验证(返回 Promise,适配异步提交场景)
  • resetForm():重置表单(恢复初始值 + 清除验证)
  • clearValidate(props):清除指定表单项验证状态
  • setFormData(data):外部动态修改表单数据(保证响应式)

3. 灵活的扩展性

(1)支持控件原生属性透传

通过 props 字段可以透传 Element UI 控件的原生属性,例如:

{
  type: 'input',
  prop: 'username',
  label: '用户名',
  props: {
    maxlength: 20, // 输入长度限制
    showWordLimit: true, // 显示字数统计
    disabled: false // 单独禁用该输入框
  }
}

(2)支持自定义插槽(特殊业务场景)

当现有控件无法满足需求时(如自定义联动控件、第三方组件),可通过 slot 类型扩展:

<!-- 父组件中使用插槽 -->
<base-form :formData="formData" :formItems="formItems">
  <!-- 自定义插槽:例如“密码强度显示”控件 -->
  <template #custom-password="scope">
    <el-input
      v-model="scope.formData.password"
      placeholder="请输入密码"
      type="password"
    />
    <password-strength :value="scope.formData.password" />
  </template>
</base-form>

<!-- 表单项配置 -->
formItems: [
  {
    type: 'slot',
    prop: 'password',
    label: '密码',
    slotName: 'custom-password' // 与插槽名称对应
  }
]

(3)支持动态显示 / 隐藏表单项

通过 hidden 字段控制表单项显示状态,结合父组件数据实现联动:

// 父组件中
data() {
  return {
    formData: {
      userType: 'ordinary',
      companyName: ''
    },
    formItems: [
      {
        type: 'select',
        prop: 'userType',
        label: '用户类型',
        options: [
          { label: '普通用户', value: 'ordinary' },
          { label: '企业用户', value: 'company' }
        ]
      },
      {
        type: 'input',
        prop: 'companyName',
        label: '企业名称',
        hidden: this.formData.userType !== 'company' // 企业用户才显示
      }
    ]
  }
}

五、组件使用示例

<template>
  <el-dialog title="新增用户" :visible.sync="isShow" width="500px">
    <base-form
      ref="addForm"
      :formData="formData"
      :formItems="formItems"
      :rules="formRules"
    />
    <div slot="footer" class="dialog-footer">
      <el-button @click="isShow = false">取消</el-button>
      <el-button type="primary" @click="handleSubmit">提交</el-button>
    </div>
  </el-dialog>
</template>

<script>
import BaseForm from '@/components/BaseForm'

export default {
  components: { BaseForm },
  data() {
    return {
      isShow: false,
      formData: {
        username: '',
        password: '',
        gender: 1,
        status: 1,
        email: ''
      },
      // 全局验证规则
      formRules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' },
          { min: 3, max: 20, message: '用户名长度在 3-20 字符之间', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { min: 6, message: '密码长度不少于 6 字符', trigger: 'blur' }
        ],
        email: [
          { required: true, message: '请输入邮箱', trigger: 'blur' },
          { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
        ]
      },
      // 表单项配置
      formItems: [
        {
          type: 'input',
          prop: 'username',
          label: '用户名',
          props: { showWordLimit: true, maxlength: 20 }
        },
        {
          type: 'input',
          prop: 'password',
          label: '密码',
          props: { type: 'password', maxlength: 20 }
        },
        {
          type: 'radio',
          prop: 'gender',
          label: '性别',
          options: [
            { label: '男', value: 1 },
            { label: '女', value: 2 }
          ]
        },
        {
          type: 'select',
          prop: 'status',
          label: '状态',
          options: [
            { label: '启用', value: 1 },
            { label: '禁用', value: 0 }
          ]
        },
        {
          type: 'input',
          prop: 'email',
          label: '邮箱'
        }
      ]
    }
  },
  methods: {
    // 打开弹窗(编辑时传入初始值)
    openDialog(initData = {}) {
      this.isShow = true
      // 重置表单
      this.$refs.addForm.resetForm()
      // 填充初始值(编辑场景)
      if (Object.keys(initData).length > 0) {
        this.$refs.addForm.setFormData(initData)
      }
    },
    // 提交表单
    async handleSubmit() {
      try {
        const formData = await this.$refs.addForm.validate()
        console.log('提交数据:', formData)
        // 调用新增/编辑接口...
        this.isShow = false
      } catch (error) {
        console.log('表单验证失败:', error)
      }
    }
  }
}
</script>