Tree组件
# 原理
直接添加的节点,操作追加dom; 定时移除节点;
组件间通讯: provide('TREE_PROVIDER', {}
# 组件初始化操作
# 1.创建树组件
export default {
name: 'RTree',
setup() {
return () => <h1>hello tree</h1>
}
}
1
2
3
4
5
6
2
3
4
5
6
# 2.注册树组件
import Tree from './tree.jsx'
import '../../style/tree.scss'
Tree.install = (app) => {
app.component(Tree.name, Tree)
}
export default Tree
1
2
3
4
5
6
2
3
4
5
6
此时树组件已经变为全局,可以直接被使用了
# 通过数据渲染树组件
# 1.组件的递归渲染
const state = reactive({
treeData: [
{
id: "1",name: "菜单1", children: [
{id: "1-1",name: "菜单1-1",children: [{ id: "1-1-1", name: "菜单1-1-1" }]}
]
},
{
id: "2", name: "菜单2",children: [
{id: "2-1",name: "菜单2-1",children: [{id: "2-1-1",name: "菜单2-1-1"}]},
{id: "2-2",name: "菜单2-2",children: [{id: "2-2-1",name: "菜单2-2-1" }]},
]
},
]
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
name: 'RTree',
props: {
data: {
type: Array,
default: () => []
},
},
setup(props) {
const data = props.data;
function renderNode(data) {
if (data && data.length == 0) { // 无节点情况
return <div>无任何节点</div>
}
function renderChild(item) { // 渲染每一个节点
return <div class="r-tree-node">
<div class="r-tree-label">{item.name}</div>
{item.children && item.children.map(child => renderChild(child))}
</div>
}
return data.map(item => renderChild(item));
}
return () => <div class="r-tree">
{renderNode(data)}
</div>
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
递归渲染树组件,但是我们把逻辑都放在Tree组件中,显得过于臃肿。我们可以将子节点渲染单独拿到一个组件中进行!
# 2.组件的分割r-tree-node
import TreeNode from './tree-node'
export default {
components:{
[TreeNode.name]:TreeNode
},
setup(props) {
const data = props.data;
function renderNode(data) {
if (data && data.length == 0) { // 无节点情况
return <div>无任何节点</div>
}
// 渲染子节点
return data.map(item => <r-tree-node data={item}></r-tree-node>);
}
return () => <div class="r-tree">
{renderNode(data)}
</div>
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default {
name: 'RTreeNode',
props: {
data: {
type: Object
}
},
setup(props) {
const data = props.data;
return () => {
return <div class="r-tree-node">
<div class="r-tree-label">{data.name}</div>
<div class="r-tree-list">
{data.children && data.children.map(child => <r-tree-node data={child}></r-tree-node>)}
</div>
</div>
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.美化树组件样式
@import './common/_var.scss';
@import './mixins/mixins.scss';
@include blockquote(tree){
position: relative;
.r-tree-label {
padding-left: 24px;
}
.r-tree-list {
padding-left: 34px;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 组件展开收缩功能
# 1.显示展开图标
const showArrow = computed(() => { // 是否显示箭头
return data.children && data.children.length > 0
});
const classes = computed(() => [
'r-tree-node',
!showArrow.value && 'r-tree-no-expand'
]);
<div class={classes.value}></div>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
通过计算属性的方式绑定样式
@include blockquote(tree){
position: relative;
.r-tree-node{
user-select: none;
&.r-tree-no-expand{
.r-icon{
visibility: hidden;
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 2.增加树的折叠功能
const methods = {
handleExpand(){
data.expand = !data.expand;
}
}
<div class="r-tree-label" onClick={methods.handleExpand}>
<r-icon icon="right"></r-icon>
<span>{data.name}</span>
</div>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
实现树的展开折叠功能
# 增加选择功能-组件通讯
增加checkbox
切换选择时动态给当前数据增加checked属性
const methods = {
handleCheck() {
data.checked = !data.checked; // 切换选中功能
}
}
<div class={classes.value}>
<div class="r-tree-label" onClick={methods.handleExpand}>
<r-icon icon="right"></r-icon>
<input type="checkbox" checked={data.checked} onClick={withModifiers(methods.handleCheck, ['stop'])} />
<span>{data.name}</span>
</div>
<div class="r-tree-list" vShow={data.expand}>
{data.children && data.children.map(child => <r-tree-node data={child}></r-tree-node>)}
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.获取选中的节点
先将所有数据拍平获得checked值为true的节点,并标记父节点。给所有节点增加唯一标识;
export const flattenTree = (data) => {
let key = 0;
function flat(data,parent){
return data.reduce((obj,currentNode)=>{
currentNode.key = key;
obj[key] = {
parent,
node:currentNode
}
key++;
if(currentNode.children){
obj = {...obj,...flat(currentNode.children,currentNode)}
}
return obj;
},{})
}
return flat(data);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.将方法暴露到上下文中
const flatMap = flattenTree(data);
const methods = {
getCheckNodes(){ // 获取所有选中的节点
return Object.values(flatMap).filter(item=>item.node.checked)
}
}
const instance = getCurrentInstance();
instance.ctx.getCheckNodes = getCheckNodes;// 将方法暴露在当前实例的上下文中
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 3.通过ref进行获取
<r-tree :data="treeData" ref="tree"></r-tree>
export default {
setup() {
let tree = ref(null); // 设置ref
function getCheckedNodes() {
console.log(tree.value.getCheckNodes()); // 获取所有节点
}
return {
...toRefs(state),
tree,
getCheckedNodes,
};
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置级联选中状态
updateTreeDown(node,checked){
if(node.children){
node.children.forEach(child=>{
child.checked = checked;
methods.updateTreeDown(child,checked)
})
}
}
updateTreeUp(node,checked){
let parentNode = flatMap[node.key].parent;
if(!parentNode) return;
let parentKey = parentNode.key;
const parent = flatMap[parentKey].node; // 找到爸爸节点
if(checked){ // 看爸爸里儿子是否有选中的项
parent.checked = parent.children.every(node=>node.checked);
}else{
parent.checked = false;
}
methods.updateTreeUp(parent,checked);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
provide('TREE_PROVIDER', {
treeMethods: methods
});
1
2
3
2
3
在父组件中将方法暴露出去, 以便子组件调用这些方法
let {treeMethods} = inject('TREE_PROVIDER')
const methods = {
handleExpand() {
data.expand = !data.expand;
},
handleChange() {
data.checked = !data.checked;
treeMethods.updateTreeDown(data,data.checked); // 通知下层元素
treeMethods.updateTreeUp(data,data.checked) // 通知上层元素
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 异步加载【要点】
# 传递异步方法
<r-tree :data="treeData" ref="tree" :load="loadFn"></r-tree>
function loadFn(data, cb) {
if (data.id == 1) {
setTimeout(() => {
cb([
{
id: "1-1",
name: "菜单1-1",
children: [],
},
]);
}, 1000);
} else if (data.id == "1-1") {
setTimeout(() => {
cb([{ id: "1-1-1", name: "菜单1-1-1" }]);
}, 1000);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
provide('TREE_PROVIDER', {
treeMethods: methods,
load:props.load
});
1
2
3
4
2
3
4
let { treeMethods,load } = inject('TREE_PROVIDER');
const methods = {
handleExpand() {
if(data.children && data.children.length == 0){// 如果没有儿子是空的
if(load){ // 有加载方法就进行加载
data.loading = true; // 正在加载
load(data,(children)=>{
data.children = children;
data.loading = false;// 加载完毕
})
}
}
data.expand = !data.expand;
},
handleChange() {
data.checked = !data.checked;
treeMethods.updateTreeDown(data, data.checked);
treeMethods.updateTreeUp(data, data.checked)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const isLoaded = ref(false); // 用来标识加载完毕
const showArrow = computed(() => { // 是否显示箭头 没儿子 而且也加载完了
return (data.children && data.children.length > 0) || (load && !isLoaded.value)
});
handleExpand() {
if (data.children && data.children.length == 0) { // 如果没有儿子是空的
if (load) { // 有加载方法就进行加载
data.loading = true; // 正在加载
load(data, (children) => {
data.children = children;
data.loading = false; // 加载完毕
isLoaded.value = true;
})
}
}else{
isLoaded.value = true;
}
data.expand = !data.expand;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
实现tree组件数据的异步加载。这里要注意数据新增后要重新构建父子关系,监控数据变化重新格式化数据。
watch(data,()=>{
flatMap = flattenTree(data);
});
1
2
3
2
3
# 定制化节点插槽实现
<r-tree :data="treeData" ref="tree" :load="loadFn">
<template v-slot="{name}">
<b>{{name}}</b>
</template>
</r-tree>
1
2
3
4
5
2
3
4
5
provide('TREE_PROVIDER', {
treeMethods: methods,
load: props.load,
slot:context.slots.default
});
1
2
3
4
5
2
3
4
5
let { treeMethods, load, slot } = inject('TREE_PROVIDER')
{slot ? slot(data) : <span>{data.name}</span>}
1
2
2
通过作用域插槽将组件内的数据传递给父组件
# 拖拽实现
const classes = computed(() => [
'r-tree-node',
!showArrow.value && 'r-tree-no-expand',
draggable && 'r-tree-draggable'
]);
const instance = getCurrentInstance()
const dragEvent = {
...(draggable ? {
onDragstart(e) {
e.stopPropagation();
treeMethods.treeNodeDragStart(e,instance, data);
},
onDragover(e) {
e.stopPropagation();
treeMethods.treeNodeDragOver(e,instance, data);
},
onDragend(e) {
e.stopPropagation();
treeMethods.treeNodeDragEnd(e,instance, data);
}
} : {})
}
<div class={classes.value} {...dragEvent}>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
根据
draggable
属性决定是否添加拖拽事件
.r-tree-node {
&.r-tree-draggable {
user-select: none;
-webkit-user-drag: element;
}
}
1
2
3
4
5
6
2
3
4
5
6
设置文字不能选择,元素可以拖动
# 1.拖动线设置
<div class="r-tree-indicator" ref="indicator" vShow={state.showDropIndicator}></div>
1
.r-tree-indicator{
position: absolute;
height: 1px;
right:0;
left:0;
background-color:#409eff;
}
1
2
3
4
5
6
7
2
3
4
5
6
7
# 2.拖动事件处理逻辑
const state = reactive({
dropPosition: '', // 拖拽的位置
dragNode: null, // 拖的是谁数据
showDropIndicator: false, // 推拽标尺
draggingNode: null // 拖拽的节点
})
treeNodeDragStart(e,nodeInstance, data) {
state.draggingNode = nodeInstance; // 拖拽的实例
state.dragNode = data; // 拖拽的数据
},
treeNodeDragOver(e,nodeInstance, data) {
// 在自己身上滑来滑去
if(state.dragNode.key == data.key){
return;
}
let overEl= nodeInstance.ctx.$el; // 经过的el,是当前拖住的儿子
if(state.draggingNode.ctx.$el.contains(overEl)){
return
}
// 获取目标节点label的位置
let targetPosition = overEl.firstElementChild.getBoundingClientRect();
// 树的位置
let treePosition = instance.ctx.$el.getBoundingClientRect();
let distance = e.clientY - targetPosition.top; // 鼠标相对于 文本的位置
if(distance < targetPosition.height * 0.2){
state.dropPosition = 1;
}else if(distance > targetPosition.height* 0.8){
state.dropPosition = -1; // 后面
}else{
state.dropPosition = 0;
}
let iconPosition = overEl.querySelector('.r-icon').getBoundingClientRect();
let indicatorTop = -9999;
if(state.dropPosition == 1){
indicatorTop = iconPosition.top - treePosition.top; // 获取线相对于树的位置
}else if(state.dropPosition == -1){
indicatorTop = iconPosition.bottom - treePosition.top;
}
state.showDropIndicator = (state.dropPosition == 1) || (state.dropPosition == -1);
const indicator = instance.ctx.$refs.indicator;
indicator.style.top = indicatorTop + 'px';
indicator.style.left = iconPosition.right - treePosition.left + 'px';
},
treeNodeDragEnd(e,nodeInstance, data) {
state.showDropIndicator = false;
state.dropPosition = '';
state.dragNode = null;
state.draggingNode = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
上次更新: 2022/04/15, 05:41:28