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 lang="ts">
export default {
inheritAttrs: false
}
</script>
<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 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.size
return 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.scrollTop
containerScrollLeft.value = containerRef.value.scrollLeft
}
function updateScrollbarState() {
containerScrollHeight.value = containerRef.value.scrollHeight
containerScrollWidth.value = containerRef.value.scrollWidth
containerClientHeight.value = containerRef.value.clientHeight
containerClientWidth.value = containerRef.value.clientWidth
containerHeight.value = containerRef.value.offsetHeight
containerWidth.value = containerRef.value.offsetWidth
contentHeight.value = contentRef.value.offsetHeight
contentWidth.value = contentRef.value.offsetWidth
railHeight.value = railVerticalRef.value.offsetHeight
railWidth.value = railHorizontalRef.value.offsetWidth
}
function updateState() {
updateScrollState()
updateScrollbarState()
}
useEventListener(window, 'resize', updateState)
const options = { childList: true, attributes: true, subtree: true }
useMutationObserver(containerRef, 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 = true
memoYTop.value = containerScrollTop.value
memoMouseY.value = e.clientY
window.onmousemove = (e: MouseEvent) => {
const diffY = e.clientY - memoMouseY.value
const dScrollTop =
(diffY * (contentHeight.value - containerHeight.value)) / (containerHeight.value - trackHeight.value)
const toScrollTopUpperBound = contentHeight.value - containerHeight.value
let toScrollTop = memoYTop.value + dScrollTop
toScrollTop = Math.min(toScrollTopUpperBound, toScrollTop)
toScrollTop = Math.max(toScrollTop, 0)
containerRef.value.scrollTop = toScrollTop
}
window.onmouseup = () => {
window.onmousemove = null
trackYPressed.value = false
if (props.trigger === 'hover' && mouseLeave.value) {
showTrack.value = false
mouseLeave.value = false
}
}
}
function onTrackHorizontalMouseDown(e: MouseEvent) {
trackXPressed.value = true
memoXLeft.value = containerScrollLeft.value
memoMouseX.value = e.clientX
window.onmousemove = (e: MouseEvent) => {
const diffX = e.clientX - memoMouseX.value
const dScrollLeft =
(diffX * (contentWidth.value - containerWidth.value)) / (containerWidth.value - trackWidth.value)
const toScrollLeftUpperBound = contentWidth.value - containerWidth.value
let toScrollLeft = memoXLeft.value + dScrollLeft
toScrollLeft = Math.min(toScrollLeftUpperBound, toScrollLeft)
toScrollLeft = Math.max(toScrollLeft, 0)
containerRef.value.scrollLeft = toScrollLeft
}
window.onmouseup = () => {
window.onmousemove = null
trackXPressed.value = false
if (props.trigger === 'hover' && mouseLeave.value) {
showTrack.value = false
mouseLeave.value = false
}
}
}
function scrollTo(...args: any[]) {
containerRef.value?.scrollTo(...args)
}
function scrollBy(...args: any[]) {
containerRef.value?.scrollBy(...args)
}
defineExpose({
scrollTo,
scrollBy
})
</script>
<template>
<div
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" v-bind="$attrs">
<div
ref="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">
<div
v-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">
<div
v-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>
<Scrollbar
style="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>