自定义的树形组件(实现动态展开,单选,全选,半选,根节点单选子节点全部选择,子节点单选父节点半选,叶子节点横向排列并且自动换行)

231 阅读5分钟

1.前情提要

该树形组件实现的功能:实现动态展开,单选,全选,半选,根节点单选子节点全部选择,子节点单选父节点半选,叶子节点横向排列并且自动换行 数据源:首先数据源很重要,要配置好几个属性,才能实现好一个树组件,expand是控制是否展开,checked是否勾选,parentCode是父节点的id(如果是根节点则为0),indeterminate是半选样式的控制。 复选框用了iview组件的复选框是为了方便用那个半选的样式

2.实现方法

2.1 简单介绍

创建一个子组件,然后导入到父组件,然后在父组件中用ul去包裹这个子组件循环创建li 在子组件中自己递归创建自己。 动态展开:给那个展开按钮加一个点击事件控制数据源的expand进行改变值同时v-show去判断这个expand的真假后进行是否展开 单选:点击之后将本节点checked变为true就好,复选框记得双向绑定 全选: 点击根节点的全选,是调用到了父组件上的方法 点击二级节点的全选是调用子组件的方法 半选:复选框双向绑定一个样式,然后子节点不是全选父节点就是半选(如何判断就是调用那个方法) 叶子节点横向排列并且自动换行:去判断叶子节点的下一层是否有节点,没有就给叶子节点的父节点修改布局样式 <ul v-if="item.children && item.children.length" class="children-list" v-show="item.expand" :style="getChildrenListStyle(item.children)" > <tree-item v-for="(child, index) in item.children" :key="index" :item="child" @change="getCheckedNodes" :tree="tree" /> getChildrenListStyle(children) { // 设置叶子节点横向排列 if (!children.length || children.every(child => !child.children || !child.children.length)) { return { display: 'flex', flexDirection: 'row', flexWrap: 'wrap' } } return {} },

2.2关键的四个个方法

2.2.1监听复选框被选中后发生的事件

getCheckedNodes({ item, checked }) {
      console.log(checked)
      // 更新当前节点的选中状态
      this.$set(item, 'checked', checked)
      this.$set(item, 'indeterminate', false)
      // 递归更新子节点的选中状态
      if (item.children) {
        this.setChildrenChecked(item.children, checked)
      }
      //   递归更新父节点的选中状态
      this.updateParentChecked(item, checked)
    },

2.2.2更新子节点状态的方法

就是递归遍历子节点修改子节点的属性, 为什么使用this.set?使用this.set? 使用this.set方法是将当前子节点的checked,属性设置为传入的checked参数值。这样做可以确保checked属性是响应式的,其变化能够触发视图的更新。

setChildrenChecked(children, checked) {
  children.forEach(child => {
    this.$set(child, 'checked', checked)
    this.$set(child, 'indeterminate', false)
    if (child.children) {
      this.setChildrenChecked(child.children, checked)
    }
  })
},

2.2.3寻找到勾选节点的父节点的id

这一步很重要就是可以去向上遍历找到哪些是勾选节点的父节点的id,返回这些id给到更新父节点状态的方法
//根据子节点的parentCode(父节点的id)去找父节点
findNodeById(nodes, id) {
  for (let node of nodes) {
    if (node.id === id) {
      return node
    }
    if (node.children && node.children.length > 0) {
      const found = this.findNodeById(node.children, id)
      if (found) {
        return found
      }
    }
  }
  return null
}

2.2.4更新父节点状态

重点是传入一个数据源和当前勾选节点的父节点id,然后根据返回的值先去判断勾选节点的同级节点是全选了还是部分选了。然后再进行更新状态 // //更新父节点的状态 updateParentChecked(node, checked) { if (!node.parentCode) return // 如果没有父节点,则停止递归 const parentNode = this.findNodeById(this.tree, node.parentCode) if (parentNode) { let allChildrenChecked = true let someChildrenChecked = false parentNode.children.forEach(child => { if (child.checked) { someChildrenChecked = true } else { allChildrenChecked = false } }) //更新一层父节点 if (allChildrenChecked) { this.set(parentNode,checked,true)this.set(parentNode, 'checked', true) this.set(parentNode, 'indeterminate', false) } //更新一层父节点 else if (someChildrenChecked) { this.set(parentNode,checked,false)this.set(parentNode, 'checked', false) this.set(parentNode, 'indeterminate', true) } //更新父节点的父节点 else { this.set(parentNode,checked,false)this.set(parentNode, 'checked', false) this.set(parentNode, 'indeterminate', true) }

    // 递归更新父节点的父节点
    console.log('4187187')
    this.updateParentChecked(parentNode)
  }
},

3.实现截图

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

4.源码

4.1父组件:

<ul>
      <tree-item
        v-for="(item, index) in 数据源"
        :key="index"
        :item="item"
        @change="getNodes"
        :tree="数据源"
      />
 </ul>
    getNodes({ item, checked }) {
      console.log(checked)
      // 更新当前节点的选中状态
      this.$set(item, 'checked', checked)
      this.$set(item, 'indeterminate', false)

      // 递归更新子节点的选中状态
      if (item.children) {
        this.setChildrenChecked(item.children, checked)
      }

      // 递归更新父节点的选中状态
      // this.updateParentChecked(item, checked)
    },
    //更新子节点的状态
    setChildrenChecked(children, checked) {
      console.log('52512', checked, children)
      children.forEach(child => {
        this.$set(child, 'checked', checked)
        this.$set(child, 'indeterminate', false)
        if (child.children) {
          this.setChildrenChecked(child.children, checked)
        }
      })
    },

4.2子组件:

 <template>
  <li>
    <div class="tree-item">
      <span class="toggle" v-if="item.children && item.children.length" @click="toggle(item)">{{
        item.expand ? '▾' : '▸'
      }}</span>
      <label>
        <BCheckbox :indeterminate="item.indeterminate" v-model="item.checked" @on-change="handleCheckboxChange(item)">
          {{ item.title }}</BCheckbox
        >
      </label>
    </div>
    <ul
      v-if="item.children && item.children.length"
      class="children-list"
      v-show="item.expand"
      :style="getChildrenListStyle(item.children)"
    >
      <tree-item
        v-for="(child, index) in item.children"
        :key="index"
        :item="child"
        @change="getCheckedNodes"
        :tree="tree"
      />
    </ul>
  </li>
</template>
<script>
export default {
  name: 'TreeItem',
  props: {
    item: {
      type: Object,
      required: true
    },
    tree: {
      type: Object
    }
  },
  data() {
    return {}
  },

  methods: {
    getChildrenListStyle(children) {
      // 设置叶子节点横向排列
      if (!children.length || children.every(child => !child.children || !child.children.length)) {
        return {
          display: 'flex',
          flexDirection: 'row',
          flexWrap: 'wrap'
        }
      }
      return {}
    },
    toggle(item) {
      item.expand = !item.expand
    },
    handleCheckboxChange(item) {
      this.$emit('change', { item, checked: item.checked })
    },
    //以下是新增组件用到的方法
    getCheckedNodes({ item, checked }) {
      console.log(checked)
      // 更新当前节点的选中状态
      this.$set(item, 'checked', checked)
      this.$set(item, 'indeterminate', false)
      // 递归更新子节点的选中状态
      if (item.children) {
        this.setChildrenChecked(item.children, checked)
      }
      //   递归更新父节点的选中状态
      this.updateParentChecked(item, checked)
    },
    //更新子节点
    setChildrenChecked(children, checked) {
      children.forEach(child => {
        this.$set(child, 'checked', checked)
        this.$set(child, 'indeterminate', false)
        if (child.children) {
          this.setChildrenChecked(child.children, checked)
        }
      })
    },
    // //更新父节点的状态
    updateParentChecked(node, checked) {
      if (!node.parentCode) return // 如果没有父节点,则停止递归
      const parentNode = this.findNodeById(this.tree, node.parentCode)
      if (parentNode) {
        let allChildrenChecked = true
        let someChildrenChecked = false
        parentNode.children.forEach(child => {
          if (child.checked) {
            someChildrenChecked = true
          } else {
            allChildrenChecked = false
          }
        })
        //更新一层父节点
        if (allChildrenChecked) {
          this.$set(parentNode, 'checked', true)
          this.$set(parentNode, 'indeterminate', false)
        } //更新一层父节点
        else if (someChildrenChecked) {
          this.$set(parentNode, 'checked', false)
          this.$set(parentNode, 'indeterminate', true)
        } //更新父节点的父节点
        else {
          this.$set(parentNode, 'checked', false)
          this.$set(parentNode, 'indeterminate', true)
        }

        // 递归更新父节点的父节点
        console.log('4187187')
        this.updateParentChecked(parentNode)
      }
    },

    //根据子节点的parentCode(父节点的id)去找父节点
    findNodeById(nodes, id) {
      for (let node of nodes) {
        if (node.id === id) {
          return node
        }
        if (node.children && node.children.length > 0) {
          const found = this.findNodeById(node.children, id)
          if (found) {
            return found
          }
        }
      }
      return null
    }
  }
}
</script>
<style scoped>
.tree-item {
  display: flex;
  align-items: center;
  margin-left: 20px;
}

.toggle {
  cursor: pointer;
  font-size: 24px;
}

.children-list {
  list-style-type: none;
  margin-left: 50px;
}
.children-list-flex {
  display: flex;
  flex-direction: row;
}
</style>

4.3示例数据源

[  
  {  
    "id": "root1",  
    "title": "根节点1",  
    "expand": true,  
    "checked": false,  
    "parentCode": 0,  
    "indeterminate": false,  
    "children": [  
      {  
        "id": "child1-1",  
        "title": "二级子节点1-1",  
        "expand": true,  
        "checked": false,  
        "parentCode": "root1",  
        "indeterminate": false,  
        "children": [  
          {  
            "id": "grandchild1-1-1",  
            "title": "三级子节点1-1-1",  
            "expand": true,  
            "checked": false,  
            "parentCode": "child1-1",  
            "indeterminate": false  
          },  
          {  
            "id": "grandchild1-1-2",  
            "title": "三级子节点1-1-2",  
            "expand": true,  
            "checked": false,  
            "parentCode": "child1-1",  
            "indeterminate": false  
          }  
        ]  
      },  
      {  
        "id": "child1-2",  
        "title": "二级子节点1-2",  
        "expand": true,  
        "checked": false,  
        "parentCode": "root1",  
        "indeterminate": false,  
        "children": [  
          {  
            "id": "grandchild1-2-1",  
            "title": "三级子节点1-2-1",  
            "expand": true,  
            "checked": false,  
            "parentCode": "child1-2",  
            "indeterminate": false  
          },  
          {  
            "id": "grandchild1-2-2",  
            "title": "三级子节点1-2-2",  
            "expand": true,  
            "checked": false,  
            "parentCode": "child1-2",  
            "indeterminate": false  
          }  
        ]  
      }  
    ]  
  },  
  {  
    "id": "root2",  
    "title": "根节点2",  
    "expand": true,  
    "checked": false,  
    "parentCode": 0,  
    "indeterminate": false,  
    "children": [  
      // 省略了与根节点1类似的二级和三级子节点...  
    ]  
  }  
]