vue简单实现书本翻页效果

简介: 偶然兴起,想要用vue来做一个书本的组件,有了这个想法后边开始动手,先简单地实现基本的效果,为后续封装为组件进行准备工作,实现该效果主要使用vue + css + JavaScript。

说在前面

偶然兴起,想要用vue来做一个书本的组件,有了这个想法后边开始动手,先简单地实现基本的效果,为后续封装为组件进行准备工作,实现该效果主要使用vue + css + JavaScript。

关键字

transform

Transform属性应用于元素的2D或3D转换。这个属性允许你将元素旋转,缩放,移动,倾斜等。语法为transform: none|*transform-functions*;
我们主要使用到其旋转效果,我们可以这样写。

transform: rotateY(90deg)
//表示沿Y轴旋转90度

animation

既然是要实现动画效果,那么肯定少不了animation的出场了,animation属性的语法为:`
animation: name duration timing-function delay iteration-count direction fill-mode play-state;`我们需要用到的只是前两个属性,name和duration,分别为指定要绑定到选择器的关键帧的名称动画指定需要多少秒或毫秒完成
我们可以这样写

animation: fanPre 2s;

@keyframes

使用@keyframes规则,你可以创建动画,创建动画是通过逐步改变从一个CSS样式设定到另一个,在动画过程中,可以更改CSS样式的设定多次,指定的变化时发生时使用%,或关键字"from"和"to",这是和0%到100%相同。语法为:@keyframes *animationname* {*keyframes-selector* {*css-styles;}* }
我们可以这样写

@keyframes fanPre {
  0% {
    transform: rotateY(0deg);
    background-color: rgba(122, 112, 112);
  }
  50% {
    background-color: rgba(122, 112, 112);
  }
  75% {
    background-color: rgba(122, 112, 112);
  }
  100% {
    transform: rotateY(-140deg);
    background-color: none;
  }
}

var

此var并不是JavaScript中的var而是css中的var,我们可以使用其来实现css与vue数据继续数据交换,及css中可以使用vue定义的data来进行属性设置,具体如下:

//html
<div :style="{ '--speed': speed }"></div>
//javascript
props: {
    speed: {
      type: String,
      default: "2s",
    }
}
//css
<style vars="{ speed, degs }" lang="scss" scoped>
    animation: fanPre var(--speed);
</style>

实现

知道了上面这几个关键词之后,我们便可以开始着手实现该效果了,首先我们需要一个书本页面列表数据

//书本页面列表
    pagesList: {
      type: Array,
      default: () => {
        return [
          {
            title: "关雎",
            text: [
              "关关雎鸠,在河之洲。窈窕淑女,君子好逑。",
              "参差荇菜,左右流之。窈窕淑女,寤寐求之。",
              "求之不得,寤寐思服。悠哉悠哉,辗转反侧。",
              "参差荇菜,左右采之。窈窕淑女,琴瑟友之。",
              "参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。",
            ],
          },
          {
            title: "声声慢·寻寻觅觅",
            text: [
              "寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍暖还寒时候,最难将息。三杯两盏淡酒,怎敌他、晚来风急!雁过也,正伤心,却是旧时相识。",
              "满地黄花堆积,憔悴损,如今有谁堪摘?守着窗儿,独自怎生得黑!梧桐更兼细雨,到黄昏、点点滴滴。这次第,怎一个愁字了得!",
            ],
          },
          {
            title: "青玉案·元夕",
            text: [
              "东风夜放花千树。更吹落、星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。",
              "蛾儿雪柳黄金缕。笑语盈盈暗香去。众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。",
            ],
          },
          {
            title: "蝶恋花·伫倚危楼风细细",
            text: [
              "伫倚危楼风细细,望极春愁,黯黯生天际。草色烟光残照里,无言谁会凭阑意。",
              "拟把疏狂图一醉,对酒当歌,强乐还无味。衣带渐宽终不悔,为伊消得人憔悴。",
            ],
          },
          {
            title: "雨霖铃·秋别",
            text: [
              "寒蝉凄切,对长亭晚,骤雨初歇。都门帐饮无绪,留恋处,兰舟催发。执手相看泪眼,竟无语凝噎。念去去,千里烟波,暮霭沉沉楚天阔。",
              "多情自古伤离别,更那堪,冷落清秋节!今宵酒醒何处?杨柳岸,晓风残月。此去经年,应是良辰好景虚设。便纵有千种风情,更与何人说",
            ],
          },
        ];
      },
    },

将数据渲染到页面上,如下例子

<div
    @click="turnPage(1)"
    class="j-book-page"
    :key="'page-now-' + index"
    :style="getPageStyle(index)"
>
    <h3>{{ nowPage.title }}</h3>
    <p
      v-for="(t, nowPageInd) in nowPage.text"
      :key="'nowPage-' + nowPageInd"
    >
      {{ t }}
    </p>
</div>

页面翻页功能实现如下,使用currentPage来记录当前展示页面页数,使用flag来区分是上一页还是下一页翻页,并进行相应的翻页操作。其中要注意对点击事件进行防抖操作,防止翻页过快,具体代码如下:

    turnPage(flag) {
      if (!this.canTurn) return;
      if (this.currentPage <= this.pagesList.length)
        this.setPage(this.currentPage, flag);
      if (this.currentPage < this.pagesList.length && this.currentPage > 0) {
        this.canTurn = false;
        setTimeout(() => {
          this.canTurn = true;
        }, parseInt(this.speed) * 1000 - 100);
      }
      if (flag === 1) {
        if (this.currentPage < this.pagesList.length) {
          this.currentPage++;
          this.nextClick = true;
          this.lastClick = false;
        }
      } else {
        if (this.currentPage > 0) {
          this.currentPage--;
          this.nextClick = false;
          this.lastClick = true;
        }
      }
    },

完整代码

<template>
  <div class="j-book" :style="getBookStyle()">
    <div :style="{ '--speed': speed, '--degs': degs }">
      <div
        class="j-book-page-pre"
        @click="turnPage(-1)"
        v-if="currentPage > 0 && showCover"
      >
        <div class="j-book-page">
          <h3>{{ prePage.title }}</h3>
          <p v-for="(t, textInd) in prePage.text" :key="'prePage-' + textInd">
            {{ t }}
          </p>
        </div>
      </div>
      <div class="j-book-pages">
        <template v-for="(item, index) in pagesList">
          <div
            @click="turnPage(-1)"
            class="j-book-page turn-page-ani"
            v-if="currentPage === index + 2 && nextClick"
            :key="'page-last--' + index"
            :style="getPageStyle(index)"
          >
            <h3>{{ item.title }}</h3>
            <p v-for="(t, itemInd) in item.text" :key="'item-' + itemInd">
              {{ t }}
            </p>
          </div>
          <div
            @click="turnPage(-1)"
            class="j-book-page turn-page-ani"
            v-if="currentPage === 1 && nextClick"
            :key="'page-last-' + index"
            :style="getPageStyle(index)"
          >
            <h3>{{ cover.title }}</h3>
            <p v-for="(t, coverInd) in cover.text" :key="'cover-' + coverInd">
              {{ t }}
            </p>
          </div>
          <div
            @click="turnPage(1)"
            class="j-book-page turn-page-pre-ani"
            v-if="lastClick && currentPage === 0"
            :key="'page-pre-currentPage' + index"
            :style="getPageStyle(5)"
          >
            <h3>{{ cover.title }}</h3>
            <p v-for="(t, coverInd) in cover.text" :key="'cover-0-' + coverInd">
              {{ t }}
            </p>
          </div>
          <div
            @click="turnPage(1)"
            class="j-book-page turn-page-pre-ani"
            v-if="lastClick && currentPage === index + 1"
            :key="'page-pre-' + index"
            :style="getPageStyle(5)"
          >
            <h3>{{ item.title }}</h3>
            <p v-for="(t, itemInd) in item.text" :key="'item-0-' + itemInd">
              {{ t }}
            </p>
          </div>
          <div
            @click="turnPage(1)"
            class="j-book-page"
            :key="'page-now-' + index"
            :style="getPageStyle(index)"
          >
            <h3>{{ nowPage.title }}</h3>
            <p
              v-for="(t, nowPageInd) in nowPage.text"
              :key="'nowPage-' + nowPageInd"
            >
              {{ t }}
            </p>
          </div>
        </template>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "book",
  props: {
    width: {
      type: Number,
      default: 300,
    },
    height: {
      type: Number,
      default: 400,
    },
    speed: {
      type: String,
      default: "2s",
    },
    //书本页面列表
    pagesList: {
      type: Array,
      default: () => {
        return [
          {
            title: "关雎",
            text: [
              "关关雎鸠,在河之洲。窈窕淑女,君子好逑。",
              "参差荇菜,左右流之。窈窕淑女,寤寐求之。",
              "求之不得,寤寐思服。悠哉悠哉,辗转反侧。",
              "参差荇菜,左右采之。窈窕淑女,琴瑟友之。",
              "参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。",
            ],
          },
          {
            title: "声声慢·寻寻觅觅",
            text: [
              "寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍暖还寒时候,最难将息。三杯两盏淡酒,怎敌他、晚来风急!雁过也,正伤心,却是旧时相识。",
              "满地黄花堆积,憔悴损,如今有谁堪摘?守着窗儿,独自怎生得黑!梧桐更兼细雨,到黄昏、点点滴滴。这次第,怎一个愁字了得!",
            ],
          },
          {
            title: "青玉案·元夕",
            text: [
              "东风夜放花千树。更吹落、星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。",
              "蛾儿雪柳黄金缕。笑语盈盈暗香去。众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。",
            ],
          },
          {
            title: "蝶恋花·伫倚危楼风细细",
            text: [
              "伫倚危楼风细细,望极春愁,黯黯生天际。草色烟光残照里,无言谁会凭阑意。",
              "拟把疏狂图一醉,对酒当歌,强乐还无味。衣带渐宽终不悔,为伊消得人憔悴。",
            ],
          },
          {
            title: "雨霖铃·秋别",
            text: [
              "寒蝉凄切,对长亭晚,骤雨初歇。都门帐饮无绪,留恋处,兰舟催发。执手相看泪眼,竟无语凝噎。念去去,千里烟波,暮霭沉沉楚天阔。",
              "多情自古伤离别,更那堪,冷落清秋节!今宵酒醒何处?杨柳岸,晓风残月。此去经年,应是良辰好景虚设。便纵有千种风情,更与何人说",
            ],
          },
        ];
      },
    },
    //书本封面
    cover: {
      type: Object,
      default: () => {
        return {
          title: "封面",
          text: ["封面"],
        };
      },
    },
  },
  data() {
    return {
      currentPage: 0,
      nextClick: false,
      lastClick: false,
      prePage: {},
      nowPage: {},
      canTurn: true,
      degs: "0deg",
      showCover: false,
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      this.nowPage = this.cover;
    },
    getBookStyle() {
      let res = "";
      res += "width:" + this.width + "px;";
      res += "height:" + this.height + "px;";
      res += "'--speed':" + this.speed + ";";
      res += "transform: rotate(" + this.degs + ");";
      return res;
    },
    getPageStyle(index) {
      let res = "";
      res += "z-index:" + (this.pagesList.length - index) + ";";
      return res;
    },
    setPage(page, flag) {
      if (flag === -1) {
        this.prePage = this.pagesList[page - 3] || this.cover;
        this.nowPage = this.pagesList[page - 1];
      } else {
        this.prePage = this.pagesList[page - 2] || this.cover;
        this.nowPage =
          this.pagesList[page] || this.pagesList[this.pagesList.length - 1];
      }
      let speed = this.speed;
      speed = parseInt(speed) * 1000 - 500;
      setTimeout(() => {
        if (this.currentPage === 1) {
          this.showCover = true;
        }
        if (this.currentPage === 0) {
          this.showCover = false;
        }
        if (flag === -1) {
          this.nowPage = this.pagesList[this.currentPage - 1] || this.cover;
          if (this.currentPage === 0) {
            this.degs = "0deg";
          }
        } else {
          this.degs = "-5deg";
          this.prePage = this.pagesList[this.currentPage - 2] || this.cover;
        }
      }, speed);
    },
    turnPage(flag) {
      if (!this.canTurn) return;
      if (this.currentPage <= this.pagesList.length)
        this.setPage(this.currentPage, flag);
      if (this.currentPage < this.pagesList.length && this.currentPage > 0) {
        this.canTurn = false;
        setTimeout(() => {
          this.canTurn = true;
        }, parseInt(this.speed) * 1000 - 100);
      }
      if (flag === 1) {
        if (this.currentPage < this.pagesList.length) {
          this.currentPage++;
          this.nextClick = true;
          this.lastClick = false;
        }
      } else {
        if (this.currentPage > 0) {
          this.currentPage--;
          this.nextClick = false;
          this.lastClick = true;
        }
      }
    },
  },
};
</script>

<style vars="{ speed, degs }" lang="scss" scoped>
.j-book {
  background-color: gray;
  position: relative;
  box-shadow: 30px 0px 30px rgb(0, 0, 0, 0.6) inset;
  transform: rotate(var(--degs));
  color: #dec38f;
  .j-book-page-pre {
    position: absolute;
    width: 100%;
    height: 100%;
    z-index: 2;
    background-size: 100%;
    transform-origin: left;
    border-top-left-radius: 2px;
    border-bottom-left-radius: 2px;
    background-color: rgba(122, 112, 112);
    transform: rotateY(-140deg);
    .j-book-page {
      position: absolute;
      width: 100%;
      height: 100%;
    }
  }
  .j-book-pages {
    position: absolute;
    width: 100%;
    height: 100%;

    .turn-page-pre-ani {
      position: absolute;
      width: 100%;
      height: 100%;
      z-index: 2;
      background-size: 100%;
      transform-origin: left;
      border-top-left-radius: 2px;
      border-bottom-left-radius: 2px;
      transform: rotateY(0deg);
      animation: fanPre var(--speed);
    }
    @keyframes fanPre {
      0% {
        transform: rotateY(-140deg);
        background-color: rgba(122, 112, 112);
      }
      50% {
        background-color: rgba(122, 112, 112);
      }
      100% {
        transform: rotateY(0deg);
        background-color: none;
      }
    }
    .turn-page-ani {
      position: absolute;
      width: 100%;
      height: 100%;
      z-index: 2;
      background-size: 100%;
      transform-origin: left;
      border-top-left-radius: 2px;
      border-bottom-left-radius: 2px;
      transform: rotateY(-140deg);
      animation: fan var(--speed);
    }
    @keyframes fan {
      0% {
        transform: rotateY(0deg);
        background-color: none;
      }
      100% {
        transform: rotateY(-140deg);
        background-color: rgba(122, 112, 112);
      }
    }
    .j-book-page {
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      h3 {
        text-align: center;
      }
      p {
      }
    }
  }
  .j-book-btns {
    position: absolute;
    bottom: 0;
    display: flex;
    justify-content: space-around;
    width: 100%;
  }
}
</style>

说在后面

这次的代码写得比较赶,有些地方可能会有些多余的设置或操作,且命名比较随意,这是个不好的习惯,后面打算将其封装成一个组件,到时候会认真重新编写代码,给大家更好地阅读体验,感谢大家的观看。

目录
相关文章
|
10天前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
113 2
|
4月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
544 0
|
4月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
3月前
|
JavaScript 安全
在 Vue 中,如何在回调函数中正确使用 this?
在 Vue 中,如何在回调函数中正确使用 this?
108 0
|
3月前
|
人工智能 JSON JavaScript
VTJ.PRO 首发 MasterGo 设计智能识别引擎,秒级生成 Vue 代码
VTJ.PRO发布「AI MasterGo设计稿识别引擎」,成为全球首个支持解析MasterGo原生JSON文件并自动生成Vue组件的AI工具。通过双引擎架构,实现设计到代码全流程自动化,效率提升300%,助力企业降本增效,引领“设计即生产”新时代。
238 1
|
4月前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
366 17
|
4月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
211 1
|
4月前
|
JavaScript API 开发者
Vue框架中常见指令的应用概述。
通过以上的详细解析,你应该已经初窥Vue.js的指令的威力了。它们是Vue声明式编程模型的核心之一,无论是构建简单的静态网站还是复杂的单页面应用,你都会经常用到。记住,尽管Vue提供了大量预定义的指令,你还可以创建自定义指令以满足特定的需求。为你的Vue应用程序加上这些功能增强器,让编码变得更轻松、更愉快吧!
71 1
|
4月前
|
存储 JavaScript 前端开发
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
309 0
|
4月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
134 0