From cc93b8a742ac08e3ebca9a04bc2467f40eaedbc0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 7 May 2026 21:46:33 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=B6=88=E6=81=AF=E8=BD=AC=E5=8F=91=20v0.4=EF=BC=9A?= =?UTF-8?q?=E7=AC=AC=E5=9B=9B=E6=AC=A1=E8=AF=84=E5=AE=A1=EF=BC=88=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E6=92=AD=E6=94=BE=E5=99=A8=E8=B5=84=E6=BA=90=E9=87=8A?= =?UTF-8?q?=E6=94=BE=E6=89=93=E7=A3=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent 三轮复审后的质量打磨,无功能变更。 - useVoicePlayer.stop:audio.removeAttribute('src') + audio.load() 替代 audio.src = '';不会触发空 src 加载的 error 事件,也能让浏览器立即释放底层 decoder buffer - useVoicePlayer.play:同 key 再点的 stop() 改 stop(key),意图自解释(我想停的就是我自己) - useVoicePlayer 移除未消费的 currentKey 暴露;调用方都走 isPlaying(key) 派生 - home/index.vue onUnmounted 追加 voicePlayer.stop():模块级单例 audio 不会随视图卸载自动停,补主壳兜底 --- src/views/im/home/composables/useVoicePlayer.ts | 13 +++++++------ src/views/im/home/index.vue | 6 +++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/views/im/home/composables/useVoicePlayer.ts b/src/views/im/home/composables/useVoicePlayer.ts index 7892543f3..a944639c1 100644 --- a/src/views/im/home/composables/useVoicePlayer.ts +++ b/src/views/im/home/composables/useVoicePlayer.ts @@ -1,4 +1,4 @@ -import { computed, ref } from 'vue' +import { ref } from 'vue' /** * 语音播放全局互斥 @@ -37,7 +37,10 @@ function stop(key?: VoiceKey) { return } task.audio.pause() - task.audio.src = '' + // removeAttribute('src') + load() 是 W3C 推荐的释放姿势:不会触发空 src 加载导致的 error 事件, + // 也能让浏览器立即释放底层 decoder buffer,比 audio.src = '' 更干净 + task.audio.removeAttribute('src') + task.audio.load() currentTask.value = null } @@ -52,7 +55,7 @@ function play(key: VoiceKey, url: string) { return } if (currentTask.value?.key === key) { - stop() + stop(key) return } stop() @@ -71,11 +74,9 @@ function play(key: VoiceKey, url: string) { } export function useVoicePlayer() { - /** 当前播放的 key;给气泡 / 调试用 */ - const currentKey = computed(() => currentTask.value?.key ?? null) /** 指定 key 是否正在播放 */ function isPlaying(key: VoiceKey): boolean { return currentTask.value?.key === key } - return { currentKey, isPlaying, play, stop } + return { isPlaying, play, stop } } diff --git a/src/views/im/home/index.vue b/src/views/im/home/index.vue index fa04284ff..b342a8798 100644 --- a/src/views/im/home/index.vue +++ b/src/views/im/home/index.vue @@ -37,6 +37,7 @@ import { useDraftStore } from './store/draftStore' import { useFaceStore } from './store/faceStore' import { useMessagePuller } from './composables/useMessagePuller' import { useMessageSender } from './composables/useMessageSender' +import { useVoicePlayer } from './composables/useVoicePlayer' import { ImConversationType } from '../utils/constants' import { StorageKeys } from '../utils/storage' import type { Conversation } from './types' @@ -56,6 +57,7 @@ const draftStore = useDraftStore() const faceStore = useFaceStore() const { pullOnce } = useMessagePuller() const { readActive, syncPrivateReadStatus } = useMessageSender() +const voicePlayer = useVoicePlayer() /** 初始化:先吃本地缓存让首屏立即渲染,再远端刷新最新数据,最后建实时通信拉离线消息 */ onMounted(async () => { @@ -135,11 +137,13 @@ function onBeforeUnload() { } window.addEventListener('beforeunload', onBeforeUnload) -/** 离开 IM 主壳:主动断 WebSocket(disconnect 内部已清掉 onclose 防自动重连)+ flush 草稿 + 表情缓存 reset + 解绑 unload */ +/** 离开 IM 主壳:主动断 WebSocket(disconnect 内部已清掉 onclose 防自动重连)+ flush 草稿 + 表情缓存 reset + 解绑 unload + 停语音 */ onUnmounted(() => { webSocketStore.disconnect() draftStore.flushPersist() faceStore.reset() + // 模块级单例 audio 不会随视图卸载自动停,主动停掉避免切路由后语音继续响 + voicePlayer.stop() window.removeEventListener('beforeunload', onBeforeUnload) })