<template>
<div>
<div class="audio-progress" v-if="isShowAudioProgress">
<!-- 顶部按钮-进度条 -->
<div class="play-icon-box" @click="playAudio">
<em class="play-icon" :class="{ 'active': isPlay }"></em>
</div>
<div class="currentTime">{{ audioCurrentTime }}</div>
<div class="progress-box">
<div class="progress-bar">
<span class="blue-progress" :style="{ 'width': progress + '%' }"></span>
<span class="dot" v-show="isDot" :style="{ 'left': dotProgress + '%' }"
@touchstart.prevent="startDrag"></span>
<span class="progress"></span>
</div>
</div>
<div class="allTime">{{ audioAllTime }}</div>
<p class="audio-tip" v-show="isAudioTip">点击这里开始收听新闻</p>
</div>
<!-- 悬浮按钮-圆环 -->
<div v-show="isShowCircle" class="audio-circle" :class="{ 'active': isCirclePlay }">
<div class="close-icon" @click="rePlayFn">
<img src="./images/close-icon.png" alt="">
</div>
<div class="circle-box" @click="playAudio">
<canvas id="audioCanvas" style="width:100%"></canvas>
</div>
</div>
</div>
</template>
<script>
import { setCookie, getCookie } from "@/util/common/cookie";
export default {
name: "audioProgress",
props: ["isShowAudioProgress"],
data() {
return {
dotProgress: -5,
progress: 0, //音频进度百分比
audioCurrentTime: '00:00', //音频当前播放时间
audioAllTime: '', //音频总播放时间
isPlay: false, //是否正在播放
isAudioTip: false,
isWidth: 0,
timer: null,
isDot: true, ///测试 先显示出来
//圆环
isCirclePlay: false, // 圆环是否正在播放
isShowCircle: false,
circleTimer: null,
ctx: null,
isDragging: false, // 是否正在拖动
dragStartX: 0, // 拖动开始时的X坐标
moveMinX: 0,
moveMaxX: 0,
progressBarWidth: 0,
audioPlayer: null,
}
},
mounted() {
this.$nextTick(() => {
this.showTip();
// 获取音频时间
this.changeProgress();
this.clearFill();
})
},
methods: {
rePlayFn() {
this.dotProgress = -5;
this.progress = 0;
this.isPlay = false;
this.isCirclePlay = false;
this.isShowCircle = false;
this.audioCurrentTime = "00:00";
this.audioPlayer.currentTime = 0;
//暂停app的语音播放
clearInterval(this.timer);
this.audioPlayer.pause();
},
startDrag(event) {
// 确保音频已加载
this.audioPlayer = document.getElementById('detailAudioPlayer');
if (!this.audioPlayer || isNaN(this.audioPlayer.duration)) return;
// 获取进度条宽度(在拖动开始时获取)
const progressBar = document.querySelector('.progress-bar');
if (!progressBar) return;
this.progressBarWidth = progressBar.offsetWidth;
this.moveMinX = progressBar.getBoundingClientRect().left;
this.moveMaxX = progressBar.getBoundingClientRect().width + this.moveMinX;
this.dragStartX = event.touches[0].clientX;
this.isDragging = true;
// 阻止默认行为(防止选中文本)
event.preventDefault();
// 添加全局事件监听
document.addEventListener('touchmove', this.dragging);
document.addEventListener('touchend', this.stopDrag);
},
dragging(event) {
if (!this.isDragging) return;
// 计算新的进度百分比
const mouseX = event.touches[0].clientX;
// console.log(mouseX, 'mousex', event);
if (mouseX < this.moveMinX) {
this.progress = 0;
this.dotProgress = 0 - 5;
} else if (mouseX > this.moveMaxX) {
this.progress = 100;
this.dotProgress = 100 - 5;
} else {
const newProgress = ((mouseX - this.moveMinX) / this.progressBarWidth) * 100;
this.progress = newProgress;
this.dotProgress = newProgress - 5;
}
this.audioCurrentTime = this.realFormatSecond((this.audioPlayer.duration * (this.progress / 100)).toFixed(2));
this.drawCircle(this.progress)
},
stopDrag() {
this.isDragging = false;
document.removeEventListener('touchmove', this.dragging);
document.removeEventListener('touchend', this.stopDrag);
// 设置音频实际播放位置
if (this.audioPlayer && this.audioPlayer.duration > 0) {
this.audioPlayer.currentTime = (this.audioPlayer.duration * (this.progress / 100)).toFixed(2);
}
},
showTip() {
let isFirst = getCookie("audio-isFirst");
if (!isFirst) {
this.isAudioTip = true; //首次才显示tip
setTimeout(() => {
this.isAudioTip = false;
}, 4000);
setCookie("audio-isFirst", true, '365');
} else {
this.isAudioTip = false;
}
},
//设置定时检测
setAudioInterval() {
let audioPlayer = document.getElementById('detailAudioPlayer');
/*语音播放结束*/
audioPlayer.addEventListener("ended",
() => {
this.dotProgress = -5;
this.progress = 0;
this.isPlay = false;
this.isCirclePlay = false;
this.drawCircle(0)
this.audioCurrentTime = "00:00";
}
);
this.timer = setInterval(() => {
const n = audioPlayer.currentTime / audioPlayer.duration;
let i = (n * 100).toFixed(2);
if (i >= 100) {
clearInterval(this.timer);
}
if (!this.isDragging) {
this.drawCircle(i);
this.progress = i;
this.dotProgress = i - 5;
this.audioCurrentTime = this.realFormatSecond(audioPlayer.currentTime);
this.audioAllTime = this.realFormatSecond(audioPlayer.duration);
}
}, 30);
},
//播放
playAudio() {
this.isPlay = !this.isPlay;
this.isCirclePlay = !this.isCirclePlay;
this.isDot = true;
let audioPlayer = document.getElementById('detailAudioPlayer');
this.isShowCircle = true; //点击播放按钮才显示悬浮按钮
if (this.isPlay) {
this.setAudioInterval();
audioPlayer.play();
} else {
//暂停
clearInterval(this.timer);
audioPlayer.pause();
}
},
//获取播放时间
changeProgress() {
let audioPlayer = document.getElementById('detailAudioPlayer');
this.audioAllTime = this.realFormatSecond(audioPlayer.duration);
this.audioCurrentTime = this.realFormatSecond(audioPlayer.currentTime);
},
//格式化秒
realFormatSecond(time) {
//分钟
if (isNaN(time)) {
return "";
}
let minute = parseInt(time / 60);
if (minute < 10) {
minute = "0" + minute;
}
//秒
let second = Math.round(time % 60);
if (second < 10) {
second = "0" + second;
}
return minute + ":" + second;
},
//绘制圆环
drawCircle(i) {
let canvas = document.getElementById('audioCanvas');
//起始一条路径
this.ctx = canvas.getContext('2d');
canvas.width = 100;
canvas.height = 100;
this.ctx.beginPath();
//连接处样式
this.ctx.lineCap = 'round';
//设置当前线条的宽度
this.ctx.lineWidth = 10; //10px
//设置笔触的颜色
this.ctx.strokeStyle = '#ff4343';
//arc(圆心x,圆心y,半径,开始角度,结束角度)
//设置开始处为0点钟方向(-90 * Math.PI / 180)
//n为百分比值(0-100)
this.ctx.arc(50, 50, 42, -90 * Math.PI / 180, (i * 3.6 - 90) * Math.PI / 180);
//绘制已定义的路径
this.ctx.stroke(); //绘制
this.ctx.closePath(); //路径结束
this.ctx.restore();
},
clearFill() {
this.circleTimer = null;
const canvas = document.getElementById('audioCanvas');
canvas.width = 0;
canvas.height = 0;
}
}
}
</script>
<style scoped>
.audio-circle {
position: fixed;
bottom: 8rem;
right: 4vw;
display: flex;
align-items: center;
justify-content: center;
gap: 4.2vw;
width: 23.644vw;
height: 12.8vw;
background-color: #ffffff;
border-radius: 1.067vw;
box-shadow: 0vw 0vw 1.778vw 0vw rgba(0, 0, 0, 0.2);
z-index: 99;
}
.audio-circle .close-icon {
width: 5.333vw;
height: 5.333vw;
display: flex;
align-items: center;
justify-content: center;
}
.audio-circle .close-icon img {
width: 3.733vw;
height: auto;
}
.audio-circle .circle-box {
width: 7.111vw;
height: 7.111vw;
background: url("./images/news-pause1.png") no-repeat center;
background-size: 100%;
}
.audio-circle.active .circle-box {
background: url("./images/news-play1.png") no-repeat center;
background-size: 100%;
}
.audio-tip {
width: 11.1rem;
height: 2.66rem;
border-radius: .33rem;
background: rgba(0, 0, 0, .8);
position: absolute;
left: 2%;
bottom: -2.8rem;
font-size: 0.933rem;
color: #fff;
text-align: center;
line-height: 2.66rem;
z-index: 99;
}
.audio-tip::after {
position: absolute;
top: -0.4rem;
left: 10%;
border-left: .4rem solid transparent;
border-right: .4rem solid transparent;
border-bottom: .4rem solid rgba(0, 0, 0, .8);
content: " ";
display: block;
width: 0;
height: 0;
}
.audio-progress {
position: relative;
width: 92vw;
height: 10.667vw;
margin-left: 4vw;
background-color: #ffffff;
border-radius: 5.333vw;
border: solid 1px #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 5vw;
}
.play-icon-box {
width: 6.489vw;
height: 6.489vw;
margin-right: 4.3vw;
}
.play-icon-box .play-icon {
display: block;
width: 6.489vw;
height: 6.489vw;
background: url("./images/news-pause.png") no-repeat center;
background-size: 100%;
flex-shrink: 0;
margin: 0 auto 0.13rem;
cursor: pointer;
}
.play-icon.active {
background: url("./images/news-play.png") no-repeat center;
background-size: 100%;
}
.currentTime,
.allTime {
font-size: 2.667vw;
color: #333333;
}
.progress-box {
width: 56.089vw;
height: 0.8vw;
margin: 0 2.6vw;
}
.progress-bar {
width: 100%;
position: relative;
height: 0.8vw;
}
.blue-progress {
position: absolute;
width: 0;
height: 0.8vw;
border-radius: 0.8vw;
background-color: #ff4343;
z-index: 99;
}
.dot {
position: absolute;
left: -2%;
top: 50%;
width: 8vw;
height: 8vw;
background: url("./images/news-dot.png") no-repeat center center;
background-size: 50%;
margin-top: -4vw;
z-index: 100;
}
.progress {
position: absolute;
width: 100%;
height: 0.8vw;
border-radius: 0.8vw;
background-color: #f7e3e5;
box-shadow: 0px 0px 0.13rem 0px rgba(0, 0, 0, .3);
z-index: 98;
}
.progress-text {
display: flex;
display: -webkit-flex;
align-items: center;
justify-content: space-between;
margin-top: .4rem;
}
.progress-text span {
font-size: .6rem;
color: #000;
}
</style>
底部圆环是联动的进度 这段代码是一个 Vue.js 组件,用于实现一个带有进度条和圆环播放指示器的音频播放器。它包括了播放、暂停、拖动进度条调整播放位置以及显示当前播放时间和总时长等功能。下面是对代码各部分的详细解释:
模板部分
- 顶部按钮-进度条:
- 包含了一个播放/暂停按钮(通过
play-icon的 class 控制图标状态),当前播放时间显示,进度条(包含已播放进度和拖动点),总播放时间显示以及一个提示用户点击开始收听新闻的文本。
- 包含了一个播放/暂停按钮(通过
- 悬浮按钮-圆环:
- 当音频播放时显示一个圆形进度指示器,其中包含了一个关闭图标用于重新播放音频。
脚本部分
数据属性 (data)
- 定义了多个状态变量,如是否正在播放(
isPlay)、是否显示进度条(isShowAudioProgress)、进度条百分比(progress)、当前播放时间(audioCurrentTime)等。 - 包含了与圆环播放指示器相关的状态变量,如是否播放中(
isCirclePlay)、是否显示圆环(isShowCircle)等。
生命周期钩子 (mounted)
- 在组件挂载后执行初始化操作,如显示提示信息、获取音频时间、清理画布等。
方法 (methods)
- rePlayFn: 重置播放器的状态,如进度、播放状态等,并停止音频播放。
- startDrag: 开始拖动进度条,计算可拖动范围并添加触摸移动和结束监听器。
- dragging: 实现拖动过程中的进度更新,同时更新当前播放时间显示。
- stopDrag: 停止拖动,移除监听器,并根据拖动的位置更新音频的实际播放位置。
- showTip: 显示首次使用的提示信息,并设置cookie避免重复显示。
- setAudioInterval: 设置定时器来监测音频播放进度,并在播放结束时重置播放器状态。
- playAudio: 切换播放和暂停状态,控制音频播放器的行为,并根据播放状态决定是否显示圆环。
- changeProgress: 获取音频的总时长和当前播放时间。
- realFormatSecond: 将秒数格式化为
MM:SS格式的时间字符串。 - drawCircle: 在canvas上绘制表示播放进度的圆环。
- clearFill: 清理canvas上的内容。
总结
该组件提供了一个功能齐全的音频播放器界面,能够展示播放进度、支持拖动调整播放位置、以图形方式展示播放状态(如进度条和圆环)。此外,它还实现了对首次使用用户的友好提示,增强了用户体验。对于需要在网页或应用中嵌入简单音频播放功能的场景来说,这是一个很好的起点。不过,需要注意的是,该组件依赖于外部HTML元素(如ID为detailAudioPlayer的音频元素)和一些自定义工具函数(如setCookie和getCookie),确保这些资源在项目中正确配置是必要的。