audio自带的样式一般满足不了ui需求,所以往往需要自己去实现。原理是将audio隐藏,监听audio事件,实现模拟播放效果。
audio属性的值可以通过父组件传值过来,此代码可以封装成一个组件。
<template>
<div class="audio-wrapper">
<audio
controls="controls"
class="audio1"
:src="url"
:preload="audio.preload"
@play="onPlay"
@error="onError"
@pause="onPause"
@timeupdate="onUpdateTime"
@loadedmetadata="onLoadedmetadata"
muted="muted"
autoplay="autoplay"
ref="audio"
></audio>
<div class="fh-progress-wrapper">
<span class="fh-time fh-time-l">{{ audio.currentTime | formatSecond}}</span>
<div class="fh-progress-bar-wrapper">
<div class="fh-progress-bar" ref="progressBar" @click="progressClick">
<div class="fh-bar-inner">
<div class="fh-progress" ref="progress"></div>
<div
class="fh-progress-btn-wrapper"
ref="progressBtn"
@touchstart.prevent="progressTouchStart"
@touchmove.prevent="progressTouchMove"
@touchend="progressTouchEnd"
>
<div class="fh-progress-btn"></div>
</div>
</div>
</div>
</div>
<span class="fh-time fh-time-r">{{ audio.maxTime | formatSecond }}</span>
</div>
<div class="audio-control">
<span>
<!-- 上一首 -->
</span>
<div @click="startPlayOrPause">
{{ audio.playing | transPlayPause }}
</div>
<span>
<!-- 下一首 -->
</span>
</div>
</div>
</template>
<script>
export default {
name: 'index',
data() {
return {
url: 'https://wdd.js.org/element-audio/static/falling-star.mp3',
audio: { //可通过父组件传值过来
currentTime: 0, // 当前播放时间
maxTime: 0, // 播放总时间
playing: false, // 播放状态
waiting: true,// 是否需要等待
preload: 'auto'
},
progressBtnWidth: 16,// 滑动进度图标宽度
touch: {
initiated: false, //是否滑动
startX: 0, //当前拖动点X轴位置
left: 0, //当前进度条位置
} // 滑动图标时记录的参数
};
},
computed: {
percent() {
return this.audio.currentTime / this.audio.maxTime;
},
},
watch: {
percent(newPrecent) {
// 非滑动进度图标时间内
if (newPrecent >= 0 && !this.touch.initiated) {
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth = this.$refs.progressBar.clientWidth - this.progressBtnWidth;
// 进度滑动条宽度 = 默认滑动条实际宽度 * 播放百分比
const offsetWidth = newPrecent * barWidth;
// 进度滑动条偏移和滑动进度图标偏移
this._offset(offsetWidth);
}
}
},
filters: {
transPlayPause(value) {
return value ? '暂停' : '播放';
},
// 将秒转化成倒计时
formatSecond(second) {
let secondType = typeof second;
if (secondType === 'number' || secondType === 'string') {
second = parseInt(second);
const hours = Math.floor(second / 3600);
second = second - hours * 3600;
const mimute = Math.floor(second / 60);
second = second - mimute * 60;
return (
hours +
':' +
('0' + mimute).slice(-2) +
':' +
('0' + second).slice(-2)
);
} else {
return '0:00:00';
}
},
},
methods: {
// 当音频开始播放
onPlay(res) {
console.log(res)
this.audio.playing = true;
},
onError () {
this.audio.waiting = true
},
// 切换音频状态
startPlayOrPause() {
return this.audio.playing ? this.pausePlay() : this.startPlay();
},
// 开始播放
startPlay() {
this.$refs.audio.play();
},
// 暂停
pausePlay() {
this.$refs.audio.pause();
},
// 当音频暂停
onPause() {
this.audio.playing = false;
},
// 当currentTime更新时会触发timeupdate事件
onUpdateTime(e) {
this.audio.currentTime = e.target.currentTime;
},
// 当加载语音流元数据完成后,会触发该事件的回调函数
// 语音元数据主要是语音的长度之类的数据
onLoadedmetadata(res) {
this.audio.waiting = false;
this.audio.maxTime = parseInt(res.target.duration);
this.autoplay = 'autoplay'
if (this.audio.currentTime > 0) {
this.$refs.audio.currentTime = this.audio.currentTime;
}
if (this.audio.playing) {
this.startPlay()
}
},
progressTouchStart(e) {
this.touch.initiated = true; // 标志位 表示初始化
this.touch.startX = e.touches[0].pageX; // 当前拖动点X轴位置
this.touch.left = this.$refs.progress.clientWidth; // 当前进度条位置
},
progressTouchMove(e) {
if (!this.touch.initiated) {
return;
}
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth = this.$refs.progressBar.clientWidth - this.progressBtnWidth;
const deltaX = e.touches[0].pageX - this.touch.startX; // 拖动偏移量
// 滑动中进度滑动条和滑动进度图标的当前位置
const offsetWidth = Math.min(
barWidth,
Math.max(0, this.touch.left + deltaX)
);
this._offset(offsetWidth);
},
progressTouchEnd() {
this.touch.initiated = false;
this._triggerPercent();
},
progressClick(e) {
// getBoundingClientRect用于获取某个元素相对于视窗的位置集合
const rect = this.$refs.progressBar.getBoundingClientRect();
// pageX() 属性是鼠标指针的位置,相对于文档的左边缘。
const offsetWidth = e.pageX - rect.left;
// 进度滑动条偏移和滑动进度图标偏移
this._offset(offsetWidth);
// 计算当前播放时间
this._triggerPercent();
},
// 计算当前时间
_triggerPercent() {
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth = this.$refs.progressBar.clientWidth - this.progressBtnWidth;
// 当前播放比例 = 进度滑动条宽度 / 默认滑动条实际宽度
const percent = this.$refs.progress.clientWidth / barWidth;
// 当前播放时长 = 总时长 * 播放比例
const currentTime = this.audio.maxTime * percent;
// 设置音频当前播放时间
this.$refs.audio.currentTime = currentTime;
},
// 计算拖动条的宽度和滑动偏移的距离
_offset(offsetWidth) {
this.$refs.progress.style.width = `${offsetWidth}px`; // 进度条偏移
this.$refs.progressBtn.style.transform = `translate3d(${offsetWidth}px, 0, 0)`; // 小球偏移
}
}
};
</script>
<style lang="less" scoped>
.audio-wrapper {
background-color: tomato;
}
.audio1 {
margin: auto;
display: none;
width: 100%;
}
.fh-progress-wrapper {
display: flex;
align-items: center;
width: 80%;
margin: 0px auto;
padding: 10px 0;
.fh-time {
color: #fff;
font-size: 12px;
flex: 0 0 30px;
line-height: 30px;
width: 60px;
display: inline-block;
padding: 0 5px;
&.fh-time-l {
text-align: left;
}
&.fh-time-r {
text-align: right;
}
}
.fh-progress-bar-wrapper {
flex: 1;
}
}
.fh-progress-bar {
height: 30px;
.fh-bar-inner {
position: relative;
top: 13px;
height: 4px;
background: rgba(0, 0, 0, 0.3);
.fh-progress {
position: absolute;
height: 100%;
background: #41b883;
}
.fh-progress-btn-wrapper {
position: absolute;
left: -7px;
top: -13px;
width: 30px;
height: 30px;
.fh-progress-btn {
position: relative;
top: 7px;
left: 7px;
box-sizing: border-box;
width: 16px;
height: 16px;
border: 3px solid #fff;
border-radius: 50%;
background: #41b883;
}
}
}
}
.audio-control {
display: flex;
justify-content: space-around;
span {
flex: 1;
font-size: 12px;
text-align: center;
}
}
</style>