useAutoScroll.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import { ref, watch, onUnmounted, unref, type Ref, onMounted } from 'vue';
  2. import { useScroll, type UseScrollReturn } from '@vueuse/core';
  3. import gsap from 'gsap';
  4. const ticker = gsap.ticker;
  5. export interface AutoScrollOptions {
  6. /** 延迟(刻),滚动启动、反转、用户交互后的重新开始的延迟 */
  7. delay?: number;
  8. /** 滚动到底部后是否回滚(否则是回到顶部重新滚动) */
  9. rollBack?: boolean;
  10. /** 每一刻滚动的像素数 */
  11. step?: number;
  12. /** 是否自动开始 */
  13. autoStart?: boolean;
  14. /** 滚动方向 */
  15. direction?: 'x' | 'y';
  16. }
  17. export interface AutoScrollReturn {
  18. /** 开始/恢复滚动 */
  19. start: () => void;
  20. /** 暂停滚动 */
  21. pause: () => void;
  22. /** 重置到初始状态 */
  23. reset: () => void;
  24. /** 反转滚动 */
  25. reverse: () => void;
  26. /** 恢复滚动 */
  27. resume: () => void;
  28. }
  29. export function useAutoScroll(container: Ref<HTMLElement | null> | HTMLElement | null, options: AutoScrollOptions = {}): AutoScrollReturn {
  30. const {
  31. delay = 300, // 默认60帧(约1秒)
  32. rollBack = false,
  33. step = 1,
  34. autoStart = true,
  35. direction = 'y',
  36. } = options;
  37. let cleanupListeners: (() => void) | null = null;
  38. // 状态管理
  39. const isActive = ref(false);
  40. const currentDirection = ref(1); // 1: 正向, -1: 反向
  41. const delayFrames = ref(0);
  42. // 使用 VueUse 的 useScroll
  43. const { arrivedState, x, y } = useScroll(container, {
  44. behavior: 'smooth',
  45. }) as UseScrollReturn;
  46. // 检查是否到达边界
  47. const checkBoundary = (): boolean => {
  48. if (direction === 'y') {
  49. return currentDirection.value > 0 ? arrivedState.bottom : arrivedState.top;
  50. } else {
  51. return currentDirection.value > 0 ? arrivedState.right : arrivedState.left;
  52. }
  53. };
  54. // 执行滚动
  55. const performScroll = () => {
  56. if (!isActive.value) return;
  57. if (delayFrames.value > 0) {
  58. delayFrames.value--;
  59. return;
  60. }
  61. // 检查边界
  62. if (checkBoundary()) {
  63. if (rollBack) {
  64. // 回滚模式:反转方向
  65. reverse();
  66. } else {
  67. // 循环模式:回到开始位置
  68. reset();
  69. }
  70. return;
  71. }
  72. // 执行滚动
  73. if (direction === 'y') {
  74. y.value += step * currentDirection.value;
  75. } else {
  76. x.value += step * currentDirection.value;
  77. }
  78. };
  79. // 开始/恢复滚动
  80. const start = () => {
  81. if (!isActive.value) {
  82. ticker.remove(performScroll);
  83. ticker.add(performScroll);
  84. isActive.value = true;
  85. delayFrames.value = delay;
  86. }
  87. };
  88. // 暂停滚动
  89. const pause = () => {
  90. delayFrames.value = Number.MAX_SAFE_INTEGER;
  91. };
  92. // 重置到初始状态
  93. const reset = () => {
  94. if (direction === 'y') {
  95. y.value = 0;
  96. } else {
  97. x.value = 0;
  98. }
  99. currentDirection.value = 1;
  100. delayFrames.value = delay;
  101. };
  102. // 反转滚动方向
  103. const reverse = () => {
  104. currentDirection.value *= -1;
  105. delayFrames.value = delay;
  106. };
  107. const resume = () => {
  108. delayFrames.value = delay;
  109. };
  110. // 监听用户交互事件
  111. const setupUserInteractionListeners = (el: HTMLElement) => {
  112. // 鼠标滚轮事件
  113. const handleWheel = () => {
  114. resume();
  115. };
  116. // 鼠标按下事件(用于拖动滚动条)
  117. const handleMouseDown = () => {
  118. pause();
  119. };
  120. // 鼠标抬起事件
  121. const handleMouseUp = () => {
  122. resume();
  123. };
  124. // 触摸事件
  125. const handleTouchStart = () => {
  126. pause();
  127. };
  128. const handleTouchEnd = () => {
  129. resume();
  130. };
  131. // 键盘事件(PageUp/PageDown/方向键)
  132. const handleKeyDown = (e: KeyboardEvent) => {
  133. const scrollKeys = ['PageUp', 'PageDown', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'];
  134. if (scrollKeys.includes(e.key)) {
  135. pause();
  136. }
  137. };
  138. const handleKeyUp = () => {
  139. resume();
  140. };
  141. // 添加事件监听
  142. el.addEventListener('wheel', handleWheel, { passive: true });
  143. el.addEventListener('mousedown', handleMouseDown);
  144. el.addEventListener('mouseup', handleMouseUp);
  145. el.addEventListener('touchstart', handleTouchStart, { passive: true });
  146. el.addEventListener('touchend', handleTouchEnd);
  147. el.addEventListener('keydown', handleKeyDown);
  148. el.addEventListener('keyup', handleKeyUp);
  149. // 返回清理函数
  150. return () => {
  151. el.removeEventListener('wheel', handleWheel);
  152. el.removeEventListener('mousedown', handleMouseDown);
  153. el.removeEventListener('mouseup', handleMouseUp);
  154. el.removeEventListener('touchstart', handleTouchStart);
  155. el.removeEventListener('touchend', handleTouchEnd);
  156. el.removeEventListener('keydown', handleKeyDown);
  157. el.removeEventListener('keyup', handleKeyUp);
  158. };
  159. };
  160. // 监听容器变化
  161. watch(
  162. () => unref(container),
  163. (newContainer) => {
  164. if (cleanupListeners) {
  165. cleanupListeners();
  166. cleanupListeners = null;
  167. }
  168. if (newContainer) {
  169. cleanupListeners = setupUserInteractionListeners(newContainer);
  170. // 容器变化时重置状态
  171. reset();
  172. }
  173. }
  174. );
  175. onMounted(() => {
  176. // 自动开始
  177. if (autoStart) {
  178. start();
  179. }
  180. });
  181. // 清理
  182. onUnmounted(() => {
  183. ticker.remove(performScroll);
  184. });
  185. return {
  186. start,
  187. pause,
  188. reset,
  189. reverse,
  190. resume,
  191. };
  192. }