diff --git a/z_ele/src/App.vue b/z_ele/src/App.vue index 4ce492f..4cb6b72 100644 --- a/z_ele/src/App.vue +++ b/z_ele/src/App.vue @@ -9,6 +9,7 @@ + @@ -18,8 +19,7 @@ import { useGlobalConfig } from '@/config/use-global-config'; import { useThemeStore } from '@/store/modules/theme'; import { useLocale } from '@/i18n/use-locale'; - import { inject } from 'vue'; - import {webSocket_handler} from "@/plugins/webSocket_handler"; + import WebsocketState from '@/components/WebsocketState/index.vue'; /** 组件全局配置 */ const { tableConfig } = useGlobalConfig(); @@ -31,6 +31,4 @@ /** 国际化配置 */ const { elLocale, eleLocale } = useLocale(); - // 通过inject获取WebSocket实例 - webSocket_handler(inject('websocket')) diff --git a/z_ele/src/components/LockScreenState/index.vue b/z_ele/src/components/LockScreenState/index.vue new file mode 100644 index 0000000..81c0c8e --- /dev/null +++ b/z_ele/src/components/LockScreenState/index.vue @@ -0,0 +1,29 @@ + + + diff --git a/z_ele/src/components/WebsocketState/index.vue b/z_ele/src/components/WebsocketState/index.vue new file mode 100644 index 0000000..379d151 --- /dev/null +++ b/z_ele/src/components/WebsocketState/index.vue @@ -0,0 +1,33 @@ + + + diff --git a/z_ele/src/main.ts b/z_ele/src/main.ts index c0d87be..e24a7ef 100644 --- a/z_ele/src/main.ts +++ b/z_ele/src/main.ts @@ -7,8 +7,8 @@ import DictData from '@/components/DictData/index.vue'; import i18n from './i18n'; import installer from './as-needed'; import { iconsInstaller } from '@/components/IconSelect/util'; -import WebSocketPlugin, { WebSocketConfig } from './plugins/webSocket_plugin'; - +import WebSocketPlugin from '@/plugins/websocket'; +import type { WebSocketConfig } from '@/plugins/websocket/type'; import 'element-plus/theme-chalk/display.css'; import 'ele-admin-plus/es/style/nprogress.scss'; import './styles/themes/rounded.scss'; @@ -27,12 +27,9 @@ const websocketConfig: Partial = { }; // 安装WebSocket插件 app.use(WebSocketPlugin, { - globalConfig: websocketConfig, - injectGlobal: true, - globalPropertyName: '$websocket' + globalConfig: websocketConfig }); - app.use(store); app.use(router); app.use(permission); diff --git a/z_ele/src/plugins/webSocket_handler.ts b/z_ele/src/plugins/webSocket_handler.ts deleted file mode 100644 index 64f7748..0000000 --- a/z_ele/src/plugins/webSocket_handler.ts +++ /dev/null @@ -1,16 +0,0 @@ - -export const webSocket_handler = (websocket: any) => { - const { - isConnected, - isConnecting, - connectionStatus, - messages, - statusClass, - connect, - disconnect, - sendMessage - } = websocket; - console.log(messages.value) -} - - diff --git a/z_ele/src/plugins/websocket/index.ts b/z_ele/src/plugins/websocket/index.ts new file mode 100644 index 0000000..f459260 --- /dev/null +++ b/z_ele/src/plugins/websocket/index.ts @@ -0,0 +1,29 @@ +// plugins/websocket +import { App, Plugin } from 'vue'; +import { + WebSocketConfig, + WebSocketPluginOptions +} from '@/plugins/websocket/type'; +import { initWebSocket } from '@/plugins/websocket/instance'; + +// 插件安装函数 +const WebSocketPlugin: Plugin = { + install(app: App, options: WebSocketPluginOptions = {}) { + const { globalConfig = {} } = options; + + // 合并默认配置和全局配置 + const defaultConfig: WebSocketConfig = { + url: 'wss://echo.websocket.org', + protocols: '', + reconnectAttempts: 5, + reconnectDelay: 3000, + autoConnect: false, + ...globalConfig + }; + // 创建WebSocket实例 + initWebSocket(defaultConfig); + console.debug('initWebSocket.', defaultConfig); + } +}; + +export default WebSocketPlugin; diff --git a/z_ele/src/plugins/webSocket_plugin.ts b/z_ele/src/plugins/websocket/instance.ts similarity index 56% rename from z_ele/src/plugins/webSocket_plugin.ts rename to z_ele/src/plugins/websocket/instance.ts index b9bea70..84a0147 100644 --- a/z_ele/src/plugins/webSocket_plugin.ts +++ b/z_ele/src/plugins/websocket/instance.ts @@ -1,45 +1,16 @@ -// plugins/WebSocketPlugin.ts -import { App, Plugin, ref, computed } from 'vue'; - -export enum ConnectionStatus { - CONNECTING = 'connecting', - CONNECTED = 'connected', - DISCONNECTED = 'disconnected', - ERROR = 'error' -} - -export enum MessageType { - INCOMING = 'incoming', - OUTGOING = 'outgoing', - SYSTEM = 'system' -} - -export interface WebSocketMessage { - content: string; - type: MessageType; - timestamp: Date; -} - -export interface WebSocketConfig { - url: string; - protocols?: string; - reconnectAttempts: number; - reconnectDelay: number; - autoConnect?: boolean; -} - -// 插件选项接口 -export interface WebSocketPluginOptions { - // 全局配置 - globalConfig?: Partial; - // 是否注入全局实例 - injectGlobal?: boolean; - // 全局属性名 - globalPropertyName?: string; -} +import { + ConnectionStatus, MessageType, + PaWebSocket, + SendEventType, + WebSocketConfig, + WebSocketMessage, + WsEvent +} from '@/plugins/websocket/type'; +import { subscriptionManager } from '@/plugins/websocket/subscriber'; +import { computed, ref } from 'vue'; // 创建WebSocket实例的函数 -export function createWebSocket(config: WebSocketConfig) { +function createWebSocket(config: WebSocketConfig) { // 响应式数据 const socket = ref(null); const isConnected = ref(false); @@ -51,9 +22,12 @@ export function createWebSocket(config: WebSocketConfig) { // 计算属性 const statusClass = computed(() => { switch (connectionStatus.value) { - case ConnectionStatus.CONNECTED: return 'status-connected'; - case ConnectionStatus.CONNECTING: return 'status-connecting'; - default: return 'status-disconnected'; + case ConnectionStatus.CONNECTED: + return 'status-connected'; + case ConnectionStatus.CONNECTING: + return 'status-connecting'; + default: + return 'status-disconnected'; } }); @@ -95,17 +69,23 @@ export function createWebSocket(config: WebSocketConfig) { const reconnect = () => { if (reconnectCount.value < config.reconnectAttempts) { reconnectCount.value++; - addSystemMessage(`尝试重新连接 (${reconnectCount.value}/${config.reconnectAttempts})...`); + addSystemMessage( + `尝试重新连接 (${reconnectCount.value}/${config.reconnectAttempts})...` + ); setTimeout(() => connect(), config.reconnectDelay); } else { addSystemMessage('已达到最大重连次数,连接终止'); } }; - const sendMessage = (content: string) => { - if (socket.value && isConnected.value && content) { - socket.value.send(content); - addMessage(content, MessageType.OUTGOING); + const sendMessage = (event: SendEventType, data: any) => { + if (socket.value && isConnected.value && data) { + const message: string = JSON.stringify({ event, data }); + socket.value.send(message); + /* + * 扔进最近的消息记录 + */ + addMessage(message, MessageType.OUTGOING); } }; @@ -132,6 +112,8 @@ export function createWebSocket(config: WebSocketConfig) { connectionStatus.value = ConnectionStatus.CONNECTED; reconnectCount.value = 0; addSystemMessage('连接已建立'); + // 触发连接成功事件 + subscriptionManager.trigger(WsEvent.CONNECTED, e); }; const onMessage = (event: MessageEvent) => { @@ -163,56 +145,27 @@ export function createWebSocket(config: WebSocketConfig) { if (config.autoConnect) { connect(); } - - return { + return { // 状态 isConnected, isConnecting, connectionStatus, messages, statusClass, - // 方法 connect, disconnect, sendMessage, - clearMessages + clearMessages, + subscription: subscriptionManager }; } -// 插件安装函数 -const WebSocketPlugin: Plugin = { - install(app: App, options: WebSocketPluginOptions = {}) { - const { - globalConfig = {}, - injectGlobal = false, - globalPropertyName = '$websocket' - } = options; +export let websocketInstance: PaWebSocket | null = null; - // 合并默认配置和全局配置 - const defaultConfig: WebSocketConfig = { - url: 'wss://echo.websocket.org', - protocols: '', - reconnectAttempts: 5, - reconnectDelay: 3000, - autoConnect: false, - ...globalConfig - }; - - // 创建WebSocket实例 - const websocketInstance = createWebSocket(defaultConfig); - - // 如果需要,注入全局实例 - if (injectGlobal) { - app.config.globalProperties[globalPropertyName] = websocketInstance; - } - - // 提供WebSocket实例,以便组件通过inject使用 - app.provide('websocket', websocketInstance); - - // 注册全局组件(如果需要) - // app.component('WebSocketComponent', WebSocketComponent); +export function initWebSocket(config: WebSocketConfig): PaWebSocket { + if (!websocketInstance) { + websocketInstance = createWebSocket(config); } -}; - -export default WebSocketPlugin; + return websocketInstance; +} diff --git a/z_ele/src/plugins/websocket/subscriber.ts b/z_ele/src/plugins/websocket/subscriber.ts new file mode 100644 index 0000000..0f10b5e --- /dev/null +++ b/z_ele/src/plugins/websocket/subscriber.ts @@ -0,0 +1,81 @@ +/* + * 事件订阅管理器 + * 用于统一管理WebSocket事件订阅与触发 + */ + +import { WsEvent } from '@/plugins/websocket/type'; +import { generateGUID } from '@/utils/common'; +import { websocketInstance } from '@/plugins/websocket/instance'; + +// 重命名接口,使用更具描述性的名称 +export interface EventListener { + handler: Function; + id: string; +} + +export interface SubscriptionContext { + id: string; + destroy(): void; +} + +export interface SubscriptionManager { + events: Record; + addListener(event: WsEvent, listener: Function): void; + removeListener(event: WsEvent, listenerId: string): void; + trigger(event: WsEvent, data: any): void; +} + +class SubscriptionManagerImpl implements SubscriptionManager { + events: Record = { + // 全局事件名称定义 + [WsEvent.CONNECTED]: [] + }; + addListener(event: WsEvent, listener: Function): void { + // 确保事件数组已初始化 + if (!this.events[event]) { + this.events[event] = []; + } + const listenerId = generateGUID(); + this.events[event].push({ + handler: listener, + id: listenerId + }); + // 部分一次性的事件(如果处于此事件,则直接触发) + if (event == WsEvent.CONNECTED) { + if (websocketInstance?.isConnected) { + this.trigger(event, null); + } + } + } + + removeListener(event: WsEvent, listenerId: string): void { + if (!this.events[event]) return; + + this.events[event] = this.events[event].filter( + (listener) => listener.id !== listenerId + ); + } + + trigger(event: WsEvent, data: any): void { + console.log('触发事件', event); + + if (!this.events[event] || this.events[event].length === 0) { + return; + } + + // 创建副本以避免在迭代过程中修改原数组 + const listeners = [...this.events[event]]; + + listeners.forEach((listener) => { + const context: SubscriptionContext = { + id: listener.id, + destroy: () => this.removeListener(event, listener.id) + }; + + listener.handler(context, websocketInstance, data); + }); + } +} + +// 创建单例实例 +export const subscriptionManager = new SubscriptionManagerImpl(); diff --git a/z_ele/src/plugins/websocket/type.ts b/z_ele/src/plugins/websocket/type.ts new file mode 100644 index 0000000..0b68cb6 --- /dev/null +++ b/z_ele/src/plugins/websocket/type.ts @@ -0,0 +1,51 @@ +import type { Ref } from 'vue'; +import { SubscriptionManager } from '@/plugins/websocket/subscriber'; + +export enum WsEvent { + CONNECTED = 'CONNECTED' // websocket连接成功 +} + +export interface PaWebSocket { + isConnected: Ref; + connectionStatus: Ref; + sendMessage: Function; + subscription: SubscriptionManager; +} + +export enum SendEventType { + BIND_CONNECT_ID = 'bind_connect_id' +} + + +export enum ConnectionStatus { + CONNECTING = 'connecting', + CONNECTED = 'connected', + DISCONNECTED = 'disconnected', + ERROR = 'error' +} + +export enum MessageType { + INCOMING = 'incoming', + OUTGOING = 'outgoing', + SYSTEM = 'system' +} + +export interface WebSocketMessage { + content: string; + type: MessageType; + timestamp: Date; +} + +export interface WebSocketConfig { + url: string; + protocols?: string; + reconnectAttempts: number; + reconnectDelay: number; + autoConnect?: boolean; +} + +// 插件选项接口 +export interface WebSocketPluginOptions { + // 全局配置 + globalConfig?: Partial; +} diff --git a/z_ele/src/store/modules/user.ts b/z_ele/src/store/modules/user.ts index f574ac8..a331d26 100644 --- a/z_ele/src/store/modules/user.ts +++ b/z_ele/src/store/modules/user.ts @@ -9,6 +9,10 @@ import type { User } from '@/api/system/user/model'; import type { Menu } from '@/api/system/menu/model'; import type { DictionaryData } from '@/api/system/dictionary-data/model'; import { getUserInfo } from '@/api/layout'; +import { inject } from 'vue'; +import { PaWebSocket, SendEventType, WsEvent } from '@/plugins/websocket/type'; +import { getToken } from '@/utils/token-util'; +import {getSubscriptionManager, SubscriptionContext, subscriptionManager} from "@/plugins/websocket/subscriber"; /** 直接指定菜单数据 */ const USER_MENUS: Menu[] | null = null; @@ -18,6 +22,7 @@ export interface UserState { authorities: (string | undefined)[]; roles: (string | undefined)[]; dicts: Record; + clientState: any; } export const useUserStore = defineStore('user', { @@ -31,7 +36,9 @@ export const useUserStore = defineStore('user', { /** 当前登录用户的角色 */ roles: [], /** 字典数据缓存 */ - dicts: {} + dicts: {}, + /** 客户端状态数据 */ + clientState: {} }), actions: { /** @@ -61,8 +68,25 @@ export const useUserStore = defineStore('user', { }) ); this.setMenus(menus); + + console.log('管理器塞事件进去', inject('websocket')); + subscriptionManager.addListener( + WsEvent.CONNECTED, + (sub: SubscriptionContext, ws: PaWebSocket) => { + console.log('事件执行'); + // 绑定用户到websocket的connect_id中 + ws.sendMessage(SendEventType.BIND_CONNECT_ID, { + token: getToken(), + userId: result.userId + }); + // sub.destroy(); + } + ); return { menus, homePath }; }, + setClientState(value: any) { + this.clientState = value; + }, /** * 更新用户信息 */ diff --git a/z_ele/src/utils/common.ts b/z_ele/src/utils/common.ts index c834754..1975881 100644 --- a/z_ele/src/utils/common.ts +++ b/z_ele/src/utils/common.ts @@ -4,8 +4,11 @@ import { ElMessageBox } from 'element-plus'; import { removeToken } from '@/utils/token-util'; export function generateGUID() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + ( + c ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) + ).toString(16) ); } /** @@ -14,11 +17,7 @@ export function generateGUID() { * @param from 登录后跳转的地址 * @param push 路由跳转方法 */ -export function logout( - route?: boolean, - from?: string, - push?: Router['push'] -) { +export function logout(route?: boolean, from?: string, push?: Router['push']) { removeToken(); if (route && push) { push({