Vue-eBookReader 学习笔记(书城页面开发)

5、书城页面开发

5.1 书城路由设置

  • 在views下再建一个文件夹来放书城的首页文件index.vue,里面内容和eBook底下的index.vue几乎一样,就一个<router-view></router-view>,再建个StoreHome.vue组件(相当于ebook底下的EbookReader组件)
  • 另外在路由文件下新建路径为/store的路由,children为StoreHome
{
	path: '/store',
	component: () => import('../views/store/index.vue'),
	redirect: '/store/home',
	children: [
    {
      path: 'home', // 这里不要加/ ,加上/是绝对路径,这里使用相对路径
      component: () => import('../views/store/StoreHome')
    }
	]
}

5.2 书城搜索部分

  • 这一部分建立一个SearchBar组件,来放页首和搜索框部分,也就是一些布局,不再多说

5.3 标题+搜索框交互部分

5.3.1 复杂交互的实现思路

  • 分析:捕捉细节,看懂需求
  • 拆分:将复杂问题转化为若干简单问题的集合
  • 求解:针对简单问题进行求解(打开脑洞)

这里的复杂动画可以拆分为以下五个过程:

  • 标题和推荐图标向下渐隐
  • 搜索框向上移动到标题位置
  • 搜索框逐渐变窄以适应屏幕(难点)
  • 返回按钮向下居中
  • 标题下方显示阴影

标题和推荐图标向下渐隐

  • 这里首先要注意到一个问题,由于左边的返回按钮和这两个东西在同一个盒子.search-bar-title-wrapper中,所以如果直接在这个外面加上transition标签,会让返回按钮也一起隐藏起来
  • 这里将返回按钮放到和.search-bar-title-wrapper平级的位置,然后让这两个盒子都变为absolute布局,浮动起来,就还是能在同一行了,这样就能直接在那个大盒子外面加上transition动画了

搜索框上移

  • 上面那样都用absolute布局后,发现搜索框移上去了(因为就它一个盒子在文档流里),所以搜索框也要用absolute布局

  • absolute的话,上移就可以直接通过改变top值来实现,同时加上transition属性就可实现过渡

  • 上移之后,滚动条下面会有留白的部分,这时因为滚动条离上面的高度变化之后,要改变它的top值以及调用它自身定义的refresh函数来重新计算

搜索框宽度渐变 返回按钮微向下移动

  • 宽度要变的话可能直接想到的就是改变width,但这里要实现这样流畅的渐变效果,使用的是flex自适应的特性来变化,也就是在搜索框左边加一个空白盒子,初始占位0,后来用flex占位31,当然外层大盒子要采用flex布局,再搜索框 flex为1,再加上transition属性就能让宽度逐渐变化
  • 按钮向下移动并不是改变说明padding top之类的,而是通过改变那个小盒子的高度,因为它内部有center,所以改变高度之后能直接垂直居中 往下移动

标题下方显示阴影

  • 最开始的时候标题下方是没有的,但滑动之后就有了,因此这里定义另一个变量shadowVisible来控制是否显示阴影
  • 其余和上面的titleVisible一致

上述变化都是通过动态添加class来实现的

5.4 书城热门搜索部分

  • 热门搜索是一整个页面HotSearchList,里面有scroll组件,scroll内部有两个HotSearch组件,就代表是上下 热门搜索和搜索历史两个区块
  • 虽然是不一样的区块,但是布局都差不多一样,所以只要传一些特定的值进去定制组件就好了
  • 同是这里在vuex中再创建一个组件存放书城相关的数据以及修改函数,和之前的book一样

点击切换 各种功能部分

  • 点击搜索框,热门搜索部分要显示出来,点击返回要回到书城主页面,这里要注意一下,由于这里是absolute布局,上面是title部分z-index为150,点击back按钮的时候可能会点击不到它上面,所以要调整一下back按钮的z-index

  • 热门搜索部分的显示和隐藏也要有一些动态效果,transition来实现,渐隐和移动

  • 热门搜索部分也要实时监听hotSearchOffsetY来看是否给title部分加上阴影,实现比较简单

  • 注意这里有一堆需要改进的地方,都是比如 页面切换完还没有开始滚动的时候,title和shadow显不显示的问题,仔细改进一下就好了,不难

进入热门搜索页面不保留上次的进度

  • 这里就直接用到 scroll组件中定义的scrollTo方法来滚动到最上面的地方,用ref获取HotSearchList组件,里面定义了reset方法会去调用scroll的scrollTo方法
  • 但是要注意,showHotSearch函数里不能直接调用reset方法,这样不会生效,这是由于 调用这个方法的时候dom还没有更新完毕,要等dom更新完毕之后再调用,那什么时候调用?在vue的$nextTick函数里的回调中调用

5.5 卡片翻转动画

  • 创建一个FlapCard组件,先建好布局(按钮 翻转外边框什么的),内部是五个圆形图案里面放着要翻转的图片,通过v-for来遍历flapCardList来实现
  • 每个圆内有左半圆和右半圆,通过调整这两个盒子的border-radius来凑成一整个圆,然后内部的样式就通过遍历的item来赋值

5.5.1 卡片翻转动画原理(重难点)

  • 首先先分析一下,卡片翻转的时候,右半圆颜色在加深,同时翻到中间位置时,背面的左半圆开始显示,且正面右半圆不显示(z-index变化了)
  • 翻转的时候,看起来是半个圆在动,实际上是两个半圆都在动,正面和背面的,所以要初始化背面的圆为翻过去180度的情况,且颜色也要预先减少值,这样后来慢慢翻过来显示背面圆的时候就是角度和颜色逐渐回到正常的情况
  • 注意设置两个data,front和back,分别代表当前旋转圆正面和背面的index值

5.5.2 卡片翻转相关函数设置

旋转函数(rotate(index, type))

  • 作用为第index个圆 类型为type(front或back),旋转到flapCardList[index]对应属性的位置,也就是将rotateDegree和_g(这里的_g和之前的g可能不一样,专门用来改变颜色深浅的,设置两个值是为了后面方便复原)赋给相应的dom,实现旋转
rotate (index, type) {
	// 第index个圆 前面还是后面的
	const item = this.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 = `rgba(${item.r}, ${item._g}, ${item.b})`
},

开始旋转函数 (startFlapCardAnimation())

  • 设置setInterval函数来定时改变圆的角度,开始之前先调用prepare()函数,这里的prepare
  • 再在setInterval回调里调用flapCardRotate函数
startFlapCardAnimation () {
	this.prepare()
	this.task = setInterval(() => {
		this.flapCardRotate()
	}, 25)
},

准备工作函数 (prepare())

  • 将当前背面的圆翻过去180度,颜色题前加深5*9
  • 记得调用rotate函数来生效
prepare () {
  const backFlapCard = this.flapCardList[this.back]
  backFlapCard.rotateDegree = 180
  backFlapCard._g = backFlapCard.g - 5 * 9
  this.rotate(this.back, 'back')
},

真正旋转 改变角度等 函数(flapCardRotate())

  • 每次先获取当前正面和背面 圆的样式(frontFlapCard和backFlapCard),让它们的rotateDegree和_g改变
  • 正面的rotateDegree每次增加10,_g减少5,背面的相反(但这里要注意以下,要在半圆翻过一半的时候背面的圆的颜色才开始变化,因为之前都是看不见的情况
  • 同时当翻过90的时候,背面圆的z-index要大于正面圆的,开始显示背面圆
  • 样式修改完毕就可调用rotate函数来生效旋转
  • 当翻过去180的时候就可以进入下一个圆的部分,调用next()函数
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())

  • 由于进入下一个圆了,上一个圆的就得先复原到最初的状态,所以先获取flapCardList中当前正面 背面圆的样式获取到,复原(记得调用rotate来生效)

  • 同时 this.front和this.back要加一,当加到边界时要为0

  • 这里要注意z-index的改变,由于这个圆已经过去了,现在要更新所有圆的z-index,使新的圆的z-index要是最大的

100 -> 96
99 -> 100
98 -> 99
97 -> 98
96 -> 97
// 以上这种变化规律
this.flapCardList.forEach((item, index) => {
  item.zIndex = 100 - ((index - this.front + len) % len)
})
  • 最后要调用prepare来初始化新的背面半圆
next () {
  const frontFlapCard = this.flapCardList[this.front]
  const backFlapCard = this.flapCardList[this.back]
  frontFlapCard._g = frontFlapCard.g
  backFlapCard._g = backFlapCard.g
  frontFlapCard.rotateDegree = 0
  backFlapCard.rotateDegree = 0
  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
  }
  this.flapCardList.forEach((item, index) => {
  	item.zIndex = 100 - ((index - this.front + len) % len)
  })
  this.prepare()
},

5.5.3 卡片归位

  • 每次点击退出 再次进入的时候卡片又要回到一开始的状态,所以要进行停止和开始动画的操作
  • 在close函数(back按钮click事件处理函数)里调用stopFlapCardAnimation函数,这个函数里先清除了那个setInterval的定时器,再调用reset函数重置数据
  • reset函数里,先将front back归为0和1,再遍历flapCardList来重置里面的_g rotateDegree z-index数据
reset () {
  this.front = 0
  this.back = 1
  this.flapCardList.forEach((item, index) => {
    item._g = item.g
    item.rotateDegree = 0
    item.zIndex = 100 - index
    this.rotate(index, 'front')
    this.rotate(index, 'back')
  })
},
stopFlapCardAnimation () {
  this.reset()
  if (this.task) {
  	clearInterval(this.task)
  }
}

5.5.4 卡片弹出时 弹动效果(keyframes)

  • 这里要有一个,弹出时先大后小再原位的弹动效果,利用CSS3的keyframes动画来实现,用runFlapCardAnimation变量来控制动态绑定animation的class到flap-card-bg上
  • 看代码吧,就以下这样
&.animation {
	animation: flap-card-move .3s ease-in;
}
@keyframes flap-card-move {
	0% {
		transform: scale(0);
		opacity: 0;
	}
	50% {
		transform: scale(1.2);
		opacity: 1;
	}
	75% {
		transform: scale(0.9);
		opacity: 1;
	}
	100% {
		transform: scale(1);
		opacity: 1;
	}
}

5.5.5 一定时间后卡片消失

  • 每次点击shake图片是想要随机推荐一本图书,所以这里是要请求一下别的地方的api,当请求完毕时翻转动画就要消失
  • 这里就先模拟一下2.5s后消失的设置,直接在startFlapCardAnimation函数中延迟调用stopFlapCardAnimation函数即可

5.6 烟花动画

5.6.1 烟花动画原理

  • 其实就是,一些重叠的小球通过控制css样式使它们从中间向外面散发
  • 这里提前定义好了小球在过程中的一些样式 在flapCard.scss文件中,包括了颜色渐变,位置渐变,以及动画 都定义好了(抽空可以研究研究这个文件,它里面用到了很多scss的知识)

5.6.2 烟花动画实现

  • 在dom中加入18个point小球来表示烟花,同时在文件created时就初始化pointList,用v-for来创建小球
  • 接下来是样式,样式直接通过动态绑定animation的class来实现,通过runPointAnimation来控制
  • 具体的animation 不同的小球各自不相同,因此要在利用scss的一些语法来完成根据index来赋予不同的样式这个功能
.point-wrapper {
  z-index: 1100;
  @include absCenter;
  .point {
    border-radius: 50%;
    @include absCenter;
      &.animation {
      @for $i from 1 to length($moves) {
        &:nth-child(#{$i}) {
        	@include move($i)
            // flapCard.scss中定义了move函数 通过index可以拿到对应小球的样式
        }
      }
    }
  }
}

5.7 书城首页

  • 首先安装 mockjs和axios
    • mockjs:在做开发时,当后端的接口还未完成,前端为了不影响工作效率,手动模拟后端接口
cnpm i mockjs --save-dev
cnpm i axios --save

5.7.1 用mockjs创建模拟接口

  • 在根路径下创建mock文件夹,里有这次要请求的相关数据,都是JSON类型的数据,先加一个index.js文件来创建接口
  • 在index.js文件内,先引入数据文件,再用Mock.mock(//book/home/, 'get', home)来创建接口,意思是 当浏览器请求路径为/book/home 且 请求方式为get时,就给home这个数据(数据在上面被import进来了)
  • 这个index.js文件要在项目入口文件里被引入,也就是main.js

5.7.2 用axios发送请求

  • 在根路径下创建api文件夹,文件夹下创建store.js文件来专门发送store相关的请求,引入axios
  • 这里还要创建一个环境变量,baseUrl的,也是在开发与发布环境里路径不一致
import axios from 'axios'
export function home () {
  return axios({
    method: 'get',
    url: `${process.env.VUE_APP_BASE_URL}/book/home`
  })
}
  • 在StoreHome组件里引入store.js文件来调用这里的home方法发送请求,得到的数据是在回调里的,所以异步返回一个response就为数据,当(response && response.status === 200)就获取一个随机数再根据data获取一本图书数据,传递给flap-card组件(实现随机推荐功能)

5.7.3 书城推荐完毕后的详情页面

  • 在FlapCard组件里,当推荐完毕后显示一个book-card-wrapper页面
  • 这里布局引入了一个flapCard.scss的文件来实现样式,另外在/utils/store.js中添加了两个方法getCategoryName和categoryText,作用分别是根据id找到分类名称以及找到相应的国际化名字,到时候book-card里调用方法显示就可以
  • 这里book-card是否显示由一个bookCardVisible来控制,注意这里显示的时候,之前的翻页动画要停止,要修改一系列的值(不细说)
  • 稍微提一下,如果在还未推荐完毕的时候就关闭的页面,再次进入推荐页面的时候要重新开始,所以要清除当前存在的定时器,下次重新开始

5.8 首页图书布局

以下的每个模块都是一个子组件,由StoreHome组件传入相应所需要的值进去。基本每个模块上面都有一个标题部分,这里又抽象出来为一个组件,传入不同的值就显示不同的内容。

5.8.1 猜你喜欢模块

  • 上面是title部分,传入当前要显示的文字
  • 下面是列表部分,这里接收到的数据是data.guessYouLike 这个数组,里面有9个元素,每次显示其中的三本:
    • 对接收到的数组进行处理,获取到其中的三本又是一个数组showData(这时一个计算属性),用v-for来显示这三本书,其中包括了左边的图片以及右边的书名、说明
    • 就用flex布局,图片一直占20%,右边自适应即可
  • 若点击了“换一批”,则调用change函数来处理,每次让当前index加一,若大于数组长度则变为0
  • 同时计算属性showData会自动进行重新计算,就会自动展示新的三本书

5.8.2 热门推荐模块

  • 上面是title部分,传入当前要显示的文字
  • 下面是recommend列表部分,接收的data就是要推荐的图书,flex布局,每个占33.3%即可

5.8.3 精选模块

  • 上面是title部分,传入当前要显示的文字
  • 下面的精选图书,还是遍历得到的数据数组,每行显示两本书,这个用flex-flow: row wrap; 来实现,其中所有item都是flex: 0 0 50%,就刚好可以每行的一半显示一本书

5.8.4 类别模块

  • 类别又社会科学、经济学等四个模块,所以这底下又要有四个部分,就在StoreHome里用v-for循环四次创建四个categoryBook组件,每次传入不同的数据就可以定制特殊的类别
  • 每个categoryBook内部布局和之前模块也差不多,还是循环获得的数据显示书本

5.8.5 分类模块

  • 上面是title部分,传入当前要显示的文字
  • 下面的列表部分,还是每行显示两个,对半分空间,所以依然是flex-flow: row wrap;
  • 这里有个图片的堆叠效果,其实也就是设置两张图片,定位稍微移开一点,利用z-index来形成上下的效果

5.9 书城详情页

5.9.1 请求部分

  • 这里新加了一个StoreDetail页面,用来专门显示书本详情页的(里面有一堆布局和样式 逻辑,没太搞明白)
  • 新的页面要在router的store下新开一个children来存放,跳转到该页面时,要请求这本书的所有信息与内容,包括章节 作者等各种信息,因此又要在api/store.js中像之前一样创建另一个函数来利用axios请求数据
export function detail (book) {
  return axios({
    method: 'get',
    url: `${process.env.VUE_APP_BOOK_URL}/book/detail`,
    params: {
      fileName: book.fileName
    }
  })
}
  • 这个函数在StoreDetail页面加载的时候就被调用,异步返回的结果就是获得的书本全部内容,对其进行一步步解析可以得到需要的各方面信息,再利用布局展示到页面上即可

5.9.2 书城点击进入阅读部分

  • 之前用的mockjs可以模拟后端来发送数据,但对于blob对象就不行,而这里获取到的书本的url之后就是对其进行下载 返回的就是blob对象,所以在这里不可以继续使用mockjs了
    • 所以一旦设计到 下载类 资源类文件时,就不可以使用mockjs
  • 自己在vue.config.js中写一个mock函数来发送请求(利用本地调试模式启动的http服务,在这个里面添加自定义的接口
function mock (app, url, data) {
  // 全局的app对象 模拟的接口的url 要传递的数据
  app.get(url, (request, response) => {
    response.json(data)
  })
}
  • 这里的所有数据都来源于本地的mock文件夹下的数据
const homeData = require('./src/mock/bookHome')
const shelfData = require('./src/mock/bookShelf')
const flatListData = require('./src/mock/bookFlatList')
const listData = require('./src/mock/bookList')
  • 同时在module.exports里定义devServer属性,里面设置一个before函数 在服务被启动前调用,在这里面调用四次mock函数分别传输四次数据
devServer: {
  before (app) {
    mock(app, '/book/home', homeData)
    mock(app, '/book/shelf', shelfData)
    mock(app, '/book/flat-list', flatListData)
    mock(app, '/book/list', listData)
  }
}

5.10 书城列表页

5.10.1 查看某一分类所有书

  • 要点击某一个分类的查看全部,就得跳转到另外一个页面(书城列表页),这个页面包含了这个分类所有的信息
showBookCategory (item) {
  this.$router.push({
    path: '/store/list',
    query: {
      category: getCategoryName(item.category),
      categoryText: this.categoryText(item.category)
    }
  })
},
  • 同样,跳转的话就是要用到路由的push函数,里面要有路径和参数,这里是参数就是当前的分类信息,这样就能显示StoreList组件
    • 里面布局为,上面是标题部分,下面是一个scroll组件 放了所有图书的信息(v-for也可以用来遍历对象,(value, key, index) of list这样),里面放的是feature组件去遍历,因为这里的布局和feature一样,每行放两本书
  • 那在StoreList组件里显示当前分类的话如何显示呢?
    • 首先还是调用list方法(定义在mixin里的全局方法,来请求数据的),获取所有的电子书
    • 再通过this.$router.query.category获取要请求的分类名字
    • 通过获得的全部电子书(回调里)利用Object.keys(data)的方法获取所有的key,找到后就直接data[key]来获取当前分类下的电子书
    • 最后赋值给当前组件的list去给循环即可

5.10.2 搜索具有某一关键字的书

  • 在searchBar组件里有搜索框,在这里输入要搜索的keyword之后按下回车(通过keyup.13.exact='search'来绑定事件处理函数)
  • 在search里,还是利用this.$router.push()函数来跳转到StoreList页面,将keyword当作query的参数传递进去
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
})
  • 遍历每个key值,通过key值再去遍历每一本书的名字,保留下含关键字的,同时只有这个分类下至少有一本满足有关键字这个条件的书 再去保留这整个分类
原文地址:https://www.cnblogs.com/TRY0929/p/13681839.html