实现树组件
tree
拆分为整个树,结点内容,生成子节点
树
xml
<template>
<div class="h-tree">
<tree-node>
...
</tree-node>
</div>
</template>
props.data作为参数传给组件
对数据进行加工,获取到其它的信息(如是否展开结点等)和解耦数据
css
const list = [{ label: "Level one 1", children: [ { label: "Level two 1-1", children: [ { parentData:"Level two 1-1", label: "Level three 1-1-1", }, ],
},
],
},
{
label: "Level one 2",
children: [
{
label: "Level two 2-1",
children: [
{ parentData:"Level two 2-1",
label: "Level three 2-1-1",
},
],
},
{
label: "Level two 2-2",
},
],
},
{
label: "Level one 3",
children: [
{
label: "Level two 3-1",
children: [
{
label: "Level three 3-1-1",
},
],
},
{
label: "Level two 3-2",
children: [
{
label: "Level three 3-2-1",
},
],
},
],
},
];
ini
let i = ref<number>(0);
const initFn = (data: any, level: number = 0) => {
copyData.value = data.map((item: any, index: number) => {
item.uid = i.value ++;//计算唯一标识
item.children = item[props.children] || []; //把儿子节点传输
item.label = item[props.label];
item.id = item[props.nodeKey];
item.isOpen = false;
item.isChecked = false;
item.level = level;
if (item.children && item.children.length) {
initFn(item.children, level + 1);//计算层级,递归加一
}
return {
uid:item.uid,//唯一标识
// id: item.id,
label: item.label,//内容
children: item.children,
isOpen: item.isOpen, //展开
isChecked: item.isChecked, //父节点选择框的的一个判断
disabled: item.disabled,//默认禁止
level: item.level,//层级
};
});
};
把我们的新数据copydata传入其中,然后来递归生成结点
ruby
<template>
<div class="h-tree">
<tree-node
v-for="(item, index) in copyData"
:key="index"
:uid="item.uid"
:level="item.level"
:items="item"
:label="label"
:children="children"
:show-checkbox="showCheckbox"
:index="0"
:node-key="nodeKey"//唯一标识
:default-expanded-keys="defaultExpandedKeys"
:default-checked-keys="checkedKeys"
:default-expand-all="defaultExpandAll"
:render-content="renderContent"
:parent-data="copyData">
</tree-node>
</div>
</template>
onMounted
钩子在组件挂载时执行,调用initFn
方法进行初始化。initFn
方法用来初始化树形数据结构。该方法接收两个参数,一个是数据源,一个是当前节点所在的层级。该方法会遍历数据源,给每个节点加上一些属性,然后返回一个新的树形结构。checkboxChange
方法用来更新选中状态。该方法会调用updateChecked
方法更新每个节点的选中状态。toggleChange
方法在节点展开或收起时触发。它会通过emits
向外发射一个toggle-change
事件。checkedChange
方法在选中状态改变时触发。它会调用getCheckedNodes
方法获取当前选中的节点,然后通过emits
向外发射一个checked-change
事件。updateChecked
方法用来更新每个节点的选中状态。如果某个节点的所有子节点都被选中,那么该节点也会被选中。如果某个节点的某个子节点被选中,那么该节点的选中状态就是半选中状态。collapseOtherlevelNodes
方法用来在展开某个节点时,收起其他同级节点。该方法会遍历树形结构,找到与当前节点同级的节点,并将其收起。collapseOtherNodes
方法用来在展开某个节点时,收起其他节点。该方法会遍历树形结构,并将除当前节点以外的所有节点收起。setCheckedKeys
方法用来设置选中的节点。它接收一个键值数组作为参数,并将其赋值给checkedKeys
变量。getCheckedKeys
方法用来获取选中节点的键值数组。它会调用getCheckedNodes
方法获取
生成节点
ini
<template>
<div >
<ul class="tree-node" >
<div :class="['tree-node__content']"
@click.stop="handleToggle(items)"
:draggable="true"
@dragstart="onDragStart"
@dragover="onDragOver"
@drop="onDrop"
@dragend="onDragEnd"
>
<span >
<i class="icon iconfont icon-xiala"></i>
</span>
<el-checkbox
v-if="showCheckbox"
v-model="items!.isChecked" //复选框检查
:indeterminate="items.indeterminate"
:disabled="items.disabled"
@change="handleCheckChange"
></el-checkbox>
<nodeContent :data="items" :render-content="renderContent" :parent-data="parentData" />
</div>
<div class="tree-ul-box" v-if="isShow" v-show="items.isOpen" >
<tree-node
v-for="(i, j) in items.children"
:key="j"
:items="i"
:uid="uid"
:label="label"
:children="children"
:show-checkbox="showCheckbox"
:index="index && index + 1"
:node-key="nodeKey"
:render-content="renderContent"
:parent-data="items.children">
</tree-node>
</div>
</ul>
</div>
作为递归函数,我们需要传入接收刚刚父组件的参数,同样是用props接收
javascript
const props = defineProps({
items: {
type: Object,
default: () => {},
},
label: String,
children: String,
showCheckbox: Boolean,
index: Number,
nodeKey: String,
// 默认展开项
defaultExpandedKeys: Array,
// 默认选中项
defaultCheckedKeys: Array,
// 默认展开所有
defaultExpandAll: Boolean,
renderContent: Function,
parentData: Array,
});
/*parentData 是在 HTree 组件中提供的一个 provide/inject 机制的值,用于获取整棵树的节点数据。在这里的子节点中,通过 :parent-data="items.children" 的方式将父节点的 children 属性向下传递,使子节点在使用 parentData 时可以获取到整个树的节点数据。这样可以方便在节点组件中实现一些功能,比如展开/收起其他节点时,需要获取到整棵树的节点数据来进行操作。*/
如果改结点存在子节点才能继续递归, 具体实现就是用v-if来判断即可。
ini
const isShow = computed(() => { return props.items.children && props.items.children.length; });
点击事件,判断展开
ini
const handleToggle = (item: any) => {
item.isOpen = !item.isOpen;
// 展开/收起子节点时触发
};
手风琴模式
scss
const handleToggle = (item: any) => {
item.isOpen = !item.isOpen;
// 展开/收起子节点时触发
if(item.isOpen) //当前节点展开,就关闭同层级节点
{
collapseOtherlevelNodes(item);
}
toggleChange(item);
};
ini
const collapseOtherlevelNodes = (item: any)=>{
collapseOtherlevelNodesFromParent (item,props.parentData?props.parentData:[])
}
生成节点向根节点接收collapse-other-level-nodes方法
typescript
const collapseOtherlevelNodesFromParent = inject<(currentNode: any, parentData: any[]) => void>("collapse-other-level-nodes", () => {});
根节点
typescript
const collapseOtherlevelNodes=(currentNode: any, parentData: any[])=>{
const collapselevelNodes = (nodes: any[]) => {
nodes.forEach((node: any) => {
// console.log("node.level",node.level);
// console.log("currennt",currentNode);
if (node.level === currentNode.level && node.uid !== currentNode.uid) { //同层级但是不同id的节点会关闭
node.isOpen = false;
}
if (node.children && node.children.length) { //检查节点还有没有儿子节点
collapselevelNodes(node.children); //有就递归
}
});
};
collapselevelNodes(parentData); //递归调用
}
复选框实现
ini
<div >
<ul class="tree-node" >
<div :class="['tree-node__content']"
@click.stop="handleToggle(items)"
:draggable="true"
@dragstart="onDragStart"
@dragover="onDragOver"
@drop="onDrop"
@dragend="onDragEnd"
>
<span >
<i class="icon iconfont icon-xiala"></i>
</span>
<el-checkbox
v-if="showCheckbox"
v-model="items!.isChecked"
:indeterminate="items.indeterminate"
:disabled="items.disabled"
@change="handleCheckChange"
></el-checkbox>
<nodeContent :data="items" :render-content="renderContent" :parent-data="parentData" />
</div>
<div class="tree-ul-box" v-if="isShow" v-show="items.isOpen" >
<tree-node
v-for="(i, j) in items.children"
:key="j"
:items="i"
:uid="uid"
:level="level"
:label="label"
:children="children"
:show-checkbox="showCheckbox"
:index="index && index + 1"
:node-key="nodeKey"
:render-content="renderContent"
:parent-data="items.children">
</tree-node>
</div>
</ul>
</div>
checkbox点击事件
1.该节点的状态变化,其子节点需要全部改变
ini
// 选中一个节点时,递归地遍历下面所属的所有子节点
const updateChildChecked = (item: any, val: any) => {
item.isChecked = val;
if (item.children && item.children.length) {
item.children.forEach((el: any) => {
updateChildChecked(el, val);
});
}
};
2.同时父级的状态需要根据子节点是否全选,等发生变化
ini
// 子有一个选中,父为半选
// 子全选中,父为全选
// 子一个都没选中,父不选
const updateChecked = (data: any) => {
data.forEach((item: any) => {
let checked;//选中
let indeterminate;//半选
let checkedNodes;
if (item.children && item.children.length) {
updateChecked(item.children);
const children = item.children;
// 过滤出选中的
checkedNodes = children.filter((child: any) => child.isChecked);
if (checkedNodes.length === 0) {
checked = false;
indeterminate = false;
} else if (checkedNodes.length === children.length) {
checked = true;
indeterminate = false;
} else {
checked = false;
indeterminate = true;
}
item.isChecked = checked;
item.indeterminate = indeterminate;
}
});
};
拖拽实现(生成节点)
typescript
const onDragStart = (event:any) => {
event.dataTransfer.setData('item', JSON.stringify(props.items)); //当前拖拽节点的信息设置传递用item的名称
const droppedItem = JSON.parse(event.dataTransfer.getData('item'));//获取信息
console.log("当前拖拽节点的parentData",props.parentData);
const updatedData = removeNode(droppedItem, props.parentData);
const proxy = new Proxy (updatedData,{});//因为props.item.parentData是proxy类型,所以赋值要转换类型
event.target.classList.add("dragging");//设置css
if (updatedData) {
nextTick(() => {
props.items.parentData = proxy;//更新了就赋值
});
}
};
ini
const onDragOver = (event:any) => {
event.preventDefault();
};
const onDrop = (event:any) => {
event.preventDefault();
const droppedItem = JSON.parse(event.dataTransfer.getData('item'));
if (!props.items.children) {
props.items.children = [];
}
console.log(props.items.children);
props.items.children = [...props.items.children, droppedItem];//添加
props.items.isOpen = true; // 展开目标节点
collapseOtherNodes(props.items); // 收缩其他节点
};
const onDragEnd = (event:any) => {
event.dataTransfer.clearData('item');
event.target.classList.remove("dragging");
};
未解决
当我再一次更新视图时结果正确
结点内容
结点内容其实非常简单,我们只需要接收结点的数据,用虚拟结点的方式返回即可。
xml
<script lang="ts">
import { h, toRefs, reactive } from "vue";
export default {
props: {
data: {
type: Object,
required: true,
},
renderContent: Function,
parentData: Array,
},
setup(props) {
const { data, renderContent, parentData } = toRefs(props);
const nodeData = reactive({
data: data.value,
parentData: parentData.value,
});
return () => [renderContent.value ? renderContent.value(h, nodeData) : h("span", data.value.label)];
},
};
</script>
结果:
参考
Vue实现一个Tree组件 - 掘金 (juejin.cn)
HTML5拖拽API实现vue树形拖拽组件 - 掘金 (juejin.cn)
ChaiMayor/hview-ui: Hview UI - A Vue.js 3 UI library 👻 (github.com)