Browse Source

refactor(audio): 优化音频控制组件逻辑和会员限制处理

重构音频控制组件,改进音色切换处理流程,增加会员限制检查,优化页面切换时的音频加载逻辑。移除未使用的授权功能,修复若干音频播放相关问题。

- 改进音色切换处理,增加请求取消和状态管理
- 添加会员限制检查,非会员无法播放会员专属内容
- 优化页面切换时的音频加载和缓存逻辑
- 移除未使用的授权功能和相关代码
- 修复音频播放中的高亮和进度显示问题
- 改进错误处理和重试机制
hfll
hflllll 1 month ago
parent
commit
624422f984
9 changed files with 985 additions and 246 deletions
  1. +781
    -113
      subPages/home/AudioControls.vue
  2. +177
    -63
      subPages/home/book.vue
  3. +16
    -7
      subPages/home/directory.vue
  4. +6
    -7
      subPages/home/music.vue
  5. +3
    -4
      subPages/home/submit.vue
  6. +1
    -1
      subPages/user/cash.vue
  7. +1
    -1
      subPages/user/promote.vue
  8. +0
    -48
      utils/common.js
  9. +0
    -2
      utils/index.js

+ 781
- 113
subPages/home/AudioControls.vue
File diff suppressed because it is too large
View File


+ 177
- 63
subPages/home/book.vue View File

@ -27,10 +27,10 @@
<view class="content-area" @click="toggleNavbar"> <view class="content-area" @click="toggleNavbar">
<!-- 会员限制页面 --> <!-- 会员限制页面 -->
<view v-if="!isMember && pagePay[index] === 'Y'" class="member-content">
<view v-if="!isMember && pagePay[index] === 'Y'" class="member-content" >
<text class="member-title">{{ pageTitles[index] }}</text> <text class="member-title">{{ pageTitles[index] }}</text>
<view class="member-button" @click="unlockBook">
<text class="member-button-text">級會員解鎖</text>
<view class="member-button" @click.stop="unlockBook">
<text class="member-button-text">级会员解锁</text>
</view> </view>
</view> </view>
@ -99,10 +99,15 @@
:voice-id="voiceId" :voice-id="voiceId"
:book-pages="bookPages" :book-pages="bookPages"
:is-text-page="isTextPage" :is-text-page="isTextPage"
:is-member="isMember"
:current-page-requires-member="currentPageRequiresMember"
:page-pay="pagePay"
@previous-page="previousPage" @previous-page="previousPage"
@next-page="nextPage" @next-page="nextPage"
@audio-state-change="onAudioStateChange" @audio-state-change="onAudioStateChange"
@highlight-change="onHighlightChange" @highlight-change="onHighlightChange"
@voice-change-complete="onVoiceChangeComplete"
@voice-change-error="onVoiceChangeError"
ref="audioControls" ref="audioControls"
/> />
@ -194,7 +199,7 @@
</view> </view>
<view class="meaning-content"> <view class="meaning-content">
<image class="meaning-image" src="/static/默认图片.png" mode="aspectFill"></image>
<image v-if="currentWordMeaning.imag" class="meaning-image" :src="currentWordMeaning.image" mode="aspectFill"></image>
<view class="word-info"> <view class="word-info">
<view class="word-main"> <view class="word-main">
@ -251,6 +256,11 @@ export default {
currentHighlightIndex: -1, // currentHighlightIndex: -1, //
wordAudioCache: {}, // wordAudioCache: {}, //
currentWordAudio: null, // currentWordAudio: null, //
//
isAudioLoading: false, //
hasAudioData: false, //
audioLoadFailed: false, //
courseIdList: [], courseIdList: [],
bookTitle: '', bookTitle: '',
courseList: [ courseList: [
@ -301,6 +311,11 @@ export default {
// //
currentPageWords() { currentPageWords() {
return this.pageWords[this.currentPage - 1] || []; return this.pageWords[this.currentPage - 1] || [];
},
//
currentPageRequiresMember() {
return this.pagePay[this.currentPage - 1] === 'Y';
} }
}, },
methods: { methods: {
@ -318,6 +333,54 @@ export default {
onAudioStateChange(audioState) { onAudioStateChange(audioState) {
// //
this.currentHighlightIndex = audioState.currentHighlightIndex; this.currentHighlightIndex = audioState.currentHighlightIndex;
// UI
if (audioState.hasOwnProperty('isLoading')) {
this.isAudioLoading = audioState.isLoading;
}
//
if (audioState.hasOwnProperty('hasAudioData')) {
this.hasAudioData = audioState.hasAudioData;
}
//
if (audioState.hasOwnProperty('audioLoadFailed')) {
this.audioLoadFailed = audioState.audioLoadFailed;
}
},
//
onVoiceChangeComplete(data) {
console.log('音色切换完成:', data);
// UI
if (data.hasAudioData) {
console.log('新音色当前页面音频已加载完成');
} else {
console.log('当前页面没有音频数据');
}
//
if (data.preloadAllPages) {
console.log('正在后台预加载所有页面的新音色音频...');
//
uni.showToast({
title: '正在加载新音色...',
icon: 'loading',
duration: 2000
});
}
},
//
onVoiceChangeError(error) {
console.error('音色切换失败:', error);
//
uni.showToast({
title: '音色切换失败,请重试',
icon: 'none',
duration: 2000
});
}, },
// //
@ -375,7 +438,14 @@ export default {
} }
// //
// type'1'
if (!this.isTextPage) { if (!this.isTextPage) {
//
if (this.currentPageType === '1') {
console.log('卡片页面的文本点击,单词播放由handleWordClick处理');
return;
}
console.log('当前页面不是文本页面'); console.log('当前页面不是文本页面');
uni.showToast({ uni.showToast({
title: '当前页面不支持音频播放', title: '当前页面不支持音频播放',
@ -424,8 +494,10 @@ export default {
async getVoiceList() { async getVoiceList() {
const voiceRes = await this.$api.music.list() const voiceRes = await this.$api.music.list()
if(voiceRes.code === 200){ if(voiceRes.code === 200){
this.voiceId = voiceRes.result[0].voiceType
console.log('获取默认音色ID:', this.voiceId);
console.log('音色列表API返回:', voiceRes.result);
console.log('第一个音色数据:', voiceRes.result[0]);
this.voiceId = Number(voiceRes.result[0].voiceType)
console.log('获取默认音色ID:', this.voiceId, '类型:', typeof this.voiceId);
} }
}, },
toggleNavbar() { toggleNavbar() {
@ -495,8 +567,9 @@ export default {
this.currentWordAudio.destroy(); this.currentWordAudio.destroy();
this.currentWordAudio = null; this.currentWordAudio = null;
} }
// 調API // 調API
console.log('playWordAudio - voiceId值:', this.voiceId, '类型:', typeof this.voiceId);
const audioRes = await this.$api.music.textToVoice({ const audioRes = await this.$api.music.textToVoice({
text: word, text: word,
voiceType: this.voiceId voiceType: this.voiceId
@ -1257,11 +1330,6 @@ export default {
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) {
await this.getBookPages(this.courseIdList[this.currentPage - 1]); await this.getBookPages(this.courseIdList[this.currentPage - 1]);
} }
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
} }
}, },
async nextPage() { async nextPage() {
@ -1271,14 +1339,29 @@ export default {
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) {
await this.getBookPages(this.courseIdList[this.currentPage - 1]); await this.getBookPages(this.courseIdList[this.currentPage - 1]);
} }
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
} }
}, },
toggleSound() { toggleSound() {
//
if (this.isAudioLoading) {
uni.showToast({
title: '音频加载中,请稍后再试',
icon: 'none',
duration: 2000
});
return;
}
// AudioControls
if (this.$refs.audioControls && this.$refs.audioControls.isAudioLoading) {
uni.showToast({
title: '音频加载中,请稍后再试',
icon: 'none',
duration: 2000
});
return;
}
console.log('音色切换') console.log('音色切换')
uni.navigateTo({ uni.navigateTo({
url: '/subPages/home/music?voiceId=' + this.voiceId url: '/subPages/home/music?voiceId=' + this.voiceId
@ -1298,21 +1381,6 @@ export default {
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) {
await this.getBookPages(this.courseIdList[this.currentPage - 1]); await this.getBookPages(this.courseIdList[this.currentPage - 1]);
} }
//
if (this.$refs.audioControls) {
//
const hasPreloadedAudio = this.$refs.audioControls.checkAudioCache(this.currentPage);
if (hasPreloadedAudio) {
//
console.log(`${this.currentPage}页音频已预加载,自动播放`);
this.$refs.audioControls.autoPlayCachedAudio();
} else {
//
this.$refs.audioControls.resetAudioState();
}
}
}, },
async onSwiperChange(e) { async onSwiperChange(e) {
this.currentPage = e.detail.current + 1 this.currentPage = e.detail.current + 1
@ -1320,27 +1388,28 @@ export default {
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) {
await this.getBookPages(this.courseIdList[this.currentPage - 1]); await this.getBookPages(this.courseIdList[this.currentPage - 1]);
} }
//
if (this.$refs.audioControls) {
//
const hasPreloadedAudio = this.$refs.audioControls.checkAudioCache(this.currentPage);
if (hasPreloadedAudio) {
//
console.log(`${this.currentPage}页音频已预加载,自动播放`);
this.$refs.audioControls.autoPlayCachedAudio();
} else {
//
this.$refs.audioControls.resetAudioState();
}
}
}, },
async getCourseList(id) { async getCourseList(id) {
const res = await this.$api.book.coursePage({ const res = await this.$api.book.coursePage({
id: id id: id
}) })
if (res.code === 200) { if (res.code === 200) {
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetForCourseChange();
}
//
this.currentPage = 1; //
this.currentCourse = 1; //
this.currentWordMeaning = null; //
this.currentWordAudio = null; //
this.currentHighlightIndex = -1; //
//
this.clearWordAudioCache();
//
this.courseIdList = res.result.map(item => item.id) this.courseIdList = res.result.map(item => item.id)
// //
this.bookPages = this.courseIdList.map(() => []) this.bookPages = this.courseIdList.map(() => [])
@ -1350,13 +1419,30 @@ export default {
this.pageTypes = this.courseIdList.map(() => '') this.pageTypes = this.courseIdList.map(() => '')
// //
this.pageWords = this.courseIdList.map(() => []) this.pageWords = this.courseIdList.map(() => [])
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
console.log('课程切换完成,courseId:', id, '总页数:', this.courseIdList.length);
// //
if (this.courseIdList.length > 0) { if (this.courseIdList.length > 0) {
await this.getBookPages(this.courseIdList[0]) await this.getBookPages(this.courseIdList[0])
//
console.log('课程切换完成,准备加载音频,当前页面类型:', this.isTextPage);
// 使$nextTickDOM
this.$nextTick(async () => {
if (this.$refs.audioControls) {
console.log('开始自动加载课程音频');
try {
// getCurrentPageAudio
await this.$refs.audioControls.getCurrentPageAudio();
console.log('课程切换后音频加载完成');
} catch (error) {
console.error('课程切换后音频加载失败:', error);
}
}
});
// //
this.preloadNextPages() this.preloadNextPages()
} }
@ -1528,8 +1614,24 @@ export default {
}, },
async onLoad(args) { async onLoad(args) {
// AudioControls // AudioControls
uni.$on('selectVoice', (voiceId) => {
console.log('音色切換:', this.voiceId, '->', voiceId);
uni.$on('selectVoice', async (voiceId) => {
if (this.voiceId === voiceId) {
console.log('音色未變化,跳過處理');
return;
}
//
if (this.isAudioLoading || (this.$refs.audioControls && this.$refs.audioControls.isAudioLoading)) {
console.log('音频正在加载中,阻止音色切换');
uni.showToast({
title: '音频加载中,请稍后再试',
icon: 'none',
duration: 2000
});
return;
}
// ID
this.voiceId = voiceId; this.voiceId = voiceId;
// //
@ -1538,24 +1640,36 @@ export default {
this.currentWordAudio.destroy(); this.currentWordAudio.destroy();
this.currentWordAudio = null; this.currentWordAudio = null;
} }
// AudioControls
if (this.$refs.audioControls) {
try {
console.log('开始处理音色切换,清理所有音频缓存并重新获取所有页面音频');
// preloadAllPages: true
await this.$refs.audioControls.handleVoiceChange(voiceId, {
preloadAllPages: true
});
console.log('音色切换处理完成');
} catch (error) {
console.error('音色切换处理失败:', error);
}
}
}) })
this.courseId = args.courseId this.courseId = args.courseId
this.memberId = args.memberId this.memberId = args.memberId
// //
await Promise.all([this.getCourseList(this.courseId), this.getCoursePageList(args.bookId), this.getMemberInfo(), this.getVoiceList()])
await Promise.all([this.getVoiceList(),this.getMemberInfo(),this.getCourseList(this.courseId), this.getCoursePageList(args.bookId)])
// AudioControls
this.$nextTick(() => {
if (this.$refs.audioControls) {
this.$refs.audioControls.autoLoadAndPlayFirstPage();
}
});
// AudioControls
this.$nextTick(() => {
if (this.$refs.audioControls) {
this.$refs.audioControls.autoLoadAndPlayFirstPage();
}
});
}, },
// //
onUnload() { onUnload() {
uni.$off('selectVoice') uni.$off('selectVoice')


+ 16
- 7
subPages/home/directory.vue View File

@ -41,12 +41,12 @@
<view class="course-list"> <view class="course-list">
<!-- 限制在做多五個課程 --> <!-- 限制在做多五個課程 -->
<view <view
v-for="(course, index) in courseList.records.slice(0, 5)"
@click="startLearning(course.id)"
v-for="(course, index) in computedList"
:key="index" :key="index"
class="course-item" class="course-item"
@click="startLearning(course.id)"
> >
<view class="course-number">{{ String(index + 1).padStart(2, '0') }}</view>
<view class="course-number" >{{ String(index + 1).padStart(2, '0') }}</view>
<view class="course-content"> <view class="course-content">
<view class="course-name">{{ course.english }}</view> <view class="course-name">{{ course.english }}</view>
<view class="course-subtitle">{{ course.chinese }}</view> <view class="course-subtitle">{{ course.chinese }}</view>
@ -117,7 +117,7 @@
> >
<view class="course-popup"> <view class="course-popup">
<view class="popup-header"> <view class="popup-header">
<view>
<view @click="closeAllCoursePopup">
<uv-icon name="arrow-down" color="black" size="20"></uv-icon> <uv-icon name="arrow-down" color="black" size="20"></uv-icon>
</view> </view>
<view class="popup-title">全部课程</view> <view class="popup-title">全部课程</view>
@ -170,6 +170,9 @@ export default {
displayAllCourseList() { displayAllCourseList() {
const list = this.allCourseList.length > 0 ? this.allCourseList : this.courseList.records || []; const list = this.allCourseList.length > 0 ? this.allCourseList : this.courseList.records || [];
return this.isCourseSortReversed ? [...list].reverse() : list; return this.isCourseSortReversed ? [...list].reverse() : list;
},
computedList(){
return this.courseList.records.slice(0, 5)
} }
}, },
methods: { methods: {
@ -190,12 +193,13 @@ export default {
} }
}, },
// //
startLearning(id) {
startLearning(id) {
// //
uni.navigateTo({ uni.navigateTo({
url: '/subPages/home/book?courseId=' + id + '&bookId=' + this.id + '&memberId=' + this.bookInfo.vip url: '/subPages/home/book?courseId=' + id + '&bookId=' + this.id + '&memberId=' + this.bookInfo.vip
}) })
}, },
// //
async getDetail() { async getDetail() {
const detailRes = await this.$api.book.detail({ const detailRes = await this.$api.book.detail({
@ -210,7 +214,7 @@ export default {
const courseRes = await this.$api.book.course({ const courseRes = await this.$api.book.course({
id: this.id, id: this.id,
pageNo: 1, pageNo: 1,
pageSize: 5
pageSize: 999
}) })
if (courseRes.code === 200){ if (courseRes.code === 200){
this.courseList = courseRes.result this.courseList = courseRes.result
@ -223,11 +227,16 @@ export default {
showAllCoursePopup() { showAllCoursePopup() {
this.$refs.allCoursePopup.open() this.$refs.allCoursePopup.open()
}, },
//
closeAllCoursePopup() {
this.$refs.allCoursePopup.close()
},
// //
toggleCourseSort() { toggleCourseSort() {
this.isCourseSortReversed = !this.isCourseSortReversed this.isCourseSortReversed = !this.isCourseSortReversed
},
}
}, },
onLoad(options) { onLoad(options) {
if (options.id){ if (options.id){


+ 6
- 7
subPages/home/music.vue View File

@ -8,7 +8,7 @@
:key="voice.voiceType" :key="voice.voiceType"
class="voice-item" class="voice-item"
:class="{ 'selected': voice.voiceType === selectedVoiceId }" :class="{ 'selected': voice.voiceType === selectedVoiceId }"
@click="selectVoice(voice.voiceType)"
@click="selectedVoiceId = voice.voiceType"
> >
<view class="voice-avatar"> <view class="voice-avatar">
<view class="play-icon"> <view class="play-icon">
@ -64,15 +64,14 @@ export default {
goBack() { goBack() {
uni.navigateBack() uni.navigateBack()
}, },
selectVoice(voiceId) {
this.selectedVoiceId = voiceId
console.log('选择音色:', voiceId)
uni.$emit('selectVoice', voiceId)
selectVoice() {
console.log('选择音色:', this.selectedVoiceId)
uni.$emit('selectVoice', this.selectedVoiceId)
uni.navigateBack()
}, },
confirmSelection() { confirmSelection() {
console.log('确认选择音色:', this.selectedVoiceId)
this.selectVoice(this.selectedVoiceId)
// //
uni.navigateBack()
}, },
async getVoice(){ async getVoice(){
const listRes = await this.$api.music.list() const listRes = await this.$api.music.list()


+ 3
- 4
subPages/home/submit.vue View File

@ -104,7 +104,7 @@ export default {
phone: '', phone: '',
looking: '', looking: '',
background: '', background: '',
id: this.id
linkId: this.linkId
} }
} }
}, },
@ -130,8 +130,7 @@ export default {
try{ try{
const subRes = await this.$api.home.getSignup({ const subRes = await this.$api.home.getSignup({
...this.formData,
id: this.id
...this.formData
}) })
if (subRes.code === 200) { if (subRes.code === 200) {
uni.showToast({ uni.showToast({
@ -157,7 +156,7 @@ export default {
} }
}, },
onLoad(options) { onLoad(options) {
this.linkId = options.id
this.formData.linkId = options.id
} }
} }
</script> </script>


+ 1
- 1
subPages/user/cash.vue View File

@ -6,7 +6,7 @@
<view class="form-container"> <view class="form-container">
<view class="header"> <view class="header">
<view class="title">微信提现</view> <view class="title">微信提现</view>
<view class="flow-link">过往流水 ></view>
<!-- <view class="flow-link">过往流水 ></view> -->
</view> </view>
<!-- 真实姓名 --> <!-- 真实姓名 -->
<view class="form-item"> <view class="form-item">


+ 1
- 1
subPages/user/promote.vue View File

@ -114,7 +114,7 @@
<text class="status" <text class="status"
:class="item.withdrawal.status === '1' :class="item.withdrawal.status === '1'
&& item.withdrawal.withdrawStatus === '0' ? 'status-available' : 'status-received'" && item.withdrawal.withdrawStatus === '0' ? 'status-available' : 'status-received'"
v-if="item.withdrawal && item.withdrawal.status != '1'">{{ getText(item.withdrawal) }}</text>
v-else-if="item.withdrawal ">{{ getText(item.withdrawal) }}</text>
</view> </view>
</view> </view>
</view> </view>


+ 0
- 48
utils/common.js View File

@ -1,50 +1,3 @@
// 用来保存图片的函数
// params: 需要授权的内容
// scope.userInfo 用户信息
// scope.userLocation 地理位置
// scope.userLocationBackground 后台定位 微信小程序
// scope.address 通信地址
// scope.record 录音功能
// scope.writePhotosAlbum 保存到相册 抖音小程序的返回值是scope.album
// scope.camera 摄像头
// scope.invoice 获取发票
// scope.invoiceTitle 发票抬头
// scope.werun wx.getWeRunData 微信运动步数
const scopeList = {
userInfo: 'scope.userInfo',
userLocation: 'scope.userLocation',
userLocationBackground: 'scope.userLocationBackground',
address: 'scope.address',
record: 'scope.record',
writePhotosAlbum: 'scope.writePhotosAlbum',
camera: 'scope.camera',
invoice: 'scope.invoice',
invoiceTitle: 'scope.invoiceTitle',
werun: 'scope.werun',
}
const authorize = ({
scope,
successfn,
failfn,
}) => {
if (!scopeList[scope]) {
uni.showToast({
title: 'scope参数错误',
icon: 'error'
})
return
}
uni.authorize({
scope: scopeList[scope],
success() {
successfn()
},
fail() {
failfn()
}
})
}
// 检验手机号格式 // 检验手机号格式
const checkPhone = (phone) => { const checkPhone = (phone) => {
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
@ -141,7 +94,6 @@ const wxPay = (paymentData, successCallback, failCallback) => {
} }
export { export {
authorize,
checkPhone, checkPhone,
formatTime, formatTime,
calculateDateDifference, calculateDateDifference,


+ 0
- 2
utils/index.js View File

@ -1,5 +1,4 @@
import { import {
authorize,
checkPhone, checkPhone,
formatTime, formatTime,
calculateDateDifference, calculateDateDifference,
@ -13,7 +12,6 @@ import {
} from '@/utils/upload' } from '@/utils/upload'
export default { export default {
authorize,
checkPhone, checkPhone,
formatTime, formatTime,
calculateDateDifference, calculateDateDifference,


Loading…
Cancel
Save