Files
workspace/code/link56/link56-frontend/src/hooks/useScrollWatcher.ts
T
2026-04-24 17:28:11 +08:00

107 lines
2.6 KiB
TypeScript

import type { Ref } from 'vue'
import { onUnmounted, watch } from 'vue'
interface UseScrollWatcherOptions {
target: Ref<HTMLElement | null>
thresholdPercent?: number
delay?: number
onReachTop?: () => void
onReachBottom?: () => void
}
export function useScrollWatcher({
target,
thresholdPercent = 0.05,
delay = 0,
onReachTop,
onReachBottom,
}: UseScrollWatcherOptions) {
if (!target)
throw new Error('target 必须传入一个 ref')
let lastScrollTop = 0
let timeout: ReturnType<typeof setTimeout> | null = null
const topTriggered = { value: false }
const bottomTriggered = { value: false }
const handleScroll = (e: Event) => {
const el = e.target as HTMLElement
const scrollTop = el.scrollTop
const scrollHeight = el.scrollHeight
const clientHeight = el.clientHeight
const scrollRange = scrollHeight - clientHeight
if (scrollRange <= 0)
return
const isScrollingUp = scrollTop < lastScrollTop
const isScrollingDown = scrollTop > lastScrollTop
lastScrollTop = scrollTop
const topThreshold = scrollRange * thresholdPercent
const bottomThreshold = scrollRange * thresholdPercent
if (timeout)
clearTimeout(timeout)
timeout = setTimeout(() => {
if (isScrollingUp && scrollTop <= topThreshold && !topTriggered.value) {
onReachTop?.()
topTriggered.value = true
}
else if (scrollTop > topThreshold) {
topTriggered.value = false
}
if (isScrollingDown && scrollHeight - scrollTop - clientHeight <= bottomThreshold && !bottomTriggered.value) {
onReachBottom?.()
bottomTriggered.value = true
}
else if (scrollHeight - scrollTop - clientHeight > bottomThreshold) {
bottomTriggered.value = false
}
}, delay)
}
const bindScroll = (el: HTMLElement | null) => {
if (!el)
return
el.addEventListener('scroll', handleScroll)
lastScrollTop = el.scrollTop
}
const unbindScroll = (el: HTMLElement | null) => {
if (!el)
return
el.removeEventListener('scroll', handleScroll)
}
// 监听 ref
watch(target, (el, oldEl) => {
if (oldEl)
unbindScroll(oldEl)
if (el)
bindScroll(el)
})
onUnmounted(() => {
if (target.value)
unbindScroll(target.value)
if (timeout)
clearTimeout(timeout)
})
const scrollToTop = () => {
if (target.value)
target.value.scrollTop = 0
}
const scrollToBottom = () => {
if (target.value) {
const el = target.value
el.scrollTop = el.scrollHeight - el.clientHeight
}
}
return { scrollToTop, scrollToBottom }
}