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] =?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 @@ + + + + +