【轮播图】H5端轮播图、横向滑动、划屏效果实现方案——Vue3+CSS position/CSS scroller/@scroll事件

402 阅读26分钟

    本文将详细介绍如何使用Vue3实现移动端流畅的划屏交互效果。我们将深入探讨触摸事件处理、页面动态切换和动画优化的完整实现方案。文章将重点解析三个关键方法:handleTouchStart记录触摸起点、handleTouchMove实现实时跟手效果、handleTouchEnd处理页面切换逻辑,特别会讲解pages.forEach循环在重置页面位置和恢复过渡动画中的重要作用。同时会分享性能优化技巧,包括transform动画的优势、直接DOM操作与Vue响应式的平衡,以及滑动阈值的合理设置,帮助开发者掌握移动端滑动交互的核心实现原理。


定位实现滑屏效果

实现了一个典型的移动端页面滑动容器,具有以下特点:

  • 支持左右滑动切换不同颜色的页面
  • 滑动时有实时跟手效果
  • 滑动结束后有平滑的过渡动画
  • 边界检测防止越界

前置知识

CSS: touch-action属性

touch-action 是 CSS 的一个属性,用来控制元素如何响应触摸操作(如滑动、缩放等)。

常见取值:

  • auto:默认,浏览器自行处理触摸行为。
  • none:完全禁用浏览器默认的触摸行为(如滚动、缩放)。
  • pan-x:允许水平滑动,禁止垂直滑动和缩放。
  • pan-y:允许垂直滑动,禁止水平滑动和缩放。
  • manipulation:允许滑动,但禁用双击缩放(提高响应速度)。

示例:

/* 禁用默认触摸行为,防止滚动 */
.prevent-scroll {
  touch-action: none;
}

/* 只允许垂直滑动 */
.vertical-only {
  touch-action: pan-y;
}

适用场景:

  • 自定义触摸手势(如画板、游戏)。
  • 防止滑动冲突(如地图内嵌滚动列表)。

简单说,它让开发者能精细控制触摸交互,避免浏览器默认行为的干扰

CSS: transform属性

transform: 是 CSS 中**把元素当成一张图来变形**的属性。 一次可以写多个 2D/3D 函数,空格隔开,按从左到右的顺序逐个执行。

  1. 常用 2D 函数
函数说明例子
translate(x, y)平移transform: translate(20px, -10px)
translateX(x) / translateY(y)单轴平移translateX(50%)
scale(sx, sy)缩放scale(1.2, .8)
rotate(angle)旋转rotate(45deg)
skew(ax, ay)倾斜skew(30deg, 10deg)
  1. 常用 3D 函数
函数说明
translate3d(x,y,z)三维平移
scale3d(sx,sy,sz)三维缩放
rotateX(a) / rotateY(a) / rotateZ(a)绕各轴旋转
  1. 组合示例
.box {
  transform: translateX(100px) rotate(45deg) scale(1.2);
}

先右移 100 px,再旋转 45°,最后放大 1.2 倍。

  1. 性能与注意点
  • 不占文档流:变形后原位置仍保留(不像 position:absolute 会脱离)。
  • 硬件加速:大多数浏览器对 transform 开 GPU 加速,动画较流畅。
  • 不影响兄弟元素:不会像 margin 那样推挤别人。

触摸事件

HTML 的触摸事件有 4 个核心,按触发顺序:

  • touchstart:手指刚碰到屏幕
  • touchmove:手指在屏幕上滑动(连续触发)
  • touchend:手指离开屏幕
  • touchcancel:系统中断(如来电、弹窗)

每个事件对象里都带 touches / changedTouches 等列表,可拿到触点坐标。

事件对象属性:

  • touches:当前屏幕上所有接触点集合。
  • targetTouches:事件绑定元素上的接触点集合。
  • changedTouches:触发此事件时状态改变的接触点集合(touchstart时即新出现的点)。

forEach

forEach 是数组的每个元素都跑一次的方法——给它一个回调函数,它把数组里的元素挨个传进去执行,不返回新数组,纯粹“副作用”。

基本语法:

array.forEach((value, index, array) => {
  /* 干点啥 */
});
  • value:当前元素
  • index:当前下标(可选)
  • array:原数组本身(几乎不用)

示例:

['a','b','c'].forEach((v,i)=>console.log(i,v));
// 0 a
// 1 b
// 2 c

回调占位符

在 JavaScript 的 forEach 回调里:

pages.forEach((_, i) => { ... })

_ 就是占位符:

  • 含义:“这个位置的参数我不打算用,随便起个名字占坑”。

  • 这里_对应的是数组元素(page 对象),但循环体里只用索引 i,所以用_表示“忽略它”。

这是一种常见习惯写法,读代码的人一看就明白“这个值被故意忽略了”。

准备阶段

基本结构代码:

<template>
  <div
    class="page-container"
    @touchstart="handleTouchStart"
    @touchmove="handleTouchMove"
    @touchend="handleTouchEnd"
  >
    <!-- 页面内容 -->
    <div
      v-for="(page, index) in pages"
      :key="page.id"
      class="page"
      :style="{ backgroundColor: page.color }"
    >
      {{ page.name }}
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";

const pages = [
  { color: "#ff7675", name: "屏幕 1", id: 0 },
  { color: "#74b9ff", name: "屏幕 2", id: 1 },
  { color: "#55efc4", name: "屏幕 3", id: 2 },
  { color: "#a29bfe", name: "屏幕 4", id: 3 },
  { color: "#ffcb20", name: "屏幕 5", id: 4 },
];

// 触摸开始
const handleTouchStart = (e) => {};

// 触摸移动
const handleTouchMove = (e) => {};

// 触摸结束
const handleTouchEnd = () => {};

</script>

<style scoped>
.page-container {}
.page {}
</style>

目前没有样式的效果如下: 在这里插入图片描述 所有内容都是堆叠在一起的,而目前我们想要的效果是每个屏幕沾满一个页面,然后通过滑动来切换,

先给内容来个沾满全屏的宽度高度

.page-container {
  width: 100vw;
  height: 100vh;
}
.page {
  width: 100%;
  height: 100%;
}

在这里插入图片描述 在处理一下page中的文字样式吧:

.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
}

在这里插入图片描述 现在所有页面都是呈现竖向排列的,与我们横向滑动的效果相违背,所以需要使用定位效果将 所有页面重叠在一起,然后再做移动效果来实现划屏效果

.page-container {
  width: 100vw;
  height: 100vh;
  position: relative;
  overflow: hidden;
  touch-action: none;
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
  position: absolute;
}

在这里插入图片描述

现在,所有屏幕都堆叠在一起了! 接下来使用transform属性让所有屏幕从左至右以此排列: 在这里插入图片描述

:style="{
    transform: `translateX(${index * 100}%)`,
    backgroundColor: page.color,
  }"

可能看不出效果,我们可以调小page 的宽高查看:width: 20%; height: 20%; 在这里插入图片描述

实现移动效果

接下来的内容就是重点啦!🫵💯

移动实现原理:视窗不变,让每个page分别向负方向移动 在这里插入图片描述

<!-- 页面内容 -->
   <div
     v-for="(page, index) in pages"
     :key="page.id"
     class="page"
     :style="{
       transform: `translateX(${index * 100 - currentIndex * 100}%)`,
       backgroundColor: page.color,
     }"
   >
     {{ page.name }}
   </div>

// 声明一个currentIndex 
import { ref } from "vue";
const currentIndex = ref(0); // 当前页面索引

可以通过改变const currentIndex = ref(2);中的变量来查看页面效果。 在这里插入图片描述 晓得了切换原理,接下来实现触摸事件啦🫴 流程示意图: 在这里插入图片描述

const currentIndex = ref(0); // 当前页面索引
const startX = ref(0);
const moveX = ref(0);
// 触摸开始:记录起点
const handleTouchStart = (e) => {
  startX.value = e.touches[0].clientX;
};

// 触摸移动:实时跟随手指滑动
const handleTouchMove = (e) => {
  moveX.value = e.touches[0].clientX - startX.value;
};

// 触摸结束:判断滑动方向并切换页面
const handleTouchEnd = () => {
  if (Math.abs(moveX.value)) {
    if (moveX.value > 0 && currentIndex.value > 0) {
      currentIndex.value--; // 向右滑,上一页
    } else if (moveX.value < 0 && currentIndex.value < pages.length - 1) {
      currentIndex.value++; // 向左滑,下一页
    }
  }
};

现在就可以通过滑动屏幕来实现切换效果了: 在这里插入图片描述

触发的有点灵敏,我们给handleTouchEnd添加一个滑动阈值

const handleTouchEnd = () => {
  const threshold = 50; // 滑动阈值(像素)
  if (Math.abs(moveX.value) > threshold) {
    if (moveX.value > 0 && currentIndex.value > 0) {
      currentIndex.value--; // 向右滑,上一页
    } else if (moveX.value < 0 && currentIndex.value < pages.length - 1) {
      currentIndex.value++; // 向左滑,下一页
    }
  }
};

再添加一个css平滑过渡效果:

.page {
  //...
  transition: transform 0.3s ease; /* 平滑过渡 */
}

在这里插入图片描述

实现跟手效果

现在就要点丝滑😻效果了,不过还不够还要添加跟手效果

  <!-- 页面内容 -->
<div
  v-for="(page, index) in pages"
  :key="page.id"
  class="page"
  ref="pageRef"
  :style="{
    transform: `translateX(${index * 100 - currentIndex * 100}%)`,
    backgroundColor: page.color,
  }"
>
  {{ page.name }}
</div>
    
const pageRef = ref(null);
// 触摸移动:实时跟随手指滑动
const handleTouchMove = (e) => {
  moveX.value = e.touches[0].clientX - startX.value;
  // 跟手效果
  pages.forEach((_, i) => {
    const page = pageRef.value[i];
    if (page) {
      page.style.transform = `translateX(${
        i * 100 - currentIndex.value * 100 + moveX.value / 10
      }%)`;
      page.style.transition = "none"; // 禁用过渡效果,保证跟手效果流畅
    }
  });
};

在这里插入图片描述

实现原理详解

  1. 获取移动距离
    • moveX.value = e.touches[0].clientX - startX.value 计算出手指从触摸开始到当前位置的水平移动距离
  2. 实时更新页面位置
    • 遍历所有页面元素,为每个页面计算新的transform
    • 基础位置:i * 100 - currentIndex.value * 100 确保页面按顺序排列
    • 跟手偏移:+ moveX.value / 10 添加移动距离的1/10作为跟手效果(除以10是为了降低跟手灵敏度)
  3. 禁用过渡效果
    • page.style.transition = "none" 临时禁用CSS过渡效果,确保跟手时的即时响应

触摸结束优化

// 触摸结束:判断滑动方向并切换页面
const handleTouchEnd = () => {
  //....
  // 重置位置并启用过渡动画
  pages.forEach((_, i) => {
    const page = document.querySelectorAll(".page")[i];
    if (page) {
      page.style.transform = `translateX(${
        i * 100 - currentIndex.value * 100
      }%)`;
      page.style.transition = "transform 0.3s ease";
    }
  });
  moveX.value = 0;
};

handleTouchEnd 里那段 pages.forEach(...) 只有一句话:把 5 张“页面”一次性摆到正确位置,并补上过渡动画

  1. 计算目标位置

    i * 100 - currentIndex.value * 100
    
    • i * 100:第 i 张默认排在“第 i 屏”位置(0%、100%、200% …)。
    • 减去 currentIndex * 100:把当前要显示的那一页拉回 0%(即屏幕正中)。
      结果:所有页瞬间排成一排,当前页居中,其余页在左右两侧。
  2. 设置样式

    • transform 赋刚才算出的值,让页面“归位”。
    • transition = "transform 0.3s ease":把被 handleTouchMove 关掉的过渡重新打开,回弹时有动画。
  3. 重置 moveX.value = 0:为下一次手势准备。

最后得到的效果如下:( 对此我们的轮播图实现效果也完成啦🎉) 在这里插入图片描述

完整代码

<template>
  <div
    class="page-container"
    @touchstart="handleTouchStart"
    @touchmove="handleTouchMove"
    @touchend="handleTouchEnd"
  >
    <!-- 页面内容 -->
    <div
      v-for="(page, index) in pages"
      :key="page.id"
      class="page"
      ref="pageRef"
      :style="{
        transform: `translateX(${index * 100 - currentIndex * 100}%)`,
        backgroundColor: page.color,
      }"
    >
      {{ page.name }}
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";

const pages = [
  { color: "#ff7675", name: "屏幕 1", id: 0 },
  { color: "#74b9ff", name: "屏幕 2", id: 1 },
  { color: "#55efc4", name: "屏幕 3", id: 2 },
  { color: "#a29bfe", name: "屏幕 4", id: 3 },
  { color: "#ffcb20", name: "屏幕 5", id: 4 },
];
const currentIndex = ref(0); // 当前页面索引
const startX = ref(0);
const moveX = ref(0);
// 触摸开始:记录起点
const handleTouchStart = (e) => {
  startX.value = e.touches[0].clientX;
};

const pageRef = ref(null);
// 触摸移动:实时跟随手指滑动
const handleTouchMove = (e) => {
  moveX.value = e.touches[0].clientX - startX.value;
  // 跟手效果
  pages.forEach((_, i) => {
    const page = pageRef.value[i];
    if (page) {
      page.style.transform = `translateX(${
        i * 100 - currentIndex.value * 100 + moveX.value / 10
      }%)`;
      page.style.transition = "none"; // 禁用过渡效果,保证跟手效果流畅
    }
  });
};

// 触摸结束:判断滑动方向并切换页面
const handleTouchEnd = () => {
  const threshold = 200; // 滑动阈值(像素)
  if (Math.abs(moveX.value) > threshold) {
    if (moveX.value > 0 && currentIndex.value > 0) {
      currentIndex.value--; // 向右滑,上一页
    } else if (moveX.value < 0 && currentIndex.value < pages.length - 1) {
      currentIndex.value++; // 向左滑,下一页
    }
  }
  // 重置位置并启用过渡动画
  pages.forEach((_, i) => {
    const page = pageRef.value[i];
    if (page) {
      page.style.transform = `translateX(${
        i * 100 - currentIndex.value * 100
      }%)`;
      page.style.transition = "transform 0.3s ease";
    }
  });
  moveX.value = 0;
};
</script>

<style scoped>
.page-container {
  width: 100vw;
  height: 100vh;
  position: relative;
  overflow: hidden;
  touch-action: none;
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
  position: absolute;
  transition: transform 0.3s ease; /* 平滑过渡 */
}
</style>

使用节流优化性能

方法一: 通过lodash库

安装lodash:

npm install lodash-es
<script setup>
import { ref } from "vue";
import { throttle } from 'lodash-es'; // 或者使用自定义节流函数

// ... existing code ...

const handleTouchMove = throttle((e) => {
  moveX.value = e.touches[0].clientX - startX.value;
  // 跟手效果
  pages.forEach((_, i) => {
    const page = pageRef.value[i];
    if (page) {
      page.style.transform = `translateX(${
        i * 100 - currentIndex.value * 100 + moveX.value / 10
      }%)`;
      page.style.transition = "none";
    }
  });
}, 16); // 约60fps的间隔

// ... existing code ...
</script>

或者使用自定义的简单节流实现:

<script setup>
import { ref } from "vue";

// ... existing code ...

let lastTime = 0;
const handleTouchMove = (e) => {
  const now = Date.now();
  if (now - lastTime < 16) return; // 约60fps
  lastTime = now;
  
  moveX.value = e.touches[0].clientX - startX.value;
  // ... rest of the original code ...
};

// ... existing code ...
</script>

滚动实现滑屏效果

前置知识

CSS: scroll-snap-type属性

scroll-snap-type 是 CSS 中的一个属性,用于控制滚动容器的滚动行为,特别是当用户滚动时,内容是否以及如何“吸附”到特定的停止点。 这个属性定义在滚动容器上,它决定了滚动的轴向以及吸附行为的严格程度。

基本语法:

scroll-snap-type: none | [ x | y | block | inline | both ] [ mandatory | proximity ]?;

取值说明:

  1. 轴向 (Axis)
    • none:不启用滚动吸附。这是默认值。
    • x:在水平轴上启用滚动吸附。
    • y:在垂直轴上启用滚动吸附。
    • block:在块级方向上启用吸附(在大多数书写模式下等同于 y)。
    • inline:在内联方向上启用吸附(在大多数书写模式下等同于 x)。
    • both:在两个轴上都启用吸附。
  2. 严格程度 (Strictness)
    • mandatory:强制吸附。当滚动操作结束时(例如用户松开鼠标、手指或滚动停止),视口必须停留在一个吸附点上。如果内容区域不足以滚动到下一个/上一个点,用户可能无法滚动。
    • proximity:邻近吸附。滚动操作结束时,如果当前视口位置足够接近一个吸附点,浏览器会自动滚动到该点。但如果用户停止在两个吸附点之间较远的位置,可能不会发生吸附。用户体验更灵活。

示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>滚动吸附示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
        }
        
        h1 {
            text-align: center;
            margin-bottom: 30px;
        }
        
        /* 垂直方向强制吸附 */
        .container {
            scroll-snap-type: y mandatory;
            overflow-y: scroll;
            height: 400px;
            border: 2px solid #333;
            margin-bottom: 40px;
        }
        
        /* 水平方向邻近吸附 */
        .horizontal-slider {
            display: flex;
            scroll-snap-type: x proximity;
            overflow-x: scroll;
            scroll-behavior: smooth;
            border: 2px solid #333;
            margin-bottom: 40px;
        }
        
        /* 子元素需要设置吸附点 */
        .item {
            scroll-snap-align: start;
            flex: 0 0 100%;
            height: 300px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            font-weight: bold;
            color: white;
        }
        
        /* 垂直容器中的项目样式 */
        .container .item {
            height: 400px;
        }
        
        /* 为不同项目设置不同背景色 */
        .item:nth-child(1) {
            background-color: #FF5733;
        }
        .item:nth-child(2) {
            background-color: #33FF57;
        }
        .item:nth-child(3) {
            background-color: #3357FF;
        }
        .item:nth-child(4) {
            background-color: #F333FF;
        }
    </style>
</head>
<body>
    <h1>滚动吸附效果演示</h1>
    
    <h2>垂直滚动吸附(强制)</h2>
    <div class="container">
        <div class="item">垂直项目 1</div>
        <div class="item">垂直项目 2</div>
        <div class="item">垂直项目 3</div>
        <div class="item">垂直项目 4</div>
    </div>
    
    <h2>水平滚动吸附(邻近)</h2>
    <div class="horizontal-slider">
        <div class="item">水平项目 1</div>
        <div class="item">水平项目 2</div>
        <div class="item">水平项目 3</div>
        <div class="item">水平项目 4</div>
    </div>
    
    <p>说明:垂直滚动容器设置了强制吸附(y mandatory),滚动结束后会精确停在项目顶部。水平滚动容器设置了邻近吸附(x proximity),滚动结束后可能会停在项目附近。</p>
</body>
</html>

在这里插入图片描述

准备阶段

基本结构代码:

<template>
  <div class="scroll-container">
    <!-- 页面内容 -->
    <div
      v-for="(page, index) in pages"
      :key="page.id"
      class="page"
      :style="{ backgroundColor: page.color }"
    >
      {{ page.name }}
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";

const pages = [
  { color: "#ff7675", name: "屏幕 1", id: 0 },
  { color: "#74b9ff", name: "屏幕 2", id: 1 },
  { color: "#55efc4", name: "屏幕 3", id: 2 },
  { color: "#a29bfe", name: "屏幕 4", id: 3 },
  { color: "#ffcb20", name: "屏幕 5", id: 4 },
];

</script>

<style scoped>
.scroll-container {
}
.page {
}
</style>

和上面步骤一样,同样的给内容来个沾满全屏的宽度高度以及page重点文字样式:

.scroll-container {
  height: 100vh;
  width: 100vw;
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
}

实现滑动效果

接下来的布局就是和上面不同的时候啦🤗,我们要给scroll-container一个flex属性,让所有的子元素横向排列,排列完之后大家会发现page子元素并没有想我们想想中的那样沾满这个页面以此排列,而是都挤在一个页面了。

在这里插入图片描述 这是因为flex布局的缘由,使得每个page的宽度被压缩了。要让页面不被压缩,需要修改 .page 的样式,将 flex-shrink: 0 添加到样式中,这样 flex 容器就不会压缩这些页面了。

.scroll-container {
    /*...*/
    overflow-x: auto;
}
.page {
    /*...*/
    flex-shrink: 0; /* 添加这行防止页面被压缩 */
}

现在就解决了页面布局问题啦😄。

实现吸附效果

添加上scroll-snap-type属性,让滚动条有吸附效果:

.scroll-container {
  /*...*/
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch; /* 启用平滑滚动 */
}
.page {
  /*...*/
  flex-shrink: 0; /* 添加这行防止页面被压缩 */
  scroll-snap-align: start;
}

划划屏幕,是不是发现现在就已经实现了我们之前想要实现的划屏效果啦😲!没错,就是这么简单,几乎纯CSS样式就可以搞定啦🤩!

滚动条隐藏

接下来优化一下下细节,先来去掉底部下方的横向滚动条: 在这里插入图片描述 要去掉横向滚动条,可以修改 .scroll-container 的样式,将 overflow-x 从 auto 改为 hidden。

.scroll-container {
  /*...*/
  /* 使用浏览器私有伪元素隐藏滚动条​ */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
}
/* WebKit 浏览器:隐藏滚动条但保留滚动功能 */
.scroll-container::-webkit-scrollbar {
  display: none; /* Chrome/Safari/Opera */
}

存在问题

可以很清晰的观察到一个现象——当用力滑动时会一下子跳过多页。 在这里插入图片描述

这是因为 scroll-snap-type: x mandatory 在惯性滚动下的典型表现。浏览器的滚动捕捉机制虽然会强制最终停在某个捕捉点上,但它不会限制滚动的“速度”或“距离”。当你用力快速滑动时,系统会产生很大的惯性,滚动容器会高速“掠过”中间的捕捉点,最终可能停在更远的一个点上。 遗憾的是,纯 CSS 目前没有直接的属性可以限制“每次滚动只移动一个捕捉点”。mandatory 只保证“停在点上”,不保证“只移动一步”。小编这里也是还没有找到适合的实现方式👉👈,就当作是这个方法的缺陷吧😖。

完整代码

<template>
  <div class="scroll-container" >
    <!-- 页面内容 -->
    <div
      v-for="(page, index) in pages"
      :key="page.id"
      class="page"
      :style="{ backgroundColor: page.color }"
    >
      {{ page.name }}
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";

const pages = [
  { color: "#ff7675", name: "屏幕 1", id: 0 },
  { color: "#74b9ff", name: "屏幕 2", id: 1 },
  { color: "#55efc4", name: "屏幕 3", id: 2 },
  { color: "#a29bfe", name: "屏幕 4", id: 3 },
  { color: "#ffcb20", name: "屏幕 5", id: 4 },
];

</script>

<style scoped>
.scroll-container {
  height: 100vh;
  width: 100vw;
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch; /* 启用平滑滚动 */
  /* 使用浏览器私有伪元素隐藏滚动条​ */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
}
/* WebKit 浏览器:隐藏滚动条但保留滚动功能 */
.scroll-container::-webkit-scrollbar {
  display: none; /* Chrome/Safari/Opera */
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
  flex-shrink: 0; /* 添加这行防止页面被压缩 */
  scroll-snap-align: start;
}
</style>

scrollLeft实现滑屏效果(最佳实践)

为了解决上面使用滚动实现的划屏效果痛点,我们不得不抛弃CSS属性,该用JS来实现我们想要的效果。具体是那种JS方法呢? 其实你已经学过了,就是第一种中的方法,通过触摸事件来控制屏幕滑动,不过这一次我们不是控制定位位置来改变位置,而是通过控制横向滚动条的滚动位置来改变位置。

前置知识

DOM: scrollLeft

scrollLeft 是一个 JavaScript DOM 属性,它用于获取或设置一个可滚动元素(如 div, body 等)的水平滚动条相对于最左侧的偏移量。

作用:

  • 获取:读取元素当前水平滚动多少像素。
  • 设置:将元素的水平滚动移动到指定的位置。

基本语法:

// 获取元素当前的水平滚动偏移量(像素)
let currentScroll = element.scrollLeft;

// 设置元素的水平滚动偏移量(像素)
element.scrollLeft = value;
  • element:一个具有滚动能力的 DOM 元素(即其 overflow 属性为 autoscroll,并且内容超出其宽度)。
  • value:一个非负的数值,表示希望滚动条距离最左侧的像素数。

PS:

  • 一个元素可滚动的最大 scrollLeft 值可以通过 element.scrollWidth - element.clientWidth 计算得出。
  • scrollTop 用于垂直方向的滚动。

实例:

<div id="scroller" style="width: 200px; overflow-x: auto; white-space: nowrap;">
  <span>这是一个很长很长很长很长很长很长的文本行,需要水平滚动才能看完。</span>
</div>
<button onclick="scrollToRight()">滚动到底部</button>
const scroller = document.getElementById('scroller');

// 获取当前滚动位置
console.log('当前水平滚动位置:', scroller.scrollLeft); // 例如: 0

// 将滚动条滚动到最右侧
scroller.scrollLeft = scroller.scrollWidth - scroller.clientWidth;

// 或者滚动到特定位置
scroller.scrollLeft = 50;

// 滚动一定距离(例如向右滚动 20 像素)
scroller.scrollLeft += 20;

// 检测是否滚动到了最右边
if (scroller.scrollLeft >= scroller.scrollWidth - scroller.clientWidth) {
  console.log('已滚动到底部!');
}

DOM: scrollWidth

scrollWidth 是一个 只读 的 JavaScript DOM 属性,它返回一个元素的内容(包括由于溢出而不可见的部分)的整个宽度,以像素为单位。 简单来说,scrollWidth 告诉你这个元素的“内容”到底有多宽,即使这些内容因为容器大小限制而被隐藏(溢出)了。 “scrollWidth = 可视内容宽 + 被卷起来的内容宽”

  • 内容区域的总宽度(包括由于溢出而在视口外不可见的部分)。
  • 包含 元素的 padding,不包含 border、margin、垂直滚动条的宽度。
  • 如果元素没有溢出,scrollWidth 与 clientWidth(可见区域宽度)通常相等;出现水平溢出时,scrollWidth > clientWidth

与相关属性的对比(横向):

属性包含内容包含 padding包含 border包含 margin包含隐藏部分
scrollWidth
clientWidth
offsetWidth

垂直方向把 “Width” 换成 “Height” 即可。

@scroll事件

@scroll 是 Vue.js 框架中的一个事件监听修饰符,用于监听 DOM 元素上的 scroll 事件。 基本语法:

<template>
  <!-- 监听某个元素的滚动 -->
  <div @scroll="handleScroll" class="scrollable-container">
    <!-- 可滚动的内容 -->
  </div>

  <!-- 或者监听整个窗口的滚动 (通常在 mounted 钩子中用 addEventListener) -->
  <!-- 但在 Vue 模板中,@scroll 通常用于具体元素 -->
</template>

<script>
export default {
  methods: {
    handleScroll(event) {
      // event 是原生的 UIEvent 对象
      console.log('滚动了!');

      // 获取滚动元素
      const element = event.target;

      // 获取滚动位置
      console.log('scrollTop:', element.scrollTop);
      console.log('scrollLeft:', element.scrollLeft);

      // 其他滚动相关的逻辑...
    }
  }
}
</script>

核心作用:@scroll 事件在元素的滚动条滚动时被触发

它可以用于:

  • 监听滚动位置:获取 scrollTopscrollLeft,判断用户滚动到了哪里。
  • 实现懒加载:当用户滚动到页面底部或某个区域附近时,动态加载更多内容。
  • 实现吸顶效果:当页面滚动超过某个元素的位置时,改变该元素的样式(如 position: fixed)。
  • 检测滚动方向:通过比较前后两次的 scrollTop 值来判断用户是向上还是向下滚动。
  • 滚动动画控制:根据滚动位置触发动画或改变元素状态。

scrollTo方法

JavaScript 的 scrollTo 方法是浏览器提供的「全局滚动 API」之一,用来把当前视口或某个可滚动元素滚动到指定坐标。任何拥有滚动条的元素(overflow: auto/scroll)也实现了 scrollTo:

const box = document.getElementById('chatList');
box.scrollTo({
  top: box.scrollHeight,
  behavior: 'smooth'
});

示例:聊天列表自动滚动到底部

<!DOCTYPE html>
<html>
<body>
  <div id="chatList" style="height: 200px; overflow-y: auto; border: 1px solid #ccc;"></div>
  <button onclick="addMessage()">添加消息</button>

  <script>
    const chatList = document.getElementById('chatList');
    
    // 核心滚动API
    function scrollToBottom() {
      chatList.scrollTo({
        top: chatList.scrollHeight,
        behavior: 'smooth'
      });
    }
    
    // 添加消息演示
    function addMessage() {
      const msg = document.createElement('div');
      msg.textContent = '新消息 ' + new Date().toLocaleTimeString();
      chatList.appendChild(msg);
      scrollToBottom(); // 调用滚动API
    }
    
    // 初始化3条消息
    for (let i = 0; i < 3; i++) {
      addMessage();
    }
  </script>
</body>
</html>

效果: 在这里插入图片描述

准备阶段

基本结构代码:

<template>
  <div
    class="scroll-container"
    ref="containerRef"
    @touchstart="touchstart"
    @touchend="touchend"
  >
    <!-- 页面内容 -->
    <div
      v-for="(page, index) in pages"
      :key="page.id"
      class="page"
      :style="{ backgroundColor: page.color }"
    >
      {{ page.name }}
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";

const pages = [  { color: "#ff7675", name: "屏幕 1", id: 0 },  { color: "#74b9ff", name: "屏幕 2", id: 1 },  { color: "#55efc4", name: "屏幕 3", id: 2 },  { color: "#a29bfe", name: "屏幕 4", id: 3 },  { color: "#ffcb20", name: "屏幕 5", id: 4 },];

const containerRef = ref(null);

const touchstart = (e) => {};
const touchend = (e) => {};
</script>

<style scoped>
.scroll-container {
  height: 100vh;
  width: 100vw;
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch; /* 启用平滑滚动 */
  /* 使用浏览器私有伪元素隐藏滚动条​ */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
  touch-action: none; /* 禁止触摸动作 */
}
/* WebKit 浏览器:隐藏滚动条但保留滚动功能 */
.scroll-container::-webkit-scrollbar {
  display: none; /* Chrome/Safari/Opera */
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
  flex-shrink: 0; /* 添加这行防止页面被压缩 */
  scroll-snap-align: start;
}
</style>

实现滑动效果

定义基本变量:

const containerRef = ref(null);
const currentIndex = ref(0); // 当前页面索引
const startX = ref(0); // 触摸开始时的X坐标
const moveX = ref(0); // 触摸移动时的X坐标

// 计算滚动容器的宽度
const scrollWidth = computed(() => {
  return containerRef.value.scrollWidth;
});
// 计算屏幕宽度
const screenWidth = computed(() => {
  return containerRef.value.offsetWidth;
});

通过scrollTo方法来实现:

// 滚动方法
const scrollTo = (diffX) => {
  containerRef.value.scrollTo({
    left: diffX,
    behavior: "smooth",
  });
};

touchstarttouchend 方法:

const touchstart = (e) => {
  if (!can_scroll.value) {
    return; // 如果正在滚动,阻止触摸事件
  }
  startX.value = e.changedTouches[0].clientX;
};

const touchend = (e) => {
  const endX = e.changedTouches[0].clientX;
  moveX.value = endX - startX.value;
  if (moveX.value < 0) {
    // 滑动至下一页
    if (currentIndex.value >= pages.length - 1) {
      // 如果当前是最后一页,滚动到第一页
      currentIndex.value = 0;
      scrollTo(screenWidth.value * currentIndex.value);
    } else {
      // 否则滚动到下一页
      currentIndex.value = currentIndex.value + 1;
      scrollTo(screenWidth.value * currentIndex.value);
    }
  } else if (moveX.value > 0) {
    // 滑动至上一页
    if (currentIndex.value == 0) {
      // 如果当前是第一页,滚动到最后一页
      currentIndex.value = pages.length - 1;
      scrollTo(screenWidth.value * currentIndex.value);
    } else {
      // 否则滚动到上一页
      currentIndex.value = currentIndex.value - 1;
      scrollTo(screenWidth.value * currentIndex.value);
    }
  }
};

实现跟手效果

touchmove方法中实现:

const touchMove = (e) => {
  const currentX = e.changedTouches[0].clientX;
  const disX = currentX - startX.value;
  const moveX = containerRef.value.scrollLeft - disX; // 反向移动实现跟手
  scrollTo(moveX);
};

设置滑动阈值

touchend方法中添加设置滑动阈值变量dis_area

const touchend = (e) => {
const endX = e.changedTouches[0].clientX;
  moveX.value = endX - startX.value;
  const dis_area = 100; // 设置滑动距离阈值
  if (Math.abs(moveX.value) > dis_area) {
    if (moveX.value < 0) {}
    //....
    }
    scrollTo(screenWidth.value * currentIndex.value);
}

这样就实现了滑动效果: 在这里插入图片描述 这样就完成啦^_^.

完整代码

<template>
  <div
    class="scroll-container"
    ref="containerRef"
    @touchstart="touchstart"
    @touchmove="touchMove"
    @touchend="touchend"
  >
    <!-- 页面内容 -->
    <div
      v-for="(page, index) in pages"
      :key="page.id"
      class="page"
      :style="{ backgroundColor: page.color }"
    >
      {{ page.name }}
    </div>
  </div>
</template>

<style scoped>
.scroll-container {
  height: 100vh;
  width: 100vw;
  display: flex;
  overflow-x: auto;
  scroll-behavior: auto; /* 添加平滑滚动效果 */
  /* scroll-snap-type: x mandatory; */
  -webkit-overflow-scrolling: touch; /* 启用平滑滚动 */
  /* 使用浏览器私有伪元素隐藏滚动条​ */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
  touch-action: none; /* 禁止触摸动作 */
}
/* WebKit 浏览器:隐藏滚动条但保留滚动功能 */
.scroll-container::-webkit-scrollbar {
  display: none; /* Chrome/Safari/Opera */
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
  flex-shrink: 0; /* 添加这行防止页面被压缩 */
  /* scroll-snap-align: start; */
}
</style>
<script setup>
import { ref, onMounted, computed } from "vue";

const pages = [
  { color: "#ff7675", name: "屏幕 1", id: 0 },
  { color: "#74b9ff", name: "屏幕 2", id: 1 },
  { color: "#55efc4", name: "屏幕 3", id: 2 },
  { color: "#a29bfe", name: "屏幕 4", id: 3 },
  { color: "#ffcb20", name: "屏幕 5", id: 4 },
];

const containerRef = ref(null);
const currentIndex = ref(0); // 当前页面索引
const startX = ref(0); // 触摸开始时的X坐标
const moveX = ref(0); // 触摸移动时的X坐标
const can_scroll = ref(true); // 控制是否可以滚动

// 计算滚动容器的宽度
const scrollWidth = computed(() => {
  return containerRef.value.scrollWidth;
});
// 计算屏幕宽度
const screenWidth = computed(() => {
  return containerRef.value.offsetWidth;
});
// 滚动方法
const scrollTo = (diffX) => {
  containerRef.value.scrollTo({
    left: diffX,
    behavior: "smooth",
  });
};
const touchstart = (e) => {
  if (!can_scroll.value) {
    return; // 如果正在滚动,阻止触摸事件
  }
  startX.value = e.changedTouches[0].clientX;
};
const touchMove = (e) => {
  const currentX = e.changedTouches[0].clientX;
  const disX = currentX - startX.value;
  moveX.value = containerRef.value.scrollLeft - disX; // 反向移动实现跟手
  scrollTo(moveX.value);
};
const touchend = (e) => {
  const endX = e.changedTouches[0].clientX;
  moveX.value = endX - startX.value;
  const dis_area = 100; // 滑动距离阈值
  if (Math.abs(moveX.value) > dis_area) {
    if (moveX.value < 0) {
      // 滑动至下一页
      if (currentIndex.value >= pages.length - 1) {
        // 如果当前是最后一页,滚动到第一页
        currentIndex.value = 0;
        scrollTo(screenWidth.value * currentIndex.value);
      } else {
        // 否则滚动到下一页
        currentIndex.value = currentIndex.value + 1;
        scrollTo(screenWidth.value * currentIndex.value);
      }
    } else if (moveX.value > 0) {
      // 滑动至上一页
      if (currentIndex.value == 0) {
        // 如果当前是第一页,滚动到最后一页
        currentIndex.value = pages.length - 1;
        scrollTo(screenWidth.value * currentIndex.value);
      } else {
        // 否则滚动到上一页
        currentIndex.value = currentIndex.value - 1;
        scrollTo(screenWidth.value * currentIndex.value);
      }
    }
  }
  scrollTo(screenWidth.value * currentIndex.value);
};
</script>

组件封装

接下来就是把实现好的划屏效果封装成组件,提供给外界使用,每个页面可以通过插槽的形式渲染不同的画面。接下来我会以第三种方案为例来进行封装教学。

流程分析:

  1. 项目结构 & 依赖
  2. 父组件:如何一次性传入所有页面
  3. 子组件(轮播图):核心实现拆解
    1. 模板结构
    2. 数据与计算属性
    3. 触摸事件三步曲:touchstart / touchmove / touchend
    4. 样式隐藏滚动条 & 禁止浏览器默认行为
  4. 使用姿势
  5. 可继续扩展的点

1. 项目结构 & 依赖

  • 框架:Vue3 <script setup> + TS(父组件)
  • 样式:scss(父组件) / 原生 css(子组件)
  • 零第三方依赖,纯原生实现。

目录示例

src
├─ App.vue            // 父组件
├─ A.vue              // 页面 A
├─ B.vue              // 页面 B
├─ C.vue              // 页面 C
└─ component
   └─ swper.vue       // 轮播图组件

2. 父组件:如何一次性传入所有页面

2.1 模板

<template>
  <div class="">
    <swper :pages="pageList"></swper>
  </div>
</template>

swper 就是待会要封装的子组件,通过 pages prop 一次性把「所有页面」塞进去。

2.2 脚本

<script setup lang="ts">
import swper from "./component/swper.vue";
import APage from "./A.vue";
import BPage from "./B.vue";
import CPage from "./C.vue";

// 动态页面配置
const pageList = [
  { component: APage, props: { title: "主页", isLoaded: false } },
  { component: BPage, props: { title: "推荐", isLoaded: false } },
  { component: CPage, props: { title: "我的", isLoaded: false } },
];
</script>
  • 每个元素都是一个「页面描述对象」:
    • component 字段:真正要渲染的页面组件。
    • props 字段:透传给该页面组件的属性。
  • 以后想加新页面,直接往 pageList push 即可,轮播图内部无需任何改动。

3. 子组件(轮播图):核心实现拆解

文件名:component/swper.vue

3.1 模板结构

<template>
  <div
    class="scroll-container"
    ref="containerRef"
    @touchstart="touchstart"
    @touchmove="touchMove"
    @touchend="touchend"
  >
    <!-- 页面内容 -->
    <div v-for="(page, index) in pages" :key="index" class="page">
      <component :is="page.component" v-bind="page.props" />
    </div>
  </div>
</template>
  • 最外层 .scroll-container 是一个可横向滚动的 flex 容器。
  • 每个 .page 宽度 = 100vw,高度 = 100vh,flex-shrink 为 0,防止被挤压。
  • <component> 动态渲染父组件传进来的页面组件,并把 props 透传下去。

3.2 数据与计算属性

<script setup>
import { ref, onMounted, computed } from "vue";

// 1. props
const props = defineProps({
  pages: {
    type: Array,
    required: true,
    default: () => [],
  },
});

// 2. refs
const containerRef = ref(null);
const currentIndex = ref(0);   // 当前页面索引
const startX = ref(0);         // 触摸起始 x
const moveX = ref(0);          // 实时位移
const can_scroll = ref(true);  // 节流锁,防止动画过程中再次触发

// 3. 计算属性
const scrollWidth = computed(() => containerRef.value.scrollWidth);
const screenWidth = computed(() => containerRef.value.offsetWidth);
  • scrollWidth:容器总宽度(所有页面之和)。
  • screenWidth:当前视口宽度,用于计算「滚动到第几屏」。

3.3 工具方法:平滑滚动

const scrollTo = (diffX) => {
  containerRef.value.scrollTo({
    left: diffX,
    behavior: "smooth",
  });
};

浏览器原生 scrollTo + behavior: smooth 轻松实现平滑动画。

3.4 触摸事件三步曲

① touchstart:记录起点
const touchstart = (e) => {
  if (!can_scroll.value) return; // 动画期间禁止再次滑动
  startX.value = e.changedTouches[0].clientX;
};
② touchmove:跟手拖动
const touchMove = (e) => {
  const currentX = e.changedTouches[0].clientX;
  const disX = currentX - startX.value;
  moveX.value = containerRef.value.scrollLeft - disX; // 反向移动实现跟手
  scrollTo(moveX.value);
};
  • 这里把「手指移动量」反向叠加到容器的 scrollLeft,实现“跟手”体验。
  • 注意 touch-action: none 在样式里已经禁止了浏览器默认滚动,避免冲突。
③ touchend:判断滑动方向 & 翻页
const touchend = (e) => {
  const endX = e.changedTouches[0].clientX;
  moveX.value = endX - startX.value;
  const dis_area = 100; // 阈值:超过 100px 才翻页

  if (Math.abs(moveX.value) > dis_area) {
    if (moveX.value < 0) {
      // 向左滑:下一页
      if (currentIndex.value >= props.pages.length - 1) {
        currentIndex.value = 0; // 循环回第一页
      } else {
        currentIndex.value += 1;
      }
    } else if (moveX.value > 0) {
      // 向右滑:上一页
      if (currentIndex.value === 0) {
        currentIndex.value = props.pages.length - 1; // 循环到最后一页
      } else {
        currentIndex.value -= 1;
      }
    }
  }
  // 兜底:滚动到当前索引所在位置
  scrollTo(screenWidth.value * currentIndex.value);
};
  • 阈值 dis_area 可自行调整,数字越小越灵敏。
  • 支持首尾循环:当在第一页继续往右滑,会跳到最后一页,反之亦然。

3.5 样式:隐藏滚动条 & 禁止系统默认行为

.scroll-container {
  height: 100vh;
  width: 100vw;
  display: flex;
  overflow-x: auto;
  scroll-behavior: auto;
  -webkit-overflow-scrolling: touch;
  /* 隐藏滚动条 */
  scrollbar-width: none;
  -ms-overflow-style: none;
  touch-action: none; /* 禁用浏览器默认手势 */
}
.scroll-container::-webkit-scrollbar {
  display: none;
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
  flex-shrink: 0;
}
  • touch-action: none 是关键,否则 iOS/Android 会在横滑时触发返回手势或刷新。
  • 隐藏滚动条后,用户视觉上完全看不出「滚动」,只感知到「滑动切屏」。

4. 使用姿势

  1. 在任意父组件里:
import swper from "@/component/swper.vue";
const pageList = [
  { component: Page1, props: {...} },
  { component: Page2, props: {...} },
  ...
];
  1. 模板:
<swper :pages="pageList" />
  1. 完。

最后附上完整代码: 父组件

<template>
  <div class="">
    <swper :pages="pageList"></swper>
  </div>
</template>

<script setup lang="ts">
import swper from "./component/swper.vue";
import APage from "./A.vue";
import BPage from "./B.vue";
import CPage from "./C.vue";
// 动态页面配置
const pageList = [
  { component: APage, props: { title: "主页", isLoaded: false } },
  { component: BPage, props: { title: "推荐", isLoaded: false } },
  { component: CPage, props: { title: "我的", isLoaded: false } },
];
</script>

<style scoped lang="scss"></style>

子组件:(也就是我们的轮播图组件)

<template>
  <div
    class="scroll-container"
    ref="containerRef"
    @touchstart="touchstart"
    @touchmove="touchMove"
    @touchend="touchend"
  >
    <!-- 页面内容 -->
    <div v-for="(page, index) in pages" :key="index" class="page">
      <!-- :style="{ backgroundColor: page.color }" -->
      <!-- {{ page.name }} -->
      <component :is="page.component" v-bind="page.props" />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, computed } from "vue";

// const pages = [
//   { color: "#ff7675", name: "屏幕 1", id: 0 },
//   { color: "#74b9ff", name: "屏幕 2", id: 1 },
//   { color: "#55efc4", name: "屏幕 3", id: 2 },
//   { color: "#a29bfe", name: "屏幕 4", id: 3 },
//   { color: "#ffcb20", name: "屏幕 5", id: 4 },
// ];
// 接收父组件传入的页面列表
const props = defineProps({
  pages: {
    type: Array,
    required: true,
    default: () => [],
  },
});

const containerRef = ref(null);
const currentIndex = ref(0); // 当前页面索引
const startX = ref(0); // 触摸开始时的X坐标
const moveX = ref(0); // 触摸移动时的X坐标
const can_scroll = ref(true); // 控制是否可以滚动

// 计算滚动容器的宽度
const scrollWidth = computed(() => {
  return containerRef.value.scrollWidth;
});
// 计算屏幕宽度
const screenWidth = computed(() => {
  return containerRef.value.offsetWidth;
});
// 滚动方法
const scrollTo = (diffX) => {
  containerRef.value.scrollTo({
    left: diffX,
    behavior: "smooth",
  });
};
const touchstart = (e) => {
  if (!can_scroll.value) {
    return; // 如果正在滚动,阻止触摸事件
  }
  startX.value = e.changedTouches[0].clientX;
};
const touchMove = (e) => {
  const currentX = e.changedTouches[0].clientX;
  const disX = currentX - startX.value;
  moveX.value = containerRef.value.scrollLeft - disX; // 反向移动实现跟手
  scrollTo(moveX.value);
};
const touchend = (e) => {
  const endX = e.changedTouches[0].clientX;
  moveX.value = endX - startX.value;
  const dis_area = 100; // 滑动距离阈值
  if (Math.abs(moveX.value) > dis_area) {
    if (moveX.value < 0) {
      // 滑动至下一页
      if (currentIndex.value >= props.pages.length - 1) {
        // 如果当前是最后一页,滚动到第一页
        currentIndex.value = 0;
        scrollTo(screenWidth.value * currentIndex.value);
      } else {
        // 否则滚动到下一页
        currentIndex.value = currentIndex.value + 1;
        scrollTo(screenWidth.value * currentIndex.value);
      }
    } else if (moveX.value > 0) {
      // 滑动至上一页
      if (currentIndex.value == 0) {
        // 如果当前是第一页,滚动到最后一页
        currentIndex.value = props.pages.length - 1;
        scrollTo(screenWidth.value * currentIndex.value);
      } else {
        // 否则滚动到上一页
        currentIndex.value = currentIndex.value - 1;
        scrollTo(screenWidth.value * currentIndex.value);
      }
    }
  }
  scrollTo(screenWidth.value * currentIndex.value);
};
</script>

<style scoped>
.scroll-container {
  height: 100vh;
  width: 100vw;
  display: flex;
  overflow-x: auto;
  scroll-behavior: auto; /* 添加平滑滚动效果 */
  /* scroll-snap-type: x mandatory; */
  -webkit-overflow-scrolling: touch; /* 启用平滑滚动 */
  /* 使用浏览器私有伪元素隐藏滚动条​ */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
  touch-action: none; /* 禁止触摸动作 */
}
/* WebKit 浏览器:隐藏滚动条但保留滚动功能 */
.scroll-container::-webkit-scrollbar {
  display: none; /* Chrome/Safari/Opera */
}
.page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: 700;
  color: aliceblue;
  flex-shrink: 0; /* 添加这行防止页面被压缩 */
  /* scroll-snap-align: start; */
}
</style>

至此,一个零依赖、可复用、支持无限循环的移动端全屏轮播图组件就封装完毕,Enjoy😄!

在这里插入图片描述