+
77
-

回答

下面我将为您提供几种在 Vue 中实现的解决方案,从最推荐到备选方案,并附上完整的代码示例。

解决方案 1: 使用事件修饰符 .prevent (最推荐)

Vue 的事件修饰符提供了一种非常优雅和声明式的方式来处理这个问题。.prevent 修饰符会自动调用 event.preventDefault()。

关键点: 将 .prevent 添加到 @touchmove 事件上。现代浏览器为了优化滚动性能,会将 touchmove 事件默认为“被动 (passive)”,这意味着在事件监听器内部调用 event.preventDefault() 可能会被忽略。而 Vue 的 .prevent 修饰符会自动处理这个问题,确保 preventDefault 生效。

完整 Vue 3 (Composition API) 示例

这是一个完整的、可直接运行的组件示例。

<template>
  <div class="page-container">
    <div class="content">
      <p>向上滚动页面查看效果</p>
      <p>...</p>
      <p>...</p>
      <p>尝试拖动下面的方块</p>

      <!-- Draggable Element -->
      <div 
        class="draggable-box"
        :style="boxStyle"
        @touchstart.stop="onTouchStart"
        @touchmove.prevent="onTouchMove"
        @touchend="onTouchEnd"
      >
        拖动我
      </div>

      <p>...</p>
      <p>...</p>
      <p>页面底部</p>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue';

// 标记当前是否正在拖拽
const isDragging = ref(false);

// 元素的位置
const position = reactive({ x: 0, y: 0 });

// 拖拽起始时,手指相对于元素左上角的偏移
const startOffset = reactive({ x: 0, y: 0 });

// 使用计算属性动态生成样式
const boxStyle = computed(() => ({
  transform: `translate(${position.x}px, ${position.y}px)`
}));

// 触摸开始事件
const onTouchStart = (event) => {
  isDragging.value = true;

  // 记录初始触摸点与元素当前位置的差值
  const touch = event.touches[0];
  startOffset.x = touch.clientX - position.x;
  startOffset.y = touch.clientY - position.y;

  console.log("Drag Start");
};

// 触摸移动事件
const onTouchMove = (event) => {
  // 如果没有在拖拽状态,则直接返回
  if (!isDragging.value) return;

  // event.preventDefault() 已由 .prevent 修饰符自动处理
  // 这就是阻止页面滚动的关键!

  const touch = event.touches[0];

  // 根据手指的移动实时更新元素位置
  position.x = touch.clientX - startOffset.x;
  position.y = touch.clientY - startOffset.y;
};

// 触摸结束事件
const onTouchEnd = () => {
  if (isDragging.value) {
    isDragging.value = false;
    console.log("Drag End");
  }
};
</script>

<style>
/* 用于创建可滚动的页面环境 */
.page-container {
  font-family: sans-serif;
  text-align: center;
}
.content {
  height: 200vh; /* 使 body 产生滚动条 */
  padding-top: 50vh;
  background: linear-gradient(to bottom, #f0f4f8, #d9e2ec);
}

/* 可拖拽元素的样式 */
.draggable-box {
  width: 100px;
  height: 100px;
  background-color: #007bff;
  color: white;
  border-radius: 10px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-weight: bold;
  cursor: grab;
  user-select: none; /* 防止拖动时选中文本 */
  touch-action: none; /* CSS属性,提示浏览器该元素有自定义触摸行为 */
  position: relative; /* 确保 transform 生效 */
  z-index: 1000;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.draggable-box:active {
  cursor: grabbing;
  background-color: #0056b3;
}
</style>

代码解释:

@touchmove.prevent: 这是解决问题的核心。它告诉 Vue 在 onTouchMove 事件触发时,自动阻止其默认行为(即页面滚动)。

@touchstart.stop: .stop 修饰符会阻止事件冒泡。虽然在这个特定场景下不是必须的,但在复杂布局中可以防止父元素上意外的 touchstart 监听器被触发。

touch-action: none;: 这是一个重要的 CSS 属性。它向浏览器提供了一个性能提示,告知该元素上的触摸操作(如平移、缩放)将由 JavaScript 处理,浏览器不应尝试任何自己的行为(如滚动或缩放)。这可以作为 preventDefault 的补充,有时甚至可以单独解决问题,但结合使用最为稳妥。

状态管理: 使用 isDragging ref 来跟踪拖拽状态,确保只在拖拽时才更新元素位置。

坐标计算: 在 onTouchStart 中计算并保存了手指位置和元素当前位置的偏移量 (startOffset)。这可以确保拖拽时元素不会“跳”到你的手指尖,而是保持相对位置平滑移动。

解决方案 2: 切换 Body 的 Overflow 属性 (备选方案)

这是一种更“强制”的方法。当拖拽开始时,通过 JavaScript 给 <body> 标签添加一个 CSS 类,该类设置 overflow: hidden; 来禁用整个页面的滚动。拖拽结束后再移除该类。

优点:

非常可靠,绝对能阻止滚动。

缺点:

在某些桌面浏览器上,隐藏滚动条可能会导致页面宽度变化,引起内容“跳动”。

感觉不如方案 1 优雅。

修改 <script setup> 部分
// ... (保留 isDragging, position, startOffset, boxStyle 的定义)

// 触摸开始事件
const onTouchStart = (event) => {
  isDragging.value = true;
  document.body.classList.add('no-scroll'); // 添加类以禁用滚动

  const touch = event.touches[0];
  startOffset.x = touch.clientX - position.x;
  startOffset.y = touch.clientY - position.y;
};

// 触摸移动事件 (不再需要 .prevent)
const onTouchMove = (event) => {
  if (!isDragging.value) return;
  const touch = event.touches[0];
  position.x = touch.clientX - startOffset.x;
  position.y = touch.clientY - startOffset.y;
};

// 触摸结束事件
const onTouchEnd = () => {
  if (isDragging.value) {
    isDragging.value = false;
    document.body.classList.remove('no-scroll'); // 移除类以恢复滚动
  }
};
添加对应的 CSS

在你的 <style> 标签中加入:

body.no-scroll {
  overflow: hidden;
}

总结与最佳实践

方案 1: .prevent 修饰符Vue 原生、代码优雅、意图清晰、无副作用无明显缺点★★★★★ (首选)
方案 2: 切换 Body Overflow绝对可靠可能导致页面跳动,代码稍显侵入式★★★☆☆ (备选)

强烈建议您使用第一种方案。它利用了 Vue 框架的特性,代码更简洁、更具可读性,并且完美地解决了问题而没有引入其他副作用。

同时,不要忘记在你的 CSS 中为可拖拽元素添加 touch-action: none;,这是一个很好的性能和行为优化。

网友回复

我知道答案,我要回答