From 8fc5273a886c83d225137ef637a2133cd38d4664 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 6 May 2026 18:52:30 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E7=BE=A4=E7=94=B3=E8=AF=B7=20v0.1=EF=BC=9A=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E6=8A=8A=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/im/group/index.ts | 7 +- src/api/im/group/request/index.ts | 18 +- .../components/group/GroupCreateDialog.vue | 10 +- src/views/im/home/index.vue | 7 + .../conversation/ConversationGroupSide.vue | 29 +- .../ConversationGroupRequestPending.vue | 267 ++++++++++++++++++ src/views/im/home/store/groupRequestStore.ts | 96 +++++++ src/views/im/home/store/groupStore.ts | 27 +- src/views/im/home/store/websocketStore.ts | 53 ++++ src/views/im/home/types/index.ts | 5 +- src/views/im/utils/constants.ts | 14 - src/views/im/utils/user.ts | 7 + 12 files changed, 498 insertions(+), 42 deletions(-) create mode 100644 src/views/im/home/pages/conversation/components/message/ConversationGroupRequestPending.vue create mode 100644 src/views/im/home/store/groupRequestStore.ts diff --git a/src/api/im/group/index.ts b/src/api/im/group/index.ts index 21a51a7a6..dc07fac4d 100644 --- a/src/api/im/group/index.ts +++ b/src/api/im/group/index.ts @@ -10,13 +10,12 @@ export interface ImGroupRespVO { notice?: string // 群公告 banned?: boolean // 是否封禁 mutedAll?: boolean // 是否全群禁言 - joinType?: number // 加群方式;参见 ImGroupJoinTypeEnum + joinApproval?: boolean // 进群是否需群主 / 管理员审批 bannedTime?: string // 封禁时间 status: number // 群状态(0=正常,1=已解散) dissolvedTime?: string // 解散时间 createTime?: string // 创建时间 pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空) - pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空 TODO @AI:看看这里,是不是可以不返回? } // 群消息置顶 / 取消置顶 Request VO @@ -29,7 +28,7 @@ export interface ImGroupMessagePinReqVO { export interface ImGroupCreateReqVO { name: string // 群名称 memberUserIds?: number[] // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己) - joinType?: number // 加群方式;不传默认 0 自由进群 TODO @AI:不要写 0,而是写对应的枚举 + joinApproval?: boolean // 进群是否需审批;不传默认 false 自由进群 } // 群更新 Request VO @@ -38,7 +37,7 @@ export interface ImGroupUpdateReqVO { name?: string // 群名称 avatar?: string // 群头像 notice?: string // 群公告 - joinType?: number // 加群方式 + joinApproval?: boolean // 进群是否需审批 } // 添加 / 撤销群管理员 Request VO diff --git a/src/api/im/group/request/index.ts b/src/api/im/group/request/index.ts index 4415fbc51..fab05e9af 100644 --- a/src/api/im/group/request/index.ts +++ b/src/api/im/group/request/index.ts @@ -48,6 +48,7 @@ export const refuseGroupRequest = (id: number | string, handleContent?: string) } // 查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页 +// TODO @AI:这个 list 接口,改成传递 groupId,查询这个群下,所有的申请。然后,group size 增加一个:「群申请列表」,里面可以看到所有的。 export const getMyGroupRequestList = (limit: number, lastRequestId?: number) => { const params: Record = { limit } if (lastRequestId != null) { @@ -59,23 +60,14 @@ export const getMyGroupRequestList = (limit: number, lastRequestId?: number) => }) } -// 查询指定群的未处理加群申请(仅群主或管理员可调);游标分页 -export const getPendingGroupRequestList = ( - groupId: number | string, - limit: number, - lastRequestId?: number -) => { - const params: Record = { groupId, limit } - if (lastRequestId != null) { - params.lastRequestId = lastRequestId - } +// 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表 +export const getUnhandledRequestList = () => { return request.get({ - url: '/im/group-request/list-pending', - params + url: '/im/group-request/unhandled-list' }) } -// 按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用) +// 按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用) export const getMyGroupRequest = (id: number) => { return request.get({ url: '/im/group-request/get', diff --git a/src/views/im/home/components/group/GroupCreateDialog.vue b/src/views/im/home/components/group/GroupCreateDialog.vue index bf2fcf428..d44cebf77 100644 --- a/src/views/im/home/components/group/GroupCreateDialog.vue +++ b/src/views/im/home/components/group/GroupCreateDialog.vue @@ -11,6 +11,12 @@
+ +
+ 进群需要群主 / 群管理确认 + +
+
(false) // 默认不需审批,自由进群 const searchText = ref('') const submitting = ref(false) const workingFriends = ref([]) // 工作副本(带 checked / disabled 标记),与 prop 隔离 @@ -142,6 +149,7 @@ watch( return } groupName.value = '' + joinApproval.value = false searchText.value = '' workingFriends.value = props.friends .filter((friend) => !friend.deleted) @@ -205,7 +213,7 @@ async function handleOk() { submitting.value = true try { // 1. 新建群聊 - const group = await createGroup({ name, memberUserIds }) + const group = await createGroup({ name, memberUserIds, joinApproval: joinApproval.value }) if (!group?.id) { throw new Error('创建群失败:未返回群编号') } diff --git a/src/views/im/home/index.vue b/src/views/im/home/index.vue index deb623aad..3a90dd190 100644 --- a/src/views/im/home/index.vue +++ b/src/views/im/home/index.vue @@ -31,6 +31,7 @@ import { useConversationStore } from './store/conversationStore' import { useImWebSocketStore } from './store/websocketStore' import { useFriendStore } from './store/friendStore' import { useGroupStore } from './store/groupStore' +import { useGroupRequestStore } from './store/groupRequestStore' import { useDraftStore } from './store/draftStore' import { useMessagePuller } from './composables/useMessagePuller' import { useMessageSender } from './composables/useMessageSender' @@ -47,6 +48,7 @@ const conversationStore = useConversationStore() const webSocketStore = useImWebSocketStore() const friendStore = useFriendStore() const groupStore = useGroupStore() +const groupRequestStore = useGroupRequestStore() const draftStore = useDraftStore() const { pullOnce } = useMessagePuller() const { readActive, syncPrivateReadStatus } = useMessageSender() @@ -85,6 +87,11 @@ onMounted(async () => { // 3. 实时通信:建 WebSocket 长连接 + 拉离线消息(pullOnce finally 把 loading 归位) webSocketStore.connect() await pullOnce() + // 3.1 我管理的群下未处理加群申请,给顶部横幅 / Drawer 派生 count 与 list;失败不阻断主流程 + // TODO @AI:可以挪到 1.2 那么?不阻塞就行呀。 + void groupRequestStore.fetchUnhandledList().catch((e) => + console.warn('[IM] 拉取未处理加群申请失败', e) + ) // 4. 默认选中第一个会话;若置顶分组处于折叠态,需跳过被折叠隐藏的置顶项,避免自动展开折叠 const sorted = conversationStore.getSortedConversations diff --git a/src/views/im/home/pages/conversation/components/conversation/ConversationGroupSide.vue b/src/views/im/home/pages/conversation/components/conversation/ConversationGroupSide.vue index db61e969d..464213039 100644 --- a/src/views/im/home/pages/conversation/components/conversation/ConversationGroupSide.vue +++ b/src/views/im/home/pages/conversation/components/conversation/ConversationGroupSide.vue @@ -272,6 +272,11 @@ 全群禁言
+ +
+ 进群需要群主 / 群管理确认 + +
@@ -387,7 +392,11 @@ import { import { quitGroup, removeGroupMember, updateGroupMember } from '@/api/im/group/member' import { useConversationStore } from '../../../../store/conversationStore' import { useGroupStore } from '../../../../store/groupStore' -import { GROUP_ADMIN_MAX_COUNT, ImConversationType, ImGroupMemberRole } from '@/views/im/utils/constants' +import { + GROUP_ADMIN_MAX_COUNT, + ImConversationType, + ImGroupMemberRole +} from '@/views/im/utils/constants' import GroupMemberGrid from '../../../../components/group/GroupMemberGrid.vue' import GroupMemberAddDialog from '../../../../components/group/GroupMemberAddDialog.vue' import GroupMemberSelector, { @@ -485,7 +494,6 @@ const isOwnerOrAdmin = computed( () => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN ) - // 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定) const visibleMembers = computed(() => { return props.members @@ -536,6 +544,17 @@ async function saveNotice() { emit('reload') } +/** 群主:切换「进群审批」开关;开启后所有「申请」「邀请」路径都需群主 / 管理员同意 */ +// TODO @AI:应该是 handleXXX;别的方法也看看,是不是统一调整过来。 +async function onJoinApprovalChange(value: boolean | string | number) { + if (!props.group) { + return + } + await updateGroup({ id: props.group.id, joinApproval: !!value }) + message.success('保存成功') + emit('reload') +} + /** 任何成员:保存群备注(仅自己可见,会替换会话列表 / 顶部群名展示) */ async function saveGroupRemark() { if (!props.group) { @@ -696,8 +715,10 @@ async function handleRemoveComplete(members: GroupMemberFlag[]) { /** 当前管理员的 userId 列表,作为 Selector 默认勾选;过滤已退群成员,避免 maxSize 名额被隐藏成员占用导致无法新增管理员 */ const adminCheckedIds = computed(() => props.members - .filter((member) => member.role === ImGroupMemberRole.ADMIN - && member.status !== CommonStatusEnum.DISABLE) + .filter( + (member) => + member.role === ImGroupMemberRole.ADMIN && member.status !== CommonStatusEnum.DISABLE + ) .map((member) => member.userId) ) diff --git a/src/views/im/home/pages/conversation/components/message/ConversationGroupRequestPending.vue b/src/views/im/home/pages/conversation/components/message/ConversationGroupRequestPending.vue new file mode 100644 index 000000000..5c5da5b71 --- /dev/null +++ b/src/views/im/home/pages/conversation/components/message/ConversationGroupRequestPending.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/src/views/im/home/store/groupRequestStore.ts b/src/views/im/home/store/groupRequestStore.ts new file mode 100644 index 000000000..c8a3fc5f6 --- /dev/null +++ b/src/views/im/home/store/groupRequestStore.ts @@ -0,0 +1,96 @@ +import { defineStore, acceptHMRUpdate } from 'pinia' +import { store } from '@/store' + +import { + agreeGroupRequest as apiAgreeGroupRequest, + getMyGroupRequest as apiGetMyGroupRequest, + getUnhandledRequestList as apiGetUnhandledRequestList, + refuseGroupRequest as apiRefuseGroupRequest, + type ImGroupRequestRespVO +} from '@/api/im/group/request' + +/** + * IM 加群申请 Store + * + * 仅维护「我管理的所有群」下未处理的申请列表(unhandledList); + * 横幅 / Drawer 都从这里派生 count 和分组列表,避免给 ImGroupRespVO 挂 pendingRequestCount 字段 + * + * 数据生命周期: + * - 进 IM 后调一次 fetchUnhandledList 拉首页全量 + * - WebSocket 1503 → 调 fetchOne(requestId) 拉单条 + push 到 unhandledList 头部 + * - WebSocket 1505 / 1506 → 按 requestId 从 unhandledList 移除 + * - WebSocket 1517 GROUP_ADMIN_ADD(自己被加为 admin)→ 重新调 fetchUnhandledList + * - 本端 agree / refuse 处理后 → 本地按 requestId 移除 + */ +export const useGroupRequestStore = defineStore('imGroupRequestStore', { + state: () => ({ + /** 我管理的所有群下未处理申请列表(按 id 倒序) */ + unhandledList: [] as ImGroupRequestRespVO[], + /** fetchUnhandledList 是否成功执行过;避免横幅显示 0 然后跳数字的闪烁 */ + loaded: false + }), + + getters: { + /** 指定群下的未处理申请数;横幅红点 */ + getUnhandledCountByGroupId: + (state) => + (groupId: number): number => + state.unhandledList.filter((r) => r.groupId === groupId).length, + /** 指定群下的未处理申请列表;Drawer 内容 */ + getUnhandledListByGroupId: + (state) => + (groupId: number): ImGroupRequestRespVO[] => + state.unhandledList.filter((r) => r.groupId === groupId) + }, + + actions: { + /** 拉取我管理的所有群下未处理申请;进 IM 后 / 升级 admin 后 / WS 推送有冲突时调用 */ + async fetchUnhandledList() { + const list = await apiGetUnhandledRequestList() + this.unhandledList = list || [] + this.loaded = true + }, + + /** WS 收到 1503:按 requestId 单查 + push 进列表头;payload 已带申请方昵称 / 头像可减一次回查 */ + async addByRequestId(requestId: number) { + const exists = this.unhandledList.some((r) => r.id === requestId) + if (exists) { + return + } + const request = await apiGetMyGroupRequest(requestId) + if (!request) { + return + } + this.unhandledList.unshift(request) + }, + + /** WS 收到 1505 / 1506 或本端处理完一条:按 requestId 从列表移除 */ + removeByRequestId(requestId: number) { + this.unhandledList = this.unhandledList.filter((r) => r.id !== requestId) + }, + + /** 同意申请;本端处理后立即从列表移除,避免被反复点击 */ + async agreeRequest(requestId: number) { + await apiAgreeGroupRequest(requestId) + this.removeByRequestId(requestId) + }, + + /** 拒绝申请 */ + async refuseRequest(requestId: number, handleContent?: string) { + await apiRefuseGroupRequest(requestId, handleContent) + this.removeByRequestId(requestId) + }, + + /** 退出 IM / 切账号时清理 */ + reset() { + this.unhandledList = [] + this.loaded = false + } + } +}) + +export const useGroupRequestStoreWithOut = () => useGroupRequestStore(store) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useGroupRequestStore, import.meta.hot)) +} diff --git a/src/views/im/home/store/groupStore.ts b/src/views/im/home/store/groupStore.ts index 2b4b0b68c..6e0ac6ada 100644 --- a/src/views/im/home/store/groupStore.ts +++ b/src/views/im/home/store/groupStore.ts @@ -13,6 +13,7 @@ import { type ImGroupMemberRespVO } from '@/api/im/group/member' import { useConversationStore } from './conversationStore' +import { useGroupRequestStore } from './groupRequestStore' import { ImConversationType, ImGroupMemberRole, ImMessageType } from '../../utils/constants' import { getCurrentUserId, @@ -523,6 +524,9 @@ export const useGroupStore = defineStore('imGroupStore', { case ImMessageType.GROUP_MEMBER_INVITE: this.applyGroupMemberInviteNotification(groupId, payload) break + case ImMessageType.GROUP_MEMBER_ENTER: + this.applyGroupMemberEnterNotification(groupId, payload) + break case ImMessageType.GROUP_MEMBER_QUIT: this.applyGroupMemberQuitNotification(groupId, payload) break @@ -534,6 +538,10 @@ export const useGroupStore = defineStore('imGroupStore', { break case ImMessageType.GROUP_ADMIN_ADD: this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.ADMIN) + // 自己被加为管理员,原本看不到的群下未处理申请现在变可见,重新拉一次 unhandledList + if (isSelfInPayloadMembers(payload)) { + useGroupRequestStore().fetchUnhandledList().catch(() => undefined) + } break case ImMessageType.GROUP_ADMIN_REMOVE: this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.NORMAL) @@ -610,6 +618,15 @@ export const useGroupStore = defineStore('imGroupStore', { this.fetchGroupMembers(groupId, true).catch(() => undefined) }, + /** 自由进群:进群者本端 group 未就位先 fetchGroupInfo bootstrap;所有人都刷成员列表 */ + applyGroupMemberEnterNotification(groupId: number, payload: GroupNotificationPayload) { + const selfUserId = getCurrentUserId() + if (selfUserId && payload.entrantUserId === selfUserId && !this.getGroup(groupId)) { + this.fetchGroupInfo(groupId).catch(() => undefined) + } + this.fetchGroupMembers(groupId, true).catch(() => undefined) + }, + /** 成员退群:退群者本人多端同步走 removeGroup;其他成员从本地列表移除 quitter */ applyGroupMemberQuitNotification(groupId: number, payload: GroupNotificationPayload) { const selfUserId = getCurrentUserId() @@ -641,11 +658,16 @@ export const useGroupStore = defineStore('imGroupStore', { } }, - /** 群主转让:旧群主 → NORMAL,新群主 → OWNER */ + /** 群主转让:旧群主 → NORMAL,新群主 → OWNER;新群主自己侧重新拉申请列表 */ applyGroupOwnerTransferNotification(groupId: number, payload: GroupNotificationPayload) { if (payload.operatorUserId && payload.newOwnerUserId) { this.transferOwner(groupId, payload.operatorUserId, payload.newOwnerUserId) } + // 自己接管群主:原本看不到的群下未处理申请现在变可见,重新拉一次 unhandledList + const selfUserId = getCurrentUserId() + if (selfUserId && payload.newOwnerUserId === selfUserId) { + useGroupRequestStore().fetchUnhandledList().catch(() => undefined) + } }, /** 群消息置顶:从 payload 直接拿完整消息对象 push 到 pinnedMessages */ @@ -735,8 +757,7 @@ function convertGroup(group: ImGroupRespVO): Group { pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO), mutedAll: group.mutedAll, banned: group.banned, - joinType: group.joinType, - pendingRequestCount: group.pendingRequestCount + joinApproval: group.joinApproval } } diff --git a/src/views/im/home/store/websocketStore.ts b/src/views/im/home/store/websocketStore.ts index afb038a1d..150ce13c7 100644 --- a/src/views/im/home/store/websocketStore.ts +++ b/src/views/im/home/store/websocketStore.ts @@ -1,4 +1,5 @@ import { defineStore, acceptHMRUpdate } from 'pinia' +import { ElNotification } from 'element-plus' import { store } from '@/store' import { getRefreshToken } from '@/utils/auth' import { useUserStore } from '@/store/modules/user' @@ -9,6 +10,7 @@ import { ImConversationType, isFriendChatTip, isFriendNotification, + isGroupRequestNotification, isNormalMessage } from '../../utils/constants' import { playAudioTip } from '../../utils/message' @@ -16,6 +18,7 @@ import { useConversationStore } from './conversationStore' import { useFriendStore, type FriendNotificationPayload } from './friendStore' import { getFriendDisplayName } from '../../utils/user' import { useGroupStore } from './groupStore' +import { useGroupRequestStore } from './groupRequestStore' import { readPrivateMessages as apiReadPrivateMessages } from '@/api/im/message/private' import { readGroupMessages as apiReadGroupMessages } from '@/api/im/message/group' import type { @@ -247,6 +250,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', { ) { this.handlePrivateMessage(websocketMessage) } + } else if (isGroupRequestNotification(websocketMessage.type)) { + // 加群申请通知(1503 / 1505 / 1506)走私聊通道,与好友通知同段位但分开 dispatcher + // TODO @AI:改成走群聊通道。不然消息不好拉到!!! + this.handleGroupRequestNotification(websocketMessage) } else { // TEXT / IMAGE / FILE / VOICE / VIDEO 等普通消息 this.handlePrivateMessage(websocketMessage) @@ -551,6 +558,52 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', { } }, + // ==================== 加群申请通知(1503 / 1505 / 1506,承载于私聊通道) ==================== + + /** + * 加群申请通知统一入口:分发到 groupRequestStore,驱动横幅 + Drawer 同步 + * + * 对应后端 ImPrivateMessageDTO.ofGroupNotification 系列: + * - 1503:admin 侧拉单条 push 进 unhandledList;申请人侧不收 + * - 1505 / 1506:双端都收 — admin 侧从 unhandledList 移除;申请人侧弹 toast + */ + handleGroupRequestNotification(websocketMessage: ImPrivateMessageDTO) { + const payload = JSON.parse(websocketMessage.content || '{}') as { + requestId?: number + groupId?: number + userId?: number + handleContent?: string + } + if (!payload.requestId) { + return + } + const groupRequestStore = useGroupRequestStore() + const userStore = useUserStore() + const myId = Number(userStore.getUser?.id) || 0 + switch (websocketMessage.type) { + case ImMessageType.GROUP_REQUEST_RECEIVED: + groupRequestStore.addByRequestId(payload.requestId).catch(() => undefined) + break + case ImMessageType.GROUP_REQUEST_APPROVED: + groupRequestStore.removeByRequestId(payload.requestId) + if (payload.userId === myId) { + ElNotification.success({ title: '入群申请已通过', message: '可以开始群聊了' }) + } + break + case ImMessageType.GROUP_REQUEST_REJECTED: + groupRequestStore.removeByRequestId(payload.requestId) + if (payload.userId === myId) { + ElNotification.warning({ + title: '入群申请被拒绝', + message: payload.handleContent || '' + }) + } + break + default: + break + } + }, + // ==================== 群关系事件(承载于群聊通道,按 inner type 分流) ==================== /** diff --git a/src/views/im/home/types/index.ts b/src/views/im/home/types/index.ts index 43c751c70..b6819fdf9 100644 --- a/src/views/im/home/types/index.ts +++ b/src/views/im/home/types/index.ts @@ -119,8 +119,7 @@ export interface Group { pinnedMessages?: Message[] // 群置顶消息列表 mutedAll?: boolean // 是否全群禁言 banned?: boolean // 是否被管理员封禁 - joinType?: number // 加群方式;参见 ImGroupJoinType - pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空 + joinApproval?: boolean // 进群是否需群主 / 管理员审批 // ========== 前端扩展字段(user-per-group 维度) ========== silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填 @@ -232,5 +231,5 @@ export interface GroupLite { showImageThumb?: string memberCount?: number ownerId?: number - joinType?: number // 加群方式;参见 ImGroupJoinType + joinApproval?: boolean // 进群是否需群主 / 管理员审批 } \ No newline at end of file diff --git a/src/views/im/utils/constants.ts b/src/views/im/utils/constants.ts index 2435f7b55..ddbeb7292 100644 --- a/src/views/im/utils/constants.ts +++ b/src/views/im/utils/constants.ts @@ -157,20 +157,6 @@ export const ImGroupMemberRole = { NORMAL: 3 // 普通成员 } as const -/** 加群方式(对齐后端 ImGroupJoinTypeEnum) */ -export const ImGroupJoinType = { - FREE: 0, // 自由进群 - APPLY: 1, // 申请需审批,邀请直进 - APPLY_AND_NORMAL_INVITE: 2 // 申请、及普通成员邀请均需审批 -} as const - -/** 加群方式文案 */ -export const IM_GROUP_JOIN_TYPE_LABELS: Record = { - [ImGroupJoinType.FREE]: '自由进群', - [ImGroupJoinType.APPLY]: '申请需审批', - [ImGroupJoinType.APPLY_AND_NORMAL_INVITE]: '申请、及普通成员邀请均需审批' -} - /** 加群来源(对齐后端 ImGroupAddSourceEnum) */ export const ImGroupAddSource = { SEARCH: 1, // 搜索 diff --git a/src/views/im/utils/user.ts b/src/views/im/utils/user.ts index 0d535540e..33ce2666b 100644 --- a/src/views/im/utils/user.ts +++ b/src/views/im/utils/user.ts @@ -170,6 +170,8 @@ export type GroupNotificationPayload = { mutedUserId?: number // 禁言目标用户 muteEndTime?: string // 禁言到期时间 banned?: boolean // 封禁状态 + entrantUserId?: number // 自由进群事件:进群者用户编号 + addSource?: number // 自由进群事件:来源(搜索 / 二维码 / 分享链接) /** PIN 事件携带的完整被置顶消息对象 */ message?: { id: number @@ -223,6 +225,11 @@ export function resolveGroupNotificationText( 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: