diff --git a/src/components/AudioMessage.vue b/src/components/AudioMessage.vue index d9b6a95..168fc73 100644 --- a/src/components/AudioMessage.vue +++ b/src/components/AudioMessage.vue @@ -1,95 +1,165 @@ + + diff --git a/src/components/ChatArea.vue b/src/components/ChatArea.vue index 90e4cae..ff2fed5 100644 --- a/src/components/ChatArea.vue +++ b/src/components/ChatArea.vue @@ -1,21 +1,18 @@ diff --git a/src/components/ChatHeader.vue b/src/components/ChatHeader.vue index 958fa1a..31f16c5 100644 --- a/src/components/ChatHeader.vue +++ b/src/components/ChatHeader.vue @@ -1,27 +1,27 @@ + + diff --git a/src/components/CustomModal.vue b/src/components/CustomModal.vue new file mode 100644 index 0000000..8a47659 --- /dev/null +++ b/src/components/CustomModal.vue @@ -0,0 +1,335 @@ + + + + + diff --git a/src/components/CustomTextarea.vue b/src/components/CustomTextarea.vue new file mode 100644 index 0000000..7b95a2f --- /dev/null +++ b/src/components/CustomTextarea.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/src/components/EmojiPicker.vue b/src/components/EmojiPicker.vue index b595e56..cfb8ce0 100644 --- a/src/components/EmojiPicker.vue +++ b/src/components/EmojiPicker.vue @@ -1,16 +1,16 @@ diff --git a/src/components/EmptyState.vue b/src/components/EmptyState.vue new file mode 100644 index 0000000..32068f0 --- /dev/null +++ b/src/components/EmptyState.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/FileUploadPreview.vue b/src/components/FileUploadPreview.vue index a504040..ee80378 100644 --- a/src/components/FileUploadPreview.vue +++ b/src/components/FileUploadPreview.vue @@ -1,43 +1,42 @@ diff --git a/src/components/FriendList.vue b/src/components/FriendList.vue index 5d13261..fe02e84 100644 --- a/src/components/FriendList.vue +++ b/src/components/FriendList.vue @@ -1,109 +1,339 @@ + + diff --git a/src/components/FriendsManagement.vue b/src/components/FriendsManagement.vue new file mode 100644 index 0000000..ea64931 --- /dev/null +++ b/src/components/FriendsManagement.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/components/GroupsManagement.vue b/src/components/GroupsManagement.vue new file mode 100644 index 0000000..853caac --- /dev/null +++ b/src/components/GroupsManagement.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/components/MediaPreview.vue b/src/components/MediaPreview.vue index e1b973f..1062eaf 100644 --- a/src/components/MediaPreview.vue +++ b/src/components/MediaPreview.vue @@ -1,91 +1,626 @@ \ No newline at end of file + +onMounted(() => { + document.addEventListener('keydown', handleKeydown); +}); + +onUnmounted(() => { + document.removeEventListener('keydown', handleKeydown); + clearTimeout(controlsTimeout.value); +}); + + + diff --git a/src/components/MessageBubble.vue b/src/components/MessageBubble.vue new file mode 100644 index 0000000..106800b --- /dev/null +++ b/src/components/MessageBubble.vue @@ -0,0 +1,400 @@ + + + + + diff --git a/src/components/MessageInput.vue b/src/components/MessageInput.vue index 25e2d1a..5e2be85 100644 --- a/src/components/MessageInput.vue +++ b/src/components/MessageInput.vue @@ -1,221 +1,529 @@ + + diff --git a/src/components/MessageList.vue b/src/components/MessageList.vue index 28cff89..9296b5f 100644 --- a/src/components/MessageList.vue +++ b/src/components/MessageList.vue @@ -1,36 +1,39 @@ + \ No newline at end of file + diff --git a/src/components/MomentsView.vue b/src/components/MomentsView.vue new file mode 100644 index 0000000..7019b65 --- /dev/null +++ b/src/components/MomentsView.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/components/RecordingIndicator.vue b/src/components/RecordingIndicator.vue index 2ee6299..77cb67f 100644 --- a/src/components/RecordingIndicator.vue +++ b/src/components/RecordingIndicator.vue @@ -1,12 +1,17 @@ diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue new file mode 100644 index 0000000..cf798c1 --- /dev/null +++ b/src/components/SettingsModal.vue @@ -0,0 +1,518 @@ + + + + + diff --git a/src/components/SideNavigation.vue b/src/components/SideNavigation.vue new file mode 100644 index 0000000..a10cb9a --- /dev/null +++ b/src/components/SideNavigation.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/src/components/UserProfileModal.vue b/src/components/UserProfileModal.vue new file mode 100644 index 0000000..8798965 --- /dev/null +++ b/src/components/UserProfileModal.vue @@ -0,0 +1,400 @@ + + + + + diff --git a/src/components/VideoCallComponent.vue b/src/components/VideoCallComponent.vue new file mode 100644 index 0000000..a0000fe --- /dev/null +++ b/src/components/VideoCallComponent.vue @@ -0,0 +1,577 @@ + + + + + diff --git a/src/components/VirtualList.vue b/src/components/VirtualList.vue new file mode 100644 index 0000000..8c21610 --- /dev/null +++ b/src/components/VirtualList.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/src/composables/useFileUpload.js b/src/composables/useFileUpload.js index 66bc93c..4146aaf 100644 --- a/src/composables/useFileUpload.js +++ b/src/composables/useFileUpload.js @@ -1,4 +1,4 @@ -import { ref } from "vue" +import { ref, nextTick } from "vue" import { message } from "ant-design-vue" export function useFileUpload() { @@ -10,37 +10,35 @@ export function useFileUpload() { const triggerFileInput = (type) => { currentFileType.value = type - if (type === "image" && imageInput.value) { - imageInput.value.value = "" - imageInput.value.click() - } else if (type === "video" && videoInput.value) { - videoInput.value.value = "" - videoInput.value.click() - } + nextTick(() => { + if (type === "image" && imageInput.value) { + imageInput.value.value = "" + imageInput.value.click() + } else if (type === "video" && videoInput.value) { + videoInput.value.value = "" + videoInput.value.click() + } + }) } const handleFileUpload = (event) => { - if (event.target.files.length === 0) return + if (!event.target.files || event.target.files.length === 0) return const file = event.target.files[0] // 确定文件类型 - let detectedType = "" + let detectedType = currentFileType.value if (event.target === imageInput.value) { detectedType = "image" } else if (event.target === videoInput.value) { detectedType = "video" } - if (detectedType) { - currentFileType.value = detectedType - } - // 文件大小检查 - const maxSize = currentFileType.value === "image" ? 10 * 1024 * 1024 : 50 * 1024 * 1024 + const maxSize = detectedType === "image" ? 10 * 1024 * 1024 : 50 * 1024 * 1024 if (file.size > maxSize) { const maxSizeMB = maxSize / 1024 / 1024 - message.error(`文件大小超过限制!${currentFileType.value === "image" ? "图片" : "视频"}最大${maxSizeMB}MB`) + message.error(`文件大小超过限制!${detectedType === "image" ? "图片" : "视频"}最大${maxSizeMB}MB`) event.target.value = "" return } @@ -49,13 +47,13 @@ export function useFileUpload() { const validImageTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/bmp"] const validVideoTypes = ["video/mp4", "video/avi", "video/mov", "video/wmv", "video/flv", "video/webm", "video/mkv"] - if (currentFileType.value === "image" && !validImageTypes.includes(file.type)) { + if (detectedType === "image" && !validImageTypes.includes(file.type)) { message.error("请选择有效的图片格式 (JPEG, PNG, GIF, WebP, BMP)") event.target.value = "" return } - if (currentFileType.value === "video" && !validVideoTypes.includes(file.type)) { + if (detectedType === "video" && !validVideoTypes.includes(file.type)) { message.error("请选择有效的视频格式 (MP4, AVI, MOV, WMV, FLV, WebM, MKV)") event.target.value = "" return @@ -64,7 +62,7 @@ export function useFileUpload() { const reader = new FileReader() reader.onload = (e) => { uploadPreview.value = { - type: currentFileType.value, + type: detectedType, url: e.target.result, name: file.name, size: file.size, diff --git a/src/composables/useRecording.js b/src/composables/useRecording.js index 481c6df..fa5e272 100644 --- a/src/composables/useRecording.js +++ b/src/composables/useRecording.js @@ -2,55 +2,65 @@ import { ref } from "vue" import { message } from "ant-design-vue" export function useRecording() { - const isRecording = ref(false) - const mediaRecorder = ref(null) - const audioChunks = ref([]) + const isRecording = ref(false) + const mediaRecorder = ref(null) + const audioChunks = ref([]) + const recordingStartTime = ref(0) - const startRecording = () => { - if (isRecording.value) return + const startRecording = () => { + if (isRecording.value) return - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then((stream) => { - isRecording.value = true - audioChunks.value = [] + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + isRecording.value = true + audioChunks.value = [] + recordingStartTime.value = Date.now() - mediaRecorder.value = new MediaRecorder(stream) + mediaRecorder.value = new MediaRecorder(stream) - mediaRecorder.value.ondataavailable = (event) => { - audioChunks.value.push(event.data) - } + mediaRecorder.value.ondataavailable = (event) => { + audioChunks.value.push(event.data) + } - mediaRecorder.value.onstop = () => { - const audioBlob = new Blob(audioChunks.value, { type: "audio/wav" }) - const audioUrl = URL.createObjectURL(audioBlob) + mediaRecorder.value.onstop = () => { + const audioBlob = new Blob(audioChunks.value, { type: "audio/wav" }) + const audioUrl = URL.createObjectURL(audioBlob) + const duration = Math.floor((Date.now() - recordingStartTime.value) / 1000) - // 这里可以触发发送音频消息的事件 - // 或者调用父组件的方法 + // 触发音频消息发送事件 + const event = new CustomEvent("audioRecorded", { + detail: { + url: audioUrl, + duration: duration, + blob: audioBlob, + }, + }) + window.dispatchEvent(event) - // 停止所有音频轨道 - stream.getTracks().forEach((track) => track.stop()) - } + // 停止所有音频轨道 + stream.getTracks().forEach((track) => track.stop()) + } + + mediaRecorder.value.start() + }) + .catch((error) => { + console.error("录音失败:", error) + message.error("无法访问麦克风,请检查权限设置") + isRecording.value = false + }) + } + + const stopRecording = () => { + if (!isRecording.value || !mediaRecorder.value) return - mediaRecorder.value.start() - }) - .catch((error) => { - console.error("录音失败:", error) - message.error("无法访问麦克风,请检查权限设置") isRecording.value = false - }) - } + mediaRecorder.value.stop() + } - const stopRecording = () => { - if (!isRecording.value || !mediaRecorder.value) return - - isRecording.value = false - mediaRecorder.value.stop() - } - - return { - isRecording, - startRecording, - stopRecording, - } + return { + isRecording, + startRecording, + stopRecording, + } } diff --git a/src/composables/useWebSocket.js b/src/composables/useWebSocket.js index 8fbcad6..f4a15d5 100644 --- a/src/composables/useWebSocket.js +++ b/src/composables/useWebSocket.js @@ -9,7 +9,7 @@ export function useWebSocket() { try { chatStore.connectionStatus = "connecting" - const wsUrl = "ws://localhost:12080/ws" + const wsUrl = "ws://g-ws.nailaoyun.cn/ws" console.log("连接WebSocket:", wsUrl) chatStore.socket = new WebSocket(wsUrl) diff --git a/src/main.js b/src/main.js index 291c778..8b12dba 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import App from "./App.vue" import "ant-design-vue/dist/reset.css" import "./style.css" import VueEasyLightbox from "vue-easy-lightbox"; +import '@fortawesome/fontawesome-free/css/all.min.css' const app = createApp(App) const pinia = createPinia() diff --git a/src/stores/chat.js b/src/stores/chat.js index 9a964b6..4eab301 100644 --- a/src/stores/chat.js +++ b/src/stores/chat.js @@ -1,5 +1,6 @@ import { defineStore } from "pinia" import { ref, computed } from "vue" +import { chatDB } from "@/utils/db" export const useChatStore = defineStore("chat", () => { const friends = ref([]) @@ -37,80 +38,121 @@ export const useChatStore = defineStore("chat", () => { } }) - // 获取聊天历史记录 - const getChatHistory = (friendId, currentUserId) => { - const chatKey = `chat_${currentUserId}_${friendId}` - const chatData = localStorage.getItem(chatKey) - return chatData ? JSON.parse(chatData) : [] - } - - // 保存聊天历史记录 - const saveChatHistory = (friendId, currentUserId, messageList) => { - const chatKey = `chat_${currentUserId}_${friendId}` - localStorage.setItem(chatKey, JSON.stringify(messageList)) + // 初始化数据库 + const initDB = async () => { + try { + await chatDB.init() + console.log("数据库初始化成功") + } catch (error) { + console.error("数据库初始化失败:", error) + } } // 加载好友列表 - const loadFriends = (presetUsers, currentUserId) => { - const friendList = presetUsers.filter((user) => user.id !== currentUserId) + const loadFriends = async (presetUsers, currentUserId) => { + try { + // 从数据库获取好友列表 + let friendList = await chatDB.getFriends(currentUserId) - friendList.forEach((friend) => { - const chatHistory = getChatHistory(friend.id, currentUserId) + // 如果数据库中没有好友,使用预设用户初始化 + if (friendList.length === 0) { + friendList = presetUsers.filter((user) => user.id !== currentUserId) - if (chatHistory.length > 0) { - const lastMsg = chatHistory[chatHistory.length - 1] - let lastMessage = lastMsg.content - if (lastMsg.type === "image") lastMessage = "[图片]" - if (lastMsg.type === "video") lastMessage = "[视频]" - if (lastMsg.type === "audio") lastMessage = "[语音]" - - friend.lastMessage = lastMessage.length > 20 ? lastMessage.substring(0, 20) + "..." : lastMessage + // 保存到数据库 + for (const friend of friendList) { + await chatDB.saveFriend( + { + ...friend, + lastMessage: "", + unreadCount: 0, + }, + currentUserId, + ) + } } - friend.unreadCount = chatHistory.filter((msg) => msg.senderId === friend.id && !msg.read).length - }) - - friends.value = friendList + friends.value = friendList + } catch (error) { + console.error("加载好友列表失败:", error) + // 降级到预设用户 + const friendList = presetUsers.filter((user) => user.id !== currentUserId) + friendList.forEach((friend) => { + friend.lastMessage = "" + friend.unreadCount = 0 + }) + friends.value = friendList + } } // 切换当前聊天好友 - const switchFriend = (friend, currentUserId) => { + const switchFriend = async (friend, currentUserId) => { currentFriend.value = friend + + // 清空未读消息计数 friend.unreadCount = 0 + await chatDB.clearUnreadCount(friend.id) // 加载聊天记录 - const chatHistory = getChatHistory(friend.id, currentUserId) - chatHistory.forEach((msg) => { - if (msg.senderId !== currentUserId) { - msg.read = true - } - }) + try { + const chatHistory = await chatDB.getChatHistory(currentUserId, friend.id) + messages.value = chatHistory.sort((a, b) => a.timestamp - b.timestamp) + } catch (error) { + console.error("加载聊天记录失败:", error) + messages.value = [] + } + } + // 切换当前聊天好友 + const setCurrentFriend = async (friend, currentUserId) => { + currentFriend.value = friend - messages.value = chatHistory - saveChatHistory(friend.id, currentUserId, chatHistory) + // 清空未读消息计数 + friend.unreadCount = 0 + await chatDB.clearUnreadCount(friend.id) + + // 加载聊天记录 + try { + const chatHistory = await chatDB.getChatHistory(currentUserId, friend.id) + console.log("chatHistory:", chatHistory) + console.log("currentUserId:", currentUserId) + console.log("friend.id:", friend.id) + messages.value = chatHistory.sort((a, b) => a.timestamp - b.timestamp) + console.log("messages.value:", messages.value) + } catch (error) { + console.error("加载聊天记录失败:", error) + messages.value = [] + } } // 添加消息 - const addMessage = (message, currentUserId) => { + const addMessage = async (message, currentUserId) => { messages.value.push(message) if (currentFriend.value) { - saveChatHistory(currentFriend.value.id, currentUserId, messages.value) + try { + // 保存到数据库 + await chatDB.saveMessage(message, currentUserId, currentFriend.value.id) - // 更新好友列表中的最后消息 - const friend = friends.value.find((f) => f.id === currentFriend.value.id) - if (friend) { - let lastMessage = message.content - if (message.type === "image") lastMessage = "[图片]" - if (message.type === "video") lastMessage = "[视频]" - if (message.type === "audio") lastMessage = "[语音]" - friend.lastMessage = lastMessage.length > 20 ? lastMessage.substring(0, 20) + "..." : lastMessage + // 更新好友列表中的最后消息 + const friend = friends.value.find((f) => f.id === currentFriend.value.id) + if (friend) { + let lastMessage = message.content + if (message.type === "image") lastMessage = "[图片]" + if (message.type === "video") lastMessage = "[视频]" + if (message.type === "audio") lastMessage = "[语音]" + + friend.lastMessage = lastMessage.length > 20 ? lastMessage.substring(0, 20) + "..." : lastMessage + + // 更新数据库中的好友信息 + await chatDB.updateFriendLastMessage(friend.id, currentUserId, friend.lastMessage, 0) + } + } catch (error) { + console.error("保存消息失败:", error) } } } // 处理接收到的消息 - const handleIncomingMessage = (messageData, currentUserId) => { + const handleIncomingMessage = async (messageData, currentUserId) => { const senderId = messageData.sender_user_id const receiverId = messageData.receiver_user_id @@ -123,31 +165,36 @@ export const useChatStore = defineStore("chat", () => { senderId: senderId, read: false, duration: messageData.duration || 0, + timestamp: Date.now(), } - // 保存消息到本地存储 - const chatHistory = getChatHistory(senderId, currentUserId) - chatHistory.push(newMessage) - saveChatHistory(senderId, currentUserId, chatHistory) + try { + // 保存消息到数据库 + await chatDB.saveMessage(newMessage, currentUserId, senderId) - // 如果消息来自当前聊天用户,直接显示 - if (currentFriend.value && senderId == currentFriend.value.id) { - newMessage.read = true - messages.value.push(newMessage) - saveChatHistory(senderId, currentUserId, messages.value) - } else { - // 更新未读消息计数 - const friend = friends.value.find((f) => f.id == senderId) - if (friend) { - friend.unreadCount = (friend.unreadCount || 0) + 1 + // 如果消息来自当前聊天用户,直接显示 + if (currentFriend.value && senderId == currentFriend.value.id) { + newMessage.read = true + messages.value.push(newMessage) + } else { + // 更新未读消息计数 + const friend = friends.value.find((f) => f.id == senderId) + if (friend) { + friend.unreadCount = (friend.unreadCount || 0) + 1 - // 更新最后消息 - let lastMessage = newMessage.content - if (newMessage.type === "image") lastMessage = "[图片]" - if (newMessage.type === "video") lastMessage = "[视频]" - if (newMessage.type === "audio") lastMessage = "[语音]" - friend.lastMessage = lastMessage + // 更新最后消息 + let lastMessage = newMessage.content + if (newMessage.type === "image") lastMessage = "[图片]" + if (newMessage.type === "video") lastMessage = "[视频]" + if (newMessage.type === "audio") lastMessage = "[语音]" + friend.lastMessage = lastMessage + + // 更新数据库 + await chatDB.updateFriendLastMessage(friend.id, currentUserId, lastMessage, friend.unreadCount) + } } + } catch (error) { + console.error("处理接收消息失败:", error) } } @@ -166,10 +213,10 @@ export const useChatStore = defineStore("chat", () => { isConnected, connectionStatusText, connectionStatusIcon, - getChatHistory, - saveChatHistory, + initDB, loadFriends, switchFriend, + setCurrentFriend, addMessage, handleIncomingMessage, } diff --git a/src/style.css b/src/style.css index 599424c..3a264ca 100644 --- a/src/style.css +++ b/src/style.css @@ -49,7 +49,7 @@ /* 全局样式 */ * { font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, - sans-serif; + sans-serif; } body { @@ -81,8 +81,8 @@ body { /* Ant Design 组件样式覆盖 */ .ant-input:focus, .ant-input-focused { - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.2); + border-color: var(--primary) !important; + box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.2) !important; } .ant-btn-primary { @@ -95,6 +95,78 @@ body { border-color: var(--secondary); } +/* 暗色模式下的Ant Design组件样式 */ +[data-theme="dark"] .ant-input { + background-color: #374151 !important; + border-color: #4b5563 !important; + color: #f3f4f6 !important; +} + +[data-theme="dark"] .ant-input::placeholder { + color: #9ca3af !important; +} + +[data-theme="dark"] .ant-input:focus, +[data-theme="dark"] .ant-input-focused { + background-color: #4b5563 !important; + border-color: var(--primary) !important; +} + +[data-theme="dark"] .ant-textarea { + background-color: #374151 !important; + border-color: #4b5563 !important; + color: #f3f4f6 !important; +} + +[data-theme="dark"] .ant-btn { + background-color: #374151 !important; + border-color: #4b5563 !important; + color: #f3f4f6 !important; +} + +[data-theme="dark"] .ant-btn:hover { + background-color: #4b5563 !important; + border-color: #6b7280 !important; +} + +[data-theme="dark"] .ant-dropdown { + background-color: #374151 !important; +} + +[data-theme="dark"] .ant-dropdown .ant-dropdown-menu { + background-color: #374151 !important; + border-color: #4b5563 !important; +} + +[data-theme="dark"] .ant-dropdown .ant-dropdown-menu-item { + color: #f3f4f6 !important; +} + +[data-theme="dark"] .ant-dropdown .ant-dropdown-menu-item:hover { + background-color: #4b5563 !important; +} + +[data-theme="dark"] .ant-modal { + background-color: #374151 !important; +} + +[data-theme="dark"] .ant-modal-content { + background-color: #374151 !important; +} + +[data-theme="dark"] .ant-modal-header { + background-color: #374151 !important; + border-bottom-color: #4b5563 !important; +} + +[data-theme="dark"] .ant-modal-title { + color: #f3f4f6 !important; +} + +[data-theme="dark"] .ant-modal-body { + color: #f3f4f6 !important; +} + /* 动画 */ @keyframes pulse { 0% { diff --git a/src/utils/db.js b/src/utils/db.js new file mode 100644 index 0000000..4e1d407 --- /dev/null +++ b/src/utils/db.js @@ -0,0 +1,143 @@ +// IndexedDB 数据库工具类 +class ChatDB { + constructor() { + this.dbName = "ChatApp" + this.version = 1 + this.db = null + } + + async init() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.version) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + this.db = request.result + resolve(this.db) + } + + request.onupgradeneeded = (event) => { + const db = event.target.result + + // 创建聊天记录表 + if (!db.objectStoreNames.contains("messages")) { + const messageStore = db.createObjectStore("messages", { keyPath: "id", autoIncrement: true }) + messageStore.createIndex("chatKey", "chatKey", { unique: false }) + messageStore.createIndex("timestamp", "timestamp", { unique: false }) + } + + // 创建用户表 + if (!db.objectStoreNames.contains("users")) { + const userStore = db.createObjectStore("users", { keyPath: "id" }) + } + + // 创建好友表 + if (!db.objectStoreNames.contains("friends")) { + const friendStore = db.createObjectStore("friends", { keyPath: "id" }) + friendStore.createIndex("userId", "userId", { unique: false }) + } + } + }) + } + + // 保存消息 + async saveMessage(message, currentUserId, friendId) { + const transaction = this.db.transaction(["messages"], "readwrite") + const store = transaction.objectStore("messages") + + const messageData = { + ...message, + chatKey: `${currentUserId}_${friendId}`, + timestamp: Date.now(), + } + + return store.add(messageData) + } + + // 获取聊天记录 + async getChatHistory(currentUserId, friendId) { + const transaction = this.db.transaction(["messages"], "readonly") + const store = transaction.objectStore("messages") + const index = store.index("chatKey") + + const chatKey = `${currentUserId}_${friendId}` + const request = index.getAll(chatKey) + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result || []) + request.onerror = () => reject(request.error) + }) + } + + // 保存好友信息 + async saveFriend(friend, currentUserId) { + const transaction = this.db.transaction(["friends"], "readwrite") + const store = transaction.objectStore("friends") + + const friendData = { + ...friend, + userId: currentUserId, + updatedAt: Date.now(), + } + + return store.put(friendData) + } + + // 获取好友列表 + async getFriends(currentUserId) { + const transaction = this.db.transaction(["friends"], "readonly") + const store = transaction.objectStore("friends") + const index = store.index("userId") + + const request = index.getAll(currentUserId) + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result || []) + request.onerror = () => reject(request.error) + }) + } + + // 更新好友最后消息 + async updateFriendLastMessage(friendId, currentUserId, lastMessage, unreadCount = 0) { + const transaction = this.db.transaction(["friends"], "readwrite") + const store = transaction.objectStore("friends") + + const request = store.get(friendId) + + return new Promise((resolve, reject) => { + request.onsuccess = () => { + const friend = request.result + if (friend) { + friend.lastMessage = lastMessage + friend.unreadCount = unreadCount + friend.updatedAt = Date.now() + store.put(friend) + } + resolve() + } + request.onerror = () => reject(request.error) + }) + } + + // 清空未读消息 + async clearUnreadCount(friendId) { + const transaction = this.db.transaction(["friends"], "readwrite") + const store = transaction.objectStore("friends") + + const request = store.get(friendId) + + return new Promise((resolve, reject) => { + request.onsuccess = () => { + const friend = request.result + if (friend) { + friend.unreadCount = 0 + store.put(friend) + } + resolve() + } + request.onerror = () => reject(request.error) + }) + } +} + +export const chatDB = new ChatDB() diff --git a/src/utils/request.js b/src/utils/request.js index eb866e2..cddbf78 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -9,44 +9,87 @@ const service = axios.create({ // 请求拦截器 service.interceptors.request.use( - (config) => { - const token = localStorage.getItem("token") - if (token) { - config.headers["Authorization"] = `Bearer ${token}` - } - return config - }, - (error) => { - console.error("Request error:", error) - return Promise.reject(error) - }, + (config) => { + const token = localStorage.getItem("token") + if (token) { + config.headers["Authorization"] = `Bearer ${token}` + } + return config + }, + (error) => { + console.error("Request error:", error) + return Promise.reject(error) + }, ) // 响应拦截器 service.interceptors.response.use( - (response) => { - const { code, result, message: msg } = response.data - if (code === 0) { - return result - } else { - if (code === 401) { + (response) => { + const { code, result, message: msg } = response.data + if (code === 0) { + return result + } else { + if (code === 401) { + message.error(msg) + localStorage.removeItem("token") + localStorage.removeItem("chatUser") + window.location.href = "/login" + return Promise.reject(new Error(msg)) + } message.error(msg) - localStorage.removeItem("token") - localStorage.removeItem("chatUser") - window.location.href = "/login" return Promise.reject(new Error(msg)) } - message.error(msg) - return Promise.reject(new Error(msg)) - } - }, - (error) => { - console.error("Response error:", error) - message.error("网络请求失败") - return Promise.reject(error) - }, + }, + (error) => { + console.error("Response error:", error) + + // 详细的错误处理 + if (error.code === "ECONNABORTED") { + console.log("请求超时,请检查网络连接") + } else if (error.response?.status === 0 || error.message.includes("CORS")) { + console.log("CORS错误检测到,尝试备用方案...") + handleCORSError(error.config) + } else if (error.response?.status >= 400 && error.response?.status < 500) { + console.log("客户端错误:", error.response.status, error.response.data) + } else if (error.response?.status >= 500) { + console.log("服务器错误:", error.response.status) + } else { + console.log("网络错误:", error.message) + } + + message.error("网络请求失败") + return Promise.reject(error) + }, ) +// CORS错误的备用处理方案 +const handleCORSError = (config) => { + console.log("执行CORS错误备用方案...") + + // /* "http://g-ws.nailaoyun.cn" + */ + const apiUrl = config.url.replace("/api", "") + + return fetch(apiUrl, { + method: config.method.toUpperCase(), + headers: { + "Content-Type": "application/json", + ...config.headers, + }, + mode: "cors", + body: config.data ? JSON.stringify(config.data) : undefined, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.json() + }) + .catch((error) => { + console.error("备用方案也失败:", error) + throw error + }) +} + // 封装请求方法 export const get = (url, params = {}) => { return service.get(url, { params }) @@ -75,12 +118,34 @@ export const sendMessage = (data) => { video: 3, } - return post("send-to-user", { + // 直接使用固定的后端接口地址 + const apiUrl = "/api/send-to-user" + + const requestData = { sender_user_id: data.senderId, receiver_user_id: data.receiverId, message_type: messageTypeMap[data.type] || 0, message_content: data.content, - }) + } + + console.log("发送消息到API:", apiUrl, requestData) + + return axios + .post(apiUrl, requestData, { + headers: { + "Content-Type": "application/json", + }, + timeout: 15000, + withCredentials: false, + }) + .then((response) => { + console.log("消息发送成功:", response.data) + return response.data + }) + .catch((error) => { + console.error("消息发送失败:", error) + throw error + }) } export default service diff --git a/src/views/Chat.vue b/src/views/Chat.vue index a08ff9b..bc4976e 100644 --- a/src/views/Chat.vue +++ b/src/views/Chat.vue @@ -1,49 +1,67 @@ diff --git a/src/views/Login.vue b/src/views/Login.vue index ad7555a..dc8b1f3 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -1,39 +1,117 @@ diff --git a/vite.config.js b/vite.config.js index 8ab9b7e..a7a916f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -19,10 +19,14 @@ export default defineConfig({ proxy: { '/api': { // target: 'http://127.0.0.1:18007/api/', - target: 'http://127.0.0.1:12080', + // target: 'http://127.0.0.1:12080', + target: 'http://g-ws.nailaoyun.cn', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } - } + }, + content: [ + './node_modules/@fortawesome/fontawesome-free/**/*.js' + ], })