A8000
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

732 lines
17 KiB

6 months ago
7 months ago
1 month ago
4 months ago
7 months ago
6 months ago
  1. // src/websocket/socket.ts
  2. import { eventBus } from "@/eventBus"
  3. // 基础消息接口
  4. interface BaseMessage {
  5. messageType: 'Report' // 消息类型
  6. dataType: string // 数据类型
  7. timestamp: number // 时间戳
  8. }
  9. export type OptScanModuleState = 'EMPTY' | 'PLATE_IS_READY' | 'SCANNING' | 'ERROR'
  10. // 耗材状态消息
  11. interface OptScanModuleStateMessage extends BaseMessage {
  12. type: 'OptScanModuleState'
  13. messageType: 'Report'
  14. dataType: 'OptScanModuleState'
  15. data: {
  16. state: OptScanModuleState // 状态
  17. isErrorPlate: boolean // 是否为错误板(标识当前光学模组中的反应板存在异常)
  18. bloodType: 'WHOLE_BLOOD' | 'SERUM_OR_PLASMA' // 血液类型
  19. sampleBarcode: string // 样本条码
  20. userid: string // 用户ID
  21. projInfo: ProjectInfo // 项目信息
  22. sampleId: string // 样本ID
  23. projId: number // 项目ID
  24. isEmergency: boolean // 是否急诊
  25. }
  26. timestamp: number
  27. }
  28. export interface EventReport extends PromptAvailable {
  29. typeName: 'AppPromptEvent'
  30. timestamp: number
  31. eventId: string
  32. prompt: TextPrompt | FormsPrompt | TablePrompt
  33. }
  34. export interface IdCardReport {
  35. typeName: 'AppIDCardMountEvent' | 'AppIDCardUnmountEvent'
  36. projectInfo: IdCardPrompt
  37. }
  38. export interface PromptAvailable {
  39. eventId?: string
  40. prompt: EventReport['prompt']
  41. }
  42. export type IdCardPrompt = {
  43. color: string,
  44. expiryDate: number,
  45. id: number,
  46. lotId: string,
  47. palteCode: number,//palteCode 字段,接口命名错了。应该是plateCode
  48. projId: number,
  49. projName: string,
  50. updateChipVersion: string,
  51. }
  52. export type TextPrompt = {
  53. messageLevel: MsgLevel
  54. title: string
  55. info: string
  56. detailInfoType: 'Text'
  57. detailInfo: string
  58. }
  59. export type FormsPrompt = {
  60. messageLevel: MsgLevel
  61. title: string
  62. info: string
  63. detailInfoType: 'Forms'
  64. detailInfo: { name: string; description: string }[]
  65. }
  66. export type TablePrompt = {
  67. messageLevel: MsgLevel
  68. title: string
  69. info: string
  70. detailInfoType: 'Table'
  71. detailInfo: {
  72. header: string[]
  73. types: string[]
  74. vars: TablePromptRecord[]
  75. }
  76. }
  77. export interface AlarmTextPrompt extends TextPrompt {
  78. triggeredStateName: string
  79. notTriggeredDisName: string
  80. flagType: string
  81. }
  82. export interface AlarmTablePrompt extends TablePrompt {
  83. triggeredStateName: string
  84. notTriggeredDisName: string
  85. flagType: string
  86. }
  87. export interface AlarmFormsPrompt extends FormsPrompt {
  88. triggeredStateName: string
  89. notTriggeredDisName: string
  90. flagType: string
  91. }
  92. export type TablePromptRecord = {
  93. messageLevel: MsgLevel
  94. val: string
  95. }[]
  96. // 应用事件消息接口
  97. interface AppEventMessage extends BaseMessage {
  98. type: 'AppEvent'
  99. messageType: 'Report'
  100. dataType: 'AppEvent'
  101. data: EventReport | IdCardReport | ReactionRecordReport
  102. timestamp: number
  103. }
  104. export type MsgLevel = 'Info' | 'Warn' | 'Error'
  105. export type MsgItem = {
  106. time: number
  107. messageLevel: MsgLevel
  108. message: string
  109. }
  110. export interface FooterMessageState extends BaseMessage {
  111. type: 'MessageBoxState'
  112. messageType: 'Report'
  113. dataType: 'MessageBoxState'
  114. data: {
  115. topMessage: MsgItem
  116. messageBoxList: MsgItem[]
  117. }
  118. }
  119. export type BloodType = 'WHOLE_BLOOD' | 'SERUM_OR_PLASMA' | 'FECES'
  120. type WorkState = 'IDLE' | 'WORKING' | 'PAUSE'
  121. // 设备工作状态消息
  122. interface DeviceWorkStateMessage extends BaseMessage {
  123. type: 'DeviceWorkState'
  124. messageType: 'Report'
  125. dataType: 'DeviceWorkState'
  126. data: {
  127. workState: WorkState // 设备工作状态
  128. lastWorkState: WorkState
  129. fatalErrorFlag: boolean // 致命错误标志
  130. ecodeList: string[] // 错误代码列表
  131. pending: boolean // 待处理状态
  132. consumeNotEnoughErrorFlag: boolean
  133. }
  134. timestamp: number // 时间戳
  135. }
  136. export type LoginUser = {
  137. id: number
  138. account: string
  139. password: string
  140. usrRole: string
  141. isBuiltInUser: boolean
  142. }
  143. export interface DeviceContextStateMessage extends BaseMessage {
  144. type: 'DeviceContext'
  145. messageType: 'Report'
  146. dataType: 'DeviceContext'
  147. data: {
  148. loginFlag: boolean
  149. loginUser: LoginUser
  150. deviceInitedFlag: boolean
  151. }
  152. }
  153. export type EmergencyTubeState =
  154. | 'EMPTY'
  155. | 'TO_BE_PROCESSED'
  156. | 'PENDING'
  157. | 'RESOURCE_IS_READY'
  158. | 'PROCESSING'
  159. | 'PROCESS_COMPLETE'
  160. | 'ERROR'
  161. export const emergencyStateDesc = {
  162. EMPTY: '空',
  163. TO_BE_PROCESSED: '待处理',
  164. PENDING: '已挂起',
  165. RESOURCE_IS_READY: '已就绪',
  166. PROCESSING: '处理中',
  167. PROCESS_COMPLETE: '完成',
  168. ERROR: '出错',
  169. }
  170. export const emergencyStateColor = {
  171. EMPTY: '',
  172. TO_BE_PROCESSED: '#56ccab',
  173. PENDING: '#a793ec',
  174. RESOURCE_IS_READY: '#17b60d',
  175. PROCESSING: '#e35958',
  176. PROCESS_COMPLETE: '#773f16',
  177. ERROR: '#e93527',
  178. }
  179. // 急诊位状态消息
  180. interface EmergencyPosStateMessage extends BaseMessage {
  181. type: 'EmergencyPosState'
  182. messageType: 'Report'
  183. dataType: 'EmergencyPosState'
  184. data: {
  185. tube: {
  186. sampleId: string | null // 样本ID
  187. pos: number // 位置
  188. isHighTube: boolean // 是否为高试管
  189. isEmergency: boolean // 是否为急诊
  190. bloodType: BloodType // 血液类型
  191. sampleBarcode: string // 样本条码
  192. userid: string // 用户ID
  193. projInfo: ProjectInfo[] // 项目信息列表
  194. projIds: number[] // 项目ID列表
  195. state: EmergencyTubeState // 状态
  196. errors: string[] // 错误信息列表
  197. }
  198. }
  199. timestamp: number
  200. }
  201. export type RunningTubeState =
  202. | 'EMPTY'
  203. | 'TO_BE_PROCESSED'
  204. | 'PENDING'
  205. | 'RESOURCE_IS_READY'
  206. | 'PROCESSING'
  207. | 'PROCESSED'
  208. | 'PROCESS_COMPLETE'
  209. | 'ERROR'
  210. export type TubeHolderType =
  211. | 'BloodTube'
  212. | 'MiniTube'
  213. | 'MiniBlood'
  214. | 'BulletTube1P5'
  215. | 'BulletTube0P5'
  216. | 'StoolTestTube'
  217. // 试管架状态消息
  218. interface TubeHolderStateMessage extends BaseMessage {
  219. type: 'TubeHolderState'
  220. messageType: 'Report'
  221. dataType: 'TubeHolderState'
  222. data: {
  223. tubeHolderType: TubeHolderType
  224. tubes: Array<{
  225. sampleId: string | null
  226. pos: number
  227. isHighTube: boolean
  228. isEmergency: boolean
  229. bloodType: BloodType
  230. sampleBarcode: string
  231. userid: string
  232. projInfo: ProjectInfo[]
  233. projIds: number[]
  234. state: RunningTubeState
  235. errors: string[]
  236. erroInfo: string
  237. }>
  238. state: string
  239. }
  240. timestamp: number
  241. }
  242. export interface TubeSetting {
  243. tubeIndex: number
  244. userid: string
  245. sampleBarcode: string
  246. projId: number[]
  247. bloodType: BloodType
  248. }
  249. export interface TestTubeRack {
  250. uuid: string
  251. state: 'INACTIVE' | 'ACTIVE' | 'LOCKED'
  252. tubeSettings: TubeSetting[]
  253. }
  254. export interface TubeHolderSettingMessage extends BaseMessage {
  255. type: 'TubeHolderSetting'
  256. messageType: 'Report'
  257. dataType: 'TubeHolderSetting'
  258. data: TestTubeRack[]
  259. }
  260. // 传感器状态消息
  261. interface SensorStateMessage extends BaseMessage {
  262. type: 'SensorState'
  263. messageType: 'Report'
  264. dataType: 'SensorState'
  265. data: {
  266. pboxTemperature: number //板夹仓温度
  267. incubateBoxTemperature: number // 孵育盘温度
  268. wasteBinFullFlag: boolean // 废废料仓状态
  269. }
  270. timestamp: number
  271. }
  272. // 项目信息接口
  273. interface ProjectInfo {
  274. projId: number
  275. projName: string
  276. projShortName: string
  277. color: string
  278. supportBloodTypes: string[]
  279. }
  280. export type SubTankState =
  281. | 'EMPTY'
  282. | 'RESERVED'
  283. | 'WAITING_FOR_DROP'
  284. | 'INCUBATING'
  285. | 'INCUBATION_COMPLETE'
  286. | 'ERROR'
  287. // 子槽位信息接口
  288. export interface SubTank {
  289. pos: string // 位置编号 SPACE01-SPACE20
  290. state: SubTankState // 槽位状态
  291. bloodType: BloodType // 血液类型
  292. sampleBarcode: string // 样本条码
  293. lotId: string
  294. userid: string // 用户ID
  295. projInfo: ProjectInfo // 项目信息
  296. sampleId: string // 样本ID
  297. projId: number // 项目ID
  298. startIncubatedTime: number // 开始孵育时间
  299. incubatedTimeSec: number // 孵育时间(秒)
  300. remainTimeSec: number //剩余孵育时间(显示)
  301. errors: string[] // 错误信息列表
  302. isEmergency: boolean //是否急诊位
  303. errorInfo: SubTankError
  304. // isPlaceholder?: boolean // 是否为占位符
  305. }
  306. interface DetailInfo {
  307. description: string
  308. name: string
  309. }
  310. interface SubTankError {
  311. detailInfo: DetailInfo[]
  312. detailInfoType: string
  313. info: string
  314. messageLevel: string
  315. title: string
  316. }
  317. // 孵育板状态消息
  318. interface IncubationPlateStateMessage extends BaseMessage {
  319. type: 'IncubationPlateState'
  320. messageType: 'Report'
  321. dataType: 'IncubationPlateState'
  322. data: {
  323. subtanks: SubTank[] // 20个子槽位信息
  324. }
  325. timestamp: number
  326. }
  327. // ��材组信息基础接口
  328. export interface ConsumableGroupBase {
  329. projId: number
  330. projName: string
  331. projShortName: string
  332. lotId: string
  333. color: string
  334. num: number
  335. isInstall: boolean
  336. reserveNum: number
  337. group: string
  338. }
  339. export interface ReactionPlateGroup extends Partial<ConsumableGroupBase> {}
  340. // 小缓冲液接口
  341. interface LittleBottleGroup extends Partial<ConsumableGroupBase> {}
  342. // 大缓冲液接口
  343. interface LargeBottleGroup extends Partial<ConsumableGroupBase> {}
  344. // Tips信息接口
  345. interface TipInfo {
  346. tipNum: number
  347. totalNum?: number
  348. }
  349. // 耗材状态消息接口 consumableStore
  350. interface ConsumablesStateMessage extends BaseMessage {
  351. type: 'ConsumablesState'
  352. messageType: 'Report'
  353. dataType: 'ConsumablesState'
  354. data: {
  355. tips: TipInfo[]
  356. reactionPlateGroup: ReactionPlateGroup[]
  357. littBottleGroup: LittleBottleGroup[]
  358. larBottleGroup: LargeBottleGroup[]
  359. }
  360. timestamp: number
  361. }
  362. // 消息类型联合
  363. type WebSocketMessage =
  364. | OptScanModuleStateMessage
  365. | DeviceWorkStateMessage
  366. | DeviceContextStateMessage
  367. | EmergencyPosStateMessage
  368. | TubeHolderStateMessage
  369. | TubeHolderSettingMessage
  370. | SensorStateMessage
  371. | IncubationPlateStateMessage
  372. | AppEventMessage
  373. | FooterMessageState
  374. | ConsumablesStateMessage
  375. | AppFlagStateListMessage
  376. | PreReactionPosGroupStateMessage
  377. // 消息处理器类型
  378. type MessageHandler<T extends WebSocketMessage> = (data: T['data']) => void
  379. class WebSocketClient {
  380. private ws: WebSocket | null = null
  381. private url: string
  382. private reconnectAttempts: number = -1
  383. private maxReconnectAttempts: number = 5
  384. private reconnectInterval: number = 3000
  385. // 使用类型安全的消息处理器映射
  386. private messageHandlers: Map<
  387. WebSocketMessage['dataType'],
  388. Set<MessageHandler<any>>
  389. > = new Map()
  390. constructor(url: string) {
  391. this.url = url
  392. }
  393. // 类型安全的订阅方法
  394. subscribe<T extends WebSocketMessage>(
  395. messageType: T['dataType'],
  396. handler: MessageHandler<T>,
  397. ): void {
  398. if (!this.messageHandlers.has(messageType)) {
  399. console.log(
  400. '🚀 ~ WebSocketClient ~ subscribe ~ messageType:',
  401. messageType,
  402. )
  403. this.messageHandlers.set(messageType, new Set())
  404. }
  405. console.log('messageType---', messageType)
  406. this.messageHandlers.get(messageType)?.add(handler)
  407. }
  408. // 类型安全的取消订阅方法
  409. unsubscribe<T extends WebSocketMessage>(
  410. messageType: T['dataType'],
  411. handler: MessageHandler<T>,
  412. ): void {
  413. this.messageHandlers.get(messageType)?.delete(handler)
  414. }
  415. private handleMessage(message: WebSocketMessage): void {
  416. const handlers = this.messageHandlers.get(message.dataType)
  417. if (!handlers) {
  418. // if (message.dataType === 'MessageBoxState') {
  419. // console.log(
  420. // '🚀 ~ WebSocketClient ~ handleMessage ~ handlers is undefined for message type:',
  421. // message.dataType,
  422. // )
  423. // }
  424. return
  425. }
  426. handlers.forEach((handler) => {
  427. try {
  428. handler(message.data)
  429. } catch (error) {
  430. console.error(`处理 ${message.dataType} 消息时出错:`, error)
  431. }
  432. })
  433. }
  434. // 连接 WebSocket
  435. connect(): void {
  436. try {
  437. // WebSocket.CONNECTING (0) WebSocket.OPEN (1)
  438. if (this.ws && this.ws.readyState <= 1) {
  439. // 已连接
  440. console.log(`${this.url} 正在连接或已连接,无需重复连接`)
  441. } else {
  442. this.ws = new WebSocket(this.url)
  443. this.bindEvents()
  444. }
  445. } catch (error) {
  446. console.error('WebSocket 连接失败:', error)
  447. this.reconnect()
  448. }
  449. }
  450. // 绑定事件
  451. private bindEvents(): void {
  452. if (!this.ws) return
  453. // 连接建立时的处理
  454. this.ws.onopen = () => {
  455. if (this.reconnectAttempts !== -1) {
  456. // 从断开连接 进入 恢复连接
  457. this.reconnectAttempts = -1
  458. eventBus.emit('socketClosed', -1)
  459. }
  460. console.log('WebSocket 连接已建立')
  461. this.reconnectAttempts = -1 // 重置重连次数
  462. }
  463. // 接收消息的处理
  464. this.ws.onmessage = (event: MessageEvent) => {
  465. try {
  466. const data = JSON.parse(event.data)
  467. // console.log('🚀 ~ WebSocketClient ~ bindEvents ~ data:', data)
  468. this.handleMessage(data)
  469. } catch (error) {
  470. console.error('消息解析错误:', error)
  471. }
  472. }
  473. // 连接关闭时的处理
  474. this.ws.onclose = () => {
  475. console.log('WebSocket 连接已关闭')
  476. eventBus.emit('socketClosed', 0)
  477. this.reconnect()
  478. }
  479. // 错误处理
  480. this.ws.onerror = (error) => {
  481. console.error('WebSocket 错误:', error)
  482. }
  483. }
  484. // 重连机制
  485. private reconnect(): void {
  486. if (this.reconnectAttempts === -1) {
  487. this.reconnectAttempts = 0
  488. }
  489. if (this.reconnectAttempts >= this.maxReconnectAttempts) {
  490. eventBus.emit('socketClosed', this.maxReconnectAttempts)
  491. console.log('达到最大重连次数,停止重连')
  492. this.reconnectAttempts = -1
  493. return
  494. }
  495. setTimeout(() => {
  496. console.log(`尝试第 ${this.reconnectAttempts + 1} 次重连...`)
  497. this.reconnectAttempts++
  498. this.connect()
  499. }, this.reconnectInterval)
  500. }
  501. // 关闭连接
  502. disconnect(): void {
  503. if (this.ws) {
  504. this.ws.close()
  505. this.ws = null
  506. }
  507. }
  508. }
  509. const urlSocketMap = new Map()
  510. // 导出消息类型
  511. // 导出消息类型
  512. export type {
  513. WebSocketMessage,
  514. OptScanModuleStateMessage,
  515. DeviceWorkStateMessage,
  516. EmergencyPosStateMessage,
  517. TubeHolderStateMessage,
  518. SensorStateMessage,
  519. IncubationPlateStateMessage,
  520. ProjectInfo,
  521. AppEventMessage,
  522. ConsumablesStateMessage,
  523. LittleBottleGroup,
  524. LargeBottleGroup,
  525. TipInfo,
  526. AppFlagStateListMessage,
  527. PreReactionPosGroupStateMessage,
  528. }
  529. // 导出 WebSocket 客户端
  530. export const createWebSocket = (url: string): WebSocketClient => {
  531. if (urlSocketMap.has(url)) {
  532. return urlSocketMap.get(url)
  533. } else {
  534. const client = new WebSocketClient(url)
  535. urlSocketMap.set(url, client)
  536. return client
  537. }
  538. }
  539. // 使用示例:
  540. /*
  541. import { createWebSocket } from './websocket/socket';
  542. import { ProjectInfo } from '../types/Index/Emergency';
  543. // 创建 WebSocket 连接
  544. const ws = createWebSocket('ws://your-websocket-server-url');
  545. ws.connect();
  546. // 在组件销毁时断开连接
  547. onUnmounted(() => {
  548. ws.disconnect();
  549. });
  550. */
  551. export type WarningState = {
  552. state: boolean
  553. keyName: string
  554. cnName: string
  555. errorDetailInfo?: TextPrompt | FormsPrompt | TablePrompt
  556. errorPromptInfo: TextPrompt | FormsPrompt | TablePrompt
  557. triggeredStateName: string
  558. notTriggeredDisName: string
  559. flagType: string
  560. }
  561. export type AppFlagStateList = {
  562. hasErrorTriggerFlag: boolean
  563. hasWarningTriggerFlag: boolean
  564. states: WarningState[]
  565. }
  566. export type TankInfo = {
  567. state: string
  568. projId: string
  569. bloodType: string
  570. errorInfo?: ErrorPromptInfo
  571. }
  572. export type Table = {
  573. messageLevel: string,
  574. title: string,
  575. info: string,
  576. detailInfoType: string,
  577. detailInfo: TableDetailInfo
  578. }
  579. export type TableDetailInfo = {
  580. header: string[],
  581. types: string[],
  582. vars: vars[][]
  583. }
  584. type vars = {
  585. messageLevel: string
  586. val: string
  587. }
  588. type Forms = {
  589. name: string
  590. description: string
  591. }
  592. export type ErrorPromptInfo = {
  593. detailInfo: TableDetailInfo | Forms | string
  594. detailInfoType: string
  595. info: string
  596. messageLevel: string
  597. title: string
  598. state: string
  599. }
  600. interface AppFlagStateListMessage extends BaseMessage {
  601. type: 'AppFlagStateList'
  602. messageType: 'Report'
  603. dataType: 'AppFlagStateList'
  604. data: AppFlagStateList
  605. }
  606. interface SampleInfo {
  607. sampleId: string
  608. isHighTube: boolean
  609. isEmergency: boolean
  610. bloodType: string
  611. sampleBarcode: string
  612. userid: string
  613. }
  614. interface Grids {
  615. state: string
  616. consumableType?: string
  617. group: string
  618. posIndex: number
  619. startReactionTime: number
  620. reactionRemainingTime: number
  621. totalReactionTime: number
  622. sampleInfo: SampleInfo
  623. bindIncubatorPos?: string | null
  624. }
  625. interface ProjBriefInfo {
  626. projId: number
  627. projName: string
  628. projShortName: string
  629. color: string
  630. }
  631. export interface PreReactionPosGroupState {
  632. grids: Grids[]
  633. consumableType?: string
  634. group: string
  635. hasSomeGridInReacting: boolean
  636. hasSomeGridReactedCompleted: boolean
  637. isInstalled: boolean
  638. version: string
  639. projBriefInfo: ProjBriefInfo
  640. inReacting: boolean
  641. }
  642. export interface PreReactionPosGroupStateMap {
  643. [key: string]: PreReactionPosGroupState
  644. }
  645. interface PreReactionPosGroupStateMessage extends BaseMessage {
  646. type: 'PreReactionPosGroupState'
  647. messageType: 'Report'
  648. dataType: 'PreReactionPosGroupState'
  649. data: PreReactionPosGroupState
  650. }
  651. type reactionRecordItem = {
  652. id: string
  653. }
  654. export type ReactionRecordReport = {
  655. typeName: 'AppNewReactionRecordEvent',
  656. reactionRecord: reactionRecordItem
  657. }
  658. export type StatusDetailReport = TextPrompt | FormsPrompt | TablePrompt