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 01/27] =?UTF-8?q?wip:=20=E4=B8=B4=E6=97=B6=E6=8F=90?= =?UTF-8?q?=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 02/27] =?UTF-8?q?wip:=20=E4=B8=B4=E6=97=B6=E6=8F=90?= =?UTF-8?q?=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 03/27] =?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 dc79401c1302a921eec76eadcfa8db3a2fb26a79 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Tue, 18 Nov 2025 18:25:21 +0800 Subject: [PATCH 04/27] feat(utils): add common utility functions and validation constants --- src/constants/index.ts | 88 ++++++++++ src/enums/common/dialog-enum.ts | 12 ++ src/enums/common/status-enum.ts | 22 +++ src/enums/index.ts | 4 + src/enums/system/user-enum.ts | 11 ++ src/utils/dom.ts | 51 ++++++ src/utils/download.ts | 73 ++++++++ src/utils/format.ts | 86 ++++++++++ src/utils/index.ts | 74 +++------ src/utils/validate.ts | 59 +++++++ src/views/login/index.vue | 112 ++++++++----- .../{DeptTree.vue => UserDeptTree.vue} | 0 .../{UserImport.vue => UserImportDialog.vue} | 0 src/views/system/user/index.vue | 157 ++++++------------ 14 files changed, 548 insertions(+), 201 deletions(-) create mode 100644 src/enums/common/dialog-enum.ts create mode 100644 src/enums/common/status-enum.ts create mode 100644 src/enums/system/user-enum.ts create mode 100644 src/utils/dom.ts create mode 100644 src/utils/download.ts create mode 100644 src/utils/format.ts create mode 100644 src/utils/validate.ts rename src/views/system/user/components/{DeptTree.vue => UserDeptTree.vue} (100%) rename src/views/system/user/components/{UserImport.vue => UserImportDialog.vue} (100%) diff --git a/src/constants/index.ts b/src/constants/index.ts index 1104d268..ef122d45 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -72,3 +72,91 @@ export const ALL_STORAGE_KEYS = { } as const; export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]; + +/** + * 表单验证规则常量 + * 提供常用的验证规则,减少重复代码 + * + * @example + * ```ts + * const rules = reactive({ + * username: [VALIDATORS.required("用户名不能为空")], + * email: [VALIDATORS.email], + * mobile: [VALIDATORS.mobile], + * }); + * ``` + */ +export const VALIDATORS = { + /** + * 必填验证 + * @param message 错误提示信息 + */ + required: (message: string) => ({ + required: true, + message, + trigger: "blur", + }), + + /** + * 邮箱格式验证 + */ + email: { + pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, + message: "请输入正确的邮箱地址", + trigger: "blur", + }, + + /** + * 手机号码验证(中国大陆) + */ + mobile: { + pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + message: "请输入正确的手机号码", + trigger: "blur", + }, + + /** + * 身份证号码验证(中国大陆) + */ + idCard: { + pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, + message: "请输入正确的身份证号码", + trigger: "blur", + }, + + /** + * URL 格式验证 + */ + url: { + pattern: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i, + message: "请输入正确的URL地址", + trigger: "blur", + }, + + /** + * 数字验证 + */ + number: { + pattern: /^\d+$/, + message: "请输入数字", + trigger: "blur", + }, + + /** + * 整数验证(正整数、负整数、0) + */ + integer: { + pattern: /^-?\d+$/, + message: "请输入整数", + trigger: "blur", + }, + + /** + * 正整数验证 + */ + positiveInteger: { + pattern: /^[1-9]\d*$/, + message: "请输入正整数", + trigger: "blur", + }, +} as const; diff --git a/src/enums/common/dialog-enum.ts b/src/enums/common/dialog-enum.ts new file mode 100644 index 00000000..90c983cc --- /dev/null +++ b/src/enums/common/dialog-enum.ts @@ -0,0 +1,12 @@ +/** + * 通用对话框模式枚举 + * @description 定义对话框的操作模式(创建、编辑、查看) + */ +export enum DialogMode { + /** 创建模式 - 新增数据 */ + CREATE = "create", + /** 编辑模式 - 修改数据 */ + EDIT = "edit", + /** 查看模式 - 只读展示 */ + VIEW = "view", +} diff --git a/src/enums/common/status-enum.ts b/src/enums/common/status-enum.ts new file mode 100644 index 00000000..cce10afd --- /dev/null +++ b/src/enums/common/status-enum.ts @@ -0,0 +1,22 @@ +/** + * 通用状态枚举 + * 适用于大多数业务实体的启用/禁用状态 + */ +export enum CommonStatus { + /** 禁用 */ + DISABLED = 0, + /** 启用 */ + ENABLED = 1, +} + +/** + * 审核状态枚举 + */ +export enum AuditStatus { + /** 待审核 */ + PENDING = 0, + /** 已通过 */ + APPROVED = 1, + /** 已拒绝 */ + REJECTED = 2, +} diff --git a/src/enums/index.ts b/src/enums/index.ts index 79189297..c7f6fa99 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -8,4 +8,8 @@ export * from "./settings/theme-enum"; export * from "./settings/locale-enum"; export * from "./settings/device-enum"; +export * from "./common/dialog-enum"; +export * from "./common/status-enum"; + export * from "./system/menu-enum"; +export * from "./system/user-enum"; diff --git a/src/enums/system/user-enum.ts b/src/enums/system/user-enum.ts new file mode 100644 index 00000000..af4258c5 --- /dev/null +++ b/src/enums/system/user-enum.ts @@ -0,0 +1,11 @@ +/** + * 用户性别枚举 + */ +export enum UserGender { + /** 未知 */ + UNKNOWN = 0, + /** 男 */ + MALE = 1, + /** 女 */ + FEMALE = 2, +} diff --git a/src/utils/dom.ts b/src/utils/dom.ts new file mode 100644 index 00000000..9c27924c --- /dev/null +++ b/src/utils/dom.ts @@ -0,0 +1,51 @@ +/** + * DOM 操作相关工具函数 + */ + +/** + * 检查元素是否包含指定 class + * @param ele HTML 元素 + * @param cls class 名称 + * @returns 是否包含 + * + * @example + * ```ts + * const hasActiveClass = hasClass(element, 'active'); + * ``` + */ +export function hasClass(ele: HTMLElement, cls: string): boolean { + return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); +} + +/** + * 为元素添加 class + * @param ele HTML 元素 + * @param cls class 名称 + * + * @example + * ```ts + * addClass(element, 'active'); + * ``` + */ +export function addClass(ele: HTMLElement, cls: string): void { + if (!hasClass(ele, cls)) { + ele.className += " " + cls; + } +} + +/** + * 从元素移除 class + * @param ele HTML 元素 + * @param cls class 名称 + * + * @example + * ```ts + * removeClass(element, 'active'); + * ``` + */ +export function removeClass(ele: HTMLElement, cls: string): void { + if (hasClass(ele, cls)) { + const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); + ele.className = ele.className.replace(reg, " "); + } +} diff --git a/src/utils/download.ts b/src/utils/download.ts new file mode 100644 index 00000000..f1f6ac6a --- /dev/null +++ b/src/utils/download.ts @@ -0,0 +1,73 @@ +/** + * 文件下载工具函数 + */ + +/** + * 从响应头中提取文件名 + * @param contentDisposition Content-Disposition 响应头 + * @returns 解码后的文件名 + */ +function extractFileName(contentDisposition: string): string { + if (!contentDisposition) { + return `download_${Date.now()}`; + } + + // 尝试从 filename*=UTF-8'' 格式中提取 + const filenameRegex = /filename\*=UTF-8''(.+)/; + const matches = filenameRegex.exec(contentDisposition); + if (matches && matches[1]) { + return decodeURIComponent(matches[1]); + } + + // 尝试从 filename= 格式中提取 + const fallbackRegex = /filename=([^;]+)/; + const fallbackMatches = fallbackRegex.exec(contentDisposition); + if (fallbackMatches && fallbackMatches[1]) { + return decodeURI(fallbackMatches[1].replace(/"/g, "")); + } + + return `download_${Date.now()}`; +} + +/** + * 下载文件 + * @param response Axios 响应对象 + * @param customFileName 自定义文件名(可选) + * + * @example + * ```ts + * // 基础用法 + * const response = await UserAPI.export(queryParams); + * downloadFile(response); + * + * // 自定义文件名 + * downloadFile(response, "用户列表.xlsx"); + * ``` + */ +export function downloadFile(response: any, customFileName?: string): void { + try { + const fileData = response.data; + const contentDisposition = response.headers["content-disposition"]; + const fileName = customFileName || extractFileName(contentDisposition); + + // 创建 Blob 对象 + const blob = new Blob([fileData]); + + // 创建下载链接 + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = downloadUrl; + link.download = fileName; + + // 触发下载 + document.body.appendChild(link); + link.click(); + + // 清理 + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + } catch (error) { + console.error("文件下载失败:", error); + throw error; + } +} diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 00000000..a6c59598 --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,86 @@ +/** + * 数据格式化相关工具函数 + */ + +/** + * 格式化增长率 + * 保留两位小数,去掉末尾的 0,取绝对值 + * + * @param growthRate 增长率(小数形式,如 0.15 表示 15%) + * @returns 格式化后的增长率字符串 + * + * @example + * ```ts + * formatGrowthRate(0.1234); // "12.34%" + * formatGrowthRate(0.1000); // "10%" + * formatGrowthRate(0); // "-" + * formatGrowthRate(-0.05); // "5%"(取绝对值) + * ``` + */ +export function formatGrowthRate(growthRate: number): string { + if (growthRate === 0) { + return "-"; + } + + const formattedRate = Math.abs(growthRate * 100) + .toFixed(2) + .replace(/\.?0+$/, ""); + + return formattedRate + "%"; +} + +/** + * 格式化文件大小 + * @param bytes 字节数 + * @param decimals 保留小数位数,默认 2 + * @returns 格式化后的文件大小字符串 + * + * @example + * ```ts + * formatFileSize(1024); // "1 KB" + * formatFileSize(1048576); // "1 MB" + * formatFileSize(1234567); // "1.18 MB" + * ``` + */ +export function formatFileSize(bytes: number, decimals: number = 2): string { + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i]; +} + +/** + * 格式化数字,添加千分位分隔符 + * @param num 数字 + * @returns 格式化后的字符串 + * + * @example + * ```ts + * formatNumber(1234567); // "1,234,567" + * formatNumber(1234567.89); // "1,234,567.89" + * ``` + */ +export function formatNumber(num: number): string { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + +/** + * 格式化金额(人民币) + * @param amount 金额 + * @param decimals 保留小数位数,默认 2 + * @returns 格式化后的金额字符串 + * + * @example + * ```ts + * formatCurrency(1234567); // "¥1,234,567.00" + * formatCurrency(1234567.8); // "¥1,234,567.80" + * formatCurrency(1234567, 0); // "¥1,234,567" + * ``` + */ +export function formatCurrency(amount: number, decimals: number = 2): string { + const formatted = amount.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return "¥" + formatted; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 67fce789..64f1209f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,58 +1,26 @@ /** - * Check if an element has a class - * @param {HTMLElement} ele - * @param {string} cls - * @returns {boolean} - */ -export function hasClass(ele: HTMLElement, cls: string) { - return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); -} - -/** - * Add class to element - * @param {HTMLElement} ele - * @param {string} cls - */ -export function addClass(ele: HTMLElement, cls: string) { - if (!hasClass(ele, cls)) ele.className += " " + cls; -} - -/** - * Remove class from element - * @param {HTMLElement} ele - * @param {string} cls - */ -export function removeClass(ele: HTMLElement, cls: string) { - if (hasClass(ele, cls)) { - const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); - ele.className = ele.className.replace(reg, " "); - } -} - -/** - * 判断是否是外部链接 + * 工具函数统一导出 * - * @param {string} path - * @returns {Boolean} + * 本文件作为 barrel export,统一管理所有工具函数的导出 + * 各类工具函数按功能分类存放在不同文件中: + * - dom.ts: DOM 操作相关 + * - validate.ts: 数据验证相关 + * - format.ts: 数据格式化相关 + * - download.ts: 文件下载相关 + * - auth.ts: 权限认证相关 + * - storage.ts: 本地存储相关 + * - request.ts: 网络请求相关 + * - theme.ts: 主题相关 */ -export function isExternal(path: string) { - const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path); - return isExternal; -} -/** - * 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值 - * - * @param growthRate - * @returns - */ -export function formatGrowthRate(growthRate: number) { - if (growthRate === 0) { - return "-"; - } +// DOM 操作 +export { hasClass, addClass, removeClass } from "./dom"; - const formattedRate = Math.abs(growthRate * 100) - .toFixed(2) - .replace(/\.?0+$/, ""); - return formattedRate + "%"; -} +// 数据验证 +export { isExternal, isValidURL, isEmail, isMobile } from "./validate"; + +// 数据格式化 +export { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "./format"; + +// 文件下载 +export { downloadFile } from "./download"; diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 00000000..88225f35 --- /dev/null +++ b/src/utils/validate.ts @@ -0,0 +1,59 @@ +/** + * 数据验证相关工具函数 + */ + +/** + * 判断是否是外部链接 + * @param path 路径字符串 + * @returns 是否是外部链接 + * + * @example + * ```ts + * isExternal('https://example.com'); // true + * isExternal('/dashboard'); // false + * isExternal('mailto:admin@example.com'); // true + * ``` + */ +export function isExternal(path: string): boolean { + return /^(https?:|http?:|mailto:|tel:)/.test(path); +} + +/** + * 判断是否是有效的 URL + * @param url URL 字符串 + * @returns 是否是有效 URL + * + * @example + * ```ts + * isValidURL('https://example.com'); // true + * isValidURL('not a url'); // false + * ``` + */ +export function isValidURL(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +/** + * 判断是否是邮箱地址 + * @param email 邮箱字符串 + * @returns 是否是有效邮箱 + */ +export function isEmail(email: string): boolean { + const pattern = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/; + return pattern.test(email); +} + +/** + * 判断是否是手机号码(中国大陆) + * @param mobile 手机号字符串 + * @returns 是否是有效手机号 + */ +export function isMobile(mobile: string): boolean { + const pattern = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/; + return pattern.test(mobile); +} diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 856f61ef..df723e32 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -1,7 +1,7 @@ @@ -52,25 +52,21 @@ import DarkModeSwitch from "@/components/DarkModeSwitch/index.vue"; type LayoutMap = "login" | "register" | "resetPwd"; -const t = useI18n().t; +const { t } = useI18n(); +const component = ref("login"); -const component = ref("login"); // 切换显示的组件 const formComponents = { login: defineAsyncComponent(() => import("./components/Login.vue")), register: defineAsyncComponent(() => import("./components/Register.vue")), resetPwd: defineAsyncComponent(() => import("./components/ResetPwd.vue")), }; -// 投票通知 -const voteUrl = "https://gitee.com/activity/2025opensource?ident=I6VXEH"; -// 保存通知实例,用于在组件卸载时关闭 let notificationInstance: ReturnType | null = null; -// 显示投票通知 const showVoteNotification = () => { notificationInstance = ElNotification({ title: "⭐ Gitee 2025 开源评选 · 诚邀您的支持! 🙏", - message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!
点击投票 →`, + message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!
点击投票 →`, type: "success", position: "bottom-right", duration: 0, @@ -78,14 +74,10 @@ const showVoteNotification = () => { }); }; -// 延迟显示 onMounted(() => { - setTimeout(() => { - showVoteNotification(); - }, 500); + setTimeout(showVoteNotification, 500); }); -// 组件卸载时关闭通知 onBeforeUnmount(() => { if (notificationInstance) { notificationInstance.close(); @@ -95,6 +87,8 @@ onBeforeUnmount(() => { diff --git a/src/views/system/user/components/DeptTree.vue b/src/views/system/user/components/UserDeptTree.vue similarity index 100% rename from src/views/system/user/components/DeptTree.vue rename to src/views/system/user/components/UserDeptTree.vue diff --git a/src/views/system/user/components/UserImport.vue b/src/views/system/user/components/UserImportDialog.vue similarity index 100% rename from src/views/system/user/components/UserImport.vue rename to src/views/system/user/components/UserImportDialog.vue diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue index b8b9bfd4..8ee23dd5 100644 --- a/src/views/system/user/index.vue +++ b/src/views/system/user/index.vue @@ -4,7 +4,7 @@ - + @@ -90,7 +90,7 @@ @@ -166,8 +166,8 @@ @@ -240,7 +240,7 @@ - +
@@ -250,32 +250,35 @@ import { computed, onMounted, reactive, ref } from "vue"; import { useDebounceFn } from "@vueuse/core"; // ==================== 2. Element Plus ==================== -import { ElMessage, ElMessageBox } from "element-plus"; +import { ElMessage, ElMessageBox, type FormInstance } from "element-plus"; // ==================== 3. 类型定义 ==================== import type { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user-api"; + +// ==================== 3.5 工具函数 ==================== +import { downloadFile } from "@/utils"; +import { VALIDATORS } from "@/constants"; // ==================== 4. API 服务 ==================== import UserAPI from "@/api/system/user-api"; import DeptAPI from "@/api/system/dept-api"; import RoleAPI from "@/api/system/role-api"; // ==================== 5. Store ==================== -import { useAppStore } from "@/store/modules/app-store"; -import { useUserStore } from "@/store"; +import { useUserStore, useAppStore } from "@/store"; // ==================== 6. Enums ==================== -import { DeviceEnum } from "@/enums/settings/device-enum"; +import { DeviceEnum, DialogMode, CommonStatus } from "@/enums"; // ==================== 7. Composables ==================== import { useAiAction, useTableSelection } from "@/composables"; // ==================== 8. 组件 ==================== -import DeptTree from "./components/DeptTree.vue"; -import UserImport from "./components/UserImport.vue"; +import UserDeptTree from "./components/UserDeptTree.vue"; +import UserImportDialog from "./components/UserImportDialog.vue"; // ==================== 组件配置 ==================== defineOptions({ - name: "SystemUser", + name: "User", inheritAttrs: false, }); @@ -286,8 +289,8 @@ const userStore = useUserStore(); // ==================== 响应式状态 ==================== // DOM 引用 -const queryFormRef = ref(); -const userFormRef = ref(); +const queryFormRef = ref(); +const userFormRef = ref(); // 列表查询参数 const queryParams = reactive({ @@ -296,20 +299,24 @@ const queryParams = reactive({ }); // 列表数据 -const pageData = ref(); +const userList = ref([]); const total = ref(0); const loading = ref(false); // 弹窗状态 -const dialog = reactive({ +const dialogState = reactive({ visible: false, title: "新增用户", + mode: DialogMode.CREATE, }); +// 初始表单数据 +const initialFormData: UserForm = { + status: CommonStatus.ENABLED, +}; + // 表单数据 -const formData = reactive({ - status: 1, -}); +const formData = reactive({ ...initialFormData }); // 下拉选项数据 const deptOptions = ref(); @@ -328,48 +335,12 @@ const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600 // ==================== 表单验证规则 ==================== const rules = reactive({ - username: [ - { - required: true, - message: "用户名不能为空", - trigger: "blur", - }, - ], - nickname: [ - { - required: true, - message: "用户昵称不能为空", - trigger: "blur", - }, - ], - deptId: [ - { - required: true, - message: "所属部门不能为空", - trigger: "blur", - }, - ], - roleIds: [ - { - required: true, - message: "用户角色不能为空", - trigger: "blur", - }, - ], - email: [ - { - pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, - message: "请输入正确的邮箱地址", - trigger: "blur", - }, - ], - mobile: [ - { - pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, - message: "请输入正确的手机号码", - trigger: "blur", - }, - ], + username: [VALIDATORS.required("用户名不能为空")], + nickname: [VALIDATORS.required("用户昵称不能为空")], + deptId: [VALIDATORS.required("所属部门不能为空")], + roleIds: [VALIDATORS.required("用户角色不能为空")], + email: [VALIDATORS.email], + mobile: [VALIDATORS.mobile], }); // ==================== 数据加载 ==================== @@ -381,7 +352,7 @@ async function fetchUserList(): Promise { loading.value = true; try { const data = await UserAPI.getPage(queryParams); - pageData.value = data.list; + userList.value = data.list; total.value = data.total; } catch (error) { ElMessage.error("获取用户列表失败"); @@ -408,7 +379,7 @@ function handleQuery(): Promise { * 重置查询条件 */ function handleResetQuery(): void { - queryFormRef.value.resetFields(); + queryFormRef.value?.resetFields(); queryParams.deptId = undefined; queryParams.createTime = undefined; handleQuery(); @@ -446,7 +417,7 @@ function handleResetPassword(row: UserPageVO): void { * @param id 用户ID(编辑时传入) */ async function handleOpenDialog(id?: string): Promise { - dialog.visible = true; + dialogState.visible = true; // 并行加载下拉选项数据 try { @@ -461,7 +432,8 @@ async function handleOpenDialog(id?: string): Promise { // 编辑:加载用户数据 if (id) { - dialog.title = "修改用户"; + dialogState.title = "修改用户"; + dialogState.mode = DialogMode.EDIT; try { const data = await UserAPI.getFormData(id); Object.assign(formData, data); @@ -471,7 +443,8 @@ async function handleOpenDialog(id?: string): Promise { } } else { // 新增:设置默认值 - dialog.title = "新增用户"; + dialogState.title = "新增用户"; + dialogState.mode = DialogMode.CREATE; } } @@ -479,20 +452,21 @@ async function handleOpenDialog(id?: string): Promise { * 关闭用户表单弹窗 */ function handleCloseDialog(): void { - dialog.visible = false; - userFormRef.value.resetFields(); - userFormRef.value.clearValidate(); + dialogState.visible = false; - // 重置表单数据 - formData.id = undefined; - formData.status = 1; + // 安全地重置表单 + userFormRef.value?.resetFields(); + userFormRef.value?.clearValidate(); + + // 完全重置表单数据 + Object.assign(formData, initialFormData); } /** * 提交用户表单(防抖) */ const handleSubmit = useDebounceFn(async () => { - const valid = await userFormRef.value.validate().catch(() => false); + const valid = await userFormRef.value?.validate().catch(() => false); if (!valid) return; const userId = formData.id; @@ -514,14 +488,14 @@ const handleSubmit = useDebounceFn(async () => { } finally { loading.value = false; } -}, 1000); +}, 300); /** * 删除用户 * @param id 用户ID(单个删除时传入) */ function handleDelete(id?: string): void { - const userIds = id ? id : selectedIds.value.join(","); + const userIds = id ?? selectedIds.value.join(","); if (!userIds) { ElMessage.warning("请勾选删除项"); @@ -579,27 +553,7 @@ function handleOpenImportDialog(): void { async function handleExport(): Promise { try { const response = await UserAPI.export(queryParams); - const fileData = response.data; - const contentDisposition = response.headers["content-disposition"]; - const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]); - const fileType = - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; - - // 创建下载链接 - const blob = new Blob([fileData], { type: fileType }); - const downloadUrl = window.URL.createObjectURL(blob); - const downloadLink = document.createElement("a"); - downloadLink.href = downloadUrl; - downloadLink.download = fileName; - - // 触发下载 - document.body.appendChild(downloadLink); - downloadLink.click(); - - // 清理 - document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); - + downloadFile(response); ElMessage.success("导出成功"); } catch (error) { ElMessage.error("导出失败"); @@ -656,9 +610,6 @@ useAiAction({ /** * 组件挂载时初始化数据 - * - * 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字), - * useAiAction 会在 nextTick 中再次执行搜索,这是预期行为 */ onMounted(() => { handleQuery(); From 7dff18863dd4223cb1949af819d2a9f8a57627b5 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 27 Nov 2025 13:32:27 +0800 Subject: [PATCH 05/27] =?UTF-8?q?refactor(websocket):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=89=93=E5=8D=B0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/websocket/useDictSync.ts | 16 +++---- src/composables/websocket/useOnlineCount.ts | 34 +------------- src/composables/websocket/useStomp.ts | 52 ++++++++++++++++++++- src/plugins/websocket.ts | 33 +++++++------ 4 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/composables/websocket/useDictSync.ts b/src/composables/websocket/useDictSync.ts index f465ac0b..1c0d1875 100644 --- a/src/composables/websocket/useDictSync.ts +++ b/src/composables/websocket/useDictSync.ts @@ -69,8 +69,6 @@ function createDictSyncComposable() { return; } - console.log(`[DictSync] 字典 "${dictCode}" 已更新,清除本地缓存`); - // 清除缓存,等待按需加载 dictStore.removeDictItem(dictCode); @@ -98,7 +96,7 @@ function createDictSyncComposable() { return; } - console.log("[DictSync] 初始化字典同步服务..."); + // console.log("[DictSync] 初始化字典同步服务..."); // 高频日志已禁用 // 建立 WebSocket 连接 stomp.connect(); @@ -106,19 +104,17 @@ function createDictSyncComposable() { // 订阅字典主题(useStomp 会自动处理重连后的订阅恢复) subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage); - if (subscriptionId) { - console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`); - } else { - console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`); - } + // if (subscriptionId) { + // console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`); + // } else { + // console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`); + // } }; /** * 关闭 WebSocket 连接并清理资源 */ const cleanup = () => { - console.log("[DictSync] 清理字典同步服务..."); - // 取消订阅(如果有的话) if (subscriptionId) { stomp.unsubscribe(subscriptionId); diff --git a/src/composables/websocket/useOnlineCount.ts b/src/composables/websocket/useOnlineCount.ts index a9b5fe74..ccef203f 100644 --- a/src/composables/websocket/useOnlineCount.ts +++ b/src/composables/websocket/useOnlineCount.ts @@ -1,4 +1,4 @@ -import { ref, watch, onMounted, onUnmounted, getCurrentInstance } from "vue"; +import { ref, onMounted, onUnmounted, getCurrentInstance } from "vue"; import { useStomp } from "./useStomp"; import { registerWebSocketInstance } from "@/plugins/websocket"; import { AuthStorage } from "@/utils/auth"; @@ -59,7 +59,6 @@ function createOnlineCountComposable() { if (count !== undefined && !isNaN(count)) { onlineUserCount.value = count; lastUpdateTime.value = Date.now(); - console.log(`[useOnlineCount] 在线用户数更新: ${count}`); } else { console.warn("[useOnlineCount] 收到无效的在线用户数:", data); } @@ -73,18 +72,11 @@ function createOnlineCountComposable() { */ const subscribeToOnlineCount = () => { if (subscriptionId) { - console.log("[useOnlineCount] 已存在订阅,跳过"); return; } // 订阅在线用户计数主题(useStomp 会处理重连后的订阅恢复) subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage); - - if (subscriptionId) { - console.log(`[useOnlineCount] 已订阅主题: ${ONLINE_COUNT_TOPIC}`); - } else { - console.log(`[useOnlineCount] 暂存订阅配置,等待连接建立后自动订阅`); - } }; /** @@ -105,8 +97,6 @@ function createOnlineCountComposable() { return; } - console.log("[useOnlineCount] 初始化在线用户计数服务..."); - // 建立 WebSocket 连接 stomp.connect(); @@ -118,8 +108,6 @@ function createOnlineCountComposable() { * 关闭 WebSocket 连接并清理资源 */ const cleanup = () => { - console.log("[useOnlineCount] 清理在线用户计数服务..."); - // 取消订阅 if (subscriptionId) { stomp.unsubscribe(subscriptionId); @@ -137,19 +125,6 @@ function createOnlineCountComposable() { lastUpdateTime.value = 0; }; - // 监听连接状态变化 - watch( - stomp.isConnected, - (connected) => { - if (connected) { - console.log("[useOnlineCount] WebSocket 已连接"); - } else { - console.log("[useOnlineCount] WebSocket 已断开"); - } - }, - { immediate: false } - ); - return { // 状态 onlineUserCount: readonly(onlineUserCount), @@ -200,17 +175,12 @@ export function useOnlineCount(options: { autoInit?: boolean } = {}) { onMounted(() => { // 只有在未连接时才尝试初始化 if (!globalInstance!.isConnected.value) { - console.log("[useOnlineCount] 组件挂载,初始化 WebSocket 连接"); globalInstance!.initialize(); - } else { - console.log("[useOnlineCount] WebSocket 已连接,跳过初始化"); } }); // 注意:不在卸载时关闭连接,保持全局连接 - onUnmounted(() => { - console.log("[useOnlineCount] 组件卸载(保持 WebSocket 连接)"); - }); + onUnmounted(() => {}); } return globalInstance; diff --git a/src/composables/websocket/useStomp.ts b/src/composables/websocket/useStomp.ts index 88f42a3a..43f05dbf 100644 --- a/src/composables/websocket/useStomp.ts +++ b/src/composables/websocket/useStomp.ts @@ -20,6 +20,16 @@ export interface UseStompOptions { debug?: boolean; /** 是否在重连时自动恢复订阅,默认为 true */ autoRestoreSubscriptions?: boolean; + /** + * 心跳接收间隔,单位毫秒,默认为 4000 + * 注意:标签页失活时,浏览器会节流定时器,建议设置较长的间隔(如 10000)以减少失活影响 + */ + heartbeatIncoming?: number; + /** + * 心跳发送间隔,单位毫秒,默认为 4000 + * 注意:标签页失活时,浏览器会节流定时器,建议设置较长的间隔(如 10000)以减少失活影响 + */ + heartbeatOutgoing?: number; } /** @@ -65,6 +75,8 @@ export function useStomp(options: UseStompOptions = {}) { maxReconnectDelay: options.maxReconnectDelay ?? 60000, autoRestoreSubscriptions: options.autoRestoreSubscriptions ?? true, debug: options.debug ?? false, + heartbeatIncoming: options.heartbeatIncoming ?? 4000, + heartbeatOutgoing: options.heartbeatOutgoing ?? 4000, }; // ==================== 状态管理 ==================== @@ -179,8 +191,8 @@ export function useStomp(options: UseStompOptions = {}) { }, debug: config.debug ? (msg) => console.log("[STOMP]", msg) : () => {}, reconnectDelay: 0, // 禁用内置重连,使用自定义重连逻辑 - heartbeatIncoming: 4000, - heartbeatOutgoing: 4000, + heartbeatIncoming: config.heartbeatIncoming, + heartbeatOutgoing: config.heartbeatOutgoing, }); // ==================== 事件监听器 ==================== @@ -312,6 +324,41 @@ export function useStomp(options: UseStompOptions = {}) { // 初始化客户端 initializeClient(); + // ==================== 标签页可见性监听 ==================== + + /** + * 处理标签页可见性变化 + * 当标签页从失活变为激活时,检查连接状态并尝试重连 + */ + const handleVisibilityChange = () => { + if (document.hidden) { + log("标签页已失活"); + } else { + log("标签页已激活,检查WebSocket连接状态..."); + + // 标签页激活时,检查连接状态 + if (stompClient.value && !stompClient.value.connected && !isManualDisconnect) { + logWarn("检测到WebSocket连接已断开,尝试重新连接..."); + // 重置重连次数,给予更多重连机会 + reconnectAttempts.value = 0; + connect(); + } + } + }; + + // 监听标签页可见性变化 + if (typeof document !== "undefined") { + document.addEventListener("visibilitychange", handleVisibilityChange); + } + + // 清理函数:移除事件监听器 + const cleanup = () => { + if (typeof document !== "undefined") { + document.removeEventListener("visibilitychange", handleVisibilityChange); + } + disconnect(); + }; + // ==================== 公共接口 ==================== /** @@ -517,6 +564,7 @@ export function useStomp(options: UseStompOptions = {}) { // 连接管理 connect, disconnect, + cleanup, // 清理资源(包括移除事件监听器) // 订阅管理 subscribe, diff --git a/src/plugins/websocket.ts b/src/plugins/websocket.ts index ef39248b..adb7b690 100644 --- a/src/plugins/websocket.ts +++ b/src/plugins/websocket.ts @@ -1,20 +1,29 @@ import { useDictSync } from "@/composables"; import { AuthStorage } from "@/utils/auth"; -// 不直接导入 store 或 userStore + +/** + * WebSocket 服务实例约定接口 + * 至少包含 disconnect/closeWebSocket/cleanup 三者之一 + */ +type WebSocketService = { + disconnect?: () => void; + closeWebSocket?: () => void; + cleanup?: () => void; + [key: string]: any; +}; // 全局 WebSocket 实例管理 -const websocketInstances = new Map(); +const websocketInstances = new Map(); // 用于防止重复初始化的状态标记 let isInitialized = false; let dictWebSocketInstance: ReturnType | null = null; /** - * 注册 WebSocket 实例 + * 注册 WebSocket 实例,便于统一清理 */ -export function registerWebSocketInstance(key: string, instance: any) { +export function registerWebSocketInstance(key: string, instance: WebSocketService) { websocketInstances.set(key, instance); - console.log(`[WebSocketPlugin] Registered WebSocket instance: ${key}`); } /** @@ -28,11 +37,8 @@ export function getWebSocketInstance(key: string) { * 初始化WebSocket服务 */ export function setupWebSocket() { - console.log("[WebSocketPlugin] 开始初始化WebSocket服务..."); - // 检查是否已经初始化 if (isInitialized) { - console.log("[WebSocketPlugin] WebSocket服务已经初始化,跳过重复初始化"); return; } @@ -60,19 +66,14 @@ export function setupWebSocket() { // 初始化字典WebSocket服务 dictWebSocketInstance.initWebSocket(); - console.log("[WebSocketPlugin] 字典WebSocket初始化完成"); - // 初始化在线用户计数WebSocket import("@/composables").then(({ useOnlineCount }) => { const onlineCountInstance = useOnlineCount({ autoInit: false }); onlineCountInstance.initWebSocket(); - console.log("[WebSocketPlugin] 在线用户计数WebSocket初始化完成"); }); // 在窗口关闭前断开WebSocket连接 window.addEventListener("beforeunload", handleWindowClose); - - console.log("[WebSocketPlugin] WebSocket服务初始化完成"); isInitialized = true; }, 1000); // 延迟1秒初始化 } catch (error) { @@ -84,7 +85,6 @@ export function setupWebSocket() { * 处理窗口关闭 */ function handleWindowClose() { - console.log("[WebSocketPlugin] 窗口即将关闭,断开WebSocket连接"); cleanupWebSocket(); } @@ -96,7 +96,6 @@ export function cleanupWebSocket() { if (dictWebSocketInstance) { try { dictWebSocketInstance.closeWebSocket(); - console.log("[WebSocketPlugin] 字典WebSocket连接已断开"); } catch (error) { console.error("[WebSocketPlugin] 断开字典WebSocket连接失败:", error); } @@ -107,10 +106,10 @@ export function cleanupWebSocket() { try { if (instance && typeof instance.disconnect === "function") { instance.disconnect(); - console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`); } else if (instance && typeof instance.closeWebSocket === "function") { instance.closeWebSocket(); - console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`); + } else if (instance && typeof instance.cleanup === "function") { + instance.cleanup(); } } catch (error) { console.error(`[WebSocketPlugin] 断开 ${key} WebSocket连接失败:`, error); From b2ef3d1b1f8edf55a4edee6eaadce4d3cf8df0f1 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 27 Nov 2025 13:36:11 +0800 Subject: [PATCH 06/27] =?UTF-8?q?chore:=20=E5=90=88=E5=B9=B6=E5=88=86?= =?UTF-8?q?=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/demo/curd-single.vue | 2 +- src/views/demo/curd/index.vue | 2 +- tsconfig.json | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/views/demo/curd-single.vue b/src/views/demo/curd-single.vue index 04f3103b..98ed7868 100644 --- a/src/views/demo/curd-single.vue +++ b/src/views/demo/curd-single.vue @@ -42,7 +42,7 @@ diff --git a/src/views/demo/curd/index.vue b/src/views/demo/curd/index.vue index 6f5b1fd3..3960bd9c 100644 --- a/src/views/demo/curd/index.vue +++ b/src/views/demo/curd/index.vue @@ -45,7 +45,7 @@ diff --git a/tsconfig.json b/tsconfig.json index 475a9e32..4b4ebba7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,13 @@ "types": ["node", "vite/client", "element-plus/global"] }, - "include": ["mock/**/*.ts", "src/**/*.ts", "src/**/*.vue", "vite.config.ts", "eslint.config.ts"], + "include": [ + "mock/**/*.ts", + "src/**/*.ts", + "src/**/*.vue", + "vite.config.ts", + "eslint.config.ts", + "uno.config.ts" + ], "exclude": ["node_modules", "dist"] } From 793dca66b19a210bd2749fc37c524b4ab25d75a5 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 27 Nov 2025 14:38:35 +0800 Subject: [PATCH 07/27] refactor: unify system list layout with filter/table sections --- .cursor/mcp.json | 7 ------- src/components/AiAssistant/index.vue | 2 +- src/styles/element-plus.scss | 2 +- src/styles/index.scss | 20 +++++++++---------- src/views/system/config/index.vue | 10 +++++----- src/views/system/dept/index.vue | 10 +++++----- src/views/system/dict/dict-item.vue | 8 ++++---- src/views/system/dict/index.vue | 10 +++++----- src/views/system/menu/index.vue | 10 +++++----- .../system/notice/components/MyNotice.vue | 6 +++--- src/views/system/notice/index.vue | 10 +++++----- src/views/system/role/index.vue | 10 +++++----- src/views/system/user/index.vue | 14 +++++++------ 13 files changed, 57 insertions(+), 62 deletions(-) delete mode 100644 .cursor/mcp.json diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index b87707f0..00000000 --- a/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "vue-mcp": { - "url": "http://localhost:3000/__mcp/sse" - } - } -} diff --git a/src/components/AiAssistant/index.vue b/src/components/AiAssistant/index.vue index 5458bf59..b5b0f548 100644 --- a/src/components/AiAssistant/index.vue +++ b/src/components/AiAssistant/index.vue @@ -530,7 +530,7 @@ const executeAction = async (action: AiAction) => { // 关闭对话框 handleClose(); - }, 800); + }, 1000); } else if (action.type === "execute") { // 执行函数调用 ElMessage.info("功能开发中,请前往 AI 命令助手页面体验完整功能"); diff --git a/src/styles/element-plus.scss b/src/styles/element-plus.scss index fc5b729b..131ce3f2 100644 --- a/src/styles/element-plus.scss +++ b/src/styles/element-plus.scss @@ -18,7 +18,7 @@ $border: 1px solid var(--el-border-color-light); } } -/** el-drawer */ +/* el-drawer */ .el-drawer { .el-drawer__header { padding: 15px 20px; diff --git a/src/styles/index.scss b/src/styles/index.scss index 22655067..2cb97b9d 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -74,10 +74,10 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { } } -// 全局搜索区域样式 -.search-container { - padding: 18px 16px 0; - margin-bottom: 16px; +// 全局筛选区域样式 +.filter-section { + padding: 8px 12px 0; + margin-bottom: 8px; background-color: var(--el-bg-color-overlay); border: 1px solid var(--el-border-color-light); border-radius: 4px; @@ -87,24 +87,24 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { } .el-form-item { - margin-bottom: 18px; + margin-bottom: 8px; } } // 表格区域样式 -.data-table { - margin-bottom: 16px; +.table-section { + margin-bottom: 12px; // 表格工具栏区域 &__toolbar { display: flex; justify-content: space-between; - margin-bottom: 16px; + margin-bottom: 4px; &--actions, &--tools { display: flex; - gap: 8px; + gap: 4px; } } @@ -116,6 +116,6 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { // 分页区域 .el-pagination { justify-content: flex-end; - margin-top: 16px; + margin-top: 12px; } } diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue index 3035806a..85980d18 100644 --- a/src/views/system/config/index.vue +++ b/src/views/system/config/index.vue @@ -2,7 +2,7 @@