✨ feat(im): 优化名片消息类型 v0.1:补充缺失的名片展示
parent
f3de29f95f
commit
3836467481
|
|
@ -18,7 +18,7 @@ import {
|
||||||
type TextMessage
|
type TextMessage
|
||||||
} from '../../utils/message'
|
} from '../../utils/message'
|
||||||
import { ImMessageType, ImMessageStatus, ImConversationType } from '../../utils/constants'
|
import { ImMessageType, ImMessageStatus, ImConversationType } from '../../utils/constants'
|
||||||
import type { Message } from '../types'
|
import type { Conversation, Message } from '../types'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
/** 非文本消息的扩展选项(通用) */
|
/** 非文本消息的扩展选项(通用) */
|
||||||
|
|
@ -26,6 +26,13 @@ interface SendExtOptions {
|
||||||
atUserIds?: number[] // 群聊 @ 的用户编号列表
|
atUserIds?: number[] // 群聊 @ 的用户编号列表
|
||||||
receipt?: boolean // 是否需要群回执(默认 false)
|
receipt?: boolean // 是否需要群回执(默认 false)
|
||||||
targetId?: number // 覆盖默认的 targetId
|
targetId?: number // 覆盖默认的 targetId
|
||||||
|
/**
|
||||||
|
* 显式指定目标会话(转发 / 名片推荐场景)
|
||||||
|
*
|
||||||
|
* 不传时默认取 conversationStore.activeConversation;传入时按本值发送 + 乐观更新到对应会话,
|
||||||
|
* 不要求该会话当前是激活状态(适合发给「非当前会话」的多个目标)
|
||||||
|
*/
|
||||||
|
conversation?: Conversation
|
||||||
/** 被引用消息(可选):写进 content.quote 用于乐观渲染,服务端按 quote.messageId 反查重算覆盖 */
|
/** 被引用消息(可选):写进 content.quote 用于乐观渲染,服务端按 quote.messageId 反查重算覆盖 */
|
||||||
quote?: QuoteMessage
|
quote?: QuoteMessage
|
||||||
/**
|
/**
|
||||||
|
|
@ -78,8 +85,8 @@ export const useMessageSender = () => {
|
||||||
* 2. type / content 由调用方构造
|
* 2. type / content 由调用方构造
|
||||||
*/
|
*/
|
||||||
const sendRaw = async (type: number, content: string, options?: SendExtOptions) => {
|
const sendRaw = async (type: number, content: string, options?: SendExtOptions) => {
|
||||||
// 1. 参数校验:必须有激活会话和目标 id
|
// 1. 参数校验:优先用显式传入的 conversation(转发场景),否则取激活会话
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = options?.conversation ?? conversationStore.activeConversation
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,15 @@
|
||||||
[视频]
|
[视频]
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 名片:人形 icon + 「个人名片:昵称」 -->
|
||||||
|
<div
|
||||||
|
v-else-if="message.type === ImMessageType.CARD"
|
||||||
|
class="inline-flex gap-1.5 items-center text-sm text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:user-outlined" :size="14" />
|
||||||
|
<span>个人名片:{{ cardOf(message)?.nickname || '' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 撤回 -->
|
<!-- 撤回 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="message.type === ImMessageType.RECALL"
|
v-else-if="message.type === ImMessageType.RECALL"
|
||||||
|
|
@ -329,7 +338,8 @@ import {
|
||||||
type TextMessage,
|
type TextMessage,
|
||||||
type ImageMessage,
|
type ImageMessage,
|
||||||
type FileMessage,
|
type FileMessage,
|
||||||
type AudioMessage
|
type AudioMessage,
|
||||||
|
type CardMessage
|
||||||
} from '@/views/im/utils/message'
|
} from '@/views/im/utils/message'
|
||||||
import type { Message } from '@/views/im/home/types'
|
import type { Message } from '@/views/im/home/types'
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
||||||
|
|
@ -666,6 +676,9 @@ function fileOf(message: Message): FileMessage | null {
|
||||||
function audioOf(message: Message): AudioMessage | null {
|
function audioOf(message: Message): AudioMessage | null {
|
||||||
return parseMessage<AudioMessage>(message.content)
|
return parseMessage<AudioMessage>(message.content)
|
||||||
}
|
}
|
||||||
|
function cardOf(message: Message): CardMessage | null {
|
||||||
|
return parseMessage<CardMessage>(message.content)
|
||||||
|
}
|
||||||
|
|
||||||
/** 关键字命中文本:文本类返回原文、文件返回文件名(利于按文件名搜)、其他返回占位词 */
|
/** 关键字命中文本:文本类返回原文、文件返回文件名(利于按文件名搜)、其他返回占位词 */
|
||||||
function textSnippetOf(message: Message): string {
|
function textSnippetOf(message: Message): string {
|
||||||
|
|
@ -683,6 +696,8 @@ function textSnippetOf(message: Message): string {
|
||||||
return '[语音]'
|
return '[语音]'
|
||||||
case ImMessageType.VIDEO:
|
case ImMessageType.VIDEO:
|
||||||
return '[视频]'
|
return '[视频]'
|
||||||
|
case ImMessageType.CARD:
|
||||||
|
return `[个人名片] ${parseMessage<CardMessage>(message.content)?.nickname ?? ''}`
|
||||||
case ImMessageType.RECALL:
|
case ImMessageType.RECALL:
|
||||||
return recallTipOf(message)
|
return recallTipOf(message)
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 名片:人形 icon + 「个人名片:昵称」 -->
|
||||||
|
<template v-else-if="isCard">
|
||||||
|
<Icon icon="ant-design:user-outlined" :size="14" class="flex-shrink-0" />
|
||||||
|
<span class="im-reply-preview__text min-w-0">
|
||||||
|
个人名片:{{ parsedPayload?.nickname || '' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 图片 / 视频缩略图 -->
|
<!-- 图片 / 视频缩略图 -->
|
||||||
<img
|
<img
|
||||||
v-if="thumbnailUrl"
|
v-if="thumbnailUrl"
|
||||||
|
|
@ -90,6 +98,7 @@ import {
|
||||||
parseMessage,
|
parseMessage,
|
||||||
getFileIconInfo,
|
getFileIconInfo,
|
||||||
type AudioMessage,
|
type AudioMessage,
|
||||||
|
type CardMessage,
|
||||||
type FileMessage,
|
type FileMessage,
|
||||||
type ImageMessage,
|
type ImageMessage,
|
||||||
type TextMessage,
|
type TextMessage,
|
||||||
|
|
@ -149,13 +158,14 @@ const senderName = computed(() => {
|
||||||
|
|
||||||
/** quote.content 解析一次缓存,让多个 computed 复用,长会话每条引用气泡少一次 JSON.parse */
|
/** quote.content 解析一次缓存,让多个 computed 复用,长会话每条引用气泡少一次 JSON.parse */
|
||||||
type AnyQuotePayload = Partial<
|
type AnyQuotePayload = Partial<
|
||||||
TextMessage & ImageMessage & FileMessage & AudioMessage & VideoMessage
|
TextMessage & ImageMessage & FileMessage & AudioMessage & VideoMessage & CardMessage
|
||||||
>
|
>
|
||||||
const parsedPayload = computed(() => parseMessage<AnyQuotePayload>(props.quote.content))
|
const parsedPayload = computed(() => parseMessage<AnyQuotePayload>(props.quote.content))
|
||||||
|
|
||||||
const isText = computed(() => props.quote.type === ImMessageType.TEXT)
|
const isText = computed(() => props.quote.type === ImMessageType.TEXT)
|
||||||
const isFile = computed(() => props.quote.type === ImMessageType.FILE)
|
const isFile = computed(() => props.quote.type === ImMessageType.FILE)
|
||||||
const isVoice = computed(() => props.quote.type === ImMessageType.VOICE)
|
const isVoice = computed(() => props.quote.type === ImMessageType.VOICE)
|
||||||
|
const isCard = computed(() => props.quote.type === ImMessageType.CARD)
|
||||||
|
|
||||||
/** 文本超过 MAX_TEXT_PREVIEW_LEN 截断,长内容不撑爆引用块 */
|
/** 文本超过 MAX_TEXT_PREVIEW_LEN 截断,长内容不撑爆引用块 */
|
||||||
const textPreview = computed(() => {
|
const textPreview = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,12 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- 名片:人形 icon + 昵称(管理后台单行预览,不渲染头像图片) -->
|
||||||
|
<span v-else-if="isCard && cardPayload" class="inline-flex gap-1.5 items-center">
|
||||||
|
<Icon icon="ant-design:user-outlined" :size="16" color="#606266" />
|
||||||
|
<span>个人名片:{{ cardPayload.nickname }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- 控制类消息:撤回 / 已读 / 回执 -->
|
<!-- 控制类消息:撤回 / 已读 / 回执 -->
|
||||||
<span
|
<span
|
||||||
v-else-if="props.type === ImMessageType.RECALL"
|
v-else-if="props.type === ImMessageType.RECALL"
|
||||||
|
|
@ -112,7 +118,8 @@ import {
|
||||||
type FileMessage,
|
type FileMessage,
|
||||||
type AudioMessage,
|
type AudioMessage,
|
||||||
type VideoMessage,
|
type VideoMessage,
|
||||||
type TextMessage
|
type TextMessage,
|
||||||
|
type CardMessage
|
||||||
} from '@/views/im/utils/message'
|
} from '@/views/im/utils/message'
|
||||||
import {
|
import {
|
||||||
resolveFriendNotificationText,
|
resolveFriendNotificationText,
|
||||||
|
|
@ -136,6 +143,7 @@ const isImage = computed(() => props.type === ImMessageType.IMAGE)
|
||||||
const isFile = computed(() => props.type === ImMessageType.FILE)
|
const isFile = computed(() => props.type === ImMessageType.FILE)
|
||||||
const isVoice = computed(() => props.type === ImMessageType.VOICE)
|
const isVoice = computed(() => props.type === ImMessageType.VOICE)
|
||||||
const isVideo = computed(() => props.type === ImMessageType.VIDEO)
|
const isVideo = computed(() => props.type === ImMessageType.VIDEO)
|
||||||
|
const isCard = computed(() => props.type === ImMessageType.CARD)
|
||||||
|
|
||||||
/** 文本内容:从 TextMessage payload 取 .content */
|
/** 文本内容:从 TextMessage payload 取 .content */
|
||||||
const textContent = computed(
|
const textContent = computed(
|
||||||
|
|
@ -154,6 +162,9 @@ const voicePayload = computed(() =>
|
||||||
const videoPayload = computed(() =>
|
const videoPayload = computed(() =>
|
||||||
isVideo.value ? parseMessage<VideoMessage>(props.content || '') : null
|
isVideo.value ? parseMessage<VideoMessage>(props.content || '') : null
|
||||||
)
|
)
|
||||||
|
const cardPayload = computed(() =>
|
||||||
|
isCard.value ? parseMessage<CardMessage>(props.content || '') : null
|
||||||
|
)
|
||||||
|
|
||||||
/** 点击视频封面:在新标签打开视频 url(不在管理后台内嵌播放,避免列表里多个 video 同时占资源) */
|
/** 点击视频封面:在新标签打开视频 url(不在管理后台内嵌播放,避免列表里多个 video 同时占资源) */
|
||||||
function openVideo() {
|
function openVideo() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue