群聊
This commit is contained in:
@@ -58,8 +58,10 @@ export function getGroup(roomId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取群成员列表
|
// 获取群成员列表
|
||||||
export function getGroupMembers(roomId: string) {
|
export function getGroupMembers(roomId: string, keyword?: string) {
|
||||||
return request.get<GroupMember[]>(`/groups/${roomId}/members`)
|
return request.get<GroupMember[]>(`/groups/${roomId}/members`, {
|
||||||
|
params: keyword ? { keyword } : undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 邀请成员入群
|
// 邀请成员入群
|
||||||
|
|||||||
@@ -1,5 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-80 bg-[#1e293b] border-l border-gray-800 flex flex-col h-full">
|
<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="border-b border-gray-800 p-4">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
@@ -99,13 +146,34 @@
|
|||||||
<h3 class="text-sm font-semibold text-white">群成员 ({{ members.length }})</h3>
|
<h3 class="text-sm font-semibold text-white">群成员 ({{ members.length }})</h3>
|
||||||
</div>
|
</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">
|
<div v-if="loadingMembers" class="text-center py-8 text-gray-500 text-xs">
|
||||||
<i class="fas fa-spinner fa-spin mr-2"></i>
|
<i class="fas fa-spinner fa-spin mr-2"></i>
|
||||||
加载中...
|
加载中...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="members.length === 0" class="text-center py-8 text-gray-500 text-xs">
|
<div v-else-if="members.length === 0" class="text-center py-8 text-gray-500 text-xs">
|
||||||
暂无成员
|
{{ memberSearchQuery ? '未找到匹配的成员' : '暂无成员' }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="space-y-2">
|
||||||
@@ -114,6 +182,7 @@
|
|||||||
:key="member.user_id"
|
:key="member.user_id"
|
||||||
class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition cursor-pointer group"
|
class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition cursor-pointer group"
|
||||||
@click="handleMemberClick(member)"
|
@click="handleMemberClick(member)"
|
||||||
|
@contextmenu.stop="showMemberContextMenu($event, member)"
|
||||||
>
|
>
|
||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -159,16 +228,123 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import Avatar from '@/components/common/Avatar.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 groupApi from '@/api/modules/room'
|
||||||
|
import * as contactApi from '@/api/modules/contact'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useChatStore } from '@/stores/chat'
|
||||||
import { useToastStore } from '@/stores/toast'
|
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 {
|
interface Props {
|
||||||
roomId: string
|
roomId: string
|
||||||
@@ -180,21 +356,45 @@ const emit = defineEmits<{
|
|||||||
'member-removed': []
|
'member-removed': []
|
||||||
'member-clicked': [member: GroupMember]
|
'member-clicked': [member: GroupMember]
|
||||||
'group-updated': []
|
'group-updated': []
|
||||||
|
'invite-members': []
|
||||||
|
'dissolve': []
|
||||||
|
'transfer-owner': [newOwnerId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const chatStore = useChatStore()
|
||||||
const toastStore = useToastStore()
|
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 loadingMembers = ref(false)
|
||||||
const members = ref<GroupMember[]>([])
|
const members = ref<GroupMember[]>([])
|
||||||
|
const memberSearchQuery = ref('')
|
||||||
const announcement = ref('')
|
const announcement = ref('')
|
||||||
const isEditingAnnouncement = ref(false)
|
const isEditingAnnouncement = ref(false)
|
||||||
const editAnnouncementText = ref('')
|
const editAnnouncementText = ref('')
|
||||||
const loadingAnnouncement = ref(false)
|
const loadingAnnouncement = ref(false)
|
||||||
const groupName = ref('')
|
const groupName = ref('')
|
||||||
|
const groupAvatar = ref('')
|
||||||
const isEditingGroupName = ref(false)
|
const isEditingGroupName = ref(false)
|
||||||
const editGroupNameText = ref('')
|
const editGroupNameText = ref('')
|
||||||
const loadingGroupName = ref(false)
|
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(() => {
|
const currentUserRole = computed(() => {
|
||||||
@@ -215,6 +415,18 @@ const canEditAnnouncement = computed(() => isOwner.value || isAdmin.value)
|
|||||||
// 是否可以编辑群名
|
// 是否可以编辑群名
|
||||||
const canEditGroupName = 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 {
|
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
|
if (loadingMembers.value) return
|
||||||
|
|
||||||
loadingMembers.value = true
|
loadingMembers.value = true
|
||||||
try {
|
try {
|
||||||
const memberList = await groupApi.getGroupMembers(props.roomId)
|
const memberList = await groupApi.getGroupMembers(props.roomId, keyword)
|
||||||
members.value = memberList as unknown as GroupMember[]
|
members.value = memberList as unknown as GroupMember[]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load group members:', 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() {
|
async function loadGroupInfo() {
|
||||||
if (loadingGroupName.value) return
|
if (loadingGroupName.value) return
|
||||||
@@ -256,6 +484,7 @@ async function loadGroupInfo() {
|
|||||||
groupApi.getGroupAnnouncement(props.roomId).catch(() => ({ announcement: '' }))
|
groupApi.getGroupAnnouncement(props.roomId).catch(() => ({ announcement: '' }))
|
||||||
])
|
])
|
||||||
groupName.value = groupInfo.name || groupInfo.room_name || ''
|
groupName.value = groupInfo.name || groupInfo.room_name || ''
|
||||||
|
groupAvatar.value = groupInfo.avatar || groupInfo.room_avatar || ''
|
||||||
announcement.value = annRes.announcement || ''
|
announcement.value = annRes.announcement || ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load group info:', error)
|
console.error('Failed to load group info:', error)
|
||||||
@@ -346,23 +575,157 @@ function handleMemberClick(member: GroupMember) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 移除成员
|
// 移除成员
|
||||||
async function handleRemoveMember(member: GroupMember) {
|
function showRemoveMemberConfirmDialog(member: GroupMember) {
|
||||||
if (!confirm(`确定要移除成员 "${member.user?.name || member.nickname || '未知'}" 吗?`)) {
|
pendingMember.value = member
|
||||||
return
|
showRemoveMemberConfirm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRemoveMember() {
|
||||||
|
if (!pendingMember.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await groupApi.removeGroupMember(props.roomId, member.user_id)
|
await groupApi.removeGroupMember(props.roomId, pendingMember.value.user_id)
|
||||||
toastStore.success('成员已移除')
|
toastStore.success('成员已移除')
|
||||||
// 重新加载成员列表
|
// 重新加载成员列表
|
||||||
await loadMembers()
|
await loadMembers()
|
||||||
emit('member-removed')
|
emit('member-removed')
|
||||||
|
showRemoveMemberConfirm.value = false
|
||||||
|
pendingMember.value = null
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to remove member:', error)
|
console.error('Failed to remove member:', error)
|
||||||
toastStore.error(error.message || '移除成员失败')
|
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 变化
|
// 监听 roomId 变化
|
||||||
watch(() => props.roomId, () => {
|
watch(() => props.roomId, () => {
|
||||||
if (props.roomId) {
|
if (props.roomId) {
|
||||||
@@ -371,11 +734,18 @@ watch(() => props.roomId, () => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
if (props.roomId) {
|
if (props.roomId) {
|
||||||
loadMembers()
|
loadMembers()
|
||||||
loadGroupInfo()
|
loadGroupInfo()
|
||||||
}
|
}
|
||||||
|
// 加载联系人列表(用于邀请)
|
||||||
|
try {
|
||||||
|
const contacts = await contactApi.getContacts()
|
||||||
|
chatStore.setContacts(contacts)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load contacts:', error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="isOwner"
|
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"
|
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>
|
<i class="fas fa-trash"></i>
|
||||||
<span>解散群聊</span>
|
<span>解散群聊</span>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="px-4 py-2 rounded-xl bg-red-600 hover:bg-red-700 text-white transition flex items-center gap-2"
|
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>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
<span>退出群聊</span>
|
<span>退出群聊</span>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="member.role !== 2"
|
v-if="member.role !== 2"
|
||||||
class="text-gray-400 hover:text-red-400 transition text-sm"
|
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>
|
<i class="fas fa-user-minus"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -190,6 +190,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -202,6 +236,7 @@ import { useToastStore } from '@/stores/toast'
|
|||||||
import * as groupApi from '@/api/modules/room'
|
import * as groupApi from '@/api/modules/room'
|
||||||
import * as contactApi from '@/api/modules/contact'
|
import * as contactApi from '@/api/modules/contact'
|
||||||
import Avatar from '@/components/common/Avatar.vue'
|
import Avatar from '@/components/common/Avatar.vue'
|
||||||
|
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -228,6 +263,10 @@ const availableContacts = ref<Contact[]>([])
|
|||||||
const showInviteModal = ref(false)
|
const showInviteModal = ref(false)
|
||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
const editForm = ref({ name: '', avatar: '' })
|
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(() => {
|
const currentUserRole = computed(() => {
|
||||||
@@ -248,6 +287,12 @@ const canInvite = computed(() => isOwner.value || isAdmin.value)
|
|||||||
// 是否可以编辑群信息
|
// 是否可以编辑群信息
|
||||||
const canEdit = computed(() => isOwner.value)
|
const canEdit = computed(() => isOwner.value)
|
||||||
|
|
||||||
|
// 移除成员确认消息
|
||||||
|
const removeMemberMessage = computed(() => {
|
||||||
|
const name = pendingMember.value?.user?.name || pendingMember.value?.nickname || '未知'
|
||||||
|
return `确定要将 ${name} 移出群聊吗?`
|
||||||
|
})
|
||||||
|
|
||||||
// 加载群信息
|
// 加载群信息
|
||||||
async function loadGroupInfo() {
|
async function loadGroupInfo() {
|
||||||
if (!props.roomId) return
|
if (!props.roomId) return
|
||||||
@@ -295,14 +340,19 @@ function handleMemberClick(member: GroupMember) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 移除成员
|
// 移除成员
|
||||||
async function handleRemoveMember(member: GroupMember) {
|
function showRemoveMemberConfirmDialog(member: GroupMember) {
|
||||||
if (!confirm(`确定要将 ${member.user?.name || member.nickname} 移出群聊吗?`)) {
|
pendingMember.value = member
|
||||||
return
|
showRemoveMemberConfirm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRemoveMember() {
|
||||||
|
if (!pendingMember.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await groupApi.removeGroupMember(props.roomId, member.user_id)
|
await groupApi.removeGroupMember(props.roomId, pendingMember.value.user_id)
|
||||||
toastStore.success('已移除成员')
|
toastStore.success('已移除成员')
|
||||||
|
showRemoveMemberConfirm.value = false
|
||||||
|
pendingMember.value = null
|
||||||
await loadGroupInfo()
|
await loadGroupInfo()
|
||||||
emit('updated')
|
emit('updated')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -345,14 +395,15 @@ async function handleUpdateGroup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 退出群聊
|
// 退出群聊
|
||||||
|
function showQuitConfirmDialog() {
|
||||||
|
showQuitConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function handleQuit() {
|
async function handleQuit() {
|
||||||
if (!confirm('确定要退出该群聊吗?')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await groupApi.quitGroup(props.roomId)
|
await groupApi.quitGroup(props.roomId)
|
||||||
toastStore.success('已退出群聊')
|
toastStore.success('已退出群聊')
|
||||||
|
showQuitConfirm.value = false
|
||||||
emit('quit')
|
emit('quit')
|
||||||
emit('close')
|
emit('close')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -361,11 +412,11 @@ async function handleQuit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解散群聊
|
// 解散群聊
|
||||||
|
function showDissolveConfirmDialog() {
|
||||||
|
showDissolveConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDissolve() {
|
async function handleDissolve() {
|
||||||
if (!confirm('确定要解散该群聊吗?此操作不可恢复!')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await groupApi.dissolveGroup(props.roomId)
|
await groupApi.dissolveGroup(props.roomId)
|
||||||
toastStore.success('群聊已解散')
|
toastStore.success('群聊已解散')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 系统/通知消息:居中灰色提示条样式 -->
|
<!-- 系统/通知消息:居中灰色提示条样式 -->
|
||||||
<div v-if="isSystemOrNotify" class="flex justify-center my-2">
|
<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 max-w-[80%] text-center">
|
<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>
|
<i :class="systemIcon" class="mr-1.5"></i>
|
||||||
<span>{{ systemContent }}</span>
|
<span>{{ systemContent }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,38 +1,58 @@
|
|||||||
<template>
|
<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">
|
<div class="flex gap-6 text-gray-400 text-lg mb-3 px-2 items-center">
|
||||||
<i
|
<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="表情"
|
title="表情"
|
||||||
@click="insertEmoji('😊')"
|
@click="!isBlocked && insertEmoji('😊')"
|
||||||
></i>
|
></i>
|
||||||
<i
|
<i
|
||||||
class="far fa-image hover:text-green-400 cursor-pointer transition-all transform hover:scale-125"
|
class="far fa-image hover:text-green-400 transition-all transform hover:scale-125"
|
||||||
@click="triggerFile(1)"
|
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
|
||||||
|
@click="!isBlocked && triggerFile(1)"
|
||||||
title="发送图片"
|
title="发送图片"
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
<!-- 新增:视频上传按钮 -->
|
<!-- 新增:视频上传按钮 -->
|
||||||
<i
|
<i
|
||||||
class="fas fa-video hover:text-purple-400 cursor-pointer transition-all transform hover:scale-125"
|
class="fas fa-video hover:text-purple-400 transition-all transform hover:scale-125"
|
||||||
@click="triggerFile(3)"
|
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
|
||||||
|
@click="!isBlocked && triggerFile(3)"
|
||||||
title="发送视频"
|
title="发送视频"
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
<i
|
<i
|
||||||
class="fas fa-folder-plus hover:text-yellow-400 cursor-pointer transition-all transform hover:scale-125"
|
class="fas fa-folder-plus hover:text-yellow-400 transition-all transform hover:scale-125"
|
||||||
@click="triggerFile(8)"
|
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
|
||||||
|
@click="!isBlocked && triggerFile(8)"
|
||||||
title="发送文件"
|
title="发送文件"
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
<!-- 长按录音 -->
|
<!-- 长按录音 -->
|
||||||
<div
|
<div
|
||||||
class="relative flex items-center group cursor-pointer select-none ml-auto mr-2"
|
class="relative flex items-center group select-none ml-auto mr-2"
|
||||||
@mousedown="handleRecordStart"
|
:class="isBlocked ? 'cursor-not-allowed' : 'cursor-pointer'"
|
||||||
@touchstart.passive="handleRecordStart"
|
@mousedown="!isBlocked && handleRecordStart()"
|
||||||
@mouseup="handleRecordStop"
|
@touchstart.passive="!isBlocked && handleRecordStart()"
|
||||||
@touchend.prevent="handleRecordStop"
|
@mouseup="!isBlocked && handleRecordStop()"
|
||||||
@mouseleave="handleRecordCancel"
|
@touchend.prevent="!isBlocked && handleRecordStop()"
|
||||||
|
@mouseleave="!isBlocked && handleRecordCancel()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-8 h-8 rounded-full flex items-center justify-center transition-all shadow-md"
|
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">
|
<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
|
<textarea
|
||||||
:value="modelValue"
|
: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 发送)..."
|
placeholder="输入消息 (Enter 发送)..."
|
||||||
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
||||||
@keydown.enter.exact.prevent="$emit('send')"
|
@keydown.enter.exact.prevent="!isBlocked && $emit('send')"
|
||||||
@paste="handlePaste"
|
@paste="handlePaste"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 rounded-2xl flex items-center justify-center ml-2 transition-all duration-300 cursor-pointer shadow-lg"
|
class="w-10 h-10 rounded-2xl flex items-center justify-center ml-2 transition-all duration-300 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'"
|
:class="isBlocked
|
||||||
@click="$emit('send')"
|
? '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>
|
<i class="fas fa-paper-plane text-sm transform -ml-0.5 mt-0.5"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +101,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
@@ -86,6 +109,7 @@ const toastStore = useToastStore()
|
|||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string
|
modelValue: string
|
||||||
isRecording: boolean
|
isRecording: boolean
|
||||||
|
isBlocked?: boolean // 是否被禁言(被移除或退出群聊)
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -119,6 +143,14 @@ function triggerFile(type: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(e: Event) {
|
function handleFileChange(e: Event) {
|
||||||
|
if (props.isBlocked) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (fileInput.value) {
|
||||||
|
fileInput.value.value = ''
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const target = e.target as HTMLInputElement
|
const target = e.target as HTMLInputElement
|
||||||
const file = target.files?.[0]
|
const file = target.files?.[0]
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -131,6 +163,11 @@ function handleFileChange(e: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePaste(e: ClipboardEvent) {
|
function handlePaste(e: ClipboardEvent) {
|
||||||
|
if (props.isBlocked) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const items = e.clipboardData?.items
|
const items = e.clipboardData?.items
|
||||||
if (!items) return
|
if (!items) return
|
||||||
|
|
||||||
@@ -198,4 +235,80 @@ function handleRecordCancel() {
|
|||||||
emit('record-cancel')
|
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>
|
</script>
|
||||||
|
|||||||
@@ -38,11 +38,15 @@
|
|||||||
v-for="(msg, index) in messages"
|
v-for="(msg, index) in messages"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex gap-3 msg-row group"
|
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)"
|
@contextmenu.stop="$emit('contextmenu', $event, msg)"
|
||||||
>
|
>
|
||||||
<!-- 头像 -->
|
<!-- 系统消息不显示头像 -->
|
||||||
<Avatar
|
<Avatar
|
||||||
|
v-if="!isSystemMessage(msg)"
|
||||||
:name="getSenderName(msg)"
|
:name="getSenderName(msg)"
|
||||||
:avatar="getSenderAvatar(msg)"
|
:avatar="getSenderAvatar(msg)"
|
||||||
:color="msg.isSelf ? generateColor(currentUser.id) : (getMemberColor(msg.sender_user_id) || target.color)"
|
: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
|
<div
|
||||||
v-if="isGroupChat && !msg.isSelf"
|
v-if="!isSystemMessage(msg)"
|
||||||
class="text-[10px] text-gray-500 mb-1 px-1"
|
|
||||||
>
|
>
|
||||||
{{ getSenderDisplayName(msg) }}
|
<!-- 群聊中显示发送者昵称(非自己发送的消息) -->
|
||||||
</div>
|
<div
|
||||||
<!-- 单聊或自己发送的消息 -->
|
v-if="isGroupChat && !msg.isSelf"
|
||||||
<div
|
class="text-[10px] text-gray-500 mb-1 px-1"
|
||||||
v-else-if="!isGroupChat || msg.isSelf"
|
>
|
||||||
class="text-[10px] text-gray-500 mb-1 px-1"
|
{{ getSenderDisplayName(msg) }}
|
||||||
:class="{ 'text-right': msg.isSelf }"
|
</div>
|
||||||
>
|
<!-- 单聊或自己发送的消息 -->
|
||||||
{{ msg.isSelf ? 'Me' : (target.remark_name || target.user?.name) }}
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 消息气泡 -->
|
<!-- 消息气泡 -->
|
||||||
@@ -82,6 +94,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { generateColor } from '@/utils/format'
|
import { generateColor } from '@/utils/format'
|
||||||
import Avatar from '@/components/common/Avatar.vue'
|
import Avatar from '@/components/common/Avatar.vue'
|
||||||
import MessageBubble from '@/components/chat/MessageBubble.vue'
|
import MessageBubble from '@/components/chat/MessageBubble.vue'
|
||||||
|
import { isSystemOrNotifyMessage } from '@/utils/messageTypes'
|
||||||
import type { ChatMessage, User, Contact } from '@/types/api'
|
import type { ChatMessage, User, Contact } from '@/types/api'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -107,6 +120,11 @@ const containerRef = ref<HTMLElement | null>(null)
|
|||||||
// 判断是否为群聊
|
// 判断是否为群聊
|
||||||
const isGroupChat = computed(() => props.target.is_group || props.target.room_type === 'group')
|
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 {
|
function getSenderName(msg: ChatMessage): string {
|
||||||
if (msg.isSelf) {
|
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="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">
|
<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
|
<button
|
||||||
class="text-gray-400 hover:text-white transition text-xl"
|
class="text-gray-400 hover:text-white transition text-xl"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
@@ -85,6 +85,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="px-6 py-4 border-t border-gray-800 flex justify-between items-center">
|
||||||
<div class="text-sm text-gray-400">
|
<div class="text-sm text-gray-400">
|
||||||
@@ -99,24 +110,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
@click="handleCreate"
|
||||||
>
|
>
|
||||||
创建群聊
|
{{ mode === 'invite' ? '邀请' : '创建群聊' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -129,13 +129,17 @@ import Avatar from '@/components/common/Avatar.vue'
|
|||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean
|
||||||
contacts: Contact[]
|
contacts: Contact[]
|
||||||
|
mode?: 'create' | 'invite' // 创建群聊或邀请好友
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
mode: 'create'
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'close': []
|
'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('')
|
const searchQuery = ref('')
|
||||||
@@ -174,20 +178,29 @@ function removeContact(contactId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建群聊
|
// 创建群聊或邀请好友
|
||||||
function handleCreate() {
|
function handleCreate() {
|
||||||
if (selectedContacts.value.length < 1 || !groupName.value.trim()) {
|
if (selectedContacts.value.length < 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberIds = selectedContacts.value.map(c => c.user_id || c.id)
|
const memberIds = selectedContacts.value.map(c => c.user_id || c.id)
|
||||||
const avatar = groupName.value.charAt(0).toUpperCase() // 使用群名称首字母作为头像
|
|
||||||
|
|
||||||
emit('create', {
|
if (props.mode === 'invite') {
|
||||||
member_ids: memberIds,
|
emit('invite', {
|
||||||
name: groupName.value.trim(),
|
member_ids: memberIds
|
||||||
avatar
|
})
|
||||||
})
|
} 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 = []
|
selectedContacts.value = []
|
||||||
|
|||||||
@@ -41,6 +41,13 @@
|
|||||||
<div v-if="toast.description" class="text-xs text-gray-400 mt-1">
|
<div v-if="toast.description" class="text-xs text-gray-400 mt-1">
|
||||||
{{ toast.description }}
|
{{ toast.description }}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 关闭按钮 -->
|
<!-- 关闭按钮 -->
|
||||||
@@ -65,6 +72,13 @@ const toasts = computed(() => toastStore.toasts)
|
|||||||
function removeToast(id: string) {
|
function removeToast(id: string) {
|
||||||
toastStore.removeToast(id)
|
toastStore.removeToast(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAction(toast: Toast) {
|
||||||
|
if (toast.action) {
|
||||||
|
toast.action.handler()
|
||||||
|
removeToast(toast.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export function useWebRTC(
|
|||||||
sender_client_id: clientId || '',
|
sender_client_id: clientId || '',
|
||||||
receiver_user_id: currentReceiverUserId,
|
receiver_user_id: currentReceiverUserId,
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
message_type: 0,
|
message_type: 4, // 系统消息类型
|
||||||
content: content,
|
content: content,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
extra: JSON.stringify({ isSystem: true }),
|
extra: JSON.stringify({ isSystem: true }),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Conversation } from '@/types/conversation'
|
|||||||
import type { ChatMessage } from '@/types/api'
|
import type { ChatMessage } from '@/types/api'
|
||||||
import * as conversationApi from '@/api/modules/conversation'
|
import * as conversationApi from '@/api/modules/conversation'
|
||||||
import { getMessageSummary } from '@/utils/messageTypes'
|
import { getMessageSummary } from '@/utils/messageTypes'
|
||||||
|
import { useChatStore } from './chat'
|
||||||
|
|
||||||
export const useConversationStore = defineStore('conversation', () => {
|
export const useConversationStore = defineStore('conversation', () => {
|
||||||
const conversations = ref<Conversation[]>([])
|
const conversations = ref<Conversation[]>([])
|
||||||
@@ -84,7 +85,23 @@ export const useConversationStore = defineStore('conversation', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = getMessageSummary(message)
|
// 判断是否为群聊
|
||||||
|
const isGroupChat = conv?.type === 2 || conv?.is_group
|
||||||
|
|
||||||
|
// 生成摘要:群聊时需要包含发送者信息
|
||||||
|
let summary = getMessageSummary(message)
|
||||||
|
if (isGroupChat && !isSelf && message.sender_user_id) {
|
||||||
|
// 从 chatStore 中查找发送者信息
|
||||||
|
const chatStore = useChatStore()
|
||||||
|
const senderContact = chatStore.contacts.find(
|
||||||
|
c => c.user_id === message.sender_user_id || c.id === message.sender_user_id
|
||||||
|
)
|
||||||
|
if (senderContact) {
|
||||||
|
const senderName = senderContact.remark_name || senderContact.user?.name || '未知用户'
|
||||||
|
summary = `${senderName}:${summary}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date(message.created_at || Date.now()).getTime()
|
const now = new Date(message.created_at || Date.now()).getTime()
|
||||||
|
|
||||||
if (conv) {
|
if (conv) {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export interface Toast {
|
|||||||
description?: string
|
description?: string
|
||||||
type: ToastType
|
type: ToastType
|
||||||
duration?: number
|
duration?: number
|
||||||
|
action?: {
|
||||||
|
label: string
|
||||||
|
handler: () => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useToastStore = defineStore('toast', () => {
|
export const useToastStore = defineStore('toast', () => {
|
||||||
@@ -21,7 +25,8 @@ export const useToastStore = defineStore('toast', () => {
|
|||||||
message: string,
|
message: string,
|
||||||
type: ToastType = 'info',
|
type: ToastType = 'info',
|
||||||
duration: number = 3000,
|
duration: number = 3000,
|
||||||
description?: string
|
description?: string,
|
||||||
|
action?: { label: string; handler: () => void }
|
||||||
) {
|
) {
|
||||||
const id = `toast-${Date.now()}-${Math.random()}`
|
const id = `toast-${Date.now()}-${Math.random()}`
|
||||||
const toast: Toast = {
|
const toast: Toast = {
|
||||||
@@ -30,6 +35,7 @@ export const useToastStore = defineStore('toast', () => {
|
|||||||
description,
|
description,
|
||||||
type,
|
type,
|
||||||
duration,
|
duration,
|
||||||
|
action,
|
||||||
}
|
}
|
||||||
|
|
||||||
toasts.value.push(toast)
|
toasts.value.push(toast)
|
||||||
|
|||||||
@@ -49,6 +49,13 @@
|
|||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
<span>发起群聊</span>
|
<span>发起群聊</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 rounded-xl bg-gray-700 hover:bg-gray-600 text-white transition flex items-center justify-center gap-2 text-sm"
|
||||||
|
@click="refreshConversations"
|
||||||
|
title="刷新会话列表"
|
||||||
|
>
|
||||||
|
<i class="fas fa-sync-alt" :class="{ 'fa-spin': refreshingConversations }"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<i class="fas fa-search absolute left-4 top-3 text-gray-500 group-focus-within:text-primary transition"></i>
|
<i class="fas fa-search absolute left-4 top-3 text-gray-500 group-focus-within:text-primary transition"></i>
|
||||||
@@ -198,9 +205,10 @@
|
|||||||
:target="chatStore.currentTarget"
|
:target="chatStore.currentTarget"
|
||||||
:loading-history="loadingHistory"
|
:loading-history="loadingHistory"
|
||||||
:has-more-history="chatStore.currentTarget ? (hasMoreHistory[getRoomId(chatStore.currentTarget)] !== false) : true"
|
:has-more-history="chatStore.currentTarget ? (hasMoreHistory[getRoomId(chatStore.currentTarget)] !== false) : true"
|
||||||
:room-members="roomMembers"
|
:room-members="currentRoomMembers"
|
||||||
@scroll="handleScroll"
|
@scroll="handleScroll"
|
||||||
@load-more="loadMoreMessages"
|
@load-more="loadMoreMessages"
|
||||||
|
@avatar-click="handleAvatarClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 新消息提示/回到底部按钮 -->
|
<!-- 新消息提示/回到底部按钮 -->
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
<MessageInput
|
<MessageInput
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
:is-recording="isRecording"
|
:is-recording="isRecording"
|
||||||
|
:is-blocked="isBlockedFromGroup"
|
||||||
@send="handleSendText"
|
@send="handleSendText"
|
||||||
@file="handleFileSelect"
|
@file="handleFileSelect"
|
||||||
@record-start="handleRecordStart"
|
@record-start="handleRecordStart"
|
||||||
@@ -306,8 +315,8 @@
|
|||||||
@dissolve="handleGroupDissolve"
|
@dissolve="handleGroupDissolve"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Profile Modal -->
|
<!-- Profile Modal (自己的) -->
|
||||||
<div v-if="showProfileModal && authStore.user" class="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm" @click.self="showProfileModal = false">
|
<div v-if="showProfileModal && authStore.user && !selectedUser" class="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm" @click.self="showProfileModal = false">
|
||||||
<div class="bg-panel rounded-2xl w-80 shadow-2xl border border-gray-700 overflow-hidden animate-fade-in">
|
<div class="bg-panel rounded-2xl w-80 shadow-2xl border border-gray-700 overflow-hidden animate-fade-in">
|
||||||
<div class="h-24 bg-gradient-to-r from-indigo-600 to-purple-600"></div>
|
<div class="h-24 bg-gradient-to-r from-indigo-600 to-purple-600"></div>
|
||||||
<div class="px-6 pb-6 text-center -mt-12">
|
<div class="px-6 pb-6 text-center -mt-12">
|
||||||
@@ -323,6 +332,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- User Info Modal (点击对方头像) -->
|
||||||
|
<div v-if="selectedUser" class="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm" @click.self="selectedUser = null">
|
||||||
|
<div class="bg-panel rounded-2xl w-96 shadow-2xl border border-gray-700 overflow-hidden animate-fade-in">
|
||||||
|
<div class="h-32 bg-gradient-to-r from-indigo-600 to-purple-600"></div>
|
||||||
|
<div class="px-6 pb-6 text-center -mt-16">
|
||||||
|
<Avatar
|
||||||
|
:name="getUserDisplayName(selectedUser)"
|
||||||
|
:avatar="getUserAvatar(selectedUser)"
|
||||||
|
size="xl"
|
||||||
|
rounded="full"
|
||||||
|
class="mx-auto border-4 border-panel shadow-xl"
|
||||||
|
/>
|
||||||
|
<h2 class="text-xl font-bold mt-3 text-white">{{ getUserDisplayName(selectedUser) }}</h2>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">{{ getUserDesc(selectedUser) || '暂无签名' }}</p>
|
||||||
|
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<div class="mt-4 space-y-2 text-left">
|
||||||
|
<div v-if="getUserId(selectedUser)" class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-400">账号ID:</span>
|
||||||
|
<span class="text-white">{{ getUserId(selectedUser) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="getUserPhone(selectedUser)" class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-400">手机号:</span>
|
||||||
|
<span class="text-white">{{ getUserPhone(selectedUser) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="getUserEmail(selectedUser)" class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-400">邮箱:</span>
|
||||||
|
<span class="text-white">{{ getUserEmail(selectedUser) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="mt-6 flex gap-3">
|
||||||
|
<button
|
||||||
|
v-if="!isFriend(selectedUser as Contact)"
|
||||||
|
class="flex-1 py-2 bg-primary hover:bg-indigo-500 text-white rounded-lg transition flex items-center justify-center gap-2"
|
||||||
|
@click="handleAddFriend"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-plus"></i>
|
||||||
|
<span>添加好友</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="flex-1 py-2 bg-primary hover:bg-indigo-500 text-white rounded-lg transition flex items-center justify-center gap-2"
|
||||||
|
@click="handleSendMessageToUser"
|
||||||
|
>
|
||||||
|
<i class="fas fa-comment"></i>
|
||||||
|
<span>发消息</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-lg transition"
|
||||||
|
@click="selectedUser = null"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -342,7 +411,7 @@ import * as attachmentApi from '@/api/modules/attachment'
|
|||||||
import { formatTime, generateColor } from '@/utils/format'
|
import { formatTime, generateColor } from '@/utils/format'
|
||||||
import { getMessageSummary } from '@/utils/messageTypes'
|
import { getMessageSummary } from '@/utils/messageTypes'
|
||||||
import { storage } from '@/utils/storage'
|
import { storage } from '@/utils/storage'
|
||||||
import type { Contact, ChatMessage } from '@/types/api'
|
import type { Contact, ChatMessage, User } from '@/types/api'
|
||||||
import type { Conversation } from '@/types/conversation'
|
import type { Conversation } from '@/types/conversation'
|
||||||
import { useWebRTCStore } from '@/stores/webrtc'
|
import { useWebRTCStore } from '@/stores/webrtc'
|
||||||
// Components...
|
// Components...
|
||||||
@@ -378,11 +447,13 @@ const inputText = ref('')
|
|||||||
const chatVisible = ref(false)
|
const chatVisible = ref(false)
|
||||||
const isMobile = ref(window.innerWidth < 768)
|
const isMobile = ref(window.innerWidth < 768)
|
||||||
const showProfileModal = ref(false)
|
const showProfileModal = ref(false)
|
||||||
|
const selectedUser = ref<Contact | User | null>(null)
|
||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
const fileModal = ref({ show: false, type: 0, preview: '', name: '', size: 0, file: null as File | null })
|
const fileModal = ref({ show: false, type: 0, preview: '', name: '', size: 0, file: null as File | null })
|
||||||
const showCreateGroupModal = ref(false)
|
const showCreateGroupModal = ref(false)
|
||||||
const showGroupInfoPanel = ref(false)
|
const showGroupInfoPanel = ref(false)
|
||||||
const roomMembers = ref<Record<string, { name: string; avatar?: string }>>({})
|
const refreshingConversations = ref(false)
|
||||||
|
const roomMembers = ref<Record<string, Record<string, { name: string; avatar?: string }>>>({})
|
||||||
|
|
||||||
// Scroll Logic State
|
// Scroll Logic State
|
||||||
const showScrollBottomBtn = ref(false)
|
const showScrollBottomBtn = ref(false)
|
||||||
@@ -446,6 +517,24 @@ function getRoomId(contact: Contact): string {
|
|||||||
// 判断是否为群聊
|
// 判断是否为群聊
|
||||||
const isGroupChat = computed(() => chatStore.currentTarget?.is_group || chatStore.currentTarget?.room_type === 'group')
|
const isGroupChat = computed(() => chatStore.currentTarget?.is_group || chatStore.currentTarget?.room_type === 'group')
|
||||||
|
|
||||||
|
// 获取当前房间的成员映射(用于 MessageList)
|
||||||
|
const currentRoomMembers = computed(() => {
|
||||||
|
if (!chatStore.currentTarget?.room_id) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return roomMembers.value[chatStore.currentTarget.room_id] || {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断当前用户是否被禁言(被移除或退出群聊)
|
||||||
|
const isBlockedFromGroup = computed(() => {
|
||||||
|
if (!isGroupChat.value || !chatStore.currentTarget?.room_id || !authStore.user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const currentMember = roomMembers.value[chatStore.currentTarget.room_id]?.[authStore.user.id]
|
||||||
|
// 如果当前用户不在群成员列表中,则认为被禁言
|
||||||
|
return !currentMember
|
||||||
|
})
|
||||||
|
|
||||||
// 获取当前目标名称
|
// 获取当前目标名称
|
||||||
function getCurrentTargetName(): string {
|
function getCurrentTargetName(): string {
|
||||||
if (!chatStore.currentTarget) return ''
|
if (!chatStore.currentTarget) return ''
|
||||||
@@ -539,9 +628,10 @@ async function selectChat(contact: Contact) {
|
|||||||
// 加载群成员信息
|
// 加载群成员信息
|
||||||
async function loadGroupMembers(roomId: string) {
|
async function loadGroupMembers(roomId: string) {
|
||||||
try {
|
try {
|
||||||
const members = await groupApi.getGroupMembers(roomId)
|
const response = await groupApi.getGroupMembers(roomId)
|
||||||
|
const members = Array.isArray(response) ? response : (response.data || [])
|
||||||
const membersMap: Record<string, { name: string; avatar?: string }> = {}
|
const membersMap: Record<string, { name: string; avatar?: string }> = {}
|
||||||
members.forEach(m => {
|
members.forEach((m: any) => {
|
||||||
membersMap[m.user_id] = {
|
membersMap[m.user_id] = {
|
||||||
name: m.user?.name || m.nickname || '未知',
|
name: m.user?.name || m.nickname || '未知',
|
||||||
avatar: m.user?.avatar
|
avatar: m.user?.avatar
|
||||||
@@ -550,6 +640,8 @@ async function loadGroupMembers(roomId: string) {
|
|||||||
roomMembers.value[roomId] = membersMap
|
roomMembers.value[roomId] = membersMap
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load group members:', error)
|
console.error('Failed to load group members:', error)
|
||||||
|
// 如果加载失败,清空成员列表(表示用户可能已被移除)
|
||||||
|
roomMembers.value[roomId] = {} as Record<string, { name: string; avatar?: string }>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,6 +739,13 @@ async function selectChatByConversation(conv: Conversation) {
|
|||||||
|
|
||||||
async function sendMessage(type: number, content: string, extra: any = {}, duration = 0) {
|
async function sendMessage(type: number, content: string, extra: any = {}, duration = 0) {
|
||||||
if (!chatStore.currentTarget) return
|
if (!chatStore.currentTarget) return
|
||||||
|
|
||||||
|
// 检查是否被禁言(群聊中被移除或退出)
|
||||||
|
if (isGroupChat.value && isBlockedFromGroup.value) {
|
||||||
|
toastStore.error('您已被移出群聊,无法发送消息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const roomId = getRoomId(chatStore.currentTarget)
|
const roomId = getRoomId(chatStore.currentTarget)
|
||||||
const isGroup = chatStore.currentTarget.is_group || chatStore.currentTarget.room_type === 'group'
|
const isGroup = chatStore.currentTarget.is_group || chatStore.currentTarget.room_type === 'group'
|
||||||
|
|
||||||
@@ -671,7 +770,17 @@ async function sendMessage(type: number, content: string, extra: any = {}, durat
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await messageApi.sendMessage(payload)
|
await messageApi.sendMessage(payload)
|
||||||
} catch(e) { toastStore.error('发送失败'); }
|
} catch(e: any) {
|
||||||
|
// 移除已添加的消息(因为发送失败)
|
||||||
|
const messages = chatStore.messages[roomId] || []
|
||||||
|
const index = messages.findIndex(m => m.id === message.id)
|
||||||
|
if (index > -1) {
|
||||||
|
messages.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMsg = e?.response?.data?.message || e?.message || '发送失败'
|
||||||
|
toastStore.error(errorMsg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWebSocketMessage(message: ChatMessage) {
|
function handleWebSocketMessage(message: ChatMessage) {
|
||||||
@@ -679,10 +788,88 @@ function handleWebSocketMessage(message: ChatMessage) {
|
|||||||
const roomId = message.room_id
|
const roomId = message.room_id
|
||||||
message.isSelf = message.sender_user_id === authStore.user!.id
|
message.isSelf = message.sender_user_id === authStore.user!.id
|
||||||
message.extra = typeof message.extra === 'string' ? JSON.parse(message.extra || '{}') : message.extra
|
message.extra = typeof message.extra === 'string' ? JSON.parse(message.extra || '{}') : message.extra
|
||||||
|
|
||||||
|
// 处理好友申请通知
|
||||||
|
if (message.message_type === 5) {
|
||||||
|
const extra = message.extra as any
|
||||||
|
toastStore.showToast(
|
||||||
|
'收到好友申请',
|
||||||
|
'info',
|
||||||
|
5000,
|
||||||
|
extra.title || message.content || '有人申请添加你为好友',
|
||||||
|
{
|
||||||
|
label: '去处理',
|
||||||
|
handler: () => {
|
||||||
|
currentTab.value = 'contact'
|
||||||
|
contactStore.setLeftPanelMode('friend-notify')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理群聊通知
|
||||||
|
if (message.message_type === 7) {
|
||||||
|
const extra = message.extra as any
|
||||||
|
toastStore.showToast(
|
||||||
|
'群聊通知',
|
||||||
|
'info',
|
||||||
|
5000,
|
||||||
|
extra.title || message.content || '收到群聊通知',
|
||||||
|
{
|
||||||
|
label: '去查看',
|
||||||
|
handler: () => {
|
||||||
|
currentTab.value = 'contact'
|
||||||
|
contactStore.setLeftPanelMode('group-notify')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
chatStore.addMessage(roomId, message)
|
chatStore.addMessage(roomId, message)
|
||||||
|
|
||||||
const contact = chatStore.contacts.find(c => c.user_id === message.sender_user_id || c.id === message.sender_user_id)
|
const contact = chatStore.contacts.find(c => c.user_id === message.sender_user_id || c.id === message.sender_user_id)
|
||||||
|
|
||||||
|
// 检查是否为群聊消息,如果是且当前正在查看该群聊,检查群成员信息
|
||||||
|
const isGroup = chatStore.currentTarget?.is_group || chatStore.currentTarget?.room_type === 'group'
|
||||||
|
const currentRoomId = chatStore.currentTarget ? getRoomId(chatStore.currentTarget) : null
|
||||||
|
const isCurrentGroupChat = isGroup && currentRoomId === roomId
|
||||||
|
|
||||||
|
// 如果是群聊消息且当前正在查看该群聊,检查发送者是否在 roomMembers 中
|
||||||
|
if (isCurrentGroupChat && !message.isSelf && message.sender_user_id) {
|
||||||
|
const roomMembersForRoom = roomMembers.value[roomId] || {}
|
||||||
|
if (!roomMembersForRoom[message.sender_user_id]) {
|
||||||
|
// 发送者不在成员列表中,立即加载群成员
|
||||||
|
loadGroupMembers(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查会话是否存在
|
||||||
|
// 对于群聊消息,使用 room_id 查找;对于私聊消息,使用 target_id 查找
|
||||||
|
const isGroupMessage = message.room_id && (message.room_id.startsWith('group_') || message.receiver_user_id === message.room_id)
|
||||||
|
let existingConv = null
|
||||||
|
|
||||||
|
if (isGroupMessage) {
|
||||||
|
// 群聊消息:使用 room_id 查找会话
|
||||||
|
existingConv = conversationStore.conversations.find(c => c.room_id === roomId)
|
||||||
|
} else {
|
||||||
|
// 私聊消息:使用 target_id 查找会话
|
||||||
|
const targetId = message.isSelf ? message.receiver_user_id : message.sender_user_id
|
||||||
|
existingConv = conversationStore.conversations.find(c => c.target_id === targetId || c.room_id === roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果会话不存在,刷新会话列表(添加防抖,避免频繁刷新)
|
||||||
|
if (!existingConv && !message.isSelf) {
|
||||||
|
const now = Date.now()
|
||||||
|
const lastRefresh = (window as any).__lastConversationRefresh || 0
|
||||||
|
// 防抖:1秒内最多刷新一次
|
||||||
|
if (now - lastRefresh > 1000) {
|
||||||
|
(window as any).__lastConversationRefresh = now
|
||||||
|
conversationStore.loadConversations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (contact) {
|
if (contact) {
|
||||||
chatStore.updateContactLastMsg(contact.id, getMsgSummary(message), Date.now())
|
chatStore.updateContactLastMsg(contact.id, getMsgSummary(message), Date.now())
|
||||||
|
|
||||||
@@ -709,6 +896,29 @@ function handleWebSocketMessage(message: ChatMessage) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 不在会话列表时收到消息,显示提示
|
||||||
|
if (!message.isSelf && currentTab.value !== 'chat') {
|
||||||
|
const senderName = message.sender_user_id || '未知用户'
|
||||||
|
toastStore.showToast(
|
||||||
|
'收到新消息',
|
||||||
|
'info',
|
||||||
|
5000,
|
||||||
|
`来自 ${senderName} 的新消息`,
|
||||||
|
{
|
||||||
|
label: '去查看',
|
||||||
|
handler: async () => {
|
||||||
|
currentTab.value = 'chat'
|
||||||
|
// 尝试查找或创建会话
|
||||||
|
await conversationStore.loadConversations()
|
||||||
|
const conv = conversationStore.conversations.find(c => c.room_id === roomId || c.target_id === message.sender_user_id)
|
||||||
|
if (conv) {
|
||||||
|
await selectChatByConversation(conv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,10 +1122,169 @@ async function handleGroupMemberRemoved() {
|
|||||||
await conversationStore.loadConversations()
|
await conversationStore.loadConversations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新会话列表
|
||||||
|
async function refreshConversations() {
|
||||||
|
if (refreshingConversations.value) return
|
||||||
|
refreshingConversations.value = true
|
||||||
|
try {
|
||||||
|
await conversationStore.loadConversations()
|
||||||
|
toastStore.success('会话列表已刷新')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh conversations:', error)
|
||||||
|
toastStore.error('刷新失败')
|
||||||
|
} finally {
|
||||||
|
refreshingConversations.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 群成员点击处理
|
// 群成员点击处理
|
||||||
function handleGroupMemberClicked(member: any) {
|
function handleGroupMemberClicked(member: any) {
|
||||||
// 可以在这里实现点击成员后的操作,比如查看成员信息、@成员等
|
// 显示成员的用户详细信息
|
||||||
console.log('Member clicked:', member)
|
if (member.user) {
|
||||||
|
// 如果有完整的用户信息,直接使用
|
||||||
|
selectedUser.value = member.user as User
|
||||||
|
} else if (member.user_id) {
|
||||||
|
// 如果没有完整信息,尝试从联系人列表中查找
|
||||||
|
const contact = chatStore.contacts.find(c => c.user_id === member.user_id || c.id === member.user_id)
|
||||||
|
if (contact) {
|
||||||
|
selectedUser.value = contact
|
||||||
|
} else {
|
||||||
|
// 如果找不到,创建一个临时的 Contact 对象
|
||||||
|
selectedUser.value = {
|
||||||
|
id: member.user_id,
|
||||||
|
user_id: member.user_id,
|
||||||
|
contact_user_id: member.user_id,
|
||||||
|
is_top: false,
|
||||||
|
is_muted: false,
|
||||||
|
user: member.user || undefined
|
||||||
|
} as Contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理头像点击
|
||||||
|
function handleAvatarClick(userOrContact: User | Contact) {
|
||||||
|
// 如果是自己,显示自己的资料
|
||||||
|
if (('id' in userOrContact && userOrContact.id === authStore.user?.id) ||
|
||||||
|
('user_id' in userOrContact && userOrContact.user_id === authStore.user?.id)) {
|
||||||
|
showProfileModal.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示对方信息
|
||||||
|
selectedUser.value = userOrContact as Contact
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为好友
|
||||||
|
function isFriend(user: Contact | User): boolean {
|
||||||
|
if (!user) return false
|
||||||
|
const userId = 'user_id' in user ? user.user_id : ('id' in user ? user.id : null)
|
||||||
|
if (!userId) return false
|
||||||
|
|
||||||
|
// 检查是否在联系人列表中
|
||||||
|
return chatStore.contacts.some(c => c.user_id === userId || c.id === userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加好友
|
||||||
|
async function handleAddFriend() {
|
||||||
|
if (!selectedUser.value) return
|
||||||
|
|
||||||
|
const userId = 'user_id' in selectedUser.value
|
||||||
|
? selectedUser.value.user_id
|
||||||
|
: ('id' in selectedUser.value ? selectedUser.value.id : null)
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
toastStore.error('无法获取用户ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await contactApi.addFriend({ to_user_id: userId })
|
||||||
|
toastStore.success('好友申请已发送')
|
||||||
|
selectedUser.value = null
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to add friend:', error)
|
||||||
|
toastStore.error(error.message || '添加好友失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给用户发消息
|
||||||
|
function handleSendMessageToUser() {
|
||||||
|
if (!selectedUser.value) return
|
||||||
|
|
||||||
|
// 查找或创建联系人
|
||||||
|
const userId = 'user_id' in selectedUser.value
|
||||||
|
? selectedUser.value.user_id
|
||||||
|
: ('id' in selectedUser.value ? selectedUser.value.id : null)
|
||||||
|
|
||||||
|
if (!userId) return
|
||||||
|
|
||||||
|
let contact = chatStore.contacts.find(c => c.user_id === userId || c.id === userId)
|
||||||
|
if (!contact) {
|
||||||
|
// 创建临时联系人对象
|
||||||
|
contact = {
|
||||||
|
id: userId,
|
||||||
|
user_id: userId,
|
||||||
|
contact_user_id: userId,
|
||||||
|
remark_name: 'name' in selectedUser.value ? selectedUser.value.name : selectedUser.value.user?.name,
|
||||||
|
is_top: false,
|
||||||
|
is_muted: false,
|
||||||
|
user: 'user' in selectedUser.value ? selectedUser.value.user : selectedUser.value as any,
|
||||||
|
} as Contact
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedUser.value = null
|
||||||
|
selectChat(contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取用户显示名称
|
||||||
|
function getUserDisplayName(user: Contact | User | null): string {
|
||||||
|
if (!user) return '未知用户'
|
||||||
|
if ('remark_name' in user && user.remark_name) return user.remark_name
|
||||||
|
if ('name' in user && user.name) return user.name
|
||||||
|
if ('user' in user && user.user?.name) return user.user.name
|
||||||
|
return '未知用户'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取用户头像
|
||||||
|
function getUserAvatar(user: Contact | User | null): string {
|
||||||
|
if (!user) return ''
|
||||||
|
if ('avatar' in user && user.avatar) return user.avatar
|
||||||
|
if ('user' in user && user.user?.avatar) return user.user.avatar
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取用户描述
|
||||||
|
function getUserDesc(user: Contact | User | null): string {
|
||||||
|
if (!user) return ''
|
||||||
|
if ('desc' in user && user.desc) return user.desc
|
||||||
|
if ('user' in user && user.user?.desc) return user.user.desc
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取用户ID
|
||||||
|
function getUserId(user: Contact | User | null): string | null {
|
||||||
|
if (!user) return null
|
||||||
|
if ('user_id' in user) return user.user_id
|
||||||
|
if ('id' in user) return user.id
|
||||||
|
if ('user' in user && user.user?.id) return user.user.id
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取用户手机号
|
||||||
|
function getUserPhone(user: Contact | User | null): string | null {
|
||||||
|
if (!user) return null
|
||||||
|
if ('phone' in user && user.phone) return user.phone
|
||||||
|
if ('user' in user && user.user?.phone) return user.user.phone
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取用户邮箱
|
||||||
|
function getUserEmail(user: Contact | User | null): string | null {
|
||||||
|
if (!user) return null
|
||||||
|
if ('email' in user && user.email) return user.email
|
||||||
|
if ('user' in user && user.user?.email) return user.user.email
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(currentTab, (newTab) => {
|
watch(currentTab, (newTab) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user