feat(Talk): 成员支持移出群聊/转让群主,新增群公告展示与编辑
成员「更多」菜单按权限显示:设为/取消管理员(群主/Talk 管理员)、转让群主(群主/Talk 管理员,二次确认)、移出群聊(群主可移出任意成员,群管理员仅普通成员,二次确认)。 群聊会话顶部展示群公告横幅(群主/管理员可直接编辑),群菜单新增「群公告」入口,抽屉内可编辑或清空。
This commit is contained in:
1 parent
899dfbc8e9
commit
04535d2dc0
2 files changed
+253
-6
No files matched your search
@@ -89,6 +89,10 @@ export interface TalkConversation {
|
||||
AdminCount?: number
|
||||
/** 当前用户是否可管理群信息(改群名等):群主或群管理员 */
|
||||
CanManage?: boolean
|
||||
/** 群公告(仅群聊,空串表示未设置) */
|
||||
Announcement?: string
|
||||
/** 群公告更新时间 */
|
||||
AnnouncementTime?: string
|
||||
}
|
||||
|
||||
/** 会话列表返回 */
|
||||
@@ -206,6 +210,36 @@ export function dissolveTalkConversation(data: { ConversationUUID: string }): Pr
|
||||
return post<void>(`${TALK_BASE}/Conversation/Dissolve`, data)
|
||||
}
|
||||
|
||||
/** 移出群成员(群主可移出任何人,群管理员仅可移出普通成员) */
|
||||
export function removeTalkMembers(data: {
|
||||
ConversationUUID: string
|
||||
MemberUUIDs: string[]
|
||||
}): Promise<{ Removed: string[]; MemberCount: number }> {
|
||||
return post<{ Removed: string[]; MemberCount: number }>(
|
||||
`${TALK_BASE}/Conversation/Member/Remove`,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
/** 转让群主(原群主转为群管理员;仅群主或 Talk 管理员) */
|
||||
export function transferTalkOwner(data: {
|
||||
ConversationUUID: string
|
||||
UserUUID: string
|
||||
}): Promise<{ OwnerUUID: string; OwnerName: string }> {
|
||||
return post<{ OwnerUUID: string; OwnerName: string }>(`${TALK_BASE}/Conversation/Transfer`, data)
|
||||
}
|
||||
|
||||
/** 设置群公告(群主/群管理员;传空串即清空) */
|
||||
export function setTalkAnnouncement(data: {
|
||||
ConversationUUID: string
|
||||
Announcement: string
|
||||
}): Promise<{ Announcement: string; AnnouncementTime: string | null }> {
|
||||
return post<{ Announcement: string; AnnouncementTime: string | null }>(
|
||||
`${TALK_BASE}/Conversation/Announcement`,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
/** 消息分页(BeforeTime 取更早一页) */
|
||||
export function getTalkMessages(params: {
|
||||
ConversationUUID: string
|
||||
|
||||
+219
-6
@@ -28,6 +28,9 @@ import {
|
||||
renameTalkConversation,
|
||||
dissolveTalkConversation,
|
||||
setTalkMemberRole,
|
||||
removeTalkMembers,
|
||||
transferTalkOwner,
|
||||
setTalkAnnouncement,
|
||||
searchTalkMembers,
|
||||
resolveTalkAttachmentUrl,
|
||||
resolveTalkAttachmentDownloadUrl,
|
||||
@@ -95,6 +98,9 @@ const InviteSubmitting = ref(false)
|
||||
const RenameVisible = ref(false)
|
||||
const RenameTitle = ref('')
|
||||
const RenameSubmitting = ref(false)
|
||||
const AnnouncementVisible = ref(false)
|
||||
const AnnouncementText = ref('')
|
||||
const AnnouncementSubmitting = ref(false)
|
||||
|
||||
// === 实时状态 ===
|
||||
const SelfUUID = ref('')
|
||||
@@ -144,6 +150,34 @@ const AdminLimit = computed(() => Entry.value?.GroupAdminLimit || 10)
|
||||
/** 该群当前管理员数量 */
|
||||
const AdminCount = computed(() => ActiveConversation.value?.AdminCount || 0)
|
||||
|
||||
/** 是否有权编辑群公告:Talk 管理员,或该群群主/群管理员 */
|
||||
const CanManageAnnouncement = computed(
|
||||
() => Entry.value?.IsAdmin === true || ActiveConversation.value?.CanManage === true,
|
||||
)
|
||||
|
||||
/** 是否有权转让群主:仅当前群主或 Talk 管理员 */
|
||||
const CanTransferOwner = computed(
|
||||
() => Entry.value?.IsAdmin === true || ActiveConversation.value?.IsOwner === true,
|
||||
)
|
||||
|
||||
/** 是否有权移出该成员:群主/Talk 管理员可移出任何人(群主除外);群管理员仅可移出普通成员 */
|
||||
function CanRemoveMember(member: TalkMemberBrief): boolean {
|
||||
const conversation = ActiveConversation.value
|
||||
if (!conversation || conversation.ConversationType !== TalkConversationType.Group) return false
|
||||
if (member.UserUUID === MyUUID.value) return false
|
||||
if (member.MemberRole === TalkMemberRole.Owner) return false
|
||||
if (Entry.value?.IsAdmin === true) return true
|
||||
if (conversation.IsOwner === true) return true
|
||||
return conversation.IsGroupAdmin === true && member.MemberRole !== TalkMemberRole.Admin
|
||||
}
|
||||
|
||||
/** 是否展示该成员的「更多」操作入口(管理动作任一可用即可) */
|
||||
function CanShowMemberActions(member: TalkMemberBrief): boolean {
|
||||
if (member.UserUUID === MyUUID.value) return false
|
||||
if (member.MemberRole === TalkMemberRole.Owner) return false
|
||||
return CanManageMembers.value || CanTransferOwner.value || CanRemoveMember(member)
|
||||
}
|
||||
|
||||
/** 过滤后的会话列表 */
|
||||
const FilteredConversations = computed(() => {
|
||||
const keyword = ConversationKeyword.value.trim().toLowerCase()
|
||||
@@ -547,14 +581,23 @@ async function UploadAndSend(file: File): Promise<void> {
|
||||
function HandleGroupCommand(command: string): void {
|
||||
if (command === 'rename') OpenRename()
|
||||
else if (command === 'invite') OpenInvite()
|
||||
else if (command === 'announcement') OpenAnnouncement()
|
||||
else if (command === 'leave') void LeaveGroup()
|
||||
else if (command === 'dissolve') void DissolveGroup()
|
||||
}
|
||||
|
||||
/** 成员操作:设为/取消群管理员(仅群主或 Talk 管理员,单群上限由后端下发) */
|
||||
/** 成员操作分发:设为/取消管理员、移出群聊、转让群主 */
|
||||
async function HandleMemberCommand(command: string, member: TalkMemberBrief): Promise<void> {
|
||||
const conversation = ActiveConversation.value
|
||||
if (!conversation) return
|
||||
if (command === 'remove') {
|
||||
await RemoveMember(conversation, member)
|
||||
return
|
||||
}
|
||||
if (command === 'transfer') {
|
||||
await TransferOwner(conversation, member)
|
||||
return
|
||||
}
|
||||
const role = command === 'setAdmin' ? TalkMemberRole.Admin : TalkMemberRole.Member
|
||||
if (role === TalkMemberRole.Admin && AdminCount.value >= AdminLimit.value) {
|
||||
ElMessage.warning(`每个群最多设置 ${AdminLimit.value} 个管理员`)
|
||||
@@ -574,6 +617,90 @@ async function HandleMemberCommand(command: string, member: TalkMemberBrief): Pr
|
||||
}
|
||||
}
|
||||
|
||||
/** 移出群成员(二次确认) */
|
||||
async function RemoveMember(
|
||||
conversation: TalkConversation,
|
||||
member: TalkMemberBrief,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定将「${MemberName(member)}」移出群聊吗?`,
|
||||
'移出群聊',
|
||||
{ type: 'warning', confirmButtonText: '移出', cancelButtonText: '取消' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await removeTalkMembers({
|
||||
ConversationUUID: conversation.ConversationUUID,
|
||||
MemberUUIDs: [member.UserUUID],
|
||||
})
|
||||
ElMessage.success('已移出群聊')
|
||||
await LoadConversations()
|
||||
} catch {
|
||||
// 业务错误已由响应拦截器统一提示
|
||||
}
|
||||
}
|
||||
|
||||
/** 转让群主(二次确认;转让后本人转为群管理员) */
|
||||
async function TransferOwner(
|
||||
conversation: TalkConversation,
|
||||
member: TalkMemberBrief,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定将群主转让给「${MemberName(member)}」吗?转让后你将成为群管理员。`,
|
||||
'转让群主',
|
||||
{ type: 'warning', confirmButtonText: '转让', cancelButtonText: '取消' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await transferTalkOwner({
|
||||
ConversationUUID: conversation.ConversationUUID,
|
||||
UserUUID: member.UserUUID,
|
||||
})
|
||||
ElMessage.success('群主已转让')
|
||||
await LoadConversations()
|
||||
} catch {
|
||||
// 业务错误已由响应拦截器统一提示
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开群公告编辑 */
|
||||
function OpenAnnouncement(): void {
|
||||
if (!ActiveConversation.value) return
|
||||
if (!CanManageAnnouncement.value) {
|
||||
ElMessage.warning('仅群主或管理员可编辑群公告')
|
||||
return
|
||||
}
|
||||
AnnouncementText.value = ActiveConversation.value.Announcement || ''
|
||||
AnnouncementVisible.value = true
|
||||
}
|
||||
|
||||
/** 提交群公告(清空即取消公告) */
|
||||
async function SubmitAnnouncement(): Promise<void> {
|
||||
const conversation = ActiveConversation.value
|
||||
if (!conversation) return
|
||||
AnnouncementSubmitting.value = true
|
||||
try {
|
||||
const result = await setTalkAnnouncement({
|
||||
ConversationUUID: conversation.ConversationUUID,
|
||||
Announcement: AnnouncementText.value.trim(),
|
||||
})
|
||||
conversation.Announcement = result?.Announcement || ''
|
||||
conversation.AnnouncementTime = result?.AnnouncementTime || ''
|
||||
ElMessage.success(conversation.Announcement ? '群公告已更新' : '群公告已清空')
|
||||
AnnouncementVisible.value = false
|
||||
} catch {
|
||||
// 业务错误已由响应拦截器统一提示
|
||||
} finally {
|
||||
AnnouncementSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开改群名 */
|
||||
function OpenRename(): void {
|
||||
if (!ActiveConversation.value) return
|
||||
@@ -899,6 +1026,9 @@ onBeforeUnmount(() => {
|
||||
<el-dropdown-item v-if="CanManageGroup" command="rename">
|
||||
<el-icon><EditPen /></el-icon>修改群名
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="CanManageAnnouncement" command="announcement">
|
||||
<el-icon><Bell /></el-icon>群公告
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="invite">
|
||||
<el-icon><Plus /></el-icon>邀请成员
|
||||
</el-dropdown-item>
|
||||
@@ -914,6 +1044,18 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 群公告(群聊且有公告时展示;群主/管理员可直接编辑) -->
|
||||
<div v-if="IsGroupChat && ActiveConversation.Announcement" class="talk-announcement">
|
||||
<el-icon class="talk-announcement-icon"><Bell /></el-icon>
|
||||
<div class="talk-announcement-body">
|
||||
<div class="talk-announcement-title">群公告</div>
|
||||
<div class="talk-announcement-text">{{ ActiveConversation.Announcement }}</div>
|
||||
</div>
|
||||
<el-button v-if="CanManageAnnouncement" text size="small" @click="OpenAnnouncement">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="talk-messages-wrap">
|
||||
<el-scrollbar ref="ScrollbarRef" @scroll="OnScrollbarScroll">
|
||||
<div v-loading="MessagesLoading" class="talk-messages">
|
||||
@@ -1035,7 +1177,7 @@ onBeforeUnmount(() => {
|
||||
</el-tag>
|
||||
<el-tag v-if="member.UserUUID === MyUUID" size="small" type="info">我</el-tag>
|
||||
<el-dropdown
|
||||
v-if="CanManageMembers && member.MemberRole !== TalkMemberRole.Owner && member.UserUUID !== MyUUID"
|
||||
v-if="CanShowMemberActions(member)"
|
||||
trigger="click"
|
||||
@command="(command: string) => HandleMemberCommand(command, member)"
|
||||
>
|
||||
@@ -1044,11 +1186,21 @@ onBeforeUnmount(() => {
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="member.MemberRole !== TalkMemberRole.Admin" command="setAdmin">
|
||||
<el-icon><Star /></el-icon>设为管理员
|
||||
<el-dropdown-item
|
||||
v-if="CanManageMembers"
|
||||
:command="member.MemberRole !== TalkMemberRole.Admin ? 'setAdmin' : 'unsetAdmin'"
|
||||
>
|
||||
<el-icon>
|
||||
<StarFilled v-if="member.MemberRole === TalkMemberRole.Admin" />
|
||||
<Star v-else />
|
||||
</el-icon>
|
||||
{{ member.MemberRole === TalkMemberRole.Admin ? '取消管理员' : '设为管理员' }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else command="unsetAdmin">
|
||||
<el-icon><StarFilled /></el-icon>取消管理员
|
||||
<el-dropdown-item v-if="CanTransferOwner" command="transfer">
|
||||
<el-icon><Switch /></el-icon>转让群主
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="CanRemoveMember(member)" divided command="remove">
|
||||
<el-icon><CircleClose /></el-icon>移出群聊
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -1095,6 +1247,30 @@ onBeforeUnmount(() => {
|
||||
<el-button type="primary" :loading="RenameSubmitting" @click="SubmitRename">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 群公告 -->
|
||||
<el-drawer
|
||||
v-model="AnnouncementVisible"
|
||||
title="群公告"
|
||||
direction="rtl"
|
||||
:size="460"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-input
|
||||
v-model="AnnouncementText"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
placeholder="填写群公告,群内成员将在会话顶部看到;清空并保存即取消公告"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="AnnouncementVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="AnnouncementSubmitting" @click="SubmitAnnouncement">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1281,6 +1457,43 @@ onBeforeUnmount(() => {
|
||||
background: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
/* ---------- 群公告 ---------- */
|
||||
.talk-announcement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
flex-shrink: 0;
|
||||
background: var(--el-color-warning-light-9);
|
||||
border-bottom: 1px solid var(--el-color-warning-light-7);
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.talk-announcement-icon {
|
||||
margin-top: 2px;
|
||||
color: var(--el-color-warning);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.talk-announcement-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.talk-announcement-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-warning-dark-2);
|
||||
}
|
||||
|
||||
.talk-announcement-text {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.talk-messages {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user