一、实现思路
动态设置高度和宽度,高度很容易,就是el-tree-v2组件本身的高度,困难点是如何找到应该设置的宽度,我的思路是直接强行取到一级节点及其展开节点中最宽的一个元素,取这个元素的宽度,来动态设置整个容器的宽度,但是会遇到竖向滚动条的位置始终在最右边,这样的话当我们的父容器宽度小于总宽度的话,虽然可以横向滚动条展示没问题,但是纵向滚动条只有当横向滚动条拉到最右边的时候才会显示,于是,我动态设置纵向滚动条的位置,始终让它位于父容器的最右边,这样一来问题就都解决掉了,话不多说直接按照思路上代码。
二、实现过程
1.封装监视元素el-tree-v2内部改变的hooks
首先是如何监视元素的内部改变,这里我们实现一个监视元素内部变化的hooks 具体Api文档:developer.mozilla.org/zh-CN/docs/…
useMutationObserver.ts
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
import { buildProps } from "element-plus/es/utils/index.mjs";
export const VirtualizedTreeProps = buildProps({
height: {
type: Number,
},
width: {
type: Number,
},
} as const);
VirtualizedTree.vue
<template>
<div
:class="wrapClassName"
:style="{
width: wrapStyle.width + 'px',
height: wrapStyle.height + 'px',
overflow: 'hidden',
position: 'relative',
}"
>
<!-- style="overflow-x: scroll"-->
<div
:class="['my-scroll', className]"
:style="
{
'overflow-x': myScrollClassOverflow,
} as StyleValue
"
@mouseenter="onMouseEnter"
@mouseleave="onMouseleave"
>
<!-- :height="280"-->
<el-tree-v2
v-bind="$attrs"
:height="(wrapStyle.height as unknown as number) - 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 { onMounted, onUnmounted, ref, type StyleValue } from "vue";
import { VirtualizedTreeProps } from "./VirtualizedTree";
import * as _ from "lodash";
import useMutationObserver from "../../../hooks/src/useMutationObserver";
const { uniqueId, isNumber } = _;
// 解决浏览器审查模式下 最外层额外显示的props
defineOptions({
inheritAttrs: false,
});
//#region <移入移出>
const myScrollClassOverflow = ref("hidden");
const onMouseEnter = () => {
myScrollClassOverflow.value = "scroll";
};
const onMouseleave = () => {
myScrollClassOverflow.value = "hidden";
};
//#endregion
//#region <实现>
const className = uniqueId("yh-scroll-v-tree");
const wrapClassName = uniqueId("yh-scroll-tree-v-wrap");
const props = defineProps(VirtualizedTreeProps);
const width = ref(`${props.width}px`);
//#region <处理未传入宽高的情况>
const wrapStyle = ref({
width: props.width,
height: props.height,
});
const fatherDom = ref<HTMLElement>();
const autoSetWidth = (dom: HTMLElement) => {
if (!props.width) {
wrapStyle.value.height = dom?.clientHeight || 0;
}
};
const autoSetHeight = (dom: HTMLElement) => {
if (!props.height) {
wrapStyle.value.width = dom?.clientWidth || 0;
}
};
const autoSetWrapStyle = () => {
if (!fatherDom.value) return;
autoSetWidth(fatherDom.value);
autoSetHeight(fatherDom.value);
};
const initWrapStyle = () => {
// 没有传递宽高
if (!props.height || !props.width) {
fatherDom.value = document.querySelector(`.${wrapClassName}`)
?.parentNode as HTMLElement;
autoSetWrapStyle();
window.addEventListener("resize", autoSetWrapStyle);
}
};
onUnmounted(() => {
window.removeEventListener("resize", autoSetWrapStyle);
});
onMounted(() => {
initWrapStyle();
});
//#endregion
const callback = () => {
// 寻找宽度
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 as unknown as HTMLElement[]
).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 as unknown as HTMLElement[]).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 lang="scss" scoped>
:deep(.my-scroll) {
.el-tree {
position: static;
.el-vl__wrapper {
position: static;
}
}
}
</style>
三、使用文档
四、实现效果
实现效果代码
<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>
学习附件:点此下载