Vue2+element动态弹框表单组件--配置化--低代码(公司内部统一化)

Vue2弹框-动态表单渲染

开头废话

该动态表单弹框组件是公司UI规范和目前涉及到的场景进行编写的,也是为了减少样式和代码量。当我写这篇文章的时候其实已经能完成大部分需求了。在此也只是记录以下,下面整体说明在公司文档里也有记录的,以方便后人维护使用。

代码实现

FormDialog.vue代码如下

<template>
  <el-dialog
    :close-on-click-modal="closeOnClickModal"
    :title="title"
    :visible.sync="dialogVisible"
    :before-close="handleClose"
    :width="width || dialogWidth"
    custom-class="center-dialog"
  >
    <el-form
      ref="form"
      :model="form"
      :rules="rules"
      :label-width="labelWidth"
      label-position="left"
      class="form-cnt"
      :class="formClass"
    >
      <!-- 预留插槽 -->
      <slot name="first-form-item"></slot>
      <!-- 渲染表单 -->
      <template v-for="(formItem, index) in formList">
        <el-form-item
          :key="formItem.bindVal"
          :label="formItem.label"
          :prop="formItem.bindVal"
          :required="formItem.required"
          :class="[
            {'upload-form-item': formItem.eleTag === 'el-upload'},
            {'fill-width': formItem.fillWidth}
          ]"
        >
          <!-- 自定义label -->
          <template v-if="formItem.customFormLabel" slot="label">
            <slot :name="`${formItem.bindVal}-label`"></slot>
          </template>
          <!-- label超长省略时使用-能不能自动判断呢?? -->
          <template v-else-if="formItem.labelTooltip" slot="label">
            <el-tooltip
              effect="dark"
              placement="top"
              popper-class="atooltip"
              :content="formItem.label"
            >
              <span>{{ formItem.label }}</span>
            </el-tooltip>
          </template>
          <!-- 上传组件 -->
          <div v-if="formItem.eleTag === 'el-upload'" class="upload-area">
            <!-- 上传图片-卡片式 -->
            <template v-if="!formItem.uploadOptions || formItem.uploadOptions.type === 'image'">
              <upload-img v-model="form[formItem.bindVal]" class="upload" />
              <div
                v-if="formItem.uploadOptions && formItem.uploadOptions.tips"
                class="upload-tips"
              >{{ formItem.uploadOptions.tips }}</div>
            </template>
            <!-- 上传文件列表 -->
            <template v-else>
              <upload
                v-bind="formItem.uploadOptions"
                @update="uploadUpdate($event, formItem.bindVal)"
              />
            </template>
          </div>
          <!-- 富文本 -->
          <div v-else-if="formItem.eleTag === 'wang-edit'" id="editor" class="form-editor" />
          <!-- 正常表单项 -->
          <component
            v-else
            :is="formItem.eleTag"
            v-model="form[formItem.bindVal]"
            v-bind="formItem.propsOptions"
          >
            <template v-if="formItem.eleChildTag">
              <component
                v-for="optionItem in formItem.childOptions"
                :key="optionItem.value"
                :is="formItem.eleChildTag"
                :label="optionItem.label"
                :value="optionItem.value"
                :name="optionItem.name"
                @change="$emit(`${formItem.bindVal}Change`, $event)"
              >
                <span v-if="optionItem.customOptionLabel">{{ optionItem.customOptionLabel }}</span>
              </component>
            </template>
          </component>
        </el-form-item>
        <!-- 预留任意位置插入的插槽 -->
        <slot :name="('form-item-' + index)" />
      </template>
    </el-form>
    <!-- 弹框底部 -->
    <div slot="footer">
      <el-button class="common_white_btn" @click="handleClose">取 消</el-button>
      <el-button class="common_red_btn" type="primary" @click="handleConfirm">确 定</el-button>
    </div>
  </el-dialog>
</template>

<script>
import E from 'wangeditor'

export default {
  name: 'FormDialog',
  components: {
    UploadImg: () => import('@/components/upload-img'),
    Upload: () => import('@/components/upload')
  },
  props: {
    dialogVisible: {
      type: Boolean,
      required: true
    },
    // 弹框标题
    title: {
      type: String,
      required: true
    },
    // 是否可以通过点击 modal 关闭 Dialog
    closeOnClickModal: {
      type: Boolean,
      required: true
    },
    /*
    *表单容器类名,决定单列还是多列
    *取值:
    *  block-cnt单列布局
    *  flex-cnt两列布局
    */
    formClass: {
      type: String,
      default: 'block-cnt',
      validator(value) {
        return ['block-cnt', 'flex-cnt'].includes(value)
      }
    },
    // 标签宽度
    labelWidth: {
      type: String,
      default: '80px'
    },
    // 表单json数组
    formList: {
      type: Array,
      required: true
    },
    // 表单props对象
    form: {
      type: Object,
      required: true
    },
    // 表单校验规则
    rules: {
      type: Object,
      default: () => {}
    },
    // 自定义弹框宽度
    width: {
      type: String,
      default: ''
    },
    // 是否初始化富文本
    isInitEditor: {
      type: Boolean,
      default: false
    },
    // 富文本对应表单字段名
    editorPropName: {
      type: String,
      default: 'details'
    }
  },
  data () {
    return {
      // formData: {},
      editor: null
    }
  },
  computed: {
    // 454px为单行时的宽度,704px为多行
    dialogWidth() {
      let width = '704px'
      const labelWidthNum = parseInt(this.labelWidth, 10)
      if (this.formClass === 'block-cnt') {
        width = '454px'
      }
      if (labelWidthNum > 80) {
        width = `${(labelWidthNum - 80) * 2 + 704}px`
      }
      return width
    }
  },
  // watch: {
  //   form(newVal) {
  //     this.formData = JSON.parse(JSON.stringify(newVal))
  //   }
  // },
  mounted() {
    this.$nextTick(() => {
      if (this.isInitEditor && document.getElementById('editor')) this.initEditor()
    })
  },
  beforeDestroy() {
    if (!this.editor) return
    this.editor.destroy()
    this.editor = null
  },
  methods: {
    handleClose() {
      this.$refs['form'].clearValidate()
      this.$emit('close')
    },
    // 确认
    handleConfirm() {
      this.$refs['form'].validate((valid) => {
        if (valid) {
          // 校验成功
          if (this.editor) this.form[this.editorPropName] = this.editor.txt.html()
          this.$emit('success', this.form)
          this.$refs['form'].clearValidate()
        }
      })
    },
    uploadUpdate({ fileList }, prop) {
      let res = fileList.map((file) => ({
        fileName: file.name,
        url: file.response?.data || 'No response'
      }))
      this.form[prop] = res
    },
    // 初始化富文本
    initEditor() {
      this.editor = new E('#editor')
      this.editor.config.menus = [
        'image',
        'table',
        'fontSize',
        'foreColor',
        'bold',
        'italic',
        'underline'
      ]
      this.editor.config.height = 150
      this.editor.config.placeholder = '请输入产品详情'
      this.editor.config.showFullScreen = true
      this.editor.config.showLinkImg = false
      this.editor.config.customUploadImg = (resultFiles, insertImgFn) => {
        let editorFormData = new FormData()
        editorFormData.append('file', resultFiles[0])
        this.$api.uploadPublic(editorFormData)
          .then(({ data }) => {
            insertImgFn(data)
          })
      }
      this.editor.config.uploadImgMaxSize = 5 * 1024 * 1024
      this.editor.config.uploadImgAccept = ['jpg', 'jpeg', 'png']
      this.editor.config.uploadImgMaxLength = 5
      this.editor.create()
      if (this.form[this.editorPropName]) this.editor.txt.html(this.form[this.editorPropName])
    }
  }
}
</script>

<style lang="scss" scoped>
  @mixin innerWidth($width) {
    /deep/.el-input__inner,
    /deep/.el-textarea__inner,
    /deep/.el-date-editor.el-input,
    /deep/.el-date-editor.el-input__inner {
      width: $width;
    }
  }

  .flex-cnt {
    @include innerWidth(240px);
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;

    .el-checkbox-group,
    .el-radio-group {
      min-width: 240px;
      max-width: 240px;
    }

    .upload-form-item {
      width: 100%;
    }
  }

  .block-cnt {
    @include innerWidth(330px);

    .el-checkbox-group,
    .el-radio-group {
      min-width: 330px;
      max-width: 330px;
    }
  }

  .fill-width {
    @include innerWidth(100%);
    width: 100%;
  }

  .el-dialog__wrapper.el-dialog__wrapper {
    display: flex;
    margin: 20px 0;
  }

  /deep/.center-dialog {
    margin: auto !important;
  }

  .form-cnt {
    .el-form-item {
      font-size: 12px;
    }

    /deep/.el-input__inner {
      height: 32px;
    }

    // 普通输入框
    /deep/.el-input__inner,
    /deep/.el-textarea__inner {
      font-size: 12px;
      border: 1px solid #d7dbe8;
      &::placeholder {
        color: #c5cad5;
      }
    }

    /deep/.el-textarea .el-input__count {
      bottom: 0;
      background: transparent;
    }

    /deep/.el-input__count-inner {
      color: #c5cad5;
    }

    /deep/.el-form-item__label,
    /deep/.el-radio__label {
      font-size: 12px;
      color: #323e58;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .upload-tips {
      margin-top: 8px;
      font-size: 12px;
      color: #85889c;
      line-height: 17px;
    }
  }

  .form-editor {
    position: relative;
    width: 100%;
    z-index: 0;

    /deep/.w-e-toolbar {
      border-radius: 4px 4px 0 0;
    }

    /deep/.w-e-text-container {
      min-height: 150px;
      height: 100% !important;
      border-radius: 0 0 4px 4px;
    }

    /deep/.w-e-text {
      max-height: 450px;
    }

    /deep/.placeholder {
      color: #c5cad5;
      font-size: 12px;
    }
    /deep/ .w-e-toolbar p,
    /deep/ .w-e-text-container p,
    /deep/ .w-e-menu-panel p {
      font-size: 12px !important;
      color: #323e58;
    }
  }
</style>

使用例子
<template>
    <!-- 测试弹框 -->
    <FormDialog
      v-if="showEditDialog"
      :dialogVisible="showEditDialog"
      title="编辑合伙人"
      form-class="flex-cnt"
      :form="curEditItem"
      :form-list="formList"
      :rules="rules"
      @success="handleUpdate"
      @close="showEditDialog = false"
    />
</template>

<script>
import FormDialog from '@/components/layoutComponet/FormDialog'
export default {
  components: {
      FormDialog
  },
 data () {
    return {
      formList: [
        {
          label: '姓名',
          eleTag: 'el-input',
          bindVal: 'name',
          propsOptions: {
            placeholder: '请输入姓名'
          }
        },
        {
          label: '花名',
          eleTag: 'el-input',
          bindVal: 'stageName',
          propsOptions: {
            placeholder: '请输入花名'
          }
        },
        {
          label: '性别',
          eleTag: 'el-radio-group',
          bindVal: 'sex',
          eleChildTag: 'el-radio',
          childOptions: [
            { label: '男' },
            { label: '女' }
          ]
        },
        {
          label: '手机号码',
          eleTag: 'el-input',
          bindVal: 'phone',
          propsOptions: {
            placeholder: '请输入手机号码',
            disabled: true
          }
        },
        {
          label: '工号',
          eleTag: 'el-input',
          bindVal: 'employNum',
          propsOptions: {
            placeholder: '请输入工号'
          }
        },
        {
          label: '所属部门',
          eleTag: 'el-select',
          bindVal: 'department',
          eleChildTag: 'el-option',
          propsOptions: {
            placeholder: '请选择所属部门'
          },
          childOptions: []
        },
        {
          label: '职位',
          eleTag: 'el-input',
          bindVal: 'position',
          propsOptions: {
            placeholder: '请输入职位'
          }
        }
      ],
      rules: {
        name: { required: true, message: '请输入姓名', trigger: 'blur' },
        department: { required: true, message: '请选择所属部门', trigger: 'change' }
      },
    }
  }
}
</script>
参数说明
组件参数
参数名参数说明类型是否必传默认
dialogVisible是否显示弹框Booleantrue
title弹框标题Stringtrue
width弹框的宽度Stringfalse为’'空字符串,需要自定义弹框宽度时可传
formClassblock-cnt单行布局 flex-cnt两行布局Stringfalse默认值block-cnt,可选flex-cnt
labelWidth最好不要超过100pxStringfalse80px
isInitEditor是否初始化富文本,使用弹框默认富文本,如果功能不满足请使用slot自定义,自定义时最好不要传该属性Booleanfalsefalse
editorPropName富文本对应表单字段名Stringfalse‘details’
formList需要渲染的表单json数组,详细参数说明见2-2Arraytrue
form表单对象Objecttrue
rules表单校验规则Objectfalse{}
formList参数说明
类名参数说明类型是否必传例子说明
labelel-form-item的label名称Stringtrue
eleTag需要渲染的表单元素名,eg: el-input/el-selectStringtrue
bindVal绑定的props值,必须对应form对象的某个属性Stringtrue
eleChildTag需要渲染的表单元素的子元素名,eg: el-select的子元素为el-optionStringfalse
fillWidth是否占满一行Booleanfalse
labelTooltiplabel超长省略的时候通过tooltip提示Booleanfalse能不能自动判断然后添加el-tooltip???
customFormLabel允许自定义label,用于标签过长的时候省略之后悬浮显示label的全称Booleanfalse
propsOptionsElement UI 表单元素的的可接受属性Objectfalse{ type: ‘textarea’, placeholder: ‘请输入文本’ }
childOptions渲染表单元素的子元素的项数组,options:[{value|name: ‘’, label: ‘’, customOptionLabel: ‘’}],value|name按表单组件传就行, customOptionLabel为可选项(自定义label)Arrayfalse具体的options查看element对应组件
uploadOptions当eleTag为el-upload时可选项,当el-upload不是上传y图片时可选对象参数见’@/components/upload.vue’组件 (样式自己改.jpg) 可以换掉这个上传文件的,毕竟是某人写的Object || 不传任何值false默认为undefined(uploadOptions, uploadOptions.type为image时同理),为图片上传组件(做上层兼容)
预留插槽

项目留有多个具名插槽
first-form-item :第一项el-form-item的位置
form-item-${index} : index为每一项表单元素后都留有一个具名插槽,可在表单之间随意插入
${bindVal}-label : 各个表单项可完全自定义label,需要和customFormLabel配合使用

其他

现已支持element UI 所有表单的可接收属性,所有属性通过propsOptions对象接收
:is="component"动态组件有性能问题,如果有需要可以加keep-alive

效果图

只有一开始设计时保留的图片,后面做了一些弹框整体的样式修改修改新的到时候再截图补充;但是弹框布局就只有下面两种,是定制化的,截图时涉及到隐私信息已经截取掉了,只保留了实现结果
在这里插入图片描述
在这里插入图片描述

组件修改原则

如果要修改记得考虑向下兼容以前的使用,非必要不更改。这是我在公司自己思考封装的一个很小小小小的想法

  • 26
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值