群聊
This commit is contained in:
@@ -1,5 +1,52 @@
|
||||
<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 bg-gradient-to-br from-primary/10 to-purple-500/10">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<Avatar
|
||||
:name="groupName"
|
||||
:avatar="groupAvatar"
|
||||
size="lg"
|
||||
rounded="xl"
|
||||
class="border-2 border-white/20"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-lg font-bold text-white truncate">{{ groupName || '未知群聊' }}</h2>
|
||||
<p class="text-xs text-gray-400">{{ members.length }} 名成员</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员/群主操作按钮 -->
|
||||
<div v-if="canEditGroupName || canEditAnnouncement" class="flex gap-2 mt-3">
|
||||
<button
|
||||
v-if="canEditGroupName || canEditAnnouncement"
|
||||
class="flex-1 py-2 bg-primary/20 hover:bg-primary/30 text-primary text-xs rounded-lg transition flex items-center justify-center gap-1"
|
||||
@click="showInviteModal = true"
|
||||
>
|
||||
<i class="fas fa-user-plus"></i>
|
||||
<span>邀请好友</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 群主专属操作 -->
|
||||
<div v-if="isOwner" class="flex gap-2 mt-2">
|
||||
<button
|
||||
class="flex-1 py-2 bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-400 text-xs rounded-lg transition flex items-center justify-center gap-1"
|
||||
@click="showTransferOwnerModal = true"
|
||||
>
|
||||
<i class="fas fa-crown"></i>
|
||||
<span>转让群主</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 text-xs rounded-lg transition flex items-center justify-center gap-1"
|
||||
@click="showDissolveConfirmDialog"
|
||||
>
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
<span>解散群聊</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群名称区域 -->
|
||||
<div class="border-b border-gray-800 p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
@@ -99,13 +146,34 @@
|
||||
<h3 class="text-sm font-semibold text-white">群成员 ({{ members.length }})</h3>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="mb-3">
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-2.5 text-gray-500 text-xs"></i>
|
||||
<input
|
||||
v-model="memberSearchQuery"
|
||||
type="text"
|
||||
class="w-full bg-input rounded-lg py-2 pl-9 pr-3 text-sm text-white focus:ring-2 ring-primary outline-none transition placeholder-gray-500"
|
||||
placeholder="搜索群成员..."
|
||||
@input="handleMemberSearch"
|
||||
/>
|
||||
<button
|
||||
v-if="memberSearchQuery"
|
||||
class="absolute right-2 top-2 text-gray-500 hover:text-white transition"
|
||||
@click="clearMemberSearch"
|
||||
>
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</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">
|
||||
暂无成员
|
||||
{{ memberSearchQuery ? '未找到匹配的成员' : '暂无成员' }}
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
@@ -114,6 +182,7 @@
|
||||
: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)"
|
||||
@contextmenu.stop="showMemberContextMenu($event, member)"
|
||||
>
|
||||
<div class="relative shrink-0">
|
||||
<Avatar
|
||||
@@ -159,16 +228,123 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邀请好友模态框 -->
|
||||
<SelectContactsModal
|
||||
v-if="showInviteModal"
|
||||
:show="showInviteModal"
|
||||
:contacts="availableContacts"
|
||||
mode="invite"
|
||||
@close="showInviteModal = false"
|
||||
@invite="handleInviteMembers"
|
||||
/>
|
||||
|
||||
<ContextMenu />
|
||||
|
||||
<!-- 转让群主模态框 -->
|
||||
<div
|
||||
v-if="showTransferOwnerModal"
|
||||
class="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm"
|
||||
@click.self="showTransferOwnerModal = false"
|
||||
>
|
||||
<div class="bg-panel rounded-2xl w-96 shadow-2xl border border-gray-700 overflow-hidden animate-fade-in">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-4">转让群主</h3>
|
||||
<p class="text-sm text-gray-400 mb-4">请选择要转让给的新群主:</p>
|
||||
<div class="max-h-64 overflow-y-auto custom-scrollbar space-y-2 mb-4">
|
||||
<div
|
||||
v-for="member in members.filter(m => m.role !== 2 && m.user_id !== authStore.user?.id)"
|
||||
:key="member.user_id"
|
||||
class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition cursor-pointer"
|
||||
@click="showTransferOwnerConfirmDialog(member.user_id)"
|
||||
>
|
||||
<Avatar
|
||||
:name="member.user?.name || member.nickname || '未知'"
|
||||
:avatar="member.user?.avatar"
|
||||
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 === 1" class="text-blue-400">管理员</span>
|
||||
<span v-else>成员</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="w-full py-2 bg-gray-700 text-gray-300 text-sm rounded-lg hover:bg-gray-600 transition"
|
||||
@click="showTransferOwnerModal = false"
|
||||
>
|
||||
取消
|
||||
</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="showDissolveConfirm"
|
||||
title="解散群聊"
|
||||
message="确定要解散群聊吗?解散后所有成员将被移除,且无法恢复。"
|
||||
type="danger"
|
||||
confirm-text="解散"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDissolveGroup"
|
||||
@cancel="showDissolveConfirm = false"
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
:show="showTransferOwnerConfirm"
|
||||
title="转让群主"
|
||||
message="确定要将群主转让给该成员吗?转让后您将失去群主权限。"
|
||||
type="warning"
|
||||
confirm-text="转让"
|
||||
cancel-text="取消"
|
||||
@confirm="handleTransferOwner"
|
||||
@cancel="showTransferOwnerConfirm = false; pendingNewOwnerId = ''"
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
:show="showSetAdminConfirm"
|
||||
title="设置管理员"
|
||||
:message="setAdminMessage"
|
||||
type="info"
|
||||
confirm-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleSetAdmin"
|
||||
@cancel="showSetAdminConfirm = false; pendingMember = null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import Avatar from '@/components/common/Avatar.vue'
|
||||
import SelectContactsModal from '@/components/chat/SelectContactsModal.vue'
|
||||
import ContextMenu from '@/components/common/ContextMenu.vue'
|
||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||
import * as groupApi from '@/api/modules/room'
|
||||
import * as contactApi from '@/api/modules/contact'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import type { GroupMember } from '@/types/api'
|
||||
import { useContextMenu } from '@/composables/useContextMenu'
|
||||
import type { GroupMember, Contact } from '@/types/api'
|
||||
|
||||
interface Props {
|
||||
roomId: string
|
||||
@@ -180,21 +356,45 @@ const emit = defineEmits<{
|
||||
'member-removed': []
|
||||
'member-clicked': [member: GroupMember]
|
||||
'group-updated': []
|
||||
'invite-members': []
|
||||
'dissolve': []
|
||||
'transfer-owner': [newOwnerId: string]
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const chatStore = useChatStore()
|
||||
const toastStore = useToastStore()
|
||||
const { showContextMenu } = useContextMenu()
|
||||
|
||||
// 可邀请的联系人(排除已在群中的成员)
|
||||
const availableContacts = computed(() => {
|
||||
const memberIds = new Set(members.value.map(m => m.user_id))
|
||||
return chatStore.contacts.filter(c => {
|
||||
const userId = c.user_id || c.id
|
||||
return userId !== authStore.user?.id && !memberIds.has(userId)
|
||||
})
|
||||
})
|
||||
|
||||
const loadingMembers = ref(false)
|
||||
const members = ref<GroupMember[]>([])
|
||||
const memberSearchQuery = ref('')
|
||||
const announcement = ref('')
|
||||
const isEditingAnnouncement = ref(false)
|
||||
const editAnnouncementText = ref('')
|
||||
const loadingAnnouncement = ref(false)
|
||||
const groupName = ref('')
|
||||
const groupAvatar = ref('')
|
||||
const isEditingGroupName = ref(false)
|
||||
const editGroupNameText = ref('')
|
||||
const loadingGroupName = ref(false)
|
||||
const showInviteModal = ref(false)
|
||||
const showTransferOwnerModal = ref(false)
|
||||
const showRemoveMemberConfirm = ref(false)
|
||||
const showDissolveConfirm = ref(false)
|
||||
const showTransferOwnerConfirm = ref(false)
|
||||
const showSetAdminConfirm = ref(false)
|
||||
const pendingMember = ref<GroupMember | null>(null)
|
||||
const pendingNewOwnerId = ref<string>('')
|
||||
|
||||
// 当前用户角色
|
||||
const currentUserRole = computed(() => {
|
||||
@@ -215,6 +415,18 @@ const canEditAnnouncement = computed(() => isOwner.value || isAdmin.value)
|
||||
// 是否可以编辑群名
|
||||
const canEditGroupName = computed(() => isOwner.value || isAdmin.value)
|
||||
|
||||
// 移除成员确认消息
|
||||
const removeMemberMessage = computed(() => {
|
||||
const name = pendingMember.value?.user?.name || pendingMember.value?.nickname || '未知'
|
||||
return `确定要移除成员 "${name}" 吗?`
|
||||
})
|
||||
|
||||
// 设置管理员确认消息
|
||||
const setAdminMessage = computed(() => {
|
||||
const name = pendingMember.value?.user?.name || pendingMember.value?.nickname || '未知'
|
||||
return `确定要将 "${name}" 设为管理员吗?`
|
||||
})
|
||||
|
||||
// 判断是否可以管理某个成员
|
||||
function canManageMember(member: GroupMember): boolean {
|
||||
// 群主可以管理所有成员(除了自己)
|
||||
@@ -229,12 +441,12 @@ function canManageMember(member: GroupMember): boolean {
|
||||
}
|
||||
|
||||
// 加载群成员
|
||||
async function loadMembers() {
|
||||
async function loadMembers(keyword?: string) {
|
||||
if (loadingMembers.value) return
|
||||
|
||||
loadingMembers.value = true
|
||||
try {
|
||||
const memberList = await groupApi.getGroupMembers(props.roomId)
|
||||
const memberList = await groupApi.getGroupMembers(props.roomId, keyword)
|
||||
members.value = memberList as unknown as GroupMember[]
|
||||
} catch (error) {
|
||||
console.error('Failed to load group members:', error)
|
||||
@@ -245,6 +457,22 @@ async function loadMembers() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理成员搜索
|
||||
function handleMemberSearch() {
|
||||
const keyword = memberSearchQuery.value.trim()
|
||||
if (keyword) {
|
||||
loadMembers(keyword)
|
||||
} else {
|
||||
loadMembers()
|
||||
}
|
||||
}
|
||||
|
||||
// 清除搜索
|
||||
function clearMemberSearch() {
|
||||
memberSearchQuery.value = ''
|
||||
loadMembers()
|
||||
}
|
||||
|
||||
// 加载群信息(包括群名和公告)
|
||||
async function loadGroupInfo() {
|
||||
if (loadingGroupName.value) return
|
||||
@@ -256,6 +484,7 @@ async function loadGroupInfo() {
|
||||
groupApi.getGroupAnnouncement(props.roomId).catch(() => ({ announcement: '' }))
|
||||
])
|
||||
groupName.value = groupInfo.name || groupInfo.room_name || ''
|
||||
groupAvatar.value = groupInfo.avatar || groupInfo.room_avatar || ''
|
||||
announcement.value = annRes.announcement || ''
|
||||
} catch (error) {
|
||||
console.error('Failed to load group info:', error)
|
||||
@@ -346,23 +575,157 @@ function handleMemberClick(member: GroupMember) {
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
async function handleRemoveMember(member: GroupMember) {
|
||||
if (!confirm(`确定要移除成员 "${member.user?.name || member.nickname || '未知'}" 吗?`)) {
|
||||
return
|
||||
}
|
||||
function showRemoveMemberConfirmDialog(member: GroupMember) {
|
||||
pendingMember.value = member
|
||||
showRemoveMemberConfirm.value = true
|
||||
}
|
||||
|
||||
async function handleRemoveMember() {
|
||||
if (!pendingMember.value) return
|
||||
|
||||
try {
|
||||
await groupApi.removeGroupMember(props.roomId, member.user_id)
|
||||
await groupApi.removeGroupMember(props.roomId, pendingMember.value.user_id)
|
||||
toastStore.success('成员已移除')
|
||||
// 重新加载成员列表
|
||||
await loadMembers()
|
||||
emit('member-removed')
|
||||
showRemoveMemberConfirm.value = false
|
||||
pendingMember.value = null
|
||||
} catch (error: any) {
|
||||
console.error('Failed to remove member:', error)
|
||||
toastStore.error(error.message || '移除成员失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请成员
|
||||
async function handleInviteMembers(data: { member_ids: string[] }) {
|
||||
if (!data.member_ids || data.member_ids.length === 0) {
|
||||
toastStore.warning('请选择要邀请的好友')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await groupApi.inviteGroupMembers(props.roomId, {
|
||||
member_ids: data.member_ids
|
||||
})
|
||||
toastStore.success('邀请成功')
|
||||
showInviteModal.value = false
|
||||
// 重新加载成员列表
|
||||
await loadMembers()
|
||||
emit('invite-members')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to invite members:', error)
|
||||
toastStore.error(error.message || '邀请失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 解散群聊
|
||||
function showDissolveConfirmDialog() {
|
||||
showDissolveConfirm.value = true
|
||||
}
|
||||
|
||||
async function handleDissolveGroup() {
|
||||
try {
|
||||
await groupApi.dissolveGroup(props.roomId)
|
||||
toastStore.success('群聊已解散')
|
||||
showDissolveConfirm.value = false
|
||||
emit('dissolve')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to dissolve group:', error)
|
||||
toastStore.error(error.message || '解散群聊失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 转让群主
|
||||
function showTransferOwnerConfirmDialog(newOwnerId: string) {
|
||||
if (!newOwnerId) {
|
||||
toastStore.warning('请选择新群主')
|
||||
return
|
||||
}
|
||||
pendingNewOwnerId.value = newOwnerId
|
||||
showTransferOwnerConfirm.value = true
|
||||
}
|
||||
|
||||
async function handleTransferOwner() {
|
||||
if (!pendingNewOwnerId.value) return
|
||||
|
||||
try {
|
||||
await groupApi.updateMemberRole(props.roomId, pendingNewOwnerId.value, { role: 2 })
|
||||
// 将自己的角色改为成员
|
||||
if (authStore.user?.id) {
|
||||
await groupApi.updateMemberRole(props.roomId, authStore.user.id, { role: 0 })
|
||||
}
|
||||
toastStore.success('群主转让成功')
|
||||
showTransferOwnerModal.value = false
|
||||
showTransferOwnerConfirm.value = false
|
||||
pendingNewOwnerId.value = ''
|
||||
// 重新加载成员列表
|
||||
await loadMembers()
|
||||
emit('transfer-owner', pendingNewOwnerId.value)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to transfer owner:', error)
|
||||
toastStore.error(error.message || '转让群主失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 显示成员右键菜单
|
||||
function showMemberContextMenu(event: MouseEvent, member: GroupMember) {
|
||||
const menuItems: any[] = []
|
||||
|
||||
// 为所有成员添加"查看资料"选项
|
||||
menuItems.push({
|
||||
label: '查看资料',
|
||||
icon: 'fas fa-user',
|
||||
action: () => handleMemberClick(member)
|
||||
})
|
||||
|
||||
// 群主可以设置管理员(仅对普通成员)
|
||||
if (isOwner.value && member.role === 0 && member.user_id !== authStore.user?.id) {
|
||||
menuItems.push({
|
||||
label: '设为管理员',
|
||||
icon: 'fas fa-user-shield',
|
||||
action: () => showSetAdminConfirmDialog(member)
|
||||
})
|
||||
}
|
||||
|
||||
// 管理员和群主可以移除成员
|
||||
if (canManageMember(member)) {
|
||||
menuItems.push({
|
||||
label: '移除群聊',
|
||||
icon: 'fas fa-user-minus',
|
||||
danger: true,
|
||||
action: () => showRemoveMemberConfirmDialog(member)
|
||||
})
|
||||
}
|
||||
|
||||
if (menuItems.length > 0) {
|
||||
showContextMenu(event, menuItems)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置管理员
|
||||
function showSetAdminConfirmDialog(member: GroupMember) {
|
||||
pendingMember.value = member
|
||||
showSetAdminConfirm.value = true
|
||||
}
|
||||
|
||||
async function handleSetAdmin() {
|
||||
if (!pendingMember.value) return
|
||||
|
||||
try {
|
||||
await groupApi.updateMemberRole(props.roomId, pendingMember.value.user_id, { role: 1 })
|
||||
toastStore.success('已设为管理员')
|
||||
// 重新加载成员列表
|
||||
await loadMembers()
|
||||
showSetAdminConfirm.value = false
|
||||
pendingMember.value = null
|
||||
emit('group-updated')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to set admin:', error)
|
||||
toastStore.error(error.message || '设置管理员失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 roomId 变化
|
||||
watch(() => props.roomId, () => {
|
||||
if (props.roomId) {
|
||||
@@ -371,11 +734,18 @@ watch(() => props.roomId, () => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
if (props.roomId) {
|
||||
loadMembers()
|
||||
loadGroupInfo()
|
||||
}
|
||||
// 加载联系人列表(用于邀请)
|
||||
try {
|
||||
const contacts = await contactApi.getContacts()
|
||||
chatStore.setContacts(contacts)
|
||||
} catch (error) {
|
||||
console.error('Failed to load contacts:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<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="handleDissolve"
|
||||
@click="showDissolveConfirmDialog"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
<span>解散群聊</span>
|
||||
@@ -70,7 +70,7 @@
|
||||
<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="handleQuit"
|
||||
@click="showQuitConfirmDialog"
|
||||
>
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span>退出群聊</span>
|
||||
@@ -111,7 +111,7 @@
|
||||
<button
|
||||
v-if="member.role !== 2"
|
||||
class="text-gray-400 hover:text-red-400 transition text-sm"
|
||||
@click.stop="handleRemoveMember(member)"
|
||||
@click.stop="showRemoveMemberConfirmDialog(member)"
|
||||
>
|
||||
<i class="fas fa-user-minus"></i>
|
||||
</button>
|
||||
@@ -190,6 +190,40 @@
|
||||
</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>
|
||||
|
||||
@@ -202,6 +236,7 @@ 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
|
||||
@@ -228,6 +263,10 @@ 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(() => {
|
||||
@@ -248,6 +287,12 @@ 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
|
||||
@@ -295,14 +340,19 @@ function handleMemberClick(member: GroupMember) {
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
async function handleRemoveMember(member: GroupMember) {
|
||||
if (!confirm(`确定要将 ${member.user?.name || member.nickname} 移出群聊吗?`)) {
|
||||
return
|
||||
}
|
||||
function showRemoveMemberConfirmDialog(member: GroupMember) {
|
||||
pendingMember.value = member
|
||||
showRemoveMemberConfirm.value = true
|
||||
}
|
||||
|
||||
async function handleRemoveMember() {
|
||||
if (!pendingMember.value) return
|
||||
|
||||
try {
|
||||
await groupApi.removeGroupMember(props.roomId, member.user_id)
|
||||
await groupApi.removeGroupMember(props.roomId, pendingMember.value.user_id)
|
||||
toastStore.success('已移除成员')
|
||||
showRemoveMemberConfirm.value = false
|
||||
pendingMember.value = null
|
||||
await loadGroupInfo()
|
||||
emit('updated')
|
||||
} catch (error: any) {
|
||||
@@ -345,14 +395,15 @@ async function handleUpdateGroup() {
|
||||
}
|
||||
|
||||
// 退出群聊
|
||||
function showQuitConfirmDialog() {
|
||||
showQuitConfirm.value = true
|
||||
}
|
||||
|
||||
async function handleQuit() {
|
||||
if (!confirm('确定要退出该群聊吗?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await groupApi.quitGroup(props.roomId)
|
||||
toastStore.success('已退出群聊')
|
||||
showQuitConfirm.value = false
|
||||
emit('quit')
|
||||
emit('close')
|
||||
} catch (error: any) {
|
||||
@@ -361,11 +412,11 @@ async function handleQuit() {
|
||||
}
|
||||
|
||||
// 解散群聊
|
||||
function showDissolveConfirmDialog() {
|
||||
showDissolveConfirm.value = true
|
||||
}
|
||||
|
||||
async function handleDissolve() {
|
||||
if (!confirm('确定要解散该群聊吗?此操作不可恢复!')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await groupApi.dissolveGroup(props.roomId)
|
||||
toastStore.success('群聊已解散')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!-- 系统/通知消息:居中灰色提示条样式 -->
|
||||
<div v-if="isSystemOrNotify" class="flex justify-center my-2">
|
||||
<div class="bg-gray-700/50 text-gray-400 text-xs px-4 py-2 rounded-full border border-gray-600/30 max-w-[80%] text-center">
|
||||
<div v-if="isSystemOrNotify" class="flex justify-center items-center w-full my-2">
|
||||
<div class="bg-gray-700/50 text-gray-400 text-xs px-4 py-2 rounded-full border border-gray-600/30 text-center mx-auto">
|
||||
<i :class="systemIcon" class="mr-1.5"></i>
|
||||
<span>{{ systemContent }}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
<template>
|
||||
<div class="p-4 bg-panel border-t border-gray-800 shrink-0 z-20">
|
||||
<div class="p-4 bg-panel border-t border-gray-800 shrink-0 z-20 relative">
|
||||
<!-- 遮罩层:限制被移除/退出用户发言 -->
|
||||
<div
|
||||
v-if="isBlocked"
|
||||
class="absolute inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center cursor-not-allowed"
|
||||
@click.stop
|
||||
@contextmenu.prevent
|
||||
@mousedown.prevent
|
||||
@selectstart.prevent
|
||||
>
|
||||
<div class="text-center px-6">
|
||||
<i class="fas fa-ban text-4xl text-red-500 mb-3"></i>
|
||||
<p class="text-white font-semibold text-sm mb-1">您已被移出群聊</p>
|
||||
<p class="text-gray-400 text-xs">无法在此群聊中发送消息</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-6 text-gray-400 text-lg mb-3 px-2 items-center">
|
||||
<i
|
||||
class="far fa-smile hover:text-primary cursor-pointer transition-all transform hover:scale-125 hover:rotate-12"
|
||||
class="far fa-smile hover:text-primary transition-all transform hover:scale-125 hover:rotate-12"
|
||||
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
|
||||
title="表情"
|
||||
@click="insertEmoji('😊')"
|
||||
@click="!isBlocked && insertEmoji('😊')"
|
||||
></i>
|
||||
<i
|
||||
class="far fa-image hover:text-green-400 cursor-pointer transition-all transform hover:scale-125"
|
||||
@click="triggerFile(1)"
|
||||
class="far fa-image hover:text-green-400 transition-all transform hover:scale-125"
|
||||
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
|
||||
@click="!isBlocked && triggerFile(1)"
|
||||
title="发送图片"
|
||||
></i>
|
||||
|
||||
<!-- 新增:视频上传按钮 -->
|
||||
<i
|
||||
class="fas fa-video hover:text-purple-400 cursor-pointer transition-all transform hover:scale-125"
|
||||
@click="triggerFile(3)"
|
||||
class="fas fa-video hover:text-purple-400 transition-all transform hover:scale-125"
|
||||
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
|
||||
@click="!isBlocked && triggerFile(3)"
|
||||
title="发送视频"
|
||||
></i>
|
||||
|
||||
<i
|
||||
class="fas fa-folder-plus hover:text-yellow-400 cursor-pointer transition-all transform hover:scale-125"
|
||||
@click="triggerFile(8)"
|
||||
class="fas fa-folder-plus hover:text-yellow-400 transition-all transform hover:scale-125"
|
||||
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
|
||||
@click="!isBlocked && triggerFile(8)"
|
||||
title="发送文件"
|
||||
></i>
|
||||
|
||||
<!-- 长按录音 -->
|
||||
<div
|
||||
class="relative flex items-center group cursor-pointer select-none ml-auto mr-2"
|
||||
@mousedown="handleRecordStart"
|
||||
@touchstart.passive="handleRecordStart"
|
||||
@mouseup="handleRecordStop"
|
||||
@touchend.prevent="handleRecordStop"
|
||||
@mouseleave="handleRecordCancel"
|
||||
class="relative flex items-center group select-none ml-auto mr-2"
|
||||
:class="isBlocked ? 'cursor-not-allowed' : 'cursor-pointer'"
|
||||
@mousedown="!isBlocked && handleRecordStart()"
|
||||
@touchstart.passive="!isBlocked && handleRecordStart()"
|
||||
@mouseup="!isBlocked && handleRecordStop()"
|
||||
@touchend.prevent="!isBlocked && handleRecordStop()"
|
||||
@mouseleave="!isBlocked && handleRecordCancel()"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center transition-all shadow-md"
|
||||
@@ -53,16 +73,19 @@
|
||||
<div class="bg-input rounded-3xl p-2 pl-4 flex items-end ring-1 ring-white/5 focus-within:ring-primary/50 transition-all shadow-inner hover:shadow-lg hover:shadow-black/20">
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
class="w-full bg-transparent border-none text-white resize-none h-10 max-h-32 py-2 px-1 text-sm focus:outline-none scrollbar-thin placeholder-gray-500 leading-6"
|
||||
:disabled="isBlocked"
|
||||
class="w-full bg-transparent border-none text-white resize-none h-10 max-h-32 py-2 px-1 text-sm focus:outline-none scrollbar-thin placeholder-gray-500 leading-6 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="输入消息 (Enter 发送)..."
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
||||
@keydown.enter.exact.prevent="$emit('send')"
|
||||
@keydown.enter.exact.prevent="!isBlocked && $emit('send')"
|
||||
@paste="handlePaste"
|
||||
></textarea>
|
||||
<div
|
||||
class="w-10 h-10 rounded-2xl flex items-center justify-center ml-2 transition-all duration-300 cursor-pointer shadow-lg"
|
||||
:class="modelValue.trim() ? 'bg-primary hover:bg-indigo-500 text-white scale-100 rotate-0' : 'bg-gray-700 text-gray-500 scale-90 rotate-90 opacity-50 cursor-not-allowed'"
|
||||
@click="$emit('send')"
|
||||
class="w-10 h-10 rounded-2xl flex items-center justify-center ml-2 transition-all duration-300 shadow-lg"
|
||||
:class="isBlocked
|
||||
? 'bg-gray-700 text-gray-500 scale-90 rotate-90 opacity-50 cursor-not-allowed'
|
||||
: (modelValue.trim() ? 'bg-primary hover:bg-indigo-500 text-white scale-100 rotate-0 cursor-pointer' : 'bg-gray-700 text-gray-500 scale-90 rotate-90 opacity-50 cursor-not-allowed')"
|
||||
@click="!isBlocked && $emit('send')"
|
||||
>
|
||||
<i class="fas fa-paper-plane text-sm transform -ml-0.5 mt-0.5"></i>
|
||||
</div>
|
||||
@@ -78,7 +101,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
|
||||
const toastStore = useToastStore()
|
||||
@@ -86,6 +109,7 @@ const toastStore = useToastStore()
|
||||
interface Props {
|
||||
modelValue: string
|
||||
isRecording: boolean
|
||||
isBlocked?: boolean // 是否被禁言(被移除或退出群聊)
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -119,6 +143,14 @@ function triggerFile(type: number) {
|
||||
}
|
||||
|
||||
function handleFileChange(e: Event) {
|
||||
if (props.isBlocked) {
|
||||
e.preventDefault()
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = ''
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const target = e.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (file) {
|
||||
@@ -131,6 +163,11 @@ function handleFileChange(e: Event) {
|
||||
}
|
||||
|
||||
function handlePaste(e: ClipboardEvent) {
|
||||
if (props.isBlocked) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const items = e.clipboardData?.items
|
||||
if (!items) return
|
||||
|
||||
@@ -198,4 +235,80 @@ function handleRecordCancel() {
|
||||
emit('record-cancel')
|
||||
}
|
||||
}
|
||||
|
||||
// 防止遮罩层被删除(通过 MutationObserver 监控)
|
||||
let observer: MutationObserver | null = null
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined' && props.isBlocked) {
|
||||
// 延迟设置 observer,确保 DOM 已渲染
|
||||
setTimeout(() => {
|
||||
const container = document.querySelector('.p-4.bg-panel.border-t.border-gray-800.relative')
|
||||
if (container) {
|
||||
observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node instanceof HTMLElement &&
|
||||
node.classList.contains('absolute') &&
|
||||
node.classList.contains('inset-0') &&
|
||||
node.classList.contains('bg-black\\/80')) {
|
||||
// 如果遮罩层被删除,刷新页面
|
||||
if (props.isBlocked) {
|
||||
console.warn('检测到遮罩层被删除,刷新页面以恢复')
|
||||
setTimeout(() => location.reload(), 100)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
observer.observe(container, { childList: true, subtree: true })
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 isBlocked 变化,重新设置 observer
|
||||
watch(() => props.isBlocked, (newVal) => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
|
||||
if (newVal && typeof window !== 'undefined') {
|
||||
setTimeout(() => {
|
||||
const container = document.querySelector('.p-4.bg-panel.border-t.border-gray-800.relative')
|
||||
if (container) {
|
||||
observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node instanceof HTMLElement &&
|
||||
node.classList.contains('absolute') &&
|
||||
node.classList.contains('inset-0') &&
|
||||
node.classList.contains('bg-black\\/80')) {
|
||||
if (props.isBlocked) {
|
||||
console.warn('检测到遮罩层被删除,刷新页面以恢复')
|
||||
setTimeout(() => location.reload(), 100)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
observer.observe(container, { childList: true, subtree: true })
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -38,11 +38,15 @@
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
class="flex gap-3 msg-row group"
|
||||
:class="{ 'flex-row-reverse': msg.isSelf }"
|
||||
:class="{
|
||||
'flex-row-reverse': msg.isSelf && !isSystemMessage(msg),
|
||||
'justify-center': isSystemMessage(msg)
|
||||
}"
|
||||
@contextmenu.stop="$emit('contextmenu', $event, msg)"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<!-- 系统消息不显示头像 -->
|
||||
<Avatar
|
||||
v-if="!isSystemMessage(msg)"
|
||||
:name="getSenderName(msg)"
|
||||
:avatar="getSenderAvatar(msg)"
|
||||
:color="msg.isSelf ? generateColor(currentUser.id) : (getMemberColor(msg.sender_user_id) || target.color)"
|
||||
@@ -53,21 +57,29 @@
|
||||
/>
|
||||
|
||||
<!-- 消息内容区 -->
|
||||
<div class="flex flex-col max-w-[75%] md:max-w-[60%]">
|
||||
<!-- 群聊中显示发送者昵称(非自己发送的消息) -->
|
||||
<div
|
||||
class="flex flex-col"
|
||||
:class="isSystemMessage(msg) ? 'w-full items-center' : 'max-w-[75%] md:max-w-[60%]'"
|
||||
>
|
||||
<!-- 系统消息不显示发送者名称 -->
|
||||
<div
|
||||
v-if="isGroupChat && !msg.isSelf"
|
||||
class="text-[10px] text-gray-500 mb-1 px-1"
|
||||
v-if="!isSystemMessage(msg)"
|
||||
>
|
||||
{{ getSenderDisplayName(msg) }}
|
||||
</div>
|
||||
<!-- 单聊或自己发送的消息 -->
|
||||
<div
|
||||
v-else-if="!isGroupChat || msg.isSelf"
|
||||
class="text-[10px] text-gray-500 mb-1 px-1"
|
||||
:class="{ 'text-right': msg.isSelf }"
|
||||
>
|
||||
{{ msg.isSelf ? 'Me' : (target.remark_name || target.user?.name) }}
|
||||
<!-- 群聊中显示发送者昵称(非自己发送的消息) -->
|
||||
<div
|
||||
v-if="isGroupChat && !msg.isSelf"
|
||||
class="text-[10px] text-gray-500 mb-1 px-1"
|
||||
>
|
||||
{{ getSenderDisplayName(msg) }}
|
||||
</div>
|
||||
<!-- 单聊或自己发送的消息 -->
|
||||
<div
|
||||
v-else-if="!isGroupChat || msg.isSelf"
|
||||
class="text-[10px] text-gray-500 mb-1 px-1"
|
||||
:class="{ 'text-right': msg.isSelf }"
|
||||
>
|
||||
{{ msg.isSelf ? 'Me' : (target.remark_name || target.user?.name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息气泡 -->
|
||||
@@ -82,6 +94,7 @@ import { ref, computed } from 'vue'
|
||||
import { generateColor } from '@/utils/format'
|
||||
import Avatar from '@/components/common/Avatar.vue'
|
||||
import MessageBubble from '@/components/chat/MessageBubble.vue'
|
||||
import { isSystemOrNotifyMessage } from '@/utils/messageTypes'
|
||||
import type { ChatMessage, User, Contact } from '@/types/api'
|
||||
|
||||
interface Props {
|
||||
@@ -107,6 +120,11 @@ const containerRef = ref<HTMLElement | null>(null)
|
||||
// 判断是否为群聊
|
||||
const isGroupChat = computed(() => props.target.is_group || props.target.room_type === 'group')
|
||||
|
||||
// 判断是否为系统消息
|
||||
function isSystemMessage(msg: ChatMessage): boolean {
|
||||
return isSystemOrNotifyMessage(msg.message_type)
|
||||
}
|
||||
|
||||
// 获取发送者名称
|
||||
function getSenderName(msg: ChatMessage): string {
|
||||
if (msg.isSelf) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="bg-panel rounded-2xl w-full max-w-md shadow-2xl border border-gray-700 overflow-hidden animate-fade-in max-h-[80vh] 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>
|
||||
<h2 class="text-xl font-bold text-white">{{ mode === 'invite' ? '邀请好友加入群聊' : '选择联系人创建群聊' }}</h2>
|
||||
<button
|
||||
class="text-gray-400 hover:text-white transition text-xl"
|
||||
@click="$emit('close')"
|
||||
@@ -85,6 +85,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群名称输入(仅在创建模式下显示) -->
|
||||
<div v-if="mode === 'create'" class="px-6 py-3 border-t border-gray-800">
|
||||
<input
|
||||
v-model="groupName"
|
||||
type="text"
|
||||
class="w-full bg-input rounded-xl py-2 px-4 text-sm text-white focus:ring-2 ring-primary outline-none transition placeholder-gray-500"
|
||||
placeholder="请输入群名称(必填)"
|
||||
maxlength="20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div class="px-6 py-4 border-t border-gray-800 flex justify-between items-center">
|
||||
<div class="text-sm text-gray-400">
|
||||
@@ -99,24 +110,13 @@
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-xl bg-primary hover:bg-indigo-600 text-white transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="selectedContacts.length < 1 || !groupName.trim()"
|
||||
:disabled="selectedContacts.length < 1 || (mode === 'create' && !groupName.trim())"
|
||||
@click="handleCreate"
|
||||
>
|
||||
创建群聊
|
||||
{{ mode === 'invite' ? '邀请' : '创建群聊' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群名称输入(在底部操作区上方) -->
|
||||
<div class="px-6 py-3 border-t border-gray-800">
|
||||
<input
|
||||
v-model="groupName"
|
||||
type="text"
|
||||
class="w-full bg-input rounded-xl py-2 px-4 text-sm text-white focus:ring-2 ring-primary outline-none transition placeholder-gray-500"
|
||||
placeholder="请输入群名称(必填)"
|
||||
maxlength="20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -129,13 +129,17 @@ import Avatar from '@/components/common/Avatar.vue'
|
||||
interface Props {
|
||||
show: boolean
|
||||
contacts: Contact[]
|
||||
mode?: 'create' | 'invite' // 创建群聊或邀请好友
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'create'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'close': []
|
||||
'create': [data: { member_ids: string[]; name: string; avatar?: string }]
|
||||
'create': [data: { member_ids: string[]; name?: string; avatar?: string }]
|
||||
'invite': [data: { member_ids: string[] }]
|
||||
}>()
|
||||
|
||||
const searchQuery = ref('')
|
||||
@@ -174,20 +178,29 @@ function removeContact(contactId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 创建群聊
|
||||
// 创建群聊或邀请好友
|
||||
function handleCreate() {
|
||||
if (selectedContacts.value.length < 1 || !groupName.value.trim()) {
|
||||
if (selectedContacts.value.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const memberIds = selectedContacts.value.map(c => c.user_id || c.id)
|
||||
const avatar = groupName.value.charAt(0).toUpperCase() // 使用群名称首字母作为头像
|
||||
|
||||
emit('create', {
|
||||
member_ids: memberIds,
|
||||
name: groupName.value.trim(),
|
||||
avatar
|
||||
})
|
||||
if (props.mode === 'invite') {
|
||||
emit('invite', {
|
||||
member_ids: memberIds
|
||||
})
|
||||
} else {
|
||||
if (!groupName.value.trim()) {
|
||||
return
|
||||
}
|
||||
const avatar = groupName.value.charAt(0).toUpperCase() // 使用群名称首字母作为头像
|
||||
emit('create', {
|
||||
member_ids: memberIds,
|
||||
name: groupName.value.trim(),
|
||||
avatar
|
||||
})
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
selectedContacts.value = []
|
||||
|
||||
@@ -41,6 +41,13 @@
|
||||
<div v-if="toast.description" class="text-xs text-gray-400 mt-1">
|
||||
{{ toast.description }}
|
||||
</div>
|
||||
<button
|
||||
v-if="toast.action"
|
||||
class="mt-2 text-xs text-primary hover:text-primary/80 underline transition"
|
||||
@click="handleAction(toast)"
|
||||
>
|
||||
{{ toast.action.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
@@ -65,6 +72,13 @@ const toasts = computed(() => toastStore.toasts)
|
||||
function removeToast(id: string) {
|
||||
toastStore.removeToast(id)
|
||||
}
|
||||
|
||||
function handleAction(toast: Toast) {
|
||||
if (toast.action) {
|
||||
toast.action.handler()
|
||||
removeToast(toast.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user