不引入 swipe 插件,使用vue自带组件实现swipe滑动切换页面功能
- 需求场景
- 1. 引入组件
- 2. 动态加载页面组件
- 3. 使用component组件
- 4. 组件属性及相关事件
- 5. 触摸事件处理
- 6. 动画和过渡控制
- 7. 节流功能
- 完整代码
需求场景
不引入 swipe 插件,使用vue自带component组件实现H5滑动切换页面
使用Vue的component组件来动态加载和渲染不同的页面组件。以下是详细分析该文件的引入方式、component的功能,以及它们如何协同工作以实现页面切换效果。
1. 引入组件
require.context: 这是Webpack提供的一个特性,用于动态引入模块。它允许你在指定目录中动态加载符合条件的文件。
…/components: 指定要搜索的目录。
false: 表示不搜索子目录。
/page_\d+.vue$/: 正则表达式,用于匹配文件名,确保只引入以 (page_007) page_开头并以.vue结尾的文件。
javascript">const pagesContext = require.context('../components', false, /page_\d+\.vue$/);
2. 动态加载页面组件
loadPages: 该方法用于加载所有符合条件的页面组件。
pagesContext.keys(): 返回该目录下所有匹配的文件名数组。
map(): 遍历这些文件名,提取页面编号并创建一个包含组件加载函数的对象。
import(): 动态导入组件,返回一个Promise,成功时返回组件,失败时捕获错误并返回null。
sort(): 根据页面编号对组件进行排序。
map(): 最后返回一个只包含组件加载函数的数组。
javascript">loadPages() { this.pages = pagesContext.keys() .map(key => { const pageNumber = parseInt(key.match(/\d+/)[0], 10); return { pageNumber, component: () => import(`../components/${key.substring(2)}`).catch(err => { console.error(`Error loading component ${key}:`, err); return null; }) }; }) .sort((a, b) => a.pageNumber - b.pageNumber) .map(page => page.component);
}
3. 使用component组件
component: Vue提供的内置组件,用于动态渲染不同的组件。
v-if: 控制组件的渲染条件,只有在isTransitioning为false时才渲染当前页面。
: key: 为动态组件提供唯一的键值,以便Vue能够高效地更新和重用组件。
:is: 指定要渲染的组件,这里使用pages[currentPageIndex],动态获取当前页面的组件。
:style: 动态设置样式,控制组件的透明度和缩放效果。
javascript"><component v-if="!isTransitioning" :key="`P_${currentPageIndex + 1}`" :is="pages[currentPageIndex]" class="page" :style="{ opacity: 1, transform: `scale(${currentScale})` }" :class="[`P_${currentPageIndex + 1}`]" />
4. 组件属性及相关事件
transition: Vue的过渡组件,用于为进入和离开的元素提供过渡效果。通过绑定事件,可以定义过渡的具体行为。
@before-enter: 在元素进入前调用的钩子,可以用于设置初始状态。
@enter: 元素进入时调用的钩子,可以用于设置动画效果。
@leave: 元素离开时调用的钩子,设置离开动画。
@before-leave: 元素离开前调用的钩子。
@after-leave: 元素离开后调用的钩子,通常用于重置状态。
javascript"><transition @before-enter="beforeEnter" @enter="enter" @leave="leave" @before-leave="beforeLeave" @after-leave="afterLeave">
5. 触摸事件处理
通过@touchstart、@touchmove.prevent和@touchend事件处理,实现了页面的滑动切换。
在touchStart、touchMove和touchEnd方法中,记录触摸的起始和结束位置,计算滑动距离,并根据滑动方向和距离决定是否切换页面。
touchStart(event):
记录触摸开始时的Y坐标。
在页面开始触摸时,检查是否需要展示提示信息(例如this.report),并通过nextTick()确保在DOM更新后执行特定的类移除操作。
touchMove(event):
记录触摸移动时的Y坐标,并计算出滑动的距离。
根据滑动的方向(向上或向下)和当前页面索引,更新当前页面的透明度和缩放,该过程通过以下条件判断:
向下滑动(distance < 0)时,若当前不是第一页,更新currentOpacity和currentScale,使页面在下滑时逐渐透明和放大。
向上滑动(distance > 0)时,若当前不是最后一页,更新currentOpacity和currentScale,使页面在上滑时逐渐透明和放大。
touchEnd(event):
计算触摸结束时的滑动距离。
根据滑动的距离和预设的阈值决定是否切换页面:
如果向上滑动且距离超过阈值且当前页面不是最后一页,触发切换到下一页的方法throttledNextPage。
如果向下滑动且距离超过阈值且当前页面不是第一页,触发切换到上一页的方法throttledPreviousPage。
如果没有滑动足够远,则重置页面的透明度和缩放,恢复为完全不透明和正常大小。
6. 动画和过渡控制
beforeEnter(el): 设置进入前的状态(如透明度为0,缩放为0.8),为进入动画做准备。
enter(el, done): 在元素进入时触发,应用CSS属性来完成动画,并调用done()表示动画结束。
leave(el, done): 在元素离开时触发,设置退出的动画效果(如透明度为0,缩放为0.8),并在动画结束后调用done()。
beforeLeave(): 在元素离开前添加动画类名,确保离开效果的应用。
afterLeave(): 完成过渡后重置状态(如重置isTransitioning、currentOpacity、currentScale),并在下一帧中添加新的进入动画类名,使下一个页面进入时使用。
7. 节流功能
throttle(fn, delay): 用于限制方法调用的频率,防止在短时间内多次调用导致的性能问题。返回一个新的函数,只有在指定的时间间隔(delay)后才能再次执行,使得nextPage和previousPage的调用得以被节流处理。
完整代码
javascript"><template><div class="swipe-container" @touchstart="touchStart"@touchmove.prevent="touchMove" @touchend="touchEnd"><transition @before-enter="beforeEnter" @enter="enter" @leave="leave" @before-leave="beforeLeave"@after-leave="afterLeave"><component v-if="!isTransitioning" :key="`P_${currentPageIndex + 1}`" :is="pages[currentPageIndex]" class="page":style="{ opacity: currentOpacity, transform: `scale(${currentScale})` }" :class="[`P_${currentPageIndex + 1}`]" /></transition></div>
</template><script>
const pagesContext = require.context('../components', false, /page_\d+\.vue$/); // 从 components 文件夹中引入所有 page_xx.vue 文件
import 'animate.css'
import { mapState } from 'vuex'export default {data() {return {currentPageIndex: 0, // 当前页面索引 startY: 0, // 触摸开始的 Y 坐标 endY: 0, // 触摸结束的 Y 坐标 isTransitioning: false, // 用于控制是否正在过渡 currentOpacity: 1, // 当前页面透明度 -- 控制页面透明度pages: [], // 页面内容 throttleDelay: 120, // 设置节流的延迟时间 currentScale: 1, // 初始化为 1,表示正常大小 --- transform: `scale(${currentScale})`threshold: 500, // 阈值};},computed: {...mapState('user', ['report']),},methods: {loadPages() {// 使用 reduce() 方法来创建页数组,同时提取页码 this.pages = pagesContext.keys().map(key => {const pageNumber = parseInt(key.match(/\d+/)[0], 10); // 提取数字部分 return {pageNumber,component: () => import(`../components/${key.substring(2)}`).catch(err => {console.error(`Error loading component ${key}:`, err);return null; // 返回 null 或一个默认页面组件 })};}).sort((a, b) => a.pageNumber - b.pageNumber) // 按页码排序 .map(page => page.component); // 只返回组件 },touchStart(event) {if (this.report?.code) {this.$toast(this.report?.message)return}const touch = event.touches[0];this.startY = touch.clientY; // 记录触摸开始的 Y 坐标 this.$nextTick(() => {const el = document.querySelector(`.P_${this.currentPageIndex + 1}`)if (!el) returnel.classList.remove('animate__animated', 'animate__fadeIn');})},touchMove(event) {if (this.report?.code) {this.$toast(this.report?.message)return}const touch = event.touches[0];this.endY = touch.clientY; // 记录触摸结束的 Y 坐标 const distance = this.startY - this.endY; // 计算滑动距离 // 更新透明度 this.currentOpacityif (this.currentPageIndex > 0 && distance < 0) {// 仅在不是第一页或正在向下滑动时 this.currentOpacity = Math.max(0, 1 - Math.abs(distance) / this.threshold);this.currentScale = Math.min(1.5, 1 + Math.abs(distance) / this.threshold); // 最大放大至 1.5 } else if (this.currentPageIndex === 0 && distance < 0) {// 当在第一页向下滑动也保持变化 this.currentOpacity = Math.max(0, 1 - Math.abs(distance) / this.threshold);this.currentScale = Math.max(.95, 1 - Math.abs(distance) / this.threshold); // 最小保持为 1 }if (this.currentPageIndex < this.pages.length - 1 && distance > 0) {// 仅在不是最后一页或正在向上滑动时 this.currentOpacity = Math.max(0, 1 - Math.abs(distance) / this.threshold);this.currentScale = Math.min(1.5, 1 + Math.abs(distance) / this.threshold); // 最大放大至 1.5 } else if (this.currentPageIndex === this.pages.length - 1 && distance > 0) {// 当在最后一页向上滑动也保持变化 this.currentOpacity = Math.max(0, 1 - Math.abs(distance) / this.threshold);this.currentScale = Math.max(.95, 1 - Math.abs(distance) / this.threshold); // 最小保持为 1 }},touchEnd(event) {const distance = this.startY - this.endY; // 计算滑动距离 const threshold = 30; // 控制滑动触发的阈值 // 判断滑动方向并决定是否切换页面 if (this.endY > 0 && distance > threshold && this.currentPageIndex < this.pages.length - 1) {// 向上滑动,切换到下一页,确保不是最后一页 this.throttledNextPage();} else if (distance < -threshold && this.currentPageIndex > 0) {// 向下滑动,切换到上一页,确保不是第一页 this.throttledPreviousPage();} else {// 如果没有滑动足够远,恢复透明度 this.currentOpacity = 1; // 恢复为完全不透明 this.currentScale = 1; // 恢复为正常大小 }this.startY = this.endY = 0; // 初始化,避免遗留值 而导致点击时切换页面},nextPage() {if (this.currentPageIndex < this.pages.length - 1) {this.isTransitioning = true; // 开始过渡 this.currentPageIndex++; // 切换到下一页 }},previousPage() {if (this.currentPageIndex > 0) {this.isTransitioning = true; // 开始过渡 this.currentPageIndex--; // 切换到上一页 }},beforeEnter(el) {// 获取子元素 const t_El = el.querySelector('.transform_');// if (t_El) {// console.log("找到的子元素:", t_El);// // t_El.style.opacity = 0;// // t_El.style.transform = 'scale(0.8)';// } el.style.opacity = 0;el.style.transform = 'scale(0.8)';},enter(el, done) {el.offsetHeight; // 触发重排 el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';el.style.opacity = 1;el.style.transform = 'scale(1)';done();},leave(el, done) {el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';el.style.opacity = 0;el.style.transform = 'scale(0.8)';done();},beforeLeave() {const el = document.querySelector(`.P_${this.currentPageIndex + 1}`);if (el) {el.classList.add('animate__animated', 'animate__fadeOut');}},afterLeave() {this.isTransitioning = false; // 过渡完成后,重置状态 this.currentOpacity = 1; // 重置透明度 this.currentScale = 1; // 确保过渡完成后重置缩放 this.$nextTick(() => {const el = document.querySelector(`.P_${this.currentPageIndex + 1}`)if (!el) returnel.classList.add('animate__animated', 'animate__fadeIn');})},// 加个节流 throttle(fn, delay) {let lastTime = 0;return function (...args) {const currentTime = Date.now();if (currentTime - lastTime >= delay) {lastTime = currentTime;fn.apply(this, args);}};},},created() {// 动态加载页面组件 this.loadPages();// 使用节流函数 this.throttledNextPage = this.throttle(this.nextPage, this.throttleDelay);this.throttledPreviousPage = this.throttle(this.previousPage, this.throttleDelay);}
};
</script>
<style lang="scss">
@import '../css/index.scss';
</style>
<style lang="scss" scoped>
.swipe-container {position: relative;width: 100%;height: 100vh;overflow: hidden;
}.page_ {background: #fff;
}.page {position: absolute;width: 100%;height: 100%;transition: opacity 0.5s ease, transform 0.5s ease;}
</style>