学以致用,基于elementui尝试封装一个form组件

2,341 阅读7分钟

学以致用,基于elementui尝试封装一个form组件

写在前面

2020年的最后一个月,掘金开始了年度征文。

这几周,陆陆续续看了10篇左右的征文,其中有技术大神也有和我一样初出校园的新人。看的不是太多,属实是不敢再看。看见大家的年度总结,才发现原来自己距离优秀还有那么远的距离.....

所以这几周下班后,开始思考这一年自己学习到了什么。作为一名大四实习生,年初的疫情在家办公期间,多数时间都游离在正式项目组之外,公司安排给我的开发任务并不多。说来惭愧,在那接近两个月的时间里,除了处理掉交给我的任务,另外拿出一些时间来解决我的毕设以外。剩下的空余时间甚至被我当作了加长版寒假......

3月底,公司复工,我开始接触微信小程序,公众号h5页面的开发。熟悉移动端开发,可能就是我这一年为数不多的收获了吧。

本月,暂时告别了前面已上线的移动端项目,回到了实习时接触的后台管理的开发。有相似却又不同,项目不是很大,前端开发只有我一人,不再是跟着师傅后面写页面了,但也因为前端就我一个人写,所以开发的自由度很高。如果不是看到这些年度总结,我估计还是会在公司以前的后台管理的主体框架内添加页面。但这一次,也许有风险,我想说一句"教练,我想打球。"。

也许会有坑,也许会有bug,但那有如何,虽千万人吾往矣,气势不能丢,大不了加班花更多的时间去解决问题呗。

上面那句写的豪迈,但其实呀这次的后台管理项目并不复杂,哈,这才是我想浪的关键。

正文开始

通过vue动态组件封装form组件

记得之前在掘金上看到过一篇讲解动态组件的文章,当时看见就有了用来封装组件的想法,但因为一些原因最终没有付诸实践,而且当时没有养成点赞关注不迷路的好习惯,所以现在尴尬的是找不到了文章地址了...如果你恰好也看到过那篇文章请在评论区告诉我或者留一道传送门,文章应该是刨析我下面这个例子的。

动态组件其实就是通过<component>元素的is属性来实现动态切换的组件。需要注意的是is属性传入的要是注册的组件名,官网介绍

先来一个简单的例子。

这是input组件页面

<template>
  <div class="item-input">
      <i-input  v-bind="$attrs"/>
  </div>
</template>

<script>
export default {
  name: 'Input',
  data() {
      return {}
  }
}
</script>

这是中间层的form组件,元素中通过is将不同的form组件传入

<template>
  <div class="hello">
    <div v-for="item in formIpt" :key="item.label">
      <component :is="item.type" v-bind="item" ></component>
    </div>
  </div>
</template>

<script>
import Inputs from "./formItem/input";

const CompFormItem = {
  components: {
    Input,
  },
  name: "FormItem",
  props: {
    formItem: {
      required: true
    }
  },
  render(h) {
    return h(`${this.formItem.type}`);
  }
};

export default {
  name: "Form",
  components: {
    Input,
    CompFormItem
  },
  props: {
    formIpt: {
      type: Array,
      required: true,
      default: () => []
    }
  }
};
</script>

最顶层的主页面

<template>
  <div id="app">
    <comForm :formIpt="formIpt"  />
  </div>
</template>

<script>
import comForm from './components/form/form.vue'
export default {
  name: 'App',
  components: {
    comForm
  },
  data() {
    return {
      formIpt: [
        {
          type: "Input",
          models: "",
          label: '文本框',
          placeholder: "我是默认值",
          clearable: true,
        }
      ],
    }
  },
}
</script>


实现的效果是

看到这个页面是不是感觉我有点呆,直接引用一个<el-input>不就好了,干嘛整这么麻烦。别急,再往后看看。

这里有几个点需要注意一下,在中间层form页面通过v-bind="item"将主页面中传入的对象全部传下级的input组件,然后在input组件中再通过v-bind="$attrs",将父作用域中不作为(且获取) prop 被识别的特性 (class 和 style 除外)传给更深的内部组件,也就是elementui的<el-input>组件,所以我们最外层的placeholder和clearable被传给了<el-input>

这个时候,其实只要我们继续将诸如<el-select><el-checkbox>组件如同<el-input>一样封装进来,那么再主页面中我们只要往formIpt这个数组中的对象传入不同的type就可以相对应的渲染不同的form组件了。

现在子组件的种类先不管,既然是表单组件,那么表单校验就必不可少,我们先将校验加入。

为form组件添加校验

首先对form组件页面动刀,将校验加入。

<template>
  <div class="form">
    <el-form
        :validate-on-rule-change='false'
        :model="formRule.model?formRule.model:''"
        :rules="formRule.rule"
        :inline="true"
        :ref="formRule.refName">
        <div :class="[fromClass.isColumn ? 'column-form' : 'search-form']">
            <el-form-item
                v-for="item in formIpt"
                :label="item.label"
                :key="item.label"
                :prop="item.ruleProp">
                <component :is="item.type" v-bind="item" :value.sync="item.models"></component>
             </el-form-item>
        </div>
    </el-form>
  </div>
</template>

<script>
import Input from "./formItem/input";

const CompFormItem = {
  components: {
    Input,
  },
  name: "FormItem",
  props: {
    formItem: {
      required: true
    }
  },
  render(h) {
    return h(`${this.formItem.type}`);
  }
};

export default {
  name: "Form",
  components: {
    Input,
    CompFormItem
  },
  props: {
    formIpt: {
      type: Array,
      required: true,
      default: () => []
    },
    formRule: {
      type: Object,
      required: false,
      default: () => {}
    },
    fromClass: {
      type: Object,
      required: false,
      default: () => {}
    },
  },
  watch: {
    'formIpt': {
      handler(newValue) {
      	//当表单值发生改变将改变后的值赋给校验所绑定的model
        this.initFormRule(newValue)
      },
      deep: true
    }
  },
  created () {
    this.initFormRule(this.formIpt)
  },
  methods: {
    initFormRule (newValue) {
      for (let k = 0; k < newValue.length; k++) {
        this.$props.formRule.model[newValue[k].ruleProp] = newValue[k].models
      }
    },
  }
};
</script>

然后是input组件

<template>
  <div class="item-input">
    <el-input
        v-bind="$attrs" 
        v-model="Val" 
        @blur="blur?blur():none()"
        @focus="focus?focus():none()"
        @change="change?change():none()"
    ></el-input>
  </div>
</template>

<script>
export default {
  name: 'Input',
  data() {
      return {
          Val: ''
      }
  },
  props: {
    value: {
      type: String,
      default: () => ''
    },
    blur: {
      type: Function,
      default: () => () => {}
    },
    focus: {
      type: Function,
      default: () => () => {}
    },
    change: {
      type: Function,
      default: () => () => {}
    }
  },
  watch: {
      Val: function (newVal) {
          this.$emit('update:value',newVal)
      },
  },
  created() {
    this.Val = this.value
  },
  methods: {
    none () {
      return false
    },
  }
}
</script>


<style scoped lang="scss">
</style>

这里也有几点需要注意一下。

  • 首先v-bind="$attrs" 会将父作用域中不作为prop传给下级,所以我们在input组件的prop中将我们需要的属性接收,比如change事件等。

  • 监听formIpt值的变化,一旦form组件的值改变,如输入框发生改变,我们要将表单双向绑定的值赋值给表单校验绑定的对应mode,所以在后面的主页面中formIpt中ruleProp值要与formRule中model内的值对应。

  • 上面说的双向绑定,通过.sync修饰符来实现,当输入框内值改变的时候,将formIpt内的models值改变,以达到双向绑定的效果。

然后在主页面中需要做如下修改

<template>
  <div id="app">
    <comForm ref="testForm" :formIpt="formIpt" :formRule="formRule" :fromClass="fromClass"  />
  </div>
</template>

<script>
import comForm from './components/form/form.vue'
export default {
  name: 'App',
  components: {
    comForm
  },
  data() {
    return {
      Val: '',
      formIpt: [
        {
          type: "Input",
          models: "",
          label: '文本框',
          ruleProp: 'label1',
          placeholder: "我是默认值",
          clearable: true,
          focus: this.focus,
        },
      ],
      formRule: {
        model: {
          label1: '',
        },
        refName: 'testForm',
        rule: {
          label1: [
            { required: true, message: '请输入内容', trigger: 'blur' }
          ],
        }
      },
      fromClass: {
        isColumn: false
      }
    }
  },
  methods: {
    focus() {
      console.log('---------------------------','获得焦点')
    },
  }
}
</script>


这里我们可以往我们的comForm组件中传入formIpt,formRule,fromClass三个值,分别对应表单项,校验规则,以及表单样式,样式先不谈。

效果如下:

这样子一个基本的form组件就封装完成了,接下来我们开始丰富form元素的种类,select,datePicker之类和input差不多就不再赘述,下面我们试试将upload封装进form组件。

封装upload组件

先来一个单文件上传,上代码。

<template>
  <div class="item-upload">
    <el-upload
      class="avatar-uploader"
      v-bind="$attrs" 
      :accept="accept"
      :action="action"
      :headers="headers"
      :data="data"
      :show-file-list="false"
      :before-upload="handleBeforeUpload"
      :on-progress="handleProgress"
      :on-success="successUpload"
      :on-error="handleError"
      :on-remove="handleRemove"
      >
        <img v-if="value&&value!==''" :src="value" class="avatar">
        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
  </div>
</template>

<script>
export default {
  name: 'Input',
  data() {
      return {
          Val: ''
      }
  },
  props: {
    value: {
      type: String,
      default: () => ''
    },
    ruleProp: {
      type: String,
      default: () => ''
    },
    action: {
      type: String,
      default: () => ''
    },
    accept: {
      type: String,
      default: () => ''
    },
    headers: {
      type: Object,
      default: () => {}
    },
    data: {
      type: Object,
      default: () => {} 
    },
    handleSuccess: {
      type: Function,
      default: () => () => {}
    },
    handleBeforeUpload: {
      type: Function,
      default: () => () => {}
    },
    handleError: {
      type: Function,
      default: () => () => {}
    },
    handleRemove: {
      type: Function,
      default: () => () => {}
    },
    handleProgress: {
      type: Function,
      default: () => () => {}
    }
  },
  watch: {
      Val: function (newVal) {
          this.$emit('update:value',newVal)
      },
  },
  created() {
    this.Val = this.value
  },
  methods: {
    none () {
      return false
    },
    async successUpload(response, file, fileList) {
      //这里我是接口返回图片地址,如果不是或者有其他操作,可以放在主页面的handleSuccess中处理
      this.Val = response.data
      await this.handleSuccess(response, file, fileList)
      //上传成功后重置校验
      this.$emit('resetItemValidate',this.ruleProp)
    }
  }
}
</script>


<style scoped lang="scss">
 .avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
  .avatar-uploader .el-upload:hover {
    border-color: #409EFF;
  }
  .avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    line-height: 178px;
    text-align: center;
  }
  .avatar {
    width: 178px;
    height: 178px;
    display: block;
  }
</style>

然后在中间层接受resetItemValidate函数重置校验

<template>
  <div class="form">
    <el-form
        :validate-on-rule-change='false'
        :model="formRule.model?formRule.model:''"
        :rules="formRule.rule"
        :inline="true"
        :ref="formRule.refName">
        <div :class="[fromClass.isColumn ? 'column-form' : 'search-form']">
            <el-form-item
                v-for="(item, index) in formIpt"
                :label="item.label"
                :key="index"
                :prop="item.ruleProp">
                <component :is="item.type" v-bind="item" :value.sync="item.models" @resetItemValidate="resetItemValidate"></component>
             </el-form-item>
        </div>
    </el-form>
  </div>
</template>
<script>
export default {
  name: "Form",
  methods: {
    resetItemValidate(name) {
      this.$refs[this.formRule.refName].validateField(name)
    }
  }
};
</script>

最后,主页面,组件需要的属性,方法与之前组件一样都放在formIpt中传入

<template>
  <div id="app">
    <comForm ref="testForm" :formIpt="formIpt" :formRule="formRule" :fromClass="fromClass"  />
       <el-button @click="btnClick" type="primary">主要按钮</el-button>
  </div>
</template>

<script>
import comForm from './components/form/form.vue'
export default {
  name: 'App',
  components: {
    comForm
  },
  data() {
    return {
      Val: '',
      formIpt: [
        {
          type: "Input",
          models: "",
          label: '文本框',
          ruleProp: 'label1',
          placeholder: "我是默认值",
          clearable: true,
          focus: this.focus,
        },
        {
          type: "Select",
          models: "",
          label: '下拉框',
          ruleProp: 'label2',
          placeholder: "我是下拉框",
          clearable: true,
          option: [
            {
              label: '选项1',
              value: '1'
            }
          ],
          blur: this.blur,
        },
        {
          type: "DatePicker",
          mold: "datetime",
          models: "",
          label: '日期选择器',
          ruleProp: 'label3',
          placeholder: "我是日期选择器",
          clearable: true,
          blur: this.blur,
        },
        {
          type: "Upload",
          models: "",
          label: '文件上传',
          ruleProp: 'label4',
          action: "你的上传文件接口地址",
          handleBeforeUpload: this.handleBeforeUpload,
          handleSuccess: this.handleSuccess,
          handleError: this.handleError,
        },
      ],
      formRule: {
        model: {
          label1: '',
          label2: '',
          label3: '',
          label4: '',
        },
        refName: 'testForm',
        rule: {
          label1: [
            { required: true, message: '请输入内容', trigger: 'blur' }
          ],
          label2: [
            { required: true, message: '请选择内容', trigger: 'change' }
          ],
          label3: [
            { required: true, message: '请选择日期', trigger: 'change' }
          ],
          label4: [
            { required: true, message: '请上传文件', trigger: 'change' }
          ],
        }
      },
      fromClass: {
        isColumn: false
      }
    }
  },
  methods: {
    focus() {
      console.log('---------------------------','获得焦点')
    },
    handleBeforeUpload(file) {},
    handleSuccess(response, file, fileList) {},
    handleError(err, file, fileList) {},
    //校验表单
    btnClick() {
      this.$refs.testForm.$refs.testForm.validate()
    }
  }
}
</script>


效果如下:

封装富文本编辑器

这次项目的业务需求中用到了富文本编辑器,所以我也将其封装到form组件中来,产品给的要求是尽可能简洁,所以我最后选择了wangEditor,一个轻量级的富文本编辑器,官网地址

<template>
  <div class="item-editor">
    <div id="editorElem" style="text-align:left"></div>
  </div>
</template>

<script>
import E from 'wangeditor'
export default {
  name: 'Editor',
  data() {
      return {
        Val: '',
        editor: '',
      }
  },
  props: {
    value: {
      type: String,
      default: () => ''
    },
    action: {
      type: String,
      default: () => ''
    },
    ruleProp: {
      type: String,
      default: () => ''
    },
    menus: {
      type: Array,
    }
  },
  watch: {
      Val: function (newVal) {
          this.$emit('update:value',newVal)
      }
  },
  created() {
    this.Val = this.value
    this.editor = new E('#editorElem')
    //编辑器菜单通过主页面传入,不传为默认值 具体参数详见文档
    if(this.menus) {
        this.editor.config.menus  = this.menus
    }
    //上传图片接口地址
    this.editor.config.uploadImgServer = this.action
    //自定义 fileName
    this.editor.config.uploadFileName = 'file'
    //自定义上传参数
    this.editor.config.uploadImgParams = {
        type: 'image',
    }
    console.log(this.editor.config)
  },
   mounted () {
    this.editor.config.uploadImgHooks = {
        // 上传图片之前
        before: function(xhr) {
            console.log(xhr)
        },
        // 图片上传并返回了结果,图片插入已成功
        success: function(xhr) {
            console.log('success', xhr)
        },
        // 图片上传并返回了结果,但图片插入时出错了
        fail: function(xhr, editor, resData) {
            console.log('fail', resData)
        },
        // 上传图片出错,一般为 http 请求的错误
        error: function(xhr, editor, resData) {
            console.log('error', xhr, resData)
        },
        // 上传图片超时
        timeout: function(xhr) {
            console.log('timeout', xhr)
        },
        // 图片上传并返回了结果,想要自己把图片插入到编辑器中
        // 例如服务器端返回的不是 { errno: 0, data: [...] } 这种格式,可使用 customInsert
        customInsert: function(insertImgFn, result) {
            // result 即服务端返回的接口
            console.log('customInsert', result)

            // insertImgFn 可把图片插入到编辑器,传入图片 src ,执行函数即可,
            insertImgFn(result.data)
        }
    }
    this.editor.config.onchange = html => {
      this.Val = html
      //当编辑器内容改变时重置校验
      this.$emit('resetItemValidate',this.ruleProp)
    }
    // 取消自动 focus
    this.editor.config.focus = false
    this.editor.create()
    this.initEditor()
  },
  methods: {
    initEditor () {
      //如果有初始值 则赋值
      if (this.value !== '') {
        this.editor.txt.html(this.value)
      }
    },
    clearEditor () {
      this.editor.txt.html('')
    },
  }
}
</script>

效果如下:

到这一步大致的form组件基本完成了,剩下的就是将其他常见的form组件再集成进来,这里就不再介绍了,附上一份目前的项目结构。 接下来,要完善form的样式将它用到我们的后台管理当中,通过上面的fromClass我们将后台管理常见的表单格式展现出来,如查询表单,操作表单。

丰富form表单样式

我的构想中这个表单组件会用在两个场景分别是搜索表单,以及新增修改查看详情时的编辑表单。

查询表单:

编辑表单:

要满足上述要求,针对form组件页面进行改动

<template>
    <el-form
      class="form"
      :validate-on-rule-change='false'
      :model="formRule.model?formRule.model:''"
      :rules="formRule.rule"
      :inline="true"
      :ref="formRule.refName">
      <div :class="[fromClass.isColumn ? 'column-form' : 'search-form']">
        <el-form-item
          v-for="(item, index) in formIpt"
          :style="{'width': getWidth(item.span, fromClass.isColumn), 'margin-right': '0'}"
          :label-width="item.labelWidth ? item.labelWidth : '120px'"
          :label="item.label"
          :key="index"
          :prop="item.ruleProp">
          <component :is="item.type" v-bind="item" :value.sync="item.models" @resetItemValidate="resetItemValidate"></component>
        </el-form-item>
      </div>
      <el-form-item :class="fromClass.searchButtom" v-if="searchFunc">
        <el-button type="primary" :size="fromClass.formSize || 'small'" @click="query(searchFunc.query)">
          {{searchFunc.queryText}}
        </el-button>
        <el-button :size="fromClass.formSize || 'small'" @click="reset(searchFunc.reset)">
          {{searchFunc.resetText}}
        </el-button>
        <el-button type="success" v-if="searchFunc.add" :size="fromClass.formSize || 'small'" @click="func(searchFunc.add)">
          {{searchFunc.addText}}
        </el-button>
      </el-form-item>
</el-form>
</template>

<script>
import Input from "./formItem/input";
import Select from "./formItem/select";
import DatePicker from "./formItem/datePicker";
import Upload from "./formItem/upload";
import Editor from "./formItem/editor";

const CompFormItem = {
  components: {
    Input,
    Select,
    DatePicker,
    Upload,
    Editor,
  },
  name: "FormItem",
  props: {
    formItem: {
      required: true
    }
  },
  render(h) {
    return h(`${this.formItem.type}`);
  }
};

export default {
  name: "Form",
  components: {
    Input,
    Select,
    DatePicker,
    Upload,
    Editor,
    CompFormItem
  },
  props: {
    formIpt: {
      type: Array,
      required: true,
      default: () => []
    },
    formRule: {
      type: Object,
      required: false,
      default: () => {}
    },
    searchFunc: {
      type: Object,
      required: false
    },
    fromClass: {
      type: Object,
      required: false,
      default: () => {
        return {
          isColumn: false,
          searchButtom: 'search-form-btn',
        }
      }
    },
  },
  watch: {
    'formIpt': {
      handler(newValue) {
        this.initFormRule(newValue)
      },
      deep: true
    }
  },
  created () {
    this.initFormRule(this.formIpt)
  },
  methods: {
    getWidth(span, isColumn = false) {
      if(!isColumn && !span) { //搜索表单 未设置span 默认一行三个搜索条件
        span = 8
      } else if(isColumn && !span) { //编辑表单 未设置span 默认一行展示两个
        span = 12
      }
      span = span > 24 ? 24 : span
      let width =  `${Number((span / 24)*100).toFixed(1)}%`
      return width
    },
    initFormRule (newValue) {
      for (let k = 0; k < newValue.length; k++) {
        this.$props.formRule.model[newValue[k].ruleProp] = newValue[k].models
      }
    },
    formResetFields () {
      this.$refs[this.$props.formRule.refName].resetFields()
    },
    resetItemValidate(name) {
      this.$nextTick(() => {
        this.$refs[this.formRule.refName].validateField(name)
      })
    },
    query (func) {
      if (this.$props.formRule.rule !== '{}') {
        this.$refs[this.$props.formRule.refName].validate((valid) => {
          if (valid) {
            func()
          }
        })
      } else {
        func()
      }
    },
    reset (fn) {
      this.$props.formIpt.forEach(item => {
        item.models = ''
      })
      console.log( this.$props.formIpt)
      if (fn) {
        fn()
      }
    },
    func (fn) {
      fn()
    }
  }
};
</script>

<style scoped lang="scss">
.form{
  display: flex;
  .search-form{
    width: calc(100% - 240px);
    display: flex;
    flex-wrap: wrap;
  }
  .el-form-item{
    /deep/.el-form-item__content{
      width: calc(100% - 120px);
      &>div{
        &>div{
          width: 100%;
        }
      }
    }
  }
  .search-form-btn{
    width: 240px;
    margin-right: 0;
    display: flex;
    align-items: flex-end;
    /deep/.el-form-item__content{
      width: 100%;
    }
  }
  .column-form {
    display: flex;
    width: 100%;
    align-items: flex-start;
    flex-wrap: wrap;
  }
}
</style>

改动完的form组件,添加了如下几点

  • 首先通过传入的fromClass中的isColumn判断是否位编辑表单,true为编辑表单, false为查询表单

  • 新增传入属性searchFunc,传入则展示查询表单右侧按钮,默认展示查询和重置按钮,并且根据是否传入子属性add判断新增按钮是否存在

    searchFunc: {
        query,//查询执行方法
        reset,//重置执行方法
        add, //新增执行方法
        queryText,//查询按钮名称
        resetText,//重置按钮名称
        addText,//新增按钮名称
    },
    
  • formIpt中每个对象新增span属性,来控制该表单项占一行大小,最大24为独占一行,默认查询表单一行三项,编辑表单一行两项

最后在主页面中调用组件

<template>
  <div id="app">
    <comForm ref="searcForm" :formIpt="searchIpt" :formRule="searchRule" :searchFunc="searchFunc"  />
    
    <el-dialog
    width="70%"
    :title="dialogInfo.title"
    :visible.sync="dialogInfo.dialogFlag"
    :destroy-on-close="true"
    @close="closeDialog"
    >
      <comForm v-if="dialogInfo.dialogFlag" ref="testForm" :formIpt="formIpt" :formRule="formRule" :fromClass="fromClass" />
      <div slot="footer" class="dialog-footer">
        <el-button @click="closeDialog">取 消</el-button>
        <el-button type="primary" @click="dialogConfirm">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import comForm from './components/form/form.vue'
export default {
  name: 'App',
  components: {
    comForm
  },
  data() {
    return {
      searchIpt: [
        {
          type: "Input",
          models: "",
          label: '查询条件1',
          ruleProp: '',
          placeholder: "我是查询条件1",
          clearable: true,
        },
        {
          type: "Input",
          models: "",
          label: '查询条件2',
          ruleProp: '',
          placeholder: "我是查询条件1",
          clearable: true,
        },
        {
          type: "Input",
          models: "",
          label: '查询条件3',
          ruleProp: '',
          placeholder: "我是查询条件1",
          clearable: true,
        },
        {
          type: "DatePicker",
          mold: "datetime",
          models: "",
          label: '查询条件4',
          ruleProp: '',
          placeholder: "我是查询条件4",
          clearable: true,
        },
      ],
      searchRule: {
        model: {},
        refName: 'searcForm',
        rule: {}
      },
      formIpt: [
        {
          type: "Input",
          models: "",
          label: '文本框',
          ruleProp: 'label1',
          placeholder: "我是默认值",
          clearable: true,
          focus: this.focus,
        },
        {
          type: "Select",
          models: "",
          label: '下拉框',
          ruleProp: 'label2',
          placeholder: "我是下拉框",
          clearable: true,
          option: [
            {
              label: '选项1',
              value: '1'
            }
          ],
          blur: this.blur,
          change: this.change,
        },
        {
          type: "DatePicker",
          mold: "datetime",
          models: "",
          label: '日期选择器',
          ruleProp: 'label3',
          placeholder: "我是日期选择器",
          clearable: true,
          blur: this.blur,
        },
        {
          type: "Upload",
          models: "",
          label: '文件上传',
          ruleProp: 'label4',
          action: "你的上传文件接口地址",
          handleBeforeUpload: this.handleBeforeUpload,
          handleSuccess: this.handleSuccess,
          handleError: this.handleError,
        },
        {
          type: "Editor",
          models: "",
          label: '富文本编辑器',
          ruleProp: 'label5',
          action: "你的上传文件接口地址",
          span: 24
        },
      ],
      formRule: {
        model: {
          label1: '',
          label2: '',
          label3: '',
          label4: '',
          label5: '',
        },
        refName: 'testForm',
        rule: {
          label1: [
            { required: true, message: '请输入内容', trigger: 'blur' }
          ],
          label2: [
            { required: true, message: '请选择内容', trigger: 'blur' }
          ],
          label3: [
            { required: true, message: '请选择内容', trigger: 'blur' }
          ],
          label4: [
            { required: true, message: '请上传文件', trigger: 'change' }
          ],
          label5: [
            { required: true, message: '请输入富文本编辑器内容', trigger: 'change' },
          ]
        }
      },
      fromClass: {
        isColumn: true
      },
      searchFunc: {
        query: this.query,
        reset: this.reset,
        add: this.add,
        queryText: '搜索',
        resetText: '重置',
        addText: '新增',
      },
      dialogInfo: {
        dialogFlag: false,
        title: '',
        type: ''
      },
    }
  },
  methods: {
    focus() {
      console.log('---------------------------','获得焦点')
    },
    change() {
      console.log('---------------------------','下拉框')
    },
    handleBeforeUpload(file) {
      console.log(file)
    },
    handleSuccess(response, file, fileList) {
      console.log(response)
      console.log(file)
      console.log(fileList)
    },
    handleError(err, file, fileList) {
      console.log(err)
      console.log(file)
      console.log(fileList)
    },
    dialogConfirm() {
      console.log(this.$refs.testForm.$refs.testForm)
      // this.$refs.testForm.$refs.testForm.validate()
     this.$refs.testForm.$refs.testForm.validate((valid) => {
          if (valid) {
            this.$message({
              message: '新增成功',
              type: 'success'
            })
            this.dialogInfo.dialogFlag = false
          }
        })
    },
    query() {
      console.log('查询')
    },
    reset() {
      console.log('重置')
    },
    add() {
      console.log('新增')
      this.dialogInfo.title = '新增'
      this.dialogInfo.dialogFlag = true
    },
    closeDialog() {
      this.dialogInfo.dialogFlag = false
    }
  }
}
</script>

可以看到封装完的form组件我们需要往里传入几个不同的属性来满足对应需求,它们分别是:

  • formIpt接收数组,内部为传入的表单项。

  • formRule接收对象,为表单校验想对应属性。

    formRule: {
        model: {},//表单校验里验证的值与formIpt中对象的ruleProp要保持一致
        refName,//表单组件绑定的ref名,可在外部通过$refs根据需求对表单进入校验/重置
        rule,//校验规则
    }
    
  • searchFunc接收对象,上文有提,用来控制查询表单对应按钮以及事件

  • fromClass接收对象,用于控制表单展现类型(查询/编辑)

最终效果:

做到这里,这个简易的form组件已经可以满足简单的后台管理,接下来我们只需要根据不同业务再往form组件里继续封装子组件即可。

最后总结来看,这个组件并不复杂,但在整个编写过程中收获还是挺多的。 另附上代码地址

感谢

感谢掘友们的支持,如果本文有帮助到你的地方,记得点赞哦。

最后,祝大家新年快乐,赶走不开心。