feat(im): 初始化消息转发 v0.4:第四次评审(语音播放器资源释放打磨)

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 不会随视图卸载自动停,补主壳兜底
im
YunaiV 2026-05-07 21:46:33 +08:00
parent 0b07091e79
commit cc93b8a742
2 changed files with 12 additions and 7 deletions

View File

@ -1,4 +1,4 @@
import { computed, ref } from 'vue' import { ref } from 'vue'
/** /**
* *
@ -37,7 +37,10 @@ function stop(key?: VoiceKey) {
return return
} }
task.audio.pause() 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 currentTask.value = null
} }
@ -52,7 +55,7 @@ function play(key: VoiceKey, url: string) {
return return
} }
if (currentTask.value?.key === key) { if (currentTask.value?.key === key) {
stop() stop(key)
return return
} }
stop() stop()
@ -71,11 +74,9 @@ function play(key: VoiceKey, url: string) {
} }
export function useVoicePlayer() { export function useVoicePlayer() {
/** 当前播放的 key给气泡 / 调试用 */
const currentKey = computed(() => currentTask.value?.key ?? null)
/** 指定 key 是否正在播放 */ /** 指定 key 是否正在播放 */
function isPlaying(key: VoiceKey): boolean { function isPlaying(key: VoiceKey): boolean {
return currentTask.value?.key === key return currentTask.value?.key === key
} }
return { currentKey, isPlaying, play, stop } return { isPlaying, play, stop }
} }

View File

@ -37,6 +37,7 @@ import { useDraftStore } from './store/draftStore'
import { useFaceStore } from './store/faceStore' import { useFaceStore } from './store/faceStore'
import { useMessagePuller } from './composables/useMessagePuller' import { useMessagePuller } from './composables/useMessagePuller'
import { useMessageSender } from './composables/useMessageSender' import { useMessageSender } from './composables/useMessageSender'
import { useVoicePlayer } from './composables/useVoicePlayer'
import { ImConversationType } from '../utils/constants' import { ImConversationType } from '../utils/constants'
import { StorageKeys } from '../utils/storage' import { StorageKeys } from '../utils/storage'
import type { Conversation } from './types' import type { Conversation } from './types'
@ -56,6 +57,7 @@ const draftStore = useDraftStore()
const faceStore = useFaceStore() const faceStore = useFaceStore()
const { pullOnce } = useMessagePuller() const { pullOnce } = useMessagePuller()
const { readActive, syncPrivateReadStatus } = useMessageSender() const { readActive, syncPrivateReadStatus } = useMessageSender()
const voicePlayer = useVoicePlayer()
/** 初始化:先吃本地缓存让首屏立即渲染,再远端刷新最新数据,最后建实时通信拉离线消息 */ /** 初始化:先吃本地缓存让首屏立即渲染,再远端刷新最新数据,最后建实时通信拉离线消息 */
onMounted(async () => { onMounted(async () => {
@ -135,11 +137,13 @@ function onBeforeUnload() {
} }
window.addEventListener('beforeunload', onBeforeUnload) window.addEventListener('beforeunload', onBeforeUnload)
/** 离开 IM 主壳:主动断 WebSocketdisconnect 内部已清掉 onclose 防自动重连)+ flush 草稿 + 表情缓存 reset + 解绑 unload */ /** 离开 IM 主壳:主动断 WebSocketdisconnect 内部已清掉 onclose 防自动重连)+ flush 草稿 + 表情缓存 reset + 解绑 unload + 停语音 */
onUnmounted(() => { onUnmounted(() => {
webSocketStore.disconnect() webSocketStore.disconnect()
draftStore.flushPersist() draftStore.flushPersist()
faceStore.reset() faceStore.reset()
// audio
voicePlayer.stop()
window.removeEventListener('beforeunload', onBeforeUnload) window.removeEventListener('beforeunload', onBeforeUnload)
}) })