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>
<div
class="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);
OK,到此滚动条的位置完成,接下来处理滚动条的拖拽。
这个就比较简单了,监听鼠标抬起,移动的事件,设置滚动距离。
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);
});
自此就成功实现一个符合要求的滚动条 👏🏻 👏🏻 👏🏻。