up. ws update.

This commit is contained in:
扶桑花间 2025-08-28 22:27:53 +08:00
parent 9afa5e5583
commit 98cb8ee0aa
11 changed files with 315 additions and 359 deletions

View File

@ -16,18 +16,16 @@
</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";
import { computed } from 'vue';
import { useUserStore } from '@/store/modules/user';
import { WsConnectionStatus, wsInstance } from '@/plugins/websocket';
const userStore = useUserStore();
// const visible = ref(false);
const visible = computed<boolean>(()=>{
const visible = computed<boolean>(() => {
return (
websocketInstance.connectionStatus.value != ConnectionStatus.CONNECTED &&
wsInstance.connectionStatus.value != WsConnectionStatus.CONNECTED &&
userStore.info
);
});
</script>

View File

@ -7,8 +7,7 @@ import DictData from '@/components/DictData/index.vue';
import i18n from './i18n';
import installer from './as-needed';
import { iconsInstaller } from '@/components/IconSelect/util';
import WebSocketPlugin from '@/plugins/websocket';
import type { WebSocketConfig } from '@/plugins/websocket/type';
import { WsConfig, WsPlugin } from '@/plugins/websocket';
import 'element-plus/theme-chalk/display.css';
import 'ele-admin-plus/es/style/nprogress.scss';
import './styles/themes/rounded.scss';
@ -19,14 +18,14 @@ import './styles/index.scss';
const app = createApp(App);
// WebSocket配置
const websocketConfig: Partial<WebSocketConfig> = {
const websocketConfig: Partial<WsConfig> = {
url: 'ws://139.155.146.146:19980', // 你的WebSocket服务器地址
reconnectAttempts: 10,
reconnectDelay: 5000,
autoConnect: true // 应用启动时自动连接
};
// 安装WebSocket插件
app.use(WebSocketPlugin, {
app.use(WsPlugin, {
globalConfig: websocketConfig
});

View File

@ -0,0 +1,57 @@
import { WsEvent, WsEventContext, WsEventListener } from '../types';
import { generateGUID } from '@/utils/common';
import { wsInstance } from './instance';
export interface WsEventManager {
listeners: Record<string, WsEventListener[]>;
subscribe(event: WsEvent, handler: Function): WsEventContext;
unsubscribe(event: WsEvent, listenerId: string): void;
emit(event: WsEvent, data?: any): void;
}
export class WsEventManagerImpl implements WsEventManager {
listeners: Record<string, WsEventListener[]> = {};
subscribe(event: WsEvent, handler: Function): WsEventContext {
if (!this.listeners[event]) this.listeners[event] = [];
const listenerId = generateGUID();
const listener: WsEventListener = { handler, id: listenerId };
this.listeners[event].push(listener);
// 立即触发已连接事件
if (event === WsEvent.CONNECTED && wsInstance?.isConnected.value) {
handler({
id: listenerId,
remove: () => this.unsubscribe(event, listenerId)
});
}
return {
id: listenerId,
remove: () => this.unsubscribe(event, listenerId)
};
}
unsubscribe(event: WsEvent, listenerId: string): void {
if (!this.listeners[event]) return;
this.listeners[event] = this.listeners[event].filter(l => l.id !== listenerId);
}
emit(event: WsEvent, data?: any): void {
console.debug(`[WS Event] Emitting: ${event}`);
const listeners = this.listeners[event] || [];
// 安全遍历
listeners.forEach((listener) => {
const context: WsEventContext = {
id: listener.id,
remove: () => this.unsubscribe(event, listener.id)
};
listener.handler(context, wsInstance, data);
});
}
}
// 单例事件管理器
export const wsEventManager = new WsEventManagerImpl();

View File

@ -0,0 +1,146 @@
import {
WsConfig,
WsInstance,
WsConnectionStatus,
WsMessageType,
WsMessage,
WsEvent,
WsSendEventType
} from '../types';
import { computed, ref } from 'vue';
import { wsEventManager } from './event-manager';
// 创建WebSocket实例
export function createWsInstance(config: WsConfig): WsInstance {
// 响应式状态
const socket = ref<WebSocket | null>(null);
const isConnected = ref(false);
const isConnecting = ref(false);
const reconnectCount = ref(0);
const connectionStatus = ref<WsConnectionStatus>(
WsConnectionStatus.DISCONNECTED
);
const messages = ref<WsMessage[]>([]);
// 计算状态类
const statusClass = computed(
() => `status-${connectionStatus.value.toLowerCase()}`
);
// 内部方法
const addMessage = (content: string, type: WsMessageType) => {
messages.value.push({ content, type, timestamp: new Date() });
};
const addSystemMessage = (content: string) =>
addMessage(content, WsMessageType.SYSTEM);
// 事件处理器
const handleOpen = (e: Event) => {
isConnected.value = true;
isConnecting.value = false;
connectionStatus.value = WsConnectionStatus.CONNECTED;
reconnectCount.value = 0;
addSystemMessage('Connection established');
wsEventManager.emit(WsEvent.CONNECTED, e);
};
const handleMessage = (e: MessageEvent) =>
addMessage(e.data, WsMessageType.INCOMING);
const handleError = (e: Event) => {
isConnecting.value = false;
connectionStatus.value = WsConnectionStatus.ERROR;
addSystemMessage(`Error: ${e.type}`);
!isConnected.value && attemptReconnect();
console.log(e);
};
const handleClose = (e: CloseEvent) => {
isConnected.value = false;
isConnecting.value = false;
connectionStatus.value = WsConnectionStatus.DISCONNECTED;
addSystemMessage(`Connection closed: ${e.code} ${e.reason || ''}`);
e.code !== 1000 && attemptReconnect();
};
// 重连逻辑
const attemptReconnect = () => {
if (reconnectCount.value >= config.reconnectAttempts) {
return addSystemMessage('Max reconnect attempts reached');
}
reconnectCount.value++;
addSystemMessage(
`Reconnecting (${reconnectCount.value}/${config.reconnectAttempts})...`
);
setTimeout(connectSocket, config.reconnectDelay);
};
// 公开方法
const connectSocket = () => {
if (isConnected.value || isConnecting.value) return;
isConnecting.value = true;
connectionStatus.value = WsConnectionStatus.CONNECTING;
addSystemMessage('Connecting...');
try {
socket.value = new WebSocket(config.url);
socket.value.onopen = handleOpen;
socket.value.onmessage = handleMessage;
socket.value.onerror = handleError;
socket.value.onclose = handleClose;
} catch (error) {
handleError(error as Event);
}
};
const disconnectSocket = () => {
socket.value?.close();
socket.value = null;
isConnected.value = false;
isConnecting.value = false;
connectionStatus.value = WsConnectionStatus.DISCONNECTED;
reconnectCount.value = 0;
addSystemMessage('Connection closed');
};
const sendMessage = (event: WsSendEventType, data: any) => {
if (!socket.value || !isConnected.value || !data) return;
const payload = JSON.stringify({ event, data });
socket.value.send(payload);
addMessage(payload, WsMessageType.OUTGOING);
};
const clearMessages = () => (messages.value = []);
// 自动连接
console.log('autoConnect->');
config.autoConnect && connectSocket();
return {
isConnected,
isConnecting,
connectionStatus,
messages,
statusClass,
events: wsEventManager,
connectSocket,
disconnectSocket,
sendMessage,
clearMessages
};
}
// 全局实例
export let wsInstance: WsInstance | null = null;
// 初始化方法
export function initWs(config: WsConfig): WsInstance {
return (wsInstance = wsInstance || createWsInstance(config));
}

View File

@ -1,29 +0,0 @@
// 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,171 +0,0 @@
import {
ConnectionStatus, MessageType,
PaWebSocket,
SendEventType,
WebSocketConfig,
WebSocketMessage,
WsEvent
} from '@/plugins/websocket/type';
import { subscriptionManager } from '@/plugins/websocket/subscriber';
import { computed, ref } from 'vue';
// 创建WebSocket实例的函数
function createWebSocket(config: WebSocketConfig) {
// 响应式数据
const socket = ref<WebSocket | null>(null);
const isConnected = ref(false);
const isConnecting = ref(false);
const reconnectCount = ref(0);
const connectionStatus = ref(ConnectionStatus.DISCONNECTED);
const messages = ref<WebSocketMessage[]>([]);
// 计算属性
const statusClass = computed(() => {
switch (connectionStatus.value) {
case ConnectionStatus.CONNECTED:
return 'status-connected';
case ConnectionStatus.CONNECTING:
return 'status-connecting';
default:
return 'status-disconnected';
}
});
// 方法
const connect = () => {
if (isConnected.value || isConnecting.value) return;
isConnecting.value = true;
connectionStatus.value = ConnectionStatus.CONNECTING;
addSystemMessage('正在连接...');
try {
socket.value = new WebSocket(
config.url,
config.protocols ? config.protocols.split(',') : undefined
);
socket.value.onopen = onOpen;
socket.value.onmessage = onMessage;
socket.value.onerror = onError;
socket.value.onclose = onClose;
} catch (error) {
onError(error as Event);
}
};
const disconnect = () => {
if (socket.value) {
socket.value.close();
socket.value = null;
}
isConnected.value = false;
isConnecting.value = false;
connectionStatus.value = ConnectionStatus.DISCONNECTED;
reconnectCount.value = 0;
addSystemMessage('连接已关闭');
};
const reconnect = () => {
if (reconnectCount.value < config.reconnectAttempts) {
reconnectCount.value++;
addSystemMessage(
`尝试重新连接 (${reconnectCount.value}/${config.reconnectAttempts})...`
);
setTimeout(() => connect(), config.reconnectDelay);
} else {
addSystemMessage('已达到最大重连次数,连接终止');
}
};
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);
}
};
const clearMessages = () => {
messages.value = [];
};
const addMessage = (content: string, type: MessageType) => {
messages.value.push({
content,
type,
timestamp: new Date()
});
};
const addSystemMessage = (content: string) => {
addMessage(content, MessageType.SYSTEM);
};
// WebSocket事件处理
const onOpen = (e: Event) => {
isConnected.value = true;
isConnecting.value = false;
connectionStatus.value = ConnectionStatus.CONNECTED;
reconnectCount.value = 0;
addSystemMessage('连接已建立');
// 触发连接成功事件
subscriptionManager.trigger(WsEvent.CONNECTED, e);
};
const onMessage = (event: MessageEvent) => {
addMessage(event.data, MessageType.INCOMING);
};
const onError = (error: Event) => {
isConnecting.value = false;
connectionStatus.value = ConnectionStatus.ERROR;
addSystemMessage(`错误: ${error.type}`);
if (!isConnected.value) {
reconnect();
}
};
const onClose = (event: CloseEvent) => {
isConnected.value = false;
isConnecting.value = false;
connectionStatus.value = ConnectionStatus.DISCONNECTED;
addSystemMessage(`连接关闭: ${event.code} ${event.reason || ''}`);
if (event.code !== 1000) {
reconnect();
}
};
// 自动连接(如果配置了)
if (config.autoConnect) {
connect();
}
return <PaWebSocket>{
// 状态
isConnected,
isConnecting,
connectionStatus,
messages,
statusClass,
// 方法
connect,
disconnect,
sendMessage,
clearMessages,
subscription: subscriptionManager
};
}
export let websocketInstance: PaWebSocket | null = null;
export function initWebSocket(config: WebSocketConfig): PaWebSocket {
if (!websocketInstance) {
websocketInstance = createWebSocket(config);
}
return websocketInstance;
}

View File

@ -0,0 +1,21 @@
import { App, Plugin } from 'vue';
import { WsPluginOptions, WsConfig } from './types';
import { initWs } from './core/instance';
const WsPlugin: Plugin = {
install(app: App, options: WsPluginOptions = {}) {
const mergedConfig: WsConfig = {
url: 'wss://echo.websocket.org',
protocols: 'chat',
reconnectAttempts: 5,
reconnectDelay: 3000,
autoConnect: false,
...options.globalConfig
};
// 初始化并挂载到app
console.log('初始化ws', mergedConfig);
app.config.globalProperties.$ws = initWs(mergedConfig);
}
};
export default WsPlugin;

View File

@ -1,81 +0,0 @@
/*
*
* 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

@ -1,51 +0,0 @@
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

@ -0,0 +1,65 @@
import type { Ref } from 'vue';
import { WsEventManager } from '@/plugins/websocket/core/event-manager';
export enum WsEvent {
CONNECTED = 'CONNECTED'
}
export enum WsSendEventType {
BIND_CONNECT_ID = 'bind_connect_id'
}
export enum WsConnectionStatus {
CONNECTING = 'connecting',
CONNECTED = 'connected',
DISCONNECTED = 'disconnected',
ERROR = 'error'
}
export enum WsMessageType {
INCOMING = 'incoming',
OUTGOING = 'outgoing',
SYSTEM = 'system'
}
export interface WsMessage {
content: string;
type: WsMessageType;
timestamp: Date;
}
export interface WsConfig {
url: string;
protocols?: string;
reconnectAttempts: number;
reconnectDelay: number;
autoConnect?: boolean;
}
export interface WsPluginOptions {
globalConfig?: Partial<WsConfig>;
}
export interface WsEventListener {
handler: Function;
id: string;
}
export interface WsEventContext {
id: string;
remove(): void;
}
export interface WsInstance {
isConnected: Ref<boolean>;
isConnecting: Ref<boolean>;
connectionStatus: Ref<WsConnectionStatus>;
messages: Ref<WsMessage[]>;
statusClass: Ref<string>;
connectSocket: () => void;
disconnectSocket: () => void;
sendMessage: (event: WsSendEventType, data: any) => void;
clearMessages: () => void;
events: WsEventManager; // 重命名为更清晰的events
}

View File

@ -9,10 +9,16 @@ 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";
import {
WsEventContext,
wsEventManager,
WsEvent,
WsInstance,
WsSendEventType,
wsInstance
} from '@/plugins/websocket';
/** 直接指定菜单数据 */
const USER_MENUS: Menu[] | null = null;
@ -69,19 +75,15 @@ export const useUserStore = defineStore('user', {
);
this.setMenus(menus);
console.log('管理器塞事件进去', inject('websocket'));
subscriptionManager.addListener(
WsEvent.CONNECTED,
(sub: SubscriptionContext, ws: PaWebSocket) => {
console.log('事件执行');
console.log('管理器塞事件进去');
wsEventManager.subscribe(WsEvent.CONNECTED, (sub: WsEventContext) => {
console.log('事件执行, 绑定连接和用户关系');
// 绑定用户到websocket的connect_id中
ws.sendMessage(SendEventType.BIND_CONNECT_ID, {
wsInstance.sendMessage(WsSendEventType.BIND_CONNECT_ID, {
token: getToken(),
userId: result.userId
});
// sub.destroy();
}
);
});
return { menus, homePath };
},
setClientState(value: any) {