460 lines
14 KiB
Vue
460 lines
14 KiB
Vue
<template>
|
||
<div
|
||
v-if="show"
|
||
class="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm"
|
||
@click.self="$emit('close')"
|
||
>
|
||
<div class="bg-panel rounded-2xl w-full max-w-2xl shadow-2xl border border-gray-700 overflow-hidden animate-fade-in max-h-[90vh] flex flex-col">
|
||
<!-- 头部 -->
|
||
<div class="px-6 py-4 border-b border-gray-800 flex justify-between items-center">
|
||
<h2 class="text-xl font-bold text-white">群资料</h2>
|
||
<button
|
||
class="text-gray-400 hover:text-white transition text-xl"
|
||
@click="$emit('close')"
|
||
>
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="flex-1 flex items-center justify-center py-20">
|
||
<div class="text-center">
|
||
<i class="fas fa-circle-notch fa-spin text-4xl text-primary mb-4"></i>
|
||
<p class="text-gray-400">加载中...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区域 -->
|
||
<div v-else-if="groupInfo" class="flex-1 overflow-y-auto custom-scrollbar">
|
||
<!-- 群基本信息 -->
|
||
<div class="px-6 py-6 border-b border-gray-800">
|
||
<div class="flex flex-col items-center mb-6">
|
||
<Avatar
|
||
:name="groupInfo.name"
|
||
:avatar="groupInfo.avatar || groupInfo.name?.charAt(0)"
|
||
size="xl"
|
||
rounded="full"
|
||
class="mb-4 border-4 border-gray-700"
|
||
/>
|
||
<h3 class="text-2xl font-bold text-white mb-2">{{ groupInfo.name }}</h3>
|
||
<p class="text-sm text-gray-400">群ID: {{ groupInfo.room_id }}</p>
|
||
<p class="text-sm text-gray-400 mt-1">成员数: {{ groupInfo.member_count || members.length }}</p>
|
||
</div>
|
||
|
||
<!-- 群操作按钮 -->
|
||
<div class="flex gap-3 justify-center">
|
||
<button
|
||
v-if="canInvite"
|
||
class="px-4 py-2 rounded-xl bg-primary hover:bg-indigo-600 text-white transition flex items-center gap-2"
|
||
@click="showInviteModal = true"
|
||
>
|
||
<i class="fas fa-user-plus"></i>
|
||
<span>邀请成员</span>
|
||
</button>
|
||
<button
|
||
v-if="canEdit"
|
||
class="px-4 py-2 rounded-xl bg-gray-700 hover:bg-gray-600 text-white transition flex items-center gap-2"
|
||
@click="showEditModal = true"
|
||
>
|
||
<i class="fas fa-edit"></i>
|
||
<span>编辑群信息</span>
|
||
</button>
|
||
<button
|
||
v-if="isOwner"
|
||
class="px-4 py-2 rounded-xl bg-red-600 hover:bg-red-700 text-white transition flex items-center gap-2"
|
||
@click="showDissolveConfirmDialog"
|
||
>
|
||
<i class="fas fa-trash"></i>
|
||
<span>解散群聊</span>
|
||
</button>
|
||
<button
|
||
v-else
|
||
class="px-4 py-2 rounded-xl bg-red-600 hover:bg-red-700 text-white transition flex items-center gap-2"
|
||
@click="showQuitConfirmDialog"
|
||
>
|
||
<i class="fas fa-sign-out-alt"></i>
|
||
<span>退出群聊</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 成员列表 -->
|
||
<div class="px-6 py-4">
|
||
<h4 class="text-lg font-semibold text-white mb-4">群成员 ({{ members.length }})</h4>
|
||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||
<div
|
||
v-for="member in members"
|
||
:key="member.user_id"
|
||
class="flex items-center gap-3 p-3 rounded-xl hover:bg-white/5 transition cursor-pointer group"
|
||
@click="handleMemberClick(member)"
|
||
>
|
||
<Avatar
|
||
:name="member.user?.name || member.nickname || '未知'"
|
||
:avatar="member.user?.avatar || (member.user?.name || member.nickname)?.charAt(0)"
|
||
size="sm"
|
||
rounded="full"
|
||
/>
|
||
<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>
|
||
</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"
|
||
@click.stop="showRemoveMemberConfirmDialog(member)"
|
||
>
|
||
<i class="fas fa-user-minus"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 错误状态 -->
|
||
<div v-else class="flex-1 flex items-center justify-center py-20">
|
||
<div class="text-center">
|
||
<i class="fas fa-exclamation-circle text-4xl text-red-500 mb-4"></i>
|
||
<p class="text-gray-400">加载失败</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 邀请成员弹窗 -->
|
||
<SelectContactsModal
|
||
v-if="showInviteModal"
|
||
:show="showInviteModal"
|
||
:contacts="availableContacts"
|
||
@close="showInviteModal = false"
|
||
@create="handleInviteMembers"
|
||
/>
|
||
|
||
<!-- 编辑群信息弹窗 -->
|
||
<div
|
||
v-if="showEditModal"
|
||
class="fixed inset-0 bg-black/80 z-[70] flex items-center justify-center p-4 backdrop-blur-sm"
|
||
@click.self="showEditModal = false"
|
||
>
|
||
<div class="bg-panel rounded-2xl w-full max-w-md shadow-2xl border border-gray-700 overflow-hidden animate-fade-in">
|
||
<div class="px-6 py-4 border-b border-gray-800 flex justify-between items-center">
|
||
<h3 class="text-lg font-bold text-white">编辑群信息</h3>
|
||
<button class="text-gray-400 hover:text-white transition" @click="showEditModal = false">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="px-6 py-4 space-y-4">
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">群名称</label>
|
||
<input
|
||
v-model="editForm.name"
|
||
type="text"
|
||
class="w-full bg-input rounded-xl py-2 px-4 text-sm text-white focus:ring-2 ring-primary outline-none"
|
||
placeholder="请输入群名称"
|
||
maxlength="20"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">群头像(首字母)</label>
|
||
<input
|
||
v-model="editForm.avatar"
|
||
type="text"
|
||
class="w-full bg-input rounded-xl py-2 px-4 text-sm text-white focus:ring-2 ring-primary outline-none"
|
||
placeholder="输入单个字符作为头像"
|
||
maxlength="1"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="px-6 py-4 border-t border-gray-800 flex justify-end gap-3">
|
||
<button
|
||
class="px-4 py-2 rounded-xl bg-gray-700 hover:bg-gray-600 text-white transition"
|
||
@click="showEditModal = false"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 rounded-xl bg-primary hover:bg-indigo-600 text-white transition"
|
||
@click="handleUpdateGroup"
|
||
>
|
||
保存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 确认模态框 -->
|
||
<ConfirmModal
|
||
:show="showRemoveMemberConfirm"
|
||
title="移除成员"
|
||
:message="removeMemberMessage"
|
||
type="danger"
|
||
confirm-text="移除"
|
||
cancel-text="取消"
|
||
@confirm="handleRemoveMember"
|
||
@cancel="showRemoveMemberConfirm = false; pendingMember = null"
|
||
/>
|
||
|
||
<ConfirmModal
|
||
:show="showQuitConfirm"
|
||
title="退出群聊"
|
||
message="确定要退出该群聊吗?"
|
||
type="warning"
|
||
confirm-text="退出"
|
||
cancel-text="取消"
|
||
@confirm="handleQuit"
|
||
@cancel="showQuitConfirm = false"
|
||
/>
|
||
|
||
<ConfirmModal
|
||
:show="showDissolveConfirm"
|
||
title="解散群聊"
|
||
message="确定要解散该群聊吗?此操作不可恢复!"
|
||
type="danger"
|
||
confirm-text="解散"
|
||
cancel-text="取消"
|
||
@confirm="handleDissolve"
|
||
@cancel="showDissolveConfirm = false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import type { GroupInfo, GroupMember, Contact } from '@/types/api'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { useChatStore } from '@/stores/chat'
|
||
import { useToastStore } from '@/stores/toast'
|
||
import * as groupApi from '@/api/modules/room'
|
||
import * as contactApi from '@/api/modules/contact'
|
||
import Avatar from '@/components/common/Avatar.vue'
|
||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||
|
||
interface Props {
|
||
show: boolean
|
||
roomId: string
|
||
}
|
||
|
||
const props = defineProps<Props>()
|
||
|
||
const emit = defineEmits<{
|
||
'close': []
|
||
'updated': []
|
||
'quit': []
|
||
'dissolve': []
|
||
}>()
|
||
|
||
const authStore = useAuthStore()
|
||
const chatStore = useChatStore()
|
||
const toastStore = useToastStore()
|
||
|
||
const loading = ref(false)
|
||
const groupInfo = ref<GroupInfo | null>(null)
|
||
const members = ref<GroupMember[]>([])
|
||
const availableContacts = ref<Contact[]>([])
|
||
const showInviteModal = ref(false)
|
||
const showEditModal = ref(false)
|
||
const editForm = ref({ name: '', avatar: '' })
|
||
const showRemoveMemberConfirm = ref(false)
|
||
const showQuitConfirm = ref(false)
|
||
const showDissolveConfirm = ref(false)
|
||
const pendingMember = ref<GroupMember | null>(null)
|
||
|
||
// 当前用户角色
|
||
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 canInvite = computed(() => isOwner.value || isAdmin.value)
|
||
|
||
// 是否可以编辑群信息
|
||
const canEdit = computed(() => isOwner.value)
|
||
|
||
// 移除成员确认消息
|
||
const removeMemberMessage = computed(() => {
|
||
const name = pendingMember.value?.user?.name || pendingMember.value?.nickname || '未知'
|
||
return `确定要将 ${name} 移出群聊吗?`
|
||
})
|
||
|
||
// 加载群信息
|
||
async function loadGroupInfo() {
|
||
if (!props.roomId) return
|
||
|
||
loading.value = true
|
||
try {
|
||
const [group, memberList] = await Promise.all([
|
||
groupApi.getGroup(props.roomId),
|
||
groupApi.getGroupMembers(props.roomId)
|
||
])
|
||
groupInfo.value = group
|
||
members.value = memberList
|
||
|
||
// 加载可用联系人(用于邀请)
|
||
const contacts = await contactApi.getContacts()
|
||
// 过滤掉已经是群成员的联系人
|
||
const memberIds = new Set(memberList.map(m => m.user_id))
|
||
availableContacts.value = contacts.filter(c => !memberIds.has(c.user_id))
|
||
|
||
// 初始化编辑表单
|
||
editForm.value = {
|
||
name: group.name || '',
|
||
avatar: group.avatar || ''
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Failed to load group info:', error)
|
||
toastStore.error('加载群信息失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 判断是否可以管理某个成员
|
||
function canManageMember(member: GroupMember): boolean {
|
||
if (!isOwner.value && !isAdmin.value) return false
|
||
if (member.user_id === authStore.user?.id) return false // 不能管理自己
|
||
if (member.role === 2) return false // 不能管理群主
|
||
if (isAdmin.value && member.role === 1) return false // 管理员不能管理其他管理员
|
||
return true
|
||
}
|
||
|
||
// 处理成员点击
|
||
function handleMemberClick(member: GroupMember) {
|
||
// 可以扩展为显示成员详情
|
||
}
|
||
|
||
// 移除成员
|
||
function showRemoveMemberConfirmDialog(member: GroupMember) {
|
||
pendingMember.value = member
|
||
showRemoveMemberConfirm.value = true
|
||
}
|
||
|
||
async function handleRemoveMember() {
|
||
if (!pendingMember.value) return
|
||
|
||
try {
|
||
await groupApi.removeGroupMember(props.roomId, pendingMember.value.user_id)
|
||
toastStore.success('已移除成员')
|
||
showRemoveMemberConfirm.value = false
|
||
pendingMember.value = null
|
||
await loadGroupInfo()
|
||
emit('updated')
|
||
} catch (error: any) {
|
||
toastStore.error('移除成员失败')
|
||
}
|
||
}
|
||
|
||
// 邀请单个联系人
|
||
async function handleInviteContact(contact: Contact) {
|
||
try {
|
||
await groupApi.inviteGroupMembers(props.roomId, { member_ids: [contact.user_id || contact.id] })
|
||
toastStore.success('邀请成功')
|
||
showInviteModal.value = false
|
||
await loadGroupInfo()
|
||
emit('updated')
|
||
} catch (error: any) {
|
||
toastStore.error('邀请失败')
|
||
}
|
||
}
|
||
|
||
// 更新群信息
|
||
async function handleUpdateGroup() {
|
||
if (!editForm.value.name.trim()) {
|
||
toastStore.warning('群名称不能为空')
|
||
return
|
||
}
|
||
|
||
try {
|
||
await groupApi.updateGroup(props.roomId, {
|
||
name: editForm.value.name.trim(),
|
||
avatar: editForm.value.avatar || editForm.value.name.charAt(0).toUpperCase()
|
||
})
|
||
toastStore.success('更新成功')
|
||
showEditModal.value = false
|
||
await loadGroupInfo()
|
||
emit('updated')
|
||
} catch (error: any) {
|
||
toastStore.error('更新失败')
|
||
}
|
||
}
|
||
|
||
// 退出群聊
|
||
function showQuitConfirmDialog() {
|
||
showQuitConfirm.value = true
|
||
}
|
||
|
||
async function handleQuit() {
|
||
try {
|
||
await groupApi.quitGroup(props.roomId)
|
||
toastStore.success('已退出群聊')
|
||
showQuitConfirm.value = false
|
||
emit('quit')
|
||
emit('close')
|
||
} catch (error: any) {
|
||
toastStore.error('退出失败')
|
||
}
|
||
}
|
||
|
||
// 解散群聊
|
||
function showDissolveConfirmDialog() {
|
||
showDissolveConfirm.value = true
|
||
}
|
||
|
||
async function handleDissolve() {
|
||
try {
|
||
await groupApi.dissolveGroup(props.roomId)
|
||
toastStore.success('群聊已解散')
|
||
emit('dissolve')
|
||
emit('close')
|
||
} catch (error: any) {
|
||
toastStore.error('解散失败')
|
||
}
|
||
}
|
||
|
||
// 监听 show 变化,加载数据
|
||
watch(() => props.show, (newVal) => {
|
||
if (newVal) {
|
||
loadGroupInfo()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.custom-scrollbar::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||
@apply bg-gray-700 rounded-full;
|
||
}
|
||
.animate-fade-in {
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
</style>
|
||
|