文章目录
默认配置
<el-tree
ref="tree"
highlight-current // 高亮当前选中节点
accordion // 只打开一个同级树节点展开
:data="treeData"
:expand-on-click-node="false" // 单击节点是否展开(懒加载模式下无效)
:props="treeDefaultProps" // 映射属性值
node-key="id" // 标识属性
:default-expanded-keys="['-1']" // 默认展开的节点
@node-click="handleNodeClick"
:filter-node-method="filterNode" // 对树节点进行筛选时执行的方法,返回 true则显示
:check-on-click-node="true" // 点击节点时选中
>
data() {
return {
treeData:[
id: '-1',
label: '全部',
leaf: false,
children: [
{
...
},
{
...
}
],
],
treeDefaultProps:{
children: 'children',
label: 'label',
isLeaf: 'leaf',
},
}
}
懒加载
<el-tree
:load="queryTreeData"
lazy
>
// 获取树目录
queryTreeData(node, resolve) {
const { level } = node
// nodeId 为 -1代表第一次请求
let nodeId = level == 0 ? '-1' : node.data.id
const params = {
displayTable: true,
parentId: nodeId,
}
api
.queryDirectoryLazyLoad(params)
.then((res) => {
const nodes = res.map((item) => ({
id: item.id,
label: item.name,
leaf: !item.hasChildren,
}))
// 初始化请求
if (level == 0) {
resolve([
{
id: '-1',
label: '全部',
leaf: false,
children: nodes, // 非必要
},
])
} else {
resolve(nodes)
}
})
.then(() => {
this.$refs.tree.setCurrentKey(this.directoryId)
})
.catch((error) => {
console.log(error)
})
},
每一级分页懒加载
(待补充)
递归处理数据
递归遍历树级结构,进行字段映射
mapTree(data) {
const haveChildren =
Array.isArray(data.childNode) && data.childNode.length
return {
label: data.nodeName,
id: data.nodeId,
children: haveChildren
? data.childNode.map((i) => this.mapTree(i))
: [],
}
},
一维数组处理为树结构
思路:
ant-design-vue
/**
* 数组转树形结构
* @param list 源数组
* @param tree 树
* @param parentId 父ID
*/
const listToTree = (list, tree, parentId) => {
list.forEach(item => {
// 判断是否为父级菜单
if (item.parentId === parentId) {
const child = {
...item,
key: item.key || item.name,
children: []
}
// 找到当前菜单相符合的所有子菜单
listToTree(list, child.children, item.id)
// 删掉不存在 children 值的属性
if (child.children.length <= 0) delete child.children
// 加入到树中
tree.push(child)
}
})
}
// 在idx=0的该次循环中,找到最高层级pid为-1的元素101。调用listToTree,把list、当前层级的信息、当前id(101)作为参数传入,去寻找pid为101的元素。当pid为101的遍历结束后,id为102和104的child信息被推入children的字段中,idx=0的此次遍历结束。以此迭代遍历,直至找到所有的子节点。
const list = [
{'id':101,'name':'语文','parentId': -1},
{'id':102,'name':'语文知识点1','parentId': 101},
{'id':103,'name':'语文知识点11','parentId': 102},
{'id':104,'name':'语文知识点2','parentId': 101},
]
let resTree = []
listToTree(list, resTree, -1)
console.log(resTree)
默认选中并展开特定节点
初始化的需求场景
// 默认展开
<el-tree
ref="tree"
:default-expanded-keys="expandedKeys"
lazy
>
computed: {
// 树目录默认展开的节点
expandedKeys() {
let directoryLink = this.$route.query.directoryLink?.split(',')
const directoryId = this.$route.query.directoryId
if (_.isEmpty(directoryId) || _.isEmpty(directoryLink)) return '-1'
return ['-1'].concat(...directoryLink, directoryId)
},
},
// 初始化选中
queryTreeData() {
...
api.queryDirectory(params).then((res) => {
this.treeData = this.convertTreeData(res.list)
this.$nextTick(() => {
// nextTick 确保组件渲染后能set得到key
this.$refs.tree.setCurrentKey(this.directoryId)
})
...
})
},
切换tab后的需求场景
思路: 切换tab, 会引起树组件的数据变动, 仅需监听数据, 在回调里设置选中第一个节点即可.
注: this.$nextTick 很关键,确保在组件渲染后才执行选中节点代码;
watch: {
treeData: {
handler: function (newVal, oldVal) {
// 只有切换tab页才会导致树组件重新渲染, 因此不用设置首次渲染的标识
// 当数据改变时, 组件元素还未更新, 用nextTick等组件更新完再执行
this.$nextTick(() => {
newVal[0] && this.$refs.treeComp.setCurrentKey(newVal[0].id)
})
},
},
},
禁止点击事件
<el-tree
ref="tree"
:class="defaultSelectedKey ? 'no-event' : ''"
>
.no-event {
pointer-events: none;
}
注:不可加在 el-tree 的父级元素上, 否则会把滚动事件也禁止掉, 导致无法正常滚动.
搜索
本地搜索
思路:el-input 的筛选回调为 filterTreeNode,该方法会调用树的 filter方法,然后在 el-tree 绑定一个动态属性 filter-node-method,在相应的值 filterNode方法中,会挨个遍历每个节点的 data,并返回匹配结果。
注:此方法的应用前提为树目录非懒加载
<!-- 左侧栏 -->
<div class="data-application-sidebar l-box-col" v-loading="treeLoading">
<el-input
class="search-input l-mb-10"
placeholder="请输入资产目录查询"
size="medium"
v-model="filterTreeVal"
@keyup.enter.native="filterTreeNode"
@clear="filterTreeNode"
clearable
>
<div slot="prefix" class="search-icon" @click="queryData">
<i class="el-icon-search"></i>
</div>
</el-input>
<div class="data-application-sidebar-tree l-flex">
<el-tree
ref="tree"
highlight-current
accordion
:data="treeData"
:expand-on-click-node="false"
:props="treeDefaultProps"
:node-key="treeDefaultProps.id"
:default-expanded-keys="['-1']"
@node-click="handleNodeClick"
:filter-node-method="filterNode"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span v-if="data.isFolder" class="l-mr-10">
<i class="el-icon-folder"></i>
</span>
<span v-else class="l-mr-10">
<i class="el-icon-document"></i>
</span>
<span class="custom-tree-node-label">{{ node.label }}</span>
</span>
</el-tree>
</div>
</div>
/**
* 过滤树目录
*/
filterTreeNode() {
this.$refs.tree.filter(this.filterTreeVal)
},
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
搜索后滚动定位结果
思路: 计算出选中节点的上边框距离父容器的上边框的距离 (offsetTop), 若此距离 h > 父容器高度, 则设置父容器滚动距离 (scrollTop) 为 h
nodeScroll() {
setTimeout(() => {
let liList = this.$refs.tree.$el.getElementsByClassName('is-current')
let offsetTop = liList[0].offsetTop
let listWrapper = document.querySelector('.market-input-directory-tree')
if (offsetTop + 30 > listWrapper.clientHeight) {
listWrapper.scrollTop = offsetTop
}
}, 0)
},
注:
- nodeScroll中要使用异步函数,先切换了选中的节点,再滚动
- 千万不可以使用 nextTick。如果在 nodeScroll中使用nextTick,offsetTop获取的值会是上一选中节点的位置
- 当树组件节点较多时, 搜索节点需要一定的时间, 此时 nodeScroll 可能会在setCurrentKey 前执行, 可设置定时器来延迟 nodeScroll 的执行. (使用第三方组件的弊端: 由于搜索执行的过程被封装了, 所以不能严格控制代码执行的先后顺序)
添加图标
方法一: 通过伪类的 background 属性
<el-tree
class="flow-chart-tree-content-item"
ref="treeInFlowChart"
node-key="id"
:default-expand-all="false"
:data="treeData"
:expand-on-click-node="true"
:filter-node-method="filterNode"
:class="workflowDisabled ? 'flow-chart-no-event' : ''"
>
<span
class="custom-tree-node"
slot-scope="{ node, data }"
@mousedown="addNodeToGraph($event, data)"
>
<span>
<span class="l-mr-5">
<i class="icon-sql"></i>
</span>
<span>{{ node.label }}</span>
</span>
</span>
</el-tree>
.icon-sql:before {
content: '';
background: url(~@/assets/agileDevTools/sql.png) no-repeat center center;
background-size: contain;
width: 20px;
height: 20px;
display: block;
position: relative;
top: 3px;
}
方法二: 通过 img 标签引入图片
<div class="data-application-sidebar-tree l-flex">
<el-tree
ref="tree"
highlight-current
accordion
:data="treeData"
:expand-on-click-node="false"
:props="treeDefaultProps"
node-key="id"
:default-expanded-keys="[directoryId]"
@node-click="handleNodeClick"
:filter-node-method="filterNode"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span class="l-mr-10">
<img :src="require(`@/assets/workbench/folder.png`)" />
</span>
<span class="custom-tree-node-label">{{ node.label }}</span>
</span>
</el-tree>
</div>
修改选中的高亮 (图标和颜色)
选中时图标切换
思路: 利用选中节点时带有的 focus 伪类, 去设置相应的子元素样式和图标
文字和背景的高亮
<el-tree
>
<span
class="custom-tree-node"
slot-scope="{ node, data }"
>
<span>
<span v-show="data.icon" class="l-mr-5">
// 修改特定节点的高亮, 需要在动态类属性做一个是否添加 directory 类名的判断
<i
:class="[data.icon, data.notNode ? 'directory' : '']"
></i>
</span>
<span>{{ node.label }}</span>
</span>
</span>
</el-tree>
::v-deep .el-tree-node__content:hover,
::v-deep .el-tree-node:focus > .el-tree-node__content {
background: #f2f9ff;
}
::v-deep .el-tree-node:focus > .el-tree-node__content {
color: #2590ff;
}
// 修改图标
::v-deep .el-tree-node:focus > .el-tree-node__content .directory:before {
background: url(~@/assets/agileDevTools/data-directory-active.png) no-repeat
center center;
}
可编辑树
点击展开后回调
@node-expand="handleNodeExpand"
注: node-expand 只针对人为点击引起的展开事件, 由 default-expanded-keys 属性值变动而展开的事件, 不会触发 node-expand 的事件.
点击节点图标切换显示(包含一键切换全部图标)
思路: 点击图标(文字), 切换 data.showField的值, 即用节点数据的字段 showField 来控制切换
<div class="popover-tree-el-content l-mb-12" :style="setContentStyle">
<el-tree
ref="treeInPopoverEl"
node-key="id"
show-checkbox
default-expand-all
:data="treeData"
:expand-on-click-node="false"
:check-on-click-node="true"
:filter-node-method="filterNode"
>
<div class="custom-tree-node" slot-scope="{ node, data }">
<span>
<span>{{ node.label }}</span>
</span>
<span class="l-mr-30">
<span
v-if="data.showField"
class="custom-tree-node-remove"
@click.stop="() => toggleField(node, data)"
>{{ node.label == '全选' ? '全部移除' : '移除' }}</span
>
<span
v-else
class="custom-tree-node-recover"
@click.stop="() => toggleField(node, data)"
>{{ node.label == '全选' ? '全部恢复' : '恢复' }}</span
>
</span>
</div>
</el-tree>
</div>
toggleField(node, data) {
// 1. 更新被点击节点的数据字段 showField, 以此切换图标显示
data.showField = !data.showField
// 2. 全选有两种情况需处理: 点击全部处理子节点, 当所有子节点状态一致, 则处理全部
if (data.label == '全选') {
// 若点击全部, 则遍历赋值, 更新所有子节点
this.treeData[0].children.forEach((item) => {
item.showField = data.showField
})
} else {
// 若点击的是子节点, 则处理当所有子节点一致时, 改变全部的图标的情况
const treeChildData = this.treeData?.[0].children
const allChange = treeChildData.every(
(item) => item.showField == data.showField
)
// 若所有字段都改变,则改变全选按钮
if (allChange) this.treeData[0].showField = data.showField
}
},
.custom-tree-node {
width: 100%;
display: flex;
justify-content: space-between;
&-recover {
color: #358df6;
}
}