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.

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