基於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上了,傳送門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM