开发背景
在某次的开发中遇到这样一个需求,表单中有一个具体的属性,该属性对应一个数组对象。

但是,项目使用的框架是ant-design-vue。该框架的实现方法是定义一个keys,当keys为3时相当于数组的长度为3。提交时获取的值是firstName1,firstName2,firstName3, lastName1, lastName2,lastName3。这就相当不友好了,每次向后台传值都要进行一番转义。如果需求就这一个那是可以接受的。后来这样的需求变成了9个…。
代码
首先要解决的问题当然是与ant-design-vue的form表单进行联动,可以通过ant-design-vue的form文档。了解如何自定义表单控件。为了在下面解释这个组件的内部逻辑,先把代码贴出来。
<template>
<div :class="[drag?'drag-wrap':'']">
<div :class="['group-wrap', bordered? 'border-wrap':'']" v-for="key in keys" :key="key">
<div class="item" v-if="param1&&!getUnVisible(1)">
<slot name="param1" :index="key - 1" :innerValue="paramsList[key - 1][param1]" :paramName="param1">
<a-input
:disabled="disabled"
:value="paramsList[key - 1][param1]"
:placeholder="placeholder1"
@change="handleChange(key - 1, $event, param1)"
></a-input>
</slot>
</div>
<div class="item" v-if="param2&&!getUnVisible(2)">
<slot name="param2" :index="key - 1" :innerValue="paramsList[key - 1][param2]" :paramName="param2">
<a-input
class="item"
:disabled="disabled"
:placeholder="placeholder2"
@change="handleChange(key - 1, $event, param2)"
:value="paramsList[key - 1][param2]"
></a-input>
</slot>
</div>
<div class="item" v-if="param3&&!getUnVisible(3)">
<slot name="param3" :index="key - 1" :innerValue="paramsList[key - 1][param3]" :paramName="param3">
<a-input
:disabled="disabled"
class="item"
:placeholder="placeholder3"
@change="handleChange(key - 1, $event, param3)"
:value="paramsList[key - 1][param3]"
></a-input>
</slot>
</div>
<div class="item" v-if="param4&&!getUnVisible(4)">
<slot name="param4" :index="key - 1" :innerValue="paramsList[key - 1][param4]" :paramName="param4">
<a-input
:disabled="disabled"
class="item"
:placeholder="placeholder4"
@change="handleChange(key - 1, $event, param4)"
:value="paramsList[key - 1][param4]"
></a-input>
</slot>
</div>
<div class="item" v-if="param5&&!getUnVisible(5)">
<slot name="param5" :index="key - 1" :innerValue="paramsList[key - 1][param5]" :paramName="param5">
<a-input
:disabled="disabled"
class="item"
:placeholder="placeholder5"
@change="handleChange(key - 1, $event, param5)"
:value="paramsList[key - 1][param5]"
></a-input>
</slot>
</div>
<!-- <div v-if="param3" class="gap-line">--</div>-->
<div v-if="!aline" class="remove-item" @click="onRemoveItem(key - 1)"><icon-font type="icontubiao_shanchu"/></div>
<div v-if="!aline&&drag" class="drag-thumb" draggable="true"><icon-font type="icontubiao_tuozhuai"/></div>
<div class="extra-line" v-if="dpsparam"><slot name="dpsparam" :index="key-1" :innerValue="paramsList[key -1][dpsparam]" :paramName="dpsparam"></slot></div>
</div>
<a-button type="dashed" v-if="!aline" block @click="onCreateItem">
<a-icon type="plus" /> <slot name="btnName">添加</slot>
</a-button>
</div>
</template>
<script>
let target;
export default {
props: [
'value',
'only', // 是否只显示一行,且没有添加
'drag', // 是否支持拖拽排序
'param1',
'param2',
'param3',
'param4',
'param5',
'dpsparam',
'disabled',
'bordered',
'placeholder1',
'placeholder2',
'placeholder3',
'placeholder4',
'placeholder5',
'UnVisibleArr', // 动态不显示[1,2,3,4,5]
],
data() {
const value = this.value || [];
return {
keys: value.length,
paramsList: value || [],
// 是否只显示一行
aline: this.only !== undefined,
// param1: "attributeName",
// param2: "attributeValue"
};
},
name: 'ParamsInput',
computed: {
getUnVisible() {
return (i) => {
if (this.UnVisibleArr) {
return this.UnVisibleArr.includes(i);
}
return false;
};
},
},
watch: {
value(val = []) {
this.paramsList = val || [];
this.keys = this.paramsList.length;
},
disabled(val = false) {
if (Object.prototype.toString.call(val).includes('Boolean')) {
this.disabled = val;
} else {
this.disabled = false;
}
},
},
mounted() {
if (this.drag) {
this.onDragMethod();
}
},
methods: {
onDragMethod() {
const dropbox = [].slice.call(document.querySelectorAll('.drag-wrap'));
dropbox.forEach((item) => {
item.addEventListener('dragstart', (event) => {
target = event.target.parentNode;
});
item.addEventListener('dragover', (event) => {
event.preventDefault();
});
item.addEventListener('drop', (event) => {
event.stopPropagation();
event.preventDefault();
let currentTarget;
[].slice.call(event.currentTarget.querySelectorAll('.group-wrap')).forEach((res) => {
const rectRange = res.getBoundingClientRect();
if (event.y >= rectRange.y && event.y <= rectRange.y + rectRange.height) {
currentTarget = res;
}
});
if (currentTarget) {
const rect = currentTarget.getBoundingClientRect();
const y = rect.y + rect.height / 2;
switch (target.compareDocumentPosition(currentTarget)) {
case 2:
if (y > event.y) {
currentTarget.before(target);
} else {
currentTarget.after(target);
}
break;
case 4:
if (y > event.y) {
currentTarget.before(target);
} else {
currentTarget.after(target);
}
break;
default:
break;
}
}
});
});
},
onCreateItem() {
this.keys = this.keys + 1;
const obj = {};
this.paramsList.push(obj);
},
onRemoveItem(k) {
const paramsList = [...this.paramsList];
paramsList.splice(k, 1);
this.triggerChange(paramsList);
this.keys = this.keys - 1;
},
handleChange(key, event, paramName) {
const paramsList = [...this.paramsList];
paramsList[key][paramName] = event.target.value;
this.triggerChange(paramsList);
},
// 外部使用
handleValueChange(key, value, paramName) {
const paramsList = [...this.paramsList];
paramsList[key][paramName] = value;
this.triggerChange(paramsList);
},
// 外部使用-列改变
handleColumnValueChange(key, value, paramName, otherValue = undefined) {
const paramsList = [...this.paramsList];
paramsList.forEach((item) => {
item[paramName] = otherValue;
});
paramsList[key][paramName] = value.target.checked;
this.triggerChange(paramsList);
},
triggerChange(changedValue) {
this.$emit('change', changedValue);
},
},
};
</script>
<style scoped>
.group-wrap {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.item {
flex: 1;
}
.item + .item {
margin-left: 45px;
}
.gap-line {
flex: none;
width: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.item + .item::before {
content: "--";
position: absolute;
width: 40px;
font-size: 14px;
margin-left: -28px;
}
.remove-item {
flex: none;
width: 35px;
text-align: center;
cursor: pointer;
}
.drag-thumb{
flex: none;
width: 35px;
text-align: center;
cursor: pointer;
}
.extra-line{
width: 100%;
}
.border-wrap{
border-width: 1px;
border-style: solid;
border-radius: 5px;
border-color: #e8e8e8;
padding: 10px;
margin: 0 0 16px;
}
</style>
组件功能
参数 | 说明 | 类型 | 默认值 |
btnName | 按钮名称,在组件内加入添加配送方式 | template | ‘添加’ |
dpsparam | 自定义组件内加入一个自定义组件 | template | – |
unVisibleArr | 动态不显示的组件,用于被关联时某列不显示。 | Array | [] |
only | 显示一行内容,隐藏添加按钮 | Boolean | false |
param<index> | index范围是1-5,例如param1=”attributeList”,若需自定义组件,则在组件内内容。index为当前行,innerValue为当前值,paramName为该列属性名。 | String | – |
placeholder<index> | index范围是1-5,例如placeholder1=”属性名”,定义了param若需要加上placeholder则添加,若param运用了自定义组件,则不需要。 | String | – |
disabled | 是否不可输入,若定义了自定义组件,则不需要。 | Boolean | false |
bordered | 加入边框 | Boolean | false |
drag | 是否支持拖拽排序 | Boolean | false |
功能点1:作为一个form控件
- props接收value参数,通过value知道数组列表的长度keys,数组列表的内容paramsList
- 为input组件绑定值,我们可知该input为第几行的数据,并知道是哪个字段,因此可以通过paramsList[第几行][哪个字段]来绑定。
- 给input设置change,每个input的change改变都会影响整体的值,因此我们会使用change函数来改变整体的值。input使用的change函数是handleChange,通过它改变整体的值,并且调用了triggerChange,通知包含该组件的form知道里面有值改变了。
功能点2:需要能够自定义子组件,例如选择等
- 通过slot作用域插槽,将index(是第几行),paramName(该列的名称),innerValue(该列的值)传递到外部。
- 外部使用时在该组件上定义ref例如ref=”list”,方便调用该组件内部的函数
- 外部使用该组件可使用的插槽名为param1,param2,…,param5。
<template v-slot:param1={ index, innerValue, paramName}> <a-select @change="refChange(index, $event, paramName)" placeholder="选择承运商" :value="form.getFieldValue('baseWhCarriers')[index][paramName]" > <a-select-option v-for="(item ,index) in autoDict.CARRIER" :value="item.id.toString()" :key="index">{{item.carrierName}}</a-select-option> </a-select> </template>
- 通过ref使用改变组件内的值的函数,函数有两个可供调用handleValueChange(key,value,paramName),该函数接收当前在第几行key,变更的值value,该列名称paramName,用于改变某行某列的值。还有handleColumnValueChange(key,value,paramName,otherValue=undefined),该函数接收的值与上面的函数类似,多了一个otherValue,用于改变某行某列的值,并将该列的其他值都变为otherValue。
- 绑定value
功能点3:动态增减表单组件递归

<a-form-item
:colon="false"
label="配送方式"
class="span2"
v-bind="modalLayout"
>
<params-input
ref="list"
param1="methodName"
param2="distMethod"
dpsparam="baseCarrierDists"
placeholder1="名称"
placeholder2="承运商code-请输入编码"
:bordered="true"
v-decorator="['otherWays',{
initialValue: [{}],
rules: [{
required: true,
message: '请填写配送方式',
}]
}]"
>
<template v-slot:btnName>
添加配送方式
</template>
<template v-slot:dpsparam="{ index, innerValue, paramName}">
<params-input
:value="dockForm.getFieldValue('otherWays')[index][paramName]"
param1="paramCode"
param2="paramName"
param3="paramValue"
placeholder1="参数名"
placeholder2="参数code"
placeholder3="参数值"
@change="refChange(index, $event, paramName)"
>
<template v-slot:btnName>
添加参数
</template>
</params-input>
</template>
</params-input>
</a-form-item>
- 在组件中调动该组件,使用的插槽是dpsParam,实现逻辑与自定义子组件并无多大区别,只是对dpsParam插槽的位置做了ui处理。
- 加上:bordered=”true”用于区分内容块
- 使用插槽btnName修改添加一行的名字
如何使用npm引入
https://github.com/zhuishao/ec-zs-components中的README有引入说明