Files
nl-um-vue-ts/src/components/chat/GroupChatPanel.vue

391 lines
12 KiB
Vue
Raw Normal View History

2025-12-05 13:13:12 +08:00
<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>