基于elementui checkbox树形多级联动

背景

公司业务有个角色权限设置的需求,数据可能有5到6层的权限,本来是想直接使用elementuiel-tree组件的,奈何ui难以修改,要做成公司想要的样子,只好自己写了。

数据结构

后台返回的数据结构是这样的:

接口权限数据
{
  code: 0,
  msg: null,
  data: [
    {
      applicationModule: 'xxx',
      menuTreeList: [
        {
          id: 40000,
          parentId: -1,
          children: [
            {
              id: 40005,
              parentId: 40000,
              children: [],
              name: 'xxx',
              label: 'xxx',
            },
            {
              id: 40002,
              parentId: 40000,
              children: [
                {
                  id: 40004,
                  parentId: 40002,
                  children: [
                    {
                      id: 40006,
                      parentId: 40004,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                    {
                      id: 40007,
                      parentId: 40004,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                  ],

                  name: 'xxx',

                  label: 'xxx',
                },
                {
                  id: 40003,
                  parentId: 40002,
                  children: [],

                  name: 'xxx',

                  label: 'xxx',
                },
              ],

              name: 'xxx',

              label: 'xxx',
            },
            {
              id: 40001,
              parentId: 40000,
              children: [
                {
                  id: 40012,
                  parentId: 40001,
                  children: [],

                  name: 'xxx',

                  label: 'xxx',
                },
                {
                  id: 40009,
                  parentId: 40001,
                  children: [
                    {
                      id: 40015,
                      parentId: 40009,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                    {
                      id: 40017,
                      parentId: 40009,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                    {
                      id: 40016,
                      parentId: 40009,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                  ],

                  name: 'xxx',

                  label: 'xxx',
                },
                {
                  id: 40014,
                  parentId: 40001,
                  children: [
                    {
                      id: 40021,
                      parentId: 40014,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                    {
                      id: 40020,
                      parentId: 40014,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                  ],

                  name: 'xxx',

                  label: 'xxx',
                },
                {
                  id: 40011,
                  parentId: 40001,
                  children: [],

                  name: 'xxx',

                  label: 'xxx',
                },
                {
                  id: 40008,
                  parentId: 40001,
                  children: [],
                  icon: null,
                  name: 'xxx',

                  label: 'xxx',
                },
                {
                  id: 40013,
                  parentId: 40001,
                  children: [
                    {
                      id: 40018,
                      parentId: 40013,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                    {
                      id: 40019,
                      parentId: 40013,
                      children: [],

                      name: 'xxx',

                      label: 'xxx',
                    },
                  ],

                  name: 'xxx',

                  label: 'xxx',
                },
                {
                  id: 40010,
                  parentId: 40001,
                  children: [],
                  name: 'xxx',
                  label: 'xxx',
                },
              ],
              name: 'xxx',
              label: 'xxx',
            },
          ],
          name: 'xxx',
          label: 'xxx',
        },
      ],
    },
  ],
}

后台会返回一个数组,每个数组对象对应一个菜单,权限数据都在menuTreeList数组里。

权限选择的ui大概的样子:

拆分组件

父组件

  • 引入封装好的组件checkboxTree,将需要的数据传入。
<checkboxTree ref="checkTreeRef" :role-list="tableData"></checkboxTree>
  • 编辑回显时,调用子组件的方法
this.$refs.checkTreeRef.refurbishTreeCheckStatus(res.data, this.tableData)
  • 初次拿到数据时,将后台返回的数据重新设置一下,给予初始的选中以及半选状态
this.tableData = this.$refs.checkTreeRef.formatTreeData(res.data)
  • 保存权限时,拿到所有已选择权限的roleId
params.menuIds = this.$refs.checkTreeRef.returnAllCheckIds(this.tableData)

checkboxTree组件

html部分,写第一级的权限

<template>
  <div>
    <template v-for="item in roleList">
      <template v-for="treeData in item.menuTreeList">
        <div :key="treeData.id">
          <p class="check-group">
            <el-checkbox v-model="treeData.mychecked" :indeterminate="treeData.isIndeterminate" @change="handleCheckAllChange({ val: treeData, checked: $event })">
              {{ treeData.name }}
            </el-checkbox>
          </p>
          <checkboxTreeRender :tree-data="treeData" @handle-check-all-change="handleCheckAllChange"></checkboxTreeRender>
        </div>
      </template>
    </template>
  </div>
</template>

点击任何checkbox,都会进入到handleCheckAllChange方法,再通过findChildrenfindParent方法不断递归设置整个数据的选中以及半选状态,代码如下:

      handleCheckAllChange(data) {
        let { val, checked } = data
        if (val.children.length > 0) {
          // 处理下级
          this.findChildren(val.children, checked)
        } else {
          // 处理本级
          val.children.forEach((v) => {
            v.mychecked = checked
          })
        }
        if (val.parentId !== -1) {
          // 处理上级
          this.findParent(this.roleList, val.parentId)
        }
        val.isIndeterminate = false
      },
      // 设置子级
      findChildren(list, checked) {
        list.forEach((child) => {
          child.mychecked = checked
          child.isIndeterminate = false
          if (child.children.length > 0) {
            this.findChildren(child.children, checked)
          }
        })
      },
      // 设置这一整条线
      findParent(list, parentId) {
        list.forEach((k) => {
          if (k.menuTreeList) {
            k.menuTreeList.forEach((child) => {
              this.handleList(child, parentId)
            })
          } else {
            this.handleList(k, parentId)
          }
        })
      },
      // 设置这一整条线具体方法
      handleList(child, parentId) {
        let parentCheckedLength = 0
        let parentIndeterminateLength = 0
        if (child.id === parentId) {
          child.children.forEach((children) => {
            if (children.isIndeterminate) {
              parentIndeterminateLength++
            } else if (children.mychecked) {
              parentCheckedLength++
            }
          })
          child.mychecked = parentCheckedLength === child.children.length
          child.isIndeterminate = (parentIndeterminateLength > 0 || parentCheckedLength > 0) && parentCheckedLength < child.children.length
          if (child.parentId !== -1) {
            this.findParent(this.roleList, child.parentId)
          }
        } else if (child.children.length > 0) {
          this.findParent(child.children, parentId)
        }
      },

这是主要checkbox选择交互的联动逻辑,下面是一些工具方法,主要是用于业务保存时需要传递权限id,以及初始拿到后台数据时需要format一下,代码如下:

  const returnCheckTree = (data, checkArr = []) => {
    data.forEach((v) => {
      if (v.mychecked || v.isIndeterminate) {
        !checkArr.includes(v.id) && checkArr.push(v.id)
      }

      if (v.children && v.children.length) {
        returnCheckTree(v.children, checkArr)
      }
    })

    return checkArr
  }

  const fmtTreeData = (data) => {
    data.forEach((v) => {
      v.mychecked = false
      v.isIndeterminate = false

      if (v.children && v.children.length > 0) {
        fmtTreeData(v.children)
      }
    })
    return data
  }

  // 返回所有已选或权限的role
  returnAllCheckIds(currentData) {
    let roleIds = []
    currentData.forEach((k) => {
      roleIds = [...returnCheckTree(k.menuTreeList), ...roleIds]
    })

    return roleIds.join(',')
  },
  // 初始化树状数据
  formatTreeData(currentData) {
    currentData.forEach((k) => {
      fmtTreeData(k.menuTreeList)
    })

    return currentData
  },

最后,编辑角色时需要回显角色权限,后台返回给我的数据结构和全部权限是一致的,只是只会返回已经选择的权限数据,当然,对我来说,什么结构都无所谓,因为我这种做法,实际上是要递归把所有权限id丢到一个数组里面,
我的思路是先拿到所有的权限id数组放到roleIds里,然后将所有权限idroleIds里的对象设置为已选,再重新去设置半选,当前对象是已选,但children对象的已选比children的长度少,说明当前对象是半选。代码如下:

      const returnEditRoleTreeIds = (data, checkArr = []) => {
        data.forEach((v) => {
          !checkArr.includes(v.id) && checkArr.push(v.id)

          if (v.children && v.children.length) {
            returnEditRoleTreeIds(v.children, checkArr)
          }
        })

        return checkArr
      }
      
      // 编辑时回显权限数据
      refurbishTreeCheckStatus(checkData, allData) {
        let roleIds = []
        let firstLevelIds = []
        let notFirstLevelIds = []
        checkData.forEach((k) => {
          roleIds = [...returnEditRoleTreeIds(k.menuTreeList), ...roleIds]
        })
        allData.forEach((k) => {
          this.setTreeCheckStatus(k.menuTreeList, roleIds)
        })

        allData.forEach((k) => {
          this.setTreeIndeterminateStatus(k.menuTreeList)
        })
      },
      // 所有已选择的role全部设置为已选
      setTreeCheckStatus(data, roleIds = []) {
        data.forEach((v) => {
          if (roleIds.includes(v.id)) {
            v.mychecked = true
          }

          if (v.children && v.children.length) {
            this.setTreeCheckStatus(v.children, roleIds)
          }
        })
      },
      // 重新递归设置半选状态
      setTreeIndeterminateStatus(data) {
        data.forEach((v) => {
          let parentCheckedLength = 0
          let parentIndeterminateLength = 0
          v.children.forEach((children) => {
            if (children.isIndeterminate) {
              parentIndeterminateLength++
            } else if (children.mychecked) {
              parentCheckedLength++
            }
          })
          v.isIndeterminate = (parentIndeterminateLength > 0 || parentCheckedLength > 0) && parentCheckedLength < v.children.length

          if (v.children && v.children.length) {
            this.setTreeIndeterminateStatus(v.children)
          }
        })
      },

应该不是最好的思路,各位有更好的建议可以在评论区告诉我。

checkboxTreeRender组件

这个组件主要是递归组件,去渲染树形dom结构。

<template>
  <div>
    <div v-if="treeData.children && treeData.children.length" style="padding-left: 24px">
      <div v-for="childrenData in treeData.children" :key="childrenData.id" :style="returnStyle(childrenData.children)">
        <el-checkbox
          v-model="childrenData.mychecked"
          style="margin-bottom: 15px"
          :indeterminate="childrenData.isIndeterminate"
          :label="childrenData.id"
          @change="handleCheckAllChange({ val: childrenData, checked: $event })"
        >
          {{ childrenData.name }}
        </el-checkbox>
        <checkboxTreeRender :tree-data="childrenData" @handle-check-all-change="handleCheckAllChange"></checkboxTreeRender>
      </div>
    </div>
  </div>
</template>

接收一个数据对象

    props: {
      treeData: {
        type: Object,
        default: function () {
          return {}
        },
      },
    },

以及将checkbox变化的方法抛给父组件去处理,这个组件只负责渲染

      returnStyle(child) {
        const premise = child && child.length
        return {
          display: premise ? '' : 'inline-block',
          marginRight: premise ? '' : '30px',
        }
      },
      handleCheckAllChange(data) {
        this.$emit('handle-check-all-change', data)
      },

至此,一个基于elementui的多层checkbox树形联动组件就写好了。

结语

最开始需求是说最多只有三层结构,所以我就写了一版写死的三层联动的逻辑,使用了checkboxGroup,只需要在checkboxGroup上进行监听就能拿到下面所有选择的checkbox。后面说要支持更多层,发现当初这样子已经无法实现,当初写的太呆了,
于是重新写了一版,通过这次对递归的使用也有了一些理解,因为以前很少使用这个,也算是学习到了,记录一下。
全部源码放到github上了,传送门

原文地址:https://www.cnblogs.com/wangxi01/p/14034320.html