diff --git a/src/assets/styles/info-panel.css b/src/assets/styles/info-panel.css index 752f7bb..79c3928 100644 --- a/src/assets/styles/info-panel.css +++ b/src/assets/styles/info-panel.css @@ -9,9 +9,9 @@ right: -400px; /* 初始隐藏在右侧 */ width: 400px; height: calc(100vh - 60px); - background: var(--color-bg-primary); - border-left: 1px solid var(--color-border-primary); - box-shadow: -4px 0 15px rgba(0, 0, 0, 0.1); + background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%); + border-left: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: -4px 0 15px rgba(0, 0, 0, 0.3); z-index: 1000; display: flex; flex-direction: column; @@ -28,8 +28,8 @@ align-items: center; justify-content: space-between; padding: 15px 20px; - background: var(--color-bg-primary); - border-bottom: 1px solid var(--color-border-primary); + background: #1e1e1e; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); min-height: 60px; } @@ -37,13 +37,13 @@ display: flex; align-items: center; gap: 10px; - color: var(--color-text-primary); + color: #ffffff; font-weight: 600; font-size: 16px; } .panel-title i { - color: var(--color-primary); + color: #2a5caa; font-size: 18px; } @@ -59,15 +59,15 @@ gap: 6px; padding: 4px 8px; border-radius: 4px; - background: var(--color-error-rgb-1); - color: var(--color-text-error); + background: rgba(244, 67, 54, 0.1); + color: #f44336; font-size: 12px; font-weight: 500; } .status-indicator.connected { - background: var(--color-success-rgb-1); - color: var(--color-text-success); + background: rgba(76, 175, 80, 0.1); + color: #4caf50; } .status-indicator i { @@ -77,7 +77,7 @@ .panel-close { background: none; border: none; - color: var(--color-text-secondary); + color: #cccccc; cursor: pointer; padding: 6px; border-radius: 4px; @@ -85,8 +85,8 @@ } .panel-close:hover { - background: var(--color-white-rgb-1); - color: var(--color-text-primary); + background: #2a2a2a; + color: #ffffff; } /* 面板内容区域 */ @@ -100,8 +100,8 @@ /* 选项卡导航 */ .panel-tabs { display: flex; - background: var(--color-bg-secondary); - border-bottom: 1px solid var(--color-border-primary); + background: #2a2a2a; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .tab-btn { @@ -113,7 +113,7 @@ padding: 12px 8px; background: none; border: none; - color: var(--color-text-secondary); + color: #cccccc; font-size: 12px; cursor: pointer; transition: all 0.2s ease; @@ -121,14 +121,14 @@ } .tab-btn:hover { - background: var(--color-white-rgb-05); - color: var(--color-text-primary); + background: rgba(255, 255, 255, 0.05); + color: #ffffff; } .tab-btn.active { - color: var(--color-primary); - border-bottom-color: var(--color-primary); - background: var(--color-bg-primary); + color: #2a5caa; + border-bottom-color: #2a5caa; + background: #1e1e1e; } .tab-btn i { @@ -155,16 +155,16 @@ display: flex; align-items: center; justify-content: space-between; - color: var(--color-text-primary); + color: #ffffff; font-size: 14px; font-weight: 600; margin: 0 0 15px 0; padding-bottom: 8px; - border-bottom: 1px solid var(--color-border-secondary); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); } .section-title i { - color: var(--color-primary); + color: #2a5caa; margin-right: 8px; } @@ -175,12 +175,12 @@ justify-content: center; gap: 10px; padding: 40px 20px; - color: var(--color-text-secondary); + color: #cccccc; font-size: 14px; } .loading-state i { - color: var(--color-primary); + color: #2a5caa; font-size: 16px; } @@ -196,15 +196,15 @@ flex-direction: column; gap: 12px; padding: 16px; - background: var(--color-white-rgb-05); - border: 1px solid var(--color-border-secondary); + background: #2a2a2a; + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; transition: all 0.2s ease; } .software-item-backend:hover { - border-color: var(--color-border-primary); - background: var(--color-white-rgb-1); + border-color: rgba(255, 255, 255, 0.3); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .software-info { @@ -220,7 +220,7 @@ display: flex; align-items: center; justify-content: center; - background: var(--color-primary-gradient); + background: linear-gradient(135deg, #2a5caa 0%, #3d7bd8 100%); border-radius: 6px; color: white; font-size: 14px; @@ -232,19 +232,19 @@ } .software-name-backend { - color: var(--color-text-primary); + color: #ffffff; font-weight: 500; font-size: 14px; margin-bottom: 2px; } .software-status { - color: var(--color-text-secondary); + color: #cccccc; font-size: 12px; } .software-status.running { - color: var(--color-text-success); + color: #4caf50; } .software-actions { @@ -271,41 +271,63 @@ } .action-btn.start { - background: var(--color-success-gradient); + background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%); color: white; } .action-btn.start:hover:not(:disabled) { - background: var(--color-success-gradient-hover); + background: linear-gradient(135deg, #66bb6a 0%, #81c784 100%); transform: translateY(-1px); - box-shadow: 0 4px 8px var(--color-success-rgb-3); + box-shadow: 0 4px 8px rgba(76, 175, 80, 0.3); } .action-btn.stop { - background: var(--color-error-gradient); + background: linear-gradient(135deg, #f44336 0%, #ef5350 100%); color: white; } .action-btn.stop:hover:not(:disabled) { - background: var(--color-error-gradient-hover); + background: linear-gradient(135deg, #ef5350 0%, #e57373 100%); transform: translateY(-1px); - box-shadow: 0 4px 8px var(--color-error-rgb-3); + box-shadow: 0 4px 8px rgba(244, 67, 54, 0.3); } .action-btn.restart { - background: var(--color-warning-gradient); + background: linear-gradient(135deg, #ff9800 0%, #ffb74d 100%); color: white; } .action-btn.restart:hover:not(:disabled) { - background: var(--color-warning-gradient-hover); + background: linear-gradient(135deg, #ffb74d 0%, #ffcc02 100%); transform: translateY(-1px); - box-shadow: 0 4px 8px var(--color-warning-rgb-3); + box-shadow: 0 4px 8px rgba(255, 152, 0, 0.3); +} + +.action-btn.refresh { + background: linear-gradient(135deg, #2196f3 0%, #42a5f5 100%); + color: white; +} + +.action-btn.refresh:hover:not(:disabled) { + background: linear-gradient(135deg, #42a5f5 0%, #64b5f6 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(33, 150, 243, 0.3); +} + +.action-btn.clear { + background: linear-gradient(135deg, #f44336 0%, #ef5350 100%); + color: white; +} + +.action-btn.clear:hover:not(:disabled) { + background: linear-gradient(135deg, #ef5350 0%, #e57373 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(244, 67, 54, 0.3); } .action-btn:disabled { - background: var(--color-white-rgb-1); - color: var(--color-text-disabled); + background: #1a1a1a; + color: #999999; cursor: not-allowed; opacity: 0.6; box-shadow: none; @@ -315,7 +337,7 @@ .refresh-btn { background: none; border: none; - color: var(--color-text-secondary); + color: #cccccc; cursor: pointer; padding: 4px; border-radius: 4px; @@ -323,8 +345,8 @@ } .refresh-btn:hover { - color: var(--color-primary); - background: var(--color-primary-rgb-1); + color: #2a5caa; + background: rgba(42, 92, 170, 0.1); } /* 日志面板样式 */ @@ -348,10 +370,10 @@ .filter-select, .filter-input { padding: 6px 10px; - border: 1px solid var(--color-border-secondary); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; - background: var(--color-white-rgb-05); - color: var(--color-text-primary); + background: #2a2a2a; + color: #ffffff; font-size: 12px; } @@ -365,20 +387,20 @@ .filter-select:focus, .filter-input:focus { outline: none; - border-color: var(--color-primary); + border-color: #2a5caa; } .logs-list { max-height: 300px; overflow-y: auto; - border: 1px solid var(--color-border-secondary); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; - background: var(--color-white-rgb-05); + background: #2a2a2a; } .log-item { padding: 10px 15px; - border-bottom: 1px solid var(--color-border-secondary); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); transition: background 0.2s ease; } @@ -387,7 +409,7 @@ } .log-item:hover { - background: var(--color-white-rgb-1); + background: #1a1a1a; } .log-header { @@ -398,18 +420,18 @@ } .log-operation { - color: var(--color-text-primary); + color: #ffffff; font-weight: 500; font-size: 13px; } .log-time { - color: var(--color-text-secondary); + color: #cccccc; font-size: 11px; } .log-details { - color: var(--color-text-secondary); + color: #cccccc; font-size: 12px; line-height: 1.4; } @@ -420,11 +442,11 @@ gap: 10px; margin-top: 4px; font-size: 11px; - color: var(--color-text-tertiary); + color: #888888; } .log-user { - color: var(--color-primary); + color: #2a5caa; } .log-status { @@ -435,13 +457,13 @@ } .log-status.success { - background: var(--color-success-rgb-1); - color: var(--color-text-success); + background: rgba(76, 175, 80, 0.1); + color: #4caf50; } .log-status.failed { - background: var(--color-error-rgb-1); - color: var(--color-text-error); + background: rgba(244, 67, 54, 0.1); + color: #f44336; } .logs-pagination { @@ -451,34 +473,34 @@ gap: 15px; margin-top: 15px; padding-top: 15px; - border-top: 1px solid var(--color-border-secondary); + border-top: 1px solid rgba(255, 255, 255, 0.2); } .page-btn { padding: 6px 12px; - border: 1px solid var(--color-border-secondary); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; - background: var(--color-white-rgb-05); - color: var(--color-text-secondary); + background: #2a2a2a; + color: #cccccc; cursor: pointer; font-size: 12px; transition: all 0.2s ease; } .page-btn:hover { - border-color: var(--color-primary); - color: var(--color-primary); + border-color: #2a5caa; + color: #2a5caa; } .page-btn:disabled { - background: var(--color-white-rgb-1); - color: var(--color-text-disabled); + background: #1a1a1a; + color: #999999; cursor: not-allowed; - border-color: var(--color-border-secondary); + border-color: rgba(255, 255, 255, 0.2); } .page-info { - color: var(--color-text-secondary); + color: #cccccc; font-size: 12px; font-weight: 500; } @@ -493,8 +515,8 @@ .stat-card { padding: 15px; - background: var(--color-white-rgb-05); - border: 1px solid var(--color-border-secondary); + background: #2a2a2a; + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; text-align: center; } @@ -503,23 +525,23 @@ display: block; font-size: 24px; font-weight: bold; - color: var(--color-primary); + color: #2a5caa; margin-bottom: 5px; } .stat-label { - color: var(--color-text-secondary); + color: #cccccc; font-size: 12px; font-weight: 500; } .operation-types { padding-top: 20px; - border-top: 1px solid var(--color-border-secondary); + border-top: 1px solid rgba(255, 255, 255, 0.2); } .operation-types h5 { - color: var(--color-text-primary); + color: #ffffff; font-size: 13px; font-weight: 600; margin: 0 0 10px 0; @@ -533,10 +555,10 @@ .type-tag { padding: 4px 8px; - background: var(--color-white-rgb-05); - border: 1px solid var(--color-border-secondary); + background: #2a2a2a; + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; - color: var(--color-text-secondary); + color: #cccccc; font-size: 11px; font-weight: 500; } diff --git a/src/components/layout/InfoManagementPanel.vue b/src/components/layout/InfoManagementPanel.vue index a91474b..ba1a09b 100644 --- a/src/components/layout/InfoManagementPanel.vue +++ b/src/components/layout/InfoManagementPanel.vue @@ -59,25 +59,25 @@ >
- +
-
{{ software.name }}
-
- {{ software.isRunning ? '运行中' : '已停止' }} +
{{ software.name || software.id }}
+
+ {{ getStatusText(software.status) }}
@@ -104,10 +104,10 @@ 操作日志
- -
@@ -137,10 +137,10 @@
{{ log.operation }}
{{ formatTime(log.timestamp) }}
-
{{ log.details }}
+
{{ log.details }}
- {{ log.user }} - {{ log.status }} + {{ log.user_id }} + {{ getLogStatusText(log.status) }}
@@ -165,20 +165,20 @@
- {{ stats.totalOperations }} - 总操作次数 + {{ stats.totalOperations || 0 }} + 总日志数
- {{ stats.successfulOperations }} - 成功操作 + {{ stats.successfulOperations || 0 }} + 用户操作
- {{ stats.failedOperations }} - 失败操作 + {{ stats.failedOperations || 0 }} + 错误日志
- {{ stats.activeConnections }} - 活动连接 + {{ stats.activeConnections || 0 }} + 系统操作
@@ -201,7 +201,8 @@ diff --git a/src/config/cad.js b/src/config/cad.js index 6f81ec0..59675fc 100644 --- a/src/config/cad.js +++ b/src/config/cad.js @@ -6,6 +6,17 @@ const API_CONFIG = { BASE_URL: import.meta.env.MODE === 'production' ? 'https://api.miany.com' : 'http://localhost', TIMEOUT: 30000, // 30秒超时 + // WebSocket配置 + WEBSOCKET: { + BASE_URL: 'ws://localhost:8000', + ENDPOINT: '/api/v1/ws/connect', + CLIENT_ID: 'web_client', + USER_ID: 'admin', + RECONNECT_ATTEMPTS: 5, + RECONNECT_DELAY: 1000, // 1秒 + HEARTBEAT_INTERVAL: 30000 // 30秒 + }, + // 通用API端点 COMMON_ENDPOINTS: { health: '/health', @@ -240,6 +251,11 @@ export const getGeometryOptimizationDefaults = () => { return API_CONFIG.GEOMETRY_OPTIMIZATION } +// 获取WebSocket配置 +export const getWebSocketConfig = () => { + return API_CONFIG.WEBSOCKET +} + // HTTP请求头配置 export const getRequestHeaders = () => { return { diff --git a/src/main.js b/src/main.js index b2e2d2f..496bf4d 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' import { useAuthStore } from './stores/auth' +import websocketService from './services/websocketService' const app = createApp(App) @@ -17,4 +18,7 @@ app.use(ElementPlus) const authStore = useAuthStore() authStore.initAuth() +// 启动WebSocket连接 +websocketService.connect() + app.mount('#app') diff --git a/src/services/websocketService.js b/src/services/websocketService.js new file mode 100644 index 0000000..28dfca0 --- /dev/null +++ b/src/services/websocketService.js @@ -0,0 +1,392 @@ +// WebSocket服务类 - 统一管理后台WebSocket连接 +// 基于参考项目接口格式实现 + +import { getWebSocketConfig } from '@/config/cad' + +class WebSocketService { + constructor() { + this.config = getWebSocketConfig() + this.ws = null + this.connectionState = 'DISCONNECTED' // CONNECTING, CONNECTED, DISCONNECTED, ERROR + this.reconnectAttempts = 0 + this.heartbeatTimer = null + + // 状态数据 + this.softwareList = [] + this.logs = [] + this.logStats = null + this.operationTypes = [] + this.connectedUsers = [] + + // 事件监听器 + this.listeners = { + onOpen: [], + onMessage: [], + onClose: [], + onError: [], + onStateChange: [], + onSoftwareUpdate: [], + onLogUpdate: [], + onStatsUpdate: [], + onOperationTypesUpdate: [] + } + + console.log('WebSocketService 初始化完成') + } + + // 获取WebSocket连接URL + getConnectionUrl() { + const { BASE_URL, ENDPOINT, CLIENT_ID, USER_ID } = this.config + return `${BASE_URL}${ENDPOINT}?client_id=${CLIENT_ID}&user_id=${USER_ID}` + } + + // 连接WebSocket + connect() { + if (this.connectionState === 'CONNECTING' || this.connectionState === 'CONNECTED') { + return + } + + const url = this.getConnectionUrl() + console.log('开始连接统一管理后台:', url) + this.setState('CONNECTING') + this.ws = new WebSocket(url) + + this.ws.onopen = (event) => { + console.log('统一管理后台连接成功') + this.setState('CONNECTED') + this.reconnectAttempts = 0 + this.startHeartbeat() + this.emit('onOpen', event) + + // 连接成功后立即获取初始数据 + setTimeout(() => { + this.getSoftwareList() + this.getLogStats() + this.getOperationTypes() + }, 500) + } + + this.ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data) + console.log('收到管理后台消息:', message) + this.handleMessage(message) + this.emit('onMessage', message) + } catch (error) { + console.error('解析管理后台消息失败:', error, event.data) + } + } + + this.ws.onclose = (event) => { + console.log('统一管理后台连接关闭:', event) + this.setState('DISCONNECTED') + this.stopHeartbeat() + this.emit('onClose', event) + + // 自动重连 + if (!event.wasClean && this.reconnectAttempts < this.config.RECONNECT_ATTEMPTS) { + const delay = this.config.RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts) + console.log(`统一管理后台将在 ${delay}ms 后重连,第 ${this.reconnectAttempts + 1} 次尝试`) + setTimeout(() => { + this.reconnectAttempts++ + this.connect() + }, delay) + } + } + + this.ws.onerror = (event) => { + console.error('统一管理后台连接错误:', event) + this.setState('ERROR') + this.emit('onError', event) + } + } + + // 断开连接 + disconnect() { + console.log('主动断开统一管理后台连接') + if (this.ws) { + this.ws.close(1000, 'Client disconnect') + this.ws = null + } + this.stopHeartbeat() + this.setState('DISCONNECTED') + } + + // 发送消息 + send(message) { + if (this.connectionState === 'CONNECTED' && this.ws) { + const jsonMessage = JSON.stringify(message) + console.log('发送管理后台消息:', message) + this.ws.send(jsonMessage) + return true + } else { + console.warn('统一管理后台未连接,消息发送失败:', message) + return false + } + } + + // 处理收到的消息 + handleMessage(message) { + switch (message.type) { + case 'info': + this.handleInfoMessage(message) + break + case 'heartbeat': + console.log('收到心跳响应') + break + case 'software_list_update': + this.handleSoftwareListUpdate(message) + break + case 'log_recorded': + console.log('操作日志已记录:', message.data) + break + case 'error': + console.error('管理后台错误:', message.message) + break + default: + console.log('未处理的消息类型:', message.type, message) + } + } + + // 处理info类型消息 + handleInfoMessage(message) { + if (message.data) { + if (message.data.software_list) { + this.softwareList = message.data.software_list + this.emit('onSoftwareUpdate', this.softwareList) + } + + if (message.data.logs) { + this.logs = message.data.logs + this.emit('onLogUpdate', this.logs) + } + + if (message.data.stats) { + this.logStats = message.data.stats + this.emit('onStatsUpdate', this.logStats) + } + + if (message.data.operations) { + this.operationTypes = message.data.operations + this.emit('onOperationTypesUpdate', this.operationTypes) + } + + if (message.data.connected_users) { + this.connectedUsers = message.data.connected_users + } + } + } + + // 处理软件列表更新 + handleSoftwareListUpdate(message) { + if (message.data && message.data.software_list) { + this.softwareList = message.data.software_list + console.log('软件列表已更新:', this.softwareList) + this.emit('onSoftwareUpdate', this.softwareList) + } + } + + // API方法 - 心跳检测 + ping() { + return this.send({ type: 'ping' }) + } + + // API方法 - 获取服务状态 + getStatus() { + return this.send({ type: 'get_status' }) + } + + // API方法 - 获取软件列表 + getSoftwareList() { + return this.send({ type: 'get_software_list' }) + } + + // API方法 - 启动软件 + startSoftware(softwareId) { + if (!softwareId || typeof softwareId !== 'string' || softwareId.length === 0) { + console.error('启动软件失败: 无效的软件ID', softwareId) + return false + } + + console.log('启动软件:', softwareId) + return this.send({ + type: 'start_software', + software_id: softwareId + }) + } + + // API方法 - 停止软件 + stopSoftware(softwareId) { + if (!softwareId || typeof softwareId !== 'string' || softwareId.length === 0) { + console.error('停止软件失败: 无效的软件ID', softwareId) + return false + } + + console.log('停止软件:', softwareId) + return this.send({ + type: 'stop_software', + software_id: softwareId + }) + } + + // API方法 - 重启软件 + restartSoftware(softwareId) { + if (!softwareId || typeof softwareId !== 'string' || softwareId.length === 0) { + console.error('重启软件失败: 无效的软件ID', softwareId) + return false + } + + console.log('重启软件:', softwareId) + return this.send({ + type: 'restart_software', + software_id: softwareId + }) + } + + // API方法 - 记录操作日志 + logOperation(operation, details = '', options = {}) { + if (!operation || typeof operation !== 'string' || operation.length === 0) { + console.error('记录日志失败: 操作名称不能为空') + return false + } + + const logData = { + type: 'log_operation', + operation: operation.substring(0, 100), // 限制长度 + details: details ? details.substring(0, 1000) : '', // 限制长度 + action_type: options.action_type || 'execute', + target_object: options.target_object || '', + status: options.status || 'success', + duration: options.duration || 0, + operation_category: options.operation_category || '软件控制' + } + + console.log('记录操作日志:', logData) + return this.send(logData) + } + + // API方法 - 查询操作日志 + queryLogs(filters = {}) { + const queryData = { + type: 'query_logs', + limit: filters.limit || 100, + offset: filters.offset || 0 + } + + // 添加可选的筛选条件 + if (filters.log_type) queryData.log_type = filters.log_type + if (filters.operation) queryData.operation = filters.operation + if (filters.user_id_filter) queryData.user_id_filter = filters.user_id_filter + if (filters.level) queryData.level = filters.level + if (filters.start_time) queryData.start_time = filters.start_time + if (filters.end_time) queryData.end_time = filters.end_time + + console.log('查询操作日志:', queryData) + return this.send(queryData) + } + + // API方法 - 根据ID获取日志 + getLogById(logId) { + if (!logId || typeof logId !== 'string' || logId.length === 0) { + console.error('获取日志失败: 无效的日志ID', logId) + return false + } + + return this.send({ + type: 'get_log_by_id', + log_id: logId + }) + } + + // API方法 - 获取日志统计信息 + getLogStats() { + console.log('获取日志统计信息') + return this.send({ type: 'get_log_stats' }) + } + + // API方法 - 清理过期日志 + cleanupLogs() { + console.log('清理过期日志') + return this.send({ type: 'cleanup_logs' }) + } + + // API方法 - 获取操作类型列表 + getOperationTypes() { + console.log('获取操作类型列表') + return this.send({ type: 'get_operation_types' }) + } + + // 心跳检测 + startHeartbeat() { + this.heartbeatTimer = setInterval(() => { + if (this.connectionState === 'CONNECTED') { + this.ping() + } + }, this.config.HEARTBEAT_INTERVAL) + } + + stopHeartbeat() { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer) + this.heartbeatTimer = null + } + } + + // 状态管理 + setState(newState) { + if (this.connectionState !== newState) { + const oldState = this.connectionState + this.connectionState = newState + console.log(`统一管理后台状态变化: ${oldState} -> ${newState}`) + this.emit('onStateChange', { oldState, newState }) + } + } + + // 事件系统 + on(event, callback) { + if (this.listeners[event]) { + this.listeners[event].push(callback) + } else { + console.warn('未知事件类型:', event) + } + } + + off(event, callback) { + if (this.listeners[event]) { + const index = this.listeners[event].indexOf(callback) + if (index > -1) { + this.listeners[event].splice(index, 1) + } + } + } + + emit(event, data) { + if (this.listeners[event]) { + this.listeners[event].forEach(callback => { + try { + callback(data) + } catch (error) { + console.error('事件回调执行失败:', event, error) + } + }) + } + } + + // 获取当前状态 + getState() { + return { + connectionState: this.connectionState, + softwareList: this.softwareList, + logs: this.logs, + logStats: this.logStats, + operationTypes: this.operationTypes, + connectedUsers: this.connectedUsers + } + } +} + +// 导出单例实例 +export const websocketService = new WebSocketService() + +// 默认导出 +export default websocketService \ No newline at end of file