目录
1.刷新指定节点
2.自定义过滤方法
3.新增子节点
4.编辑节点名
5.拖拽节点
6.某节点高亮
7.总结
8. 组件整体的代码
1.刷新指定节点
node节点有一个loaded的属性,用来存储该节点是否展开过,
刷新指定节点的思路:无论该节点是否展开过,通通设置loaded=false,然后触发node节点的expand函数去展开节点
refreshNode(node){
node.loaded = false
node.expand()
},
2.自定义过滤方法
如果使用的不是懒加载,就可以直接使用官方给的方式去过滤,简单省事!
但是如果是使用的懒加载,就需要配合接口(跟获取节点是同一个接口,只是传递了过滤字段)处理了,就得自定义。
自定义过滤节点思路:带过滤参数从顶级节点开始展开,展开之后,根据子节点是否是叶子节点,再展开这些节点,这样一直展开下去,直到最里层
其实主要也是expand这个函数起的作用,真的很好用,界面上能看到一层一层的展开效果!
下面的代码仅供参考哦,html里很多data中的数据,懒得挨个复制了...重点看一下搜索区域和过滤函数(使用了递归)
<!-- 搜索区域 -->
<div class="menu-header">
<el-input
v-model="filterText"
placeholder
id="input"
size="mini"
suffix-icon="el-input__icon el-icon-search"
@keyup.enter.native="filterMenu(-1)"
></el-input>
</div>
<!-- tree区域 -->
<el-scrollbar class="scrollbar-container" wrapStyle="margin-bottom:0">
<el-tree
ref="tree"
lazy
:load="loadNode"
node-key="id"
:props="defaultProps"
:highlight-current="true"
:expand-on-click-node="false"
:current-node-key="currentNodekey"
:default-expanded-keys="defaultExpandKeys"
draggable
:allow-drop="allowDrop"
:allow-drag="allowDrag"
@node-drag-start="handleDragStart"
@node-drop="handleDrop"
@node-click="handleNodeClick">
<div class="custom-tree-node flex-space-between" slot-scope="{ node, data }" >
<div class="tree-icon">
<icon-svg class="file" icon-class="file"></icon-svg>
</div>
<div class="tree-label">
<span v-show="editDirId !== data.id">
{{node.label}}
<template v-if="data.num">({{data.num}})</template>
</span>
<el-input
:ref="data.id"
v-show="editDirId === data.id"
v-model.trim="data.title"
size="mini"
maxlength="15"
@keyup.enter.native="$event.target.blur"
@blur="labelBlur(node)"
></el-input>
</div>
<!-- 添加、编辑、删除按钮 -->
<div class="tree-options">
<!-- 带子节点的节点 有刷新功能 -->
<i v-if="!node.isLeaf" class="btn blue el-icon-refresh" @click.stop="refreshNode(node)"></i>
<!-- 添加节点 -->
<i class="btn blue el-icon-plus" @click.stop="addNode(node)"></i>
<!-- 我的目录(id=-1)不能修改 -->
<i v-if="data.id != -1" class="btn el-icon-edit" @click.stop="showInput(data.id, data.title)"></i>
<!-- 我的目录(id=-1)不能删除 :aaa=" data.id"-->
<i v-if="data.id != -1" class="btn red el-icon-delete" @click.stop="deleteDir(node, data.id)"></i>
</div>
</div>
</el-tree>
</el-scrollbar>
// 我的顶级目录是固定的,对应的值是-1
filterMenu(nodeId){
// 过滤文本为空时,刷新顶级节点
if(this.filterText.trim() == '') {
let topNode = this.$refs.tree.getNode(-1)
topNode.childNodes = []
topNode.loaded = false
topNode.expand()
return
}
let node = this.$refs.tree.getNode(nodeId)
// 重新展开该节点
node.loaded = false
node.expand(()=>{
// 检查子元素是否有非叶子结点(注意:我们后台给的接口返回的isLeaf是字符串形式的true false)
node.childNodes.forEach(item => {
if(item.data.isLeaf == 'false'){
// 递归处理含有子节点的节点
this.filterMenu(item.data.id)
}else{
// 递归出口
}
});
})
},
看一下过滤之后的样子,带01的名字全部会展开
3.新增子节点
新增子节点,主要使用了tree提供的append函数,但是会有一些细节需要注意:比如说有的节点在还未加载的时候就新增子节点,会导致新增的节点没办法聚焦等问题
解决思路:先触发节点展开,展开之后再向其中追加子节点,子节点的名需要不重复
以下代码仅供参考,根据自己的情况做修改
addNode(node){
node.expand(()=>{
this.dirIdSuff = 0;
let newDirName = this.getOnlyDirName('新建目录', node.childNodes)
let order = node.childNodes.length > 0 ? node.childNodes[node.childNodes.length-1].data.ord + 1 : 0
let newChild = {
title: newDirName,
isLeaf: "true",
ord: order,
num: 0
}
// 调接口新增子节点
insertDirectory({
"parentId": node.data.id,
"path": node.data.path,
"title": newDirName,
"ord": order
})
.then(res => {
newChild.parentId = res.data.parentId
newChild.id = res.data.id
newChild.path = res.data.path
this.$refs.tree.append(newChild, node)
this.showInput(res.data.id, newChild.title)
})
})
},
// 节点显示编辑状态的输入框
showInput(id, title){
this.oldDirName = title
this.editDirId = id
this.$nextTick(() => {
this.$refs[id].focus()
})
},
// 寻找一个合适的目录名
getOnlyDirName (title, arr) {
let dirName = title + this.dirIdSuff
isArray(arr) ? '' : arr = []
if(arr.find(val => val.data.title === dirName)){
this.dirIdSuff++
return this.getOnlyDirName(title, arr)
}else{
this.dirIdSuff = 0
return dirName
}
},
看一下效果
4.编辑节点名
使用懒加载的时候你会发现,节点的数据集合我们不会在data中管理,el-tree这个组件在懒加载的时候也不需要传入data,就像下面这样:
<!-- 基本用法 -->
<el-tree
:data="data"
:props="defaultProps"
></el-tree>
<!-- 懒加载用法 -->
<el-tree
:props="defaultProps"
lazy
:load="loadNode"
></el-tree>
那我们在修改树节点的信息的时候就不能通过修改data去改变了...
解决思路:通过修改node中的data去改变树节点信息
node中存储的有该节点的基本信息(文章总结的地方有),还有渲节点时使用的data数据(也就是接口返回的某节点数据)
我的需求是点击了编辑按钮之后,显示节点的输入框供用户编辑,然后回车或者失焦,触发接口。来吧,上代码(html的部分前面自定义过滤方法的地方已经粘出来了,这里就不重复了)
// 点击了编辑按钮
showInput(id, title){
// 保存修改前的目录名
this.oldDirName = title
// 修改当前处于编辑状态的节点,为了触发该节点显示输入框
this.editDirId = id
this.$nextTick(() => {
// 输入框聚焦
this.$refs[id].focus()
})
},
// input失去焦点(编辑目录名)
labelBlur (node) {
let data = node.data
this.editDirId = ''
// 名字相等的时候不用改
if (data.title === this.oldDirName) return
// 检查名字是否重复(isArray是我自己写的一个检查是否是数组的函数)
let sameName = isArray(node.parent.childNodes) ?
node.parent.childNodes.some( childNode => childNode.data.title===data.title && childNode.data.id!==data.id )
:
false
if(sameName) {
data.title = this.oldDirName
this.$message({
type: 'error',
message: '同目录不允许同名哦!'
})
return
}
// 目录名称校验,需要的自行添加这部分代码,我这里没有
// 修改目录,调用接口
updateDirectory({
"id": data.id,
"title": data.title
})
.then(res => {
this.$message({ type: 'success', message: '修改成功!' })
})
.catch(() => {
// 失败的话,将目录名复原
data.title = this.oldDirName
})
},
5.拖拽节点
el-tree组件也提供了节点的拖拽功能,但是当使用的时候还是会存在很多需要注意的地方的:
比如说:
拖拽三种情况,before,after,inner ,
我这里每个目录都包含了它的上级目录甚至还有一个path记录这个节点的在树中的层级关系,所以拖拽成功了的时候,一定得记得更新一下节点信息哦,要不然用户直接再拖拽这个节点,就会出问题了。
那要是拖拽失败了呢?我百度了很多,没看到别人对这种情况得处理,所以我自己处理了一下,将拖拽的那个节点位置复原
同样的html部分不重复了,去上面看一下就行,下面粘一下js部分(小括号标注了我的项目需求,方便各位看官能更快的修改成自己需要的代码)
// 允许放置的节点设置(我的需求是顶级目录只能允许用户inner节点进去)
allowDrop(draggingNode, dropNode, type) {
if (dropNode.data.id == -1) {
return type === 'inner';
} else {
return true;
}
},
// 允许拖拽的节点设置(我的需求是顶级节点不允许拖拽)
allowDrag(draggingNode) {
return draggingNode.data.id != -1;
},
// 拖拽前: 保存被拖拽元素的下标(保存下来为了复原节点位置使用)
handleDragStart(draggingNode, event){
draggingNode.parent.childNodes.forEach((childNode, nodeIndex) =>{
if(childNode.key == draggingNode.key){
this.beforeDragNodeIndex = nodeIndex
return
}
})
},
// 拖拽完成(请求成功时:更新节点信息;请求失败时:节点位置复原)
handleDrop(draggingNode, dropNode, dropType, ev) {
// console.log('tree drop 拖拽完成: ',draggingNode, dropNode, dropType, ev);
// 将dropType对应成数字
switch (dropType) {
case 'before':
dropType = 0;
break;
case 'after':
dropType = 1;
break;
case 'inner':
dropType = 2;
break;
default:
dropType = 0;
break;
}
let params = {
affectedNode: {
id: dropNode.data.id,
parentId: dropNode.data.parentId,
title:dropNode.data.title,
ord: dropNode.data.ord,
path: dropNode.data.path
},
dragNode: {
id: draggingNode.data.id,
parentId: draggingNode.data.parentId,
title:draggingNode.data.title,
ord: draggingNode.data.ord,
path: draggingNode.data.path
},
type: dropType
}
moveDirectory(params)
.then(res=>{
console.log(res)
// 更新节点数据
})
.catch(()=>{
// 删除放置的节点
this.$refs.tree.remove(draggingNode.data)
// 节点位置还原
draggingNode.parent = this.$refs.tree.getNode(draggingNode.data.parentId)
this.$refs.tree.getNode(draggingNode.data.parentId).childNodes.splice(this.beforeDragNodeIndex, 0, draggingNode)
})
},
6.某节点高亮
我的项目需求是点击了一个资源之后将它所在的目录高亮显示,当然了,如果大家的树不是懒加载的情况,那直接使用组件提供的setCurrentKey这个方法就可以了
如果有人跟我一样是懒加载,一定会发现个鸡肋的问题,就是有的节点还没加载子节点,如果高亮显示的是他的子节点,根本不会高亮显示的
解决思路:必须拿到需要高亮显示节点的父级层级关系,也就是它的父级是谁,它的父级的父级是谁,直到顶层,然后我们可以利用这个节点的层级信息,依次从顶点开始展开直到该节点,随后高亮显示该节点
来吧,代码来了
// 设置当前高亮(dirId:需要高亮显示的目录id也就是每个节点绑定的那个key dirPath:节点层级关系,我这里是以/分隔开的字符串)
setCurrentKey(dirId, dirPath){
let pathArr = dirPath.split('/')
// 去掉数组首尾:首节点是空,尾节点是当前节点(同dirId)
pathArr = pathArr.slice(1, pathArr.length - 1)
// 展开节点的所有父级
this.expandNodes(pathArr, 0, ()=>{
// 节点展开之后触发高亮节点
this.$refs.tree.setCurrentKey(dirId)
})
/**
* 循环展开节点
* nodesArr:需要展开的节点数组
* index:从第几个下标开始(为了方便函数递归使用)
* expandCallback: 所有节点展开之后的回调
*/
expandNodes(nodesArr, index = 0, expandCallback){
if(index < nodesArr.length){
this.$refs.tree.getNode(nodesArr[index]).expand(()=>{
index++
if(index == nodesArr.length){
expandCallback ? expandCallback() : ''
}else{
this.expandNodes(nodesArr, index, expandCallback)
}
})
}else{
// 递归出口
}
},
7.总结
起关键作用的几个东西
- getNode() : 根据 data 或者 key 拿到 Tree 组件中的 node,官网给了这个函数,自己去查一下就行
- expand(): 节点node对象的函数,展开指定节点,如果已经展开过,直接展开,如果没有展开过,先加载该节点的子节点,然后展开
- Node节点:箭头指向的是我此次实践中常用到的哦,它里面的data就是我们从接口拿到的该节点的数据,parent是父节点的Node对象,...
8. 组件整体的代码
粘贴一下给大家,方便看的清楚
// 带有管理操作的目录树
<template>
<div class="container-menu" :style="{height: viewHeight + 'px'}">
<!-- 搜索区域 -->
<div class="menu-header">
<el-input
v-model="filterText"
placeholder
id="input"
size="mini"
suffix-icon="el-input__icon el-icon-search"
@keyup.enter.native="filterMenu(-1)"
></el-input>
</div>
<!-- 目录 -->
<el-scrollbar class="scrollbar-container" wrapStyle="margin-bottom:0">
<el-tree
ref="tree"
lazy
:load="loadNode"
node-key="id"
:props="defaultProps"
:highlight-current="true"
:expand-on-click-node="false"
:current-node-key="currentNodekey"
:default-expanded-keys="defaultExpandKeys"
draggable
:allow-drop="allowDrop"
:allow-drag="allowDrag"
@node-drag-start="handleDragStart"
@node-drop="handleDrop"
@node-click="handleNodeClick">
<div class="custom-tree-node flex-space-between" slot-scope="{ node, data }" >
<div class="tree-icon">
<icon-svg class="file" icon-class="file"></icon-svg>
</div>
<div class="tree-label">
<span v-show="editDirId !== data.id">
{{node.label}}
<template v-if="data.num">({{data.num}})</template>
</span>
<el-input
:ref="data.id"
v-show="editDirId === data.id"
v-model.trim="data.title"
size="mini"
maxlength="15"
@keyup.enter.native="$event.target.blur"
@blur="labelBlur(node)"
></el-input>
</div>
<!-- 添加、编辑、删除按钮 -->
<div class="tree-options">
<!-- 带子节点的节点 有刷新功能 -->
<i v-if="!node.isLeaf" class="btn blue el-icon-refresh" @click.stop="refreshNode(node)"></i>
<!-- 添加节点 -->
<i class="btn blue el-icon-plus" @click.stop="addNode(node)"></i>
<!-- 我的目录(id=-1)不能修改 -->
<i v-if="data.id != -1" class="btn el-icon-edit" @click.stop="showInput(data.id, data.title)"></i>
<!-- 我的目录(id=-1)不能删除 :aaa=" data.id"-->
<i v-if="data.id != -1" class="btn red el-icon-delete" @click.stop="deleteDir(node, data.id)"></i>
</div>
</div>
</el-tree>
</el-scrollbar>
</div>
</template>
<script>
import { isArray } from 'utils/index'
import { mapGetters } from 'vuex'
import {
getChildernDirectoryById,
insertDirectory,
updateDirectory,
deleteDirectoryById,
moveDirectory
} from 'api/datameta/sourceRegister/index'
export default {
name: 'menuWithManage',
data () {
return {
// 树状图默认配置
defaultProps: {
children: 'children',
label: 'title',
isLeaf: function(data, node){
return data.isLeaf == 'true' ? true : false
}
},
// 当前选中的节点
currentNodekey: '',
// 过滤文本
filterText: '',
// 编辑目录id:控制input框显示
editDirId: '',
// 编辑目录名:记录旧目录名
oldDirName: '',
// 默认展开的节点的 key 的数组
defaultExpandKeys: ["-1"],
// 新增目录id的后缀: 用于新增时起的初始目录名不重复
dirIdSuff: 0,
// 节点拖拽前在父节点的位置下标
beforeDragNodeIndex: 0
}
},
computed: {
...mapGetters(['viewHeight'])
},
methods: {
// 获取目录列表
getMenuList(parentId, success, error){
getChildernDirectoryById({
directoryId: parentId,
dirName: this.filterText
})
.then(res => {
// 获取"我的目录"子节点时, 更新"我的目录"的资源条数
if(parentId == -1){
this.$refs.tree.getNode(-1).data.num = res.data.num
}
success ? success(res.data.dirInfo) : ''
})
.catch(res=>{
error ? error(res) : ''
})
},
// 点击节点的回调 node-click
handleNodeClick (data, node) {
this.$emit('chooseMenuItem', data, node)
},
// 子节点加载方法
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{
id: '-1',
title: '我的目录',
path: '/-1',
num: 0,
isLeaf: false
}]);
}else{
this.getMenuList(
node.data.id,
data=>{
resolve(data)
},
error=>{
resolve([])
}
)
}
},
// 允许放置的节点设置
allowDrop(draggingNode, dropNode, type) {
if (dropNode.data.id == -1) {
return type === 'inner';
} else {
return true;
}
},
// 允许拖拽的节点设置
allowDrag(draggingNode) {
return draggingNode.data.id != -1;
},
// 拖拽前: 保存被拖拽元素的下标
handleDragStart(draggingNode, event){
draggingNode.parent.childNodes.forEach((childNode, nodeIndex) =>{
if(childNode.key == draggingNode.key){
this.beforeDragNodeIndex = nodeIndex
return
}
})
},
// 拖拽完成
handleDrop(draggingNode, dropNode, dropType, ev) {
// console.log('tree drop 拖拽完成: ',draggingNode.parent, dropNode.parent, dropType, ev);
// 将dropType对应成数字
switch (dropType) {
case 'before':
dropType = 0;
break;
case 'after':
dropType = 1;
break;
case 'inner':
dropType = 2;
break;
default:
dropType = 0;
break;
}
let params = {
affectedNode: {
id: dropNode.data.id,
parentId: dropNode.data.parentId,
title:dropNode.data.title,
ord: dropNode.data.ord,
path: dropNode.data.path
},
dragNode: {
id: draggingNode.data.id,
parentId: draggingNode.data.parentId,
title:draggingNode.data.title,
ord: draggingNode.data.ord,
path: draggingNode.data.path
},
type: dropType
}
moveDirectory(params)
.then(res=>{
console.log(res)
// 更新节点数据
})
.catch(()=>{
// 删除放置的节点
this.$refs.tree.remove(draggingNode.data)
// 节点位置还原
draggingNode.parent = this.$refs.tree.getNode(draggingNode.data.parentId)
this.$refs.tree.getNode(draggingNode.data.parentId).childNodes.splice(this.beforeDragNodeIndex, 0, draggingNode)
})
},
// 刷新指定节点
refreshNode(node){
node.loaded = false
node.expand()
},
// 点击了添加子节点按钮
addNode(node){
node.expand(()=>{
this.dirIdSuff = 0;
let newDirName = this.getOnlyDirName('新建目录', node.childNodes)
let order = node.childNodes.length > 0 ? node.childNodes[node.childNodes.length-1].data.ord + 1 : 0
let newChild = {
title: newDirName,
isLeaf: "true",
ord: order,
num: 0
}
insertDirectory({
"parentId": node.data.id,
"path": node.data.path,
"title": newDirName,
"ord": order
})
.then(res => {
newChild.parentId = res.data.parentId
newChild.id = res.data.id
newChild.path = res.data.path
this.$refs.tree.append(newChild, node)
this.showInput(res.data.id, newChild.title)
})
})
},
// 点击了编辑按钮
showInput(id, title){
this.oldDirName = title
this.editDirId = id
this.$nextTick(() => {
this.$refs[id].focus()
})
},
// 点击了删除节点
deleteDir(node, dirId){
this.$confirm('确定删除[ '+node.data.title+' ]吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
deleteDirectoryById({ directoryId: dirId })
.then(res=>{
this.$message({
type: 'success',
message: '删除成功!'
})
this.$refs.tree.remove(node)
})
});
},
// input失去焦点(编辑目录名)
labelBlur (node) {
let data = node.data
this.editDirId = ''
// 名字相等的时候不用改
if (data.title === this.oldDirName) return
// 检查名字是否重复
let sameName = isArray(node.parent.childNodes) ?
node.parent.childNodes.some( childNode => childNode.data.title===data.title && childNode.data.id!==data.id )
:
false
if(sameName) {
data.title = this.oldDirName
this.$message({
type: 'error',
message: '同目录不允许同名哦!'
})
return
}
// 目录名称校验
// 修改目录,调用接口
updateDirectory({
"id": data.id,
"title": data.title
})
.then(res => {
this.$message({ type: 'success', message: '修改成功!' })
})
.catch(() => {
data.title = this.oldDirName
})
},
// 过滤函数(递归展开所有符合规则的节点)
filterMenu(nodeId = -1){
// 过滤文本为空时,刷新顶级节点
if(this.filterText.trim() == '') {
let topNode = this.$refs.tree.getNode(-1)
topNode.childNodes = []
topNode.loaded = false
topNode.expand()
return
}
let node = this.$refs.tree.getNode(nodeId)
// 重新展开该节点
console.log(nodeId)
node.loaded = false
node.expand(()=>{
// 检查子元素是否有非叶子结点
node.childNodes.forEach(item => {
if(item.data.isLeaf == 'false'){
// 递归处理含有子节点的节点
this.filterMenu(item.data.id)
}else{
// 递归出口
}
});
})
},
// 寻找一个合适的目录名
getOnlyDirName (title, arr) {
let dirName = title + this.dirIdSuff
isArray(arr) ? '' : arr = []
if(arr.find(val => val.data.title === dirName)){
this.dirIdSuff++
return this.getOnlyDirName(title, arr)
}else{
this.dirIdSuff = 0
return dirName
}
},
// 寻找一个合适的目录id
getOnlyDirId (data){
let dirId = data.id + '' + this.dirIdSuff
let arr = isArray(data.children) ? data.children : []
if(arr.find(val => val.id == dirId)){
this.dirIdSuff++
return this.getOnlyDirId(data)
}else{
this.dirIdSuff = 0
return dirId
}
},
// 设置当前高亮
setCurrentKey(dirId, dirPath){
let pathArr = dirPath.split('/')
// 去掉数组首尾:首节点是空,尾节点是当前节点(同dirId)
pathArr = pathArr.slice(1, pathArr.length - 1)
// 展开节点的所有父级
this.expandNodes(pathArr, 0, ()=>{
// 节点展开之后触发高亮节点
this.$refs.tree.setCurrentKey(dirId)
})
},
/**
* 循环展开节点
* nodesArr:需要展开的节点数组
* index:从第几个下标开始(为了方便函数递归使用)
* expandCallback: 所有节点展开之后的回调
*/
expandNodes(nodesArr, index = 0, expandCallback){
if(index < nodesArr.length){
this.$refs.tree.getNode(nodesArr[index]).expand(()=>{
index++
if(index == nodesArr.length){
expandCallback ? expandCallback() : ''
}else{
this.expandNodes(nodesArr, index, expandCallback)
}
})
}else{
// 递归出口
}
},
}
}
</script>
<style rel='stylesheet/scss' lang='scss' scoped>
.container-menu{
.menu-header{
border-bottom: 1px solid #e8e8e8;
/deep/.el-input__inner{
height: 32px;
line-height: 32px;
border: 0;
}
}
.scrollbar-container{
height: calc(100% - 46px);
padding-top: 7px;
}
}
.custom-tree-node {
width: calc(100% - 24px);
font-size: 12px;
line-height: 24px;
align-items: center;
.tree-icon {
width: 20px;
.file {
width: 20px;
font-size: 20px;
vertical-align: text-bottom;
}
}
.tree-label {
// 100% - 50px - 20px
width: calc(100% - 70px);
// 1.先强制一行内显示文本
white-space: nowrap;
// 2.超出部分隐藏
overflow: hidden;
// 3.文字用省略号代替
text-overflow: ellipsis;
height: 24px;
line-height: 24px;
.el-input {
/deep/.el-input__inner {
height: 21px;
}
}
}
.tree-options{
display: flex;
justify-content: flex-end;
opacity: 0;
width: 50px;
margin-right: 6px;
}
.btn {
font-size: 10px;
font-weight: bold;
}
.btn:nth-child(odd){
margin: 0 4px;
}
.blue {
color: #409eff;
}
.red {
color: #f56c6c;
}
}
// 节点hover,操作按钮显示
.custom-tree-node:hover .tree-options{
opacity: 1 !important;
}
</style>
有啥更好的解决办法,还望大家不吝赐教!同时也欢迎指出问题!