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.

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