diff --git a/src/api/modules/search.ts b/src/api/modules/search.ts new file mode 100644 index 0000000..fcec7ab --- /dev/null +++ b/src/api/modules/search.ts @@ -0,0 +1,58 @@ +import request from '../request' +import type { User } from '@/types/api' + +/** + * 搜索相关API + */ + +// 联系人搜索结果 +export interface ContactSearchResult { + user: User + remark_name: string + room_id: string +} + +// 群聊搜索结果 +export interface GroupSearchResult { + room_id: string + room_name: string + room_avatar: string + owner_id: string + member_count: number +} + +// 消息搜索结果 +export interface MessageSearchResult { + id: number + room_id: string + room_name: string + content: string + message_type: number + created_at: string + sender: User + is_group_chat: boolean + match_content: string +} + +// 聚合搜索结果 +export interface GlobalSearchResult { + contacts: ContactSearchResult[] + groups: GroupSearchResult[] + messages: MessageSearchResult[] +} + +// 搜索类型 +export type SearchType = 'all' | 'contacts' | 'groups' | 'messages' + +/** + * 聚合搜索 + * @param keyword 搜索关键词 + * @param type 搜索类型:all(默认)、contacts、groups、messages + * @param limit 每类结果的最大数量,默认20 + */ +export function globalSearch(keyword: string, type: SearchType = 'all', limit: number = 20) { + return request.get('/search', { + params: { keyword, type, limit } + }) +} + diff --git a/src/components/common/SearchResults.vue b/src/components/common/SearchResults.vue new file mode 100644 index 0000000..7e31ea9 --- /dev/null +++ b/src/components/common/SearchResults.vue @@ -0,0 +1,220 @@ + + + + + + diff --git a/src/composables/useWebRTC.ts b/src/composables/useWebRTC.ts index 521f597..41814b0 100644 --- a/src/composables/useWebRTC.ts +++ b/src/composables/useWebRTC.ts @@ -171,20 +171,23 @@ export function useWebRTC( const roomId = getSafeRoomId(currentReceiverUserId) if (!roomId) return + // 通话类型说明 + const callTypeText = call.type === 'video' ? '[视频通话]' : '[语音通话]' + // 根据原因生成系统消息内容 let content = '' switch (reason) { case 'connected': - content = `通话时长 ${formatDuration(call.duration)}` + content = `${callTypeText} 通话时长 ${formatDuration(call.duration)}` break case 'cancelled': - content = '已取消' + content = `${callTypeText} 已取消` break case 'rejected': - content = '对方未接听' + content = `${callTypeText} 对方未接听` break case 'busy': - content = '对方忙' + content = `${callTypeText} 对方忙` break } @@ -402,17 +405,15 @@ export function useWebRTC( function endCall() { stopRingtone() - // 主动挂断时发送通话结束系统消息 + // 只有发起方发送通话结束系统消息,避免重复 if (isCaller.value) { if (call.status === 'outgoing') { sendSummaryMessage('cancelled') // 呼出状态主动取消 } else if (call.status === 'connected') { sendSummaryMessage('connected') // 通话中主动挂断 } - } else if (call.status === 'connected') { - // 被叫方在通话中挂断也发送消息 - sendSummaryMessage('connected') } + // 被叫方不发送消息,由发起方统一发送 sendSignal('hangup') closeCall() } diff --git a/src/views/chat/ChatView.vue b/src/views/chat/ChatView.vue index 885c7dc..a9b75d9 100644 --- a/src/views/chat/ChatView.vue +++ b/src/views/chat/ChatView.vue @@ -111,12 +111,26 @@
- + + +
@@ -712,8 +726,11 @@ 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 SearchResults from '@/components/common/SearchResults.vue' import * as groupApi from '@/api/modules/room' import * as systemApi from '@/api/modules/system' +import * as searchApi from '@/api/modules/search' +import type { GlobalSearchResult, ContactSearchResult, GroupSearchResult, MessageSearchResult } from '@/api/modules/search' const router = useRouter() const authStore = useAuthStore() @@ -737,6 +754,12 @@ let connectionCheckInterval: number | null = null const currentTab = ref<'chat' | 'contact' | 'moment'>(storage.getCurrentTab() as 'chat' | 'contact' | 'moment') const searchQuery = ref('') const inputText = ref('') + +// 聚合搜索状态 +const showSearchResults = ref(false) +const searchLoading = ref(false) +const searchResult = ref({ contacts: [], groups: [], messages: [] }) +let searchDebounceTimer: ReturnType | null = null const chatVisible = ref(false) const isMobile = ref(window.innerWidth < 768) const showProfileModal = ref(false) @@ -1926,6 +1949,114 @@ async function handleGroupMemberRemoved() { await conversationStore.loadConversations() } +// ========== 聚合搜索相关函数 ========== + +// 搜索输入处理(带防抖) +function handleSearchInput() { + if (searchDebounceTimer) { + clearTimeout(searchDebounceTimer) + } + + const query = searchQuery.value.trim() + if (!query) { + showSearchResults.value = false + searchResult.value = { contacts: [], groups: [], messages: [] } + return + } + + showSearchResults.value = true + searchLoading.value = true + + searchDebounceTimer = setTimeout(async () => { + try { + const result = await searchApi.globalSearch(query, 'all', 20) + searchResult.value = result + } catch (error) { + console.error('搜索失败:', error) + searchResult.value = { contacts: [], groups: [], messages: [] } + } finally { + searchLoading.value = false + } + }, 300) +} + +// 搜索框获得焦点 +function handleSearchFocus() { + if (searchQuery.value.trim()) { + showSearchResults.value = true + } +} + +// 关闭搜索结果 +function closeSearchResults() { + showSearchResults.value = false +} + +// 选择搜索结果中的联系人 +function handleSearchContactSelect(contact: ContactSearchResult) { + closeSearchResults() + searchQuery.value = '' + + // 构造 Contact 对象并选中 + const targetContact: Contact = { + id: contact.user.id, + user_id: contact.user.id, + contact_user_id: contact.user.id, + room_id: contact.room_id, + room_type: 'p2p', + is_group: false, + remark_name: contact.remark_name, + is_top: false, + is_muted: false, + user: contact.user + } + chatStore.setCurrentTarget(targetContact) +} + +// 选择搜索结果中的群聊 +function handleSearchGroupSelect(group: GroupSearchResult) { + closeSearchResults() + searchQuery.value = '' + + // 构造群聊 Contact 对象并选中 + const targetGroup: Contact = { + id: group.room_id, + user_id: group.room_id, + contact_user_id: group.room_id, + room_id: group.room_id, + room_type: 'group', + is_group: true, + remark_name: group.room_name, + member_count: group.member_count, + owner_id: group.owner_id, + is_top: false, + is_muted: false + } + chatStore.setCurrentTarget(targetGroup) +} + +// 选择搜索结果中的消息 +function handleSearchMessageSelect(message: MessageSearchResult) { + closeSearchResults() + searchQuery.value = '' + + // 根据消息的 room_id 跳转到对应会话 + const isGroup = message.is_group_chat + const targetContact: Contact = { + id: message.room_id, + user_id: isGroup ? message.room_id : (message.sender?.id || message.room_id), + contact_user_id: isGroup ? message.room_id : (message.sender?.id || message.room_id), + room_id: message.room_id, + room_type: isGroup ? 'group' : 'p2p', + is_group: isGroup, + remark_name: message.room_name || message.sender?.name || '', + is_top: false, + is_muted: false, + user: message.sender + } + chatStore.setCurrentTarget(targetContact) +} + // 刷新会话列表 async function refreshConversations() { if (refreshingConversations.value) return diff --git a/src/views/contact/ContactCircle.vue b/src/views/contact/ContactCircle.vue index a38e504..2026fef 100644 --- a/src/views/contact/ContactCircle.vue +++ b/src/views/contact/ContactCircle.vue @@ -4,12 +4,26 @@
- + + +
@@ -470,7 +484,10 @@ import ContextMenu from '@/components/common/ContextMenu.vue' import ConfirmModal from '@/components/common/ConfirmModal.vue' import SelectContactsModal from '@/components/chat/SelectContactsModal.vue' import GroupChatCard from '@/components/contact/GroupChatCard.vue' +import SearchResults from '@/components/common/SearchResults.vue' +import * as searchApi from '@/api/modules/search' import type { Contact, ContactGroup } from '@/types/api' +import type { GlobalSearchResult, ContactSearchResult, GroupSearchResult, MessageSearchResult } from '@/api/modules/search' const router = useRouter() const contactStore = useContactStore() @@ -487,6 +504,12 @@ const showActionMenu = ref(false) const showCreateGroupModal = ref(false) const showJoinGroupModal = ref(false) +// 聚合搜索状态 +const showSearchResults = ref(false) +const searchLoading = ref(false) +const searchResult = ref({ contacts: [], groups: [], messages: [] }) +let searchDebounceTimer: ReturnType | null = null + // 群聊列表状态 const groupChats = ref { }) }) +// --- 聚合搜索相关函数 --- + +function handleSearchInput() { + if (searchDebounceTimer) { + clearTimeout(searchDebounceTimer) + } + + const query = searchKeyword.value.trim() + if (!query) { + showSearchResults.value = false + searchResult.value = { contacts: [], groups: [], messages: [] } + return + } + + showSearchResults.value = true + searchLoading.value = true + + searchDebounceTimer = setTimeout(async () => { + try { + const result = await searchApi.globalSearch(query, 'all', 20) + searchResult.value = result + } catch (error) { + console.error('搜索失败:', error) + searchResult.value = { contacts: [], groups: [], messages: [] } + } finally { + searchLoading.value = false + } + }, 300) +} + +function handleSearchFocus() { + if (searchKeyword.value.trim()) { + showSearchResults.value = true + } +} + +function closeSearchResults() { + showSearchResults.value = false +} + +function handleSearchContactSelect(contact: ContactSearchResult) { + closeSearchResults() + searchKeyword.value = '' + + const targetContact: Contact = { + id: contact.user.id, + user_id: contact.user.id, + contact_user_id: contact.user.id, + room_id: contact.room_id, + room_type: 'p2p', + is_group: false, + remark_name: contact.remark_name, + is_top: false, + is_muted: false, + user: contact.user + } + chatStore.setCurrentTarget(targetContact) + router.push('/chat') +} + +function handleSearchGroupSelect(group: GroupSearchResult) { + closeSearchResults() + searchKeyword.value = '' + + const targetGroup: Contact = { + id: group.room_id, + user_id: group.room_id, + contact_user_id: group.room_id, + room_id: group.room_id, + room_type: 'group', + is_group: true, + remark_name: group.room_name, + member_count: group.member_count, + owner_id: group.owner_id, + is_top: false, + is_muted: false + } + chatStore.setCurrentTarget(targetGroup) + router.push('/chat') +} + +function handleSearchMessageSelect(message: MessageSearchResult) { + closeSearchResults() + searchKeyword.value = '' + + const isGroup = message.is_group_chat + const targetContact: Contact = { + id: message.room_id, + user_id: isGroup ? message.room_id : (message.sender?.id || message.room_id), + contact_user_id: isGroup ? message.room_id : (message.sender?.id || message.room_id), + room_id: message.room_id, + room_type: isGroup ? 'group' : 'p2p', + is_group: isGroup, + remark_name: message.room_name || message.sender?.name || '', + is_top: false, + is_muted: false, + user: message.sender + } + chatStore.setCurrentTarget(targetContact) + router.push('/chat') +} + // --- API & Actions --- async function loadGroups() {