Vue3 Element-Plus 一站式生成动态表单

背景

经常开发管理系统的小伙伴们肯定或多或少都遇到过表单需求,对于一个系统而言,动辄就是十几,几十个表单;如果每个表单都按照传统模式编写的话,简直要把前端累死,看着一段段大同小异的代码,也是提不上一点劲,甚至看着这些它懂你,你不想懂它的代码就犯恶心。

本着偷懒的精神,我就想能否封装一个动态表单,实现思路大致就是通过JSON配置,动态生成表单页面,于是说干就干,咱玩的就是真实对吧。开撸,开撸.....

项目地址:github地址

数据接口设计

废话不多说,代码敬上

咋眼一看,代码有点多哈,别着急,注释已安排上。

js 复制代码
type TreeItem = {
  value: string
  label: string
  children?: TreeItem[]
}

export type FormListItem = {
  // 栅格占据的列数
  colSpan?: number 
  placeholder?: string
  // 表单元素特有的属性
  props?: {  
    defaultValue?: unknown // 绑定的默认值
    clearable?: boolean
    disabled?: boolean | ((data: { [key: string]: any }) => boolean)
    size?: 'large' | 'default' | 'small'
    group?: unknown // 父级特有属性,针对嵌套组件 Select、Checkbox、Radio
    child?: unknown // 子级特有属性,针对嵌套组件 Select、Checkbox、Radio
    [key: string]: unknown
  } 
  // 表单元素特有的插槽
  slots?: {  
    name: string
    content: unknown
  }[] 
  // 组件类型
  typeName?: 'input' | 'select' | 'date-picker' | 'time-picker' | 'switch' | 'checkbox' | 'checkbox-group' | 'checkbox-button' | 'radio-group' | 'radio-button' | 'input-number' | 'tree-select' | 'upload' | 'slider' 
  // 表单元素特有的样式
  styles?: {
    [key: string]: number | string
  } 
  // select options 替换字段
  replaceField?: { value: string; label: string } 
  // 列表项
  options?: {
    value?: string | number | boolean | object
    label?: string | number
    disabled?: ((data: { [key: string]: any }) => boolean) | boolean
    [key: string]: unknown
  }[]
  // <el-form-item> 独有属性,同 FormItem Attributes
  formItem: Partial<FormItemProps & { class: string }>
  // 嵌套<el-form-item>
  children?: FormListItem[]
  // 树形选择器数据
  treeData?: TreeItem[] // 只针对 'tree-select'组件
  // 组件显示条件
  isShow?: ((data: { [key: string]: any }) => boolean) | boolean
}

export type FConfig = {
  form: Partial<InstanceType<typeof ElForm>> // Form Attributes 与Element属性一致
  configs: FormListItem[]  // 表单主体配置
}

常见表单需求

  • 如何控制某个组件的显示隐藏

实现思路,提供一个isShow方法,将方法绑定在对应的组件上,从而组件显示隐藏条件

js 复制代码
isShow: (data = {}) => {
  return model.value.region == 'shanghai'
}
....
<el-form-item v-if="isShow(model)" v-bind="item.formItem">
  • 目标组件是否禁用,需要根据某个组件是否有值来判断
js 复制代码
disabled: (data = {}) => {
    return !model.value.date1
}
....
<component :disabled="disabled(model)"></component>
  • 组件之间相互赋值,A组件的值赋值给B组件B组件的值赋值给 A组件
  • 表单验证
js 复制代码
formItem: {
  prop: 'name',
  label: 'Activity name',
  rules: [
    {
      required: true,
      message: 'Please enter content',
      trigger: 'blur'
    }
  ]
}

组件封装

1. 输入框组件

js 复制代码
<template>
  <el-input v-bind="attrs.props"
            ref="elInputRef"
            :style="attrs.styles">
    <template v-for="item in attrs.slots"
              #[item.name]
              :key="item.name">
      <component :is="item.content"></component>
    </template>
  </el-input>
</template>

2. 下拉选择器组件

js 复制代码
<template>
  <el-select v-bind="attrs.props?.group"
             ref="elSelectRef"
             :style="attrs.styles">
    <el-option v-for="item in attrs.options"
               v-bind="attrs.props?.child"
               :key="item[attrs.replaceField?.value || 'value']"
               :label="item[attrs.replaceField?.label || 'label']"
               :value="item[attrs.replaceField?.value || 'value']"
               :disabled="item.disabled"></el-option>
  </el-select>
</template>

3. 日期选择器组件

js 复制代码
<template>
  <el-date-picker v-bind="attrs.props"
                  ref="elDatePickerRef"
                  :style="attrs.styles"></el-date-picker>
</template>

封装方法都一致,还有很多组件,这里就不一个个列出来,具体大家就移步源码查看哈

项目路径 src/components/Form

附上完整配置

js 复制代码
const config = ref<FConfig>({
    form: {
      labelWidth: '140px'
    },
    configs: [
      // 输入框
      {
        colSpan: 12,
        typeName: 'input',
        placeholder: 'Please enter content',
        props: {
          defaultValue: '',
          clearable: true
        },
        slots: [
          {
            name: 'suffix',
            content: () => (
              <ElIcon class="el-input__icon">
                <Search />
              </ElIcon>
            )
          }
        ],
        formItem: {
          prop: 'name',
          label: 'Activity name',
          rules: [
            {
              required: true,
              message: 'Please enter content',
              trigger: 'blur'
            }
          ]
        }
      },
      // 选择器
      {
        colSpan: 12,
        typeName: 'select',
        placeholder: 'Please select content',
        props: {
          defaultValue: undefined,
          group: {
            clearable: true,
            onChange: events.changeSelect
          },
          child: {}
        },
        replaceField: { value: 'key', label: 'title' },
        options: [
          { key: 'shanghai', title: 'Zone one' },
          { key: 'beijing', title: 'Zone two' }
        ],
        styles: {
          width: '100%'
        },
        formItem: {
          prop: 'region',
          label: 'Activity zone',
          rules: [
            {
              required: true,
              message: 'Please select Activity zone',
              trigger: 'change'
            }
          ]
        }
      },
      {
        colSpan: 24,
        formItem: {
          required: true,
          label: 'Activity time'
        },
        children: [
          // 日期选择器
          {
            colSpan: 12,
            typeName: 'date-picker',
            placeholder: 'Pick a day',
            props: {
              type: 'datetime',
              clearable: true,
              valueFormat: 'YYYY-MM-DD HH:mm:ss'
            },
            styles: { width: '100%' },
            formItem: {
              prop: 'date1',
              rules: [
                {
                  type: 'date',
                  required: true,
                  message: 'Please pick a date',
                  trigger: 'change'
                }
              ]
            }
          },
          // 时间选择器
          {
            colSpan: 12,
            typeName: 'time-picker',
            placeholder: 'Pick a time',
            props: {
              disabled: (data = {}) => {
                return !model.value.date1
              },
              clearable: true
            },
            styles: { width: '100%' },
            formItem: {
              prop: 'date2',
              rules: [
                {
                  type: 'date',
                  required: true,
                  message: 'Please pick a time',
                  trigger: 'change'
                }
              ]
            }
          }
        ]
      },
      // 开关
      {
        colSpan: 24,
        typeName: 'switch',
        props: {
          defaultValue: false
        },
        formItem: {
          prop: 'delivery',
          label: 'Instant delivery'
        }
      },
      // 多选框
      {
        colSpan: 12,
        typeName: 'checkbox-group',
        props: {
          group: {},
          child: {}
        },
        formItem: {
          prop: 'type',
          label: 'Activity type',
          rules: [
            {
              type: 'array',
              required: true,
              message: 'Please select at least one activity type',
              trigger: 'change'
            }
          ]
        },
        // replaceField: { value: 'value', label: 'label' },
        options: [
          { value: 'shanghai', label: 'Zone one' },
          { value: 'beijing', label: 'Zone two' }
        ]
      },
      // 多选按钮框
      {
        colSpan: 12,
        typeName: 'checkbox-button',
        props: {
          group: {},
          child: {}
        },
        formItem: {
          prop: 'button',
          label: 'Activity button',
          rules: [
            {
              type: 'array',
              required: true,
              message: 'Please select at least one activity type',
              trigger: 'change'
            }
          ]
        },
        // replaceField: { value: 'value', label: 'label' },
        options: [
          { value: 'shanghai', label: 'Zone one' },
          { value: 'beijing', label: 'Zone two' }
        ]
      },
      // 单选框
      {
        colSpan: 12,
        typeName: 'radio-group',
        props: {},
        formItem: {
          prop: 'resource',
          label: 'Resources',
          rules: [
            {
              required: true,
              message: 'Please select activity resource',
              trigger: 'change'
            }
          ]
        },
        options: [
          { value: 'shanghai', label: 'Sponsorship' },
          { value: 'beijing', label: 'Venue' }
        ]
      },
      // 单选按钮框
      {
        colSpan: 12,
        typeName: 'radio-button',
        props: {},
        formItem: {
          prop: 'resourceButton',
          label: 'Resources button',
          rules: [
            {
              required: true,
              message: 'Please select activity resource',
              trigger: 'change'
            }
          ]
        },
        options: [
          { value: 'shanghai', label: 'Sponsorship' },
          { value: 'beijing', label: 'Venue' }
        ]
      },
      // 文本域
      {
        colSpan: 24,
        typeName: 'input',
        placeholder: 'Please enter content',
        formItem: {
          prop: 'desc',
          label: 'Activity form'
        },
        props: {
          rows: 5,
          type: 'textarea',
          clearable: true
        },
        isShow: (data = {}) => {
          return model.value.region == 'shanghai'
        }
      },
      // 文件上传
      {
        colSpan: 24,
        typeName: 'upload',
        formItem: {
          prop: 'fileName',
          label: 'Upload File',
          rules: [
            {
              required: true,
              message: 'Please select at least one activity type',
              trigger: 'change'
            }
          ]
        },
        props: {
          httpRequest: events.httpRequest
        },
        slots: [
          {
            name: 'default',
            content: () => <ElButton type="primary">上传</ElButton>
          },
          {
            name: 'tip',
            content: () => <span style="margin-left:10px">jpg/png files with a size less than 500KB</span>
          }
        ]
      },
      // 滑块
      {
        colSpan: 16,
        typeName: 'slider',
        props: {
          onChange: (val: number) => {
            model.value.number = val
          }
        },
        formItem: {
          label: 'Activity slider',
          prop: 'slider',
          rules: [
            {
              required: true,
              message: 'Please enter content',
              trigger: 'change'
            }
          ]
        }
      },
      // 数字输入框
      {
        colSpan: 8,
        typeName: 'input-number',
        formItem: {
          prop: 'number',
          label: 'Activity number'
        },
        props: {
          min: 1,
          max: 100,
          onChange: (val: number) => {
            model.value.slider = val
          }
        }
      },
      // 树形选择器
      {
        colSpan: 24,
        typeName: 'tree-select',
        placeholder: 'Please select content',
        formItem: {
          prop: 'tree',
          label: 'Activity tree'
        },
        styles: { width: '100%' },
        props: {
          multiple: true,
          showCheckbox: true
        },
        treeData: []
      }
    ]
  })

实现效果

详细的实现逻辑,就委屈大家移步到项目中查看了。

最后

文章暂时就写到这,如果本文对您有什么帮助,别忘了动动手指点个赞❤️。 本文如果有错误和不足之处,欢迎大家在评论区指出,多多提出您宝贵的意见!

最后分享项目地址:github地址

相关推荐
王哲晓8 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v13 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云23 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落3 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
麦麦大数据3 小时前
基于vue+neo4j 的中药方剂知识图谱可视化系统
vue.js·知识图谱·neo4j
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
理想不理想v3 小时前
vue经典前端面试题
前端·javascript·vue.js