vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue TS语音播放

Vue3+TS实现语音播放组件的示例代码

作者:叨唠

这篇文章主要介绍了如何利用Vue+TS实现一个简易的语音播放组件,文中的示例代码讲解详细,对我们学习Vue有一定的帮助,需要的可以参考一下

该功能将使用vue3 + TS来实现语音播放组件,使用什么技术不重要,重要的是看懂了核心逻辑后,通过原生js、react、vue2等都可以轻松实现

所涉及到重要点有以下几个:

(1)进度条的实现:拖拽进度条、点击进度条

(2)操作audio语音播放:通过js操作audio媒体

(3)播放进度与进度条紧密关联:播放的进度改变时,进度条也随之改变;进度条改变时,播放的进度也随之改变

效果图:

开始我们的设计吧!

第一步:点击拖拽进度条

进度条的css样式如下:

父元素设置灰色背景色,圆圈进行position定位,使用left百分比,同时黑色进度条的width也是百分比,这样圆圈的left值是多少,黑色进度条的width就是多少。

.slider-wrap {
      position: relative;
      display: flex;
      align-items: center;
      height: 4px;
      max-width: 194px;
      min-width: 36px;
      width: 194px;
      background-color: rgba(23, 23, 23, 0.15);
      cursor: pointer;
      .circle {
        position: absolute;
        width: 14px;
        height: 14px;
        background-color: #555555;
        border-radius: 100%;
        cursor: pointer;
        user-select: none;
        transform: translate(-50%);
      }
      .slider-bar {
        height: 4px;
        max-width: 200px;
        background-color: #555555;
      }
    }

先说拖拽圆圈,圆圈上绑定mousedown事件,根据事件e.target确定圆圈、黑色进度条、灰色父元素,三者的element。同时知道了圆圈当前的left值,比如30%,还知道了当前鼠标mousedown时,事件e.pageX,即鼠标mousedown时,距离页面左边的水平值,因为对比后续鼠标移动时,触发的mousemove事件的e.pageX可以判断移动了多少。同时还要知道灰色背景的父元素的width。因为鼠标移动的距离 / width 要赋值给圆圈的left。知道移动了%多少。

    const circleMousedown = (e) => {
      circleTarget.el = e.target; // 圆圈自身
      wrapEle.el = e.target.parentElement; // 圆圈父元素
      sliderBar.el = e.target.nextElementSibling; // 圆圈的兄弟节点
 
      circleTarget.circleLeft = e.target.style.left;
      circleTarget.pageX = e.pageX;
      circleTarget.circleMouseMouve = true;
      wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');
    };

然后,监听document文档的mousemove,注意鼠标是可以在整个文档上移动的,不过圆圈可不能在灰色父元素之外。移动的e.pageX - 鼠标mousedown时的e.pageX 就是鼠标水平移动的距离。超出最大值时,圆圈left设置为100%,小于最小值时,left设置为0%,总之left要在0%~100%之间,才能保证圆圈不超出到外面去。这样圆圈就可以随着鼠标移动了,同时黑色进度条的width值与圆圈的left值一样,所以黑色进度条的width也随着鼠标移动。

document.addEventListener('mousemove', (e) => {
      if (circleTarget.circleMouseMouve) {
        const nowLeft =
          parseFloat(circleTarget.circleLeft) +
          getPercentage(e.pageX - circleTarget.pageX, wrapEle.width);
        if (nowLeft >= 100) {
          circleDragLeft = '100%';
        } else if (nowLeft <= 0) {
          circleDragLeft = '0%';
        } else {
          circleDragLeft = `${nowLeft}%`;
        }
        updateProgressBar(circleDragLeft);
        currentTimeByProgressBar(circleDragLeft);
      }
    });
    document.addEventListener('mouseup', () => {
      circleTarget.circleMouseMouve = false;
    });

再说说点击父元素时,圆圈到指定位置

点击事件在灰色父元素上进行监听,注意e.target可不一定是灰色父元素,e.target表示鼠标点击到哪个元素,随后click冒泡到父元素上的。同样点击事件的e.pageX 可以确定鼠标点击的水平位置,转换为%值,设置圆圈的left值和黑色进度条的width值。

    // 只处理e.target是slider-wrap 或 slider-bar的情况
    const clickSliderWrap = (e) => {
      if (e.target.getAttribute('target') === 'wrap') {
        wrapEle.el = e.target;
        circleTarget.el = e.target.firstElementChild;
        sliderBar.el = e.target.lastElementChild;
      } else if (e.target.getAttribute('target') === 'sliderbar') {
        sliderBar.el = e.target;
        circleTarget.el = e.target.previousElementSibling;
        wrapEle.el = e.target.parentElement;
      } else {
        return;
      }
      wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');
 
      const styleLeft = `${getPercentage(e.offsetX, wrapEle.width)}%`;
      updateProgressBar(styleLeft);
      currentTimeByProgressBar(styleLeft);
    };

以上就可以实现进度条功能了。

第二步:操作媒体音频

获取audio的element,audioElement上面有play、pause等方法,还有currentTime播放进度时间,以及duration总时长。所以说HTML5的audio标签,上面的方法和属性还是非常直观的,这也正是web发展的一个特点,某个新的特性的产生,功能会很明了。

首先当媒体的第一帧加载完成时,我们就获取audio的element:(audio自身的loadeddata事件)

// 当媒体音频第一帧加载完成时
    const audioLoadeddata = (e) => {
      audioEl = e.target;
      audioData.duration = e.target.duration;
    };

其次,对播放中进行监听:(audio的timeupdate事件)

    // 播放进度:表示audio正在播放,currentTime在更新
    const audioTimeupdate = (e) => {
      audioData.currentTime = e.target.currentTime;
      progressBarBycurrentTime();
    };

最后,播放完成进行监听:(audio的ended事件)

    // 音频播放结束
    const audioEnded = () => {
      audioData.playing = false;
    };

如果对audio标签不是很熟悉,请参考文档

上述操作还是很简单的,audio上的属性、方法、事件都是非常简单明了且实用的。

第三步:进度条和播放进度关联

通过audio当前的播放时间 / 总时长,得到的%值,赋值给圆圈的left和黑色进度条的width。

通过圆圈的left值的% * 总时长,得到audio的当前播放时间。(audio的currentTime属性直接赋值,语音播放就会跳转到指定的时间进行播放,比如 1,就会从1秒的位置开始)

完整代码

<template>
  <div class="glowe-audio">
    <div class="audio">
      <div class="icon-div" @click="playPauseAudio">
        <video-play class="icon" v-if="!audioData.playing"></video-play>
        <video-pause class="icon" v-else></video-pause>
      </div>
      <div
        class="slider-wrap"
        :style="{ width: durationToWidth(audioData.duration) }"
        target="wrap"
        @click="clickSliderWrap"
      >
        <div class="circle" target="circle" style="left: 0%" @mousedown="circleMousedown"></div>
        <div class="slider-bar" target="sliderbar" style="width: 0%"></div>
      </div>
      <div class="time-wrap">
        <span class="time">{{ durationFormat(Math.round(audioData.duration)) }}</span>
      </div>
    </div>
    <audio
      :src="audioData.audiourl"
      preload="auto"
      @ended="audioEnded"
      @timeupdate="audioTimeupdate"
      @loadeddata="audioLoadeddata"
    ></audio>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import { VideoPause, VideoPlay } from '@element-plus/icons';
import { durationToFormat } from '@/utils/refactor';
 
export default defineComponent({
  name: 'GloweAudio',
  components: {
    VideoPlay,
    VideoPause,
  },
  props: {
    audioUrl: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    const audioData = reactive({
      audiourl: props.audioUrl,
      playing: false,
      duration: 0, // 音频总时长
      currentTime: 0, // 当前播放的位置
    });
    let audioEl: HTMLAudioElement | null = null;
    const wrapEle: {
      width: string;
      el: any;
    } = {
      width: '0px',
      el: null,
    };
    const sliderBar: {
      width: string;
      el: any;
    } = {
      width: '0%',
      el: null,
    };
    const circleTarget: {
      circleMouseMouve: boolean;
      pageX: number;
      circleLeft: string;
      el: any;
    } = {
      circleMouseMouve: false,
      pageX: 0,
      circleLeft: '0%',
      el: null,
    };
    let circleDragLeft = '0%'; // 圆圈被鼠标水平拖拽的距离(默认向左)
 
    document.addEventListener('mousemove', (e) => {
      if (circleTarget.circleMouseMouve) {
        const nowLeft =
          parseFloat(circleTarget.circleLeft) +
          getPercentage(e.pageX - circleTarget.pageX, wrapEle.width);
        if (nowLeft >= 100) {
          circleDragLeft = '100%';
        } else if (nowLeft <= 0) {
          circleDragLeft = '0%';
        } else {
          circleDragLeft = `${nowLeft}%`;
        }
        updateProgressBar(circleDragLeft);
        currentTimeByProgressBar(circleDragLeft);
      }
    });
    document.addEventListener('mouseup', () => {
      circleTarget.circleMouseMouve = false;
    });
    const circleMousedown = (e) => {
      circleTarget.el = e.target; // 圆圈自身
      wrapEle.el = e.target.parentElement; // 圆圈父元素
      sliderBar.el = e.target.nextElementSibling; // 圆圈的兄弟节点
 
      circleTarget.circleLeft = e.target.style.left;
      circleTarget.pageX = e.pageX;
      circleTarget.circleMouseMouve = true;
      wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');
    };
    // 只处理e.target是slider-wrap 或 slider-bar的情况
    const clickSliderWrap = (e) => {
      if (e.target.getAttribute('target') === 'wrap') {
        wrapEle.el = e.target;
        circleTarget.el = e.target.firstElementChild;
        sliderBar.el = e.target.lastElementChild;
      } else if (e.target.getAttribute('target') === 'sliderbar') {
        sliderBar.el = e.target;
        circleTarget.el = e.target.previousElementSibling;
        wrapEle.el = e.target.parentElement;
      } else {
        return;
      }
      wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');
 
      const styleLeft = `${getPercentage(e.offsetX, wrapEle.width)}%`;
      updateProgressBar(styleLeft);
      currentTimeByProgressBar(styleLeft);
    };
 
    // 播放或暂停音频
    const playPauseAudio = (e) => {
      const iconDiv = findParentsEl(e.target.parentElement, 'icon-div');
      wrapEle.el = iconDiv?.nextElementSibling;
      circleTarget.el = wrapEle.el.firstElementChild;
      sliderBar.el = wrapEle.el.lastElementChild;
      const parentAudio = findParentsEl(e.target.parentElement, 'audio');
      if (parentAudio) {
        if (!audioData.playing) {
          audioPlay();
        } else {
          audioPause();
        }
      }
    };
 
    // 计算百分比的分子
    function getPercentage(num: number | string, den: number | string): number {
      const numerator = typeof num === 'number' ? num : parseFloat(num);
      const denominator = typeof den === 'number' ? den : parseFloat(den);
      return Math.round((numerator / denominator) * 10000) / 100;
    }
    // 查找自身或最近的一个父元素有className的
    function findParentsEl(el: HTMLElement, classname: string): HTMLElement | null {
      // 注意avg className得到一个对象而非字符串
      if (el && el.className?.split && el.className.split(' ').includes(classname)) {
        return el;
      }
      if (el.parentElement) {
        if (el.parentElement.className.split(' ').includes(classname)) {
          return el.parentElement;
        } else {
          return findParentsEl(el.parentElement, classname);
        }
      }
      return null;
    }
    /**
     * 更新进度条
     * @param percentage 得到一个百分比的字符串
     */
    function updateProgressBar(percentage: string) {
      circleTarget.el.style.left = percentage;
      sliderBar.el.style.width = percentage;
    }
 
    /**
     * 以下是对音频的操作
     */
 
    // 音频播放结束
    const audioEnded = () => {
      audioData.playing = false;
    };
    // 播放进度:表示audio正在播放,currentTime在更新
    const audioTimeupdate = (e) => {
      audioData.currentTime = e.target.currentTime;
      progressBarBycurrentTime();
    };
    // 当媒体音频第一帧加载完成时
    const audioLoadeddata = (e) => {
      audioEl = e.target;
      audioData.duration = e.target.duration;
    };
 
    // 播放
    function audioPlay() {
      if (audioEl) {
        audioEl.play();
        audioData.playing = true;
      }
    }
 
    // 暂停播放
    function audioPause() {
      if (audioEl) {
        audioEl.pause();
        audioData.playing = false;
      }
    }
 
    // 进度条和音频播放进度进行关联
    function progressBarBycurrentTime() {
      const progress = getPercentage(audioData.currentTime, audioData.duration);
      updateProgressBar(`${progress}%`);
    }
    /**
     * 播放进度与进度条进行关联
     * @param stylePercentage 圆圈的left值
     */
    function currentTimeByProgressBar(styleLeft: string) {
      if (audioEl) {
        const currentTime = (parseFloat(styleLeft) / 100) * audioData.duration;
        audioEl.currentTime = currentTime;
        audioData.currentTime = currentTime;
      }
    }
 
    const durationFormat = (num: number): string => {
      return durationToFormat(num, 'm:ss');
    };
    const durationToWidth = (num: number): string => {
      return `${Math.ceil((158 / 58) * num + 33)}px`;
    };
    return {
      audioData,
      circleMousedown,
      clickSliderWrap,
      playPauseAudio,
      audioEnded,
      audioTimeupdate,
      audioLoadeddata,
      durationFormat,
      durationToWidth,
    };
  },
});
</script>
<style scoped lang="scss">
.glowe-audio {
  .audio {
    display: flex;
    justify-content: space-evenly;
    align-items: center;
    max-width: 308px;
    height: 48px;
    .icon-div {
      width: 20px;
      height: 20px;
      border-radius: 100%;
      margin-left: 22px;
      margin-right: 17px;
      .icon {
        cursor: pointer;
      }
    }
    .slider-wrap {
      position: relative;
      display: flex;
      align-items: center;
      height: 4px;
      max-width: 194px;
      min-width: 36px;
      width: 194px;
      background-color: rgba(23, 23, 23, 0.15);
      cursor: pointer;
      .circle {
        position: absolute;
        width: 14px;
        height: 14px;
        background-color: #555555;
        border-radius: 100%;
        cursor: pointer;
        user-select: none;
        transform: translate(-50%);
      }
      .slider-bar {
        height: 4px;
        max-width: 200px;
        background-color: #555555;
      }
    }
    .time-wrap {
      margin-left: 15px;
      margin-right: 18px;
      .time {
        font-size: 12px;
      }
    }
  }
}
</style>

到此这篇关于Vue3+TS实现语音播放组件的示例代码的文章就介绍到这了,更多相关Vue TS语音播放内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文