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.

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