|
|
@@ -0,0 +1,220 @@
|
|
|
+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> | 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,
|
|
|
+ };
|
|
|
+}
|