✨ feat(im): 优化消息的 format 相关的逻辑,从 user 抽到 message 工具类里,更加统一
parent
841d2cb763
commit
a170ae37ab
|
|
@ -154,7 +154,7 @@
|
|||
v-else-if="isGroupNotification(message.type)"
|
||||
class="px-4 py-3 text-12px text-center italic text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<TipSegments :segments="resolveGroupNotificationSegments(message)" />
|
||||
<TipSegments :segments="resolveGroupNotificationSegments(message, resolveGroupMemberName(message))" />
|
||||
</div>
|
||||
|
||||
<!-- 普通消息行 -->
|
||||
|
|
@ -252,11 +252,13 @@ import { IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys'
|
|||
import {
|
||||
getMemberDisplayName,
|
||||
getSenderDisplayName,
|
||||
getSenderRealNickname,
|
||||
getSenderRealNickname
|
||||
} from '@/views/im/utils/user'
|
||||
import {
|
||||
resolveFriendNotificationSegments,
|
||||
resolveFriendNotificationText,
|
||||
resolveGroupNotificationSegments
|
||||
} from '@/views/im/utils/user'
|
||||
} from '@/views/im/utils/message'
|
||||
import {
|
||||
buildFacePreviewText,
|
||||
buildRecallTip,
|
||||
|
|
@ -322,6 +324,12 @@ function senderDisplayNameOf(message: Message): string {
|
|||
)
|
||||
}
|
||||
|
||||
/** 群广播事件 segments 的成员名解析器;按当前会话 targetId 走 getSenderDisplayName */
|
||||
function resolveGroupMemberName(message: Message): (userId: number) => string {
|
||||
return (id: number) =>
|
||||
getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0)
|
||||
}
|
||||
|
||||
/** 单条消息的发送人真实昵称:给 UserAvatar 色卡 / alt 用,永远是 nickname 不掺备注 */
|
||||
function senderRealNicknameOf(message: Message): string {
|
||||
return getSenderRealNickname(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,40 @@
|
|||
<TipSegments :segments="groupNotificationSegments" />
|
||||
</div>
|
||||
|
||||
<!-- 群通话事件(RTC_CALL_START / RTC_CALL_END):居中灰色 tip 两段式 -->
|
||||
<div
|
||||
v-else-if="isRtcCallTipMessage"
|
||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||
>
|
||||
<TipSegments :segments="rtcCallTipSegments" />
|
||||
</div>
|
||||
|
||||
<!-- 私聊通话结束(RTC_CALL_END):仿微信「准气泡」;按 selfSend 左右分布;电话图标 + 文案 -->
|
||||
<div v-else-if="isRtcCallPrivateBubbleMessage" class="flex gap-2 items-start px-4 py-2">
|
||||
<div
|
||||
class="flex flex-1 min-w-0 gap-2 items-start"
|
||||
:class="{ 'flex-row-reverse': message.selfSend }"
|
||||
>
|
||||
<UserAvatar
|
||||
:id="message.selfSend ? userStore.getUser?.id : message.senderId"
|
||||
:name="senderRealNickname"
|
||||
:url="message.selfSend ? userStore.getUser?.avatar : senderAvatar"
|
||||
:size="36"
|
||||
/>
|
||||
<div
|
||||
class="flex gap-2 items-center px-3.5 py-2 text-sm rounded-lg"
|
||||
:class="
|
||||
message.selfSend
|
||||
? 'text-black bg-[#95ec69]'
|
||||
: 'text-[var(--el-text-color-primary)] bg-[var(--el-fill-color-light)]'
|
||||
"
|
||||
>
|
||||
<Icon icon="ant-design:phone-outlined" :size="16" class="rotate-[135deg] flex-shrink-0" />
|
||||
<span class="whitespace-nowrap">{{ rtcCallPrivateBubbleText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 撤回消息:整行灰色 tip,sender 名段可点击 -->
|
||||
<div
|
||||
v-else-if="isRecall"
|
||||
|
|
@ -166,7 +200,8 @@ import {
|
|||
isFriendChatTip,
|
||||
isGroupNotification,
|
||||
isMediaMessageType,
|
||||
isNormalMessage
|
||||
isNormalMessage,
|
||||
isRtcCallTip
|
||||
} from '@/views/im/utils/constants'
|
||||
import {
|
||||
MESSAGE_TIME_TIP_GAP_MS,
|
||||
|
|
@ -197,10 +232,15 @@ import {
|
|||
getMemberDisplayName,
|
||||
getMentionCandidates,
|
||||
getSenderDisplayName,
|
||||
getSenderRealNickname,
|
||||
resolveFriendNotificationSegments,
|
||||
resolveGroupNotificationSegments
|
||||
getSenderRealNickname
|
||||
} from '@/views/im/utils/user'
|
||||
import {
|
||||
resolveFriendNotificationSegments,
|
||||
resolveGroupNotificationSegments,
|
||||
resolveRtcCallTipSegments,
|
||||
resolveRtcCallPrivateBubbleText,
|
||||
parseRtcCallPayload
|
||||
} from '@/views/im/utils/message'
|
||||
import { useImUiStore } from '../../../../store/uiStore'
|
||||
import { useMessageSender } from '../../../../composables/useMessageSender'
|
||||
import { mediaTypeHandlers, useMediaUploader } from '../../../../composables/useMediaUploader'
|
||||
|
|
@ -294,7 +334,39 @@ const friendChatTipSegments = computed(() => resolveFriendNotificationSegments(p
|
|||
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
|
||||
|
||||
/** 群广播事件 segments */
|
||||
const groupNotificationSegments = computed(() => resolveGroupNotificationSegments(props.message))
|
||||
const groupNotificationSegments = computed(() =>
|
||||
resolveGroupNotificationSegments(props.message, (id: number) =>
|
||||
getSenderDisplayName(id, ImConversationType.GROUP, props.message.targetId ?? 0)
|
||||
)
|
||||
)
|
||||
|
||||
/** 私聊 RTC_CALL_END 走「准气泡」(左右分布 + 电话图标 + 文案);非私聊场景为 null */
|
||||
const rtcCallEndPrivatePayload = computed(() => {
|
||||
if (props.message.type !== ImMessageType.RTC_CALL_END) {
|
||||
return null
|
||||
}
|
||||
const payload = parseRtcCallPayload(props.message.content)
|
||||
return payload?.conversationType === ImConversationType.PRIVATE ? payload : null
|
||||
})
|
||||
|
||||
/** 是否私聊通话气泡 */
|
||||
const isRtcCallPrivateBubbleMessage = computed(() => rtcCallEndPrivatePayload.value !== null)
|
||||
|
||||
/** 私聊通话气泡文案(按 operatorUserId 是否当前用户区分;对齐微信两端不同视角) */
|
||||
const rtcCallPrivateBubbleText = computed(() =>
|
||||
resolveRtcCallPrivateBubbleText(rtcCallEndPrivatePayload.value)
|
||||
)
|
||||
|
||||
/** 是否会话内通话事件居中 tip:仅群聊场景(START 总是群聊;END 私聊走气泡分支,群聊走 tip) */
|
||||
const isRtcCallTipMessage = computed(() => {
|
||||
if (!isRtcCallTip(props.message.type)) {
|
||||
return false
|
||||
}
|
||||
return !isRtcCallPrivateBubbleMessage.value
|
||||
})
|
||||
|
||||
/** 通话事件 segments;仅群聊 tip 用 */
|
||||
const rtcCallTipSegments = computed(() => resolveRtcCallTipSegments(props.message))
|
||||
|
||||
// ==================== 消息内容解析 / payload ====================
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ import {
|
|||
setQuietly,
|
||||
StorageKeys
|
||||
} from '../../utils/storage'
|
||||
import { getGroupDisplayName, type GroupNotificationPayload } from '../../utils/user'
|
||||
import { getGroupDisplayName } from '../../utils/user'
|
||||
import { type GroupNotificationPayload } from '../../utils/message'
|
||||
import type { Group, GroupMember, Message } from '../types'
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -134,6 +134,8 @@ import CardLineLabel from '@/views/im/home/components/card/CardLineLabel.vue'
|
|||
import {
|
||||
parseMessage,
|
||||
getFileIconInfo,
|
||||
resolveFriendNotificationText,
|
||||
resolveGroupNotificationText,
|
||||
type ImageMessage,
|
||||
type FileMessage,
|
||||
type AudioMessage,
|
||||
|
|
@ -143,10 +145,6 @@ import {
|
|||
type FaceMessage,
|
||||
type MergeMessage
|
||||
} from '@/views/im/utils/message'
|
||||
import {
|
||||
resolveFriendNotificationText,
|
||||
resolveGroupNotificationText
|
||||
} from '@/views/im/utils/user'
|
||||
import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation'
|
||||
|
||||
defineOptions({ name: 'ImMessageContentPreview' })
|
||||
|
|
@ -242,7 +240,7 @@ const friendChatTipText = computed(() => resolveFriendNotificationText({ type: p
|
|||
/** 是否群广播事件 */
|
||||
const isGroupNotificationType = computed(() => isGroupNotification(props.type ?? -1))
|
||||
|
||||
/** 群广播事件文案:复用 utils/user.resolveGroupNotificationText;admin 端 operator 用 senderNickname 直接覆盖,其它 id 退化为 用户(id) */
|
||||
/** 群广播事件文案:admin 端 operator 用 senderNickname 直接覆盖,其它 id 退化为 用户(id) */
|
||||
const groupNotificationText = computed(() =>
|
||||
resolveGroupNotificationText(
|
||||
{ type: props.type, content: props.content },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// 2. fallbackName 由调用方传入(典型来源:Conversation.lastSenderDisplayName 快照),透传到 getSenderDisplayName 内部,算不出真名时兜底
|
||||
// ====================================================================
|
||||
|
||||
import { ImMessageType, isFriendChatTip, isGroupNotification } from './constants'
|
||||
import { ImConversationType, ImMessageType, isFriendChatTip, isGroupNotification } from './constants'
|
||||
import {
|
||||
getCardLabelInfo,
|
||||
parseMessage,
|
||||
|
|
@ -19,11 +19,8 @@ import {
|
|||
type TextMessage,
|
||||
type TipSegment
|
||||
} from './message'
|
||||
import {
|
||||
getSenderDisplayName,
|
||||
resolveFriendNotificationText,
|
||||
resolveGroupNotificationText
|
||||
} from './user'
|
||||
import { getSenderDisplayName } from './user'
|
||||
import { resolveFriendNotificationText, resolveGroupNotificationText } from './message'
|
||||
import type { Message } from '../home/types'
|
||||
|
||||
/** 会话主键:`type-targetId` 拼成稳定字符串,给 v-for :key、active 比对、map key 等场景共用 */
|
||||
|
|
@ -151,7 +148,9 @@ export function resolveConversationLastContent(
|
|||
return resolveFriendNotificationText(message)
|
||||
}
|
||||
if (isGroupNotification(message.type)) {
|
||||
return resolveGroupNotificationText(message)
|
||||
return resolveGroupNotificationText(message, (id) =>
|
||||
getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0)
|
||||
)
|
||||
}
|
||||
return summarizeMessageContent(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { generateUUID } from '@/utils'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ImConversationType, ImMessageType, type ImConversationTypeValue } from './constants'
|
||||
import {
|
||||
ImCallEndReason,
|
||||
ImConversationType,
|
||||
ImMessageType,
|
||||
type ImConversationTypeValue
|
||||
} from './constants'
|
||||
import { getCurrentUserId } from './storage'
|
||||
import { formatCallDuration } from './time'
|
||||
import { useFriendStore } from '../home/store/friendStore'
|
||||
import { useGroupStore } from '../home/store/groupStore'
|
||||
import type { Conversation, Message, User, GroupLite } from '../home/types'
|
||||
|
|
@ -644,3 +650,303 @@ export const formatJson = (content?: string): string => {
|
|||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 群广播事件 payload + 文案 ====================
|
||||
|
||||
// 群广播事件 payload;对齐后端 GroupNotificationMessage 子类聚合字段
|
||||
export type GroupNotificationPayload = {
|
||||
operatorUserId?: number
|
||||
memberUserIds?: number[]
|
||||
newOwnerUserId?: number
|
||||
oldName?: string
|
||||
newName?: string
|
||||
oldNotice?: string
|
||||
newNotice?: string
|
||||
oldAvatar?: string
|
||||
newAvatar?: string
|
||||
displayUserName?: string
|
||||
messageId?: number
|
||||
// 禁言事件
|
||||
mutedUserId?: number
|
||||
muteEndTime?: string
|
||||
// 全群封禁
|
||||
banned?: boolean
|
||||
// 自由进群事件
|
||||
entrantUserId?: number
|
||||
addSource?: number
|
||||
// PIN 事件携带的完整被置顶消息对象
|
||||
message?: {
|
||||
id: number
|
||||
clientMessageId?: string
|
||||
senderId: number
|
||||
groupId: number
|
||||
type: number
|
||||
content: string
|
||||
status: number
|
||||
sendTime: string
|
||||
atUserIds?: number[]
|
||||
receiverUserIds?: number[]
|
||||
receiptStatus?: number
|
||||
readCount?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 群广播事件 segments
|
||||
*
|
||||
* resolveName 由调用方注入(默认场景传 getSenderDisplayName);
|
||||
* operatorNameOverride 仅覆盖 operator 段文案,mention userId 仍用 payload.operatorUserId
|
||||
*/
|
||||
export function resolveGroupNotificationSegments(
|
||||
message: { type?: number; content?: string; targetId?: number },
|
||||
resolveName: (userId: number) => string,
|
||||
operatorNameOverride?: string
|
||||
): TipSegment[] {
|
||||
let payload: GroupNotificationPayload = {}
|
||||
try {
|
||||
payload = JSON.parse(message.content || '{}')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
// ENTER 主语是 entrant 而非 operator,独立处理;其它 case 都以 operatorUserId 为主语
|
||||
if (message.type === ImMessageType.GROUP_MEMBER_ENTER) {
|
||||
const entrantId = payload.entrantUserId ?? payload.operatorUserId
|
||||
return entrantId
|
||||
? [tipMention(entrantId, resolveName(entrantId)), tipText(' 加入了群聊')]
|
||||
: []
|
||||
}
|
||||
if (!payload.operatorUserId) {
|
||||
return []
|
||||
}
|
||||
const operatorSegment = tipMention(
|
||||
payload.operatorUserId,
|
||||
operatorNameOverride ?? resolveName(payload.operatorUserId)
|
||||
)
|
||||
const memberSegments = joinMentionSegments(payload.memberUserIds || [], '、', resolveName)
|
||||
|
||||
switch (message.type) {
|
||||
case ImMessageType.GROUP_CREATE:
|
||||
return [operatorSegment, tipText(' 创建了群聊')]
|
||||
case ImMessageType.GROUP_NAME_UPDATE:
|
||||
return [operatorSegment, tipText(` 将群名修改为 "${payload.newName ?? ''}"`)]
|
||||
case ImMessageType.GROUP_NOTICE_UPDATE:
|
||||
return [operatorSegment, tipText(' 更新了群公告')]
|
||||
case ImMessageType.GROUP_INFO_UPDATE:
|
||||
return payload.newAvatar
|
||||
? [operatorSegment, tipText(' 更换了群头像')]
|
||||
: [operatorSegment, tipText(' 更新了群信息')]
|
||||
case ImMessageType.GROUP_DISSOLVE:
|
||||
return [operatorSegment, tipText(' 解散了群聊')]
|
||||
case ImMessageType.GROUP_MEMBER_INVITE:
|
||||
return [operatorSegment, tipText(' 邀请 '), ...memberSegments, tipText(' 加入群聊')]
|
||||
case ImMessageType.GROUP_MEMBER_QUIT:
|
||||
return [operatorSegment, tipText(' 退出了群聊')]
|
||||
case ImMessageType.GROUP_MEMBER_KICK:
|
||||
return [operatorSegment, tipText(' 移出了 '), ...memberSegments]
|
||||
case ImMessageType.GROUP_MEMBER_NICKNAME_UPDATE:
|
||||
return [operatorSegment, tipText(` 修改群昵称为 "${payload.displayUserName ?? ''}"`)]
|
||||
case ImMessageType.GROUP_ADMIN_ADD:
|
||||
return [operatorSegment, tipText(' 将 '), ...memberSegments, tipText(' 设为管理员')]
|
||||
case ImMessageType.GROUP_ADMIN_REMOVE:
|
||||
return [operatorSegment, tipText(' 撤销了 '), ...memberSegments, tipText(' 的管理员身份')]
|
||||
case ImMessageType.GROUP_OWNER_TRANSFER:
|
||||
return payload.newOwnerUserId
|
||||
? [
|
||||
operatorSegment,
|
||||
tipText(' 已将群主转让给 '),
|
||||
tipMention(payload.newOwnerUserId, resolveName(payload.newOwnerUserId))
|
||||
]
|
||||
: []
|
||||
case ImMessageType.GROUP_MESSAGE_PIN:
|
||||
return [operatorSegment, tipText(' 置顶了一条消息')]
|
||||
case ImMessageType.GROUP_MESSAGE_UNPIN:
|
||||
return [operatorSegment, tipText(' 取消了一条置顶消息')]
|
||||
case ImMessageType.GROUP_MEMBER_MUTED:
|
||||
return payload.mutedUserId
|
||||
? [
|
||||
operatorSegment,
|
||||
tipText(' 将 '),
|
||||
tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)),
|
||||
tipText(' 禁言')
|
||||
]
|
||||
: []
|
||||
case ImMessageType.GROUP_MEMBER_CANCEL_MUTED:
|
||||
return payload.mutedUserId
|
||||
? [
|
||||
operatorSegment,
|
||||
tipText(' 解除了 '),
|
||||
tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)),
|
||||
tipText(' 的禁言')
|
||||
]
|
||||
: []
|
||||
case ImMessageType.GROUP_MUTED:
|
||||
return [operatorSegment, tipText(' 开启了全群禁言')]
|
||||
case ImMessageType.GROUP_CANCEL_MUTED:
|
||||
return [operatorSegment, tipText(' 关闭了全群禁言')]
|
||||
case ImMessageType.GROUP_BANNED:
|
||||
return [operatorSegment, tipText(payload.banned ? ' 封禁了该群' : ' 解封了该群')]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** 群广播事件中文文案 */
|
||||
export function resolveGroupNotificationText(
|
||||
message: { type?: number; content?: string; targetId?: number },
|
||||
resolveName: (userId: number) => string,
|
||||
operatorNameOverride?: string
|
||||
): string {
|
||||
return segmentsToText(
|
||||
resolveGroupNotificationSegments(message, resolveName, operatorNameOverride)
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 好友事件 ====================
|
||||
|
||||
/** 会话内好友事件 segments */
|
||||
export function resolveFriendNotificationSegments(message: { type?: number }): TipSegment[] {
|
||||
switch (message.type) {
|
||||
case ImMessageType.FRIEND_ADD:
|
||||
return [tipText('你们已经是好友了,开始聊天吧')]
|
||||
case ImMessageType.FRIEND_DELETE:
|
||||
return [tipText('你已删除好友')]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** 会话内好友事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡,文案固定不依赖 payload */
|
||||
export function resolveFriendNotificationText(message: { type?: number }): string {
|
||||
return segmentsToText(resolveFriendNotificationSegments(message))
|
||||
}
|
||||
|
||||
// ==================== RTC 通话事件 ====================
|
||||
|
||||
// RTC_CALL_START payload;仅群聊;用于聊天 tip 文案「{inviter} 发起了{voice/video}通话」
|
||||
export type RtcCallStartPayload = {
|
||||
room?: string
|
||||
conversationType?: number
|
||||
mediaType?: number
|
||||
inviterUserId?: number
|
||||
inviterNickname?: string
|
||||
inviterAvatar?: string
|
||||
}
|
||||
|
||||
// RTC_CALL_END payload;私聊准气泡 + 群聊「通话已结束 [时长 X]」共用
|
||||
export type RtcCallEndPayload = {
|
||||
room?: string
|
||||
conversationType?: number
|
||||
mediaType?: number
|
||||
endReason?: number
|
||||
durationSeconds?: number
|
||||
operatorUserId?: number
|
||||
operatorNickname?: string
|
||||
operatorAvatar?: string
|
||||
}
|
||||
|
||||
/** 解析 RTC_CALL_START / RTC_CALL_END 消息 content;解析失败返回 null */
|
||||
export function parseRtcCallPayload(
|
||||
content?: string
|
||||
): (RtcCallStartPayload & RtcCallEndPayload) | null {
|
||||
return content ? parseMessage<RtcCallStartPayload & RtcCallEndPayload>(content) : null
|
||||
}
|
||||
|
||||
/** 媒体类型文案;TODO 字典化 */
|
||||
function callMediaText(mediaType: number | undefined): string {
|
||||
return mediaType === 2 ? '视频' : '语音'
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话内通话事件 segments(RTC_CALL_START / RTC_CALL_END)
|
||||
* <p>
|
||||
* 群聊两段式:START「{inviter} 发起了{voice/video}通话」+ END「{voice/video}通话已结束 [时长 X]」
|
||||
* <p>
|
||||
* 私聊气泡走 {@link resolveRtcCallPrivateBubbleText}
|
||||
*/
|
||||
export function resolveRtcCallTipSegments(message: {
|
||||
type?: number
|
||||
content?: string
|
||||
selfSend?: boolean
|
||||
}): TipSegment[] {
|
||||
const payload = parseRtcCallPayload(message.content)
|
||||
if (!payload) {
|
||||
return []
|
||||
}
|
||||
const media = callMediaText(payload.mediaType)
|
||||
if (message.type === ImMessageType.RTC_CALL_START) {
|
||||
const inviter = payload.inviterNickname?.trim() || `用户 ${payload.inviterUserId ?? ''}`
|
||||
return [tipText(`${inviter} 发起了${media}通话`)]
|
||||
}
|
||||
if (message.type === ImMessageType.RTC_CALL_END) {
|
||||
if (payload.durationSeconds && payload.durationSeconds > 0) {
|
||||
return [tipText(`${media}通话已结束(时长 ${formatCallDuration(payload.durationSeconds)})`)]
|
||||
}
|
||||
return [tipText(`${media}通话已结束`)]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* 私聊 RTC_CALL_END 气泡内文案;按 operatorUserId 是不是自己渲染两端不同文案(对齐微信)
|
||||
* <p>
|
||||
* 文案规则:
|
||||
* HANGUP duration > 0 → 「通话时长 N」(双方一致)
|
||||
* HANGUP duration ≤ 0 → 「通话中断」(未接通的兜底)
|
||||
* CANCEL → 操作者「已取消」/ 另一方「对方已取消」
|
||||
* REJECT → 操作者「已拒绝」/ 另一方「对方已拒绝」
|
||||
* BUSY → 操作者「忙线未接听」/ 另一方「对方忙线中」
|
||||
* ERROR → 「通话中断 [N]」(接通后异常断开;duration > 0 时带时长)
|
||||
*/
|
||||
export function resolveRtcCallPrivateBubbleText(payload: RtcCallEndPayload | null): string {
|
||||
if (!payload) {
|
||||
return '通话已结束'
|
||||
}
|
||||
const duration = payload.durationSeconds ?? 0
|
||||
const hasDuration = duration > 0
|
||||
const isOperator = payload.operatorUserId === getCurrentUserId()
|
||||
switch (payload.endReason) {
|
||||
case ImCallEndReason.HANGUP:
|
||||
return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话中断'
|
||||
case ImCallEndReason.CANCEL:
|
||||
return isOperator ? '已取消' : '对方已取消'
|
||||
case ImCallEndReason.REJECT:
|
||||
return isOperator ? '已拒绝' : '对方已拒绝'
|
||||
case ImCallEndReason.BUSY:
|
||||
return isOperator ? '忙线未接听' : '对方忙线中'
|
||||
case ImCallEndReason.ERROR:
|
||||
return hasDuration ? `通话中断 ${formatCallDuration(duration)}` : '通话中断'
|
||||
default:
|
||||
return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话已结束'
|
||||
}
|
||||
}
|
||||
|
||||
/** 会话内通话事件文案 */
|
||||
export function resolveRtcCallTipText(message: {
|
||||
type?: number
|
||||
content?: string
|
||||
selfSend?: boolean
|
||||
}): string {
|
||||
return segmentsToText(resolveRtcCallTipSegments(message))
|
||||
}
|
||||
|
||||
/**
|
||||
* RTC_CALL_END 结束原因兜底文案;前端 toast / console 兜底用
|
||||
* <p>
|
||||
* 缺 operator 信息(同步响应 + WS push 兜底场景)时的通用文案;细分文案(按发送方视角)走 {@link resolveRtcCallPrivateBubbleText}
|
||||
*/
|
||||
export function resolveCallEndReasonText(reason: number | undefined): string {
|
||||
switch (reason) {
|
||||
case ImCallEndReason.REJECT:
|
||||
return '对方已拒绝'
|
||||
case ImCallEndReason.CANCEL:
|
||||
return '对方已取消'
|
||||
case ImCallEndReason.BUSY:
|
||||
return '对方忙线中'
|
||||
case ImCallEndReason.HANGUP:
|
||||
return '通话已结束'
|
||||
case ImCallEndReason.ERROR:
|
||||
return '通话异常'
|
||||
default:
|
||||
return '通话已断开'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,3 +88,13 @@ export function formatMergeItemTime(timestamp: number): string {
|
|||
}
|
||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
/** RTC 通话时长(秒)→ "00:06" / "1:23:45" */
|
||||
export function formatCallDuration(seconds: number | undefined): string {
|
||||
const total = Math.max(0, Math.floor(seconds || 0))
|
||||
const h = Math.floor(total / 3600)
|
||||
const m = Math.floor((total % 3600) / 60)
|
||||
const s = total % 60
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,11 @@ import { SystemUserSexEnum } from '@/utils/constants'
|
|||
import {
|
||||
ImConversationType,
|
||||
ImFriendAddSource,
|
||||
ImMessageType,
|
||||
IM_AT_ALL_NICKNAME,
|
||||
IM_AT_ALL_USER_ID
|
||||
} from './constants'
|
||||
import { getCurrentUserId } from './storage'
|
||||
import {
|
||||
joinMentionSegments,
|
||||
segmentsToText,
|
||||
tipMention,
|
||||
tipText,
|
||||
type MentionCandidate,
|
||||
type TipSegment
|
||||
} from './message'
|
||||
import { type MentionCandidate } from './message'
|
||||
import { useConversationStore } from '../home/store/conversationStore'
|
||||
import { useFriendStore } from '../home/store/friendStore'
|
||||
import { useGroupStore } from '../home/store/groupStore'
|
||||
|
|
@ -301,183 +293,6 @@ export function openMentionUserInfoCardAtEvent(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 群广播事件(GROUP_* 系列)的中文文案
|
||||
*
|
||||
* 按 message.type 取 content payload 字段,昵称默认走 getSenderDisplayName(备注 / 群昵称 / 真实昵称兜底);
|
||||
* 管理后台无 store,可传入 resolveName 自定义 id → 名字(如 senderNickname + 用户(id) 兜底);
|
||||
* home 端 MessageItem.vue / ConversationItem.vue / MessageHistory.vue 与 admin 端 MessageContentPreview.vue 共用
|
||||
*/
|
||||
export type GroupNotificationPayload = {
|
||||
operatorUserId?: number
|
||||
memberUserIds?: number[]
|
||||
newOwnerUserId?: number
|
||||
oldName?: string
|
||||
newName?: string
|
||||
oldNotice?: string
|
||||
newNotice?: string
|
||||
oldAvatar?: string
|
||||
newAvatar?: string
|
||||
displayUserName?: string
|
||||
messageId?: number
|
||||
mutedUserId?: number // 禁言目标用户
|
||||
muteEndTime?: string // 禁言到期时间
|
||||
banned?: boolean // 封禁状态
|
||||
entrantUserId?: number // 自由进群事件:进群者用户编号
|
||||
addSource?: number // 自由进群事件:来源(搜索 / 二维码 / 分享链接)
|
||||
/** PIN 事件携带的完整被置顶消息对象 */
|
||||
message?: {
|
||||
id: number
|
||||
clientMessageId?: string
|
||||
senderId: number
|
||||
groupId: number
|
||||
type: number
|
||||
content: string
|
||||
status: number
|
||||
sendTime: string
|
||||
atUserIds?: number[]
|
||||
receiverUserIds?: number[]
|
||||
receiptStatus?: number
|
||||
readCount?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 群广播事件 segments
|
||||
*
|
||||
* resolveName 默认走 getSenderDisplayName,可注入自定义 resolver;
|
||||
* operatorNameOverride 仅覆盖 operator 段文案,mention userId 仍用 payload.operatorUserId
|
||||
*/
|
||||
export function resolveGroupNotificationSegments(
|
||||
message: { type?: number; content?: string; targetId?: number },
|
||||
resolveName?: (userId: number) => string,
|
||||
operatorNameOverride?: string
|
||||
): TipSegment[] {
|
||||
let payload: GroupNotificationPayload = {}
|
||||
try {
|
||||
payload = JSON.parse(message.content || '{}')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
const resolve =
|
||||
resolveName ||
|
||||
((id: number) => getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0))
|
||||
|
||||
// ENTER 主语是 entrant 而非 operator,独立处理;其它 case 都以 operatorUserId 为主语
|
||||
if (message.type === ImMessageType.GROUP_MEMBER_ENTER) {
|
||||
const entrantId = payload.entrantUserId ?? payload.operatorUserId
|
||||
return entrantId ? [tipMention(entrantId, resolve(entrantId)), tipText(' 加入了群聊')] : []
|
||||
}
|
||||
if (!payload.operatorUserId) {
|
||||
return []
|
||||
}
|
||||
const operatorSegment = tipMention(
|
||||
payload.operatorUserId,
|
||||
operatorNameOverride ?? resolve(payload.operatorUserId)
|
||||
)
|
||||
const memberSegments = joinMentionSegments(payload.memberUserIds || [], '、', resolve)
|
||||
|
||||
switch (message.type) {
|
||||
case ImMessageType.GROUP_CREATE:
|
||||
return [operatorSegment, tipText(' 创建了群聊')]
|
||||
case ImMessageType.GROUP_NAME_UPDATE:
|
||||
return [operatorSegment, tipText(` 将群名修改为 "${payload.newName ?? ''}"`)]
|
||||
case ImMessageType.GROUP_NOTICE_UPDATE:
|
||||
return [operatorSegment, tipText(' 更新了群公告')]
|
||||
case ImMessageType.GROUP_INFO_UPDATE:
|
||||
return payload.newAvatar
|
||||
? [operatorSegment, tipText(' 更换了群头像')]
|
||||
: [operatorSegment, tipText(' 更新了群信息')]
|
||||
case ImMessageType.GROUP_DISSOLVE:
|
||||
return [operatorSegment, tipText(' 解散了群聊')]
|
||||
case ImMessageType.GROUP_MEMBER_INVITE:
|
||||
return [operatorSegment, tipText(' 邀请 '), ...memberSegments, tipText(' 加入群聊')]
|
||||
case ImMessageType.GROUP_MEMBER_QUIT:
|
||||
return [operatorSegment, tipText(' 退出了群聊')]
|
||||
case ImMessageType.GROUP_MEMBER_KICK:
|
||||
return [operatorSegment, tipText(' 移出了 '), ...memberSegments]
|
||||
case ImMessageType.GROUP_MEMBER_NICKNAME_UPDATE:
|
||||
return [operatorSegment, tipText(` 修改群昵称为 "${payload.displayUserName ?? ''}"`)]
|
||||
case ImMessageType.GROUP_ADMIN_ADD:
|
||||
return [operatorSegment, tipText(' 将 '), ...memberSegments, tipText(' 设为管理员')]
|
||||
case ImMessageType.GROUP_ADMIN_REMOVE:
|
||||
return [
|
||||
operatorSegment,
|
||||
tipText(' 撤销了 '),
|
||||
...memberSegments,
|
||||
tipText(' 的管理员身份')
|
||||
]
|
||||
case ImMessageType.GROUP_OWNER_TRANSFER:
|
||||
return payload.newOwnerUserId
|
||||
? [
|
||||
operatorSegment,
|
||||
tipText(' 已将群主转让给 '),
|
||||
tipMention(payload.newOwnerUserId, resolve(payload.newOwnerUserId))
|
||||
]
|
||||
: []
|
||||
case ImMessageType.GROUP_MESSAGE_PIN:
|
||||
return [operatorSegment, tipText(' 置顶了一条消息')]
|
||||
case ImMessageType.GROUP_MESSAGE_UNPIN:
|
||||
return [operatorSegment, tipText(' 取消了一条置顶消息')]
|
||||
case ImMessageType.GROUP_MEMBER_MUTED:
|
||||
return payload.mutedUserId
|
||||
? [
|
||||
operatorSegment,
|
||||
tipText(' 将 '),
|
||||
tipMention(payload.mutedUserId, resolve(payload.mutedUserId)),
|
||||
tipText(' 禁言')
|
||||
]
|
||||
: []
|
||||
case ImMessageType.GROUP_MEMBER_CANCEL_MUTED:
|
||||
return payload.mutedUserId
|
||||
? [
|
||||
operatorSegment,
|
||||
tipText(' 解除了 '),
|
||||
tipMention(payload.mutedUserId, resolve(payload.mutedUserId)),
|
||||
tipText(' 的禁言')
|
||||
]
|
||||
: []
|
||||
case ImMessageType.GROUP_MUTED:
|
||||
return [operatorSegment, tipText(' 开启了全群禁言')]
|
||||
case ImMessageType.GROUP_CANCEL_MUTED:
|
||||
return [operatorSegment, tipText(' 关闭了全群禁言')]
|
||||
case ImMessageType.GROUP_BANNED:
|
||||
return [operatorSegment, tipText(payload.banned ? ' 封禁了该群' : ' 解封了该群')]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** 群广播事件中文文案 */
|
||||
export function resolveGroupNotificationText(
|
||||
message: { type?: number; content?: string; targetId?: number },
|
||||
resolveName?: (userId: number) => string,
|
||||
operatorNameOverride?: string
|
||||
): string {
|
||||
return segmentsToText(
|
||||
resolveGroupNotificationSegments(message, resolveName, operatorNameOverride)
|
||||
)
|
||||
}
|
||||
|
||||
/** 会话内好友事件 segments */
|
||||
export function resolveFriendNotificationSegments(message: {
|
||||
type?: number
|
||||
}): TipSegment[] {
|
||||
switch (message.type) {
|
||||
case ImMessageType.FRIEND_ADD:
|
||||
return [tipText('你们已经是好友了,开始聊天吧')]
|
||||
case ImMessageType.FRIEND_DELETE:
|
||||
return [tipText('你已删除好友')]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** 会话内好友事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡,文案固定不依赖 payload */
|
||||
export function resolveFriendNotificationText(message: { type?: number }): string {
|
||||
return segmentsToText(resolveFriendNotificationSegments(message))
|
||||
}
|
||||
|
||||
/** 性别图标;UNKNOWN / null / undefined 一律不展示,对齐微信留白 */
|
||||
export function getGenderIcon(sex?: number): string {
|
||||
if (sex === SystemUserSexEnum.MALE) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue