diff --git a/api/modules/music.js b/api/modules/music.js index 0247391..6b39647 100644 --- a/api/modules/music.js +++ b/api/modules/music.js @@ -9,6 +9,17 @@ export default{ method: "GET", needToken: true }) - } + }, + + // 文字转语音 + async textToVoice(data){ + return request({ + url: "/tts/textToVoice", + method: "POST", + needToken: true, + data + }) + }, + } diff --git a/subPages/home/book.vue b/subPages/home/book.vue index f27b703..ac91e8e 100644 --- a/subPages/home/book.vue +++ b/subPages/home/book.vue @@ -224,6 +224,7 @@ export default { data() { return { + voiceId: '', courseId: '', showNavbar: true, currentPage: 1, @@ -233,10 +234,17 @@ export default { // 音频控制相关数据 isPlaying: false, currentTime: 0, - totalTime: 317, // 5:17 = 317秒 + totalTime: 0, isLoop: false, playSpeed: 1.0, speedOptions: [1.0, 1.25, 1.5, 2.0], + // 音频数组管理 + currentPageAudios: [], // 当前页面的音频数组 + currentAudioIndex: 0, // 当前播放的音频索引 + audioContext: null, // 音频上下文 + currentAudio: null, // 当前音频实例 + // 音频缓存管理 + audioCache: {}, // 页面音频缓存 {pageIndex: {audios: [], totalDuration: 0}} courseIdList: [], bookTitle: '', courseList: [ @@ -271,6 +279,114 @@ export default { } }, methods: { + // 获取当前页面的音频内容 + 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.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, + voiceId: this.voiceId, + }); + console.log(`音频请求响应 ${index + 1}:`, radioRes); + + if(radioRes.code === 200){ + // API返回的是Base64编码的WAV音频数据,在uniapp环境中需要特殊处理 + const base64Data = radioRes.result; + // 在uniapp中,可以直接使用base64数据作为音频源 + const audioUrl = `data:audio/wav;base64,${base64Data}`; + + // 同时保存到原始数据中以保持兼容性 + item.audioUrl = audioUrl; + console.log(`音频URL设置成功 ${index + 1}:`, audioUrl); + + return { + url: audioUrl, + text: item.content, + duration: 0, // 音频时长,需要在加载后获取 + 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); + } + } + }, + // 获取音色列表 拿第一个做默认的音色id + async getVoiceList() { + const voiceRes = await this.$api.music.list() + if(voiceRes.code === 200){ + this.voiceId = voiceRes.result.id + console.log('111'); + + await this.getCurrentPageAudio() + } + }, toggleNavbar() { this.showNavbar = !this.showNavbar }, @@ -310,35 +426,233 @@ export default { } this.currentWordMeaning = null }, + // 计算音频总时长 + async calculateTotalDuration() { + let totalDuration = 0; + for (let i = 0; i < this.currentPageAudios.length; i++) { + const audio = this.currentPageAudios[i]; + try { + const duration = await this.getAudioDuration(audio.url); + audio.duration = duration; + totalDuration += duration; + } catch (error) { + console.error('获取音频时长失败:', error); + // 如果无法获取时长,估算一个默认值(假设每100字符约5秒) + const estimatedDuration = Math.max(5, audio.text.length / 20); + audio.duration = estimatedDuration; + totalDuration += estimatedDuration; + } + } + this.totalTime = totalDuration; + console.log('音频总时长:', totalDuration, '秒'); + }, + + // 获取音频时长 + getAudioDuration(audioUrl) { + return new Promise((resolve, reject) => { + const audio = uni.createInnerAudioContext(); + audio.src = audioUrl; + + audio.onCanplay(() => { + resolve(audio.duration || 5); // 如果无法获取时长,默认5秒 + audio.destroy(); + }); + + audio.onError((error) => { + console.error('音频加载失败:', error); + reject(error); + audio.destroy(); + }); + + // 设置超时 + setTimeout(() => { + reject(new Error('获取音频时长超时')); + audio.destroy(); + }, 3000); + }); + }, + // 音频控制方法 togglePlay() { - this.isPlaying = !this.isPlaying; - // 这里可以添加实际的音频播放/暂停逻辑 + if (this.currentPageAudios.length === 0) { + uni.showToast({ + title: '当前页面没有音频内容', + icon: 'none' + }); + return; + } + + if (this.isPlaying) { + this.pauseAudio(); + } else { + this.playAudio(); + } + }, + + // 播放音频 + playAudio() { + if (this.currentPageAudios.length === 0) return; + + // 如果没有当前音频实例或者需要切换音频 + if (!this.currentAudio || this.currentAudio.src !== this.currentPageAudios[this.currentAudioIndex].url) { + this.createAudioInstance(); + } + + this.currentAudio.play(); + this.isPlaying = true; + }, + + // 暂停音频 + pauseAudio() { + if (this.currentAudio) { + this.currentAudio.pause(); + } + this.isPlaying = false; + }, + + // 创建音频实例 + createAudioInstance() { + // 销毁之前的音频实例 + if (this.currentAudio) { + this.currentAudio.destroy(); + } + + const audio = uni.createInnerAudioContext(); + audio.src = this.currentPageAudios[this.currentAudioIndex].url; + audio.playbackRate = this.playSpeed; + + // 音频事件监听 + audio.onPlay(() => { + console.log('音频开始播放'); + this.isPlaying = true; + }); + + audio.onPause(() => { + console.log('音频暂停'); + this.isPlaying = false; + }); + + audio.onTimeUpdate(() => { + this.updateCurrentTime(); + }); + + audio.onEnded(() => { + console.log('当前音频播放结束'); + this.onAudioEnded(); + }); + + audio.onError((error) => { + console.error('音频播放错误:', error); + this.isPlaying = false; + uni.showToast({ + title: '音频播放失败', + icon: 'none' + }); + }); + + this.currentAudio = audio; + }, + + // 更新当前播放时间 + updateCurrentTime() { + if (!this.currentAudio) return; + + let totalTime = 0; + // 计算之前音频的总时长 + for (let i = 0; i < this.currentAudioIndex; i++) { + totalTime += this.currentPageAudios[i].duration; + } + // 加上当前音频的播放时间 + totalTime += this.currentAudio.currentTime; + + this.currentTime = totalTime; + }, + + // 音频播放结束处理 + onAudioEnded() { + if (this.currentAudioIndex < this.currentPageAudios.length - 1) { + // 播放下一个音频 + this.currentAudioIndex++; + this.playAudio(); + } else { + // 所有音频播放完毕 + if (this.isLoop) { + // 循环播放 + this.currentAudioIndex = 0; + this.playAudio(); + } else { + // 停止播放 + this.isPlaying = false; + this.currentTime = this.totalTime; + } + } }, toggleLoop() { this.isLoop = !this.isLoop; }, + toggleSpeed() { const currentIndex = this.speedOptions.indexOf(this.playSpeed); const nextIndex = (currentIndex + 1) % this.speedOptions.length; this.playSpeed = this.speedOptions[nextIndex]; + + // 如果当前有音频在播放,更新播放速度 + if (this.currentAudio) { + this.currentAudio.playbackRate = this.playSpeed; + } + }, + + // 上一个音频 + previousAudio() { + if (this.currentPageAudios.length === 0) return; + + if (this.currentAudioIndex > 0) { + this.currentAudioIndex--; + if (this.isPlaying) { + this.playAudio(); + } + } + }, + + // 下一个音频 + nextAudio() { + if (this.currentPageAudios.length === 0) return; + + if (this.currentAudioIndex < this.currentPageAudios.length - 1) { + this.currentAudioIndex++; + if (this.isPlaying) { + this.playAudio(); + } + } }, async previousPage() { if (this.currentPage > 1) { + // 停止当前音频播放 + this.pauseAudio(); + this.currentPage--; // 获取对应页面的数据(如果还没有获取过) if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { await this.getBookPages(this.courseIdList[this.currentPage - 1]); } + + // 重新获取当前页面的音频 + await this.getCurrentPageAudio(); } }, async nextPage() { if (this.currentPage < this.bookPages.length) { + // 停止当前音频播放 + this.pauseAudio(); + this.currentPage++; // 获取对应页面的数据(如果还没有获取过) if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { await this.getBookPages(this.courseIdList[this.currentPage - 1]); } + + // 重新获取当前页面的音频 + await this.getCurrentPageAudio(); } }, formatTime(seconds) { @@ -360,19 +674,31 @@ export default { }) }, async goToPage(page) { + // 停止当前音频播放 + this.pauseAudio(); + this.currentPage = page console.log('跳转到页面:', page) // 获取对应页面的数据(如果还没有获取过) if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) { await this.getBookPages(this.courseIdList[this.currentPage - 1]); } + + // 重新获取当前页面的音频 + await this.getCurrentPageAudio(); }, async onSwiperChange(e) { + // 停止当前音频播放 + this.pauseAudio(); + 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]); } + + // 重新获取当前页面的音频 + await this.getCurrentPageAudio(); }, async getCourseList(id) { const res = await this.$api.book.coursePage({ @@ -423,7 +749,43 @@ export default { this.courseId = args.courseId // 先获取点进来的课程的页面列表 await Promise.all([this.getCourseList(this.courseId), this.getCoursePageList(args.bookId)]) + await this.getVoiceList() + }, + + // 页面卸载时清理音频资源 + onUnload() { + if (this.currentAudio) { + this.currentAudio.destroy(); + this.currentAudio = null; + } + this.isPlaying = false; + // 清理音频缓存 + this.clearAudioCache(); + }, + + // 页面隐藏时暂停音频 + onHide() { + this.pauseAudio(); + }, + + // 清理音频缓存 + clearAudioCache() { + this.audioCache = {}; + console.log('音频缓存已清理'); + }, + + // 限制缓存大小,保留最近访问的页面 + limitCacheSize(maxSize = 10) { + const cacheKeys = Object.keys(this.audioCache); + if (cacheKeys.length > maxSize) { + // 删除最旧的缓存项 + const keysToDelete = cacheKeys.slice(0, cacheKeys.length - maxSize); + keysToDelete.forEach(key => { + delete this.audioCache[key]; + }); + console.log('缓存大小已限制,删除了', keysToDelete.length, '个缓存项'); + } } } diff --git a/subPages/user/share.vue b/subPages/user/share.vue index 9bf2070..1202c84 100644 --- a/subPages/user/share.vue +++ b/subPages/user/share.vue @@ -9,7 +9,7 @@ @@ -44,12 +44,29 @@ export default { icon: 'none' }) }, + save() { + uni.saveImageToPhotosAlbum({ + filePath: this.Qrcode, + success: (res) => { + uni.showToast({ + title: '保存成功', + icon: 'success' + }) + }, + fail: (err) => { + uni.showToast({ + title: '保存失败', + icon: 'none' + }) + } + }) + }, async getQrcode() { // const res = await this.$api.promotion.qrCode() // if (res.code === 200) { uni.getImageInfo({ src: `${this.$config.baseURL}/promotion/qrCode?token=${uni.getStorageSync('token')}`, - success: function (image) { + success: (image) => { console.log(image.width); console.log(image.path); this.Qrcode = image.path; @@ -75,6 +92,7 @@ export default { } .content { + height: 100vh; flex: 1; padding-bottom: 200rpx; display: flex; @@ -82,11 +100,14 @@ export default { justify-content: center; .image-container { + // background: red; display: flex; + // height: 100%; justify-content: center; align-items: center; .share-image { + height: 90vh; width: 670rpx; border-radius: 16rpx; }