Vue 折騰記 - (16) 基於Ant Design Vue 封裝一個配置式的表單搜尋元件

CRPER發表於2019-04-10

前言

這次的後臺管理系統專案選型用了Vue來作為主技術棧;

因為前端時間用過React來寫過專案(用了antd),感覺很棒棒。

這次就排除了Element UI,而採用了Ant Design Vue;

分析整個專案原型後,發現又可以抽離類似之前的React表格搜尋元件

React 折騰記 - (6) 基於React 16.x+ Antd 3.封裝的一個宣告式的查詢元件(實用強大)


效果圖

Vue 折騰記 - (16) 基於Ant Design Vue 封裝一個配置式的表單搜尋元件

  • 響應式
  • 控制元件數量的摺疊
  • 統一回撥

具體可以看下面的導圖


實現思路

  • 用什麼來實現元件之間的通訊

昨天寫了第一版的時候(程式碼已丟棄),思維還沒繞過來。直接用props和自定義事件($on,$emit)來實現,

實現出來的程式碼量賊多,因為每細化多一層元件,複雜度就越高。各種互相回撥來實現。

passpass。仔細翻了下Ant Design Vue的文件,發下可以類似React的套路實現

  • 怎麼來實現

要實現一個東西可複用的東東(結合業務),首先我們必須先梳理我們要實現的功能點。

props儘量不破壞文件控制元件暴露的特性,而是折中去實現,擴充。

所以有了這麼個思維導圖

Vue 折騰記 - (16) 基於Ant Design Vue 封裝一個配置式的表單搜尋元件

遇到的問題

  • jsx來實現的問題

一開始想用jsx來實現,發現還是太天真了。各種報錯,特別對Vue指令的支援一團糟

以及函式式元件的寫法也是坑挺多,沒辦法,乖乖的迴歸template的寫法

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

  • 控制元件擠成一坨的問題

這個可能是antd vue版本的樣式沒處理好,我仔細排查了。若沒有複寫他的樣式,完全沒法展開。

placeholder不會自動撐開,數字控制元件也是很小

修正前:

Vue 折騰記 - (16) 基於Ant Design Vue 封裝一個配置式的表單搜尋元件

修正後

Vue 折騰記 - (16) 基於Ant Design Vue 封裝一個配置式的表單搜尋元件

  • 補全當初寫react版本一些欠缺考慮的東東(比如返回的查詢物件上)

用法

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

子項會覆蓋全域性帶過來的同名特性,優先順序比較高

選項 型別 解釋
responsive 物件 柵欄的佈局物件
size 字串 控制元件規格大小(大部分都有default,small,large)
gutter 數字 控制元件的間距
datetimeTotimeStamp 布林型別 若是為true,所有時間控制元件都會轉為時間戳返回
SearchDataSource 陣列物件 就是需要渲染控制元件的資料來源,具體看原始碼的props
@change 函式 就是查詢的回撥
// SearchDataSource是資料來源,具體可以看props的預設值
<table-search :SearchDataSource="SearchDataSource" :immediate="true" @change="tableSearchChange" />

// 物件預設為true的,null這個特殊物件會給if直接過濾掉
methods: {
    tableSearchChange(searchParams) {
      if (searchParams) {
        // 執行查詢
      } else {
        // 執行了重置,一般預設重新請求整個不帶引數的列表
      }
      console.log('回撥接受的表單資料: ', searchParams);
    }
}
複製程式碼
  • TableSearch.vue
<template>
  <div class="table-page-search-wrapper">
    <a-form layout="inline" :form="form" @submit="handleSubmit">
      <a-card>
        <template v-slot:title>
          <h4 style="text-align:left;margin:0;">
            {{ title }}
          </h4>
        </template>
        <template v-slot:extra>
          <div>
            <a-button type="primary" @click="handleSubmit">查詢</a-button>
            <a-button style="margin-left: 8px" @click="resetSearchForm">重置</a-button>
            <a @click="togglecollapsed" v-if="maxItem < renderDataSource.length" style="margin-left: 8px">
              {{ collapsed ? '收起' : '展開' }}
              <a-icon :type="collapsed ? 'up' : 'down'" />
            </a>
          </div>
        </template>
        <a-row :gutter="gutter">
          <template v-for="(item, index) in renderDataSource">
            <field-render
              :SearchGlobalOptions="SearchGlobalOptions"
              :itemOptions="item"
              :key="item.fieldName"
              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: 'default'
    },
    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',
            value: '', // 控制元件的值
            size: '', // 控制元件大小
            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: 24px;
      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(渲染對應控制元件)
<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
            :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>

複製程式碼

總結

到這類一箇中規中矩的查詢元件就實現了,有什麼不對之處請留言,會及時修正。

還有一些功能沒有擴充進去,比如任意控制元件觸發回撥。更豐富的元件支援,類似匯出功能

具體業務具體分析,有興趣的可以自行擴充,謝謝閱讀、

相關文章