Vue 2.x折腾记 - (17) 基于Ant Design Vue 封装一个配置式的表单组件

简介: 写了个类似上篇搜索的封装,但是要考虑的东西更多。具体业务比展示的代码要复杂,篇幅太长就不引入了。

前言

写了个类似上篇搜索的封装,但是要考虑的东西更多。

具体业务比展示的代码要复杂,篇幅太长就不引入了。

效果图


  • 2019-04-25
  • 添加了下拉多选的渲染,并搜索默认过滤文本而非值
  • 简化了渲染的子组件的代码
  • 2019-04-28
  • 增加了对input type的控制


实现思路和功能


基础的功能直接配置上来渲染,而上传组件就不大合适了;


所以选择了slot来实现,如何保证传入的form-item的布局一致,则是拿slot-scope


我这边选型用的是vue 2.6 +的版本,所以直接用的是最新的写法

而且作为表单组件,校验这些肯定需要考虑,所以数据的构造改造了下,

对于校验规则这些走的是antd form用的那套,所以在传递的时候把对应的属性拍平了,


到里面再进行数据结构调整,目前部分控件样式依旧需要自己修正!!!


演示的代码用法


<form-list @change="onFormListChange">
       <template #field="{options}">
         <a-form-item label="Upload" v-bind="options">
           <a-upload
             v-decorator="[
               'upload',
               {
                 valuePropName: 'fileList',
                 getValueFromEvent: normFile
               }
             ]"
             name="logo"
             action="/upload.do"
             list-type="picture"
           >
             <a-button> <a-icon type="upload" /> Click to upload </a-button>
           </a-upload>
         </a-form-item>
       </template>
     </form-list>


代码


  • FieldRender.vue


<template>
<a-form-item
  :label="fieldOptions.labelText"
  :label-col="fieldOptions.labelCol"
  :wrapper-col="fieldOptions.wrapperCol"
>
  <a-input
    v-if="fieldOptions.fieldName && fieldOptions.type === 'text'"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '',
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
    :placeholder="fieldOptions.placeholder"
  />
  <a-select
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'"
    style="width: 100%"
    showSearch
    :options="fieldOptions.options"
    :filterOption="selectFilterOption"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    allowClear
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : undefined,
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
    :placeholder="fieldOptions.placeholder"
  />
  <a-input-number
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'number'"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    :min="fieldOptions.min ? fieldOptions.min : 1"
    style="width: 100%"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '',
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
    :placeholder="fieldOptions.placeholder"
  />
  <a-radio-group
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    buttonStyle="solid"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '',
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
  >
    <template v-for="(item, index) in fieldOptions.options">
      <a-radio-button :key="index" :value="item.value">{{ item.label }} </a-radio-button>
    </template>
  </a-radio-group>
  <a-date-picker
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    :placeholder="fieldOptions.placeholder"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
  />
  <a-range-picker
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
    :placeholder="fieldOptions.placeholder"
  />
  <a-cascader
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'cascader'"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    :options="fieldOptions.options"
    :showSearch="{ cascaderFilter }"
    v-decorator="[
      fieldOptions.fieldName,
      { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [] }
    ]"
    :placeholder="fieldOptions.placeholder"
  />
  <a-time-picker
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'timepicker'"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
  />
  <a-textarea
    :placeholder="fieldOptions.placeholder"
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'textarea'"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
    :autosize="{ minRows: 6, maxRows: 24 }"
  />
  <a-select
    mode="multiple"
    :size="fieldOptions.size ? fieldOptions.size : 'default'"
    optionFilterProp="children"
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'multiple'"
    :placeholder="fieldOptions.placeholder"
    style="width: 100%"
    :options="fieldOptions.options"
    v-decorator="[
      fieldOptions.fieldName,
      {
        initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [],
        rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
      }
    ]"
  />
</a-form-item>
</template>
<script>
export default {
props: {
  fieldOptions: {
    // 待渲染的对象
    type: Object,
    default: function() {
      return {};
    }
  }
},
methods: {
  selectFilterOption(input, option) {
    // 下拉框过滤函数
    return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0;
  },
  cascaderFilter(inputValue, path) {
    // 级联过滤函数
    return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1);
  }
}
};
</script>


  • FormList.vue


<template>
 <div class="form-list-wrapper">
   <a-form :layout="formLayout" :form="form">
     <template v-for="(item, index) in renderDataSource">
       <template v-if="item.type && item.fieldName">
         <field-render :fieldOptions="item" :key="item.fieldName" />
       </template>
     </template>
     <slot name="field" :options="GlobalOptions" />
     <a-form-item :wrapper-col="buttonItemLayout.wrapperCol">
       <a-tooltip placement="bottom">
         <template slot="title">
           <span>提交表单</span>
         </template>
         <a-button type="primary" :size="size" @click="handleSubmit">提交</a-button>
       </a-tooltip>
       <a-tooltip placement="bottom">
         <template slot="title">
           <span>清空所有控件的值</span>
         </template>
         <a-button :size="size" style="margin-left: 8px" @click="resetSearchForm">重置</a-button>
       </a-tooltip>
     </a-form-item>
   </a-form>
 </div>
</template>
<script>
import FieldRender from './FieldRender';
export default {
 name: 'FormList',
 components: {
   FieldRender
 },
 props: {
   formLayout: {
     // 表单布局
     type: String, // 'horizontal'|'vertical'|'inline'
     default: 'horizontal'
   },
   datetimeTotimeStamp: {
     // 是否把时间控件的返回值全部转为时间戳
     type: Boolean,
     default: false
   },
   size: {
     // 全局控件大小
     type: String,
     default: 'default'
   },
   responsive: {
     // 表单项的响应布局
     type: Object,
     default: function() {
       return {
         labelCol: { span: 5 },
         wrapperCol: { span: 16 }
       };
     }
   },
   dataSource: {
     type: Array,
     default: function() {
       return [
         {
           type: 'text', // 控件类型
           labelText: '控件名称', // 控件显示的文本
           fieldName: 'formField1',
           placeholder: '文本输入区域', // 默认控件的空值文本
           rules: [
             {
               required: true,
               message: '必填'
             }
           ]
         },
         {
           labelText: '数字输入框',
           type: 'number',
           fieldName: 'formField2',
           placeholder: '这只是一个数字的文本输入框'
         },
         {
           labelText: '单选框',
           type: 'radio',
           fieldName: 'formField3',
           defaultValue: '0',
           options: [
             {
               label: '选项1',
               value: '0'
             },
             {
               label: '选项2',
               value: '1'
             }
           ]
         },
         {
           labelText: '日期选择',
           type: 'datetime',
           fieldName: 'formField4',
           placeholder: '选择日期'
         },
         {
           labelText: '日期范围',
           type: 'datetimeRange',
           fieldName: 'formField5',
           placeholder: ['开始日期', '选择日期']
         },
         {
           labelText: '时刻选择',
           type: 'timepicker',
           fieldName: 'formField8',
           placeholder: '请选择时刻(时间)'
         },
         {
           labelText: '文本区域',
           type: 'textarea',
           fieldName: 'formField9',
           placeholder: '请输入文本了内容'
         },
         {
           type: 'multiple',
           labelText: '角色',
           fieldName: 'role',
           defaultValue: [],
           rules: [
             {
               required: true,
               message: '必须选择一种角色'
             }
           ],
           options: [
             {
               label: '系统管理员',
               value: '0'
             },
             {
               label: '风控管理员',
               value: '1'
             },
             {
               label: '催收管理员',
               value: '2'
             },
             {
               label: '催收员',
               value: '3'
             },
             {
               label: '审核员',
               value: '4'
             },
             {
               label: '财务',
               value: '5'
             }
           ]
         },
         {
           labelText: '下拉框',
           type: 'select',
           fieldName: 'formField7',
           placeholder: '下拉选择你要的',
           options: [
             {
               label: 'text1',
               value: '0'
             },
             {
               label: 'text2',
               value: '1'
             }
           ]
         },
         {
           labelText: '联动',
           type: 'cascader',
           fieldName: 'formField6',
           placeholder: '级联选择',
           options: [
             {
               value: 'zhejiang',
               label: 'Zhejiang',
               children: [
                 {
                   value: 'hangzhou',
                   label: 'Hangzhou',
                   children: [
                     {
                       value: 'xihu',
                       label: 'West Lake'
                     },
                     {
                       value: 'xiasha',
                       label: 'Xia Sha',
                       disabled: true
                     }
                   ]
                 }
               ]
             },
             {
               value: 'jiangsu',
               label: 'Jiangsu',
               children: [
                 {
                   value: 'nanjing',
                   label: 'Nanjing',
                   children: [
                     {
                       value: 'zhonghuamen',
                       label: 'Zhong Hua men'
                     }
                   ]
                 }
               ]
             }
           ]
         }
       ];
     }
   }
 },
 beforeCreate() {
   this.form = this.$form.createForm(this);
 },
 computed: {
   GlobalOptions() {
     // 全局配置
     return {
       size: this.size,
       ...this.formItemLayout
     };
   },
   renderDataSource() {
     // 重组传入的数据,合并全局配置,子项的配置优先全局
     return this.dataSource.map(item => ({ ...this.GlobalOptions, ...item }));
   },
   formItemLayout() {
     // 更改布局项目的尺寸
     const { formLayout } = this;
     if (formLayout === 'horizontal') {
       return this.responsive;
     } else {
       return {};
     }
   },
   buttonItemLayout() {
     // 提交按钮布局
     const { formLayout } = this;
     return formLayout === 'horizontal'
       ? {
           wrapperCol: { span: 14, offset: 4 }
         }
       : {};
   }
 },
 methods: {
   handleParams(obj) {
     // 判断必须为obj
     if (!(Object.prototype.toString.call(obj) === '[object Object]')) {
       return {};
     }
     let tempObj = {};
     for (let [key, value] of Object.entries(obj)) {
       if (Array.isArray(value) && value.length <= 0) continue;
       if (Object.prototype.toString.call(value) === '[object Function]') continue;
       if (this.datetimeTotimeStamp) {
         // 若是为true,则转为时间戳
         if (Object.prototype.toString.call(value) === '[object Object]' && value._isAMomentObject) {
           // 判断moment
           value = value.valueOf();
         }
         if (Array.isArray(value) && value[0]._isAMomentObject && value[1]._isAMomentObject) {
           // 判断moment
           value = value.map(item => item.valueOf());
         }
       }
       // 若是为字符串则清除两边空格
       if (value && typeof value === 'string') {
         value = value.trim();
       }
       tempObj[key] = value;
     }
     return tempObj;
   },
   handleSubmit(e) {
     // 触发表单提交,也就是搜索按钮
     e.preventDefault();
     this.form.validateFields((err, values) => {
       if (!err) {
         console.log('处理前的表单数据', values);
         const queryParams = this.handleParams(values);
         this.$emit('change', queryParams);
       }
     });
   },
   resetSearchForm() {
     // 重置整个查询表单
     this.form.resetFields();
     this.$emit('change', null);
   }
 }
};
</script>
<style lang="scss">
.form-list-wrapper {
 .ant-form-inline {
   .ant-form-item {
     display: flex;
     margin-bottom: 12px;
     margin-right: 0;
     .ant-form-item-control-wrapper {
       flex: 1;
       display: inline-block;
       vertical-align: middle;
     }
     > .ant-form-item-label {
       line-height: 32px;
       padding-right: 8px;
       width: auto;
     }
     .ant-form-item-control {
       height: 32px;
       line-height: 32px;
       display: flex;
       justify-content: flex-start;
       align-items: center;
       .ant-form-item-children {
         min-width: 160px;
       }
     }
   }
 }
 .table-page-search-submitButtons {
   display: block;
   margin-bottom: 24px;
   white-space: nowrap;
 }
}
</style>


问题


暴露的方法和搜索组件一样,@change回来表单数据;


问题:


操作父的props会造成死循环(在有slot的情况下,因slot-scope拿的是父props经过computed后的值)。


解决方案:


已经改用其他实现姿势,抽离成独立组件,再联动数据。


总结


antd vue版本目前的功能复现上,还是有所欠缺,可能vuereact实现的机子不一致导致;


不管怎么说,不考虑极端情况下,目前这个库用起来感觉还好;


至少是可用状态,后续若有修正,会继续更新文章,谢谢阅读

目录
相关文章
|
11天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
27 1
vue学习第四章
|
11天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
23 1
vue学习第九章(v-model)
|
11天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
24 1
vue学习第十章(组件开发)
|
17天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
17天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
17天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
17天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
16天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
26 3
|
18天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
16天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
36 2