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.

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