群聊
This commit is contained in:
390
src/components/chat/GroupChatPanel.vue
Normal file
390
src/components/chat/GroupChatPanel.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<div class="w-80 bg-[#1e293b] border-l border-gray-800 flex flex-col h-full">
|
||||
<!-- 群名称区域 -->
|
||||
<div class="border-b border-gray-800 p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-semibold text-white">群名称</h3>
|
||||
<button
|
||||
v-if="canEditGroupName && !isEditingGroupName"
|
||||
class="text-xs text-primary hover:text-primary/80 transition"
|
||||
@click="startEditGroupName"
|
||||
>
|
||||
<i class="fas fa-edit mr-1"></i>编辑
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditingGroupName" class="min-h-[40px]">
|
||||
<p v-if="groupName" class="text-base font-medium text-white">
|
||||
{{ groupName }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-gray-500 italic">
|
||||
暂无群名称
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<input
|
||||
v-model="editGroupNameText"
|
||||
type="text"
|
||||
class="w-full bg-gray-800 text-white text-sm rounded-lg p-2 border border-gray-700 focus:border-primary focus:outline-none"
|
||||
placeholder="请输入群名称..."
|
||||
maxlength="20"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 py-1.5 bg-primary text-white text-xs rounded-lg hover:bg-primary/80 transition"
|
||||
@click="saveGroupName"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-1.5 bg-gray-700 text-gray-300 text-xs rounded-lg hover:bg-gray-600 transition"
|
||||
@click="cancelEditGroupName"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群公告区域 -->
|
||||
<div class="border-b border-gray-800 p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-white">群公告</h3>
|
||||
<button
|
||||
v-if="canEditAnnouncement && !isEditingAnnouncement"
|
||||
class="text-xs text-primary hover:text-primary/80 transition"
|
||||
@click="startEditAnnouncement"
|
||||
>
|
||||
<i class="fas fa-edit mr-1"></i>编辑
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditingAnnouncement" class="min-h-[60px]">
|
||||
<p v-if="announcement" class="text-sm text-gray-300 whitespace-pre-wrap">
|
||||
{{ announcement }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-gray-500 italic">
|
||||
暂无群公告
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<textarea
|
||||
v-model="editAnnouncementText"
|
||||
class="w-full bg-gray-800 text-white text-sm rounded-lg p-2 border border-gray-700 focus:border-primary focus:outline-none resize-none"
|
||||
rows="4"
|
||||
placeholder="请输入群公告内容..."
|
||||
></textarea>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 py-1.5 bg-primary text-white text-xs rounded-lg hover:bg-primary/80 transition"
|
||||
@click="saveAnnouncement"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-1.5 bg-gray-700 text-gray-300 text-xs rounded-lg hover:bg-gray-600 transition"
|
||||
@click="cancelEditAnnouncement"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群成员列表 -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-white">群成员 ({{ members.length }})</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingMembers" class="text-center py-8 text-gray-500 text-xs">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>
|
||||
加载中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="members.length === 0" class="text-center py-8 text-gray-500 text-xs">
|
||||
暂无成员
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="member in members"
|
||||
:key="member.user_id"
|
||||
class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition cursor-pointer group"
|
||||
@click="handleMemberClick(member)"
|
||||
>
|
||||
<div class="relative shrink-0">
|
||||
<Avatar
|
||||
:name="member.user?.name || member.nickname || '未知'"
|
||||
:avatar="member.user?.avatar"
|
||||
size="sm"
|
||||
rounded="full"
|
||||
/>
|
||||
<!-- 在线状态指示器 -->
|
||||
<div
|
||||
v-if="member.user?.is_online"
|
||||
class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-emerald-500 rounded-full border-2 border-[#1e293b]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-white truncate">
|
||||
{{ member.user?.name || member.nickname || '未知' }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
<span v-if="member.role === 2" class="text-yellow-400">群主</span>
|
||||
<span v-else-if="member.role === 1" class="text-blue-400">管理员</span>
|
||||
<span v-else>成员</span>
|
||||
<span v-if="member.user?.is_online" class="ml-2 text-emerald-400">在线</span>
|
||||
<span v-else class="ml-2 text-gray-500">离线</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 成员操作按钮(仅群主/管理员可见) -->
|
||||
<div
|
||||
v-if="canManageMember(member)"
|
||||
class="opacity-0 group-hover:opacity-100 transition"
|
||||
>
|
||||
<button
|
||||
v-if="member.role !== 2"
|
||||
class="text-gray-400 hover:text-red-400 transition text-sm p-1"
|
||||
@click.stop="handleRemoveMember(member)"
|
||||
title="移除成员"
|
||||
>
|
||||
<i class="fas fa-user-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import Avatar from '@/components/common/Avatar.vue'
|
||||
import * as groupApi from '@/api/modules/room'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import type { GroupMember } from '@/types/api'
|
||||
|
||||
interface Props {
|
||||
roomId: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'member-removed': []
|
||||
'member-clicked': [member: GroupMember]
|
||||
'group-updated': []
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
|
||||
const loadingMembers = ref(false)
|
||||
const members = ref<GroupMember[]>([])
|
||||
const announcement = ref('')
|
||||
const isEditingAnnouncement = ref(false)
|
||||
const editAnnouncementText = ref('')
|
||||
const loadingAnnouncement = ref(false)
|
||||
const groupName = ref('')
|
||||
const isEditingGroupName = ref(false)
|
||||
const editGroupNameText = ref('')
|
||||
const loadingGroupName = ref(false)
|
||||
|
||||
// 当前用户角色
|
||||
const currentUserRole = computed(() => {
|
||||
if (!authStore.user) return -1
|
||||
const member = members.value.find(m => m.user_id === authStore.user!.id)
|
||||
return member?.role ?? -1
|
||||
})
|
||||
|
||||
// 是否为群主
|
||||
const isOwner = computed(() => currentUserRole.value === 2)
|
||||
|
||||
// 是否为管理员
|
||||
const isAdmin = computed(() => currentUserRole.value === 1)
|
||||
|
||||
// 是否可以编辑群公告
|
||||
const canEditAnnouncement = computed(() => isOwner.value || isAdmin.value)
|
||||
|
||||
// 是否可以编辑群名
|
||||
const canEditGroupName = computed(() => isOwner.value || isAdmin.value)
|
||||
|
||||
// 判断是否可以管理某个成员
|
||||
function canManageMember(member: GroupMember): boolean {
|
||||
// 群主可以管理所有成员(除了自己)
|
||||
if (isOwner.value && member.user_id !== authStore.user?.id) {
|
||||
return true
|
||||
}
|
||||
// 管理员可以管理普通成员
|
||||
if (isAdmin.value && member.role === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 加载群成员
|
||||
async function loadMembers() {
|
||||
if (loadingMembers.value) return
|
||||
|
||||
loadingMembers.value = true
|
||||
try {
|
||||
const memberList = await groupApi.getGroupMembers(props.roomId)
|
||||
members.value = memberList as unknown as GroupMember[]
|
||||
} catch (error) {
|
||||
console.error('Failed to load group members:', error)
|
||||
toastStore.error('加载群成员失败')
|
||||
members.value = []
|
||||
} finally {
|
||||
loadingMembers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载群信息(包括群名和公告)
|
||||
async function loadGroupInfo() {
|
||||
if (loadingGroupName.value) return
|
||||
|
||||
loadingGroupName.value = true
|
||||
try {
|
||||
const [groupInfo, annRes] = await Promise.all([
|
||||
groupApi.getGroup(props.roomId),
|
||||
groupApi.getGroupAnnouncement(props.roomId).catch(() => ({ announcement: '' }))
|
||||
])
|
||||
groupName.value = groupInfo.name || groupInfo.room_name || ''
|
||||
announcement.value = annRes.announcement || ''
|
||||
} catch (error) {
|
||||
console.error('Failed to load group info:', error)
|
||||
// 加载失败不影响使用
|
||||
} finally {
|
||||
loadingGroupName.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载群公告
|
||||
async function loadAnnouncement() {
|
||||
if (loadingAnnouncement.value) return
|
||||
|
||||
loadingAnnouncement.value = true
|
||||
try {
|
||||
const res = await groupApi.getGroupAnnouncement(props.roomId)
|
||||
announcement.value = res.announcement || ''
|
||||
} catch (error) {
|
||||
console.error('Failed to load announcement:', error)
|
||||
// 公告加载失败不影响使用
|
||||
} finally {
|
||||
loadingAnnouncement.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑群公告
|
||||
function startEditAnnouncement() {
|
||||
editAnnouncementText.value = announcement.value
|
||||
isEditingAnnouncement.value = true
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
function cancelEditAnnouncement() {
|
||||
isEditingAnnouncement.value = false
|
||||
editAnnouncementText.value = ''
|
||||
}
|
||||
|
||||
// 保存群公告
|
||||
async function saveAnnouncement() {
|
||||
try {
|
||||
await groupApi.updateGroupAnnouncement(props.roomId, editAnnouncementText.value)
|
||||
announcement.value = editAnnouncementText.value
|
||||
isEditingAnnouncement.value = false
|
||||
toastStore.success('群公告已更新')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update announcement:', error)
|
||||
toastStore.error(error.message || '更新群公告失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑群名
|
||||
function startEditGroupName() {
|
||||
editGroupNameText.value = groupName.value
|
||||
isEditingGroupName.value = true
|
||||
}
|
||||
|
||||
// 取消编辑群名
|
||||
function cancelEditGroupName() {
|
||||
isEditingGroupName.value = false
|
||||
editGroupNameText.value = ''
|
||||
}
|
||||
|
||||
// 保存群名
|
||||
async function saveGroupName() {
|
||||
if (!editGroupNameText.value.trim()) {
|
||||
toastStore.warning('群名称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await groupApi.updateGroup(props.roomId, {
|
||||
name: editGroupNameText.value.trim()
|
||||
})
|
||||
groupName.value = editGroupNameText.value.trim()
|
||||
isEditingGroupName.value = false
|
||||
toastStore.success('群名称已更新')
|
||||
// 触发更新事件,让父组件刷新
|
||||
emit('group-updated')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update group name:', error)
|
||||
toastStore.error(error.message || '更新群名称失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理成员点击
|
||||
function handleMemberClick(member: GroupMember) {
|
||||
emit('member-clicked', member)
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
async function handleRemoveMember(member: GroupMember) {
|
||||
if (!confirm(`确定要移除成员 "${member.user?.name || member.nickname || '未知'}" 吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await groupApi.removeGroupMember(props.roomId, member.user_id)
|
||||
toastStore.success('成员已移除')
|
||||
// 重新加载成员列表
|
||||
await loadMembers()
|
||||
emit('member-removed')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to remove member:', error)
|
||||
toastStore.error(error.message || '移除成员失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 roomId 变化
|
||||
watch(() => props.roomId, () => {
|
||||
if (props.roomId) {
|
||||
loadMembers()
|
||||
loadGroupInfo()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
if (props.roomId) {
|
||||
loadMembers()
|
||||
loadGroupInfo()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-700 rounded-full;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user