自定义指令 列表滚动

自己对列表的自动滚动理解,在项目的需求上用到了就记录一下

<template>
  <!-- 基本用法 -->
  <div v-auto-scroll class="scroll-box"></div>

  <!-- 自定义配置 -->
  <div v-auto-scroll="{ 
    speed: 300, 
    smoothReset: false 
  }" class="scroll-box"></div>
</template>

<style>
.scroll-box {
  height: 400px;
  overflow-y: auto;
  /* 必须指定高度 */
}
</style>


Vue.directive('autoscroll', {
  inserted(el, binding) {
    console.log('autoscroll',el, binding)
    const defaultOptions = {
      speed: 200,          // 滚动速度(px/s)
      smoothReset: true,   // 是否平滑重置
      pauseOnHover: true,  // 悬停暂停
      pauseOnClick: true,  // 点击暂停
      pauseOnScroll: true  // 滚动暂停
    }

    // 合并配置参数
    const options = {
      ...defaultOptions,
      ...(binding.value || {})
    }

    // 计算帧间隔时间
    const gap = 1000 / (options.speed / 2)
    let animationFrameId = null
    let isUsing = false
    const listeners = []

    // 事件处理器工厂
    const createHandler = (type, handler) => {
      const wrapper = (e) => {
        if (typeof handler === 'function') handler(e)
      }
      listeners.push({ type, handler: wrapper })
      return wrapper
    }

    // 平滑滚动到底部
    const smoothScrollToBottom = () => {
      const start = el.scrollTop
      const end = el.scrollHeight - el.clientHeight
      const duration = 500 // 毫秒

      const animate = (timestamp) => {
        const runtime = timestamp - startTime
        const progress = Math.min(runtime / duration, 1)
        el.scrollTop = start + (end - start) * progress
        
        if (runtime < duration) {
          animationFrameId = requestAnimationFrame(animate)
        }
      }
      
      const startTime = performance.now()
      animationFrameId = requestAnimationFrame(animate)
    }

    // 主滚动动画
    const scrollAnimation = () => {
      let lastTime = 0
      let accumulatedDiff = 0

      const animate = (timestamp) => {
        if (!lastTime) lastTime = timestamp
        
        const delta = timestamp - lastTime
        accumulatedDiff += delta
        lastTime = timestamp

        if (isUsing) {
          cancelAnimationFrame(animationFrameId)
          return
        }

        if (accumulatedDiff >= gap) {
          const remaining = el.scrollHeight - (el.scrollTop + el.clientHeight)
          
          if (remaining > 1) {
            el.scrollTop += 1
          } else if (options.smoothReset) {
            smoothScrollToBottom()
            return
          } else {
            el.scrollTop = 0
          }
          
          accumulatedDiff = 0
        }

        animationFrameId = requestAnimationFrame(animate)
      }

      animationFrameId = requestAnimationFrame(animate)
    }

    // 事件监听
    if (options.pauseOnHover) {
      el.addEventListener('mouseenter', createHandler('mouseenter', () => {
        isUsing = true
      }), { passive: true })
      
      el.addEventListener('mouseleave', createHandler('mouseleave', () => {
        isUsing = false
        scrollAnimation()
      }), { passive: true })
    }

    if (options.pauseOnScroll) {
      el.addEventListener('wheel', createHandler('wheel', () => {
        isUsing = true
      }), { passive: true })
    }

    if (options.pauseOnClick) {
      el.addEventListener('click', createHandler('click', () => {
        isUsing = true
      }), { passive: true })
    }

    // 初始化滚动
    scrollAnimation()

    // 响应式尺寸变化
    const resizeObserver = new ResizeObserver(() => {
      cancelAnimationFrame(animationFrameId)
      scrollAnimation()
    })
    resizeObserver.observe(el)

    // 保存引用用于卸载
    el._autoScroll = {
      resizeObserver,
      options
    }
  },

  unmounted(el) {
    // 清理所有资源
    cancelAnimationFrame(el._autoScroll?.animationFrameId)
    el._autoScroll?.resizeObserver.disconnect()
    
    // 移除事件监听
    el._autoScroll?.listeners.forEach(({ type, handler }) => {
      el.removeEventListener(type, handler)
    })
  }
})
posted @ 2025-05-21 09:19  網友攃  阅读(24)  评论(0)    收藏  举报