部分联系人信息和会话模块

This commit is contained in:
2025-12-04 09:46:29 +08:00
parent 07a219a183
commit 47fac373b7
6 changed files with 104 additions and 25 deletions

View File

@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { storage } from '@/utils/storage'
import type { FriendRequest, ContactGroup, Contact } from '@/types/api'
export type LeftPanelMode = 'default' | 'friend-manager' | 'friend-notify' | 'group-notify'
@@ -7,9 +8,9 @@ export type LeftPanelMode = 'default' | 'friend-manager' | 'friend-notify' | 'gr
export const useContactStore = defineStore('contact', () => {
const friendRequests = ref<FriendRequest[]>([])
const groups = ref<ContactGroup[]>([])
const leftPanelMode = ref<LeftPanelMode>('default')
const leftPanelMode = ref<LeftPanelMode>(storage.getContactLeftPanelMode() as LeftPanelMode)
const selectedContact = ref<Contact | null>(null)
const contactListTab = ref<'friends' | 'groups'>('friends')
const contactListTab = ref<'friends' | 'groups'>(storage.getContactListTab())
/**
* 设置好友申请列表
@@ -74,6 +75,7 @@ export const useContactStore = defineStore('contact', () => {
*/
function setLeftPanelMode(mode: LeftPanelMode) {
leftPanelMode.value = mode
storage.setContactLeftPanelMode(mode)
// 切换到管理/通知模式时,清除选中联系人
if (mode !== 'default') {
selectedContact.value = null
@@ -91,6 +93,11 @@ export const useContactStore = defineStore('contact', () => {
}
}
// 监听 contactListTab 变化并保存
watch(contactListTab, (newTab) => {
storage.setContactListTab(newTab)
})
return {
friendRequests,
groups,

View File

@@ -6,6 +6,10 @@ const TOKEN_KEY = 'token'
const USER_ID_KEY = 'user_id'
const USER_INFO_KEY = 'user_info'
const REMEMBER_KEY = 'remember'
const CURRENT_TAB_KEY = 'current_tab'
const CONTACT_LEFT_PANEL_MODE_KEY = 'contact_left_panel_mode'
const CONTACT_LIST_TAB_KEY = 'contact_list_tab'
const SELECTED_CONVERSATION_KEY = 'selected_conversation'
export const storage = {
// Token
@@ -57,12 +61,55 @@ export const storage = {
return localStorage.getItem(REMEMBER_KEY) === 'true'
},
// Current Tab (chat/contact)
setCurrentTab(tab: 'chat' | 'contact') {
localStorage.setItem(CURRENT_TAB_KEY, tab)
},
getCurrentTab(): 'chat' | 'contact' {
const tab = localStorage.getItem(CURRENT_TAB_KEY)
return (tab === 'chat' || tab === 'contact') ? tab : 'chat'
},
// Contact Left Panel Mode
setContactLeftPanelMode(mode: string) {
localStorage.setItem(CONTACT_LEFT_PANEL_MODE_KEY, mode)
},
getContactLeftPanelMode(): string {
return localStorage.getItem(CONTACT_LEFT_PANEL_MODE_KEY) || 'default'
},
// Contact List Tab
setContactListTab(tab: 'friends' | 'groups') {
localStorage.setItem(CONTACT_LIST_TAB_KEY, tab)
},
getContactListTab(): 'friends' | 'groups' {
const tab = localStorage.getItem(CONTACT_LIST_TAB_KEY)
return (tab === 'friends' || tab === 'groups') ? tab : 'friends'
},
// Selected Conversation
setSelectedConversation(targetId: string | null) {
if (targetId) {
localStorage.setItem(SELECTED_CONVERSATION_KEY, targetId)
} else {
localStorage.removeItem(SELECTED_CONVERSATION_KEY)
}
},
getSelectedConversation(): string | null {
return localStorage.getItem(SELECTED_CONVERSATION_KEY)
},
// Clear all
clear() {
this.removeToken()
this.removeUserId()
this.removeUserInfo()
this.setRemember(false)
// 不清除 UI 状态,让用户刷新后保持界面状态
},
}

View File

@@ -264,6 +264,7 @@ import * as messageApi from '@/api/modules/message'
import * as contactApi from '@/api/modules/contact'
import * as attachmentApi from '@/api/modules/attachment'
import { formatTime } from '@/utils/format'
import { storage } from '@/utils/storage'
import type { Contact, ChatMessage } from '@/types/api'
import type { Conversation } from '@/types/conversation'
import { useWebRTC } from '@/composables/useWebRTC'
@@ -296,8 +297,8 @@ function handleIncomingCall(senderUserId: string) {
}
const webrtc = useWebRTC(authStore.user?.id || '', handleIncomingCall)
// State
const currentTab = ref<'chat' | 'contact'>('chat')
// State - 从本地存储恢复
const currentTab = ref<'chat' | 'contact'>(storage.getCurrentTab())
const searchQuery = ref('')
const inputText = ref('')
const chatVisible = ref(false)
@@ -364,6 +365,8 @@ function scrollToBottom(smooth = true) {
}
async function selectChat(contact: Contact) {
// 保存选中的会话
storage.setSelectedConversation(contact.user_id || contact.id)
chatStore.setCurrentTarget(contact)
chatVisible.value = true
unreadCount.value = 0 // 切换聊天时重置
@@ -496,6 +499,11 @@ async function handleRecordStop(blob: Blob, duration: number) { isRecording.valu
function handleRecordCancel() { isRecording.value = false }
async function loadContacts() { try { const contacts = await contactApi.getContacts(); chatStore.setContacts(contacts) } catch (e) { console.error(e) } }
// 监听 currentTab 变化并保存
watch(currentTab, (newTab) => {
storage.setCurrentTab(newTab)
})
onMounted(async () => {
if (!authStore.isAuthenticated) {
const isValid = await authStore.checkAuth()
@@ -509,6 +517,16 @@ onMounted(async () => {
}
await loadContacts()
await conversationStore.loadConversations()
// 恢复选中的会话
const savedTargetId = storage.getSelectedConversation()
if (savedTargetId) {
const contact = chatStore.contacts.find(c => c.user_id === savedTargetId || c.id === savedTargetId)
if (contact) {
await selectChat(contact)
}
}
window.addEventListener('resize', () => isMobile.value = window.innerWidth < 768)
})
onUnmounted(() => {

View File

@@ -274,14 +274,16 @@ function handleSendMessage() {
function handleAudioCall() {
if (contact.value) {
const receiverUserId = contact.value.user_id || contact.value.id
const receiverUserId = contact.value.user_id || contact.value.id || contact.value.contact_user_id
// 直接发起通话,不跳转到会话模块
webrtc.startCall('audio', receiverUserId)
}
}
function handleVideoCall() {
if (contact.value) {
const receiverUserId = contact.value.user_id || contact.value.id
const receiverUserId = contact.value.user_id || contact.value.id || contact.value.contact_user_id
// 直接发起通话,不跳转到会话模块
webrtc.startCall('video', receiverUserId)
}
}

View File

@@ -288,26 +288,16 @@ function handleSendMessage() {
function handleAudioCall() {
if (contact.value) {
const receiverUserId = contact.value.user_id || contact.value.id
chatStore.setCurrentTarget(contact.value)
// 切换到 Chat Tab 后再发起通话
emit('switchToChat')
// 延迟一下确保 Tab 切换完成
setTimeout(() => {
webrtc.startCall('audio', receiverUserId)
}, 100)
// 直接发起通话,不跳转到会话模块
webrtc.startCall('audio', receiverUserId)
}
}
function handleVideoCall() {
if (contact.value) {
const receiverUserId = contact.value.user_id || contact.value.id
chatStore.setCurrentTarget(contact.value)
// 切换到 Chat Tab 后再发起通话
emit('switchToChat')
// 延迟一下确保 Tab 切换完成
setTimeout(() => {
webrtc.startCall('video', receiverUserId)
}, 100)
// 直接发起通话,不跳转到会话模块
webrtc.startCall('video', receiverUserId)
}
}

View File

@@ -14,9 +14,15 @@
<div
v-for="request in friendRequests"
:key="request.id"
class="flex items-center p-4 bg-input/50 rounded-2xl border border-primary/20 hover:border-primary/50 transition relative overflow-hidden"
class="flex items-center p-4 bg-input/50 rounded-2xl border transition relative overflow-hidden"
:class="request.status === 'rejected'
? 'border-danger/20 hover:border-danger/50'
: 'border-primary/20 hover:border-primary/50'"
>
<div class="absolute left-0 top-0 bottom-0 w-1 bg-primary"></div>
<div
class="absolute left-0 top-0 bottom-0 w-1"
:class="request.status === 'rejected' ? 'bg-danger' : 'bg-primary'"
></div>
<Avatar
:name="request.from_user?.name"
:avatar="request.from_user?.avatar"
@@ -28,13 +34,18 @@
<div class="font-bold text-white">{{ request.from_user?.name }}</div>
<div class="text-xs text-gray-400 mt-1 flex items-center gap-1">
<i class="fas fa-quote-left text-[10px] opacity-50"></i>
{{ request.message || '请求添加你为好友' }}
<span v-if="request.status === 'rejected'">
对方拒绝了你的好友申请
</span>
<span v-else>
{{ request.message || '请求添加你为好友' }}
</span>
</div>
<div class="text-xs text-gray-500 mt-1">
{{ formatTime(new Date(request.created_at).getTime()) }}
</div>
</div>
<div class="flex gap-2">
<div v-if="request.status === 'pending'" class="flex gap-2">
<button
class="w-9 h-9 bg-success/20 hover:bg-success text-success hover:text-white rounded-lg transition flex items-center justify-center"
@click="handleAcceptRequest(request.id)"
@@ -50,6 +61,10 @@
<i class="fas fa-times"></i>
</button>
</div>
<div v-else class="flex items-center gap-2 text-danger text-sm">
<i class="fas fa-times-circle"></i>
<span>已拒绝</span>
</div>
</div>
</div>
</div>