my-form/src/Form.vue

<template>
  <Form class="my-form"
        ref="elForm"
        v-bind="$attrs"
        v-on="$listeners"
        @submit.native.prevent
        :model="currentModel">
        <!-- @keyup.native.enter="submit" -->
    <slot></slot>
    <FormItem label=" " v-if="footer" class="my-form__footer" :class="footerClass">
      <slot name="footer">
        <Button v-if="submitText"
                type="primary"
                @click="submit"
                :loading="prevent && submitting">{{(prevent && submitting) ? submittingText : submitText}}
        </Button>
        <Button v-if="resetText" :disabled="prevent && submitting" type="default" @click="reset">{{resetText}}</Button>
        <slot name="actions"></slot>
        <Button v-if="collapsible"
                :disabled="prevent && submitting"
                type="text"
                @click="toggleCollapsed"
                class="my-form__collapse">
         <span>
           {{currentCollapsed ? '展开': '收起'}}
           <i
             :class="currentCollapsed ? 'el-icon-arrow-down' : 'el-icon-arrow-up'"></i>
           </span>
        </Button>
      </slot>

    </FormItem>
  </Form>
</template>

<script>
  /**
   * 表单组件
   * @module $ui/components/my-form
   */
  import {Form, FormItem, Button} from 'element-ui'
  import {cloneDeep, isEqual} from '$ui/utils/util'

  // const _set = require('lodash/set')

  /**
   * 插槽
   * @member slots
   * @property {string} default 默认插槽,放置表单项
   * @property {string} actions 追加底部操作按钮
   * @property {string} footer重新定义整个底部内容,设置后,自带的提交、重置、收起/展开功能将失效
   */
  export default {
    name: 'MyForm',
    components: {
      Form,
      FormItem,
      Button
    },
    provide() {
      return {
        myForm: this
      }
    },
    /**
     * 属性参数
     * @member props
     * @property {Object} [model] 表单初始化数据模型,需要设置表单默认值或回填表单时使用
     * @property {string} [itemWidth] 表单项的宽度,默认自适应,支持任何css宽度设置
     * @property {boolean} [footer=true] 显示底部功能,设置false,自带的提交、重置、收起/展开功能将失效
     * @property {string} [footerAlign] 底部对齐方式,可选值:'left', 'right', 'center'
     * @property {String|Boolean} [submitText=提交] 提交按钮文本,false 时不显示提交按钮
     * @property {String} [submittingText=正在提交数据...] 正在提交时按钮文本, 需要设置 prevent=true 时才有效
     * @property {Function} [onSubmit] 提交表单回调函数,必须要返回Promise
     * @property {string} [resetText] 重置按钮文本,false 时不显示
     * @property {Boolean} [resetSubmitSuccess=false] 提交数据成功后,重置表单
     * @property {Boolean} [collapsible] 支持表单项展开、收起
     * @property {boolean} [collapsed=true] 初始收起表单项目,collapsible=true是才有效
     * @property {string} [collapseEffect] 展开、收起动画效果,transition 组件的 name属性
     * @property {boolean} [resetCollapsed] 收起时重置折叠的表单项
     * @property {boolean} [footerBlock] 底部另起一行显示
     * @property {boolean} [footerExpandBlock=true] 展开后底部另起一行显示,
     * @property {boolean} [footerFloat=false] 底部浮动右边,常在做筛选条件是使用
     * @property {Function} [loader] 加载数据函数, 必须返回Promise, 注意:如定义loader,所有表单项目都会回调,函数需要自己实现处理逻辑
     * @property {Object} [dictMap] 字典数据集合, 格式:{字典类型编码:字典列表}
     * @property {boolean} [prevent=true] 防止重复提交
     */

    /**
     * 继承 el-form事件, 任一表单项被校验后触发
     * @event validate
     * @param {*} value 被校验的表单项 prop 值,校验是否通过,错误消息(如果存在)
     */

    props: {
      // 表单模型数据
      model: Object,
      // 表单项的宽度,默认自适应
      itemWidth: String,
      // 显示底部
      footer: {
        type: Boolean,
        default: true
      },
      // 底部对齐方式
      footerAlign: {
        type: String,
        validator(val) {
          return ['', 'left', 'right', 'center'].includes(val)
        }
      },
      // 提交按钮文本,false 时不显示
      submitText: {
        type: [String, Boolean],
        default: '提交'
      },
      // 正在提交时按钮文本
      submittingText: {
        type: String,
        default: '正在提交数据...'
      },
      // 提交表单时回调函数,必须要返回Promise
      onSubmit: {
        type: Function
      },
      // 重置按钮文本,false 时不显示
      resetText: {
        type: [String, Boolean],
        default: '重置'
      },
      // 提交数据成功后,重置表单
      resetSubmitSuccess: Boolean,

      // 可折叠
      collapsible: Boolean,

      // 默认收起
      collapsed: {
        type: Boolean,
        default: true
      },
      // 折叠动画效果,transition 组件的 name属性
      collapseEffect: String,

      // 收起时重置表单项
      resetCollapsed: Boolean,

      // 底部另起一行显示
      footerBlock: Boolean,

      // 展开后底部另起一行显示
      footerExpandBlock: {
        type: Boolean,
        default: true
      },
      // 底部浮动右边,常在做筛选条件是使用
      footerFloat: Boolean,
      // 加载数据函数, 必须返回Promise, 注意:如在表单定义loader,所有表单项目都会回调,函数需要自己实现处理逻辑
      loader: Function,

      // 字典数据集合
      dictMap: Object,

      // 防止重复提交
      prevent: {
        type: Boolean,
        default: true
      }
    },
    data() {
      // 子表单项实例集合
      this.items = {}
      return {
        currentModel: {},
        // 正在提交
        submitting: false,
        // 当前的折叠状态
        currentCollapsed: true
      }
    },
    computed: {
      footerClass() {
        return {
          [`is-align-${this.footerAlign}`]: !!this.footerAlign,
          'is-block': this.footerBlock || (!this.currentCollapsed && this.footerExpandBlock),
          'is-float-right': this.footerFloat
        }
      }
    },
    watch: {
      model: {
        immediate: true,
        deep: true,
        handler(val) {
          // 如果两者相等,不修改,避免死循环
          if (!isEqual(val, this.currentModel)) {
            this.currentModel = cloneDeep(val || {})
          }
        }
      },
      collapsed: {
        immediate: true,
        handler(val) {
          this.currentCollapsed = val
        }
      },
      currentModel: {
        deep: true,
        handler(val, old) {
          if (isEqual(val, old)) return
          /**
           * 表单值改变时触发
           * @event change
           * @param {Object} model 表单模型数据
           */
          this.triggerChange(val)
        }
      },
      dictMap(val) {
        if (!val) return
        Object.keys(this.items).forEach(name => {
          const vm = this.items[name]
          const {dict, loader, options} = vm
          // 如果子组件没有设置 options 和 loader, 但有设置字典名称,用表单的字典数据填充到子组件
          if (!options && !loader && dict) {
            vm.currentOptions = val[dict] || []
          }

        })
      }
    },
    methods: {
      /**
       * 提交表单
       * @method submit
       */
      submit() {
        return new Promise((resolve, reject) => { 
          this.$refs.elForm.validate((valid, object) => {
            if (valid) {
              /**
               *  表单验证通过
               *  @event validate-success
               */
              this.$emit('validate-success')
              const model = cloneDeep(this.currentModel)
              if (this.onSubmit) {
                this.submitting = true
                this.onSubmit(model, this)
                  .then(() => {
                    // 提交表单成功后,重置
                    this.resetSubmitSuccess && this.reset()
                  })
                  .finally(() => {
                    this.submitting = false
                  })
              }
              /**
               * 表单提交时触发
               * @event submit
               * @param {object} model 表单模型数据
               * @param {VueComponent} vm 表单实例
               */
              this.$emit('submit', model, this)
              resolve(model)
            } else {
              /**
               *  表单验证不通过
               *  @event validate-fail
               *  @param {object} object 未通过校验的字段
               */
              this.$emit('validate-fail', object)
              reject(object)
            } 
          })
        })
      },
      /**
       * 重置表单
       * @method reset
       */
      reset() {
        this.currentModel = cloneDeep(this.getDefaultValue())
        this.$nextTick(() => {
          this.$refs.elForm && this.$refs.elForm.clearValidate()
          /**
           * 重置表单时触发
           * @event reset
           * @param {VueComponent} vm 表单实例
           */
          this.$emit('reset', this)
        })
      },
      toggleCollapsed() {
        this.currentCollapsed = !this.currentCollapsed
        this.$emit('collapse', this.currentCollapsed, this)
      },
      addItem(vm) {
        const {name} = vm
        if (!name) return
        if (name in this.items) {
          throw new Error(`表单中的项中 name:${name} 重复,请确保在同一个表单中保持唯一`)
        }
        this.items[name] = vm
      },
      removeItem(vm) {
        if (!vm.name) return
        delete this.items[vm.name]
      },
      getDefaultValue() {
        // const values = {}
        // Object.entries(this.items).forEach(([name, item]) => {
        //   values[name] = item.getDefaultValue()
        //   // _set(values, name, item.getDefaultValue())
        // })
        // return {...values, ...(this.model || {})}
        return cloneDeep(this.model || {})
      },
      triggerChange(val) {
        clearTimeout(this.changeTimer)
        this.changeTimer = setTimeout(() => {
          /**
           * 表单值改变时触发
           * @event change
           * @param {Object} model 表单模型数据
           */
          this.$emit('change', cloneDeep(val))
        }, 100)

      },
      setFormModel(data) {
        const currentModel = cloneDeep(this.currentModel)
        const newCurrentModel = {...currentModel, ...data}
        this.currentModel = newCurrentModel
      }
    }
  }
</script>