From f3807e30d559ebe81ea56cc3222ecee83fb4acec Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 25 May 2026 00:28:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(im):=20=E6=89=B9=E9=87=8F=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20P1/P2=20=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复管理端消息内容搜索和私聊双向查询 - 加强 RTC 通话并发状态保护,去除重复接口错误提示 - 支持成员永久禁言 - 脱敏群消息 WebSocket 定向收件人字段 - 更新 IM bug 台账,剩余 P1/P2 共 35 个 --- .../group/GroupMuteMemberDialog.vue | 3 +- .../home/components/rtc/RtcCallContainer.vue | 146 ++++++++++-------- .../home/components/rtc/RtcCallIncoming.vue | 12 ++ .../im/home/components/rtc/RtcCallRunning.vue | 2 + .../components/rtc/RtcGroupCallBanner.vue | 9 +- .../conversation/ConversationGroupSide.vue | 11 +- .../conversation/ConversationItem.vue | 25 ++- .../conversation/ConversationPrivateSide.vue | 3 +- .../components/input/FacePicker.vue | 26 ++-- .../components/message/MessagePanel.vue | 15 +- src/views/im/manager/message/group/index.vue | 11 +- .../im/manager/message/private/index.vue | 11 +- 12 files changed, 170 insertions(+), 104 deletions(-) diff --git a/src/views/im/home/components/group/GroupMuteMemberDialog.vue b/src/views/im/home/components/group/GroupMuteMemberDialog.vue index 0b50570dc..41ecce6b7 100644 --- a/src/views/im/home/components/group/GroupMuteMemberDialog.vue +++ b/src/views/im/home/components/group/GroupMuteMemberDialog.vue @@ -63,7 +63,8 @@ const presets = [ { label: '12 小时', value: 43200 }, { label: '1 天', value: 86400 }, { label: '7 天', value: 604800 }, - { label: '30 天', value: 2592000 } + { label: '30 天', value: 2592000 }, + { label: '永久', value: 0 } ] /** 打开弹窗 */ diff --git a/src/views/im/home/components/rtc/RtcCallContainer.vue b/src/views/im/home/components/rtc/RtcCallContainer.vue index 48cb17406..a53f27272 100644 --- a/src/views/im/home/components/rtc/RtcCallContainer.vue +++ b/src/views/im/home/components/rtc/RtcCallContainer.vue @@ -22,6 +22,8 @@ v-else-if="rtcStore.stage === ImRtcCallStage.INCOMING" :payload="rtcStore.incomingPayload" :is-group="isGroup" + :accepting="accepting" + :rejecting="rejecting" @accept="handleAccept" @reject="handleReject" /> @@ -42,6 +44,7 @@ :local-stream="localStream" :remote-video-stream="remoteVideoStream" :remote-audio-stream="remoteAudioStream" + :hanging-up="hangingUp" @hangup="handleHangup" @toggle-mic="toggleMic" @toggle-camera="toggleCamera" @@ -90,6 +93,11 @@ const message = useMessage() const lk = useLiveKitRoom() const memberPickerRef = ref>() +const connecting = ref(false) +const accepting = ref(false) +const rejecting = ref(false) +const cancelling = ref(false) +const hangingUp = ref(false) // ==================== 视图模型 ==================== @@ -224,17 +232,22 @@ const participants = computed(() => { /** 连入 LiveKit 房间并注册离开回调;INVITING 主叫预连和被叫 accept 后连入共用 */ async function connectLiveKit(livekitUrl: string, token: string) { // 幂等:lk.connect 内部进入后就把 room.value 赋值;非空表示已经在连接或已连接;stage 多次切换时重复触发也跳过 - if (lk.room.value) { + if (lk.room.value || connecting.value) { return } - // 先注册回调,再 connect;信令握手过程会即时推送已在房参与者,业务 handler 必须先就绪 - lk.onDisconnected(() => handlePeerDisconnected()) - lk.onParticipantConnected(maybeEnterRunning) - lk.onParticipantDisconnected((userId) => rtcStore.markUserLeft(userId)) - await lk.connect(livekitUrl, token, { audio: true, video: initialCamera.value }) - // 兜底:connect 期间若已有远端在房,事件可能在 handler 注册前已触发,主动切到 RUNNING - if (lk.remoteParticipants.value.length > 0) { - maybeEnterRunning() + connecting.value = true + try { + // 先注册回调,再 connect;信令握手过程会即时推送已在房参与者,业务 handler 必须先就绪 + lk.onDisconnected(() => handlePeerDisconnected()) + lk.onParticipantConnected(maybeEnterRunning) + lk.onParticipantDisconnected((userId) => rtcStore.markUserLeft(userId)) + await lk.connect(livekitUrl, token, { audio: true, video: initialCamera.value }) + // 兜底:connect 期间若已有远端在房,事件可能在 handler 注册前已触发,主动切到 RUNNING + if (lk.remoteParticipants.value.length > 0) { + maybeEnterRunning() + } + } finally { + connecting.value = false } } @@ -297,71 +310,85 @@ watch( /** 主叫取消邀请 */ async function handleCancel() { - const room = rtcStore.call?.room - if (room) { - try { - await cancelCall(room) - } catch (e) { - console.warn('[Call] cancel 失败', { room }, e) - } + if (cancelling.value) { + return + } + cancelling.value = true + const room = rtcStore.call?.room + try { + if (room) { + await cancelCall(room) + } + await lk.disconnect() + rtcStore.reset() + } finally { + cancelling.value = false } - await lk.disconnect() - rtcStore.reset() } /** 被叫拒绝来电 */ async function handleReject() { - const payload = rtcStore.incomingPayload - if (payload?.room) { - try { - await rejectCall(payload.room) - } catch (e) { - console.warn('[Call] reject 失败', { room: payload.room }, e) - } - // 本端先行从胶囊条移除自己,免等后端 RTC_CALL(REJECTED) 推回;私聊场景 store 内部 no-op - rtcStore.applyParticipantRejected({ - room: payload.room, - conversationType: payload.conversationType, - groupId: payload.groupId, - operatorUserId: getCurrentUserId() - }) + if (rejecting.value) { + return + } + rejecting.value = true + const payload = rtcStore.incomingPayload + try { + if (payload?.room) { + await rejectCall(payload.room) + // 本端先行从胶囊条移除自己,免等后端 RTC_CALL(REJECTED) 推回;私聊场景 store 内部 no-op + rtcStore.applyParticipantRejected({ + room: payload.room, + conversationType: payload.conversationType, + groupId: payload.groupId, + operatorUserId: getCurrentUserId() + }) + } + rtcStore.reset() + } finally { + rejecting.value = false } - rtcStore.reset() } /** 被叫接听来电 */ async function handleAccept() { + if (accepting.value) { + return + } const payload = rtcStore.incomingPayload if (!payload) return + accepting.value = true try { const data = await acceptCall(payload.room) rtcStore.enterRunning(data) - } catch (e: any) { - console.error('[Call] accept 失败', { room: payload.room }, e) - message.error(e?.msg || '接听失败') - rtcStore.reset() + } finally { + accepting.value = false } } /** 通话中挂断 */ async function handleHangup() { - const call = rtcStore.call - if (call?.room) { - try { - await leaveCall(call.room) - } catch (e) { - console.warn('[Call] leave 失败', { room: call.room }, e) - } - // 本端先行从胶囊条移除自己,免等后端 RTC_PARTICIPANT_DISCONNECTED 推回;私聊场景 store 内部 no-op,整通话由 END 关掉 - rtcStore.applyParticipantDisconnected({ - room: call.room, - userId: getCurrentUserId(), - conversationType: call.conversationType, - groupId: call.groupId - }) + if (hangingUp.value) { + return + } + hangingUp.value = true + const call = rtcStore.call + try { + if (call?.room) { + await leaveCall(call.room) + // 本端先行从胶囊条移除自己,免等后端 RTC_PARTICIPANT_DISCONNECTED 推回;私聊场景 store 内部 no-op,整通话由 END 关掉 + rtcStore.applyParticipantDisconnected({ + room: call.room, + userId: getCurrentUserId(), + conversationType: call.conversationType, + groupId: call.groupId + }) + } + await lk.disconnect() + rtcStore.reset() + } finally { + hangingUp.value = false } - await lk.disconnect() - rtcStore.reset() } /** LiveKit Room 异常断开;多见于网络中断 */ @@ -453,14 +480,9 @@ async function handleAddMemberSuccess(userIds: number[]) { if (!call?.room || userIds.length === 0) { return } - try { - await inviteCall({ room: call.room, inviteeIds: userIds }) - // 同步本地 inviteeIds,让新成员立即作为 pending 占位出现在网格里 - rtcStore.appendInvitees(userIds) - message.success('已发送邀请') - } catch (e: any) { - console.error('[Call] invite 追加失败', { room: call.room, inviteeIds: userIds }, e) - message.error(e?.msg || '添加成员失败') - } + await inviteCall({ room: call.room, inviteeIds: userIds }) + // 同步本地 inviteeIds,让新成员立即作为 pending 占位出现在网格里 + rtcStore.appendInvitees(userIds) + message.success('已发送邀请') } diff --git a/src/views/im/home/components/rtc/RtcCallIncoming.vue b/src/views/im/home/components/rtc/RtcCallIncoming.vue index 8577e6d36..3b3379700 100644 --- a/src/views/im/home/components/rtc/RtcCallIncoming.vue +++ b/src/views/im/home/components/rtc/RtcCallIncoming.vue @@ -53,12 +53,16 @@