实现一个audio模拟器

audio自带的样式一般满足不了ui需求,所以往往需要自己去实现。原理是将audio隐藏,监听audio事件,实现模拟播放效果。

audio属性的值可以通过父组件传值过来,此代码可以封装成一个组件。

<template>
  <div class="audio-wrapper">
    <audio
      controls="controls"
      class="audio1"
      :src="url"
      :preload="audio.preload"
      @play="onPlay"
      @error="onError"
      @pause="onPause"
      @timeupdate="onUpdateTime"
      @loadedmetadata="onLoadedmetadata"
      muted="muted"
      autoplay="autoplay"
      ref="audio"
    ></audio>
    <div class="fh-progress-wrapper">
      <span class="fh-time fh-time-l">{{ audio.currentTime | formatSecond}}</span>
      <div class="fh-progress-bar-wrapper">
        <div class="fh-progress-bar" ref="progressBar" @click="progressClick">
          <div class="fh-bar-inner">
            <div class="fh-progress" ref="progress"></div>
            <div
              class="fh-progress-btn-wrapper"
              ref="progressBtn"
              @touchstart.prevent="progressTouchStart"
              @touchmove.prevent="progressTouchMove"
              @touchend="progressTouchEnd"
            >
              <div class="fh-progress-btn"></div>
            </div>
          </div>
        </div>
      </div>
      <span class="fh-time fh-time-r">{{ audio.maxTime | formatSecond }}</span>
    </div>
    <div class="audio-control">
      <span>
        <!-- 上一首 -->
      </span>
      <div @click="startPlayOrPause">
        {{ audio.playing | transPlayPause }}
      </div>
      <span>
        <!-- 下一首 -->
      </span>
    </div>
  </div>
</template>
<script>
export default {
  name: 'index',

  data() {
    return {
      url: 'https://wdd.js.org/element-audio/static/falling-star.mp3',
      audio: { //可通过父组件传值过来
        currentTime: 0, // 当前播放时间
        maxTime: 0, // 播放总时间
        playing: false, // 播放状态
        waiting: true,// 是否需要等待
        preload: 'auto'
      },
      progressBtnWidth: 16,// 滑动进度图标宽度

      touch: {
        initiated: false, //是否滑动
        startX: 0, //当前拖动点X轴位置
        left: 0, //当前进度条位置
      } // 滑动图标时记录的参数
    };
  },
  computed: {
    percent() {
      return this.audio.currentTime / this.audio.maxTime;
    },
  },
  watch: {
    percent(newPrecent) {
      // 非滑动进度图标时间内
      if (newPrecent >= 0 && !this.touch.initiated) {
        // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
        const barWidth = this.$refs.progressBar.clientWidth - this.progressBtnWidth;
        // 进度滑动条宽度 = 默认滑动条实际宽度 * 播放百分比
        const offsetWidth = newPrecent * barWidth;
        // 进度滑动条偏移和滑动进度图标偏移
        this._offset(offsetWidth);
      }
    }
  },
  filters: {
    transPlayPause(value) {
      return value ? '暂停' : '播放';
    },
    // 将秒转化成倒计时
    formatSecond(second) {
      let secondType = typeof second;
      if (secondType === 'number' || secondType === 'string') {
        second = parseInt(second);
        const hours = Math.floor(second / 3600);
        second = second - hours * 3600;
        const mimute = Math.floor(second / 60);
        second = second - mimute * 60;
        return (
          hours +
          ':' +
          ('0' + mimute).slice(-2) +
          ':' +
          ('0' + second).slice(-2)
        );
      } else {
        return '0:00:00';
      }
    },

  },
  methods: {
    // 当音频开始播放
    onPlay(res) {
      console.log(res)
      this.audio.playing = true;
    },
    onError () {
      this.audio.waiting = true
    },
    // 切换音频状态
    startPlayOrPause() {
      return this.audio.playing ? this.pausePlay() : this.startPlay();
    },
    // 开始播放
    startPlay() {
      this.$refs.audio.play();
    },
    // 暂停
    pausePlay() {
      this.$refs.audio.pause();
    },
    // 当音频暂停
    onPause() {
      this.audio.playing = false;
    },
    // 当currentTime更新时会触发timeupdate事件
    onUpdateTime(e) {
      this.audio.currentTime = e.target.currentTime;
    },
    // 当加载语音流元数据完成后,会触发该事件的回调函数
    // 语音元数据主要是语音的长度之类的数据
    onLoadedmetadata(res) {
      this.audio.waiting = false;
      this.audio.maxTime = parseInt(res.target.duration);

      this.autoplay = 'autoplay'
      if (this.audio.currentTime > 0) {
        this.$refs.audio.currentTime = this.audio.currentTime;
      }
       if (this.audio.playing) {
        this.startPlay()
      }
    },
    progressTouchStart(e) {
      this.touch.initiated = true; // 标志位 表示初始化
      this.touch.startX = e.touches[0].pageX; // 当前拖动点X轴位置
      this.touch.left = this.$refs.progress.clientWidth; // 当前进度条位置
    },
    progressTouchMove(e) {
      if (!this.touch.initiated) {
        return;
      }
      // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
      const barWidth = this.$refs.progressBar.clientWidth - this.progressBtnWidth;
      const deltaX = e.touches[0].pageX - this.touch.startX; // 拖动偏移量
      // 滑动中进度滑动条和滑动进度图标的当前位置
      const offsetWidth = Math.min(
        barWidth,
        Math.max(0, this.touch.left + deltaX)
      );
      this._offset(offsetWidth);
    },
    progressTouchEnd() {
      this.touch.initiated = false;
      this._triggerPercent();
    },
    progressClick(e) {
      // getBoundingClientRect用于获取某个元素相对于视窗的位置集合
      const rect = this.$refs.progressBar.getBoundingClientRect();
      // pageX() 属性是鼠标指针的位置,相对于文档的左边缘。
      const offsetWidth = e.pageX - rect.left;
      // 进度滑动条偏移和滑动进度图标偏移
      this._offset(offsetWidth);
      // 计算当前播放时间
      this._triggerPercent();
    },
    // 计算当前时间
    _triggerPercent() {
      // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
      const barWidth = this.$refs.progressBar.clientWidth - this.progressBtnWidth;
      // 当前播放比例 = 进度滑动条宽度 / 默认滑动条实际宽度
      const percent = this.$refs.progress.clientWidth / barWidth;
      // 当前播放时长 = 总时长 * 播放比例
      const currentTime = this.audio.maxTime * percent;
      // 设置音频当前播放时间
      this.$refs.audio.currentTime = currentTime;
    },
    // 计算拖动条的宽度和滑动偏移的距离
    _offset(offsetWidth) {
      this.$refs.progress.style.width = `${offsetWidth}px`; // 进度条偏移
      this.$refs.progressBtn.style.transform = `translate3d(${offsetWidth}px, 0, 0)`; // 小球偏移
    }
  }
};
</script>
<style lang="less" scoped>
.audio-wrapper {
  background-color: tomato;
}
.audio1 {
  margin: auto;
  display: none;
  width: 100%;
}
.fh-progress-wrapper {
  display: flex;
  align-items: center;
  width: 80%;
  margin: 0px auto;
  padding: 10px 0;

  .fh-time {
    color: #fff;
    font-size: 12px;
    flex: 0 0 30px;
    line-height: 30px;
    width: 60px;
    display: inline-block;
    padding: 0 5px;

    &.fh-time-l {
      text-align: left;
    }

    &.fh-time-r {
      text-align: right;
    }
  }

  .fh-progress-bar-wrapper {
    flex: 1;
  }
}
.fh-progress-bar {
  height: 30px;

  .fh-bar-inner {
    position: relative;
    top: 13px;
    height: 4px;
    background: rgba(0, 0, 0, 0.3);

    .fh-progress {
      position: absolute;
      height: 100%;
      background: #41b883;
    }

    .fh-progress-btn-wrapper {
      position: absolute;
      left: -7px;
      top: -13px;
      width: 30px;
      height: 30px;

      .fh-progress-btn {
        position: relative;
        top: 7px;
        left: 7px;
        box-sizing: border-box;
        width: 16px;
        height: 16px;
        border: 3px solid #fff;
        border-radius: 50%;
        background: #41b883;
      }
    }
  }
}
.audio-control {
  display: flex;
  justify-content: space-around;
  span {
    flex: 1;
    font-size: 12px;
    text-align: center;
  }
}
</style>

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值