封装树形组件
使用示例
支持的属性:
data:树形结果的数据,例如:
jsconst data = ref([ { label: '水果', checked: false, // 添加初始勾选状态 children: [ { label: '苹果', checked: false, children: [ { label: '红富士', checked: false }, { label: '黄元帅', checked: false } ] }, ] }, ])
show-checkbox:是否显示复选框
transition:是否应用过渡效果
支持事件 @update:child-check,可以获取最新的状态
vue
<Tree
:data="data"
:show-checkbox="true"
:transition="true"
@update:child-check="handleChildCheck"
/>
实现
思路
渲染视图:
* 把数据渲染成视图
* 子节点递归渲染
* 实现show-checkbox属性
* 实现展开逻辑
定义响应式数据 数据是对子节点展开的映射map 点击节点的时候,改变映射关系
* 实现勾选
勾选父,选中所有的子 勾选所有的子,选中父
组件实现
vue
<template>
<div class="tree-node" v-for="(node, index) in data" :key="node.label">
<div class="node-label">
<button
class="toggle-button"
v-show="hasChild(node)"
@click="isOpenArr[index] = !isOpenArr[index]"
>
{{ isOpenArr[index] ? "▼" : "►" }}
</button>
<input
type="checkbox"
v-if="showCheckbox"
v-model="node.checked"
@change="onCheck(node, node.checked)"
/>
<label>{{ node.label }}</label>
</div>
<!-- 递归组件:将属性传递给子组件 -->
<div v-if="transition">
<Transition
name="expand"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<div v-show="isOpenArr[index]" v-if="node.children">
<!-- 递归调用的时候,触发上一层的事件
除了点击第一层的checkbox触发emit
点击递归组件的时候,也要绑定checked事件,然后触发上一层
-->
<Tree
:data="node.children"
:showCheckbox
@checked="$emit('checked', node)"
/>
</div>
</Transition>
</div>
<div v-else>
<div v-show="isOpenArr[index]" v-if="node.children">
<Tree
:data="node.children"
:showCheckbox
:transition
@checked="$emit('checked', node)"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { inject, provide, watch } from "vue";
// props
const props = defineProps({
data: {
type: Array,
required: true,
},
showCheckbox: {
type: Boolean,
default: true,
},
transition: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(["checked"]);
//依赖注入当前的父节点,并且将父节点提供给子组件
const parentNode = inject("parentNode", null); //注入父节点,根节点默认为null
provide("parentNode", props.data);
//对应每一层的展开状态 [false,false,false]
const isOpenArr = ref(props.data.map(() => true));
// 点击展开箭头
const onToggle = (node) => {};
//是否有子节点
const hasChild = (node) => {
return node.children && node.children.length > 0;
};
// 勾选
const onCheck = (node, checked) => {
/**
* 选中所有的子节点
* @param node
* @param checked
*/
const updateAllChild = (node, checked) => {
node.children?.forEach((item) => {
item.checked = checked;
// 递归
if (hasChild(node)) {
node.children.forEach((child) => {
updateAllChild(child, checked);
});
}
});
};
/**更新父亲的选中状态
* @param node 当前节点
*/
const updateParent = (node) => {
/*
这里的难点是如何将父节点传递进来
1.可以给<Tree/>传递一个parent属性,值就是node(当前的节点),在props中接收
2.可以使用provide/inject来获取
*/
if (parentNode) {
// 在所有的节点中找到当前的父节点
console.log("parentNode", parentNode);
console.log("当前的node", node);
for (const pNode of parentNode) {
if (pNode.children.includes(node)) {
console.log("找到了", pNode);
pNode.checked = pNode.children.every((child) => child.checked);
updateParent(pNode);
}
}
}
};
//选中的同时去更新父子节点的选中状态
updateAllChild(node, checked);
updateParent(node);
//触发自定义事件
emits("checked", node);
};
//解决子元素全选了但根元素无法选中的Bug
watch(
() => props.data,
(newVal) => {
parentNode?.forEach((node) => {
if (node.children && node.children.length) {
node.checked = node.children.every((child) => child.checked);
}
});
},
{ deep: true }
);
// 过渡动画相关的方法
function beforeEnter(el) {
el.style.maxHeight = "0";
el.style.opacity = "0";
el.style.overflow = "hidden";
}
function enter(el) {
el.style.transition = "max-height 0.3s ease, opacity 0.3s ease";
el.style.maxHeight = el.scrollHeight + "px";
el.style.opacity = "1";
}
function afterEnter(el) {
el.style.maxHeight = "none";
}
function beforeLeave(el) {
el.style.maxHeight = el.scrollHeight + "px";
el.style.opacity = "1";
el.style.overflow = "hidden";
}
function leave(el) {
el.style.transition = "max-height 0.3s ease, opacity 0.3s ease";
el.style.maxHeight = "0";
el.style.opacity = "0";
}
function afterLeave(el) {
el.style.maxHeight = "none";
}
</script>
<style scoped>
.tree-node {
margin-left: 20px;
font-family: Arial, sans-serif;
}
.node-label {
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
}
.toggle-button {
background: none;
border: none;
cursor: pointer;
padding: 0;
font-size: 14px;
color: black;
}
</style>
使用
vue
<template>
<div id="app">
<Tree
:data="data"
:show-checkbox="true"
:transition="true"
@checked="onChecked"
/>
</div>
</template>
<script setup>
import Tree from "./components/Tree.vue";
import { ref } from "vue";
// tree组件的数据
const data = ref([
{
label: "水果",
checked: false, // 添加初始勾选状态
children: [
{
label: "苹果",
checked: false,
children: [
{
label: "红富士",
checked: false,
},
{
label: "黄元帅",
checked: false,
},
],
},
{
label: "香蕉",
checked: false,
children: [
{
label: "大香蕉",
checked: false,
},
{
label: "小香蕉",
checked: false,
},
],
},
],
},
{
label: "游戏",
checked: false,
children: [
{
label: "英雄联盟",
checked: false,
children: [
{
label: "剑圣",
checked: false,
},
{
label: "盖伦",
checked: false,
},
{
label: "流浪法师",
checked: false,
},
{
label: "女枪",
checked: false,
},
],
},
{
label: "王者荣耀",
checked: false,
children: [
{
label: "后羿",
checked: false,
},
{
label: "鲁班七号",
checked: false,
},
],
},
],
},
{
label: "电影",
checked: false,
children: [
{
label: "动作片",
checked: false,
children: [
{
label: "速度与激情",
checked: false,
},
{
label: "碟中谍",
checked: false,
},
],
},
{
label: "爱情片",
checked: false,
children: [
{
label: "泰坦尼克号",
checked: false,
},
{
label: "罗密欧与朱丽叶",
checked: false,
},
],
},
],
},
]);
//
const onChecked = (node) => {
console.log("选中了", node);
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin: 60px auto;
width: 300px;
}
</style>