功能说明
拖动功能:
- 鼠标按下时记录初始位置和滚动位置
- 拖动过程中计算移动距离并更新滚动位置
- 松开鼠标后根据速度实现惯性滚动
滚动控制:
- 支持鼠标滚轮横向滚动(通过 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>