众所周知,当容器高度固定而内容部分高度超出容器高度时,浏览器会渲染出一个可以滚动并用于显示剩余界面的条 -- 滚动条。它可以简单的样式修改,但是位置是固定的,无法移动。而我们需要改变位置的时候,它就不能满足我们的需求了,这时我们可以自己手写一个。
话不多说,步入正题,先创建测试文件,测试手写滚动条是否可用
xml
// test.vue
<template>
<div class="scrollLayout">
<!-- 内容 -->
<div class="content" ref="content" @scroll="scroll">
<template v-for="i in 30">
<div style="width: 10rem; text-align: center">{{ i }}</div>
</template>
</div>
<!-- 滚动条 -->
<div class="scrollBar" ref="scrollBar">
<div class="slider" :style="sliderStyle"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const content = ref(null); // ref绑定的内容元素
const scrollBar = ref(null); // ref绑定的手写滚动条
const sliderHeight = ref(0); // 滑块高度
const position = ref(0); // 手写滚动条的位置
const sliderStyle = ref(`height:${sliderHeight}%;margin-top:${position}px;`);
const contentCH = ref(0); // content盒子高度
const contentSH = ref(0); // content内容高度
const scrollBarCH = ref(0); // 滚动条高度
const activeScrollDistance = ref(0); // 内容滚动的距离
const contentScrollDistance = ref(0); // 内容滚动的距离
onMounted(() => {
const { clientHeight, scrollHeight } = content.value;
contentCH.value = clientHeight;
contentSH.value = scrollHeight;
scrollBarCH.value = scrollBar.value.clientHeight;
sliderHeight.value = (clientHeight / scrollHeight) * 100;
activeScrollDistance.value = scrollBarCH.value - scrollBarCH.value * (sliderHeight.value / 100);
contentScrollDistance.value = contentSH.value - contentCH.value;
})
// 内容滚动时
const scroll = () => {
const { scrollTop } = content.value;
position.value = (scrollTop * activeScrollDistance.value) / contentScrollDistance.value; // 滑块需要滑动的距离
};
</script>
<style scoped>
.scrollLayout {
position: relative;
padding: 1rem;
font-size: 1rem;
}
.content {
height: 20rem;
overflow: auto;
background: skyblue;
}
.scrollBar {
position: absolute;
top: 0;
right: 1rem;
width: 5px; /* 滚动条的宽度 */
height: 18rem; /* 滚动条的高度 */
background-color: pink; /* 滚动条的颜色 */
}
.slider {
width: 100%;
background-color: palevioletred; /* 滑块的颜色 */
border-radius: 0.5rem; /* 滑块圆角 */
}
</style>
获取元素的 clientHeight 和 scrollHeight 来计算滑块的高度以及可滚动距离,通过scrollTop 获取滚动的距离通过 scroll 事件来监听内容的滚动,从而实现一个简单的手搓滚动条,下面开始封装。
封装前还要考虑到的问题:可否在同一页面多次复用;内容容器一般都是调接口数据进行遍历渲染,而v-for在渲染每个条目时是逐个插入到DOM中 的,这说明vue会先创建一个空的父元素,并将每个条目插入到该父元素中,这意味着 通过用ref绑定父页面的内容容器provide给子组件,子组件inject到dom元素的scrollHeight 这种方法不可行。
我想了一个办法,父页面把内容通过slot给子组件把接口数据父传子,在子组件可以拿到接口数据和内容dom的scrollHeight,然后用watch监听props的接口数据,若发生变化,重新获取scrollHeight即可。
父页面使用setTimeout模拟接口:
xml
// test.vue
<template>
<div class="scrollLayout">
<scroll :data="arrList">
<template v-for="i in arrList">
<div style="width: 10rem; text-align: center">num: {{ i.num }}</div>
</template>
</scroll>
</div>
</template>
<script setup>
import { ref } from 'vue';
import scroll from '../components/scroll.vue';
const arrList = ref([]);
setTimeout(() => {
for (let i = 1; i <= 30; i++) {
const obj = { num: i < 10 ? '0' + i : i };
arrList.value.push(obj);
}
}, 3000);
</script>
<style lang="scss" scoped>
.scrollLayout {
height: 10rem;
background: pink;
}
</style>
组件部分:
xml
// scroll.vue
<template>
<div class="scrollable">
<div class="content" ref="content" @scroll="scroll">
<slot></slot>
</div>
<div class="scrollBar" ref="scrollBar" :style="scrollBarStyle">
<div class="slider" :style="sliderStyle"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch, nextTick } from 'vue';
const props = defineProps({
scrollColor: {
type: String,
default: '',
},
sliderColor: {
type: String,
default: '#000',
},
data: {
type: Array,
default: [],
},
right: {
type: String,
default: '0',
},
});
const content = ref(null); // ref内容
const scrollBar = ref(null); // ref滚动条
const contentCH = ref(0); // content盒子高度
const contentSH = ref(0); // content内容高度
const scrollBarCH = ref(0); // 滚动条高度
const activeScrollDistance = ref(0); // 内容滚动的距离
const contentScrollDistance = ref(0); // 内容滚动的距离
const sliderHeight = ref(0); // 滑块高度
const position = ref(0); // 滚动条滑动距离
const scrollBarStyle = computed(() => `right:${props.right}px;background:${props.scrollColor};`);
const sliderStyle = computed(() => `height:${sliderHeight.value}%;margin-top:${position.value}px;background:${props.sliderColor};`);
onMounted(() => {
watch(
() => props.data,
() => {
// nextTick确保在DOM更新完毕后再执行
nextTick(() => {
const { clientHeight, scrollHeight } = content.value;
console.log('容器的高度:', clientHeight, '内容高度:', scrollHeight);
contentCH.value = clientHeight;
contentSH.value = scrollHeight;
scrollBarCH.value = scrollBar.value.clientHeight;
sliderHeight.value = (clientHeight / scrollHeight) * 100;
activeScrollDistance.value = scrollBarCH.value - scrollBarCH.value * (sliderHeight.value / 100);
contentScrollDistance.value = contentSH.value - contentCH.value;
});
},
{ immediate: true, deep: true }
);
});
// 监听滚动
const scroll = () => {
const { scrollTop } = content.value;
position.value = (scrollTop * activeScrollDistance.value) / contentScrollDistance.value;
};
</script>
<style lang="scss" scoped>
.scrollable {
position: relative;
display: flex;
height: 100%;
overflow: hidden;
}
.content {
height: 100%;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
}
.scrollBar {
position: absolute;
top: 0;
width: 5px;
height: 100%;
border-radius: 5px;
.slider {
width: 100%;
border-radius: 3px;
}
}
</style>
这样就可以解决初始获取的scrollHeight是内容插入前的高度------即容器高度。
(^▽^)