不用swipe插件,用<component>组件实现H5的swipe切换
不引入 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结尾的文件。
const pagesContext = require.context('../components', false, /page_\d+\.vue$/);
2. 动态加载页面组件
loadPages: 该方法用于加载所有符合条件的页面组件。
pagesContext.keys(): 返回该目录下所有匹配的文件名数组。
map(): 遍历这些文件名,提取页面编号并创建一个包含组件加载函数的对象。
import(): 动态导入组件,返回一个Promise,成功时返回组件,失败时捕获错误并返回null。
sort(): 根据页面编号对组件进行排序。
map(): 最后返回一个只包含组件加载函数的数组。
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: 动态设置样式,控制组件的透明度和缩放效果。
<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: 元素离开后调用的钩子,通常用于重置状态。
<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的调用得以被节流处理。
完整代码
<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) return
el.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.currentOpacity
if (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) return
el.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>