From 5a4f8b4e2a72b8e245de3e0a6c706089bdb9e929 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 18 Jun 2026 08:59:19 -0700 Subject: [PATCH] =?UTF-8?q?fix(im):=20=E5=AF=B9=E9=BD=90=E7=BE=A4=E5=A4=87?= =?UTF-8?q?=E6=B3=A8=E5=B1=95=E7=A4=BA=E5=B9=B6=E4=BF=AE=E5=A4=8D=20IM=20?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=AE=A1=E7=90=86=E5=AD=97=E5=85=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 聊天端: - 群 API 类型补充 groupRemark 和 silent - 群列表同步时以接口返回的个人群设置为准,只保留成员缓存 - 会话名写入入口统一使用 getGroupDisplayName,避免群备注被原群名覆盖 - 聊天标题、转发、推荐名片、新建群入口同步群展示名逻辑 - 空群头像且成员未加载时异步预拉群成员,用于合成群头像 - 通讯录和合并消息详情补充滚动容器 - 消息历史日期选择改用 antd Calendar 卡片模式并修正样式 管理端: - IM 字典常量统一为 im_content_type、im_message_status、im_message_receipt_status - 私聊 / 群聊消息列表和详情页切换到统一内容类型、消息状态、回执状态字典 - 私聊消息 API 和详情页补充 receiptStatus - 统计消息类型分布改用内容类型字典 --- apps/web-antd/src/api/im/group/index.ts | 2 ++ .../api/im/manager/message/private/index.ts | 1 + .../components/user/recommend-card-dialog.vue | 4 +-- .../im/home/composables/useMessagePuller.ts | 4 +-- .../src/views/im/home/pages/contact/index.vue | 3 ++- .../conversation-private-side.vue | 4 +-- .../forward/message-forward-dialog.vue | 4 +-- .../forward/message-merge-detail-dialog.vue | 3 ++- .../components/message/message-history.vue | 25 +++++++++++------- .../components/message/message-panel.vue | 5 ++-- .../im/home/pages/conversation/index.vue | 3 ++- .../src/views/im/home/store/groupStore.ts | 26 ++++++++++++++++--- .../src/views/im/home/store/websocketStore.ts | 4 +-- .../src/views/im/manager/message/data.ts | 24 ++++++++++++----- .../manager/message/group/modules/detail.vue | 6 ++--- .../message/private/modules/detail.vue | 12 +++++++-- .../components/distribution-chart.vue | 2 +- packages/constants/src/dict-enum.ts | 7 +++-- 18 files changed, 93 insertions(+), 46 deletions(-) diff --git a/apps/web-antd/src/api/im/group/index.ts b/apps/web-antd/src/api/im/group/index.ts index ed726ba21..ca5e78d73 100644 --- a/apps/web-antd/src/api/im/group/index.ts +++ b/apps/web-antd/src/api/im/group/index.ts @@ -19,6 +19,8 @@ export namespace ImGroupApi { createTime?: string; // 创建时间 pinnedMessages?: ImGroupMessageApi.GroupMessageRespVO[]; // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空) joinStatus?: number; // 当前登录用户在该群的成员状态(参见 CommonStatusEnum:0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像 + groupRemark?: string; // 当前登录用户对该群的备注 + silent?: boolean; // 当前登录用户是否免打扰 } /** 群消息置顶 / 取消置顶 Request VO */ diff --git a/apps/web-antd/src/api/im/manager/message/private/index.ts b/apps/web-antd/src/api/im/manager/message/private/index.ts index 48b319233..f9ff6ca12 100644 --- a/apps/web-antd/src/api/im/manager/message/private/index.ts +++ b/apps/web-antd/src/api/im/manager/message/private/index.ts @@ -14,6 +14,7 @@ export namespace ImManagerPrivateMessageApi { type: number; content: string; status: number; + receiptStatus?: number; sendTime: Date; createTime: Date; } diff --git a/apps/web-antd/src/views/im/home/components/user/recommend-card-dialog.vue b/apps/web-antd/src/views/im/home/components/user/recommend-card-dialog.vue index 07ef6fb3a..cff2505f5 100644 --- a/apps/web-antd/src/views/im/home/components/user/recommend-card-dialog.vue +++ b/apps/web-antd/src/views/im/home/components/user/recommend-card-dialog.vue @@ -14,7 +14,7 @@ import { ImContentType, ImConversationType, isGroupConversation } from '../../.. import { getConversationKey } from '../../../utils/conversation' import { buildDefaultGroupName } from '../../../utils/group' import { type CardTarget, serializeMessage } from '../../../utils/message' -import { isGroupQuit } from '../../../utils/user' +import { getGroupDisplayName, isGroupQuit } from '../../../utils/user' import { useMessageSender } from '../../composables/useMessageSender' import { FacePicker } from '../../pages/conversation/components/input' import { useConversationStore } from '../../store/conversationStore' @@ -181,7 +181,7 @@ async function handleCreateGroupAndSend() { const newConversation: Conversation = { type: ImConversationType.GROUP, targetId: group.id, - name: group.name || name, + name: getGroupDisplayName(group) || name, avatar: group.avatar || '', unreadCount: 0, lastContent: '', diff --git a/apps/web-antd/src/views/im/home/composables/useMessagePuller.ts b/apps/web-antd/src/views/im/home/composables/useMessagePuller.ts index 59994b94c..50af50259 100644 --- a/apps/web-antd/src/views/im/home/composables/useMessagePuller.ts +++ b/apps/web-antd/src/views/im/home/composables/useMessagePuller.ts @@ -26,7 +26,7 @@ import { } from '../../utils/constants' import { generateClientMessageId, getPrivateMessagePeerId } from '../../utils/message' import { runMinIdPull } from '../../utils/pull' -import { getFriendDisplayName } from '../../utils/user' +import { getFriendDisplayName, getGroupDisplayName } from '../../utils/user' import { useConversationStore } from '../store/conversationStore' import { useFriendStore } from '../store/friendStore' import { useGroupRequestStore } from '../store/groupRequestStore' @@ -146,7 +146,7 @@ export const useMessagePuller = () => { return { type: ImConversationType.GROUP, targetId: message.groupId, - name: group?.name || String(message.groupId), + name: group ? getGroupDisplayName(group) : String(message.groupId), avatar: group?.avatar || '', silent: group?.silent } diff --git a/apps/web-antd/src/views/im/home/pages/contact/index.vue b/apps/web-antd/src/views/im/home/pages/contact/index.vue index 949e2082f..128be6500 100644 --- a/apps/web-antd/src/views/im/home/pages/contact/index.vue +++ b/apps/web-antd/src/views/im/home/pages/contact/index.vue @@ -241,7 +241,8 @@ function onRemarkSaved(displayName: string) { -
+ +
以下是 {{ currentPayload.messages.length }} 条消息
-
+ +
选择发送日期
- +
@@ -785,18 +789,19 @@ function locateMessage(messageId: number) { border-radius: 1px; } -/* :deep 穿透 el-calendar 子组件 DOM;默认偏大压一压让它能塞进 320 popover */ -.im-message-history__calendar :deep(.el-calendar) { - 40px: 36px; -} -.im-message-history__calendar :deep(.el-calendar__header) { +/* :deep 穿透 antd Calendar(fullscreen=false 卡片模式)子 DOM,压一压塞进 320 popover */ +.im-message-history__calendar :deep(.ant-picker-calendar-header) { padding: 4px 8px; } -.im-message-history__calendar :deep(.el-calendar-table) { +.im-message-history__calendar :deep(.ant-picker-content) { font-size: 12px; } -.im-message-history__calendar :deep(.el-calendar-day) { - height: 36px; - padding: 4px; +.im-message-history__calendar :deep(.ant-picker-cell) { + padding: 1px 0; +} +.im-message-history__calendar :deep(.ant-picker-cell .ant-picker-calendar-date) { + height: 28px; + margin: 0 2px; + padding: 2px 4px 0; } diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue index cd1abcdab..908de3a92 100644 --- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue +++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue @@ -13,7 +13,7 @@ import { getCurrentUserId } from '#/views/im/utils/auth' import { ImConversationType, ImRtcCallMediaType, ImRtcCallStatus } from '#/views/im/utils/constants' import { getClientConversationId } from '#/views/im/utils/db' import { resolveCallEndReasonText } from '#/views/im/utils/message' -import { getMemberDisplayName, isGroupQuit } from '#/views/im/utils/user' +import { getGroupDisplayName, getMemberDisplayName, isGroupQuit } from '#/views/im/utils/user' import { GroupMuteMemberDialog } from '../../../../components/group' import { @@ -185,10 +185,11 @@ const groupInfo = computed< } const group = groupStore.getGroup(conversation.targetId) const selfMember = group?.members?.find((member) => member.userId === getCurrentUserId()) + const showGroupName = group ? getGroupDisplayName(group) : conversation.name return { id: conversation.targetId, name: group?.name || conversation.name, - showGroupName: group?.name || conversation.name, + showGroupName, showImage: group?.avatar || conversation.avatar, notice: group?.notice, remarkNickName: selfMember?.displayUserName, diff --git a/apps/web-antd/src/views/im/home/pages/conversation/index.vue b/apps/web-antd/src/views/im/home/pages/conversation/index.vue index 9aa947c73..b18421510 100644 --- a/apps/web-antd/src/views/im/home/pages/conversation/index.vue +++ b/apps/web-antd/src/views/im/home/pages/conversation/index.vue @@ -10,6 +10,7 @@ import { Button, Dropdown, Input, Menu } from 'ant-design-vue' import { ImConversationType } from '../../../utils/constants' import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation' import { StorageKeys } from '../../../utils/db' +import { getGroupDisplayName } from '../../../utils/user' import { ResizableAside } from '../../components' import { FriendAddDialog } from '../../components/friend' import { GroupCreateDialog } from '../../components/group' @@ -124,7 +125,7 @@ function handleGroupCreated(groupId: number) { conversationStore.openConversation( groupId, ImConversationType.GROUP, - group.name, + getGroupDisplayName(group), group.avatar || '', { silent: !!group.silent } ) diff --git a/apps/web-antd/src/views/im/home/store/groupStore.ts b/apps/web-antd/src/views/im/home/store/groupStore.ts index 161d21c40..adacf614f 100644 --- a/apps/web-antd/src/views/im/home/store/groupStore.ts +++ b/apps/web-antd/src/views/im/home/store/groupStore.ts @@ -223,7 +223,7 @@ export const useGroupStore = defineStore('imGroupStore', { return } const fresh = (list || []).map((group) => convertGroup(group)) - // 合并而非全量替换:silent / groupRemark / 成员缓存这些字段不在 ImGroupApi.GroupRespVO 里,得从旧 group 保留 + // 合并而非全量替换:成员缓存只在成员列表接口维护,群个人设置以群列表接口为准 const groupMap = new Map(this.groups.map((group) => [group.id, group])) this.groups = fresh.map((group) => { const existing = groupMap.get(group.id) @@ -234,8 +234,6 @@ export const useGroupStore = defineStore('imGroupStore', { ...group, members: existing.members, memberCount: existing.memberCount ?? group.memberCount, - silent: existing.silent ?? group.silent, - groupRemark: existing.groupRemark, membersLoaded: existing.membersLoaded, membersExpired: existing.membersExpired } @@ -250,6 +248,24 @@ export const useGroupStore = defineStore('imGroupStore', { }) } this.saveGroupList() + this.preloadMembersForEmptyAvatarGroups() + }, + + /** 预加载空群头像的成员列表,供 GroupAvatar 异步合成群头像 */ + preloadMembersForEmptyAvatarGroups() { + for (const group of this.groups) { + if ( + group.avatar || + group.joinStatus === CommonStatusEnum.DISABLE || + (group.membersLoaded && !group.membersExpired && group.members?.length) + ) { + continue + } + const force = !!group.membersLoaded && !group.membersExpired && !group.members?.length + this.fetchGroupMemberList(group.id, force).catch((error) => { + console.warn('[IM groupStore] 预加载群头像成员失败', { groupId: group.id }, error) + }) + } }, /** 失效全部群成员缓存 */ @@ -908,7 +924,9 @@ function convertGroup(group: ImGroupApi.GroupRespVO): Group { mutedAll: group.mutedAll, banned: group.banned, joinApproval: group.joinApproval, - joinStatus: group.joinStatus + joinStatus: group.joinStatus, + groupRemark: group.groupRemark, + silent: group.silent } } diff --git a/apps/web-antd/src/views/im/home/store/websocketStore.ts b/apps/web-antd/src/views/im/home/store/websocketStore.ts index 03e4da9f8..682311ef3 100644 --- a/apps/web-antd/src/views/im/home/store/websocketStore.ts +++ b/apps/web-antd/src/views/im/home/store/websocketStore.ts @@ -45,7 +45,7 @@ import { playAudioTip, resolveCallEndReasonText } from '../../utils/message' -import { getFriendDisplayName } from '../../utils/user' +import { getFriendDisplayName, getGroupDisplayName } from '../../utils/user' import { useConversationStore } from './conversationStore' import { type FriendNotificationPayload, useFriendStore } from './friendStore' import { useGroupRequestStore } from './groupRequestStore' @@ -781,7 +781,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', { { type: ImConversationType.GROUP, targetId: websocketMessage.groupId, - name: group?.name || String(websocketMessage.groupId), + name: group ? getGroupDisplayName(group) : String(websocketMessage.groupId), avatar: group?.avatar || '', silent: group?.silent }, diff --git a/apps/web-antd/src/views/im/manager/message/data.ts b/apps/web-antd/src/views/im/manager/message/data.ts index 0129e7ff9..06ab5d01a 100644 --- a/apps/web-antd/src/views/im/manager/message/data.ts +++ b/apps/web-antd/src/views/im/manager/message/data.ts @@ -35,7 +35,7 @@ export function usePrivateGridFormSchema(): VbenFormSchema[] { component: 'Select', componentProps: { allowClear: true, - options: getDictOptions(DICT_TYPE.IM_MESSAGE_TYPE, 'number'), + options: getDictOptions(DICT_TYPE.IM_CONTENT_TYPE, 'number'), placeholder: '请选择内容类型', }, }, @@ -84,7 +84,7 @@ export function usePrivateGridColumns(showReadColumns: boolean): VxeTableGridOpt width: 100, cellRender: { name: 'CellDict', - props: { type: DICT_TYPE.IM_MESSAGE_TYPE }, + props: { type: DICT_TYPE.IM_CONTENT_TYPE }, }, }, { @@ -101,7 +101,17 @@ export function usePrivateGridColumns(showReadColumns: boolean): VxeTableGridOpt width: 100, cellRender: { name: 'CellDict', - props: { type: DICT_TYPE.IM_PRIVATE_MESSAGE_STATUS }, + props: { type: DICT_TYPE.IM_MESSAGE_STATUS }, + }, + }, + { + field: 'receiptStatus', + title: '回执', + width: 110, + cellRender: { + name: 'CellDict', + // 回执状态(私聊 / 群聊共用 im_message_receipt_status),与源端「回执」列对齐 + props: { type: DICT_TYPE.IM_MESSAGE_RECEIPT_STATUS }, }, }, ] @@ -143,7 +153,7 @@ export function useGroupGridFormSchema(): VbenFormSchema[] { component: 'Select', componentProps: { allowClear: true, - options: getDictOptions(DICT_TYPE.IM_MESSAGE_TYPE, 'number'), + options: getDictOptions(DICT_TYPE.IM_CONTENT_TYPE, 'number'), placeholder: '请选择内容类型', }, }, @@ -192,7 +202,7 @@ export function useGroupGridColumns(showReadColumns: boolean): VxeTableGridOptio width: 100, cellRender: { name: 'CellDict', - props: { type: DICT_TYPE.IM_MESSAGE_TYPE }, + props: { type: DICT_TYPE.IM_CONTENT_TYPE }, }, }, { @@ -214,7 +224,7 @@ export function useGroupGridColumns(showReadColumns: boolean): VxeTableGridOptio width: 100, cellRender: { name: 'CellDict', - props: { type: DICT_TYPE.IM_GROUP_MESSAGE_STATUS }, + props: { type: DICT_TYPE.IM_MESSAGE_STATUS }, }, }, { @@ -223,7 +233,7 @@ export function useGroupGridColumns(showReadColumns: boolean): VxeTableGridOptio width: 110, cellRender: { name: 'CellDict', - props: { type: DICT_TYPE.IM_GROUP_MESSAGE_RECEIPT_STATUS }, + props: { type: DICT_TYPE.IM_MESSAGE_RECEIPT_STATUS }, }, }, ] diff --git a/apps/web-antd/src/views/im/manager/message/group/modules/detail.vue b/apps/web-antd/src/views/im/manager/message/group/modules/detail.vue index 57490f346..2b4f01095 100644 --- a/apps/web-antd/src/views/im/manager/message/group/modules/detail.vue +++ b/apps/web-antd/src/views/im/manager/message/group/modules/detail.vue @@ -48,14 +48,14 @@ defineExpose({ open }); {{ formatUserLabel(detail.senderNickname, detail.senderId) }} - + - + diff --git a/apps/web-antd/src/views/im/manager/message/private/modules/detail.vue b/apps/web-antd/src/views/im/manager/message/private/modules/detail.vue index 486b9eaca..27e0a0495 100644 --- a/apps/web-antd/src/views/im/manager/message/private/modules/detail.vue +++ b/apps/web-antd/src/views/im/manager/message/private/modules/detail.vue @@ -13,6 +13,7 @@ import { formatJsonText, formatUserLabel, } from '#/views/im/manager/utils/format'; +import { MESSAGE_PRIVATE_READ_ENABLED } from '#/views/im/utils/config'; import { MessageContentPreview } from '../..'; @@ -44,10 +45,17 @@ defineExpose({ open }); {{ formatUserLabel(detail.receiverNickname, detail.receiverId) }} - + - + + + + + {{ formatDateTimeText(detail.sendTime) }} diff --git a/apps/web-antd/src/views/im/manager/statistics/components/distribution-chart.vue b/apps/web-antd/src/views/im/manager/statistics/components/distribution-chart.vue index b3f391f61..e95a230da 100644 --- a/apps/web-antd/src/views/im/manager/statistics/components/distribution-chart.vue +++ b/apps/web-antd/src/views/im/manager/statistics/components/distribution-chart.vue @@ -56,7 +56,7 @@ async function loadData() { const data = await getMessageTypeDistribution(); const items = data.map((item) => ({ name: - getDictObj(DICT_TYPE.IM_MESSAGE_TYPE, String(item.type))?.label || + getDictObj(DICT_TYPE.IM_CONTENT_TYPE, String(item.type))?.label || `未知(${item.type})`, value: item.value, })); diff --git a/packages/constants/src/dict-enum.ts b/packages/constants/src/dict-enum.ts index ae431d905..73e0095e3 100644 --- a/packages/constants/src/dict-enum.ts +++ b/packages/constants/src/dict-enum.ts @@ -178,17 +178,16 @@ const IOT_DICT = { /** ========== IM - 即时通讯模块 ========== */ const IM_DICT = { IM_CHANNEL_MATERIAL_TYPE: 'im_channel_material_type', // IM 频道素材类型 + IM_CONTENT_TYPE: 'im_content_type', // IM 消息内容类型 IM_FRIEND_ADD_SOURCE: 'im_friend_add_source', // IM 好友添加来源 IM_FRIEND_REQUEST_HANDLE_RESULT: 'im_friend_request_handle_result', // IM 好友申请处理结果 IM_FRIEND_STATUS: 'im_friend_status', // IM 好友状态 IM_GROUP_ADD_SOURCE: 'im_group_add_source', // IM 加群来源 IM_GROUP_MEMBER_ROLE: 'im_group_member_role', // IM 群成员角色 - IM_GROUP_MESSAGE_RECEIPT_STATUS: 'im_group_message_receipt_status', // IM 群消息回执状态 - IM_GROUP_MESSAGE_STATUS: 'im_group_message_status', // IM 群聊消息状态 IM_GROUP_REQUEST_HANDLE_RESULT: 'im_group_request_handle_result', // IM 加群申请处理结果 IM_GROUP_STATUS: 'im_group_status', // IM 群状态 - IM_MESSAGE_TYPE: 'im_message_type', // IM 消息类型 - IM_PRIVATE_MESSAGE_STATUS: 'im_private_message_status', // IM 私聊消息状态 + IM_MESSAGE_RECEIPT_STATUS: 'im_message_receipt_status', // IM 消息回执状态(私聊 / 群聊共用) + IM_MESSAGE_STATUS: 'im_message_status', // IM 消息状态(私聊 / 群聊共用) IM_RTC_CALL_CONVERSATION_TYPE: 'im_rtc_call_conversation_type', // IM 通话会话类型 IM_RTC_CALL_END_REASON: 'im_rtc_call_end_reason', // IM 通话结束原因 IM_RTC_CALL_MEDIA_TYPE: 'im_rtc_call_media_type', // IM 通话媒体类型