✨ feat(im): 增加群角色(管理员)
parent
3146f64edc
commit
fa27c27831
|
|
@ -27,6 +27,18 @@ export interface ImGroupUpdateReqVO {
|
||||||
notice?: string // 群公告
|
notice?: string // 群公告
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 / 撤销群管理员 Request VO
|
||||||
|
export interface ImGroupAdminReqVO {
|
||||||
|
groupId: number // 群编号
|
||||||
|
userIds: number[] // 目标用户编号列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群主转让 Request VO
|
||||||
|
export interface ImGroupTransferOwnerReqVO {
|
||||||
|
groupId: number // 群编号
|
||||||
|
newOwnerUserId: number // 新群主用户编号
|
||||||
|
}
|
||||||
|
|
||||||
// 获得当前登录用户的群列表
|
// 获得当前登录用户的群列表
|
||||||
export const getMyGroupList = () => {
|
export const getMyGroupList = () => {
|
||||||
return request.get<ImGroupRespVO[]>({ url: '/im/group/list' })
|
return request.get<ImGroupRespVO[]>({ url: '/im/group/list' })
|
||||||
|
|
@ -51,3 +63,18 @@ export const updateGroup = (data: ImGroupUpdateReqVO) => {
|
||||||
export const dissolveGroup = (id: number | string) => {
|
export const dissolveGroup = (id: number | string) => {
|
||||||
return request.delete<boolean>({ url: '/im/group/dissolve', params: { id } })
|
return request.delete<boolean>({ url: '/im/group/dissolve', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加群管理员(仅群主可调)
|
||||||
|
export const addGroupAdmin = (data: ImGroupAdminReqVO) => {
|
||||||
|
return request.put<boolean>({ url: '/im/group/add-admin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤销群管理员(仅群主可调)
|
||||||
|
export const removeGroupAdmin = (data: ImGroupAdminReqVO) => {
|
||||||
|
return request.put<boolean>({ url: '/im/group/remove-admin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转让群主(仅老群主可调;旧群主转让后降为普通成员)
|
||||||
|
export const transferGroupOwner = (data: ImGroupTransferOwnerReqVO) => {
|
||||||
|
return request.put<boolean>({ url: '/im/group/transfer-owner', data })
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ export interface ImGroupMemberRespVO {
|
||||||
groupId: number // 群编号
|
groupId: number // 群编号
|
||||||
userId: number // 用户编号
|
userId: number // 用户编号
|
||||||
displayUserName?: string // 组内显示名(群主设置的备注)
|
displayUserName?: string // 组内显示名(群主设置的备注)
|
||||||
displayGroupName?: string // 群显示备注(当前用户对群的备注)
|
groupRemark?: string // 群备注(当前用户对群的备注)
|
||||||
muted?: boolean // 是否免打扰
|
muted?: boolean // 是否免打扰
|
||||||
status?: number // 成员状态(0=在群,1=退群)
|
status?: number // 成员状态(0=在群,1=退群)
|
||||||
|
role?: number // 成员角色,参见 ImGroupMemberRole 枚举
|
||||||
joinTime?: string // 入群时间
|
joinTime?: string // 入群时间
|
||||||
quitTime?: string // 退群时间
|
quitTime?: string // 退群时间
|
||||||
createTime?: string // 创建时间
|
createTime?: string // 创建时间
|
||||||
|
|
@ -33,7 +34,7 @@ export interface ImGroupMemberRemoveReqVO {
|
||||||
export interface ImGroupMemberUpdateReqVO {
|
export interface ImGroupMemberUpdateReqVO {
|
||||||
groupId: number // 群编号
|
groupId: number // 群编号
|
||||||
displayUserName?: string // 群内昵称
|
displayUserName?: string // 群内昵称
|
||||||
displayGroupName?: string // 群名备注
|
groupRemark?: string // 群备注
|
||||||
muted?: boolean // 是否免打扰
|
muted?: boolean // 是否免打扰
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ export interface ImManagerGroupMemberVO {
|
||||||
nickname?: string
|
nickname?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
displayUserName?: string
|
displayUserName?: string
|
||||||
displayGroupName?: string
|
groupRemark?: string
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
status: number
|
status: number
|
||||||
|
role?: number // 成员角色,参见 ImGroupMemberRole 枚举
|
||||||
joinTime?: Date
|
joinTime?: Date
|
||||||
quitTime?: Date
|
quitTime?: Date
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -333,5 +333,6 @@ export enum DICT_TYPE {
|
||||||
IM_GROUP_MESSAGE_STATUS = 'im_group_message_status', // IM 群聊消息状态:0=正常 / 2=已撤回
|
IM_GROUP_MESSAGE_STATUS = 'im_group_message_status', // IM 群聊消息状态:0=正常 / 2=已撤回
|
||||||
IM_GROUP_MESSAGE_RECEIPT_STATUS = 'im_group_message_receipt_status', // IM 群消息回执状态
|
IM_GROUP_MESSAGE_RECEIPT_STATUS = 'im_group_message_receipt_status', // IM 群消息回执状态
|
||||||
IM_FRIEND_STATUS = 'im_friend_status', // IM 好友状态
|
IM_FRIEND_STATUS = 'im_friend_status', // IM 好友状态
|
||||||
IM_GROUP_STATUS = 'im_group_status' // IM 群状态
|
IM_GROUP_STATUS = 'im_group_status', // IM 群状态
|
||||||
|
IM_GROUP_MEMBER_ROLE = 'im_group_member_role' // IM 群成员角色
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export interface GroupMemberLite {
|
||||||
showName: string // 展示昵称:好友备注 > 用户群备注(displayUserName) > 真实昵称(nickname),给"显示给用户看"的位置用(行内文字、@候选标签等)
|
showName: string // 展示昵称:好友备注 > 用户群备注(displayUserName) > 真实昵称(nickname),给"显示给用户看"的位置用(行内文字、@候选标签等)
|
||||||
avatar?: string
|
avatar?: string
|
||||||
status?: number
|
status?: number
|
||||||
|
role?: number // 成员角色,仅在群信息抽屉等需要展示角色标签的场景透传;@候选 / 已读列表等场景可不传
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,14 @@
|
||||||
>
|
>
|
||||||
{{ member.showName }}
|
{{ member.showName }}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 角色标签:群主 / 管理员;普通成员不显示。仅在传入 member.role 时生效 -->
|
||||||
|
<span
|
||||||
|
v-if="roleLabel"
|
||||||
|
class="px-1.5 py-px rounded text-xs whitespace-nowrap"
|
||||||
|
:class="roleLabelClass"
|
||||||
|
>
|
||||||
|
{{ roleLabel }}
|
||||||
|
</span>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -32,6 +40,8 @@ import { computed } from 'vue'
|
||||||
|
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import UserAvatar from '../user/UserAvatar.vue'
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './GroupMember.vue'
|
||||||
|
import { ImGroupMemberRole } from '../../../utils/constants'
|
||||||
|
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMemberItem' })
|
defineOptions({ name: 'ImGroupMemberItem' })
|
||||||
|
|
||||||
|
|
@ -52,4 +62,21 @@ defineEmits<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const avatarSize = computed(() => Math.ceil(props.height * 0.75))
|
const avatarSize = computed(() => Math.ceil(props.height * 0.75))
|
||||||
|
|
||||||
|
/** 角色标签文案:普通成员不显示,其余取 im_group_member_role 字典 label */
|
||||||
|
// TODO DONE @AI:排除成员,剩余通过字典去 get 下,这样逻辑更统一!
|
||||||
|
const roleLabel = computed(() => {
|
||||||
|
if (props.member.role == null || props.member.role === ImGroupMemberRole.NORMAL) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return getDictLabel(DICT_TYPE.IM_GROUP_MEMBER_ROLE, props.member.role)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 角色标签样式:群主用主色;管理员用次要色 */
|
||||||
|
const roleLabelClass = computed(() => {
|
||||||
|
if (props.member.role === ImGroupMemberRole.OWNER) {
|
||||||
|
return 'text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)]'
|
||||||
|
}
|
||||||
|
return 'text-[var(--el-color-info)] bg-[var(--el-fill-color)]'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@ function convertGroupMemberLite(member: GroupMember, friend: Friend | undefined)
|
||||||
showName: getMemberDisplayName(member, friend),
|
showName: getMemberDisplayName(member, friend),
|
||||||
nickname: member.nickname,
|
nickname: member.nickname,
|
||||||
avatar: member.avatar,
|
avatar: member.avatar,
|
||||||
status: member.status
|
status: member.status,
|
||||||
|
role: member.role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,7 @@ const groups = computed<GroupLite[]>(() =>
|
||||||
groupStore.groups.map((group: Group) => ({
|
groupStore.groups.map((group: Group) => ({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
// 优先用群备注 displayGroupName,没设置时回落到原群名;避免点"进入群聊"时把已同步的备注会话名刷回原名
|
showGroupName: getGroupDisplayName(group), // 优先用群备注 groupRemark,没设置时回落到原群名;避免点"进入群聊"时把已同步的备注会话名刷回原名
|
||||||
showGroupName: getGroupDisplayName(group),
|
|
||||||
showImage: group.avatar,
|
showImage: group.avatar,
|
||||||
showImageThumb: group.avatar,
|
showImageThumb: group.avatar,
|
||||||
memberCount: group.memberCount
|
memberCount: group.memberCount
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<div v-if="group" class="im-conversation-group-side flex flex-col h-full">
|
<div v-if="group" class="im-conversation-group-side flex flex-col h-full">
|
||||||
<!-- 上部:可滚动内容区 -->
|
<!-- 上部:可滚动内容区 -->
|
||||||
<div class="im-conversation-group-side__scroll flex-1 overflow-y-auto">
|
<div class="im-conversation-group-side__scroll flex-1 overflow-y-auto">
|
||||||
<!-- 群成员区 -->
|
<!-- ==================== 群成员区 ==================== -->
|
||||||
<div class="im-conversation-group-side__section im-conversation-group-side__members">
|
<div class="im-conversation-group-side__section im-conversation-group-side__members">
|
||||||
<el-input v-model="searchText" placeholder="搜索群成员" clearable>
|
<el-input v-model="searchText" placeholder="搜索群成员" clearable>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
|
|
@ -43,9 +43,9 @@
|
||||||
<div class="im-conversation-group-side__tile-label">添加</div>
|
<div class="im-conversation-group-side__tile-label">添加</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 移出(仅群主) -->
|
<!-- 移出(群主或管理员;管理员只能移出普通成员,由后端校验) -->
|
||||||
<div
|
<div
|
||||||
v-if="isOwner"
|
v-if="isOwnerOrAdmin"
|
||||||
class="im-conversation-group-side__tile-wrap"
|
class="im-conversation-group-side__tile-wrap"
|
||||||
title="移出群成员"
|
title="移出群成员"
|
||||||
@click="removeVisible = true"
|
@click="removeVisible = true"
|
||||||
|
|
@ -70,7 +70,8 @@
|
||||||
|
|
||||||
<div class="im-conversation-group-side__spacer"></div>
|
<div class="im-conversation-group-side__spacer"></div>
|
||||||
|
|
||||||
<!-- 群信息:label 在上、value 在下,纵向堆叠(对齐微信 PC 设计);只有 "群公告" 因为内容长加 > chevron -->
|
<!-- ==================== 群信息 ==================== -->
|
||||||
|
<!-- label 在上、value 在下,纵向堆叠(对齐微信 PC 设计);只有 "群公告" 因为内容长加 > chevron -->
|
||||||
<div class="im-conversation-group-side__section">
|
<div class="im-conversation-group-side__section">
|
||||||
<!-- 群聊名称(群主可改) -->
|
<!-- 群聊名称(群主可改) -->
|
||||||
<el-popover
|
<el-popover
|
||||||
|
|
@ -162,9 +163,9 @@
|
||||||
<span v-else class="im-conversation-group-side__value-placeholder">未设置</span>
|
<span v-else class="im-conversation-group-side__value-placeholder">未设置</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 备注(仅自己可见的群备注;后端没字段,落 localStorage 做"个人记忆"用) -->
|
<!-- 备注(仅自己可见;保存后会替换会话列表 / 顶部群名展示) -->
|
||||||
<el-popover
|
<el-popover
|
||||||
v-model:visible="notePopoverVisible"
|
v-model:visible="groupRemarkPopoverVisible"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="left-start"
|
placement="left-start"
|
||||||
:width="280"
|
:width="280"
|
||||||
|
|
@ -175,28 +176,28 @@
|
||||||
>
|
>
|
||||||
<span class="im-conversation-group-side__label">备注</span>
|
<span class="im-conversation-group-side__label">备注</span>
|
||||||
<span
|
<span
|
||||||
v-if="personalNote"
|
v-if="group.groupRemark"
|
||||||
class="im-conversation-group-side__value im-conversation-group-side__value--clamp"
|
class="im-conversation-group-side__value im-conversation-group-side__value--clamp"
|
||||||
>
|
>
|
||||||
{{ personalNote }}
|
{{ group.groupRemark }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="im-conversation-group-side__value-placeholder">
|
||||||
|
群聊的备注仅自己可见
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="im-conversation-group-side__value-placeholder"
|
|
||||||
>群聊的备注仅自己可见</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="editNote"
|
v-model="editGroupRemark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
maxlength="200"
|
maxlength="64"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="仅自己可见"
|
placeholder="仅自己可见"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<el-button size="small" @click="notePopoverVisible = false">取消</el-button>
|
<el-button size="small" @click="groupRemarkPopoverVisible = false">取消</el-button>
|
||||||
<el-button size="small" type="primary" @click="saveNote">保存</el-button>
|
<el-button size="small" type="primary" @click="saveGroupRemark">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
@ -219,9 +220,7 @@
|
||||||
>
|
>
|
||||||
{{ group.remarkNickName }}
|
{{ group.remarkNickName }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="im-conversation-group-side__value-placeholder"
|
<span v-else class="im-conversation-group-side__value-placeholder">点击设置</span>
|
||||||
>点击设置</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|
@ -236,7 +235,8 @@
|
||||||
|
|
||||||
<div class="im-conversation-group-side__spacer"></div>
|
<div class="im-conversation-group-side__spacer"></div>
|
||||||
|
|
||||||
<!-- 查找聊天内容(点击 → 父组件打开 MessageHistory 弹窗) -->
|
<!-- ==================== 查找聊天内容 ==================== -->
|
||||||
|
<!-- 点击 → 父组件打开 MessageHistory 弹窗 -->
|
||||||
<div class="im-conversation-group-side__section">
|
<div class="im-conversation-group-side__section">
|
||||||
<div
|
<div
|
||||||
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||||
|
|
@ -253,7 +253,7 @@
|
||||||
|
|
||||||
<div class="im-conversation-group-side__spacer"></div>
|
<div class="im-conversation-group-side__spacer"></div>
|
||||||
|
|
||||||
<!-- 开关项 -->
|
<!-- ==================== 开关项 ==================== -->
|
||||||
<div class="im-conversation-group-side__section">
|
<div class="im-conversation-group-side__section">
|
||||||
<div class="im-conversation-group-side__row">
|
<div class="im-conversation-group-side__row">
|
||||||
<span class="im-conversation-group-side__label">消息免打扰</span>
|
<span class="im-conversation-group-side__label">消息免打扰</span>
|
||||||
|
|
@ -264,17 +264,54 @@
|
||||||
<el-switch :model-value="!!conversation?.top" @change="onTopChange" />
|
<el-switch :model-value="!!conversation?.top" @change="onTopChange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 群主操作 ==================== -->
|
||||||
|
<!-- 仅群主可见,含管理员设置 + 群主管理权转让 -->
|
||||||
|
<template v-if="isOwner">
|
||||||
|
<div class="im-conversation-group-side__spacer"></div>
|
||||||
|
<div class="im-conversation-group-side__section">
|
||||||
|
<div
|
||||||
|
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||||
|
@click="adminVisible = true"
|
||||||
|
>
|
||||||
|
<span class="im-conversation-group-side__label">群管理员</span>
|
||||||
|
<Icon
|
||||||
|
icon="ant-design:right-outlined"
|
||||||
|
:size="11"
|
||||||
|
class="im-conversation-group-side__chevron"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||||
|
@click="transferOwnerVisible = true"
|
||||||
|
>
|
||||||
|
<span class="im-conversation-group-side__label">群主管理权转让</span>
|
||||||
|
<Icon
|
||||||
|
icon="ant-design:right-outlined"
|
||||||
|
:size="11"
|
||||||
|
class="im-conversation-group-side__chevron"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部:退出群聊(仅非群主入口;群主退出走"解散群"另起一条路径,这里不处理) -->
|
<!-- ==================== 底部:退出群聊 ==================== -->
|
||||||
|
<!-- 仅非群主入口;群主退出走"解散群"另起一条路径,这里不处理 -->
|
||||||
<div v-if="!isOwner" class="im-conversation-group-side__footer">
|
<div v-if="!isOwner" class="im-conversation-group-side__footer">
|
||||||
<el-button class="im-conversation-group-side__quit-btn" type="danger" plain @click="handleQuit">
|
<el-button
|
||||||
|
class="im-conversation-group-side__quit-btn"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
@click="handleQuit"
|
||||||
|
>
|
||||||
退出群聊
|
退出群聊
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 子对话框:邀请新成员 / 选成员移除 -->
|
<!-- ==================== 子对话框 ==================== -->
|
||||||
|
<!-- 邀请新成员 / 选成员移除 -->
|
||||||
<GroupMemberAddDialog
|
<GroupMemberAddDialog
|
||||||
v-model="inviteVisible"
|
v-model="inviteVisible"
|
||||||
:group-id="group?.id"
|
:group-id="group?.id"
|
||||||
|
|
@ -286,10 +323,29 @@
|
||||||
v-model="removeVisible"
|
v-model="removeVisible"
|
||||||
title="选择成员进行移除"
|
title="选择成员进行移除"
|
||||||
:members="members"
|
:members="members"
|
||||||
:hide-ids="group?.ownerId ? [group.ownerId] : []"
|
:hide-ids="removeHideIds"
|
||||||
:max-size="50"
|
:max-size="50"
|
||||||
@complete="handleRemoveComplete"
|
@complete="handleRemoveComplete"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 群主操作:管理员设置(一个弹窗合并增 / 删,提交时 diff)+ 群主管理权转让 -->
|
||||||
|
<GroupMemberSelector
|
||||||
|
v-model="adminVisible"
|
||||||
|
title="设置群管理员"
|
||||||
|
:members="members"
|
||||||
|
:checked-ids="adminCheckedIds"
|
||||||
|
:hide-ids="adminHideIds"
|
||||||
|
:max-size="GROUP_ADMIN_MAX_COUNT"
|
||||||
|
@complete="handleAdminUpdate"
|
||||||
|
/>
|
||||||
|
<GroupMemberSelector
|
||||||
|
v-model="transferOwnerVisible"
|
||||||
|
title="选择新群主"
|
||||||
|
:members="members"
|
||||||
|
:hide-ids="transferOwnerHideIds"
|
||||||
|
:max-size="1"
|
||||||
|
@complete="handleTransferOwnerComplete"
|
||||||
|
/>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -300,11 +356,15 @@ import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
import { updateGroup } from '@/api/im/group'
|
import { updateGroup, addGroupAdmin, removeGroupAdmin, transferGroupOwner } from '@/api/im/group'
|
||||||
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 { ImConversationType } 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, {
|
||||||
|
|
@ -315,14 +375,12 @@ import type { GroupMemberLite } from '../../../../components/group/GroupMember.v
|
||||||
|
|
||||||
defineOptions({ name: 'ImConversationGroupSide' })
|
defineOptions({ name: 'ImConversationGroupSide' })
|
||||||
|
|
||||||
/** 大群默认只展示前 N 个成员,避免抽屉一打开拖到很长 */
|
const MEMBER_PREVIEW_COUNT = 14 // 大群默认只展示前 N 个成员(4×4 宫格 − 2 个瓦片预留给"添加 / 移出"按钮)
|
||||||
// TODO @AI:需要解释下,为什么是 14 个。因为 4 x 4 - 2,,2 预留给添加和移除
|
|
||||||
const MEMBER_PREVIEW_COUNT = 14
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue?: boolean // 抽屉是否打开(v-model)
|
modelValue?: boolean // 抽屉是否打开(v-model)
|
||||||
group?: GroupLite & { notice?: string; remarkNickName?: string } // 当前群信息(可空:无激活群会话时)
|
group?: GroupLite & { notice?: string; remarkNickName?: string; groupRemark?: string } // 当前群信息(可空:无激活群会话时)
|
||||||
conversation?: Conversation | null // 当前会话:用于读 / 切免打扰、置顶状态
|
conversation?: Conversation | null // 当前会话:用于读 / 切免打扰、置顶状态
|
||||||
members?: GroupMemberLite[]
|
members?: GroupMemberLite[]
|
||||||
friends?: FriendLite[]
|
friends?: FriendLite[]
|
||||||
|
|
@ -353,27 +411,19 @@ const visible = computed({
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const inviteVisible = ref(false)
|
const inviteVisible = ref(false)
|
||||||
const removeVisible = ref(false)
|
const removeVisible = ref(false)
|
||||||
|
const adminVisible = ref(false)
|
||||||
|
const transferOwnerVisible = ref(false)
|
||||||
const showAllMembers = ref(false)
|
const showAllMembers = ref(false)
|
||||||
const namePopoverVisible = ref(false)
|
const namePopoverVisible = ref(false)
|
||||||
const noticePopoverVisible = ref(false)
|
const noticePopoverVisible = ref(false)
|
||||||
const remarkPopoverVisible = ref(false)
|
const remarkPopoverVisible = ref(false)
|
||||||
const notePopoverVisible = ref(false)
|
const groupRemarkPopoverVisible = ref(false)
|
||||||
const editName = ref('')
|
const editName = ref('')
|
||||||
const editNotice = ref('')
|
const editNotice = ref('')
|
||||||
const editRemark = ref('')
|
const editRemark = ref('')
|
||||||
const editNote = ref('')
|
const editGroupRemark = ref('')
|
||||||
|
|
||||||
// 备注是"仅自己可见"的本地标签,后端没字段;按 groupId 落 localStorage,跟当前群绑定
|
// ==================== 状态同步 watch ====================
|
||||||
const NOTE_STORAGE_PREFIX = 'im:group:note:'
|
|
||||||
const personalNote = ref('')
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.group?.id,
|
|
||||||
(groupId) => {
|
|
||||||
personalNote.value = groupId ? localStorage.getItem(NOTE_STORAGE_PREFIX + groupId) || '' : ''
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 抽屉关闭时重置临时态:搜索 / 折叠展开 / 还在打开的 popover 都清掉
|
// 抽屉关闭时重置临时态:搜索 / 折叠展开 / 还在打开的 popover 都清掉
|
||||||
watch(visible, (v) => {
|
watch(visible, (v) => {
|
||||||
|
|
@ -383,7 +433,7 @@ watch(visible, (v) => {
|
||||||
namePopoverVisible.value = false
|
namePopoverVisible.value = false
|
||||||
noticePopoverVisible.value = false
|
noticePopoverVisible.value = false
|
||||||
remarkPopoverVisible.value = false
|
remarkPopoverVisible.value = false
|
||||||
notePopoverVisible.value = false
|
groupRemarkPopoverVisible.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -397,28 +447,40 @@ watch(noticePopoverVisible, (v) => {
|
||||||
watch(remarkPopoverVisible, (v) => {
|
watch(remarkPopoverVisible, (v) => {
|
||||||
if (v) editRemark.value = props.group?.remarkNickName || ''
|
if (v) editRemark.value = props.group?.remarkNickName || ''
|
||||||
})
|
})
|
||||||
watch(notePopoverVisible, (v) => {
|
watch(groupRemarkPopoverVisible, (v) => {
|
||||||
if (v) editNote.value = personalNote.value
|
if (v) editGroupRemark.value = props.group?.groupRemark || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ==================== 角色 / 成员展示 ====================
|
||||||
|
|
||||||
const myId = computed(() => Number(userStore.getUser?.id) || 0)
|
const myId = computed(() => Number(userStore.getUser?.id) || 0)
|
||||||
const isOwner = computed(() => props.group != null && props.group.ownerId === myId.value)
|
const isOwner = computed(() => props.group != null && props.group.ownerId === myId.value)
|
||||||
|
/** 当前用户在群里的角色(来自 props.members 的 me 行);用于判定是否可移出他人 */
|
||||||
// 排除已退群成员 + 关键字过滤
|
const myRole = computed(() => props.members.find((m) => m.userId === myId.value)?.role)
|
||||||
const visibleMembers = computed(() =>
|
/** 群主或管理员:在抽屉里有 "移出群成员" 入口 */
|
||||||
props.members.filter(
|
const isOwnerOrAdmin = computed(
|
||||||
(member) =>
|
() => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN
|
||||||
member.status !== CommonStatusEnum.DISABLE &&
|
|
||||||
(member.showName || '').includes(searchText.value)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定)
|
||||||
|
const visibleMembers = computed(() => {
|
||||||
|
return props.members
|
||||||
|
.filter(
|
||||||
|
(member) =>
|
||||||
|
member.status !== CommonStatusEnum.DISABLE &&
|
||||||
|
(member.showName || '').includes(searchText.value)
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const roleA = a.role ?? ImGroupMemberRole.NORMAL
|
||||||
|
const roleB = b.role ?? ImGroupMemberRole.NORMAL
|
||||||
|
return roleA !== roleB ? roleA - roleB : a.userId - b.userId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// 折叠规则:搜索 / 已展开 时不折叠,其余只取前 N 个
|
// 折叠规则:搜索 / 已展开 时不折叠,其余只取前 N 个
|
||||||
const moreMembersHidden = computed(
|
const moreMembersHidden = computed(
|
||||||
() =>
|
() =>
|
||||||
!searchText.value &&
|
!searchText.value && !showAllMembers.value && visibleMembers.value.length > MEMBER_PREVIEW_COUNT
|
||||||
!showAllMembers.value &&
|
|
||||||
visibleMembers.value.length > MEMBER_PREVIEW_COUNT
|
|
||||||
)
|
)
|
||||||
const displayMembers = computed(() =>
|
const displayMembers = computed(() =>
|
||||||
moreMembersHidden.value
|
moreMembersHidden.value
|
||||||
|
|
@ -426,6 +488,8 @@ const displayMembers = computed(() =>
|
||||||
: visibleMembers.value
|
: visibleMembers.value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ==================== 群信息编辑 ====================
|
||||||
|
|
||||||
/** 群主:保存群名(走 /im/group/update) */
|
/** 群主:保存群名(走 /im/group/update) */
|
||||||
async function saveName() {
|
async function saveName() {
|
||||||
if (!props.group) {
|
if (!props.group) {
|
||||||
|
|
@ -448,19 +512,18 @@ async function saveNotice() {
|
||||||
emit('reload')
|
emit('reload')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 备注:仅本地 localStorage 落盘(后端无字段;多端不同步是已知限制) */
|
/** 任何成员:保存群备注(仅自己可见,会替换会话列表 / 顶部群名展示) */
|
||||||
function saveNote() {
|
async function saveGroupRemark() {
|
||||||
if (!props.group) {
|
if (!props.group) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const value = editNote.value.trim()
|
await updateGroupMember({
|
||||||
if (value) {
|
groupId: props.group.id,
|
||||||
localStorage.setItem(NOTE_STORAGE_PREFIX + props.group.id, value)
|
groupRemark: editGroupRemark.value.trim()
|
||||||
} else {
|
})
|
||||||
localStorage.removeItem(NOTE_STORAGE_PREFIX + props.group.id)
|
groupRemarkPopoverVisible.value = false
|
||||||
}
|
message.success('保存成功')
|
||||||
personalNote.value = value
|
emit('reload')
|
||||||
notePopoverVisible.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 任何成员:保存自己在群里的昵称(走 /im/group-member/update) */
|
/** 任何成员:保存自己在群里的昵称(走 /im/group-member/update) */
|
||||||
|
|
@ -477,6 +540,8 @@ async function saveRemark() {
|
||||||
emit('reload')
|
emit('reload')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 开关切换 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息免打扰:本地 conversationStore 立即切;后端 /muted 异步同步,失败回滚本地
|
* 消息免打扰:本地 conversationStore 立即切;后端 /muted 异步同步,失败回滚本地
|
||||||
*
|
*
|
||||||
|
|
@ -504,6 +569,8 @@ function onTopChange(value: boolean | string | number) {
|
||||||
conversationStore.setTop(props.conversation.type, props.conversation.targetId, !!value)
|
conversationStore.setTop(props.conversation.type, props.conversation.targetId, !!value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 退出群聊 ====================
|
||||||
|
|
||||||
/** 退出群聊(普通成员入口;群主退出走"解散群"是另一条路径,这里不处理) */
|
/** 退出群聊(普通成员入口;群主退出走"解散群"是另一条路径,这里不处理) */
|
||||||
async function handleQuit() {
|
async function handleQuit() {
|
||||||
if (!props.group) {
|
if (!props.group) {
|
||||||
|
|
@ -524,7 +591,26 @@ async function handleQuit() {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除群成员(仅群主入口)*/
|
// ==================== 群主操作 ====================
|
||||||
|
// 移除群成员(群主 / 管理员可见)+ 设置群管理员(仅群主)+ 群主管理权转让(仅群主)
|
||||||
|
|
||||||
|
// ---------- 移除群成员 ----------
|
||||||
|
|
||||||
|
/** 移除群成员的 hideIds:始终隐藏群主;管理员视角额外隐藏其它管理员(管理员不能移出管理员) */
|
||||||
|
const removeHideIds = computed(() => {
|
||||||
|
const hideIds: number[] = []
|
||||||
|
if (props.group?.ownerId) {
|
||||||
|
hideIds.push(props.group.ownerId)
|
||||||
|
}
|
||||||
|
if (myRole.value === ImGroupMemberRole.ADMIN) {
|
||||||
|
props.members
|
||||||
|
.filter((m) => m.role === ImGroupMemberRole.ADMIN)
|
||||||
|
.forEach((m) => hideIds.push(m.userId))
|
||||||
|
}
|
||||||
|
return hideIds
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 移除群成员入口(群主可移出任意非群主,管理员只能移出普通成员;具体由后端校验) */
|
||||||
async function handleRemoveComplete(members: GroupMemberFlag[]) {
|
async function handleRemoveComplete(members: GroupMemberFlag[]) {
|
||||||
if (!props.group || members.length === 0) {
|
if (!props.group || members.length === 0) {
|
||||||
return
|
return
|
||||||
|
|
@ -537,6 +623,72 @@ async function handleRemoveComplete(members: GroupMemberFlag[]) {
|
||||||
message.success(`已移除 ${members.length} 位成员`)
|
message.success(`已移除 ${members.length} 位成员`)
|
||||||
emit('reload')
|
emit('reload')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 设置群管理员 ----------
|
||||||
|
|
||||||
|
/** 当前管理员的 userId 列表,作为 Selector 默认勾选 */
|
||||||
|
const adminCheckedIds = computed(() =>
|
||||||
|
props.members
|
||||||
|
.filter((member) => member.role === ImGroupMemberRole.ADMIN)
|
||||||
|
.map((member) => member.userId)
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 设置管理员时隐藏:群主(不能设为管理员) */
|
||||||
|
const adminHideIds = computed(() => (props.group?.ownerId ? [props.group.ownerId] : []))
|
||||||
|
|
||||||
|
/** 群管理员变更:跟当前管理员列表 diff,新增 → addGroupAdmin,撤销 → removeGroupAdmin */
|
||||||
|
async function handleAdminUpdate(selected: GroupMemberFlag[]) {
|
||||||
|
if (!props.group) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 跟当前管理员列表做差集:分别拿到要新增 / 撤销的 userId
|
||||||
|
const previousIds = adminCheckedIds.value
|
||||||
|
const previousIdSet = new Set(previousIds)
|
||||||
|
const selectedIds = selected.map((member) => member.userId)
|
||||||
|
const selectedIdSet = new Set(selectedIds)
|
||||||
|
const addedIds = selectedIds.filter((id) => !previousIdSet.has(id))
|
||||||
|
const removedIds = previousIds.filter((id) => !selectedIdSet.has(id))
|
||||||
|
if (addedIds.length === 0 && removedIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (addedIds.length > 0) {
|
||||||
|
await addGroupAdmin({ groupId: props.group.id, userIds: addedIds })
|
||||||
|
}
|
||||||
|
if (removedIds.length > 0) {
|
||||||
|
await removeGroupAdmin({ groupId: props.group.id, userIds: removedIds })
|
||||||
|
}
|
||||||
|
message.success(`已更新群管理员(新增 ${addedIds.length} 位,撤销 ${removedIds.length} 位)`)
|
||||||
|
emit('reload')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 群主管理权转让 ----------
|
||||||
|
|
||||||
|
/** 转让群主时隐藏当前用户(不能转让给自己) */
|
||||||
|
const transferOwnerHideIds = computed(() => [myId.value])
|
||||||
|
|
||||||
|
async function handleTransferOwnerComplete(selected: GroupMemberFlag[]) {
|
||||||
|
if (!props.group || selected.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newOwner = selected[0]
|
||||||
|
// 二次确认:转让后旧群主降为普通成员
|
||||||
|
try {
|
||||||
|
await message.confirm(
|
||||||
|
`确定将群主转让给 ${newOwner.showName}?转让后你将变为普通成员,无法撤销。`,
|
||||||
|
'确认转让群主'
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 转让群主
|
||||||
|
await transferGroupOwner({
|
||||||
|
groupId: props.group.id,
|
||||||
|
newOwnerUserId: newOwner.userId
|
||||||
|
})
|
||||||
|
// 提示结果 + 刷新数据
|
||||||
|
message.success('群主转让成功')
|
||||||
|
emit('reload')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,26 @@
|
||||||
<div
|
<div
|
||||||
class="message-panel__header flex items-center justify-between h-14 px-5 bg-[var(--el-fill-color-light)]"
|
class="message-panel__header flex items-center justify-between h-14 px-5 bg-[var(--el-fill-color-light)]"
|
||||||
>
|
>
|
||||||
<span class="flex items-baseline gap-1.5 min-w-0">
|
<span class="flex flex-col min-w-0">
|
||||||
<span
|
<span class="flex items-baseline gap-1.5 min-w-0">
|
||||||
class="overflow-hidden text-base font-medium truncate text-[var(--el-text-color-primary)]"
|
<span
|
||||||
>
|
class="overflow-hidden text-base font-medium truncate text-[var(--el-text-color-primary)]"
|
||||||
{{ conversationStore.activeConversation?.name || '' }}
|
>
|
||||||
|
{{ conversationStore.activeConversation?.name || '' }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isGroup && headerMemberCount > 0"
|
||||||
|
class="flex-shrink-0 text-sm text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
|
({{ headerMemberCount }})
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<!-- 副标题:备注 ≠ 群名时展示原群名,提示用户当前看到的主名是自己设的备注 -->
|
||||||
<span
|
<span
|
||||||
v-if="isGroup && headerMemberCount > 0"
|
v-if="headerSubtitle"
|
||||||
class="flex-shrink-0 text-sm text-[var(--el-text-color-secondary)]"
|
class="overflow-hidden text-xs truncate text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
({{ headerMemberCount }})
|
{{ headerSubtitle }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
|
|
@ -170,6 +179,13 @@ const headerMemberCount = computed(() => {
|
||||||
return group?.memberCount ?? group?.members?.length ?? 0
|
return group?.memberCount ?? group?.members?.length ?? 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 顶部副标题:仅当群备注 ≠ 原群名时显示原群名(对齐微信 PC 双行 header) */
|
||||||
|
const headerSubtitle = computed(() => {
|
||||||
|
const remark = groupInfo.value?.groupRemark
|
||||||
|
const name = groupInfo.value?.name
|
||||||
|
return remark && name && remark !== name ? name : ''
|
||||||
|
})
|
||||||
|
|
||||||
const BOTTOM_THRESHOLD = 80 // "是否停留在底部"的阈值:距离底部 < 80px 视为底部
|
const BOTTOM_THRESHOLD = 80 // "是否停留在底部"的阈值:距离底部 < 80px 视为底部
|
||||||
const showJumpToBottom = ref(false) // 当前是否已不在底部(显示"回到底部"按钮)
|
const showJumpToBottom = ref(false) // 当前是否已不在底部(显示"回到底部"按钮)
|
||||||
const newMessageCount = ref(0) // 不在底部期间累计的新消息数
|
const newMessageCount = ref(0) // 不在底部期间累计的新消息数
|
||||||
|
|
@ -182,7 +198,13 @@ const newMessageCount = ref(0) // 不在底部期间累计的新消息数
|
||||||
* 必须等 store 就位才有值(这些字段在 conversation 里没有)
|
* 必须等 store 就位才有值(这些字段在 conversation 里没有)
|
||||||
*/
|
*/
|
||||||
const groupInfo = computed<
|
const groupInfo = computed<
|
||||||
(GroupLite & { notice?: string; remarkNickName?: string; ownerId?: number }) | undefined
|
| (GroupLite & {
|
||||||
|
notice?: string
|
||||||
|
remarkNickName?: string
|
||||||
|
groupRemark?: string
|
||||||
|
ownerId?: number
|
||||||
|
})
|
||||||
|
| undefined
|
||||||
>(() => {
|
>(() => {
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = conversationStore.activeConversation
|
||||||
if (!conversation || conversation.type !== ImConversationType.GROUP) {
|
if (!conversation || conversation.type !== ImConversationType.GROUP) {
|
||||||
|
|
@ -195,6 +217,7 @@ const groupInfo = computed<
|
||||||
showGroupName: group?.name || conversation.name,
|
showGroupName: group?.name || conversation.name,
|
||||||
showImage: group?.avatar || conversation.avatar,
|
showImage: group?.avatar || conversation.avatar,
|
||||||
notice: group?.notice,
|
notice: group?.notice,
|
||||||
|
groupRemark: group?.groupRemark,
|
||||||
ownerId: group?.ownerUserId,
|
ownerId: group?.ownerUserId,
|
||||||
memberCount: group?.memberCount
|
memberCount: group?.memberCount
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +238,8 @@ const groupMembers = computed<GroupMemberLite[]>(() => {
|
||||||
showName: getMemberDisplayName(member, friend),
|
showName: getMemberDisplayName(member, friend),
|
||||||
nickname: member.nickname,
|
nickname: member.nickname,
|
||||||
avatar: member.avatar,
|
avatar: member.avatar,
|
||||||
status: member.status
|
status: member.status,
|
||||||
|
role: member.role
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -731,7 +731,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
*
|
*
|
||||||
* 调用方负责把好友 / 群的信息整理成 Conversation 视角的字段:
|
* 调用方负责把好友 / 群的信息整理成 Conversation 视角的字段:
|
||||||
* - 私聊:name = friend.nickname;avatar = friend.avatar
|
* - 私聊:name = friend.nickname;avatar = friend.avatar
|
||||||
* - 群聊:name = group.name(或叠加 displayGroupName);avatar = group.avatar
|
* - 群聊:name = group.name(或叠加 groupRemark);avatar = group.avatar
|
||||||
*/
|
*/
|
||||||
updateConversation(
|
updateConversation(
|
||||||
type: number,
|
type: number,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +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 { ImConversationType } from '../../utils/constants'
|
import { ImConversationType, ImGroupMemberRole } from '../../utils/constants'
|
||||||
import {
|
import {
|
||||||
getCurrentUserId,
|
getCurrentUserId,
|
||||||
imStorage,
|
imStorage,
|
||||||
|
|
@ -170,7 +170,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 fetchGroupMembers)
|
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 fetchGroupMembers)
|
||||||
const list = await apiGetMyGroupList()
|
const list = await apiGetMyGroupList()
|
||||||
const fresh = (list || []).map(convertGroup)
|
const fresh = (list || []).map(convertGroup)
|
||||||
// 合并而非全量替换:保留 user-per-group 字段(muted / displayGroupName)+ 成员缓存——这些字段不在 ImGroupRespVO 里,全量替换会把它们冲掉
|
// 合并而非全量替换:muted / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留
|
||||||
const groupMap = new Map(this.groups.map((group) => [group.id, group]))
|
const groupMap = new Map(this.groups.map((group) => [group.id, group]))
|
||||||
this.groups = fresh.map((group) => {
|
this.groups = fresh.map((group) => {
|
||||||
const existing = groupMap.get(group.id)
|
const existing = groupMap.get(group.id)
|
||||||
|
|
@ -182,7 +182,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
members: existing.members,
|
members: existing.members,
|
||||||
memberCount: existing.memberCount ?? group.memberCount,
|
memberCount: existing.memberCount ?? group.memberCount,
|
||||||
muted: existing.muted ?? group.muted,
|
muted: existing.muted ?? group.muted,
|
||||||
displayGroupName: existing.displayGroupName,
|
groupRemark: existing.groupRemark,
|
||||||
membersLoaded: existing.membersLoaded
|
membersLoaded: existing.membersLoaded
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -230,8 +230,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
return inflight
|
return inflight
|
||||||
}
|
}
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
// 拉接口 + 单 pass 转换:同时捕获 me 的原始 VO,给下面回填 user-per-group 字段(muted / displayGroupName)用
|
// 拉接口 + 单 pass 转换:同时捕获 me 的原始 VO,给下面回填 user-per-group 字段(muted / groupRemark)用
|
||||||
// convertGroupMember 不带 muted / displayGroupName(已搬到 Group 上),所以从 raw VO 里捞
|
|
||||||
const list = await apiGetGroupMemberList(groupId)
|
const list = await apiGetGroupMemberList(groupId)
|
||||||
let meRaw: ImGroupMemberRespVO | undefined
|
let meRaw: ImGroupMemberRespVO | undefined
|
||||||
const members = (list || []).map((member) => {
|
const members = (list || []).map((member) => {
|
||||||
|
|
@ -241,7 +240,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
return convertGroupMember(member, groupId)
|
return convertGroupMember(member, groupId)
|
||||||
})
|
})
|
||||||
const muted = !!meRaw?.muted
|
const muted = !!meRaw?.muted
|
||||||
const displayGroupName = meRaw?.displayGroupName || ''
|
const groupRemark = meRaw?.groupRemark || ''
|
||||||
|
|
||||||
// 必须 await 之后重新 getGroup,避免 fetchGroups 已并发写入真实 group 的 race
|
// 必须 await 之后重新 getGroup,避免 fetchGroups 已并发写入真实 group 的 race
|
||||||
const group = this.getGroup(groupId)
|
const group = this.getGroup(groupId)
|
||||||
|
|
@ -256,18 +255,17 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
members,
|
members,
|
||||||
memberCount: members.length,
|
memberCount: members.length,
|
||||||
muted,
|
muted,
|
||||||
displayGroupName,
|
groupRemark,
|
||||||
membersLoaded: true
|
membersLoaded: true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
group.members = members
|
group.members = members
|
||||||
group.memberCount = members.length
|
group.memberCount = members.length
|
||||||
group.membersLoaded = true
|
group.membersLoaded = true
|
||||||
// muted / displayGroupName 任一变化就同步到 conversation + 触发 saveGroups;
|
// muted / groupRemark 任一变化才同步到 conversation 和 IDB;groupRemark 变化要顺带刷会话名
|
||||||
// 后续,displayGroupName 变化要刷会话名("我对该群的备注"是会话列表的展示名)
|
if (group.muted !== muted || group.groupRemark !== groupRemark) {
|
||||||
if (group.muted !== muted || group.displayGroupName !== displayGroupName) {
|
|
||||||
group.muted = muted
|
group.muted = muted
|
||||||
group.displayGroupName = displayGroupName
|
group.groupRemark = groupRemark
|
||||||
groupFieldsChanged = true
|
groupFieldsChanged = true
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
|
conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
|
||||||
|
|
@ -294,7 +292,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
/**
|
/**
|
||||||
* 按 (groupId, memberUserId) 单成员补齐——deriveLastSenderDisplayName 兜底场景用
|
* 按 (groupId, memberUserId) 单成员补齐——deriveLastSenderDisplayName 兜底场景用
|
||||||
*
|
*
|
||||||
* 跟 fetchGroupMembers 区别:只拉这一个成员,不动 me 的 muted / displayGroupName(不是 me 的话拿不到);
|
* 跟 fetchGroupMembers 区别:只拉这一个成员,不动 me 的 muted / groupRemark(不是 me 的话拿不到);
|
||||||
* 命中时把成员 upsert 进 group.members 数组并落 IDB,让后续渲染能用 displayUserName
|
* 命中时把成员 upsert 进 group.members 数组并落 IDB,让后续渲染能用 displayUserName
|
||||||
*/
|
*/
|
||||||
fetchGroupMember(groupId: number, memberUserId: number): Promise<GroupMember | null> {
|
fetchGroupMember(groupId: number, memberUserId: number): Promise<GroupMember | null> {
|
||||||
|
|
@ -395,6 +393,44 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
this.saveGroups()
|
this.saveGroups()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 批量更新群成员角色;本地不命中则忽略,等 fetchGroupMembers 兜底 */
|
||||||
|
updateMembersRole(groupId: number, userIds: number[], role: number) {
|
||||||
|
const group = this.getGroup(groupId)
|
||||||
|
if (!group?.members?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO @AI:计算是否更新?【优化注释】
|
||||||
|
const idSet = new Set(userIds)
|
||||||
|
let changed = false
|
||||||
|
// TODO @AI:newMembers 更合适?
|
||||||
|
// TODO @AI:m 是不是改成 member;
|
||||||
|
const next = group.members.map((m) => {
|
||||||
|
if (!idSet.has(m.userId) || m.role === role) {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
return { ...m, role }
|
||||||
|
})
|
||||||
|
// TODO @AI:有更新则进行替换,补充下注释【优化注释】
|
||||||
|
if (changed) {
|
||||||
|
group.members = next
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 群主转让:群表 ownerUserId 改为新值;旧群主 role → NORMAL;新群主 role → OWNER */
|
||||||
|
transferOwner(groupId: number, oldOwnerId: number, newOwnerId: number) {
|
||||||
|
const group = this.getGroup(groupId)
|
||||||
|
if (!group) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (group.ownerUserId !== newOwnerId) {
|
||||||
|
group.ownerUserId = newOwnerId
|
||||||
|
}
|
||||||
|
this.updateMembersRole(groupId, [oldOwnerId], ImGroupMemberRole.NORMAL)
|
||||||
|
this.updateMembersRole(groupId, [newOwnerId], ImGroupMemberRole.OWNER)
|
||||||
|
this.saveGroups()
|
||||||
|
},
|
||||||
|
|
||||||
/** 切账号时仅清 in-memory,IDB 按 userId 分桶天然隔离,回切秒开 */
|
/** 切账号时仅清 in-memory,IDB 按 userId 分桶天然隔离,回切秒开 */
|
||||||
clear() {
|
clear() {
|
||||||
this.groups = []
|
this.groups = []
|
||||||
|
|
@ -424,7 +460,8 @@ function convertGroupMember(member: ImGroupMemberRespVO, groupId: number): Group
|
||||||
nickname: member.nickname || String(member.userId),
|
nickname: member.nickname || String(member.userId),
|
||||||
avatar: member.avatar,
|
avatar: member.avatar,
|
||||||
displayUserName: member.displayUserName,
|
displayUserName: member.displayUserName,
|
||||||
status: member.status
|
status: member.status,
|
||||||
|
role: member.role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,8 @@ export interface Group {
|
||||||
ownerUserId?: number // 群主用户编号
|
ownerUserId?: number // 群主用户编号
|
||||||
|
|
||||||
// ========== 前端扩展字段(user-per-group 维度) ==========
|
// ========== 前端扩展字段(user-per-group 维度) ==========
|
||||||
muted?: boolean // 是否免打扰。从当前用户的 GroupMember 回填(当前用户对该群的自定义名)
|
muted?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
||||||
displayGroupName?: string // 群显示备注。从当前用户的 GroupMember 回填(当前用户对该群的自定义名)
|
groupRemark?: string // 群备注。从当前用户的 GroupMember 回填(当前用户对该群的自定义名)
|
||||||
members?: GroupMember[] // 群成员缓存(按需懒加载)
|
members?: GroupMember[] // 群成员缓存(按需懒加载)
|
||||||
membersLoaded?: boolean // members 是否"完整加载"——只有整群 loadGroupMembers / fetchGroupMembers 命中时为 true;fetchGroupMember 单成员补齐不置位,避免 fetchGroupMembers(force=false) 命中缓存时误判整群已加载
|
membersLoaded?: boolean // members 是否"完整加载"——只有整群 loadGroupMembers / fetchGroupMembers 命中时为 true;fetchGroupMember 单成员补齐不置位,避免 fetchGroupMembers(force=false) 命中缓存时误判整群已加载
|
||||||
memberCount?: number // 成员总数
|
memberCount?: number // 成员总数
|
||||||
|
|
@ -132,6 +132,7 @@ export interface GroupMember {
|
||||||
nickname: string // 用户昵称
|
nickname: string // 用户昵称
|
||||||
displayUserName?: string // 该成员在群内自定义昵称(每个 member 一份;不与 nickname 合并,由消费方按需取舍)
|
displayUserName?: string // 该成员在群内自定义昵称(每个 member 一份;不与 nickname 合并,由消费方按需取舍)
|
||||||
status?: number // 在群 / 退群状态,对齐 CommonStatusEnum
|
status?: number // 在群 / 退群状态,对齐 CommonStatusEnum
|
||||||
|
role?: number // 成员角色,参见 ImGroupMemberRole 枚举:1=群主 2=管理员 3=普通成员
|
||||||
|
|
||||||
// ========== 前端扩展字段 ==========
|
// ========== 前端扩展字段 ==========
|
||||||
isOwner?: boolean // 是否群主(前端从 Group.ownerUserId 计算)
|
isOwner?: boolean // 是否群主(前端从 Group.ownerUserId 计算)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="用户编号" prop="userId" width="100" align="center" />
|
<el-table-column label="用户编号" prop="userId" width="100" align="center" />
|
||||||
|
<el-table-column label="角色" prop="role" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<dict-tag :type="DICT_TYPE.IM_GROUP_MEMBER_ROLE" :value="row.role" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="昵称" prop="nickname" min-width="120" show-overflow-tooltip />
|
<el-table-column label="昵称" prop="nickname" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="组内显示名"
|
label="组内显示名"
|
||||||
|
|
@ -50,12 +55,12 @@
|
||||||
<template #default="{ row }">{{ row.displayUserName || '-' }}</template>
|
<template #default="{ row }">{{ row.displayUserName || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="群显示备注"
|
label="群备注"
|
||||||
prop="displayGroupName"
|
prop="groupRemark"
|
||||||
min-width="120"
|
min-width="120"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
>
|
>
|
||||||
<template #default="{ row }">{{ row.displayGroupName || '-' }}</template>
|
<template #default="{ row }">{{ row.groupRemark || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="免打扰" prop="muted" width="80" align="center">
|
<el-table-column label="免打扰" prop="muted" width="80" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,16 @@ export const ImGroupReceiptStatus = {
|
||||||
DONE: 2 // 已完成
|
DONE: 2 // 已完成
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/** 群成员角色(对齐后端 ImGroupMemberRoleEnum) */
|
||||||
|
export const ImGroupMemberRole = {
|
||||||
|
OWNER: 1, // 群主
|
||||||
|
ADMIN: 2, // 管理员
|
||||||
|
NORMAL: 3 // 普通成员
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** 群管理员人数上限(对齐后端 GROUP_ADMIN_MAX_COUNT) */
|
||||||
|
export const GROUP_ADMIN_MAX_COUNT = 3
|
||||||
|
|
||||||
/** 每次拉取私聊消息的最大条数(后端上限 1000,前端取保守值 100) */
|
/** 每次拉取私聊消息的最大条数(后端上限 1000,前端取保守值 100) */
|
||||||
export const PRIVATE_MESSAGE_PULL_SIZE = 100
|
export const PRIVATE_MESSAGE_PULL_SIZE = 100
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,9 @@ export function getMemberDisplayName(
|
||||||
return friend?.displayName || member.displayUserName || member.nickname
|
return friend?.displayName || member.displayUserName || member.nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 群显示名:当前用户对该群的备注(displayGroupName) > 群名(name) */
|
/** 群显示名:当前用户对该群的备注(groupRemark) > 群名(name) */
|
||||||
export function getGroupDisplayName(group: Pick<Group, 'name' | 'displayGroupName'>): string {
|
export function getGroupDisplayName(group: Pick<Group, 'name' | 'groupRemark'>): string {
|
||||||
return group.displayGroupName || group.name
|
return group.groupRemark || group.name
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue