Browse Source

fix:兼容了Websocket;配置了动态ip和端口以及动态的wsurl;运行中页面适配

feature/history-20250108
gzt 8 months ago
parent
commit
cc4e9f04a4
  1. 4
      .env.development
  2. 2
      .env.production
  3. 4
      src/components/SimpleKeyboard.vue
  4. 204
      src/pages/Index/Index.vue
  5. 44
      src/pages/Index/Regular/Consumables.vue
  6. 25
      src/pages/Index/Regular/Emergency.vue
  7. 63
      src/pages/Index/Regular/Running.vue
  8. 3
      src/pages/Index/components/Consumables/MoveLiquidArea.vue
  9. 7
      src/pages/Index/components/Consumables/SpttingPlates.vue
  10. 2
      src/pages/Index/components/Running/LittleBufferDisplay.vue
  11. 8
      src/pages/Index/components/Running/PlateDisplay.vue
  12. 26
      src/pages/Index/components/Running/SampleDisplay.vue
  13. 5
      src/utils/axios.ts
  14. 22
      src/utils/getServerInfo.ts
  15. 326
      src/websocket/socket.ts

4
.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

2
.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
VITE_API_BASE_URL=http://127.0.0.1:8082
# http://127.0.0.1:8081

4
src/components/SimpleKeyboard.vue

@ -18,7 +18,7 @@ export default {
},
hideKeyBoard: {
type: Function,
default: () => {},
default: () => { },
},
},
data: () => ({
@ -60,7 +60,7 @@ export default {
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
<style lang="less" scoped>
.simple-keyboard {
z-index: 999 !important;
}

204
src/pages/Index/Index.vue

@ -83,9 +83,11 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { Time, InitWarn, LoadingModal } from './components/Consumables';
import { getCheckList, startWork, pauseWork, continueWork, stopWork, pollAllAppEvents, getInitState, initDevice, saveMountedCardInfo, openBuzzer, closeBuzzer } from '../../services/index';
import { getCheckList, startWork, pauseWork, continueWork, stopWork, getInitState, initDevice, saveMountedCardInfo, openBuzzer, closeBuzzer } from '../../services/index';
import { CheckItem, User } from '../../types/Index';
import { useConsumablesStore } from '../../store';
// import { useConsumablesStore } from '../../store';
import { createWebSocket } from '../../websocket/socket';
import type { AppEventMessage } from '../../websocket/socket';
const selectedTab = ref(sessionStorage.getItem('selectedTab') || '常规');
const lineWidth = ref(0);
const lineLeft = ref(0);
@ -95,7 +97,7 @@ const showAlreadyModal = ref(false);
const showFailModal = ref(false);
const checkData = ref<CheckItem[]>([]);
const failItems = ref<CheckItem[]>([]);
const consumableStore = useConsumablesStore();
// const consumableStore = useConsumablesStore();
//
const user = ref<User>(JSON.parse(sessionStorage.getItem('token') || '{}') as unknown as User)
const isTesting = ref(false); //
@ -109,104 +111,99 @@ const showWarnModal = ref(false)
const ErrorMessage = ref<string>('')
const showErrorModal = ref(false)
const WarnMessage = ref<string>('')
interface EventType {
typeName: string,
timestamp: number
prompt?: {
type: string,
info: string,
detailInfos: string[],
stackInfo: null
}
actionStep?: string,
actionStepName?: string
}
// A8kEcodeContextListPromptEvent
// const handleA8kEcodeContextListPromptEvent = (error: any): string => {
// switch (error.code) {
// case "APPE_CONSUME_NOT_ENOUGH":
// return `${error.projName}`;
// case "CODEERROR":
// return `${error.exmsg}`;
// case "LOW_ERROR_CHECKCODE_IS_ERROR":
// return "";
// default:
// return ''; //
// interface EventType {
// typeName: string,
// timestamp: number
// prompt?: {
// type: string,
// info: string,
// detailInfos: string[],
// stackInfo: null
// }
// };
const getEventText = (data: EventType | EventType[]): string => {
let eventName = '';
// if
// const eventTypeMap: { [key: string]: string } = {
// "AppIDCardMountEvent": "id",
// "AppIDCardUnmountEvent": "id",
// "AppTubeholderSettingUpdateEvent": "",
// "A8kEcodeContextListPromptEvent": "",
// };
// data
const processEvent = async (item: EventType) => {
//id
if (item.typeName === "AppIDCardMountEvent") {
consumableStore.isIdCardInserted = true;
idCardInserted.value = true;
eventName = "id卡已插入"
} else if (item.typeName === "AppIDCardUnmountEvent") {
consumableStore.isIdCardInserted = false;
eventName = "id卡已拔出"
} else if (item.typeName === "DoA8kStepActionEvent") {
eventName = item.actionStepName!
} else if (item.typeName === "AppPromptEvents") {
//propmt
if (Array.isArray(item.prompt)) {
console.log("propmt是个数组")
item.prompt.forEach(async (item) => {
if (item.type === "Error") {
showErrorModal.value = true
ErrorMessage.value = item.info
await openBuzzer()
} else if (item.type === "Warn") {
showWarnModal.value = true
WarnMessage.value = item.info
}
})
} else {
console.log("propmt不是数组", item)
// actionStep?: string,
// actionStepName?: string
// }
// WebSocket
const ws = createWebSocket("/api/v1/app/ws/event");
//
const handleAppEvent = (data: AppEventMessage['data']) => {
if (data.typeName === 'AppPromptEvents' && data.prompt) {
data.prompt.forEach(async (item) => {
if (item.type === 'Error') {
showErrorModal.value = true;
ErrorMessage.value = item.info;
await openBuzzer();
} else if (item.type === 'Warn') {
showWarnModal.value = true;
WarnMessage.value = item.info;
}
} else if (item.typeName === "AppTubeholderSettingUpdateEvent ") {
eventName = "试管架配置更新"
//
}
else {
eventName = "闲置..."
}
};
if (Array.isArray(data)) {
// item
data.forEach(processEvent);
} else {
//
processEvent(data);
});
}
return eventName;
};
// const getEventText = (data: EventType | EventType[]): string => {
// let eventName = '';
// // data
// const processEvent = async (item: EventType) => {
// //id
// if (item.typeName === "AppIDCardMountEvent") {
// consumableStore.isIdCardInserted = true;
// idCardInserted.value = true;
// eventName = "id"
// } else if (item.typeName === "AppIDCardUnmountEvent") {
// consumableStore.isIdCardInserted = false;
// eventName = "id"
// } else if (item.typeName === "DoA8kStepActionEvent") {
// eventName = item.actionStepName!
// } else if (item.typeName === "AppPromptEvents") {
// //propmt
// if (Array.isArray(item.prompt)) {
// console.log("propmt")
// item.prompt.forEach(async (item) => {
// if (item.type === "Error") {
// showErrorModal.value = true
// ErrorMessage.value = item.info
// await openBuzzer()
// } else if (item.type === "Warn") {
// showWarnModal.value = true
// WarnMessage.value = item.info
// }
// })
// } else {
// console.log("propmt", item)
// }
// } else if (item.typeName === "AppTubeholderSettingUpdateEvent ") {
// eventName = ""
// //
// }
// else {
// eventName = "..."
// }
// };
// if (Array.isArray(data)) {
// // item
// data.forEach(processEvent);
// } else {
// //
// processEvent(data);
// }
// return eventName;
// };
//
const getEvent = async () => {
const res = await pollAllAppEvents();
// console.log("", res);
if (res.success && res.data.length > 0) {
EventText.value = getEventText(res.data);
} else {
//return
return
}
}
// const getEvent = async () => {
// const res = await pollAllAppEvents();
// if (res.success && res.data.length > 0) {
// EventText.value = getEventText(res.data);
// } else {
// //return
// return
// }
// }
//
const confirmError = async () => {
showErrorModal.value = false
@ -228,19 +225,23 @@ const saveIdInfo = async () => {
}
}
// 500ms
const pollingInterval = 500;
let pollingTimer: ReturnType<typeof setInterval>;
// const pollingInterval = 500;
// let pollingTimer: ReturnType<typeof setInterval>;
onMounted(() => {
//
pollingTimer = setInterval(getEvent, pollingInterval);
// pollingTimer = setInterval(getEvent, pollingInterval);
ws.connect();
ws.subscribe<AppEventMessage>('AppEvent', handleAppEvent);
});
onBeforeUnmount(() => {
//
if (pollingTimer) {
clearInterval(pollingTimer);
}
// if (pollingTimer) {
// clearInterval(pollingTimer);
// }
ws.unsubscribe<AppEventMessage>('AppEvent', handleAppEvent);
ws.disconnect();
});
//
const generateErrorMessages = (data: CheckItem[]): string[] => {
@ -558,7 +559,6 @@ onMounted(() => {
.main-content {
flex: 1;
min-height: 0; //
overflow: auto; //
}
}
</style>

44
src/pages/Index/Regular/Consumables.vue

@ -13,12 +13,12 @@
<!--耗材页面 -->
<div class="main-top">
<div class="plate-area">
<SpttingPlates :plates="plates" />
<SpttingPlates :plates="plates" :temperature="currentTemperature" />
</div>
<div class="move-liquid-area">
<MoveLiquidArea :isLoad="isLoad" :isLoading="isLoading" :moveLiquids="moveLiquids" :tempTipNum="tempTipNum"
:bufferBig="bufferBig" :emergencyInfo="emergencyInfo" @loadConsumables="handleIsLoad"
@unloadConsumables="handleIsUnload" @updateTipNum="updateTipNum" />
:bufferBig="bufferBig" :emergencyInfo="emergencyInfo" :wasteStatus="wasteStatus"
@loadConsumables="handleIsLoad" @unloadConsumables="handleIsUnload" @updateTipNum="updateTipNum" />
</div>
</div>
<div class="main-bottom">
@ -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<SensorStateMessage>('SensorState', handleSensorState);
getEmergencyInfo()
})
onBeforeUnmount(() => {
// 线
eventBus.off('confirm', updatePlatesAndBuffers)
if (socket) {
socket.close()
if (socket !== null) {
socket.disconnect() //
}
socket.unsubscribe<SensorStateMessage>('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

25
src/pages/Index/Regular/Emergency.vue

@ -83,10 +83,8 @@
<!-- 键盘 -->
<transition name="slide-up">
<div class="keyboard" v-if="keyboardVisible">
<Keyboard v-model="currentInputValue" layoutName="default" @onChange="handleKeyboardInput"
@close="hideKeyboard" />
<SimpleKeyboard @onChange="handleKeyboardInput" @close="hideKeyboard" v-model="currentInputValue" />
</div>
<!-- <SimpleKeyboard /> -->
</transition>
</div>
</template>
@ -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;

63
src/pages/Index/Regular/Running.vue

@ -41,7 +41,7 @@
<div class="row-first">
<!-- 急诊按钮 -->
<div class="emergency-button" @click="showEmergencyAlert = !showEmergencyAlert">
<span>添加急诊</span>
<span>急诊</span>
</div>
<!-- 试管架区域 -->
<div class="test-tube-rack-area">
@ -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;

3
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

7
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) => {

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

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

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

5
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',

22
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,
}
}

326
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<T extends WebSocketMessage> = (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<MessageHandler<any>>
> = new Map()
constructor(url: string) {
this.url = url
}
// 类型安全的订阅方法
subscribe<T extends WebSocketMessage>(
messageType: T['type'],
handler: MessageHandler<T>,
): void {
if (!this.messageHandlers.has(messageType)) {
this.messageHandlers.set(messageType, new Set())
}
this.messageHandlers.get(messageType)?.add(handler)
}
// 类型安全的取消订阅方法
unsubscribe<T extends WebSocketMessage>(
messageType: T['type'],
handler: MessageHandler<T>,
): 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();
});
*/
Loading…
Cancel
Save