手动撸一个移动端音频播放组件

1.src/components/common/player/variable.scss

// 颜色定义规范
$color-background: #F2F3F4;
$color-background-d: rgba(0, 0, 0, 0.3);
$color-highlight-background: rgb(253, 108, 98);
$color-dialog-background: rgb(204, 204, 204);
$color-theme: rgb(212, 68, 57);
$color-theme-l: rgba(185, 9, 1, 0.2);
$color-theme-g: rgb(219, 219, 219);
$color-theme-d: rgba(19, 19, 19, 0.6);
$color-sub-theme: rgb(240, 116, 107);
$color-text: #2E3030;
$color-text-g: #757575; 
$color-text-ggg: #c7c7c7; 
$color-text-gg: rgb(219, 219, 219); 
$color-text-l: rgb(241, 241, 241);
$color-text-lm: rgb(228, 228, 228);
$color-text-ll: rgba(255, 255, 255, 0.8);

//字体定义规范
$font-size-small-ss: 9px;
$font-size-small-s: 10px;
$font-size-small: 11px;
$font-size-small-x: 12px;
$font-size-medium: 14px;
$font-size-medium-x: 16px;
$font-size-large-s: 17px;
$font-size-large: 18px;
$font-size-large-x: 22px;

2.src/components/common/player/dom.js

export function addClass (el, className) {
  if (hasClass(el, className)) {
    return
  }
  let newClass = el.className.split(' ')
  newClass.push(className)
  el.className = newClass.join(' ')
}

export function hasClass (el, className) {
  let reg = new RegExp('(^|\\s)' + className + '(\\s|$)')
  return reg.test(el.className)
}

export function getData (el, name, val) {
  const prefix = 'data-'
  name = prefix + name
  if (val) {
    // 如果有 val 就添加这个 val 到 dom 中
    // name="val"
    return el.setAttribute(name, val)
  } else {
    // 没有 val ,就获取 dom 中的 name
    return el.getAttribute(name)
  }
}

// 能力检测
let elementStyle = document.createElement('div').style

let vendor = (() => {
  // 定义游览器前缀
  let transformNames = {
    webkit: 'webkitTransform',
    Moz: 'MozTransform',
    O: 'OTransform',
    ms: 'msTransform',
    standard: 'transform'
  }

  // 遍历前缀,如果游览器支持的话,就返回对应 key
  for (let key in transformNames) {
    if (elementStyle[transformNames[key]] !== undefined) {
      return key
    }
  }

  // 如果都不支持,那肯定是有问题的,返回 false
  return false
})()

export function prefixStyle (style) {
  if (vendor === false) {
    return false
  }
  // 如果 vendor 为标准,就不改变 style
  if (vendor === 'standard') {
    return style
  }

  // 否则返回 vender(也就是 webkit Moz O ms 中的一个) + 样式首字母大写
  // 例如:webkit + transform ---> webkitTransform
  return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}

3.components/common/progress-bar.vue

<template>
  <div class="progress-bar" ref="progressBar" @click="progressClick">
    <div class="bar-inner">
      <div class="progress" ref="progress"></div>
      <div class="progress-btn-wrapper" ref="progressBtn"
      @touchstart.prevent="progressTouchStart"
      @touchmove.prevent="progressTouchMove"
      @touchend.prevent="progressTouchEnd">
        <div class="progress-btn">
          <div class="inner-play-btn"></div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import {prefixStyle} from './dom'
const progressBtnWidth = 15
const transform = prefixStyle('transform')

export default {
  name: 'progressBar',
  data () {
    return {
      newPercent: 0
    }
  },
  props: {
    percent: {
      type: Number,
      default: 0
    }
  },
  created () {
    this.touch = {}
  },
  methods: {
    progressClick (e) {
      // 这个有 bug
      // this._offset(e.offsetX)
      const rect = this.$refs.progressBar.getBoundingClientRect()
      // rect.left 元素距离左边的距离
      // e.pageX 点击距离左边的距离
      const offsetWidth = e.pageX - rect.left
      // console.log(rect, e.pageX)
      this._offset(offsetWidth)
      const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
      const percent = this.$refs.progress.clientWidth / barWidth
      this.$emit('percentChangeEnd', percent)
    },
    progressTouchStart (e) {
      this.touch.initiated = true
      this.touch.startX = e.touches[0].pageX
      this.touch.left = this.$refs.progress.clientWidth
    },
    progressTouchMove (e) {
      if (!this.touch.initiated) {
        return
      }
      this._triggerPercent()
      const deltaX = e.touches[0].pageX - this.touch.startX
      const offsetWidth = Math.min(Math.min(this.$refs.progressBar.clientWidth - progressBtnWidth, Math.max(0, this.touch.left + deltaX)))
      this._offset(offsetWidth)
    },
    progressTouchEnd (e) {
      this.touch.initiated = false
      const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
      const percent = this.$refs.progress.clientWidth / barWidth
      this.$emit('percentChangeEnd', percent)
    },
    _triggerPercent () {
      const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
      const percent = this.$refs.progress.clientWidth / barWidth
      this.$emit('percentChange', percent)
    },
    _offset (offsetWidth) {
      this.$refs.progress.style.width = `${offsetWidth}px`
      this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)`
    }
  },
  watch: {
    percent (newPercent) {
      if (newPercent >= 0 && !this.touch.initiated) {
        const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
        
        if(newPercent<=1){
          const offsetWidth = newPercent * barWidth;
          var u = navigator.userAgent;
          if (u.indexOf("Android") > -1 || u.indexOf("Adr") > -1) {
            this._offset(offsetWidth+5);
          }else{
            this._offset(offsetWidth);
          }
          
        }
      }
    }
  }
}
</script>

<style scoped lang="scss">
@import "./variable";
.progress-bar {
  height: 20px;
  width: 100%;
  .bar-inner {
    position: relative;
    top: 10px;
    height: 3px;
    background: rgba(0, 0, 0, 0.3);
    width: 245px;
    .progress {
      position: absolute;
      height: 100%;
      background: $color-theme;
    }
    .progress-btn-wrapper {
      position: absolute;
      left: -8px;
      top: -13px;
      width: 30px;
      height: 30px;
      .progress-btn {
        position: relative;
        top: 7px;
        left: 5px;
        box-sizing: border-box;
        width: 15px;
        height: 15px;
        border: 4px solid $color-theme-l;
        border-radius: 50%;
        .inner-play-btn{
          position: absolute;
          top: -1px;
          left: -1px;
          height: 9px;
          width: 9px;
          border-radius: 50%;
          background: $color-theme;
        }
      }
    }
  }
}
</style>

4.use.vue

<style lang="scss" scoped>
.evening-broadcasting{
    background: #f7f7f7;
    padding: 10px 14px;
    .broadcasting{
        background: #fff;
        padding: 11px 14px 18px 15.5px;
        .broadcasting-top{
            border-bottom: 1px solid #eee;
            .play-box{
                border-radius: 50%;
                width: 42px;
                height: 42px;
                margin-top: 15px;
                .play-button{
                    width: 42px;
                    height: 42px;
                }
            }
            .play-content{
                box-sizing: border-box;
                padding-left: 19px;
                width: 270px;
                overflow: hidden;
                .play-date{
                    font-size: 16px;
                }
                .play-title{
                    font-size: 12px;
                    color: #999999;
                    padding-top: 2px;
                }
                .play-control{
                    // width: 270px;
                    width: 100%;
                    #play-audio{
                        width: 100%;
                    }
                    .timingLength{
                        padding-top: 3px;
                        padding-bottom: 12px;
                        .time-l{
                            font-size: 11px;
                            color:#666;
                        }
                        .time-r{
                            font-size: 11px;
                            color:#999;
                        }
                    }
                }
            }
        }
        .broadcasting-bottom{
            padding-top: 13px;
            .broadcasting-bottom-title{
                color: #383838;
                font-size: 16px;
            }
            .broadcasting-bottom-image{
                height: 20px;
                width: 25px;
                padding:0 5px 5px 5px;
            }
            .broadcasting-bottom-abs{
                padding-top: 13px;
                .broadcasting-bottom-abs-item{
                    font-size: 13px;
                    color: #666;
                    line-height: 1.4;
                }
            }
        }
    }
}
</style>

<template>
    <div class="evening-broadcasting">
        <div class="broadcasting">
            <div class="broadcasting-top clearfix">
                <div class="play-box left" @click="transPlayStatus">
                    <img src="../image/播放.png" class="play-button" @touchstart="play" v-if="!isPlay">
                    <img src="../image/暂停.png" class="play-button" @touchstart="pause" v-else>
                </div>
                <div class="play-content left">
                    <div class="play-date">
                        {{broadTitle}}
                    </div>
                    <div class="play-title">
                        晚间播报
                    </div>
                    <div class="play-control">
                        <audio id="audio" :src="audioUrl" ref="audio" @timeupdate="updateTime">该浏览器不支持audio属性</audio>
                        <progress-bar id="play-audio" :percent="percent" @percentChangeEnd="percentChangeEnd" @percentChange="percentChange"></progress-bar>
                        <div class="clearfix timingLength">
                            <span class="time time-l left" v-if="timeLoading">{{format(currentTime)}}</span>
                            <span class="time time-l left" v-else>-:--</span>
                            <span class="time time-r right" v-if="timeLoading">{{format(duration)}}</span>
                            <span class="time time-r right" v-else>-:--</span>
                        </div>
                    </div>
                </div>
            </div>
            <div class="broadcasting-bottom">
                <div class="clearfix">
                    <div class="left broadcasting-bottom-title">新闻概览</div>
                    <div class="right broadcasting-bottom-image" @click="onCopyAbsList" v-if="!isWeixin">
                        <img src="../image/矢量智能对象.png" alt="">
                    </div>
                </div>
                <div class="broadcasting-bottom-abs">
                    <div v-for="(item,index) in newSList" class="broadcasting-bottom-abs-item word-wrap">
                        {{item|formatPoint|formatAbstractNumber}}
                    </div>
                </div>
            </div>
        </div>
        
    </div>
</template>
<script>
//Vuex公共状态辅助函数
import { mapState } from 'vuex'
//Vuex公共状态动作
import {mapActions} from 'vuex'
import ProgressBar from './progressBar'
import wx from 'weixin-js-sdk'
//执行成功
const ERROR_CODE = "ERRORCODE0000"; 
export default {
    name: 'player',
    props:{
        //晚间播报ID
        fieldid:{
            type:String,
        },
        //理财经理ID
        userid:{
            type:String,
        },
        //时间戳
        todaytime:{
            type:String
        },
    },
    data() {
        return {
            playStatus:true,
            audioUrl:'',
            isPlay: false, // 是否播放
            width: 0, // 视频音频的总时间的长度条
            percent: 0,
            currentTime: 0,// 播放时间
            duration: 0,// 视频音频的总时长
            playing: false,
            timeLoading:true,
            newSList:[],
            waitingTime:700,
            broadTitle:'',
            //微信端显示
            isWeixin:false,
        };
    },
    components: {
        ProgressBar,
    },
    computed:{
        //Vuex辅助函数,将公共状态中的属性映射成当前的computed中
        ...mapState(['newBroadCast']),
    },
    watch: {
        newBroadCast:{
            deep:true,
            handler:function(newV,oldV){
                //时长
                this.duration = this.newBroadCast.audioDuration;
                this.broadTitle=this.newBroadCast.broadTitle;
                if(this.duration>220){
                    this.waitingTime=2200;
                }
                else if(this.duration>200){
                    this.waitingTime=2000;
                }
                else if(this.duration>180){
                    this.waitingTime=1800;
                }
                else if(this.duration>120){
                    this.waitingTime=1200;
                }
                else if(this.duration>60){
                    this.waitingTime=1000;
                }else{
                    this.waitingTime=700;
                }
                this.audioUrl= this.newBroadCast.audioUrl;
                //摘要列表
                var getNewSList=this.newBroadCast.broadAbstract.split('。').filter(item => item);
                var getAbsList=[];
                for (var i = 0; i < getNewSList.length; i++) { 
                    if(getAbsList.join().length+getNewSList[i].length<200){
                        getAbsList.push(getNewSList[i]);
                    }
                }
                this.newSList=getAbsList;
            }
        },
        currentTime () {
            this.percent = this.currentTime / this.duration;
        },
    },
    methods: {
        //Vuex辅助函数,将公共状态中的方法映射成当前的this方法
        ...mapActions(['SET_NEWBROADCAST']),
        //拷贝摘要
        onCopyAbsList() {
            var u = navigator.userAgent;
            if (!!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {
                var titleSummaryParame={
                    title:'新闻概览',
                    summary:this.newSList.join('。')
                }
                window.webkit.messageHandlers.executeTitleSummary.postMessage(titleSummaryParame);
                app.executeTitleSummary('新闻概览',this.newSList.join('。'));    
            }
            else if (u.indexOf("Android") > -1 || u.indexOf("Adr") > -1) {
                Android.goNewsOverview('新闻概览',this.newSList.join('。'));
            }
        },
        format (interval) {
            interval = interval | 0
            let minute = interval / 60 | 0
            let second = interval % 60
            if (second < 10) {
                second = '0' + second
            }
            return minute + ':' + second
        },
        percentChange (percent) {
            const currentTime = this.duration * percent
            this.currentTime = currentTime
        },
        percentChangeEnd (percent) {
            const currentTime = this.duration * percent
            this.$refs.audio.currentTime = currentTime
            if (!this.playing) {
                this.$refs.audio.play()
                this.playing=true;
                this.isPlay=true;
            }
        },
        updateTime (e) {
            this.currentTime = e.target.currentTime;
            if (this.audio.ended) {
                this.isPlay = false
            }
        },
        transPlayStatus(){
            this.playStatus=!this.playStatus;
        },
        // 播放
        play () {
            if(this.currentTime==0){
                var u = navigator.userAgent;
                if (!!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {
                    this.audio.load();
                    this.timeLoading=false;
                    setTimeout(() => {
                        this.timeLoading=true;
                    }, this.waitingTime);
                    this.audio.play();
                    this.isPlay = true
                }
                else if (u.indexOf("Android") > -1 || u.indexOf("Adr") > -1) {
                    this.audio.load()
                    this.timeLoading=false;
                    this.isPlay = true
                    setTimeout(() => {
                        this.timeLoading=true;
                        this.audio.play();
                    }, this.waitingTime);
                }
            }else{
                this.isPlay = true;
                this.audio.currentTime = this.currentTime;//重新播放
                this.audio.play();
            }
            
        },
        // 暂停
        pause () {
            this.audio.pause()
            this.isPlay = false
        },
    },
    created(){
        //判断微信环境
        var ua = navigator.userAgent.toLowerCase(); 
        if (ua.match(/MicroMessenger/i) == "micromessenger" && this.isWXApplet!=1) {
            this.isWeixin=true;
        }
        let parame1 = {
            "functionName": "newBroadCastService",
            "methodName": "selectDetailById",
            "data": {
                "interfaceType":"1",
                "id":this.fieldid
            }
        };
        this.$post(JSON.stringify(parame1)).then(result => {
            if (result.errorCode == ERROR_CODE && result.data.codeType == 200) {
                console.log('晚间播报',result);
                this.SET_NEWBROADCAST(result.data.newBroadCase[0]);
                //时长
                this.duration = result.data.newBroadCase[0].audioDuration;
                this.broadTitle=result.data.newBroadCase[0].broadTitle;
                if(this.duration>180){
                    this.waitingTime=1600;
                }
                if(this.duration>120){
                    this.waitingTime=1300;
                }
                else if(this.duration>60){
                    this.waitingTime=1000;
                }else{
                    this.waitingTime=700;
                }
                this.audioUrl= result.data.newBroadCase[0].audioUrl;
                //摘要列表
                var getNewSList=result.data.newBroadCase[0].broadAbstract.split('。').filter(item => item);
                var getAbsList=[];
                for (var i = 0; i < getNewSList.length; i++) { 
                    if(getAbsList.join().length+getNewSList[i].length<200){
                        getAbsList.push(getNewSList[i]);
                    }
                }
                this.newSList=getAbsList;
            }
        });

    },
    mounted(){
        this.audio = this.$refs.audio;
        this.audio.load();
    },
}
</script>

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值