✨ feat(im): 初始化群申请 v0.1:第二把 review
parent
3be0daf115
commit
8fc5273a88
|
|
@ -10,13 +10,12 @@ export interface ImGroupRespVO {
|
||||||
notice?: string // 群公告
|
notice?: string // 群公告
|
||||||
banned?: boolean // 是否封禁
|
banned?: boolean // 是否封禁
|
||||||
mutedAll?: boolean // 是否全群禁言
|
mutedAll?: boolean // 是否全群禁言
|
||||||
joinType?: number // 加群方式;参见 ImGroupJoinTypeEnum
|
joinApproval?: boolean // 进群是否需群主 / 管理员审批
|
||||||
bannedTime?: string // 封禁时间
|
bannedTime?: string // 封禁时间
|
||||||
status: number // 群状态(0=正常,1=已解散)
|
status: number // 群状态(0=正常,1=已解散)
|
||||||
dissolvedTime?: string // 解散时间
|
dissolvedTime?: string // 解散时间
|
||||||
createTime?: string // 创建时间
|
createTime?: string // 创建时间
|
||||||
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
|
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
|
||||||
pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空 TODO @AI:看看这里,是不是可以不返回?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 群消息置顶 / 取消置顶 Request VO
|
// 群消息置顶 / 取消置顶 Request VO
|
||||||
|
|
@ -29,7 +28,7 @@ export interface ImGroupMessagePinReqVO {
|
||||||
export interface ImGroupCreateReqVO {
|
export interface ImGroupCreateReqVO {
|
||||||
name: string // 群名称
|
name: string // 群名称
|
||||||
memberUserIds?: number[] // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)
|
memberUserIds?: number[] // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)
|
||||||
joinType?: number // 加群方式;不传默认 0 自由进群 TODO @AI:不要写 0,而是写对应的枚举
|
joinApproval?: boolean // 进群是否需审批;不传默认 false 自由进群
|
||||||
}
|
}
|
||||||
|
|
||||||
// 群更新 Request VO
|
// 群更新 Request VO
|
||||||
|
|
@ -38,7 +37,7 @@ export interface ImGroupUpdateReqVO {
|
||||||
name?: string // 群名称
|
name?: string // 群名称
|
||||||
avatar?: string // 群头像
|
avatar?: string // 群头像
|
||||||
notice?: string // 群公告
|
notice?: string // 群公告
|
||||||
joinType?: number // 加群方式
|
joinApproval?: boolean // 进群是否需审批
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加 / 撤销群管理员 Request VO
|
// 添加 / 撤销群管理员 Request VO
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
export const getMyGroupRequestList = (limit: number, lastRequestId?: number) => {
|
||||||
const params: Record<string, number> = { limit }
|
const params: Record<string, number> = { limit }
|
||||||
if (lastRequestId != null) {
|
if (lastRequestId != null) {
|
||||||
|
|
@ -59,23 +60,14 @@ export const getMyGroupRequestList = (limit: number, lastRequestId?: number) =>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询指定群的未处理加群申请(仅群主或管理员可调);游标分页
|
// 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表
|
||||||
export const getPendingGroupRequestList = (
|
export const getUnhandledRequestList = () => {
|
||||||
groupId: number | string,
|
|
||||||
limit: number,
|
|
||||||
lastRequestId?: number
|
|
||||||
) => {
|
|
||||||
const params: Record<string, number | string> = { groupId, limit }
|
|
||||||
if (lastRequestId != null) {
|
|
||||||
params.lastRequestId = lastRequestId
|
|
||||||
}
|
|
||||||
return request.get<ImGroupRequestRespVO[]>({
|
return request.get<ImGroupRequestRespVO[]>({
|
||||||
url: '/im/group-request/list-pending',
|
url: '/im/group-request/unhandled-list'
|
||||||
params
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)
|
// 按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)
|
||||||
export const getMyGroupRequest = (id: number) => {
|
export const getMyGroupRequest = (id: number) => {
|
||||||
return request.get<ImGroupRequestRespVO | null>({
|
return request.get<ImGroupRequestRespVO | null>({
|
||||||
url: '/im/group-request/get',
|
url: '/im/group-request/get',
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<el-input v-model="groupName" placeholder="请输入群名称" maxlength="20" show-word-limit />
|
<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 gap-2.5">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
|
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 groupName = ref('')
|
||||||
|
const joinApproval = ref<boolean>(false) // 默认不需审批,自由进群
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 checked / disabled 标记),与 prop 隔离
|
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 checked / disabled 标记),与 prop 隔离
|
||||||
|
|
@ -142,6 +149,7 @@ watch(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
groupName.value = ''
|
groupName.value = ''
|
||||||
|
joinApproval.value = false
|
||||||
searchText.value = ''
|
searchText.value = ''
|
||||||
workingFriends.value = props.friends
|
workingFriends.value = props.friends
|
||||||
.filter((friend) => !friend.deleted)
|
.filter((friend) => !friend.deleted)
|
||||||
|
|
@ -205,7 +213,7 @@ async function handleOk() {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
// 1. 新建群聊
|
// 1. 新建群聊
|
||||||
const group = await createGroup({ name, memberUserIds })
|
const group = await createGroup({ name, memberUserIds, joinApproval: joinApproval.value })
|
||||||
if (!group?.id) {
|
if (!group?.id) {
|
||||||
throw new Error('创建群失败:未返回群编号')
|
throw new Error('创建群失败:未返回群编号')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { useConversationStore } from './store/conversationStore'
|
||||||
import { useImWebSocketStore } from './store/websocketStore'
|
import { useImWebSocketStore } from './store/websocketStore'
|
||||||
import { useFriendStore } from './store/friendStore'
|
import { useFriendStore } from './store/friendStore'
|
||||||
import { useGroupStore } from './store/groupStore'
|
import { useGroupStore } from './store/groupStore'
|
||||||
|
import { useGroupRequestStore } from './store/groupRequestStore'
|
||||||
import { useDraftStore } from './store/draftStore'
|
import { useDraftStore } from './store/draftStore'
|
||||||
import { useMessagePuller } from './composables/useMessagePuller'
|
import { useMessagePuller } from './composables/useMessagePuller'
|
||||||
import { useMessageSender } from './composables/useMessageSender'
|
import { useMessageSender } from './composables/useMessageSender'
|
||||||
|
|
@ -47,6 +48,7 @@ const conversationStore = useConversationStore()
|
||||||
const webSocketStore = useImWebSocketStore()
|
const webSocketStore = useImWebSocketStore()
|
||||||
const friendStore = useFriendStore()
|
const friendStore = useFriendStore()
|
||||||
const groupStore = useGroupStore()
|
const groupStore = useGroupStore()
|
||||||
|
const groupRequestStore = useGroupRequestStore()
|
||||||
const draftStore = useDraftStore()
|
const draftStore = useDraftStore()
|
||||||
const { pullOnce } = useMessagePuller()
|
const { pullOnce } = useMessagePuller()
|
||||||
const { readActive, syncPrivateReadStatus } = useMessageSender()
|
const { readActive, syncPrivateReadStatus } = useMessageSender()
|
||||||
|
|
@ -85,6 +87,11 @@ onMounted(async () => {
|
||||||
// 3. 实时通信:建 WebSocket 长连接 + 拉离线消息(pullOnce finally 把 loading 归位)
|
// 3. 实时通信:建 WebSocket 长连接 + 拉离线消息(pullOnce finally 把 loading 归位)
|
||||||
webSocketStore.connect()
|
webSocketStore.connect()
|
||||||
await pullOnce()
|
await pullOnce()
|
||||||
|
// 3.1 我管理的群下未处理加群申请,给顶部横幅 / Drawer 派生 count 与 list;失败不阻断主流程
|
||||||
|
// TODO @AI:可以挪到 1.2 那么?不阻塞就行呀。
|
||||||
|
void groupRequestStore.fetchUnhandledList().catch((e) =>
|
||||||
|
console.warn('[IM] 拉取未处理加群申请失败', e)
|
||||||
|
)
|
||||||
|
|
||||||
// 4. 默认选中第一个会话;若置顶分组处于折叠态,需跳过被折叠隐藏的置顶项,避免自动展开折叠
|
// 4. 默认选中第一个会话;若置顶分组处于折叠态,需跳过被折叠隐藏的置顶项,避免自动展开折叠
|
||||||
const sorted = conversationStore.getSortedConversations
|
const sorted = conversationStore.getSortedConversations
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,11 @@
|
||||||
<span class="im-conversation-group-side__label">全群禁言</span>
|
<span class="im-conversation-group-side__label">全群禁言</span>
|
||||||
<el-switch :model-value="!!currentMutedAll" @change="onMuteAllChange" />
|
<el-switch :model-value="!!currentMutedAll" @change="onMuteAllChange" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- ==================== 群主操作 ==================== -->
|
<!-- ==================== 群主操作 ==================== -->
|
||||||
|
|
@ -387,7 +392,11 @@ import {
|
||||||
import { quitGroup, removeGroupMember, updateGroupMember } from '@/api/im/group/member'
|
import { quitGroup, removeGroupMember, updateGroupMember } from '@/api/im/group/member'
|
||||||
import { useConversationStore } from '../../../../store/conversationStore'
|
import { useConversationStore } from '../../../../store/conversationStore'
|
||||||
import { useGroupStore } from '../../../../store/groupStore'
|
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 GroupMemberGrid from '../../../../components/group/GroupMemberGrid.vue'
|
||||||
import GroupMemberAddDialog from '../../../../components/group/GroupMemberAddDialog.vue'
|
import GroupMemberAddDialog from '../../../../components/group/GroupMemberAddDialog.vue'
|
||||||
import GroupMemberSelector, {
|
import GroupMemberSelector, {
|
||||||
|
|
@ -485,7 +494,6 @@ const isOwnerOrAdmin = computed(
|
||||||
() => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN
|
() => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定)
|
// 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定)
|
||||||
const visibleMembers = computed(() => {
|
const visibleMembers = computed(() => {
|
||||||
return props.members
|
return props.members
|
||||||
|
|
@ -536,6 +544,17 @@ async function saveNotice() {
|
||||||
emit('reload')
|
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() {
|
async function saveGroupRemark() {
|
||||||
if (!props.group) {
|
if (!props.group) {
|
||||||
|
|
@ -696,8 +715,10 @@ async function handleRemoveComplete(members: GroupMemberFlag[]) {
|
||||||
/** 当前管理员的 userId 列表,作为 Selector 默认勾选;过滤已退群成员,避免 maxSize 名额被隐藏成员占用导致无法新增管理员 */
|
/** 当前管理员的 userId 列表,作为 Selector 默认勾选;过滤已退群成员,避免 maxSize 名额被隐藏成员占用导致无法新增管理员 */
|
||||||
const adminCheckedIds = computed(() =>
|
const adminCheckedIds = computed(() =>
|
||||||
props.members
|
props.members
|
||||||
.filter((member) => member.role === ImGroupMemberRole.ADMIN
|
.filter(
|
||||||
&& member.status !== CommonStatusEnum.DISABLE)
|
(member) =>
|
||||||
|
member.role === ImGroupMemberRole.ADMIN && member.status !== CommonStatusEnum.DISABLE
|
||||||
|
)
|
||||||
.map((member) => member.userId)
|
.map((member) => member.userId)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
type ImGroupMemberRespVO
|
type ImGroupMemberRespVO
|
||||||
} from '@/api/im/group/member'
|
} from '@/api/im/group/member'
|
||||||
import { useConversationStore } from './conversationStore'
|
import { useConversationStore } from './conversationStore'
|
||||||
|
import { useGroupRequestStore } from './groupRequestStore'
|
||||||
import { ImConversationType, ImGroupMemberRole, ImMessageType } from '../../utils/constants'
|
import { ImConversationType, ImGroupMemberRole, ImMessageType } from '../../utils/constants'
|
||||||
import {
|
import {
|
||||||
getCurrentUserId,
|
getCurrentUserId,
|
||||||
|
|
@ -523,6 +524,9 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
case ImMessageType.GROUP_MEMBER_INVITE:
|
case ImMessageType.GROUP_MEMBER_INVITE:
|
||||||
this.applyGroupMemberInviteNotification(groupId, payload)
|
this.applyGroupMemberInviteNotification(groupId, payload)
|
||||||
break
|
break
|
||||||
|
case ImMessageType.GROUP_MEMBER_ENTER:
|
||||||
|
this.applyGroupMemberEnterNotification(groupId, payload)
|
||||||
|
break
|
||||||
case ImMessageType.GROUP_MEMBER_QUIT:
|
case ImMessageType.GROUP_MEMBER_QUIT:
|
||||||
this.applyGroupMemberQuitNotification(groupId, payload)
|
this.applyGroupMemberQuitNotification(groupId, payload)
|
||||||
break
|
break
|
||||||
|
|
@ -534,6 +538,10 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
break
|
break
|
||||||
case ImMessageType.GROUP_ADMIN_ADD:
|
case ImMessageType.GROUP_ADMIN_ADD:
|
||||||
this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.ADMIN)
|
this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.ADMIN)
|
||||||
|
// 自己被加为管理员,原本看不到的群下未处理申请现在变可见,重新拉一次 unhandledList
|
||||||
|
if (isSelfInPayloadMembers(payload)) {
|
||||||
|
useGroupRequestStore().fetchUnhandledList().catch(() => undefined)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case ImMessageType.GROUP_ADMIN_REMOVE:
|
case ImMessageType.GROUP_ADMIN_REMOVE:
|
||||||
this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.NORMAL)
|
this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.NORMAL)
|
||||||
|
|
@ -610,6 +618,15 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
this.fetchGroupMembers(groupId, true).catch(() => undefined)
|
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 */
|
/** 成员退群:退群者本人多端同步走 removeGroup;其他成员从本地列表移除 quitter */
|
||||||
applyGroupMemberQuitNotification(groupId: number, payload: GroupNotificationPayload) {
|
applyGroupMemberQuitNotification(groupId: number, payload: GroupNotificationPayload) {
|
||||||
const selfUserId = getCurrentUserId()
|
const selfUserId = getCurrentUserId()
|
||||||
|
|
@ -641,11 +658,16 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 群主转让:旧群主 → NORMAL,新群主 → OWNER */
|
/** 群主转让:旧群主 → NORMAL,新群主 → OWNER;新群主自己侧重新拉申请列表 */
|
||||||
applyGroupOwnerTransferNotification(groupId: number, payload: GroupNotificationPayload) {
|
applyGroupOwnerTransferNotification(groupId: number, payload: GroupNotificationPayload) {
|
||||||
if (payload.operatorUserId && payload.newOwnerUserId) {
|
if (payload.operatorUserId && payload.newOwnerUserId) {
|
||||||
this.transferOwner(groupId, 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 */
|
/** 群消息置顶:从 payload 直接拿完整消息对象 push 到 pinnedMessages */
|
||||||
|
|
@ -735,8 +757,7 @@ function convertGroup(group: ImGroupRespVO): Group {
|
||||||
pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO),
|
pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO),
|
||||||
mutedAll: group.mutedAll,
|
mutedAll: group.mutedAll,
|
||||||
banned: group.banned,
|
banned: group.banned,
|
||||||
joinType: group.joinType,
|
joinApproval: group.joinApproval
|
||||||
pendingRequestCount: group.pendingRequestCount
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||||
|
import { ElNotification } from 'element-plus'
|
||||||
import { store } from '@/store'
|
import { store } from '@/store'
|
||||||
import { getRefreshToken } from '@/utils/auth'
|
import { getRefreshToken } from '@/utils/auth'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
@ -9,6 +10,7 @@ import {
|
||||||
ImConversationType,
|
ImConversationType,
|
||||||
isFriendChatTip,
|
isFriendChatTip,
|
||||||
isFriendNotification,
|
isFriendNotification,
|
||||||
|
isGroupRequestNotification,
|
||||||
isNormalMessage
|
isNormalMessage
|
||||||
} from '../../utils/constants'
|
} from '../../utils/constants'
|
||||||
import { playAudioTip } from '../../utils/message'
|
import { playAudioTip } from '../../utils/message'
|
||||||
|
|
@ -16,6 +18,7 @@ import { useConversationStore } from './conversationStore'
|
||||||
import { useFriendStore, type FriendNotificationPayload } from './friendStore'
|
import { useFriendStore, type FriendNotificationPayload } from './friendStore'
|
||||||
import { getFriendDisplayName } from '../../utils/user'
|
import { getFriendDisplayName } from '../../utils/user'
|
||||||
import { useGroupStore } from './groupStore'
|
import { useGroupStore } from './groupStore'
|
||||||
|
import { useGroupRequestStore } from './groupRequestStore'
|
||||||
import { readPrivateMessages as apiReadPrivateMessages } from '@/api/im/message/private'
|
import { readPrivateMessages as apiReadPrivateMessages } from '@/api/im/message/private'
|
||||||
import { readGroupMessages as apiReadGroupMessages } from '@/api/im/message/group'
|
import { readGroupMessages as apiReadGroupMessages } from '@/api/im/message/group'
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -247,6 +250,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
) {
|
) {
|
||||||
this.handlePrivateMessage(websocketMessage)
|
this.handlePrivateMessage(websocketMessage)
|
||||||
}
|
}
|
||||||
|
} else if (isGroupRequestNotification(websocketMessage.type)) {
|
||||||
|
// 加群申请通知(1503 / 1505 / 1506)走私聊通道,与好友通知同段位但分开 dispatcher
|
||||||
|
// TODO @AI:改成走群聊通道。不然消息不好拉到!!!
|
||||||
|
this.handleGroupRequestNotification(websocketMessage)
|
||||||
} else {
|
} else {
|
||||||
// TEXT / IMAGE / FILE / VOICE / VIDEO 等普通消息
|
// TEXT / IMAGE / FILE / VOICE / VIDEO 等普通消息
|
||||||
this.handlePrivateMessage(websocketMessage)
|
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 分流) ====================
|
// ==================== 群关系事件(承载于群聊通道,按 inner type 分流) ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,7 @@ export interface Group {
|
||||||
pinnedMessages?: Message[] // 群置顶消息列表
|
pinnedMessages?: Message[] // 群置顶消息列表
|
||||||
mutedAll?: boolean // 是否全群禁言
|
mutedAll?: boolean // 是否全群禁言
|
||||||
banned?: boolean // 是否被管理员封禁
|
banned?: boolean // 是否被管理员封禁
|
||||||
joinType?: number // 加群方式;参见 ImGroupJoinType
|
joinApproval?: boolean // 进群是否需群主 / 管理员审批
|
||||||
pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空
|
|
||||||
|
|
||||||
// ========== 前端扩展字段(user-per-group 维度) ==========
|
// ========== 前端扩展字段(user-per-group 维度) ==========
|
||||||
silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
||||||
|
|
@ -232,5 +231,5 @@ export interface GroupLite {
|
||||||
showImageThumb?: string
|
showImageThumb?: string
|
||||||
memberCount?: number
|
memberCount?: number
|
||||||
ownerId?: number
|
ownerId?: number
|
||||||
joinType?: number // 加群方式;参见 ImGroupJoinType
|
joinApproval?: boolean // 进群是否需群主 / 管理员审批
|
||||||
}
|
}
|
||||||
|
|
@ -157,20 +157,6 @@ export const ImGroupMemberRole = {
|
||||||
NORMAL: 3 // 普通成员
|
NORMAL: 3 // 普通成员
|
||||||
} as const
|
} 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) */
|
/** 加群来源(对齐后端 ImGroupAddSourceEnum) */
|
||||||
export const ImGroupAddSource = {
|
export const ImGroupAddSource = {
|
||||||
SEARCH: 1, // 搜索
|
SEARCH: 1, // 搜索
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,8 @@ export type GroupNotificationPayload = {
|
||||||
mutedUserId?: number // 禁言目标用户
|
mutedUserId?: number // 禁言目标用户
|
||||||
muteEndTime?: string // 禁言到期时间
|
muteEndTime?: string // 禁言到期时间
|
||||||
banned?: boolean // 封禁状态
|
banned?: boolean // 封禁状态
|
||||||
|
entrantUserId?: number // 自由进群事件:进群者用户编号
|
||||||
|
addSource?: number // 自由进群事件:来源(搜索 / 二维码 / 分享链接)
|
||||||
/** PIN 事件携带的完整被置顶消息对象 */
|
/** PIN 事件携带的完整被置顶消息对象 */
|
||||||
message?: {
|
message?: {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -223,6 +225,11 @@ export function resolveGroupNotificationText(
|
||||||
return `${operatorName} 解散了群聊`
|
return `${operatorName} 解散了群聊`
|
||||||
case ImMessageType.GROUP_MEMBER_INVITE:
|
case ImMessageType.GROUP_MEMBER_INVITE:
|
||||||
return `${operatorName} 邀请 ${memberNames} 加入群聊`
|
return `${operatorName} 邀请 ${memberNames} 加入群聊`
|
||||||
|
case ImMessageType.GROUP_MEMBER_ENTER: {
|
||||||
|
// 自由进群 / 主动申请通过:操作人 = 进群者;文案统一展示「XX 加入了群聊」
|
||||||
|
const entrantName = payload.entrantUserId ? resolve(payload.entrantUserId) : operatorName
|
||||||
|
return `${entrantName} 加入了群聊`
|
||||||
|
}
|
||||||
case ImMessageType.GROUP_MEMBER_QUIT:
|
case ImMessageType.GROUP_MEMBER_QUIT:
|
||||||
return `${operatorName} 退出了群聊`
|
return `${operatorName} 退出了群聊`
|
||||||
case ImMessageType.GROUP_MEMBER_KICK:
|
case ImMessageType.GROUP_MEMBER_KICK:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue