You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
            }
        }
    }
}

问题分析与修复方案

核心问题点

  1. wheel事件逻辑错误:普通鼠标滚轮的deltaX通常为0,直接累加会导致无响应;未阻止事件默认行为,会触发页面整体滚动。
  2. touchmove事件逻辑错误:直接累加触摸的当前X坐标而非位移差,导致滚动位置跳变。
  3. 缺少鼠标拖拽事件:未实现mousedown/mousemove/mouseup监听,无法支持鼠标拖拽滚动。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 13:18:10