diff --git a/frontend/src/utils/jwtUtil.ts b/frontend/src/utils/jwtUtil.ts new file mode 100644 index 0000000..8b24c1e --- /dev/null +++ b/frontend/src/utils/jwtUtil.ts @@ -0,0 +1,234 @@ +import { useUserStore } from "@/stores/modules/user"; + +// JWT Token解析后的载荷接口 +export interface JwtPayload { + userId?: string; + loginName?: string; + exp?: number; + iat?: number; + [key: string]: any; +} + +// Token信息摘要接口 +export interface TokenInfo { + userId: string | null; + loginName: string | null; + expiration: string | null; + isExpired: boolean; + remainingTime: string; +} + +/** + * JWT工具类 + * 提供JWT token的解析、验证等功能 + */ +export class JwtUtil { + /** + * Base64URL解码 + * @param str Base64URL编码的字符串 + */ + private static base64UrlDecode(str: string): string { + try { + // Base64URL转Base64 + let base64 = str.replace(/-/g, '+').replace(/_/g, '/'); + + // 补齐padding + while (base64.length % 4) { + base64 += '='; + } + + // Base64解码 + const decoded = atob(base64); + + // 处理UTF-8编码 + return decodeURIComponent( + decoded + .split('') + .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ); + } catch (error) { + throw new Error('Base64URL解码失败: ' + error); + } + } + + /** + * 解析JWT Token获取载荷信息 + * @param token JWT token字符串,如果不传则从store中获取 + */ + static parseToken(token?: string): JwtPayload | null { + try { + let targetToken = token; + + // 如果没有传入token,从store中获取 + if (!targetToken) { + const userStore = useUserStore(); + targetToken = userStore.accessToken; + } + + if (!targetToken) { + console.warn('Token不存在'); + return null; + } + + // JWT token由三部分组成,用.分割:header.payload.signature + const parts = targetToken.split('.'); + if (parts.length !== 3) { + console.error('无效的JWT token格式'); + return null; + } + + // 解码payload部分(第二部分) + const payload = parts[1]; + const decodedPayload = this.base64UrlDecode(payload); + const tokenData: JwtPayload = JSON.parse(decodedPayload); + + return tokenData; + } catch (error) { + console.error('解析JWT Token失败:', error); + return null; + } + } + + /** + * 获取指定字段的值 + * @param field 字段名 + * @param token JWT token字符串,可选 + */ + static getField(field: string, token?: string): T | null { + const tokenData = this.parseToken(token); + return tokenData?.[field] || null; + } + + /** + * 获取用户ID + * @param token JWT token字符串,可选 + */ + static getUserId(token?: string): string | null { + return this.getField('userId', token); + } + + /** + * 获取登录名 + * @param token JWT token字符串,可选 + */ + static getLoginName(token?: string): string | null { + return this.getField('loginName', token); + } + + /** + * 获取Token过期时间戳(秒) + * @param token JWT token字符串,可选 + */ + static getExpiration(token?: string): number | null { + return this.getField('exp', token); + } + + /** + * 获取Token签发时间戳(秒) + * @param token JWT token字符串,可选 + */ + static getIssuedAt(token?: string): number | null { + return this.getField('iat', token); + } + + /** + * 检查Token是否过期 + * @param token JWT token字符串,可选 + */ + static isExpired(token?: string): boolean { + const exp = this.getExpiration(token); + if (!exp) return true; + + // JWT中的exp是秒级时间戳,需要转换为毫秒 + const expTime = exp * 1000; + return Date.now() >= expTime; + } + + /** + * 获取Token剩余有效时间(毫秒) + * @param token JWT token字符串,可选 + */ + static getRemainingTime(token?: string): number { + const exp = this.getExpiration(token); + if (!exp) return 0; + + const expTime = exp * 1000; + const remaining = expTime - Date.now(); + return Math.max(0, remaining); + } + + /** + * 格式化剩余时间为可读字符串 + * @param ms 毫秒数 + */ + static formatRemainingTime(ms: number): string { + if (ms <= 0) return '已过期'; + + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) return `${days}天 ${hours % 24}小时`; + if (hours > 0) return `${hours}小时 ${minutes % 60}分钟`; + if (minutes > 0) return `${minutes}分钟 ${seconds % 60}秒`; + return `${seconds}秒`; + } + + /** + * 获取完整的Token信息摘要 + * @param token JWT token字符串,可选 + */ + static getTokenInfo(token?: string): TokenInfo { + const exp = this.getExpiration(token); + const remainingMs = this.getRemainingTime(token); + + return { + userId: this.getUserId(token), + loginName: this.getLoginName(token), + expiration: exp ? new Date(exp * 1000).toLocaleString() : null, + isExpired: this.isExpired(token), + remainingTime: this.formatRemainingTime(remainingMs) + }; + } + + /** + * 验证Token是否有效(格式正确且未过期) + * @param token JWT token字符串,可选 + */ + static isValid(token?: string): boolean { + const tokenData = this.parseToken(token); + return tokenData !== null && !this.isExpired(token); + } + + /** + * 获取Token的头部信息 + * @param token JWT token字符串,可选 + */ + static getHeader(token?: string): any | null { + try { + let targetToken = token; + + if (!targetToken) { + const userStore = useUserStore(); + targetToken = userStore.accessToken; + } + + if (!targetToken) return null; + + const parts = targetToken.split('.'); + if (parts.length !== 3) return null; + + const header = parts[0]; + const decodedHeader = this.base64UrlDecode(header); + return JSON.parse(decodedHeader); + } catch (error) { + console.error('解析JWT Header失败:', error); + return null; + } + } +} + +// 导出单例方法,方便直接调用 +export const jwtUtil = JwtUtil; \ No newline at end of file diff --git a/frontend/src/utils/webSocketClient.ts b/frontend/src/utils/webSocketClient.ts index 94165f1..209cc89 100644 --- a/frontend/src/utils/webSocketClient.ts +++ b/frontend/src/utils/webSocketClient.ts @@ -1,186 +1,703 @@ -import {ElMessage} from "element-plus"; +/** + * WebSocket客户端服务 + * 提供WebSocket连接管理、心跳机制、消息处理等功能 + * 集成JWT token解析,支持自动获取用户登录名 + * + * @author hongawen + * @version 2.0 + */ +import { ElMessage } from "element-plus"; +import { jwtUtil } from "./jwtUtil"; +// ============================================================================ +// 类型定义 (Types & Interfaces) +// ============================================================================ + +/** + * WebSocket消息接口定义(对应后端WebSocketVO结构) + */ +interface WebSocketMessage { + type: string; // 消息类型 + requestId?: string; // 请求ID + operateCode?: string; // 操作代码 + code?: number; // 状态码 + desc?: string; // 描述信息 + data?: T; // 泛型数据 +} + +/** + * 回调函数类型定义 + */ +type CallbackFunction = (message: WebSocketMessage) => void; + +/** + * WebSocket配置接口 + */ +interface SocketConfig { + url: string; // WebSocket服务器地址 + heartbeatInterval?: number; // 心跳间隔时间(ms) + reconnectDelay?: number; // 重连延迟时间(ms) + maxReconnectAttempts?: number; // 最大重连次数 + timeout?: number; // 超时时间(ms) +} + +/** + * 连接状态枚举 + */ +enum ConnectionStatus { + DISCONNECTED = 'disconnected', // 未连接 + CONNECTING = 'connecting', // 连接中 + CONNECTED = 'connected', // 已连接 + RECONNECTING = 'reconnecting', // 重连中 + ERROR = 'error' // 连接错误 +} + +/** + * 常用的WebSocket消息类型定义命名空间 + */ +namespace WebSocketMessageTypes { + /** + * 预检测相关消息 + */ + export interface PreTestMessage { + deviceId?: string; + status?: string; + progress?: number; + errorInfo?: string; + } + + /** + * 系数校准相关消息 + */ + export interface CoefficientMessage { + deviceId: string; + channel: number; + voltage?: string; + current?: string; + calibrationResult?: boolean; + } + + /** + * 正式检测相关消息 + */ + export interface TestMessage { + deviceId: string; + testType: string; + testResult?: 'success' | 'failed' | 'processing'; + testData?: any; + } + + /** + * 通用响应消息 + */ + export interface CommonResponse { + success: boolean; + message?: string; + timestamp?: number; + } +} + +// ============================================================================ +// 导出类型 +// ============================================================================ + +export type { + WebSocketMessage, + CallbackFunction, + SocketConfig, + WebSocketMessageTypes +}; + +export { + ConnectionStatus +}; + +// ============================================================================ +// 主要服务类 +// ============================================================================ + +/** + * WebSocket服务类 + * 单例模式实现,提供完整的WebSocket连接管理功能 + */ export default class SocketService { - static instance = null; - static get Instance() { + // ======================================================================== + // 静态属性和方法 (Static) + // ======================================================================== + + /** + * 单例实例 + */ + private static instance: SocketService | null = null; + + /** + * 获取单例实例 + * @returns SocketService实例 + */ + static get Instance(): SocketService { if (!this.instance) { this.instance = new SocketService(); } return this.instance; } - // 和服务端连接的socket对象 - ws = null; - // 存储回调函数 - callBackMapping = {}; - // 标识是否连接成功 - connected = false; - // 记录重试的次数 - sendRetryCount = 0; - // 重新连接尝试的次数 - connectRetryCount = 0; - work:any; - workerBlobUrl:any; - lastActivityTime= 0; // 上次活动时间戳 - lastResponseHeartTime = Date.now();//最后一次收到心跳回复时间 - reconnectDelay= 5000; // 重新连接延迟,单位毫秒 + // ======================================================================== + // 实例属性 (Properties) + // ======================================================================== - // 定义连接服务器的方法 - connect() { + /** + * WebSocket连接实例 + */ + private ws: WebSocket | null = null; + + /** + * 消息回调函数映射表 + */ + private callBackMapping: Record> = {}; + + /** + * 当前连接状态 + */ + private connectionStatus: ConnectionStatus = ConnectionStatus.DISCONNECTED; + + /** + * 发送消息重试计数器 + */ + private sendRetryCount: number = 0; + + /** + * 连接重试计数器 + */ + private connectRetryCount: number = 0; + + /** + * 心跳Worker实例 + */ + private heartbeatWorker: Worker | null = null; + + /** + * Worker脚本的Blob URL + */ + private workerBlobUrl: string | null = null; + + /** + * 最后一次收到心跳响应的时间戳 + */ + private lastResponseHeartTime: number = Date.now(); + + /** + * WebSocket连接配置 + */ + private config: SocketConfig = { + url: 'ws://127.0.0.1:7777/hello', + heartbeatInterval: 9000, // 9秒心跳间隔 + reconnectDelay: 5000, // 5秒重连延迟 + maxReconnectAttempts: 5, // 最多重连5次 + timeout: 30000 // 30秒超时 + }; - // 连接服务器 + // ======================================================================== + // 构造函数 (Constructor) + // ======================================================================== + + /** + * 私有构造函数,防止外部直接实例化 + */ + private constructor() { + this.initializeProperties(); + } + + /** + * 初始化属性 + */ + private initializeProperties(): void { + this.lastResponseHeartTime = Date.now(); + } + + // ======================================================================== + // Getter属性 (Computed) + // ======================================================================== + + /** + * 获取连接状态 + * @returns 是否已连接 + */ + get connected(): boolean { + return this.connectionStatus === ConnectionStatus.CONNECTED; + } + + // ======================================================================== + // 公共方法 (Public Methods) + // ======================================================================== + + /** + * 配置WebSocket连接参数 + * @param config 部分配置对象 + */ + public configure(config: Partial): void { + this.config = { ...this.config, ...config }; + } + + /** + * 连接WebSocket服务器(同步方式,保持向后兼容) + */ + public connect(): Promise | void { + // 检查浏览器支持 if (!window.WebSocket) { - return console.log('您的浏览器不支持WebSocket'); + console.log('您的浏览器不支持WebSocket'); + return; } - // let token = $.cookie('123'); - // let token = '4E6EF539AAF119D82AC4C2BC84FBA21F'; + // 防止重复连接 + if (this.connectionStatus === ConnectionStatus.CONNECTING || this.connected) { + console.warn('WebSocket已连接或正在连接中'); + return; + } + + this.connectionStatus = ConnectionStatus.CONNECTING; + + try { + this.ws = new WebSocket(this.buildWebSocketUrl()); + this.setupEventHandlersLegacy(); + } catch (error) { + this.connectionStatus = ConnectionStatus.ERROR; + console.error('WebSocket连接失败:', error); + } + } + + /** + * 异步连接WebSocket服务器 + * @returns Promise + */ + public connectAsync(): Promise { + return new Promise((resolve, reject) => { + // 检查浏览器支持 + if (!window.WebSocket) { + const error = '您的浏览器不支持WebSocket'; + console.error(error); + reject(new Error(error)); + return; + } + + // 防止重复连接 + if (this.connectionStatus === ConnectionStatus.CONNECTING || this.connected) { + console.warn('WebSocket已连接或正在连接中'); + resolve(); + return; + } + + this.connectionStatus = ConnectionStatus.CONNECTING; + + try { + this.ws = new WebSocket(this.buildWebSocketUrl()); + this.setupEventHandlers(resolve, reject); + } catch (error) { + this.connectionStatus = ConnectionStatus.ERROR; + reject(error); + } + }); + } + + /** + * 注册消息回调函数(支持泛型) + * @param messageType 消息类型 + * @param callback 回调函数 + */ + public registerCallBack(messageType: string, callback: CallbackFunction): void { + if (!messageType || typeof callback !== 'function') { + console.error('注册回调函数参数无效'); + return; + } + this.callBackMapping[messageType] = callback; + console.log(`注册消息处理器: ${messageType}`); + } + + /** + * 注销消息回调函数 + * @param messageType 消息类型 + */ + public unRegisterCallBack(messageType: string): void { + if (this.callBackMapping[messageType]) { + delete this.callBackMapping[messageType]; + console.log(`注销消息处理器: ${messageType}`); + } + } + + /** + * 发送数据到WebSocket服务器 + * @param data 要发送的数据 + * @returns Promise + */ + public send(data: any): Promise { + return new Promise((resolve, reject) => { + if (!this.connected || !this.ws) { + // 未连接时的重试机制 + if (this.sendRetryCount < 3) { + this.sendRetryCount++; + setTimeout(() => { + this.send(data).then(resolve).catch(reject); + }, this.sendRetryCount * 500); + return; + } else { + reject(new Error('WebSocket未连接且重试失败')); + return; + } + } + + try { + // 重置重试计数 + this.sendRetryCount = 0; + + // 尝试发送JSON数据,失败则发送原始数据 + const message = typeof data === 'string' ? data : JSON.stringify(data); + this.ws.send(message); + + console.log('发送消息:', message); + resolve(); + } catch (error) { + console.error('发送消息失败:', error); + reject(error); + } + }); + } + + /** + * 关闭WebSocket连接 + */ + public closeWs(): void { + console.log('正在关闭WebSocket连接...'); + + // 清理心跳 + this.clearHeartbeat(); + + // 关闭连接 + if (this.ws) { + this.ws.close(1000, '主动关闭连接'); + this.ws = null; + } + + // 更新状态 + this.connectionStatus = ConnectionStatus.DISCONNECTED; + this.connectRetryCount = 0; + this.sendRetryCount = 0; + + console.log('WebSocket连接已关闭'); + } + + /** + * 获取当前连接状态 + * @returns 连接状态枚举值 + */ + public getConnectionStatus(): ConnectionStatus { + return this.connectionStatus; + } + + /** + * 获取连接统计信息 + * @returns 连接统计对象 + */ + public getConnectionStats(): { + status: ConnectionStatus; + connectRetryCount: number; + lastResponseHeartTime: number; + } { + return { + status: this.connectionStatus, + connectRetryCount: this.connectRetryCount, + lastResponseHeartTime: this.lastResponseHeartTime + }; + } +<<<<<<< HEAD const url = 'ws://127.0.0.1:7777/hello?name=cdf' //const url = 'ws://192.168.1.124:7777/hello?name=cdf' this.ws = new WebSocket(url); // 连接成功的事件 +======= + // ======================================================================== + // 私有方法 (Private Methods) + // ======================================================================== + + /** + * 构建完整的WebSocket URL + * 自动从JWT token中获取loginName作为name参数 + * @returns 完整的WebSocket URL + */ + private buildWebSocketUrl(): string { + const { url } = this.config; + + // 直接从JWT token中获取loginName作为name参数 + const loginName = jwtUtil.getLoginName(); + + if (loginName) { + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}name=${encodeURIComponent(loginName)}`; + } + + // 如果无法获取loginName,返回原始URL并输出警告 + console.warn('无法从JWT token中获取loginName,WebSocket连接可能会失败'); + return url; + } + + /** + * 设置WebSocket事件处理器(异步版本) + * @param resolve Promise resolve回调 + * @param reject Promise reject回调 + */ + private setupEventHandlers(resolve: () => void, reject: (error: Error) => void): void { + if (!this.ws) return; + + // 连接成功事件 + this.ws.onopen = () => { + ElMessage.success("WebSocket连接服务端成功"); + console.log('WebSocket连接成功'); + this.connectionStatus = ConnectionStatus.CONNECTED; + this.connectRetryCount = 0; + this.startHeartbeat(); + resolve(); + }; + + // 连接关闭事件 + this.ws.onclose = (event: CloseEvent) => { + console.log('WebSocket连接关闭', event.code, event.reason); + this.connectionStatus = ConnectionStatus.DISCONNECTED; + this.clearHeartbeat(); + + // 非正常关闭且未超过最大重连次数,尝试重连 + if (event.code !== 1000 && this.connectRetryCount < this.config.maxReconnectAttempts!) { + this.attemptReconnect(); + } + }; + + // 连接错误事件 + this.ws.onerror = (error: Event) => { + console.error('WebSocket连接错误:', error); + ElMessage.error("WebSocket连接异常"); + this.connectionStatus = ConnectionStatus.ERROR; + reject(new Error('WebSocket连接失败')); + }; + + // 消息接收事件 + this.ws.onmessage = (event: MessageEvent) => { + this.handleMessage(event); + }; + } + + /** + * 设置WebSocket事件处理器(兼容版本) + */ + private setupEventHandlersLegacy(): void { + if (!this.ws) return; + + // 连接成功事件 +>>>>>>> d6d63523a3b094a6b60ea32f7779cb3888d13923 this.ws.onopen = () => { ElMessage.success("webSocket连接服务端成功了"); console.log('连接服务端成功了'); - this.connected = true; - // 重置重新连接的次数 + this.connectionStatus = ConnectionStatus.CONNECTED; this.connectRetryCount = 0; - this.updateLastActivityTime(); this.startHeartbeat(); }; - // 1.连接服务端失败 - // 2.当连接成功之后, 服务器关闭的情况 - this.ws.onclose = () => { + + // 连接关闭事件 + this.ws.onclose = (event: CloseEvent) => { console.log('连接webSocket服务端关闭'); - this.connected = false; - this.connectRetryCount++; + this.connectionStatus = ConnectionStatus.DISCONNECTED; this.clearHeartbeat(); - /* setTimeout(() => { + + // 保持原有的重连逻辑(被注释掉的) + // this.connectRetryCount++; + /* setTimeout(() => { this.connect(); }, 500 * this.connectRetryCount);*/ - - }; + // 连接错误事件 this.ws.onerror = () => { ElMessage.error("webSocket连接异常!"); - - + this.connectionStatus = ConnectionStatus.ERROR; }; + // 消息接收事件 + this.ws.onmessage = (event: MessageEvent) => { + this.handleMessage(event); + }; + } - // 得到服务端发送过来的数据 - this.ws.onmessage = (event) => { - // console.log('🚀 ~ SocketService ~ connect ~ event:', event) - if(event.data == 'over') { - //心跳消息处理 - this.lastResponseHeartTime = Date.now(); - this.updateLastActivityTime(); // 收到心跳响应时更新活动时间 - }else { - let message: { [key: string]: any }; - try { - console.log('Received message:',event.data) - message = JSON.parse(event.data); - } catch (e) { - return console.error("消息解析失败", event.data, e); - } + /** + * 处理接收到的消息 + * 支持心跳响应、JSON消息和普通文本消息 + * @param event WebSocket消息事件 + */ + private handleMessage(event: MessageEvent): void { + // console.log('Received message:', event.data); + + // 心跳响应处理 + if (event.data === 'over') { + console.log(`${new Date().toLocaleTimeString()} - 收到心跳响应`); + this.lastResponseHeartTime = Date.now(); + return; + } - /* 通过接受服务端发送的type字段来回调函数 */ + // 检查消息是否为空或无效 + if (!event.data || event.data.trim() === '') { + console.warn('收到空消息,跳过处理'); + return; + } + + // 业务消息处理 + try { + // 检查是否为JSON格式 + if (typeof event.data === 'string' && (event.data.startsWith('{') || event.data.startsWith('['))) { + const message: WebSocketMessage = JSON.parse(event.data); if (message?.type && this.callBackMapping[message.type]) { this.callBackMapping[message.type](message); } else { - console.log("抛弃====>") - console.log(event.data) - /* 丢弃或继续写你的逻辑 */ + console.warn('未找到对应的消息处理器:', message.type); + } + } else { + // 非JSON格式的消息,作为普通文本处理 + console.log('收到非JSON格式消息:', event.data); + // 可以添加文本消息的处理逻辑 + if (this.callBackMapping['text']) { + this.callBackMapping['text']({ + type: 'text', + data: event.data + }); } } - - - }; - } - - - - - - startHeartbeat() { - this.lastResponseHeartTime = Date.now(); - const _this = this - _this.workerBlobUrl = window.URL.createObjectURL(new Blob(['(function(e){setInterval(function(){this.postMessage(null)},9000)})()'])); - - this.work = new Worker(_this.workerBlobUrl); - this.work.onmessage = function(e){ - //判断多久没收到心跳响应 - - if(_this.lastActivityTime - _this.lastResponseHeartTime > 30000){ - //说明已经三轮心跳没收到回复了,关闭检测,提示用户。 - ElMessage.error("业务主体模块发生未知异常,请尝试重新启动!"); - _this.clearHeartbeat(); - return; - } - _this.sendHeartbeat(); - } - - } - sendHeartbeat() { - console.log(new Date()+"进入心跳消息发送。。。。。。。。。。。。。") - this.ws.send('alive'); - this.updateLastActivityTime(); // 发送心跳后更新活动时间 - } - - - updateLastActivityTime() { - this.lastActivityTime = Date.now(); - } - - clearHeartbeat() { - const _this = this - if (_this.work) { - _this.work.terminate(); - _this.work = null; - } - if (_this.workerBlobUrl) { - window.URL.revokeObjectURL(_this.workerBlobUrl); // 释放临时的Blob URL - _this.workerBlobUrl = null; + + } catch (error) { + console.error('消息解析失败:', event.data, error); + console.error('消息类型:', typeof event.data); + console.error('消息长度:', event.data?.length || 0); } } + // ======================================================================== + // 重连机制相关方法 + // ======================================================================== + /** + * 尝试重新连接WebSocket + */ + private attemptReconnect(): void { + if (this.connectionStatus === ConnectionStatus.RECONNECTING) { + return; + } - // 回调函数的注册 - registerCallBack(socketType, callBack) { - this.callBackMapping[socketType] = callBack; - } - // 取消某一个回调函数 - unRegisterCallBack(socketType) { - this.callBackMapping[socketType] = null; - } - // 发送数据的方法 - send(data) { - // 判断此时此刻有没有连接成功 - if (this.connected) { - this.sendRetryCount = 0; + this.connectionStatus = ConnectionStatus.RECONNECTING; + this.connectRetryCount++; + + const delay = this.config.reconnectDelay! * this.connectRetryCount; + + console.log(`尝试第${this.connectRetryCount}次重连,${delay}ms后开始...`); + + setTimeout(() => { try { - this.ws.send(JSON.stringify(data)); - } catch (e) { - this.ws.send(data); + const result = this.connect(); + if (result instanceof Promise) { + result.catch((error: any) => { + console.error('重连失败:', error); + }); + } + } catch (error: any) { + console.error('重连失败:', error); } - } else { - this.sendRetryCount++; - setTimeout(() => { - this.send(data); - }, this.sendRetryCount * 500); + }, delay); + } + + // ======================================================================== + // 心跳机制相关方法 + // ======================================================================== + + /** + * 启动心跳机制 + */ + private startHeartbeat(): void { + this.lastResponseHeartTime = Date.now(); + this.createHeartbeatWorker(); + } + + /** + * 创建心跳Worker + * 使用Worker在独立线程中处理心跳定时器,避免主线程阻塞 + */ + private createHeartbeatWorker(): void { + try { + // 创建Worker脚本 + const workerScript = ` + setInterval(function() { + postMessage('heartbeat'); + }, ${this.config.heartbeatInterval}); + `; + + this.workerBlobUrl = window.URL.createObjectURL( + new Blob([workerScript], { type: 'application/javascript' }) + ); + + this.heartbeatWorker = new Worker(this.workerBlobUrl); + + // 心跳Worker消息处理 + this.heartbeatWorker.onmessage = (event: MessageEvent) => { + this.handleHeartbeatTick(); + }; + + // Worker错误处理 + this.heartbeatWorker.onerror = (error: ErrorEvent) => { + console.error('心跳Worker错误:', error); + this.clearHeartbeat(); + }; + + } catch (error) { + console.error('创建心跳Worker失败:', error); } } - // 断开方法 - closeWs() { - if (this.connected) { - this.ws.close() + /** + * 处理心跳定时器事件 + * 检查连接超时并发送心跳消息 + */ + private handleHeartbeatTick(): void { + // 检查是否超时(距离上次收到心跳响应的时间) + const timeSinceLastResponse = Date.now() - this.lastResponseHeartTime; + + if (timeSinceLastResponse > this.config.timeout!) { + console.error(`WebSocket心跳超时: ${timeSinceLastResponse}ms > ${this.config.timeout}ms`); + ElMessage.error("WebSocket连接超时,请检查网络连接!"); + this.clearHeartbeat(); + this.closeWs(); + return; } - console.log('执行WS关闭命令..'); + + this.sendHeartbeat(); } -} + + /** + * 发送心跳消息到服务器 + */ + private sendHeartbeat(): void { + if (this.connected && this.ws) { + console.log(`${new Date().toLocaleTimeString()} - 发送心跳消息`); + this.ws.send('alive'); + } + } + + /** + * 清理心跳机制 + * 终止Worker并清理相关资源 + */ + private clearHeartbeat(): void { + if (this.heartbeatWorker) { + this.heartbeatWorker.terminate(); + this.heartbeatWorker = null; + } + + if (this.workerBlobUrl) { + window.URL.revokeObjectURL(this.workerBlobUrl); + this.workerBlobUrl = null; + } + } +} \ No newline at end of file diff --git a/frontend/src/views/home/components/testPopup.vue b/frontend/src/views/home/components/testPopup.vue index ac061d7..fc2e62f 100644 --- a/frontend/src/views/home/components/testPopup.vue +++ b/frontend/src/views/home/components/testPopup.vue @@ -199,7 +199,6 @@ const handleSubmitFast = () => { if (preTestStatus.value == 'waiting') { if (checkStore.selectTestItems.preTest) { startPreTest({ - userPageId: "cdf", devIds: deviceIds, planId: planId, operateType: checkStore.reCheckType == 1 ? '1' : '2', @@ -227,7 +226,6 @@ const handleSubmitFast = () => { if (channelsTestStatus.value == 'waiting') { if (!checkStore.selectTestItems.preTest && checkStore.selectTestItems.channelsTest) { startPreTest({ - userPageId: "cdf", devIds: deviceIds, planId: planId, operateType: checkStore.reCheckType == 1 ? '1' : '2', @@ -253,7 +251,6 @@ const handleSubmitFast = () => { if (TestStatus.value == "waiting") { if (!checkStore.selectTestItems.preTest && !checkStore.selectTestItems.channelsTest && checkStore.selectTestItems.test) { startPreTest({ - userPageId: "cdf", devIds: deviceIds, planId: planId, operateType: checkStore.reCheckType == 1 ? '1' : '2', @@ -392,7 +389,6 @@ const sendResume = () => { const sendReCheck = () => { console.log('发送重新检测指令') startPreTest({ - userPageId: "cdf", devIds: checkStore.devices.map((item) => item.deviceId), planId: checkStore.plan.id, operateType: '2', // 0:'系数校验','1'为预检测、‘2‘为正式检测、'8'为不合格项复检