diff --git a/src/api/im/manager/rtc/index.ts b/src/api/im/manager/rtc/index.ts new file mode 100644 index 000000000..579256977 --- /dev/null +++ b/src/api/im/manager/rtc/index.ts @@ -0,0 +1,40 @@ +import request from '@/config/axios' + +export interface ImManagerRtcCallVO { + id: number + room: string + conversationType: number + mediaType: number + inviterUserId: number + inviterNickname?: string + groupId?: number + groupName?: string + status: number + endReason?: number + startTime: Date + acceptTime?: Date + endTime?: Date + createTime: Date +} + +export interface ImManagerRtcParticipantVO { + id: number + callId: number + userId: number + userNickname?: string + role: number + status: number + inviteTime: Date + acceptTime?: Date + leaveTime?: Date +} + +// 获得通话记录分页 +export const getManagerRtcCallPage = (params: PageParam) => { + return request.get({ url: '/im/manager/rtc/page', params }) +} + +// 获得通话参与者列表 +export const getManagerRtcCallParticipantList = (id: number) => { + return request.get({ url: '/im/manager/rtc/participant-list?id=' + id }) +} diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 1a2cbc83f..f813cb0a4 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -339,5 +339,10 @@ export enum DICT_TYPE { IM_GROUP_MEMBER_ROLE = 'im_group_member_role', // IM 群成员角色 IM_GROUP_ADD_SOURCE = 'im_group_add_source', // IM 加群来源 IM_GROUP_REQUEST_HANDLE_RESULT = 'im_group_request_handle_result', // IM 加群申请处理结果 - IM_RTC_CALL_MEDIA_TYPE = 'im_rtc_call_media_type' // IM 通话媒体类型:1=语音 / 2=视频 + IM_RTC_CALL_MEDIA_TYPE = 'im_rtc_call_media_type', // IM 通话媒体类型:1=语音 / 2=视频 + IM_RTC_CALL_CONVERSATION_TYPE = 'im_rtc_call_conversation_type', // IM 通话会话类型:1=私聊 / 2=群聊 + IM_RTC_CALL_STATUS = 'im_rtc_call_status', // IM 通话状态:10=创建 / 20=进行中 / 30=已结束 + IM_RTC_CALL_END_REASON = 'im_rtc_call_end_reason', // IM 通话结束原因:1=通话结束 / 2=已拒绝 / 3=已取消 / 4=无人接听 / 5=对方正忙 / 9=通话异常 + IM_RTC_PARTICIPANT_ROLE = 'im_rtc_participant_role', // IM 通话参与角色:1=发起人 / 2=被邀请者 / 3=主动加入者 + IM_RTC_PARTICIPANT_STATUS = 'im_rtc_participant_status' // IM 通话参与状态:10=邀请中 / 20=已加入 / 30=已拒绝 / 40=未应答 / 50=已离开 } diff --git a/src/views/im/manager/message/MessageContentPreview.vue b/src/views/im/manager/message/MessageContentPreview.vue index 527d0e3c4..ea2d0ec88 100644 --- a/src/views/im/manager/message/MessageContentPreview.vue +++ b/src/views/im/manager/message/MessageContentPreview.vue @@ -119,6 +119,15 @@ {{ friendChatTipText }} + + + + {{ rtcCallTipText }} + + {{ fallbackText }} @@ -128,11 +137,20 @@ import { computed } from 'vue' import Icon from '@/components/Icon/src/Icon.vue' import { formatFileSize } from '@/utils/file' import { formatSeconds } from '@/utils/formatTime' -import { ImMessageType, isFriendChatTip, isGroupNotification } from '@/views/im/utils/constants' +import { DICT_TYPE, getDictLabel } from '@/utils/dict' +import { + ImMessageType, + ImRtcCallEndReason, + ImRtcCallMediaType, + isFriendChatTip, + isGroupNotification, + isRtcCallTip +} from '@/views/im/utils/constants' import { MESSAGE_MERGE_PREVIEW_LINES } from '@/views/im/utils/config' import CardLineLabel from '@/views/im/home/components/card/CardLineLabel.vue' import { parseMessage, + parseRtcCallPayload, getFileIconInfo, resolveFriendNotificationText, resolveGroupNotificationText, @@ -248,4 +266,34 @@ const groupNotificationText = computed(() => props.senderNickname ) ) + +/** 是否通话事件气泡(RTC_CALL_START / RTC_CALL_END) */ +const isRtcCallTipType = computed(() => isRtcCallTip(props.type ?? -1)) + +/** 通话事件文案:START 显示「{发起人} 发起了{媒体}通话」;END 显示「{媒体}通话已结束 [原因] [时长]」 */ +const rtcCallTipText = computed(() => { + const payload = parseRtcCallPayload(props.content) + if (!payload) { + return '' + } + const mediaLabel = payload.mediaType === ImRtcCallMediaType.VIDEO ? '视频' : '语音' + if (props.type === ImMessageType.RTC_CALL_START) { + const inviter = payload.inviterNickname?.trim() || `用户(${payload.inviterUserId ?? ''})` + return `${inviter} 发起了${mediaLabel}通话` + } + // RTC_CALL_END + const segments = [`${mediaLabel}通话已结束`] + // HANGUP 字典 label 是「通话结束」,会和前缀重复;跳过 + if (payload.endReason && payload.endReason !== ImRtcCallEndReason.HANGUP) { + const reason = getDictLabel(DICT_TYPE.IM_RTC_CALL_END_REASON, payload.endReason) + if (reason) { + segments.push(reason) + } + } + const duration = payload.durationSeconds ?? 0 + if (duration > 0) { + segments.push(`时长 ${formatSeconds(duration)}`) + } + return segments.join(',') +}) diff --git a/src/views/im/manager/rtc/RtcCallDetail.vue b/src/views/im/manager/rtc/RtcCallDetail.vue new file mode 100644 index 000000000..f62c9c768 --- /dev/null +++ b/src/views/im/manager/rtc/RtcCallDetail.vue @@ -0,0 +1,112 @@ + + + diff --git a/src/views/im/manager/rtc/index.vue b/src/views/im/manager/rtc/index.vue new file mode 100644 index 000000000..30026d662 --- /dev/null +++ b/src/views/im/manager/rtc/index.vue @@ -0,0 +1,231 @@ + + + diff --git a/src/views/im/utils/conversation.ts b/src/views/im/utils/conversation.ts index 0ced78434..ccf1af8af 100644 --- a/src/views/im/utils/conversation.ts +++ b/src/views/im/utils/conversation.ts @@ -123,6 +123,9 @@ export function summarizeMessageContent( return buildFacePreviewText(parseMessage(message.content)) case ImMessageType.MERGE: return '[聊天记录]' + case ImMessageType.RTC_CALL_START: + case ImMessageType.RTC_CALL_END: + return '[语音通话]' default: return '' } diff --git a/src/views/im/utils/time.ts b/src/views/im/utils/time.ts index a84bdac21..a9c64bf7c 100644 --- a/src/views/im/utils/time.ts +++ b/src/views/im/utils/time.ts @@ -98,3 +98,15 @@ export function formatCallDuration(seconds: number | undefined): string { const pad = (n: number) => String(n).padStart(2, '0') return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}` } + +/** 接通到结束的通话时长;任一时间缺失返回 '-' */ +export function resolveCallDuration( + acceptTime: Date | string | undefined, + endTime: Date | string | undefined +): string { + if (!acceptTime || !endTime) { + return '-' + } + const seconds = Math.floor((new Date(endTime).getTime() - new Date(acceptTime).getTime()) / 1000) + return seconds > 0 ? formatCallDuration(seconds) : '-' +}