diff --git a/src/api/im/group/index.ts b/src/api/im/group/index.ts index d981a50df..21a51a7a6 100644 --- a/src/api/im/group/index.ts +++ b/src/api/im/group/index.ts @@ -10,11 +10,13 @@ export interface ImGroupRespVO { notice?: string // 群公告 banned?: boolean // 是否封禁 mutedAll?: boolean // 是否全群禁言 + joinType?: number // 加群方式;参见 ImGroupJoinTypeEnum bannedTime?: string // 封禁时间 status: number // 群状态(0=正常,1=已解散) dissolvedTime?: string // 解散时间 createTime?: string // 创建时间 pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空) + pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空 TODO @AI:看看这里,是不是可以不返回? } // 群消息置顶 / 取消置顶 Request VO @@ -27,6 +29,7 @@ export interface ImGroupMessagePinReqVO { export interface ImGroupCreateReqVO { name: string // 群名称 memberUserIds?: number[] // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己) + joinType?: number // 加群方式;不传默认 0 自由进群 TODO @AI:不要写 0,而是写对应的枚举 } // 群更新 Request VO @@ -35,6 +38,7 @@ export interface ImGroupUpdateReqVO { name?: string // 群名称 avatar?: string // 群头像 notice?: string // 群公告 + joinType?: number // 加群方式 } // 添加 / 撤销群管理员 Request VO diff --git a/src/api/im/group/request/index.ts b/src/api/im/group/request/index.ts new file mode 100644 index 000000000..4415fbc51 --- /dev/null +++ b/src/api/im/group/request/index.ts @@ -0,0 +1,84 @@ +import request from '@/config/axios' + +// IM 加群申请 Response VO +export interface ImGroupRequestRespVO { + id: number // 申请编号 + groupId: number // 群编号 + userId: number // 申请人 / 被邀请人用户编号 + inviterUserId?: number // 邀请人;NULL 表示用户主动申请 + handleResult: number // 处理结果;0=未处理;1=同意;2=拒绝 + applyContent?: string // 申请理由 + handleContent?: string // 处理理由(拒绝时可选填) + handleUserId?: number // 处理人用户编号 + addSource?: number // 加入来源;参见 ImGroupAddSourceEnum + handleTime?: string // 处理时间 + createTime: string // 申请创建时间 + // 聚合字段 + userNickname?: string // 申请人 / 被邀请人昵称 + userAvatar?: string // 申请人 / 被邀请人头像 + inviterNickname?: string // 邀请人昵称 + inviterAvatar?: string // 邀请人头像 + groupName?: string // 群名称 + groupAvatar?: string // 群头像 +} + +// IM 加群申请发起 Request VO +export interface ImGroupRequestApplyReqVO { + groupId: number // 群编号 + applyContent?: string // 申请理由 + addSource?: number // 加入来源 +} + +// 申请加群 +export const applyJoinGroup = (data: ImGroupRequestApplyReqVO) => { + return request.post({ url: '/im/group-request/apply', data }) +} + +// 同意加群申请(群主或管理员) +export const agreeGroupRequest = (id: number | string) => { + return request.put({ url: '/im/group-request/agree', params: { id } }) +} + +// 拒绝加群申请(群主或管理员) +export const refuseGroupRequest = (id: number | string, handleContent?: string) => { + return request.put({ + url: '/im/group-request/refuse', + params: { id, handleContent } + }) +} + +// 查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页 +export const getMyGroupRequestList = (limit: number, lastRequestId?: number) => { + const params: Record = { limit } + if (lastRequestId != null) { + params.lastRequestId = lastRequestId + } + return request.get({ + url: '/im/group-request/list', + params + }) +} + +// 查询指定群的未处理加群申请(仅群主或管理员可调);游标分页 +export const getPendingGroupRequestList = ( + groupId: number | string, + limit: number, + lastRequestId?: number +) => { + const params: Record = { groupId, limit } + if (lastRequestId != null) { + params.lastRequestId = lastRequestId + } + return request.get({ + url: '/im/group-request/list-pending', + params + }) +} + +// 按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用) +export const getMyGroupRequest = (id: number) => { + return request.get({ + url: '/im/group-request/get', + params: { id } + }) +} diff --git a/src/api/im/manager/group/request/index.ts b/src/api/im/manager/group/request/index.ts new file mode 100644 index 000000000..a58bdeb56 --- /dev/null +++ b/src/api/im/manager/group/request/index.ts @@ -0,0 +1,24 @@ +import request from '@/config/axios' + +export interface ImManagerGroupRequestVO { + id: number + groupId: number + groupName?: string + userId: number + userNickname?: string + inviterUserId?: number + inviterNickname?: string + applyContent?: string + addSource?: number + handleResult: number + handleUserId?: number + handleNickname?: string + handleContent?: string + handleTime?: Date + createTime: Date +} + +// 获得加群申请分页 +export const getManagerGroupRequestPage = (params: PageParam) => { + return request.get({ url: '/im/manager/group-request/page', params }) +} diff --git a/src/utils/dict.ts b/src/utils/dict.ts index c0cd1afda..990cf78fe 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -336,5 +336,8 @@ export enum DICT_TYPE { IM_FRIEND_ADD_SOURCE = 'im_friend_add_source', // IM 好友添加来源 IM_FRIEND_REQUEST_HANDLE_RESULT = 'im_friend_request_handle_result', // IM 好友申请处理结果 IM_GROUP_STATUS = 'im_group_status', // IM 群状态 - IM_GROUP_MEMBER_ROLE = 'im_group_member_role' // IM 群成员角色 + IM_GROUP_MEMBER_ROLE = 'im_group_member_role', // IM 群成员角色 + IM_GROUP_JOIN_TYPE = 'im_group_join_type', // IM 群加群方式 + IM_GROUP_ADD_SOURCE = 'im_group_add_source', // IM 加群来源 + IM_GROUP_REQUEST_HANDLE_RESULT = 'im_group_request_handle_result' // IM 加群申请处理结果 } diff --git a/src/views/im/home/store/groupStore.ts b/src/views/im/home/store/groupStore.ts index 44c505c70..2b4b0b68c 100644 --- a/src/views/im/home/store/groupStore.ts +++ b/src/views/im/home/store/groupStore.ts @@ -734,7 +734,9 @@ function convertGroup(group: ImGroupRespVO): Group { ownerUserId: group.ownerUserId, pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO), mutedAll: group.mutedAll, - banned: group.banned + banned: group.banned, + joinType: group.joinType, + pendingRequestCount: group.pendingRequestCount } } diff --git a/src/views/im/home/types/index.ts b/src/views/im/home/types/index.ts index fc8f173b5..43c751c70 100644 --- a/src/views/im/home/types/index.ts +++ b/src/views/im/home/types/index.ts @@ -119,6 +119,8 @@ export interface Group { pinnedMessages?: Message[] // 群置顶消息列表 mutedAll?: boolean // 是否全群禁言 banned?: boolean // 是否被管理员封禁 + joinType?: number // 加群方式;参见 ImGroupJoinType + pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空 // ========== 前端扩展字段(user-per-group 维度) ========== silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填 @@ -230,4 +232,5 @@ export interface GroupLite { showImageThumb?: string memberCount?: number ownerId?: number + joinType?: number // 加群方式;参见 ImGroupJoinType } \ No newline at end of file diff --git a/src/views/im/manager/group/request/index.vue b/src/views/im/manager/group/request/index.vue new file mode 100644 index 000000000..b9197b7eb --- /dev/null +++ b/src/views/im/manager/group/request/index.vue @@ -0,0 +1,190 @@ + + + diff --git a/src/views/im/utils/constants.ts b/src/views/im/utils/constants.ts index 19d646443..2435f7b55 100644 --- a/src/views/im/utils/constants.ts +++ b/src/views/im/utils/constants.ts @@ -7,6 +7,7 @@ export const ImMessageType = { VIDEO: 104, // 视频(对应 OpenIM Video=104) FILE: 105, // 文件(对应 OpenIM File=105) CARD: 108, // 名片(对应 OpenIM Card=108) + FACE: 115, // 表情贴图(对应 OpenIM Face=115;Unicode emoji 仍走 TEXT) // ========== 信号类(2101 / 2200 直接复用 OpenIM 段位编号;2201 自有扩展) ========== RECALL: 2101, // 撤回(对应 OpenIM RevokeNotification=2101) RECEIPT: 2200, // 回执(对应 OpenIM HasReadReceipt=2200) @@ -25,14 +26,14 @@ export const ImMessageType = { // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 自有扩展段) ========== GROUP_CREATE: 1501, // 群创建 GROUP_INFO_UPDATE: 1502, // 群信息变更(NAME / NOTICE 之外字段兜底) - // 1503 GROUP_JOIN_APPLICATION TODO 未实现:入群申请 + GROUP_REQUEST_RECEIVED: 1503, // 收到新的入群申请(私聊定向推送给群主 + 全部管理员) GROUP_MEMBER_QUIT: 1504, // 成员退群 - // 1505 GROUP_APPLICATION_ACCEPTED TODO 未实现:入群申请通过 - // 1506 GROUP_APPLICATION_REJECTED TODO 未实现:入群申请拒绝 + GROUP_REQUEST_APPROVED: 1505, // 入群申请被同意(私聊推给申请人 + 群主 + 全部管理员) + GROUP_REQUEST_REJECTED: 1506, // 入群申请被拒绝(同上) GROUP_OWNER_TRANSFER: 1507, // 群主转让 GROUP_MEMBER_KICK: 1508, // 成员被移出 GROUP_MEMBER_INVITE: 1509, // 成员加入 - // 1510 GROUP_MEMBER_ENTER TODO 未实现:自由进群 + GROUP_MEMBER_ENTER: 1510, // 自由进群(FREE 模式或申请通过后;全员广播) GROUP_DISSOLVE: 1511, // 群解散 GROUP_MEMBER_MUTED: 1512, // 单成员禁言 GROUP_MEMBER_CANCEL_MUTED: 1513, // 单成员取消禁言 @@ -64,6 +65,15 @@ export function isFriendNotification(type: number): boolean { return type >= ImMessageType.FRIEND_REQUEST_APPROVED && type <= ImMessageType.FRIEND_UPDATE } +/** 判断是否「加群申请通知事件」:1503/1505/1506 走私聊通道,按段位识别 */ +export function isGroupRequestNotification(type: number): boolean { + return ( + type === ImMessageType.GROUP_REQUEST_RECEIVED + || type === ImMessageType.GROUP_REQUEST_APPROVED + || type === ImMessageType.GROUP_REQUEST_REJECTED + ) +} + /** 判断是否「会话内的好友事件气泡」:FRIEND_ADD / FRIEND_DELETE 直接渲染成灰色提示,与群事件同处理 */ export function isFriendChatTip(type: number): boolean { return type === ImMessageType.FRIEND_ADD || type === ImMessageType.FRIEND_DELETE @@ -78,7 +88,7 @@ export function isFriendChatTip(type: number): boolean { * 3. 前端会话列表 lastType / @ 标签(ConversationItem)—— 只有 normal 才算「最后一条聊天消息」 * 4. 前端群消息置顶菜单(MessageItem.vue 的 canPin)—— normal 才允许群主 / 管理员置顶 * - * 名片(CARD)属于「用户主动发的聊天消息」,1/2/3 都符合预期;4 同时被放开 = 群主可置顶名片,语义合理 + * 名片(CARD)/ 表情(FACE)都是「用户主动发的聊天消息」,1/2/3 都符合预期;4 同时放开 = 群主可置顶,语义合理 */ const ImMessageTypeNormals: number[] = [ ImMessageType.TEXT, @@ -86,7 +96,8 @@ const ImMessageTypeNormals: number[] = [ ImMessageType.FILE, ImMessageType.VOICE, ImMessageType.VIDEO, - ImMessageType.CARD + ImMessageType.CARD, + ImMessageType.FACE ] /** 判断是否"普通消息" */ @@ -146,6 +157,35 @@ 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, // 搜索 + INVITE: 2, // 邀请 + QR_CODE: 3, // 扫码 + SHARE_LINK: 4 // 分享链接 +} as const + +/** 加群申请处理结果(对齐后端 ImGroupRequestHandleResultEnum) */ +export const ImGroupRequestHandleResult = { + UNHANDLED: 0, // 未处理 + AGREED: 1, // 同意 + REFUSED: 2 // 拒绝 +} as const + /** 好友添加来源(对齐后端 ImFriendAddSourceEnum) */ export const ImFriendAddSource = { SEARCH: 1, // 搜索 @@ -176,6 +216,9 @@ export const GROUP_MESSAGE_PULL_SIZE = 100 /** 「我相关」好友申请列表的单次拉取条数(游标分页 page size,前端控制) */ export const FRIEND_REQUEST_PAGE_SIZE = 100 +/** 「我相关」加群申请列表的单次拉取条数 */ +export const GROUP_REQUEST_PAGE_SIZE = 100 + /** 消息之间渲染「时间分隔条」的阈值:10 分钟 */ export const TIME_TIP_GAP_MS = 10 * 60 * 1000