✨ 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
parent
5b85a4c469
commit
9eb221e8d2
|
|
@ -64,6 +64,15 @@ export const SystemUserSocialTypeEnum = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户性别枚举(对齐后端 system_user_sex 字典)
|
||||||
|
*/
|
||||||
|
export const SystemUserSexEnum = {
|
||||||
|
UNKNOWN: 0, // 未知
|
||||||
|
MALE: 1, // 男
|
||||||
|
FEMALE: 2 // 女
|
||||||
|
}
|
||||||
|
|
||||||
// ========== INFRA 模块 ==========
|
// ========== INFRA 模块 ==========
|
||||||
/**
|
/**
|
||||||
* 代码生成模板类型
|
* 代码生成模板类型
|
||||||
|
|
|
||||||
|
|
@ -139,25 +139,22 @@
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<template
|
<template v-for="message in currentList" :key="message.id || message.clientMessageId">
|
||||||
v-for="message in currentList"
|
|
||||||
:key="message.id || message.clientMessageId"
|
|
||||||
>
|
|
||||||
<!-- 好友会话事件(FRIEND_ADD / FRIEND_DELETE):居中灰色,不挂头像 / sender,
|
<!-- 好友会话事件(FRIEND_ADD / FRIEND_DELETE):居中灰色,不挂头像 / sender,
|
||||||
跟主聊天面板里 MessageItem 的渲染语义对齐 -->
|
跟主聊天面板里 MessageItem 的渲染语义对齐 -->
|
||||||
<div
|
<div
|
||||||
v-if="isFriendChatTip(message.type)"
|
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)]"
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- 群广播事件文案:跟好友事件同灰色样式 -->
|
<!-- 群广播事件:跟好友事件同灰色样式,mention 段挂点击弹 UserInfoCard -->
|
||||||
<div
|
<div
|
||||||
v-else-if="isGroupNotification(message.type)"
|
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)]"
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- 普通消息行 -->
|
<!-- 普通消息行 -->
|
||||||
|
|
@ -194,12 +191,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-1.5">
|
<div class="mt-1.5">
|
||||||
<!-- 撤回:单独走灰色 tip 文案,不渲染气泡 -->
|
<!-- 撤回:单独走灰色 tip,sender 名段可点击 -->
|
||||||
<div
|
<div
|
||||||
v-if="message.type === ImMessageType.RECALL"
|
v-if="message.type === ImMessageType.RECALL"
|
||||||
class="text-sm italic text-[var(--el-text-color-secondary)]"
|
class="text-sm italic text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
{{ recallTipOf(message) }}
|
<TipSegments :segments="recallTipSegmentsOf(message)" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 其它类型走 MessageBubble 复用主聊天气泡 -->
|
<!-- 其它类型走 MessageBubble 复用主聊天气泡 -->
|
||||||
<MessageBubble
|
<MessageBubble
|
||||||
|
|
@ -256,10 +253,15 @@ import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
getSenderRealNickname,
|
getSenderRealNickname,
|
||||||
|
resolveFriendNotificationSegments,
|
||||||
resolveFriendNotificationText,
|
resolveFriendNotificationText,
|
||||||
resolveGroupNotificationText
|
resolveGroupNotificationSegments
|
||||||
} from '@/views/im/utils/user'
|
} 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 { useMessagePuller } from '@/views/im/home/composables/useMessagePuller'
|
||||||
import { useVoicePlayer } from '@/views/im/home/composables/useVoicePlayer'
|
import { useVoicePlayer } from '@/views/im/home/composables/useVoicePlayer'
|
||||||
import {
|
import {
|
||||||
|
|
@ -281,6 +283,7 @@ import type { Message } from '@/views/im/home/types'
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
||||||
import MessageBubble from './MessageBubble.vue'
|
import MessageBubble from './MessageBubble.vue'
|
||||||
import GroupMember, { type GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import GroupMember, { type GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
||||||
|
import TipSegments from './TipSegments.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageHistory' })
|
defineOptions({ name: 'ImMessageHistory' })
|
||||||
|
|
||||||
|
|
@ -332,7 +335,17 @@ function senderRealNicknameOf(message: Message): string {
|
||||||
function recallTipOf(message: Message): string {
|
function recallTipOf(message: Message): string {
|
||||||
return buildRecallTip(
|
return buildRecallTip(
|
||||||
message.senderId,
|
message.senderId,
|
||||||
!!message.selfSend,
|
message.selfSend,
|
||||||
|
conversation.value?.type ?? 0,
|
||||||
|
conversation.value?.targetId ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单条撤回消息的 tip segments:sender 名段挂可点击 mention */
|
||||||
|
function recallTipSegmentsOf(message: Message) {
|
||||||
|
return buildRecallTipSegments(
|
||||||
|
message.senderId,
|
||||||
|
message.selfSend,
|
||||||
conversation.value?.type ?? 0,
|
conversation.value?.type ?? 0,
|
||||||
conversation.value?.targetId ?? 0
|
conversation.value?.targetId ?? 0
|
||||||
)
|
)
|
||||||
|
|
@ -392,7 +405,11 @@ const filterChipLabel = computed(() => {
|
||||||
|
|
||||||
/** 点 tab 落筛选;同 kind 重复点击 → 当 toggle 关掉(避免迷惑) */
|
/** 点 tab 落筛选;同 kind 重复点击 → 当 toggle 关掉(避免迷惑) */
|
||||||
function setFilter(filter: ActiveFilter) {
|
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
|
activeFilter.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -552,11 +569,7 @@ async function loadEarlier() {
|
||||||
}
|
}
|
||||||
// 4. 合并到 conversationStore:prependMessages 内部去重 + 升序合并 + 落 IndexedDB;
|
// 4. 合并到 conversationStore:prependMessages 内部去重 + 升序合并 + 落 IndexedDB;
|
||||||
// 主聊天面板的 messages 是同一份引用,老消息也会一起出现在主面板里(符合预期)
|
// 主聊天面板的 messages 是同一份引用,老消息也会一起出现在主面板里(符合预期)
|
||||||
conversationStore.prependMessages(
|
conversationStore.prependMessages(conversation.value.type, conversation.value.targetId, earlier)
|
||||||
conversation.value.type,
|
|
||||||
conversation.value.targetId,
|
|
||||||
earlier
|
|
||||||
)
|
|
||||||
} finally {
|
} finally {
|
||||||
loadingMore.value = false
|
loadingMore.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -599,9 +612,7 @@ function getAvatar(message: Message): string {
|
||||||
}
|
}
|
||||||
if (isGroup.value) {
|
if (isGroup.value) {
|
||||||
const group = groupStore.getGroup(conversation.value.targetId)
|
const group = groupStore.getGroup(conversation.value.targetId)
|
||||||
return (
|
return group?.members?.find((member) => member.userId === message.senderId)?.avatar || ''
|
||||||
group?.members?.find((member) => member.userId === message.senderId)?.avatar || ''
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return conversation.value.avatar || ''
|
return conversation.value.avatar || ''
|
||||||
}
|
}
|
||||||
|
|
@ -650,7 +661,6 @@ function locateMessage(messageId: number) {
|
||||||
emit('locate', messageId)
|
emit('locate', messageId)
|
||||||
visible.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,23 @@
|
||||||
v-if="isFriendChatTipMessage"
|
v-if="isFriendChatTipMessage"
|
||||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
{{ friendChatTipText }}
|
<TipSegments :segments="friendChatTipSegments" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群广播事件:跟好友事件同灰色样式,文案按 type 拼装 -->
|
<!-- 群广播事件:跟好友事件同灰色样式,mention 段挂点击弹 UserInfoCard -->
|
||||||
<div
|
<div
|
||||||
v-else-if="isGroupNotificationMessage"
|
v-else-if="isGroupNotificationMessage"
|
||||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
{{ groupNotificationText }}
|
<TipSegments :segments="groupNotificationSegments" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 撤回消息:整行展示灰色 tip 文案 -->
|
<!-- 撤回消息:整行灰色 tip,sender 名段可点击 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="isRecall"
|
v-else-if="isRecall"
|
||||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
{{ recallTip }}
|
<TipSegments :segments="recallTipSegments" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 普通消息气泡 -->
|
<!-- 普通消息气泡 -->
|
||||||
|
|
@ -149,6 +149,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
import Icon from '@/components/Icon/src/Icon.vue'
|
import Icon from '@/components/Icon/src/Icon.vue'
|
||||||
import { useMessage } from '@/hooks/web/useMessage'
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
|
@ -173,9 +174,11 @@ import {
|
||||||
buildQuoteFromMessage,
|
buildQuoteFromMessage,
|
||||||
extractAddableFace,
|
extractAddableFace,
|
||||||
getQuoteFromMessage,
|
getQuoteFromMessage,
|
||||||
type CardMessage
|
parseMessage,
|
||||||
|
type CardMessage,
|
||||||
|
type TextMessage
|
||||||
} from '@/views/im/utils/message'
|
} 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 { formatTimeTip } from '@/views/im/utils/time'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { useConversationStore } from '../../../../store/conversationStore'
|
import { useConversationStore } from '../../../../store/conversationStore'
|
||||||
|
|
@ -187,8 +190,8 @@ import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
getSenderRealNickname,
|
getSenderRealNickname,
|
||||||
resolveFriendNotificationText,
|
resolveFriendNotificationSegments,
|
||||||
resolveGroupNotificationText
|
resolveGroupNotificationSegments
|
||||||
} from '@/views/im/utils/user'
|
} from '@/views/im/utils/user'
|
||||||
import { useImUiStore } from '../../../../store/uiStore'
|
import { useImUiStore } from '../../../../store/uiStore'
|
||||||
import { useMessageSender } from '../../../../composables/useMessageSender'
|
import { useMessageSender } from '../../../../composables/useMessageSender'
|
||||||
|
|
@ -197,6 +200,7 @@ import { useMuteOverlay } from '../../../../composables/useMuteOverlay'
|
||||||
import type { Message } from '../../../../types'
|
import type { Message } from '../../../../types'
|
||||||
import MessageReadStatus from './MessageReadStatus.vue'
|
import MessageReadStatus from './MessageReadStatus.vue'
|
||||||
import ReplyPreview from './ReplyPreview.vue'
|
import ReplyPreview from './ReplyPreview.vue'
|
||||||
|
import TipSegments from './TipSegments.vue'
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
||||||
import MessageBubble from './MessageBubble.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 } from './forward/keys'
|
||||||
|
|
@ -234,6 +238,8 @@ const { uploadAndSendMedia } = useMediaUploader()
|
||||||
const muteOverlay = useMuteOverlay()
|
const muteOverlay = useMuteOverlay()
|
||||||
// 仅用 confirm,避免 message 跟 props.message 同名冲突(vue/no-dupe-keys)
|
// 仅用 confirm,避免 message 跟 props.message 同名冲突(vue/no-dupe-keys)
|
||||||
const { confirm: confirmDialog, success: successMessage } = useMessage()
|
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 */
|
/** 是否已撤回:pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL,渲染只需识别 type */
|
||||||
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
|
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
|
||||||
|
|
||||||
/** 撤回提示文案:buildRecallTip 实时算 sender 名(按 conversation 上下文走 WeChat 优先级) */
|
/** 撤回提示 segments:依赖 activeConversation 实时算 sender 名 */
|
||||||
const recallTip = computed(() => {
|
const recallTipSegments = computed(() => {
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = conversationStore.activeConversation
|
||||||
return buildRecallTip(
|
return buildRecallTipSegments(
|
||||||
props.message.senderId,
|
props.message.senderId,
|
||||||
props.message.selfSend,
|
props.message.selfSend,
|
||||||
conversation?.type ?? 0,
|
conversation?.type ?? 0,
|
||||||
|
|
@ -273,14 +279,14 @@ const recallTip = computed(() => {
|
||||||
/** 是否会话内好友事件气泡(FRIEND_ADD / FRIEND_DELETE) */
|
/** 是否会话内好友事件气泡(FRIEND_ADD / FRIEND_DELETE) */
|
||||||
const isFriendChatTipMessage = computed(() => isFriendChatTip(props.message.type))
|
const isFriendChatTipMessage = computed(() => isFriendChatTip(props.message.type))
|
||||||
|
|
||||||
/** 好友事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示,文案固定 */
|
/** 好友事件 segments */
|
||||||
const friendChatTipText = computed(() => resolveFriendNotificationText(props.message))
|
const friendChatTipSegments = computed(() => resolveFriendNotificationSegments(props.message))
|
||||||
|
|
||||||
/** 是否群广播事件(GROUP_CREATE..GROUP_BANNED 段位,排除 GROUP_MEMBER_SETTING_UPDATE 个人信号) */
|
/** 是否群广播事件(GROUP_CREATE..GROUP_BANNED 段位,排除 GROUP_MEMBER_SETTING_UPDATE 个人信号) */
|
||||||
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
|
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
|
||||||
|
|
||||||
/** 群广播事件文案:按 type 拼装(成员加入 / 退群 / 公告变更等) */
|
/** 群广播事件 segments */
|
||||||
const groupNotificationText = computed(() => resolveGroupNotificationText(props.message))
|
const groupNotificationSegments = computed(() => resolveGroupNotificationSegments(props.message))
|
||||||
|
|
||||||
// ==================== 消息内容解析 / payload ====================
|
// ==================== 消息内容解析 / payload ====================
|
||||||
|
|
||||||
|
|
@ -446,6 +452,7 @@ const isAtMe = computed(() => {
|
||||||
|
|
||||||
/** 右键菜单 key 常量;push 端和分发端从同一处取,typo 编译期就能抓 */
|
/** 右键菜单 key 常量;push 端和分发端从同一处取,typo 编译期就能抓 */
|
||||||
const MENU_KEYS = {
|
const MENU_KEYS = {
|
||||||
|
COPY: 'COPY',
|
||||||
REPLY: 'REPLY',
|
REPLY: 'REPLY',
|
||||||
FORWARD: 'FORWARD',
|
FORWARD: 'FORWARD',
|
||||||
MULTI_SELECT: 'MULTI_SELECT',
|
MULTI_SELECT: 'MULTI_SELECT',
|
||||||
|
|
@ -489,6 +496,14 @@ async function handleContextMenu(e: MouseEvent) {
|
||||||
danger?: boolean
|
danger?: boolean
|
||||||
icon?: string
|
icon?: string
|
||||||
}> = []
|
}> = []
|
||||||
|
// 「复制」:仅文本消息支持;放在第一项,对齐微信桌面右键习惯
|
||||||
|
if (props.message.type === ImMessageType.TEXT) {
|
||||||
|
items.push({
|
||||||
|
key: MENU_KEYS.COPY,
|
||||||
|
name: '复制',
|
||||||
|
icon: 'ant-design:copy-outlined'
|
||||||
|
})
|
||||||
|
}
|
||||||
// 「引用」:已落库(id≠0)+ 未撤回 + 非合并转发;MERGE 内嵌快照在引用预览里无法降级展示
|
// 「引用」:已落库(id≠0)+ 未撤回 + 非合并转发;MERGE 内嵌快照在引用预览里无法降级展示
|
||||||
if (!!props.message.id && !isRecall.value && !isMerge.value) {
|
if (!!props.message.id && !isRecall.value && !isMerge.value) {
|
||||||
items.push({
|
items.push({
|
||||||
|
|
@ -584,6 +599,7 @@ async function handleContextMenu(e: MouseEvent) {
|
||||||
|
|
||||||
// 把菜单渲染交给全局 uiStore(单例,避免每条消息都挂一份菜单 DOM)
|
// 把菜单渲染交给全局 uiStore(单例,避免每条消息都挂一份菜单 DOM)
|
||||||
const menuHandlers: Record<MenuKey, () => void | Promise<void>> = {
|
const menuHandlers: Record<MenuKey, () => void | Promise<void>> = {
|
||||||
|
[MENU_KEYS.COPY]: handleCopy,
|
||||||
[MENU_KEYS.REPLY]: handleReply,
|
[MENU_KEYS.REPLY]: handleReply,
|
||||||
[MENU_KEYS.FORWARD]: handleForward,
|
[MENU_KEYS.FORWARD]: handleForward,
|
||||||
[MENU_KEYS.MULTI_SELECT]: handleEnterMultiSelect,
|
[MENU_KEYS.MULTI_SELECT]: handleEnterMultiSelect,
|
||||||
|
|
@ -707,6 +723,16 @@ async function handlePin() {
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 复制文本消息:解出 content 字段写入剪贴板,提示「内容已复制到剪贴板」 */
|
||||||
|
async function handleCopy() {
|
||||||
|
const text = parseMessage<TextMessage>(props.message.content)?.content
|
||||||
|
if (!text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await copyToClipboard(text)
|
||||||
|
successMessage('内容已复制到剪贴板')
|
||||||
|
}
|
||||||
|
|
||||||
/** 进入引用模式:把当前消息构造成 QuoteMessage 写入 draftStore,MessageInput 顶部引用条响应式出现 */
|
/** 进入引用模式:把当前消息构造成 QuoteMessage 写入 draftStore,MessageInput 顶部引用条响应式出现 */
|
||||||
function handleReply() {
|
function handleReply() {
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = conversationStore.activeConversation
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -10,10 +10,14 @@ import { ImMessageType, isFriendChatTip, isGroupNotification } from './constants
|
||||||
import {
|
import {
|
||||||
getCardLabelInfo,
|
getCardLabelInfo,
|
||||||
parseMessage,
|
parseMessage,
|
||||||
|
segmentsToText,
|
||||||
|
tipMention,
|
||||||
|
tipText,
|
||||||
type CardMessage,
|
type CardMessage,
|
||||||
type FaceMessage,
|
type FaceMessage,
|
||||||
type FileMessage,
|
type FileMessage,
|
||||||
type TextMessage
|
type TextMessage,
|
||||||
|
type TipSegment
|
||||||
} from './message'
|
} from './message'
|
||||||
import {
|
import {
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
|
|
@ -47,6 +51,36 @@ export function buildFacePreviewText(facePayload: { name?: string } | null | und
|
||||||
return facePayload?.name ? `[表情] ${facePayload.name}` : '[表情]'
|
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 兜底) */
|
/** 撤回提示文案:自己撤回固定文案,对方撤回带 sender 名(实时算 + fallbackName 兜底) */
|
||||||
export function buildRecallTip(
|
export function buildRecallTip(
|
||||||
senderId: number,
|
senderId: number,
|
||||||
|
|
@ -55,16 +89,9 @@ export function buildRecallTip(
|
||||||
conversationTargetId: number,
|
conversationTargetId: number,
|
||||||
fallbackName?: string
|
fallbackName?: string
|
||||||
): string {
|
): string {
|
||||||
if (selfSend) {
|
return segmentsToText(
|
||||||
return '你撤回了一条消息'
|
buildRecallTipSegments(senderId, selfSend, conversationType, conversationTargetId, fallbackName)
|
||||||
}
|
|
||||||
const senderDisplayName = getSenderDisplayName(
|
|
||||||
senderId,
|
|
||||||
conversationType,
|
|
||||||
conversationTargetId,
|
|
||||||
fallbackName
|
|
||||||
)
|
)
|
||||||
return `${senderDisplayName || '对方'} 撤回了一条消息`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,41 @@ export const generateClientMessageId = (): string => {
|
||||||
return generateUUID()
|
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) */
|
/** 引用消息 payload(对齐后端 QuoteMessage) */
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,16 @@
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { SystemUserSexEnum } from '@/utils/constants'
|
||||||
import { ImConversationType, ImMessageType } from './constants'
|
import { ImConversationType, ImMessageType } from './constants'
|
||||||
import { getCurrentUserId } from './storage'
|
import { getCurrentUserId } from './storage'
|
||||||
|
import {
|
||||||
|
joinMentionSegments,
|
||||||
|
segmentsToText,
|
||||||
|
tipMention,
|
||||||
|
tipText,
|
||||||
|
type TipSegment
|
||||||
|
} from './message'
|
||||||
import { useFriendStore } from '../home/store/friendStore'
|
import { useFriendStore } from '../home/store/friendStore'
|
||||||
import { useGroupStore } from '../home/store/groupStore'
|
import { useGroupStore } from '../home/store/groupStore'
|
||||||
import type { Friend, Group } from '../home/types'
|
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(
|
export function resolveGroupNotificationText(
|
||||||
message: { type?: number; content?: string; targetId?: number },
|
message: { type?: number; content?: string; targetId?: number },
|
||||||
resolveName?: (userId: number) => string,
|
resolveName?: (userId: number) => string,
|
||||||
operatorNameOverride?: string
|
operatorNameOverride?: string
|
||||||
): string {
|
): string {
|
||||||
let payload: GroupNotificationPayload = {}
|
return segmentsToText(
|
||||||
try {
|
resolveGroupNotificationSegments(message, resolveName, operatorNameOverride)
|
||||||
payload = JSON.parse(message.content || '{}')
|
)
|
||||||
} catch {
|
}
|
||||||
return ''
|
|
||||||
}
|
/** 会话内好友事件 segments */
|
||||||
const resolve =
|
export function resolveFriendNotificationSegments(message: {
|
||||||
resolveName ||
|
type?: number
|
||||||
((id: number) => getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0))
|
}): TipSegment[] {
|
||||||
const operatorName = payload.operatorUserId
|
|
||||||
? (operatorNameOverride ?? resolve(payload.operatorUserId))
|
|
||||||
: ''
|
|
||||||
const memberNames = (payload.memberUserIds || []).map(resolve).join('、')
|
|
||||||
const newOwnerName = payload.newOwnerUserId ? resolve(payload.newOwnerUserId) : ''
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case ImMessageType.GROUP_CREATE:
|
case ImMessageType.FRIEND_ADD:
|
||||||
return `${operatorName} 创建了群聊`
|
return [tipText('你们已经是好友了,开始聊天吧')]
|
||||||
case ImMessageType.GROUP_NAME_UPDATE:
|
case ImMessageType.FRIEND_DELETE:
|
||||||
return `${operatorName} 将群名修改为 "${payload.newName ?? ''}"`
|
return [tipText('你已删除好友')]
|
||||||
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} 解封了该群`
|
|
||||||
default:
|
default:
|
||||||
return ''
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 会话内好友事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡,文案固定不依赖 payload */
|
/** 会话内好友事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡,文案固定不依赖 payload */
|
||||||
export function resolveFriendNotificationText(message: { type?: number }): string {
|
export function resolveFriendNotificationText(message: { type?: number }): string {
|
||||||
switch (message.type) {
|
return segmentsToText(resolveFriendNotificationSegments(message))
|
||||||
case ImMessageType.FRIEND_ADD:
|
|
||||||
return '你们已经是好友了,开始聊天吧'
|
|
||||||
case ImMessageType.FRIEND_DELETE:
|
|
||||||
return '你已删除好友'
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 性别图标:男 1 / 女 2,0 / null / undefined 一律不展示,对齐微信留白 */
|
/** 性别图标;UNKNOWN / null / undefined 一律不展示,对齐微信留白 */
|
||||||
export function getGenderIcon(sex?: number): string {
|
export function getGenderIcon(sex?: number): string {
|
||||||
if (sex === 1) {
|
if (sex === SystemUserSexEnum.MALE) {
|
||||||
return 'mdi:human-male'
|
return 'mdi:human-male'
|
||||||
}
|
}
|
||||||
if (sex === 2) {
|
if (sex === SystemUserSexEnum.FEMALE) {
|
||||||
return 'mdi:human-female'
|
return 'mdi:human-female'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
|
|
@ -323,10 +379,10 @@ export function getGenderIcon(sex?: number): string {
|
||||||
|
|
||||||
/** 性别图标主题色:男蓝、女粉 */
|
/** 性别图标主题色:男蓝、女粉 */
|
||||||
export function getGenderColor(sex?: number): string {
|
export function getGenderColor(sex?: number): string {
|
||||||
if (sex === 1) {
|
if (sex === SystemUserSexEnum.MALE) {
|
||||||
return '#5b97f5'
|
return '#5b97f5'
|
||||||
}
|
}
|
||||||
if (sex === 2) {
|
if (sex === SystemUserSexEnum.FEMALE) {
|
||||||
return '#f56c92'
|
return '#f56c92'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue