|
|
|
@ -17,7 +17,6 @@ |
|
|
|
<swiper |
|
|
|
class="content-swiper" |
|
|
|
:current="currentPage - 1" |
|
|
|
:disable-touch="isAudioLoading" |
|
|
|
@change="onSwiperChange" |
|
|
|
> |
|
|
|
<swiper-item |
|
|
|
@ -91,70 +90,19 @@ |
|
|
|
|
|
|
|
<!-- 自定义底部控制栏 --> |
|
|
|
<view class="custom-tabbar" :class="{ 'tabbar-hidden': !showNavbar }"> |
|
|
|
<!-- 音频控制栏 --> |
|
|
|
<!-- 获取音频按钮 --> |
|
|
|
<view v-if="!hasAudioData && !isAudioLoading && isTextPage" class="audio-get-button-container"> |
|
|
|
<view class="get-audio-btn" @click="handleGetAudio"> |
|
|
|
<uv-icon name="play-circle" size="24" color="#06DADC"></uv-icon> |
|
|
|
<text class="get-audio-text">获取第{{currentPage}}页音频</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 音频加载状态 --> |
|
|
|
<view v-else-if="isAudioLoading && isTextPage" class="audio-loading-container"> |
|
|
|
<uv-loading-icon mode="spinner" size="30" color="#06DADC"></uv-loading-icon> |
|
|
|
<text class="loading-text">正在加载第{{currentPage}}页音频...</text> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 正常音频控制栏 --> |
|
|
|
<view v-else-if="hasAudioData" class="audio-controls" :class="{ 'audio-hidden': !isTextPage }"> |
|
|
|
<view class="audio-time"> |
|
|
|
<text class="time-text">{{ formatTime(currentTime) }}</text> |
|
|
|
<view class="progress-container"> |
|
|
|
<uv-slider |
|
|
|
v-model="sliderValue" |
|
|
|
:min="0" |
|
|
|
:max="totalTime" |
|
|
|
:step="0.1" |
|
|
|
activeColor="#06DADC" |
|
|
|
backgroundColor="#e0e0e0" |
|
|
|
:blockSize="16" |
|
|
|
blockColor="#ffffff" |
|
|
|
:disabled="!hasAudioData || totalTime <= 0" |
|
|
|
@input="onSliderInput" |
|
|
|
@changing="onSliderChanging" |
|
|
|
@change="onSliderChange" |
|
|
|
:customStyle="{ flex: 1, margin: '0 10px' }" |
|
|
|
/> |
|
|
|
</view> |
|
|
|
<text class="time-text">{{ formatTime(totalTime) }}</text> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="audio-controls-row"> |
|
|
|
<view class="control-btn" @click="toggleLoop"> |
|
|
|
<uv-icon name="reload" size="20" :color="isLoop ? '#06DADC' : '#999'"></uv-icon> |
|
|
|
<text class="control-text">循环</text> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="control-btn" @click="previousPage"> |
|
|
|
<text class="control-text">上一页</text> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="play-btn" @click="togglePlay"> |
|
|
|
<uv-icon :name="isPlaying ? 'pause-circle-fill' : 'play-circle-fill'" size="40" color="#666"></uv-icon> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="control-btn" @click="nextPage"> |
|
|
|
<text class="control-text">下一页</text> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="control-btn" @click="toggleSpeed" :class="{ 'disabled': !playbackRateSupported }"> |
|
|
|
<text class="control-text" :style="{ opacity: playbackRateSupported ? 1 : 0.5 }"> |
|
|
|
{{ playbackRateSupported ? playSpeed + 'x' : '不支持' }} |
|
|
|
</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<!-- 音频控制栏组件 --> |
|
|
|
<AudioControls |
|
|
|
:current-page="currentPage" |
|
|
|
:course-id="courseId" |
|
|
|
:voice-id="voiceId" |
|
|
|
:book-pages="bookPages" |
|
|
|
:is-text-page="isTextPage" |
|
|
|
@previous-page="previousPage" |
|
|
|
@next-page="nextPage" |
|
|
|
@audio-state-change="onAudioStateChange" |
|
|
|
@highlight-change="onHighlightChange" |
|
|
|
ref="audioControls" |
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
<view style="background-color: #fff;position: relative;z-index: 100" > |
|
|
|
@ -280,7 +228,12 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import AudioControls from './AudioControls.vue' |
|
|
|
|
|
|
|
export default { |
|
|
|
components: { |
|
|
|
AudioControls |
|
|
|
}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
isMember: false, |
|
|
|
@ -292,30 +245,10 @@ export default { |
|
|
|
currentCourse: 1, // 当前课程索引 |
|
|
|
currentWordMeaning: null, // 当前显示的单词释义 |
|
|
|
isReversed: false, // 是否倒序显示 |
|
|
|
// 音频控制相关数据 |
|
|
|
isPlaying: false, |
|
|
|
currentTime: 0, |
|
|
|
totalTime: 0, |
|
|
|
sliderValue: 0, // 滑動條的值 |
|
|
|
isDragging: false, // 是否正在拖動滑動條 |
|
|
|
isLoop: false, |
|
|
|
playSpeed: 1.0, |
|
|
|
speedOptions: [0.5, 0.8, 1.0, 1.25, 1.5, 2.0], // 根據uni-app文檔的官方支持值 |
|
|
|
playbackRateSupported: true, // 播放速度控制是否支持 |
|
|
|
// 音频数组管理 |
|
|
|
currentPageAudios: [], // 当前页面的音频数组 |
|
|
|
currentAudioIndex: 0, // 当前播放的音频索引 |
|
|
|
audioContext: null, // 音频上下文 |
|
|
|
currentAudio: null, // 当前音频实例 |
|
|
|
// 音频缓存管理 |
|
|
|
audioCache: {}, // 页面音频缓存 {pageIndex: {audios: [], totalDuration: 0}} |
|
|
|
wordAudioCache: {}, // 單詞語音緩存 |
|
|
|
currentWordAudio: null, // 當前播放的單詞音頻實例 |
|
|
|
// 音频加载状态 |
|
|
|
isAudioLoading: false, // 音频是否正在加载 |
|
|
|
hasAudioData: false, // 当前页面是否已有音频数据 |
|
|
|
// 文本高亮相关 |
|
|
|
currentHighlightIndex: -1, // 当前高亮的文本索引 |
|
|
|
wordAudioCache: {}, // 單詞語音緩存 |
|
|
|
currentWordAudio: null, // 當前播放的單詞音頻實例 |
|
|
|
courseIdList: [], |
|
|
|
bookTitle: '', |
|
|
|
courseList: [ |
|
|
|
@ -351,10 +284,7 @@ export default { |
|
|
|
// currentPageData是一个数组 其中的一个元素的type是text就会返回true |
|
|
|
return currentPageData && currentPageData.some(item => item.type === 'text'); |
|
|
|
}, |
|
|
|
// 计算音频播放进度百分比 |
|
|
|
progressPercent() { |
|
|
|
return this.totalTime > 0 ? (this.currentTime / this.totalTime) * 100 : 0; |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 动态页面标题 |
|
|
|
currentPageTitle() { |
|
|
|
@ -381,170 +311,19 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
// 获取当前页面的音频内容 |
|
|
|
async getCurrentPageAudio() { |
|
|
|
// 检查缓存中是否已有当前页面的音频数据 |
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}`; |
|
|
|
if (this.audioCache[cacheKey]) { |
|
|
|
console.log('从缓存加载音频数据:', cacheKey); |
|
|
|
// 从缓存加载音频数据 |
|
|
|
this.currentPageAudios = this.audioCache[cacheKey].audios; |
|
|
|
this.totalTime = this.audioCache[cacheKey].totalDuration; |
|
|
|
this.currentAudioIndex = 0; |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 开始加载状态 |
|
|
|
this.isAudioLoading = true; |
|
|
|
|
|
|
|
// 清空当前页面音频数组 |
|
|
|
this.currentPageAudios = []; |
|
|
|
this.currentAudioIndex = 0; |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
this.totalTime = 0; |
|
|
|
|
|
|
|
// 对着当前页面的每一个[]元素进行切割 如果是文本text类型则进行音频请求 |
|
|
|
const currentPageData = this.bookPages[this.currentPage - 1]; |
|
|
|
if (currentPageData) { |
|
|
|
// 收集所有text类型的元素 |
|
|
|
const textItems = currentPageData.filter(item => item.type === 'text'); |
|
|
|
|
|
|
|
if (textItems.length > 0) { |
|
|
|
// 并行发送所有音频请求 |
|
|
|
const audioPromises = textItems.map(async (item, index) => { |
|
|
|
try { |
|
|
|
// 进行音频请求 - 修正字段名:使用content而不是text |
|
|
|
const radioRes = await this.$api.music.textToVoice({ |
|
|
|
text: item.content, |
|
|
|
voiceType: this.voiceId, |
|
|
|
}); |
|
|
|
console.log(`音频请求响应 ${index + 1}:`, radioRes); |
|
|
|
|
|
|
|
if(radioRes.code === 200){ |
|
|
|
// 新格式:API直接返回音頻URL |
|
|
|
const audioUrl = radioRes.result.audio; |
|
|
|
// 檢查API是否返回時長信息 |
|
|
|
const duration = radioRes.result.duration || 0; |
|
|
|
|
|
|
|
// 同时保存到原始数据中以保持兼容性 |
|
|
|
item.audioUrl = audioUrl; |
|
|
|
console.log(`音频URL设置成功 ${index + 1}:`, audioUrl, '时长:', duration); |
|
|
|
|
|
|
|
return { |
|
|
|
url: audioUrl, |
|
|
|
text: item.content, |
|
|
|
duration: duration, // 優先使用API返回的時長 |
|
|
|
index: index // 保持原始顺序 |
|
|
|
}; |
|
|
|
} else { |
|
|
|
console.error(`音频请求失败 ${index + 1}:`, radioRes); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error(`音频请求异常 ${index + 1}:`, error); |
|
|
|
return null; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 等待所有音频请求完成 |
|
|
|
const audioResults = await Promise.all(audioPromises); |
|
|
|
|
|
|
|
// 按原始顺序添加到音频数组中,过滤掉失败的请求 |
|
|
|
this.currentPageAudios = audioResults |
|
|
|
.filter(result => result !== null) |
|
|
|
.sort((a, b) => a.index - b.index) |
|
|
|
.map(result => ({ |
|
|
|
url: result.url, |
|
|
|
text: result.text, |
|
|
|
duration: result.duration |
|
|
|
})); |
|
|
|
|
|
|
|
console.log('所有音频请求完成,共获取', this.currentPageAudios.length, '个音频'); |
|
|
|
} |
|
|
|
|
|
|
|
// 如果有音频,计算总时长 |
|
|
|
if (this.currentPageAudios.length > 0) { |
|
|
|
await this.calculateTotalDuration(); |
|
|
|
|
|
|
|
// 将音频数据保存到缓存中 |
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}`; |
|
|
|
this.audioCache[cacheKey] = { |
|
|
|
audios: [...this.currentPageAudios], // 深拷贝音频数组 |
|
|
|
totalDuration: this.totalTime |
|
|
|
}; |
|
|
|
console.log('音频数据已缓存:', cacheKey, this.audioCache[cacheKey]); |
|
|
|
|
|
|
|
// 限制缓存大小 |
|
|
|
this.limitCacheSize(10); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 结束加载状态 |
|
|
|
this.isAudioLoading = false; |
|
|
|
|
|
|
|
// 设置音频数据状态 |
|
|
|
this.hasAudioData = this.currentPageAudios.length > 0; |
|
|
|
|
|
|
|
// 处理AudioControls组件的事件 |
|
|
|
onAudioStateChange(audioState) { |
|
|
|
// 更新高亮状态 |
|
|
|
this.currentHighlightIndex = audioState.currentHighlightIndex; |
|
|
|
}, |
|
|
|
|
|
|
|
// 重置音频状态 |
|
|
|
resetAudioState() { |
|
|
|
// 检查当前页面是否已有缓存的音频数据 |
|
|
|
const pageKey = `${this.courseId}_${this.currentPage}`; |
|
|
|
const cachedAudio = this.audioCache[pageKey]; |
|
|
|
|
|
|
|
if (cachedAudio && cachedAudio.audios && cachedAudio.audios.length > 0) { |
|
|
|
// 如果有缓存的音频数据,恢复音频状态 |
|
|
|
this.currentPageAudios = cachedAudio.audios; |
|
|
|
this.totalTime = cachedAudio.totalDuration || 0; |
|
|
|
this.hasAudioData = true; |
|
|
|
} else { |
|
|
|
// 如果没有缓存的音频数据,重置为初始状态 |
|
|
|
this.currentPageAudios = []; |
|
|
|
this.totalTime = 0; |
|
|
|
this.hasAudioData = false; |
|
|
|
} |
|
|
|
|
|
|
|
// 重置播放状态,翻頁時統一設置播放速度為1.0 |
|
|
|
this.currentAudioIndex = 0; |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
this.isAudioLoading = false; |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
// 翻頁時強制設置播放速度為1.0(適應微信小程序特性) |
|
|
|
this.playSpeed = 1.0; |
|
|
|
}, |
|
|
|
|
|
|
|
// 手动获取音频 |
|
|
|
async handleGetAudio() { |
|
|
|
// 检查是否有音色ID |
|
|
|
if (!this.voiceId) { |
|
|
|
uni.showToast({ |
|
|
|
title: '音色未加载,请稍后重试', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查当前页面是否有文本内容 |
|
|
|
if (!this.isTextPage) { |
|
|
|
uni.showToast({ |
|
|
|
title: '当前页面没有文本内容', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查是否正在加载 |
|
|
|
if (this.isAudioLoading) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 调用获取音频方法 |
|
|
|
await this.getCurrentPageAudio(); |
|
|
|
}, |
|
|
|
onHighlightChange(highlightIndex) { |
|
|
|
// 更新高亮索引 |
|
|
|
this.currentHighlightIndex = highlightIndex; |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 获取音色列表 拿第一个做默认的音色id |
|
|
|
async getVoiceList() { |
|
|
|
const voiceRes = await this.$api.music.list() |
|
|
|
@ -1376,58 +1155,33 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
async previousPage() { |
|
|
|
// 如果正在加载音频,禁止翻页 |
|
|
|
if (this.isAudioLoading) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.currentPage > 1) { |
|
|
|
// 停止当前音频播放並銷毀實例 |
|
|
|
this.pauseAudio(); |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.destroy(); |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
|
|
|
|
this.currentPage--; |
|
|
|
// 获取对应页面的数据(如果还没有获取过) |
|
|
|
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { |
|
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
|
} |
|
|
|
|
|
|
|
// 清空当前音频状态 |
|
|
|
this.resetAudioState(); |
|
|
|
// 通知音频控制组件重置状态 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
async nextPage() { |
|
|
|
// 如果正在加载音频,禁止翻页 |
|
|
|
if (this.isAudioLoading) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.currentPage < this.bookPages.length) { |
|
|
|
// 停止当前音频播放並銷毀實例 |
|
|
|
this.pauseAudio(); |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.destroy(); |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
|
|
|
|
this.currentPage++; |
|
|
|
// 获取对应页面的数据(如果还没有获取过) |
|
|
|
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { |
|
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
|
} |
|
|
|
|
|
|
|
// 清空当前音频状态 |
|
|
|
this.resetAudioState(); |
|
|
|
// 通知音频控制组件重置状态 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
formatTime(seconds) { |
|
|
|
const mins = Math.floor(seconds / 60); |
|
|
|
const secs = Math.floor(seconds % 60); |
|
|
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
|
|
|
}, |
|
|
|
toggleSound() { |
|
|
|
console.log('音色切换') |
|
|
|
uni.navigateTo({ |
|
|
|
@ -1442,18 +1196,6 @@ export default { |
|
|
|
}) |
|
|
|
}, |
|
|
|
async goToPage(page) { |
|
|
|
// 如果正在加载音频,禁止翻页 |
|
|
|
if (this.isAudioLoading) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 停止当前音频播放並銷毀實例 |
|
|
|
this.pauseAudio(); |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.destroy(); |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
|
|
|
|
this.currentPage = page |
|
|
|
console.log('跳转到页面:', page) |
|
|
|
// 获取对应页面的数据(如果还没有获取过) |
|
|
|
@ -1461,30 +1203,22 @@ export default { |
|
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
|
} |
|
|
|
|
|
|
|
// 清空当前音频状态 |
|
|
|
this.resetAudioState(); |
|
|
|
// 通知音频控制组件重置状态 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
} |
|
|
|
}, |
|
|
|
async onSwiperChange(e) { |
|
|
|
// 如果正在加载音频,禁止翻页 |
|
|
|
if (this.isAudioLoading) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 停止当前音频播放並銷毀實例 |
|
|
|
this.pauseAudio(); |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.destroy(); |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
|
|
|
|
this.currentPage = e.detail.current + 1 |
|
|
|
// 获取对应页面的数据(如果还没有获取过) |
|
|
|
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { |
|
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
|
} |
|
|
|
|
|
|
|
// 清空当前音频状态 |
|
|
|
this.resetAudioState(); |
|
|
|
// 通知音频控制组件重置状态 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
} |
|
|
|
}, |
|
|
|
async getCourseList(id) { |
|
|
|
const res = await this.$api.book.coursePage({ |
|
|
|
@ -1500,8 +1234,10 @@ export default { |
|
|
|
this.pageTypes = this.courseIdList.map(() => '') |
|
|
|
// 初始化页面单词数组 |
|
|
|
this.pageWords = this.courseIdList.map(() => []) |
|
|
|
// 重置音频状态 |
|
|
|
this.resetAudioState() |
|
|
|
// 通知音频控制组件重置状态 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
} |
|
|
|
// 初始化第一页 |
|
|
|
if (this.courseIdList.length > 0) { |
|
|
|
await this.getBookPages(this.courseIdList[0]) |
|
|
|
@ -1600,81 +1336,58 @@ export default { |
|
|
|
}, |
|
|
|
}, |
|
|
|
async onLoad(args) { |
|
|
|
// 监听音色切换事件,传递给AudioControls组件处理 |
|
|
|
uni.$on('selectVoice', (voiceId) => { |
|
|
|
console.log('音色切換:', this.voiceId, '->', voiceId); |
|
|
|
this.voiceId = voiceId; |
|
|
|
|
|
|
|
// 音色切換時清除所有音頻緩存,因為不同音色的音頻文件不同 |
|
|
|
this.clearAudioCache(); |
|
|
|
// 清理單詞語音資源 |
|
|
|
this.clearWordAudioCache(); |
|
|
|
|
|
|
|
// 停止當前播放的音頻 |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.destroy(); |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
if (this.currentWordAudio) { |
|
|
|
this.currentWordAudio.destroy(); |
|
|
|
this.currentWordAudio = null; |
|
|
|
} |
|
|
|
|
|
|
|
// 重置音頻狀態 |
|
|
|
this.currentPageAudios = []; |
|
|
|
this.totalTime = 0; |
|
|
|
this.hasAudioData = false; |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
this.currentAudioIndex = 0; |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
this.sliderValue = 0; |
|
|
|
this.isDragging = false; |
|
|
|
|
|
|
|
// 重新請求當前頁面的音頻數據 |
|
|
|
if (this.isTextPage) { |
|
|
|
console.log('音色切換後重新獲取音頻數據'); |
|
|
|
this.getAudio(); |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
this.courseId = args.courseId |
|
|
|
this.memberId = args.memberId |
|
|
|
// 重置音频状态 |
|
|
|
this.resetAudioState() |
|
|
|
|
|
|
|
// 初始檢測播放速度支持 |
|
|
|
this.checkInitialPlaybackRateSupport() |
|
|
|
|
|
|
|
// 先获取点进来的课程的页面列表 |
|
|
|
await Promise.all([this.getCourseList(this.courseId), this.getCoursePageList(args.bookId), this.getMemberInfo(), this.getVoiceList()]) |
|
|
|
|
|
|
|
// 頁面加載完成後,自動加載第一頁音頻並播放 |
|
|
|
await this.autoLoadAndPlayFirstPage(); |
|
|
|
|
|
|
|
// 页面加载完成后,通知AudioControls组件自动加载第一页音频 |
|
|
|
this.$nextTick(() => { |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.autoLoadAndPlayFirstPage(); |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 页面卸载时清理音频资源 |
|
|
|
// 页面卸载时清理资源 |
|
|
|
onUnload() { |
|
|
|
uni.$off('selectVoice') |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.destroy(); |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
// 清理單詞語音資源 |
|
|
|
if (this.currentWordAudio) { |
|
|
|
this.currentWordAudio.destroy(); |
|
|
|
this.currentWordAudio = null; |
|
|
|
} |
|
|
|
this.isPlaying = false; |
|
|
|
// 清理音频缓存 |
|
|
|
this.clearAudioCache(); |
|
|
|
// 清理單詞語音緩存 |
|
|
|
this.clearWordAudioCache(); |
|
|
|
|
|
|
|
// 通知AudioControls组件清理资源 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.destroyAudio(); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 页面隐藏时暂停音频 |
|
|
|
onHide() { |
|
|
|
this.pauseAudio(); |
|
|
|
// 通知AudioControls组件暂停音频 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.pauseOnHide(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|
@ -2247,149 +1960,5 @@ export default { |
|
|
|
line-height: 48rpx; |
|
|
|
} |
|
|
|
|
|
|
|
/* 音频控制栏样式 */ |
|
|
|
.audio-controls { |
|
|
|
background: #fff; |
|
|
|
padding: 20rpx 40rpx; |
|
|
|
border-bottom: 1rpx solid #eee; |
|
|
|
transition: transform 0.3s ease; |
|
|
|
position: relative; |
|
|
|
z-index: 10; |
|
|
|
&.audio-hidden { |
|
|
|
transform: translateY(100%); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.audio-time { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
margin-bottom: 20rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.time-text { |
|
|
|
font-size: 28rpx; |
|
|
|
color: #999; |
|
|
|
min-width: 80rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.progress-container { |
|
|
|
flex: 1; |
|
|
|
margin: 0 20rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.progress-bar { |
|
|
|
position: relative; |
|
|
|
height: 6rpx; |
|
|
|
background: #f0f0f0; |
|
|
|
border-radius: 3rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.progress-fill { |
|
|
|
position: absolute; |
|
|
|
left: 0; |
|
|
|
top: 0; |
|
|
|
height: 100%; |
|
|
|
background: #06DADC; |
|
|
|
border-radius: 3rpx; |
|
|
|
transition: width 0.1s ease; |
|
|
|
} |
|
|
|
|
|
|
|
.progress-thumb { |
|
|
|
position: absolute; |
|
|
|
top: 50%; |
|
|
|
width: 16rpx; |
|
|
|
height: 16rpx; |
|
|
|
background: #06DADC; |
|
|
|
border-radius: 50%; |
|
|
|
transform: translate(-50%, -50%); |
|
|
|
transition: left 0.1s ease; |
|
|
|
} |
|
|
|
|
|
|
|
.audio-controls-row { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
} |
|
|
|
|
|
|
|
.control-btn { |
|
|
|
display: flex; |
|
|
|
// flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
|
|
|
|
padding: 10rpx; |
|
|
|
gap: 8rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.control-btn.disabled { |
|
|
|
pointer-events: none; |
|
|
|
opacity: 0.6; |
|
|
|
} |
|
|
|
|
|
|
|
.control-text { |
|
|
|
font-size: 28rpx; |
|
|
|
color: #4A4A4A; |
|
|
|
// margin-top: 8rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.play-btn { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
padding: 10rpx; |
|
|
|
} |
|
|
|
|
|
|
|
/* 音频加载状态样式 */ |
|
|
|
.audio-loading-container { |
|
|
|
background: #fff; |
|
|
|
padding: 40rpx; |
|
|
|
border-bottom: 1rpx solid #eee; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
gap: 20rpx; |
|
|
|
position: relative; |
|
|
|
z-index: 10; |
|
|
|
} |
|
|
|
|
|
|
|
.loading-text { |
|
|
|
font-size: 28rpx; |
|
|
|
color: #999; |
|
|
|
} |
|
|
|
|
|
|
|
/* 获取音频按钮样式 */ |
|
|
|
.audio-get-button-container { |
|
|
|
background: rgba(255, 255, 255, 0.95); |
|
|
|
backdrop-filter: blur(10px); |
|
|
|
padding: 30rpx; |
|
|
|
border-radius: 20rpx; |
|
|
|
border: 2rpx solid #E5E5E5; |
|
|
|
transition: all 0.3s ease; |
|
|
|
position: relative; |
|
|
|
z-index: 10; |
|
|
|
} |
|
|
|
|
|
|
|
.get-audio-btn { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
gap: 16rpx; |
|
|
|
padding: 20rpx 40rpx; |
|
|
|
background: linear-gradient(135deg, #06DADC 0%, #04B8BA 100%); |
|
|
|
border-radius: 50rpx; |
|
|
|
box-shadow: 0 8rpx 20rpx rgba(6, 218, 220, 0.3); |
|
|
|
transition: all 0.3s ease; |
|
|
|
} |
|
|
|
|
|
|
|
.get-audio-btn:active { |
|
|
|
transform: scale(0.95); |
|
|
|
box-shadow: 0 4rpx 10rpx rgba(6, 218, 220, 0.2); |
|
|
|
} |
|
|
|
|
|
|
|
.get-audio-text { |
|
|
|
font-size: 32rpx; |
|
|
|
color: #FFFFFF; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
</style> |