bug修复

This commit is contained in:
2025-12-08 15:53:41 +08:00
parent 11f2e8780e
commit 22c647b10d
7 changed files with 664 additions and 5 deletions

View File

@@ -300,6 +300,24 @@
</div>
</div>
</Teleport>
<!-- 用户信息卡片 -->
<UserInfoCard
:show="showUserInfoCard"
:user="selectedMemberUser"
@close="showUserInfoCard = false; selectedMemberUser = null"
@send-message="handleSendMessageFromCard"
@audio-call="handleAudioCallFromCard"
@video-call="handleVideoCallFromCard"
@view-moments="handleViewMomentsFromCard"
/>
<!-- 用户朋友圈模态框 -->
<UserMomentsModal
:show="showUserMomentsModal"
:user="selectedMemberUser"
@close="showUserMomentsModal = false"
/>
</div>
</template>
@@ -307,8 +325,10 @@
import { ref, computed, onMounted, watch } from 'vue'
import Avatar from '@/components/common/Avatar.vue'
import SelectContactsModal from '@/components/chat/SelectContactsModal.vue'
import GroupCallModal from '@/components/chat/GroupCallModal.vue' // 引入新组件
import GroupCallModal from '@/components/chat/GroupCallModal.vue'
import ContextMenu from '@/components/common/ContextMenu.vue'
import UserInfoCard from '@/components/common/UserInfoCard.vue'
import UserMomentsModal from '@/components/moment/UserMomentsModal.vue'
import * as groupApi from '@/api/modules/room'
import * as contactApi from '@/api/modules/contact'
import { useAuthStore } from '@/stores/auth'
@@ -316,7 +336,8 @@ import { useChatStore } from '@/stores/chat'
import { useToastStore } from '@/stores/toast'
import { useContextMenu } from '@/composables/useContextMenu'
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
import type { GroupMember, Contact } from '@/types/api'
import { useWebRTCStore } from '@/stores/webrtc'
import type { GroupMember, Contact, User } from '@/types/api'
import type { MenuItem } from '@/stores/contextMenu'
const props = defineProps<{ roomId: string }>()
@@ -327,9 +348,15 @@ const chatStore = useChatStore()
const toastStore = useToastStore()
const { showContextMenu } = useContextMenu()
const groupWebRTC = useGroupWebRTC()
const webrtcStore = useWebRTCStore()
const webrtc = webrtcStore.webrtc
// State
const members = ref<GroupMember[]>([])
// 用户信息卡片状态
const showUserInfoCard = ref(false)
const selectedMemberUser = ref<User | null>(null)
const showUserMomentsModal = ref(false)
const groupName = ref('')
const groupAvatar = ref('')
const announcement = ref('')
@@ -441,9 +468,85 @@ async function handleMemberSearch() {
}
function handleMemberClick(member: GroupMember) {
// 打开用户信息卡片
if (member.user) {
selectedMemberUser.value = member.user as User
} else {
// 如果没有完整用户信息,构造一个基本的 User 对象
selectedMemberUser.value = {
id: member.user_id,
name: member.nickname || '未知',
avatar: '',
email: '',
phone: '',
desc: '',
region: '',
created_at: '',
updated_at: '',
} as User
}
showUserInfoCard.value = true
emit('member-clicked', member)
}
// 从用户信息卡片发送消息
function handleSendMessageFromCard() {
if (!selectedMemberUser.value) return
const userId = selectedMemberUser.value.id
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,
room_id: '',
room_type: 'p2p',
is_group: false,
remark_name: selectedMemberUser.value.name,
is_top: false,
is_muted: false,
user: selectedMemberUser.value,
} as Contact
}
chatStore.setCurrentTarget(contact)
showUserInfoCard.value = false
selectedMemberUser.value = null
}
// 从用户信息卡片发起语音通话
async function handleAudioCallFromCard() {
if (!selectedMemberUser.value) return
const userId = selectedMemberUser.value.id
const contact = chatStore.contacts.find(c => c.user_id === userId || c.id === userId)
if (contact) {
await webrtc.startCall('audio', userId, contact.room_id, contact)
}
showUserInfoCard.value = false
selectedMemberUser.value = null
}
// 从用户信息卡片发起视频通话
async function handleVideoCallFromCard() {
if (!selectedMemberUser.value) return
const userId = selectedMemberUser.value.id
const contact = chatStore.contacts.find(c => c.user_id === userId || c.id === userId)
if (contact) {
await webrtc.startCall('video', userId, contact.room_id, contact)
}
showUserInfoCard.value = false
selectedMemberUser.value = null
}
// 从用户信息卡片查看朋友圈
function handleViewMomentsFromCard() {
if (!selectedMemberUser.value) return
showUserInfoCard.value = false
showUserMomentsModal.value = true
}
function showMemberContextMenu(event: MouseEvent, member: GroupMember) {
const items: MenuItem[] = []

View File

@@ -331,6 +331,24 @@
</div>
</div>
<!-- 用户信息卡片 -->
<UserInfoCard
:show="showUserInfoCard"
:user="selectedMemberUser"
@close="showUserInfoCard = false; selectedMemberUser = null"
@send-message="handleSendMessageFromCard"
@audio-call="handleAudioCallFromCard"
@video-call="handleVideoCallFromCard"
@view-moments="handleViewMomentsFromCard"
/>
<!-- 用户朋友圈模态框 -->
<UserMomentsModal
:show="showUserMomentsModal"
:user="selectedMemberUser"
@close="showUserMomentsModal = false"
/>
</div>
</Teleport>
</template>
@@ -340,13 +358,16 @@ import { ref, computed, watch } from 'vue'
import Avatar from '@/components/common/Avatar.vue'
import SelectContactsModal from '@/components/chat/SelectContactsModal.vue'
import GroupCallModal from '@/components/chat/GroupCallModal.vue'
import UserInfoCard from '@/components/common/UserInfoCard.vue'
import UserMomentsModal from '@/components/moment/UserMomentsModal.vue'
import * as groupApi from '@/api/modules/room'
import * as contactApi from '@/api/modules/contact'
import { useAuthStore } from '@/stores/auth'
import { useChatStore } from '@/stores/chat'
import { useToastStore } from '@/stores/toast'
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
import type { GroupInfo, GroupMember, Contact } from '@/types/api'
import { useWebRTCStore } from '@/stores/webrtc'
import type { GroupInfo, GroupMember, Contact, User } from '@/types/api'
const props = defineProps<{ show: boolean; roomId: string }>()
const emit = defineEmits(['close', 'updated', 'quit', 'dissolve'])
@@ -355,8 +376,14 @@ const authStore = useAuthStore()
const toastStore = useToastStore()
const chatStore = useChatStore()
const groupWebRTC = useGroupWebRTC()
const webrtcStore = useWebRTCStore()
const webrtc = webrtcStore.webrtc
const loading = ref(false)
// 用户信息卡片状态
const showUserInfoCard = ref(false)
const selectedMemberUser = ref<User | null>(null)
const showUserMomentsModal = ref(false)
const groupInfo = ref<GroupInfo | null>(null)
const members = ref<GroupMember[]>([])
const availableContacts = ref<Contact[]>([])
@@ -505,7 +532,87 @@ async function handleDissolve() {
} catch(e) { toastStore.error('解散失败') }
}
function handleMemberClick(member: GroupMember) { /* 可以添加查看成员资料逻辑 */ }
function handleMemberClick(member: GroupMember) {
// 打开用户信息卡片
if (member.user) {
selectedMemberUser.value = member.user as User
} else {
// 如果没有完整用户信息,构造一个基本的 User 对象
selectedMemberUser.value = {
id: member.user_id,
name: member.nickname || '未知',
avatar: '',
email: '',
phone: '',
desc: '',
region: '',
created_at: '',
updated_at: '',
} as User
}
showUserInfoCard.value = true
}
// 从用户信息卡片发送消息
function handleSendMessageFromCard() {
if (!selectedMemberUser.value) return
const userId = selectedMemberUser.value.id
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,
room_id: '',
room_type: 'p2p',
is_group: false,
remark_name: selectedMemberUser.value.name,
is_top: false,
is_muted: false,
user: selectedMemberUser.value,
} as Contact
}
chatStore.setCurrentTarget(contact)
showUserInfoCard.value = false
selectedMemberUser.value = null
emit('close')
}
// 从用户信息卡片发起语音通话
async function handleAudioCallFromCard() {
if (!selectedMemberUser.value) return
const userId = selectedMemberUser.value.id
const contact = chatStore.contacts.find(c => c.user_id === userId || c.id === userId)
if (contact) {
await webrtc.startCall('audio', userId, contact.room_id, contact)
}
showUserInfoCard.value = false
selectedMemberUser.value = null
emit('close')
}
// 从用户信息卡片发起视频通话
async function handleVideoCallFromCard() {
if (!selectedMemberUser.value) return
const userId = selectedMemberUser.value.id
const contact = chatStore.contacts.find(c => c.user_id === userId || c.id === userId)
if (contact) {
await webrtc.startCall('video', userId, contact.room_id, contact)
}
showUserInfoCard.value = false
selectedMemberUser.value = null
emit('close')
}
// 从用户信息卡片查看朋友圈
function handleViewMomentsFromCard() {
if (!selectedMemberUser.value) return
showUserInfoCard.value = false
showUserMomentsModal.value = true
}
// 移除成员相关
function showRemoveConfirm(member: GroupMember) {

View File

@@ -4,6 +4,15 @@
<div class="h-16 border-b border-gray-800 flex items-center justify-between px-6 bg-panel/90 backdrop-blur shrink-0">
<h2 class="text-lg font-bold text-white">朋友圈</h2>
<div class="flex items-center gap-2">
<!-- 刷新按钮 -->
<button
@click="handleRefresh"
class="p-2.5 text-gray-400 hover:text-primary rounded-xl hover:bg-white/5 transition"
:disabled="refreshing"
title="刷新动态"
>
<i class="fas fa-sync-alt text-lg" :class="{ 'fa-spin': refreshing }"></i>
</button>
<!-- 通知入口 -->
<button
@click="showNotifications = true"
@@ -317,6 +326,14 @@
:user="selectedUser"
@close="showUserCard = false"
@send-message="handleSendMessage"
@view-moments="handleViewMoments"
/>
<!-- 用户朋友圈模态框 -->
<UserMomentsModal
:show="showUserMomentsModal"
:user="selectedUser"
@close="showUserMomentsModal = false"
/>
</div>
</template>
@@ -333,6 +350,7 @@ import UserInfoCard from '@/components/common/UserInfoCard.vue'
import VideoPlayer from '@/components/common/VideoPlayer.vue'
import MomentPublisherDark from './MomentPublisherDark.vue'
import MomentNotificationPanel from './MomentNotificationPanel.vue'
import UserMomentsModal from './UserMomentsModal.vue'
import { parseMediaUrls } from '@/types/moment'
import { formatRelativeTime } from '@/utils/format'
import { storage } from '@/utils/storage'
@@ -348,10 +366,12 @@ const chatStore = useChatStore()
const showPublisher = ref(false)
const showNotifications = ref(false)
const expandedMoments = reactive(new Set<number>())
const refreshing = ref(false)
// 用户信息卡片相关
const showUserCard = ref(false)
const selectedUser = ref<User | null>(null)
const showUserMomentsModal = ref(false)
// 评论相关
const commentingMoment = ref<Moment | null>(null)
@@ -378,6 +398,18 @@ onMounted(() => {
momentStore.fetchUnreadCount()
})
// 刷新朋友圈
async function handleRefresh() {
if (refreshing.value) return
refreshing.value = true
try {
await momentStore.fetchMoments()
await momentStore.fetchUnreadCount()
} finally {
refreshing.value = false
}
}
// 方法
function formatTime(time: string): string {
return formatRelativeTime(time)
@@ -555,6 +587,13 @@ async function handleSendMessage() {
}
}
}
// 从用户信息卡片查看朋友圈
function handleViewMoments() {
if (!selectedUser.value) return
showUserCard.value = false
showUserMomentsModal.value = true
}
</script>
<style scoped>

View File

@@ -0,0 +1,256 @@
<template>
<Teleport to="body">
<div
v-if="show"
class="fixed inset-0 bg-black/80 z-[10000] flex items-center justify-center p-4 backdrop-blur-sm animate-fade-in"
@click.self="$emit('close')"
>
<div class="bg-[#1e1e24] rounded-2xl w-full max-w-2xl shadow-2xl border border-gray-700/50 overflow-hidden flex flex-col max-h-[85vh] relative font-sans select-none">
<!-- 头部 -->
<div class="px-6 py-4 flex justify-between items-center border-b border-white/5 bg-gradient-to-r from-primary/10 to-transparent shrink-0">
<div class="flex items-center gap-3">
<Avatar
v-if="user"
:name="user.name || ''"
:avatar="user.avatar"
size="sm"
rounded="full"
/>
<div>
<h2 class="text-lg font-bold text-white">{{ user?.name || '用户' }}的朋友圈</h2>
<p class="text-xs text-gray-400"> {{ totalCount }} 条动态</p>
</div>
</div>
<button
class="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-gray-400 hover:text-white transition flex items-center justify-center"
@click="$emit('close')"
>
<i class="fas fa-times"></i>
</button>
</div>
<!-- 加载中 -->
<div v-if="loading && moments.length === 0" class="flex-1 flex items-center justify-center min-h-[300px]">
<i class="fas fa-circle-notch fa-spin text-2xl text-primary"></i>
</div>
<!-- 空状态 -->
<div v-else-if="moments.length === 0" class="flex-1 flex flex-col items-center justify-center min-h-[300px] text-gray-500">
<div class="w-16 h-16 rounded-full bg-white/5 flex items-center justify-center mb-4">
<i class="fas fa-camera-retro text-2xl text-gray-600"></i>
</div>
<p class="text-sm">暂无朋友圈动态</p>
</div>
<!-- 动态列表 -->
<div v-else class="flex-1 overflow-y-auto custom-scrollbar" @scroll="handleScroll">
<div
v-for="moment in moments"
:key="moment.id"
class="border-b border-gray-800 p-5 hover:bg-white/[0.02] transition-colors"
>
<!-- 时间 -->
<div class="flex items-center justify-between mb-3">
<span class="text-gray-500 text-xs">{{ formatTime(moment.created_at) }}</span>
</div>
<!-- 文字内容 -->
<p v-if="moment.content" class="text-gray-200 text-sm whitespace-pre-wrap break-words leading-relaxed mb-3">
{{ moment.content }}
</p>
<!-- 媒体内容 -->
<div v-if="getMediaUrls(moment).length > 0">
<!-- 图片 -->
<div v-if="moment.media_type === 1" class="grid gap-2" :class="getGridClass(getMediaUrls(moment).length)">
<div
v-for="(url, index) in getMediaUrls(moment)"
:key="index"
class="relative aspect-square bg-gray-800 rounded-xl overflow-hidden cursor-pointer group"
@click="openImagePreview(getMediaUrls(moment), index)"
>
<img :src="url" class="w-full h-full object-cover transition-transform group-hover:scale-105" loading="lazy" />
</div>
</div>
<!-- 视频 -->
<div v-else-if="moment.media_type === 2" class="max-w-sm">
<VideoPlayer :src="getMediaUrls(moment)[0]" class="aspect-video rounded-xl" />
</div>
</div>
<!-- 位置信息 -->
<div v-if="moment.location" class="mt-3 flex items-center text-gray-500 text-xs">
<i class="fas fa-map-marker-alt mr-2 text-primary/60"></i>
{{ moment.location }}
</div>
<!-- 互动数据 -->
<div class="mt-3 flex items-center gap-4 text-xs text-gray-500">
<span class="flex items-center gap-1">
<i class="fas fa-heart" :class="moment.is_liked ? 'text-red-500' : ''"></i>
{{ moment.like_count || 0 }}
</span>
<span class="flex items-center gap-1">
<i class="fas fa-comment"></i>
{{ moment.comment_count || 0 }}
</span>
</div>
</div>
<!-- 加载更多 -->
<div v-if="loading && moments.length > 0" class="flex justify-center py-4">
<i class="fas fa-circle-notch fa-spin text-primary"></i>
</div>
<!-- 没有更多 -->
<div v-if="!hasMore && moments.length > 0" class="text-center py-6 text-gray-600 text-xs">
没有更多了
</div>
</div>
</div>
<!-- 图片预览 -->
<ImagePreview
:show="showImagePreview"
:images="previewImages"
:initial-index="previewIndex"
@close="showImagePreview = false"
/>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import Avatar from '@/components/common/Avatar.vue'
import ImagePreview from '@/components/common/ImagePreview.vue'
import VideoPlayer from '@/components/common/VideoPlayer.vue'
import * as momentApi from '@/api/modules/moment'
import { parseMediaUrls } from '@/types/moment'
import { formatRelativeTime } from '@/utils/format'
import type { Moment } from '@/types/moment'
import type { User } from '@/types/api'
interface Props {
show: boolean
user: User | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
close: []
}>()
// 状态
const moments = ref<Moment[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = 20
const totalCount = ref(0)
const hasMore = ref(true)
// 图片预览
const showImagePreview = ref(false)
const previewImages = ref<string[]>([])
const previewIndex = ref(0)
// 监听显示状态和用户变化
watch(() => [props.show, props.user], async ([show, user]) => {
if (show && user && user.id) {
await loadMoments(true)
} else {
// 关闭时重置状态
moments.value = []
page.value = 1
hasMore.value = true
totalCount.value = 0
}
}, { immediate: true })
// 加载用户朋友圈
async function loadMoments(refresh = false) {
if (!props.user?.id) return
if (loading.value) return
if (refresh) {
page.value = 1
hasMore.value = true
}
if (!hasMore.value) return
loading.value = true
try {
const response = await momentApi.getUserMoments(props.user.id, page.value, pageSize)
const newMoments = response.data || []
if (refresh) {
moments.value = newMoments
} else {
moments.value = [...moments.value, ...newMoments]
}
totalCount.value = response.total || moments.value.length
hasMore.value = moments.value.length < totalCount.value
if (!refresh && newMoments.length > 0) {
page.value++
}
} catch (e) {
console.error('Failed to load user moments:', e)
} finally {
loading.value = false
}
}
// 滚动加载更多
function handleScroll(e: Event) {
const target = e.target as HTMLElement
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight
if (scrollBottom < 100 && !loading.value && hasMore.value) {
page.value++
loadMoments()
}
}
// 格式化时间
function formatTime(time: string): string {
return formatRelativeTime(time)
}
// 获取媒体URL
function getMediaUrls(moment: Moment): string[] {
return parseMediaUrls(moment.media_urls)
}
// 获取图片网格样式
function getGridClass(count: number): string {
if (count === 1) return 'grid-cols-1 max-w-xs'
if (count === 2) return 'grid-cols-2 max-w-sm'
if (count <= 4) return 'grid-cols-2 max-w-sm'
return 'grid-cols-3 max-w-md'
}
// 打开图片预览
function openImagePreview(images: string[], index: number) {
previewImages.value = images
previewIndex.value = index
showImagePreview.value = true
}
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
@apply bg-gray-700 rounded-full;
}
.animate-fade-in {
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>

View File

@@ -208,6 +208,12 @@
</div>
</div>
<!-- 群通话进行中提示Banner -->
<GroupCallBanner
v-if="isGroupChat && chatStore.currentTarget?.room_id"
:room-id="chatStore.currentTarget.room_id"
/>
<!-- 消息列表 -->
<div class="flex-1 overflow-hidden relative flex flex-col min-h-0">
<MessageList
@@ -659,6 +665,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 GroupCallBanner from '@/components/chat/GroupCallBanner.vue'
import UserInfoCard from '@/components/common/UserInfoCard.vue'
import MomentPanel from '@/components/moment/MomentPanel.vue'
import * as groupApi from '@/api/modules/room'

View File

@@ -231,6 +231,66 @@
</button>
</div>
</div>
<!-- 朋友圈预览区 (仅好友显示) -->
<div v-if="!isGroupChat" class="bg-white/5 rounded-2xl border border-white/5 overflow-hidden mt-3">
<div class="flex items-center justify-between p-4 border-b border-white/5">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-white/5 flex items-center justify-center text-gray-500 shrink-0">
<i class="fas fa-images"></i>
</div>
<span class="text-sm text-gray-400 font-medium">朋友圈</span>
</div>
<button
@click="handleViewMoreMoments"
class="text-xs text-primary hover:text-primary-hover transition flex items-center gap-1"
>
查看更多 <i class="fas fa-chevron-right"></i>
</button>
</div>
<!-- 加载状态 -->
<div v-if="loadingMoments" class="p-6 flex justify-center">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
</div>
<!-- 朋友圈内容 -->
<div v-else-if="userMoments.length > 0" class="p-4">
<!-- 图片网格 -->
<div class="grid grid-cols-3 gap-2">
<div
v-for="(moment, index) in userMoments.slice(0, 3)"
:key="moment.id"
class="relative aspect-square rounded-xl overflow-hidden cursor-pointer group"
@click="openMomentPreview(index)"
>
<img
v-if="getMomentImage(moment)"
:src="getMomentImage(moment)"
class="w-full h-full object-cover transition-transform group-hover:scale-110"
loading="lazy"
/>
<div v-else class="w-full h-full bg-gray-800 flex items-center justify-center">
<i class="fas fa-file-alt text-gray-600 text-xl"></i>
</div>
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors"></div>
</div>
</div>
<!-- 最新动态文字预览 -->
<div v-if="userMoments[0]?.content" class="mt-3 text-xs text-gray-400 line-clamp-2">
{{ userMoments[0].content }}
</div>
</div>
<!-- 空状态 -->
<div v-else class="p-6 text-center">
<div class="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center mx-auto mb-3">
<i class="fas fa-camera-retro text-gray-600 text-lg"></i>
</div>
<p class="text-gray-500 text-xs">暂无朋友圈动态</p>
</div>
</div>
</div>
<!-- 底部快捷操作栏 -->
@@ -310,6 +370,21 @@
<ConfirmModal v-if="!isGroupChat" :show="showDeleteConfirm" title="删除好友" message="确定要彻底删除该好友吗?此操作不可恢复。" type="danger" confirm-text="确认删除" @confirm="handleDeleteContact" @cancel="showDeleteConfirm = false" />
<ConfirmModal :show="showRoomIdErrorModal" title="无法通话" message="无法获取通话线路 (RoomID missing)。" type="warning" :cancel-text="''" @confirm="showRoomIdErrorModal = false" />
<!-- 朋友圈图片预览 -->
<ImagePreview
:show="showMomentPreview"
:images="momentPreviewImages"
:initial-index="momentPreviewIndex"
@close="showMomentPreview = false"
/>
<!-- 用户朋友圈模态框 -->
<UserMomentsModal
:show="showUserMomentsModal"
:user="contact?.user || null"
@close="showUserMomentsModal = false"
/>
</div>
</template>
@@ -321,9 +396,14 @@ import { useToastStore } from '@/stores/toast'
import { useWebRTCStore } from '@/stores/webrtc'
import * as contactApi from '@/api/modules/contact'
import * as groupApi from '@/api/modules/room'
import * as momentApi from '@/api/modules/moment'
import Avatar from '@/components/common/Avatar.vue'
import ConfirmModal from '@/components/common/ConfirmModal.vue'
import ImagePreview from '@/components/common/ImagePreview.vue'
import UserMomentsModal from '@/components/moment/UserMomentsModal.vue'
import { parseMediaUrls } from '@/types/moment'
import type { Contact, ContactGroup, GroupInfo, GroupMember } from '@/types/api'
import type { Moment } from '@/types/moment'
const emit = defineEmits<{ switchToChat: [] }>()
const chatStore = useChatStore()
@@ -346,6 +426,14 @@ const groupInfo = ref<GroupInfo | null>(null)
const groupMembers = ref<GroupMember[]>([])
const groupAnnouncement = ref<string>('')
// 朋友圈相关状态
const userMoments = ref<Moment[]>([])
const loadingMoments = ref(false)
const showUserMomentsModal = ref(false)
const showMomentPreview = ref(false)
const momentPreviewImages = ref<string[]>([])
const momentPreviewIndex = ref(0)
const currentGroup = computed(() => groups.value.find(g => g.id === contact.value?.group_id))
const allGroups = computed(() => [{id: 0, group_name: '未分组'}, ...groups.value])
const isGroupChat = computed(() => contact.value?.is_group || contact.value?.room_type === 'group')
@@ -357,6 +445,7 @@ watch(() => contactStore.selectedContact, async (newVal) => {
groupInfo.value = null
groupMembers.value = []
groupAnnouncement.value = ''
userMoments.value = []
return
}
@@ -370,6 +459,7 @@ watch(() => contactStore.selectedContact, async (newVal) => {
// 3. 后台静默加载完整详情
loading.value = true
userMoments.value = [] // 重置朋友圈
try {
if (isGroup) {
// 群聊:获取群信息、群成员和群公告
@@ -382,7 +472,8 @@ watch(() => contactStore.selectedContact, async (newVal) => {
groupMembers.value = members
groupAnnouncement.value = announcementRes.announcement || ''
} else {
// 好友:获取好友详情分组列表
// 好友:获取好友详情分组列表和朋友圈
const userId = newVal.contact_user_id || newVal.user_id || newVal.id
const [detail, groupList] = await Promise.all([
contactApi.getContactDetail(newVal.id),
contactApi.getGroups() // 同时刷新分组列表
@@ -396,6 +487,9 @@ watch(() => contactStore.selectedContact, async (newVal) => {
if (!contact.value.room_id && newVal.room_id) {
contact.value.room_id = newVal.room_id
}
// 加载朋友圈(静默加载,不阻塞主流程)
loadUserMoments(userId)
}
} catch (e: any) {
console.error('Fetch detail failed, using basic info', e)
@@ -404,6 +498,51 @@ watch(() => contactStore.selectedContact, async (newVal) => {
}
}, { immediate: true })
// 加载用户朋友圈
async function loadUserMoments(userId: string) {
loadingMoments.value = true
try {
const response = await momentApi.getUserMoments(userId, 1, 3)
userMoments.value = response.data || []
} catch (e) {
console.error('Failed to load user moments:', e)
userMoments.value = []
} finally {
loadingMoments.value = false
}
}
// 获取动态的第一张图片
function getMomentImage(moment: Moment): string | null {
if (moment.media_type === 1 && moment.media_urls) {
const urls = parseMediaUrls(moment.media_urls)
return urls[0] || null
}
return null
}
// 打开朋友圈图片预览
function openMomentPreview(index: number) {
const images: string[] = []
userMoments.value.slice(0, 3).forEach(moment => {
if (moment.media_type === 1 && moment.media_urls) {
const urls = parseMediaUrls(moment.media_urls)
images.push(...urls)
}
})
if (images.length > 0) {
momentPreviewImages.value = images
momentPreviewIndex.value = Math.min(index, images.length - 1)
showMomentPreview.value = true
}
}
// 查看更多朋友圈
function handleViewMoreMoments() {
showUserMomentsModal.value = true
}
// Actions
function copyText(text?: string) {
if (!text) return
@@ -514,4 +653,10 @@ async function handleDeleteContact() {
@keyframes scaleIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -130,6 +130,8 @@