From 96a1492b4011ceb38609caa631356f13161f47d3 Mon Sep 17 00:00:00 2001 From: huliyong <2783385703@qq.com> Date: Mon, 20 Oct 2025 00:50:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=9F=B3=E9=A2=91=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BB=9F=E4=B8=80=E9=9F=B3=E9=A2=91=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=E5=AE=9E=E7=8E=B0=E5=8D=95=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(组件): 优化MeaningPopup组件布局和样式结构 - 新增AudioManager类实现音频单例管理,支持播放控制、倍速调节和事件监听 - 重构MeaningPopup组件模板结构,使用scroll-view优化长内容滚动 - 调整样式布局提高组件可维护性 --- subPages/home/AudioControls.vue | 5998 ++++++++++++++--------------- subPages/home/book.vue | 3919 +++++++++---------- subPages/home/components/MeaningPopup.vue | 303 +- utils/audioManager.js | 524 +++ 4 files changed, 5432 insertions(+), 5312 deletions(-) create mode 100644 utils/audioManager.js diff --git a/subPages/home/AudioControls.vue b/subPages/home/AudioControls.vue index f60c24f..fa8ed28 100644 --- a/subPages/home/AudioControls.vue +++ b/subPages/home/AudioControls.vue @@ -1,3366 +1,3104 @@ \ No newline at end of file diff --git a/subPages/home/book.vue b/subPages/home/book.vue index 473d00b..35d445a 100644 --- a/subPages/home/book.vue +++ b/subPages/home/book.vue @@ -1,63 +1,49 @@  diff --git a/subPages/home/components/MeaningPopup.vue b/subPages/home/components/MeaningPopup.vue index 98a075a..9ac4414 100644 --- a/subPages/home/components/MeaningPopup.vue +++ b/subPages/home/components/MeaningPopup.vue @@ -1,225 +1,220 @@ \ No newline at end of file diff --git a/utils/audioManager.js b/utils/audioManager.js new file mode 100644 index 0000000..44d718a --- /dev/null +++ b/utils/audioManager.js @@ -0,0 +1,524 @@ +/** + * 统一音频管理器 + * 实现单实例音频管理,避免多个音频同时播放造成的冲突 + */ +class AudioManager { + constructor() { + // 当前音频实例 + this.currentAudio = null; + // 音频类型:'sentence' | 'word' + this.currentAudioType = null; + // 音频状态 + this.isPlaying = false; + // 复用的音频实例池 + this.audioInstance = null; + // 事件监听器 + this.listeners = { + play: [], + pause: [], + ended: [], + error: [], + timeupdate: [], + canplay: [] + }; + // 播放速度支持检测 + this.playbackRateSupported = true; + + // 统一音频配置 + this.globalPlaybackRate = 1.0; // 全局播放速度 + this.globalVoiceId = ''; // 全局音色ID + this.speedOptions = [0.5, 0.8, 1.0, 1.25, 1.5, 2.0]; // 支持的播放速度选项 + } + + /** + * 创建HTML5 Audio实例并包装为uni-app兼容接口 + */ + createHTML5Audio() { + const audio = new Audio(); + + // 包装为uni-app兼容的接口 + const wrappedAudio = { + // 原生HTML5 Audio实例 + _nativeAudio: audio, + + // 基本属性 + get src() { return audio.src; }, + set src(value) { audio.src = value; }, + + get duration() { return audio.duration || 0; }, + get currentTime() { return audio.currentTime || 0; }, + + get paused() { return audio.paused; }, + + // 支持倍速的关键属性 + get playbackRate() { return audio.playbackRate; }, + set playbackRate(value) { + try { + audio.playbackRate = value; + console.log(`🎵 HTML5 Audio倍速设置成功: ${value}x`); + } catch (error) { + console.error('❌ HTML5 Audio倍速设置失败:', error); + } + }, + + // 基本方法 + play() { + return audio.play().catch(error => { + console.error('HTML5 Audio播放失败:', error); + }); + }, + + pause() { + audio.pause(); + }, + + stop() { + audio.pause(); + audio.currentTime = 0; + }, + + seek(time) { + audio.currentTime = time; + }, + + destroy() { + audio.pause(); + audio.src = ''; + audio.load(); + }, + + // 事件绑定方法 + onCanplay(callback) { + audio.addEventListener('canplay', callback); + }, + + onPlay(callback) { + audio.addEventListener('play', callback); + }, + + onPause(callback) { + audio.addEventListener('pause', callback); + }, + + onEnded(callback) { + audio.addEventListener('ended', callback); + }, + + onTimeUpdate(callback) { + audio.addEventListener('timeupdate', callback); + }, + + onError(callback) { + // 包装错误事件,过滤掉非关键错误 + const wrappedCallback = (error) => { + // 只在有src且音频正在播放时才传递错误事件 + if (audio.src && audio.src.trim() !== '' && !audio.paused) { + callback(error); + } else { + console.log('🔇 HTML5 Audio错误(已忽略):', { + hasSrc: !!audio.src, + paused: audio.paused, + errorType: error.type || 'unknown' + }); + } + }; + audio.addEventListener('error', wrappedCallback); + }, + + // 移除事件监听 + offCanplay(callback) { + audio.removeEventListener('canplay', callback); + }, + + offPlay(callback) { + audio.removeEventListener('play', callback); + }, + + offPause(callback) { + audio.removeEventListener('pause', callback); + }, + + offEnded(callback) { + audio.removeEventListener('ended', callback); + }, + + offTimeUpdate(callback) { + audio.removeEventListener('timeupdate', callback); + }, + + offError(callback) { + audio.removeEventListener('error', callback); + } + }; + + return wrappedAudio; + } + + /** + * 创建音频实例 + * 优先使用HTML5 Audio(支持倍速),降级到uni.createInnerAudioContext + * 复用已存在的音频实例以提高性能 + */ + createAudioInstance() { + // 如果已有音频实例,直接复用 + if (this.audioInstance) { + console.log('🔄 复用现有音频实例'); + return this.audioInstance; + } + + // 在H5环境下优先使用HTML5 Audio + // #ifdef H5 + try { + this.audioInstance = this.createHTML5Audio(); + console.log('🎵 创建新的HTML5 Audio实例'); + } catch (error) { + console.warn('⚠️ HTML5 Audio创建失败,降级到uni音频:', error); + audio = uni.createInnerAudioContext(); + this.playbackRateSupported = false; + } + // #endif + + // 在非H5环境下使用uni音频 + // #ifndef H5 + this.audioInstance = uni.createInnerAudioContext(); + this.playbackRateSupported = false; + console.log('🎵 创建新的uni音频实例'); + // #endif + + return this.audioInstance; + } + + /** + * 停止当前音频 + */ + stopCurrentAudio() { + if (this.currentAudio) { + console.log(`🛑 停止当前音频 (类型: ${this.currentAudioType})`); + + try { + this.currentAudio.pause(); + // 不销毁音频实例,保留以供复用 + // this.currentAudio.destroy(); + } catch (error) { + console.error('⚠️ 停止音频时出错:', error); + } + + this.currentAudio = null; + this.currentAudioType = null; + this.isPlaying = false; + + // 触发暂停事件 + this.emit('pause'); + } + } + + /** + * 播放音频 + * @param {string} audioUrl - 音频URL + * @param {string} audioType - 音频类型 ('sentence' | 'word') + * @param {Object} options - 播放选项 + */ + async playAudio(audioUrl, audioType = 'word', options = {}) { + try { + console.log(`🎵 开始播放${audioType}音频:`, audioUrl); + + // 停止当前播放的音频 + this.stopCurrentAudio(); + + // 创建新的音频实例 + const audio = this.createAudioInstance(); + audio.src = audioUrl; + + // 设置播放选项,优先使用全局播放速度 + const playbackRate = options.playbackRate || this.globalPlaybackRate; + if (playbackRate && this.playbackRateSupported) { + audio.playbackRate = playbackRate; + console.log(`🎵 设置音频播放速度: ${playbackRate}x`); + } + + // 绑定事件监听器 + this.bindAudioEvents(audio, audioType); + + // 保存当前音频实例 + this.currentAudio = audio; + this.currentAudioType = audioType; + + // 延迟播放,确保音频实例完全准备好 + setTimeout(() => { + if (this.currentAudio === audio) { + try { + audio.play(); + console.log(`✅ ${audioType}音频开始播放`); + } catch (playError) { + console.error('❌ 播放命令失败:', playError); + this.emit('error', playError); + } + } + }, 100); + + return audio; + } catch (error) { + console.error('❌ 播放音频异常:', error); + this.emit('error', error); + throw error; + } + } + + /** + * 绑定音频事件监听器 + */ + bindAudioEvents(audio, audioType) { + // 播放开始 + audio.onPlay(() => { + console.log(`🎵 ${audioType}音频播放开始`); + this.isPlaying = true; + this.emit('play', { audioType, audio }); + }); + + // 播放暂停 + audio.onPause(() => { + console.log(`⏸️ ${audioType}音频播放暂停`); + this.isPlaying = false; + this.emit('pause', { audioType, audio }); + }); + + // 播放结束 + audio.onEnded(() => { + console.log(`🏁 ${audioType}音频播放结束`); + this.isPlaying = false; + + // 不销毁音频实例,保留以供复用 + // 只清理当前播放状态 + if (this.currentAudio === audio) { + this.currentAudio = null; + this.currentAudioType = null; + } + + this.emit('ended', { audioType, audio }); + }); + + // 播放错误 + audio.onError((error) => { + console.error(`❌ ${audioType}音频播放失败:`, error); + this.isPlaying = false; + + // 发生错误时才销毁音频实例 + try { + audio.destroy(); + } catch (destroyError) { + console.error('⚠️ 销毁音频实例时出错:', destroyError); + } + + if (this.currentAudio === audio) { + this.currentAudio = null; + this.currentAudioType = null; + } + + // 清理复用的音频实例引用 + if (this.audioInstance === audio) { + this.audioInstance = null; + } + + this.emit('error', { error, audioType, audio }); + }); + + // 时间更新 + audio.onTimeUpdate(() => { + this.emit('timeupdate', { + currentTime: audio.currentTime, + duration: audio.duration, + audioType, + audio + }); + }); + + // 可以播放 + audio.onCanplay(() => { + this.emit('canplay', { audioType, audio }); + }); + } + + /** + * 暂停当前音频 + */ + pause() { + if (this.currentAudio && this.isPlaying) { + this.currentAudio.pause(); + } + } + + /** + * 恢复播放 + */ + resume() { + if (this.currentAudio && !this.isPlaying) { + this.currentAudio.play(); + } + } + + /** + * 设置播放速度 + */ + setPlaybackRate(rate) { + if (this.currentAudio && this.playbackRateSupported) { + this.currentAudio.playbackRate = rate; + return true; + } + return false; + } + + /** + * 设置全局播放速度 + * @param {number} rate - 播放速度 (0.5 - 2.0) + */ + setGlobalPlaybackRate(rate) { + if (rate < 0.5 || rate > 2.0) { + console.warn('⚠️ 播放速度超出支持范围 (0.5-2.0):', rate); + return false; + } + + this.globalPlaybackRate = rate; + console.log(`🎵 设置全局播放速度: ${rate}x`); + + // 如果当前有音频在播放,立即应用新的播放速度 + if (this.currentAudio && this.playbackRateSupported) { + try { + this.currentAudio.playbackRate = rate; + console.log(`✅ 当前音频播放速度已更新: ${rate}x`); + } catch (error) { + console.error('❌ 更新当前音频播放速度失败:', error); + } + } + + return true; + } + + /** + * 获取全局播放速度 + */ + getGlobalPlaybackRate() { + return this.globalPlaybackRate; + } + + /** + * 设置全局音色ID + * @param {string|number} voiceId - 音色ID + */ + setGlobalVoiceId(voiceId) { + this.globalVoiceId = String(voiceId); + console.log(`🎵 设置全局音色ID: ${this.globalVoiceId}`); + } + + /** + * 获取全局音色ID + */ + getGlobalVoiceId() { + return this.globalVoiceId; + } + + /** + * 获取支持的播放速度选项 + */ + getSpeedOptions() { + return [...this.speedOptions]; + } + + /** + * 切换到下一个播放速度 + */ + togglePlaybackRate() { + const currentIndex = this.speedOptions.indexOf(this.globalPlaybackRate); + const nextIndex = (currentIndex + 1) % this.speedOptions.length; + const nextRate = this.speedOptions[nextIndex]; + + this.setGlobalPlaybackRate(nextRate); + return nextRate; + } + + /** + * 获取当前播放状态 + */ + getPlaybackState() { + return { + isPlaying: this.isPlaying, + currentAudioType: this.currentAudioType, + hasAudio: !!this.currentAudio, + playbackRateSupported: this.playbackRateSupported, + currentTime: this.currentAudio ? this.currentAudio.currentTime : 0, + duration: this.currentAudio ? this.currentAudio.duration : 0 + }; + } + + /** + * 添加事件监听器 + */ + on(event, callback) { + if (this.listeners[event]) { + // 检查是否已经绑定过相同的回调函数,避免重复绑定 + if (this.listeners[event].indexOf(callback) === -1) { + this.listeners[event].push(callback); + console.log(`🎵 添加事件监听器: ${event}, 当前监听器数量: ${this.listeners[event].length}`); + } else { + console.warn(`⚠️ 事件监听器已存在,跳过重复绑定: ${event}`); + } + } + } + + /** + * 移除事件监听器 + */ + off(event, callback) { + if (this.listeners[event]) { + const index = this.listeners[event].indexOf(callback); + if (index > -1) { + this.listeners[event].splice(index, 1); + } + } + } + + /** + * 触发事件 + */ + emit(event, data) { + if (this.listeners[event]) { + this.listeners[event].forEach(callback => { + try { + callback(data); + } catch (error) { + console.error(`事件监听器执行错误 (${event}):`, error); + } + }); + } + } + + /** + * 销毁音频管理器 + */ + destroy() { + this.stopCurrentAudio(); + + // 销毁复用的音频实例 + // if (this.audioInstance) { + // try { + // this.audioInstance.destroy(); + // } catch (error) { + // console.error('⚠️ 销毁音频实例时出错:', error); + // } + // this.audioInstance = null; + // } + + this.listeners = { + play: [], + pause: [], + ended: [], + error: [], + timeupdate: [], + canplay: [] + }; + } +} + +// 创建全局单例 +const audioManager = new AudioManager(); + +export default audioManager; \ No newline at end of file