1. 程式人生 > 實用技巧 >基於elementui checkbox樹形多級聯動

基於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。後面說要支援更多層,發現當初這樣子已經無法實現,當初寫的太呆了,
於是重新寫了一版,通過這次對遞迴的使用也有了一些理解,因為以前很少使用這個,也算是學習到了,記錄一下。