Code Monkey home page Code Monkey logo

vue-ebook's Introduction

vue-Ebook

基于Vue3.0制作的小说阅读器 webapp

一、阅读器开发

项目技术难点分析

一、阅读器开发:分页算法、全文搜索算法、引入Web字体、主题设置
二、离线存储缓存机制:LocalStorage + IndexedDB
三、实现各种复杂手势 + 交互动画、 如何兼容手势 + 鼠标操作
四、利用vuex + mixin 实现组件复用 + 解耦,大大精简代码量
五、利用es6 优雅的实现数据结构变化
六、科大讯飞api在线语音合成API开发

项目准备

* 字体准备
* 项目依赖包下载 + 项目配置
* 准备Web 字体
* viewport 配置
* rem 设置 + 自适应布局实现思路
* global.scss + reset.scss
* 引入Vuex

搭建静态资源服务器

* Nginx 安装
* 配置文件
* 常见问题及处理方法

需求分析

阅读器-> 解析分析 -> 字号 + 字体 -> 主题 -> 进度 -> 目录 -> 搜索 -> 书签 -> 页眉 + 页脚 -> 分页(难点)

技术难点

1.epub.js
2.vuex + mixin
3.vue-i18n 实现国际化
4.动态切换主题 + 书签操作手势

二、 书城首页、搜索页、列表页、详情页开发

需求分析

标题搜索 -> 随机推荐(难点) -> 猜你喜欢 -> 热门推荐 -> 精选 -> 分类推荐 -> 全部分类 -> 分类列表 -> 搜索页面 -> 详情页

三、 书架页面逻辑开发


1.书架标题组件布局实现
2.书架标题组件交互实现
3.书架搜索框布局实现
4.书架搜索框交互实现
5.书架图书列表布局
6.书架编辑模式开发
7.书架弹出框功能开发
8.电子书离线缓存功能开发
9.书架分类列表开发
10.书架修改分组功能开发

项目总结

vue使用

1.通过vuex同步组件状态(理解mapGetters、mapActions原理)
2.通过mixins混入机制大幅精简组件代码
3.组件插槽slot机制大幅提升组件的复用性
4.动态组件提升组件应用的灵活性
5.组件化API化大幅简化组件调用
6.通过动态路由加载组件

vue动画进阶

1.阅读器下拉添加书签(同时支持手势和鼠标操作)
2.阅读器目录加载时的精致动画、听书播放时的播放动画
3.阅读器标题+菜单+设置+按钮+目录多组件交互动画
4.推荐图书弹跳+翻转+烟花动画
5.书架更新时的列表位移动画(transition-group)
6.书城首页/书架的标题+搜索框交互动画

vue-ebook's People

Contributors

panpanxiong3 avatar

Stargazers

 avatar

Watchers

 avatar

vue-ebook's Issues

2019.12.24 更新内容

1. 新增目录组件 【EbookSlide.vue】目录浮出功能

创建动态组件【EbookSlideContents】

判断【menuVisible && settingVisible===3】显示目录组件,并且新增动画显示效果

2.Tab选项切换以及搜索效果样式显示

设置状态【currentTab】“1”:显示目录功能,“2”:显示书签功能
新增对应的css显示效果用于显示对应的搜索样式布局,未编写文本搜索功能

3.图书内容样式布局,显示对应的图书图片以及文本信息

//获取阅读器封面
        this.book.loaded.cover.then (cover => {
          this.book.archive.createUrl (cover).then (url => {
            this.setCover (url);
          })
        });
        //获取图书基本信息
        this.book.loaded.metadata.then (metadata => {
          this.setMetadata (metadata);
        });

通过以上方法实现图书文本信息的获取,并且通过样式布局,vuex的数据调取。对页面进行文本图片渲染,以及阅读进度的显示功能

4.目录结构开发

新增公共方法:flatten()
利用递归函数对回调的数组进行数据拆分,将多维数组转化为一维数组

export function flatten ( array ) {
  return [].concat(array.map (item => [].concat (item, ...flatten(item.subitems))));
}

再利用find()函数对数组进行等级【level】过滤,区分其何级数,并且设置vuex的【Navigation】进行保存渲染

 //获取图书列表信息
        this.book.loaded.navigation.then (nav => {
          const navItem = flatten (nav.toc);

          function find ( item, level = 0 ) {
            return !item.parent ? level : find (navItem.filter (parentItem => parentItem.id === item.parent)[ 0 ], ++level);
          }

          navItem.forEach (item => {
            item.level = find (item);
          });
          this.setNavigation(navItem);
        })

5.多级目录开发

新增公共组件【Scroll.vue】: 用于显示目录列表
新增公共方法【/utils/utils.js】:设置目录定位问题,以及下拉时显示目录功能

EbookSlideContents组件新增【Scroll.vue】组件
渲染vuex 【navigation】的值,进行目录渲染,并且设置vuex【section === index】判断目录章节位置,一致显示对应章节高亮

并且点击对应章节,通过mixins的公共函数【renditionDisplay】切换对应章节,以及【hideTitleAndMenu】隐藏目录组件

2019.12.17 更新内容

新增进度条组件

新增组件:【EbookSettingProgress.vue】
路径: /src/components/EbookSettingProgress.vue

配置显示所有页数,新增页数算法:

this.book.ready.then (() => { return this.book.locations.generate (750 * ( window.innerWidth / 375 ) * ( getFontSize (this.fileName / 16) )) }).then (locations => { this.setBookAvailable(true); })

【注:算法计算不准确,未算入图片样式以及特殊字体样式】

2019.12.13 更新内容

新增小说阅读器开发以及阅读器标题栏 和 阅读器菜单栏

技术点记录:

1,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性

getters.js文件配置:

const book = {
  fileNames: state => state.book.fileNames,
  menuVisitor: state => state.book.menuVisitor
};

export default book;

调用getters:

import { mapGetters } from "vuex";

//计算属性调用
computed: {
    ...mapGetters(['fileNames', 'menuVisitor'])//vuex插件
  }

2.mapActions组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)

Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。

**actions.js文件配置**

import { mapActions } from "vuex";
//函数调用
methods: {
    ...mapActions(['setFileName', 'setMenuVisitor'])
  }

3.mixin 分发可重用功能

mixin对象可以包含任何组件选项。当组件使用混入时,混入中的所有选项都将被“混入”到组件自己的选项中

minix 配置文件

import { mapGetters, mapActions } from "vuex";
export const eookMixin = {
  computed: {
    ...mapGetters(['fileNames', 'menuVisitor'])//vuex插件
  },
  methods: {
    ...mapActions(['setFileName', 'setMenuVisitor'])
  }
};

使用minix

import { eookMixin } from "../../utils/mixin";
export default {
   mixins: [ eookMixin ]
}

epub.js 使用

import Epub from 'epubjs' //引入epub.js
global.ePub = Epub;

initEpub() {
  // 渲染电子书 epub文件 ,read 渲染位置
   this.rendition = this.book.renderTo('read', {
              width: window.innerWidth,
              height: window.innerHeight,
              method: 'default'// 微信的兼容性,可以在微信上正常使用
            });
   this.rendition.display();

//电子书手势操作
this.rendition.on('touchstart', event => { //点击屏幕时
this.touchStartX = event.changedTouches[0].clientX; //点击的横向x轴位置
this.touchStartTime = event.timeStamp; //点击时间
}

 this.rendition.on('touchend', event => { //移开屏幕时
              event.stopPropagation();
              const offsetX = event.changedTouches[0].clientX - this.touchStartX; //移动范围
              const touchTime = event.timeStamp - this.touchStartTime; //点击时间范围
              //监听左右滑动事件,修改页面
              if(touchTime < 500 && offsetX > 40 ){ 
                this.prevPage();
              } else if (touchTime < 500 && offsetX < -40 ){
                this.nextPage();
              } else {
                this.showTitleAndMenu() //判断是否显示标题和菜单栏
              }
            });
            //阅读器的向前增加一页
            prevPage() {
            this.rendition.prev();
            this.hideTitleAndMenu();
          },
          //阅读器的向后增加一页
          nextPage() {
            this.rendition.next();
            this.hideTitleAndMenu();
          }, 
}

2019.01.09 更新内容

书架页面新增编辑功能

StoreShelf 页面加载完成,设置vuex 公共数据【ShelfList】,并渲染数据加载完成页面的数据渲染

判断页面是否为编辑模式

否:
1.点击书本直接跳入详情页面
设置公共跳转方法
export function gotoBookDetail(vue, book) { vue.$router.push({ path: '/store/detail', query: { fileName: book.fileName, category: book.categoryText } }) }

2.点击添加直接跳转至首页

export function gotoStoreHome(vue) {
  vue.$router.push({
    path: '/store/home'
 
``` })
}



### 新增底部编辑组件

新增组件【ShelfFooter.vue】组件

判断在编辑模式下显示,并且新增transition 动画属性,添加显示动画

2019.12.26 更新内容

书签功能实现 (页面下拉)

删除之前设置的【initGestart】即epubjs提供的api翻转阅读器的方法
在阅读器前添加一个蒙版,并且对蒙版进行手势监听操作

<div class="ebook-reader-mask" @click="onMaskClick" @touchmove="move" @touchend="moveEnd" @touchstart="moveStart"></div>

      move ( e ) {
        let offsetY = 0;
        if (this.firstOffsetY) {
          offsetY = e.changedTouches[ 0 ].clientY - this.firstOffsetY;
          this.setOffsetY (offsetY);
        } else {
          this.firstOffsetY = e.changedTouches[ 0 ].clientY;
        }
        e.preventDefault ();
        e.stopPropagation ();
      },
      moveStart ( e ) {
        this.touchStartX = e.changedTouches[ 0 ].clientX;
        this.touchStartTime = e.timeStamp;
      },
      moveEnd ( e ) {
        this.setOffsetY (0);
        this.firstOffsetY = 0;
        const offsetX = e.changedTouches[ 0 ].clientX - this.touchStartX;
        const touchTime = e.timeStamp - this.touchStartTime;
        if (touchTime < 500 && offsetX > 40) {
          this.prevPage ();
        } else if (touchTime < 500 && offsetX < -40) {
          this.nextPage ();
        }
      },
      onMaskClick ( e ) {
        const offsetX = e.offsetX;
        const width = window.innerWidth;
        if (offsetX > 0 && offsetX < width * 0.3) {
          this.prevPage ();
        } else if (offsetX > 0 && offsetX > width * 0.7) {
          this.nextPage ();
        } else {
          this.showTitleAndMenu ();
        }
      },

并且设置index.vue 的Dom元素未绝对定位,利用ref进行定位操作

2.书签组件开发

新增组件【EbookBookMark】,以及公共组件【BookMark】组件

设置组件定位也是为绝对定位,【watch】页面的高度下拉时显示

3.书签的添加删除交互

监听书签下拉的四组工作状态:

1.恢复状态
2.未到临界状态
3.超越临界状态
4.归位状态

通过判断状态条件【isFlexed】是否为true,判断是否添加书签
以下为四种状态源代码:

restore () {
        //归位:状态4
        setTimeout (() => {
          this.$refs.ebookBookmark.style.top = `${-this.height}px`;
          this.$refs.ebookBookmark.style.transform = `rotate(0deg)`;
        }, 200);
        if (this.isFlexed) {
          this.setIsBookmark (true);
          this.flexStyle.top=`${this.height}px`;
        } else {
          this.setIsBookmark (false);
        }
      },
      beforeHeight () {
        //恢复状态:状态1
        if (this.isBookmark) {
          this.text = this.$t ('book.pulldownDeleteMark');
          this.markColor = '#0070ff';
          this.isFlexed = true
        } else {
          this.text = this.$t ('book.pulldownAddMark');
          this.markColor = '#333333';
          this.isFlexed = false;
        }
      },
      beforeThreshold ( v ) {
        //未到达临界状态 ,
        this.$refs.ebookBookmark.style.top = `${-v}px`;
        this.beforeHeight ();
        this.flexStyle.top=`${this.height - 28}px`;
        const iconDown = this.$refs.iconDown;
        if (iconDown.style.transform === 'rotate(180deg)') {
          iconDown.style.transform = 'rotate(0deg)';
        }
      },
      afterThreshold ( v ) {
        //超越临界状态
        this.$refs.ebookBookmark.style.top = `${-v}px`;
        if (this.isBookmark) {
          this.text = this.$t ('book.releaseDeleteMark');
          this.markColor = '#333333';
          this.isFlexed = false;
          this.flexStyle.top=`${this.height - 28}px`;
        } else {
          this.text = this.$t ('book.releaseAddMark');
          this.markColor = '#0070ff';
          this.isFlexed = true;
          this.flexStyle.top=`${this.height}px`;
        }
        const iconDown = this.$refs.iconDown;
        if (iconDown.style.transform === '' || iconDown.style.transform === 'rotate(0deg)') {
          iconDown.style.transform = 'rotate(180deg)';
        }
      }

2020.01.18 更新内容

书架分类列表开发

新增页面StoreCategory 页面,即分类详情页面【/view/StoreCategory】

新增组件【ShelfGroupDialog.vue】

新增vuex管理属性:

shelfCategory: [], // 书架分类列表数据
currentType: 1 // 书架列表为1,书架分类列表为2

新增公共方法,利用分类名称,获取对应的分类存在的书籍,并且显示

getCategoryList ( title ) {
        const categoryList = this.shelfList.filter (book => book.type === 2 && book.title === title)[ 0 ]
        this.setShelfCategory (categoryList)
    },

书架分组功能优化

即进入分组页面时,显示标题的改变,以及返回标签的显示

修改【ShelfTitle.vue】组件

修改原先显示的属性,改为父级传递数值

props: {
      title: String,
      isShowBack: {
        type: Boolean,
        default: false
      }
    }

当页面为分类页面时,isShowBack为true,显示返回标签,标题为分类标题

2019.12.31 更新内容

弹出卡片新增动画效果

新增烟花效果的scss文件【falpCard.scss】

利用css的animation动画效果 遍历参数实现烟花效果

&.animation { @for $i from 1 to length($moves) { &:nth-child(#{$i}) { @include move($i); } } }

新增mock.js 实现替换XHR,实现数据交互

mock.js介绍:

  • 替换原生XHR,使用简洁
  • 丰富的数据类型
  • 无法支持bolb类型,无法实现模拟下载

bolb类型:二进制大对象,是一个存储在二进制的对象,在计算机当中,BLOB常常是数据库用来存储

利用新增【api】利用axios进行数据交互,实现动画卡片消失后,弹出推荐图书组件
推荐图书组件未完成

2019.01.08 更新内容

新增列表头部组件以及交互动画

新增组件【shelf-title / shelf-search / shelf-list 】三个组件

shelf-title:标题组件,设置状态判断是否固定定位
shelf-search:标题分类搜索组件,判断页面下拉时是否点击进行判断是否固定定位
shelf-list:书架列表组件

书架列表组件动态组件布局

书架组件存在三种状态,因此创建三种动态组件进行样式布局
【shelfItemImage \ shelfItemCategory \ shelfItemAdd】

判断数据【tyep】进行判断哪种类型的书架信息

computed: {
      item () {
        return this.data.type === 1 ? this.book : ( this.data.type === 2 ? this.category : this.add );
      }

2019.12.27 更新内容

1.书签功能实现

利用EpubCFi 提供的利用正则表达式【this.currentBook.rendition.currentLocation ()】进行拼接成新的字符串,对当前的书签保存在数组【this.bookmark】并且保存在location中。

难点:主要对书签定位【cfi】位置进行定位

代码如下:

      addBookMark () {
        this.bookmark = getBookmark (this.fileName);
        if ( !this.bookmark) {
          this.bookmark = [];
        }
        const currentLocation = this.currentBook.rendition.currentLocation ();
        const cfiBase = currentLocation.start.cfi.replace (/!.*/, '');
        const cfiStart = currentLocation.start.cfi.replace (/.*!/, '').replace (/\)$/, '');
        const cfiEnd = currentLocation.end.cfi.replace (/.*!/, '').replace (/\)$/, '');
        const cfiRange = `${cfiBase}!,${cfiStart},${cfiEnd})`;
        this.currentBook.getRange (cfiRange).then (range => {
          const text = range.toString ().replace (/\s\s/g, '');
          this.bookmark.map (item => {
            if (item.cfi === currentLocation.start.cfi) return;
          });
          this.bookmark.push ({
            cfi: currentLocation.start.cfi,
            text: text
          });
          saveBookmark (this.fileName, this.bookmark);
        })
      },

删除定位:下拉时获取当前的【cfi】定位,对【this.bookmark】进行过滤,删除对应的数据,再保存至location中

removeBookMark () { const currentLocation = this.currentBook.rendition.currentLocation (); const cfi = currentLocation.start.cfi; this.bookmark = getBookmark (this.fileName); if (this.bookmark) { saveBookmark (this.fileName, this.bookmark.filter (item => item.cfi !== cfi)); this.setIsBookmark (false); } },

2.目录书签组件

新增组件【EbookSlideBoolmaek.vue】,对【this.mark】进行循环遍历,显示对应的书签内容

3.页眉页脚功能

对页面新增对应的内边距,用于显示新的页眉页脚内容
新增两个组件【EbookHeader.vue】,【EbookFooter.vue】

获取vuex,mixins中的公共方法【getSectionName】,【this。progress】的数据,对当前的章节信息和进度百分比进行展示

2019.01.04 更新内容

搜索功能实现

点击页面图书类型时,根据路由信息对图书类型页面进行数据过滤,并且实现页面渲染

 getList() {
        list().then(response => {
          this.list = response.data.data
          this.total = response.data.total
          const category = this.$route.query.category
          const keyword = this.$route.query.keyword
          if (category) {
            // 如果用户传入了分类数据,则从结果中搜索相应的分类数据并进行展示
            const key = Object.keys(this.list).filter(item => item === category)[0]
            const data = this.list[key]
            this.list = {}
            this.list[key] = data
          } else if (keyword) {
            // 如果用户输入了关键字,则通过书名进行关键字匹配(搜索算法)
            Object.keys(this.list).filter(key => {
              this.list[key] = this.list[key].filter(book => book.fileName.indexOf(keyword) >= 0)
              return this.list[key].length > 0
            })
          }
        })
      }

新增书架页面

新增页面【StoreShelf.VUE】页面
新增组件【ShelfTitle.vue】组件

并且根据ui样式对页面顶部进行css布局

2019.12.23 更新内容

一、上下章切换功能实现

首先判断vuex的值【this.section】必须大于0并且小于当前电子书的最后章节

prevSection () {
        if (this.section > 0 && this.bookAvailable) {
          this.setSection (this.section - 1).then (() => {
            this.displaySection ();
          })
        }
      },
      nextSection () {
        if (this.section < this.currentBook.spine.length - 1 && this.bookAvailable) {
          this.setSection (this.section + 1).then (() => {
            this.displaySection ();
          })
        }

this.currentBook.spine.length - 1:【电子书最大的章节】
this.section > 0:【电子书章节大于0】

二、章节切换以及进度同步

mixin新增公共函数方法:【refreshLocation()】
利用epubjs的API方法获取章节进度,并且转换为百分比保存至vuex当中,在阅读器切换和左右切换时调用该方法,并且在公共函数里新增【saveLocation】方法,保存进度值location当中,使页面刷新时保存进度

refreshLocation () {
      const currentLocation = this.currentBook.rendition.currentLocation ();
      const startCfi = currentLocation.start.cfi;
      const progress = this.currentBook.locations.percentageFromCfi (startCfi);
      this.setProgress (Math.floor (progress * 100));
      this.setSection (currentLocation.start.index);
      saveLocation (this.fileName, startCfi);
    },

三、保存进度功能

localStorage.js新增两个方法【saveLocation,getLocation】;
修改【EbookReader.vue】的初始渲染方法:
首先判断location是否存在location:
存在:调整阅读器进度,并且渲染阅读器显示内容,调用【refreshLocation】方法

  renditionDisplay ( target, cb ) {
      if (target) {
        this.currentBook.rendition.display (target).then (() => {
          this.refreshLocation ();
          if (cb) cb ();
        })
      } else {
        this.currentBook.rendition.display ().then (() => {
          this.refreshLocation ();
          if (cb) cb ();
        })
      }
    },

阅读器初始加载方法修改:【EbookReader.vue】

initRendition () {
        this.rendition = this.book.renderTo ('read', {
          width: window.innerWidth,
          height: window.innerHeight,
          method: 'default'// 微信的兼容性,可以在微信上正常使用
        });
        const location = getLocation (this.fileName);
        this.renditionDisplay (location, () => {
          this.initFontSize ();
          this.initFontFamily ();
          this.initTheme ();
          this.initGlobStyle ();
        });

2019.01.10 更新内容

书架弹出框功能实现

新增createApi 插件

更好的利用公共组件,实现数据调用变换

import CreateAPI from 'vue-create-api';
import Vue from 'vue'
import Toast from "../components/common/Toast";
import Popup from "../components/common/Popup";

Vue.use (CreateAPI);
Vue.createAPI (Toast, true);
Vue.createAPI (Popup, true);
Vue.mixin ({
  methods: {
    toast ( setting ) {
      return this.$createToast ({
        $props: setting
      })
    },
    popup ( setting ) {
      return this.$createPopup ({
        $props: setting
      })
    },
    simpToast ( text ) {
      this.toast ({
        text: text
      }).show ();
    }
}
});

新增【私密阅读、 开启离线、移出书架】功能

利用vuex的【shelfList,shelfSelected】的数据判断用户是否选中私密阅读,以及下载状态,
监听用户点击事件,过滤渲染数据,并且显示页面

主要方法:

showPrivate () {
        this.popupMenu = this.popup ({
          title: this.isPrivate ? this.$t ('shelf.closePrivateTitle') : this.$t ('shelf.setPrivateTitle'),
          btn: [
            {
              text: this.isPrivate ? this.$t ('shelf.close') : this.$t ('shelf.open'),
              click: () => {
                this.setPrivate ()
              }
            },
            {
              text: this.$t ('shelf.close'),
              click: () => {
                this.toast ({text: '正在取消'}).show ();
                this.hidePopup ()
              }
            },
          ]
        }).show ()
      },
      showDownLoad () {
        this.popupMenu = this.popup ({
          title: this.isDownload ? this.$t ('shelf.removeDownloadTitle') : this.$t ('shelf.setDownloadTitle'),
          btn: [
            {
              text: this.isDownload ? this.$t ('shelf.delete') : this.$t ('shelf.open'),
              click: () => {
                this.setDownload ()
              }
            },
            {
              text: this.$t ('shelf.close'),
              click: () => {
                this.toast ({text: '正在取消'}).show ();
                this.hidePopup ()
              }
            },
          ]
        }).show ()
      },
      showRemove () {
        let title;
        if (this.shelfSelected.length === 1) {
          title = this.$t ('shelf.removeBookTitle').replace ('$1', `《${this.shelfSelected[ 0 ].title}》`)
        } else {
          title = this.$t ('shelf.removeBookTitle').replace ('$1', this.$t ('shelf.selectedBooks'))
        }
        this.popupMenu = this.popup ({
          title: title,
          btn: [
            {
              text: this.$t ('shelf.removeBook'),
              type: 'danger',
              click: () => {
                this.setRemoveSelect ()
              }
            },
            {
              text: this.$t ('shelf.close'),
              click: () => {
                this.toast ({text: '正在取消'}).show ();
                this.hidePopup ()
              }
            },
          ]
        }).show ()
      },

2019.01.05 更新内容

新增书架页面vuex数据

因为书架页面存在多个组件间的数据交互,因此采用vuex的数据交互形式,这样利于后期维护

/store/model/store.js

isEditMode: false,//是否进入编辑模式 shelfList: [], // 书架图书列表 shelfSelected: [], //书架图书选择状态 shelfTitleVisible: true, //书架标题显示状态 shelfCategory: [], // 书架分类列表数据 currentType: 1 // 书架列表为1,书架分类列表为2

新增书架页面的标题的逻辑交互

判断vuex 【shelfSelected】的数量,点击编辑的时候显示替换,判断是否选择书架图书是否为选中状态,根据数量进行显示替换

computed: {
      selectedText () {
        const selectedNumber = this.shelfSelected ? this.shelfSelected.length : '0';
        return selectedNumber <= 0 ? this.$t ('shelf.selectBook') : selectedNumber === 1 ? this.$t ('shelf.haveSelectedBook').replace ('$1', selectedNumber) : this.$t ('shelf.haveSelectedBooks').replace ('$1', selectedNumber);
      }
    },

2019.12.15 更新内容

字号字体设置离线缓存机制

利用vue插件:【web-storage-cache】
WebStorageCache 对HTML5 localStorage 和sessionStorage 进行了扩展,添加了超时时间,序列化方法。可以直接存储json对象,同时可以非常简单的进行超时时间的设置。
优化:WebStorageCache自动清除访问的过期数据,避免了过期数据的累积。另外也提供了清除全部过期数据的方法:wsCache.deleteAllExpires();

存储于公共文件 /src/unti/localstorage.js

字体设置标题国际化

利用vue 插件:【vue-i18n】
Vue I18n是Vue.js的国际化插件。它可以轻松地将一些本地化功能集成到您的Vue.js应用程序中
官方文档:http://kazupon.github.io/vue-i18n/

新增文件夹:/src/lang
新增文件: /src/lang/en.js /src/lang/cn.js
配置文件:/src/lang/index.js

配置内容:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import en from './en'
import cn from './cn'
import { getLocale, saveLocale } from '../utils/localStorage'

Vue.use(VueI18n);

const messages = {
  en, cn
};

let locale = getLocale();
if (!locale) {
  locale = 'en';
  saveLocale(locale)
}

const i18n = new VueI18n({
  locale,
  messages
});

export default i18n

2019.12.22 更新内容

滑动组件的实现切换阅读器内容

绑定滑动器 监听

@change="onProgressChange($event.target.value)" 
@input="onProgressInput($event.target.value)"

绑定监听事件

 displayProgress () {
        const cfi = this.currentBook.locations.cfiFromPercentage (this.progress / 100);
        this.currentBook.rendition.display (cfi);
      }

this.currentBook.locations.cfiFromPercentage
Epub.js封装的插件

const cfi = this.currentBook.locations.cfiFromPercentage (this.progress / 100);
this.currentBook.rendition.display (cfi);

//获取对应调节的位置

this.currentBook.locations.cfiFromPercentage (this.progress / 100

//切换阅读器
this.currentBook.rendition.display (cfi);

2019.12.28 更新内容

1.新增适配pc端的书签下拉功能

因为使用touch监听页面的点击下拉事件,在pc端上无法使用下拉事件,因此在【bookreader.vue】组件上新增鼠标监听事件

    // 1- 鼠标进入
      // 2- 鼠标进入后的移动
      // 3- 鼠标从移动状态松手
      // 4- 鼠标还原
      onMouseMove ( e ) {
        if (this.moveState === 1) {
          this.moveState = 2
        } else if (this.moveState === 2) {
          let offsetY = 0;
          if (this.firstOffsetY) {
            offsetY = e.clientY - this.firstOffsetY;
            this.setOffsetY (offsetY);
          } else {
            this.firstOffsetY = e.clientY;
          }
        }
        e.preventDefault ();
        e.stopPropagation ();
      },
      onMouseEnter ( e ) {
        this.moveState = 1;
        this.moveStateTime = e.timeStamp;
        e.preventDefault ();
        e.stopPropagation ();
      },
      onMouseEnd ( e ) {
        if (this.moveState === 2) {
          this.setOffsetY (0);
          this.firstOffsetY = 0;
          this.moveState = 3;
        };
        const time = e.timeStamp - this.moveStateTime;
        if (time <= 200) {
          this.moveState = 4;
        }
        e.preventDefault ();
        e.stopPropagation ();
      },

2. 书城首页标题和搜索框制作

一、新增动态路由,以及新增页面store.vue

在/router/index 新增路由信息

 {
    path: '/store',
    component: () => import('../views/store/index'),
    children: [
      {
        path: 'home',
        component: () => import('../views/store/storeHome')
      }
    ]
  }

二、新增书城标题以及搜索框组件

新增/home/Search.vue 组件

根据需求进行页面编写和css布局,未做交互处理,明天继续努力,奥里给!!!!

2019.12.14 更新内容

新增字体设置功能

技术点记录:

1.新增字体修改组件 【EbookSettingFont】

2.新增字体样式修改组件 【EbookSettingFontPopup】

主要是利用vuex 的 “defaultFontSize ” / "defaultFontFamily" 进行数据之间的数据传值

新增公共数据 /unit/book.js,主要用于设置字体大小和字体样式

export const Font_SIZE_LIST= [
  { fontSize : 12 },
  { fontSize : 14 },
  { fontSize : 16 },
  { fontSize : 18 },
  { fontSize : 20 },
  { fontSize : 22 },
  { fontSize : 24 }
];

export const FONT_FAMILY = [
  { font: 'Default' },
  { font: 'Cabin' },
  { font: 'Days One' },
  { font: 'Montserrat' },
  { font: 'Tangerine' }
];

2020.01.11 更新内容

新增缓存功能

新增插件【localforage】

缓存图书文件到IndexDB

import localForage from 'localforage'

export function setLocalForage(key, data, cb, cb2) {
  localForage.setItem(key, data).then((value) => {
    if (cb) cb(value)
  }).catch(function(err) {
    if (cb2) cb2(err)
  })
}

export function getLocalForage(key, cb) {
  localForage.getItem(key, (err, value) => {
    cb(err, value)
  })
}

export function removeLocalForage(key, cb, cb2) {
  localForage.removeItem(key).then(function() {
    if (cb) cb()
  }).catch(function(err) {
    if (cb2) cb2(err)
  })
}

export function clearLocalForage(cb, cb2) {
  localForage.clear().then(function() {
    if (cb) cb()
  }).catch(function(err) {
    if (cb2) cb2(err)
  })
}

export function lengthLocalForage(cb) {
  localForage.length().then(
    numberOfKeys => {
      if (cb) cb(numberOfKeys)
      // console.log(numberOfKeys)
    }).catch(function(err) {
      if (err) {}
    // console.log(err)
  })
}

export function iteratorLocalForage() {
  localForage.iterate(function(value, key, iterationNumber) {
    // console.log([key, value])
  }).then(function() {
    // console.log('Iteration has completed')
  }).catch(function(err) {
    if (err) {}
    // console.log(err)
  })
}

export function support() {
  const indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || null
  if (indexedDB) {
    return true
  } else {
    return false
  }
}

新增download 方法点击缓存时,触发:

export function download ( book, onSuccess, onError, onProgress ) {
  if ( !onProgress) {
    onProgress = onError;
  }
  return axios.create ({
    baseURL: process.env.VUE_APP_EPUB_URL,
    method: 'get',
    responseType: 'blob',
    timeout: 180 * 1000,
    onDownloadProgress: progressEvent => {
      if (onProgress) onProgress (progressEvent)
    }
  }).get (`${book.categoryText}/${book.fileName}.epub`).then (res => {
    const blob = new Blob ([res.data]);
    setLocalForage (book.filename, blob, () => {
      if (onSuccess) onSuccess (book)
    }, err => {
      if (onError) onError (err)
    })
    if (onSuccess) onSuccess (res)
  }).catch (err => {
    if (onError) onError (err)
  })
}

点击时触发:

async downloadSelectedBook () {
        for (let i = 0; i < this.shelfSelected.length; i++) {
          await this.downloadBook (this.shelfSelected[ i ]).then (book => {
            book.cache = true;
          })
        }
      },
      downloadBook ( book ) {
        let text = '';
        const toast = this.toast ({
          text
        });
        toast.continueShow ();
        return new Promise (( resolve, reject ) => {
          download (book, () => {
            toast.remove ();
            resolve (book)
          }, progressEvent => {
            const process = Math.floor (progressEvent.loaded / progressEvent.total * 100) + '%';
            text = this.$t ('shelf.progressDownload').replace ('$1', `${book.fileName}.epub(${process})`);
            toast.updateText (text);
          })
        })
      },

删除缓存功能

removeSelectedBook () {
        Promise.all (this.shelfSelected.map (book => this.removeBook (book))).then (books => {
          books.map (book => {
            book.cache = false;
          });
          saveBookShelf (this.shelfList);
          this.simpToast (this.$t ('shelf.removeDownloadTitle'));
        })
      },
      removeBook ( book ) {
        return new Promise (( resolve, reject ) => {
          removeLocalStorage (`${book.categoryText}/${book.fileName}`);
          removeLocalForage (`${book.fileName}`, resolve, reject);
          resolve (book)
        })
      },

2020.01.14 更新内容

书架分组功能开发

新增【Dialog \ ShelfGroupDialog】两个组件

主要用于显示分组组件样式

移入分组的方法:

moveToGroup(group) {
        this.setShelfList(this.shelfList
          .filter(book => {
            if (book.itemList) {
              book.itemList = book.itemList.filter(subBook => this.shelfSelected.indexOf(subBook) < 0)
            }
            return this.shelfSelected.indexOf(book) < 0
          }))
          .then(() => {
            if (group && group.itemList) {
              group.itemList = [...group.itemList, ...this.shelfSelected]
            }
            group.itemList.forEach((item, index) => {
              item.id = index + 1
            })
            this.simpleToast(this.$t('shelf.moveBookInSuccess').replace('$1', group.title))
            this.onComplete()
          })
      },

创建分组的方法:

 createNewGroup() {
        if (!this.newGroupName || this.newGroupName.length === 0) {
          return
        }
        if (this.showNewGroup) {
          this.shelfCategory.title = this.newGroupName
          this.onComplete()
        } else {
          const group = {
            id: this.shelfList[this.shelfList.length - 2].id + 1,
            itemList: [],
            selected: false,
            title: this.newGroupName,
            type: 2
          };
          let list = removeAddFromShelf(this.shelfList)
          list.push(group)
          list = appendAddToShelf(list)
          this.setShelfList(list).then(() => {
            this.moveToGroup(group)
          })
        }
      },

新增书架列表过度动画

书架列表新增【transition-group】,并且添加css样式

实现了列表的过渡,以及它会渲染成真实的元素。当我们去修改列表的数据的时候,如果是添加或者删除数据,则会触发相应元素本身的过渡动画,这点和 组件实现效果一样,除此之外 还实现了 move 的过渡效果,让我们的列表过渡动画更加丰富。

2019.12.16 更新内容

新增阅读器主题设置 / 全局主题设置

新增组件 【EbookSettingTheme】

主要用于切换阅读器主题样式,利用vuex:【defalutTheme】设置全局的主题样式
设置阅读器主题方法:
this.rendition.themes.select (defaultTheme);

新增公共属性函数

主要利用html的link标签对阅读器其他的公共样式进行css设置,并且根据全局样式一起调整对应样式。

export function addCss ( href ) {
  const link = document.createElement ('link');
  link.setAttribute ('rel', 'stylesheet');
  link.setAttribute ('type', 'text/css');
  link.setAttribute ('href', href);
  document.getElementsByTagName ('head')[ 0 ].appendChild (link);
}


export function removeCss ( href ) {
  const links = document.getElementsByTagName ('link');
  for (let i = links.length; i >= 0; i--) {
    const link = links[ i ];
    if (link && link.getAttribute ('href') && link.getAttribute ('href') === href) {
      link.parentNode.removeChild (link);
    }
  }
}

export function removeAllCss () {
  removeCss (`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`);
  removeCss (`${process.env.VUE_APP_RES_URL}/theme/theme_gold.css`);
  removeCss (`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`);
  removeCss (`${process.env.VUE_APP_RES_URL}/theme/theme_night.css`);
}

2019.01.03 更新内容

书城首页布局

新增组件【Category.vue / CategoryBook.vue / GuessYouLike.vue / Featured.vue / Recommend.vue 】
利用mock.js生成的在线数据,对页面进行模拟数据渲染,并且利用props进行数据传递子组件

  home ().then (response => {
        if (response && response.status === 200) {
          const data = response.data;
          const randomIndex = Math.floor (Math.random () * data.random.length);
          this.random = data.random[ randomIndex ];
          this.banner = data.banner;
          this.guessYouLike = data.guessYouLike;
          this.recommend = data.recommend;
          this.featured = data.featured;
          this.categoryList = data.categoryList;
          this.categories = data.categories;
        }

书城详情页面开发

新增页面【StoreDetail.vue / StoreList.vue】
新增组件【DetaiTitle.vue / BookInfo.vue /Toast.vue】

新增路由,点击页面后生成对应的路由参数 【fileName】

利用epubJS的api调用:
生成对应的图书信息

init () {
        // 获取电子书书名
        this.fileName = this.$route.query.fileName;
        // 获取电子书分类
        this.categoryText = this.$route.query.category;
        if (this.fileName) {
          // 请求API,获取电子书详情数据
          detail ({
            fileName: this.fileName
          }).then (response => {
            if (response.status === 200 && response.data.error_code === 0 && response.data.data) {
              const data = response.data.data
              console.log (data);
              // 保存电子书详情数据
              this.bookItem = data
              // 获取封面数据
              this.cover = this.bookItem.cover
              // 获取rootFile数据
              let rootFile = data.rootFile
              if (rootFile.startsWith ('/')) {
                rootFile = rootFile.substring (1, rootFile.length)
              }
              // 根据rootFile拼接出opf文件路径
              this.opf = `${process.env.VUE_APP_EPUB_OPF_URL}/${this.fileName}/${rootFile}`
              // 解析电子书
              this.parseBook (this.opf)
            } else {
              // 请求失败时打印错误提示
              this.showToast (response.data.msg)
            }
          })
        }
      },

并且根据数据进行页面布局

2019.12.30 更新内容

书城首页 卡片翻出动画实现

推荐卡片的交互细节:

  1. 弹出卡片
  2. 卡片翻出动画
  3. 烟花效果
  4. 弹出推荐图书

一:弹出卡片
vuex新增判断条件:【flapCardVisible】判断卡片是否弹出

新增卡片组件【FlapCard.vue】根据ui进行排版布局

二、卡片翻出动画(难点)

对翻转动画进行分析:
新增公共文件【store.js】,对翻转进行页面布局,
分析翻转动画,在动画开始前,利用绝对定位将六个样式居中显示,根据中心点进行翻页

具体实现代码如下:

semiCircleStyle ( item, dir ) {
        return {
          backgroundColor: `rgba(${item.r},${item.g},${item.b})`,
          backgroundSize: item.backgroundSize,
          backgroundImage: dir === 'left' ? item.imgLeft : item.imgRight,
        }
      },
      rotate ( index, type ) {
        const item = flapCardList[ index ];
        let dom;
        if (type === 'front') {
          dom = this.$refs.right[ index ]
        } else {
          dom = this.$refs.left[ index ]
        }
        dom.style.transform = `rotateY(${item.rotateDegree}deg)`;
        dom.style.backgroundColor = `rgb(${item.r},${item._g},${item.b})`
      },
      flapCardRotate () {
        const frontFlapCard = this.flapCardList[ this.front ];
        const backFlapCard = this.flapCardList[ this.back ];
        frontFlapCard.rotateDegree += 10;
        frontFlapCard._g -= 5;
        backFlapCard.rotateDegree -= 10;
        if (backFlapCard.rotateDegree < 90) {
          backFlapCard._g += 5;
        }
        if (frontFlapCard.rotateDegree === 90 && backFlapCard.rotateDegree === 90) {
          backFlapCard.zIndex += 2;
        }
        this.rotate (this.front, 'front');
        this.rotate (this.back, 'back');
        if (frontFlapCard.rotateDegree === 180 && backFlapCard.rotateDegree === 0) {
          this.next ();
        }
      },
      next () {
        const frontFlapCard = this.flapCardList[ this.front ];
        const backFlapCard = this.flapCardList[ this.back ];
        frontFlapCard.rotateDegree = 0;
        frontFlapCard._g = frontFlapCard.g;
        backFlapCard.rotateDegree = 0;
        backFlapCard._g = backFlapCard.g;
        this.rotate (this.front, 'front');
        this.rotate (this.back, 'back');
        this.front++;
        this.back++;
        const len = this.flapCardList.length;
        if (this.front >= len) {
          this.front = 0
        }
        if (this.back >= len) {
          this.back = 0;
        }
        //z-index
        // 100 -> 96
        // 99 ->100
        // 98 ->99
        // 97 -> 98
        this.flapCardList.forEach (( item, index ) => {
          item.zIndex = 100 - ( ( index - this.front + len ) % len );
        });
        this.prepare ();
      },
      prepare () {
        const backFlapCard = this.flapCardList[ this.back ];
        backFlapCard.rotateDegree = 180;
        backFlapCard._g = backFlapCard.g - 5 * 9;
        this.rotate (this.back, 'back');
      },
      reset () {
        this.front = 0;
        this.back = 1;
        flapCardList.forEach (( item, index ) => {
          item.zIndex = 100 - index;
          item._g = item.g;
          item.rotateDegree = 0;
          this.rotate (index, 'left');
          this.rotate (index, 'right');
        })
      },
      startFlapCardAnimate () {
        this.prepare ();
        this.task = setInterval (() => {
          this.flapCardRotate ();
        }, this.intervalTime)
      },
      stopFlapAnimate () {
        if (this.task) {
          clearInterval (this.task);
        }
        this.reset ()
      }

2019.12.25 更新内容(Merry Christmas)

1.全文搜索功能实现(算法实现)

一、根据epub.js github文档提供的wiki方法【doSearch (q)】,进行全文搜索,以及数组降维

 doSearch ( q ) {
        return Promise.all (
          this.currentBook.spine.spineItems.map (item => item.load (this.currentBook.load.bind (this.currentBook)).then (item.find.bind (item, q)).finally (item.unload.bind (item)))
        ).then (results => Promise.resolve ([].concat.apply ([], results)));
      },

二、通过双向数据绑定根据搜索关键词进行全文,并且添加高亮

seach () {
        if (this.searchText !== '' && this.searchText.length > 0) {
          this.currentBook.ready.then (() => {
            this.doSearch (this.searchText).then (result => {
              this.searchList = result;
              this.searchList.map (item => {
                item.excerpt = item.excerpt.replace (this.searchText, `<span class="content-search-text">${this.searchText}</span>`);
                return item;
              });
            })
          })
        }
      }

三、通过【this.currentBook.rendition.annotations.highlight】找到阅读器组件中的关键词,并且添加高亮

switchChapters ( navigation, highLight = false ) {
        this.renditionDisplay (navigation, () => {
          this.hideTitleAndMenu ();
          if (highLight) {
            this.currentBook.rendition.annotations.highlight (navigation);
          }
        });
      },

2.目录加载动画实现

新增加载动画组件【EbookLoading.Vue】

通过自定义函数,生成循环递减的加载动画,主要难点在于判断动画的执行顺序,以及切换动画的过程,结束时自动再次加载

动画实现方法如下:

mounted () {
      this.task = setInterval (() => {
        this.$refs.mask.forEach (( item, index ) => {
          const mask = this.$refs.mask[ index ];
          const line = this.$refs.line[ index ];
          let maskWidth = this.maskWidth[ index ];
          let lineWidth = this.lineWidth[ index ];
          if (index === 0) {
            if (this.add && maskWidth.value < 56) {
              maskWidth.value++;
              lineWidth.value--
            } else if ( !this.add && lineWidth.value < 56) {
              maskWidth.value--;
              lineWidth.value++
            }
          } else {
            if (this.add && maskWidth.value < 56) {
              let preMaskWidth = this.maskWidth[ index - 1 ];
              if (preMaskWidth.value >= 28) {
                maskWidth.value++
                lineWidth.value--
              }
            } else if ( !this.add && lineWidth.value < 56) {
              let preLineWidth = this.lineWidth[ index - 1 ];
              if (preLineWidth.value >= 28) {
                maskWidth.value--
                lineWidth.value++
              }
            }
          }
          mask.style.width = `${px2rem (maskWidth.value)}rem`;
          mask.style.flex = `0 0 ${px2rem (maskWidth.value)}rem`;
          line.style.width = `${px2rem (lineWidth.value)}rem`;
          line.style.flex = `0 0 ${px2rem (lineWidth.value)}rem`;
          if (index === this.maskWidth.length - 1) {
            if (this.add) {
              if (maskWidth.value === 56) {
                this.end = true
              }
            } else {
              if (maskWidth.value === 0) {
                this.end = true
              }
            }
          }
          if (this.end) {
            this.add = !this.add;
            this.end = false
          }
        })
      }, 5)
    },

VUEX 引用 + 使用

state: {
test: 1
},
mutations: {
'SET_TEST': (state , newTest) => {
state.test = newTest
}
},
actions: {
setTest : ({ commit, state}, newTest) => {
return commit('SET_TEST', newTest)
}
}

参考文档:https://vuex.vuejs.org/zh/

2019.12.29 更新内容

书城首页顶部导航栏交互动画实现

**- 需求分析:捕捉细节,看懂需求

  • 需求拆封:将复杂问题转换为若干问题的集合
  • 求解:针对简单问题进行求解
  • 优化:对实现过程进行优化**

标题+搜索

1. 向下滑动和屏幕细节的交互分析:
2. 标题和推荐图标向下渐隐
3. 搜索框向上移动到标题位置
4. 搜索框逐渐变窄已适应屏幕宽度
5. 返回键向下居中
6. 标题下方显示阴影

针对以上需求,对组件【Search.vue】增加判断条件
监听vuex的【offsetY】 属性,当页面向下滑动的时候,隐藏标题,反至显示,并且增加对应的css属性和tranisation属性,增加动画效果

    watch: {
      offsetY ( offsetY ) {
        if (offsetY > 0) {
          this.hideTitle ();
          this.showShadow ();
        } else {
          this.showTitle ();
          this.hideShadow ();
        }
      },
      hotSearchOffsetY ( offsetY ) {
        if (offsetY > 0) {
          this.showShadow ();
        } else {
          this.hideShadow ();
        }
      }
    },
    data () {
      return {
        searchText: '',
        titleVisible: true,
        shadowVisible: false,
        hotSearchVisible: false
      }
    },
    methods: {
      back () {
        this.hideHotSearchVisible ();
      },
      hideHotSearchVisible () {
        this.hotSearchVisible = false;
        if (this.offsetY > 0) {
          this.hideTitle ();
          this.showShadow ();
        } else {
          this.showTitle ();
          this.hideShadow ();
        }
      },
      showHotSearchVisible () {
        this.hideTitle ();
        this.hideShadow ();
        this.hotSearchVisible = true;
        this.$nextTick (() => {
          this.$refs.hotSearch.reset ()
        });
      },
      showTitle () {
        this.titleVisible = true
      },
      hideTitle () {
        this.titleVisible = false
      },
      hideShadow () {
        this.shadowVisible = false
      },
      showShadow () {
        this.shadowVisible = true;
      }
    },

搜索提示页面

新增两个组件【HotSearch】【HotsearchList】至【SearchBar】组件

点击搜索框时显示,点击返回按钮隐藏
新增新的vuex参数:【store.js】,将阅读器和书城页面区分开,采用不同的model

注意点,页面的细节交互:
导航栏的底部阴影显示,需要判断vuex中的offsetY是否大于0,大:隐藏,小:显示
搜索提示返回时,判断页面是否下拉时,下拉:不显示标题栏,直接显示搜索框
点击搜索框,每次出现都使【HotsearchList】组件显示顶部,需对在显示提示框增加【this.$nextTick】

this.nextTick:
用法: 在下次DOM更新循环之后执行延迟回调,在修改数据之后立即使用该方法,获取之后的DOM
用途:需要在视图更新之后,基于新的视图进行操作

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.