fix(im): 批量修复 P1/P2 问题
- 修复管理端消息内容搜索和私聊双向查询 - 加强 RTC 通话并发状态保护,去除重复接口错误提示 - 支持成员永久禁言 - 脱敏群消息 WebSocket 定向收件人字段 - 更新 IM bug 台账,剩余 P1/P2 共 35 个im
parent
8b06efe5ee
commit
f3807e30d5
|
|
@ -63,7 +63,8 @@ const presets = [
|
||||||
{ label: '12 小时', value: 43200 },
|
{ label: '12 小时', value: 43200 },
|
||||||
{ label: '1 天', value: 86400 },
|
{ label: '1 天', value: 86400 },
|
||||||
{ label: '7 天', value: 604800 },
|
{ label: '7 天', value: 604800 },
|
||||||
{ label: '30 天', value: 2592000 }
|
{ label: '30 天', value: 2592000 },
|
||||||
|
{ label: '永久', value: 0 }
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
v-else-if="rtcStore.stage === ImRtcCallStage.INCOMING"
|
v-else-if="rtcStore.stage === ImRtcCallStage.INCOMING"
|
||||||
:payload="rtcStore.incomingPayload"
|
:payload="rtcStore.incomingPayload"
|
||||||
:is-group="isGroup"
|
:is-group="isGroup"
|
||||||
|
:accepting="accepting"
|
||||||
|
:rejecting="rejecting"
|
||||||
@accept="handleAccept"
|
@accept="handleAccept"
|
||||||
@reject="handleReject"
|
@reject="handleReject"
|
||||||
/>
|
/>
|
||||||
|
|
@ -42,6 +44,7 @@
|
||||||
:local-stream="localStream"
|
:local-stream="localStream"
|
||||||
:remote-video-stream="remoteVideoStream"
|
:remote-video-stream="remoteVideoStream"
|
||||||
:remote-audio-stream="remoteAudioStream"
|
:remote-audio-stream="remoteAudioStream"
|
||||||
|
:hanging-up="hangingUp"
|
||||||
@hangup="handleHangup"
|
@hangup="handleHangup"
|
||||||
@toggle-mic="toggleMic"
|
@toggle-mic="toggleMic"
|
||||||
@toggle-camera="toggleCamera"
|
@toggle-camera="toggleCamera"
|
||||||
|
|
@ -90,6 +93,11 @@ const message = useMessage()
|
||||||
const lk = useLiveKitRoom()
|
const lk = useLiveKitRoom()
|
||||||
|
|
||||||
const memberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>()
|
const memberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>()
|
||||||
|
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<CallParticipantVM[]>(() => {
|
||||||
/** 连入 LiveKit 房间并注册离开回调;INVITING 主叫预连和被叫 accept 后连入共用 */
|
/** 连入 LiveKit 房间并注册离开回调;INVITING 主叫预连和被叫 accept 后连入共用 */
|
||||||
async function connectLiveKit(livekitUrl: string, token: string) {
|
async function connectLiveKit(livekitUrl: string, token: string) {
|
||||||
// 幂等:lk.connect 内部进入后就把 room.value 赋值;非空表示已经在连接或已连接;stage 多次切换时重复触发也跳过
|
// 幂等:lk.connect 内部进入后就把 room.value 赋值;非空表示已经在连接或已连接;stage 多次切换时重复触发也跳过
|
||||||
if (lk.room.value) {
|
if (lk.room.value || connecting.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 先注册回调,再 connect;信令握手过程会即时推送已在房参与者,业务 handler 必须先就绪
|
connecting.value = true
|
||||||
lk.onDisconnected(() => handlePeerDisconnected())
|
try {
|
||||||
lk.onParticipantConnected(maybeEnterRunning)
|
// 先注册回调,再 connect;信令握手过程会即时推送已在房参与者,业务 handler 必须先就绪
|
||||||
lk.onParticipantDisconnected((userId) => rtcStore.markUserLeft(userId))
|
lk.onDisconnected(() => handlePeerDisconnected())
|
||||||
await lk.connect(livekitUrl, token, { audio: true, video: initialCamera.value })
|
lk.onParticipantConnected(maybeEnterRunning)
|
||||||
// 兜底:connect 期间若已有远端在房,事件可能在 handler 注册前已触发,主动切到 RUNNING
|
lk.onParticipantDisconnected((userId) => rtcStore.markUserLeft(userId))
|
||||||
if (lk.remoteParticipants.value.length > 0) {
|
await lk.connect(livekitUrl, token, { audio: true, video: initialCamera.value })
|
||||||
maybeEnterRunning()
|
// 兜底:connect 期间若已有远端在房,事件可能在 handler 注册前已触发,主动切到 RUNNING
|
||||||
|
if (lk.remoteParticipants.value.length > 0) {
|
||||||
|
maybeEnterRunning()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connecting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,71 +310,85 @@ watch(
|
||||||
|
|
||||||
/** 主叫取消邀请 */
|
/** 主叫取消邀请 */
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
const room = rtcStore.call?.room
|
if (cancelling.value) {
|
||||||
if (room) {
|
return
|
||||||
try {
|
}
|
||||||
await cancelCall(room)
|
cancelling.value = true
|
||||||
} catch (e) {
|
const room = rtcStore.call?.room
|
||||||
console.warn('[Call] cancel 失败', { room }, e)
|
try {
|
||||||
}
|
if (room) {
|
||||||
|
await cancelCall(room)
|
||||||
|
}
|
||||||
|
await lk.disconnect()
|
||||||
|
rtcStore.reset()
|
||||||
|
} finally {
|
||||||
|
cancelling.value = false
|
||||||
}
|
}
|
||||||
await lk.disconnect()
|
|
||||||
rtcStore.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 被叫拒绝来电 */
|
/** 被叫拒绝来电 */
|
||||||
async function handleReject() {
|
async function handleReject() {
|
||||||
const payload = rtcStore.incomingPayload
|
if (rejecting.value) {
|
||||||
if (payload?.room) {
|
return
|
||||||
try {
|
}
|
||||||
await rejectCall(payload.room)
|
rejecting.value = true
|
||||||
} catch (e) {
|
const payload = rtcStore.incomingPayload
|
||||||
console.warn('[Call] reject 失败', { room: payload.room }, e)
|
try {
|
||||||
}
|
if (payload?.room) {
|
||||||
// 本端先行从胶囊条移除自己,免等后端 RTC_CALL(REJECTED) 推回;私聊场景 store 内部 no-op
|
await rejectCall(payload.room)
|
||||||
rtcStore.applyParticipantRejected({
|
// 本端先行从胶囊条移除自己,免等后端 RTC_CALL(REJECTED) 推回;私聊场景 store 内部 no-op
|
||||||
room: payload.room,
|
rtcStore.applyParticipantRejected({
|
||||||
conversationType: payload.conversationType,
|
room: payload.room,
|
||||||
groupId: payload.groupId,
|
conversationType: payload.conversationType,
|
||||||
operatorUserId: getCurrentUserId()
|
groupId: payload.groupId,
|
||||||
})
|
operatorUserId: getCurrentUserId()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rtcStore.reset()
|
||||||
|
} finally {
|
||||||
|
rejecting.value = false
|
||||||
}
|
}
|
||||||
rtcStore.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 被叫接听来电 */
|
/** 被叫接听来电 */
|
||||||
async function handleAccept() {
|
async function handleAccept() {
|
||||||
|
if (accepting.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const payload = rtcStore.incomingPayload
|
const payload = rtcStore.incomingPayload
|
||||||
if (!payload) return
|
if (!payload) return
|
||||||
|
accepting.value = true
|
||||||
try {
|
try {
|
||||||
const data = await acceptCall(payload.room)
|
const data = await acceptCall(payload.room)
|
||||||
rtcStore.enterRunning(data)
|
rtcStore.enterRunning(data)
|
||||||
} catch (e: any) {
|
} finally {
|
||||||
console.error('[Call] accept 失败', { room: payload.room }, e)
|
accepting.value = false
|
||||||
message.error(e?.msg || '接听失败')
|
|
||||||
rtcStore.reset()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通话中挂断 */
|
/** 通话中挂断 */
|
||||||
async function handleHangup() {
|
async function handleHangup() {
|
||||||
const call = rtcStore.call
|
if (hangingUp.value) {
|
||||||
if (call?.room) {
|
return
|
||||||
try {
|
}
|
||||||
await leaveCall(call.room)
|
hangingUp.value = true
|
||||||
} catch (e) {
|
const call = rtcStore.call
|
||||||
console.warn('[Call] leave 失败', { room: call.room }, e)
|
try {
|
||||||
}
|
if (call?.room) {
|
||||||
// 本端先行从胶囊条移除自己,免等后端 RTC_PARTICIPANT_DISCONNECTED 推回;私聊场景 store 内部 no-op,整通话由 END 关掉
|
await leaveCall(call.room)
|
||||||
rtcStore.applyParticipantDisconnected({
|
// 本端先行从胶囊条移除自己,免等后端 RTC_PARTICIPANT_DISCONNECTED 推回;私聊场景 store 内部 no-op,整通话由 END 关掉
|
||||||
room: call.room,
|
rtcStore.applyParticipantDisconnected({
|
||||||
userId: getCurrentUserId(),
|
room: call.room,
|
||||||
conversationType: call.conversationType,
|
userId: getCurrentUserId(),
|
||||||
groupId: call.groupId
|
conversationType: call.conversationType,
|
||||||
})
|
groupId: call.groupId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await lk.disconnect()
|
||||||
|
rtcStore.reset()
|
||||||
|
} finally {
|
||||||
|
hangingUp.value = false
|
||||||
}
|
}
|
||||||
await lk.disconnect()
|
|
||||||
rtcStore.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** LiveKit Room 异常断开;多见于网络中断 */
|
/** LiveKit Room 异常断开;多见于网络中断 */
|
||||||
|
|
@ -453,14 +480,9 @@ async function handleAddMemberSuccess(userIds: number[]) {
|
||||||
if (!call?.room || userIds.length === 0) {
|
if (!call?.room || userIds.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
await inviteCall({ room: call.room, inviteeIds: userIds })
|
||||||
await inviteCall({ room: call.room, inviteeIds: userIds })
|
// 同步本地 inviteeIds,让新成员立即作为 pending 占位出现在网格里
|
||||||
// 同步本地 inviteeIds,让新成员立即作为 pending 占位出现在网格里
|
rtcStore.appendInvitees(userIds)
|
||||||
rtcStore.appendInvitees(userIds)
|
message.success('已发送邀请')
|
||||||
message.success('已发送邀请')
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('[Call] invite 追加失败', { room: call.room, inviteeIds: userIds }, e)
|
|
||||||
message.error(e?.msg || '添加成员失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,16 @@
|
||||||
<div class="flex flex-shrink-0 gap-2 items-center">
|
<div class="flex flex-shrink-0 gap-2 items-center">
|
||||||
<button
|
<button
|
||||||
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#f04a4a] hover:opacity-90"
|
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#f04a4a] hover:opacity-90"
|
||||||
|
:class="{ 'opacity-60 cursor-not-allowed': rejectDisabled }"
|
||||||
|
:disabled="rejectDisabled"
|
||||||
@click="$emit('reject')"
|
@click="$emit('reject')"
|
||||||
>
|
>
|
||||||
<Icon icon="ant-design:phone-outlined" :size="18" class="rotate-[135deg]" />
|
<Icon icon="ant-design:phone-outlined" :size="18" class="rotate-[135deg]" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#2ec27e] hover:opacity-90"
|
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#2ec27e] hover:opacity-90"
|
||||||
|
:class="{ 'opacity-60 cursor-not-allowed': acceptDisabled }"
|
||||||
|
:disabled="acceptDisabled"
|
||||||
@click="$emit('accept')"
|
@click="$emit('accept')"
|
||||||
>
|
>
|
||||||
<Icon icon="ant-design:phone-outlined" :size="18" />
|
<Icon icon="ant-design:phone-outlined" :size="18" />
|
||||||
|
|
@ -78,6 +82,8 @@ import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
payload: ImRtcCallNotification | null
|
payload: ImRtcCallNotification | null
|
||||||
isGroup?: boolean
|
isGroup?: boolean
|
||||||
|
accepting?: boolean
|
||||||
|
rejecting?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{ accept: []; reject: [] }>()
|
defineEmits<{ accept: []; reject: [] }>()
|
||||||
|
|
@ -88,6 +94,12 @@ const tipText = computed(() => {
|
||||||
return `邀请你${getDictLabel(DICT_TYPE.IM_RTC_CALL_MEDIA_TYPE, props.payload.mediaType)}通话`
|
return `邀请你${getDictLabel(DICT_TYPE.IM_RTC_CALL_MEDIA_TYPE, props.payload.mediaType)}通话`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 接听按钮禁用态 */
|
||||||
|
const acceptDisabled = computed(() => !!props.accepting || !!props.rejecting)
|
||||||
|
|
||||||
|
/** 拒绝按钮禁用态 */
|
||||||
|
const rejectDisabled = computed(() => !!props.rejecting || !!props.accepting)
|
||||||
|
|
||||||
/** 群通话成员;缓存为空时用 INVITE 载荷里的主叫兜底,避免空白 */
|
/** 群通话成员;缓存为空时用 INVITE 载荷里的主叫兜底,避免空白 */
|
||||||
const callMembers = useGroupCallMembers(
|
const callMembers = useGroupCallMembers(
|
||||||
computed(() => (props.isGroup ? props.payload?.groupId : undefined)),
|
computed(() => (props.isGroup ? props.payload?.groupId : undefined)),
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,7 @@
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
||||||
|
:class="{ 'opacity-60 pointer-events-none': hangingUp }"
|
||||||
@click="$emit('hangup')"
|
@click="$emit('hangup')"
|
||||||
>
|
>
|
||||||
<span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-[#f04a4a]">
|
<span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-[#f04a4a]">
|
||||||
|
|
@ -212,6 +213,7 @@ const props = defineProps<{
|
||||||
localStream?: MediaStream | null
|
localStream?: MediaStream | null
|
||||||
remoteVideoStream?: MediaStream | null
|
remoteVideoStream?: MediaStream | null
|
||||||
remoteAudioStream?: MediaStream | null
|
remoteAudioStream?: MediaStream | null
|
||||||
|
hangingUp?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|
|
||||||
|
|
@ -166,12 +166,7 @@ async function handleJoin() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
popoverVisible.value = false
|
popoverVisible.value = false
|
||||||
try {
|
const data = await joinCall(call.room)
|
||||||
const data = await joinCall(call.room)
|
rtcStore.startInviting(data)
|
||||||
rtcStore.startInviting(data)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('[GroupCallBanner] join 失败', { room: call.room }, e)
|
|
||||||
message.error(e?.msg || '加入失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -622,7 +622,6 @@ function onMutedChange(value: boolean | string | number) {
|
||||||
groupStore.setSilent(targetId, next).catch((error) => {
|
groupStore.setSilent(targetId, next).catch((error) => {
|
||||||
console.error('[IM ConversationGroupSide] setSilent 失败', { targetId }, error)
|
console.error('[IM ConversationGroupSide] setSilent 失败', { targetId }, error)
|
||||||
conversationStore.setSilent(type, targetId, !next)
|
conversationStore.setSilent(type, targetId, !next)
|
||||||
message.error('操作失败')
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -650,13 +649,9 @@ async function onMuteAllChange(value: boolean | string | number) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newValue = !!value
|
const newValue = !!value
|
||||||
try {
|
await muteAll({ groupId: props.group.id, mutedAll: newValue })
|
||||||
await muteAll({ groupId: props.group.id, mutedAll: newValue })
|
message.success(newValue ? '已开启全群禁言' : '已关闭全群禁言')
|
||||||
message.success(newValue ? '已开启全群禁言' : '已关闭全群禁言')
|
emit('reload')
|
||||||
emit('reload')
|
|
||||||
} catch {
|
|
||||||
message.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 进群审批 ====================
|
// ==================== 进群审批 ====================
|
||||||
|
|
|
||||||
|
|
@ -58,20 +58,28 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-1 leading-5">
|
<div class="flex items-center mt-1 leading-5">
|
||||||
<!-- 进群申请红字前缀:群主 / 管理员看到自己管理的群下还有未处理申请时显示 -->
|
<!-- 进群申请红字前缀:群主 / 管理员看到自己管理的群下还有未处理申请时显示 -->
|
||||||
<span v-if="requestText" class="flex-shrink-0 text-12px text-[#c70b0b]">
|
<span
|
||||||
|
v-if="requestText"
|
||||||
|
class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap"
|
||||||
|
>
|
||||||
{{ requestText }}
|
{{ requestText }}
|
||||||
</span>
|
</span>
|
||||||
<!-- @红字提示:atMe 优先于 atAll -->
|
<!-- @红字提示:atMe 优先于 atAll -->
|
||||||
<span v-if="atText" class="flex-shrink-0 text-12px text-[#c70b0b]">{{ atText }}</span>
|
<span
|
||||||
|
v-if="atText"
|
||||||
|
class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{ atText }}
|
||||||
|
</span>
|
||||||
<!-- 群聊最后一条发送者前缀:按 lastSenderId + 当前会话上下文实时算名字 -->
|
<!-- 群聊最后一条发送者前缀:按 lastSenderId + 当前会话上下文实时算名字 -->
|
||||||
<span
|
<span
|
||||||
v-if="showSendName"
|
v-if="showSendName"
|
||||||
class="flex-shrink-0 text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap"
|
class="conversation-item__sender flex-shrink overflow-hidden text-12px text-[var(--el-text-color-secondary)] truncate whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ lastSenderDisplayName }}:
|
{{ lastSenderDisplayName }}:
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex-1 overflow-hidden text-12px truncate text-[var(--el-text-color-secondary)]"
|
class="flex-1 min-w-0 overflow-hidden text-12px truncate text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
{{ lastContentDisplay }}
|
{{ lastContentDisplay }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -239,7 +247,6 @@ function handleMuted() {
|
||||||
: groupStore.setSilent(targetId, next)
|
: groupStore.setSilent(targetId, next)
|
||||||
sync.catch((e) => {
|
sync.catch((e) => {
|
||||||
console.error('[IM] 切换免打扰失败', e)
|
console.error('[IM] 切换免打扰失败', e)
|
||||||
message.error('切换免打扰失败')
|
|
||||||
conversationStore.setSilent(type, targetId, !next)
|
conversationStore.setSilent(type, targetId, !next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -285,4 +292,12 @@ function handleContextMenu(e: MouseEvent) {
|
||||||
.conversation-item__silent :deep(svg) {
|
.conversation-item__silent :deep(svg) {
|
||||||
fill: currentColor !important;
|
fill: currentColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-item__prefix {
|
||||||
|
max-width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-item__sender {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ watch(visible, (open) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 备注 popover 点击保存:先走 store API 同步后端,成功后再关 popover + 提示(接口错误由全局拦截器统一 toast,不重复 catch) */
|
/** 备注 popover 点击保存 */
|
||||||
async function handleSaveDisplayName() {
|
async function handleSaveDisplayName() {
|
||||||
if (!props.friend) {
|
if (!props.friend) {
|
||||||
return
|
return
|
||||||
|
|
@ -227,7 +227,6 @@ function handleMutedChange(value: boolean | string | number) {
|
||||||
}
|
}
|
||||||
friendStore.setSilent(targetId, next).catch((error) => {
|
friendStore.setSilent(targetId, next).catch((error) => {
|
||||||
console.error('[IM ConversationPrivateSide] 切换免打扰失败', { targetId }, error)
|
console.error('[IM ConversationPrivateSide] 切换免打扰失败', { targetId }, error)
|
||||||
message.error('切换免打扰失败')
|
|
||||||
conversationStore.setSilent(type, targetId, !next)
|
conversationStore.setSilent(type, targetId, !next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -280,28 +280,26 @@ async function onUploadPicked(e: Event) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uploading.value = true
|
uploading.value = true
|
||||||
let payload: { url: string; width: number; height: number }
|
let size: { width: number; height: number }
|
||||||
|
try {
|
||||||
|
size = await probeImageSize(file)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[IM] 解析个人表情失败', err)
|
||||||
|
ElMessage.error('图片解析失败')
|
||||||
|
uploading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// probe 本地图片宽高 + 上传到 OSS 并行起跑(probe 通常远快于上传,几乎完全被遮蔽)
|
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
const [size, uploadRes] = await Promise.all([
|
const uploadRes = (await updateFile(form)) as { data?: string }
|
||||||
probeImageSize(file),
|
|
||||||
updateFile(form) as Promise<{ data?: string }>
|
|
||||||
])
|
|
||||||
const url = uploadRes?.data
|
const url = uploadRes?.data
|
||||||
if (!url) {
|
if (!url) {
|
||||||
ElMessage.error('上传失败')
|
ElMessage.error('上传失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload = { url, width: size.width, height: size.height }
|
const payload = { url, width: size.width, height: size.height }
|
||||||
} catch (err) {
|
|
||||||
console.warn('[IM] 上传个人表情失败', err)
|
|
||||||
ElMessage.error('上传失败')
|
|
||||||
uploading.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await faceStore.addFaceUserItem(payload)
|
await faceStore.addFaceUserItem(payload)
|
||||||
} finally {
|
} finally {
|
||||||
uploading.value = false
|
uploading.value = false
|
||||||
|
|
|
||||||
|
|
@ -464,6 +464,7 @@ function toggleSide() {
|
||||||
|
|
||||||
/** 私聊通话入口:popover 触发;点 语音 / 视频 直接发起 */
|
/** 私聊通话入口:popover 触发;点 语音 / 视频 直接发起 */
|
||||||
const callPopoverVisible = ref(false)
|
const callPopoverVisible = ref(false)
|
||||||
|
const callInviting = ref(false) // 通话发起中
|
||||||
async function startPrivateCall(mediaType: number) {
|
async function startPrivateCall(mediaType: number) {
|
||||||
callPopoverVisible.value = false
|
callPopoverVisible.value = false
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = conversationStore.activeConversation
|
||||||
|
|
@ -503,13 +504,21 @@ async function onCallMemberPicked(selectedIds: number[]) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 实际调 create 接口;统一处理成功 / ENDED(如忙线立即结束)/ 异常三种返回 */
|
/** 实际调 create 接口;统一处理成功 / ENDED(如忙线立即结束) */
|
||||||
async function doInvite(reqVO: {
|
async function doInvite(reqVO: {
|
||||||
conversationType: number
|
conversationType: number
|
||||||
mediaType: number
|
mediaType: number
|
||||||
groupId?: number
|
groupId?: number
|
||||||
inviteeIds: number[]
|
inviteeIds: number[]
|
||||||
}) {
|
}) {
|
||||||
|
if (callInviting.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rtcStore.isActive) {
|
||||||
|
message.warning('当前已有通话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callInviting.value = true
|
||||||
try {
|
try {
|
||||||
const data = await createCall(reqVO)
|
const data = await createCall(reqVO)
|
||||||
// 后端已 INSERT + 立即 end(如忙线):toast 提示,不进 INVITING 阶段;chat tip 由 RTC_CALL_END 推送写入消息流
|
// 后端已 INSERT + 立即 end(如忙线):toast 提示,不进 INVITING 阶段;chat tip 由 RTC_CALL_END 推送写入消息流
|
||||||
|
|
@ -519,8 +528,8 @@ async function doInvite(reqVO: {
|
||||||
}
|
}
|
||||||
// 正常进入 INVITING 阶段:走 store 逻辑发起通话,后续状态更新 / 消息流更新由 RTC 模块监听推送处理
|
// 正常进入 INVITING 阶段:走 store 逻辑发起通话,后续状态更新 / 消息流更新由 RTC 模块监听推送处理
|
||||||
rtcStore.startInviting(data)
|
rtcStore.startInviting(data)
|
||||||
} catch (e: any) {
|
} finally {
|
||||||
message.error(e?.msg || '发起通话失败')
|
callInviting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,15 @@
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- 「消息内容」搜索入口暂时移除:后端 page 接口没有 content 字段,留着会误导管理员;后端补上后再恢复 -->
|
<el-form-item label="消息内容" prop="content">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.content"
|
||||||
|
placeholder="请输入消息内容"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="发送时间" prop="sendTime">
|
<el-form-item label="发送时间" prop="sendTime">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="queryParams.sendTime"
|
v-model="queryParams.sendTime"
|
||||||
|
|
@ -176,6 +184,7 @@ const queryParams = reactive({
|
||||||
groupId: undefined as number | undefined,
|
groupId: undefined as number | undefined,
|
||||||
senderId: undefined as number | undefined,
|
senderId: undefined as number | undefined,
|
||||||
type: undefined as number | undefined,
|
type: undefined as number | undefined,
|
||||||
|
content: undefined as string | undefined,
|
||||||
sendTime: [] as string[]
|
sendTime: [] as string[]
|
||||||
})
|
})
|
||||||
const queryFormRef = ref() // 搜索的表单
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,15 @@
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- 「消息内容」搜索入口暂时移除:后端 page 接口没有 content 字段,留着会误导管理员;后端补上后再恢复 -->
|
<el-form-item label="消息内容" prop="content">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.content"
|
||||||
|
placeholder="请输入消息内容"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="发送时间" prop="sendTime">
|
<el-form-item label="发送时间" prop="sendTime">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="queryParams.sendTime"
|
v-model="queryParams.sendTime"
|
||||||
|
|
@ -151,6 +159,7 @@ const queryParams = reactive({
|
||||||
senderId: undefined as number | undefined,
|
senderId: undefined as number | undefined,
|
||||||
receiverId: undefined as number | undefined,
|
receiverId: undefined as number | undefined,
|
||||||
type: undefined as number | undefined,
|
type: undefined as number | undefined,
|
||||||
|
content: undefined as string | undefined,
|
||||||
sendTime: [] as string[]
|
sendTime: [] as string[]
|
||||||
})
|
})
|
||||||
const queryFormRef = ref() // 搜索的表单
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue