up. ws update.

This commit is contained in:
扶桑花间 2025-08-28 22:04:50 +08:00
parent 5f69431080
commit 9afa5e5583
11 changed files with 298 additions and 120 deletions

View File

@ -9,6 +9,7 @@
<ele-app> <ele-app>
<router-view /> <router-view />
</ele-app> </ele-app>
<websocket-state />
</ele-config-provider> </ele-config-provider>
</el-config-provider> </el-config-provider>
</template> </template>
@ -18,8 +19,7 @@
import { useGlobalConfig } from '@/config/use-global-config'; import { useGlobalConfig } from '@/config/use-global-config';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useLocale } from '@/i18n/use-locale'; import { useLocale } from '@/i18n/use-locale';
import { inject } from 'vue'; import WebsocketState from '@/components/WebsocketState/index.vue';
import {webSocket_handler} from "@/plugins/webSocket_handler";
/** 组件全局配置 */ /** 组件全局配置 */
const { tableConfig } = useGlobalConfig(); const { tableConfig } = useGlobalConfig();
@ -31,6 +31,4 @@
/** 国际化配置 */ /** 国际化配置 */
const { elLocale, eleLocale } = useLocale(); const { elLocale, eleLocale } = useLocale();
// injectWebSocket
webSocket_handler(inject('websocket'))
</script> </script>

View File

@ -0,0 +1,29 @@
<template>
<ele-modal
title="安全锁定"
:width="480"
v-model="visible"
:modal-penetrable="false"
:append-to-body="true"
:lock-scroll="true"
center
:close-on-press-escape="false"
:close-on-click-modal="false"
:show-close="false"
>
</ele-modal>
</template>
<script setup lang="ts">
import {computed, inject} from 'vue';
import {ConnectionStatus} from "@/plugins/webSocket_plugin";
import {useUserStore} from "@/store/modules/user";
const { connectionStatus } = <any>inject('websocket');
const userStore = useUserStore();
const visible = computed<boolean>(()=> {
return (connectionStatus.value != ConnectionStatus.CONNECTED && userStore.info)
});
</script>

View File

@ -0,0 +1,33 @@
<template>
<ele-modal
title="AdminWorker错误"
:width="480"
v-model="visible"
:modal-penetrable="false"
:append-to-body="true"
:lock-scroll="true"
center
:close-on-press-escape="false"
:close-on-click-modal="false"
:show-close="false"
>
<h1 style="color: red">错误admin:worker未正确连接</h1>
</ele-modal>
</template>
<script setup lang="ts">
import {computed, inject} from 'vue';
import {useUserStore} from "@/store/modules/user";
import { ConnectionStatus} from "@/plugins/websocket/type";
import { websocketInstance } from "@/plugins/websocket/instance";
const userStore = useUserStore();
// const visible = ref(false);
const visible = computed<boolean>(()=>{
return (
websocketInstance.connectionStatus.value != ConnectionStatus.CONNECTED &&
userStore.info
);
});
</script>

View File

@ -7,8 +7,8 @@ import DictData from '@/components/DictData/index.vue';
import i18n from './i18n'; import i18n from './i18n';
import installer from './as-needed'; import installer from './as-needed';
import { iconsInstaller } from '@/components/IconSelect/util'; 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 'element-plus/theme-chalk/display.css';
import 'ele-admin-plus/es/style/nprogress.scss'; import 'ele-admin-plus/es/style/nprogress.scss';
import './styles/themes/rounded.scss'; import './styles/themes/rounded.scss';
@ -27,12 +27,9 @@ const websocketConfig: Partial<WebSocketConfig> = {
}; };
// 安装WebSocket插件 // 安装WebSocket插件
app.use(WebSocketPlugin, { app.use(WebSocketPlugin, {
globalConfig: websocketConfig, globalConfig: websocketConfig
injectGlobal: true,
globalPropertyName: '$websocket'
}); });
app.use(store); app.use(store);
app.use(router); app.use(router);
app.use(permission); app.use(permission);

View File

@ -1,16 +0,0 @@
export const webSocket_handler = (websocket: any) => {
const {
isConnected,
isConnecting,
connectionStatus,
messages,
statusClass,
connect,
disconnect,
sendMessage
} = websocket;
console.log(messages.value)
}

View File

@ -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;

View File

@ -1,45 +1,16 @@
// plugins/WebSocketPlugin.ts import {
import { App, Plugin, ref, computed } from 'vue'; ConnectionStatus, MessageType,
PaWebSocket,
export enum ConnectionStatus { SendEventType,
CONNECTING = 'connecting', WebSocketConfig,
CONNECTED = 'connected', WebSocketMessage,
DISCONNECTED = 'disconnected', WsEvent
ERROR = 'error' } from '@/plugins/websocket/type';
} import { subscriptionManager } from '@/plugins/websocket/subscriber';
import { computed, ref } from 'vue';
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<WebSocketConfig>;
// 是否注入全局实例
injectGlobal?: boolean;
// 全局属性名
globalPropertyName?: string;
}
// 创建WebSocket实例的函数 // 创建WebSocket实例的函数
export function createWebSocket(config: WebSocketConfig) { function createWebSocket(config: WebSocketConfig) {
// 响应式数据 // 响应式数据
const socket = ref<WebSocket | null>(null); const socket = ref<WebSocket | null>(null);
const isConnected = ref(false); const isConnected = ref(false);
@ -51,9 +22,12 @@ export function createWebSocket(config: WebSocketConfig) {
// 计算属性 // 计算属性
const statusClass = computed(() => { const statusClass = computed(() => {
switch (connectionStatus.value) { switch (connectionStatus.value) {
case ConnectionStatus.CONNECTED: return 'status-connected'; case ConnectionStatus.CONNECTED:
case ConnectionStatus.CONNECTING: return 'status-connecting'; return 'status-connected';
default: return 'status-disconnected'; case ConnectionStatus.CONNECTING:
return 'status-connecting';
default:
return 'status-disconnected';
} }
}); });
@ -95,17 +69,23 @@ export function createWebSocket(config: WebSocketConfig) {
const reconnect = () => { const reconnect = () => {
if (reconnectCount.value < config.reconnectAttempts) { if (reconnectCount.value < config.reconnectAttempts) {
reconnectCount.value++; reconnectCount.value++;
addSystemMessage(`尝试重新连接 (${reconnectCount.value}/${config.reconnectAttempts})...`); addSystemMessage(
`尝试重新连接 (${reconnectCount.value}/${config.reconnectAttempts})...`
);
setTimeout(() => connect(), config.reconnectDelay); setTimeout(() => connect(), config.reconnectDelay);
} else { } else {
addSystemMessage('已达到最大重连次数,连接终止'); addSystemMessage('已达到最大重连次数,连接终止');
} }
}; };
const sendMessage = (content: string) => { const sendMessage = (event: SendEventType, data: any) => {
if (socket.value && isConnected.value && content) { if (socket.value && isConnected.value && data) {
socket.value.send(content); const message: string = JSON.stringify({ event, data });
addMessage(content, MessageType.OUTGOING); socket.value.send(message);
/*
*
*/
addMessage(message, MessageType.OUTGOING);
} }
}; };
@ -132,6 +112,8 @@ export function createWebSocket(config: WebSocketConfig) {
connectionStatus.value = ConnectionStatus.CONNECTED; connectionStatus.value = ConnectionStatus.CONNECTED;
reconnectCount.value = 0; reconnectCount.value = 0;
addSystemMessage('连接已建立'); addSystemMessage('连接已建立');
// 触发连接成功事件
subscriptionManager.trigger(WsEvent.CONNECTED, e);
}; };
const onMessage = (event: MessageEvent) => { const onMessage = (event: MessageEvent) => {
@ -163,56 +145,27 @@ export function createWebSocket(config: WebSocketConfig) {
if (config.autoConnect) { if (config.autoConnect) {
connect(); connect();
} }
return <PaWebSocket>{
return {
// 状态 // 状态
isConnected, isConnected,
isConnecting, isConnecting,
connectionStatus, connectionStatus,
messages, messages,
statusClass, statusClass,
// 方法 // 方法
connect, connect,
disconnect, disconnect,
sendMessage, sendMessage,
clearMessages clearMessages,
subscription: subscriptionManager
}; };
} }
// 插件安装函数 export let websocketInstance: PaWebSocket | null = null;
const WebSocketPlugin: Plugin = {
install(app: App, options: WebSocketPluginOptions = {}) {
const {
globalConfig = {},
injectGlobal = false,
globalPropertyName = '$websocket'
} = options;
// 合并默认配置和全局配置 export function initWebSocket(config: WebSocketConfig): PaWebSocket {
const defaultConfig: WebSocketConfig = { if (!websocketInstance) {
url: 'wss://echo.websocket.org', websocketInstance = createWebSocket(config);
protocols: '',
reconnectAttempts: 5,
reconnectDelay: 3000,
autoConnect: false,
...globalConfig
};
// 创建WebSocket实例
const websocketInstance = createWebSocket(defaultConfig);
// 如果需要,注入全局实例
if (injectGlobal) {
app.config.globalProperties[globalPropertyName] = websocketInstance;
} }
return websocketInstance;
// 提供WebSocket实例以便组件通过inject使用
app.provide('websocket', websocketInstance);
// 注册全局组件(如果需要)
// app.component('WebSocketComponent', WebSocketComponent);
} }
};
export default WebSocketPlugin;

View File

@ -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<string, EventListener[]>;
addListener(event: WsEvent, listener: Function): void;
removeListener(event: WsEvent, listenerId: string): void;
trigger(event: WsEvent, data: any): void;
}
class SubscriptionManagerImpl implements SubscriptionManager {
events: Record<string, EventListener[]> = {
// 全局事件名称定义
[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();

View File

@ -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<boolean>;
connectionStatus: Ref<string>;
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<WebSocketConfig>;
}

View File

@ -9,6 +9,10 @@ import type { User } from '@/api/system/user/model';
import type { Menu } from '@/api/system/menu/model'; import type { Menu } from '@/api/system/menu/model';
import type { DictionaryData } from '@/api/system/dictionary-data/model'; import type { DictionaryData } from '@/api/system/dictionary-data/model';
import { getUserInfo } from '@/api/layout'; 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; const USER_MENUS: Menu[] | null = null;
@ -18,6 +22,7 @@ export interface UserState {
authorities: (string | undefined)[]; authorities: (string | undefined)[];
roles: (string | undefined)[]; roles: (string | undefined)[];
dicts: Record<string, DictionaryData[]>; dicts: Record<string, DictionaryData[]>;
clientState: any;
} }
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
@ -31,7 +36,9 @@ export const useUserStore = defineStore('user', {
/** 当前登录用户的角色 */ /** 当前登录用户的角色 */
roles: [], roles: [],
/** 字典数据缓存 */ /** 字典数据缓存 */
dicts: {} dicts: {},
/** 客户端状态数据 */
clientState: {}
}), }),
actions: { actions: {
/** /**
@ -61,8 +68,25 @@ export const useUserStore = defineStore('user', {
}) })
); );
this.setMenus(menus); 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 }; return { menus, homePath };
}, },
setClientState(value: any) {
this.clientState = value;
},
/** /**
* *
*/ */

View File

@ -4,8 +4,11 @@ import { ElMessageBox } from 'element-plus';
import { removeToken } from '@/utils/token-util'; import { removeToken } from '@/utils/token-util';
export function generateGUID() { export function generateGUID() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) (
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
); );
} }
/** /**
@ -14,11 +17,7 @@ export function generateGUID() {
* @param from * @param from
* @param push * @param push
*/ */
export function logout( export function logout(route?: boolean, from?: string, push?: Router['push']) {
route?: boolean,
from?: string,
push?: Router['push']
) {
removeToken(); removeToken();
if (route && push) { if (route && push) {
push({ push({