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

732 lines
18 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
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
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. @onSelectCraft="(craftData)=>{onSelectCraft(item, craftData)}"
  11. ></HeatPosition>
  12. </div>
  13. <!--执行中状态的遮罩层-->
  14. </div>
  15. <!--拍照区-->
  16. <div class="picture_area">
  17. <!--加液区和拍照区可切换-->
  18. <div style="display: flex; justify-content: center; align-items: center">
  19. <div style="font-size: 1rem; margin-left: 0.3rem">加液区</div>
  20. <van-switch
  21. v-model="switchModule"
  22. size="1rem"
  23. active-color="#ee0a24"
  24. inactive-color="green"
  25. />
  26. <div style="font-size: 1rem; margin-left: 0.3rem">拍照区</div>
  27. </div>
  28. <TakePickture v-if="switchModule"></TakePickture>
  29. <div v-else class="home_tube">
  30. <div
  31. class="inner-circle"
  32. v-for="(tubeItem, index) in tubeList"
  33. :key="index"
  34. :style="{ background: tubeItem.color }"
  35. ></div><!--@click="onChooseTube(tubeItem, index)"-->
  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" @click="onCraftStart"
  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" v-if="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" @selectedCraft="onHandleSelectedCraft"></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 { craftStart } from "@/services/ore/oreManage";
  133. import { getTxnRecord } from "@/services/txn";
  134. import { useSettingStore } from "@/stores/setting";
  135. const settingStore = useSettingStore();
  136. //设备的全局状态
  137. const statusStore = useStatusStore();
  138. const heatList: any = ref([]);
  139. const craftVisible = ref(false);
  140. const switchModule = ref(false);
  141. let tubeList = reactive<any>([]);
  142. const selectedColor = "#4F85FB";
  143. const emptyColor = "#FFFFFF";
  144. const defaultColor = "#189952";
  145. const taskName = ref('')
  146. onMounted(() => {
  147. //6个加热区数据
  148. heatList.value = settingStore.heatAreaConfig.map((item:any) => {
  149. //添加一个字段,默认为未选中
  150. item.isSelect = false;
  151. return item;
  152. });
  153. //设备16个试管的基础数据
  154. tubeBaseConfig();
  155. //连接socket
  156. const wsClient = createWebSocket(sharedWsUrl);
  157. const subscription = wsClient.dataOb.subscribe((data) => {
  158. if (data.type === "cmd") {
  159. const cmdInfo = getTxnRecord(data.data.commandId, "task");
  160. if (cmdInfo) {
  161. const cmdName = CmdDescMap[cmdInfo.command];
  162. const result = data.data.success
  163. ? "执行完毕"
  164. : `执行失败 ${data.data.message}`;
  165. ElMessage({
  166. message: `${cmdName} ${result}`,
  167. type: data.data.success ? "success" : "error",
  168. });
  169. }
  170. }
  171. });
  172. wsClient.connect();
  173. onUnmounted(() => {
  174. subscription.unsubscribe();
  175. });
  176. });
  177. //选中的托盘
  178. const selectedTrayList = ref<any>([]);
  179. const selectedTrayObj: any = {};
  180. const onSelectedTray = (heatAreaItem: any, type:string) => {
  181. heatList.value[heatAreaItem.index] = heatAreaItem;
  182. //取消选中,已经存在selectedTrayList中
  183. let ids = selectedTrayList.value.map((tube:any) =>tube.id)
  184. if(type == 'isClick'){//点击加热区
  185. if(ids.includes(heatAreaItem.id)){
  186. heatAreaItem.isSelect = false;
  187. selectedTrayList.value = selectedTrayList.value.filter((selectedItem:any) => selectedItem.id != heatAreaItem.id)
  188. }else{
  189. heatAreaItem.isSelect = true;
  190. selectedTrayList.value.push(heatAreaItem)
  191. }
  192. }else{
  193. heatAreaItem.isSelect = true;
  194. }
  195. heatList.value.forEach((item:any) => {
  196. if(item.id == heatAreaItem.id){
  197. item = heatAreaItem
  198. }
  199. })
  200. // selectedTrayList.value = Object.values(selectedTrayObj);
  201. };
  202. //设置加热区温度
  203. const onSetHeatAreaTemp = (dataInfo:any) => {
  204. selectedTrayObj[dataInfo.id] = dataInfo;
  205. selectedTrayList.value = Object.values(selectedTrayObj);
  206. }
  207. //加热区选择的工艺
  208. const onSelectCraft = (item:any, craftInfo:any) => {
  209. item.isSelect = true;
  210. item.craftInfo = craftInfo
  211. onSelectedTray(item,'isMove')
  212. console.log('加热区选择的工艺===', item,craftInfo)
  213. }
  214. //开始执行工艺
  215. const onCraftStart = () => {
  216. if (!selectedTrayList.value.length) {
  217. ElMessage.error("请选择目标加热区");
  218. return;
  219. }
  220. //判断是否选择了工艺
  221. //可能会选择多个加热区执行工艺, 批量发送指令
  222. selectedTrayList.value.forEach((item:any) => {
  223. const params = {
  224. craftId: item.craftInfo.id,
  225. heatId: item.id
  226. }
  227. craftStart(params).then(res => {
  228. ElMessage.success('执行工艺的指令已发送')
  229. })
  230. })
  231. }
  232. const tubeBaseConfig = () => {
  233. //默认为16个
  234. for (let i = 0; i < 16; i++) {
  235. tubeList.push({
  236. id: i + 1,
  237. color: "rgb(212, 212, 212)",
  238. });
  239. }
  240. };
  241. //添加溶液
  242. const liquidVisible = ref(false);
  243. const onAddLiquid = () => {
  244. //检查加液区是否有试管。但是现在没有这个状态,暂时不考虑。
  245. liquidVisible.value = true;
  246. };
  247. //选择的工艺
  248. const onHandleSelectedCraft = (craftData:any) => {
  249. changeVisible();
  250. }
  251. const onAddSolution = (data: any) => {
  252. let ids = data.map((item: any) => item.id);
  253. //批量发送加液指令
  254. data.forEach((item:any) => {
  255. onSendCmd('injectFluid', item)
  256. });
  257. tubeList.forEach((item: any) => {
  258. if (ids.includes(item.id)) {
  259. item.default = defaultColor;
  260. item.color = defaultColor;
  261. item.isSelected = true;
  262. }
  263. });
  264. };
  265. const changeVisible = () => {
  266. craftVisible.value = false;
  267. };
  268. //选择工艺
  269. const onChooseCaft = () => {
  270. craftVisible.value = true;
  271. };
  272. const onChooseTube = (tubeItem: any, index: any) => {
  273. if (!tubeItem.id) return;
  274. //@ts-ignore
  275. let list = [...tubeList];
  276. for (let i = 0; i < list.length; i++) {
  277. let item = list[i];
  278. if (index == i) {
  279. item.color = selectedColor;
  280. item.isSelected = true;
  281. } else {
  282. item.color = item.default ? item.default : emptyColor;
  283. item.isSelected = false;
  284. }
  285. }
  286. tubeList = [...list];
  287. };
  288. //移至加热
  289. const onMoveToHeat = () => {
  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. //2、判断选择的加热区是否已经有了试管架, 加热区是否有试管是通过设备上报的数据获取的。此处为mock的数据。 TODO
  302. let heatAearStatus: any = statusStore.status?.heater ? statusStore.status?.heater[selectedDataItem.index].trayStatus : graphiteMock.heatAreaStatus;
  303. if (!heatAearStatus[selectedDataItem.index]) {
  304. ElMessage.error("选择的加热区已有试管架,重新选择加热区");
  305. return;
  306. }
  307. //调用移至加热接口
  308. const params = {
  309. areaId: selectedDataItem.id,
  310. };
  311. const command: OperationCmd = "moveToHeatArea";
  312. taskCmd({ command, params }).then((res) => {
  313. if (res.success) {
  314. ElMessage.success("指令已发送,请稍等");
  315. } else {
  316. ElMessage.error(res.msg);
  317. }
  318. });
  319. //指令完成成更新UI
  320. let list = [...heatList.value];
  321. list.forEach((item: any) => {
  322. if (item.id == selectedDataItem.id) {
  323. item.tubeList = JSON.parse(JSON.stringify(tubeList));
  324. selectedDataItem.tubeList = JSON.parse(JSON.stringify(tubeList));
  325. //标注该加热区是选中状态
  326. item.isSelect = true;
  327. }
  328. });
  329. tubeList.forEach((item:any) => {
  330. item.color = '';
  331. item.default = ''
  332. });
  333. heatList.value = [...list];
  334. };
  335. //移至加液区(操作区)
  336. const onMoveToOperationArea = () => {
  337. //1、判断加液区是否有试管架(暂时获取不到这个状态)
  338. //1、是否选择了试管架/加热区
  339. if (!selectedTrayList.value.length) {
  340. ElMessage.error("请选择试管架");
  341. return;
  342. }
  343. //2、只能选择一个试管架/加热区
  344. if (selectedTrayList.value.length != 1) {
  345. ElMessage.error("只能选择一个试管架");
  346. return;
  347. }
  348. let selectedDataItem = selectedTrayList.value[0];
  349. //3、选择的加热区有没有试管架
  350. if(!selectedDataItem.tubeList || !selectedDataItem.tubeList.length){
  351. ElMessage.error("选择的加热区没有试管架");
  352. return;
  353. }
  354. //4、发送移至加液区指令
  355. const params = {
  356. areaId: selectedDataItem.id
  357. }
  358. onSendCmd('moveToActionArea', params)
  359. //更新UI
  360. heatList.value.forEach((item:any) => {
  361. if(item.id == selectedDataItem.id){
  362. tubeList = [...item.tubeList]
  363. item.tubeList = null;
  364. }
  365. })
  366. onSelectedTray(selectedDataItem, 'isMove')
  367. }
  368. //移至特殊区域
  369. const onMoveToSpecial = () => {
  370. //检查是否设置了异常区域
  371. const systemSetting = settingStore.systemSetting
  372. let specialArea:any = {}
  373. if(systemSetting && systemSetting.length){
  374. systemSetting.forEach(item => {
  375. if(item.code == "sys_setting_abnormal_area"){
  376. specialArea = item;
  377. }
  378. })
  379. if(!specialArea.id){
  380. ElMessage.error('未设置异常区域,请在系统配置中设置')
  381. return;
  382. }
  383. //是否选择了加热区的试管架
  384. if (!selectedTrayList.value.length) {
  385. ElMessage.error("请选择试管架");
  386. return;
  387. }
  388. //2、只能选择一个试管架/加热区
  389. if (selectedTrayList.value.length != 1) {
  390. ElMessage.error("只能选择一个试管架");
  391. return;
  392. }
  393. let selectedDataItem = selectedTrayList.value[0];
  394. selectedDataItem.isSelect = false
  395. onSelectedTray(selectedDataItem, 'isMove')
  396. const params = {
  397. areaId:selectedDataItem.id
  398. }
  399. onSendCmd('moveToActionArea', params)
  400. }
  401. }
  402. //开始实验
  403. const taskNameVisible = ref(false)
  404. const onSave = ()=> {
  405. const params = {
  406. name: taskName.value
  407. }
  408. saveTaskName(params).then(res => {
  409. if(res.success){
  410. ElMessage.success('保存成功')
  411. onCancel()
  412. }else{
  413. ElMessage.error(res.msg)
  414. }
  415. })
  416. }
  417. const onCancel = ()=> {
  418. taskNameVisible.value = false;
  419. }
  420. //开门
  421. const onOPen = () => {
  422. const params = {};
  423. const command: OperationCmd = "openDoor";
  424. taskCmd({ command, params }).then((res) => {
  425. if (res.success) {
  426. ElMessage.success("指令已发送,请稍等");
  427. } else {
  428. ElMessage.error(res.msg);
  429. }
  430. });
  431. };
  432. //开始加热
  433. const onStartHeat = () => {
  434. //选择的加热区
  435. if (!selectedTrayList.value.length) {
  436. ElMessage.error("请选择目标加热区");
  437. return;
  438. }
  439. //判断选中的加热区是否有试管架
  440. let existTubeRack = true;
  441. //加热区是否设置了加热温度
  442. let hasSetTemp = true;
  443. selectedTrayList.value.forEach((item:any) => {
  444. let tubeList = item.tubeList;
  445. if(!tubeList || !tubeList.length){
  446. existTubeRack = false;
  447. }
  448. if(!item.temperature){
  449. hasSetTemp = false;
  450. }
  451. })
  452. if(!existTubeRack){
  453. ElMessage.error("选择的加热区未放置试管架,请重新选择")
  454. return;
  455. }
  456. if(!hasSetTemp){
  457. ElMessage.error("选择的加热区未设置温度,请设置温度")
  458. return;
  459. }
  460. //后台批量发送指令
  461. const cmdList:any = []
  462. selectedTrayList.value.forEach((heatArea:any) => {
  463. cmdList.push({
  464. command:'startHeat',
  465. params:{
  466. areaId : heatArea.id
  467. }
  468. })
  469. })
  470. // onSendCmd("startHeat", cmdList)
  471. }
  472. const onSendCmd = (command:OperationCmd,params:any)=> {
  473. //发送加热指令
  474. taskCmd({ command, params }).then((res) => {
  475. if (res.success) {
  476. ElMessage.success("指令已发送,请稍等");
  477. } else {
  478. ElMessage.error(res.msg);
  479. }
  480. });
  481. }
  482. </script>
  483. <style lang="scss" scoped>
  484. @use "@/assets/style/mixin.scss" as *;
  485. .graphite_home {
  486. background: #f6f6f6;
  487. display: flex;
  488. gap: 1rem;
  489. // @media (min-width: $md) {
  490. flex-direction: column;
  491. align-items: stretch;
  492. // }
  493. .picture_area {
  494. display: flex;
  495. flex-direction: column-reverse;
  496. }
  497. @media (min-width: $lg) {
  498. flex-direction: row;
  499. align-items: start;
  500. .picture_area {
  501. display: flex;
  502. flex-direction: column;
  503. height: 47.5rem;
  504. }
  505. }
  506. }
  507. .heat_area {
  508. margin: 5px 0;
  509. background: #ffffff;
  510. border-radius: 20px;
  511. column-gap: 8px;
  512. row-gap: 10px;
  513. padding: 1.5rem 0.5rem;
  514. min-width: 600px;
  515. flex: 1 1 auto;
  516. display: grid;
  517. grid-template-columns: repeat(3, 1fr);
  518. > * {
  519. justify-self: center;
  520. align-self: center;
  521. }
  522. @media (min-width: $md) {
  523. column-gap: 12px;
  524. row-gap: 20px;
  525. height: 47.5rem;
  526. }
  527. @media (min-width: $xl) {
  528. padding: 4.5rem 1rem;
  529. }
  530. .craft_executing_modal {
  531. position: absolute;
  532. width: 10.5rem;
  533. height: 18rem;
  534. background: rgb(230, 230, 230);
  535. opacity: 0.5;
  536. z-index: 9999;
  537. }
  538. }
  539. .picture_area {
  540. display: flex;
  541. flex-direction: column;
  542. margin: 5px;
  543. // margin-left: 1.25rem;
  544. background: #ffffff;
  545. border-radius: 20px;
  546. padding: 0 1.5rem;
  547. @media (min-width: $lg) {
  548. flex: 1 1 180px;
  549. }
  550. @media (min-width: $xl) {
  551. flex: 0 0 auto;
  552. width: 27rem;
  553. }
  554. .graphite_btn_container {
  555. margin: 2rem 0;
  556. gap: 0.625rem;
  557. display: grid;
  558. @media (max-width: calc($md - 0.1px)) {
  559. grid-template-columns: repeat(2, 1fr);
  560. .op_open_door {
  561. grid-column: 1/-1;
  562. }
  563. .op_up_tray {
  564. grid-column: 1/-1;
  565. }
  566. }
  567. @media (min-width: $md) and (max-width: calc($lg - 0.1px)) {
  568. grid-template-columns: repeat(6, 1fr);
  569. > * {
  570. grid-column: span 3;
  571. }
  572. .op_open_door {
  573. grid-column: 1/-1;
  574. }
  575. .op_move_heat {
  576. grid-column: span 2;
  577. }
  578. .op_move_act {
  579. grid-column: span 2;
  580. }
  581. .op_move_exception {
  582. grid-column: span 2;
  583. }
  584. }
  585. @media (min-width: $lg) and (max-width: calc($xl - 0.1px)) {
  586. grid-template-columns: repeat(2, 1fr);
  587. .op_open_door {
  588. grid-column: 1/-1;
  589. }
  590. .op_up_tray {
  591. grid-column: 1/-1;
  592. }
  593. }
  594. @media (min-width: $xl) {
  595. grid-template-columns: repeat(6, 1fr);
  596. > * {
  597. grid-column: span 3;
  598. }
  599. .op_open_door {
  600. grid-column: 1/-1;
  601. }
  602. .op_move_heat {
  603. grid-column: span 2;
  604. }
  605. .op_move_act {
  606. grid-column: span 2;
  607. }
  608. .op_move_exception {
  609. grid-column: span 2;
  610. }
  611. }
  612. }
  613. }
  614. .btn_size {
  615. height: 2.75rem;
  616. color: #1989fa;
  617. border: 1px solid #1989fa;
  618. font-size: 1.25rem;
  619. }
  620. .liquid {
  621. display: flex;
  622. justify-content: center;
  623. width: 100%;
  624. height: 100%;
  625. align-items: center;
  626. .addLiquid {
  627. width: 70.375rem;
  628. height: 52rem;
  629. background: #ffffff;
  630. }
  631. }
  632. .home_tube {
  633. height: 13rem;
  634. background: #384d5d;
  635. opacity: 1;
  636. margin-top: 0.5rem;
  637. display: flex;
  638. flex-wrap: wrap;
  639. justify-content: center;
  640. gap: 0.6rem;
  641. padding-top: 0.2rem;
  642. border-radius: 1.5rem;
  643. .inner-circle {
  644. border-radius: 50%;
  645. width: 2.5rem;
  646. height: 2.5rem;
  647. background-color: rgb(212, 212, 212);
  648. }
  649. }
  650. .task_name{
  651. height:17.25rem;
  652. width: 27.5rem;
  653. background: #ffffff;
  654. .task_title{
  655. font-size: 1.25rem;
  656. color: #40474E;
  657. margin-left: 1.25rem;
  658. margin-top: 1.875rem
  659. }
  660. .task_name_content{
  661. margin-top:1.875rem;
  662. margin-left: 1.5rem;
  663. font-size: 1.25rem;
  664. color: #40474E;
  665. .task_input{
  666. border: 1px solid #dcdcdc;
  667. border-radius: 8px;
  668. height: 3rem;
  669. }
  670. }
  671. .task_button{
  672. display: flex;
  673. justify-content: center;
  674. }
  675. }
  676. </style>