石墨仪设备 前端仓库
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.

684 lines
16 KiB

6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
  1. <template>
  2. <div class="graphite_home component-page overflow-auto" id="heatArea">
  3. <div class="heat_area">
  4. <div v-for="(item, index) in heatList" :key="item.id">
  5. <HeatPosition
  6. :heatInfo="{ ...item, index }"
  7. :tubeIndex="index + 1"
  8. @onSelectedTray="onSelectedTray"
  9. @onSetHeatAreaTemp = "onSetHeatAreaTemp"
  10. ></HeatPosition>
  11. </div>
  12. <!--执行中状态的遮罩层-->
  13. </div>
  14. <!--拍照区-->
  15. <div class="picture_area">
  16. <!--加液区和拍照区可切换-->
  17. <div style="display: flex; justify-content: center; align-items: center">
  18. <div style="font-size: 1rem; margin-left: 0.3rem">加液区</div>
  19. <van-switch
  20. v-model="switchModule"
  21. size="1rem"
  22. active-color="#ee0a24"
  23. inactive-color="green"
  24. />
  25. <div style="font-size: 1rem; margin-left: 0.3rem">拍照区</div>
  26. </div>
  27. <TakePickture v-if="switchModule"></TakePickture>
  28. <div v-else class="home_tube">
  29. <div
  30. class="inner-circle"
  31. v-for="(tubeItem, index) in tubeList"
  32. :key="index"
  33. :style="{ background: tubeItem.color }"
  34. @click="onChooseTube(tubeItem, index)"
  35. ></div>
  36. </div>
  37. <!--操作区-->
  38. <div class="graphite_btn_container">
  39. <van-button size="large" class="btn_size op_open_door" @click="onOPen"
  40. >开门</van-button
  41. >
  42. <van-button size="large" class="btn_size op_start_task" @click="taskNameVisible = true"
  43. >开始实验</van-button
  44. >
  45. <van-button size="large" class="btn_size op_stop_task"
  46. >结束实验</van-button
  47. >
  48. <van-button
  49. size="large"
  50. class="btn_size op_select_craft"
  51. @click="onChooseCaft"
  52. >选择工艺</van-button
  53. >
  54. <van-button size="large" class="btn_size op_exec_craft"
  55. >执行工艺</van-button
  56. >
  57. <van-button
  58. size="large"
  59. class="btn_size op_add_liquid"
  60. @click="onAddLiquid"
  61. >添加溶液</van-button
  62. >
  63. <van-button size="large" class="btn_size op_shake_up">摇匀</van-button>
  64. <van-button
  65. size="large"
  66. class="btn_size op_move_heat"
  67. @click="onMoveToHeat"
  68. >移至加热</van-button
  69. >
  70. <van-button size="large" class="btn_size op_move_act" @click="onMoveToOperationArea"
  71. >移至加液</van-button
  72. >
  73. <van-button size="large" class="btn_size op_move_exception" @click="onMoveToSpecial"
  74. >移至特殊</van-button
  75. >
  76. <van-button size="large" class="btn_size op_start_heat" @click="onStartHeat"
  77. >开始加热</van-button
  78. >
  79. <van-button size="large" class="btn_size op_up_tray"
  80. >抬起托盘</van-button
  81. >
  82. </div>
  83. </div>
  84. <van-overlay :show="liquidVisible" style="z-index: 9999">
  85. <div class="liquid">
  86. <div class="addLiquid">
  87. <AddLiquid
  88. @cancel="liquidVisible = false"
  89. @onAddSolution="onAddSolution"
  90. ></AddLiquid>
  91. </div>
  92. </div>
  93. </van-overlay>
  94. <!--选择工艺-->
  95. <OverlayModal :visible="craftVisible">
  96. <CraftList @changeVisible="changeVisible" @onSelectedCraft="onSelectedCraft"></CraftList>
  97. </OverlayModal>
  98. <!--实验名称-->
  99. <OverlayModal :visible="taskNameVisible">
  100. <div class="task_name">
  101. <div class="task_title">
  102. 开始新实验
  103. </div>
  104. <div class="task_name_content">
  105. 实验名称<input v-model="taskName" class="task_input">
  106. </div>
  107. <br/>
  108. <footer class="task_button">
  109. <button class="btn-dark px-2 py-1 min-w-20" @click="onSave">保存</button>
  110. <button class="px-4 py-1 min-w-20" @click="onCancel">取消</button>
  111. </footer>
  112. </div>
  113. </OverlayModal>
  114. </div>
  115. </template>
  116. <script lang="ts" setup>
  117. import { ref, reactive, onMounted, onUnmounted, watch } from "vue";
  118. //@ts-ignore
  119. import { ElMessage } from "element-plus";
  120. import { createWebSocket, sharedWsUrl } from "@/services/socket";
  121. import { HeatPosition, TakePickture, AddLiquid } from "./components";
  122. import OverlayModal from "@/components/OverlayModal.vue";
  123. import CraftList from "@/views/graphite/components/CraftList.vue";
  124. import { graphiteMock } from "./components/mock";
  125. import { useStatusStore } from "@/stores/status";
  126. import { saveTaskName } from '@/services/task/task';
  127. import {
  128. CmdDescMap,
  129. taskCmd,
  130. type OperationCmd,
  131. } from "@/services/globalCmd/globalCmd";
  132. import { getTxnRecord } from "@/services/txn";
  133. import { useSettingStore } from "@/stores/setting";
  134. const settingStore = useSettingStore();
  135. //设备的全局状态
  136. const statusStore = useStatusStore();
  137. const heatList: any = ref([]);
  138. const craftVisible = ref(false);
  139. const switchModule = ref(false);
  140. let tubeList = reactive<any>([]);
  141. const selectedColor = "#4F85FB";
  142. const emptyColor = "#FFFFFF";
  143. const defaultColor = "#189952";
  144. const taskName = ref('')
  145. onMounted(() => {
  146. //6个加热区数据
  147. heatList.value = settingStore.heatAreaConfig;
  148. //设备16个试管的基础数据
  149. tubeBaseConfig();
  150. //连接socket
  151. const wsClient = createWebSocket(sharedWsUrl);
  152. const subscription = wsClient.dataOb.subscribe((data) => {
  153. if (data.type === "cmd") {
  154. const cmdInfo = getTxnRecord(data.data.commandId, "task");
  155. if (cmdInfo) {
  156. const cmdName = CmdDescMap[cmdInfo.command];
  157. const result = data.data.success
  158. ? "执行完毕"
  159. : `执行失败 ${data.data.message}`;
  160. ElMessage({
  161. message: `${cmdName} ${result}`,
  162. type: data.data.success ? "success" : "error",
  163. });
  164. }
  165. }
  166. });
  167. wsClient.connect();
  168. onUnmounted(() => {
  169. subscription.unsubscribe();
  170. });
  171. });
  172. //选择的托盘
  173. const selectedTrayList = ref<any>([]);
  174. const selectedTrayObj: any = {};
  175. const onSelectedTray = (data: any) => {
  176. console.log("data==操作加热区:=", data);
  177. if (!data.isSelect) {
  178. delete selectedTrayObj[data.id];
  179. } else {
  180. selectedTrayObj[data.id] = data;
  181. }
  182. selectedTrayList.value = Object.values(selectedTrayObj);
  183. };
  184. //设置加热区温度
  185. const onSetHeatAreaTemp = (dataInfo:any) => {
  186. selectedTrayObj[dataInfo.id] = dataInfo;
  187. selectedTrayList.value = Object.values(selectedTrayObj);
  188. }
  189. const tubeBaseConfig = () => {
  190. //默认为16个
  191. for (let i = 0; i < 16; i++) {
  192. tubeList.push({
  193. id: i + 1,
  194. color: "rgb(212, 212, 212)",
  195. });
  196. }
  197. };
  198. //添加溶液
  199. const liquidVisible = ref(false);
  200. const onAddLiquid = () => {
  201. //检查加液区是否有试管。但是现在没有这个状态,暂时不考虑。
  202. liquidVisible.value = true;
  203. };
  204. //选择的工艺
  205. const onSelectedCraft = (craftData:any) => {
  206. console.log('craftData===', craftData)
  207. }
  208. const onAddSolution = (data: any) => {
  209. let ids = data.map((item: any) => item.id);
  210. tubeList.forEach((item: any) => {
  211. if (ids.includes(item.id)) {
  212. item.default = defaultColor;
  213. item.color = defaultColor;
  214. item.isSelected = true;
  215. }
  216. });
  217. };
  218. const changeVisible = () => {
  219. craftVisible.value = false;
  220. };
  221. //选择工艺
  222. const onChooseCaft = () => {
  223. craftVisible.value = true;
  224. };
  225. const onChooseTube = (tubeItem: any, index: any) => {
  226. if (!tubeItem.id) return;
  227. //@ts-ignore
  228. let list = [...tubeList];
  229. for (let i = 0; i < list.length; i++) {
  230. let item = list[i];
  231. if (index == i) {
  232. item.color = selectedColor;
  233. item.isSelected = true;
  234. } else {
  235. item.color = item.default ? item.default : emptyColor;
  236. item.isSelected = false;
  237. }
  238. }
  239. tubeList = [...list];
  240. };
  241. //移至加热
  242. const onMoveToHeat = () => {
  243. //1、是否选择了加热区
  244. if (!selectedTrayList.value.length) {
  245. ElMessage.error("请选择目标加热区");
  246. return;
  247. }
  248. //2、只能选择一个加热区
  249. if (selectedTrayList.value.length != 1) {
  250. ElMessage.error("只能选择一个加热区");
  251. return;
  252. }
  253. let selectedDataItem = selectedTrayList.value[0];
  254. //2、判断选择的加热区是否已经有了试管架
  255. let heatAearStatus: any = statusStore.status?.trayStatus || graphiteMock.heatAreaStatus;
  256. if (!heatAearStatus[selectedDataItem.index]) {
  257. ElMessage.error("选择的加热区已有试管架,重新选择加热区");
  258. return;
  259. }
  260. //调用移至加热接口
  261. const params = {
  262. areaId: selectedDataItem.id,
  263. };
  264. const command: OperationCmd = "moveToHeatArea";
  265. taskCmd({ command, params }).then((res) => {
  266. if (res.success) {
  267. ElMessage.success("指令已发送,请稍等");
  268. } else {
  269. ElMessage.error(res.msg);
  270. }
  271. });
  272. //指令完成成更新UI
  273. let list = [...heatList.value];
  274. list.forEach((item: any) => {
  275. if (item.id == selectedDataItem.id) {
  276. item.tubeList = JSON.parse(JSON.stringify(tubeList));
  277. selectedTrayObj[item.id] = item;
  278. selectedTrayList.value = Object.values(selectedTrayObj);
  279. }
  280. });
  281. tubeList.forEach((item:any) => {
  282. item.color = '';
  283. item.default = ''
  284. });
  285. heatList.value = [...list];
  286. };
  287. //移至加液区(操作区)
  288. const onMoveToOperationArea = () => {
  289. //1、判断加液区是否有试管架(暂时获取不到这个状态)
  290. //1、是否选择了试管架/加热区
  291. if (!selectedTrayList.value.length) {
  292. ElMessage.error("请选择试管架");
  293. return;
  294. }
  295. //2、只能选择一个试管架/加热区
  296. if (selectedTrayList.value.length != 1) {
  297. ElMessage.error("只能选择一个试管架");
  298. return;
  299. }
  300. let selectedDataItem = selectedTrayList.value[0];
  301. selectedDataItem.isSelect = false
  302. //3、选择的加热区有没有试管架
  303. if(!selectedDataItem.tubeList || !selectedDataItem.tubeList.length){
  304. ElMessage.error("选择的加热区没有试管架");
  305. return;
  306. }
  307. //4、发送移至加液区指令
  308. const params = {
  309. areaId: selectedDataItem.id
  310. }
  311. onSendCmd('moveToActionArea', params)
  312. //更新UI
  313. heatList.value.forEach((item:any) => {
  314. if(item.id == selectedDataItem.id){
  315. tubeList = [...item.tubeList]
  316. item.tubeList = null;
  317. }
  318. })
  319. onSelectedTray(selectedDataItem)
  320. }
  321. //移至特殊区域
  322. const onMoveToSpecial = () => {
  323. //检查是否设置了异常区域
  324. const systemSetting = settingStore.systemSetting
  325. let specialArea:any = {}
  326. if(systemSetting && systemSetting.length){
  327. systemSetting.forEach(item => {
  328. if(item.code == "sys_setting_abnormal_area"){
  329. specialArea = item;
  330. }
  331. })
  332. if(!specialArea.id){
  333. ElMessage.error('未设置异常区域,请在系统配置中设置')
  334. return;
  335. }
  336. //是否选择了加热区的试管架
  337. if (!selectedTrayList.value.length) {
  338. ElMessage.error("请选择试管架");
  339. return;
  340. }
  341. //2、只能选择一个试管架/加热区
  342. if (selectedTrayList.value.length != 1) {
  343. ElMessage.error("只能选择一个试管架");
  344. return;
  345. }
  346. let selectedDataItem = selectedTrayList.value[0];
  347. selectedDataItem.isSelect = false
  348. onSelectedTray(selectedDataItem)
  349. const params = {
  350. areaId:selectedDataItem.id
  351. }
  352. onSendCmd('moveToActionArea', params)
  353. }
  354. }
  355. //开始实验
  356. const taskNameVisible = ref(false)
  357. const onSave = ()=> {
  358. const params = {
  359. name: taskName.value
  360. }
  361. saveTaskName(params).then(res => {
  362. if(res.success){
  363. console.log('保存实验名称===', res)
  364. ElMessage.success('保存成功')
  365. onCancel()
  366. }else{
  367. ElMessage.error(res.msg)
  368. }
  369. })
  370. }
  371. const onCancel = ()=> {
  372. taskNameVisible.value = false;
  373. }
  374. //开门
  375. const onOPen = () => {
  376. const params = {};
  377. const command: OperationCmd = "openDoor";
  378. taskCmd({ command, params }).then((res) => {
  379. if (res.success) {
  380. ElMessage.success("指令已发送,请稍等");
  381. } else {
  382. ElMessage.error(res.msg);
  383. }
  384. });
  385. };
  386. //开始加热
  387. const onStartHeat = () => {
  388. //选择的加热区
  389. if (!selectedTrayList.value.length) {
  390. ElMessage.error("请选择目标加热区");
  391. return;
  392. }
  393. //判断选中的加热区是否有试管架
  394. let existTubeRack = true;
  395. //加热区是否设置了加热温度
  396. let hasSetTemp = true;
  397. selectedTrayList.value.forEach((item:any) => {
  398. let tubeList = item.tubeList;
  399. if(!tubeList || !tubeList.length){
  400. existTubeRack = false;
  401. }
  402. if(!item.temperature){
  403. hasSetTemp = false;
  404. }
  405. })
  406. if(!existTubeRack){
  407. ElMessage.error("选择的加热区未放置试管架,请重新选择")
  408. return;
  409. }
  410. if(!hasSetTemp){
  411. ElMessage.error("选择的加热区未设置温度,请设置温度")
  412. return;
  413. }
  414. //后台批量发送指令
  415. const cmdList:any = []
  416. selectedTrayList.value.forEach((heatArea:any) => {
  417. cmdList.push({
  418. command:'startHeat',
  419. params:{
  420. areaId : heatArea.id
  421. }
  422. })
  423. })
  424. onSendCmd("startHeat", cmdList)
  425. }
  426. const onSendCmd = (command:OperationCmd,params:any)=> {
  427. //发送加热指令
  428. taskCmd({ command, params }).then((res) => {
  429. if (res.success) {
  430. ElMessage.success("指令已发送,请稍等");
  431. } else {
  432. ElMessage.error(res.msg);
  433. }
  434. });
  435. }
  436. </script>
  437. <style lang="scss" scoped>
  438. @use "@/assets/style/mixin.scss" as *;
  439. .graphite_home {
  440. background: #f6f6f6;
  441. display: flex;
  442. gap: 1rem;
  443. // @media (min-width: $md) {
  444. flex-direction: column;
  445. align-items: stretch;
  446. // }
  447. .picture_area {
  448. display: flex;
  449. flex-direction: column-reverse;
  450. }
  451. @media (min-width: $lg) {
  452. flex-direction: row;
  453. align-items: start;
  454. .picture_area {
  455. display: flex;
  456. flex-direction: column;
  457. height: 47.5rem;
  458. }
  459. }
  460. }
  461. .heat_area {
  462. margin: 5px 0;
  463. background: #ffffff;
  464. border-radius: 20px;
  465. column-gap: 8px;
  466. row-gap: 10px;
  467. padding: 1.5rem 0.5rem;
  468. min-width: 600px;
  469. flex: 1 1 auto;
  470. display: grid;
  471. grid-template-columns: repeat(3, 1fr);
  472. > * {
  473. justify-self: center;
  474. align-self: center;
  475. }
  476. @media (min-width: $md) {
  477. column-gap: 12px;
  478. row-gap: 20px;
  479. height: 47.5rem;
  480. }
  481. @media (min-width: $xl) {
  482. padding: 4.5rem 1rem;
  483. }
  484. .craft_executing_modal {
  485. position: absolute;
  486. width: 10.5rem;
  487. height: 18rem;
  488. background: rgb(230, 230, 230);
  489. opacity: 0.5;
  490. z-index: 9999;
  491. }
  492. }
  493. .picture_area {
  494. display: flex;
  495. flex-direction: column;
  496. margin: 5px;
  497. // margin-left: 1.25rem;
  498. background: #ffffff;
  499. border-radius: 20px;
  500. padding: 0 1.5rem;
  501. @media (min-width: $lg) {
  502. flex: 1 1 180px;
  503. }
  504. @media (min-width: $xl) {
  505. flex: 0 0 auto;
  506. width: 27rem;
  507. }
  508. .graphite_btn_container {
  509. margin: 2rem 0;
  510. gap: 0.625rem;
  511. display: grid;
  512. @media (max-width: calc($md - 0.1px)) {
  513. grid-template-columns: repeat(2, 1fr);
  514. .op_open_door {
  515. grid-column: 1/-1;
  516. }
  517. .op_up_tray {
  518. grid-column: 1/-1;
  519. }
  520. }
  521. @media (min-width: $md) and (max-width: calc($lg - 0.1px)) {
  522. grid-template-columns: repeat(6, 1fr);
  523. > * {
  524. grid-column: span 3;
  525. }
  526. .op_open_door {
  527. grid-column: 1/-1;
  528. }
  529. .op_move_heat {
  530. grid-column: span 2;
  531. }
  532. .op_move_act {
  533. grid-column: span 2;
  534. }
  535. .op_move_exception {
  536. grid-column: span 2;
  537. }
  538. }
  539. @media (min-width: $lg) and (max-width: calc($xl - 0.1px)) {
  540. grid-template-columns: repeat(2, 1fr);
  541. .op_open_door {
  542. grid-column: 1/-1;
  543. }
  544. .op_up_tray {
  545. grid-column: 1/-1;
  546. }
  547. }
  548. @media (min-width: $xl) {
  549. grid-template-columns: repeat(6, 1fr);
  550. > * {
  551. grid-column: span 3;
  552. }
  553. .op_open_door {
  554. grid-column: 1/-1;
  555. }
  556. .op_move_heat {
  557. grid-column: span 2;
  558. }
  559. .op_move_act {
  560. grid-column: span 2;
  561. }
  562. .op_move_exception {
  563. grid-column: span 2;
  564. }
  565. }
  566. }
  567. }
  568. .btn_size {
  569. height: 2.75rem;
  570. color: #1989fa;
  571. border: 1px solid #1989fa;
  572. font-size: 1.25rem;
  573. }
  574. .liquid {
  575. display: flex;
  576. justify-content: center;
  577. width: 100%;
  578. height: 100%;
  579. align-items: center;
  580. .addLiquid {
  581. width: 70.375rem;
  582. height: 52rem;
  583. background: #ffffff;
  584. }
  585. }
  586. .home_tube {
  587. height: 13rem;
  588. background: #384d5d;
  589. opacity: 1;
  590. margin-top: 0.5rem;
  591. display: flex;
  592. flex-wrap: wrap;
  593. justify-content: center;
  594. gap: 0.6rem;
  595. padding-top: 0.2rem;
  596. border-radius: 1.5rem;
  597. .inner-circle {
  598. border-radius: 50%;
  599. width: 2.5rem;
  600. height: 2.5rem;
  601. background-color: rgb(212, 212, 212);
  602. }
  603. }
  604. .task_name{
  605. height:17.25rem;
  606. width: 27.5rem;
  607. background: #ffffff;
  608. .task_title{
  609. font-size: 1.25rem;
  610. color: #40474E;
  611. margin-left: 1.25rem;
  612. margin-top: 1.875rem
  613. }
  614. .task_name_content{
  615. margin-top:1.875rem;
  616. margin-left: 1.5rem;
  617. font-size: 1.25rem;
  618. color: #40474E;
  619. .task_input{
  620. border: 1px solid #dcdcdc;
  621. border-radius: 8px;
  622. height: 3rem;
  623. }
  624. }
  625. .task_button{
  626. display: flex;
  627. justify-content: center;
  628. }
  629. }
  630. </style>