diff --git a/.env.development b/.env.development index 41c0026..1b0da2e 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,5 @@ +# 获取服务器信息 VITE_USE_MOCK=true # VITE_API_BASE_URL=http://localhost:5173 -VITE_API_BASE_URL=http://127.0.0.1:8080 +VITE_API_BASE_URL=http://127.0.0.1:8082 +VITE_WS_URL=ws://127.0.0.1:8082 \ No newline at end of file diff --git a/.env.production b/.env.production index 7bae5d3..da44451 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ VITE_USE_MOCK=false # VITE_API_BASE_URL=http://localhost:5173 -VITE_API_BASE_URL=http://127.0.0.1:8080 -# http://127.0.0.1:8081 \ No newline at end of file +VITE_API_BASE_URL=http://127.0.0.1:8082 +# http://127.0.0.1:8081 diff --git a/src/components/SimpleKeyboard.vue b/src/components/SimpleKeyboard.vue index 42f4dbc..f088c43 100644 --- a/src/components/SimpleKeyboard.vue +++ b/src/components/SimpleKeyboard.vue @@ -18,7 +18,7 @@ export default { }, hideKeyBoard: { type: Function, - default: () => {}, + default: () => { }, }, }, data: () => ({ @@ -60,7 +60,7 @@ export default { - \ No newline at end of file diff --git a/src/pages/Index/Regular/Consumables.vue b/src/pages/Index/Regular/Consumables.vue index 71ec318..34105a2 100644 --- a/src/pages/Index/Regular/Consumables.vue +++ b/src/pages/Index/Regular/Consumables.vue @@ -13,12 +13,12 @@
- +
+ :bufferBig="bufferBig" :emergencyInfo="emergencyInfo" :wasteStatus="wasteStatus" + @loadConsumables="handleIsLoad" @unloadConsumables="handleIsUnload" @updateTipNum="updateTipNum" />
@@ -48,8 +48,16 @@ import { LiquidState, Tube, } from '../../../types/Index' +import { createWebSocket } from '../../../websocket/socket' +import type { SensorStateMessage } from '../../../websocket/socket'; +const socket = createWebSocket("/api/v1/app/ws/state") + const consumableStore = useConsumablesStore() const emergencyStore = useEmergencyStore() +// 温度状态 +const currentTemperature = ref(40); +// 废料区状态 +const wasteStatus = ref(false) // 添加加载进度状态 const loadingProgress = ref(0) // 父组件状态 @@ -149,18 +157,22 @@ const getEmergencyInfo = async () => { emergencyInfo.value = res.data.tube emergencyStore.setInfo(res.data.tube) } -let socket: WebSocket | null = null //使用websocket保证数据的实时性 const startWebSocket = () => { - socket = new WebSocket('ws://localhost:8080/ws') - socket.onmessage = (event) => { - const res = JSON.parse(event.data) - moveLiquids.value = res.moveLiquidInfo - plates.value = res.plateInfo - bufferLittles.value = res.bufferLittleInfo - bufferBig.value = res.bufferBigInfo - } + socket.connect() } +// 处理传感器状态消息 +const handleSensorState = (data: SensorStateMessage['data']) => { + // 更新温度值(这里使用孵育盒温度) + currentTemperature.value = data.incubateBoxTemperature; + wasteStatus.value = data.wasteBinFullFlag + + // 可以添加温度异常处理逻辑 + if (currentTemperature.value > 40) { + console.warn('温度过高警告'); + // 可以在这里添加其他警告逻辑 + } +}; // 使用事件总线更新状态 const updatePlatesAndBuffers = ({ value, @@ -179,14 +191,16 @@ const updatePlatesAndBuffers = ({ onMounted(() => { eventBus.on('confirm', updatePlatesAndBuffers) startWebSocket() + socket.subscribe('SensorState', handleSensorState); getEmergencyInfo() }) onBeforeUnmount(() => { // 清除事件总线的监听 eventBus.off('confirm', updatePlatesAndBuffers) - if (socket) { - socket.close() + if (socket !== null) { + socket.disconnect() // 断开连接 } + socket.unsubscribe('SensorState', handleSensorState); }) // 在组件激活时恢复状态 onActivated(() => { @@ -202,7 +216,7 @@ const handleIsLoad = async () => { // 创建进度动画 const startTime = Date.now() - const duration = 3000 // 3秒 + const duration = 30000 // 30秒 const updateProgress = () => { const elapsed = Date.now() - startTime diff --git a/src/pages/Index/Regular/Emergency.vue b/src/pages/Index/Regular/Emergency.vue index 2807547..3374518 100644 --- a/src/pages/Index/Regular/Emergency.vue +++ b/src/pages/Index/Regular/Emergency.vue @@ -83,10 +83,8 @@
- +
-
@@ -99,7 +97,6 @@ import { nanoid } from 'nanoid'; import { insertEmergency } from '../../../services/Index/index'; import { useEmergencyStore, useConsumablesStore } from '../../../store'; import type { ReactionPlate, AddEmergencyInfo, Subtank } from '../../../types/Index'; -import { Keyboard } from '../../../components'; const consumableStore = useConsumablesStore(); const emergencyStore = useEmergencyStore(); @@ -273,9 +270,11 @@ const currentInputValue = ref(''); // 当前键盘绑定的值 const showKeyboard = (field: string) => { if (field === 'sampleBarcode') { + console.log("sampleBarcode"); currentInputField.value = 'sampleBarcode'; currentInputValue.value = emergencyPosition.value.sampleBarcode || ''; // 确保清空上一次输入的值 } else { + console.log("userid"); currentInputField.value = 'userid'; currentInputValue.value = emergencyPosition.value.userid || ''; // 同上 } @@ -283,9 +282,11 @@ const showKeyboard = (field: string) => { }; const handleKeyboardInput = (value: string) => { if (currentInputField.value === 'sampleBarcode') { + console.log("sampleBarcode"); emergencyPosition.value.sampleBarcode = value; currentInputValue.value = value; // 更新输入框的值 } else if (currentInputField.value === 'userid') { + console.log("userid"); emergencyPosition.value.userid = value; currentInputValue.value = value; // 更新输入框的值 } @@ -305,7 +306,7 @@ const hideKeyboard = () => { margin: 0; padding: 0; position: relative; - height: 1775px; + height: 100%; width: 100%; background-color: #f4f6f9; box-sizing: border-box; @@ -327,7 +328,7 @@ const hideKeyboard = () => { } .page-header { - width: 1200px; + width: 100%; height: 100px; display: flex; align-items: center; @@ -392,7 +393,7 @@ const hideKeyboard = () => { } .emergency-info { - width: 1200px; + width: 100%; height: auto; margin-top: 20px; border-radius: 8px; @@ -427,7 +428,7 @@ const hideKeyboard = () => { padding: 8px 5px; border: 1px solid #ccc; border-radius: 4px; - width: 98%; + width: 80%; font-size: 32px; transition: box-shadow 0.2s ease; @@ -456,7 +457,7 @@ const hideKeyboard = () => { justify-content: center; .open-button { - width: 100%; + width: 80%; height: 70%; background-color: #4caf50; /* 开启状态颜色:绿色 */ @@ -470,7 +471,7 @@ const hideKeyboard = () => { } .close-button { - width: 100%; + width: 80%; height: 70%; background-color: #f44336; /* 关闭状态颜色:红色 */ @@ -742,7 +743,7 @@ const hideKeyboard = () => { } .emergency-controller { - width: 1200px; + width: 100%; height: 120px; display: flex; margin-top: 20px; @@ -784,7 +785,7 @@ const hideKeyboard = () => { /* 宽度为视口宽度,确保不超出 */ max-width: 100%; /* 最多占满父容器的宽度 */ - height: 600px; + height: 400px; background-color: #f0f0f0; border-top-left-radius: 10px; border-top-right-radius: 10px; diff --git a/src/pages/Index/Regular/Running.vue b/src/pages/Index/Regular/Running.vue index ea2ef44..2b43c0f 100644 --- a/src/pages/Index/Regular/Running.vue +++ b/src/pages/Index/Regular/Running.vue @@ -41,7 +41,7 @@
- 添加急诊 + 急诊
@@ -208,7 +208,6 @@ const confirmEmergency = () => { } //确认结果 const confirmEmergencyWarn = (val: any) => { - console.log('点击确认的值是', val) isDialogVisible.value = false } //获取路由参数 @@ -530,10 +529,10 @@ onUnmounted(() => { #running-container { display: flex; flex-direction: column; - justify-content: space-between; + justify-content: space-around; align-items: center; background-color: #f9fafb; - height: 93.5vh; + height: 92vh; /* 更柔和的背景色 */ // 孵育盘 @@ -642,7 +641,7 @@ onUnmounted(() => { .consumables-container { width: 100%; box-sizing: border-box; - padding: 20px 30px; + padding: 0 30px; display: flex; flex-direction: column; background-color: #ffffff; @@ -654,13 +653,9 @@ onUnmounted(() => { display: flex; align-items: center; justify-content: flex-start; - margin-bottom: 30px; - gap: 30px; // 急诊按钮 .emergency-button { - width: 140px; - height: 140px; background: linear-gradient(135deg, #ff6b6b, #ff4757); border-radius: 20px; display: flex; @@ -689,7 +684,7 @@ onUnmounted(() => { gap: 20px; .tube-project-tab { - width: 120px; + width: 100px; height: 50px; background: linear-gradient(to right, #e3f2fd, #bbdefb); border-radius: 12px; @@ -720,7 +715,6 @@ onUnmounted(() => { .row-second { display: grid; grid-template-columns: 3fr 4fr 2fr 1fr; - gap: 20px; padding: 10px 0; .tips-and-big-buffer { @@ -729,8 +723,8 @@ onUnmounted(() => { gap: 20px; .tips-item { - width: 160px; - height: 110px; + width: 200px; + height: 137px; border-radius: 16px; display: flex; align-items: center; @@ -759,9 +753,11 @@ onUnmounted(() => { } } + .buffer-grid { + margin-top: -10px; + } + .waste-area { - width: 100px; - height: 380px; border-radius: 25px; display: flex; flex-direction: column; @@ -772,7 +768,7 @@ onUnmounted(() => { box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); .waste-text { - font-size: 36px; + font-size: 28px; font-weight: 700; color: #ffffff; writing-mode: vertical-rl; @@ -788,16 +784,16 @@ onUnmounted(() => { } // 添加响应式设计 - @media screen and (max-width: 1366px) { - padding: 15px 20px; + @media screen and (max-width: 800px) { + padding: 15px 20px; // 修改padding值以适应小屏幕 .row-first { margin-bottom: 20px; gap: 20px; .emergency-button { - width: 120px; - height: 120px; + width: 200px; + height: 100px; span { font-size: 28px; @@ -809,8 +805,8 @@ onUnmounted(() => { gap: 15px; .tips-item { - width: 220px; - height: 160px; + width: 180px; + height: 120px; .tip-text { font-size: 32px; @@ -819,10 +815,10 @@ onUnmounted(() => { .waste-area { width: 90px; - height: 340px; + height: 285px; .waste-text { - font-size: 32px; + font-size: 24px; } } } @@ -850,6 +846,25 @@ onUnmounted(() => { text-align: center; max-width: 600px; animation: slideIn 0.3s ease; + position: relative; + + .alert-icon { + .icon { + width: 80px; + height: 80px; + margin: 0 auto; + } + + span { + font-size: 32px; + position: absolute; + top: 30%; + left: 50%; + transform: translate(-50%, -50%); + color: #fff; + } + } + .alert-title { font-size: 36px; diff --git a/src/pages/Index/components/Consumables/MoveLiquidArea.vue b/src/pages/Index/components/Consumables/MoveLiquidArea.vue index 90f2f32..3fcd836 100644 --- a/src/pages/Index/components/Consumables/MoveLiquidArea.vue +++ b/src/pages/Index/components/Consumables/MoveLiquidArea.vue @@ -128,6 +128,7 @@ const props = defineProps({ type: Object as () => Tube, }, tempTipNum: Array, + wasteStatus: Boolean, }) const emit = defineEmits([ 'loadConsumables', @@ -146,7 +147,7 @@ const handleIsUnload = () => { } const activeTab = ref(0) // 废料区状态判断 -const wasteStatus = ref(false) +const wasteStatus = ref(props.wasteStatus) //设置轮询时间间隔 const pollInterVal = 3000 //声明id接口 diff --git a/src/pages/Index/components/Consumables/SpttingPlates.vue b/src/pages/Index/components/Consumables/SpttingPlates.vue index 45104f2..3928405 100644 --- a/src/pages/Index/components/Consumables/SpttingPlates.vue +++ b/src/pages/Index/components/Consumables/SpttingPlates.vue @@ -44,14 +44,19 @@ import Plate from './Plate.vue'; import { ref, onMounted, onBeforeUnmount, defineProps } from 'vue'; import ChangeNum from './ChangeNum.vue'; + const props = defineProps({ plates: { type: Array, default: () => [] + }, + temperature: { + type: Number, + default: 40 } }) // 温度状态 -const currentTemperature = ref(40); +const currentTemperature = ref(props.temperature); const changeNumRef = ref() const changeNumber = (plate, index) => { diff --git a/src/pages/Index/components/Running/LittleBufferDisplay.vue b/src/pages/Index/components/Running/LittleBufferDisplay.vue index 1f6b584..978c7f3 100644 --- a/src/pages/Index/components/Running/LittleBufferDisplay.vue +++ b/src/pages/Index/components/Running/LittleBufferDisplay.vue @@ -32,7 +32,7 @@ const getFillStyle = (item: BottleGroup) => { .buffer-item { position: relative; width: 110px; - height: 155px; + height: 137px; background-color: #d3d3d3; border-radius: 10px; display: flex; diff --git a/src/pages/Index/components/Running/PlateDisplay.vue b/src/pages/Index/components/Running/PlateDisplay.vue index 6d4bf91..8bc16ff 100644 --- a/src/pages/Index/components/Running/PlateDisplay.vue +++ b/src/pages/Index/components/Running/PlateDisplay.vue @@ -40,13 +40,13 @@ const getPercentage = (num: number) => { justify-content: space-between; .project-info { - width: 160px; - height: 46px; + width: 120px; + height: 40px; border-radius: 40px; flex: 1; .project-name { - font-size: 20px; + font-size: 26px; font-weight: 700; color: white; } @@ -54,7 +54,7 @@ const getPercentage = (num: number) => { .project-count { width: 90px; - height: 46px; + height: 40px; border-radius: 40px; background-color: #1485ee; align-items: center; diff --git a/src/pages/Index/components/Running/SampleDisplay.vue b/src/pages/Index/components/Running/SampleDisplay.vue index 2d9515c..ba53646 100644 --- a/src/pages/Index/components/Running/SampleDisplay.vue +++ b/src/pages/Index/components/Running/SampleDisplay.vue @@ -122,12 +122,30 @@ const showPopover = (index: number) => { .samples { display: flex; align-items: center; - flex-wrap: wrap; + flex-wrap: nowrap; background-color: #fafafa; padding: 10px; + &::-webkit-scrollbar { + height: 6px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; + + &:hover { + background: #a8a8a8; + } + } + .item-detail { - width: 300px; + width: 100%; height: auto; display: flex; flex-direction: column; @@ -136,8 +154,8 @@ const showPopover = (index: number) => { } .sample-item { - width: 85px; - height: 85px; + width: 70px; + height: 70px; border-radius: 50%; border: 2px solid black; display: flex; diff --git a/src/utils/axios.ts b/src/utils/axios.ts index 8a4bbab..e9ffcbb 100644 --- a/src/utils/axios.ts +++ b/src/utils/axios.ts @@ -4,11 +4,12 @@ import axios, { InternalAxiosRequestConfig, AxiosHeaders, } from 'axios' - +import { getServerInfo } from './getServerInfo' import { eventBus } from '../eventBus' +const serverInfo = getServerInfo() // 创建 Axios 实例 const apiClient: AxiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL, + baseURL: serverInfo.httpUrl, // 设置请求的根路径 timeout: 10000, headers: { 'Content-Type': 'application/json', diff --git a/src/utils/getServerInfo.ts b/src/utils/getServerInfo.ts new file mode 100644 index 0000000..9b98eb1 --- /dev/null +++ b/src/utils/getServerInfo.ts @@ -0,0 +1,22 @@ +export function getServerInfo() { + // 获取当前页面的 URL 对象 + const url = new URL(window.location.href) + + // 获取主机名(IP 或域名)和端口号 + const host = url.hostname // 例如: "192.168.1.100" 或 "localhost" + const port = '8082' // 使用固定的后端端口 + + // 构建 WebSocket URL + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = `${wsProtocol}//${host}:${port}/api/v1/app/ws/state` + + // 构建 HTTP URL + const httpUrl = `${window.location.protocol}//${host}:${port}` // 例如: "http://192.168.1.100:8082" 或 "http://localhost:8082" + + return { + wsUrl, + httpUrl, + host, + port, + } +} diff --git a/src/websocket/socket.ts b/src/websocket/socket.ts new file mode 100644 index 0000000..39c2cfa --- /dev/null +++ b/src/websocket/socket.ts @@ -0,0 +1,326 @@ +// src/websocket/socket.ts + +import { getServerInfo } from '../utils/getServerInfo' +const serverInfo = getServerInfo() +// 基础消息接口 +interface BaseMessage { + messageType: 'Report' // 消息类型 + dataType: string // 数据类型 + timestamp: number // 时间戳 +} + +// 耗材状态消息 +interface OptScanModuleStateMessage extends BaseMessage { + type: 'OptScanModuleState' + messageType: 'Report' + dataType: 'OptScanModuleState' + data: { + state: 'EMPTY' | 'OCCUPIED' // 状态 + isErrorPlate: boolean // 是否为错误板 + bloodType: 'WHOLE_BLOOD' | 'SEUM_OR_PLASMA' // 血液类型 + sampleBarcode: string // 样本条码 + userid: string // 用户ID + projInfo: ProjectInfo // 项目信息 + sampleId: string // 样本ID + projId: number // 项目ID + } + timestamp: number +} +// 提示信息接口 +interface PromptInfo { + type: 'Warn' | 'Error' + info: string + detailInfos: string[] + stackInfo: null +} + +// 应用事件消息接口 +interface AppEventMessage extends BaseMessage { + type: 'AppEvent' + messageType: 'Report' + dataType: 'AppEvent' + data: { + typeName: 'AppPromptEvents' + timestamp: number + prompt: PromptInfo[] + } + timestamp: number +} +// 设备工作状态消息 +interface DeviceWorkStateMessage extends BaseMessage { + type: 'DeviceWorkState' + messageType: 'Report' + dataType: 'DeviceWorkState' + data: { + workState: 'IDLE' | 'RUNNING' | 'ERROR' | 'PAUSE' | 'STOP' // 设备工作状态 + fatalErrorFlag: boolean // 致命错误标志 + ecodeList: string[] // 错误代码列表 + pending: boolean // 待处理状态 + } + timestamp: number // 时间戳 +} + +// 急诊位状态消息 +interface EmergencyPosStateMessage extends BaseMessage { + type: 'EmergencyPosState' + messageType: 'Report' + dataType: 'EmergencyPosState' + data: { + tube: { + sampleId: string | null // 样本ID + pos: number // 位置 + isHighTube: boolean // 是否为高试管 + isEmergency: boolean // 是否为急诊 + bloodType: 'WHOLE_BLOOD' | 'SEUM_OR_PLASMA' // 血液类型 + sampleBarcode: string // 样本条码 + userid: string // 用户ID + projInfo: ProjectInfo[] // 项目信息列表 + projIds: number[] // 项目ID列表 + state: 'EMPTY' | 'OCCUPIED' // 状态 + errors: string[] // 错误信息列表 + } + } + timestamp: number +} + +// 试管信息接口 +interface Tube { + sampleId: string | null // 样本ID + pos: number // 位置 + isHighTube: boolean // 是否为高试管 + isEmergency: boolean // 是否为急诊 + bloodType: 'WHOLE_BLOOD' | 'SEUM_OR_PLASMA' // 血液类型 + sampleBarcode: string // 样本条码 + userid: string // 用户ID + projInfo: ProjectInfo[] // 项目信息列表 + projIds: number[] // 项目ID列表 + state: 'EMPTY' | 'OCCUPIED' // 状态 + errors: string[] // 错误信息列表 +} + +// 试管架状态消息 +interface TubeHolderStateMessage extends BaseMessage { + type: 'TubeHolderState' + messageType: 'Report' + dataType: 'TubeHolderState' + data: { + tubeHolderType: 'BloodTube' // 试管架类型 + tubes: Tube[] // 试管列表 + state: 'IDLE' | 'RUNNING' | 'ERROR' // 试管架状态 + } + timestamp: number +} + +// 传感器状态消息 +interface SensorStateMessage extends BaseMessage { + type: 'SensorState' + messageType: 'Report' + dataType: 'SensorState' + data: { + pboxTemperature: number // P盒温度 + incubateBoxTemperature: number // 孵育盒温度 + wasteBinFullFlag: boolean // 废物箱满标志 + } + timestamp: number +} + +// 项目信息接口 +interface ProjectInfo { + projId: number + projName: string + projShortName: string + color: string +} + +// 子槽位信息接口 +interface Subtank { + pos: string // 位置编号 SPACE01-SPACE20 + state: 'EMPTY' | 'OCCUPIED' // 槽位状态 + bloodType: 'WHOLE_BLOOD' | 'SEUM_OR_PLASMA' // 血液类型 + sampleBarcode: string // 样本条码 + userid: string // 用户ID + projInfo: ProjectInfo // 项目信息 + sampleId: string // 样本ID + projId: number // 项目ID + startIncubatedTime: number // 开始孵育时间 + incubatedTimeSec: number // 孵育时间(秒) + errors: string[] // 错误信息列表 +} + +// 孵育板状态消息 +interface IncubationPlateStateMessage extends BaseMessage { + type: 'IncubationPlateState' + messageType: 'Report' + dataType: 'IncubationPlateState' + data: { + subtanks: Subtank[] // 20个子槽位信息 + } + timestamp: number +} +// 消息类型联合 +type WebSocketMessage = + | OptScanModuleStateMessage + | DeviceWorkStateMessage + | EmergencyPosStateMessage + | TubeHolderStateMessage + | SensorStateMessage + | IncubationPlateStateMessage + | AppEventMessage // 添加这一行 + +// 消息处理器类型 +type MessageHandler = (data: T['data']) => void + +class WebSocketClient { + private ws: WebSocket | null = null + private url: string + private reconnectAttempts: number = 0 + private maxReconnectAttempts: number = 5 + private reconnectInterval: number = 3000 + + // 使用类型安全的消息处理器映射 + private messageHandlers: Map< + WebSocketMessage['type'], + Set> + > = new Map() + + constructor(url: string) { + this.url = url + } + + // 类型安全的订阅方法 + subscribe( + messageType: T['type'], + handler: MessageHandler, + ): void { + if (!this.messageHandlers.has(messageType)) { + this.messageHandlers.set(messageType, new Set()) + } + this.messageHandlers.get(messageType)?.add(handler) + } + + // 类型安全的取消订阅方法 + unsubscribe( + messageType: T['type'], + handler: MessageHandler, + ): void { + this.messageHandlers.get(messageType)?.delete(handler) + } + + private handleMessage(message: WebSocketMessage): void { + const handlers = this.messageHandlers.get(message.type) + + if (handlers) { + handlers.forEach((handler) => { + try { + handler(message.data) + } catch (error) { + console.error(`处理 ${message.type} 消息时出错:`, error) + } + }) + } + } + + // 连接 WebSocket + connect(): void { + try { + this.ws = new WebSocket(serverInfo.wsUrl) + this.bindEvents() + } catch (error) { + console.error('WebSocket 连接失败:', error) + this.reconnect() + } + } + + // 绑定事件 + private bindEvents(): void { + if (!this.ws) return + + // 连接建立时的处理 + this.ws.onopen = () => { + console.log('WebSocket 连接已建立') + this.reconnectAttempts = 0 // 重置重连次数 + } + + // 接收消息的处理 + this.ws.onmessage = (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + this.handleMessage(data) + } catch (error) { + console.error('消息解析错误:', error) + } + } + + // 连接关闭时的处理 + this.ws.onclose = () => { + console.log('WebSocket 连接已关闭') + this.reconnect() + } + + // 错误处理 + this.ws.onerror = (error) => { + console.error('WebSocket 错误:', error) + } + } + + // 重连机制 + private reconnect(): void { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.log('达到最大重连次数,停止重连') + return + } + + setTimeout(() => { + console.log(`尝试第 ${this.reconnectAttempts + 1} 次重连...`) + this.reconnectAttempts++ + this.connect() + }, this.reconnectInterval) + } + + // 关闭连接 + disconnect(): void { + if (this.ws) { + this.ws.close() + this.ws = null + } + } +} + +// 创建单例 +let wsInstance: WebSocketClient | null = null + +// 导出消息类型 +export type { + WebSocketMessage, + OptScanModuleStateMessage, + DeviceWorkStateMessage, + EmergencyPosStateMessage, + TubeHolderStateMessage, + SensorStateMessage, + IncubationPlateStateMessage, + ProjectInfo, + Subtank, + AppEventMessage, +} + +// 导出 WebSocket 客户端 +export const createWebSocket = (url: string): WebSocketClient => { + if (!wsInstance) { + wsInstance = new WebSocketClient(url) + } + return wsInstance +} + +// 使用示例: +/* +import { createWebSocket } from './websocket/socket'; + +// 创建 WebSocket 连接 +const ws = createWebSocket('ws://your-websocket-server-url'); +ws.connect(); + +// 在组件销毁时断开连接 +onUnmounted(() => { + ws.disconnect(); +}); +*/