前言
在寫這個的時候,還是不信邪的用了vue jsx
的風格去寫東西,
當元件的東西多了起來之後,各種報錯;沒錯,最終我又迴歸到傳統的寫法;
上一篇寫了個搜尋的封裝,到寫這個的時候發現有所可以優化的地方。
效果圖
- 2019-04-25
- 新增了下拉多選的渲染,並搜尋預設過濾文字而非值
- 簡化了渲染的子元件的程式碼
實現思路和功能
基礎的功能直接配置上來渲染,而上傳元件就不大合適了;
所以選擇了slot
來實現,如何保證傳入的form-item
的佈局一致,則是拿slot-scope
我這邊選型用的是vue 2.6 +
的版本,所以直接用的是最新的寫法
而且作為表單元件,校驗這些肯定需要考慮,所以資料的構造改造了下,
對於校驗規則這些走的是antd form
用的那套,所以在傳遞的時候把對應的屬性拍平了,
到裡面再進行資料結構調整,目前部分控制元件樣式依舊需要自己修正!!!
演示的程式碼用法
<form-list @change="onFormListChange">
<template #field="{options}">
<a-form-item label="Upload" v-bind="options">
<a-upload
v-decorator="[
'upload',
{
valuePropName: 'fileList',
getValueFromEvent: normFile
}
]"
name="logo"
action="/upload.do"
list-type="picture"
>
<a-button> <a-icon type="upload" /> Click to upload </a-button>
</a-upload>
</a-form-item>
</template>
</form-list>
複製程式碼
程式碼
- FieldRender.vue
<template>
<a-form-item
:label="fieldOptions.labelText"
:label-col="fieldOptions.labelCol"
:wrapper-col="fieldOptions.wrapperCol"
>
<a-input
v-if="fieldOptions.fieldName && fieldOptions.type === 'text'"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '',
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
:placeholder="fieldOptions.placeholder"
/>
<a-select
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'"
style="width: 100%"
showSearch
:options="fieldOptions.options"
:filterOption="selectFilterOption"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
allowClear
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : undefined,
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
:placeholder="fieldOptions.placeholder"
/>
<a-input-number
v-else-if="fieldOptions.fieldName && fieldOptions.type === '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 : '',
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
:placeholder="fieldOptions.placeholder"
/>
<a-radio-group
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
buttonStyle="solid"
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '',
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
>
<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-date-picker
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
:placeholder="fieldOptions.placeholder"
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
/>
<a-range-picker
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
:placeholder="fieldOptions.placeholder"
/>
<a-cascader
v-else-if="fieldOptions.fieldName && fieldOptions.type === '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-time-picker
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'timepicker'"
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
/>
<a-textarea
:placeholder="fieldOptions.placeholder"
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'textarea'"
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
:autosize="{ minRows: 6, maxRows: 24 }"
/>
<a-select
mode="multiple"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
optionFilterProp="children"
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'multiple'"
:placeholder="fieldOptions.placeholder"
style="width: 100%"
:options="fieldOptions.options"
v-decorator="[
fieldOptions.fieldName,
{
initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [],
rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []
}
]"
/>
</a-form-item>
</template>
<script>
export default {
props: {
fieldOptions: {
// 待渲染的物件
type: Object,
default: function() {
return {};
}
}
},
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>
複製程式碼
- FormList.vue
<template>
<div class="form-list-wrapper">
<a-form :layout="formLayout" :form="form">
<template v-for="(item, index) in renderDataSource">
<template v-if="item.type && item.fieldName">
<field-render :fieldOptions="item" :key="item.fieldName" />
</template>
</template>
<slot name="field" :options="GlobalOptions" />
<a-form-item :wrapper-col="buttonItemLayout.wrapperCol">
<a-tooltip placement="bottom">
<template slot="title">
<span>提交表單</span>
</template>
<a-button type="primary" :size="size" @click="handleSubmit">提交</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template slot="title">
<span>清空所有控制元件的值</span>
</template>
<a-button :size="size" style="margin-left: 8px" @click="resetSearchForm">重置</a-button>
</a-tooltip>
</a-form-item>
</a-form>
</div>
</template>
<script>
import FieldRender from './FieldRender';
export default {
name: 'FormList',
components: {
FieldRender
},
props: {
formLayout: {
// 表單佈局
type: String, // 'horizontal'|'vertical'|'inline'
default: 'horizontal'
},
datetimeTotimeStamp: {
// 是否把時間控制元件的返回值全部轉為時間戳
type: Boolean,
default: false
},
size: {
// 全域性控制元件大小
type: String,
default: 'default'
},
responsive: {
// 表單項的響應佈局
type: Object,
default: function() {
return {
labelCol: { span: 5 },
wrapperCol: { span: 16 }
};
}
},
dataSource: {
type: Array,
default: function() {
return [
{
type: 'text', // 控制元件型別
labelText: '控制元件名稱', // 控制元件顯示的文字
fieldName: 'formField1',
placeholder: '文字輸入區域', // 預設控制元件的空值文字
rules: [
{
required: true,
message: '必填'
}
]
},
{
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: 'timepicker',
fieldName: 'formField8',
placeholder: '請選擇時刻(時間)'
},
{
labelText: '文字區域',
type: 'textarea',
fieldName: 'formField9',
placeholder: '請輸入文字了內容'
},
{
type: 'multiple',
labelText: '角色',
fieldName: 'role',
defaultValue: [],
rules: [
{
required: true,
message: '必須選擇一種角色'
}
],
options: [
{
label: '系統管理員',
value: '0'
},
{
label: '風控管理員',
value: '1'
},
{
label: '催收管理員',
value: '2'
},
{
label: '催收員',
value: '3'
},
{
label: '稽核員',
value: '4'
},
{
label: '財務',
value: '5'
}
]
},
{
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'
}
]
}
]
}
]
}
];
}
}
},
beforeCreate() {
this.form = this.$form.createForm(this);
},
computed: {
GlobalOptions() {
// 全域性配置
return {
size: this.size,
...this.formItemLayout
};
},
renderDataSource() {
// 重組傳入的資料,合併全域性配置,子項的配置優先全域性
return this.dataSource.map(item => ({ ...this.GlobalOptions, ...item }));
},
formItemLayout() {
// 更改佈局專案的尺寸
const { formLayout } = this;
if (formLayout === 'horizontal') {
return this.responsive;
} else {
return {};
}
},
buttonItemLayout() {
// 提交按鈕佈局
const { formLayout } = this;
return formLayout === 'horizontal'
? {
wrapperCol: { span: 14, offset: 4 }
}
: {};
}
},
methods: {
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) {
console.log('處理前的表單資料', values);
const queryParams = this.handleParams(values);
this.$emit('change', queryParams);
}
});
},
resetSearchForm() {
// 重置整個查詢表單
this.form.resetFields();
this.$emit('change', null);
}
}
};
</script>
<style lang="scss">
.form-list-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>
複製程式碼
問題
暴露的方法和搜尋元件一樣,@change
回來表單資料;然而目前有點bug
的是,
操作父的props
會造成死迴圈(在有slot
的情況下,因slot-scope
拿的是父props
經過computed
後的值)
排查了許久,目前還沒找出具體緣由;
總結
antd vue
版本目前的功能復現上,還是有所欠缺,可能vue
和react
實現的機子不一致導致;
不管怎麼說,不考慮極端情況下,目前這個庫用起來感覺還好;
至少是可用狀態,後續若有修正,會繼續更新文章,謝謝閱讀