vue3+element plus通用公共表单组件(支持ts)

场景:

作为一名前端战士,在页面、弹窗等多种展示容器内,我们都有可能涉及对表单的展示提交、编辑回显等功能实现。

封装一个公共的表单组件,只需在要展示表单的容器内将本组件引入,搭配对应的配置项来描述表单内的相关信息(如表单内的展示文本、表单控件类型、校验方式等),就可以实现一个组件满足多种展示需求,从而提升开发效率和代码的可扩展性。

示例 组件展示效果:

组件宽度会根据引入它的外层容器宽度自适应,占满外层容器的宽。

在页面内引入本组件

在弹窗内引入本组件

组件可实现的功能:

以下内容均为动态,根据传入组件的配置项来展示与实现:

1. 每个表单项需要展示的文本、表单控件类型(包括:输入框、下拉框、日期选择器、多选框组、单选框组、开关)

2. 每个表单控件的状态(禁用等)及事件绑定(change事件等)

3. 表单项的校验规则(必填、自定义校验规则等)及校验方法的调用(基于elememt plus)

4. 表单内所填写数据的整体获取及整体回显(避免繁琐的单个赋值或传值操作)

组件的使用:

1. 将组件引入

2. 给组件内传入参数:

(1) 表单展示配置项 formSet

(2) 用于存储/回显表单内填写数据的对象 formData

(3) 表单校验规则配置项(若不涉及校验,可不传)formRules

使用举例:

步骤一

将下面的代码放入单独的.vue文件,这就是我们的公共表单组件。(基于elememt plus封装)

<template>
  <div class="form-public">
    <!-- element plus 组件 -->
    <el-form
      :model="formData"
      label-width="auto"
      :rules="formRules"
      ref="formRef"
    >
      <!-- 循环配置项得到 -->
      <el-form-item
        v-for="obj of formSet"
        :key="obj.propName"
        :label="obj.text"
        :prop="obj.propName"
      >
        <!-- 输入框展示 -->
        <el-input
          v-if="obj.showType === 'input'"
          v-model="formData[obj.propName]"
          :placeholder="obj.placeholder"
          v-bind="obj.addSetProp || {}"
          v-on="obj.methodsSetProp || {}"
          :style="obj.styleSet"
        />

        <!-- 下拉框展示 -->
        <el-select
          v-if="obj.showType === 'select'"
          v-model="formData[obj.propName]"
          :placeholder="obj.placeholder"
          v-bind="obj.addSetProp || {}"
          v-on="obj.methodsSetProp || {}"
          :style="obj.styleSet"
        >
          <el-option
            v-for="optionObj of obj.optionListSet.optionsList"
            :key="optionObj[obj.optionListSet.valueProp]"
            :value="optionObj[obj.optionListSet.valueProp]"
            :label="optionObj[obj.optionListSet.labelProp]"
            :disabled="optionObj[obj.optionListSet.disabledProp]"
          />
        </el-select>

        <!-- 日期选择器/日期时间选择器展示 -->
        <el-date-picker
          v-if="obj.showType === 'datePicker'"
          v-model="formData[obj.propName]"
          v-bind="obj.addSetProp || {}"
          v-on="obj.methodsSetProp || {}"
          :style="obj.styleSet"
        />

        <!-- 多选框组展示 -->
        <el-checkbox-group
          v-if="obj.showType === 'checkboxGroup'"
          v-model="formData[obj.propName]"
          v-bind="obj.addSetProp || {}"
          v-on="obj.methodsSetProp || {}"
          :style="obj.styleSet"
        >
          <el-checkbox
            v-for="optionObj of obj.optionListSet.optionsList"
            :key="optionObj[obj.optionListSet.valueProp] || optionObj[obj.optionListSet.labelProp]"
            :label="optionObj[obj.optionListSet.labelProp]"
            :value="optionObj[obj.optionListSet.valueProp]"
            :disabled="optionObj[obj.optionListSet.disabledProp]"
          />
        </el-checkbox-group>

        <!-- 单选框组展示 -->
        <el-radio-group
          v-if="obj.showType === 'radioGroup'"
          v-model="formData[obj.propName]"
          v-bind="obj.addSetProp || {}"
          v-on="obj.methodsSetProp || {}"
          :style="obj.styleSet"
        >
          <el-radio
            v-for="optionObj of obj.optionListSet.optionsList"
            :key="optionObj[obj.optionListSet.valueProp] || optionObj[obj.optionListSet.labelProp]"
            :value="optionObj[obj.optionListSet.valueProp]"
            :disabled="optionObj[obj.optionListSet.disabledProp]"
          >
            {{ optionObj[obj.optionListSet.labelProp] }}
          </el-radio>
        </el-radio-group>

        <!-- switch展示 -->
        <el-switch
          v-if="obj.showType === 'switch'"
          v-model="formData[obj.propName]"
          v-bind="obj.addSetProp || {}"
          v-on="obj.methodsSetProp || {}"
          :style="obj.styleSet"
        />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { reactive, ref } from "vue"
import type { FormInstance } from "element-plus"

// 获取父组件传来的参数
let props = defineProps({
  // 表单配置项
  formSet: {
    type: Object,
    default() {
      return {}
    }
  },
  // 表单数据存储对象
  formData: {
    type: Object,
    default() {
      return {}
    }
  },
  // 表单校验规则配置项
  formRules: {
    type: Object,
    default() {
      return {}
    }
  }
})
// 将父组件传来的参数解构到变量中 供本组件使用
let { formSet, formData } = props
// 将数据存储对象的地址进行转存 并将转存后的地址直接用于本组件的各表单项双向绑定值处(不能直接使用父组件中传来的对象地址的原因是 有时直接使用并修改里面的属性值 es-lint会报错)
// 这样用户对表单的修改 就会直接同步到父组件内的数据存储对象中去 前端不需要再做额外的传值处理 nice
formData = reactive(formData) // 使用reactive包裹已经是代理对象的对象时 只会返回代理对象的原地址

// 获取表单实例
const formRef = ref<FormInstance | null>()

// 表单的校验方法 会在父组件中被调用
const checkForm = () => {
  return new Promise((resolve, reject) => {
    (async () => {
      if (!formRef.value) {
        reject(false)
        return
      }
      // 调用element plus表单组件自带的校验方法
      await formRef.value.validate((valid) => {
        if (valid) {
          resolve(true)
        } else {
          reject(false)
        }
      })
    })()
  })
}

// 重置表单 会在父组件中被调用
const resetForm = () => {
  if (!formRef.value) {
    return
  }
  formRef.value.resetFields()
}

// 使父组件中可以使用本组件中的方法
defineExpose({
  checkForm,
  resetForm
})
</script>

<style lang="scss" scoped>
.form-public {
  width: 100%;
  // 将date组件的宽度调整为100% 和其他组件一样(不需要可以去掉)
  :deep(.el-date-editor.el-input) {
    width: 100%;
  }
}
</style>

步骤二

若项目使用的是TypeScript,那么我们需要一些关于即将添加的配置项的类型声明。在单独的.ts文件中,按需进行下述类型声明,以备后期引用。

使用JavaScript的家人们,浏览一下下述代码,了解各配置项属性对应的含义和值的类型即可。不需要将下述内容添加到文件中。

// 类型声明 表单的展示配置项
export interface FormSetData {
  // 表单展示名称
  text: string,
  // 表单控件类型(使用过程中如需要有新增的控件类型 可以在此处增加配置)
  // input 输入框 | select 下拉框 | datePicker 日期选择器/日期时间选择器 | checkboxGroup多选框组 | radioGroup单选框组 | switch开关
  showType: "input" | "select" | "datePicker" | "checkboxGroup" | "radioGroup" | "switch",
  // 当前项对应的数据存储对象中的属性值
  propName: string,
  // placeholder
  placeholder?: string,
  // 如果表单控件是 下拉框/多选框组/单选框组 会需要
  optionListSet?: {
    // 存储下拉框/多选框组/单选框组列表数据
    optionsList: any[],
    // 下拉框的el-option/多选框组的el-checkbox/单选框组的el-radio组件所需配置项:
    // value label disabled 值在列表数据中对应的属性名
    valueProp: string,
    labelProp: string,
    disabledProp?: string
    // 其他配置项因不是很常用 暂未做专门设计 如需使用可按需在el-option/el-checkbox/el-radio组件标签上增加获取的相关设置
  },
  // 根据使用的表单控件类型不同 可能会需要给表单控件组件绑定其他的配置项 因可能涉及到的配置项较多 在实际使用时直接按需添加在addSetProp对象中即可 在ts中不做明确的类型声明
  // (在表单配置项中 input及datePicker及switch组件使用时 对下面这个对象的使用方式进行了举例)
  addSetProp?: {
    [key: string]: any
  },
  // 若需要在表单控件的组件上增加事件绑定 在实际使用时直接按需添加在methodsSetProp对象中即可 在ts中不做明确的类型声明
  // (在表单配置项中 select组件使用时 对下面这个对象的使用方式进行了举例)
  methodsSetProp?: {
    [key: string]: any
  }
  // 若需要在表单控件的组件上增加自定义的行内样式(一般情况下不会涉及) 在实际使用时直接按需添加在styleObj对象中即可 在ts中不做明确的类型声明
  // (在表单配置项中 switch组件使用时 对下面这个对象的使用方式进行了举例)
  styleSet?: {
    [key: string]: any
  }
  // 注: 如果在使用中 优秀的你所涉及的项目对ts的类型声明要求较高 不允许设置any类型 那么就根据你实际会使用到的属性在上面对应对象中进行配置即可
}

// 类型声明 表单的双向绑定值存储对象 存储的属性名需要与表单配置项中的propName属性值对应
export interface FormData {
  code: string,
  leader: string,
  createTime: string,
  timeFrame: string[] | null,
  level: number | "",
  project: string,
  equipment: string[],
  report: string,
  public: boolean,
  // 为解决在值的获取过程中可能出现的某些类型判断报错
  [key: string]: string | string[] | number | boolean | null;
}

// 下述类型声明仅为当前示例表单内所需 实际开发过程中按自己表单的需要设置即可

// 类型声明 级别下拉框数据
export interface levelOptionsData {
  levelText: string,
  level: number
}

// 类型声明 关联项目下拉框数据
export interface projectOptionsData {
  projectText: string,
  project: string,
  // 是否禁用
  stop?: boolean
}

// 类型声明 设备需求多选框组数据
export interface equipmentOptionsData {
  equipmentText: string,
  equipment: string,
  // 是否禁用
  repair?: boolean
}

// 类型声明 汇报方式单选框组数据
export interface reportOptionsData {
  reportText: string,
  report: string,
  // 是否禁用
  cannot?: boolean
}

步骤三 

在想要使用公共表单组件的.vue文件内,引入刚刚设置的ts的类型声明并设置表单配置项。

创建一个宽度确定的容器,在容器内部引入公共表单组件并将配置项传入。

 ps:如果在引入文件的过程中想使用@来作为src的别名,但之前项目中又没有配置,可以配置下再引入哈

vue3+vite配置项--@别名

在想要使用公共表单组件的.vue文件内(以配置顶部示例图内的表单为例)

<template>
  <div class="form-page">
    <!-- 引入公共表单组件 -->
    <formPublic
      ref="formPublicRef"
      :formSet="formSet"
      :formData="formData"
      :formRules="formRules"
    >
    </formPublic>

    <!-- 按钮区域(考虑到各种场景所需的按钮位置样式可能不同 直接在直接在使用时创建即可) -->
    <div class="button-row">
      <el-button
        type="primary"
        @click="clickToSubmit"
      >
        创建行动
      </el-button>
      
      <el-button
        @click="clickToCancel"
      >
        再想想 清空内容
      </el-button>

      <!-- 模拟编辑场景 -->
      <el-button
        type="success"
        plain
        @click="clickToEdit"
      >
        编辑刚刚创建的内容
      </el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive, ref } from "vue"

// element plus相关
import { ElMessage } from "element-plus"
import type { FormRules } from 'element-plus'

// 引入公共表单组件
import formPublic from "./formPublic/index.vue"
// 引入定义的类型声明(若是js环境不需引入)
import type { FormSetData, FormData, levelOptionsData, projectOptionsData, equipmentOptionsData, reportOptionsData } from "./types/form-page.ts"

// 需要使用到的正则(仅为当前示例表单内进行自定义校验所需 实际开发过程中若不需要可去掉)
const regAll: Record<string, RegExp> = {
  // 属性名就是需要被校验项在表单中对应存储的属性名
  code: /^\d{0,3}-/
}

// 公共方法 使用正则对文本进行校验(仅为当前示例表单内进行自定义校验所需 实际开发过程中若不需要可去掉)
const checkTextByReg = (rule: any, value: string, callback: any) => {
  if (regAll[rule.field]?.test(value)) {
    callback()
  } else {
    // 若校验不通过
    callback(new Error(rule.message))
  }
}

// 公共方法 将选项列表数据放入到表单配置项的对应位置
const pushListToFormSet = <T>(list: Array<T>, propName: string) => {
  for (let obj of formSet) {
    if (obj.propName === propName && obj.optionListSet) {
      obj.optionListSet.optionsList = list
    }  
  }
}

// 表单配置项及校验规则配置项在定义后一般不会整个改变 使用reactive声明即可
// 若业务需求特殊 存在表单或校验规则配置项可能整个改变并且需要页面响应式更新的情况 可以使用ref声明
// 按需设置表单配置项
const formSet: FormSetData[] = reactive([
  {
    text: "行动代号",
    showType: "input",
    propName: "code",
    placeholder: "请输入数字编号-代号  如:01-满汉全席",
    addSetProp: {
      // 输入的最大长度
      maxlength: 20
    }
  },
  {
    text: "Leader",
    showType: "input",
    propName: "leader",
    placeholder: "请输入尊贵的Leader"
  },
  {
    text: "初次登记时间",
    showType: "datePicker",
    propName: "createTime",
    // 其他关于日期选择器的配置项(根据开发需求 按照element plus中给出的配置项名称及对应属性值 添加即可 下述配置项都会被添加到当前表单控件的组件上)
    addSetProp: {
      // 关于属性名: element组件中给出的属性有的带有- 我们这里要写为小驼峰
      // 日期选择器
      type: "date",
      // 获取到的绑定值的格式
      valueFormat: "YYYY/MM/DD",
      // 判断面板内日期是否被禁用的函数
      disabledDate(date: Date) {
        // 将今天及今天之前的时间禁用
        let dateNow = new Date()
        return date <= dateNow
      }
    }
  },
  {
    text: "行动时间",
    showType: "datePicker",
    propName: "timeFrame",
    // 其他关于日期时间选择器的配置项(根据开发需求 按照element plus中给出的配置项规则 添加即可 下述配置项都会被添加到当前表单控件的组件上)
    addSetProp: {
      // 关于属性名: element组件中给出的属性有的带有- 我们这里要写为小驼峰
      // 日期时间选择器(范围选择)
      type: "datetimerange",
      // 占位文字
      startPlaceholder: "开始的开始",
      endPlaceholder: "最后的最后",
      // 获取到的绑定值的格式
      valueFormat: "YYYY/MM/DD HH:mm:ss",
    }
  },
  {
    text: "行动级别",
    showType: "select",
    propName: "level",
    optionListSet: {
      // 存储下拉框数据
      optionsList: [],
      // 配置选项的展示和存储取值(根据实际需要的属性名配置即可)
      labelProp: "levelText",
      valueProp: "level"
    },
    // 想要在当前组件上绑定的事件
    methodsSetProp: {
      // 选中值改变时触发
      change(value: number) {
        ElMessage({
          message: `对应的级别是:${value}`,
          grouping: true,
          type: "success",
        })
      } 
    }
  },
  {
    text: "关联项目",
    showType: "select",
    propName: "project",
    optionListSet: {
      // 存储下拉框数据
      optionsList: [],
      // 配置选项的展示和存储取值(根据实际需要的属性名配置即可)
      labelProp: "projectText",
      valueProp: "project",
      // 配置单个选项禁用的判断取值对应的属性名
      disabledProp: "stop"
    }
  },
  {
    text: "设备需求",
    showType: "checkboxGroup",
    propName: "equipment",
    optionListSet: {
      // 存储多选框组数据
      optionsList: [],
      // 配置选项的展示和存储取值(根据实际需要的属性名配置即可)
      labelProp: "equipmentText",
      valueProp: "equipment",
      // 配置单个选项禁用的判断取值对应的属性名
      disabledProp: "repair"
    }
  },
  {
    text: "汇报方式",
    showType: "radioGroup",
    propName: "report",
    optionListSet: {
      // 存储单选框组数据
      optionsList: [],
      // 配置选项的展示和存储取值(根据实际需要的属性名配置即可)
      labelProp: "reportText",
      valueProp: "report",
      // 配置单个选项禁用的判断取值对应的属性名
      disabledProp: "cannot"
    }
  },
  {
    text: "组内公开",
    showType: "switch",
    propName: "public",
    // 其他关于switch的配置项(根据开发需求 按照element plus中给出的配置项名称及对应属性值 添加即可 下述配置项都会被添加到当前表单控件的组件上)
    addSetProp: {
      // 关于属性名: element组件中给出的属性有的带有- 我们这里要写为小驼峰
      activeText: "公开",
      inactiveText: "保密",
    },
    // 自定义行内样式 设置修改开关颜色
    styleSet: {
      '--el-switch-on-color': "#13ce66",
      '--el-switch-off-color': "#6000f9"
    }
  }
])

// 存储表单内的双向绑定值(内部属性名可以直接按照后端传出的要求来设置 在传出数据的时候就可以直接将本对象传出)
const formData = reactive<FormData>({
  code: "",
  leader: "",
  createTime: "",
  timeFrame: [],
  level: "",
  project: "",
  equipment: [],
  report: "",
  public: false
})

// 按element plus给出的规则 设置校验规则配置项
const formRules = reactive<FormRules<FormData>>({
  code: [
    {
      required: true,
      message: '请输入行动代号', 
      trigger: 'blur' 
    },
    // 自定义校验规则
    {
      validator: checkTextByReg,
      message: '请输入数字编号-代号',
      trigger: 'blur'
    }
  ],
  createTime: [
    { 
      required: true,
      message: '请选择初次登记时间', 
      trigger: 'blur' 
    }
  ],
  timeFrame: [
    { 
      type: "array",
      required: true,
      message: '请选择行动时间', 
      // trigger: 'change' 
    }
  ],
  level: [
    { 
      required: true,
      message: '请选择行动级别'
    }
  ],
  equipment: [
    { 
      type: "array",
      required: true,
      message: '请选择设备需求', 
      trigger: 'change'
    }
  ],
  report:[
    { 
      required: true,
      message: '请选择汇报方式', 
      trigger: 'change'
    }
  ]
})

// 获取表单组件实例
const formPublicRef = ref<InstanceType<typeof formPublic> | null>(null)

// 点击表单提交按钮时触发
const clickToSubmit = async() => {
  try {
    // 调用子组件中的校验方法
    let pass = await formPublicRef.value?.checkForm()
    // 若校验通过 提交数据
    if (pass) {
      // 此处模拟数据的提交 将数据进行存储 以实现在点击编辑按钮时回显(实际开发过程中会将formData内数据直接提交到后端 不需要本步骤)
      submittedData = JSON.parse(JSON.stringify(formData))
      // 成功提示
      ElMessage({
        message: "sir,创建成功!",
        grouping: true,
        type: "success",
      })
      // 表单重置
      formPublicRef.value?.resetForm()
    }
  } catch (error) {
    ElMessage({
      message: "sir,请正确填写表单",
      grouping: true,
      type: "warning",
    })
  }
}

// 点击编辑时触发 (模拟编辑场景 实际开发中 回显数据通常从后端直接获取)
const clickToEdit = () => {
  if (!submittedData) {
    ElMessage({
      message: "sir,先创建行动才可以回显",
      grouping: true,
      type: "warning",
    })
    return
  }
  // 放入双向绑定容器中 以实现回显
  for (let key in formData) {
    formData[key] = submittedData[key] ?? ""
  }
}

// 点击取消按钮时触发
const clickToCancel = () => {
  ElMessage({
    message: "Yes sir!那你再想想吧",
    grouping: true,
    type: "warning",
  })
  // 表单重置
  formPublicRef.value?.resetForm()
}

// 下述内容仅为当前示例表单内所需 表单内选项的模拟数据及放入对应容器以供展示 实际开发过程中按需添加即可

// 模拟数据 级别下拉框数据
const levelOptions: levelOptionsData[] = [
  {
    levelText: "汉堡级",
    level: 0
  },
  {
    levelText: "双层汉堡级",
    level: 1
  },
  {
    levelText: "套餐级",
    level: 2
  }
]

// 模拟数据 关联项目下拉框数据
const projectOptions: projectOptionsData[] = [
  {
    projectText: "猎豹计划",
    project: "cheetah"
  },
  {
    projectText: "猫头鹰计划",
    project: "owl"
  },
  {
    projectText: "野狼计划",
    project: "wolf"
  },
  {
    projectText: "躺平计划",
    project: "lieFlat",
    // 本选项禁用
    stop: true
  }
]

// 模拟数据 设备需求多选框组数据
const equipmentOptions: equipmentOptionsData[] = [
  {
    equipmentText: "实时通讯",
    equipment: "communication"
  },
  {
    equipmentText: "位置获取支持",
    equipment: "position"
  },
  {
    equipmentText: "匿名搭档",
    equipment: "partner"
  },
  {
    equipmentText: "交通工具",
    equipment: "traffic"
  },
  {
    equipmentText: "科技与狠活",
    equipment: "highTech"
  },
  {
    equipmentText: "旋转木马",
    equipment: "carousel",
    // 本选项禁用
    repair: true
  }
]

// 模拟数据 汇报方式单选框组数据
const reportOptions: reportOptionsData[] = [
  {
    reportText: "按阶段汇报",
    report: "stage"
  },
  {
    reportText: "固定时间汇报",
    report: "regular"
  },
  {
    reportText: "不汇报",
    report: "noReport",
    // 本选项禁用
    cannot: true
  }
]

// 将下拉框/多选框组/单选框组 数据放入表单配置项的对应位置
pushListToFormSet(levelOptions, "level")
pushListToFormSet(projectOptions, "project")
pushListToFormSet(equipmentOptions, "equipment")
pushListToFormSet(reportOptions, "report")
  

// 为模拟数据提交效果 定义一个存储提交数据的容器(实际开发过程中数据会直接提交到后端 不需要本容器)
let submittedData: FormData | null = null

</script>

<style lang="scss" scoped>
.form-page {
  // 外层容器 宽度固定
  width: 800px;
  padding: 20px;
  // 去除部分浏览器中可能出现的按钮边框(不需要可以去掉)
  button:focus {
    outline: 0;
  }
  // 按钮行样式设置
  .button-row {
    margin-top: 20px;
    text-align: center;
  }
}
</style>

ps:此处为了方便查看,直接将配置项相关的变量都定义在了当前文件中,实际开发中考虑到代码的美观,也可以在单独文件内定义配置项,在当前文件中引入。

完成了以上三步,相信你已经可以在本地看到示例图中表单的展示效果了,可以尝试点击创建行动,再点击编辑,看看效果吧。

从此告别重复的表单构建工作,面对不同内容的表单,我们只需要修改配置项。组件在手,天下你有!

  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MakerZz

创作不易,给点鼓励,万分感激!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值