4.阅读器–阅读进度、目录、全文搜索功能开发
4.12. 全文搜索功能实现(搜索算法+数组降维)
该方法 中,q为输入的关键词,在全篇电子书中,查找关键词,返回关键词所在的位置。
doSearch(q){
return Promise.all(
this.currentBook.spine.spineItems.map(
section => section.load(this.currentBook.load.bind(this.currentBook))
.then(section.find.bind(section,q))
.finally(section.unload.bind(section))
)
)
.then(results => {
// return Promise.resolve([].concat.apply([],results))
console.log(results);
})
},
试一下调用该方法
if (this.currentBook) {
this.currentBook.ready.then(()=>{
this.doSearch('added').then(results=>{
console.log(results);
})
})
}
得到的是一组多维数组
因为得到的是二维数组,所以需要像 目录那样降维。
实际上这句就已经采用了降维的方法。
想要实现搜索功能,需要在我们搜索的时候,把目录列表隐藏掉,然后把搜索列表展示出来
再添加搜索列表
试一下 执行
看,成功打印出来了
4-13 全文搜索功能实现 (搜索关键字高亮+搜索结果高亮显示)
把返回的text 中的关键字 replace成 用span包裹起来,让span中的文字高亮
search(){
if (this.currentBook&&this.searchText.length>0&&this.searchText){
this.currentBook.ready.then(()=>{
this.doSearch(this.searchText)
.then((results)=>{
this.searchList = results
this.searchList.map(item=>{
item.excerpt = item.excerpt.replace(this.searchText,`<span class="content-search-text">${this.searchText}</span>`);
return item;
})
});
})
}
},
当在输入框按下回车键的时候执行该方法
成功完成任务
接下来,要实现的是 点击对应的搜索列表项 跳转到对应的搜索结果页面,
只需要添加这个点击事件即可实现,当然,跳转后应该将菜单栏消失
所以我们把点击方法改成
displaySearch(target){
this.display(target,()=>{
this.hideMenuVisible();
})
},
还没完成,跳转到对应的页面之后,也应该将关键词高亮显示
电子书提供了一个方法
同时把上面方法改一下
displaySearch(target,highlight = false){
this.display(target,()=>{
this.hideMenuVisible();
if (highlight){
this.currentBook.rendition.annotations.highlight(target);
}
})
},
可见搜索结果高亮显示也完成了。
4-14 目录加载动画实现
效果如图
组件代码
<template>
<div class="ebook-loading">
<div class="ebook-loading-wrapper">
<div class="ebook-loading-item" v-for="(item, index) in data" :key="index">
<div class="ebook-loading-line-wrapper" v-for="(subItem, subIndex) in item" :key="subIndex">
<div class="ebook-loading-line" ref="line"></div>
<div class="ebook-loading-mask" ref="mask"></div>
</div>
</div>
<div class="ebook-loading-center"></div>
</div>
</div>
</template>
<script>
import { px2rem } from './../../utils/utils'
export default {
data() {
return {
data: [
[{}, {}, {}],
[{}, {}, {}]
],
maskWidth: [
{ value: 0 },
{ value: 0 },
{ value: 0 },
{ value: 0 },
{ value: 0 },
{ value: 0 }
],
lineWidth: [
{ value: 16 },
{ value: 16 },
{ value: 16 },
{ value: 16 },
{ value: 16 },
{ value: 16 }
],
add: true,
end: false
}
},
methods: {},
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 < 16) {
maskWidth.value++
lineWidth.value--
} else if (!this.add && lineWidth.value < 16) {
maskWidth.value--
lineWidth.value++
}
} else {
if (this.add && maskWidth.value < 16) {
let preMaskWidth = this.maskWidth[index - 1]
if (preMaskWidth.value >= 8) {
maskWidth.value++
lineWidth.value--
}
} else if (!this.add && lineWidth.value < 16) {
let preLineWidth = this.lineWidth[index - 1]
if (preLineWidth.value >= 8) {
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 === 16) {
this.end = true
}
} else {
if (maskWidth.value === 0) {
this.end = true
}
}
}
if (this.end) {
this.add = !this.add
this.end = false
}
})
}, 20)
},
beforeDestroy() {
if (this.task) {
clearInterval(this.task)
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
@import "../../assets/styles/global";
.ebook-loading {
position: relative;
z-index: 500;
width: px2rem(63);
height: px2rem(40);
background: transparent;
border: px2rem(1.5) solid #d7d7d7;
border-radius: px2rem(3);
.ebook-loading-wrapper {
display: flex;
width: 100%;
height: 100%;
.ebook-loading-item {
display: flex;
flex-direction: column;
flex: 1;
padding: px2rem(7) 0;
box-sizing: border-box;
.ebook-loading-line-wrapper {
flex: 1;
padding: 0 px2rem(7);
box-sizing: border-box;
@include left;
.ebook-loading-mask {
flex: 0 0 0;
width: 0;
height: px2rem(1.5);
}
.ebook-loading-line {
flex: 0 0 px2rem(16);
width: px2rem(16);
height: px2rem(2);
background: #d7d7d7;
}
}
}
.ebook-loading-center {
position: absolute;
left: 50%;
top: 0;
width: px2rem(1.5);
height: 100%;
background: #d7d7d7;
}
}
}
</style>
5.阅读器–书签功能、页眉页脚及兼容性优化
5-1 书签手势实现(页面下拉)
epub电子书是没有touchmove这个监听事件的,要想要这个效果,就必须添加个蒙层
当然要把它背景弄成透明
这样,监听事件就放在 这个蒙层上就好,通过蒙层来改变 offsetY的值,再通过offsetY的值来改变ebook的位置
蒙层上的监听事件
move(e){
let offsetY = 0;
if (this.firstOffsetY){
offsetY = e.changedTouches[0].clientY - this.firstOffsetY
this.setOffsetY(offsetY)
console.log(this.offsetY);
} else{
this.firstOffsetY = e.changedTouches[0].clientY;
}
e.preventDefault();
e.stopPropagation();
},
moveEnd(e){
this.setOffsetY(0);
this.firstOffsetY = null;
},
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.toggleTitleAndMenu();
}
},
可见,已经改变了offsetY的值
接下来 index.vue中监听offsetY的改变来改变reader的top
watch:{
offsetY(v){
if(v>0){
this.move(v)
}else if(v===0){
this.restore()
}
}
},
methods:{
restore(){
this.$refs.ebook.style.top = 0+'px';
this.$refs.ebook.style.transition = 'all 0.2s linear';
setTimeout(()=>{
this.$refs.ebook.style.transition = '';
},200)
},
move(v){
this.$refs.ebook.style.top = v+'px';
},
},
这样就实现了 下拉功能。
这里需要解释一下里面的
setTimeout(()=>{
this.$refs.ebook.style.transition = '';
},200)
这是因为,那里设置的
this.$refs.ebook.style.transition = ‘all 0.2s linear’; 刚好为两秒,当松手后,动画结束,事件刚好200ms
5-2 书签手势实现(书签组件)
先创建这个大组件
<template>
<div class="ebook-bookmark">
<div class="ebook-bookmark-text-wrapper">
<div class="ebook-bookmark-down-wrapper">
<span class="icon-down"></span>
</div>
<div class="ebook-bookmark-text">{{text}}</div>
</div>
<div class="ebook-bookmark-icon-wrapper">
<Bookmark :color="'red'" :width="15" :height="35"></Bookmark>
<!--<div class="icon"></div>-->
</div>
</div>
</template>
<script>
import Bookmark from "./../../components/common/Bookmark"
const BLUE = "#346cbc";
const WHITE = "#fff";
export default {
name: "EbookBookmark",
data(){
return{
text:this.$t('book.pulldownAddMark')
}
},
components:{Bookmark}
}
</script>
<style scoped lang="scss">
@import "./../../assets/styles/global";
.ebook-bookmark{
position: absolute;
top: px2rem(-35);
left: 0;
z-index: 200;
width: 100%;
height: px2rem(35);
background-color: black;
.ebook-bookmark-text-wrapper{
position: absolute;
right: px2rem(45);
bottom: 0;
display: flex;
.ebook-bookmark-down-wrapper{
font-size: px2rem(14);
color: white;
transition:all 0.2s linear;
@include center;
.icon-down{
}
}
.ebook-bookmark-text{
font-size: px2rem(14);
color: white;
}
}
.ebook-bookmark-icon-wrapper{
position: absolute;
right: 0;
bottom: 0;
margin-right:px2rem(15) ;
.icon{
/*width: 0;*/
/*height: 0;*/
/*border-width: px2rem(50) px2rem(10) px2rem(10) px2rem(10);*/
/*border-style: solid;*/
/*border-color: white white transparent white;*/
}
}
}
</style>
这也是一个组件
<template>
<div class="bookmark" :style="style" ref="bookmark"></div>
</template>
<script>
import {px2rem} from "../../utils/utils";
export default {
name: "Bookmark",
props:{
width:Number,
height:Number,
color:String
},
computed:{
style(){
if (this.color){
console.log(this.color);
return {
borderColor:`${this.color} ${this.color} transparent ${this.color}`
}
} else{
return{ borderColor:``}
}
}
},
methods:{
refresh(){
if (this.height && this.width){
this.$refs.bookmark.style.borderWidth = `${px2rem(this.height - 5)}rem ${px2rem(this.width / 2)}rem ${px2rem(5)}rem ${px2rem(this.width / 2)}rem`
}
},
},
mounted(){
this.refresh();
}
}
</script>
<style scoped lang="scss">
@import "./../../assets/styles/global";
.bookmark{
width: 0;
height: 0;
border-width: px2rem(50) px2rem(10) px2rem(10) px2rem(10);
border-style: solid;
border-color: white white transparent white;
}
</style>
5-3 书签手势实现(下拉状态管理)
下拉阶段分为三个阶段
第一阶段:不变。
第二阶段:
第三阶段:
代码实现,通过监听offsetY:
watch:{
offsetY(v){
if(v>=this.height && v<=this.threshold){
console.log("到达第二阶段");
this.$refs.bookmark.style.top = `${-v}px` //使书签吸顶
this.text = this.$t('book.pulldownAddMark'); //文字改为添加书签
this.color = WHITE;
}else if(v>=this.threshold){
console.log("到达第三阶段");
this.$refs.bookmark.style.top = `${-v}px` //使书签吸顶
this.text = this.$t('book.pulldownAddMark'); //文字改为释放书签
this.color = BLUE;
}
}
},
5-4 书签手势实现(书签添加删除交互)
先改变 箭头方向,在第三阶段的时候,箭头向上
第二阶段的时候,箭头向下
在上面代码中添加这个即可实现
添加功能完成 ,我们也应该想想一下,删除书签该怎么操作,
所以下拉的时候,要判断,是已经有了标签,还是没有,有了就是要进行的是删除操作
添加一阶段,和释放阶段
释放的时候,判断 是否 当前为书签页,是则 删除,否则添加,
watch:{
offsetY(v){
if (!this.bookAvailable || this.menuVisible){
return;
}
if(v>=this.height && v<=this.threshold){//console.log("到达第二阶段");
this.beforeThreshold(v);
}else if(v>=this.threshold){// console.log("到达第三阶段");
this.afterThreshold(v);
}else if(v>0&&v<this.height){//第一阶段
this.beforeHeight(v);
}else if (v ===0 ){//状态为0时
this.restore()
}
},
isFixed(v){
// console.log(v);
}
},
methods:{
addBookmark(){
},
removeBookmark(){
},
//下拉的第一阶段
beforeHeight(){
if (this.isBookmark){
this.text = this.$t('book.pulldownDeleteMark'); //文字改为释放书签
this.color = BLUE;
this.isFixed = true;
} else{
this.text = this.$t('book.pulldownAddMark'); //文字改为释放书签
this.color = WHITE;
this.isFixed = false;
}
},
//下拉的第二阶段
beforeThreshold(v){
const iconDown = this.$refs.iconDown;
this.$refs.bookmark.style.top = `${-v}px`; //使书签吸顶
if (this.isBookmark){//当页是标签页
this.text = this.$t('book.pulldownDeleteMark'); //文字改为添加书签
this.color = BLUE;
} else {
this.text = this.$t('book.pulldownAddMark'); //文字改为添加书签
this.color = WHITE;
}
if(iconDown.style.transform === 'rotate(180deg)'){
iconDown.style.transform = ''
}
this.isFixed = false;
},
//下拉的第三阶段
afterThreshold(v){
const iconDown = this.$refs.iconDown;
this.$refs.bookmark.style.top = `${-v}px` //使书签吸顶
if (this.isBookmark){
this.text = this.$t('book.releaseDeleteMark'); //文字改为释放书签
this.color = WHITE;
this.isFixed = false;
} else{
this.text = this.$t('book.releaseAddMark'); //文字改为释放书签
this.color = BLUE;
this.isFixed = true;
}
if(iconDown.style.transform === ''){
iconDown.style.transform = 'rotate(180deg)'
}
},
//归为
restore(){
setTimeout(()=>{
this.$refs.bookmark.style.top = `${-this.height}px`;
this.$refs.bookmark.style.transform = 'rotate(0deg)';
},200);
if (this.isFixed){
console.log("isFixed");
this.setIsBookmark(true);
this.addBookmark();
}else {
this.setIsBookmark(false);
console.log("nofixed");
this.removeBookmark();
}
}
},
5-6 书签功能实现
添加书签
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.push({
cfi:currentLocation.start.cfi,
text:text
})
saveBookmark(this.fileName,this.bookmark);
})
/**@@@ 1.console.log(currentLocation)
* end: {index: 8, href: "A468350_1_En_5_Chapter.html", cfi: "epubcfi (/6/18[A468350_1_En_5_Chapter]!/4/12/6[Sec3]/10[Par20]/3:426)", displayed: {…}, location: 235, …}
* start: {index: 8, href: "A468350_1_En_5_Chapter.html", cfi: "epubcfi(/6/18[A468350_1_En_5_Chapter]!/4/12/6[Sec3]/8[Par19]/1:680)", displayed: {…}, location: 234, …}
__proto__: Object
2.console.log(cfibase) --> epubcfi(/6/18[A468350_1_En_5_Chapter]
3.console.log(cfistart) --> /4/12/6[Sec3]/8[Par19]/1:680
4.console.log(range);Range {startContainer: text, startOffset: 680, endContainer: text, endOffset: 680,…}
5.console.log(cfiend) --> /4/12/6[Sec3]/10[Par20]/3:426
6.console.log(range.toString());//得到的是本页的内容
*/
},
删除 书签
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));
console.log(this.bookmark);
this.setIsBookmark(false);
}
},
接下来就要把书签渲染上去呢,过程和 目录一样
这是复制搜索列表的,稍微做了点修改
<template>
<div class="ebook-slide-marks">
<scroll class="slide-search-list"
:top="66"
:bottom="48"
>
<div class="slide-search-item" v-for="(item,index) in bookmark" :key="index" v-html="item.text"
@click="displaySearch(item.cfi)"
>
</div>
</scroll>
</div>
</template>
<script>
import {ebookMixin} from "./../../utils/mixin"
import {getBookmark} from "./../../utils/localStorage"
import scroll from "./../../components/common/Scroll"
export default {
name: "EbookSlideMarks",
mixins:[ebookMixin],
components:{scroll},
data(){
return {
bookmark:''
}
},
mounted(){
this.bookmark = getBookmark(this.fileName)
},
methods: {
displaySearch(target) {
this.display(target, () => {
this.hideMenuVisible();
})
}
}
}
</script>
<style scoped lang="scss">
@import "./../../assets/styles/global";
.ebook-slide-marks{
font-size: px2rem(12);
.slide-search-list{
width: 100%;
padding: 0 px2rem(15);
box-sizing: border-box;
.slide-search-item{
font-size: px2rem(14);
line-height: px2rem(16);
padding: px2rem(20) 0;
}
}
}
</style>
5-7 页眉和页脚功能实现
两个组件
<template>
<div class="ebook-header">
<span class="ebook-header-text">{{getSectionName}}</span>
</div>
</template>
<script>
import {ebookMixin} from "../../utils/mixin";
export default {
name: "EbookHeader",
mixins:[ebookMixin],
computed:{
getSectionName() {
console.log("草泥马");
if (this.section) {
if (this.currentBook){
const section = this.currentBook.section(this.section)
if (section && section.href && this.currentBook && this.currentBook.navigation) {
// return this.currentBook.navigation.get(section.href).label
return this.navigation[this.section].label
}
}
}else {
console.log("草222泥马");
}
}
}
}
</script>
<style scoped lang="scss">
@import "./../../assets/styles/global";
.ebook-header{
position: absolute;
top: 0;
left: 0;
z-index: 100;
width: 100%;
height: px2rem(48);
padding: 0 px2rem(15);
box-sizing: border-box;
overflow: hidden;
@include left;
.ebook-header-text{
font-size: px2rem(12);
color: #6d7178;
}
}
</style>
<template>
<div class="ebook-footer">
<span class="ebook-footer-text">{{progress}}%</span>
</div>
</template>
<script>
import {ebookMixin} from "../../utils/mixin";
export default {
name: "EbookFooter",
mixins:[ebookMixin],
}
</script>
<style scoped lang="scss">
@import "./../../assets/styles/global";
.ebook-footer{
position: absolute;
bottom: 0;
left: 0;
z-index: 100;
width: 100%;
height: px2rem(48);
padding: 0 px2rem(15);
box-sizing: border-box;
@include right;
.ebook-footer-text{
font-size: px2rem(12);
color: #6d7178;
}
}
</style>
5-10 自适应布局优化(PC端布局优化)
主要修改这里
已经把这里改为弹性布局
5-11 自适应布局优化(书签支持鼠标事件)
touch那一套 对pc是不管用的
所以需要另外添加 mouse事件
//1.鼠标进入
// 2.鼠标进入后移动
// 3.鼠标从移动转台松手
// 4.鼠标还原
onMouseEnter(e){
this.mouseState = 1;
e.preventDefault();
e.stopPropagation();
},
onMouseMove(e){
if (this.mouseState === 1){
this.mouseState = 2;
} else if(this.mouseState ===2){
let offsetY = 0;
if (this.firstOffsetY){
offsetY = e.clientY - this.firstOffsetY
this.setOffsetY(offsetY)
} else{
this.firstOffsetY = e.clientY;
}
}
e.preventDefault();
e.stopPropagation();
},
onMouseEnd(e){
if (this.mouseState ===2){
this.setOffsetY(0);
this.firstOffsetY =null;
this.mouseState = 3;
}else{
this.mouseState = 4;
}
e.preventDefault();
e.stopPropagation();
},
onMaskClick(e){
if (this.mouseState && (this.mouseState === 2 || this.mouseState ===3)){
return;
}
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.toggleTitleAndMenu();
}
},