APIs
Scrollbar
参数 | 说明 | 类型 | 默认值 | 必传 |
---|
contentStyle | 内容样式 | CSSProperties | {} | false |
size | 滚动条的大小,单位 px | number | 5 | false |
trigger | 显示滚动条的时机,'none' 表示一直显示 | ‘hover’ | ‘none’ | ‘hover’ | false |
horizontal | 是否使用横向滚动 | boolean | false | false |
Methods
名称 | 说明 | 类型 |
---|
scrollTo | 滚动内容 | (options: { left?: number, top?: number, behavior?: ScrollBehavior }): void & (x: number, y: number) => void |
scrollBy | 滚动特定距离 | (options: { left?: number, top?: number, behavior?: ScrollBehavior }): void & (x: number, y: number) => void |
ScrollBehavior Type
名称 | 说明 |
---|
smooth | 平滑滚动并产生过渡效果 |
instant | 滚动会直接跳转到目标位置,没有过渡效果 |
auto | 或缺省值表示浏览器会自动选择滚动时的过渡效果 |
Events
名称 | 说明 | 类型 |
---|
scroll | 滚动的回调 | (e: Event) => void |
创建滚动条组件Scrollbar.vue
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { CSSProperties } from 'vue'
import { useEventListener, useMutationObserver } from '../utils'
interface Props {contentStyle?: CSSProperties size?: number trigger?: 'hover' | 'none' horizontal?: boolean
}
const props = withDefaults(defineProps<Props>(), {contentStyle: () => ({}),size: 5,trigger: 'hover',horizontal: false
})
const scrollbarRef = ref()
const containerRef = ref()
const contentRef = ref()
const railVerticalRef = ref()
const railHorizontalRef = ref()
const showTrack = ref(false)
const containerScrollHeight = ref(0)
const containerScrollWidth = ref(0)
const containerClientHeight = ref(0)
const containerClientWidth = ref(0)
const containerHeight = ref(0)
const containerWidth = ref(0)
const contentHeight = ref(0)
const contentWidth = ref(0)
const railHeight = ref(0)
const railWidth = ref(0)
const containerScrollTop = ref(0)
const containerScrollLeft = ref(0)
const trackYPressed = ref(false)
const trackXPressed = ref(false)
const mouseLeave = ref(false)
const memoYTop = ref<number>(0)
const memoXLeft = ref<number>(0)
const memoMouseY = ref<number>(0)
const memoMouseX = ref<number>(0)
const horizontalContentStyle = { width: 'fit-content' }
const emit = defineEmits(['scroll'])
const isYScroll = computed(() => {return containerScrollHeight.value > containerClientHeight.value
})
const isXScroll = computed(() => {return containerScrollWidth.value > containerClientWidth.value
})
const isScroll = computed(() => {return isYScroll.value || (props.horizontal && isXScroll.value)
})
const trackHeight = computed(() => {if (isYScroll.value) {if (containerHeight.value && contentHeight.value && railHeight.value) {const value = Math.min(containerHeight.value,(railHeight.value * containerHeight.value) / contentHeight.value + 1.5 * props.size)return Number(value.toFixed(4))}}return 0
})
const trackTop = computed(() => {if (containerHeight.value && contentHeight.value && railHeight.value) {return ((containerScrollTop.value / (contentHeight.value - containerHeight.value)) *(railHeight.value - trackHeight.value))}return 0
})
const trackWidth = computed(() => {if (props.horizontal && isXScroll.value) {if (containerWidth.value && contentWidth.value && railWidth.value) {const value = (railWidth.value * containerWidth.value) / contentWidth.value + 1.5 * props.sizereturn Number(value.toFixed(4))}}return 0
})
const trackLeft = computed(() => {if (containerWidth.value && contentWidth.value && railWidth.value) {return ((containerScrollLeft.value / (contentWidth.value - containerWidth.value)) * (railWidth.value - trackWidth.value))}return 0
})
onMounted(() => {updateState()
})
function updateScrollState() {containerScrollTop.value = containerRef.value.scrollTopcontainerScrollLeft.value = containerRef.value.scrollLeft
}
function updateScrollbarState() {containerScrollHeight.value = containerRef.value.scrollHeightcontainerScrollWidth.value = containerRef.value.scrollWidthcontainerClientHeight.value = containerRef.value.clientHeightcontainerClientWidth.value = containerRef.value.clientWidthcontainerHeight.value = containerRef.value.offsetHeightcontainerWidth.value = containerRef.value.offsetWidthcontentHeight.value = contentRef.value.offsetHeightcontentWidth.value = contentRef.value.offsetWidthrailHeight.value = railVerticalRef.value.offsetHeightrailWidth.value = railHorizontalRef.value.offsetWidth
}
function updateState() {updateScrollState()updateScrollbarState()
}
useEventListener(window, 'resize', updateState)
const options = { childList: true, attributes: true, subtree: true }
useMutationObserver(scrollbarRef, updateState, options)
function onScroll(e: Event) {emit('scroll', e)updateScrollState()
}
function onMouseEnter() {if (props.horizontal) {if (trackXPressed.value) {mouseLeave.value = false} else {showTrack.value = true}} else {if (trackYPressed.value) {mouseLeave.value = false} else {showTrack.value = true}}
}
function onMouseLeave() {if (props.horizontal) {if (trackXPressed.value) {mouseLeave.value = true} else {showTrack.value = false}} else {if (trackYPressed.value) {mouseLeave.value = true} else {showTrack.value = false}}
}
function onTrackVerticalMouseDown(e: MouseEvent) {trackYPressed.value = truememoYTop.value = containerScrollTop.valuememoMouseY.value = e.clientYwindow.onmousemove = (e: MouseEvent) => {const diffY = e.clientY - memoMouseY.valueconst dScrollTop =(diffY * (contentHeight.value - containerHeight.value)) / (containerHeight.value - trackHeight.value)const toScrollTopUpperBound = contentHeight.value - containerHeight.valuelet toScrollTop = memoYTop.value + dScrollToptoScrollTop = Math.min(toScrollTopUpperBound, toScrollTop)toScrollTop = Math.max(toScrollTop, 0)containerRef.value.scrollTop = toScrollTop}window.onmouseup = () => {window.onmousemove = nulltrackYPressed.value = falseif (props.trigger === 'hover' && mouseLeave.value) {showTrack.value = falsemouseLeave.value = false}}
}
function onTrackHorizontalMouseDown(e: MouseEvent) {trackXPressed.value = truememoXLeft.value = containerScrollLeft.valuememoMouseX.value = e.clientXwindow.onmousemove = (e: MouseEvent) => {const diffX = e.clientX - memoMouseX.valueconst dScrollLeft =(diffX * (contentWidth.value - containerWidth.value)) / (containerWidth.value - trackWidth.value)const toScrollLeftUpperBound = contentWidth.value - containerWidth.valuelet toScrollLeft = memoXLeft.value + dScrollLefttoScrollLeft = Math.min(toScrollLeftUpperBound, toScrollLeft)toScrollLeft = Math.max(toScrollLeft, 0)containerRef.value.scrollLeft = toScrollLeft}window.onmouseup = () => {window.onmousemove = nulltrackXPressed.value = falseif (props.trigger === 'hover' && mouseLeave.value) {showTrack.value = falsemouseLeave.value = false}}
}
function scrollTo(...args: any[]) {containerRef.value?.scrollTo(...args)
}
function scrollBy(...args: any[]) {containerRef.value?.scrollBy(...args)
}defineExpose({scrollTo,scrollBy
})
</script>
<template><divref="scrollbarRef"class="m-scrollbar":style="`--scrollbar-size: ${size}px;`"@mouseenter="isScroll && trigger === 'hover' ? onMouseEnter() : () => false"@mouseleave="isScroll && trigger === 'hover' ? onMouseLeave() : () => false"><div ref="containerRef" class="m-scrollbar-container" @scroll="onScroll"><divref="contentRef"class="m-scrollbar-content":style="[horizontal ? { ...horizontalContentStyle, ...contentStyle } : contentStyle]"><slot></slot></div></div><div ref="railVerticalRef" class="m-scrollbar-rail rail-vertical"><Transition name="fade"><divv-if="trigger === 'none' || showTrack"class="m-scrollbar-track":style="`top: ${trackTop}px; height: ${trackHeight}px;`"@mousedown.prevent.stop="onTrackVerticalMouseDown"></div></Transition></div><div ref="railHorizontalRef" v-show="horizontal" class="m-scrollbar-rail rail-horizontal"><Transition name="fade"><divv-if="trigger === 'none' || showTrack"class="m-scrollbar-track":style="`left: ${trackLeft}px; width: ${trackWidth}px;`"@mousedown.prevent.stop="onTrackHorizontalMouseDown"></div></Transition></div></div>
</template>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.fade-enter-from,
.fade-leave-to {opacity: 0;
}
.m-scrollbar {overflow: hidden;position: relative;z-index: auto;height: 100%;width: 100%;.m-scrollbar-container {width: 100%;overflow: scroll;height: 100%;min-height: inherit;max-height: inherit;scrollbar-width: none;&::-webkit-scrollbar,&::-webkit-scrollbar-track-piece,&::-webkit-scrollbar-thumb {width: 0;height: 0;display: none;}.m-scrollbar-content {box-sizing: border-box;min-width: 100%;}}.m-scrollbar-rail {position: absolute;pointer-events: none;user-select: none;background: transparent;-webkit-user-select: none;.m-scrollbar-track {z-index: 1;position: absolute;cursor: pointer;pointer-events: all;background-color: rgba(0, 0, 0, 0.25);transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);&:hover {background-color: rgba(0, 0, 0, 0.4);}}}.rail-vertical {inset: 2px 4px 2px auto;width: var(--scrollbar-size);.m-scrollbar-track {width: var(--scrollbar-size);border-radius: var(--scrollbar-size);bottom: 0;}}.rail-horizontal {inset: auto 2px 4px 2px;height: var(--scrollbar-size);.m-scrollbar-track {height: var(--scrollbar-size);border-radius: var(--scrollbar-size);right: 0;}}
}
</style>
在要使用的页面引入
<script setup lang="ts">
import Scrollbar from './Scrollbar.vue'
function onScroll(e: Event) {console.log('scroll:', e)
}
</script>
<template><div><h1>{{ $route.name }} {{ $route.meta.title }}</h1><h2 class="mt30 mb10">基本使用</h2><Scrollbar style="max-height: 120px" @scroll="onScroll">我们在田野上面找猪<br />想象中已找到了三只<br />小鸟在白云上面追逐<br />它们在树底下跳舞<br />啦啦啦啦啦啦啦啦咧<br />啦啦啦啦咧<br />我们在想象中度过了许多年<br />想象中我们是如此的疯狂<br />我们在城市里面找猪<br />想象中已找到了几百万只<br />小鸟在公园里面唱歌<br />它们独自在想象里跳舞<br />啦啦啦啦啦啦啦啦咧<br />啦啦啦啦咧<br />我们在想象中度过了许多年<br />许多年之后我们又开始想象<br />啦啦啦啦啦啦啦啦咧</Scrollbar><h2 class="mt30 mb10">横向滚动</h2><Scrollbar horizontal><div style="white-space: nowrap; padding: 12px">我们在田野上面找猪 想象中已找到了三只 小鸟在白云上面追逐 它们在树底下跳舞 啦啦啦啦啦啦啦啦咧 啦啦啦啦咧我们在想象中度过了许多年 想象中我们是如此的疯狂 我们在城市里面找猪 想象中已找到了几百万只 小鸟在公园里面唱歌它们独自在想象里跳舞 啦啦啦啦啦啦啦啦咧 啦啦啦啦咧 我们在想象中度过了许多年 许多年之后我们又开始想象啦啦啦啦啦啦啦啦咧</div></Scrollbar><h2 class="mt30 mb10">触发方式</h2><Scrollbar horizontal style="max-height: 120px" trigger="none">我们在田野上面找猪<br />想象中已找到了三只<br />小鸟在白云上面追逐<br />它们在树底下跳舞<br />啦啦啦啦啦啦啦啦咧<br />啦啦啦啦咧<br />我们在想象中度过了许多年<br />想象中我们是如此的疯狂<br />我们在城市里面找猪<br />想象中已找到了几百万只<br />小鸟在公园里面唱歌<br />它们独自在想象里跳舞<br />啦啦啦啦啦啦啦啦咧<br />啦啦啦啦咧<br />我们在想象中度过了许多年<br />许多年之后我们又开始想象<br />啦啦啦啦啦啦啦啦咧</Scrollbar><h2 class="mt30 mb10">自定义内容样式</h2><Scrollbarstyle="max-height: 120px; border-radius: 12px":content-style="{ backgroundColor: '#e6f4ff', padding: '16px 24px', fontSize: '16px' }">我们在田野上面找猪<br />想象中已找到了三只<br />小鸟在白云上面追逐<br />它们在树底下跳舞<br />啦啦啦啦啦啦啦啦咧<br />啦啦啦啦咧<br />我们在想象中度过了许多年<br />想象中我们是如此的疯狂<br />我们在城市里面找猪<br />想象中已找到了几百万只<br />小鸟在公园里面唱歌<br />它们独自在想象里跳舞<br />啦啦啦啦啦啦啦啦咧<br />啦啦啦啦咧<br />我们在想象中度过了许多年<br />许多年之后我们又开始想象<br />啦啦啦啦啦啦啦啦咧</Scrollbar></div>
</template>