📝 作者:一名 Vue 的学习者
🕒 记录时间:2025年12月
💡 目标:熟悉表单组件的使用,实现如下效果:


在后台系统中,表单几乎是最核心的交互形式:新增、编辑、查询、配置……
如果每个页面都从头写一套,不仅重复,而且难维护。所以我在学习过程中,尝试总结并封装了一套通用表单方案

本篇记录我对 Vue3 + Element Plus 表单的实践与思考。


一、常见表单需求

实际开发中,表单通常需要支持:

  • 多种控件:input / select / radio / checkbox / date 等
  • 表单校验(必填、长度、格式)
  • 新增 / 编辑复用同一套表单
  • 支持重置、回显数据
  • 动态表单项(不同页面字段不同)
  • 与弹窗 Dialog 组合使用

目标:一套 BaseForm,多个页面复用


二、设计思路

  1. 基于 el-form 二次封装
  2. 表单项用配置驱动(JSON 描述)
  3. 内部自动渲染不同控件
  4. 暴露方法:validate / reset / getData / setData
  5. 父组件只关心:字段配置 + 数据 + 提交逻辑

三、表单项配置示例

// utils/formSchemas.js
export const userFormSchemas = [
  {
    prop: "username",
    label: "用户名",
    type: "input",
    placeholder: "请输入用户名",
    rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }]
  },
  {
    prop: "password",
    label: "密码",
    type: "password",
    placeholder: "请输入密码",
    rules: [{ required: true, message: "密码不能为空", trigger: "blur" }]
  },
  {
    prop: "role",
    label: "角色",
    type: "select",
    options: [
      { label: "管理员", value: "admin" },
      { label: "普通用户", value: "user" }
    ],
    rules: [{ required: true, message: "请选择角色", trigger: "change" }]
  },
  {
    prop: "status",
    label: "状态",
    type: "radio",
    options: [
      { label: "启用", value: 1 },
      { label: "禁用", value: 0 }
    ],
    default: 1
  }
];

四、BaseForm.vue 封装实现

src/components/base/BaseForm.vue

<template>
  <el-form
    ref="formRef"
    :model="formModel"
    :rules="rules"
    label-width="100px"
  >
    <el-row :gutter="20">
      <el-col
        v-for="item in schemas"
        :key="item.prop"
        :span="item.span || 24"
      >
        <el-form-item :label="item.label" :prop="item.prop">
          <!-- 输入框 -->
          <el-input
            v-if="item.type === 'input'"
            v-model="formModel[item.prop]"
            :placeholder="item.placeholder"
          />
          <el-input
            v-else-if="item.type === 'password'"
            v-model="formModel[item.prop]"
            show-password
            :placeholder="item.placeholder"
          />

          <!-- 下拉框 -->
          <el-select
            v-else-if="item.type === 'select'"
            v-model="formModel[item.prop]"
            :placeholder="item.placeholder"
            style="width: 100%"
          >
            <el-option
              v-for="opt in item.options || []"
              :key="opt.value"
              :label="opt.label"
              :value="opt.value"
            />
          </el-select>
          <!-- 单选 -->
          <el-radio-group
            v-else-if="item.type === 'radio'"
            v-model="formModel[item.prop]"
          >
            <el-radio
              v-for="opt in item.options || []"
              :key="opt.value"
              :label="opt.value"
            >
              {{ opt.label }}
            </el-radio>
          </el-radio-group>
          <!-- 插槽扩展 -->
          <slot
            v-else
            :name="item.prop"
            :model="formModel"
          />
        </el-form-item>
      </el-col>
    </el-row>
    <!-- 操作区 -->
    <div class="form-actions">
      <el-button type="primary" @click="handleSubmit">提交</el-button>
      <el-button @click="handleReset">重置</el-button>
    </div>
  </el-form>
</template>
<script setup>
import { ref, reactive, watch, defineExpose } from "vue";

const props = defineProps({
  schemas: { type: Array, default: () => [] },
  modelValue: { type: Object, default: () => ({}) }
});

const emits = defineEmits(["update:modelValue", "submit"]);

const formRef = ref(null);
const formModel = reactive({});
const rules = reactive({});

// 初始化表单
const initForm = () => {
  props.schemas.forEach(item => {
    formModel[item.prop] =
      props.modelValue[item.prop] ??
      item.default ??
      "";
    if (item.rules) rules[item.prop] = item.rules;
  });
};

watch(
  () => props.modelValue,
  () => initForm(),
  { immediate: true, deep: true }
);

// 提交
const handleSubmit = () => {
  formRef.value.validate(valid => {
    if (valid) {
      emits("submit", { ...formModel });
    }
  });
};

// 重置
const handleReset = () => {
  formRef.value.resetFields();
};

// 暴露方法
defineExpose({
  validate: () => formRef.value.validate(),
  reset: handleReset,
  getData: () => ({ ...formModel }),
  setData: (data) => {
    Object.assign(formModel, data);
  }
});
</script>
<style scoped>
.form-actions {
  text-align: right;
  margin-top: 20px;
}
</style>

五、页面中使用表单

<template>
  <el-card>
    <BaseForm
      ref="formRef"
      :schemas="schemas"
      :model-value="formData"
      @submit="handleSubmit"
    />
  </el-card>
</template>
<script setup>
import { ref } from "vue";
import BaseForm from "@/components/base/BaseForm.vue";
import { userFormSchemas } from "@/utils/formSchemas";

const formRef = ref(null);
const schemas = userFormSchemas;
const formData = ref({});

const handleSubmit = (data) => {
  console.log("提交表单数据:", data);
  // 调接口即可
};
</script>

六、与弹窗 Dialog 结合

<el-dialog v-model="visible" title="新增用户" width="500px">
  <BaseForm
    ref="formRef"
    :schemas="schemas"
    :model-value="formData"
    @submit="submitForm"
  />
</el-dialog>

✔ 新增 / 编辑复用
✔ 打开弹窗时 setData 回显
✔ 关闭时 reset


七、小结

本篇我完成了:

✅ 基于 Element Plus 封装 BaseForm
✅ 表单项配置驱动渲染
✅ 支持校验、重置、回显
✅ 暴露方法给父组件调用
✅ 可与 Dialog / 页面自由组合

通过这种方式,后续新页面只需要:

👉 写 schemas + 提交逻辑,就能快速完成表单开发。


八、下一篇预告

👉 Vue 学习笔记(十四)|文件上传组件封装(单文件 / 多文件 / 进度条 / 预览)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐