Vue3+TS實現(xiàn)語音播放組件的示例代碼
該功能將使用vue3 + TS來實現(xiàn)語音播放組件,使用什么技術不重要,重要的是看懂了核心邏輯后,通過原生js、react、vue2等都可以輕松實現(xiàn)
所涉及到重要點有以下幾個:
(1)進度條的實現(xiàn):拖拽進度條、點擊進度條
(2)操作audio語音播放:通過js操作audio媒體
(3)播放進度與進度條緊密關聯(lián):播放的進度改變時,進度條也隨之改變;進度條改變時,播放的進度也隨之改變
效果圖:

開始我們的設計吧!
第一步:點擊拖拽進度條
進度條的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時,距離頁面左邊的水平值,因為對比后續(xù)鼠標移動時,觸發(fā)的mousemove事件的e.pageX可以判斷移動了多少。同時還要知道灰色背景的父元素的width。因為鼠標移動的距離 / 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,注意鼠標是可以在整個文檔上移動的,不過圓圈可不能在灰色父元素之外。移動的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;
});再說說點擊父元素時,圓圈到指定位置
點擊事件在灰色父元素上進行監(jiān)聽,注意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);
};以上就可以實現(xiàn)進度條功能了。
第二步:操作媒體音頻
獲取audio的element,audioElement上面有play、pause等方法,還有currentTime播放進度時間,以及duration總時長。所以說HTML5的audio標簽,上面的方法和屬性還是非常直觀的,這也正是web發(fā)展的一個特點,某個新的特性的產生,功能會很明了。
首先當媒體的第一幀加載完成時,我們就獲取audio的element:(audio自身的loadeddata事件)
// 當媒體音頻第一幀加載完成時
const audioLoadeddata = (e) => {
audioEl = e.target;
audioData.duration = e.target.duration;
};
其次,對播放中進行監(jiān)聽:(audio的timeupdate事件)
// 播放進度:表示audio正在播放,currentTime在更新
const audioTimeupdate = (e) => {
audioData.currentTime = e.target.currentTime;
progressBarBycurrentTime();
};最后,播放完成進行監(jiān)聽:(audio的ended事件)
// 音頻播放結束
const audioEnded = () => {
audioData.playing = false;
};如果對audio標簽不是很熟悉,請參考文檔
上述操作還是很簡單的,audio上的屬性、方法、事件都是非常簡單明了且實用的。
第三步:進度條和播放進度關聯(lián)
通過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; // 圓圈的兄弟節(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;
}
/**
* 更新進度條
* @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;
}
}
// 進度條和音頻播放進度進行關聯(lián)
function progressBarBycurrentTime() {
const progress = getPercentage(audioData.currentTime, audioData.duration);
updateProgressBar(`${progress}%`);
}
/**
* 播放進度與進度條進行關聯(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>到此這篇關于Vue3+TS實現(xiàn)語音播放組件的示例代碼的文章就介紹到這了,更多相關Vue TS語音播放內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Electron采集桌面共享和系統(tǒng)音頻(桌面捕獲)實例
這篇文章主要為大家介紹了Electron采集桌面共享和系統(tǒng)音頻(桌面捕獲)實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10
Vue3報錯‘defineProps‘?is?not?defined的解決方法
最近工作中遇到vue3中使用defineProps中報錯,飄紅,所以這篇文章主要給大家介紹了關于Vue3報錯‘defineProps‘?is?not?defined的解決方法,需要的朋友可以參考下2023-01-01
vue如何實現(xiàn)左右滑動tab(vue-touch)
這篇文章主要介紹了vue如何實現(xiàn)左右滑動tab(vue-touch),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07
element-ui 限制日期選擇的方法(datepicker)
本篇文章主要介紹了element-ui 限制日期選擇的方法(datepicker),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05
vue3+vite中開發(fā)環(huán)境與生產環(huán)境全局變量配置指南
最近在使用vite生成項目,這篇文章主要給大家介紹了關于vue3+vite中開發(fā)環(huán)境與生產環(huán)境全局變量配置的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-08-08

