前言
這次的後臺管理系統專案選型用了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>
複製程式碼
總結
到這類一箇中規中矩的查詢元件就實現了,有什麼不對之處請留言,會及時修正。
還有一些功能沒有擴充進去,比如任意控制元件觸發回撥。更豐富的元件支援,類似匯出功能
具體業務具體分析,有興趣的可以自行擴充,謝謝閱讀、