1. scrollContainer 组件代码
- template分为滚动区域与滚动条区域, 先隐藏浏览器滚动条,再做自定义滚动条与内容联动
- 主要思路:
- 滑块绑定mousedown事件, 用于给body绑定mouseMove事件与mouseup事件, move事件用于处理滑块移动, 以及滑块移动后, 内容区域的移动
- mouseup事件用于解除mousemove事件
- 内容区域绑定scroll事件, 用于内容滚动影响滑块位置
- 内容区域绑定滚轮事件, 用于滚轮事件触发滚动事件, 滚动事件影响滑块位置, 形成联动
<template>
<!-- 主体内容-->
<div
ref="scrollbarContent"
class="scrollbar-content"
:class="[scrollX ? 'scroll-x-center' : 'scroll-y-center']"
:style="{
height,
width,
}"
>
<!-- 滚动内容区域 -->
<div
class="scroll-wrap"
ref="scrollWrap"
:class="[isSelect ? 'cannotselect' : '']"
:style="{
[scrollX ? 'overflow-x' : 'overflow-y']: 'scroll',
}"
@scroll="handlerScroll"
@wheel="handlerWheel"
@DOMMouseScroll="handlerWheel"
>
<slot></slot>
</div>
<!-- 滚动条Y -->
<div
class="scrollbar-y"
ref="scrollbarY"
v-if="scrollY"
:style="{
height: '100%',
width: barWidth,
position: 'relative',
}"
@mousedown="handlerDown"
>
<!-- 滑块 -->
<div
class="sliding-block"
ref="slidingBlockY"
:style="{
width: barWidth,
height: sliderLength + 'px',
position: 'absolute',
top: slidingTop + 'px',
}"
></div>
</div>
<!-- 滚动条X -->
<div
class="scrollbar-x"
ref="scrollbarX"
v-if="scrollX"
:style="{
width: '100%',
height: barHeight,
position: 'relative',
}"
>
<!-- 滑块 -->
<div
class="sliding-block"
:style="{
height: barHeight,
position: 'absolute',
width: sliderLength + 'px',
left: slidingLeft + 'px',
}"
@mousedown="handlerDown"
></div>
</div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
height?: string;
scrollX?: boolean;
scrollY?: boolean;
width?: string;
barHeight?: string;
barWidth?: string;
sliderLength?: number;
}>(),
{
height: "100%",
scrollX: false,
scrollY: false,
width: "100%",
barHeight: "6px",
barWidth: "6px",
sliderLength: 100,
}
);
const scrollbarContent = ref<HTMLElement>();
const scrollWrap = ref<HTMLElement>();
const scrollbarY = ref<HTMLElement>();
const slidingBlockY = ref<HTMLElement>();
const scrollbarX = ref<HTMLElement>();
const slidingLeft = ref<number>(0);
const slidingTop = ref<number>(0);
const isSelect = ref<boolean>(false);
const handlerDown = (e:MouseEvent) => {
if (!props.scrollX) {
document.body?.addEventListener("mousemove", handleMoveY);
handleMoveY(e)
} else {
document.body?.addEventListener("mousemove", handleMoveX);
handleMoveX(e)
}
isSelect.value = true;
document.body.addEventListener("mouseup", handlerReomveMove);
};
const handleMoveY = (e: MouseEvent) => {
const scrollbarYTop = scrollbarY.value?.getBoundingClientRect().top ?? 0;
let top = e.clientY - scrollbarYTop - props.sliderLength / 2;
let maxMoveLength =
(scrollbarY.value?.offsetHeight as number) - props.sliderLength;
if (top < 0) {
top = 0;
} else if (top > maxMoveLength) {
top = maxMoveLength;
}
slidingTop.value = top;
if (scrollWrap.value) {
scrollWrap.value.scrollTop =
((scrollWrap.value.scrollHeight - scrollWrap.value.offsetHeight) /
maxMoveLength) *
top;
}
};
const handleMoveX = (e: MouseEvent) => {
if (!scrollbarX.value || !scrollWrap.value) {
return false;
}
let maxLength = scrollbarX.value?.offsetWidth - props.sliderLength;
const scrollbarXLeft = scrollbarX.value?.getBoundingClientRect().left ?? 0;
let left = e.clientX - scrollbarXLeft - props.sliderLength / 2;
if (left < 0) {
left = 0;
} else if (left > maxLength) {
left = maxLength;
}
slidingLeft.value = left;
scrollWrap.value.scrollLeft =
((scrollWrap.value.scrollWidth - scrollWrap.value.offsetWidth) /
maxLength) *
left;
};
const handlerReomveMove = () => {
if (!props.scrollX) {
document.body?.removeEventListener("mousemove", handleMoveY);
} else {
document.body?.removeEventListener("mousemove", handleMoveX);
}
isSelect.value = false;
};
const handlerScroll = () => {
if (!scrollWrap.value) {
return false;
}
if (!props.scrollX) {
let maxMoveLength =
(scrollbarY.value?.offsetHeight as number) - props.sliderLength;
let contentMoveLength = scrollWrap.value?.scrollTop;
slidingTop.value =
(contentMoveLength * maxMoveLength) /
(scrollWrap.value.scrollHeight - scrollWrap.value.offsetHeight);
} else {
let maxMoveLength =
(scrollbarX.value?.offsetWidth as number) - props.sliderLength;
let contentMoveLength = scrollWrap.value.scrollLeft;
slidingLeft.value =
(contentMoveLength * maxMoveLength) /
(scrollWrap.value.scrollWidth - scrollWrap.value.offsetWidth);
}
};
let state = false;
const handlerWheel = (e) => {
if (!scrollWrap.value) {
return false;
}
if (!state) {
state = true;
setTimeout(() => {
let step = 0;
if (e.type == "wheel") {
step = e.wheelDelta > 0 ? -10 : +10;
} else if (e.type == "DOMMouseScroll") {
step = e.detail > 0 ? +10 : -10;
}
if (!props.scrollX) {
let top = (scrollWrap.value?.scrollTop as number) + step;
let maxScrollTop =
(scrollWrap.value?.scrollHeight as number) -
(scrollWrap.value?.offsetHeight as number);
if (top < 0) {
top = 0;
} else if (top > maxScrollTop) {
top = maxScrollTop;
}
(scrollWrap.value as HTMLElement).scrollTop = top;
} else {
let left = (scrollWrap.value?.scrollLeft as number) + step * 20;
let maxScrollLeft =
(scrollWrap.value?.scrollWidth as number) -
(scrollWrap.value?.offsetWidth as number);
if (left < 0) {
left = 0;
} else if (left > maxScrollLeft) {
left = maxScrollLeft;
}
(scrollWrap.value as HTMLElement).scrollLeft = left;
console.log(
(scrollWrap.value as HTMLElement).scrollLeft,
"(scrollWrap.value as HTMLElement).scrollLeft"
);
}
state = false;
}, 100);
}
};
onUnmounted(() => {
document.body.removeEventListener("mouseup", handlerReomveMove);
});
</script>
<style scoped lang="scss">
.scrollbar-content {
display: flex;
height: 100%;
width: 100%;
.scrollbar-y,
.scrollbar-x {
flex-shrink: 0;
}
.sliding-block {
background-color: #ccc;
border-radius: 10px;
}
.scroll-wrap {
position: relative;
height: 100%;
width: 100%;
flex: 1;
scrollbar-width: none;
-ms-overflow-style: none;
}
.scroll-wrap::-webkit-scrollbar {
width: 0px;
height: 0px;
}
.cannotselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
.scroll-x-center {
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.scroll-y-center {
justify-content: space-between;
align-items: center;
}
</style>
2.基础使用
<scrollContainer scroll-x width="400px">
<ul style="display:flex;flex-wrap: nowrap;list-style: none;">
<li v-for="item in 100" :key="item" style="flex-shrink: 0;">{{ item }} 信息列表</li>
</ul>
</scrollContainer>
<scrollContainer scroll-y height="400px">
<ul >
<li v-for="item in 100" :key="item">{{ item }} 信息列表</li>
</ul>
</scrollContainer>
3.属性
height?: string;
scrollY?: boolean;
scrollX?: boolean;
width?: string;
barHeight?: string;
barWidth?: string;
sliderLength?: number;