From 55218701d073878115fb898993673f3273737835 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Tue, 22 Apr 2025 22:15:15 +0800 Subject: [PATCH 1/5] =?UTF-8?q?wip:=20=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/system/dict.api.ts | 10 +- src/hooks/useWebSocketDict.js | 232 +++++++++++ src/hooks/useWebSocketDict.ts | 248 +++++++++++ src/plugins/index.ts | 3 + src/plugins/websocket.ts | 16 + src/store/modules/dict.store.ts | 11 + src/types/websocket.ts | 15 + src/views/demo/dict-websocket.vue | 259 ++++++++++++ src/views/system/websocket/websocket-test.vue | 390 ++++++++++++++++++ 9 files changed, 1177 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useWebSocketDict.js create mode 100644 src/hooks/useWebSocketDict.ts create mode 100644 src/plugins/websocket.ts create mode 100644 src/types/websocket.ts create mode 100644 src/views/demo/dict-websocket.vue create mode 100644 src/views/system/websocket/websocket-test.vue diff --git a/src/api/system/dict.api.ts b/src/api/system/dict.api.ts index 3a3d4da9..1e1beaef 100644 --- a/src/api/system/dict.api.ts +++ b/src/api/system/dict.api.ts @@ -303,12 +303,8 @@ export interface DictItemForm { * 字典项下拉选项 */ export interface DictItemOption { - /** 字典数据值 */ - value: string; - - /** 字典数据标签 */ + value: number | string; label: string; - - /** 标签类型 */ - tagType: string; + tagType?: "" | "success" | "info" | "warning" | "danger"; + [key: string]: any; } diff --git a/src/hooks/useWebSocketDict.js b/src/hooks/useWebSocketDict.js new file mode 100644 index 00000000..0ded907c --- /dev/null +++ b/src/hooks/useWebSocketDict.js @@ -0,0 +1,232 @@ +import { ref } from "vue"; +import { useUserStore } from "@/store/modules/user"; +import { useDictStoreHook } from "@/store/modules/dict.store"; +import SockJS from "sockjs-client"; +import Stomp from "webstomp-client"; +import { ElMessage } from "element-plus"; + +export function useWebSocketDict() { + const userStore = useUserStore(); + const dictStore = useDictStoreHook(); + + // WebSocket状态 + const isConnected = ref(false); + const stompClient = ref(null); + const subscriptions = ref([]); + + /** + * 初始化WebSocket + */ + const initWebSocket = async () => { + try { + await connectWebSocket(); + setupDictSubscription(); + } catch (error) { + console.error("初始化WebSocket失败:", error); + } + }; + + /** + * 关闭WebSocket + */ + const closeWebSocket = () => { + disconnectWebSocket(); + }; + + /** + * 连接WebSocket服务器 + */ + const connectWebSocket = () => { + return new Promise((resolve, reject) => { + try { + const serverUrl = import.meta.env.VITE_APP_BASE_API + "/ws"; + + // 创建SockJS连接 + const socket = new SockJS(serverUrl); + + // 创建STOMP客户端 + const client = Stomp.over(socket); + + // 禁用调试日志 + client.debug = () => {}; + + // 添加认证头信息 + const headers = { + Authorization: userStore.token, + }; + + // 连接到WebSocket服务器 + client.connect( + headers, + () => { + stompClient.value = client; + isConnected.value = true; + console.log("WebSocket连接成功"); + resolve(); + }, + (error) => { + console.error("WebSocket连接失败:", error); + isConnected.value = false; + reject(error); + } + ); + } catch (error) { + console.error("创建WebSocket连接时出错:", error); + reject(error); + } + }); + }; + + /** + * 断开WebSocket连接 + */ + const disconnectWebSocket = () => { + // 取消所有订阅 + subscriptions.value.forEach((subscription) => { + if (subscription && typeof subscription.unsubscribe === "function") { + subscription.unsubscribe(); + } + }); + subscriptions.value = []; + + // 断开连接 + if (stompClient.value && stompClient.value.connected) { + stompClient.value.disconnect(); + stompClient.value = null; + } + + isConnected.value = false; + console.log("WebSocket连接已断开"); + }; + + /** + * 设置字典订阅 + */ + const setupDictSubscription = () => { + // 订阅字典更新 + subscribe("/topic/dict", (message) => { + handleDictEvent(message); + }); + }; + + /** + * 处理字典事件 + * @param {Object} event 字典事件 + */ + const handleDictEvent = (event) => { + // 尝试解析消息,防止服务端发送字符串格式的JSON + let eventData = event; + if (typeof event === "string") { + try { + eventData = JSON.parse(event); + } catch (error) { + console.error("解析WebSocket消息失败:", error); + return; + } + } + + const { type, dictCode } = eventData; + + if (type === "DICT_UPDATED") { + // 删除缓存,强制重新加载 + dictStore.removeDictItem(dictCode); + console.log(`字典 ${dictCode} 已更新,缓存已清除`); + ElMessage.success(`字典 ${dictCode} 已更新`); + } else if (type === "DICT_DELETED") { + // 删除缓存 + dictStore.removeDictItem(dictCode); + console.log(`字典 ${dictCode} 已删除,缓存已清除`); + ElMessage.warning(`字典 ${dictCode} 已删除`); + } + }; + + /** + * 发送消息到WebSocket服务器 + * @param {string} destination 目标地址 + * @param {string} content 消息内容 + */ + const sendMessage = (destination, content) => { + if (!isConnected.value || !stompClient.value) { + console.error("WebSocket未连接,无法发送消息"); + return false; + } + + try { + // 发送消息 + stompClient.value.send( + destination, + JSON.stringify({ + content: content, + sender: userStore.userInfo.username, + timestamp: new Date().getTime(), + }), + {} + ); + return true; + } catch (error) { + console.error("发送消息失败:", error); + return false; + } + }; + + /** + * 订阅WebSocket主题 + * @param {string} destination 订阅地址 + * @param {Function} callback 回调函数 + */ + const subscribe = (destination, callback) => { + if (!isConnected.value || !stompClient.value) { + console.error("WebSocket未连接,无法订阅"); + return null; + } + + try { + // 订阅主题 + const subscription = stompClient.value.subscribe(destination, (message) => { + if (message.body) { + try { + // 尝试解析JSON格式消息 + const data = JSON.parse(message.body); + + // 如果返回的是JSON字符串,再次解析 + if (typeof data === "string" && data.startsWith("{") && data.endsWith("}")) { + try { + const parsedData = JSON.parse(data); + callback(parsedData); + } catch (e) { + // 如果再次解析失败,传递原始数据 + callback(data); + } + } else { + // 直接传递已解析的数据 + callback(data); + } + } catch (e) { + // 如果解析失败,传递原始消息 + console.warn("解析WebSocket消息失败,传递原始消息:", e); + callback(message.body); + } + } + }); + + // 保存订阅引用,以便后续取消订阅 + subscriptions.value.push(subscription); + + return subscription; + } catch (error) { + console.error("订阅失败:", error); + return null; + } + }; + + return { + isConnected, + connectWebSocket, + disconnectWebSocket, + sendMessage, + subscribe, + initWebSocket, + closeWebSocket, + handleDictEvent, + }; +} diff --git a/src/hooks/useWebSocketDict.ts b/src/hooks/useWebSocketDict.ts new file mode 100644 index 00000000..48b6d5c3 --- /dev/null +++ b/src/hooks/useWebSocketDict.ts @@ -0,0 +1,248 @@ +import { ref } from "vue"; +import { useUserStore } from "@/store/modules/user"; +import { useDictStoreHook } from "@/store/modules/dict.store"; +import SockJS from "sockjs-client"; +import Stomp, { Client, Subscription } from "webstomp-client"; +import { ElMessage } from "element-plus"; + +// 字典WebSocket事件类型定义 +interface DictWebSocketEvent { + type: "DICT_UPDATED" | "DICT_DELETED"; + dictCode: string; + timestamp: number; +} + +// 消息类型定义 +interface WebSocketMessage { + content: string; + sender: string; + timestamp: number; +} + +export function useWebSocketDict() { + const userStore = useUserStore(); + const dictStore = useDictStoreHook(); + + // WebSocket状态 + const isConnected = ref(false); + const stompClient = ref(null); + const subscriptions = ref([]); + + /** + * 初始化WebSocket + */ + const initWebSocket = async (): Promise => { + try { + await connectWebSocket(); + setupDictSubscription(); + } catch (error) { + console.error("初始化WebSocket失败:", error); + } + }; + + /** + * 关闭WebSocket + */ + const closeWebSocket = (): void => { + disconnectWebSocket(); + }; + + /** + * 连接WebSocket服务器 + */ + const connectWebSocket = (): Promise => { + return new Promise((resolve, reject) => { + try { + const serverUrl = import.meta.env.VITE_APP_BASE_API + "/ws"; + + // 创建SockJS连接 + const socket = new SockJS(serverUrl); + + // 创建STOMP客户端 + const client = Stomp.over(socket); + + // 禁用调试日志 + client.debug = () => {}; + + // 添加认证头信息 + const headers = { + Authorization: userStore.token, + }; + + // 连接到WebSocket服务器 + client.connect( + headers, + () => { + stompClient.value = client; + isConnected.value = true; + console.log("WebSocket连接成功"); + resolve(); + }, + (error) => { + console.error("WebSocket连接失败:", error); + isConnected.value = false; + reject(error); + } + ); + } catch (error) { + console.error("创建WebSocket连接时出错:", error); + reject(error); + } + }); + }; + + /** + * 断开WebSocket连接 + */ + const disconnectWebSocket = (): void => { + // 取消所有订阅 + subscriptions.value.forEach((subscription) => { + if (subscription && typeof subscription.unsubscribe === "function") { + subscription.unsubscribe(); + } + }); + subscriptions.value = []; + + // 断开连接 + if (stompClient.value && stompClient.value.connected) { + stompClient.value.disconnect(); + stompClient.value = null; + } + + isConnected.value = false; + console.log("WebSocket连接已断开"); + }; + + /** + * 设置字典订阅 + */ + const setupDictSubscription = (): void => { + // 订阅字典更新 + subscribe("/topic/dict", (message: any) => { + handleDictEvent(message); + }); + }; + + /** + * 处理字典事件 + * @param {Object | string} event 字典事件 + */ + const handleDictEvent = (event: any): void => { + // 尝试解析消息,防止服务端发送字符串格式的JSON + let eventData: DictWebSocketEvent; + if (typeof event === "string") { + try { + eventData = JSON.parse(event) as DictWebSocketEvent; + } catch (error) { + console.error("解析WebSocket消息失败:", error); + return; + } + } else { + eventData = event as DictWebSocketEvent; + } + + const { type, dictCode } = eventData; + + if (type === "DICT_UPDATED") { + // 删除缓存,强制重新加载 + dictStore.removeDictItem(dictCode); + console.log(`字典 ${dictCode} 已更新,缓存已清除`); + ElMessage.success(`字典 ${dictCode} 已更新`); + } else if (type === "DICT_DELETED") { + // 删除缓存 + dictStore.removeDictItem(dictCode); + console.log(`字典 ${dictCode} 已删除,缓存已清除`); + ElMessage.warning(`字典 ${dictCode} 已删除`); + } + }; + + /** + * 发送消息到WebSocket服务器 + * @param {string} destination 目标地址 + * @param {string} content 消息内容 + * @returns {boolean} 是否发送成功 + */ + const sendMessage = (destination: string, content: string): boolean => { + if (!isConnected.value || !stompClient.value) { + console.error("WebSocket未连接,无法发送消息"); + return false; + } + + try { + // 发送消息 + const message: WebSocketMessage = { + content: content, + sender: userStore.userInfo.username, + timestamp: new Date().getTime(), + }; + + stompClient.value.send(destination, JSON.stringify(message), {}); + return true; + } catch (error) { + console.error("发送消息失败:", error); + return false; + } + }; + + /** + * 订阅WebSocket主题 + * @param {string} destination 订阅地址 + * @param {Function} callback 回调函数 + * @returns {Subscription | null} 订阅对象 + */ + const subscribe = (destination: string, callback: (data: any) => void): Subscription | null => { + if (!isConnected.value || !stompClient.value) { + console.error("WebSocket未连接,无法订阅"); + return null; + } + + try { + // 订阅主题 + const subscription = stompClient.value.subscribe(destination, (message) => { + if (message.body) { + try { + // 尝试解析JSON格式消息 + const data = JSON.parse(message.body); + + // 如果返回的是JSON字符串,再次解析 + if (typeof data === "string" && data.startsWith("{") && data.endsWith("}")) { + try { + const parsedData = JSON.parse(data); + callback(parsedData); + } catch { + // 如果再次解析失败,传递原始数据 + callback(data); + } + } else { + // 直接传递已解析的数据 + callback(data); + } + } catch (e) { + // 如果解析失败,传递原始消息 + console.warn("解析WebSocket消息失败,传递原始消息:", e); + callback(message.body); + } + } + }); + + // 保存订阅引用,以便后续取消订阅 + subscriptions.value.push(subscription); + + return subscription; + } catch (error) { + console.error("订阅失败:", error); + return null; + } + }; + + return { + isConnected, + connectWebSocket, + disconnectWebSocket, + sendMessage, + subscribe, + initWebSocket, + closeWebSocket, + handleDictEvent, + }; +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts index e5262519..98d26205 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -6,6 +6,7 @@ import { setupRouter } from "@/router"; import { setupStore } from "@/store"; import { setupElIcons } from "./icons"; import { setupPermission } from "./permission"; +import { setupWebSocket } from "./websocket"; import { InstallCodeMirror } from "codemirror-editor-vue3"; export default { @@ -22,6 +23,8 @@ export default { setupElIcons(app); // 路由守卫 setupPermission(); + // WebSocket服务 + setupWebSocket(); // 注册 CodeMirror app.use(InstallCodeMirror); }, diff --git a/src/plugins/websocket.ts b/src/plugins/websocket.ts new file mode 100644 index 00000000..55a4d403 --- /dev/null +++ b/src/plugins/websocket.ts @@ -0,0 +1,16 @@ +import { useWebSocketDict } from "@/hooks/useWebSocketDict"; + +/** + * 初始化WebSocket服务 + */ +export function setupWebSocket() { + const dictWebSocket = useWebSocketDict(); + + // 初始化字典WebSocket服务 + dictWebSocket.initWebSocket(); + + // 在窗口关闭前断开WebSocket连接 + window.addEventListener("beforeunload", () => { + dictWebSocket.closeWebSocket(); + }); +} diff --git a/src/store/modules/dict.store.ts b/src/store/modules/dict.store.ts index e053ed94..84f7ff56 100644 --- a/src/store/modules/dict.store.ts +++ b/src/store/modules/dict.store.ts @@ -42,6 +42,16 @@ export const useDictStore = defineStore("dict", () => { return dictCache.value[dictCode] || []; }; + /** + * 移除指定字典项 + * @param dictCode 字典编码 + */ + const removeDictItem = (dictCode: string) => { + if (dictCache.value[dictCode]) { + Reflect.deleteProperty(dictCache.value, dictCode); + } + }; + /** * 清空字典缓存 */ @@ -52,6 +62,7 @@ export const useDictStore = defineStore("dict", () => { return { loadDictItems, getDictItems, + removeDictItem, clearDictCache, }; }); diff --git a/src/types/websocket.ts b/src/types/websocket.ts new file mode 100644 index 00000000..30b56a2d --- /dev/null +++ b/src/types/websocket.ts @@ -0,0 +1,15 @@ +/** + * WebSocket相关类型定义 + */ + +/** + * 字典WebSocket事件类型 + */ +export interface DictWebSocketEvent { + /** 事件类型:更新或删除 */ + type: "DICT_UPDATED" | "DICT_DELETED"; + /** 字典编码 */ + dictCode: string; + /** 时间戳 */ + timestamp: number; +} diff --git a/src/views/demo/dict-websocket.vue b/src/views/demo/dict-websocket.vue new file mode 100644 index 00000000..dd8dcd1d --- /dev/null +++ b/src/views/demo/dict-websocket.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/src/views/system/websocket/websocket-test.vue b/src/views/system/websocket/websocket-test.vue new file mode 100644 index 00000000..553ea4a5 --- /dev/null +++ b/src/views/system/websocket/websocket-test.vue @@ -0,0 +1,390 @@ + + + + + From 964dba59c74cdc7d8d31da8745b48179a7e8b62a Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Wed, 23 Apr 2025 08:29:22 +0800 Subject: [PATCH 2/5] =?UTF-8?q?wip:=20=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useWebSocketDict.js | 232 -------------------------------- src/hooks/useWebSocketDict.ts | 245 +++++++--------------------------- src/plugins/websocket.ts | 1 + 3 files changed, 51 insertions(+), 427 deletions(-) delete mode 100644 src/hooks/useWebSocketDict.js diff --git a/src/hooks/useWebSocketDict.js b/src/hooks/useWebSocketDict.js deleted file mode 100644 index 0ded907c..00000000 --- a/src/hooks/useWebSocketDict.js +++ /dev/null @@ -1,232 +0,0 @@ -import { ref } from "vue"; -import { useUserStore } from "@/store/modules/user"; -import { useDictStoreHook } from "@/store/modules/dict.store"; -import SockJS from "sockjs-client"; -import Stomp from "webstomp-client"; -import { ElMessage } from "element-plus"; - -export function useWebSocketDict() { - const userStore = useUserStore(); - const dictStore = useDictStoreHook(); - - // WebSocket状态 - const isConnected = ref(false); - const stompClient = ref(null); - const subscriptions = ref([]); - - /** - * 初始化WebSocket - */ - const initWebSocket = async () => { - try { - await connectWebSocket(); - setupDictSubscription(); - } catch (error) { - console.error("初始化WebSocket失败:", error); - } - }; - - /** - * 关闭WebSocket - */ - const closeWebSocket = () => { - disconnectWebSocket(); - }; - - /** - * 连接WebSocket服务器 - */ - const connectWebSocket = () => { - return new Promise((resolve, reject) => { - try { - const serverUrl = import.meta.env.VITE_APP_BASE_API + "/ws"; - - // 创建SockJS连接 - const socket = new SockJS(serverUrl); - - // 创建STOMP客户端 - const client = Stomp.over(socket); - - // 禁用调试日志 - client.debug = () => {}; - - // 添加认证头信息 - const headers = { - Authorization: userStore.token, - }; - - // 连接到WebSocket服务器 - client.connect( - headers, - () => { - stompClient.value = client; - isConnected.value = true; - console.log("WebSocket连接成功"); - resolve(); - }, - (error) => { - console.error("WebSocket连接失败:", error); - isConnected.value = false; - reject(error); - } - ); - } catch (error) { - console.error("创建WebSocket连接时出错:", error); - reject(error); - } - }); - }; - - /** - * 断开WebSocket连接 - */ - const disconnectWebSocket = () => { - // 取消所有订阅 - subscriptions.value.forEach((subscription) => { - if (subscription && typeof subscription.unsubscribe === "function") { - subscription.unsubscribe(); - } - }); - subscriptions.value = []; - - // 断开连接 - if (stompClient.value && stompClient.value.connected) { - stompClient.value.disconnect(); - stompClient.value = null; - } - - isConnected.value = false; - console.log("WebSocket连接已断开"); - }; - - /** - * 设置字典订阅 - */ - const setupDictSubscription = () => { - // 订阅字典更新 - subscribe("/topic/dict", (message) => { - handleDictEvent(message); - }); - }; - - /** - * 处理字典事件 - * @param {Object} event 字典事件 - */ - const handleDictEvent = (event) => { - // 尝试解析消息,防止服务端发送字符串格式的JSON - let eventData = event; - if (typeof event === "string") { - try { - eventData = JSON.parse(event); - } catch (error) { - console.error("解析WebSocket消息失败:", error); - return; - } - } - - const { type, dictCode } = eventData; - - if (type === "DICT_UPDATED") { - // 删除缓存,强制重新加载 - dictStore.removeDictItem(dictCode); - console.log(`字典 ${dictCode} 已更新,缓存已清除`); - ElMessage.success(`字典 ${dictCode} 已更新`); - } else if (type === "DICT_DELETED") { - // 删除缓存 - dictStore.removeDictItem(dictCode); - console.log(`字典 ${dictCode} 已删除,缓存已清除`); - ElMessage.warning(`字典 ${dictCode} 已删除`); - } - }; - - /** - * 发送消息到WebSocket服务器 - * @param {string} destination 目标地址 - * @param {string} content 消息内容 - */ - const sendMessage = (destination, content) => { - if (!isConnected.value || !stompClient.value) { - console.error("WebSocket未连接,无法发送消息"); - return false; - } - - try { - // 发送消息 - stompClient.value.send( - destination, - JSON.stringify({ - content: content, - sender: userStore.userInfo.username, - timestamp: new Date().getTime(), - }), - {} - ); - return true; - } catch (error) { - console.error("发送消息失败:", error); - return false; - } - }; - - /** - * 订阅WebSocket主题 - * @param {string} destination 订阅地址 - * @param {Function} callback 回调函数 - */ - const subscribe = (destination, callback) => { - if (!isConnected.value || !stompClient.value) { - console.error("WebSocket未连接,无法订阅"); - return null; - } - - try { - // 订阅主题 - const subscription = stompClient.value.subscribe(destination, (message) => { - if (message.body) { - try { - // 尝试解析JSON格式消息 - const data = JSON.parse(message.body); - - // 如果返回的是JSON字符串,再次解析 - if (typeof data === "string" && data.startsWith("{") && data.endsWith("}")) { - try { - const parsedData = JSON.parse(data); - callback(parsedData); - } catch (e) { - // 如果再次解析失败,传递原始数据 - callback(data); - } - } else { - // 直接传递已解析的数据 - callback(data); - } - } catch (e) { - // 如果解析失败,传递原始消息 - console.warn("解析WebSocket消息失败,传递原始消息:", e); - callback(message.body); - } - } - }); - - // 保存订阅引用,以便后续取消订阅 - subscriptions.value.push(subscription); - - return subscription; - } catch (error) { - console.error("订阅失败:", error); - return null; - } - }; - - return { - isConnected, - connectWebSocket, - disconnectWebSocket, - sendMessage, - subscribe, - initWebSocket, - closeWebSocket, - handleDictEvent, - }; -} diff --git a/src/hooks/useWebSocketDict.ts b/src/hooks/useWebSocketDict.ts index 48b6d5c3..9570b701 100644 --- a/src/hooks/useWebSocketDict.ts +++ b/src/hooks/useWebSocketDict.ts @@ -1,246 +1,101 @@ import { ref } from "vue"; -import { useUserStore } from "@/store/modules/user"; import { useDictStoreHook } from "@/store/modules/dict.store"; -import SockJS from "sockjs-client"; -import Stomp, { Client, Subscription } from "webstomp-client"; +import { useStomp } from "@/hooks/useStomp"; import { ElMessage } from "element-plus"; +import { IMessage } from "@stomp/stompjs"; -// 字典WebSocket事件类型定义 -interface DictWebSocketEvent { - type: "DICT_UPDATED" | "DICT_DELETED"; +// 字典事件类型 +interface DictEvent { + type: string; dictCode: string; - timestamp: number; -} - -// 消息类型定义 -interface WebSocketMessage { - content: string; - sender: string; - timestamp: number; + timestamp?: number; } export function useWebSocketDict() { - const userStore = useUserStore(); const dictStore = useDictStoreHook(); - // WebSocket状态 - const isConnected = ref(false); - const stompClient = ref(null); - const subscriptions = ref([]); + // 使用现有的useStomp + const { isConnected, connect, subscribe, unsubscribe, disconnect } = useStomp(); + + // 存储订阅ID + const subscriptionIds = ref([]); /** * 初始化WebSocket */ - const initWebSocket = async (): Promise => { + const initWebSocket = async () => { try { - await connectWebSocket(); + // 连接WebSocket + connect(); + + // 设置字典订阅 setupDictSubscription(); + + console.log("字典WebSocket初始化完成"); } catch (error) { - console.error("初始化WebSocket失败:", error); + console.error("初始化字典WebSocket失败:", error); } }; /** * 关闭WebSocket */ - const closeWebSocket = (): void => { - disconnectWebSocket(); - }; - - /** - * 连接WebSocket服务器 - */ - const connectWebSocket = (): Promise => { - return new Promise((resolve, reject) => { - try { - const serverUrl = import.meta.env.VITE_APP_BASE_API + "/ws"; - - // 创建SockJS连接 - const socket = new SockJS(serverUrl); - - // 创建STOMP客户端 - const client = Stomp.over(socket); - - // 禁用调试日志 - client.debug = () => {}; - - // 添加认证头信息 - const headers = { - Authorization: userStore.token, - }; - - // 连接到WebSocket服务器 - client.connect( - headers, - () => { - stompClient.value = client; - isConnected.value = true; - console.log("WebSocket连接成功"); - resolve(); - }, - (error) => { - console.error("WebSocket连接失败:", error); - isConnected.value = false; - reject(error); - } - ); - } catch (error) { - console.error("创建WebSocket连接时出错:", error); - reject(error); - } - }); - }; - - /** - * 断开WebSocket连接 - */ - const disconnectWebSocket = (): void => { + const closeWebSocket = () => { // 取消所有订阅 - subscriptions.value.forEach((subscription) => { - if (subscription && typeof subscription.unsubscribe === "function") { - subscription.unsubscribe(); - } + subscriptionIds.value.forEach((id) => { + unsubscribe(id); }); - subscriptions.value = []; + subscriptionIds.value = []; // 断开连接 - if (stompClient.value && stompClient.value.connected) { - stompClient.value.disconnect(); - stompClient.value = null; - } + disconnect(); - isConnected.value = false; - console.log("WebSocket连接已断开"); + console.log("字典WebSocket已关闭"); }; /** * 设置字典订阅 */ - const setupDictSubscription = (): void => { + const setupDictSubscription = () => { // 订阅字典更新 - subscribe("/topic/dict", (message: any) => { + const subId = subscribe("/topic/dict", (message: IMessage) => { handleDictEvent(message); }); + + if (subId) { + subscriptionIds.value.push(subId); + } }; /** * 处理字典事件 - * @param {Object | string} event 字典事件 + * @param message STOMP消息 */ - const handleDictEvent = (event: any): void => { - // 尝试解析消息,防止服务端发送字符串格式的JSON - let eventData: DictWebSocketEvent; - if (typeof event === "string") { - try { - eventData = JSON.parse(event) as DictWebSocketEvent; - } catch (error) { - console.error("解析WebSocket消息失败:", error); - return; + const handleDictEvent = (message: IMessage) => { + if (!message.body) return; + + try { + // 尝试解析消息 + const eventData = JSON.parse(message.body) as DictEvent; + + if (eventData.type === "DICT_UPDATED") { + // 删除缓存,强制重新加载 + dictStore.removeDictItem(eventData.dictCode); + console.log(`字典 ${eventData.dictCode} 已更新,缓存已清除`); + ElMessage.success(`字典 ${eventData.dictCode} 已更新`); + } else if (eventData.type === "DICT_DELETED") { + // 删除缓存 + dictStore.removeDictItem(eventData.dictCode); + console.log(`字典 ${eventData.dictCode} 已删除,缓存已清除`); + ElMessage.warning(`字典 ${eventData.dictCode} 已删除`); } - } else { - eventData = event as DictWebSocketEvent; - } - - const { type, dictCode } = eventData; - - if (type === "DICT_UPDATED") { - // 删除缓存,强制重新加载 - dictStore.removeDictItem(dictCode); - console.log(`字典 ${dictCode} 已更新,缓存已清除`); - ElMessage.success(`字典 ${dictCode} 已更新`); - } else if (type === "DICT_DELETED") { - // 删除缓存 - dictStore.removeDictItem(dictCode); - console.log(`字典 ${dictCode} 已删除,缓存已清除`); - ElMessage.warning(`字典 ${dictCode} 已删除`); - } - }; - - /** - * 发送消息到WebSocket服务器 - * @param {string} destination 目标地址 - * @param {string} content 消息内容 - * @returns {boolean} 是否发送成功 - */ - const sendMessage = (destination: string, content: string): boolean => { - if (!isConnected.value || !stompClient.value) { - console.error("WebSocket未连接,无法发送消息"); - return false; - } - - try { - // 发送消息 - const message: WebSocketMessage = { - content: content, - sender: userStore.userInfo.username, - timestamp: new Date().getTime(), - }; - - stompClient.value.send(destination, JSON.stringify(message), {}); - return true; } catch (error) { - console.error("发送消息失败:", error); - return false; - } - }; - - /** - * 订阅WebSocket主题 - * @param {string} destination 订阅地址 - * @param {Function} callback 回调函数 - * @returns {Subscription | null} 订阅对象 - */ - const subscribe = (destination: string, callback: (data: any) => void): Subscription | null => { - if (!isConnected.value || !stompClient.value) { - console.error("WebSocket未连接,无法订阅"); - return null; - } - - try { - // 订阅主题 - const subscription = stompClient.value.subscribe(destination, (message) => { - if (message.body) { - try { - // 尝试解析JSON格式消息 - const data = JSON.parse(message.body); - - // 如果返回的是JSON字符串,再次解析 - if (typeof data === "string" && data.startsWith("{") && data.endsWith("}")) { - try { - const parsedData = JSON.parse(data); - callback(parsedData); - } catch { - // 如果再次解析失败,传递原始数据 - callback(data); - } - } else { - // 直接传递已解析的数据 - callback(data); - } - } catch (e) { - // 如果解析失败,传递原始消息 - console.warn("解析WebSocket消息失败,传递原始消息:", e); - callback(message.body); - } - } - }); - - // 保存订阅引用,以便后续取消订阅 - subscriptions.value.push(subscription); - - return subscription; - } catch (error) { - console.error("订阅失败:", error); - return null; + console.error("解析字典WebSocket消息失败:", error); } }; return { isConnected, - connectWebSocket, - disconnectWebSocket, - sendMessage, - subscribe, initWebSocket, closeWebSocket, handleDictEvent, diff --git a/src/plugins/websocket.ts b/src/plugins/websocket.ts index 55a4d403..520b10a3 100644 --- a/src/plugins/websocket.ts +++ b/src/plugins/websocket.ts @@ -8,6 +8,7 @@ export function setupWebSocket() { // 初始化字典WebSocket服务 dictWebSocket.initWebSocket(); + console.log("字典WebSocket初始化完成"); // 在窗口关闭前断开WebSocket连接 window.addEventListener("beforeunload", () => { From 6f9c4c64dee7c45b9737dc9814b90e11e3a004e9 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 24 Apr 2025 08:20:52 +0800 Subject: [PATCH 3/5] =?UTF-8?q?wip:=20=E5=AD=97=E5=85=B8=20websocket=20?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dict/index.vue | 14 +- src/hooks/useWebSocketDict.ts | 30 +- src/views/dashboard/index.vue | 4 +- src/views/demo/dict-websocket.vue | 442 +++++++++++++++++++----------- 4 files changed, 309 insertions(+), 181 deletions(-) diff --git a/src/components/Dict/index.vue b/src/components/Dict/index.vue index 14dbc9c7..79d98e4d 100644 --- a/src/components/Dict/index.vue +++ b/src/components/Dict/index.vue @@ -23,12 +23,7 @@ :style="style" @change="handleChange" > - + {{ option.label }} @@ -40,12 +35,7 @@ :style="style" @change="handleChange" > - + {{ option.label }} diff --git a/src/hooks/useWebSocketDict.ts b/src/hooks/useWebSocketDict.ts index 9570b701..3f2d5bd6 100644 --- a/src/hooks/useWebSocketDict.ts +++ b/src/hooks/useWebSocketDict.ts @@ -77,20 +77,44 @@ export function useWebSocketDict() { try { // 尝试解析消息 const eventData = JSON.parse(message.body) as DictEvent; + console.log( + `[WebSocket] 接收到字典事件: ${eventData.type}, 字典编码: ${eventData.dictCode}`, + eventData + ); if (eventData.type === "DICT_UPDATED") { // 删除缓存,强制重新加载 dictStore.removeDictItem(eventData.dictCode); - console.log(`字典 ${eventData.dictCode} 已更新,缓存已清除`); + console.log(`[WebSocket] 字典 ${eventData.dictCode} 已更新,缓存已清除`); ElMessage.success(`字典 ${eventData.dictCode} 已更新`); + + // 派发自定义事件,通知组件刷新数据 + window.dispatchEvent( + new CustomEvent("dict-updated", { + detail: { + dictCode: eventData.dictCode, + timestamp: eventData.timestamp, + }, + }) + ); } else if (eventData.type === "DICT_DELETED") { // 删除缓存 dictStore.removeDictItem(eventData.dictCode); - console.log(`字典 ${eventData.dictCode} 已删除,缓存已清除`); + console.log(`[WebSocket] 字典 ${eventData.dictCode} 已删除,缓存已清除`); ElMessage.warning(`字典 ${eventData.dictCode} 已删除`); + + // 派发自定义事件,通知组件刷新数据 + window.dispatchEvent( + new CustomEvent("dict-deleted", { + detail: { + dictCode: eventData.dictCode, + timestamp: eventData.timestamp, + }, + }) + ); } } catch (error) { - console.error("解析字典WebSocket消息失败:", error); + console.error("[WebSocket] 解析字典WebSocket消息失败:", error, message.body); } }; diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index 7f9f3c64..2271030e 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -210,8 +210,8 @@
访问趋势 - - + 近7天 + 近30天
diff --git a/src/views/demo/dict-websocket.vue b/src/views/demo/dict-websocket.vue index dd8dcd1d..ed5e4f82 100644 --- a/src/views/demo/dict-websocket.vue +++ b/src/views/demo/dict-websocket.vue @@ -3,124 +3,160 @@ - -

本示例展示了当字典数据在服务端更新时,如何通过WebSocket实时更新前端缓存。

-

- 当管理员修改字典数据后,其他在线用户的字典缓存将自动刷新,无需手动刷新页面。 -

+ + + 本示例展示WebSocket实时更新字典缓存的效果。您可以编辑"男"性别字典项,保存后后端将通过WebSocket通知所有客户端刷新缓存。 -
- - - - -
- - - - + + + + + +
+
+ + + + + + + + + + + + + + success + + + warning + + + danger + + + info + + + primary + + + 保存 + 重置 + - - - - - -
- - + +
+
+
- - - -
-
- 暂无WebSocket消息 -
-
-
-
- {{ msg.title }} - {{ formatTime(msg.time) }} -
-
{{ JSON.stringify(msg.data, null, 2) }}
+ + + + +
+
+ 暂无WebSocket消息 +
+
+
+
+ {{ msg.title }} + {{ formatTime(msg.time) }}
+
{{ JSON.stringify(msg.data, null, 2) }}
- - - -
+
+ + -
- - -

这里模拟后端管理员更新字典数据后发送WebSocket通知

-

注意:这只是前端模拟,实际应用中由后端触发

- - - - - - - - 字典更新 - 字典删除 - - - - 模拟发送WebSocket消息 - 清空字典缓存 - - -
-
+ + + + +
+
{{
+                JSON.stringify(dictStore.getDictItems("gender"), null, 2)
+              }}
+
+
+
+
From d3f34c278961dc6e0d4d11190d37ab16f448f5be Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Tue, 2 Dec 2025 13:51:01 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20default-passive?= =?UTF-8?q?-events=EF=BC=8C=E9=81=BF=E5=85=8D=E5=BC=BA=E5=88=B6=E8=AE=BE?= =?UTF-8?q?=E4=B8=BA=20passive=20=E7=9A=84=E7=9B=91=E5=90=AC=E4=B8=8E?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=86=85=E9=83=A8=20preventDefault=20?= =?UTF-8?q?=E5=86=B2=E7=AA=81=EF=BC=8C=E5=87=8F=E5=B0=91=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8F=B0=E8=AD=A6=E5=91=8A=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - src/main.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/package.json b/package.json index 6f2bb4e1..9ee9f5f8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "axios": "^1.13.2", "codemirror": "^5.65.20", "codemirror-editor-vue3": "^2.8.0", - "default-passive-events": "^2.0.0", "echarts": "^6.0.0", "element-plus": "^2.11.8", "exceljs": "^4.4.0", diff --git a/src/main.ts b/src/main.ts index 74fe30d9..253922b2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,9 +13,6 @@ import "uno.css"; // 过渡动画 import "animate.css"; -// 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告 -import "default-passive-events"; - const app = createApp(App); // 注册插件 app.use(setupPlugins); From 39912dfa88f0103594d66bda2b3a1d457374c014 Mon Sep 17 00:00:00 2001 From: white Date: Thu, 4 Dec 2025 12:29:45 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(mock):=20:bug:=20=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E5=A4=B4=E5=83=8F=E5=8E=8B=E7=BC=A9=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E3=80=81=E5=AF=BC=E8=88=AA=E6=A0=8F=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/NavBar/components/NavbarActions.vue | 8 +++++++- src/views/dashboard/index.vue | 11 +++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/layouts/components/NavBar/components/NavbarActions.vue b/src/layouts/components/NavBar/components/NavbarActions.vue index 3806c3d2..bcd27d1b 100644 --- a/src/layouts/components/NavBar/components/NavbarActions.vue +++ b/src/layouts/components/NavBar/components/NavbarActions.vue @@ -32,7 +32,13 @@