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

功能说明

​​拖动功能​​:

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

​​滚动控制​​:

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

实现代码

html 复制代码
<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>
相关推荐
黄同学real2 小时前
Vue 项目中运行 `npm run dev` 时发生的过程
前端·vue.js·npm
黄同学real2 小时前
vue 优化策略,大白话版本
前端·javascript·vue.js
麦麦大数据6 小时前
vue+django农产品价格预测和推荐可视化系统[带知识图谱]
vue.js·python·django·知识图谱·推荐算法·价格预测·农业大数据
好名字08216 小时前
el-tabs与table样式冲突导致高度失效问题解决(vue2+elementui)
前端·vue.js·elementui
qq_278063716 小时前
vue elementui 去掉默认填充 密码input导致的默认填充
前端·vue.js·elementui
沉迷...7 小时前
tsconfig.json和tsconfig.node.json和tsconfig.app.json有什么区别
前端·vue.js·node.js
小彭努力中12 小时前
8.Three.js中的 StereoCamera 立体相机详解+示例代码
开发语言·前端·javascript·vue.js·深度学习·数码相机·ecmascript
哎哟喂_!16 小时前
UniApp 实现分享功能
前端·javascript·vue.js·uni-app
小彭努力中19 小时前
9.Three.js中 ArrayCamera 多视角相机详解+示例代码
开发语言·前端·javascript·vue.js·数码相机·ecmascript·webgl