动手实现一个简单版的Tree树形组件

73 阅读1分钟

一百多行代码实现Tree树形组件,支持的propsevents是参考elementUI设置的,功能相对简单,支持配置如下:

Props

参数说明类型可选值默认值
data展示数据array————
showCheckbox是否可选择boolean——false
defaultExpandAll默认展开所有boolean——true
props配置项object————

Events

事件名说明回调参数
node-click节点被点击的回调该节点所对应的对象

组件代码

<template>
  <ul class="tree">
    <li class="tree-item" v-for="(item, index) in data" :key="item[props.key]">
      <div class="node-item" @click="clickNodeItem($event, item, index)">
        <!-- 展开收缩图标 -->
        <i v-show="item[props.children]?.length" :class="{ down: expandStates[index] }" class="icon"
          @click="changeExpandStatus(index)"></i>
        <!-- 多选框 -->
        <input v-if="showCheckbox" :disabled="item[props.disabled]" class="checkbox" type="checkbox">
        <!-- 节点的内容 -->
        <span class="label">{{ item[props.label] }}</span>
      </div>
      <template v-if="item[props.children]?.length">
        <!-- 使用 v-bind="$props" v-on="$listeners" 将props和绑定的事件监听往下传递, -->
        <WuTree v-show="expandStates[index]" :data="item[props.children]" v-bind="$props" v-on="$listeners"></WuTree>
      </template>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'WuTree',
  props: {
    // 展示数据
    data: {
      type: Array,
      default() {
        return []
      }
    },
    // 是否可选择
    showCheckbox: {
      type: Boolean,
      default: false
    },
    // 默认展开所有
    defaultExpandAll: {
      type: Boolean,
      default() {
        return false
      }
    },
    // 配置选项
    props: {
      type: Object,
      default() {
        return {
          label: 'label', // 指定节点标签为节点对象的某个属性值
          children: 'children', // 指定子树为节点对象的某个属性值
          key: 'id', // 每个节点表示,必须要唯一
          disabled: 'disabled' // 指定节点选择框是否禁用为节点对象的某个属性值
        }
      }
    }
  },
  data() {
    return {
      expandStates: [] // 展开状态
    }
  },
  created() {
    this.expandStates = this.data.map(() => this.defaultExpandAll)
  },
  methods: {
    // 节点点击回调
    clickNodeItem(e, item, index) {
      // 点击伸缩三角形按钮和多选框不往外排除 node-click事件
      const classNames = ['icon', 'checkbox']
      if (classNames.some(className => e.target.className.includes(className))) {
        return
      }
      this.$emit('node-click', item)
      if (item.children && item.children.length > 0) {
        this.changeExpandStatus(index)
      }
    },
    // 通过索引改变节点展开状态
    changeExpandStatus(index) {
      this.$set(this.expandStates, index, !this.expandStates[index])
    }
  }
}
</script>

<style lang="scss" scoped>
.tree {
  user-select: none;

  .tree-item {
    position: relative;
    padding-left: 20px;

    .node-item {
      display: flex;
      align-items: center;
      cursor: pointer;

      &:hover {
        background-color: #f0f7ff;
      }

      .icon {
        position: absolute;
        left: 6px;
        width: 0;
        height: 0;
        transform-origin: 0% 50%;
        border: 6px solid transparent;
        border-left-color: grey;
        transition: all 0.1s;

        &.down {
          transform: rotate(90deg);
        }
      }
    }

    .checkbox {
      margin-right: 5px;

      &:disabled {
        cursor: not-allowed;
      }
    }

  }
}
</style>

使用

<template>
  <div class="container">
    <WuTree :data="treeData" :showCheckbox="true" :default-expand-all="true" @node-click="handleNodeClick"></WuTree>
  </div>
</template>

<script>
import WuTree from '@/components/WuTree'
export default {
  name: 'Home',
  components: {
    WuTree
  },
  data() {
    return {
      treeData: [
        {
          id: 1,
          label: '一级 1',
          disabled: true,
          children: [{
            id: 4,
            label: '二级 1-1',
            children: [{
              id: 9,
              label: '三级 1-1-1'
            }, {
              id: 10,
              label: '三级 1-1-2'
            }]
          }]
        },
        {
          id: 2,
          label: '一级 2',
          children: [{
            id: 5,
            label: '二级 2-1'
          }, {
            id: 6,
            label: '二级 2-2'
          }]
        },
        {
          id: 3,
          label: '一级 3',
          children: [{
            id: 7,
            label: '二级 3-1'
          }, {
            id: 8,
            label: '二级 3-2'
          }]
        }]
    }
  },
  methods: {
    // 树形组件节点被点击回调
    handleNodeClick(e) {
      console.log('点击节点', e)
    }
  }
}
</script>

<style lang="scss" scoped>
.container {
  padding: 50px;
}
</style>

效果

动画.gif