当前位置:   article > 正文

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

close-on-click-modal

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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
使用例子
<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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
参数说明
组件参数
参数名参数说明类型是否必传默认
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

效果图

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

组件修改原则

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/115434
推荐阅读
相关标签
  

闽ICP备14008679号