Nuxt3轮播图拖拽/触摸滚动异常:滚动重置至起始位置求助
问题描述
我正在开发一个带轮播功能的Nuxt 3应用,目前轮播仅支持水平滚动,但只有触控板用户能正常滚动。我想实现抓取拖拽功能,让用户可以点击拖拽左右滚动轮播,但尝试后功能不符合预期:每次滚动轮播都会重置到起始位置,仿佛滚动是基于窗口尺寸而非轮播本身,不过原有的水平滚动是正常的。
现有代码如下:
JavaScript 代码
const carousel: any = ref(); const items: any = reactive([]); const images: any = reactive([]); const progressLine: any = ref(); const progress: any = ref(0); const progressContent: any = ref(0); const tweened = reactive({ number: 0, }); const duration = reactive({ number: 1 }); onMounted(async () => { await nextTick(); let carouselWidth: any = carousel.value.clientWidth; let itemWidth: any = items[0].clientWidth; let itemPadding: any = 16 * (items.length - 1); let wrapWidth: any = items.length * itemWidth + itemPadding; let scrollPosition = 0; let maxScrollPosition: any; // Calculate the maximum scroll position maxScrollPosition = wrapWidth - carouselWidth; let handleScrollEvent = (e: { deltaX: any }) => { const { deltaX } = e; // Update the scroll position based on the mousewheel delta scrollPosition += deltaX; // Clamp the scroll position to prevent scrolling beyond the carousel edges scrollPosition = Math.max(0, Math.min(scrollPosition, maxScrollPosition)); // Calculate the progress of the scroll position as a percentage progress.value = (scrollPosition / maxScrollPosition) * 100; progressContent.value = progress.value; // Use GSAP to animate the carousel to the new scroll position gsap.to(carousel.value, { duration: duration.number, ease: 'expo.out', x: -scrollPosition }); // Update the progress line width to match the scroll position gsap.to(progressLine.value, { duration: duration.number, ease: 'expo.out', width: `${progress.value}%` }); }; let handleTouchEvent = (e: { deltaX: any, touches: { clientX: any, 0: { clientX: any }, }[], }) => { const { touches } = e; // Get the touch position const touchPosition = touches[0].clientX; // Update the scroll position based on the touch delta scrollPosition += touchPosition; // Clamp the scroll position to prevent scrolling beyond the carousel edges scrollPosition = Math.max(0, Math.min(scrollPosition, maxScrollPosition)); // Calculate the progress of the scroll position as a percentage progress.value = (scrollPosition / maxScrollPosition) * 100; progressContent.value = progress.value; // Use GSAP to animate the carousel to the new scroll position gsap.to(carousel.value, { duration: duration.number, ease: 'expo.out', x: -scrollPosition }); // Update the progress line width to match the scroll position gsap.to(progressLine.value, { duration: duration.number, ease: 'expo.out', width: `${progress.value}%` }); }; carousel.value.addEventListener('wheel', handleScrollEvent); carousel.value.addEventListener('touchmove', handleTouchEvent); window.addEventListener('resize', () => { carouselWidth = carousel.value.clientWidth; itemWidth = items[0].clientWidth; itemPadding = 16 * items.length; wrapWidth = items.length * itemWidth + itemPadding; }); });
HTML 代码
<ul class="slider" ref="carousel"> <li v-for="(project, i) in fetchedProjects" :key="project.slug" class="project-item" ref="items"> <NuxtLink ref="project" v-if="project.img" class="project-link"> <div class="project-figure"> <img :src="'https://picsum.photos/5' + i.toString().padStart(2, '0')" :alt="project.description" class="project-figure_img" ref="images" /> <span class="project-figure_span">{{ project.name }}</span> </div> </NuxtLink> </li> </ul>
CSS 代码
ul.slider { --imgSize: 14.6875rem; height: 100%; display: inline-flex; flex-wrap: nowrap; li.project-item { flex: 0 0 var(--imgSize); &:not(:last-of-type) { margin-right: 1em; } div.project-figure { user-select: none; img { pointer-events: none; user-select: none; height: var(--imgSize); width: var(--imgSize); object-fit: cover; } span { display: none; } } } }
问题分析与修复方案
核心问题点
wheel事件逻辑错误:普通鼠标滚轮的deltaX通常为0,直接累加会导致无响应;未阻止事件默认行为,会触发页面整体滚动。touchmove事件逻辑错误:直接累加触摸的当前X坐标而非位移差,导致滚动位置跳变。- 缺少鼠标拖拽事件:未实现
mousedown/mousemove/mouseup监听,无法支持鼠标拖拽滚动。 - resize事件未更新关键参数:窗口尺寸变化后,未重新计算
maxScrollPosition,导致滚动边界错误。
修复后的完整代码
JavaScript 代码
const carousel = ref(null); const items = ref([]); const progressLine = ref(null); const progress = ref(0); const duration = ref(0.3); // 缩短动画时长,提升跟手感 let carouselWidth = 0; let itemWidth = 0; let itemPadding = 0; let wrapWidth = 0; let scrollPosition = 0; let maxScrollPosition = 0; // 拖拽状态变量 let isDragging = false; let startX = 0; let startScrollPosition = 0; onMounted(async () => { await nextTick(); calculateCarouselDimensions(); // 滚轮事件处理 const handleWheel = (e) => { e.preventDefault(); // 将垂直滚轮转换为水平滚动量,适配非触控板设备 const scrollDelta = e.deltaY * -0.5; updateScrollPosition(scrollDelta); }; // 触摸开始 const handleTouchStart = (e) => { startX = e.touches[0].clientX; startScrollPosition = scrollPosition; }; // 触摸移动 const handleTouchMove = (e) => { e.preventDefault(); const deltaX = e.touches[0].clientX - startX; updateScrollPosition(-deltaX); // 触摸右滑对应轮播左移,取反位移值 }; // 鼠标拖拽开始 const handleMouseDown = (e) => { isDragging = true; startX = e.clientX; startScrollPosition = scrollPosition; carousel.value.style.cursor = 'grabbing'; // 绑定全局事件,防止鼠标移出轮播后无法结束拖拽 document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; // 鼠标拖拽移动 const handleMouseMove = (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; updateScrollPosition(-deltaX); }; // 鼠标拖拽结束 const handleMouseUp = () => { isDragging = false; carousel.value.style.cursor = 'grab'; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; // 统一更新滚动位置的函数 const updateScrollPosition = (delta) => { scrollPosition += delta; // 限制滚动范围在0到最大滚动位置之间 scrollPosition = Math.max(0, Math.min(scrollPosition, maxScrollPosition)); // 计算滚动进度(避免除以0) progress.value = maxScrollPosition > 0 ? (scrollPosition / maxScrollPosition) * 100 : 0; // 更新轮播和进度条 gsap.to(carousel.value, { duration: duration.value, ease: 'expo.out', x: -scrollPosition }); gsap.to(progressLine.value, { duration: duration.value, ease: 'expo.out', width: `${progress.value}%` }); }; // 窗口尺寸变化时重新计算参数 const handleResize = () => { calculateCarouselDimensions(); // 确保当前滚动位置在新的范围内 scrollPosition = Math.max(0, Math.min(scrollPosition, maxScrollPosition)); progress.value = maxScrollPosition > 0 ? (scrollPosition / maxScrollPosition) * 100 : 0; // 直接更新位置,无需动画 gsap.set(carousel.value, { x: -scrollPosition }); gsap.set(progressLine.value, { width: `${progress.value}%` }); }; // 计算轮播尺寸的通用函数 const calculateCarouselDimensions = () => { if (!carousel.value || items.value.length === 0) return; carouselWidth = carousel.value.clientWidth; itemWidth = items.value[0].clientWidth; itemPadding = 16 * (items.value.length - 1); wrapWidth = items.value.length * itemWidth + itemPadding; maxScrollPosition = Math.max(0, wrapWidth - carouselWidth); }; // 添加事件监听 carousel.value.addEventListener('wheel', handleWheel); carousel.value.addEventListener('touchstart', handleTouchStart); carousel.value.addEventListener('touchmove', handleTouchMove); carousel.value.addEventListener('mousedown', handleMouseDown); window.addEventListener('resize', handleResize); // 组件卸载时清理事件监听 onUnmounted(() => { carousel.value?.removeEventListener('wheel', handleWheel); carousel.value?.removeEventListener('touchstart', handleTouchStart); carousel.value?.removeEventListener('touchmove', handleTouchMove); carousel.value?.removeEventListener('mousedown', handleMouseDown); window.removeEventListener('resize', handleResize); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }); });
CSS 代码(新增拖拽光标样式)
ul.slider { --imgSize: 14.6875rem; height: 100%; display: inline-flex; flex-wrap: nowrap; cursor: grab; user-select: none; /* 防止拖拽时选中文字 */ li.project-item { flex: 0 0 var(--imgSize); &:not(:last-of-type) { margin-right: 1em; } div.project-figure { user-select: none; img { pointer-events: none; user-select: none; height: var(--imgSize); width: var(--imgSize); object-fit: cover; } span { display: none; } } } &:active { cursor: grabbing; /* 拖拽时的光标样式 */ } }
HTML 代码保持原有结构即可。
内容的提问来源于stack exchange,提问作者Andrew Mathieu
相关产品推荐
相关产品推荐

