element-plus中el-tree-v2虚拟树组件实现自适应横向滚动条

一、实现思路

动态设置高度和宽度,高度很容易,就是el-tree-v2组件本身的高度,困难点是如何找到应该设置的宽度,我的思路是直接强行取到一级节点及其展开节点中最宽的一个元素,取这个元素的宽度,来动态设置整个容器的宽度,但是会遇到竖向滚动条的位置始终在最右边,这样的话当我们的父容器宽度小于总宽度的话,虽然可以横向滚动条展示没问题,但是纵向滚动条只有当横向滚动条拉到最右边的时候才会显示,于是,我动态设置纵向滚动条的位置,始终让它位于父容器的最右边,这样一来问题就都解决掉了,话不多说直接按照思路上代码。

二、实现过程

1.封装监视元素el-tree-v2内部改变的hooks

首先是如何监视元素的内部改变,这里我们实现一个监视元素内部变化的hooks 具体Api文档:developer.mozilla.org/zh-CN/docs/...

useMutationObserver.ts

js 复制代码
import { onMounted, onUnmounted, ref } from "vue";
import * as _ from "lodash";
const { isString, throttle } = _;

interface IMutationObserverOptions {
  childList: boolean; // 观察目标子节点的变化,是否有添加或者删除
  attributes: boolean; // 观察属性变动
  subtree: boolean; // 观察后代节点,默认为 false
}

/**
 * 创建并返回一个新的观察器,它会在触发指定 DOM 事件时,调用指定的回调函数
 * @param el
 * @param callback
 * @param observerOptions
 */
const useMutationObserver = (
  el: HTMLElement | string,
  callback: (
    mutationList: MutationRecord[],
    observer: MutationObserver
  ) => void = () => {},
  observerOptions: IMutationObserverOptions = {
    childList: true, // 观察目标子节点的变化,是否有添加或者删除
    attributes: false, // 观察属性变动
    subtree: true, // 观察后代节点,默认为 false
  }
) => {
  const observer = ref<MutationObserver>();
  const observerCallback = throttle(callback, 200);
  /**
   *
   * @param el {HTMLElement}  需要观察的元素
   */
  const creatObserver = function (el: HTMLElement) {
    if (!el) return;
    // 选择需要观察变动的节点
    const targetNode = el;

    // 观察器的配置(需要观察什么变动)
    const config = observerOptions;

    // 创建一个观察器实例并传入回调函数
    const observer = new MutationObserver(observerCallback);

    // 以上述配置开始观察目标节点
    observer.observe(targetNode, config);
    return observer;
  };
  onMounted(() => {
    const targetObserverEl = isString(el)
      ? (document.querySelector(el) as HTMLElement)
      : el;
    if (targetObserverEl) {
      observer.value = creatObserver(targetObserverEl);
    }
  });

  onUnmounted(() => {
    observer.value?.disconnect?.();
  });
  return {
    observer,
  };
};
export default useMutationObserver;

2.开始封装组件

文件目录结构

VirtualizedTree.ts

js 复制代码
import { buildProps } from "element-plus/es/utils/index.mjs";

export const VirtualizedTreeProps = buildProps({
  height: {
    type: Number,
    required: true,
  },
  width: {
    type: Number,
    required: true,
  },
} as const);
```const);

VirtualizedTree.vue

js 复制代码
<template>
  <div
    :class="wrapClassName"
    :style="{
      width: props.width + 'px',
      height: props.height + 'px',
      overflow: 'hidden',
      position: 'relative',
    }"
  >
    <div :class="['my-scroll', className]" style="overflow-x: scroll">
      <!--      :height="280"-->
      <el-tree-v2
        v-bind="$attrs"
        :height="props.height - 10"
        :style="{
          width: width,
        }"
      >
        <!-- 遍历子组件作用域插槽,并对父组件暴露 -->
        <template v-for="(index, name) in $slots" v-slot:[name]="data">
          <slot :name="name" v-bind="data"></slot>
        </template>
      </el-tree-v2>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { defineProps, ref } from "vue";
import { VirtualizedTree } from "./VirtualizedTree";
import * as _ from "lodash";
import useMutationObserver from "../../../hooks/src/useMutationObserver";
const { uniqueId, isNumber } = _;
// 解决浏览器审查模式下 最外层额外显示的props
defineOptions({
  inheritAttrs: false,
});
//#region <实现>
const className = uniqueId("yh-scroll-v-tree");
const wrapClassName = uniqueId("yh-scroll-tree-v-wrap");
const props = defineProps(VirtualizedTree);
const left = ref(`${props.width}px`);
const width = ref(`${props.width}px`);
const callback = (mutationList: MutationRecord[]) => {
  // 寻找宽度
  let maxWidth = 0;
  let maxPaddingLeft = 0;
  let checkBoxWidth = 0;
  // 获取虚拟树dom
  const treeDom = document.querySelector(
    `.${wrapClassName} .${className} .el-tree .el-tree-virtual-list`
  );
  Array.from(treeDom?.children?.[0]?.children).forEach((item: HTMLElement) => {
    getWidth(item);
  });
  // 是否存在checkbox
  const checkbox = document.querySelector(
    `.${wrapClassName} .${className} .el-tree .el-tree-virtual-list .el-checkbox`
  ) as HTMLElement;
  if (checkbox) {
    const checkBoxMargin =
      +getElNodeAttrValue(checkbox, "margin-right")?.split("px")?.[0] || 0;
    const checkBoxClientWidth = checkbox.clientWidth;
    checkBoxWidth = checkBoxMargin + checkBoxClientWidth;
  }

  // // 寻找最小宽度
  const minWidthNode = document.querySelector(`.${wrapClassName}`);
  const minWidth = minWidthNode?.clientWidth || 0;
  const targetWidth = maxWidth + maxPaddingLeft + checkBoxWidth;
  width.value = isNumber(targetWidth)
    ? (targetWidth > minWidth ? targetWidth : minWidth) + "px"
    : "100%";
  function getWidth(el: HTMLElement) {
    const elWidthNode = Array.from(el.children).find((item) => {
      return Array.from(item.classList || []).includes("el-tree-node__content");
    }) as HTMLElement;
    if (elWidthNode) {
      const paddingLeftValue =
        +getElNodeAttrValue(elWidthNode, "padding-left")?.split("px")?.[0] || 0;
      let elWidthNodeList = elWidthNode?.children || ([] as HTMLElement[]);
      let elWidth = 0;
      // 获取padding
      Array.from(elWidthNodeList).forEach((item) => {
        elWidth += item.clientWidth;
      });

      maxWidth = maxWidth > elWidth ? maxWidth : elWidth;
      maxPaddingLeft =
        maxPaddingLeft > paddingLeftValue ? maxPaddingLeft : paddingLeftValue;
    }

    if (el.ariaExpanded === "false") {
      return;
    }
    if (el.children) {
      Array.from(el.children).forEach((item: HTMLElement) => {
        getWidth(item);
      });
    }
  }
};
const getElNodeAttrValue = (el: HTMLElement, attrKey: string) => {
  const computedStyles = getComputedStyle(el);
  return computedStyles.getPropertyValue(attrKey) as string;
};
useMutationObserver(`.${wrapClassName} .${className} .el-tree`, callback);

//#endregion
</script>
<style scoped lang="scss">
:deep(.el-tree) {
  position: static;
  .el-vl__wrapper {
    position: static;
    .el-virtual-scrollbar {
      left: v-bind(left);
      right: auto !important;
      transform: translateX(-150%);
    }
  }
}
</style>

三、实现效果

实现效果代码

js 复制代码
<template>
  <div>
    <h2>直接使用</h2>
    <VirtualizedTree :data="data" :props="props" :height="400" :width="400" />
    <h2>带有选择器使用</h2>
    <VirtualizedTree
      show-checkbox
      :data="data"
      :props="props"
      :height="300"
      :width="300"
    />
  </div>
</template>
<script lang="ts" setup>
import { VirtualizedTree } from "../../../packages";

interface Tree {
  id: string;
  label: string;
  children?: Tree[];
}

const getKey = (prefix: string, id: number) => {
  return `${prefix}-${id}-测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试--`;
};

const createData = (
  maxDeep: number,
  maxChildren: number,
  minNodesNumber: number,
  deep = 1,
  key = "node"
): Tree[] => {
  let id = 0;
  return Array.from({ length: minNodesNumber })
    .fill(deep)
    .map(() => {
      const childrenNumber =
        deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren);
      const nodeKey = getKey(key, ++id);
      return {
        id: nodeKey,
        label: nodeKey,
        children: childrenNumber
          ? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
          : undefined,
      };
    });
};

const props = {
  value: "id",
  label: "label",
  children: "children",
};
const data = createData(3, 3, 100);
</script>
相关推荐
道不尽世间的沧桑1 小时前
第17篇:网络请求与Axios集成
开发语言·前端·javascript
diemeng11192 小时前
AI前端开发技能变革时代:效率与创新的新范式
前端·人工智能
bin91534 小时前
DeepSeek 助力 Vue 开发:打造丝滑的复制到剪贴板(Copy to Clipboard)
前端·javascript·vue.js·ecmascript·deepseek
晴空万里藏片云6 小时前
elment Table多级表头固定列后,合计行错位显示问题解决
前端·javascript·vue.js
曦月合一6 小时前
html中iframe标签 隐藏滚动条
前端·html·iframe
奶球不是球6 小时前
el-button按钮的loading状态设置
前端·javascript
kidding7236 小时前
前端VUE3的面试题
前端·typescript·compositionapi·fragment·teleport·suspense
无责任此方_修行中7 小时前
每周见闻分享:杂谈AI取代程序员
javascript·资讯
Σίσυφος19008 小时前
halcon 条形码、二维码识别、opencv识别
前端·数据库
学代码的小前端8 小时前
0基础学前端-----CSS DAY13
前端·css