From 38364674818adfafa9ae6a261a10f0ee0d3564bc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 6 May 2026 08:21:52 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=90=8D=E7=89=87=E6=B6=88=E6=81=AF=E7=B1=BB=E5=9E=8B=20v0.1?= =?UTF-8?q?=EF=BC=9A=E8=A1=A5=E5=85=85=E7=BC=BA=E5=A4=B1=E7=9A=84=E5=90=8D?= =?UTF-8?q?=E7=89=87=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../im/home/composables/useMessageSender.ts | 13 ++++++++++--- .../components/message/MessageHistory.vue | 17 ++++++++++++++++- .../components/message/ReplyPreview.vue | 12 +++++++++++- .../manager/message/MessageContentPreview.vue | 13 ++++++++++++- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/views/im/home/composables/useMessageSender.ts b/src/views/im/home/composables/useMessageSender.ts index 2b0981682..0fc019d46 100644 --- a/src/views/im/home/composables/useMessageSender.ts +++ b/src/views/im/home/composables/useMessageSender.ts @@ -18,7 +18,7 @@ import { type TextMessage } from '../../utils/message' import { ImMessageType, ImMessageStatus, ImConversationType } from '../../utils/constants' -import type { Message } from '../types' +import type { Conversation, Message } from '../types' import { useUserStore } from '@/store/modules/user' /** 非文本消息的扩展选项(通用) */ @@ -26,6 +26,13 @@ interface SendExtOptions { atUserIds?: number[] // 群聊 @ 的用户编号列表 receipt?: boolean // 是否需要群回执(默认 false) targetId?: number // 覆盖默认的 targetId + /** + * 显式指定目标会话(转发 / 名片推荐场景) + * + * 不传时默认取 conversationStore.activeConversation;传入时按本值发送 + 乐观更新到对应会话, + * 不要求该会话当前是激活状态(适合发给「非当前会话」的多个目标) + */ + conversation?: Conversation /** 被引用消息(可选):写进 content.quote 用于乐观渲染,服务端按 quote.messageId 反查重算覆盖 */ quote?: QuoteMessage /** @@ -78,8 +85,8 @@ export const useMessageSender = () => { * 2. type / content 由调用方构造 */ const sendRaw = async (type: number, content: string, options?: SendExtOptions) => { - // 1. 参数校验:必须有激活会话和目标 id - const conversation = conversationStore.activeConversation + // 1. 参数校验:优先用显式传入的 conversation(转发场景),否则取激活会话 + const conversation = options?.conversation ?? conversationStore.activeConversation if (!conversation) { return } diff --git a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue index c165dfa4c..dbd5a2ff2 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue @@ -252,6 +252,15 @@ [视频] + +
+ + 个人名片:{{ cardOf(message)?.nickname || '' }} +
+
(message.content) } +function cardOf(message: Message): CardMessage | null { + return parseMessage(message.content) +} /** 关键字命中文本:文本类返回原文、文件返回文件名(利于按文件名搜)、其他返回占位词 */ function textSnippetOf(message: Message): string { @@ -683,6 +696,8 @@ function textSnippetOf(message: Message): string { return '[语音]' case ImMessageType.VIDEO: return '[视频]' + case ImMessageType.CARD: + return `[个人名片] ${parseMessage(message.content)?.nickname ?? ''}` case ImMessageType.RECALL: return recallTipOf(message) default: diff --git a/src/views/im/home/pages/conversation/components/message/ReplyPreview.vue b/src/views/im/home/pages/conversation/components/message/ReplyPreview.vue index bbdfd484f..59c0a623d 100644 --- a/src/views/im/home/pages/conversation/components/message/ReplyPreview.vue +++ b/src/views/im/home/pages/conversation/components/message/ReplyPreview.vue @@ -57,6 +57,14 @@ + + + { /** quote.content 解析一次缓存,让多个 computed 复用,长会话每条引用气泡少一次 JSON.parse */ type AnyQuotePayload = Partial< - TextMessage & ImageMessage & FileMessage & AudioMessage & VideoMessage + TextMessage & ImageMessage & FileMessage & AudioMessage & VideoMessage & CardMessage > const parsedPayload = computed(() => parseMessage(props.quote.content)) const isText = computed(() => props.quote.type === ImMessageType.TEXT) const isFile = computed(() => props.quote.type === ImMessageType.FILE) const isVoice = computed(() => props.quote.type === ImMessageType.VOICE) +const isCard = computed(() => props.quote.type === ImMessageType.CARD) /** 文本超过 MAX_TEXT_PREVIEW_LEN 截断,长内容不撑爆引用块 */ const textPreview = computed(() => { diff --git a/src/views/im/manager/message/MessageContentPreview.vue b/src/views/im/manager/message/MessageContentPreview.vue index 4ee21d44d..c255f0680 100644 --- a/src/views/im/manager/message/MessageContentPreview.vue +++ b/src/views/im/manager/message/MessageContentPreview.vue @@ -56,6 +56,12 @@ + + + + 个人名片:{{ cardPayload.nickname }} + + props.type === ImMessageType.IMAGE) const isFile = computed(() => props.type === ImMessageType.FILE) const isVoice = computed(() => props.type === ImMessageType.VOICE) const isVideo = computed(() => props.type === ImMessageType.VIDEO) +const isCard = computed(() => props.type === ImMessageType.CARD) /** 文本内容:从 TextMessage payload 取 .content */ const textContent = computed( @@ -154,6 +162,9 @@ const voicePayload = computed(() => const videoPayload = computed(() => isVideo.value ? parseMessage(props.content || '') : null ) +const cardPayload = computed(() => + isCard.value ? parseMessage(props.content || '') : null +) /** 点击视频封面:在新标签打开视频 url(不在管理后台内嵌播放,避免列表里多个 video 同时占资源) */ function openVideo() {