最近做了蛮多需求都是在vue里面去操作dom,着实让人头大
需求如下:要求树形结构按照设计稿样式(ztree原本的样式ui接受不了)
鼠标浮动上去的时候,有功能按钮出现,浮在功能按钮上面,出现对应的功能提示
支持树形结构搜索,搜索后高亮显示,并出现默认选中样式
树结构很长,支持滚动(会有一个问题,浮动的功能提示信息受overflow的影响,那么第一行的提示信息会被覆盖)
好了,看下做好的效果图吧:
搜索高亮,支持模糊搜索,右侧其实还有一个面包屑,和这个是联动效果,主要是介绍ztree用法,就不说太多业务场景了
这个样式比ztree自带的样式好看多了,需要修改ztree的样式文件,下面就说说用法步骤吧
ztree没有依赖包可以下载,需要在官网上下载后在vue中引用,ztree依赖jquery,使用前先安装好jqery的依赖包,在需要使用的页面引入,ztree的样式文件在main.js中引入使用
首先就是样式问题,找到页面上原本ztree的样式文件,替换ui切的icon就行,基操,就不赘述了
在data()中做ztree的定义,以下内容写在setting {}对象中,vue的data(){return {}}中定义ztreeObj:null,//zTree对象,用于赋值ztree初始化之后的树对象
setting: {
treeId: "id",
data: {
simpleData: {
enable: true,
idKey:'id',
pIdKey:'pId',
rootPId:'-'
},
key:{
isParent: "parent",
name:'name',
title:'name'
},
},
callback: {
// 树的点击事件
onClick: this.zTreeOnClick,
onAsyncSuccess: this.zTreeOnAsyncSuccess,//异步加载成功的fun
beforeAsync: this.zTreeBeforeAsync,//异步加载前的回调
onExpand: this.expandNode//节点展开回调
},
edit: {//是否支持拖拽,enable我改成了false,代表此功能禁用,也可以直接删除,为了防止后续他们提这个需求,所以我还是写上了
drag: {
isMove: true,
prev: true,
next: true,
inner: true
},
enable: false,
showRemoveBtn: false,
showRenameBtn: false
},
view:{
addHoverDom:this.addhoverdom,//ztree提供的可以自定义添加dom
removeHoverDom:this.removehoverdom,//和addHoverDom成对出现,离开节点时需要移除自定义的dom
fontCss: function (treeId, treeNode) {//设置所有节点的样式,我这里的代码的意思是,当前节点是否高亮(树节点搜索的时候会高亮命中的节点),高亮就设置节点高亮样式,否则就是普通样式
return (!!treeNode.highlight) ? {'backgroundColor':'#F6F7F8','display':'inline-block','width':'95%','min-width':'225px','padding':'3px 0'} : {
color: "#000000", "font-weight": "normal"
};
},
showTitle:true //是否显示titie属性(就是鼠标放到节点上是否显示html元素的title属性)
},
async:{//节点很多的情况下设置懒加载
enable:true,//是否开启异步加载模式
contentType: "application/json",//Ajax 提交参数的数据类型
dataType: "json",//Ajax 获取的数据类型
url:'/aa/bbb/ccc/loadTree',//点击树的展开节点,会重新加载子节点,这里是请求的url地址
type:'POST',//当前的请求类型
// autoParam:['id=parentId'],//将节点的pId值作为参数传递
// otherParam:{'userId':()=>{return this.userId;},'userName':()=>{return this.userName;},'tenantId':()=>{return this.tenantId;}}
otherParam:{'userId':this.userId,'userName':this.userName,'tenantId':this.tenantId,'parentId':'-'},//每次异步请求携带的参数
dataFilter:function(treeId, parentNode, resp){
sessionStorage.setItem('tongbunodes',JSON.stringify(resp.jsSubjects.children));
return resp.subjects;
}//对 Ajax 返回数据进行预处理的函数,就是异步加载返回的数据你可以处理一下再用
}
},
页面结构
<div class="panel-bottom">
<div class="data-trees-style data-trees-style-hasvy">
<ul id="treeDemo" class="ztree" style="padding-top:13px"></ul>
</div>
</div>
//ul为存放树结构的位置,同一个页面如果有多个树结构,id不能重复(id本身也不能重复),否则就会遇到一个问题是你加载第二个树的时候,会发现第二个树没有渲染出来,因为他把第一个替换掉了
异步加载前的回调:
zTreeBeforeAsync(treeId, treeNode){
if(this.ztreeObj){//树节点对象是否存在,如果存在,说明已经加载了根节点,通过点击展开节点,开启加载下一层级节点信息,这个时候传递的上级节点id的参数需要发生变化
if(treeNode){
this.ztreeObj.setting.async.otherParam['parentId'] = treeNode.id;
}else{
//说明加载的是第一级根节点,无上级节点信息,和后台约束好传递什么,这里我传‘-’
this.ztreeObj.setting.async.otherParam['parentId'] = '-';
}
}
return true;
},
树加载,在mounted中初始化
mounted(){
this.ztreeObj = $.fn.zTree.init($("#treeDemo"), this.setting);
this.ClientRect = $('#panel-top')[0].getBoundingClientRect()
//getBoundingClientRect获得dom元素在页面上的位置,无关定位,卷曲高度等,目的是为了获得“专题目录”这个固定定位的元素在页面中的位置,而当树节点滚动加载的时候,也可以使用同样的方法获得当前树节点在页面的位置,两者之间的差值是不变的,就可以将当前位置的第一个(并非dom结构中的第一个位置)的功能提示信息放到下面
},
在生命周期函数中初始化树节点之后,页面就有节点信息了,现在需要将鼠标hover到节点上去的时候出现灰色背景,并且出现六个功能小图标
addhoverdom(treeId, treeNode){ console.log(111) //this.removehoverdom(); let _this= this // treeId 对应的是当前 tree dom 元素的 id // treeNode 是当前节点的数据 var aObj = $("#" + treeNode.tId + "_a"); // 获取节点 dom let spanObj = $("#" + treeNode.tId + "_a").find('.node_name') if ($("#diyBtnGroup").length>0) return; // 查看是否存在自定义的按钮组,因为 addHoverDom 会触发多次 <p class="toolnames" id='diyBtn_${treeNode.id}_names'>${treeNode.name}</p> <li id='diyBtn_space_${treeNode.id}'> </li> var editStr = `<div id='diyBtnGroup'> <ol> <li class='mydiydiv hot' id='diyBtn_${treeNode.id}_hot' οnfοcus='this.blur();'><span class="tooltips hothover">热点</span></li> <li class='mydiydiv del' id='diyBtn_${treeNode.id}_delete' οnfοcus='this.blur();'><span class="tooltips deletehover">删除</span></li> <li class='mydiydiv mod' id='diyBtn_${treeNode.id}_modify' οnfοcus='this.blur();'><span class="tooltips edithover">编辑</span></li> <li class='mydiydiv offline' id='diyBtn_${treeNode.id}_offline' οnfοcus='this.blur();'><span class="tooltips offhover">下线</span></li> <li class='mydiydiv online' id='diyBtn_${treeNode.id}_online' οnfοcus='this.blur();'><span class="tooltips onhover">上线</span></li> <li class='mydiydiv add' id='diyBtn_${treeNode.id}_add' οnfοcus='this.blur();'><span class="tooltips addhover">新建</span></li> </ol> </div>`; spanObj.append(editStr); if(treeNode.isHover){//这里是核心代码,遇到一个贼大的坑,搞了两天,hover上去之后,图标可以显示出来,但是如果是点击某一个节点,再将鼠标移入到别的dom结构,会发现小图标不会出现,isHover是节点自带属性,hover上去的时候,给a标签设置hover属性,然后给a绑定一个鼠标离开事件, 设置鼠标从节点移出时,删除由 addHoverDom 增加的按钮 aObj.css({'backgroundColor':'#F6F7F8','display':'inline-block','width':'95%','min-width':'225px','padding':'3.5px 0'}); aObj.on('mouseleave',function(){ _this.removehoverdom(null,treeNode); }); } //$("#diyBtnGroup").css({'backgroundColor':'red'}); var bObj = $("#" + treeNode.tId + "_icon"); // 获取节点 dom bObj.css({'vertical-align':'inherit'});//避免鼠标浮上去之后,发生抖动(巨大一个坑,也搞了两天) var btnHot = $('#diyBtn_'+treeNode.id + '_hot'); var btnDelete = $('#diyBtn_'+treeNode.id + '_delete'); var btnModify = $('#diyBtn_'+treeNode.id + '_modify'); var btnAdd = $('#diyBtn_'+treeNode.id + '_add'); var btnOffline = $('#diyBtn_'+treeNode.id + '_offline'); var btnOnline = $('#diyBtn_'+treeNode.id + '_online'); // 小图标点击事件 if (btnDelete) btnDelete.bind("click", function (){_this.delete(treeNode)}); if (btnAdd) btnAdd.bind("click", function (){_this.add(treeNode)}); if (btnModify) btnModify.bind("click", function (){_this.modify(treeNode)}); if (btnHot) btnHot.bind("click", function (){_this.hot(treeNode)}); if (btnOffline) btnOffline.bind("click", function (){_this.offline(treeNode)}); if (btnOnline) btnOnline.bind("click", function (){_this.online(treeNode)}); // 小图标hover事件 if (btnDelete) btnDelete.bind("mouseover", function (e){_this.deleteiconhover(treeNode,e)}); if (btnAdd) btnAdd.bind("mouseover", function (e){_this.addiconhover(treeNode,e)}); if (btnModify) btnModify.bind("mouseover", function (e){_this.modifyiconhover(treeNode,e)}); if (btnHot) btnHot.bind("mouseover", function (e){_this.hoticonhover(treeNode,e)}); if (btnOffline) btnOffline.bind("mouseover", function (e){_this.offlineiconhover(treeNode,e)}); if (btnOnline) btnOnline.bind("mouseover", function (e){_this.onlineiconhover(treeNode,e)}); // 鼠标离开事件 if (btnDelete) btnDelete.bind("mouseleave", function (){_this.deleteiconleave(treeNode)}); if (btnAdd) btnAdd.bind("mouseleave", function (){_this.addiconleave(treeNode)}); if (btnModify) btnModify.bind("mouseleave", function (){_this.modifyiconleave(treeNode)}); if (btnHot) btnHot.bind("mouseleave", function (){_this.hoticonleave(treeNode)}); if (btnOffline) btnOffline.bind("mouseleave", function (){_this.offlineiconleave(treeNode)}); if (btnOnline) btnOnline.bind("mouseleave", function (){_this.onlineiconleave(treeNode)}); },
鼠标移除节点事件,和addhoverDom成对出现
removehoverdom(treeId, treeNode){
// 为了方便删除整个 button 组,上面我用 #diyBtnGroup 这个包了起来,这里直接删除外层即可,不用挨个找了。
$("#diyBtnGroup").unbind().remove();
var aObj = $("#" + treeNode.tId + "_a");
if(!treeNode.highlight){
aObj.css({'backgroundColor':'#fff'});
}
},
以删除功能为例,其他的功能一样写就完事了
deleteiconhover(treeNode,event){
let domname = $('.deletehover')
domname.css({'display':'inline-block'})
this.measurement(domname,event)
this.isHovering =treeNode.name || treeNode.text;记录当前鼠标所在的节点的title,原因是鼠标移入节点,我希望是弹出title,移到小图标上的时候,由于它属于这个dom结构,也会出现title提示和功能提示框,会发生遮挡
this.operNode = treeNode;
$("#" + treeNode.tId + "_a").attr('title','');//鼠标移入小图标的时候移除title属性
},
deleteiconleave(){
$('.deletehover').css({'display':'none'})
$("#" + this.operNode.tId + "_a").attr('title',this.isHovering);//鼠标离开的时候加上title属性(为我聪明的头脑骄傲)
},
//上面提到的,如果是靠近“专题目录最近的一个,将功能提示移到下方”
measurement(domname,event){
let coordinates = $('#diyBtnGroup')[0].getBoundingClientRect()
if(coordinates.bottom - this.ClientRect.bottom < 35){
domname.css({'display':'inline-block','top':'25px'})
} else {
domname.css({'display':'inline-block','top':'-35px'})
}
},
接下来就是用户搜索树节点的时候,我希望高亮命中的节点树,这部分提供参考,由于我们的后台比较懒惰,不愿意按照我的想法返回后台数据(我的想法:既然要求是懒加载返回数据,那么我希望第一次请求返回给我所有的根节点信息,当我点击某一个带+号的根节点,将该id传递异步加载返回该节点下所有的子节点)我们后台的处理方式:第一次请求,给我返回了两个数组,一个是所有的根节点,第二个,给我返回了已存在的所有节点,包括子节点,真是优秀。so我在data()中又定义了一个同步的setting
tongbusetting: {
treeId: "id",
data: {
simpleData: {
enable: true,
idKey:'id',
pIdKey:'parentId',
rootPId:'-'
},
key:{
isParent: "parent",
name:'text',
title:'text'
},
},
callback: {
// 树的点击事件
onClick: this.zTreeOnClick,
onExpand: this.expandNode
},
edit: {
drag: {
isMove: true,
prev: true,
next: true,
inner: true
},
enable: false,
showRemoveBtn: false,
showRenameBtn: false
},
view:{
addHoverDom:this.addhoverdom,
removeHoverDom:this.removehoverdom,
fontCss: function (treeId, treeNode) {
return (!!treeNode.highlight) ? {'color':'red','background-color':'#F6F7F8','display':'inline-block','width':'95%','min-width':'225px','padding':'3px 0','font-weight':'bold'} : {
color: "#000000", "font-weight": "normal"
};
},
showTitle:true
}
},
// 树搜索
searchtrees(){
/**以下为同步搜索节点的方法***/
if(this.searchProject == null || '' == this.searchProject){
return this.$message.error('请输入搜索关键字');
}
// let _this = this;
// this.getSubjectTrees(function(){
// _this.updateNodes(false);
// let firstNodes = _this.ztreeObj.getNodes();
// let nodeArr = _this.ztreeObj.transformToArray(firstNodes);
// _this.oldNodes = nodeArr.filter(item=>item.text.indexOf(_this.searchProject) > -1);
// // _this.oldNodes = _this.ztreeObj.getNodesByParamFuzzy("name", _this.searchProject, null);
// _this.updateNodes(true);
// });
if(this.ztreeObj){
this.ztreeObj.destroy();
}
// 回调成功之后,初始化树结构
this.ztreeObj = $.fn.zTree.init($("#treeDemo"), this.tongbusetting, JSON.parse(sessionStorage.getItem('tongbunodes')));
this.updateNodes(false);
let firstNodes = this.ztreeObj.getNodes();//ztree的回调,返回所有根节点简单数据结构
let nodeArr = this.ztreeObj.transformToArray(firstNodes);//ztree回调,返回所有节点简单数据结构
this.oldNodes = nodeArr.filter(item=>item.text.indexOf(this.searchProject) > -1);//判断是否有命中节点
if(this.oldNodes.length == 0) return this.$message.error('没有该专题资源')
this.updateNodes(true);
},
将命中的节点高亮
updateNodes(flag){
//遍历搜索高亮显示
for (var i = 0, l = this.oldNodes.length; i < l; i++) {
this.oldNodes[i].highlight = flag;
this.ztreeObj.updateNode(this.oldNodes[i]);
this.ztreeObj.expandNode(this.oldNodes[i].getParentNode(), flag, null, null, false);
}
},
插补一下,由于考虑到某些客户会暴力检测,故意输入很长的节点名称,导致页面难看,我在节点展开回调增加如下代码:设置每一个层级展开的宽度,超出省略号显示,并且在input输入的时候限制最大长度为20个字符,就可以了
// 树节点展开事件
expandNode(event, treeId, treeNode){
switch(treeNode.level){
case 0:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'200px'})
break;
case 1:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'180px'})
break;
case 2:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'160px'})
break;
case 3:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'140px'})
break;
case 4:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'130px'})
break;
case 5:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'115px'})
break;
case 6:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'95px'})
break;
case 7:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'80px'})
break;
case 8:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'75px'})
break;
case 9:
$(`.level${treeNode.level+1}>.node_name`).css({'width':'60px'})
break;
}
if(treeNode.level>9){
$(`.level${treeNode.level+1}>.node_name`).css({'width':'60px'})
}
},
最后,来看下动态效果吧