-
+
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 @@
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
通用设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
聊天设置
+
+
+
+
+
Enter键发送
+
按Enter键直接发送消息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ {{ userStore.currentUser?.name?.charAt(0) || 'U' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
{{ friendsCount }}
+
好友
+
+
+
{{ groupsCount }}
+
群聊
+
+
+
{{ messagesCount }}
+
消息
+
+
+
+
+
+
个人设置
+
+
+
+
+
+
昵称
+
{{ userStore.currentUser?.name || '未设置' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ callerInfo.name.charAt(0) }}
+
+
+
{{ callerInfo.name }}
+
{{ callStatus }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ callerInfo.name.charAt(0) }}
+
+
{{ callerInfo.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WebSocket 聊天系统
+
+
现代化实时通讯平台
+
选择用户身份开始您的聊天之旅
-
-
-
-
高级WebSocket聊天系统
-
选择以下用户之一登录聊天系统
-
-
+
+
+
+
+
+
-
-
-
- {{ user.name.charAt(0) }}
-
-
{{ user.name }}
-
ID: {{ user.id }}
+
+
+
+
+
+ {{ user.name.charAt(0) }}
+
+
+
+
+
+
+ {{ user.name }}
+
+
+ ID: {{ user.id }}
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+ 安全加密
+
+
•
+
+
+ 实时通讯
+
+
•
+
+
+ 多媒体支持
+
+
+
+
+
+
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'
+ ],
})