右键菜单

This commit is contained in:
2025-12-08 13:57:37 +08:00
parent b7a90e2fc2
commit 9012caa7aa
2 changed files with 217 additions and 26 deletions

View File

@@ -204,6 +204,31 @@
</div>
<ContextMenu />
<!-- 弹窗 4移除成员确认 -->
<div v-if="removingMember" class="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm animate-fade-in">
<div class="bg-gray-800 rounded-xl p-6 w-80 text-center border border-gray-700 shadow-2xl">
<div class="flex justify-center mb-4">
<Avatar
:name="removingMember.user?.name || removingMember.nickname"
:avatar="removingMember.user?.avatar"
size="lg"
rounded="full"
/>
</div>
<h3 class="text-white font-bold mb-2">移除成员</h3>
<p class="text-gray-400 text-sm mb-6">
确定要将 <span class="text-white font-medium">{{ removingMember.nickname || removingMember.user?.name || '该成员' }}</span> 移出群聊吗
</p>
<div class="flex gap-3">
<button class="flex-1 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 text-sm" @click="removingMember = null">取消</button>
<button class="flex-1 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm" :disabled="removing" @click="handleRemoveMember">
<i v-if="removing" class="fas fa-circle-notch fa-spin mr-1"></i>
确定移除
</button>
</div>
</div>
</div>
</div>
</template>
@@ -221,9 +246,10 @@ import { useToastStore } from '@/stores/toast'
import { useContextMenu } from '@/composables/useContextMenu'
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
import type { GroupMember, Contact } from '@/types/api'
import type { MenuItem } from '@/stores/contextMenu'
const props = defineProps<{ roomId: string }>()
const emit = defineEmits(['member-clicked', 'group-updated', 'show-info', 'show-notice'])
const emit = defineEmits(['member-clicked', 'member-removed', 'group-updated', 'show-info', 'show-notice'])
const authStore = useAuthStore()
const chatStore = useChatStore()
@@ -243,13 +269,32 @@ const showCallModal = ref(false)
const showAnnouncementModal = ref(false)
const isEditingAnnouncement = ref(false)
const editAnnouncementText = ref('')
const removingMember = ref<GroupMember | null>(null)
const removing = ref(false)
// Computed
const isOwner = computed(() => members.value.find(m => m.user_id === authStore.user?.id)?.role === 2)
const isAdmin = computed(() => members.value.find(m => m.user_id === authStore.user?.id)?.role === 1)
const currentUserRole = computed(() => members.value.find(m => m.user_id === authStore.user?.id)?.role ?? 0)
const isOwner = computed(() => currentUserRole.value === 2)
const isAdmin = computed(() => currentUserRole.value === 1)
const canInvite = computed(() => isOwner.value || isAdmin.value)
const canEditAnnouncement = computed(() => isOwner.value || isAdmin.value)
// 判断是否为好友
function isFriendCheck(userId: string): boolean {
return chatStore.contacts.some(c => c.user_id === userId || c.id === userId)
}
// 判断是否可以移除某个成员
function canRemoveMember(member: GroupMember): boolean {
// 不能移除自己
if (member.user_id === authStore.user?.id) return false
// 群主可以移除任何人
if (isOwner.value) return true
// 管理员只能移除普通成员
if (isAdmin.value && member.role === 0) return true
return false
}
// 过滤出可通话的候选人(排除自己)
const callCandidates = computed(() => {
return members.value
@@ -308,12 +353,108 @@ function handleMemberClick(member: GroupMember) {
}
function showMemberContextMenu(event: MouseEvent, member: GroupMember) {
const items = [
{ label: '查看资料', icon: 'fas fa-user', action: () => handleMemberClick(member) }
]
const items: MenuItem[] = []
// 1. 查看资料 - 始终显示
items.push({
label: '查看资料',
icon: 'fas fa-user',
action: () => handleMemberClick(member)
})
// 不是自己时才显示以下选项
if (member.user_id !== authStore.user?.id) {
// 2. 添加好友 - 非好友时显示
if (!isFriendCheck(member.user_id)) {
items.push({
label: '添加好友',
icon: 'fas fa-user-plus',
action: () => handleAddFriendFromMenu(member)
})
}
// 3. 设置/取消管理员 - 仅群主可操作
if (isOwner.value) {
if (member.role === 0) {
// 普通成员 -> 设置为管理员
items.push({
label: '设置为管理员',
icon: 'fas fa-user-shield',
action: () => handleSetAdmin(member)
})
} else if (member.role === 1) {
// 管理员 -> 取消管理员
items.push({
label: '取消管理员',
icon: 'fas fa-user-minus',
action: () => handleRemoveAdmin(member)
})
}
}
// 4. 移除成员 - 群主可移除任何人,管理员只能移除普通成员
if (canRemoveMember(member)) {
items.push({
label: '移除成员',
icon: 'fas fa-user-times',
danger: true,
action: () => { removingMember.value = member }
})
}
}
showContextMenu(event, items)
}
// --- 成员管理逻辑 ---
async function handleAddFriendFromMenu(member: GroupMember) {
try {
await contactApi.addFriend({ to_user_id: member.user_id })
toastStore.success('好友申请已发送')
} catch (e: any) {
toastStore.error(e?.response?.data?.message || e?.message || '添加好友失败')
}
}
async function handleSetAdmin(member: GroupMember) {
try {
await groupApi.updateMemberRole(props.roomId, member.user_id, { role: 1 })
toastStore.success('已设置为管理员')
loadData()
emit('group-updated')
} catch (e: any) {
toastStore.error(e?.response?.data?.message || e?.message || '设置失败')
}
}
async function handleRemoveAdmin(member: GroupMember) {
try {
await groupApi.updateMemberRole(props.roomId, member.user_id, { role: 0 })
toastStore.success('已取消管理员')
loadData()
emit('group-updated')
} catch (e: any) {
toastStore.error(e?.response?.data?.message || e?.message || '取消失败')
}
}
async function handleRemoveMember() {
if (!removingMember.value) return
removing.value = true
try {
await groupApi.removeGroupMember(props.roomId, removingMember.value.user_id)
toastStore.success('已移除该成员')
removingMember.value = null
loadData()
emit('member-removed')
emit('group-updated')
} catch (e: any) {
toastStore.error(e?.response?.data?.message || e?.message || '移除失败')
} finally {
removing.value = false
}
}
// --- 通话逻辑 ---
function handleStartCallClick() {
if (callCandidates.value.length === 0) {

View File

@@ -1490,47 +1490,97 @@ async function startCall(type: 'audio' | 'video') {
// 会话右键菜单
function showConversationMenu(event: MouseEvent, conv: any) {
showContextMenu(event, [
const isGroup = conv.is_group || conv.room_type === 'group'
const contact = chatStore.contacts.find(c => c.user_id === conv.target_id || c.id === conv.target_id)
const menuItems: Array<{ label: string; icon: string; action: () => void; danger?: boolean }> = [
// 1. 置顶会话
{
label: conv.is_top ? '取消置顶' : '置顶会话',
icon: 'fas fa-thumbtack',
action: async () => {
// 查找对应联系人以更新状态
const contact = chatStore.contacts.find(c => c.user_id === conv.target_id || c.id === conv.target_id)
if (contact) {
try {
await contactApi.updateContact(contact.id, { is_top: !contact.is_top })
// 手动更新store中的状态触发 filteredConversations 重新计算
contact.is_top = !contact.is_top
// 强制刷新会话列表 (触发 computed 更新)
await conversationStore.loadConversations()
toastStore.success(contact.is_top ? '已置顶' : '已取消置顶')
} catch(e) {
toastStore.error('操作失败')
}
} else {
toastStore.warning('无法找到对应联系人')
// 群聊可能没有 contact使用 conversationApi
try {
await conversationApi.updateConversation({ target_id: conv.target_id, is_top: !conv.is_top })
await conversationStore.loadConversations()
toastStore.success(!conv.is_top ? '已置顶' : '已取消置顶')
} catch(e) {
toastStore.error('操作失败')
}
}
}
},
// 2. 免打扰
{
label: '标记已读',
icon: 'fas fa-check-double',
label: conv.is_muted ? '开启提醒' : '消息免打扰',
icon: conv.is_muted ? 'fas fa-bell' : 'fas fa-bell-slash',
action: async () => {
await conversationStore.clearUnread(conv.target_id)
toastStore.success('已标记已读')
}
},
{
label: '删除会话',
icon: 'fas fa-trash-alt',
danger: true,
action: () => {
pendingDeleteConversation.value = conv
showDeleteConversationConfirm.value = true
try {
if (contact) {
await contactApi.updateContact(contact.id, { is_muted: !conv.is_muted })
contact.is_muted = !conv.is_muted
} else {
await conversationApi.updateConversation({ target_id: conv.target_id, is_muted: !conv.is_muted })
}
await conversationStore.loadConversations()
toastStore.success(!conv.is_muted ? '已开启免打扰' : '已开启提醒')
} catch(e) {
toastStore.error('操作失败')
}
}
}
])
]
// 3. 特别关心 - 仅私聊显示
if (!isGroup && contact) {
menuItems.push({
label: conv.is_special_care ? '取消特别关心' : '特别关心',
icon: conv.is_special_care ? 'fas fa-heart' : 'far fa-heart',
action: async () => {
try {
await contactApi.updateContact(contact.id, { is_special_care: !conv.is_special_care })
contact.is_special_care = !conv.is_special_care
await conversationStore.loadConversations()
toastStore.success(!conv.is_special_care ? '已设为特别关心' : '已取消特别关心')
} catch(e) {
toastStore.error('操作失败')
}
}
})
}
// 4. 标记已读
menuItems.push({
label: '标记已读',
icon: 'fas fa-check-double',
action: async () => {
await conversationStore.clearUnread(conv.target_id)
toastStore.success('已标记已读')
}
})
// 5. 删除会话
menuItems.push({
label: '删除会话',
icon: 'fas fa-trash-alt',
danger: true,
action: () => {
pendingDeleteConversation.value = conv
showDeleteConversationConfirm.value = true
}
})
showContextMenu(event, menuItems)
}
async function handleMarkRead(conv: Conversation) {