如何实现一个Tree组件

本文介绍了一个使用 Vue.js 开发的自定义树形组件的实现过程,包括核心思路是递归,以及组件的结构和调用方式。组件包含外层的树组件(index.vue)和内部的树节点组件(treeNode.vue),实现了节点的展开、折叠和选中功能,并允许自定义样式。通过数据结构定义树节点,提供了展示和操作树形数据的灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


开发技术:vue

实现思路

写这个组件的初衷就是因为上级需要一个展示demo,内容就是一个竖向滚动卡片与目录展示,想着内容不多,也懒得去用什么第三方组件库了,就干脆自己实现一个树组件,也不用多么高大上,拥有基本功能就行。
其实,树组件的核心实现思想就是递归
所以就先定义了一下数据结构:

data:[
	{
    	key: '0',
        title: '0',
        children: [
          {
            key: '0-1',
            title: '0-1',
            children: []
          },
          {
            key: '0-2',
            title: '0-2',
            children: []
           },
           {
             key: '0-3',
             title: '0-3',
             children: []
           },
           {
             key: '0-4',
             title: '0-4',
             children: [
               {
                 key: '0-4-0',
                 title: '0-4-0',
                 children: []
               },
               {
                 key: '0-4-1',
                 title: '0-4-1',
                 children: []
               },
               {
                 key: '0-4-2',
                 title: '0-4-2',
                 children: []
               }
             ]
           }
         ]
       },
       {
         key: '1',
         title: '1',
         children: []
       }
]

组件实现

这个组件主要有两个部分,一个包裹树节点的外层组件(index.vue),负责树的一些统一样式与状态管理;一个树节点组件,负责递归展示各节点(treeNode.vue)。两个放在一个目录(Tree)下,都是组件的组成部分。

树组件(index.vue)

果然还是代码最简捷明了。

<template>
  <div class="tree">
    <ul>
      <tree-node
        v-on="$listeners"
        v-for="item in $attrs.treeList"
         v-bind="$attrs"
        :treeItem="item"
        :key="item.key"
        @changeTree="changeTree"
        :expandedKeys="expandedList"
      />
    </ul>
  </div>
</template>
<script>
import TreeNode from '@components/Tree/treeNode'
export default {
  name: 'Tree',
  data() {
    return {
      expandedList: [] // 已展开节点
    }
  },
  components: { TreeNode },
  created() {
    this.expandedList = this.$attrs.expandedKeys
  },
  methods: {
  // 展开事件
    changeTree(key) {
      if (this.expandedList.includes(key)) {
        this.expandedList.splice(this.expandedList.indexOf(key), 1)
      } else {
        this.expandedList.push(key)
      }
    }
  }
}
</script>
<style lang="less" scoped>
.tree {
  line-height: 20px;
  ul > li {
    padding-left: 0;
  }
  .treeNode-show-line:not(:last-child):before {
    position: absolute;
    left: 6px;
    width: 1px;
    height: calc(100% - 22px);
    margin: 22px 0 0;
    border-left: 1px solid #d9d9d9;
    content: ' ';
  }
}
</style>

树节点组件(treeNode.vue)

<template>
  <li :class="['treeNode',$attrs.showLine?'treeNode-show-line':'']" v-if="$attrs.treeItem.children.length>0">
    <div
      @click="changeTree($attrs.treeItem.key)"
      :class="['expandedIcon', $attrs.expandedKeys.includes($attrs.treeItem.key)?'close':'open']"
    ></div>
    <span :class="['treeNode-content',$attrs.selected===$attrs.treeItem.key?'selected':'']">{{$attrs.treeItem.title}}</span>
    <ul v-if="$attrs.expandedKeys.includes($attrs.treeItem.key)">
      <tree-node
        v-on="$listeners"
        v-for="item in $attrs.treeItem.children"
        :treeItem="item"
        :key="item.key"
         v-bind="$attrs"
      />
    </ul>
  </li>
  <li :class="['treeNode',$attrs.showLine?'treeNode-show-line':'']" v-else>
    <div class="expandedIcon leaf"></div>
    <span :class="['treeNode-content',$attrs.selected===$attrs.treeItem.key?'selected':'']">{{$attrs.treeItem.title}}</span>
  </li>
</template>
<script>
import TreeNode from '@components/Tree/treeNode'
export default {
  name: 'TreeNode',
  components: {
    TreeNode
  },
  methods: {
    changeTree(key) {
      this.$emit('changeTree', key)
    }
  }
}
</script>
<style lang="less" scoped>
.treeNode {
  padding: 4px 0 4px 18px;
  position: relative;
  .treeNode-show-line:not(:last-child):before {
    position: absolute;
    left: 20px;
    width: 1px;
    height: calc(100% - 22px);
    margin: 22px 0 0;
    border-left: 1px solid #d9d9d9;
    content: ' ';
  }
  .expandedIcon {
    width: 16px;
    height: 16px;
    display: inline-block;
    &.open {
      background: url('data:image/png;base64,iVBORw...FTkSuQmCC') // 展开图标,需要自己设置,也可以弄成动态的属性
        no-repeat;
      background-size: 100%;
    }
    &.close {
      background: url('data:image/png;base64,iVBOR...I=')// 关闭图标,需要自己设置,也可以弄成动态的属性
        no-repeat;
      background-size: 100%;
    }
    &.leaf {
      background: url('data:image/png;base64,iVBORw0KG...YII=')// 叶子节点图标,需要自己设置,也可以弄成动态的属性
        no-repeat;
      background-size: 100%;
    }
  }
  .treeNode-content {
    padding: 0 5px;
  }
  .selected {
    font-weight: bold;
  }
}
</style>

调用

最后调用Index.vue就可以啦。

<template>
	<Tree
	  :treeList="treeList"
	  :expandedKeys="expandedKeys"
	  :selected="selected"
	  :showLine="showLine"
	/>
</template>
<script>
import Tree from '@components/Tree'
export default {
  name: 'Tsubasa',
  data() {
    return {
      treeList: [...], // 要展示的数据
      expandedKeys:[], //已展开节点
      selected:'',//选中节点
      showLine:true // 是否显示连接线
    }
  },
  components: { Tree },
}
</script>

因为是自己开发的组件,所以更多样式细节和功能可以自己修改和添加。
我的实现肯定不是最好最优的,之后有时间我会继续完善的。

最后附上项目地址:https://github.com/Gshengx/demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值