目录
遇到问题,持续更新
最新更改:继续优化【问题五】的第二种解决方案(将vue-treeselect + 拖动改变宽度方法进行二次封装,可直接重复调用,并增加 saveOldWidth 属性,用来是否保存上一次组件宽度)复制代码即可使用!
上次更改:
1. 新增【问题六】@input 事件里,想要拿到node参数,而不是value参数
2. 优化【问题五】的第二种解决方案(考虑页面上存在多个treeselect)
安装使用
vue-treeselect 安装使用
文章只记录部分问题,完整的 组件属性 及 组件的事件 等 可到文档中去查阅
官方文档:Vue-Treeselect
中文的这里有: Vue-Treeselect | Vue-Treeselect 中文网
- npm install --save @riophae/vue-treeselect
- 在main.js中
import Treeselect from '@riophae/vue-treeselect' // 导入vue-treeselect
import '@riophae/vue-treeselect/dist/vue-treeselect.css' // 导入样式
Vue.component('Treeselect', Treeselect); // 注册组件
- 在vue组件中进行使用
<treeselect
:multiple="true"
:options="options"
placeholder="Select your favourite(s)..."
v-model="value"
></treeselect>
相关问题解决方案
问题一:单选 绑定的值为""时,组件上会显示unkown
绑定的值设置为 null 或 undefined
提供另外一个思路,使用计算属性 computed 组件绑定的值 是 通过计算属性处理的值value1。使用了 setter 和 getter
- get(对value值为"" 时,进行处理,返回null或undefined)
- set(选择改变时,拿到value1的值,然后对value进行赋值)
问题二:数据提示设置为中文
组件默认的是英文
设置属性
<treeselect
:multiple="true"
:options="options"
placeholder="Select your favourite(s)..."
v-model="value"
noChildrenText="没有数据了"
noOptionsText="没有数据"
noResultsText="没有搜索结果"
></treeselect>
问题三:节点下没有数据时(children为空时),不显示展开图标
对chlidren源数据进行控制
normalizer: (node) => {
return{
children: node.children && node.children.length > 0 ? node.children: 0,
}
},
问题四:vue-treeselect样式设置
因为当时使用时,项目elementui的控件风格用发mini,所以这里样式示例匹配的也是mini大小
// treeselect 样式设置
// search 为父元素的calss
.search {
::v-deep .vue-treeselect {
width: 198px;
height: 28px;
line-height: 28px;
margin-top: 7px;
font-size: 12px;
}
::v-deep .vue-treeselect__control {
height: 28px;
}
::v-deep .vue-treeselect__placeholder,
::v-deep .vue-treeselect__single-value {
line-height: 28px;
}
}
问题五:因为宽度原因导致的节点(子节点)文本显示不全
解决方案1:设置样式
使用插槽slot, 并对其设置样式(达到悬浮显示文本的效果)
<treeselect
:multiple="true"
:options="options"
placeholder="Select your favourite(s)..."
v-model="value"
noChildrenText="没有数据了"
noOptionsText="没有数据"
noResultsText="没有搜索结果"
>
<p
style="overflow: hidden;white-space: nowrap;text-overflow: ellipsis;width: 90%;"
slot="option-label"
slot-scope="{node}"
:title="node.label">
<template> {{ node.label }}</template>
</p>
</treeselect>
解决方案2:拖动改变宽度【保留(1)原新更改的方案思路,这里目前最佳解决方案直接看(2)最新更改即可,同样粘贴可用】
(1)新更改:对原来的解决方案进行优化。【考虑同时存在多个treeselect 情况】
直接使用下方代码(js、css),实测可用的哈,直接粘贴应该就行了
此方案有点繁杂,想了解的朋友可以看看,若有缺陷或更好的方案,欢迎大家补充
实现treeselect的右边拖动,直接改变组件宽度,使文字显示完全。动图如下:
treeselect拉伸改变宽度
【优化:找到谁处于展开状态】:现在将页面中存在的treeselect 都考虑进来,通过treeselect子级是否存在,来找到当前处于打开状态的treeselect,对其添加拉伸条,通过拉伸 拉伸条条,进而改变组件宽度
使用了MutationObserver接口对treeselect组件内的高度变化进行监听,这样能够做到:当子节点被展开时,组件内部高度增大。拖动条的高度能够跟随其变化。【保证拖动条的高度与组件内部高度一致】
MutationObserver 接口提供了监视对 DOM 树所做更改的能力。
代码如下(因最新更改处已包含这里的代码及代码的注释,所以这里的代码就不做展示了,直接看最新更改即可)
(2)最新更改: 将vue-treeselect + 拖动改变宽度方法进行二次封装,可直接重复调用,并增加 saveOldWidth 属性,用来是否保存上一次组件宽度(附带效果视频)
实例化两个treeselect 效果视频:
treeselect封装并保存上一次宽度
对vue-treeselect + 拖动改变宽度方法进行二次封装,创建公共组件 MyTreeselect.vue 文件,直接使用
在父组件引入并使用 MyTreeselect.vue 组件
alwaysOpen 是控制组件是否总是处于打开状态
saveOldWidth 用来是否保存上一次组件宽度
<my-treeselect
:options="options"
:placeholder="placeholder"
v-model="value"
@open="placeholder = '11111'"
@select="alwaysOpen = false"
:alwaysOpen="alwaysOpen"
:saveOldWidth="true"
></my-treeselect>
options: [ {
id:'a',
label: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
children: [ {
id: 'aa',
label: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa',
}, {
id: 'ab',
label: 'ab',
} ],
}, {
id: 'b',
label: 'b',
}, {
id: 'c',
label: 'c',
} ],
以下是 MyTreeselect.vue 完整代码,使用了 v-bind=“$ attrs” 和 v-on=“$ listeners”
- v-bind=“$attrs”
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外)- v-on=“$listeners”
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
<template>
<treeselect
v-bind="$attrs"
v-on="$listeners"
:noChildrenText="noChildrenText"
:noOptionsText="noOptionsText"
:noResultsText="noResultsText"
:saveOldWidth="saveOldWidth"
@open="open"
>
<p
style="
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 90%;
"
:class="node.id === 'a' ? 'aClass' : 'bClass'"
slot="option-label"
slot-scope="{ node }"
:title="node.label"
>
<template> {{ node.label }}</template>
</p>
</treeselect>
</template>
<script>
export default {
props: {
noChildrenText: {
type: String,
default: "没有数据",
},
noOptionsText: {
type: String,
default: "没有数据",
},
noResultsText: {
type: String,
default: "没有搜索结果",
},
// 是否保存上一次组件的宽度
saveOldWidth: {
type: Boolean,
default: false,
}
},
data() {
return {
parentNode: null,
offsetNode: null,
treeSelectLeft: null, // 保存tree-select左边到视口左边的距离
oldTreeselectWidth: null, // 上一次组件的宽度
}
},
created() {
console.log(this.$attrs, this.$listeners);
},
methods: {
open(val) {
this.openTreeSelect();
this.$emit("open", val);
},
// 上次优化修改了openTreeSelect方法,并增加了addDragNode方法
/**
* 首先绑定treeselect组件的open事件(菜单打开事件)
*/
openTreeSelect() {
// 先对之前保存的旧数据进行清除
this.parentNode = null;
this.offsetNode = null;
// 测试发现在一个treeselect已经打开的情况下,直接点击打开另一个treeselect,会出现同时存在两个 .vue-treeselect__menu 的DOM元素,
// 即(i.childNodes[0] instanceof HTMLElement)存在两个true,则会导致拖动条永远是加在最后一个.vue-treeselect__menu DOM元素上
// 解决一: 考虑使用setTimeout,异步执行(但经过对此测试,发现偶尔还是会出现上述情况,不能完全解决)
// 解决二: 使用了setInterval 进行循环,对 .vue-treeselect__menu 的DOM元素数量进行判断,只有在数量为1的情况下,才进行后面的操作(调用addDragNode方法)
let timer = setInterval(() => {
this.$nextTick(() => {
let offsetNodeArr = document.getElementsByClassName(
"vue-treeselect__menu-container"
);
let num = 0;
for (let i of offsetNodeArr) {
if (i.childNodes.length > 0) {
// 判断其类型是否是DOM元素
if (!(i.childNodes[0] instanceof HTMLElement)) continue;
// console.log(i.childNodes[0] instanceof HTMLElement)
num++;
if (num >= 2) break;
this.offsetNode = i;
this.parentNode = i.childNodes[0];
// 判断是否需要将组件宽度设置为上一次的宽度
if(this.saveOldWidth && this.oldTreeselectWidth) {
this.parentNode.style = `width: ${
this.oldTreeselectWidth
}px; max-height: 300px`;;
}
}
}
if (num >= 2) {
return; // 存在多个 .vue-treeselect__menu DOM元素,终止函数执行
} else {
clearInterval(timer); // 清除timer
this.addDragNode(); // 满足条件,调用增加拖动条的方法
}
});
}, 200);
},
// 增加拖动条的方法
addDragNode() {
// 如果当前存在创建过的拖动条,将其移除
const dragNodes = document.getElementsByClassName("appendNode");
if (dragNodes && dragNodes.length > 0) {
for (let dragNode of dragNodes) {
dragNode.remove();
}
}
const dragNode = document.createElement("div"); // 创建拖动条
dragNode.className = "appendNode"; // 给拖动条设置样式
const treeSelectContentNode = document.getElementsByClassName(
"vue-treeselect__list"
)[0]; // 获取并保存 .vue-treeselect__list dom元素;根据childNode的高度变化,更新 拖动条(node) 的高度
// 如果元素都不存在
if (!this.parentNode) return;
dragNode.style = `height: ${treeSelectContentNode.clientHeight}px`; // 获取treeSelectContentNode 的高度,并赋值给拖动条
this.treeSelectLeft = this.offsetNode.getBoundingClientRect().left; // 保存tree-select左边到视口左边的距离 (getBoundingClientRect().left:dom的左边到视口左边的距离)
// 将拖动条添加至parentNode(.vue-treeselect__menu dom元素)下
this.parentNode.appendChild(dragNode);
/**
* 设置点击、拖动事件
*/
// 监听拖动条被左键点击按下
dragNode.addEventListener("mousedown", (event) => {
// 监听鼠标移动
document.addEventListener("mousemove", this.resizeMove);
});
// 监听鼠标左键抬起
document.addEventListener("mouseup", () => {
// 移除鼠标移动监听
document.removeEventListener("mousemove", this.resizeMove);
});
/**
* MutationObserver用来监视 DOM 变动
* 特点: 它与事件有一个本质不同。
* 事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件
* Mutation Observer 是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。
*/
const MutationObserver =
window.MutationObserver ||
window.webkitMutationObserver ||
window.MozMutationObserver;
const observer = new MutationObserver((list) => {
// 当被监听的元素发生变化,会执行该方法
// 异步触发,得到最新的高度后,再对node的高度进行赋值 (保证node)
dragNode.style = `height: ${treeSelectContentNode.clientHeight}px`;
});
// 监听 treeSelectContentNode 的变化(treeselect内部变化)
observer.observe(treeSelectContentNode, {
attributes: true,
subtree: true,
}); // attributes: 监听属性变化 subtree:监听子元素变化
/**
* disconnect() 方法告诉观察者停止观察变动
* observer.disconnect() 无参数
* 说明:如果被观察的元素被从 DOM 中移除,然后被浏览器的垃圾回收机制释放,此 MutationObserver 将同样被删除。
*/
},
// 拖动方法
resizeMove(event) {
if (event.clientX - this.treeSelectLeft > this.offsetNode.clientWidth) {
this.parentNode.style = `width: ${
event.clientX - this.treeSelectLeft
}px; max-height: 300px`;
this.oldTreeselectWidth = event.clientX - this.treeSelectLeft;
}
},
/**
1. 绑定treeselect组件的close事件(菜单关闭事件)
*/
closeTreeSelect() {
// treeselect 关闭时, 将创建的拖动条进行移除
const dragNodes = document.getElementsByClassName("appendNode");
if (dragNodes && dragNodes.length > 0) {
for (let dragNode of dragNodes) {
dragNode.remove();
}
}
this.parentNode = null;
this.offsetNode = null;
},
},
};
</script>
<style scoped>
::v-deep .appendNode {
position: absolute;
top: 0;
right: 0px;
height: 300px;
width: 2px;
transition: all linear 200ms;
}
::v-deep .appendNode:hover {
background-color: #999;
cursor: w-resize;
}
</style>
问题六:@input 事件里设置参数为node,而不是value
在treeselect事件中,我们来看看默认的input事件和select事件:
可以看到,这里input事件传递的参数为value,在大多时候,我们都会将treeselect的value绑定为每一项的id。但在某些特定的时候,我们既需要使用input事件,又想事件传递的参数是整个node,就像select事件传递的node 参数一样。那么这个时候就需要我们进行一些额外的设置。
先看两个图:
value:当multiple="false"时,value对应的是id或node对象,当multiple="true"时,value对应的是id或nodeobject的数组。其格式取决于valueFormat属性。
valueFormat:能够决定value属性的格式。当设置为"id"时,value属性的格式就是 id 或 id数组。当设置为"object"时,value属性的格式就是 node 或 node数组。
所以,如果我们想在input事件中,拿到整个node对象,可以使用
<treeselect
v-model="name"
:options="nameList"
valueFormat="object"
:multiple="true"
:flat="true"
:normalizer="normalizer"
@input="valueChange"
/>
valueChange(nodeArr) {
console.log(nodeArr); // [node,node,node]
}