• Vue 2.x折腾记


    前言

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

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

    效果图

    • 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=" 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=" 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=" 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;
            auto;
         }
         .ant-form-item-control {
           height: 32px;
           line-height: 32px;
           display: flex;
           justify-content: flex-start;
           align-items: center;
           .ant-form-item-children {
             min- 160px;
           }
         }
       }
     }
    
     .table-page-search-submitButtons {
       display: block;
       margin-bottom: 24px;
       white-space: nowrap;
     }
    }
    </style>
    
    
    复制代码

    问题

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

    问题:

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

    解决方案:

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

    总结

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

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

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

    转载于:https://juejin.im/post/5cb5d89af265da03a85ab689

  • 相关阅读:
    黑客工具包ShadowBrokers浅析
    浅谈Miller-Rabin素数检测算法
    辗转相除法(欧几里得算法)的证明
    2019年年终感言
    详解矩阵乘法
    计数类问题中的取模运算总结
    浅谈同余方程的求解与中国剩余定理
    模板测试题
    洛谷 P3811 【模板】乘法逆元
    同余知识点全析
  • 原文地址:https://www.cnblogs.com/twodog/p/12134910.html
走看看 - 开发者的网上家园