Bootstrap

vue-treeselect 问题合集、好用的树形下拉组件(vue-treeselect的使用、相关问题解决方案)

目录

遇到问题,持续更新

最新更改:继续优化【问题五】的第二种解决方案(将vue-treeselect + 拖动改变宽度方法进行二次封装,可直接重复调用,并增加 saveOldWidth 属性,用来是否保存上一次组件宽度)复制代码即可使用!

上次更改:

1. 新增【问题六】@input 事件里,想要拿到node参数,而不是value参数

2. 优化【问题五】的第二种解决方案(考虑页面上存在多个treeselect)

安装使用

vue-treeselect 安装使用

文章只记录部分问题,完整的 组件属性 及 组件的事件 等 可到文档中去查阅
官方文档:Vue-Treeselect
中文的这里有: Vue-Treeselect | Vue-Treeselect 中文网

  1. npm install --save @riophae/vue-treeselect
  2. 在main.js中
import Treeselect from '@riophae/vue-treeselect' // 导入vue-treeselect
import '@riophae/vue-treeselect/dist/vue-treeselect.css' // 导入样式

Vue.component('Treeselect', Treeselect); // 注册组件
  1. 在vue组件中进行使用
<treeselect
  :multiple="true"
  :options="options"
  placeholder="Select your favourite(s)..."
  v-model="value"

></treeselect>

相关问题解决方案

问题一:单选 绑定的值为""时,组件上会显示unkown

绑定的值设置为 null 或 undefined

提供另外一个思路,使用计算属性 computed 组件绑定的值 是 通过计算属性处理的值value1。使用了 setter 和 getter

  1. get(对value值为"" 时,进行处理,返回null或undefined)
  2. 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”

  1. v-bind=“$attrs”
    包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外)
  2. 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]
}

如有其他问题或其他解决方案,欢迎评论、欢迎补充

;