效果
使用
<div style="position:relative;width:600px;height: 200px;border:1px solid #ddd">
<Scroll>
<template v-for="i in 20">
<div style="width:500px;height: 20px;margin: 20px;background: #ddd"></div>
</template>
</Scroll>
</div>
用途
想要个能够操作但是又不占据容器空间的滚动条,且最好不依赖其他的UI框架,方便二次开发。
原理
A层为实际滚动内容,为其设置overflow:auto
并且设置其滚动条display:none
。
B层为模拟滚动条,滚动条的宽度和高度分别和实际内容的宽度和高度相同。
同步A层和B层的滚动条滚动事件和滚动距离。
代码
<template>
<div ref="scrollWrap" class="scroll-wrap" @scroll="scroll(1)">
<div ref="scrollInner" class="scroll-inner">
<slot></slot>
</div>
</div>
<div ref="barH" :style="{'--d':width+'px'}" @scroll="scroll(2)" class="bar_h"></div>
<div ref="barV" :style="{'--d':height+'px'}" @scroll="scroll(3)" class="bar_v"></div>
</template>
<script setup>
import {getCurrentInstance, onMounted, ref} from "vue";
//使用举例,确保父节点为其子节点的定位元素
/*
<div style="position:relative;width:600px;height: 200px;border:1px solid #ddd">
<Scroll>
<template v-for="i in 20">
<div style="width:500px;height: 20px;margin: 20px;background: #ddd"></div>
</template>
</Scroll>
</div>
*/
/**
* 相较于 element-plus 的 scrollbar:
* 1、本设计涉及更少的计算,核心内容只是同步真实滚动条和虚拟滚动条的滚动位置。
* 2、不需要通过具体的尺寸控制滑块的位置,因此在表现上更为准确。
* 3、根节点没有固定为div,可以更换为其他标签或者组件,当然也可以根据习惯给组件添加一个根节点。
*
* 有关虚拟滚动条的定位问题,部分使用浏览器的默认行为
*/
import {useElementSize} from "@vueuse/core/index";
const {ctx} = getCurrentInstance()
const scrollWrap = ref()
const scrollInner = ref()
const barH = ref()
const barV = ref()
//DOM初始化之后给父元素添加 scroll 控制类 用以控制 虚拟滚动条的悬浮显示
onMounted(_ => {
ctx.$el.parentElement.classList.add('scroll')
})
//同步内容高度
let {height, width} = useElementSize(scrollInner)
/**
* 同步滚动条时因为会交替循环触发滚动事件所以在一个类型的滚动条滑动期间不应处理其他类型的滚动条滑动事件。
* 通过类型锁和定时器组合控制。
*/
const scroll = (() => {
let typeLock = null,typeLockTimer = null;
return type => {
//类型锁
if (typeLock == null) {
typeLock = type
} else if (typeLock !== type) {
return
}
//重置类型锁定时器
clearTimeout(typeLockTimer)
typeLockTimer = setTimeout(_ => {
typeLock = null
clearTimeout(typeLockTimer)
}, 100)
switch (type) {
case 1:
barH.value.scrollTo(scrollWrap.value.scrollLeft, 0)
barV.value.scrollTo(0, scrollWrap.value.scrollTop)
break
case 2:
scrollWrap.value.scrollTo(barH.value.scrollLeft, scrollWrap.value.scrollTop)
break
case 3:
scrollWrap.value.scrollTo(scrollWrap.value.scrollLeft, barV.value.scrollTop)
break
default:
break
}
}
})();
</script>
<style scoped lang="scss">
.scroll-wrap {
width: 100%;
height: 100%;
position: relative;
overflow: auto;
}
.scroll-inner {
width: fit-content;
height: fit-content;
}
.bar_h {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
overflow: auto hidden;
cursor: pointer;
display: none;
}
.bar_v {
position: absolute;
right: 0;
top: 0;
height: 100%;
overflow: hidden auto;
cursor: pointer;
display: none;
}
.bar_h:before {
content: '';
display: block;
width: var(--d);
height: 1px;
}
.bar_v:before {
content: '';
display: block;
width: 1px;
height: var(--d);
}
.scroll:hover .bar_h,
.scroll:hover .bar_v {
display: block;
}
.scroll-wrap::-webkit-scrollbar {
display: none;
}
/*滚动条美化*/
.bar_h::-webkit-scrollbar {
height: 6px;
}
.bar_v::-webkit-scrollbar {
width: 6px;
}
.bar_h,
.bar_v {
&::-webkit-scrollbar-thumb {
background-color: #0003;
border-radius: 10px;
transition: all .2s ease-in-out;
}
&::-webkit-scrollbar-track {
border-radius: 10px;
}
}
</style>