From 04535d2dc078d8b37773916dd7e62ca8a3af327e Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 13 Oct 2026 16:38:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(Talk):=20=E6=88=90=E5=91=98=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=A7=BB=E5=87=BA=E7=BE=A4=E8=81=8A/=E8=BD=AC?= =?UTF-8?q?=E8=AE=A9=E7=BE=A4=E4=B8=BB=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=BE=A4?= =?UTF-8?q?=E5=85=AC=E5=91=8A=E5=B1=95=E7=A4=BA=E4=B8=8E=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 成员「更多」菜单按权限显示:设为/取消管理员(群主/Talk 管理员)、转让群主(群主/Talk 管理员,二次确认)、移出群聊(群主可移出任意成员,群管理员仅普通成员,二次确认)。 群聊会话顶部展示群公告横幅(群主/管理员可直接编辑),群菜单新增「群公告」入口,抽屉内可编辑或清空。 --- src/api/talk.ts | 34 ++++++ src/views/talk/Talk.vue | 225 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 253 insertions(+), 6 deletions(-) diff --git a/src/api/talk.ts b/src/api/talk.ts index c38a8e3..7df9144 100644 --- a/src/api/talk.ts +++ b/src/api/talk.ts @@ -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(`${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 diff --git a/src/views/talk/Talk.vue b/src/views/talk/Talk.vue index 6081aec..d6a37e8 100644 --- a/src/views/talk/Talk.vue +++ b/src/views/talk/Talk.vue @@ -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 { 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 { 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 { + 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 { + 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 { + 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(() => { 修改群名 + + 群公告 + 邀请成员 @@ -914,6 +1044,18 @@ onBeforeUnmount(() => { + +
+ +
+
群公告
+
{{ ActiveConversation.Announcement }}
+
+ + + +
+
@@ -1035,7 +1177,7 @@ onBeforeUnmount(() => { 我 @@ -1044,11 +1186,21 @@ onBeforeUnmount(() => { @@ -1095,6 +1247,30 @@ onBeforeUnmount(() => { 保存 + + + + + +
@@ -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; }