最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue3+TS實現(xiàn)語音播放組件的示例代碼

 更新時間:2022年03月25日 15:04:25   作者:叨嘮  
這篇文章主要介紹了如何利用Vue+TS實現(xiàn)一個簡易的語音播放組件,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Vue有一定的幫助,需要的可以參考一下

該功能將使用vue3 + TS來實現(xiàn)語音播放組件,使用什么技術(shù)不重要,重要的是看懂了核心邏輯后,通過原生js、react、vue2等都可以輕松實現(xiàn)

所涉及到重要點有以下幾個:

(1)進(jìn)度條的實現(xiàn):拖拽進(jìn)度條、點擊進(jìn)度條

(2)操作audio語音播放:通過js操作audio媒體

(3)播放進(jìn)度與進(jìn)度條緊密關(guān)聯(lián):播放的進(jìn)度改變時,進(jìn)度條也隨之改變;進(jìn)度條改變時,播放的進(jìn)度也隨之改變

效果圖:

開始我們的設(shè)計吧!

第一步:點擊拖拽進(jìn)度條

進(jìn)度條的css樣式如下:

父元素設(shè)置灰色背景色,圓圈進(jìn)行position定位,使用left百分比,同時黑色進(jìn)度條的width也是百分比,這樣圓圈的left值是多少,黑色進(jìn)度條的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事件,根據(jù)事件e.target確定圓圈、黑色進(jìn)度條、灰色父元素,三者的element。同時知道了圓圈當(dāng)前的left值,比如30%,還知道了當(dāng)前鼠標(biāo)mousedown時,事件e.pageX,即鼠標(biāo)mousedown時,距離頁面左邊的水平值,因為對比后續(xù)鼠標(biāo)移動時,觸發(fā)的mousemove事件的e.pageX可以判斷移動了多少。同時還要知道灰色背景的父元素的width。因為鼠標(biāo)移動的距離 / width 要賦值給圓圈的left。知道移動了%多少。

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

然后,監(jiān)聽document文檔的mousemove,注意鼠標(biāo)是可以在整個文檔上移動的,不過圓圈可不能在灰色父元素之外。移動的e.pageX - 鼠標(biāo)mousedown時的e.pageX 就是鼠標(biāo)水平移動的距離。超出最大值時,圓圈left設(shè)置為100%,小于最小值時,left設(shè)置為0%,總之left要在0%~100%之間,才能保證圓圈不超出到外面去。這樣圓圈就可以隨著鼠標(biāo)移動了,同時黑色進(jìn)度條的width值與圓圈的left值一樣,所以黑色進(jìn)度條的width也隨著鼠標(biāo)移動。

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;
    });

再說說點擊父元素時,圓圈到指定位置

點擊事件在灰色父元素上進(jìn)行監(jiān)聽,注意e.target可不一定是灰色父元素,e.target表示鼠標(biāo)點擊到哪個元素,隨后click冒泡到父元素上的。同樣點擊事件的e.pageX 可以確定鼠標(biāo)點擊的水平位置,轉(zhuǎn)換為%值,設(shè)置圓圈的left值和黑色進(jìn)度條的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);
    };

以上就可以實現(xiàn)進(jìn)度條功能了。

第二步:操作媒體音頻

獲取audio的element,audioElement上面有play、pause等方法,還有currentTime播放進(jìn)度時間,以及duration總時長。所以說HTML5的audio標(biāo)簽,上面的方法和屬性還是非常直觀的,這也正是web發(fā)展的一個特點,某個新的特性的產(chǎn)生,功能會很明了。

首先當(dāng)媒體的第一幀加載完成時,我們就獲取audio的element:(audio自身的loadeddata事件)

// 當(dāng)媒體音頻第一幀加載完成時
    const audioLoadeddata = (e) => {
      audioEl = e.target;
      audioData.duration = e.target.duration;
    };

其次,對播放中進(jìn)行監(jiān)聽:(audio的timeupdate事件)

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

最后,播放完成進(jìn)行監(jiān)聽:(audio的ended事件)

    // 音頻播放結(jié)束
    const audioEnded = () => {
      audioData.playing = false;
    };

如果對audio標(biāo)簽不是很熟悉,請參考文檔

上述操作還是很簡單的,audio上的屬性、方法、事件都是非常簡單明了且實用的。

第三步:進(jìn)度條和播放進(jìn)度關(guān)聯(lián)

通過audio當(dāng)前的播放時間 / 總時長,得到的%值,賦值給圓圈的left和黑色進(jìn)度條的width。

通過圓圈的left值的% * 總時長,得到audio的當(dāng)前播放時間。(audio的currentTime屬性直接賦值,語音播放就會跳轉(zhuǎn)到指定的時間進(jìn)行播放,比如 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, // 當(dāng)前播放的位置
    });
    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%'; // 圓圈被鼠標(biāo)水平拖拽的距離(默認(rèn)向左)
 
    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; // 圓圈的兄弟節(jié)點
 
      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;
    }
    /**
     * 更新進(jìn)度條
     * @param percentage 得到一個百分比的字符串
     */
    function updateProgressBar(percentage: string) {
      circleTarget.el.style.left = percentage;
      sliderBar.el.style.width = percentage;
    }
 
    /**
     * 以下是對音頻的操作
     */
 
    // 音頻播放結(jié)束
    const audioEnded = () => {
      audioData.playing = false;
    };
    // 播放進(jìn)度:表示audio正在播放,currentTime在更新
    const audioTimeupdate = (e) => {
      audioData.currentTime = e.target.currentTime;
      progressBarBycurrentTime();
    };
    // 當(dāng)媒體音頻第一幀加載完成時
    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;
      }
    }
 
    // 進(jìn)度條和音頻播放進(jìn)度進(jìn)行關(guān)聯(lián)
    function progressBarBycurrentTime() {
      const progress = getPercentage(audioData.currentTime, audioData.duration);
      updateProgressBar(`${progress}%`);
    }
    /**
     * 播放進(jìn)度與進(jìn)度條進(jìn)行關(guān)聯(lián)
     * @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>

到此這篇關(guān)于Vue3+TS實現(xiàn)語音播放組件的示例代碼的文章就介紹到這了,更多相關(guān)Vue TS語音播放內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • vue項目純前端實現(xiàn)的模板打印功能示例代碼

    vue項目純前端實現(xiàn)的模板打印功能示例代碼

    在Vue項目中,通過使用vue-print-nb插件,可以實現(xiàn)頁面的打印功能,這篇文章主要介紹了vue項目純前端實現(xiàn)的模板打印功能的相關(guān)資料,需要的朋友可以參考下
    2024-10-10
  • vuex刷新之后數(shù)據(jù)丟失,數(shù)據(jù)持久化,vuex-persistedstate問題

    vuex刷新之后數(shù)據(jù)丟失,數(shù)據(jù)持久化,vuex-persistedstate問題

    這篇文章主要介紹了vuex刷新之后數(shù)據(jù)丟失,數(shù)據(jù)持久化,vuex-persistedstate問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • vue多種彈框的彈出形式的示例代碼

    vue多種彈框的彈出形式的示例代碼

    本篇文章主要介紹了vue多種彈框的彈出形式的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-09-09
  • Vue自定義可以選擇日期區(qū)間段的日歷插件

    Vue自定義可以選擇日期區(qū)間段的日歷插件

    這篇文章主要為大家詳細(xì)介紹了Vue自定義可以選擇日期區(qū)間段的日歷插件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • vue+elementUI封裝一個根據(jù)后端變化的動態(tài)table(完整代碼)

    vue+elementUI封裝一個根據(jù)后端變化的動態(tài)table(完整代碼)

    這篇文章主要介紹了vue+elementUI,封裝一個根據(jù)后端變化的動態(tài)table,實現(xiàn)了自動生成和插槽兩個方式,主要把el-table 和el-pagination封裝在一起,結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2022-08-08
  • vue靜態(tài)界面之左二級菜單右表單表格的實例代碼

    vue靜態(tài)界面之左二級菜單右表單表格的實例代碼

    這篇文章主要介紹了vue靜態(tài)界面之左二級菜單右表單表格,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-08-08
  • vuex進(jìn)階知識點鞏固

    vuex進(jìn)階知識點鞏固

    本片文章是小編在網(wǎng)上整理的關(guān)于VUEX進(jìn)階的相關(guān)知識點內(nèi)容,對此有興趣的朋友可以跟著學(xué)習(xí)參考下。
    2018-05-05
  • vue更新數(shù)據(jù)卻不渲染頁面的解決

    vue更新數(shù)據(jù)卻不渲染頁面的解決

    這篇文章主要介紹了vue更新數(shù)據(jù)卻不渲染頁面的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • vue3 Class 與 Style 綁定操作方法

    vue3 Class 與 Style 綁定操作方法

    數(shù)據(jù)綁定的一個常見需求場景是操縱元素的 CSS class 列表和內(nèi)聯(lián)樣式,因為 class 和 style 都是 attribute,我們可以和其他 attribute 一樣使用 v-bind 將它們和動態(tài)的字符串綁定,這篇文章主要介紹了vue3 Class 與 Style 綁定操作方法,需要的朋友可以參考下
    2024-05-05
  • vue-router 組件復(fù)用問題詳解

    vue-router 組件復(fù)用問題詳解

    本篇文章主要介紹了vue-router 組件復(fù)用問題詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01

最新評論

汝阳县| 沾化县| 衡东县| 米林县| 泽库县| 徐闻县| 台安县| 揭东县| 香港| 宣汉县| 沁源县| 甘德县| 林西县| 灵武市| 徐闻县| 韶关市| 晋宁县| 宝清县| 临桂县| 柳江县| 邵阳市| 仪征市| 津市市| 黄冈市| 尼木县| 福泉市| 仲巴县| 金沙县| 晴隆县| 阜新市| 乐东| 龙门县| 兴化市| 平潭县| 霍林郭勒市| 兴隆县| 城固县| 舟山市| 中卫市| 安义县| 鄯善县|