bug修复
This commit is contained in:
@@ -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[] = []
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
256
src/components/moment/UserMomentsModal.vue
Normal file
256
src/components/moment/UserMomentsModal.vue
Normal 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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -130,6 +130,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user