【记录有趣的需求】之二.实现配置多层次树形数据的动态表单

700 阅读7分钟

需求:

pm说,这次要开发的功能是供第三方使用的,所以我们无法得知第三方可能每次的字段要求,所以要求把表单做的灵活一点,提供多种表单类型供第三方自由选择配置。pm的要求是通过弹框选择要添加的组件类型,插入到页面。我一听,这不有点类似之前了解过的动态表单或者拖拽式表单嘛,虽然了解过,但是具体实现的话还没试过。一下子不知道从何入手,我不知道他具体要配置几个字段,不知道他要加什么类型的组件,所以页面写死用循环是不行的。这时候朋友的一句:用vue的动态组件。让我懵了,用vue这么久的我居然还不知道这东西,属实让我惭愧。就这样我有了入口思路,接下来的就好办了

先上效果图:

1.配置组件类型,字段标题,字段key image.png

2.确定后插入到表单页面 image.png

3.配置后可修改选项

image.png

4.修改完成后

image.png

这次开发主要遇到的难点

  • 怎么实现动态插入组件
  • 怎么实现一个类似树形组件来配置组件选项
  • 怎么处理树形数据结构
  • 判断当前操作项的数据层级

实现步骤:

1. 表单页面动态组件的实现

因为表单页面的字段数量、类型是完全不可知的,所以靠写死是完全行不通的,这里经高人指点,用了vue的动态组件选项。

1649820690(1).png

通过:is来指定渲染的组件,所以你只需要把需要的组件另起文件写好,在表单页引入在:is绑定一下即可,所以表单主页面的代码就很简洁了:

image.png 这里把要添加的组件类型以及对应的组件名称,数据,数据深度,标题,是否可编辑,字段名作为一个对象存入到数组里,再使用v-for把所有组件渲染出来。

2. 选择组件类型

这次做的组件和传统的动态表单组件类型完全不一样,传统常见的组件类型是输入框,单选radio,多选select那些。这次我要做的组件非常奇葩。。。全都是选择类型的,我们常做的都是单选,多选两种类型并且都是单层级的,这次做的有:

image.png

遇事不要慌,看着有很多类型,实际上他们都有同一个特点,都是数据的结构都可以用树形数据来渲染,只要判断该组件属于多少层的数据 传入对应的数据即可 这里先定义好三个对应的树形数据:

image.png

然后再根据选择出的组件类型,传入对应的数组(这里其实用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函数编写还不太有把握,所以还是用了另一个方法:组件递归

实现效果:

image.png

实现代码如下:

<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. 对已配置的字段数据的查询修改

再一个就是查询,这里的查询操作其实就是配置后想要追加、编辑或者删除选项的时候回显已配置选项

image.png

查询的时候 涉及的组件数据传输比较多

(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的需求是对于第三级别的数据要用一个连线的方式实现,如下图

image.png

这里一开始以为很好实现,到做的时候居然脑子突然短路,寻求大佬意见,丢下一句:数据驱动。

052b56f3c21d662d6ef82d8f8a5f2b8.jpg

仔细想想,有道理,这里的数据都是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).实现对数据面板的折叠,如下图

image.png

这里是一个两层数据组件,可以随意第一层下的数据进行折叠

image.png

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 自身技术还是有很多要学习的,如果有大佬能指点一二 那就更感激了,如有不妥或者更好的方法,欢迎指正交流!