此次的後臺管理系統項目選型用了Vue
來做爲主技術棧;javascript
由於前段時間用過React
來寫過項目(用了antd
),感受棒棒的。css
因此此次就排除了Element UI
,而採用了Ant Design Vue
;html
在分析整個項目原型後,發現又能夠抽離相似以前的React表格搜索組件
vue
props
及聯動,新增了slot
的傳遞AdvancedSearch.vue
2019-04-23: 新增slider
組件的配置java
2019-04-25:如果傳入的數據長度小於最大格式,默認顯示爲內聯模式,不然爲卡片模式react
2019-05-12: 回調支持傳入自定義函數(用於返回本身組合的數據格式)git
其餘特性等,具體能夠看下面的思惟導圖.github
具體業務的封裝中還要複雜的多,還結合了一些自定義封裝組件,展現出來代碼篇幅太長。數組
昨天寫初版的時候,思惟還沒繞過來,用props
和自定義事件($on,$emit
)來實現,antd
實現出來的代碼量賊多,由於每細化多一層組件,複雜度就越高。各類互相回調來實現。
仔細翻了下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 | 函數 | 就是查詢的回調 |
@callbackFormat | 可選函數 | 傳遞會改動回調數據,不傳遞則忽略 |
// SearchDataSource是數據源,具體能夠看props的默認值
<table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange" />
<table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange" @callbackFormat="formatFunc">
<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);
}
}
複製代碼
<template>
<div class="advance-search-wrapper">
<a-form :form="form" @submit="handleSubmit">
<template v-if="layoutMode === 'inline'">
<a-card :bordered="bordered">
<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-col :style="{ width: collapsed ? '100%' : 'auto' }">
<a-tooltip placement="bottom">
<template slot="title">
<span>執行查詢</span>
</template>
<a-button type="primary" :size="SearchGlobalOptions.size" @click="handleSubmit" icon="search">
查詢
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template slot="title">
<span>清空全部控件的值</span>
</template>
<a-button :size="SearchGlobalOptions.size" style="margin-left: 8px" @click="resetSearchForm" icon="border" >
重置
</a-button>
</a-tooltip>
<template v-if="showCollapsedText">
<a @click="togglecollapsed" style="margin-left: 8px">
<a-tooltip placement="bottom">
<template slot="title">
<span>{{ collapsed ? '點擊收起部分控件' : '點擊展開全部控件' }}</span>
</template>
{{ collapsed ? '收起' : '展開' }}
<a-icon :type="collapsed ? 'up' : 'down'" />
</a-tooltip>
</a>
</template>
<slot name="extra" />
</a-col>
</a-row>
</a-card>
</template>
<template v-else>
<a-card :bordered="bordered">
<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-tooltip placement="bottom">
<template slot="title">
<span>執行查詢</span>
</template>
<a-button type="primary" :size="SearchGlobalOptions.size" @click="handleSubmit" icon="search">
查詢
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template slot="title">
<span>清空全部控件的值</span>
</template>
<a-button :size="SearchGlobalOptions.size" style="margin-left: 8px" @click="resetSearchForm" icon="border" >
重置
</a-button>
</a-tooltip>
</slot>
<template v-if="showCollapsedText">
<a @click="togglecollapsed" style="margin-left: 8px">
<a-tooltip placement="bottom">
<template slot="title">
<span>{{ collapsed ? '點擊收起部分控件' : '點擊展開全部控件' }}</span>
</template>
{{ collapsed ? '收起' : '展開' }}
<a-icon :type="collapsed ? 'up' : 'down'" />
</a-tooltip>
</a>
</template>
<slot name="extra" />
</a-row>
</template>
<a-row :gutter="gutter">
<template v-for="(item, index) in renderDataSource">
<template v-if="item.type && item.fieldName">
<field-render :SearchGlobalOptions="SearchGlobalOptions" :itemOptions="item" :key="item.fieldName" v-show="index < SearchGlobalOptions.maxItem || (index >= SearchGlobalOptions.maxItem && collapsed)" />
</template>
</template>
</a-row>
</a-card>
</template>
</a-form>
</div>
</template>
<script> import FieldRender from './FieldRender'; export default { name: 'AdvancedSearch', components: { FieldRender }, computed: { showCollapsedText() { // 顯示展開搜索和收縮的斷定 return this.renderDataSource.length > this.maxItem; }, SearchGlobalOptions() { // 全局配置 return { maxItem: this.maxItem, size: this.size, immediate: this.immediate, responsive: this.responsive }; }, renderDataSource() { // 重組傳入的數據,合併全局配置,子項的配置優先全局 return this.dataSource.map(item => ({ ...this.SearchGlobalOptions, ...item })); }, layoutMode() { // 展現模式優化 if (this.layout) return this.layout; if (this.maxItem > this.dataSource.length) { return 'inline'; } else { return 'card'; } } }, props: { layout: { //搜索區域的佈局 type: String, default: '' }, bordered: { // 是否顯示邊框 type: Boolean, default: false }, datetimeTotimeStamp: { // 是否把時間控件的返回值所有轉爲時間戳 type: Boolean, default: false }, maxItem: { // 超過多少個摺疊 type: Number, default: 4 }, gutter: { // 控件的間距 type: Number, default: 48 }, size: { // 控件的尺寸 type: String, default: 'default' }, responsive: { type: Object, default: function() { return { xxl: 6, xl: 8, md: 12, sm: 24 }; } }, title: { type: String, default: '搜索條件區域' }, dataSource: { // 數據源 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: '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' } ] } ] } ] } ]; } } }, 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 (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) { if (this.$listeners.callBackFormat && typeof this.$listeners.callBackFormat === 'function') { let formatData = this.$listeners.callBackFormat(values); this.$emit('change', formatData); } else { const queryParams = this.handleParams(values); this.$emit('change', queryParams); } } }); }, resetSearchForm() { // 重置整個查詢表單 this.form.resetFields(); this.$emit('change', null); } } }; </script>
<style lang="scss"> .advance-search-wrapper { .ant-form-item { display: flex; margin-bottom: 12px !important; 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>
複製代碼
<template>
<a-col v-bind="fieldOptions.responsive" v-if="fieldOptions.fieldName && fieldOptions.type === 'text'">
<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>
<a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'">
<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>
<a-col v-else-if="fieldOptions.fieldName && fieldOptions.type === 'number'" 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>
<a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)" >
<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>
<a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'">
<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>
<a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'">
<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>
<a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'cascader'">
<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>
<a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'slider'">
<a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
<a-slider :min="1" range :marks="fieldOptions.marks" :tipFormatter="e => e * (fieldOptions.baseMultiple ? fieldOptions.baseMultiple : 500)" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [0, 0] } ]" />
</a-form-item>
</a-col>
</template>
<script> export default { computed: { fieldOptions() { if (this.itemOptions.baseMultiple) { return { marks: { 0: 0, 1: this.itemOptions.baseMultiple, 100: this.itemOptions.baseMultiple * 100 }, ...this.itemOptions }; } 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>
複製代碼
到這類一箇中規中矩的查詢組件就實現了,有什麼不對之處請留言,會及時修正。
還有一些功能沒有拓展進去,好比任意控件觸發回調,更豐富的組件支持,相似導出功能。
具體業務具體分析,有興趣的能夠自行拓展,謝謝閱讀。