Vue 折騰記 - (16) 基於Ant Design Vue 封裝一個配置式的表單搜尋元件
這次的後臺管理系統專案選型用了 Vue
來作為主技術棧;
因為前端時間用過 React
來寫過專案(用了 antd
),感覺很棒棒。
這次就排除了 Element UI
,而採用了 Ant Design Vue
;
分析整個專案原型後,發現又可以抽離類似之前的 React表格搜尋元件
React 折騰記 - (6) 基於React 16.x+ Antd 3.封裝的一個宣告式的查詢元件(實用強大)
效果圖

- 響應式
- 控制元件數量的摺疊
- 統一回調
具體可以看下面的導圖
實現思路
- 用什麼來實現元件之間的通訊
昨天寫了第一版的時候(程式碼已丟棄),思維還沒繞過來。直接用 props
和自定義事件( $on,$emit
)來實現,
實現出來的程式碼量賊多,因為每細化多一層元件,複雜度就越高。各種互相回撥來實現。
說 pass
就 pass
。仔細翻了下 Ant Design Vue
的文件,發下可以類似 React
的套路實現
- 怎麼來實現
要實現一個東西可複用的東東(結合業務),首先我們必須先梳理我們要實現的功能點。
props
儘量不破壞文件控制元件暴露的特性,而是折中去實現,拓展。
所以有了這麼個思維導圖

遇到的問題
-
jsx
來實現的問題
一開始想用 jsx
來實現,發現還是太天真了。各種報錯,特別對 Vue
指令的支援一團糟
以及函式式元件的寫法也是坑挺多,沒辦法,乖乖的迴歸 template
的寫法
vue
官方提供了 jsx
的支援,日漸完善; Github:vue/jsx
- 控制元件擠成一坨的問題
這個可能是 antd vue
版本的樣式沒處理好,我仔細排查了。若沒有複寫他的樣式,完全沒法展開。
placeholder
不會自動撐開,數字控制元件也是很小
修正前:

修正後

- 補全當初寫
react
版本一些欠缺考慮的東東(比如返回的查詢物件上)
用法
就普通的引入,具體暴露的 props
和 change
如下
子項會覆蓋全域性帶過來的同名特性,優先順序比較高
選項 | 型別 | 解釋 |
---|---|---|
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> 複製程式碼
總結
到這類一箇中規中矩的查詢元件就實現了,有什麼不對之處請留言,會及時修正。
還有一些功能沒有拓展進去,比如任意控制元件觸發回撥。更豐富的元件支援,類似匯出功能
謝謝閱讀、