feat(im): 初始化群申请 v0.1:第二把 review

im
YunaiV 2026-05-06 18:52:30 +08:00
parent 3be0daf115
commit 8fc5273a88
12 changed files with 498 additions and 42 deletions

View File

@ -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

View File

@ -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<string, number> = { 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<string, number | string> = { groupId, limit }
if (lastRequestId != null) {
params.lastRequestId = lastRequestId
}
// 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表
export const getUnhandledRequestList = () => {
return request.get<ImGroupRequestRespVO[]>({
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<ImGroupRequestRespVO | null>({
url: '/im/group-request/get',

View File

@ -11,6 +11,12 @@
<div class="flex flex-col gap-3">
<el-input v-model="groupName" placeholder="请输入群名称" maxlength="20" show-word-limit />
<!-- TODO @AI暂时不用这个入口对齐微信 -->
<div class="flex items-center gap-2 text-13px text-[var(--el-text-color-secondary)]">
<span class="shrink-0">进群需要群主 / 群管理确认</span>
<el-switch v-model="joinApproval" />
</div>
<div class="flex gap-2.5">
<div
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
@ -131,6 +137,7 @@ const visible = computed({
})
const groupName = ref('')
const joinApproval = ref<boolean>(false) //
const searchText = ref('')
const submitting = ref(false)
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 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('创建群失败:未返回群编号')
}

View File

@ -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

View File

@ -272,6 +272,11 @@
<span class="im-conversation-group-side__label">全群禁言</span>
<el-switch :model-value="!!currentMutedAll" @change="onMuteAllChange" />
</div>
<!-- 进群审批仅群主可操作开启后所有申请邀请路径都需群主 / 管理员同意 -->
<div v-if="isOwner" class="im-conversation-group-side__row">
<span class="im-conversation-group-side__label">进群需要群主 / 群管理确认</span>
<el-switch :model-value="!!group.joinApproval" @change="onJoinApprovalChange" />
</div>
</div>
<!-- ==================== 群主操作 ==================== -->
@ -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)
)

View File

@ -0,0 +1,267 @@
<template>
<!--
群顶部待处理加群申请横幅
- 仅当登录用户是该群 owner / admin 且该群下未处理申请数 > 0 时显示
- count list 都从 groupRequestStore 派生全局存本端处理 / WS 通知到达后 store 自动更新
- 单条胶囊一行点击展开下拉每条带同意 / 拒绝按钮
-->
<!-- TODO @AI还不是新建一个 components/group这个改成 GroupRequestPending然后 GroupPinnedMessages 这样风格更一致一点 -->
<div v-if="canManage && pendingCount > 0" class="im-conversation-group-request">
<div
class="im-conversation-group-request__row im-conversation-group-request__row--clickable"
@click="expanded = !expanded"
>
<Icon
icon="ant-design:user-add-outlined"
:size="14"
class="im-conversation-group-request__icon"
/>
<span class="im-conversation-group-request__text">
{{ pendingCount }} 条新的入群申请待处理
</span>
<Icon
:icon="expanded ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
:size="11"
class="im-conversation-group-request__chevron"
/>
</div>
<!-- 展开列表面板浅色面板 + 每条独立卡片行内含同意 / 拒绝按钮 -->
<div v-if="expanded" class="im-conversation-group-request__list">
<div v-for="item in list" :key="item.id" class="im-conversation-group-request__item">
<UserAvatar
:url="item.userAvatar"
:name="item.userNickname"
:size="32"
:clickable="false"
/>
<div class="im-conversation-group-request__item-body">
<div class="im-conversation-group-request__item-name truncate">
{{ item.userNickname || `用户 ${item.userId}` }}
<span v-if="item.inviterUserId" class="im-conversation-group-request__item-inviter">
{{ item.inviterNickname || `用户 ${item.inviterUserId}` }} 邀请
</span>
</div>
<div v-if="item.applyContent" class="im-conversation-group-request__item-msg truncate">
{{ item.applyContent }}
</div>
</div>
<div class="im-conversation-group-request__item-actions">
<el-button
size="small"
type="primary"
:loading="actingId === item.id"
@click="handleAgree(item)"
>
同意
</el-button>
<el-button size="small" :loading="actingId === item.id" @click="handleRefuse(item)">
拒绝
</el-button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import Icon from '@/components/Icon/src/Icon.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useUserStore } from '@/store/modules/user'
import type { ImGroupRequestRespVO } from '@/api/im/group/request'
import { ImGroupMemberRole } from '@/views/im/utils/constants'
import { useGroupStore } from '../../../../store/groupStore'
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
import UserAvatar from '../../../../components/user/UserAvatar.vue'
defineOptions({ name: 'ImConversationGroupRequestPending' })
const props = defineProps<{
groupId: number
}>()
const userStore = useUserStore()
const groupStore = useGroupStore()
const groupRequestStore = useGroupRequestStore()
const message = useMessage()
const expanded = ref(false)
const actingId = ref<number | null>(null)
/** 当前群(含 ownerUserId / members */
const group = computed(() => groupStore.getGroup(props.groupId))
/** 当前用户在群里的角色;优先用 group.members懒加载未到时回退到 ownerUserId 直判 */
const myRole = computed(() => {
const myId = Number(userStore.getUser?.id) || 0
if (group.value?.ownerUserId === myId) {
return ImGroupMemberRole.OWNER
}
return group.value?.members?.find((m) => m.userId === myId)?.role
})
/** 仅群主 / 管理员可见 */
const canManage = computed(
() => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN
)
/** 当前群未处理申请数;从 store 派生 */
const pendingCount = computed(() => groupRequestStore.getUnhandledCountByGroupId(props.groupId))
/** 当前群未处理申请列表Drawer 内容 */
const list = computed<ImGroupRequestRespVO[]>(() =>
groupRequestStore.getUnhandledListByGroupId(props.groupId)
)
/** 切群时收起 */
watch(
() => props.groupId,
() => {
expanded.value = false
}
)
/** 同意申请 */
async function handleAgree(item: ImGroupRequestRespVO) {
if (actingId.value !== null) return
actingId.value = item.id
try {
await groupRequestStore.agreeRequest(item.id)
message.success('已同意')
if (list.value.length === 0) {
expanded.value = false
}
} finally {
actingId.value = null
}
}
/** 拒绝申请 */
async function handleRefuse(item: ImGroupRequestRespVO) {
if (actingId.value !== null) return
let handleContent = ''
try {
const result = await message.prompt('请输入拒绝理由(可选)', '拒绝申请')
handleContent = result.value || ''
} catch {
return
}
actingId.value = item.id
try {
await groupRequestStore.refuseRequest(item.id, handleContent || undefined)
message.success('已拒绝')
if (list.value.length === 0) {
expanded.value = false
}
} finally {
actingId.value = null
}
}
// todo @AI style unocss
</script>
<style scoped>
.im-conversation-group-request {
position: relative;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 6px 16px 8px;
background-color: var(--el-fill-color-light);
}
.im-conversation-group-request__row {
display: flex;
align-items: center;
gap: 6px;
width: 360px;
padding: 6px 12px;
background-color: var(--el-bg-color);
border-radius: 10px;
font-size: 13px;
color: var(--el-text-color-primary);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.im-conversation-group-request__row--clickable {
cursor: pointer;
}
.im-conversation-group-request__row--clickable:hover {
background-color: var(--el-fill-color-lighter);
}
.im-conversation-group-request__icon {
flex-shrink: 0;
color: var(--el-color-primary);
}
.im-conversation-group-request__text {
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.im-conversation-group-request__chevron {
flex-shrink: 0;
color: var(--el-text-color-placeholder);
}
.im-conversation-group-request__list {
position: absolute;
top: 100%;
margin-top: -1px;
left: 6px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 8px;
width: 380px;
max-height: 360px;
overflow-y: auto;
padding: 12px;
border-radius: 12px;
background-color: var(--el-bg-color);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.im-conversation-group-request__item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background-color: var(--el-fill-color-light);
border-radius: 8px;
}
.im-conversation-group-request__item-body {
flex: 1;
min-width: 0;
}
.im-conversation-group-request__item-name {
font-size: 13px;
color: var(--el-text-color-primary);
}
.im-conversation-group-request__item-inviter {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-left: 4px;
}
.im-conversation-group-request__item-msg {
margin-top: 2px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.im-conversation-group-request__item-actions {
flex-shrink: 0;
display: flex;
gap: 6px;
}
</style>

View File

@ -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))
}

View File

@ -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
}
}

View File

@ -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
* - 1503admin 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 分流) ====================
/**

View File

@ -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 // 进群是否需群主 / 管理员审批
}

View File

@ -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<number, string> = {
[ImGroupJoinType.FREE]: '自由进群',
[ImGroupJoinType.APPLY]: '申请需审批',
[ImGroupJoinType.APPLY_AND_NORMAL_INVITE]: '申请、及普通成员邀请均需审批'
}
/** 加群来源(对齐后端 ImGroupAddSourceEnum */
export const ImGroupAddSource = {
SEARCH: 1, // 搜索

View File

@ -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: