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

577 lines
14 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
  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"
  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"
  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"></CraftList>
  97. </OverlayModal>
  98. </div>
  99. </template>
  100. <script lang="ts" setup>
  101. import { ref, reactive, onMounted, onUnmounted, watch } from "vue";
  102. //@ts-ignore
  103. import { ElMessage } from "element-plus";
  104. import { createWebSocket, sharedWsUrl } from "@/services/socket";
  105. import { HeatPosition, TakePickture, AddLiquid } from "./components";
  106. import OverlayModal from "@/components/OverlayModal.vue";
  107. import CraftList from "@/views/graphite/components/CraftList.vue";
  108. import { graphiteMock } from "./components/mock";
  109. import { useStatusStore } from "@/stores/status";
  110. import {
  111. CmdDescMap,
  112. taskCmd,
  113. type OperationCmd,
  114. } from "@/services/globalCmd/globalCmd";
  115. import { getTxnRecord } from "@/services/txn";
  116. import { useSettingStore } from "@/stores/setting";
  117. const settingStore = useSettingStore();
  118. //设备的全局状态
  119. const statusStore = useStatusStore();
  120. const heatList: any = ref([]);
  121. const craftVisible = ref(false);
  122. const switchModule = ref(false);
  123. let tubeList = reactive<any>([]);
  124. const selectedColor = "#4F85FB";
  125. const emptyColor = "#FFFFFF";
  126. const defaultColor = "#189952";
  127. onMounted(() => {
  128. //6个加热区数据
  129. heatList.value = settingStore.heatAreaConfig;
  130. //设备16个试管的基础数据
  131. tubeBaseConfig();
  132. //连接socket
  133. const wsClient = createWebSocket(sharedWsUrl);
  134. const subscription = wsClient.dataOb.subscribe((data) => {
  135. if (data.type === "cmd") {
  136. const cmdInfo = getTxnRecord(data.data.commandId, "task");
  137. if (cmdInfo) {
  138. const cmdName = CmdDescMap[cmdInfo.command];
  139. const result = data.data.success
  140. ? "执行完毕"
  141. : `执行失败 ${data.data.message}`;
  142. ElMessage({
  143. message: `${cmdName} ${result}`,
  144. type: data.data.success ? "success" : "error",
  145. });
  146. }
  147. } else if (data.type === "status") {
  148. statusStore.setStatus(data.data);
  149. } else if (data.type === "warn") {
  150. ElMessage({
  151. message: data.data.message,
  152. type: "warning",
  153. });
  154. }
  155. });
  156. wsClient.connect();
  157. onUnmounted(() => {
  158. subscription.unsubscribe();
  159. });
  160. });
  161. //选择的托盘
  162. const selectedTrayList = ref<any>([]);
  163. const selectedTrayObj: any = {};
  164. const onSelectedTray = (data: any) => {
  165. console.log("data==操作加热区:=", data);
  166. if (!data.isSelect) {
  167. delete selectedTrayObj[data.id];
  168. } else {
  169. selectedTrayObj[data.id] = data;
  170. }
  171. selectedTrayList.value = Object.values(selectedTrayObj);
  172. };
  173. //设置加热区温度
  174. const onSetHeatAreaTemp = (dataInfo:any) => {
  175. selectedTrayObj[dataInfo.id] = dataInfo;
  176. selectedTrayList.value = Object.values(selectedTrayObj);
  177. }
  178. const tubeBaseConfig = () => {
  179. //默认为16个
  180. for (let i = 0; i < 16; i++) {
  181. tubeList.push({
  182. id: i + 1,
  183. color: "rgb(212, 212, 212)",
  184. });
  185. }
  186. };
  187. //添加溶液
  188. const liquidVisible = ref(false);
  189. const onAddLiquid = () => {
  190. //检查加液区是否有试管。但是现在没有这个状态,暂时不考虑。
  191. liquidVisible.value = true;
  192. };
  193. const onAddSolution = (data: any) => {
  194. let ids = data.map((item: any) => item.id);
  195. tubeList.forEach((item: any) => {
  196. if (ids.includes(item.id)) {
  197. item.default = defaultColor;
  198. item.color = defaultColor;
  199. item.isSelected = true;
  200. }
  201. });
  202. };
  203. const changeVisible = () => {
  204. craftVisible.value = false;
  205. };
  206. //选择工艺
  207. const onChooseCaft = () => {
  208. craftVisible.value = true;
  209. };
  210. const onChooseTube = (tubeItem: any, index: any) => {
  211. if (!tubeItem.id) return;
  212. //@ts-ignore
  213. let list = [...tubeList];
  214. for (let i = 0; i < list.length; i++) {
  215. let item = list[i];
  216. if (index == i) {
  217. item.color = selectedColor;
  218. item.isSelected = true;
  219. } else {
  220. item.color = item.default ? item.default : emptyColor;
  221. item.isSelected = false;
  222. }
  223. }
  224. tubeList = [...list];
  225. };
  226. //移至加热
  227. const onMoveToHeat = () => {
  228. //1、是否选择了加热区
  229. if (!selectedTrayList.value.length) {
  230. ElMessage.error("请选择目标加热区");
  231. return;
  232. }
  233. //2、只能选择一个加热区
  234. if (selectedTrayList.value.length != 1) {
  235. ElMessage.error("只能选择一个加热区");
  236. return;
  237. }
  238. let selectedDataItem = selectedTrayList.value[0];
  239. //2、判断选择的加热区是否已经有了试管架
  240. let heatAearStatus: any = statusStore.status?.trayStatus || graphiteMock.heatAreaStatus;
  241. if (!heatAearStatus[selectedDataItem.index]) {
  242. ElMessage.error("选择的加热区已有试管架,重新选择加热区");
  243. return;
  244. }
  245. //调用移至加热接口
  246. const params = {
  247. areaId: selectedDataItem.id,
  248. };
  249. const command: OperationCmd = "moveToHeatArea";
  250. taskCmd({ command, params }).then((res) => {
  251. if (res.success) {
  252. ElMessage.success("指令已发送,请稍等");
  253. } else {
  254. ElMessage.error(res.msg);
  255. }
  256. });
  257. //指令完成成更新UI
  258. let list = [...heatList.value];
  259. list.forEach((item: any) => {
  260. if (item.id == selectedDataItem.id) {
  261. item.tubeList = JSON.parse(JSON.stringify(tubeList));
  262. selectedTrayObj[item.id] = item;
  263. selectedTrayList.value = Object.values(selectedTrayObj);
  264. }
  265. });
  266. tubeList.forEach((item:any) => {
  267. item.color = '';
  268. item.default = ''
  269. });
  270. heatList.value = [...list];
  271. };
  272. //移至加液区(操作区)
  273. const onMoveToOperationArea = () => {
  274. //1、判断加液区是否有试管架(暂时获取不到这个状态)
  275. //1、是否选择了试管架/加热区
  276. if (!selectedTrayList.value.length) {
  277. ElMessage.error("请选择试管架");
  278. return;
  279. }
  280. //2、只能选择一个试管架/加热区
  281. if (selectedTrayList.value.length != 1) {
  282. ElMessage.error("只能选择一个试管架");
  283. return;
  284. }
  285. let selectedDataItem = selectedTrayList.value[0];
  286. selectedDataItem.isSelect = false
  287. //3、选择的加热区有没有试管架
  288. if(!selectedDataItem.tubeList || !selectedDataItem.tubeList.length){
  289. ElMessage.error("选择的加热区没有试管架");
  290. return;
  291. }
  292. //4、发送移至加液区指令
  293. const params = {
  294. areaId: selectedDataItem.id
  295. }
  296. onSendCmd('moveToActionArea', params)
  297. //更新UI
  298. heatList.value.forEach((item:any) => {
  299. if(item.id == selectedDataItem.id){
  300. tubeList = [...item.tubeList]
  301. item.tubeList = null;
  302. }
  303. })
  304. onSelectedTray(selectedDataItem)
  305. }
  306. //开门
  307. const onOPen = () => {
  308. const params = {};
  309. const command: OperationCmd = "openDoor";
  310. taskCmd({ command, params }).then((res) => {
  311. if (res.success) {
  312. ElMessage.success("指令已发送,请稍等");
  313. } else {
  314. ElMessage.error(res.msg);
  315. }
  316. });
  317. };
  318. //开始加热
  319. const onStartHeat = () => {
  320. //选择的加热区
  321. if (!selectedTrayList.value.length) {
  322. ElMessage.error("请选择目标加热区");
  323. return;
  324. }
  325. //判断选中的加热区是否有试管架
  326. let existTubeRack = true;
  327. //加热区是否设置了加热温度
  328. let hasSetTemp = true;
  329. selectedTrayList.value.forEach((item:any) => {
  330. let tubeList = item.tubeList;
  331. if(!tubeList || !tubeList.length){
  332. existTubeRack = false;
  333. }
  334. if(!item.temperature){
  335. hasSetTemp = false;
  336. }
  337. })
  338. if(!existTubeRack){
  339. ElMessage.error("选择的加热区未放置试管架,请重新选择")
  340. return;
  341. }
  342. if(!hasSetTemp){
  343. ElMessage.error("选择的加热区未设置温度,请设置温度")
  344. return;
  345. }
  346. //后台批量发送指令
  347. const cmdList:any = []
  348. selectedTrayList.value.forEach((heatArea:any) => {
  349. cmdList.push({
  350. command:'startHeat',
  351. params:{
  352. areaId : heatArea.id
  353. }
  354. })
  355. })
  356. console.log('cmdList---', cmdList)
  357. onSendCmd("startHeat", cmdList)
  358. }
  359. const onSendCmd = (command:OperationCmd,params:any)=> {
  360. //发送加热指令
  361. taskCmd({ command, params }).then((res) => {
  362. if (res.success) {
  363. ElMessage.success("指令已发送,请稍等");
  364. } else {
  365. ElMessage.error(res.msg);
  366. }
  367. });
  368. }
  369. </script>
  370. <style lang="scss" scoped>
  371. @use "@/assets/style/mixin.scss" as *;
  372. .graphite_home {
  373. background: #f6f6f6;
  374. display: flex;
  375. gap: 1rem;
  376. // @media (min-width: $md) {
  377. flex-direction: column;
  378. align-items: stretch;
  379. // }
  380. .picture_area {
  381. display: flex;
  382. flex-direction: column-reverse;
  383. }
  384. @media (min-width: $lg) {
  385. flex-direction: row;
  386. align-items: start;
  387. .picture_area {
  388. display: flex;
  389. flex-direction: column;
  390. height: 47.5rem;
  391. }
  392. }
  393. }
  394. .heat_area {
  395. margin: 5px 0;
  396. background: #ffffff;
  397. border-radius: 20px;
  398. column-gap: 8px;
  399. row-gap: 10px;
  400. padding: 1.5rem 0.5rem;
  401. min-width: 600px;
  402. flex: 1 1 auto;
  403. display: grid;
  404. grid-template-columns: repeat(3, 1fr);
  405. > * {
  406. justify-self: center;
  407. align-self: center;
  408. }
  409. @media (min-width: $md) {
  410. column-gap: 12px;
  411. row-gap: 20px;
  412. height: 47.5rem;
  413. }
  414. @media (min-width: $xl) {
  415. padding: 4.5rem 1rem;
  416. }
  417. .craft_executing_modal {
  418. position: absolute;
  419. width: 10.5rem;
  420. height: 18rem;
  421. background: rgb(230, 230, 230);
  422. opacity: 0.5;
  423. z-index: 9999;
  424. }
  425. }
  426. .picture_area {
  427. display: flex;
  428. flex-direction: column;
  429. margin: 5px;
  430. // margin-left: 1.25rem;
  431. background: #ffffff;
  432. border-radius: 20px;
  433. padding: 0 1.5rem;
  434. @media (min-width: $lg) {
  435. flex: 1 1 180px;
  436. }
  437. @media (min-width: $xl) {
  438. flex: 0 0 auto;
  439. width: 27rem;
  440. }
  441. .graphite_btn_container {
  442. margin: 2rem 0;
  443. gap: 0.625rem;
  444. display: grid;
  445. @media (max-width: calc($md - 0.1px)) {
  446. grid-template-columns: repeat(2, 1fr);
  447. .op_open_door {
  448. grid-column: 1/-1;
  449. }
  450. .op_up_tray {
  451. grid-column: 1/-1;
  452. }
  453. }
  454. @media (min-width: $md) and (max-width: calc($lg - 0.1px)) {
  455. grid-template-columns: repeat(6, 1fr);
  456. > * {
  457. grid-column: span 3;
  458. }
  459. .op_open_door {
  460. grid-column: 1/-1;
  461. }
  462. .op_move_heat {
  463. grid-column: span 2;
  464. }
  465. .op_move_act {
  466. grid-column: span 2;
  467. }
  468. .op_move_exception {
  469. grid-column: span 2;
  470. }
  471. }
  472. @media (min-width: $lg) and (max-width: calc($xl - 0.1px)) {
  473. grid-template-columns: repeat(2, 1fr);
  474. .op_open_door {
  475. grid-column: 1/-1;
  476. }
  477. .op_up_tray {
  478. grid-column: 1/-1;
  479. }
  480. }
  481. @media (min-width: $xl) {
  482. grid-template-columns: repeat(6, 1fr);
  483. > * {
  484. grid-column: span 3;
  485. }
  486. .op_open_door {
  487. grid-column: 1/-1;
  488. }
  489. .op_move_heat {
  490. grid-column: span 2;
  491. }
  492. .op_move_act {
  493. grid-column: span 2;
  494. }
  495. .op_move_exception {
  496. grid-column: span 2;
  497. }
  498. }
  499. }
  500. }
  501. .btn_size {
  502. height: 2.75rem;
  503. color: #1989fa;
  504. border: 1px solid #1989fa;
  505. font-size: 1.25rem;
  506. }
  507. .liquid {
  508. display: flex;
  509. justify-content: center;
  510. width: 100%;
  511. height: 100%;
  512. align-items: center;
  513. .addLiquid {
  514. width: 70.375rem;
  515. height: 52rem;
  516. background: #ffffff;
  517. }
  518. }
  519. .home_tube {
  520. height: 12rem;
  521. background: #384d5d;
  522. opacity: 1;
  523. margin-top: 0.5rem;
  524. display: flex;
  525. flex-wrap: wrap;
  526. gap: 0.5rem;
  527. padding-left: 0.4rem;
  528. padding-top: 0.2rem;
  529. border-radius: 1.5rem;
  530. .inner-circle {
  531. border-radius: 50%;
  532. width: 2.5rem;
  533. height: 2.5rem;
  534. background-color: rgb(212, 212, 212);
  535. }
  536. }
  537. </style>