Skip to content

Form 表单

TIP

  1. ElPlusForm 完全支持官方 ElementPlus Form 的所有属性,如果属性不生效,请将属性包在 formAttrs: { xxx: 'xxx' } 中
  2. 由于公共网络,文档中未配置 upload 相关属性,所以表单中的上传组件并不能真正上传,请悉知!

基础用法

多样性表单 - 新增商品

表单实时数据

选项数据 (为了简化代码,我将选项数据注册进了 globalData 中)
ts
// @/data/index.ts

/**
 * 商品类型
 */
export const goodsTypeList = [
  {
    label: '食品',
    value: 1,
    children: [
      { label: '副食', value: 11 },
      { label: '生鲜', value: 12 },
      { label: '酒水', value: 13 }
    ]
  },
  {
    label: '数码',
    value: 2,
    children: [
      { label: '电脑', value: 21 },
      { label: '手机', value: 22 },
      { label: '家电', value: 23 }
    ]
  }
]

/**
 * 商品税率
 */
export const taxRateOptions = [
  { label: '0%', value: 0 },
  { label: '5%', value: 0.05 },
  { label: '10%', value: 0.1 }
]

/**
 * 保质期
 */
export const expirationOptions = [
  { label: '1个月', value: 30 },
  { label: '6个月', value: 180 },
  { label: '一年', value: 360 }
]

/**
 * 上架平台
 */
export const onLineOptions = [
  { label: '淘宝', value: 1 },
  { label: '京东', value: 2 },
  { label: '拼多多', value: 3 },
  { label: '唯品会', value: 4 },
  { label: '抖音', value: 5 }
]

/**
 * 优惠类型
 */
export const sellTypeOptions = [
  { label: '折扣', value: 1 },
  { label: '团购', value: 2 },
  { label: '原价', value: 3 }
]
// @/data/index.ts

/**
 * 商品类型
 */
export const goodsTypeList = [
  {
    label: '食品',
    value: 1,
    children: [
      { label: '副食', value: 11 },
      { label: '生鲜', value: 12 },
      { label: '酒水', value: 13 }
    ]
  },
  {
    label: '数码',
    value: 2,
    children: [
      { label: '电脑', value: 21 },
      { label: '手机', value: 22 },
      { label: '家电', value: 23 }
    ]
  }
]

/**
 * 商品税率
 */
export const taxRateOptions = [
  { label: '0%', value: 0 },
  { label: '5%', value: 0.05 },
  { label: '10%', value: 0.1 }
]

/**
 * 保质期
 */
export const expirationOptions = [
  { label: '1个月', value: 30 },
  { label: '6个月', value: 180 },
  { label: '一年', value: 360 }
]

/**
 * 上架平台
 */
export const onLineOptions = [
  { label: '淘宝', value: 1 },
  { label: '京东', value: 2 },
  { label: '拼多多', value: 3 },
  { label: '唯品会', value: 4 },
  { label: '抖音', value: 5 }
]

/**
 * 优惠类型
 */
export const sellTypeOptions = [
  { label: '折扣', value: 1 },
  { label: '团购', value: 2 },
  { label: '原价', value: 3 }
]
表单代码 (这里的 options 直接使用字符串特性)
vue
<ElPlusForm v-model="formData" v-bind="formConfig" />

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

let formData = reactive({} as any)
const formConfig = ref({
  column: 2,
  formDesc: {
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true, colspan: 2 },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    type: { type: 'cascader', label: '商品类型', options: 'goodsTypeList', required: true },
    stock: { type: 'number', label: '商品库存', required: true, tip: '件' },
    taxRate: { type: 'select', label: '税率', default: 0.05, required: true, options: 'taxRateOptions' },
    expiration: { type: 'select', label: '保质期', required: true, options: 'expirationOptions' },
    enabled: { type: 'switch', label: '启用状态', default: 1 },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 },
    sellType: { type: 'radio', label: '折扣类型', required: true, options: 'sellTypeOptions', colspan: 2 },
    storeTime: { type: 'date', label: '入库时间' },
    activeTime: { type: 'daterange', label: '活动时间' },
    imageList: { type: 'upload', label: '商品图片', limit: 9, required: true, multiple: true, colspan: 2 },
    fileList: { type: 'upload', upType: 'file', label: '商品附件', multiple: true, colspan: 2 },
    remark: { type: 'textarea', label: '备注', colspan: 2 }
  } as IFormDesc
} as IFormConfig)
</script>
<ElPlusForm v-model="formData" v-bind="formConfig" />

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

let formData = reactive({} as any)
const formConfig = ref({
  column: 2,
  formDesc: {
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true, colspan: 2 },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    type: { type: 'cascader', label: '商品类型', options: 'goodsTypeList', required: true },
    stock: { type: 'number', label: '商品库存', required: true, tip: '件' },
    taxRate: { type: 'select', label: '税率', default: 0.05, required: true, options: 'taxRateOptions' },
    expiration: { type: 'select', label: '保质期', required: true, options: 'expirationOptions' },
    enabled: { type: 'switch', label: '启用状态', default: 1 },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 },
    sellType: { type: 'radio', label: '折扣类型', required: true, options: 'sellTypeOptions', colspan: 2 },
    storeTime: { type: 'date', label: '入库时间' },
    activeTime: { type: 'daterange', label: '活动时间' },
    imageList: { type: 'upload', label: '商品图片', limit: 9, required: true, multiple: true, colspan: 2 },
    fileList: { type: 'upload', upType: 'file', label: '商品附件', multiple: true, colspan: 2 },
    remark: { type: 'textarea', label: '备注', colspan: 2 }
  } as IFormDesc
} as IFormConfig)
</script>

动态排版

可以通过设置 响应式 column 来动态控制表单的排版。设置子组件的 colspan 属性,可以控制该子组件的占位栅格宽度(最小 = 默认 = 1,最大 = 表单的 column 值,超出后按 column 计算)。

代码
vue

<el-slider class="demo" v-model="sliderColumn" :min="1" :max="4" />
<ElPlusForm v-model="formData" v-bind="formConfig" />

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

const sliderColumn = ref(2) 
let formData = reactive({} as any)
const formConfig = ref({
  column: sliderColumn, 
  formDesc: {
    test: { type: 'input', label: '我占10列', colspan: 10 }, 
    test2: { type: 'input', label: '我占2列', colspan: 2 },
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true, colspan: 2 },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    type: { type: 'cascader', label: '商品类型', options: 'goodsTypeList', required: true },
    stock: { type: 'number', label: '商品库存', required: true, tip: '件' },
    taxRate: { type: 'select', label: '税率', default: 0.05, required: true, options: 'taxRateOptions' },
    expiration: { type: 'select', label: '保质期', required: true, options: 'expirationOptions' },
    enabled: { type: 'switch', label: '启用状态', default: 1 },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 },
    sellType: { type: 'radio', label: '折扣类型', required: true, options: 'sellTypeOptions', colspan: 2 },
    storeTime: { type: 'date', label: '入库时间' },
    activeTime: { type: 'daterange', label: '活动时间' },
    imageList: { type: 'upload', label: '商品图片', limit: 9, required: true, multiple: true, colspan: 2 },
    fileList: { type: 'upload', upType: 'file', label: '商品附件', multiple: true, colspan: 2 },
    remark: { type: 'textarea', label: '备注', colspan: 2 }
  } as IFormDesc
} as IFormConfig)
</script>

<el-slider class="demo" v-model="sliderColumn" :min="1" :max="4" />
<ElPlusForm v-model="formData" v-bind="formConfig" />

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

const sliderColumn = ref(2) 
let formData = reactive({} as any)
const formConfig = ref({
  column: sliderColumn, 
  formDesc: {
    test: { type: 'input', label: '我占10列', colspan: 10 }, 
    test2: { type: 'input', label: '我占2列', colspan: 2 },
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true, colspan: 2 },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    type: { type: 'cascader', label: '商品类型', options: 'goodsTypeList', required: true },
    stock: { type: 'number', label: '商品库存', required: true, tip: '件' },
    taxRate: { type: 'select', label: '税率', default: 0.05, required: true, options: 'taxRateOptions' },
    expiration: { type: 'select', label: '保质期', required: true, options: 'expirationOptions' },
    enabled: { type: 'switch', label: '启用状态', default: 1 },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 },
    sellType: { type: 'radio', label: '折扣类型', required: true, options: 'sellTypeOptions', colspan: 2 },
    storeTime: { type: 'date', label: '入库时间' },
    activeTime: { type: 'daterange', label: '活动时间' },
    imageList: { type: 'upload', label: '商品图片', limit: 9, required: true, multiple: true, colspan: 2 },
    fileList: { type: 'upload', upType: 'file', label: '商品附件', multiple: true, colspan: 2 },
    remark: { type: 'textarea', label: '备注', colspan: 2 }
  } as IFormDesc
} as IFormConfig)
</script>

底部按钮控制

showBtns: boolean 控制表单底部所有的按钮显示和隐藏

showSubmit: boolean / showCancel: boolean / showReset: boolean 控制底部单个按钮的显示和隐藏

submitBtnText: string / cancelBtnText: string / resetBtnText: string 控制底部单个按钮的文本

代码
vue
<ElPlusForm v-model="formData1" v-bind="formConfig" :showBtns="false" />
<ElPlusForm v-model="formData2" v-bind="formConfig" :showCancel="true" />
<ElPlusForm v-model="formData3" v-bind="formConfig" :showCancel="true" submitBtnText="确定提交" cancelBtnText="我再想想" />

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

let formData1 = reactive({} as any)
let formData2 = reactive({} as any)
let formData3 = reactive({} as any)
const formConfig = ref({
  column: 2,
  formDesc: {
    test1: { type: 'input', label: '字段1', required: true },
    test2: { type: 'input', label: '字段2', required: true },
    test3: { type: 'input', label: '字段3' },
    test4: { type: 'input', label: '字段4' },
    test5: { type: 'input', label: '字段5', required: true }
  } as IFormDesc
} as IFormConfig)
</script>
<ElPlusForm v-model="formData1" v-bind="formConfig" :showBtns="false" />
<ElPlusForm v-model="formData2" v-bind="formConfig" :showCancel="true" />
<ElPlusForm v-model="formData3" v-bind="formConfig" :showCancel="true" submitBtnText="确定提交" cancelBtnText="我再想想" />

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

let formData1 = reactive({} as any)
let formData2 = reactive({} as any)
let formData3 = reactive({} as any)
const formConfig = ref({
  column: 2,
  formDesc: {
    test1: { type: 'input', label: '字段1', required: true },
    test2: { type: 'input', label: '字段2', required: true },
    test3: { type: 'input', label: '字段3' },
    test4: { type: 'input', label: '字段4' },
    test5: { type: 'input', label: '字段5', required: true }
  } as IFormDesc
} as IFormConfig)
</script>

表单校验

表单校验完全支持官方 ElementPlus Form 的写法,即:给 form 对象传递 rules 校验规则对象,或者是子组件中单独增加 rules: {} ,官方的使用这里不再赘述。这里仅介绍作者进行的简单&常用的校验封装。

作者对绝大部分的表单组件校验规则进行了简单的封装,仅需配置 required: true 则能够实现不同组件的必填项校验,如果需要其他复杂校验规则,请参考官方文档;你可以配置 beforeValidate: (formData) => boolean 属性来对表单进行提前校验或拦截(可以打开 f12 查看控制台)。

代码
vue
<ElPlusForm v-model="validFormData" v-bind="validFormConfig" />

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

let validFormData = reactive({} as any)
const validFormConfig = ref({
  column: 2,
  beforeValidate: (formData: any) => {
    console.log('当前表单数据为:', formData)
    ElMessage.warning('表单校验已拦截,请打开控制台查看...')
    return false
  },
  formDesc: {
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true, colspan: 2 },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    type: { type: 'cascader', label: '商品类型', options: 'goodsTypeList', required: true },
    stock: { type: 'number', label: '商品库存', required: true, tip: '件' },
    taxRate: { type: 'select', label: '税率', default: 0.05, required: true, options: 'taxRateOptions' },
    expiration: { type: 'select', label: '保质期', required: true, options: 'expirationOptions' },
    enabled: { type: 'switch', label: '启用状态', default: 1, required: true },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 },
    sellType: { type: 'radio', label: '折扣类型', required: true, options: 'sellTypeOptions', colspan: 2 },
    storeTime: { type: 'date', label: '入库时间', required: true },
    activeTime: { type: 'daterange', label: '活动时间', required: true },
    imageList: { type: 'upload', label: '商品图片', limit: 9, required: true, multiple: true, colspan: 2 },
    fileList: { type: 'upload', upType: 'file', label: '商品附件', multiple: true, colspan: 2, required: true },
    remark: { type: 'textarea', label: '备注', colspan: 2, required: true }
  } as IFormDesc
} as IFormConfig)
</script>
<ElPlusForm v-model="validFormData" v-bind="validFormConfig" />

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

let validFormData = reactive({} as any)
const validFormConfig = ref({
  column: 2,
  beforeValidate: (formData: any) => {
    console.log('当前表单数据为:', formData)
    ElMessage.warning('表单校验已拦截,请打开控制台查看...')
    return false
  },
  formDesc: {
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true, colspan: 2 },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    type: { type: 'cascader', label: '商品类型', options: 'goodsTypeList', required: true },
    stock: { type: 'number', label: '商品库存', required: true, tip: '件' },
    taxRate: { type: 'select', label: '税率', default: 0.05, required: true, options: 'taxRateOptions' },
    expiration: { type: 'select', label: '保质期', required: true, options: 'expirationOptions' },
    enabled: { type: 'switch', label: '启用状态', default: 1, required: true },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 },
    sellType: { type: 'radio', label: '折扣类型', required: true, options: 'sellTypeOptions', colspan: 2 },
    storeTime: { type: 'date', label: '入库时间', required: true },
    activeTime: { type: 'daterange', label: '活动时间', required: true },
    imageList: { type: 'upload', label: '商品图片', limit: 9, required: true, multiple: true, colspan: 2 },
    fileList: { type: 'upload', upType: 'file', label: '商品附件', multiple: true, colspan: 2, required: true },
    remark: { type: 'textarea', label: '备注', colspan: 2, required: true }
  } as IFormDesc
} as IFormConfig)
</script>

表单提交

配置 beforeRequest: (formData) => { [key: string]: any } | boolean 属性来对表单提交的数据进行二次处理和拦截;配置 requestFn: (formData) => Promise 属性来进行真正的表单提交;success: (formBack: IFormBack) => void 属性来控制表单提交后的逻辑处理(可以打开 f12 查看控制台)。

代码
vue
<ElPlusForm v-model="submitFormData" v-bind="submitFormConfig" />

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

let submitFormData = reactive({} as any)
const submitFormConfig = ref({
  column: 2,
  beforeRequest: (formData: any) => {
    console.log('当前表单数据为:', formData)
    if (formData.online.indexOf(3) < 0) {
      ElMessage.warning('上架平台必须包含 拼多多,请重新选择!')
      return false
    }
    // 设置其他属性
    formData.xxx = 'xxx'
    return formData
  },
  requestFn: (postData: any) => {
    return new Promise((resolve) => {
      // 模拟数据异步提交
      setTimeout(() => {
        console.log('数据已成功保存: ', postData)
        resolve({ code: 200, msg: 'success' })
      }, 1000)
    })
  },
  success: ({ response, callBack }: IFormBack) => {
    console.log('请求结果: ', response)
    ElMessage.success('保存成功~')
    callBack()
  },
  formDesc: {
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 }
  } as IFormDesc
} as IFormConfig)
</script>
<ElPlusForm v-model="submitFormData" v-bind="submitFormConfig" />

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

let submitFormData = reactive({} as any)
const submitFormConfig = ref({
  column: 2,
  beforeRequest: (formData: any) => {
    console.log('当前表单数据为:', formData)
    if (formData.online.indexOf(3) < 0) {
      ElMessage.warning('上架平台必须包含 拼多多,请重新选择!')
      return false
    }
    // 设置其他属性
    formData.xxx = 'xxx'
    return formData
  },
  requestFn: (postData: any) => {
    return new Promise((resolve) => {
      // 模拟数据异步提交
      setTimeout(() => {
        console.log('数据已成功保存: ', postData)
        resolve({ code: 200, msg: 'success' })
      }, 1000)
    })
  },
  success: ({ response, callBack }: IFormBack) => {
    console.log('请求结果: ', response)
    ElMessage.success('保存成功~')
    callBack()
  },
  formDesc: {
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 4 }
  } as IFormDesc
} as IFormConfig)
</script>

保存 or 更新

很多时候,保存和更新后台是拆分为 2 个接口的: save / update 。它们唯一的入参区别就是:保存时没有 id/主键,但是更新时必填。

上文讲到,配置 requestFn: (formData) => Promise 属性来进行真正的表单提交;此时我们只需再配置一个 updateFn: (formData) => Promise 属性就能实现新增/更新的分离;这里有另一个属性 idKey: string 需要注意,该属性是判断新增还是更新的关键 key ,默认为 'id' ,当表单中存在该字段时,则走更新,否则就走新增。

代码
vue
<ElPlusForm v-model="submitFormData2" v-bind="submitFormConfig2" />

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

let submitFormData2 = reactive({} as any)
const submitFormConfig2 = ref({
  column: 2,
  requestFn: (postData: any) => {
    ElMessage.warning('新增!新增!新增!')
    return new Promise((resolve) => resolve(true))
  },
  updateFn: (postData: any) => {
    ElMessage.warning('更新!更新!更新!')
    return new Promise((resolve) => resolve(true))
  },
  formDesc: {
    id: { type: 'input', label: '手填id', placeholder: '手动填写id后,进行更新操作', maxlength: 30, colspan: 2 },
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 2 }
  } as IFormDesc
} as IFormConfig)
</script>
<ElPlusForm v-model="submitFormData2" v-bind="submitFormConfig2" />

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

let submitFormData2 = reactive({} as any)
const submitFormConfig2 = ref({
  column: 2,
  requestFn: (postData: any) => {
    ElMessage.warning('新增!新增!新增!')
    return new Promise((resolve) => resolve(true))
  },
  updateFn: (postData: any) => {
    ElMessage.warning('更新!更新!更新!')
    return new Promise((resolve) => resolve(true))
  },
  formDesc: {
    id: { type: 'input', label: '手填id', placeholder: '手动填写id后,进行更新操作', maxlength: 30, colspan: 2 },
    name: { type: 'input', label: '商品名称', maxlength: 30, required: true },
    price: { type: 'number', label: '商品价格', required: true, precision: 2, tip: '元' },
    online: { type: 'checkbox', label: '上架平台', required: true, options: 'onLineOptions', colspan: 2 }
  } as IFormDesc
} as IFormConfig)
</script>

TIP

对于新增 or 更新,你当然可以在 requestFn 一个函数中通过逻辑判断完成,这里之所以分开,是因为作者将 API 进行了如下封装:

ts
// @/api/xxx.ts

/**
 * 保存
 * @returns
 */
export const saveInfo = (data: RequestParams) => {
  return https<any>({ url: 'xxxx/save', method: Method.POST, data })
}
/**
 * 保存
 * @returns
 */
export const updateInfo = (data: RequestParams) => {
  return https<any>({ url: 'xxxx/update', method: Method.POST, data })
}
// @/api/xxx.ts

/**
 * 保存
 * @returns
 */
export const saveInfo = (data: RequestParams) => {
  return https<any>({ url: 'xxxx/save', method: Method.POST, data })
}
/**
 * 保存
 * @returns
 */
export const updateInfo = (data: RequestParams) => {
  return https<any>({ url: 'xxxx/update', method: Method.POST, data })
}

使用时,引入两个 api 后直接配置进去就完事儿了

ts
import { saveInfo, updateInfo } from '@/api/xxx'

// ...
const submitFormConfig2 = ref({
  column: 2,
  requestFn: saveInfo,
  updateFn: updateInfo,
  formDesc: {
    // ...
  }
})
import { saveInfo, updateInfo } from '@/api/xxx'

// ...
const submitFormConfig2 = ref({
  column: 2,
  requestFn: saveInfo,
  updateFn: updateInfo,
  formDesc: {
    // ...
  }
})

是不是很简单?