小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
js:
Page({data: {visible: false,videoSrc: '***/video.mp4',videoSize: 44.29,audiovisible: false, //音频弹窗currentAudio: null, //点击的音频audioContext: null,isPlaying: false,isLoading: false,audioError: false,duration: 0, //视频时长currentTimeText: '00:00',durationText: '00:00',//音频audioList: [],musicCurrentIndex: 0, //当前播放第几首isAudioReady: false, // 新增:音频是否准备code: null,detailInfo: null,unitInfo: null,bookInfo: null,seeking: false, // 是否正在拖动/跳转中currentTime: 0,audioDuration: 0, //音频总时长},onLoad(options) {console.log(options)if (options.code) {this.setData({code: options.code,})this.getAudio(options.code)}if (options.scene) {console.log(options.scene, 'options.scene888');this.decryptSpm(options.scene);}},// 解析Spm decryptSpm: async function (spm) {let shareParamsArray = spm.split('.');//type=1 课本 2课文, code='NDAxMj',this.setData({type: parseInt(shareParamsArray[0]), // 转换为数字 类型code: shareParamsArray[1],})console.log('解析来的参数', this.data.type, this.data.code)if (this.data.code) {this.getAudio(this.data.code)}},//根据课文code拿音频getAudio: async function (code) {const res = await instance.get('***?code=' + code)if (res) {// console.log("获得课文", res);this.setData({detailInfo: res,audioList: res.audio})this.getUnit(res.unitId)}},// 打开音频弹窗hangdleaudiovisible(e) {// console.log(e)const index = e.currentTarget.dataset.index;const audioItem = this.data.audioList[index];this.setData({audiovisible: true,currentAudio: audioItem,musicCurrentIndex: index});this.initAudio();},// 初始化initAudio() {if (this.data.audioContext) {this.data.audioContext.destroy();this.setData({audioContext: null});}this.setData({isAudioReady: false,audioError: false,isPlaying: false,currentTime: 0,audioDuration: 0,currentTimeText: '00:00',durationText: '00:00'});// 创建新的音频上下文const innerAudioContext = wx.createInnerAudioContext();const currentIndex = this.data.musicCurrentIndex;innerAudioContext.src = this.data.currentAudio.url;innerAudioContext.autoplay = false;innerAudioContext.loop = false;innerAudioContext.obeyMuteSwitch = false;// 音频可以播放事件innerAudioContext.onCanplay(() => {console.log('onCanplay 触发,当前 duration =', innerAudioContext.duration);if (innerAudioContext.duration && innerAudioContext.duration > 0) {console.log('onCanplay 有效,音频已可播放,duration 正常');this.handleAudioReady(innerAudioContext.duration);} else {console.warn('onCanplay 触发了,但 duration 为 0,暂不设为 ready');// 不调用 this.handleAudioReady(),避免错误设置 isAudioReady=true}});// 音频开始播放事件innerAudioContext.onPlay(() => {this.setData({isPlaying: true,});});// 音频暂停事件innerAudioContext.onPause(() => {// console.log('暂停播放');sthis.setData({isPlaying: false // 设置播放状态为false});});// 播放过程中更新当前时间和进度条innerAudioContext.onTimeUpdate(() => {const duration = innerAudioContext.duration;const currentTime = innerAudioContext.currentTime;// console.log("更新当前时间和进度条", duration, currentTime)if (!this.data.seeking && duration && duration > 0) {this.setData({currentTime: currentTime,audioDuration: duration,currentTimeText: this.formatTime(currentTime),durationText: this.formatTime(duration),isAudioReady: true // ✅ 重要:有 duration 说明已准备好了});}});// 音频错误innerAudioContext.onError((res) => {console.error('音频播放错误:', res);this.setData({audioError: true,isPlaying: false,isAudioReady: true // ❗即使出错,也设为 true,隐藏加载中});});// 音频结束innerAudioContext.onEnded(() => {this.setData({isPlaying: false,currentTime: this.data.audioDuration,currentTimeText: this.formatTime(this.data.audioDuration)});});// ✅ 监听加载中状态 监听音频加载中事件。当音频因为数据不足,需要停下来加载时会触发innerAudioContext.onWaiting(() => {this.setData({isAudioReady: true});});// ✅ 【关键】定时器:轮询 duration,兜底用let durationCheckTimer = setInterval(() => {const duration = innerAudioContext.duration;if (duration && duration > 0) {clearInterval(durationCheckTimer);// console.log('定时器检测到 duration,设置为 ready');this.handleAudioReady(duration);}}, 100);// ✅ 【关键】超时处理:2 秒后没加载出来再次初始化setTimeout(() => {clearInterval(durationCheckTimer);const duration = innerAudioContext.duration;console.log('音频加载超时,当前 duration =', duration);if (!this.data.isAudioReady) {this.initAudio()}}, 2000);this.setData({audioContext: innerAudioContext});},// 处理音频准备完成handleAudioReady(duration) {this.setData({audioDuration: duration,durationText: this.formatTime(duration),isAudioReady: true,});},// 切换播放/暂停(添加检查)toggleAudioPlayback() {if (!this.data.audioContext) {console.error('音频上下文不存在');return;}if (!this.data.isAudioReady) {wx.showToast({title: '音频尚未加载完成',icon: 'none'});return;}if (this.data.audioError) {this.retryAudio();return;}if (this.data.isPlaying) {this.data.audioContext.pause();} else {this.data.audioContext.play();}},onSliderChanging(e) {this.setData({currentTime: e.detail.value});},onSliderChange(e) {const timeInSeconds = e.detail.value;this.setData({currentTime: timeInSeconds, // 先更新 UI,让用户看到滑块跳转目标seeking: true // 标记:正在跳转中,接下来忽略 onTimeUpdate 的自动更新});this.data.audioContext.pause();//暂停播放// 执行跳转this.seekAudioTo(timeInSeconds);// 延迟一小段时间后,恢复播放(如果之前是在播放)setTimeout(() => {this.setData({seeking: false // 允许 onTimeUpdate 正常更新});this.data.audioContext.play(); // 恢复播放}, 200); // 200ms 后,确保跳转完成},seekAudioTo(seconds) {if (this.data.audioContext) {this.data.audioContext.seek(seconds);}},// 重试加载音频retryAudio() {this.initAudio();},// 格式化时间显示formatTime(seconds) {seconds = Math.floor(seconds || 0);const minutes = Math.floor(seconds / 60);const remainingSeconds = seconds % 60;return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;},closeAudioPopup() {if (this.data.audioContext) {this.data.audioContext.stop();this.data.audioContext.destroy();this.setData({audioContext: null});}this.setData({audiovisible: false,isPlaying: false,});},// 音频弹窗显示变化onAudioVisibleChange(e) {if (!e.detail.visible && this.data.audioContext) {this.data.audioContext.stop();this.data.audioContext.destroy();this.setData({audioContext: null,isPlaying: false,audiovisible: false,});}},// 弹出层显示变化回调onVisibleChange(e) {this.setData({visible: e.detail.visible,audiovisible: e.detail.visible,});if (!e.detail.visible) {if (this.videoContext) {this.videoContext.pause();}}},// 统一的切换音频方法(上一首/下一首)switchAudio(e) {// 获取方向参数:prev(上一首)或 next(下一首)const direction = e.currentTarget.dataset.direction;// 计算目标索引let targetIndex;if (direction === 'next') {// 下一首:当前索引 + 1,到最后一首则循环到第一首targetIndex = this.data.musicCurrentIndex + 1;if (targetIndex >= this.data.audioList.length) {targetIndex = 0;}} else if (direction === 'prev') {// 上一首:当前索引 - 1,到第一首则循环到最后一首targetIndex = this.data.musicCurrentIndex - 1;if (targetIndex < 0) {targetIndex = this.data.audioList.length - 1;}} else {// 未知方向,直接返回return;}// 获取目标音频项const targetAudioItem = this.data.audioList[targetIndex];// 更新当前播放索引和音频项this.setData({musicCurrentIndex: targetIndex,currentAudio: targetAudioItem});// 关闭当前音频弹窗this.closeAudioPopup();// 延迟一点时间后打开新的音频(确保上一个音频完全关闭)setTimeout(() => {this.hangdleaudiovisible({currentTarget: {dataset: {index: targetIndex}}});}, 300);},})
wxml:
<view class="fffbox"><navigation-bar class="navigation" title="" back="{{true}}" color="black" background="#fff"><t-icon slot="left" name="chevron-left" size="48rpx" bind:tap="onBack" class="mr-5" /><text slot="center" name="center" class="nav-title">{{unitInfo.name}}</text></navigation-bar><scroll-view class="scrollarea" scroll-y type="list"><view class="bluebox"><view class="border-blue pad8"><view>{{bookInfo.name}}</view><view class="fle between ali" style="padding-top: 16rpx;"><text>英语·{{bookInfo.grade}}·{{bookInfo.edition}}</text></view></view><view class="block"><t-steps layout="vertical" theme="dot" current="1" bind:change="onThirdChange"><t-step-item content="{{unitInfo.name}}" /><t-step-item content="{{detailInfo.name}}" /></t-steps></view></view><view class="fff"><view class="pads16"><block wx:if="{{audioList.length>0}}"><view wx:for="{{audioList}}" wx:key="index" class="fle ali ma55 " bind:tap="hangdleaudiovisible" data-index="{{index}}"><image src="https://tuzheng-manager-1328247176.cos.ap-shanghai.myqcloud.com/study-tour/index/sound-wave.png" mode="" class="icons" /><text class="fon15 wei">{{item.name}}</text></view></block><block wx:else><view style="width:100%;flex-direction: column;" class="fle ali ma55"><view style="color: #ccc;">暂无数据</view></view></block></view></view></scroll-view>
</view><!-- 音频弹出层 -->
<t-popup visible="{{audiovisible}}" usingCustomNavbar bind:visible-change="onAudioVisibleChange" placement="bottom"><view wx:if="{{!isAudioReady}}" class="loading-overlay"><view class="loading-spinner"></view><text class="loading-text">音频加载中,请稍候...</text></view><view class="audio-popup"><view class="audio-header"><image class="close-btn" src="https://tuzheng-manager-1328247176.cos.ap-shanghai.myqcloud.com/study-tour/index/close-blue.png" mode="aspectFit" bindtap="closeAudioPopup" /></view><view class="audio-content"><view class="audio-visualization"><image class="audio-play {{isPlaying ? 'rotate-animation' : ''}}" src="https://tuzheng-manager-1328247176.cos.ap-shanghai.myqcloud.com/study-tour/index/audio-bg.png" /><image class="stick" src="https://tuzheng-manager-1328247176.cos.ap-shanghai.myqcloud.com/study-tour/index/stick.png" mode="" /></view><view class="audio-name">{{currentAudio.name}}</view><view class="progress-container"><!-- 当前播放时间 --><text class="time-text">{{currentTimeText}}</text><!-- 滑块进度条 --><slider class="audio-slider" min="0" max="{{audioDuration}}" value="{{currentTime}}" step="0.1" activeColor="#006AFD" backgroundColor="#E5E5E5" block-color="#006AFD" block-size="16" bindchanging="onSliderChanging" bindchange="onSliderChange" disabled="{{isAudioReady ? false : true}}" /><!-- 总时长 --><text class="time-text">{{durationText}}</text></view><!-- 播放控件 --><view class="audio-controls"><image class="control-smallbtn {{isAudioReady ? '' : 'disabled'}}" src="https://tuzheng-patriarch-mini-1328247176.cos.ap-shanghai.myqcloud.com/textbook/audio-left.png" mode="" bind:tap="{{isAudioReady ? 'switchAudio' : ''}}" data-direction="prev" /><!-- 自定义播放按钮 --><view class="custom-play-btn {{isAudioReady ? '' : 'disabled'}}" bindtap="{{isAudioReady ? 'toggleAudioPlayback' : ''}}"><!-- 播放状态:显示波浪动画 --><view class="play-icon-container" wx:if="{{isPlaying}}"><view class="play-icon-bars"><view class="wave-bar" style="animation-delay: 0s"></view><view class="wave-bar" style="animation-delay: 0.2s"></view><view class="wave-bar" style="animation-delay: 0.4s"></view><view class="wave-bar" style="animation-delay: 0.6s"></view><view class="wave-bar" style="animation-delay: 0.8s"></view></view></view><!-- 暂停状态:显示播放图标 --><image wx:else class="control-btn" src="https://tuzheng-manager-1328247176.cos.ap-shanghai.myqcloud.com/study-tour/index/audio-cloce.png" mode="" /></view><image class="control-smallbtn {{isAudioReady ? '' : 'disabled'}}" src="https://tuzheng-manager-1328247176.cos.ap-shanghai.myqcloud.com/study-tour/index/right-blue.png" mode="" bind:tap="{{isAudioReady ? 'switchAudio' : ''}}" data-direction="next" /></view></view></view><view wx:if="{{audioError}}" class="loading-overlay"><text class="error-text">音频加载失败,请重试</text><button class="retry-btn" bindtap="retryAudio">重试</button></view>
</t-popup>
wxss:
.fffbox {width: 100vw;height: 100vh;
}.scrollarea {height: calc(100vh - 170rpx);position: relative;
}.bluebox {background: rgba(0, 106, 253, 0.1);padding: 16rpx 32rpx;
}.border-blue {background: #F5F9FF;border-radius: 16rpx 16rpx 16rpx 16rpx;border: 2rpx solid #D7E8FE;font-weight: 400;font-size: 24rpx;color: #2F64AD;
}.block {margin: 20rpx 0;
}.t-steps-item__description {font-weight: 600 !important;font-size: 28rpx !important;color: #000 !important;
}.fff {width: 100%;background: #FFFFFF;border-radius: 32rpx 32rpx 0rpx 0rpx;margin-top: -16rpx;
}.icons {width: 32rpx;height: 32rpx;
}.pads16 {padding: 32rpx 32rpx;
}.wei {font-weight: bold;color: #1A1A1A;padding-left: 20rpx;width: 90%;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 1;overflow: hidden;
}.ma55 {margin-bottom: 55rpx;
}.block {color: var(--td-text-color-secondary);display: flex;flex-direction: column;/* background: #F5F9FF; */justify-content: center;
}.tops {width: 95%;display: flex;justify-content: flex-end;
}video {margin: 20rpx auto;border-radius: 20rpx;width: 90%;
}.tops image {width: 48rpx;height: 48rpx;
}.times {width: 24rpx;height: 24rpx;margin-right: 10rpx;
}.bluetext {font-weight: 400;font-size: 24rpx;color: #2F64AD;
}.title {font-weight: bold;font-size: 34rpx;color: #1A1A1A;padding: 10rpx 32rpx;
}.audiotitle {width: 80%;margin: 0 auto;font-weight: bold;font-size: 32rpx;color: #1A1A1A;display: flex;align-items: center;justify-content: center;text-align: center;min-height: 64rpx;
}.audio-play {width: 460rpx;height: 432rpx;margin: 40rpx auto;}.stick {width: 128rpx;height: 154rpx;position: absolute;top: 30rpx;right: 120rpx;z-index: 10;/* 确保stick在音频背景图上方 */
}.audio-popup {background: #fff;border-top-left-radius: 24rpx;border-top-right-radius: 24rpx;padding-bottom: env(safe-area-inset-bottom);min-height: 60vh;position: relative;
}.audio-header {display: flex;justify-content: flex-end;align-items: center;padding: 32rpx;
}.audio-title {font-size: 32rpx;font-weight: bold;
}.close-btn {width: 48rpx;height: 48rpx;
}.audio-content {padding: 20rpx 32rpx;
}.audio-visualization {display: flex;justify-content: center;margin-bottom: 40rpx;position: relative;
}.audio-icon {width: 120rpx;height: 120rpx;
}.audio-play {width: 428rpx;height: 428rpx;margin: 40rpx auto;transition: transform 0.3s ease-out;
}/* 旋转动画关键帧 */
.rotate-animation {animation: rotate 10s linear infinite;
}/* 暂停状态 */
.rotate-paused {animation-play-state: paused;
}/* 旋转动画定义 */
@keyframes rotate {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}
}.audio-name {text-align: center;font-weight: bold;font-size: 32rpx;color: #1A1A1A;padding-bottom: 20rpx;
}.progress-container {display: flex;align-items: center;margin-bottom: 50rpx;width: 100%;padding: 10px;
}.time-text {font-size: 24rpx;color: #999;width: 100rpx;text-align: center;
}.audio-slider {flex: 1;margin: 0 10px;
}.progress-bar {flex: 1;height: 8rpx;background: #eee;border-radius: 4rpx;position: relative;margin: 0 20rpx;
}.progress-background {position: absolute;top: 0;left: 0;width: 100%;height: 100%;border-radius: 4rpx;background: #e0e0e0;
}.progress-filled {position: absolute;top: 0;left: 0;height: 100%;border-radius: 4rpx;background: #1E90FF;z-index: 2;
}.progress-thumb {position: absolute;top: 50%;transform: translate(-50%, -50%);width: 28rpx;height: 28rpx;border-radius: 50%;background: #1E90FF;z-index: 3;
}.audio-controls {display: flex;justify-content: space-around;align-items: center;
}.control-btn {width: 128rpx;height: 128rpx;
}.control-smallbtn {width: 56rpx;height: 56rpx;
}.loading-container,
.error-container {position: absolute;top: 50%;left: 0;right: 0;transform: translateY(-50%);text-align: center;
}.loading-text,
.error-text {font-size: 28rpx;color: #999;display: block;margin-bottom: 20rpx;
}.retry-btn {background: #1E90FF;color: white;border: none;border-radius: 8rpx;padding: 16rpx 32rpx;font-size: 26rpx;
}/* 音频波浪动画 */
.audio-waves {display: flex;align-items: center;justify-content: center;height: 432rpx;width: 460rpx;
}.wave-bar {width: 10rpx;height: 40rpx;background-color: #fff;margin: 0 5rpx;border-radius: 5rpx;animation: wave 1s infinite ease-in-out;
}@keyframes wave {0%,100% {transform: scaleY(0.5);}50% {transform: scaleY(1.5);}
}/* 自定义播放按钮 */
.custom-play-btn {width: 128rpx;height: 128rpx;border-radius: 50%;background: linear-gradient(135deg, #47DFF4 0%, #006AFD 100%);display: flex;justify-content: center;align-items: center;box-shadow: 0 4rpx 16rpx rgba(30, 144, 255, 0.4);
}.play-icon-container {width: 60rpx;height: 60rpx;display: flex;justify-content: center;align-items: center;
}.play-icon-bars {width: 100%;height: 100%;display: flex;justify-content: space-between;align-items: flex-end;margin-bottom: 20rpx;
}.play-bar {width: 8rpx;background-color: white;border-radius: 2rpx;
}.play-bar:nth-child(1) {height: 20rpx;animation: barHeight 1s infinite ease-in-out;
}.play-bar:nth-child(2) {height: 40rpx;animation: barHeight 1s infinite ease-in-out 0.2s;
}.play-bar:nth-child(3) {height: 60rpx;animation: barHeight 1s infinite ease-in-out 0.4s;
}@keyframes barHeight {0%,100% {height: 20rpx;}50% {height: 60rpx;}
}.play-icon-triangle {width: 0;height: 0;border-top: 15rpx solid transparent;border-left: 30rpx solid white;border-bottom: 15rpx solid transparent;margin-left: 5rpx;
}/* 进度条样式修复 */
.progress-bar {flex: 1;height: 8rpx;background: #eee;border-radius: 4rpx;position: relative;margin: 0 20rpx;
}.progress-background {position: absolute;top: 0;left: 0;width: 100%;height: 100%;border-radius: 4rpx;background: #e0e0e0;
}.progress-filled {position: absolute;top: 0;left: 0;height: 100%;border-radius: 4rpx;background: #1E90FF;z-index: 2;transition: width 0.2s ease;
}.progress-thumb {position: absolute;top: 50%;transform: translate(-50%, -50%);width: 28rpx;height: 28rpx;border-radius: 50%;background: #1E90FF;z-index: 3;box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}.loading-overlay {position: absolute;top: 0;left: 0;right: 0;bottom: 0;background: rgba(255, 255, 255, 0.9);display: flex;flex-direction: column;justify-content: center;align-items: center;z-index: 100;
}.loading-spinner {width: 60rpx;height: 60rpx;border: 6rpx solid #f3f3f3;border-top: 6rpx solid #006AFD;border-radius: 50%;animation: spin 1s linear infinite;margin-bottom: 20rpx;
}@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}
}/* 禁用状态样式 */
.disabled {opacity: 0.5;pointer-events: none;
}.progress-bar.disabled .progress-thumb {display: none;
}.control-smallbtn.disabled,
.custom-play-btn.disabled {filter: grayscale(100%);
}
.weui-navigation-bar__inner .weui-navigation-bar__center{width: 50% !important;}
.navigation .nav-title{white-space: nowrap; /* 禁止换行 */overflow: hidden; /* 隐藏超出部分 */text-overflow: ellipsis; /* 超出显示省略号 */display: inline-block; /* 或 block,根据布局 */max-width: 500rpx; /* 限制不超过父容器宽度 */margin-left: 50rpx;
}