1. 话题引入
存在这样一个场景:当页面尺寸发生变化时,希望滚动条能够随之动态调整,始终展示在 table 的可视区域的最下方,而不是整个 table 本身的最底部。
这种行为可以提升用户的使用体验,尤其是在处理大数据表格时,用户不需要滚动到整个页面的最底部才能看到滚动条,而是始终在当前可视区域操作。
效果如下,在 Table 未完全展示下的滚动条:
完全展示后,滚动条在 右侧 和 底部。
2. 如何实现?
这个实现方法比较困难,不能通过控制 Table 自带的滚动条或简单的 CSS 来实现效果。我尝试了很久,都不OK,因此决定手动实现一个与其本身风格一致的自定义滚动条。
需要解决的问题包括:
1、监听横向滚动条,实时捕获水平滚动状态并动态更新滚动条的位置和宽度。样式部分可以简单处理,不是重点。
2、监听视口可视区域高度变化,动态展示滚动条,使用 position 固定到可视区域底部。
3、实现拖拽滚动功能,通过鼠标按下滚动条并拖动,能够实现同步滚动的效果。
3. 解决实现
感兴趣的小伙伴可以自行实现下哦O。
HTML 结构
在 Table 下使用一个 div 实现滚动条效果,📢 外层的 div 是必须的,负责实时的定位效果。
<div class="custom-container" style={{ position: "relative" }}><ElTable ref={tableRef} {...props} {...attrs} v-slots={slots}>{slots.default && slots.default()}</ElTable><divclass="custom-scrollbar"style={{zIndex: 999,width: `${scrollBarWidthRef.value}px`,height: `${SCROLLBAR_HEIGHT}px`,position: "absolute",top: `${scrollBarTopRef.value}px`,left: `${scrollBarLeftRef.value}px`}}onMousedown={onMouseDown}></div>
</div>
第一个问题:滚动条宽度和位置处理
难点:监听横向滚动条变化
const getScrollBarWidth = () => {// 获取横向滚动条元素const scrollWrapper = tableRef.value?.$el.querySelector(".el-scrollbar__wrap");// 获取 table 元素大小及其相对位置const tableRect = tableRef.value?.$el.getBoundingClientRect();// ---- 获取滚动条的宽度(源码) ----const GAP = 4;const offsetWidth = scrollWrapper.offsetWidth - GAP;const originalWidth = offsetWidth ** 2 / scrollWrapper.scrollWidth;const scrollbarWidth = Math.max(originalWidth, SCROLLBAR_MIN_WIDTH);scrollBarWidthRef.value = scrollbarWidth;return {tableRect, // 表格大小scrollbarWidth // 滚动条宽度};
};
接下来设置滚动条的位置
// 设置滚动条宽度
const setScrollBarWidth = () => {const { scrollbarWidth } = getScrollBarWidth();scrollBarWidthRef.value = scrollbarWidth;
};// 设置滚动条高度
const setScrollBarTop = () => {const { tableRect } = getScrollBarWidth();scrollBarTopRef.value =window.innerHeight - tableRect.top - SCROLLBAR_HEIGHT > tableRect.height? tableRect.height - SCROLLBAR_HEIGHT: window.innerHeight - tableRect.top - SCROLLBAR_HEIGHT;
};// 监听滚动的同时设置 Left
const handleScroll = e => {const { tableRect } = getScrollBarWidth();setScrollBarTop();setScrollBarWidth();if (e.target) {// 比例计算scrollBarLeftRef.value = (e.target.scrollLeft / e.target.scrollWidth) * tableRect.width;}
};const handleWindowScroll = () => {setScrollBarTop();
};
既然方法都写好啦,那就开始调用。
onMounted(() => {// 初始化时执行// 为什么在 setTimeout 呢?避免过早地执行这些方法,它们依赖于表格 DOM 完全渲染完成后的尺寸信息。setTimeout(() => {setScrollBarWidth();setScrollBarTop();}, 0);// 确保 DOM 已完全更新nextTick(() => {if (tableRef.value) {scrollWrapperRef.value = tableRef.value?.$el.querySelector(".el-scrollbar__wrap");bodyWrapperRef.value = tableRef.value?.$el.querySelector(".el-table__body-wrapper");if (scrollWrapperRef.value) {scrollWrapperRef.value.addEventListener("scroll", handleScroll);}if (bodyWrapperRef.value) {window.addEventListener("scroll", handleWindowScroll);}}});
});
注意:当窗口尺寸变化时,需要重新计算滚动条的高度和宽度。
const handleResize = () => {setScrollBarWidth();setScrollBarTop();
};
window.addEventListener("resize", handleResize);
这个就比较简单了,监听鼠标抬起,移动的事件,设置滚动距离。
let isDragging = false;
let startX = 0;
let startLeft = 0;
let rafId: number | null = null;// 鼠标抬起
const onMouseUp = () => {isDragging = false;document.removeEventListener("mousemove", onMouseMove);document.removeEventListener("mouseup", onMouseUp);
};
// 鼠标移动
const onMouseMove = (e: MouseEvent) => {if (!isDragging) return;if (rafId !== null) {cancelAnimationFrame(rafId);}rafId = requestAnimationFrame(() => {const moveX = e.clientX - startX; // 计算拖动距离const newLeft = startLeft + moveX; // 计算滚动条的新位置scrollBarLeftRef.value = Math.max(0,Math.min(newLeft, tableRef.value?.$el.scrollWidth - scrollBarWidthRef.value));// 更新表格滚动位置if (scrollWrapperRef.value) {scrollWrapperRef.value.scrollLeft = (scrollBarLeftRef.value / tableRef.value?.$el.scrollWidth) * scrollWrapperRef.value.scrollWidth;}});
};
// 鼠标按下
const onMouseDown = (e: MouseEvent) => {isDragging = true;startX = e.clientX;startLeft = scrollBarLeftRef.value; // 当前滚动条的位置document.addEventListener("mousemove", onMouseMove);document.addEventListener("mouseup", onMouseUp);
};
按下滚动条可滑动 Table。
最后清空一下监听的事件。
onBeforeUnmount(() => {if (scrollWrapperRef.value) {scrollWrapperRef.value.removeEventListener("scroll", handleScroll);}window.removeEventListener("scroll", handleWindowScroll);window.removeEventListener("resize", handleResize);
});
自此就成功实现一个符合要求的滚动条 👏🏻 👏🏻 👏🏻。