feat(im): 灰条 tip 文案的 mention 段支持点击弹 UserInfoCard

群广播 / 撤回 / 好友事件 tip 文案从纯字符串拆成 TipSegment[],mention
段携带 userId,渲染层挂点击 → uiStore.openUserInfoCardAtEvent。

- utils/message.ts:加 TipSegment 协议 + 零依赖 helper
- utils/user.ts、utils/conversation.ts:加 segments builder,string 版
  改写为 segmentsToText 包装,避免 case 表分叉
- TipSegments.vue:按 activeConversation 推断 addSource,群里走
  GROUP+群名、私聊走 SEARCH;nickname 不传备注避免 UserInfo 首屏闪
- MessageItem.vue / MessageHistory.vue:tip 块切 <TipSegments>

顺手补:utils/constants.ts 新增 SystemUserSexEnum,替换 IM 模块 sex
硬编码 1 / 2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
im
YunaiV 2026-05-07 23:46:50 +08:00
parent 5b85a4c469
commit 9eb221e8d2
7 changed files with 352 additions and 127 deletions

View File

@ -64,6 +64,15 @@ export const SystemUserSocialTypeEnum = {
}
}
/**
* system_user_sex
*/
export const SystemUserSexEnum = {
UNKNOWN: 0, // 未知
MALE: 1, // 男
FEMALE: 2 // 女
}
// ========== INFRA 模块 ==========
/**
*

View File

@ -139,25 +139,22 @@
<!-- 消息列表 -->
<div class="flex-1 overflow-y-auto">
<template
v-for="message in currentList"
:key="message.id || message.clientMessageId"
>
<template v-for="message in currentList" :key="message.id || message.clientMessageId">
<!-- 好友会话事件FRIEND_ADD / FRIEND_DELETE居中灰色不挂头像 / sender
跟主聊天面板里 MessageItem 的渲染语义对齐 -->
<div
v-if="isFriendChatTip(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)]"
>
{{ resolveFriendNotificationText(message) }}
<TipSegments :segments="resolveFriendNotificationSegments(message)" />
</div>
<!-- 群广播事件文案跟好友事件同灰色样式 -->
<!-- 群广播事件跟好友事件同灰色样式mention 段挂点击弹 UserInfoCard -->
<div
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)]"
>
{{ resolveGroupNotificationText(message) }}
<TipSegments :segments="resolveGroupNotificationSegments(message)" />
</div>
<!-- 普通消息行 -->
@ -194,12 +191,12 @@
</div>
<div class="mt-1.5">
<!-- 撤回单独走灰色 tip 文案不渲染气泡 -->
<!-- 撤回单独走灰色 tipsender 名段可点击 -->
<div
v-if="message.type === ImMessageType.RECALL"
class="text-sm italic text-[var(--el-text-color-secondary)]"
>
{{ recallTipOf(message) }}
<TipSegments :segments="recallTipSegmentsOf(message)" />
</div>
<!-- 其它类型走 MessageBubble 复用主聊天气泡 -->
<MessageBubble
@ -256,10 +253,15 @@ import {
getMemberDisplayName,
getSenderDisplayName,
getSenderRealNickname,
resolveFriendNotificationSegments,
resolveFriendNotificationText,
resolveGroupNotificationText
resolveGroupNotificationSegments
} from '@/views/im/utils/user'
import { buildFacePreviewText, buildRecallTip } from '@/views/im/utils/conversation'
import {
buildFacePreviewText,
buildRecallTip,
buildRecallTipSegments
} from '@/views/im/utils/conversation'
import { useMessagePuller } from '@/views/im/home/composables/useMessagePuller'
import { useVoicePlayer } from '@/views/im/home/composables/useVoicePlayer'
import {
@ -281,6 +283,7 @@ import type { Message } from '@/views/im/home/types'
import UserAvatar from '../../../../components/user/UserAvatar.vue'
import MessageBubble from './MessageBubble.vue'
import GroupMember, { type GroupMemberLite } from '../../../../components/group/GroupMember.vue'
import TipSegments from './TipSegments.vue'
defineOptions({ name: 'ImMessageHistory' })
@ -332,7 +335,17 @@ function senderRealNicknameOf(message: Message): string {
function recallTipOf(message: Message): string {
return buildRecallTip(
message.senderId,
!!message.selfSend,
message.selfSend,
conversation.value?.type ?? 0,
conversation.value?.targetId ?? 0
)
}
/** 单条撤回消息的 tip segmentssender 名段挂可点击 mention */
function recallTipSegmentsOf(message: Message) {
return buildRecallTipSegments(
message.senderId,
message.selfSend,
conversation.value?.type ?? 0,
conversation.value?.targetId ?? 0
)
@ -392,7 +405,11 @@ const filterChipLabel = computed(() => {
/** 点 tab 落筛选;同 kind 重复点击 → 当 toggle 关掉(避免迷惑) */
function setFilter(filter: ActiveFilter) {
if (activeFilter.value?.kind === filter.kind && filter.kind !== 'date' && filter.kind !== 'member') {
if (
activeFilter.value?.kind === filter.kind &&
filter.kind !== 'date' &&
filter.kind !== 'member'
) {
activeFilter.value = null
return
}
@ -552,11 +569,7 @@ async function loadEarlier() {
}
// 4. conversationStoreprependMessages + + IndexedDB
// messages
conversationStore.prependMessages(
conversation.value.type,
conversation.value.targetId,
earlier
)
conversationStore.prependMessages(conversation.value.type, conversation.value.targetId, earlier)
} finally {
loadingMore.value = false
}
@ -599,9 +612,7 @@ function getAvatar(message: Message): string {
}
if (isGroup.value) {
const group = groupStore.getGroup(conversation.value.targetId)
return (
group?.members?.find((member) => member.userId === message.senderId)?.avatar || ''
)
return group?.members?.find((member) => member.userId === message.senderId)?.avatar || ''
}
return conversation.value.avatar || ''
}
@ -650,7 +661,6 @@ function locateMessage(messageId: number) {
emit('locate', messageId)
visible.value = false
}
</script>
<style scoped>

View File

@ -12,23 +12,23 @@
v-if="isFriendChatTipMessage"
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
>
{{ friendChatTipText }}
<TipSegments :segments="friendChatTipSegments" />
</div>
<!-- 群广播事件跟好友事件同灰色样式文案按 type 拼装 -->
<!-- 群广播事件跟好友事件同灰色样式mention 段挂点击弹 UserInfoCard -->
<div
v-else-if="isGroupNotificationMessage"
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
>
{{ groupNotificationText }}
<TipSegments :segments="groupNotificationSegments" />
</div>
<!-- 撤回消息整行展示灰色 tip 文案 -->
<!-- 撤回消息整行灰色 tipsender 名段可点击 -->
<div
v-else-if="isRecall"
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
>
{{ recallTip }}
<TipSegments :segments="recallTipSegments" />
</div>
<!-- 普通消息气泡 -->
@ -149,6 +149,7 @@
<script lang="ts" setup>
import { computed, inject } from 'vue'
import { useClipboard } from '@vueuse/core'
import Icon from '@/components/Icon/src/Icon.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { ElMessageBox } from 'element-plus'
@ -173,9 +174,11 @@ import {
buildQuoteFromMessage,
extractAddableFace,
getQuoteFromMessage,
type CardMessage
parseMessage,
type CardMessage,
type TextMessage
} from '@/views/im/utils/message'
import { buildRecallTip } from '@/views/im/utils/conversation'
import { buildRecallTipSegments } from '@/views/im/utils/conversation'
import { formatTimeTip } from '@/views/im/utils/time'
import { useUserStore } from '@/store/modules/user'
import { useConversationStore } from '../../../../store/conversationStore'
@ -187,8 +190,8 @@ import {
getMemberDisplayName,
getSenderDisplayName,
getSenderRealNickname,
resolveFriendNotificationText,
resolveGroupNotificationText
resolveFriendNotificationSegments,
resolveGroupNotificationSegments
} from '@/views/im/utils/user'
import { useImUiStore } from '../../../../store/uiStore'
import { useMessageSender } from '../../../../composables/useMessageSender'
@ -197,6 +200,7 @@ import { useMuteOverlay } from '../../../../composables/useMuteOverlay'
import type { Message } from '../../../../types'
import MessageReadStatus from './MessageReadStatus.vue'
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'
@ -234,6 +238,8 @@ const { uploadAndSendMedia } = useMediaUploader()
const muteOverlay = useMuteOverlay()
// confirm message props.message vue/no-dupe-keys
const { confirm: confirmDialog, success: successMessage } = useMessage()
// legacy:true HTTP navigator.clipboard execCommand
const { copy: copyToClipboard } = useClipboard({ legacy: true })
// ==================== ====================
@ -259,10 +265,10 @@ const isMerge = computed(() => props.message.type === ImMessageType.MERGE)
/** 是否已撤回pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL渲染只需识别 type */
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
/** 撤回提示文案buildRecallTip 实时算 sender 名(按 conversation 上下文走 WeChat 优先级) */
const recallTip = computed(() => {
/** 撤回提示 segments依赖 activeConversation 实时算 sender 名 */
const recallTipSegments = computed(() => {
const conversation = conversationStore.activeConversation
return buildRecallTip(
return buildRecallTipSegments(
props.message.senderId,
props.message.selfSend,
conversation?.type ?? 0,
@ -273,14 +279,14 @@ const recallTip = computed(() => {
/** 是否会话内好友事件气泡FRIEND_ADD / FRIEND_DELETE */
const isFriendChatTipMessage = computed(() => isFriendChatTip(props.message.type))
/** 好友事件文案FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示,文案固定 */
const friendChatTipText = computed(() => resolveFriendNotificationText(props.message))
/** 好友事件 segments */
const friendChatTipSegments = computed(() => resolveFriendNotificationSegments(props.message))
/** 是否群广播事件GROUP_CREATE..GROUP_BANNED 段位,排除 GROUP_MEMBER_SETTING_UPDATE 个人信号) */
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
/** 群广播事件文案:按 type 拼装(成员加入 / 退群 / 公告变更等) */
const groupNotificationText = computed(() => resolveGroupNotificationText(props.message))
/** 群广播事件 segments */
const groupNotificationSegments = computed(() => resolveGroupNotificationSegments(props.message))
// ==================== / payload ====================
@ -446,6 +452,7 @@ const isAtMe = computed(() => {
/** 右键菜单 key 常量push 端和分发端从同一处取typo 编译期就能抓 */
const MENU_KEYS = {
COPY: 'COPY',
REPLY: 'REPLY',
FORWARD: 'FORWARD',
MULTI_SELECT: 'MULTI_SELECT',
@ -489,6 +496,14 @@ async function handleContextMenu(e: MouseEvent) {
danger?: boolean
icon?: string
}> = []
//
if (props.message.type === ImMessageType.TEXT) {
items.push({
key: MENU_KEYS.COPY,
name: '复制',
icon: 'ant-design:copy-outlined'
})
}
// id0+ + MERGE
if (!!props.message.id && !isRecall.value && !isMerge.value) {
items.push({
@ -584,6 +599,7 @@ async function handleContextMenu(e: MouseEvent) {
// uiStore DOM
const menuHandlers: Record<MenuKey, () => void | Promise<void>> = {
[MENU_KEYS.COPY]: handleCopy,
[MENU_KEYS.REPLY]: handleReply,
[MENU_KEYS.FORWARD]: handleForward,
[MENU_KEYS.MULTI_SELECT]: handleEnterMultiSelect,
@ -707,6 +723,16 @@ async function handlePin() {
} catch {}
}
/** 复制文本消息:解出 content 字段写入剪贴板,提示「内容已复制到剪贴板」 */
async function handleCopy() {
const text = parseMessage<TextMessage>(props.message.content)?.content
if (!text) {
return
}
await copyToClipboard(text)
successMessage('内容已复制到剪贴板')
}
/** 进入引用模式:把当前消息构造成 QuoteMessage 写入 draftStoreMessageInput 顶部引用条响应式出现 */
function handleReply() {
const conversation = conversationStore.activeConversation

View File

@ -0,0 +1,62 @@
<!--
会话内灰条 tip 片段渲染依赖 activeConversation 推断点击落点的 addSource / 群名
-->
<template>
<template v-for="(segment, _index) in segments" :key="_index">
<span
v-if="segment.type === 'mention'"
class="cursor-pointer text-[#576b95] hover:underline"
@click.stop="handleMentionClick(segment, $event)"
>{{ segment.text }}</span
>
<span v-else>{{ segment.text }}</span>
</template>
</template>
<script lang="ts" setup>
import { ImConversationType, ImFriendAddSource } from '@/views/im/utils/constants'
import type { TipSegment } from '@/views/im/utils/message'
import { getSenderAvatar } from '@/views/im/utils/user'
import type { User } from '../../../../types'
import { useConversationStore } from '../../../../store/conversationStore'
import { useFriendStore } from '../../../../store/friendStore'
import { useGroupStore } from '../../../../store/groupStore'
import { useImUiStore } from '../../../../store/uiStore'
defineOptions({ name: 'ImTipSegments' })
defineProps<{
segments: TipSegment[]
}>()
/**
* nickname 不传 friend.displayName / member.displayUserName 等备注避免 UserInfo 首屏闪非真实昵称
* addSourceExtra 不用 conversation.name 兜底conversation.name = groupRemark || group.name
* 个人备注会污染加好友话术
*/
function handleMentionClick(
segment: { type: 'mention'; userId: number; text: string },
event: MouseEvent
) {
const conversation = useConversationStore().activeConversation
const isGroup = conversation?.type === ImConversationType.GROUP
const group = isGroup ? useGroupStore().getGroup(conversation!.targetId) : undefined
const member = group?.members?.find((m) => m.userId === segment.userId)
const friend = useFriendStore().getFriend(segment.userId)
const user: User = {
id: segment.userId,
nickname: friend?.nickname || member?.nickname || segment.text,
avatar: getSenderAvatar(
segment.userId,
conversation?.type ?? 0,
conversation?.targetId ?? 0
)
}
useImUiStore().openUserInfoCardAtEvent(
user,
event,
isGroup ? ImFriendAddSource.GROUP : ImFriendAddSource.SEARCH,
isGroup ? group?.name || '' : ''
)
}
</script>

View File

@ -10,10 +10,14 @@ import { ImMessageType, isFriendChatTip, isGroupNotification } from './constants
import {
getCardLabelInfo,
parseMessage,
segmentsToText,
tipMention,
tipText,
type CardMessage,
type FaceMessage,
type FileMessage,
type TextMessage
type TextMessage,
type TipSegment
} from './message'
import {
getSenderDisplayName,
@ -47,6 +51,36 @@ export function buildFacePreviewText(facePayload: { name?: string } | null | und
return facePayload?.name ? `[表情] ${facePayload.name}` : '[表情]'
}
/**
* segments
*
* senderId mention
*/
export function buildRecallTipSegments(
senderId: number,
selfSend: boolean,
conversationType: number,
conversationTargetId: number,
fallbackName?: string
): TipSegment[] {
if (selfSend) {
return [tipText('你撤回了一条消息')]
}
const senderDisplayName = getSenderDisplayName(
senderId,
conversationType,
conversationTargetId,
fallbackName
)
if (!senderId) {
return [tipText(`${senderDisplayName || '对方'} 撤回了一条消息`)]
}
return [
tipMention(senderId, senderDisplayName || '对方'),
tipText(' 撤回了一条消息')
]
}
/** 撤回提示文案:自己撤回固定文案,对方撤回带 sender 名(实时算 + fallbackName 兜底) */
export function buildRecallTip(
senderId: number,
@ -55,16 +89,9 @@ export function buildRecallTip(
conversationTargetId: number,
fallbackName?: string
): string {
if (selfSend) {
return '你撤回了一条消息'
}
const senderDisplayName = getSenderDisplayName(
senderId,
conversationType,
conversationTargetId,
fallbackName
return segmentsToText(
buildRecallTipSegments(senderId, selfSend, conversationType, conversationTargetId, fallbackName)
)
return `${senderDisplayName || '对方'} 撤回了一条消息`
}
/**

View File

@ -22,6 +22,41 @@ export const generateClientMessageId = (): string => {
return generateUUID()
}
// ==================== Tip 片段(灰条文案渲染用) ====================
// 把"XX 邀请 YY 加入群聊""XX 撤回了一条消息"等 tip 文案拆成 segment 数组,
// mention 段携带 userId渲染层据此挂点击事件弹 UserInfoCard。
export type TipSegment =
| { type: 'text'; text: string }
| { type: 'mention'; userId: number; text: string }
export const tipText = (text: string): TipSegment => ({ type: 'text', text })
export const tipMention = (userId: number, text: string): TipSegment => ({
type: 'mention',
userId,
text
})
export const segmentsToText = (segments: TipSegment[]): string =>
segments.map((s) => s.text).join('')
/** 多个 userId 用同一个分隔符插值成 segments每个 user 单独成 mention 段 */
export function joinMentionSegments(
userIds: number[],
separator: string,
resolveName: (userId: number) => string
): TipSegment[] {
const out: TipSegment[] = []
userIds.forEach((id, index) => {
if (index > 0) {
out.push(tipText(separator))
}
out.push(tipMention(id, resolveName(id)))
})
return out
}
// ==================== 引用消息 ====================
/** 引用消息 payload(对齐后端 QuoteMessage) */

View File

@ -10,8 +10,16 @@
// ====================================================================
import { useUserStore } from '@/store/modules/user'
import { SystemUserSexEnum } from '@/utils/constants'
import { ImConversationType, ImMessageType } from './constants'
import { getCurrentUserId } from './storage'
import {
joinMentionSegments,
segmentsToText,
tipMention,
tipText,
type TipSegment
} from './message'
import { useFriendStore } from '../home/store/friendStore'
import { useGroupStore } from '../home/store/groupStore'
import type { Friend, Group } from '../home/types'
@ -222,100 +230,148 @@ export type GroupNotificationPayload = {
}
}
/**
* 广 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 {
let payload: GroupNotificationPayload = {}
try {
payload = JSON.parse(message.content || '{}')
} catch {
return ''
}
const resolve =
resolveName ||
((id: number) => getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0))
const operatorName = payload.operatorUserId
? (operatorNameOverride ?? resolve(payload.operatorUserId))
: ''
const memberNames = (payload.memberUserIds || []).map(resolve).join('、')
const newOwnerName = payload.newOwnerUserId ? resolve(payload.newOwnerUserId) : ''
return segmentsToText(
resolveGroupNotificationSegments(message, resolveName, operatorNameOverride)
)
}
/** 会话内好友事件 segments */
export function resolveFriendNotificationSegments(message: {
type?: number
}): TipSegment[] {
switch (message.type) {
case ImMessageType.GROUP_CREATE:
return `${operatorName} 创建了群聊`
case ImMessageType.GROUP_NAME_UPDATE:
return `${operatorName} 将群名修改为 "${payload.newName ?? ''}"`
case ImMessageType.GROUP_NOTICE_UPDATE:
return `${operatorName} 更新了群公告`
case ImMessageType.GROUP_INFO_UPDATE:
// 兜底事件:按非 null 字段优先匹配特化文案,全部为空时降级为 "更新了群信息" 通用文案
if (payload.newAvatar) {
return `${operatorName} 更换了群头像`
}
return `${operatorName} 更新了群信息`
case ImMessageType.GROUP_DISSOLVE:
return `${operatorName} 解散了群聊`
case ImMessageType.GROUP_MEMBER_INVITE:
return `${operatorName} 邀请 ${memberNames} 加入群聊`
case ImMessageType.GROUP_MEMBER_ENTER: {
// 自由进群 / 主动申请通过:操作人 = 进群者文案统一展示「XX 加入了群聊」
const entrantName = payload.entrantUserId ? resolve(payload.entrantUserId) : operatorName
return `${entrantName} 加入了群聊`
}
case ImMessageType.GROUP_MEMBER_QUIT:
return `${operatorName} 退出了群聊`
case ImMessageType.GROUP_MEMBER_KICK:
return `${operatorName} 移出了 ${memberNames}`
case ImMessageType.GROUP_MEMBER_NICKNAME_UPDATE:
return `${operatorName} 修改群昵称为 "${payload.displayUserName ?? ''}"`
case ImMessageType.GROUP_ADMIN_ADD:
return `${operatorName}${memberNames} 设为管理员`
case ImMessageType.GROUP_ADMIN_REMOVE:
return `${operatorName} 撤销了 ${memberNames} 的管理员身份`
case ImMessageType.GROUP_OWNER_TRANSFER:
return `${operatorName} 已将群主转让给 ${newOwnerName}`
case ImMessageType.GROUP_MESSAGE_PIN:
return `${operatorName} 置顶了一条消息`
case ImMessageType.GROUP_MESSAGE_UNPIN:
return `${operatorName} 取消了一条置顶消息`
case ImMessageType.GROUP_MEMBER_MUTED: {
const mutedName = payload.mutedUserId ? resolve(payload.mutedUserId) : ''
return `${operatorName}${mutedName} 禁言`
}
case ImMessageType.GROUP_MEMBER_CANCEL_MUTED: {
const mutedName = payload.mutedUserId ? resolve(payload.mutedUserId) : ''
return `${operatorName} 解除了 ${mutedName} 的禁言`
}
case ImMessageType.GROUP_MUTED:
return `${operatorName} 开启了全群禁言`
case ImMessageType.GROUP_CANCEL_MUTED:
return `${operatorName} 关闭了全群禁言`
case ImMessageType.GROUP_BANNED:
return payload.banned ? `${operatorName} 封禁了该群` : `${operatorName} 解封了该群`
case ImMessageType.FRIEND_ADD:
return [tipText('你们已经是好友了,开始聊天吧')]
case ImMessageType.FRIEND_DELETE:
return [tipText('你已删除好友')]
default:
return ''
return []
}
}
/** 会话内好友事件文案FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡,文案固定不依赖 payload */
export function resolveFriendNotificationText(message: { type?: number }): string {
switch (message.type) {
case ImMessageType.FRIEND_ADD:
return '你们已经是好友了,开始聊天吧'
case ImMessageType.FRIEND_DELETE:
return '你已删除好友'
default:
return ''
}
return segmentsToText(resolveFriendNotificationSegments(message))
}
/** 性别图标:男 1 / 女 20 / null / undefined 一律不展示,对齐微信留白 */
/** 性别图标UNKNOWN / null / undefined 一律不展示,对齐微信留白 */
export function getGenderIcon(sex?: number): string {
if (sex === 1) {
if (sex === SystemUserSexEnum.MALE) {
return 'mdi:human-male'
}
if (sex === 2) {
if (sex === SystemUserSexEnum.FEMALE) {
return 'mdi:human-female'
}
return ''
@ -323,10 +379,10 @@ export function getGenderIcon(sex?: number): string {
/** 性别图标主题色:男蓝、女粉 */
export function getGenderColor(sex?: number): string {
if (sex === 1) {
if (sex === SystemUserSexEnum.MALE) {
return '#5b97f5'
}
if (sex === 2) {
if (sex === SystemUserSexEnum.FEMALE) {
return '#f56c92'
}
return ''