From f3de29f95f29de74e7f524be866b3f2b753844f3 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 6 May 2026 08:00:36 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=90=8D=E7=89=87=E6=B6=88=E6=81=AF=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/user/RecommendCardDialog.vue | 340 ++++++++++++++++++ .../im/home/components/user/UserInfo.vue | 33 +- src/views/im/utils/constants.ts | 17 +- src/views/im/utils/conversation.ts | 2 + src/views/im/utils/message.ts | 26 ++ 5 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 src/views/im/home/components/user/RecommendCardDialog.vue diff --git a/src/views/im/home/components/user/RecommendCardDialog.vue b/src/views/im/home/components/user/RecommendCardDialog.vue new file mode 100644 index 000000000..17d8c7110 --- /dev/null +++ b/src/views/im/home/components/user/RecommendCardDialog.vue @@ -0,0 +1,340 @@ + + + diff --git a/src/views/im/home/components/user/UserInfo.vue b/src/views/im/home/components/user/UserInfo.vue index f4b11f24a..d4a40721a 100644 --- a/src/views/im/home/components/user/UserInfo.vue +++ b/src/views/im/home/components/user/UserInfo.vue @@ -49,9 +49,13 @@ @@ -178,6 +191,7 @@ import { useMessage } from '@/hooks/web/useMessage' import UserAvatar from './UserAvatar.vue' import FriendAddDialog from '../friend/FriendAddDialog.vue' +import RecommendCardDialog from './RecommendCardDialog.vue' import { getSimpleUser, type UserVO } from '@/api/system/user' import { useFriendStore } from '../../store/friendStore' import { ImFriendAddSource } from '../../../utils/constants' @@ -337,6 +351,15 @@ function handleComingSoon(featureName: string) { const addFriendVisible = ref(false) const presetUserForAdd = ref(null) +/** 把他推荐给朋友:弹 RecommendCardDialog 选目标会话 */ +const recommendVisible = ref(false) // 推荐名片弹窗显隐:「把他推荐给朋友」入口控制 +function handleRecommend() { + if (!props.user?.id) { + return + } + recommendVisible.value = true +} + /** 加为好友:弹 FriendAddDialog(带预填用户),让用户填申请理由 + 备注后再发申请 */ function handleAddFriend() { if (!props.user?.id) { diff --git a/src/views/im/utils/constants.ts b/src/views/im/utils/constants.ts index 334eee47c..1f1d8a667 100644 --- a/src/views/im/utils/constants.ts +++ b/src/views/im/utils/constants.ts @@ -6,6 +6,7 @@ export const ImMessageType = { VOICE: 103, // 语音(对应 OpenIM Sound=103) VIDEO: 104, // 视频(对应 OpenIM Video=104) FILE: 105, // 文件(对应 OpenIM File=105) + CARD: 108, // 名片(对应 OpenIM Card=108) // ========== 信号类(2101 / 2200 直接复用 OpenIM 段位编号;2201 自有扩展) ========== RECALL: 2101, // 撤回(对应 OpenIM RevokeNotification=2101) RECEIPT: 2200, // 回执(对应 OpenIM HasReadReceipt=2200) @@ -74,7 +75,8 @@ const ImMessageTypeNormals: number[] = [ ImMessageType.IMAGE, ImMessageType.FILE, ImMessageType.VOICE, - ImMessageType.VIDEO + ImMessageType.VIDEO, + ImMessageType.CARD ] /** 判断是否"普通消息" */ @@ -82,6 +84,19 @@ export function isNormalMessage(type: number): boolean { return ImMessageTypeNormals.includes(type) } +/** IM 媒体消息类型集合:发送依赖本地 File 上传,刷新后 _localFile 丢失即不可恢复 */ +const ImMessageTypeMedia: number[] = [ + ImMessageType.IMAGE, + ImMessageType.FILE, + ImMessageType.VOICE, + ImMessageType.VIDEO +] + +/** 判断是否「媒体消息」:图片 / 文件 / 语音 / 视频 */ +export function isMediaMessageType(type: number): boolean { + return ImMessageTypeMedia.includes(type) +} + /** * IM 消息状态枚举(对齐后端 ImMessageStatusEnum,前端扩展 SENDING + FAILED) * diff --git a/src/views/im/utils/conversation.ts b/src/views/im/utils/conversation.ts index 32a9712ea..74ef70432 100644 --- a/src/views/im/utils/conversation.ts +++ b/src/views/im/utils/conversation.ts @@ -56,6 +56,8 @@ export function resolveConversationLastContent( return '[语音]' case ImMessageType.VIDEO: return '[视频]' + case ImMessageType.CARD: + return '[个人名片]' case ImMessageType.RECALL: return buildRecallTip( message.senderId, diff --git a/src/views/im/utils/message.ts b/src/views/im/utils/message.ts index cdd22f439..b161270b8 100644 --- a/src/views/im/utils/message.ts +++ b/src/views/im/utils/message.ts @@ -83,6 +83,16 @@ export interface VideoMessage extends Quotable { size?: number } +/** 名片消息 payload(对齐后端 CardMessage) */ +export interface CardMessage extends Quotable { + /** 名片用户编号 */ + userId: number + /** 名片用户昵称(取真实昵称,非备注) */ + nickname: string + /** 名片用户头像 */ + avatar?: string +} + /** 解析消息 content(JSON 字符串)为指定 payload,非法 JSON 返回 null */ export const parseMessage = (content: string): T | null => { try { @@ -95,6 +105,22 @@ export const parseMessage = (content: string): T | null => { /** 序列化消息 payload 为 content JSON 字符串;与 parseMessage 对称 */ export const serializeMessage = (payload: T): string => JSON.stringify(payload) +/** + * 释放 content 中所有 blob: URL 的内存映射 + * + * 媒体上传链路占位时用 URL.createObjectURL(file) 当临时 url 写进 content; + * ack / 重发 / 删除消息时调本函数把映射释放,避免 File 对象在浏览器内存里悬空(视频几百 MB 很伤) + * + * 仅对当前 document 内创建的 blob URL 有效;IndexedDB 恢复出来的旧 blob URL 已随旧 document 失效,调它无害但无意义 + */ +export const revokeBlobUrlsInContent = (content: string): void => { + if (!content || !content.includes('blob:')) { + return + } + const matches = content.match(/blob:[^"'\s)]+/g) + matches?.forEach((url) => URL.revokeObjectURL(url)) +} + // ==================== 引用消息 helper ==================== /** 把 quote 合进 payload(序列化前调用);quote 缺失时原样返回 */