1. scrollContainer 组件代码
template分为滚动区域与滚动条区域, 先隐藏浏览器滚动条,再做自定义滚动条与内容联动主要思路: 滑块绑定mousedown事件, 用于给body绑定mouseMove事件与mouseup事件, move事件用于处理滑块移动, 以及滑块移动后, 内容区域的移动mouseup事件用于解除mousemove事件内容区域绑定scroll事件, 用于内容滚动影响滑块位置内容区域绑定滚轮事件, 用于滚轮事件触发滚动事件, 滚动事件影响滑块位置, 形成联动
<template><!-- 主体内容--><divref="scrollbarContent"class="scrollbar-content":class="[scrollX ? 'scroll-x-center' : 'scroll-y-center']":style="{height,width,}"><!-- 滚动内容区域 --><divclass="scroll-wrap"ref="scrollWrap":class="[isSelect ? 'cannotselect' : '']":style="{[scrollX ? 'overflow-x' : 'overflow-y']: 'scroll',}"@scroll="handlerScroll"@wheel="handlerWheel"@DOMMouseScroll="handlerWheel"><slot></slot></div><!-- 滚动条Y --><divclass="scrollbar-y"ref="scrollbarY"v-if="scrollY":style="{height: '100%',width: barWidth,position: 'relative',}"@mousedown="handlerDown"><!-- 滑块 --><divclass="sliding-block"ref="slidingBlockY":style="{width: barWidth,height: sliderLength + 'px',position: 'absolute',top: slidingTop + 'px',}"></div></div><!-- 滚动条X --><divclass="scrollbar-x"ref="scrollbarX"v-if="scrollX":style="{width: '100%',height: barHeight,position: 'relative',}"><!-- 滑块 --><divclass="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; //滚动方向x轴scrollY?: boolean; //滚动方向y轴width?: string;barHeight?: string; //滚动条高度, X轴传barWidth?: string; //滚轮动条宽度,Y轴传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); //xconst slidingTop = ref<number>(0); //yconst 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);};// 鼠标移动事件Yconst 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;}};// 鼠标移动事件Xconst 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;};// scroll事件,用来反过来控制滚动条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 {// 此次滚动设置距离, Y方向步长为10, x方向为其20倍即200let 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;/* / 兼容IE10+ */-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.基础使用
//X方向滚动, 需要设置scroll-x和width 以及ul的样式, ul需要设置flex<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>//y方向滚动, 需要设置scroll-y和height<scrollContainer scroll-y height="400px"><ul ><li v-for="item in 100" :key="item">{{item }} 信息列表</li></ul></scrollContainer>
3.属性
//传给组件的propsheight?: string; //y方向滚动必传,指定滚动区域高度100%, 100px scrollY?: boolean; //滚动方向y轴,与X轴方向指定二选一必传scrollX?: boolean; //滚动方向x轴width?: string; //x方向滚动必传,指定滚动区域宽度, 100%, 100px barHeight?: string; //滚动条高度, 滚动条粗细, X轴传, 为0时,滚动条消失barWidth?: string; //滚轮动条宽度, 滚动条粗细, Y轴传, 为0时,滚动条消失sliderLength?: number; // 即滑块长度, 默认为100px, 自带单位
如果觉得《v3自定义滚动条组件》对你有帮助,请点赞、收藏,并留下你的观点哦!