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 @@
+
+
+
+
+ {{ detail.id }}
+ {{ detail.room }}
+
+ {{ detail.inviterNickname || '-' }} ({{ detail.inviterUserId }})
+
+
+
+
+
+
+ {{ detail.groupName || '-' }} ({{ detail.groupId }})
+
+ -
+
+
+
+
+
+
+
+
+
+ -
+
+ {{ formatDate(detail.startTime) }}
+
+ {{ detail.acceptTime ? formatDate(detail.acceptTime) : '-' }}
+
+
+ {{ detail.endTime ? formatDate(detail.endTime) : '-' }}
+
+ {{ duration }}
+
+
+
+ 参与者列表
+
+
+
+ {{ row.userNickname || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.acceptTime ? formatDate(row.acceptTime) : '-' }}
+
+
+
+
+ {{ row.leaveTime ? formatDate(row.leaveTime) : '-' }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+ {{ row.inviterNickname || '-' }}
+ ({{ row.inviterUserId }})
+
+
+
+
+
+
+
+
+
+
+ {{ row.groupName || '-' }}
+ ({{ row.groupId }})
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+ {{ resolveCallDuration(row.acceptTime, row.endTime) }}
+
+
+
+
+
+
+ 详情
+
+
+
+
+
+
+
+
+
+
+
+
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) : '-'
+}