Vue3+TS實現(xiàn)語音播放組件的示例代碼
該功能將使用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)文章
Electron采集桌面共享和系統(tǒng)音頻(桌面捕獲)實例
這篇文章主要為大家介紹了Electron采集桌面共享和系統(tǒng)音頻(桌面捕獲)實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10Vue3報錯‘defineProps‘?is?not?defined的解決方法
最近工作中遇到vue3中使用defineProps中報錯,飄紅,所以這篇文章主要給大家介紹了關(guān)于Vue3報錯‘defineProps‘?is?not?defined的解決方法,需要的朋友可以參考下2023-01-01vue如何實現(xiàn)左右滑動tab(vue-touch)
這篇文章主要介紹了vue如何實現(xiàn)左右滑動tab(vue-touch),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07element-ui 限制日期選擇的方法(datepicker)
本篇文章主要介紹了element-ui 限制日期選擇的方法(datepicker),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05vue3+vite中開發(fā)環(huán)境與生產(chǎn)環(huán)境全局變量配置指南
最近在使用vite生成項目,這篇文章主要給大家介紹了關(guān)于vue3+vite中開發(fā)環(huán)境與生產(chǎn)環(huán)境全局變量配置的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08