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.

696 lines
18 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
7 months ago
8 months ago
8 months ago
7 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
7 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
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="index-container">
  3. <el-header class="nav-bar">
  4. <div class="logo">
  5. <img src="@/assets/Index/logo.svg" alt="" width="150px" />
  6. </div>
  7. <div class="nav-tabs" @click="handleTabClick">
  8. <!-- 使用 router-link 进行路由跳转 -->
  9. <router-link
  10. to="/index/regular"
  11. class="nav-tab"
  12. :class="{ active: selectedTab === '常规' }"
  13. data-tab="常规"
  14. >常规</router-link
  15. >
  16. <router-link
  17. to="/index/history"
  18. class="nav-tab"
  19. :class="{ active: selectedTab === '历史' }"
  20. data-tab="历史"
  21. >历史</router-link
  22. >
  23. <router-link
  24. to="/index/setting"
  25. class="nav-tab"
  26. :class="{ active: selectedTab === '设置' }"
  27. data-tab="设置"
  28. >设置</router-link
  29. >
  30. </div>
  31. <div
  32. v-if="deviceStore.deviceState.workState === 'IDLE'"
  33. class="test-control"
  34. >
  35. <!-- 显示开始测试按钮 -->
  36. <el-button type="primary" class="start-test" @click="startTest"
  37. >开始测试</el-button
  38. >
  39. </div>
  40. <div v-else class="test-control">
  41. <!-- 显示暂停和停止按钮 -->
  42. <el-button
  43. type="warning"
  44. class="pause-test"
  45. @click="pauseTest"
  46. v-if="deviceStore.deviceState.workState !== 'PAUSE'"
  47. >暂停</el-button
  48. >
  49. <el-button
  50. type="success"
  51. class="continue-test"
  52. @click="continueTest"
  53. v-else
  54. >继续</el-button
  55. >
  56. <el-button type="danger" class="stop-test" @click="stopTest"
  57. >停止</el-button
  58. >
  59. </div>
  60. </el-header>
  61. <!-- 间隔线 -->
  62. <div class="interval">
  63. <div
  64. class="blue-line"
  65. :style="{ width: `${lineWidth}px`, left: `${lineLeft}px` }"
  66. ></div>
  67. </div>
  68. <!-- 主内容区域 -->
  69. <main class="main-content">
  70. <router-view v-slot="{ Component }">
  71. <keep-alive :exclude="['TubeUserId']">
  72. <component :is="Component" />
  73. </keep-alive>
  74. </router-view>
  75. </main>
  76. <!-- 底部操作信息 -->
  77. <el-footer class="footer-info">
  78. <el-dropdown placement="top-start">
  79. <div class="user-card">
  80. <img class="user-logo" src="@/assets/Index/user.svg" />
  81. <div class="user-name">操作人:{{ username || '未登录' }}</div>
  82. </div>
  83. <template #dropdown>
  84. <el-dropdown-menu>
  85. <el-dropdown-item>
  86. <img class="user-logo" src="@/assets/Index/user.svg" height="30"></img>
  87. <button class="logout" style="width:100px" @click="onLogout">注销</button>
  88. </el-dropdown-item>
  89. </el-dropdown-menu>
  90. </template>
  91. </el-dropdown>
  92. <div class="equipment-status">
  93. <div class="status-text">系统:{{ EventText }}</div>
  94. </div>
  95. <div class="time-card">
  96. <img class="time-logo" src="@/assets/Index/clock.svg" />
  97. <div class="time-text">
  98. <Time></Time>
  99. </div>
  100. </div>
  101. </el-footer>
  102. <InitWarn
  103. v-if="showModal"
  104. :visible="showModal"
  105. title="确认操作"
  106. message="确认试管架中的试管槽是否取下,避免自检/自动复位时对设备产生损坏!"
  107. icon="/src/assets/Warn.svg"
  108. cancelText="返回"
  109. confirmText="确认取下/开始复位"
  110. @close="showModal = false"
  111. @confirm="handleConfirm"
  112. />
  113. <!-- 通用加载弹窗组件 -->
  114. <LoadingModal
  115. v-if="showDeviceResettingModal"
  116. :visible="showDeviceResettingModal"
  117. title="正在自检/自动复位中"
  118. message="请不要有任何手动操作!"
  119. cancelText="返回"
  120. confirmText="确认取下/开始复位"
  121. disableButtons
  122. @close="showDeviceResettingModal = false"
  123. />
  124. <LoadingModal
  125. v-if="showDeviceWaitingModal"
  126. :visible="showDeviceWaitingModal"
  127. title="设备正在响应中"
  128. message="请不要有任何手动操作!"
  129. :showBtns="false"
  130. />
  131. <!-- 自动自检已完成 -->
  132. <InitWarn
  133. v-if="showAlreadyModal"
  134. :visible="showAlreadyModal"
  135. title="确认操作"
  136. message="自检/自动复位已完成!"
  137. icon="/src/assets/OK.svg"
  138. cancelText="返回"
  139. confirmText="确认"
  140. @close="showAlreadyModal = false"
  141. @confirm="handleAlreadyConfirm"
  142. />
  143. <!-- 自动自检失败 -->
  144. <InitWarn
  145. v-if="showFailModal"
  146. :visible="showFailModal"
  147. title="检测失败"
  148. :message="failMessage"
  149. icon="/src/assets/Warn.svg"
  150. cancelText="返回"
  151. confirmText="重试"
  152. @close="showFailModal = false"
  153. @confirm="startInit"
  154. />
  155. <InitWarn
  156. v-if="idCardInserted"
  157. :visible="idCardInserted"
  158. title="检测到id卡插入"
  159. message="是否保存id卡信息"
  160. cancelText="返回"
  161. icon="/src/assets/update-pin-icon.svg"
  162. confirmText="确认保存"
  163. @close="idCardInserted = false"
  164. @confirm="saveIdInfo"
  165. />
  166. <InitWarn
  167. v-if="showErrorModal"
  168. :visible="showErrorModal"
  169. title="错误提示"
  170. :message="ErrorMessage"
  171. icon="/src/assets/Warn.svg"
  172. cancelText="返回"
  173. confirmText="确认"
  174. @close="confirmError"
  175. @confirm="confirmError"
  176. />
  177. <InitWarn
  178. v-if="showWarnModal"
  179. :visible="showWarnModal"
  180. title="注意"
  181. :message="WarnMessage"
  182. cancelText="返回"
  183. icon="/src/assets/update-pin-icon.svg"
  184. confirmText="确认"
  185. @close="confirmWarn"
  186. @confirm="confirmWarn"
  187. />
  188. </div>
  189. </template>
  190. <script setup lang="ts">
  191. import { useRouter } from 'vue-router'
  192. import { ref, onMounted, onBeforeUnmount } from 'vue'
  193. import { ElMessage } from 'element-plus'
  194. import { Time, InitWarn, LoadingModal } from './Components/Consumables'
  195. import {
  196. startWork,
  197. pauseWork,
  198. continueWork,
  199. stopWork,
  200. getDeviceWorkState,
  201. getInitState,
  202. initDevice,
  203. saveMountedCardInfo,
  204. openBuzzer,
  205. closeBuzzer,
  206. } from '../../services/index'
  207. import { User } from '../../types/Index'
  208. import { useConsumablesStore, useDeviceStore } from '../../store'
  209. import { createWebSocket } from '../../websocket/socket'
  210. import type { AppEventMessage } from '../../websocket/socket'
  211. import { getServerInfo } from '../../utils/getServerInfo'
  212. import { eventBus } from '../../eventBus'
  213. import { logout } from '@/services/Login/login'
  214. const selectedTab = ref(sessionStorage.getItem('selectedTab') || '常规')
  215. const lineWidth = ref(0)
  216. const lineLeft = ref(0)
  217. const showModal = ref(false)
  218. const showDeviceResettingModal = ref(false)
  219. const showDeviceWaitingModal = ref(false)
  220. const showAlreadyModal = ref(false)
  221. const showFailModal = ref(false)
  222. // const checkData = ref<CheckItem[]>([]);
  223. // const failItems = ref<CheckItem[]>([]);
  224. const consumableStore = useConsumablesStore()
  225. const deviceStore = useDeviceStore()
  226. // 新增的变量
  227. const user = ref<User>(
  228. JSON.parse(sessionStorage.getItem('token') || '{}') as unknown as User,
  229. )
  230. const username = ref<string>(user.value.account)
  231. const failMessage = ref('') // 存储动态生成的错误信息
  232. const idCardInserted = ref(false) // id卡插入状态
  233. //事件状态
  234. const EventText = ref<string>('闲置...')
  235. const showWarnModal = ref(false)
  236. const ErrorMessage = ref<string>('')
  237. const showErrorModal = ref(false)
  238. const WarnMessage = ref<string>('')
  239. // WebSocket 实例
  240. const { wsUrl } = getServerInfo('/api/v1/app/ws/event')
  241. const ws = createWebSocket(wsUrl)
  242. // 处理应用事件消息
  243. const handleAppEvent = (data: AppEventMessage['data']) => {
  244. console.log('🚀 ~ handleAppEvent ~ data:', data)
  245. if (data.typeName === 'AppPromptEvents' && data.prompt) {
  246. data.prompt.forEach(async (item) => {
  247. if (item.type === 'Error') {
  248. showErrorModal.value = true
  249. ErrorMessage.value = item.info
  250. await openBuzzer()
  251. } else if (item.type === 'Warn') {
  252. showWarnModal.value = true
  253. WarnMessage.value = item.info
  254. }
  255. })
  256. } else if (data.typeName === 'AppIDCardMountEvent') {
  257. consumableStore.setIdCardInserted(true)
  258. idCardInserted.value = true
  259. EventText.value = 'id卡已插入'
  260. } else if (data.typeName === 'AppIDCardUnmountEvent') {
  261. consumableStore.setIdCardInserted(false)
  262. idCardInserted.value = false
  263. EventText.value = 'id卡已拔出'
  264. } else if (data.typeName === 'AppTubeholderSettingUpdateEvent') {
  265. EventText.value = '试管架配置更新'
  266. eventBus.emit('AppTubeSettingUpdateEvent')
  267. } else if (data.typeName === 'DoA8kStepActionEvent') {
  268. EventText.value = data.actionStepName!
  269. } else {
  270. EventText.value = '闲置...'
  271. }
  272. }
  273. //注销用户
  274. const router = useRouter()
  275. const onLogout = () => {
  276. logout().then(() => {
  277. router.push({
  278. path: '/login',
  279. })
  280. sessionStorage.setItem('token', '')
  281. })
  282. }
  283. //确认错误事件
  284. const confirmError = async () => {
  285. showErrorModal.value = false
  286. //关闭蜂鸣器
  287. await closeBuzzer()
  288. EventText.value = '闲置...'
  289. }
  290. //确认警告事件
  291. const confirmWarn = async () => {
  292. showWarnModal.value = false
  293. }
  294. //保存id卡信息
  295. const saveIdInfo = async () => {
  296. const res = await saveMountedCardInfo()
  297. if (res.success) {
  298. console.log('保存id卡信息成功')
  299. idCardInserted.value = false
  300. }
  301. }
  302. const showInitDeviceAlert = () => {
  303. showModal.value = true
  304. }
  305. onMounted(() => {
  306. eventBus.on('initDevice', showInitDeviceAlert)
  307. ws.connect()
  308. ws.subscribe<AppEventMessage>('AppEvent', handleAppEvent)
  309. })
  310. onBeforeUnmount(() => {
  311. eventBus.off('initDevice', showInitDeviceAlert)
  312. ws.unsubscribe<AppEventMessage>('AppEvent', handleAppEvent)
  313. ws.disconnect()
  314. })
  315. // 动态生成错误信息
  316. // const generateErrorMessages = (data: CheckItem[]): string[] => {
  317. // return data
  318. // .filter(item => !item.pass)
  319. // .map(item => `错误:${item.typechinfo} 检测未通过,请检查设备状态。`);
  320. // };
  321. const untilDeviceReady = async () => {
  322. showDeviceWaitingModal.value = true
  323. const res = await getDeviceWorkState()
  324. if (res.ecode === 'SUC') {
  325. if (res.data.pending) {
  326. setTimeout(async () => await untilDeviceReady(), 250)
  327. } else {
  328. showDeviceWaitingModal.value = false
  329. }
  330. } else {
  331. showDeviceWaitingModal.value = false
  332. }
  333. }
  334. // 开始测试
  335. const startTest = async () => {
  336. const res = await getInitState()
  337. if (res.ecode === 'SUC' && !res.data.deviceInited) {
  338. eventBus.emit('initDevice')
  339. return
  340. }
  341. try {
  342. showDeviceWaitingModal.value = true
  343. const res = await startWork()
  344. if (res.success) {
  345. await untilDeviceReady()
  346. } else {
  347. showDeviceWaitingModal.value = false
  348. ElMessage({
  349. message: res.data.info,
  350. type: 'error',
  351. duration: 2000,
  352. })
  353. }
  354. } catch (error) {
  355. console.error('开始测试失败:', error)
  356. }
  357. }
  358. // 暂停测试
  359. const pauseTest = async () => {
  360. showDeviceWaitingModal.value = true
  361. const res = await pauseWork()
  362. if (res.success) {
  363. await untilDeviceReady()
  364. } else {
  365. showDeviceWaitingModal.value = false
  366. ElMessage({
  367. message: res.data.info,
  368. type: 'error',
  369. duration: 2000,
  370. })
  371. }
  372. }
  373. // 停止测试时清除标记
  374. const stopTest = async () => {
  375. showDeviceWaitingModal.value = true
  376. const res = await stopWork()
  377. if (res.success) {
  378. await untilDeviceReady()
  379. } else {
  380. showDeviceWaitingModal.value = false
  381. ElMessage({
  382. message: res.data.info,
  383. type: 'error',
  384. duration: 2000,
  385. })
  386. }
  387. }
  388. //继续测试
  389. const continueTest = async () => {
  390. showDeviceWaitingModal.value = true
  391. const res = await continueWork()
  392. if (res.success) {
  393. await untilDeviceReady()
  394. } else {
  395. showDeviceWaitingModal.value = false
  396. ElMessage({
  397. message: res.data.info,
  398. type: 'error',
  399. duration: 2000,
  400. })
  401. }
  402. }
  403. const handleConfirm = async () => {
  404. showModal.value = false // 关闭初始弹窗
  405. await startInit()
  406. }
  407. const startInit = async () => {
  408. await initDevice()
  409. await pollingInitState()
  410. }
  411. const pollingInitState = async () => {
  412. showDeviceResettingModal.value = true // 显示 LoadingModal
  413. const res = await getInitState()
  414. if (res.ecode === 'SUC') {
  415. if (res.data.isBusy) {
  416. setTimeout(async () => await pollingInitState(), 500)
  417. } else {
  418. showDeviceResettingModal.value = false
  419. if (res.data.passed) {
  420. console.log('初始化成功')
  421. sessionStorage.setItem('deviceResetFinished', 'true')
  422. showAlreadyModal.value = true
  423. } else {
  424. const infos = res.data.promopt.detailInfos
  425. failMessage.value =
  426. infos && infos.length > 0
  427. ? infos.map((d: any) => d.name).join('\n')
  428. : res.data.promopt.info
  429. showFailModal.value = true // 显示失败弹窗
  430. }
  431. }
  432. }
  433. }
  434. // const checkIfResetCompleted = async () => {
  435. // if (showFailModal.value) {
  436. // showFailModal.value = false;
  437. // }
  438. // showLoadingModal.value = true; // 显示 LoadingModal
  439. // // 模拟 2 秒延时
  440. // await new Promise(resolve => setTimeout(resolve, 2000));
  441. // //通过获取初始化状态来判断是否初始化成功
  442. // const initState = await getInitState();
  443. // if (initState.ecode === "SUC") {
  444. // //检测初始化是否成功
  445. // if (initState.data.passed) {
  446. // console.log("初始化成功")
  447. // sessionStorage.setItem('deviceResetFinished', "true");
  448. // showLoadingModal.value = false;
  449. // showAlreadyModal.value = true;
  450. // } else {
  451. // console.log("初始化失败")
  452. // await getCheckData(); // 获取检查数据找到错误项
  453. // const failedItems = checkData.value.filter(item => !item.pass);
  454. // if (failedItems.length > 0) {
  455. // const errorMessages = generateErrorMessages(failedItems);
  456. // console.log('生成的错误信息:', errorMessages);
  457. // failItems.value = failedItems; // 更新失败的检查项
  458. // failMessage.value = errorMessages.join('\n'); // 更新错误信息
  459. // showLoadingModal.value = false; // 隐藏 LoadingModal
  460. // showFailModal.value = true; // 显示失败弹窗
  461. // } else {
  462. // console.log("初始化失败,但是没有失败项")
  463. // showLoadingModal.value = false; // 隐藏 LoadingModal
  464. // showAlreadyModal.value = true; // 显示已完成弹窗
  465. // sessionStorage.setItem('deviceResetFinished', "true");
  466. // }
  467. // }
  468. // }
  469. // };
  470. // const isTestTubeSlotReady = ref(false); // 试管槽状态标记
  471. // 在获取检测数据后,判断试管槽状态
  472. // const checkTestTubeSlotStatus = (data: CheckItem[]) => {
  473. // const slotCheck = data.find(item => item.type === 'CHECK_TEST_TUBE_SLOT_READY');
  474. // isTestTubeSlotReady.value = slotCheck?.pass ?? false;
  475. // console.log('试管槽状态:', isTestTubeSlotReady.value ? '准备就绪' : '未准备好');
  476. // };
  477. const handleAlreadyConfirm = () => {
  478. console.log('用户确认操作')
  479. showAlreadyModal.value = false
  480. showFailModal.value = false
  481. }
  482. onMounted(() => {
  483. checkInit()
  484. })
  485. // 开始检测
  486. const checkInit = () => {
  487. const hasExecutedReset = sessionStorage.getItem('deviceResetFinished')
  488. if (!hasExecutedReset || hasExecutedReset === 'false') {
  489. showModal.value = true
  490. }
  491. }
  492. // 更新蓝线的宽度和左偏移量
  493. const updateLinePosition = (element: any) => {
  494. lineWidth.value = element.offsetWidth // 当前选中标签的宽度
  495. lineLeft.value = element.offsetLeft // 当前选中标签的左偏移量
  496. }
  497. // 处理标签点击事件
  498. const handleTabClick = (event: any) => {
  499. const tabElement = event.target.closest('.nav-tab')
  500. if (tabElement) {
  501. const tab = tabElement.dataset.tab
  502. selectedTab.value = tab
  503. sessionStorage.setItem('selectedTab', tab) // 将选中标签存储到 localStorage
  504. updateLinePosition(tabElement) // 更新蓝线的位置和宽度
  505. }
  506. }
  507. // 页面加载时默认选中"常规"标签,并设置蓝线的位置
  508. onMounted(() => {
  509. const defaultTab = document.querySelector(
  510. `.nav-tab[data-tab="${selectedTab.value}"]`,
  511. )
  512. if (defaultTab) {
  513. updateLinePosition(defaultTab)
  514. }
  515. })
  516. </script>
  517. <style scoped lang="less">
  518. #index-container {
  519. margin: 0;
  520. padding: 0;
  521. width: 100vw;
  522. height: 100vh;
  523. background-color: #fff;
  524. position: relative;
  525. box-sizing: border-box;
  526. display: flex;
  527. flex-direction: column;
  528. .fade-slide-enter-active,
  529. .fade-slide-leave-active {
  530. transition:
  531. opacity 0.5s,
  532. transform 0.5s;
  533. /* 控制透明度和滑动的过渡时间 */
  534. }
  535. .fade-slide-enter-from {
  536. opacity: 0;
  537. transform: translateX(20px);
  538. /* 从右侧滑入 */
  539. }
  540. .fade-slide-leave-to {
  541. opacity: 0;
  542. transform: translateX(-20px);
  543. /* 向左滑出 */
  544. }
  545. .nav-bar {
  546. width: 100%;
  547. height: 60px;
  548. margin-bottom: 2px;
  549. display: flex;
  550. justify-content: space-between;
  551. align-items: center;
  552. .logo {
  553. width: 100px;
  554. height: 75px;
  555. padding-left: 10px;
  556. }
  557. .nav-tabs {
  558. width: 60%;
  559. font-size: 24px;
  560. display: flex;
  561. justify-content: space-around;
  562. font-size: 40px;
  563. .nav-tab {
  564. cursor: pointer;
  565. padding-bottom: 10px;
  566. color: black;
  567. transition: color 0.3s;
  568. position: relative;
  569. }
  570. .nav-tab.active {
  571. color: #478ffe;
  572. }
  573. }
  574. .test-control {
  575. display: flex;
  576. justify-content: space-evenly;
  577. padding-right: 10px;
  578. .start-test {
  579. width: 200px;
  580. height: 40px;
  581. background-color: #73bc54;
  582. font-size: 20px;
  583. }
  584. .pause-test,
  585. .stop-test,
  586. .continue-test {
  587. width: 100px;
  588. height: 40px;
  589. font-size: 20px;
  590. }
  591. }
  592. }
  593. .interval {
  594. width: 100%;
  595. height: 5px;
  596. background-color: #f5f5f5;
  597. position: relative;
  598. .blue-line {
  599. position: absolute;
  600. bottom: 0;
  601. height: 5px;
  602. background-color: #478ffe;
  603. transition: all 0.3s ease;
  604. /* 添加平滑过渡效果 */
  605. }
  606. }
  607. .footer-info {
  608. width: 100%;
  609. height: 40px;
  610. display: flex;
  611. justify-content: space-between;
  612. align-items: center;
  613. position: relative;
  614. bottom: 0;
  615. margin-top: auto;
  616. .user-logo,
  617. .time-logo {
  618. width: 40px;
  619. height: 40px;
  620. }
  621. .user-card,
  622. .time-card {
  623. display: flex;
  624. align-items: center;
  625. font-size: 26px;
  626. }
  627. .equipment-status {
  628. background-color: #f5f5f5;
  629. width: 400px;
  630. height: 40px;
  631. line-height: 40px;
  632. text-align: center;
  633. border-radius: 10px;
  634. .status-text {
  635. font-size: 26px;
  636. }
  637. }
  638. .logout {
  639. width: 70px;
  640. text-align: center;
  641. font-size: 20px;
  642. margin: auto;
  643. }
  644. }
  645. .main-content {
  646. flex: 1;
  647. min-height: 0; // 添加:防止溢出
  648. }
  649. }
  650. </style>