当前位置: 首页 > article >正文

不用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>

http://www.kler.cn/a/456013.html

相关文章:

  • 【Halcon】例程讲解:基于形状匹配与OCR的多图像处理(附图像、程序下载链接)
  • 3.若依前端项目拉取、部署、访问
  • StableSR: Exploiting Diffusion Prior for Real-World Image Super-Resolution
  • jpeg文件学习
  • SpringCloudAlibaba实战入门之路由网关Gateway断言(十二)
  • 怎么把多个PDF合并到一起-免费实用PDF编辑处理工具分享
  • Passlib库介绍及使用指南
  • 计算机组成——Cache
  • 解决gitcode 单文件上传大小10M的问题及清理缓存区
  • 探究音频丢字位置和丢字时间对pesq分数的影响
  • html+css+js网页设计 美食 美拾9个页面
  • 30天面试打卡计划 2024-12-25 26 27 面试题
  • 渗透测试常用专业术语(二)
  • 硬件开发笔记(三十二):TPS54331电源设计(五):原理图BOM表导出、元器件封装核对
  • 改进爬山算法之一:随机化爬山法(Stochastic Hill Climbing,SHC)
  • LeetCode-字符串转换整数(008)
  • 华为配置命令
  • python大数据国内旅游景点的数据爬虫与可视化分析
  • 考研互学互助系统|Java|SSM|VUE| 前后端分离
  • 使用云计算开发App 有哪些坑需要避免