Vue3 + TypeScript 实现 PC 端鼠标横向拖动滚动

发布于:2025-05-07 ⋅ 阅读:(16) ⋅ 点赞:(0)

功能说明

​​拖动功能​​:

  • 鼠标按下时记录初始位置和滚动位置
  • 拖动过程中计算移动距离并更新滚动位置
  • 松开鼠标后根据速度实现惯性滚动

​​滚动控制​​:

  • 支持鼠标滚轮横向滚动(通过 wheel 事件)
  • 自动边界检测防止滚动超出内容范围

实现代码

<template>
  <div 
    ref="scrollContainer" 
    class="horizontal-scroll-container"
    @mousedown="startDrag"
    @mousemove="onDrag"
    @mouseup="stopDrag"
    @mouseleave="stopDrag"
  >
    <div class="scroll-content">
      <div v-for="(item, index) in items" :key="index" class="item">
        {{ item }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

// 定义滚动容器引用
const scrollContainer = ref<HTMLElement | null>(null)

// 定义滚动内容数据
const items = ref<string[]>(Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`))

// 拖动状态变量
let isDragging = false
let startX = 0
let scrollLeft = 0
let lastTime = 0
let velocity = 0

// 开始拖动
const startDrag = (e: MouseEvent): void => {
  if (!scrollContainer.value) return
  
  isDragging = true
  startX = e.pageX - scrollContainer.value.getBoundingClientRect().left
  scrollLeft = scrollContainer.value.scrollLeft
  lastTime = performance.now()
  scrollContainer.value.style.cursor = 'grabbing'
}

// 拖动中
const onDrag = (e: MouseEvent): void => {
  if (!isDragging || !scrollContainer.value) return
  
  const x = e.pageX - scrollContainer.value.getBoundingClientRect().left
  const walk = (x - startX) * 1.5 // 调整滚动速度系数
  scrollContainer.value.scrollLeft = scrollLeft - walk
  
  // 计算速度(用于惯性滚动)
  const now = performance.now()
  velocity = (x - startX) / (now - lastTime)
  lastTime = now
}

// 停止拖动
const stopDrag = (): void => {
  if (!isDragging || !scrollContainer.value) return
  
  isDragging = false
  if (Math.abs(velocity) > 0.1) {
    requestAnimationFrame(inertiaScroll)
  }
  scrollContainer.value.style.cursor = 'grab'
}

// 惯性滚动
const inertiaScroll = (): void => {
  if (!scrollContainer.value || Math.abs(velocity) < 0.01) return
  
  scrollContainer.value.scrollLeft += velocity * 10
  velocity *= 0.95 // 摩擦系数
  
  requestAnimationFrame(inertiaScroll)
}

// 边界检测
const checkBounds = (): void => {
  if (!scrollContainer.value) return
  
  const containerWidth = scrollContainer.value.clientWidth
  const contentWidth = scrollContainer.value.scrollWidth
  
  if (scrollContainer.value.scrollLeft < 0) {
    scrollContainer.value.scrollLeft = 0
  } else if (scrollContainer.value.scrollLeft > contentWidth - containerWidth) {
    scrollContainer.value.scrollLeft = contentWidth - containerWidth
  }
}

// 鼠标滚轮横向滚动
const handleWheel = (e: WheelEvent): void => {
  if (!scrollContainer.value) return
  
  e.preventDefault()
  scrollContainer.value.scrollLeft += e.deltaY
  checkBounds()
}

// 生命周期钩子
onMounted(() => {
  if (scrollContainer.value) {
    scrollContainer.value.addEventListener('wheel', handleWheel, { passive: false })
  }
})

onUnmounted(() => {
  if (scrollContainer.value) {
    scrollContainer.value.removeEventListener('wheel', handleWheel)
  }
})
</script>

<style scoped>
.horizontal-scroll-container {
  width: 100%;
  overflow-x: auto;
  white-space: nowrap;
  cursor: grab;
  height: 220px; /* 确保容器有固定高度 */
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 10px;
  box-sizing: border-box;
}

.horizontal-scroll-container:active {
  cursor: grabbing;
}

.scroll-content {
  display: inline-block;
}

.item {
  display: inline-block;
  width: 200px;
  height: 200px;
  margin-right: 10px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  border: 1px solid #ddd;
  box-sizing: border-box;
  border-radius: 4px;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.item:hover {
  transform: scale(1.02);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>

网站公告

今日签到

点亮在社区的每一天
去签到