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.

592 lines
15 KiB

8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
  1. <template>
  2. <div id="regular-container">
  3. <!-- 加载蒙层 -->
  4. <div class="loading-overlay" v-if="isLoading">
  5. <div class="loading-content">
  6. <div class="loading-spinner"></div>
  7. <span class="loading-text">正在扫描耗材...</span>
  8. </div>
  9. </div>
  10. <!-- 扫描结果弹窗 -->
  11. <div v-if="showScanResults" class="scan-results-overlay">
  12. <div class="scan-results-container">
  13. <div class="scan-results-header">
  14. <span class="title">扫描结果</span>
  15. </div>
  16. <div class="scan-results-content">
  17. <table class="scan-results-table">
  18. <thead>
  19. <tr>
  20. <th>通道</th>
  21. <th>项目名称</th>
  22. <th>批次号</th>
  23. <th>状态</th>
  24. </tr>
  25. </thead>
  26. <tbody>
  27. <tr v-for="report in formattedReports" :key="report.channel" :class="{ 'error-row': report.isError }">
  28. <td>通道{{ report.channel + 1 }}</td>
  29. <td>{{ report.projName || '--' }}</td>
  30. <td>{{ report.lotId || '--' }}</td>
  31. <td :class="report.isError ? 'error-text' : 'success-text'">
  32. {{ report.message }}
  33. </td>
  34. </tr>
  35. </tbody>
  36. </table>
  37. </div>
  38. <div class="scan-results-footer">
  39. <button class="confirm-btn" @click="handleConfirmScan">确认</button>
  40. </div>
  41. </div>
  42. </div>
  43. <!--耗材页面 -->
  44. <div class="main-top">
  45. <div class="plate-area">
  46. <SpttingPlates :plates="plates" :temperature="currentTemperature" />
  47. </div>
  48. <div class="move-liquid-area">
  49. <MoveLiquidArea :isLoad="isLoad" :isLoading="isLoading" :moveLiquids="moveLiquids" :tempTipNum="tempTipNum"
  50. :bufferBig="bufferBig" :emergencyInfo="emergencyInfo" :wasteStatus="wasteStatus"
  51. @loadConsumables="handleIsLoad" @unloadConsumables="handleIsUnload" @updateTipNum="updateTipNum" />
  52. </div>
  53. </div>
  54. <div class="main-bottom">
  55. <!--缓冲液区域小-->
  56. <div class="buffer-little-title">
  57. <div class="line"></div>
  58. <div class="content">缓冲液()</div>
  59. </div>
  60. <div class="ball-area">
  61. <MainComponent v-for="item in bufferLittles" :key="item" class="ball-grid" :projectName="item.projShortName"
  62. :currentCount="item.num" :totalBalls="25" :activatedBalls="item.num" :columns="5" gridWidth="240px"
  63. gridHeight="235px" :activeColor="item.color" />
  64. </div>
  65. </div>
  66. </div>
  67. </template>
  68. <script setup lang="ts">
  69. import { MoveLiquidArea, SpttingPlates, MainComponent } from '../components'
  70. import { ref, onMounted, onActivated, onBeforeUnmount, watch } from 'vue'
  71. import { scanConsumables, updateTipsNum } from '../../../services/Index/index'
  72. import { useConsumablesStore, useEmergencyStore } from '../../../store'
  73. import { useDeviceStore } from '../../../store/index'
  74. import { eventBus } from '../../../eventBus'
  75. import {
  76. ReactionPlate,
  77. BottleGroup,
  78. LiquidState,
  79. } from '../../../types/Index'
  80. import { createWebSocket } from '../../../websocket/socket'
  81. import type { ConsumablesStateMessage, SensorStateMessage, EmergencyPosStateMessage } from '../../../websocket/socket';
  82. import { getServerInfo } from '../../../utils/getServerInfo'
  83. import { formatScanReports } from '../../../utils/errorHandler'
  84. import { ElMessage } from 'element-plus'
  85. const { wsUrl } = getServerInfo('/api/v1/app/ws/state')
  86. const socket = createWebSocket(wsUrl)
  87. const consumableStore = useConsumablesStore()
  88. const emergencyStore = useEmergencyStore()
  89. const deviceStore = useDeviceStore()
  90. // 温度状态
  91. const currentTemperature = ref(40);
  92. // 废料区状态
  93. const wasteStatus = ref(false)
  94. // 父组件状态
  95. const isLoad = ref(false)
  96. const isLoading = ref(false)
  97. //管理反应板夹的状态
  98. const plates = ref<ReactionPlate[]>([])
  99. // 管理移液盘的状态
  100. const moveLiquids = ref<LiquidState[]>([
  101. {
  102. id: 1,
  103. tipNum: 0,
  104. },
  105. {
  106. id: 2,
  107. tipNum: 0,
  108. },
  109. {
  110. id: 3,
  111. tipNum: 0,
  112. },
  113. ])
  114. // 新增状态
  115. const showScanResults = ref(false)
  116. interface ScanReport {
  117. channel: number
  118. code: string
  119. message: string
  120. isError: boolean
  121. projName: string | null
  122. lotId: string | null
  123. }
  124. const formattedReports = ref<ScanReport[]>([])
  125. //是否加载
  126. const isAlreadyLoad = ref(false)
  127. // 临时状态管理小球激活数量
  128. const tempTipNum = ref<number[]>([
  129. ...moveLiquids.value.map((liquid) => liquid.tipNum),
  130. ])
  131. //管理缓冲液小的状态
  132. interface BufferLittle {
  133. id: number
  134. num: number
  135. projShortName: string
  136. color: string
  137. lotId?: string
  138. type?: string
  139. projId?: number
  140. projName?: string
  141. }
  142. const bufferLittles = ref<BufferLittle[]>([
  143. {
  144. id: 1,
  145. num: 0,
  146. projShortName: '',
  147. color: '#4caf50',
  148. },
  149. {
  150. id: 2,
  151. num: 0,
  152. projShortName: '',
  153. color: '#4caf50',
  154. },
  155. {
  156. id: 3,
  157. num: 0,
  158. projShortName: '',
  159. color: '#4caf50',
  160. },
  161. {
  162. id: 4,
  163. num: 0,
  164. projShortName: '',
  165. color: '#4caf50',
  166. },
  167. {
  168. id: 5,
  169. num: 0,
  170. projShortName: '',
  171. color: '#4caf50',
  172. },
  173. {
  174. id: 6,
  175. num: 0,
  176. projShortName: '',
  177. color: '#4caf50',
  178. },
  179. ])
  180. //管理大缓冲液的状态
  181. const bufferBig = ref<BottleGroup[]>([])
  182. //急诊区状态
  183. const emergencyInfo = ref(emergencyStore.$state.emergencyInfo || {})
  184. //是否处理扫描结果
  185. const isHandleScan = ref(false)
  186. // 确认扫描结果
  187. const handleConfirmScan = () => {
  188. showScanResults.value = false
  189. isHandleScan.value = true
  190. if (formattedReports.value.some(report => report.isError)) {
  191. isLoad.value = false
  192. isAlreadyLoad.value = false
  193. ElMessage.warning('存在错误,请检查耗材')
  194. }
  195. }
  196. //使用websocket保证数据的实时性
  197. const startWebSocket = () => {
  198. socket.connect()
  199. }
  200. // 处理传感器状态消息
  201. const handleSensorState = (data: SensorStateMessage['data']) => {
  202. // 更新温度值(这里使用孵育盒温度)
  203. currentTemperature.value = data.incubateBoxTemperature;
  204. wasteStatus.value = data.wasteBinFullFlag
  205. consumableStore.updateWasteStatus(wasteStatus.value)
  206. // 可以添加温度异常处理逻辑
  207. if (currentTemperature.value > 40) {
  208. console.warn('温度过高警告');
  209. // 可以在这里添加其他警告逻辑
  210. }
  211. };
  212. //处理耗材状态
  213. const handleConsumablesState = (data: ConsumablesStateMessage['data']) => {
  214. if (isAlreadyLoad.value && isHandleScan.value) {
  215. consumableStore.setConsumablesData(data)
  216. moveLiquids.value = data.tips
  217. plates.value = data.reactionPlateGroup as ReactionPlate[]
  218. bufferLittles.value = data.littBottleGroup as BufferLittle[]
  219. bufferBig.value = data.larBottleGroup as BottleGroup[]
  220. } else {
  221. return
  222. }
  223. }
  224. // 使用事件总线更新状态
  225. const updatePlatesAndBuffers = ({
  226. value,
  227. index,
  228. }: {
  229. value: number
  230. index: number
  231. }) => {
  232. if (plates.value && plates.value[index]) plates.value[index].num = value
  233. if (bufferLittles.value && bufferLittles.value[index])
  234. bufferLittles.value[index].num = value
  235. if (bufferBig.value && bufferBig.value[index])
  236. bufferBig.value[index].isUse = value > 0
  237. }
  238. onMounted(() => {
  239. eventBus.on('confirm', updatePlatesAndBuffers)
  240. startWebSocket()
  241. socket.subscribe<SensorStateMessage>('SensorState', handleSensorState);
  242. socket.subscribe<ConsumablesStateMessage>('ConsumablesStateService', handleConsumablesState);
  243. // getEmergencyInfo()
  244. })
  245. onBeforeUnmount(() => {
  246. // 清除事件总线的监听
  247. eventBus.off('confirm', updatePlatesAndBuffers)
  248. if (socket !== null) {
  249. socket.disconnect() // 断开连接
  250. }
  251. socket.unsubscribe<SensorStateMessage>('SensorState', handleSensorState);
  252. socket.unsubscribe<ConsumablesStateMessage>('ConsumablesStateService', handleConsumablesState);
  253. })
  254. // 在组件激活时恢复状态
  255. onActivated(() => {
  256. emergencyInfo.value = emergencyStore.$state.emergencyInfo || {}
  257. if (!isLoad.value) {
  258. console.log('组件被激活了')
  259. }
  260. })
  261. // 子组件传值
  262. //存储耗材数据
  263. const consumablesData = ref<ConsumablesStateMessage['data'] | null>(null)
  264. // 修改加载耗材的处理函数
  265. const handleIsLoad = async () => {
  266. isLoading.value = true
  267. try {
  268. const res = await scanConsumables()
  269. isLoading.value = false
  270. // 格式化扫描结果
  271. formattedReports.value = formatScanReports(res.data.scanReports)
  272. console.log('🚀 ~ handleIsLoad ~ formattedReports:', formattedReports.value)
  273. // 显示扫描结果弹窗
  274. showScanResults.value = true
  275. consumablesData.value = res.data.consumableState
  276. // 更新状态
  277. if (res.data.consumableState) {
  278. }
  279. } catch (error) {
  280. console.error('加载耗材失败:', error)
  281. isLoading.value = false
  282. isAlreadyLoad.value = false
  283. ElMessage.error('加载耗材失败')
  284. }
  285. }
  286. watch(isHandleScan, (newVal) => {
  287. if (newVal) {
  288. // 确认后更新状态
  289. if (consumablesData.value) {
  290. moveLiquids.value = consumablesData.value.tips
  291. plates.value = consumablesData.value.reactionPlateGroup as ReactionPlate[]
  292. bufferLittles.value = consumablesData.value.littBottleGroup as BufferLittle[]
  293. bufferBig.value = consumablesData.value.larBottleGroup as BottleGroup[]
  294. tempTipNum.value = [...moveLiquids.value.map((liquid) => liquid.tipNum)]
  295. isLoad.value = true
  296. isAlreadyLoad.value = true
  297. consumableStore.setConsumablesData(consumablesData.value)
  298. // 清空临时数据
  299. consumablesData.value = null
  300. }
  301. } else {
  302. // 取消时清空临时数据
  303. consumablesData.value = null
  304. }
  305. })
  306. const handleIsUnload = () => {
  307. isLoad.value = !isLoad.value
  308. isLoading.value = false
  309. isAlreadyLoad.value = false
  310. socket.unsubscribe<ConsumablesStateMessage>('ConsumablesStateService', handleConsumablesState);
  311. // 重置 moveLiquids 和 tempTipNum
  312. moveLiquids.value = [
  313. { id: 1, tipNum: 0 },
  314. { id: 2, tipNum: 0 },
  315. { id: 3, tipNum: 0 },
  316. ]
  317. tempTipNum.value = [...moveLiquids.value.map((liquid) => liquid.tipNum)]
  318. plates.value = []
  319. emergencyStore.unloadInfo()
  320. emergencyInfo.value = {} as EmergencyPosStateMessage['data']['tube']
  321. bufferLittles.value = [
  322. {
  323. id: 1,
  324. num: 0,
  325. projShortName: '',
  326. color: '#4caf50',
  327. },
  328. {
  329. id: 2,
  330. num: 0,
  331. projShortName: '',
  332. color: '#4caf50',
  333. },
  334. {
  335. id: 3,
  336. num: 0,
  337. projShortName: '',
  338. color: '#4caf50',
  339. },
  340. {
  341. id: 4,
  342. num: 0,
  343. projShortName: '',
  344. color: '#4caf50',
  345. },
  346. {
  347. id: 5,
  348. num: 0,
  349. projShortName: '',
  350. color: '#4caf50',
  351. },
  352. {
  353. id: 6,
  354. num: 0,
  355. projShortName: '',
  356. color: '#4caf50',
  357. },
  358. ]
  359. bufferBig.value = []
  360. }
  361. const updateTipNum = async ({ index, tipNum }: { index: number; tipNum: number }) => {
  362. // 新临时状态
  363. tempTipNum.value[index] = tipNum
  364. console.log('🚀 ~ updateTipNum ~ tempTipNum:', tempTipNum.value)
  365. //调用接口
  366. try {
  367. if (deviceStore.status === 'IDLE') {
  368. await updateTipsNum({ group: `CG${index}`, num: tipNum })
  369. } else {
  370. ElMessage.error('设备正在工作,无法修改数值')
  371. }
  372. } catch (error) {
  373. console.error('修改耗材数量失败:', error)
  374. }
  375. }
  376. </script>
  377. <style scoped lang="less">
  378. #regular-container {
  379. width: 100%;
  380. height: 100%;
  381. display: flex;
  382. flex-direction: column;
  383. .loading-overlay {
  384. position: fixed;
  385. top: 0;
  386. left: 0;
  387. width: 100vw;
  388. height: 100vh;
  389. background: rgba(0, 0, 0, 0.7);
  390. backdrop-filter: blur(4px);
  391. display: flex;
  392. justify-content: center;
  393. align-items: center;
  394. z-index: 9998;
  395. }
  396. .loading-content {
  397. display: flex;
  398. flex-direction: column;
  399. align-items: center;
  400. gap: 20px;
  401. }
  402. .loading-spinner {
  403. width: 60px;
  404. height: 60px;
  405. border: 4px solid #f3f3f3;
  406. border-top: 4px solid #4caf50;
  407. border-radius: 50%;
  408. animation: spin 1s linear infinite;
  409. }
  410. .loading-text {
  411. color: white;
  412. font-size: 24px;
  413. font-weight: 500;
  414. }
  415. .loading-progress {
  416. width: 300px;
  417. height: 6px;
  418. background: rgba(255, 255, 255, 0.2);
  419. border-radius: 3px;
  420. overflow: hidden;
  421. }
  422. .progress-bar {
  423. height: 100%;
  424. background: #4caf50;
  425. border-radius: 3px;
  426. transition: width 0.1s linear;
  427. }
  428. @keyframes spin {
  429. 0% {
  430. transform: rotate(0deg);
  431. }
  432. 100% {
  433. transform: rotate(360deg);
  434. }
  435. }
  436. .main-top {
  437. display: flex;
  438. justify-content: space-between;
  439. margin-top: 10px;
  440. height: 100%;
  441. .plate-area {
  442. width: 355px;
  443. }
  444. .move-liquid-area {
  445. width: calc(100% - 355px);
  446. }
  447. }
  448. .main-bottom {
  449. width: 100%;
  450. flex: 1;
  451. margin-top: -90px;
  452. .buffer-little-title {
  453. display: flex;
  454. height: 40px;
  455. line-height: 40px;
  456. align-items: center;
  457. .line {
  458. width: 4px;
  459. height: 20px;
  460. background-color: #4caf50;
  461. margin: 0 10px 0;
  462. }
  463. .content {
  464. font-size: 26px;
  465. }
  466. }
  467. .ball-area {
  468. display: flex;
  469. justify-self: space-between;
  470. flex-wrap: wrap;
  471. .ball-grid {
  472. margin: 0 15px 5px 15px
  473. }
  474. }
  475. }
  476. .scan-results-overlay {
  477. position: fixed;
  478. top: 0;
  479. left: 0;
  480. width: 100vw;
  481. height: 100vh;
  482. background: rgba(0, 0, 0, 0.7);
  483. backdrop-filter: blur(4px);
  484. display: flex;
  485. justify-content: center;
  486. align-items: center;
  487. z-index: 10000;
  488. pointer-events: auto;
  489. }
  490. .scan-results-container {
  491. background: white;
  492. border: 2px solid #ff0000;
  493. border-radius: 8px;
  494. width: 800px;
  495. padding: 20px;
  496. position: relative;
  497. z-index: 10001;
  498. pointer-events: auto;
  499. .scan-results-header {
  500. text-align: center;
  501. margin-bottom: 20px;
  502. .title {
  503. color: #ff0000;
  504. font-size: 32px;
  505. font-weight: bold;
  506. }
  507. }
  508. .scan-results-content {
  509. .scan-results-table {
  510. width: 100%;
  511. border-collapse: collapse;
  512. margin: 20px 0;
  513. th,
  514. td {
  515. padding: 15px;
  516. text-align: left;
  517. font-size: 24px;
  518. border-bottom: 1px solid #eee;
  519. }
  520. th {
  521. color: #ff0000;
  522. font-weight: bold;
  523. background-color: #fff;
  524. }
  525. .error-row {
  526. background-color: rgba(255, 0, 0, 0.05);
  527. }
  528. .error-text {
  529. color: #ff0000;
  530. }
  531. .success-text {
  532. color: #4caf50;
  533. }
  534. }
  535. }
  536. .scan-results-footer {
  537. margin-top: 20px;
  538. text-align: center;
  539. .confirm-btn {
  540. background: #ff0000;
  541. color: white;
  542. border: none;
  543. border-radius: 25px;
  544. padding: 10px 40px;
  545. font-size: 24px;
  546. cursor: pointer;
  547. &:hover {
  548. background: darken(#ff0000, 10%);
  549. }
  550. }
  551. }
  552. }
  553. }
  554. </style>