Vue树形组件如何实现

237 阅读3分钟

实现树形其实最基本的思想就是递归。

效果图:

image.png 首先是数据结构,也就是渲染这个树形组件需要什么样子的数据。下面是数据的例子:

  treeList: [
        {
          label: "一级1",
          show: false, // 是否展开
          children: [
            {
              label: "二级1",
              show: false,
              children: [
                {
                  label: "三级1",
                  show: false,
                },
              ],
            },
            {
              label: "二级2",
              show: false,
            },
          ],
        },
        {
          label: "一级2",
          show: true,
          children: [
            {
              label: "二级2",
              show: true,
            },
            {
              label: "二级2",
              show: true,
            },
          ],
        },
      ],

然后的组件的布局

<template>
  <div class="tree">
    <div class="tree-item" v-for="(item, i) in treeList" :key="i">
      <div class="label">{{ item.label }}</div>
      <div class="tree-children">
       <div class="tc-item" v-for="(v, j) in item.children" :key="j">
         <div class="label">{{ v.label }}</div>
         <div class="tree-children"></div>
       </div>
      </div>
    </div>
  </div>
</template>
1
<script>
export default {
  name: "Tree",
  data() {
    return {
      treeList: [
        {
          label: "一级1",
          show: false, // 是否展开
          children: [
            {
              label: "二级1",
              show: false,
              children: [
                {
                  label: "三级1",
                  show: false,
                },
              ],
            },
            {
              label: "二级2",
              show: false,
            },
          ],
        },
        {
          label: "一级2",
          show: true,
          children: [
            {
              label: "二级2",
              show: true,
            },
            {
              label: "二级2",
              show: true,
            },
          ],
        },
      ],
    };
  },
  methods: {},
};
</script>

<style scoped>
.label {
  height: 40px;
  line-height: 40px;
}

.tree-children {
  padding: 0 20px;
}
</style>

image.png

其实看布局,已经有递归的意思了,所以就可以把第二层v-for循环抽离成组件,我这里就叫TreeItem了, 抽离之后, Tree.vue组件布局

<template>
  <div class="tree">
    <div class="tree-item" v-for="(item, i) in treeList" :key="i">
      <div class="label">{{ item.label }}</div>
      <TreeItem :item="v"></TreeItem>
    </div>
  </div>
</template>

TreeItem.vue组件

<template>
  <div class="tree-children">
    <div class="tc-item" v-for="(v, j) in item.children" :key="j">
      <div class="label">
        {{ v.label }}
      </div>
      <TreeItem
        v-if="v.show"
        :item="v"
      ></TreeItem>
    </div>
</template>

<script>
export default {
  name: "TreeItem",
  props: {
    item: {
      type: Object,
      default: () => {},
    },
  },
  data() {
    return {};
  },
  methods: {},
};
</script>

<style scoped>
.tree-children {
  padding: 0 20px;
}

.label {
  height: 40px;
  cursor: pointer;
  line-height: 40px;
}
</style>

TreeItem组件通过props接收外层循环传递的item,然后在组件自身递归调用自身。这样基本的树形结构就出渲染出来了,效果大概是这样。

image.png

然后我们需要控制节点的是否展开,在数据结构中我已经用show这个字段来控制是否展开。展开收缩其实只要控制TreeItem组件是否显示就可以做到了。 Tree.vue组件

<template>
  <div class="tree">
    <div class="tree-item" v-for="(item, i) in treeList" :key="i">
      <div class="label" @click="toggleCollage(item)">{{ item.label }}</div>
      <TreeItem v-if="item.show" :item.sync="item"></TreeItem>
    </div>
  </div>
</template>

TreeItem.vue组件

<template>
 <div class="tree-children">
    <div class="tc-item" v-for="(v, j) in item.children" :key="j">
      <div class="label" @click="toggleCollage(v)">
        {{ v.label }}
      </div>
      <TreeItem
        v-if="v.show"
        :item.sync="v"
      ></TreeItem>
    </div>
  </div>
</template>

一级1节点未展开,一级2展开了,效果:

image.png

然后我们思路肯定是通过点击节点来展开收缩子节点。 在这之前我们先把数据踢出去,通过外部传入,然后将TreeItem.vue组件通过ElementUI的 el-collapse-transition 组件包裹(没有安装ElementUI记得通过npm i element-ui安装下),以便后面展开收缩有过渡的效果,这样看起来更好,没有那么僵硬。

image.png

image.png

image.png 接着在给节点绑定点击事件,给组件绑定自定义事件,节点点击让show字段取反即可。

image.png 父组件需要在节点展开收缩做一些逻辑,这里绑定的自定义事件node-click,将值传递给父组件。 然后简单再做一些样式上的调整,hover效果和label前面的小箭头(有子节点的就显示小箭头)。

image.png

image.png

最后完整代码: Tree.vue

<template>
  <div class="tree">
    <div class="tree-item" v-for="(item, i) in treeList" :key="i">
      <div class="label" @click="toggleCollage(item)">
        <i
          :style="{ transform: item.show ? 'rotate(90deg)' : '' }"
          class="el-icon-caret-right rotate"
          v-if="item.children && item.children.length"
        ></i>
        {{ item.label }}
      </div>
      <template>
        <el-collapse-transition>
          <TreeItem
            @node-click="nodeClick"
            v-if="item.show"
            :item.sync="item"
          ></TreeItem>
        </el-collapse-transition>
      </template>
    </div>
  </div>
</template>

<script>
import TreeItem from "./TreeItem.vue";
export default {
  name: "Tree",
  props: {
    treeList: {
      type: Array,
    },
  },
  components: {
    TreeItem,
  },
  data() {
    return {};
  },
  methods: {
    toggleCollage(item) {
      console.log(item);
      const findRes = this.treeList.find((v) => v.label === item.label);
      findRes.show = !findRes.show;
      this.$emit("node-click", item, this.treeList);
    },
    nodeClick(node, treeList) {},
  },
};
</script>

<style scoped>
.label {
  height: 40px;
  cursor: pointer;
  line-height: 40px;
}

.rotate {
  transform-origin: 50% 40%;
  transition: all 0.5s ease;
}

.label:hover {
  background-color: #f5f7fa;
}
</style>

TreeItem.vue

<template>
  <div class="tree-children">
    <div class="tc-item" v-for="(v, j) in item.children" :key="j">
      <div class="label" @click="toggleCollage(v)">
        <i
          class="el-icon-caret-right rotate"
          :style="{ transform: v.show ? 'rotate(90deg)' : '' }"
          v-if="v.children && v.children.length"
        ></i>
        {{ v.label }}
      </div>
      <template>
        <el-collapse-transition>
          <TreeItem
            @node-click="nodeClick"
            v-if="v.show"
            :item.sync="v"
          ></TreeItem>
        </el-collapse-transition>
      </template>
    </div>
  </div>
</template>

<script>
export default {
  name: "TreeItem",
  props: {
    item: {
      type: Object,
      default: () => {},
    },
  },
  data() {
    return {};
  },
  methods: {
    toggleCollage(item) {
      item.show = !item.show;
      this.$emit("node-click", item, item.children);
    },
    nodeClick(node, treeList) {},
  },
};
</script>

<style scoped>
.tree-children {
  padding: 0 20px;
}

.label {
  height: 40px;
  cursor: pointer;
  line-height: 40px;
}

.rotate {
  transform-origin: 50% 40%;
  transition: all 0.5s ease;
}

.label:hover {
  background-color: #f5f7fa;
}
</style>

使用组件App.vue

<template>
  <div class="box">
    <Tree @node-click="nodeClick" :treeList.sync="treeList"></Tree>
  </div>
</template>

<script>
import Tree from "./components/Tree/index.vue";
export default {
  name: "App",
  components: {
    Tree,
  },
  data() {
    return {
      treeList: [
        {
          label: "一级1",
          show: false, // 是否展开
          children: [
            {
              label: "二级1",
              show: false,
              children: [
                {
                  label: "三级1",
                  show: false,
                },
              ],
            },
            {
              label: "二级2",
              show: false,
            },
          ],
        },
        {
          label: "一级2",
          show: true,
          children: [
            {
              label: "二级2",
              show: true,
            },
            {
              label: "二级2",
              show: true,
            },
          ],
        },
      ],
    };
  },
  methods: {
    nodeClick(node, treeList) {
      console.log(node, treeList);
    },
  },
};
</script>

<style scoped>
.box {
  width: 100%;
  height: 100%;
}
</style>

有问题可以提出来,希望对您有所帮助,感谢您的观看。