需求:
pm说,这次要开发的功能是供第三方使用的,所以我们无法得知第三方可能每次的字段要求,所以要求把表单做的灵活一点,提供多种表单类型供第三方自由选择配置。pm的要求是通过弹框选择要添加的组件类型,插入到页面。我一听,这不有点类似之前了解过的动态表单或者拖拽式表单嘛,虽然了解过,但是具体实现的话还没试过。一下子不知道从何入手,我不知道他具体要配置几个字段,不知道他要加什么类型的组件,所以页面写死用循环是不行的。这时候朋友的一句:用vue的动态组件。让我懵了,用vue这么久的我居然还不知道这东西,属实让我惭愧。就这样我有了入口思路,接下来的就好办了
先上效果图:
1.配置组件类型,字段标题,字段key
2.确定后插入到表单页面
3.配置后可修改选项
4.修改完成后
这次开发主要遇到的难点
- 怎么实现动态插入组件
- 怎么实现一个类似树形组件来配置组件选项
- 怎么处理树形数据结构
- 判断当前操作项的数据层级
实现步骤:
1. 表单页面动态组件的实现
因为表单页面的字段数量、类型是完全不可知的,所以靠写死是完全行不通的,这里经高人指点,用了vue的动态组件选项。
通过:is
来指定渲染的组件,所以你只需要把需要的组件另起文件写好,在表单页引入在:is
绑定一下即可,所以表单主页面的代码就很简洁了:
这里把要添加的组件类型以及对应的组件名称,数据,数据深度,标题,是否可编辑,字段名作为一个对象存入到数组里,再使用v-for
把所有组件渲染出来。
2. 选择组件类型
这次做的组件和传统的动态表单组件类型完全不一样,传统常见的组件类型是输入框,单选radio,多选select那些。这次我要做的组件非常奇葩。。。全都是选择类型的,我们常做的都是单选,多选两种类型并且都是单层级的,这次做的有:
遇事不要慌,看着有很多类型,实际上他们都有同一个特点,都是数据的结构都可以用树形数据来渲染,只要判断该组件属于多少层的数据 传入对应的数据即可 这里先定义好三个对应的树形数据:
然后再根据选择出的组件类型,传入对应的数组(这里其实用switch case更佳)
selectChange(e) {
//这里定义一个depth,来存储当前数据属于多少层结构,方便后续操作数据
if (e == 4) {
//多选,无,无
this.treeMenusData = this.firstMenuData
this.depth = 1
} else if (e == 2) {
//单选,无,无
this.treeMenusData = this.firstMenuData
this.depth = 1
} else if (e == 5) {
//多选,多选,无
this.treeMenusData = this.secondMenuData
this.depth = 2
} else if (e == 3) {
//多选,多选,多选
this.treeMenusData = this.thirdMenuData
this.depth = 3
}
...
}
3. 组件递归实现配置组件选项
这里的实现有点像ant,element组件库里的数选择,大佬其实推荐我使用render函数来实现会更好,因为组件递归容易出问题,奈何本人当时对render函数编写还不太有把握,所以还是用了另一个方法:组件递归
实现效果:
实现代码如下:
<template>
<div>
<div v-for="(v, i) in list" :key="i" style="margin-bottom: 20px">
<div class="firstInput">
<img
v-if="i == 0"
style="width: 24px; height: 24px; align-self: center"
src="~@/assets/add.png"
@click="firstAdd(v,i)"
/>
<img
v-if="i > 0"
style="width: 24px; height: 24px; align-self: center"
src="~@/assets/delete.png"
@click="firstDel(v,i)"
/>
<a-input v-model="v.name" @blur="e=>inputChange(e,v,i)" autocomplete="on"></a-input>
</div>
<div :style="styleObj" v-if="v.children.length > 0">
<cascader-config :list="v.children" :fatherIndex="i"></cascader-config>
</div>
</div>
</div>
</template>
组件递归实质就是 组件内部调用自身,特别契合我使用的树形结构,这里在递归时要注意判断当前节点的children
是否为空,否则会存在内存泄露的问题。
4. 对递归组件的数据增删改
本人在实现这一部分的逻辑时,花的时间是最多的,根据一开始的开发设计,我在操作数据时,不知道当前数据到底是第几层,所以在执行操作数据前 一. 先循环一遍数组,给每一层的每一个节点加上一个deep,代表当前层级(这里可以改为递归实现)
this.treeMenusData.forEach((v, i) => {
v.deep = 1
if (v.children.length > 0) {
v.children.forEach((k, j) => {
k.deep = 2
if (k.children.length > 0) {
k.children.forEach((a, m) => {
a.deep = 3
a.nodeIndex = i
//设置第三层数据爷节点,方便后续定位修改数据
})
}
})
}
})
二. 根据设置的deep,操作对应层次的数据,这里以删除为例
if (v.deep == 1) {
this.treeMenusData.splice(i, 1)
} else if (v.deep == 2) {
this.treeMenusData[fi].children.splice(i, 1)
} else if (v.deep == 3) {
this.treeMenusData[v.nodeIndex].children[fi].children.splice(i, 1)
}
i
:当前层次的当前节点fi
:当前节点的上一层节点,该数据是在递归组件时,当前递归到的组件获取的上一层的index属性
<div :style="styleObj" v-if="v.children.length > 0">
<cascader-config :list="v.children" :fatherIndex="i"></cascader-config>
</div>
nodeIndex
:根节点(只有第三层数据有设置该字段)详情见上一步循环数组的操作
在做新增操作的时候 会略微麻烦点,eg.因为两层结构的第二层
和三层结构的第二层
push的节点children一个为空一个不为空,所以要结合之前的depth层次字段加多一层判断再对应push进去。
if (this.depth == 3) {
if (v.deep == 1) {
//三层新增第一层
...
} else if (v.deep == 2) {
//三层下新增第二层
...
} else if (v.deep == 3) {
//三层下新增第三层
...
} else if (this.depth == 2) {
if (v.deep == 1) {
//两层新增第一层
...
} else if (v.deep == 2) {
//两层下添加第二层
...
} else if (this.depth == 1) {
//一层下新增第一层
...
}
5. 对已配置的字段数据的查询修改
再一个就是查询,这里的查询操作其实就是配置后想要追加、编辑或者删除选项的时候回显已配置选项
查询的时候 涉及的组件数据传输比较多
(1)点击自定义时传送对应字段的key,主页面根据key查询对应的数据list
openEditModal() {
this.$emit('openEditModal', this.key)
}
this.accessComponentList.find((v, i) => {
//根据对应的key找出对应节点的数据,存起来作为当前要修改的数据
if (v.keyword == key) {
this.editList = v.children
}
})
<edit-modal
v-if="editModalConfig.show"
:visible="editModalConfig.show"
:keyword="nowKey"
:list="editList"
@change="treeChange"
@delete="treeDelete"
@closeModal="closeEditModal"
></edit-modal>
(2)往递归组件传入list渲染
<cascader-config ref="getList" :list="editList"></cascader-config>
(3)增删改操作和前面第四步描述一样,这里增删改数据传输用的是eventBus
组件通信,同时要注意 要在组件销毁时结束掉eventBus
否则他会时刻监听着bus事件
beforeDestroy() {
// 注销监听事件
bus.$off(['editConfig'])
bus.$off(['addConfig'])
bus.$off(['delConfig'])
},
6. 编写对应组件类型
接下来就是枯燥的各类型组件的编写,基本就是组件库radio、checkbox的组合。然后编写勾选逻辑,这里设计的数据结构是根据当前节点的checkStatus
为1或者0来判断是否勾选。做到checkbox的时候,还是要感慨一下element的组件库api做的比ant完善太多了,有个api是直接根据值判断是否勾选
<el-checkbox
:disabled="v.checkStatus == 1 && a.checkStatus == 1 ? false : true"
:true-label="1"
:false-label="0"
:value="j.checkStatus"
@change="(e) => onChange(e, j, m)"
style="position: relative"
>
{{ j.name }}
<div class="dottedBox"></div>
</el-checkbox>
一些有趣的小需求
(1).在做三级组件的时候,pm的需求是对于第三级别的数据要用一个连线的方式实现,如下图
这里一开始以为很好实现,到做的时候居然脑子突然短路,寻求大佬意见,丢下一句:数据驱动。
仔细想想,有道理,这里的数据都是v-for
循环出来的,那就在循环体内部用absolute
相对checkbox选项定位。
<div class="dottedBox"></div>
样式:
.dottedBox {
//其实就是把上边和左边隐藏掉的 虚线长方形
border: 1px dashed rgba(33, 157, 245, 0.5);
border-top: none;
border-right: none;
width: 26px;
height: 38px;
position: absolute;
top: -23px;
left: -39px;
margin-top: none;
}
(2).实现对数据面板的折叠,如下图
这里是一个两层数据组件,可以随意第一层下的数据进行折叠
1.这里其实通过定义一个数组,存入需要折叠数据的节点。
childrenShow(item) {
//判断数组里是否有该节点
const isShow = this.showList.includes(item)
console.log(a)
if (isShow) {
this.showList = this.showList.filter((v, i) => {
return v != item
})
} else {
this.showList.push(item)
}
},
2.编写小箭头旋转样式
<img
:class="{ arrowTransform: !showList.includes(v.name), arrow: showList.includes(v.name) }"
src="~@/assets/arrow.png"
@click="childrenShow(v.name)"
/>
.arrow {
width: 16px;
height: 16px;
align-self: center;
cursor: pointer;
transition: 0.2s;
transform-origin: center;
transform: rotateZ(0deg);
}
.arrowTransform {
width: 16px;
height: 16px;
align-self: center;
cursor: pointer;
transition: 0.2s;
transform-origin: center;
transform: rotateZ(90deg);
}
小结
以上就是我初次尝试特殊的动态表单实现啦,也算是工作以来做的最久的一个模块吧,从一开始的听到需求内心无限mmp,到动手实现逐步入正轨,到最后实现需求,这种心理变化也是工作历练的一部分。其实现在还是挺感激pm出的一些不常见的需求的,可以让我打磨自己的技术,而不是每天都是重复无聊的简单需求。
Btw 自身技术还是有很多要学习的,如果有大佬能指点一二 那就更感激了,如有不妥或者更好的方法,欢迎指正交流!