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.

555 lines
13 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
  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
  51. :plates="consumableStore.consumableData?.reactionPlateGroup"
  52. :temperature="deviceStore.sensorState?.pboxTemperature || 20"
  53. @updateReactionPlateNum="updateReactionPlateNum"
  54. @unloadConsumable="handleUnloadConsumable"
  55. />
  56. <!-- <SliderAreaEx v-model="sliderValue" v-model:totalValue="totalVal"/>
  57. <DragAreaEx v-model:hVal="hVal" v-model:hTotal="hTotal" v-model:vVal="vVal" v-model:vTotal="vTotal"/> -->
  58. </div>
  59. <div class="move-liquid-area">
  60. <MoveLiquidArea
  61. :moveLiquids="consumableStore.consumableData?.tips"
  62. :tempTipNum="
  63. consumableStore.consumableData?.tips.map((t) => t.tipNum)
  64. "
  65. :bufferBig="consumableStore.consumableData.larBottleGroup"
  66. :emergencyInfo="emergencyInfo"
  67. :wasteStatus="deviceStore.sensorState?.wasteBinFullFlag || false"
  68. @loadConsumables="handleIsLoad"
  69. @unloadConsumables="handleUnloadAll"
  70. @updateTipNum="updateTipNum"
  71. />
  72. </div>
  73. </div>
  74. <div class="main-bottom">
  75. <!--缓冲液区域小-->
  76. <div class="buffer-little-title">
  77. <div class="line"></div>
  78. <div class="content">缓冲液()</div>
  79. </div>
  80. <div class="ball-area">
  81. <MainComponent
  82. v-for="(item, idx) in consumableStore.consumableData?.littBottleGroup"
  83. :key="idx"
  84. class="ball-grid"
  85. :projectName="item.projShortName || ''"
  86. :currentCount="item.num"
  87. :totalBalls="25"
  88. :activatedBalls="item.num"
  89. :columns="5"
  90. gridWidth="240px"
  91. gridHeight="240px"
  92. :activeColor="item.color"
  93. :inUse="item.isInstall"
  94. />
  95. </div>
  96. </div>
  97. <InitWarn
  98. v-if="showUnloadConsumableWarnModal"
  99. :visible="showUnloadConsumableWarnModal"
  100. title="注意"
  101. message="请先卸载部分耗材后再扫描耗材"
  102. cancelText="返回"
  103. icon="/src/assets/update-pin-icon.svg"
  104. confirmText="确认"
  105. @close="confirmWarn"
  106. @confirm="confirmWarn"
  107. />
  108. </div>
  109. </template>
  110. <script setup lang="ts">
  111. import { MoveLiquidArea, SpttingPlates, MainComponent } from '../Components'
  112. import { ref, onMounted, onActivated, onBeforeUnmount } from 'vue'
  113. // import * as R from 'ramda'
  114. import {
  115. getInitState,
  116. scanConsumables,
  117. updateTipsNum,
  118. updateConsumables,
  119. unloadAllConsumable,
  120. unloadConsumable,
  121. ConsumableGroupNo,
  122. } from '../../../services/Index/index'
  123. import { useConsumablesStore, useEmergencyStore } from '../../../store'
  124. import { useDeviceStore } from '../../../store/index'
  125. import { eventBus } from '../../../eventBus'
  126. import { createWebSocket } from '../../../websocket/socket'
  127. import type {
  128. ConsumablesStateMessage,
  129. SensorStateMessage,
  130. } from '../../../websocket/socket'
  131. import InitWarn from '../Components/Consumables/Warn/InitWarn.vue'
  132. import { getServerInfo } from '../../../utils/getServerInfo'
  133. import { formatScanReports } from '../../../utils/errorHandler'
  134. import { ElMessage } from 'element-plus'
  135. const { wsUrl } = getServerInfo('/api/v1/app/ws/state')
  136. const socket = createWebSocket(wsUrl)
  137. const consumableStore = useConsumablesStore()
  138. const emergencyStore = useEmergencyStore()
  139. const deviceStore = useDeviceStore()
  140. // 扫描耗材中
  141. const isLoading = ref(false)
  142. // 是否显示扫描报告(列表弹窗)
  143. const showScanResults = ref(false)
  144. const showUnloadConsumableWarnModal = ref(false)
  145. interface ScanReport {
  146. channel: number
  147. code: string
  148. message: string
  149. isError: boolean
  150. projName: string | null
  151. lotId: string | null
  152. }
  153. const formattedReports = ref<ScanReport[]>([])
  154. //急诊区状态
  155. const emergencyInfo = ref(emergencyStore.$state.emergencyInfo || {})
  156. // 确认扫描结果
  157. const handleConfirmScan = () => {
  158. showScanResults.value = false
  159. if (formattedReports.value.some((report) => report.isError)) {
  160. ElMessage.warning('存在错误,请检查耗材')
  161. }
  162. }
  163. const confirmWarn = () => {
  164. showUnloadConsumableWarnModal.value = false
  165. }
  166. //使用websocket保证数据的实时性
  167. const startWebSocket = () => {
  168. socket.connect()
  169. }
  170. // 处理传感器状态消息
  171. const handleSensorState = (data: SensorStateMessage['data']) => {
  172. deviceStore.setSensorState(data)
  173. }
  174. //处理耗材状态
  175. const handleConsumablesState = (data: ConsumablesStateMessage['data']) => {
  176. if (!isDragging.value) {
  177. consumableStore.setConsumablesData(data)
  178. } else {
  179. console.log('正在拖动,不更新耗材')
  180. }
  181. }
  182. onMounted(() => {
  183. startWebSocket()
  184. socket.subscribe<SensorStateMessage>('SensorState', handleSensorState)
  185. socket.subscribe<ConsumablesStateMessage>(
  186. 'ConsumablesState',
  187. handleConsumablesState,
  188. )
  189. })
  190. onBeforeUnmount(() => {
  191. if (socket !== null) {
  192. socket.disconnect() // 断开连接
  193. }
  194. socket.unsubscribe<SensorStateMessage>('SensorState', handleSensorState)
  195. socket.unsubscribe<ConsumablesStateMessage>(
  196. 'ConsumablesState',
  197. handleConsumablesState,
  198. )
  199. })
  200. // 在组件激活时恢复状态
  201. onActivated(() => {
  202. emergencyInfo.value = emergencyStore.$state.emergencyInfo || {}
  203. // if (!isLoadedConsumables.value) {
  204. // console.log('组件被激活了')
  205. // }
  206. })
  207. const handleIsLoad = async () => {
  208. if (deviceStore.deviceState.workState === 'WORKING') {
  209. ElMessage.error('设备正在运行中,不可操作耗材')
  210. return
  211. }
  212. const res = await getInitState()
  213. if (res.ecode === 'SUC' && !res.data.deviceInited) {
  214. eventBus.emit('initDevice')
  215. return
  216. }
  217. try {
  218. if (
  219. consumableStore.consumableData.reactionPlateGroup.every(
  220. (p) => !!p.isInstall,
  221. )
  222. ) {
  223. showUnloadConsumableWarnModal.value = true
  224. return
  225. }
  226. isLoading.value = true
  227. const res = await scanConsumables()
  228. isLoading.value = false
  229. if (res.success) {
  230. // 格式化扫描结果
  231. formattedReports.value = formatScanReports(res.data.scanReports)
  232. console.log(
  233. '🚀 ~ handleIsLoad ~ formattedReports:',
  234. formattedReports.value,
  235. )
  236. // 显示扫描结果弹窗
  237. showScanResults.value = true
  238. } else {
  239. ElMessage.error(res.data.info)
  240. }
  241. } catch (error) {
  242. isLoading.value = false
  243. ElMessage.error('加载耗材失败')
  244. }
  245. }
  246. const handleUnloadAll = async () => {
  247. if (deviceStore.deviceState.workState === 'WORKING') {
  248. ElMessage.error('设备正在运行中,不可操作耗材')
  249. return
  250. }
  251. const res = await unloadAllConsumable()
  252. if (res.success) {
  253. // 接口未返回数据
  254. }
  255. }
  256. const handleUnloadConsumable = async (index: number) => {
  257. if (deviceStore.deviceState.workState === 'WORKING') {
  258. ElMessage.error('设备正在运行中,不可操作耗材')
  259. return
  260. }
  261. const res = await unloadConsumable({
  262. group: `CG${index + 1}` as ConsumableGroupNo,
  263. })
  264. if (!res.success && res.data.info) {
  265. ElMessage.error(res.data.info)
  266. }
  267. }
  268. const isDragging = ref(false)
  269. const updateReactionPlateNum = async ({
  270. index,
  271. plateNum,
  272. sync,
  273. }: {
  274. index: number
  275. plateNum: number
  276. sync: boolean
  277. }) => {
  278. if (plateNum < 0) {
  279. return
  280. }
  281. if (deviceStore.deviceState.workState !== 'WORKING') {
  282. consumableStore.updateReactionPlateNum(index, plateNum)
  283. if (sync) {
  284. console.log(`🚀 ~ updatePlateNum ~ order ${index + 1}, num ${plateNum}`)
  285. try {
  286. await updateConsumables({ group: `CG${index + 1}`, num: plateNum })
  287. } catch (error) {
  288. console.error('修改耗材数量失败:', error)
  289. }
  290. isDragging.value = false
  291. } else {
  292. isDragging.value = true
  293. }
  294. } else {
  295. ElMessage.error('设备正在工作,无法修改数值')
  296. }
  297. }
  298. const updateTipNum = async ({
  299. index,
  300. tipNum,
  301. sync,
  302. }: {
  303. index: number
  304. tipNum: number
  305. sync: boolean
  306. }) => {
  307. if (tipNum < 0) {
  308. return
  309. }
  310. if (deviceStore.deviceState.workState !== 'WORKING') {
  311. consumableStore.updateTipNum(index, tipNum)
  312. if (sync) {
  313. console.log(`🚀 ~ updateTipNum ~ order ${index + 1}, num ${tipNum}`)
  314. try {
  315. await updateTipsNum({ group: `TipG${index + 1}`, num: tipNum })
  316. } catch (error) {
  317. console.error('修改耗材数量失败:', error)
  318. }
  319. isDragging.value = false
  320. } else {
  321. isDragging.value = true
  322. }
  323. } else {
  324. ElMessage.error('设备正在工作,无法修改数值')
  325. }
  326. }
  327. </script>
  328. <style scoped lang="less">
  329. #regular-container {
  330. width: 100%;
  331. height: 100%;
  332. display: flex;
  333. flex-direction: column;
  334. .loading-overlay {
  335. position: fixed;
  336. top: 0;
  337. left: 0;
  338. width: 100vw;
  339. height: 100vh;
  340. background: rgba(0, 0, 0, 0.7);
  341. backdrop-filter: blur(4px);
  342. display: flex;
  343. justify-content: center;
  344. align-items: center;
  345. z-index: 9998;
  346. }
  347. .loading-content {
  348. display: flex;
  349. flex-direction: column;
  350. align-items: center;
  351. gap: 20px;
  352. }
  353. .loading-spinner {
  354. width: 60px;
  355. height: 60px;
  356. border: 4px solid #f3f3f3;
  357. border-top: 4px solid #4caf50;
  358. border-radius: 50%;
  359. animation: spin 1s linear infinite;
  360. }
  361. .loading-text {
  362. color: white;
  363. font-size: 24px;
  364. font-weight: 500;
  365. }
  366. .loading-progress {
  367. width: 300px;
  368. height: 6px;
  369. background: rgba(255, 255, 255, 0.2);
  370. border-radius: 3px;
  371. overflow: hidden;
  372. }
  373. .progress-bar {
  374. height: 100%;
  375. background: #4caf50;
  376. border-radius: 3px;
  377. transition: width 0.1s linear;
  378. }
  379. @keyframes spin {
  380. 0% {
  381. transform: rotate(0deg);
  382. }
  383. 100% {
  384. transform: rotate(360deg);
  385. }
  386. }
  387. .main-top {
  388. display: flex;
  389. justify-content: space-between;
  390. margin-top: 10px;
  391. height: 100%;
  392. .plate-area {
  393. width: 355px;
  394. }
  395. .move-liquid-area {
  396. width: calc(100% - 355px);
  397. }
  398. }
  399. .main-bottom {
  400. width: 100%;
  401. flex: 1;
  402. .buffer-little-title {
  403. display: flex;
  404. height: 40px;
  405. line-height: 40px;
  406. align-items: center;
  407. .line {
  408. width: 4px;
  409. height: 20px;
  410. background-color: #4caf50;
  411. margin: 0 10px 0;
  412. }
  413. .content {
  414. font-size: 26px;
  415. }
  416. }
  417. .ball-area {
  418. display: flex;
  419. flex-wrap: wrap;
  420. column-gap: 20px;
  421. row-gap: 12px;
  422. padding: 0 24px;
  423. .ball-grid {
  424. overflow: hidden;
  425. }
  426. }
  427. }
  428. .scan-results-overlay {
  429. position: fixed;
  430. top: 0;
  431. left: 0;
  432. width: 100vw;
  433. height: 100vh;
  434. background: rgba(0, 0, 0, 0.7);
  435. backdrop-filter: blur(4px);
  436. display: flex;
  437. justify-content: center;
  438. align-items: center;
  439. z-index: 10000;
  440. pointer-events: auto;
  441. }
  442. .scan-results-container {
  443. background: white;
  444. border: 2px solid #ff0000;
  445. border-radius: 8px;
  446. width: 800px;
  447. padding: 20px;
  448. position: relative;
  449. z-index: 10001;
  450. pointer-events: auto;
  451. .scan-results-header {
  452. text-align: center;
  453. margin-bottom: 20px;
  454. .title {
  455. color: #ff0000;
  456. font-size: 32px;
  457. font-weight: bold;
  458. }
  459. }
  460. .scan-results-content {
  461. .scan-results-table {
  462. width: 100%;
  463. border-collapse: collapse;
  464. margin: 20px 0;
  465. th,
  466. td {
  467. padding: 15px;
  468. text-align: left;
  469. font-size: 24px;
  470. border-bottom: 1px solid #eee;
  471. }
  472. th {
  473. color: #ff0000;
  474. font-weight: bold;
  475. background-color: #fff;
  476. }
  477. .error-row {
  478. background-color: rgba(255, 0, 0, 0.05);
  479. }
  480. .error-text {
  481. color: #ff0000;
  482. }
  483. .success-text {
  484. color: #4caf50;
  485. }
  486. }
  487. }
  488. .scan-results-footer {
  489. margin-top: 20px;
  490. text-align: center;
  491. .confirm-btn {
  492. background: #ff0000;
  493. color: white;
  494. border: none;
  495. border-radius: 25px;
  496. padding: 10px 40px;
  497. font-size: 24px;
  498. cursor: pointer;
  499. &:hover {
  500. background: darken(#ff0000, 10%);
  501. }
  502. }
  503. }
  504. }
  505. }
  506. </style>