前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue 折腾记 - (16) 基于Ant Design Vue 封装一个配置式的表单搜索组件

Vue 折腾记 - (16) 基于Ant Design Vue 封装一个配置式的表单搜索组件

作者头像
CRPER
发布2019-04-18 16:27:32
8.2K0
发布2019-04-18 16:27:32
举报
文章被收录于专栏:CRPER折腾记CRPER折腾记

前言

这次的后台管理系统项目选型用了Vue来作为主技术栈;

因为前端时间用过React来写过项目(用了antd),感觉棒棒的。

所以这次就排除了Element UI,而采用了Ant Design Vue;

在分析整个项目原型后,发现又可以抽离类似之前的React表格搜索组件

React 折腾记 - (6) 基于React 16.x+ Antd 3.封装的一个声明式的查询组件(实用强大)


效果图

  • 2019-04-10 14:50 : 修正了部分的初始化props及联动,新增了slot的传递

其他特性等,具体可以看下面的思维导图.


实现思路

  • 用什么来实现组件之间的通讯

昨天写第一版的时候,思维还没绕过来,用props和自定义事件($on,$emit)来实现,

实现出来的代码量贼多,因为每细化多一层组件,复杂度就越高。各种互相回调来实现。

仔细翻了下Ant Design Vue的文档,发下可以类似React的套路实现

  • 怎么来实现

要实现一个结合业务可复用的东东,首先我们必须先梳理我们要实现的功能点。

props尽量不破坏文档控件暴露的特性,而是折中去实现,拓展。

先画个思维导图梳理下功能点


遇到的问题

  • jsx来实现的问题

一开始想用jsx来实现,发现还是太天真了。各种报错,特别对Vue指令的支持一团糟

以及函数式组件的写法也是坑挺多,没办法,乖乖的回归template的写法

vue官方提供了jsx的支持,日渐完善;Github:vue/jsx

  • 控件挤成一坨的问题

这个可能是antd vue版本的样式没处理好,我仔细排查了。若没有复写他的样式,完全没法展开。

placeholder不会自动撑开,数字控件也是很小

修正前:

修正后

  • 补全当初写react版本一些欠缺考虑的东东(比如返回的查询对象上)

用法

就普通的引入,具体暴露的propschange如下

子项会覆盖全局带过来的同名特性,优先级比较高

选项

类型

解释

responsive

对象

栅栏的布局对象

size

字符串

控件规格大小(大部分都有default,small,large)

gutter

数字

控件的间距

datetimeTotimeStamp

布尔类型

若是为true,所有时间控件都会转为时间戳返回

searchDataSource

数组对象

就是需要渲染控件的数据源,具体看源码的props

@change

函数

就是查询的回调

代码语言:javascript
复制
// SearchDataSource是数据源,具体可以看props的默认值
<table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange" />


<table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange">
  <a-button type="primary" @click="test">xxxx</a-button>
  <template v-slot:extra>
    <div>fasdfas</div>
  </template>
</table-search>

// 对象默认为true的,null这个特殊对象会给if直接过滤掉
methods: {
    tableSearchChange(searchParams) {
      if (searchParams) {
        // 执行查询
      } else {
        // 执行了重置,一般默认重新请求整个不带参数的列表
      }
      console.log('回调接受的表单数据: ', searchParams);
    }
}
复制代码
  • TableSearch.vue
代码语言:javascript
复制
<template>
  <div class="table-page-search-wrapper">
    <a-form layout="inline" :form="form" @submit="handleSubmit">
      <a-card>
        <template v-slot:title>
          <span style="text-align:left;margin:0;">
            {{ title }}
          </span>
        </template>

        <template v-slot:extra>
          <a-row type="flex" justify="start" align="middle">
            <slot>
              <a-button type="primary" @click="handleSubmit">查询</a-button>
              <a-button style="margin-left: 8px" @click="resetSearchForm">重置</a-button>
            </slot>
            <slot name="extra" />
            <template v-if="maxItem < renderDataSource.length">
              <a-divider type="vertical" />
              <a @click="togglecollapsed" style="margin-left: 8px">
                {{ collapsed ? '收起' : '展开' }}
                <a-icon :type="collapsed ? 'up' : 'down'" />
              </a>
            </template>
          </a-row>
        </template>

        <a-row :gutter="gutter">
          <template v-for="(item, index) in renderDataSource">
            <field-render
              :SearchGlobalOptions="SearchGlobalOptions"
              :itemOptions="item"
              :key="index"
              v-show="index < SearchGlobalOptions.maxItem || (index >= SearchGlobalOptions.maxItem && collapsed)"
            />
          </template>
        </a-row>
      </a-card>
    </a-form>
  </div>
</template>

<script>
import FieldRender from './FieldRender';
export default {
  components: {
    FieldRender
  },
  computed: {
    SearchGlobalOptions() {
      // 全局配置
      return {
        maxItem: this.maxItem,
        size: this.size,
        immediate: this.immediate,
        responsive: this.responsive
      };
    },
    renderDataSource() {
      // 重组传入的数据,合并全局配置,子项的配置优先全局
      return this.searchDataSource.map(item => ({ ...this.SearchGlobalOptions, ...item }));
    }
  },
  props: {
    datetimeTotimeStamp: {
      // 是否把时间控件的返回值全部转为时间戳
      type: Boolean,
      default: false
    },
    maxItem: {
      // 超过多少个折叠
      type: Number,
      default: 3
    },
    gutter: {
      // 控件的间距
      type: Number,
      default: 48
    },
    size: {
      //  控件的尺寸
      type: String,
      default: 'small'
    },
    responsive: {
      type: Object,
      default: function() {
        return {
          xl: 8,
          md: 12,
          sm: 24
        };
      }
    },
    title: {
      type: String,
      default: '搜索条件区域'
    },
    searchDataSource: {
      // 数据源
      type: Array,
      default: function() {
        return [
          {
            type: 'text', // 控件类型
            labelText: '控件名称', // 控件显示的文本
            fieldName: 'formField1',
            placeholder: '文本输入区域' // 默认控件的空值文本
          },
          {
            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: 'select',
            fieldName: 'formField7',
            placeholder: '下拉选择你要的',
            options: [
              {
                label: 'text1',
                value: '0'
              },
              {
                label: 'text2',
                value: '0'
              }
            ]
          },
          {
            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'
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ];
      }
    }
  },
  data() {
    return {
      // 高级搜索 展开/关闭
      collapsed: false
    };
  },
  beforeCreate() {
    this.form = this.$form.createForm(this);
  },

  methods: {
    togglecollapsed() {
      this.collapsed = !this.collapsed;
    },
    handleParams(obj) {
      // 判断必须为obj
      if (!(Object.prototype.toString.call(obj) === '[object Object]')) {
        return {};
      }
      let tempObj = {};
      for (let [key, value] of Object.entries(obj)) {
        if (!value) continue;
        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">
.table-page-search-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>


复制代码
  • FieldRender.vue(渲染对应控件)
代码语言:javascript
复制
<template>
  <div>
    <template v-if="fieldOptions.fieldName && fieldOptions.type === 'text'">
      <a-col v-bind="fieldOptions.responsive">
        <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
          <a-input
            :size="fieldOptions.size ? fieldOptions.size : 'default'"
            v-decorator="[
              fieldOptions.fieldName,
              { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' }
            ]"
            :placeholder="fieldOptions.placeholder"
          />
        </a-form-item>
      </a-col>
    </template>
    <template v-if="fieldOptions.fieldName && fieldOptions.type === 'select'">
      <a-col v-bind="fieldOptions.responsive">
        <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
          <a-select
            style="width: 100%"
            showSearch
            :filterOption="selectFilterOption"
            :size="fieldOptions.size ? fieldOptions.size : 'default'"
            allowClear
            v-decorator="[
              fieldOptions.fieldName,
              { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : undefined }
            ]"
            :placeholder="fieldOptions.placeholder"
          >
            <template v-for="(item, index) in fieldOptions.options">
              <a-select-option :value="item.value" :key="index">
                {{ item.label }}
              </a-select-option>
            </template>
          </a-select>
        </a-form-item>
      </a-col>
    </template>
    <template v-if="fieldOptions.fieldName && fieldOptions.type === 'number'">
      <a-col v-bind="fieldOptions.responsive">
        <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
          <a-input-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 : '' }
            ]"
            :placeholder="fieldOptions.placeholder"
          />
        </a-form-item>
      </a-col>
    </template>
    <template v-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)">
      <a-col v-bind="fieldOptions.responsive">
        <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
          <a-radio-group
            :size="fieldOptions.size ? fieldOptions.size : 'default'"
            buttonStyle="solid"
            v-decorator="[
              fieldOptions.fieldName,
              { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' }
            ]"
          >
            <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-form-item>
      </a-col>
    </template>
    <template v-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'">
      <a-col v-bind="fieldOptions.responsive">
        <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
          <a-date-picker
            :size="fieldOptions.size ? fieldOptions.size : 'default'"
            :placeholder="fieldOptions.placeholder"
            v-decorator="[
              fieldOptions.fieldName,
              { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null }
            ]"
          />
        </a-form-item>
      </a-col>
    </template>
    <template v-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'">
      <a-col v-bind="fieldOptions.responsive">
        <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
          <a-range-picker
            :size="fieldOptions.size ? fieldOptions.size : 'default'"
            v-decorator="[
              fieldOptions.fieldName,
              { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null }
            ]"
            :placeholder="fieldOptions.placeholder"
          />
        </a-form-item>
      </a-col>
    </template>
    <template v-if="fieldOptions.fieldName && fieldOptions.type === 'cascader'">
      <a-col v-bind="fieldOptions.responsive">
        <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
          <a-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-form-item>
      </a-col>
    </template>
  </div>
</template>

<script>
export default {
  computed: {
    fieldOptions() {
      return this.itemOptions;
    }
  },
  props: {
    itemOptions: {
      // 控件的基本参数
      type: Object,
      default: function() {
        return {
          type: 'text', // 控件类型
          defaultValue: '', // 默认值
          label: '控件名称', // 控件显示的文本
          value: '', // 控件的值
          responsive: {
            md: 8,
            sm: 24
          },
          size: '', // 控件大小
          placeholder: '' // 默认控件的空值文本
        };
      }
    }
  },
  data() {
    return {
      labelCol: { span: 6 },
      wrapperCol: { span: 18 }
    };
  },
  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>

复制代码

总结

到这类一个中规中矩的查询组件就实现了,有什么不对之处请留言,会及时修正。

还有一些功能没有拓展进去,比如任意控件触发回调。更丰富的组件支持,类似导出功能

具体业务具体分析,有兴趣的可以自行拓展,谢谢阅读、

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年04月10日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 效果图
  • 实现思路
  • 遇到的问题
  • 用法
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档