标题听书模块

在书本详情页进入播放发器
播放器现实科大讯飞播放的数量:
原因是播放器是在线解析只能解析1000个字符,超过了就不能在线解析了
播放器api

听书组件开发

1到store中创建storeSpeakit组件,
2添加路由
3复制听书方法到utils中的store.js中

export function flatBookList(bookList) {
  if (bookList) {
    let orgBookList = bookList.filter(item => {
      return item.type !== 3
    })
    const categoryList = bookList.filter(item => {
      return item.type === 2
    })
    categoryList.forEach(item => {
      const index = orgBookList.findIndex(v => {
        return v.id === item.id
      })
      if (item.itemList) {
        item.itemList.forEach(subItem => {
          orgBookList.splice(index, 0, subItem)
        })
      }
    })
    orgBookList.forEach((item, index) => {
      item.id = index + 1
    })
    orgBookList = orgBookList.filter(item => item.type !== 2)
    return orgBookList
  } else {
    return []
  }
}

export function findBook(fileName) {
  const bookList = getLocalStorage('shelf')
  return flatBookList(bookList).find(item => item.fileName === fileName)
}

4.把api也加进来

export function flatList() {
  return axios({
    method: 'get',
    url: `${process.env.VUE_APP_BOOK_URL}/book/flat-list`
  })
}

5在components中添加文件夹,里面有三个组件
6.添加一个新的环境变量

VUE_APP_VOICE_URL=http://47.99.166.157:3000

7.书详情页上没有还没进行路由跳转,到StoreDetail中添加方法。跳转到听书页面
需要把存储文件中getLocalForage方法引入

trialListening() {
		//获取缓存中的电子书,第一个参数是否报错,第二个参数是实际的值(电子书)
        getLocalForage(this.bookItem.fileName, (err, blob) => {
        	//blob instanceof Blob表示Blob是blob 函数的子类
          if (!err && blob && blob instanceof Blob) { //判断不存在时,离线解析
            this.$router.push({  //进行路由跳转
              path: '/store/speaking',
              query: {
                fileName: this.bookItem.fileName
              }
            })
          } else { //不然的话就是在线解析电子书
            this.$router.push({
              path: '/store/speaking',
              query: {
                fileName: this.bookItem.fileName,
                opf: this.opf
              }
            })
          }
        })
      },

分析如何实现听书功能

一.听书组件中列表前的动画实现组件
分析:1.通过for生成多个小树线,长度
2.竖线是通过styles生成的,通过外部传入的number来竖线数量
3.它的高度是通过random方法随机生成一个整数,给styles中方法给rem调用,每次点击高度都是不一样的
4.提供startAnimation方法和stopAnimation方法
//点击后按200毫秒更新一次高度,高度更新也是随机的
stopAnimation就是把startAnimation中的定时器关闭

<template>
  <div class="playing-item-wrapper">
    <div class="playing-item" :style="item" v-for="(item, index) in styles" :key="index" ref="playingItem"></div>
  </div>
</template>

<script>
  import { px2rem } from '@/utils/utils'

  export default {
    props: {
      number: Number
    },
    computed: {
      styles() {
        const styles = new Array(this.number)
        for (let i = 0; i < styles.length; i++) {
          styles[i] = {
            height: px2rem(this.random()) + 'rem'
          }
        }
        return styles
      }
    },
    methods: {
      startAnimation() {//点击后按200毫秒更新一次高度,高度更新也是随机的
        this.task = setInterval(() => {
          this.$refs.playingItem.forEach(item => {
            item.style.height = px2rem(this.random()) + 'rem'
          })
        }, 200) 
      },
      stopAnimation() {
        if (this.task) {
          clearInterval(this.task)
        }
      },
      random() {  //高度的生成
        return Math.ceil(Math.random() * 10)
      }
    }
  }
</script>

<style lang="scss" rel="stylesheet/scss" scoped>
  @import "../../assets/styles/global";

  .playing-item-wrapper {
    @include center;
    .playing-item {
      flex: 0 0 px2rem(2);
      width: px2rem(2);
      height: px2rem(1);
      background: $color-blue;
      margin-left: px2rem(2);
      transition: all .2s ease-in-out;
      &:first-child {
        margin: 0;
      }
    }
  }
</style>

audio语音播放器控件

分析:1.现在是没有实际存在的,加入controls='controls’就会默认浏览器的播放器,去掉就会引入一个播放器,但是没有在页面上现实

speakBottom组件播放器组件

分析:1.点击播放按钮时调用togglePlay,里面调用父组件中的操作方法

togglePlay() {
        this.$parent.togglePlay()
      },
  //父组件中的方法    
 togglePlay() {
        if (!this.isPlaying) { //判断是否处于播放状态
          if (this.playStatus === 0) { //么有播放就执行播放
            this.play()
          } else if (this.playStatus === 2) { //2.如果是暂停状态
            this.continuePlay() //
          }
        } else { //播放就暂停播放
          this.pausePlay()
        }
      },
      //暂停播放
   pausePlay() {
   		//通过ref调用播放器中的内置方法pause,进行暂停
        this.$refs.audio.pause()
        //找到正在播放的动画,进行暂停
        this.$refs.speakPlaying[0].stopAnimation()
        //播放状态设false
        this.isPlaying = false
        //playStatus 在data中定义好了,播放状态0未播放,1播放中,2暂停
        this.playStatus = 2
      },
      //初次播放
     // paragraph是在其他方中被调用了,进行更新的在speak调用了
      play() {
      	//
        this.createVoice(this.paragraph)
      },
      //播放方法
      createVoice(text) {
      	//调用XMLHttpRequest,进行http请求
        const xmlhttp = new XMLHttpRequest()
        //toLowerCase播放的语种,false采用同步方式请求
        xmlhttp.open('GET', `${process.env.VUE_APP_VOICE_URL}/voice?text=${text}&lang=${this.lang.toLowerCase()}`, false)
        xmlhttp.send()  //发送
        const xmlDoc = xmlhttp.responseText  //进行响应请求
        if (xmlDoc) {
          const json = JSON.parse(xmlDoc) //解析
          if (json.path) {  //下载
            this.$refs.audio.src = json.path //播发的文件路径,框架会自行下载
            this.continuePlay() //播放
          } else {
            this.showToast('播放失败,未生成链接')
          }
        } else {
          this.showToast('播放失败')
        }
        /*
        axios.create({
          baseURL: process.env.VUE_APP_VOICE_URL + '/voice'
        })({
          method: 'get',
          params: {
            text: text,
            lang: this.lang.toLowerCase()
          }
        }).then(response => {
          if (response.status === 200) {
            if (response.data.error === 0) {
              const downloadUrl = response.data.path
              console.log('开始下载...%s', downloadUrl)
              downloadMp3(downloadUrl, blob => {
                const url = window.URL.createObjectURL(blob)
                console.log(blob, url)
                this.$refs.audio.src = url
                this.continuePlay()
              })
            } else {
              this.showToast(response.data.msg)
            }
          } else {
            this.showToast('请求失败')
          }
        }).catch(err => {
          console.log(err)
          this.showToast('播放失败')
        })
        */
      },
      //播放
      continuePlay() {
        this.$refs.audio.play().then(() => {
          this.$refs.speakPlaying[0].startAnimation()
          this.isPlaying = true
          this.playStatus = 1
        })
      },
     // 初次点击播放,拿到文本,进行播放
      speak(item, index) {
        this.resetPlay() //进行第一次播放
        this.playingIndex = index //目录索引
        this.$nextTick(() => {  //刷新滚动条
          this.$refs.scroll.refresh()
        })
        if (this.chapter) { //章节是否存在
        	//获取章节
          this.section = this.book.spine.get(this.chapter.href)
          this.rendition.display(this.section.href).then(section => {
          	//获取位置信息
            const currentPage = this.rendition.currentLocation()
            //看页面到底要现实多少文本和内容
            const cfibase = section.cfiBase
            const cfistart = currentPage.start.cfi.replace(/.*!/, '').replace(/\)/, '')
            const cfiend = currentPage.end.cfi.replace(/.*!/, '').replace(/\)/, '')
            this.currentSectionIndex = currentPage.start.displayed.page
            this.currentSectionTotal = currentPage.start.displayed.total
            const cfi = `epubcfi(${cfibase}!,${cfistart},${cfiend})`
            // console.log(currentPage, cfi, cfibase, cfistart, cfiend)
            //拿到cfi就可以左字符的转译
            this.book.getRange(cfi).then(range => {
              let text = range.toLocaleString()
              text = text.replace(/\s(2,)/g, '')
              text = text.replace(/\r/g, '')
              text = text.replace(/\n/g, '')
              text = text.replace(/\t/g, '')
              text = text.replace(/\f/g, '')
              this.updateText(text)
            })
          })
        }
      },
     //查找章节是否存在
	chapter() {
        return this.flatNavigation[this.playingIndex]
      },

播发器面板源码分析

分析:左侧是一个播放按钮,
当前播放时长和总时长获取
在SpeakBottom组件中,在props中接收

props: {
      chapter: Object,// 章节
      currentSectionIndex: Number,  //当前章节
      currentSectionTotal: Number, //总章节
      showPlay: Boolean,  //是否显示面板
      isPlaying: Boolean, //是否正在播放
      playInfo: Object  //传递的是播放信息
    },
    
//父组件中的代码
 playInfo() {
        if (this.audioCanPlay) {  //进入播放转态
          return {
            currentMinute: this.currentMinute,  //当前播放的分钟
            currentSecond: this.currentSecond,  //当前播放的总时间
            totalMinute: this.totalMinute,  //总分钟
            totalSecond: this.totalSecond,  //总秒数
            leftMinute: this.leftMinute,  //剩余的分钟
            leftSecond: this.leftSecond  //剩余的秒
          }
        } else {
          return null
        }
      },
      
      //传入的事件
	      onCanPlay() {
        this.audioCanPlay = true //播放控件
        this.currentPlayingTime = this.$refs.audio.currentTime //播放时间
        this.totalPlayingTime = this.$refs.audio.duration  //
      },
	//时间的转换
	 currentMinute() {
        const m = Math.floor(this.currentPlayingTime / 60)
        return m < 10 ? '0' + m : m
      },
      currentSecond() {
        const s = Math.floor(this.currentPlayingTime - parseInt(this.currentMinute) * 60)
        return s < 10 ? '0' + s : s
      },
     	//剩余时长 = 总时长-
      leftMinute() {
        const m = Math.floor((this.totalPlayingTime - this.currentPlayingTime) / 60)
        return m < 10 ? '0' + m : m
      },
      leftSecond() {
        const s = Math.floor((this.totalPlayingTime - this.currentPlayingTime) - parseInt(this.leftMinute) * 60)
        return s < 10 ? '0' + s : s
      },

播放过程中时间的更新

//播放过程中时间的更新
   onTimeUpdate() {
   		//当前播放时间
        this.currentPlayingTime = this.$refs.audio.currentTime
        //保存一个百分比
        const percent = Math.floor((this.currentPlayingTime / this.totalPlayingTime) * 100)
        //
        this.$refs.speakWindow.refreshProgress(percent)
      },

SMinsd大播放面板分析
分析:需要把书的一些信息传入

//滚动条的变化
  onProgressInput(progress) {
        this.progress = progress
        this.$refs.progress.style.backgroundSize = `${this.progress}% 100%`
      },
      //刷新进度百分比
   refreshProgress(p) {
        this.progress = p
        this.$refs.progress.style.backgroundSize = `${this.progress}% 100%`
      },

播放完成后停止,在ssk组件中

<audio @canplay="onCanPlay"
             @timeupdate="onTimeUpdate"
             @ended="onAudioEnded"
             ref="audio"></audio>
   //播放重置          
  onAudioEnded() {
        this.resetPlay()
        //播放结束了,更新播放时间
        this.currentPlayingTime = this.$refs.audio.currentTime
        //百分比
        const percent = Math.floor((this.currentPlayingTime / this.totalPlayingTime) * 100)
        //刷新进度条
        this.$refs.speakWindow.refreshProgress(percent)
      },