feat(im): 优化 rtc 整体弹窗界面

im
YunaiV 2026-05-14 09:44:39 +08:00
parent a170ae37ab
commit e579a4de13
7 changed files with 337 additions and 40 deletions

View File

@ -22,6 +22,9 @@
<UserInfoCard />
<GroupInfoCard />
<ContextMenu />
<!-- 实时通话浮层监听 rtcStore 全局状态可在任意 IM 子页弹出 -->
<CallContainer />
</div>
</template>
@ -47,6 +50,7 @@ import ToolBar from './components/ToolBar.vue'
import UserInfoCard from './components/user/UserInfoCard.vue'
import GroupInfoCard from './components/group/GroupInfoCard.vue'
import ContextMenu from './components/ContextMenu.vue'
import CallContainer from './components/rtc/CallContainer.vue'
defineOptions({ name: 'ImIndex' })

View File

@ -44,12 +44,13 @@
:size="36"
/>
<div
class="flex gap-2 items-center px-3.5 py-2 text-sm rounded-lg"
class="flex gap-2 items-center px-3.5 py-2 text-sm rounded-lg cursor-pointer"
:class="
message.selfSend
? 'text-black bg-[#95ec69]'
: 'text-[var(--el-text-color-primary)] bg-[var(--el-fill-color-light)]'
"
@click="handleRtcCallBubbleClick"
>
<Icon icon="ant-design:phone-outlined" :size="16" class="rotate-[135deg] flex-shrink-0" />
<span class="whitespace-nowrap">{{ rtcCallPrivateBubbleText }}</span>
@ -251,7 +252,7 @@ import ReplyPreview from './ReplyPreview.vue'
import TipSegments from './TipSegments.vue'
import UserAvatar from '../../../../components/user/UserAvatar.vue'
import MessageBubble from './MessageBubble.vue'
import { IM_FORWARD_DIALOG_KEY, IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys'
import { IM_FORWARD_DIALOG_KEY, IM_MERGE_DETAIL_DIALOG_KEY, IM_RTC_REDIAL_KEY } from './forward/keys'
import { useMessageMultiSelect } from '../../../../composables/useMessageMultiSelect'
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
@ -376,6 +377,16 @@ const quote = computed(() => getQuoteFromMessage(props.message.content))
/** MessagePanel 注入的弹窗触发函数 */
const openForwardDialog = inject(IM_FORWARD_DIALOG_KEY)
const openMergeDetail = inject(IM_MERGE_DETAIL_DIALOG_KEY)
const redialRtcCall = inject(IM_RTC_REDIAL_KEY)
/** 私聊 RTC_CALL_END 气泡点击:用同款 mediaType 重拨 */
function handleRtcCallBubbleClick() {
const mediaType = rtcCallEndPrivatePayload.value?.mediaType
if (mediaType == null) {
return
}
redialRtcCall?.(mediaType)
}
/** 多选模式:模块级单例 composable */
const multiSelect = useMessageMultiSelect()

View File

@ -36,13 +36,45 @@
@click="historyDialogRef?.open()"
/>
</el-tooltip>
<!-- 通话入口暂未开放先放占位图标对齐微信 PC -->
<el-tooltip content="通话" placement="bottom">
<!-- 通话入口私聊弹语音 / 视频popover群聊直接进选人弹窗 -->
<el-popover
v-if="isPrivate"
v-model:visible="callPopoverVisible"
placement="bottom-end"
:width="140"
trigger="click"
popper-class="message-panel__call-popover"
>
<template #reference>
<Icon
icon="ant-design:phone-outlined"
:size="20"
class="message-panel__header-icon cursor-pointer"
/>
</template>
<div class="message-panel__call-menu">
<div
class="message-panel__call-menu-item"
@click="startPrivateCall(ImRtcCallMediaType.VOICE)"
>
<Icon icon="ant-design:phone-outlined" :size="16" />
<span>语音通话</span>
</div>
<div
class="message-panel__call-menu-item"
@click="startPrivateCall(ImRtcCallMediaType.VIDEO)"
>
<Icon icon="ant-design:video-camera-outlined" :size="16" />
<span>视频通话</span>
</div>
</div>
</el-popover>
<el-tooltip v-else content="通话" placement="bottom">
<Icon
icon="ant-design:phone-outlined"
:size="20"
class="message-panel__header-icon cursor-pointer"
@click="handleCall"
@click="handleGroupCall"
/>
</el-tooltip>
<!-- 信息抽屉入口 -->
@ -57,6 +89,12 @@
</div>
</div>
<!-- 群通话胶囊条仅群聊 + 该群有活跃通话时显示点击展开看成员 + 加入按钮 -->
<GroupCallBanner
v-if="isGroup && conversationStore.activeConversation"
:group-id="conversationStore.activeConversation.targetId"
/>
<!-- 群置顶消息第二行嵌入 header仅群聊 + 有置顶时显示 -->
<GroupPinnedMessage
v-if="isGroup && conversationStore.activeConversation"
@ -75,11 +113,7 @@
<Icon icon="ant-design:user-outlined" :size="11" />
</span>
<span>对方还不是你的朋友</span>
<Icon
icon="ep:arrow-right"
:size="12"
class="text-[var(--el-text-color-secondary)]"
/>
<Icon icon="ep:arrow-right" :size="12" class="text-[var(--el-text-color-secondary)]" />
</div>
</div>
</div>
@ -132,10 +166,7 @@
<!-- 底部输入框常驻多选模式底栏作为浮层盖在上面保持下方输入框尺寸不变 -->
<div class="relative">
<MessageInput />
<MessageMultiSelectBar
v-if="multiSelect.state.active"
class="absolute inset-0 z-10"
/>
<MessageMultiSelectBar v-if="multiSelect.state.active" class="absolute inset-0 z-10" />
</div>
<!-- 右侧信息抽屉群聊 / 私聊各自一份 -->
@ -165,6 +196,9 @@
<!-- 禁言时长选择弹窗 -->
<GroupMuteMemberDialog ref="muteMemberDialogRef" @success="reloadGroupData" />
<!-- 群通话成员选择弹窗 -->
<CallMemberPickerDialog ref="callMemberPickerRef" @success="onCallMemberPicked" />
</template>
<div
v-else
@ -185,13 +219,16 @@ import { useFriendStore } from '../../../../store/friendStore'
import { useImUiStore } from '../../../../store/uiStore'
import { getMemberDisplayName } from '@/views/im/utils/user'
import { useGroupStore } from '../../../../store/groupStore'
import { ImConversationType } from '@/views/im/utils/constants'
import MessageItem from './MessageItem.vue'
import MessageInput from '../input/MessageInput.vue'
import MessageMultiSelectBar from '../input/MessageMultiSelectBar.vue'
import MessageForwardDialog from './forward/MessageForwardDialog.vue'
import MessageMergeDetailDialog from './forward/MessageMergeDetailDialog.vue'
import { IM_FORWARD_DIALOG_KEY, IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys'
import {
IM_FORWARD_DIALOG_KEY,
IM_MERGE_DETAIL_DIALOG_KEY,
IM_RTC_REDIAL_KEY
} from './forward/keys'
import { useMessageMultiSelect } from '../../../../composables/useMessageMultiSelect'
import { useVoicePlayer } from '../../../../composables/useVoicePlayer'
import MessageHistory from './MessageHistory.vue'
@ -202,6 +239,12 @@ import ConversationPrivateSide from '../conversation/ConversationPrivateSide.vue
import type { GroupLite } from '../../../../types'
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
import GroupMuteMemberDialog from '../../../../components/group/GroupMuteMemberDialog.vue'
import CallMemberPickerDialog from '../../../../components/rtc/CallMemberPickerDialog.vue'
import GroupCallBanner from '../../../../components/rtc/GroupCallBanner.vue'
import { createCall } from '@/api/im/home/rtc'
import { ImRtcCallMediaType, ImRtcCallStatus, ImConversationType } from '@/views/im/utils/constants'
import { resolveCallEndReasonText } from '@/views/im/utils/message'
import { useRtcStore } from '../../../../store/rtcStore'
defineOptions({ name: 'ImMessagePanel' })
@ -210,6 +253,7 @@ const friendStore = useFriendStore()
const uiStore = useImUiStore()
const groupStore = useGroupStore()
const message = useMessage()
const rtcStore = useRtcStore()
const listRef = ref<HTMLElement>()
// ==================== / dialog ====================
@ -220,6 +264,11 @@ const mergeDetailDialogRef = ref<InstanceType<typeof MessageMergeDetailDialog>>(
provide(IM_FORWARD_DIALOG_KEY, (opts) => forwardDialogRef.value?.open(opts))
provide(IM_MERGE_DETAIL_DIALOG_KEY, (content) => mergeDetailDialogRef.value?.open(content))
provide(IM_RTC_REDIAL_KEY, (mediaType: number) => {
if (isPrivate.value) {
void startPrivateCall(mediaType)
}
}) // RTC_CALL_END MessageItem
// ==================== ====================
// statecomposable退 + template
@ -243,6 +292,9 @@ const messages = computed(() => conversationStore.getActiveMessages)
const isGroup = computed(
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
)
const isPrivate = computed(
() => conversationStore.activeConversation?.type === ImConversationType.PRIVATE
)
/** 私聊会话且对端不是有效好友(本端 friend 记录缺失或 DISABLE单边删除语义下「被对方删除」不触发本端横幅 */
const showNotFriendBanner = computed(() => {
@ -383,6 +435,9 @@ function reloadGroupData() {
const historyDialogRef = ref<InstanceType<typeof MessageHistory>>()
const sideVisible = ref(false) // 信息抽屉开关:群聊 / 私聊共用一个 ref
const muteMemberDialogRef = ref<InstanceType<typeof GroupMuteMemberDialog>>()
const callMemberPickerRef = ref<InstanceType<typeof CallMemberPickerDialog>>()
/** 群通话发起:成员选择弹窗打开期间临时持有的 mediaType */
const pendingMediaType = ref<number | null>(null)
/** 消息右键菜单「禁言」→ 打开时长选择弹窗 */
function handleMuteMember(groupId: number, userId: number, displayName: string) {
@ -394,9 +449,75 @@ function toggleSide() {
sideVisible.value = !sideVisible.value
}
/** 通话入口:功能未开放,先弹提示占位 */
function handleCall() {
message.warning('通话功能暂未开放')
/** 私聊通话入口popover 触发;点 语音 / 视频 直接发起 */
const callPopoverVisible = ref(false)
async function startPrivateCall(mediaType: number) {
callPopoverVisible.value = false
const conversation = conversationStore.activeConversation
if (!conversation) {
return
}
await doInvite(
{
conversationType: ImConversationType.PRIVATE,
mediaType,
inviteeIds: [conversation.targetId]
},
{ nickname: conversation.name, avatar: conversation.avatar }
)
}
/** 群通话入口:默认语音直接弹选人;与微信群通话一致,进通话后用户按需开摄像头 */
function handleGroupCall() {
const conversation = conversationStore.activeConversation
if (!conversation) {
return
}
pendingMediaType.value = ImRtcCallMediaType.VOICE
callMemberPickerRef.value?.open({ groupId: conversation.targetId, mode: 'invite' })
}
/** 选人弹窗确认;带选中 ID 发起群通话 */
async function onCallMemberPicked(selectedIds: number[]) {
const conversation = conversationStore.activeConversation
const mediaType = pendingMediaType.value
pendingMediaType.value = null
if (!conversation || mediaType == null || selectedIds.length === 0) {
return
}
await doInvite(
{
conversationType: ImConversationType.GROUP,
mediaType,
groupId: conversation.targetId,
inviteeIds: selectedIds
},
{ nickname: conversation.name, avatar: conversation.avatar }
)
}
/** 实际调 create 接口;统一处理成功 / ENDED如忙线立即结束/ 异常三种返回 */
async function doInvite(
reqVO: {
conversationType: number
mediaType: number
groupId?: number
inviteeIds: number[]
},
peer: { nickname?: string; avatar?: string }
) {
try {
const resp = await createCall(reqVO)
// INSERT + end线toast INVITING chat tip RTC_CALL_END
if (resp.status === ImRtcCallStatus.ENDED) {
message.warning(resolveCallEndReasonText(resp.endReason))
return
}
// INVITING store / RTC
rtcStore.startInviting(resp, peer)
} catch (e: any) {
message.error(e?.msg || '发起通话失败')
}
}
/** 当前私聊对应的好友(抽屉头部展示用) */
@ -670,4 +791,33 @@ watch(
opacity: 0;
transform: translate(-50%, 20px);
}
.message-panel__call-menu {
display: flex;
flex-direction: column;
gap: 2px;
}
.message-panel__call-menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: var(--el-text-color-primary);
}
.message-panel__call-menu-item:hover {
background-color: var(--el-fill-color-light);
}
</style>
<style>
/* el-popover 全局样式:紧贴菜单的小 padding非 scoped 才能命中 popper-class */
.message-panel__call-popover.el-popover.el-popper {
min-width: auto;
padding: 6px;
}
</style>

View File

@ -13,8 +13,12 @@ export type OpenForwardDialog = (opts: {
/** 打开合并消息详情弹窗 */
export type OpenMergeDetailDialog = (content: string) => void
/** 重拨 RTC 通话;点私聊 RTC_CALL_END 气泡触发 */
export type RtcRedial = (mediaType: number) => void
/** MessagePanel 通过 provide 暴露给子树 */
export const IM_FORWARD_DIALOG_KEY: InjectionKey<OpenForwardDialog> = Symbol('IM_FORWARD_DIALOG')
export const IM_MERGE_DETAIL_DIALOG_KEY: InjectionKey<OpenMergeDetailDialog> = Symbol(
'IM_MERGE_DETAIL_DIALOG'
)
export const IM_RTC_REDIAL_KEY: InjectionKey<RtcRedial> = Symbol('IM_RTC_REDIAL')

View File

@ -7,18 +7,26 @@ import {
ImWebSocketMessageType,
ImMessageType,
ImConversationType,
ImRtcParticipantStatus,
isFriendChatTip,
isFriendNotification,
isGroupRequestNotification,
isNormalMessage
} from '../../utils/constants'
import { playAudioTip } from '../../utils/message'
import { playAudioTip, resolveCallEndReasonText } from '../../utils/message'
import { MESSAGE_PRIVATE_READ_ENABLED, MESSAGE_GROUP_READ_ENABLED } from '../../utils/config'
import { useConversationStore } from './conversationStore'
import { useFriendStore, type FriendNotificationPayload } from './friendStore'
import { getFriendDisplayName } from '../../utils/user'
import { useGroupStore } from './groupStore'
import { useGroupRequestStore } from './groupRequestStore'
import {
useRtcStore,
type ImRtcCallNotification,
type ImRtcParticipantConnectedNotification,
type ImRtcParticipantDisconnectedNotification,
type ImRtcCallEndNotification
} from './rtcStore'
import { readPrivateMessages as apiReadPrivateMessages } from '@/api/im/home/message/private'
import { readGroupMessages as apiReadGroupMessages } from '@/api/im/home/message/group'
import type {
@ -238,6 +246,16 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
case ImMessageType.RECEIPT:
this.handlePrivateReceipt(websocketMessage)
break
case ImMessageType.RTC_CALL:
case ImMessageType.RTC_PARTICIPANT_CONNECTED:
case ImMessageType.RTC_PARTICIPANT_DISCONNECTED:
this.handleRtcSignaling(websocketMessage)
break
case ImMessageType.RTC_CALL_END:
// 入库 + 关闭通话窗 + 渲染聊天 tip私聊场景
this.handleRtcCallEnd(websocketMessage)
this.handlePrivateMessage(websocketMessage)
break
default:
if (isFriendNotification(websocketMessage.type)) {
this.handleFriendNotification(websocketMessage)
@ -282,6 +300,15 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
case ImMessageType.GROUP_MEMBER_SETTING_UPDATE:
this.handleGroupMemberSettingUpdate(websocketMessage)
break
case ImMessageType.RTC_CALL_START:
// 入库 + 渲染聊天 tip胶囊条状态走 1602/1603本帧不动 rtcStore避免与首次填充竞争
this.handleGroupMessage(websocketMessage)
break
case ImMessageType.RTC_CALL_END:
// 入库 + 移除胶囊条 + 关闭通话窗(如果当前在该群通话内)
this.handleRtcCallEnd(websocketMessage)
this.handleGroupMessage(websocketMessage)
break
default:
// TEXT / IMAGE / FILE / VOICE / VIDEO + GROUP_* 群广播事件
this.handleGroupMessage(websocketMessage)
@ -689,6 +716,93 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
},
// ==================== 实时通话信令分发 ====================
/**
* 1601 RTC_CALL status INVITING / JOINED / REJECTED / NO_ANSWER / LEFT+ 1602 / 1603 /
* <p>
* dispatcher JSON mirror handleFriendNotification
*/
handleRtcSignaling(websocketMessage: ImPrivateMessageDTO) {
const rtcStore = useRtcStore()
switch (websocketMessage.type) {
case ImMessageType.RTC_CALL: {
const payload = this.safeParse(websocketMessage.content) as ImRtcCallNotification | null
if (!payload) {
return
}
switch (payload.status) {
case ImRtcParticipantStatus.INVITING:
// 当前已在通话中:忽略新来电;后端层面也会拒绝,这里是兜底
if (!rtcStore.isActive) {
rtcStore.showIncoming(payload)
}
break
case ImRtcParticipantStatus.REJECTED:
// 群通话单人拒绝;把拒绝者从 pending 占位移除(私聊拒绝走 RTC_CALL_END 入消息流,不走本通道)
if (payload.operatorUserId) {
rtcStore.markUserLeft(payload.operatorUserId)
}
break
case ImRtcParticipantStatus.JOINED:
case ImRtcParticipantStatus.NO_ANSWER:
case ImRtcParticipantStatus.LEFT:
// ACCEPT / CANCEL / HUNGUP 暂不需要本端额外响应rtcStore 状态由 1602/1603 + END 维护
break
default:
console.warn('[IM WS] 未识别的 RTC_CALL status', payload)
}
return
}
case ImMessageType.RTC_PARTICIPANT_CONNECTED: {
const payload = this.safeParse(
websocketMessage.content
) as ImRtcParticipantConnectedNotification | null
if (payload?.room && payload.userId) {
rtcStore.applyParticipantConnected(payload)
}
return
}
case ImMessageType.RTC_PARTICIPANT_DISCONNECTED: {
const payload = this.safeParse(
websocketMessage.content
) as ImRtcParticipantDisconnectedNotification | null
if (payload?.room && payload.userId) {
rtcStore.applyParticipantDisconnected(payload)
}
}
}
},
/**
* RTC_CALL_END + payload conversationType
* <p>
*
*
*/
handleRtcCallEnd(websocketMessage: ImPrivateMessageDTO | ImGroupMessageDTO) {
const payload = this.safeParse(websocketMessage.content) as ImRtcCallEndNotification | null
if (!payload?.room) {
return
}
const rtcStore = useRtcStore()
const isGroup = payload.conversationType === ImConversationType.GROUP
// 群通话:移除胶囊条(按外层 groupId 取,不依赖 payload
const groupId = (websocketMessage as ImGroupMessageDTO).groupId
if (isGroup && groupId) {
rtcStore.removeGroupCall(groupId)
}
// 通话窗 / 来电窗指向同一 room 时关闭:
// RUNNING / INVITING 阶段对比 session.roomINCOMING 阶段对比 incomingPayload.room
const matchSession = rtcStore.session?.room === payload.room
const matchIncoming = rtcStore.incomingPayload?.room === payload.room
if (rtcStore.isActive && (matchSession || matchIncoming)) {
const reasonText = resolveCallEndReasonText(payload.endReason)
console.info('[Call] end:', reasonText)
rtcStore.reset()
}
}
}
})

View File

@ -165,20 +165,20 @@ export function isGroupConversation(type: number | undefined): boolean {
}
/** IM 通话媒体类型(对齐后端 ImRtcCallMediaTypeEnum */
export const ImCallMediaType = {
export const ImRtcCallMediaType = {
VOICE: 1,
VIDEO: 2
} as const
/** IM 通话状态(对齐后端 ImRtcCallStatusEnum */
export const ImCallStatus = {
export const ImRtcCallStatus = {
CREATED: 10, // 创建:私聊等被叫接听;群聊发起人已进房等其他人加入
RUNNING: 20, // 进行中:第一个非发起人接通后进入
ENDED: 30 // 已结束
} as const
/** IM 通话结束原因(对齐后端 ImRtcCallEndReasonEnum */
export const ImCallEndReason = {
export const ImRtcCallEndReason = {
HANGUP: 1, // 接通后任一方主动挂断
REJECT: 2, // 被叫接通前点拒接
CANCEL: 3, // 主叫接通前主动取消
@ -186,11 +186,11 @@ export const ImCallEndReason = {
ERROR: 9 // 网络中断 / 设备失败
} as const
/** ImCallEndReason 取值类型 */
export type ImCallEndReasonValue = (typeof ImCallEndReason)[keyof typeof ImCallEndReason]
/** ImRtcCallEndReason 取值类型 */
export type ImRtcCallEndReasonValue = (typeof ImRtcCallEndReason)[keyof typeof ImRtcCallEndReason]
/** IM 通话参与者状态(对齐后端 ImRtcParticipantStatusEnum同时作为 RTC_CALL 信令 status 字段取值 */
export const ImCallParticipantStatus = {
export const ImRtcParticipantStatus = {
INVITING: 10, // 来电邀请
JOINED: 20, // 接听 / 已加入
REJECTED: 30, // 拒接
@ -198,9 +198,23 @@ export const ImCallParticipantStatus = {
LEFT: 50 // 挂断离开
} as const
/** ImCallParticipantStatus 取值类型 */
export type ImCallParticipantStatusValue =
(typeof ImCallParticipantStatus)[keyof typeof ImCallParticipantStatus]
/** ImRtcParticipantStatus 取值类型 */
export type ImRtcParticipantStatusValue =
(typeof ImRtcParticipantStatus)[keyof typeof ImRtcParticipantStatus]
/**
* IM UI inviting / incoming / running
* ImRtcCallStatus 1:1 stage
*/
export const ImRtcCallStage = {
IDLE: 'idle', // 空闲;后端无对应(本端无 session
INVITING: 'inviting', // 主叫等待对方接受;对应后端 ImRtcCallStatus.CREATED自己是主叫
INCOMING: 'incoming', // 被叫来电响铃;对应后端 ImRtcCallStatus.CREATED自己是被叫
RUNNING: 'running' // 通话中;对应后端 ImRtcCallStatus.RUNNING
} as const
/** ImRtcCallStage 取值类型 */
export type ImRtcCallStageValue = (typeof ImRtcCallStage)[keyof typeof ImRtcCallStage]
/** IM WebSocket 外层帧类型(对齐后端 ImPrivateMessageDTO.TYPE / ImGroupMessageDTO.TYPE */
export const ImWebSocketMessageType = {

View File

@ -1,7 +1,7 @@
import { generateUUID } from '@/utils'
import { useUserStore } from '@/store/modules/user'
import {
ImCallEndReason,
ImRtcCallEndReason,
ImConversationType,
ImMessageType,
type ImConversationTypeValue
@ -905,15 +905,15 @@ export function resolveRtcCallPrivateBubbleText(payload: RtcCallEndPayload | nul
const hasDuration = duration > 0
const isOperator = payload.operatorUserId === getCurrentUserId()
switch (payload.endReason) {
case ImCallEndReason.HANGUP:
case ImRtcCallEndReason.HANGUP:
return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话中断'
case ImCallEndReason.CANCEL:
case ImRtcCallEndReason.CANCEL:
return isOperator ? '已取消' : '对方已取消'
case ImCallEndReason.REJECT:
case ImRtcCallEndReason.REJECT:
return isOperator ? '已拒绝' : '对方已拒绝'
case ImCallEndReason.BUSY:
case ImRtcCallEndReason.BUSY:
return isOperator ? '忙线未接听' : '对方忙线中'
case ImCallEndReason.ERROR:
case ImRtcCallEndReason.ERROR:
return hasDuration ? `通话中断 ${formatCallDuration(duration)}` : '通话中断'
default:
return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话已结束'
@ -936,15 +936,15 @@ export function resolveRtcCallTipText(message: {
*/
export function resolveCallEndReasonText(reason: number | undefined): string {
switch (reason) {
case ImCallEndReason.REJECT:
case ImRtcCallEndReason.REJECT:
return '对方已拒绝'
case ImCallEndReason.CANCEL:
case ImRtcCallEndReason.CANCEL:
return '对方已取消'
case ImCallEndReason.BUSY:
case ImRtcCallEndReason.BUSY:
return '对方忙线中'
case ImCallEndReason.HANGUP:
case ImRtcCallEndReason.HANGUP:
return '通话已结束'
case ImCallEndReason.ERROR:
case ImRtcCallEndReason.ERROR:
return '通话异常'
default:
return '通话已断开'