diff --git a/src/components/chat/GroupChatPanel.vue b/src/components/chat/GroupChatPanel.vue index f599372..db89e26 100644 --- a/src/components/chat/GroupChatPanel.vue +++ b/src/components/chat/GroupChatPanel.vue @@ -300,6 +300,24 @@ + + + + + + @@ -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([]) +// 用户信息卡片状态 +const showUserInfoCard = ref(false) +const selectedMemberUser = ref(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[] = [] diff --git a/src/components/chat/GroupInfoPanel.vue b/src/components/chat/GroupInfoPanel.vue index ec0cc8f..bd9ab9e 100644 --- a/src/components/chat/GroupInfoPanel.vue +++ b/src/components/chat/GroupInfoPanel.vue @@ -331,6 +331,24 @@ + + + + + + @@ -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(null) +const showUserMomentsModal = ref(false) const groupInfo = ref(null) const members = ref([]) const availableContacts = ref([]) @@ -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) { diff --git a/src/components/moment/MomentPanel.vue b/src/components/moment/MomentPanel.vue index 6d83ad7..02fc44d 100644 --- a/src/components/moment/MomentPanel.vue +++ b/src/components/moment/MomentPanel.vue @@ -4,6 +4,15 @@

朋友圈

+ +
@@ -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()) +const refreshing = ref(false) // 用户信息卡片相关 const showUserCard = ref(false) const selectedUser = ref(null) +const showUserMomentsModal = ref(false) // 评论相关 const commentingMoment = ref(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 +} diff --git a/src/views/chat/ChatView.vue b/src/views/chat/ChatView.vue index caa4dec..087d2a0 100644 --- a/src/views/chat/ChatView.vue +++ b/src/views/chat/ChatView.vue @@ -208,6 +208,12 @@
+ + +
+ + +
+
+
+
+ +
+ 朋友圈 +
+ +
+ + +
+
+
+ + +
+ +
+
+ +
+ +
+
+
+
+ + +
+ {{ userMoments[0].content }} +
+
+ + +
+
+ +
+

暂无朋友圈动态

+
+
@@ -310,6 +370,21 @@ + + + + + + @@ -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(null) const groupMembers = ref([]) const groupAnnouncement = ref('') +// 朋友圈相关状态 +const userMoments = ref([]) +const loadingMoments = ref(false) +const showUserMomentsModal = ref(false) +const showMomentPreview = ref(false) +const momentPreviewImages = ref([]) +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; +} diff --git a/src/views/contact/README.md b/src/views/contact/README.md index 81080e7..ecd0265 100644 --- a/src/views/contact/README.md +++ b/src/views/contact/README.md @@ -130,6 +130,8 @@ + +