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('暂停播放');s
this.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;
}