diff --git a/src/api/modules/room.ts b/src/api/modules/room.ts index 49b6853..4820845 100644 --- a/src/api/modules/room.ts +++ b/src/api/modules/room.ts @@ -133,3 +133,44 @@ export function getGroupNotifications(params?: { }>('/group-notifications', { params }) } +// ========== 预留 API 接口(需后端支持) ========== + +/** + * 禁言成员 + * @param roomId 群ID + * @param userId 用户ID + * @param duration 禁言时长(秒),0 表示永久禁言,-1 表示解除禁言 + */ +export function muteGroupMember(roomId: string, userId: string, duration: number) { + return request.post(`/groups/${roomId}/members/${userId}/mute`, { duration }) +} + +/** + * 更新群设置(扩展版本,包含简介和邀请权限) + * @param roomId 群ID + * @param data 群设置数据 + */ +export function updateGroupSettings(roomId: string, data: { + name?: string + avatar?: string + description?: string // 群简介 + invite_permission?: number // 邀请权限:0-所有人 1-仅管理员 2-仅群主 +}) { + return request.post(`/groups/${roomId}/settings`, data) +} + +/** + * 获取群设置 + * @param roomId 群ID + */ +export function getGroupSettings(roomId: string) { + return request.get<{ + name: string + avatar: string + description: string + invite_permission: number + created_at: string + updated_at: string + }>(`/groups/${roomId}/settings`) +} + diff --git a/src/components/chat/GroupCallWindow.vue b/src/components/chat/GroupCallWindow.vue index ed459a9..04c4bd8 100644 --- a/src/components/chat/GroupCallWindow.vue +++ b/src/components/chat/GroupCallWindow.vue @@ -55,10 +55,10 @@ :style="gridStyle" > -
+
-
+
{{ member.nickname || member.user?.name || '未知用户' }} - - 群主 - 管理 +
+ + + + + + 群主 + 管理 +

{{ member.user?.email || '无签名' }} @@ -176,59 +182,124 @@ /> -

-
-
-

群公告

- -
-
- -
- {{ announcement || '暂无公告' }} + +
+
+
+

群公告

+ +
+
+ +
+ {{ announcement || '暂无公告' }} +
+
+
+ +
-
- - -
-
+ -
-
-
- -
-

移除成员

-

- 确定要将 {{ removingMember.nickname || removingMember.user?.name || '该成员' }} 移出群聊吗? -

-
- - + +
+
+
+ +
+

移除成员

+

+ 确定要将 {{ removingMember.nickname || removingMember.user?.name || '该成员' }} 移出群聊吗? +

+
+ + +
-
+ + + + +
+
+
+
+ +
+ +
+
+
+

禁言成员

+

+ 选择禁言 {{ mutingMember.nickname || mutingMember.user?.name || '该成员' }} 的时长 +

+
+ + + + + + +
+ +
+
+
@@ -271,6 +342,8 @@ const isEditingAnnouncement = ref(false) const editAnnouncementText = ref('') const removingMember = ref(null) const removing = ref(false) +const mutingMember = ref(null) +const muting = ref(false) // Computed const currentUserRole = computed(() => members.value.find(m => m.user_id === authStore.user?.id)?.role ?? 0) @@ -295,6 +368,25 @@ function canRemoveMember(member: GroupMember): boolean { return false } +// 判断是否可以禁言某个成员 +function canMuteMember(member: GroupMember): boolean { + // 不能禁言自己 + if (member.user_id === authStore.user?.id) return false + // 不能禁言群主 + if (member.role === 2) return false + // 群主可以禁言任何人 + if (isOwner.value) return true + // 管理员只能禁言普通成员 + if (isAdmin.value && member.role === 0) return true + return false +} + +// 检查成员是否被禁言 +function isMemberMuted(member: GroupMember): boolean { + if (!member.muted_until) return false + return new Date(member.muted_until) > new Date() +} + // 过滤出可通话的候选人(排除自己) const callCandidates = computed(() => { return members.value @@ -392,7 +484,24 @@ function showMemberContextMenu(event: MouseEvent, member: GroupMember) { } } - // 4. 移除成员 - 群主可移除任何人,管理员只能移除普通成员 + // 4. 禁言/解除禁言 - 群主和管理员可操作 + if (canMuteMember(member)) { + if (isMemberMuted(member)) { + items.push({ + label: '解除禁言', + icon: 'fas fa-volume-up', + action: () => handleUnmuteMember(member) + }) + } else { + items.push({ + label: '禁言', + icon: 'fas fa-volume-mute', + action: () => { mutingMember.value = member } + }) + } + } + + // 5. 移除成员 - 群主可移除任何人,管理员只能移除普通成员 if (canRemoveMember(member)) { items.push({ label: '移除成员', @@ -455,6 +564,33 @@ async function handleRemoveMember() { } } +// --- 禁言逻辑 --- +async function handleMuteMember(duration: number) { + if (!mutingMember.value) return + muting.value = true + try { + await groupApi.muteGroupMember(props.roomId, mutingMember.value.user_id, duration) + const durationText = duration === 0 ? '永久' : duration < 3600 ? `${duration / 60}分钟` : duration < 86400 ? `${duration / 3600}小时` : `${duration / 86400}天` + toastStore.success(`已禁言 ${durationText}`) + mutingMember.value = null + loadData() + } catch (e: any) { + toastStore.error(e?.response?.data?.message || e?.message || '禁言失败') + } finally { + muting.value = false + } +} + +async function handleUnmuteMember(member: GroupMember) { + try { + await groupApi.muteGroupMember(props.roomId, member.user_id, -1) + toastStore.success('已解除禁言') + loadData() + } catch (e: any) { + toastStore.error(e?.response?.data?.message || e?.message || '解除禁言失败') + } +} + // --- 通话逻辑 --- function handleStartCallClick() { if (callCandidates.value.length === 0) { diff --git a/src/components/chat/GroupInfoPanel.vue b/src/components/chat/GroupInfoPanel.vue index a8f1792..ec0cc8f 100644 --- a/src/components/chat/GroupInfoPanel.vue +++ b/src/components/chat/GroupInfoPanel.vue @@ -75,7 +75,15 @@ 编辑 - + + + + - + + +
{{ member.nickname || member.user?.name || '未知' }} @@ -224,6 +241,96 @@
+ +
+
+
+
+ +
+ +
+
+
+

转让群主

+

+ 确定要将群主转让给 {{ transferringTo.nickname || transferringTo.user?.name || '该成员' }} 吗?
+ 转让后您将成为普通成员 +

+
+ + +
+
+
+ + +
+
+
+
+

管理员设置

+

已选择 {{ selectedAdminIds.size }} 人

+
+ +
+ +
+
+
+
+ +
+ +
+
+
+

{{ member.nickname || member.user?.name || '未知' }}

+

{{ member.role === 1 ? '当前管理员' : '普通成员' }}

+
+
+ +
+
+
+
+ +
+ + +
+
+
+
@@ -261,6 +368,11 @@ const showDissolveConfirm = ref(false) const showCallModal = ref(false) const removingMember = ref(null) const removing = ref(false) +const transferringTo = ref(null) +const transferring = ref(false) +const showAdminModal = ref(false) +const selectedAdminIds = ref>(new Set()) +const savingAdmins = ref(false) const editForm = ref({ name: '', avatar: '' }) @@ -296,6 +408,11 @@ const callCandidates = computed(() => { } as unknown as Contact)) }) +// 管理员候选人(排除群主) +const adminCandidates = computed(() => { + return members.value.filter(m => m.user_id !== authStore.user?.id && m.role !== 2) +}) + // Methods async function loadGroupInfo() { if (!props.roomId) return @@ -411,6 +528,80 @@ async function handleRemoveMember() { } } +// 转让群主相关 +function showTransferConfirm(member: GroupMember) { + transferringTo.value = member +} + +async function handleTransferOwner() { + if (!transferringTo.value) return + transferring.value = true + try { + // 调用 updateMemberRole 将目标成员设置为群主 (role=2) + await groupApi.updateMemberRole(props.roomId, transferringTo.value.user_id, { role: 2 }) + toastStore.success('群主已转让') + transferringTo.value = null + loadGroupInfo() + emit('updated') + } catch (e: any) { + toastStore.error(e?.response?.data?.message || e?.message || '转让失败') + } finally { + transferring.value = false + } +} + +// 管理员设置相关 +function openAdminModal() { + // 初始化选中状态,当前管理员默认选中 + selectedAdminIds.value = new Set( + members.value.filter(m => m.role === 1).map(m => m.user_id) + ) + showAdminModal.value = true +} + +function toggleAdminSelection(userId: string) { + if (selectedAdminIds.value.has(userId)) { + selectedAdminIds.value.delete(userId) + } else { + selectedAdminIds.value.add(userId) + } + // 触发响应式更新 + selectedAdminIds.value = new Set(selectedAdminIds.value) +} + +async function handleSaveAdmins() { + savingAdmins.value = true + try { + // 获取当前管理员列表 + const currentAdminIds = new Set(members.value.filter(m => m.role === 1).map(m => m.user_id)) + + // 需要设置为管理员的(新增) + const toAdd = [...selectedAdminIds.value].filter(id => !currentAdminIds.has(id)) + // 需要取消管理员的(移除) + const toRemove = [...currentAdminIds].filter(id => !selectedAdminIds.value.has(id)) + + // 批量设置 + const promises: Promise[] = [] + for (const userId of toAdd) { + promises.push(groupApi.updateMemberRole(props.roomId, userId, { role: 1 })) + } + for (const userId of toRemove) { + promises.push(groupApi.updateMemberRole(props.roomId, userId, { role: 0 })) + } + + await Promise.all(promises) + + toastStore.success('管理员设置已保存') + showAdminModal.value = false + loadGroupInfo() + emit('updated') + } catch (e: any) { + toastStore.error(e?.response?.data?.message || e?.message || '保存失败') + } finally { + savingAdmins.value = false + } +} + watch(() => props.show, (val) => { if (val) loadGroupInfo() }) diff --git a/src/components/chat/SelectContactsModal.vue b/src/components/chat/SelectContactsModal.vue index f8b65ee..f7978e7 100644 --- a/src/components/chat/SelectContactsModal.vue +++ b/src/components/chat/SelectContactsModal.vue @@ -1,10 +1,11 @@