商业级web阅读器项目(下下)

23 篇文章 0 订阅
19 篇文章 1 订阅
本文详细介绍了商业级web阅读器的全文搜索功能开发,包括搜索算法、数组降维、关键字高亮和搜索结果展示。同时,讲解了书签功能的实现,包括手势操作、下拉状态管理和添加删除交互。此外,还讨论了阅读器的自适应布局优化,以确保在PC端的良好体验。
摘要由CSDN通过智能技术生成

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();
                }
            },
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值