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

目录
  • 第一步:点击拖拽进度条
  • 第二步:操作媒体音频
  • 第三步:进度条和播放进度关联
  • 完整代码

该功能将使用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语音播放内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue调用语音播放的方法

    本文实例为大家分享了vue调用语音播放的具体代码,供大家参考,具体内容如下 功能展示 输入文字 点击播放 调用语音 直接上代码 <template> <div> <!-- 语音播放功能 --> <div> <input type="text" id="ttsText"> <input type="button" id="tts_btn" @click="

  • vue实现百度语音合成的实例讲解

    这里我完全依照百度提供的语音合成api来做:https://ai.baidu.com/docs#/TTS-Online-Node-SDK/top 直接去看node.js SDK. 1.换取access_token: 参考:https://ai.baidu.com/docs#/TTS-API/top,在这里用浏览器换取token 2.下载sdk依赖: npm install baidu-aip-sdk 完整代码(非原创,参考了一个demo,但是很久以前写的找不到了): btts(text) { c

  • VUE 文字转语音播放的实现示例

    目录 一.技术:Web Speech API 二.语音合成及发音接口 三.vue项目案例 一.技术:Web Speech API Web Speech API​​使您能够将语音数据合并到 Web 应用程序中. Web Speech API 有两个部分:SpeechSynthesis 语音合成 (文本到语音 TTS)和 SpeechRecognition  语音识别(异步语音识别). 二.语音合成及发音接口 ​ ​SpeechSynthesis​​:语音合成服务的控制器接口,可用于获取设备上可用的

  • vue项目或网页上实现文字转换成语音播放功能

    一.在网页上实现文字转换成语音 方式一: 摘要:语音合成:也被称为文本转换技术(TTS),它是将计算机自己产生的.或外部输入的文字信息转变为可以听得懂的.流利的口语输出的技术. 1. 使用百度的接口: http://tts.baidu.com/text2audio?lan=zh&ie=UTF-8&spd=2&text=你要转换的文字 2.参数说明: lan=zh:语言是中文,如果改为lan=en,则语言是英文. ie=UTF-8:文字格式. spd=2:语速,可以是1-9的数字,数

  • vue中解决chrome浏览器自动播放音频和MP3语音打包到线上的实现方法

    一.vue中解决chrome浏览器自动播放音频 需求 有新订单的时候,页面自动语音提示和弹出提示框: 问题 chrome浏览器在18年4月起,就在桌面浏览器全面禁止了音视频的自动播放功能.严格地来说,是Chrome不允许在用户对网页进行触发之前播放音频.不光是这样,在页面加载完毕的情况下,用户没有click.dbclick.touch等主动交互行为,使用js直接调用.play() 方法的话,chrome都会抛出如下错误:Uncaught (in promise) DOMException: 解决

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

    目录 第一步:点击拖拽进度条 第二步:操作媒体音频 第三步:进度条和播放进度关联 完整代码 该功能将使用vue3 + TS来实现语音播放组件,使用什么技术不重要,重要的是看懂了核心逻辑后,通过原生js.react.vue2等都可以轻松实现 所涉及到重要点有以下几个: (1)进度条的实现:拖拽进度条.点击进度条 (2)操作audio语音播放:通过js操作audio媒体 (3)播放进度与进度条紧密关联:播放的进度改变时,进度条也随之改变:进度条改变时,播放的进度也随之改变 效果图: 开始我们的设计吧

  • vue3+ts使用bus事件总线的示例代码

    1.在vue2中我是这样使用的 //创建一个vueBus.js import Bus from 'vue'; let install = function (Vue) { Vue.prototype.$bus = new Bus() } export default { install }; //在main.js中引入 import vueBus from '@/utils/vueBus'; Vue.use(vueBus); //可根据this.$bus._events['事件名'] 来查看是否

  • 使用Vue3实现一个Upload组件的示例代码

    通用上传组件开发 开发上传组件前我们需要了解: FormData上传文件所需API dragOver文件拖拽到区域时触发 dragLeave文件离开拖动区域 drop文件移动到有效目标时 首先实现一个最基本的上传流程: 基本上传流程,点击按钮选择,完成上传 代码如下: <template> <div class="app-container"> <!--使用change事件--> <input type="file" @ch

  • 使用Vue3+ts 开发ProTable源码教程示例

    目录 前台实现 实现效果 技术栈 使用方法 ProTable 设计思路 编码风格 css 小知识 表格操作 小结 后期功能扩展 后台实现 数据库 mysql 新建项目 RESTful 风格的 URL 定义 Sequelize controller model router.js API 文档 Apifox ts用到的一些 前台实现 实现效果 技术栈 vue3 + typescript + element-plus 使用方法 <template> <el-tabs type="b

  • Vue3实现刷新页面局部内容的示例代码

    目录 第一步:定义状态标识 第二步.借用v-if 指令让dom节点重新渲染 第三步.修改isRouterAlive 值,实现dom的重新渲染 想要实现页面的局部刷新,我们只需要实现局部组件(dom)的重新渲染.在Vue中,想要实现这一效果最简便的方式方法就是使用v-if 指令. 在Vue2中我们除了使用v-if 指令让局部dom的重新渲染,也可以新建一个空白组件,需要刷新局部页面时跳转至这个空白组件页面,然后在空白组件内的beforeRouteEnter 守卫中又跳转回原来的页面. 如下图所示,

  • 开发Vue树形组件的示例代码

    本文介绍了Vue树形组件的示例代码,分享给大家,具体如下: 使用SemanticUI和vue做一个menubar组件,实现方法大概是这样的: <template> <div class="ui menu"> <template v-for="item in leftItems"> <a " v-if="!item.children" @click="item.click"&g

  • 基于vue的tab-list类目切换商品列表组件的示例代码

    在大多数电商场景中,页面都会有类目切换加上商品列表的部分,页面大概会长这样 每次写类似场景的时候,都需要去为类目商品列表写很多逻辑,为了提高开发效率我决定将这一部分抽离成组件. 实现 1.样式 所有tab栏的样式和商品列表的样式都提供插槽,供业务自己定制 2.变量 isTabFixed: false,//是否吸顶 tab: 1,//当前tab page: 1,//当前页数 listStatus: { finished: false,//是否已是最后一页 loading: false,//是否加载

  • 原生JavaScript实现弹幕组件的示例代码

    前言 如今几乎所有的视频网站都有弹幕功能,那么今天我们就自己用原生 JavaScript 封装一个弹幕类.这个类希望有如下属性和实例方法: 属性 el容器节点的选择器,容器节点应为绝对定位,设置好宽高 height 每条弹幕的高度 mode 弹幕模式,half则为一半容器高度,top为三分之一,full为占满 speed弹幕划过屏幕的时间 gapWidth后一条弹幕与前一条弹幕的距离 方法 pushData 添加弹幕元数据 addData持续加入弹幕 start开始调度弹幕 stop停止弹幕 r

  • 原生JS实现音乐播放器的示例代码

    本文主要介绍了原生JS实现音乐播放器的示例代码,分享给大家,具体如下: 效果图 音乐播放器 播放控制 播放进度条控制 歌词显示及高亮 播放模式设置 播放器属性归类 按照播放器的功能划分,对播放器的属性和DOM元素归类,实现同一功能的元素和属性保存在同一对象中,便于管理和操作 const control = { //存放播放器控制 play: document.querySelector('#myplay'), ... index: 2,//当前播放歌曲序号 ... } const audioFi

随机推荐