import { ref, watch, onUnmounted, unref, type Ref, onMounted } from 'vue'; import { useScroll, type UseScrollReturn } from '@vueuse/core'; import gsap from 'gsap'; const ticker = gsap.ticker; export interface AutoScrollOptions { /** 延迟(刻),滚动启动、反转、用户交互后的重新开始的延迟 */ delay?: number; /** 滚动到底部后是否回滚(否则是回到顶部重新滚动) */ rollBack?: boolean; /** 每一刻滚动的像素数 */ step?: number; /** 是否自动开始 */ autoStart?: boolean; /** 滚动方向 */ direction?: 'x' | 'y'; } export interface AutoScrollReturn { /** 开始/恢复滚动 */ start: () => void; /** 暂停滚动 */ pause: () => void; /** 重置到初始状态 */ reset: () => void; /** 反转滚动 */ reverse: () => void; /** 恢复滚动 */ resume: () => void; } export function useAutoScroll(container: Ref | HTMLElement | null, options: AutoScrollOptions = {}): AutoScrollReturn { const { delay = 300, // 默认60帧(约1秒) rollBack = false, step = 1, autoStart = true, direction = 'y', } = options; let cleanupListeners: (() => void) | null = null; // 状态管理 const isActive = ref(false); const currentDirection = ref(1); // 1: 正向, -1: 反向 const delayFrames = ref(0); // 使用 VueUse 的 useScroll const { arrivedState, x, y } = useScroll(container, { behavior: 'smooth', }) as UseScrollReturn; // 检查是否到达边界 const checkBoundary = (): boolean => { if (direction === 'y') { return currentDirection.value > 0 ? arrivedState.bottom : arrivedState.top; } else { return currentDirection.value > 0 ? arrivedState.right : arrivedState.left; } }; // 执行滚动 const performScroll = () => { if (!isActive.value) return; if (delayFrames.value > 0) { delayFrames.value--; return; } // 检查边界 if (checkBoundary()) { if (rollBack) { // 回滚模式:反转方向 reverse(); } else { // 循环模式:回到开始位置 reset(); } return; } // 执行滚动 if (direction === 'y') { y.value += step * currentDirection.value; } else { x.value += step * currentDirection.value; } }; // 开始/恢复滚动 const start = () => { if (!isActive.value) { ticker.remove(performScroll); ticker.add(performScroll); isActive.value = true; delayFrames.value = delay; } }; // 暂停滚动 const pause = () => { delayFrames.value = Number.MAX_SAFE_INTEGER; }; // 重置到初始状态 const reset = () => { if (direction === 'y') { y.value = 0; } else { x.value = 0; } currentDirection.value = 1; delayFrames.value = delay; }; // 反转滚动方向 const reverse = () => { currentDirection.value *= -1; delayFrames.value = delay; }; const resume = () => { delayFrames.value = delay; }; // 监听用户交互事件 const setupUserInteractionListeners = (el: HTMLElement) => { // 鼠标滚轮事件 const handleWheel = () => { resume(); }; // 鼠标按下事件(用于拖动滚动条) const handleMouseDown = () => { pause(); }; // 鼠标抬起事件 const handleMouseUp = () => { resume(); }; // 触摸事件 const handleTouchStart = () => { pause(); }; const handleTouchEnd = () => { resume(); }; // 键盘事件(PageUp/PageDown/方向键) const handleKeyDown = (e: KeyboardEvent) => { const scrollKeys = ['PageUp', 'PageDown', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space']; if (scrollKeys.includes(e.key)) { pause(); } }; const handleKeyUp = () => { resume(); }; // 添加事件监听 el.addEventListener('wheel', handleWheel, { passive: true }); el.addEventListener('mousedown', handleMouseDown); el.addEventListener('mouseup', handleMouseUp); el.addEventListener('touchstart', handleTouchStart, { passive: true }); el.addEventListener('touchend', handleTouchEnd); el.addEventListener('keydown', handleKeyDown); el.addEventListener('keyup', handleKeyUp); // 返回清理函数 return () => { el.removeEventListener('wheel', handleWheel); el.removeEventListener('mousedown', handleMouseDown); el.removeEventListener('mouseup', handleMouseUp); el.removeEventListener('touchstart', handleTouchStart); el.removeEventListener('touchend', handleTouchEnd); el.removeEventListener('keydown', handleKeyDown); el.removeEventListener('keyup', handleKeyUp); }; }; // 监听容器变化 watch( () => unref(container), (newContainer) => { if (cleanupListeners) { cleanupListeners(); cleanupListeners = null; } if (newContainer) { cleanupListeners = setupUserInteractionListeners(newContainer); // 容器变化时重置状态 reset(); } } ); onMounted(() => { // 自动开始 if (autoStart) { start(); } }); // 清理 onUnmounted(() => { ticker.remove(performScroll); }); return { start, pause, reset, reverse, resume, }; }