修复了一些逻辑漏洞
This commit is contained in:
@@ -224,6 +224,16 @@
|
||||
@confirm="handleDissolve"
|
||||
@cancel="showDissolveConfirm = false"
|
||||
/>
|
||||
|
||||
<!-- 用户信息卡片 -->
|
||||
<UserInfoCard
|
||||
:show="showUserInfoCard"
|
||||
:user="selectedMember?.user || null"
|
||||
@close="showUserInfoCard = false; selectedMember = null"
|
||||
@send-message="handleSendMessageToMember"
|
||||
@audio-call="handleAudioCallToMember"
|
||||
@video-call="handleVideoCallToMember"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -237,6 +247,8 @@ 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'
|
||||
import UserInfoCard from '@/components/common/UserInfoCard.vue'
|
||||
import SelectContactsModal from '@/components/chat/SelectContactsModal.vue'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
@@ -267,6 +279,8 @@ const showRemoveMemberConfirm = ref(false)
|
||||
const showQuitConfirm = ref(false)
|
||||
const showDissolveConfirm = ref(false)
|
||||
const pendingMember = ref<GroupMember | null>(null)
|
||||
const selectedMember = ref<GroupMember | null>(null)
|
||||
const showUserInfoCard = ref(false)
|
||||
|
||||
// 当前用户角色
|
||||
const currentUserRole = computed(() => {
|
||||
@@ -336,7 +350,42 @@ function canManageMember(member: GroupMember): boolean {
|
||||
|
||||
// 处理成员点击
|
||||
function handleMemberClick(member: GroupMember) {
|
||||
// 可以扩展为显示成员详情
|
||||
selectedMember.value = member
|
||||
showUserInfoCard.value = true
|
||||
}
|
||||
|
||||
// 处理发送消息
|
||||
function handleSendMessageToMember() {
|
||||
if (!selectedMember.value) return
|
||||
const contact: Contact = {
|
||||
id: selectedMember.value.user_id,
|
||||
user_id: selectedMember.value.user_id,
|
||||
contact_user_id: selectedMember.value.user_id,
|
||||
room_id: '',
|
||||
room_type: 'p2p',
|
||||
is_group: false,
|
||||
remark_name: selectedMember.value.user?.name || selectedMember.value.nickname || '未知',
|
||||
is_top: false,
|
||||
is_muted: false,
|
||||
user: selectedMember.value.user,
|
||||
}
|
||||
chatStore.setCurrentTarget(contact)
|
||||
showUserInfoCard.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 处理语音通话
|
||||
function handleAudioCallToMember() {
|
||||
if (!selectedMember.value) return
|
||||
// 这里可以调用 WebRTC 相关功能
|
||||
showUserInfoCard.value = false
|
||||
}
|
||||
|
||||
// 处理视频通话
|
||||
function handleVideoCallToMember() {
|
||||
if (!selectedMember.value) return
|
||||
// 这里可以调用 WebRTC 相关功能
|
||||
showUserInfoCard.value = false
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center text-white font-bold shadow-md transition"
|
||||
class="flex items-center justify-center text-white font-bold shadow-md transition overflow-hidden"
|
||||
:class="[sizeClass, roundedClass, { 'cursor-pointer hover:opacity-80': $attrs.onClick }]"
|
||||
:style="{ background: color || generateColor(name || avatar || '') }"
|
||||
:style="isImage ? {} : { background: color || generateColor(name || avatar || '') }"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
{{ avatar || name?.charAt(0).toUpperCase() || '?' }}
|
||||
<img
|
||||
v-if="isImage"
|
||||
:src="avatar"
|
||||
:alt="name || 'Avatar'"
|
||||
class="w-full h-full object-cover"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<span v-else>{{ avatar || name?.charAt(0).toUpperCase() || '?' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { generateColor } from '@/utils/format'
|
||||
|
||||
interface Props {
|
||||
avatar?: string
|
||||
name?: string
|
||||
color?: string
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'contact'
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'contact'
|
||||
rounded?: 'full' | 'xl' | 'lg'
|
||||
}
|
||||
|
||||
@@ -26,12 +33,44 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
rounded: 'full',
|
||||
})
|
||||
|
||||
const imageError = ref(false)
|
||||
|
||||
// 判断 avatar 是否是图片URL
|
||||
const isImage = computed(() => {
|
||||
if (!props.avatar || imageError.value) return false
|
||||
|
||||
// 检查是否是 base64 图片
|
||||
if (props.avatar.startsWith('data:image/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否是 http/https URL
|
||||
if (props.avatar.startsWith('http://') || props.avatar.startsWith('https://')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否是相对路径的图片(以 / 开头)
|
||||
if (props.avatar.startsWith('/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否是图片文件扩展名
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']
|
||||
const lowerAvatar = props.avatar.toLowerCase()
|
||||
if (imageExtensions.some(ext => lowerAvatar.endsWith(ext))) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
const sizes = {
|
||||
sm: 'w-9 h-9 text-xs',
|
||||
md: 'w-10 h-10 text-sm',
|
||||
lg: 'w-12 h-12 text-base',
|
||||
xl: 'w-16 h-16 text-xl',
|
||||
'2xl': 'w-24 h-24 text-2xl',
|
||||
contact: 'w-11 h-11 text-lg',
|
||||
}
|
||||
return sizes[props.size]
|
||||
@@ -46,6 +85,11 @@ const roundedClass = computed(() => {
|
||||
return rounded[props.rounded]
|
||||
})
|
||||
|
||||
// 图片加载失败时的处理
|
||||
function handleImageError() {
|
||||
imageError.value = true
|
||||
}
|
||||
|
||||
defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
@@ -328,18 +328,202 @@
|
||||
|
||||
<!-- Profile Modal (自己的) -->
|
||||
<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="h-24 bg-gradient-to-r from-indigo-600 to-purple-600"></div>
|
||||
<div class="px-6 pb-6 text-center -mt-12">
|
||||
<Avatar
|
||||
:name="authStore.user.name"
|
||||
:avatar="authStore.user.avatar"
|
||||
size="xl"
|
||||
rounded="full"
|
||||
class="mx-auto border-4 border-panel shadow-xl"
|
||||
/>
|
||||
<h2 class="text-xl font-bold mt-3 text-white">{{ authStore.user.name }}</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">{{ authStore.user.desc || '暂无签名' }}</p>
|
||||
<div class="bg-[#111827] rounded-2xl w-full max-w-2xl shadow-2xl border border-gray-700 overflow-hidden animate-fade-in max-h-[90vh] flex flex-col relative">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="absolute top-0 left-0 right-0 h-48 bg-gradient-to-b from-primary/20 to-transparent pointer-events-none"></div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
class="absolute top-5 right-5 z-50 w-9 h-9 rounded-full bg-black/20 hover:bg-white/10 text-gray-300 hover:text-white flex items-center justify-center transition backdrop-blur-md border border-white/5"
|
||||
@click="showProfileModal = false"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar relative z-10 flex flex-col">
|
||||
<!-- 头部:头像与核心信息 -->
|
||||
<div class="pt-14 pb-8 px-8 text-center flex flex-col items-center animate-fade-in">
|
||||
<div class="relative group cursor-pointer" @click="showAvatarEditModal = true">
|
||||
<Avatar
|
||||
:name="authStore.user.name"
|
||||
:avatar="authStore.user.avatar"
|
||||
size="2xl"
|
||||
rounded="full"
|
||||
class="border-[6px] border-[#111827] shadow-2xl relative z-10 transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
<div class="absolute inset-0 rounded-full bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<i class="fas fa-camera text-white text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-bold text-white mt-5 flex items-center gap-2 justify-center select-text">
|
||||
{{ authStore.user.name }}
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-400 text-sm mt-2 max-w-md truncate px-4 opacity-80">
|
||||
{{ authStore.user.desc || '暂无签名' }}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-3 mt-4 text-xs font-medium text-gray-400">
|
||||
<span class="bg-white/5 px-2.5 py-1 rounded-md border border-white/5 select-text">ID: {{ authStore.user.id }}</span>
|
||||
<span v-if="authStore.user.region" class="bg-white/5 px-2.5 py-1 rounded-md border border-white/5">{{ authStore.user.region }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息列表 -->
|
||||
<div class="px-8 py-4 space-y-3 max-w-xl mx-auto w-full animate-slide-up">
|
||||
<div class="bg-white/5 rounded-2xl border border-white/5 overflow-hidden">
|
||||
<!-- 网名 -->
|
||||
<div class="flex items-center p-4 hover:bg-white/5 transition-colors group relative border-b border-white/5 cursor-pointer" @click="showNameEditModal = true">
|
||||
<div class="w-9 h-9 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 mr-4 shrink-0">
|
||||
<i class="far fa-user"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 mr-8">
|
||||
<div class="text-[10px] text-gray-500 uppercase tracking-wider mb-0.5 font-bold">网名</div>
|
||||
<div class="text-sm text-gray-200 font-medium truncate select-text">{{ authStore.user.name || '未知' }}</div>
|
||||
</div>
|
||||
<i class="fas fa-edit text-xs text-gray-600 absolute right-4 group-hover:text-gray-400 transition-colors"></i>
|
||||
</div>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<div class="flex items-center p-4 hover:bg-white/5 transition-colors group relative border-b border-white/5 cursor-pointer" @click="showPhoneEditModal = true">
|
||||
<div class="w-9 h-9 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 mr-4 shrink-0">
|
||||
<i class="fas fa-phone-alt"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 mr-8">
|
||||
<div class="text-[10px] text-gray-500 uppercase tracking-wider mb-0.5 font-bold">手机号</div>
|
||||
<div class="text-sm text-gray-200 font-medium truncate select-text font-mono">{{ authStore.user.phone || '未设置' }}</div>
|
||||
</div>
|
||||
<i class="fas fa-edit text-xs text-gray-600 absolute right-4 group-hover:text-gray-400 transition-colors"></i>
|
||||
</div>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<div v-if="authStore.user.email" class="flex items-center p-4 hover:bg-white/5 transition-colors group relative border-b border-white/5 last:border-0">
|
||||
<div class="w-9 h-9 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 mr-4 shrink-0">
|
||||
<i class="far fa-envelope"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 mr-8">
|
||||
<div class="text-[10px] text-gray-500 uppercase tracking-wider mb-0.5 font-bold">邮箱</div>
|
||||
<div class="text-sm text-gray-200 font-medium truncate select-text font-mono">{{ authStore.user.email }}</div>
|
||||
</div>
|
||||
<button class="absolute right-4 w-8 h-8 rounded-lg hover:bg-white/10 text-gray-500 hover:text-primary transition flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
@click.stop="copyText(authStore.user.email)" title="复制">
|
||||
<i class="far fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 地区 -->
|
||||
<div v-if="authStore.user.region" class="flex items-center p-4 hover:bg-white/5 transition-colors group relative border-b border-white/5 last:border-0">
|
||||
<div class="w-9 h-9 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 mr-4 shrink-0">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 mr-8">
|
||||
<div class="text-[10px] text-gray-500 uppercase tracking-wider mb-0.5 font-bold">地区</div>
|
||||
<div class="text-sm text-gray-200 font-medium truncate select-text">{{ authStore.user.region }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 签名 -->
|
||||
<div class="flex items-start p-4 hover:bg-white/5 transition-colors group relative border-b border-white/5 last:border-0">
|
||||
<div class="w-9 h-9 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 mr-4 shrink-0">
|
||||
<i class="far fa-comment"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 mr-8">
|
||||
<div class="text-[10px] text-gray-500 uppercase tracking-wider mb-0.5 font-bold">个性签名</div>
|
||||
<div class="text-sm text-gray-200 font-medium break-words">{{ authStore.user.desc || '暂无签名' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 头像编辑弹窗 -->
|
||||
<div v-if="showAvatarEditModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[100] flex items-center justify-center p-4" @click.self="showAvatarEditModal = false">
|
||||
<div class="bg-panel w-96 rounded-2xl border border-gray-700 p-5 shadow-2xl animate-scale-in">
|
||||
<h3 class="text-lg font-bold text-white mb-4">编辑头像</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">当前头像</label>
|
||||
<div class="flex justify-center mb-4">
|
||||
<Avatar
|
||||
:name="authStore.user?.name || ''"
|
||||
:avatar="editingAvatar || authStore.user?.avatar"
|
||||
size="xl"
|
||||
rounded="full"
|
||||
class="border-4 border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">上传图片</label>
|
||||
<input
|
||||
ref="avatarFileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleAvatarFileSelect"
|
||||
/>
|
||||
<button
|
||||
class="w-full py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition mb-2"
|
||||
@click="avatarFileInput?.click()"
|
||||
>
|
||||
<i class="fas fa-upload mr-2"></i>选择文件
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">或输入图片URL</label>
|
||||
<input
|
||||
v-model="editingAvatar"
|
||||
type="text"
|
||||
class="w-full bg-black/30 text-white p-3 rounded-xl border border-gray-600 focus:border-primary outline-none"
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button class="flex-1 py-2.5 rounded-lg text-gray-400 hover:bg-white/5 transition" @click="showAvatarEditModal = false; editingAvatar = ''">取消</button>
|
||||
<button class="flex-1 py-2.5 rounded-lg bg-primary text-white font-bold hover:bg-primary-hover transition" @click="handleUpdateAvatar">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 昵称编辑弹窗 -->
|
||||
<div v-if="showNameEditModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[100] flex items-center justify-center p-4" @click.self="showNameEditModal = false">
|
||||
<div class="bg-panel w-96 rounded-2xl border border-gray-700 p-5 shadow-2xl animate-scale-in">
|
||||
<h3 class="text-lg font-bold text-white mb-4">编辑昵称</h3>
|
||||
<input
|
||||
v-model="editingName"
|
||||
type="text"
|
||||
class="w-full bg-black/30 text-white p-3 rounded-xl border border-gray-600 focus:border-primary outline-none"
|
||||
placeholder="请输入昵称"
|
||||
maxlength="20"
|
||||
@keyup.enter="handleUpdateName"
|
||||
/>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button class="flex-1 py-2.5 rounded-lg text-gray-400 hover:bg-white/5 transition" @click="showNameEditModal = false; editingName = ''">取消</button>
|
||||
<button class="flex-1 py-2.5 rounded-lg bg-primary text-white font-bold hover:bg-primary-hover transition" @click="handleUpdateName">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机号编辑弹窗 -->
|
||||
<div v-if="showPhoneEditModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[100] flex items-center justify-center p-4" @click.self="showPhoneEditModal = false">
|
||||
<div class="bg-panel w-96 rounded-2xl border border-gray-700 p-5 shadow-2xl animate-scale-in">
|
||||
<h3 class="text-lg font-bold text-white mb-4">编辑手机号</h3>
|
||||
<input
|
||||
v-model="editingPhone"
|
||||
type="tel"
|
||||
class="w-full bg-black/30 text-white p-3 rounded-xl border border-gray-600 focus:border-primary outline-none font-mono"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="20"
|
||||
@keyup.enter="handleUpdatePhone"
|
||||
/>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button class="flex-1 py-2.5 rounded-lg text-gray-400 hover:bg-white/5 transition" @click="showPhoneEditModal = false; editingPhone = ''">取消</button>
|
||||
<button class="flex-1 py-2.5 rounded-lg bg-primary text-white font-bold hover:bg-primary-hover transition" @click="handleUpdatePhone">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,6 +587,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息卡片(群聊消息列表点击头像) -->
|
||||
<UserInfoCard
|
||||
:show="showUserInfoCard"
|
||||
:user="selectedUserForInfo as User | null"
|
||||
@close="showUserInfoCard = false; selectedUserForInfo = null"
|
||||
@send-message="handleSendMessageFromInfoCard"
|
||||
@audio-call="handleAudioCallFromInfoCard"
|
||||
@video-call="handleVideoCallFromInfoCard"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -420,6 +614,7 @@ import * as messageApi from '@/api/modules/message'
|
||||
import * as contactApi from '@/api/modules/contact'
|
||||
import * as attachmentApi from '@/api/modules/attachment'
|
||||
import * as conversationApi from '@/api/modules/conversation'
|
||||
import * as userApi from '@/api/modules/user'
|
||||
import { formatTime, generateColor } from '@/utils/format'
|
||||
import { getMessageSummary } from '@/utils/messageTypes'
|
||||
import { storage } from '@/utils/storage'
|
||||
@@ -441,6 +636,7 @@ import GroupNotifyView from '@/views/contact/GroupNotifyView.vue'
|
||||
import SelectContactsModal from '@/components/chat/SelectContactsModal.vue'
|
||||
import GroupInfoPanel from '@/components/chat/GroupInfoPanel.vue'
|
||||
import GroupChatPanel from '@/components/chat/GroupChatPanel.vue'
|
||||
import UserInfoCard from '@/components/common/UserInfoCard.vue'
|
||||
import * as groupApi from '@/api/modules/room'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -469,6 +665,16 @@ const pendingDeleteConversation = ref<Conversation | null>(null)
|
||||
const showGroupInfoPanel = ref(false)
|
||||
const hoveredBadgeId = ref<number | null>(null)
|
||||
const refreshingConversations = ref(false)
|
||||
const showAvatarEditModal = ref(false)
|
||||
const showNameEditModal = ref(false)
|
||||
const showPhoneEditModal = ref(false)
|
||||
const editingAvatar = ref('')
|
||||
const editingName = ref('')
|
||||
const editingPhone = ref('')
|
||||
const avatarFileInput = ref<HTMLInputElement | null>(null)
|
||||
const selectedAvatarFile = ref<File | null>(null)
|
||||
const showUserInfoCard = ref(false)
|
||||
const selectedUserForInfo = ref<User | Contact | null>(null)
|
||||
const roomMembers = ref<Record<string, Record<string, { name: string; avatar?: string }>>>({})
|
||||
|
||||
// Scroll Logic State
|
||||
@@ -991,7 +1197,7 @@ function handleWebSocketMessage(message: ChatMessage) {
|
||||
// 如果接口失败,仍然添加消息(使用原始 roomId),然后刷新列表
|
||||
chatStore.addMessage(roomId, message)
|
||||
processMessageAfterConversation(message, roomId)
|
||||
conversationStore.loadConversations()
|
||||
conversationStore.loadConversations()
|
||||
})
|
||||
return // 异步处理,先返回
|
||||
} else {
|
||||
@@ -1016,7 +1222,7 @@ function processMessageAfterConversation(message: ChatMessage, roomId: string) {
|
||||
|
||||
// 检查是否为群聊消息,如果是且当前正在查看该群聊,检查群成员信息
|
||||
const isGroup = chatStore.currentTarget?.is_group || chatStore.currentTarget?.room_type === 'group'
|
||||
const currentRoomId = chatStore.currentTarget ? getRoomId(chatStore.currentTarget) : null
|
||||
const currentRoomId = chatStore.currentTarget ? getRoomId(chatStore.currentTarget) : null
|
||||
const isCurrentGroupChat = isGroup && currentRoomId === roomId
|
||||
|
||||
// 如果是群聊消息且当前正在查看该群聊,检查发送者是否在 roomMembers 中
|
||||
@@ -1028,7 +1234,7 @@ function processMessageAfterConversation(message: ChatMessage, roomId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const isCurrentChat = currentRoomId === roomId
|
||||
const isCurrentChat = currentRoomId === roomId
|
||||
|
||||
if (contact) {
|
||||
chatStore.updateContactLastMsg(contact.id, getMsgSummary(message), Date.now())
|
||||
@@ -1106,6 +1312,144 @@ function processMessageAfterConversation(message: ChatMessage, roomId: string) {
|
||||
function backToList() { if (isMobile.value) chatVisible.value = false }
|
||||
function handleLogout() { authStore.logout(); wsManager.disconnect(); router.push('/login') }
|
||||
function getMsgSummary(msg: ChatMessage): string { return getMessageSummary(msg) }
|
||||
|
||||
// 复制文本
|
||||
function copyText(text?: string) {
|
||||
if (!text) return
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
toastStore.success('复制成功')
|
||||
}).catch(() => {
|
||||
toastStore.error('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
// 处理头像文件选择
|
||||
function handleAvatarFileSelect(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toastStore.error('请选择图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 保存文件对象用于上传
|
||||
selectedAvatarFile.value = file
|
||||
|
||||
// 预览图片(用于显示)
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
editingAvatar.value = e.target?.result as string
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// 更新头像
|
||||
async function handleUpdateAvatar() {
|
||||
if (!authStore.user) return
|
||||
|
||||
try {
|
||||
let avatarUrl = editingAvatar.value
|
||||
|
||||
// 如果有选择的文件,先上传文件到服务器
|
||||
if (selectedAvatarFile.value) {
|
||||
const uploadResult: any = await attachmentApi.uploadAttachment(selectedAvatarFile.value, 'image')
|
||||
avatarUrl = uploadResult.file_url || uploadResult.url || editingAvatar.value
|
||||
selectedAvatarFile.value = null
|
||||
} else if (avatarUrl && !avatarUrl.startsWith('http') && !avatarUrl.startsWith('/') && !avatarUrl.startsWith('data:image/')) {
|
||||
// 如果输入的是普通文本,不处理
|
||||
toastStore.warning('请输入有效的图片URL或上传图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
await userApi.updateUser({
|
||||
id: authStore.user.id,
|
||||
updates: { avatar: avatarUrl }
|
||||
})
|
||||
|
||||
// 刷新用户信息
|
||||
const updatedUser = await userApi.getMyInfo() as any
|
||||
authStore.updateUserInfo(updatedUser)
|
||||
|
||||
toastStore.success('头像已更新')
|
||||
showAvatarEditModal.value = false
|
||||
editingAvatar.value = ''
|
||||
} catch (e: any) {
|
||||
console.error('Failed to update avatar:', e)
|
||||
toastStore.error(e?.response?.data?.message || e?.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新昵称
|
||||
async function handleUpdateName() {
|
||||
if (!authStore.user || !editingName.value.trim()) {
|
||||
toastStore.warning('昵称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await userApi.updateUser({
|
||||
id: authStore.user.id,
|
||||
updates: { name: editingName.value.trim() }
|
||||
})
|
||||
|
||||
// 刷新用户信息
|
||||
const updatedUser = await userApi.getMyInfo() as any
|
||||
authStore.updateUserInfo(updatedUser)
|
||||
|
||||
toastStore.success('昵称已更新')
|
||||
showNameEditModal.value = false
|
||||
editingName.value = ''
|
||||
} catch (e: any) {
|
||||
console.error('Failed to update name:', e)
|
||||
toastStore.error(e?.response?.data?.message || e?.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新手机号
|
||||
async function handleUpdatePhone() {
|
||||
if (!authStore.user) return
|
||||
|
||||
try {
|
||||
await userApi.updateUser({
|
||||
id: authStore.user.id,
|
||||
updates: { phone: editingPhone.value.trim() }
|
||||
})
|
||||
|
||||
// 刷新用户信息
|
||||
const updatedUser = await userApi.getMyInfo() as any
|
||||
authStore.updateUserInfo(updatedUser)
|
||||
|
||||
toastStore.success('手机号已更新')
|
||||
showPhoneEditModal.value = false
|
||||
editingPhone.value = ''
|
||||
} catch (e: any) {
|
||||
console.error('Failed to update phone:', e)
|
||||
toastStore.error(e?.response?.data?.message || e?.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听编辑弹窗打开,初始化编辑值
|
||||
watch(showNameEditModal, (show) => {
|
||||
if (show && authStore.user) {
|
||||
editingName.value = authStore.user.name || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(showPhoneEditModal, (show) => {
|
||||
if (show && authStore.user) {
|
||||
editingPhone.value = authStore.user.phone || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(showAvatarEditModal, (show) => {
|
||||
if (show && authStore.user) {
|
||||
editingAvatar.value = authStore.user.avatar || ''
|
||||
selectedAvatarFile.value = null
|
||||
}
|
||||
})
|
||||
async function startCall(type: 'audio' | 'video') {
|
||||
if (!chatStore.currentTarget) return
|
||||
const receiverUserId = chatStore.currentTarget.user_id || chatStore.currentTarget.id
|
||||
@@ -1386,8 +1730,83 @@ function handleAvatarClick(userOrContact: User | Contact) {
|
||||
return
|
||||
}
|
||||
|
||||
// 显示对方信息
|
||||
selectedUser.value = userOrContact as Contact
|
||||
// 显示对方信息 - 使用新的 UserInfoCard 组件
|
||||
// 如果是 Contact,提取 User 信息
|
||||
if ('user' in userOrContact && userOrContact.user) {
|
||||
selectedUserForInfo.value = userOrContact.user
|
||||
} else if ('user_id' in userOrContact) {
|
||||
// 如果是 Contact 但没有 user 信息,需要查找或构造
|
||||
const contact = userOrContact as Contact
|
||||
// 尝试从联系人列表中找到完整的用户信息
|
||||
const fullContact = chatStore.contacts.find(c => c.user_id === contact.user_id || c.id === contact.user_id)
|
||||
if (fullContact?.user) {
|
||||
selectedUserForInfo.value = fullContact.user
|
||||
} else {
|
||||
// 如果没有找到,构造一个基本的 User 对象
|
||||
selectedUserForInfo.value = {
|
||||
id: contact.user_id || contact.id,
|
||||
name: contact.remark_name || contact.user?.name || '未知',
|
||||
avatar: contact.user?.avatar || '',
|
||||
email: contact.user?.email || '',
|
||||
phone: contact.user?.phone || '',
|
||||
desc: contact.user?.desc || '',
|
||||
region: contact.user?.region || '',
|
||||
created_at: contact.user?.created_at || '',
|
||||
updated_at: contact.user?.updated_at || '',
|
||||
} as User
|
||||
}
|
||||
} else {
|
||||
selectedUserForInfo.value = userOrContact as User
|
||||
}
|
||||
showUserInfoCard.value = true
|
||||
}
|
||||
|
||||
// 从用户信息卡片发送消息
|
||||
function handleSendMessageFromInfoCard() {
|
||||
if (!selectedUserForInfo.value) return
|
||||
|
||||
// 确保是 User 类型
|
||||
const user = selectedUserForInfo.value as User
|
||||
|
||||
// 查找或创建对应的联系人
|
||||
const userId = user.id
|
||||
let contact = chatStore.contacts.find(c => c.user_id === userId || c.id === userId)
|
||||
|
||||
if (!contact) {
|
||||
// 如果不在联系人列表中,创建一个临时的 Contact 对象
|
||||
contact = {
|
||||
id: userId,
|
||||
user_id: userId,
|
||||
contact_user_id: userId,
|
||||
room_id: '',
|
||||
room_type: 'p2p',
|
||||
is_group: false,
|
||||
remark_name: user.name,
|
||||
is_top: false,
|
||||
is_muted: false,
|
||||
user: user,
|
||||
} as Contact
|
||||
}
|
||||
|
||||
chatStore.setCurrentTarget(contact)
|
||||
showUserInfoCard.value = false
|
||||
selectedUserForInfo.value = null
|
||||
}
|
||||
|
||||
// 从用户信息卡片发起语音通话
|
||||
function handleAudioCallFromInfoCard() {
|
||||
if (!selectedUserForInfo.value) return
|
||||
// 这里可以调用 WebRTC 相关功能
|
||||
showUserInfoCard.value = false
|
||||
selectedUserForInfo.value = null
|
||||
}
|
||||
|
||||
// 从用户信息卡片发起视频通话
|
||||
function handleVideoCallFromInfoCard() {
|
||||
if (!selectedUserForInfo.value) return
|
||||
// 这里可以调用 WebRTC 相关功能
|
||||
showUserInfoCard.value = false
|
||||
selectedUserForInfo.value = null
|
||||
}
|
||||
|
||||
// 判断是否为好友
|
||||
@@ -1564,6 +1983,10 @@ onUnmounted(() => {
|
||||
.fade-slide-enter-active, .fade-slide-leave-active { transition: all 0.3s ease; }
|
||||
.fade-slide-enter-from, .fade-slide-leave-to { opacity: 0; transform: translateY(20px); }
|
||||
.animate-fade-in { animation: fadeIn 0.3s ease-out; }
|
||||
.animate-scale-in { animation: scaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||
.animate-slide-up { animation: slideUp 0.4s ease-out; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes scaleIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
||||
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
.animate-pulse-fast { animation: pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user