trainChapter.vue 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196
  1. <template>
  2. <div class="chapter-container">
  3. <div v-if="!state.hasRead" class="chapter-describe">
  4. <div class="describe-cnt" v-html="state.readHtml"></div>
  5. <div class="describe-readed">
  6. <a class="readed-btn" :class="{ reading: state.readingTime > 0 }" @click="hasReadClick">知道了{{ state.readingTime > 0 ? ` (${state.readingTime}s)` : '' }}</a>
  7. </div>
  8. </div>
  9. <div v-if="state.hasRead && state.showCnt" class="chapter-box">
  10. <NavMenus :back-confirm="state.lastChapterNum != maxChapterNum"></NavMenus>
  11. <div class="chapter-content">
  12. <!-- 原始 -->
  13. <!-- <div class="menu-box" :class="{ fold: state.menuFold }"> -->
  14. <!-- 新增新的三维逻辑,控制该UI什么时候显示 -->
  15. <div v-show="menuBoxOpen == true"
  16. class="menu-box" :class="{ fold: state.menuFold }">
  17. <div class="menu-title">{{ state.chapterTree[state.currentStep]?.name ?? '' }}</div>
  18. <div class="menu-list">
  19. <el-scrollbar :max-height="state.menuListHeight">
  20. <el-steps direction="vertical" space="7rem" :active="state.currentStep">
  21. <el-step v-for="(item, idx) in state.chapterTree" :title="item.name" @click="menuClick($event, idx)">
  22. <template #icon>
  23. <div class="step-icon">
  24. <div class="step-icon-inner"></div>
  25. </div>
  26. </template>
  27. <template #description v-if="item.children?.length > 0">
  28. <el-steps
  29. direction="vertical"
  30. space="1.8rem"
  31. :active="idx < state.currentStep ? item.children.length : idx > state.currentStep ? -1 : state.activeSecondMenu"
  32. >
  33. <el-step v-for="item1 in item.children" :title="item1.name" @click="menuClick($event, idx, item1.id)">
  34. <template #icon><div class="step-icon-second"></div></template>
  35. </el-step>
  36. </el-steps>
  37. </template>
  38. </el-step>
  39. </el-steps>
  40. </el-scrollbar>
  41. <div class="menu-switch" @click="menuSwitch"></div>
  42. </div>
  43. </div>
  44. <div v-if="state.currentChapter.type != 30" class="chapter-detail">
  45. <div class="detail-title">{{ state.currentChapter.name }}</div>
  46. <div class="detail-cnt" v-html="htmlContent"></div>
  47. </div>
  48. <!-- <div v-else class="chapter-3d">
  49. <CourseChapter3d :config="state.chapterTree[state.currentStep]?.threeDimensionalConfig ?? ''"></CourseChapter3d>
  50. <CourseChapter3dView></CourseChapter3dView>
  51. </div> -->
  52. </div>
  53. <!-- 上一步,下一步 -->
  54. <!-- <StepTips
  55. v-if="
  56. (courseChapter3dShow().show.showToastViewBool == false && courseChapter3dShow().show.showLinkOkNextBool == false)
  57. ||
  58. (courseChapter3dShow().show.showToastViewBool == true &&
  59. courseChapter3dShow().show.showToastState == '' &&
  60. courseChapter3dShow().show.showLinkOkNextBool == false)
  61. "
  62. :msg="state.tipsMsg"
  63. :btns="state.tipsBtns"
  64. :countdown="tipsCountdown"
  65. :key="state.tipsKey"
  66. ></StepTips> -->
  67. <StepTips
  68. v-if="
  69. (
  70. (
  71. (
  72. courseChapter3dShow().show.showToastViewBool == false
  73. && courseChapter3dShow().show.showLinkOkNextBool == false
  74. )
  75. ||
  76. (
  77. courseChapter3dShow().show.showToastViewBool == true &&
  78. courseChapter3dShow().show.showToastState == '' &&
  79. courseChapter3dShow().show.showLinkOkNextBool == false
  80. )
  81. ) && threeWorldLoadType == 'cctv'
  82. )
  83. || (
  84. (
  85. threeWorldLoadType == 'qv'
  86. || threeWorldLoadType == 'sonar'
  87. ) && stepTipsBool == true && stepThreeWorldEndOk == false
  88. ) || (
  89. (
  90. threeWorldLoadType == 'qv'
  91. || threeWorldLoadType == 'sonar'
  92. ) && stepThreeWorldEndOk == true
  93. )
  94. "
  95. :msg="state.tipsMsg"
  96. :btns="state.tipsBtns"
  97. :countdown="tipsCountdown"
  98. :key="state.tipsKey"
  99. ></StepTips>
  100. </div>
  101. <!-- 3d组件放在这里,让它可以隐藏起来,提前加载好场景。要不然后续切换3d场景每次要重新加载,比较麻烦 -->
  102. <!-- <div v-show="state.hasRead && state.showCnt && state.currentChapter.type == 30" class="chapter-3d"> -->
  103. <div v-if="courseChapter3dViewBool == true" class="chapter-3d">
  104. <!-- type == 30 是当前流程是三维流程 -->
  105. <div v-show="state.hasRead && state.showCnt && state.currentChapter.type == 30">
  106. <!-- CCTV实训检测三维场景逻辑 -->
  107. <div v-if="threeWorldLoadType == 'cctv'">
  108. <CourseChapter3d
  109. :config="state?.currentChapter ?? ''"
  110. :studentTaskIdList="studentTaskIdList"
  111. @tipsBtnsUpOpen="tipsBtnsUpOpenEvent"
  112. @tipsBtnsDownOpen="tipsBtnsDownOpenEvent"
  113. ></CourseChapter3d>
  114. <CourseChapter3dView
  115. @showOperationHelpExitEvent="showOperationHelpExitEvent"
  116. @carCameraScreenshot="carCameraScreenshotEvent"
  117. @carShutDown="carShutDownEvent"
  118. ></CourseChapter3dView>
  119. </div>
  120. <!-- QV实训检测 -->
  121. <div v-if="threeWorldLoadType == 'qv'">
  122. <QvSimulationMain
  123. v-show="QvSimulationMainLook == true"
  124. :studentTaskIdList="studentTaskIdList"
  125. :processInitRestTime="processInitRestTimeQv"
  126. @callbackProcessUpdate="callbackProcessUpdateQvEvent"
  127. @callbackScreenshot="callbackScreenshotEventQv"
  128. ></QvSimulationMain>
  129. <CarSimulationMain
  130. :processInitRestTime="processInitRestTimeQv"
  131. v-show="CarSimulationMainLook == true"
  132. @callbackChuanGongZuoFu="callbackChuanGongZuoFuQvEvent"
  133. ></CarSimulationMain>
  134. </div>
  135. <!-- 声纳实训检测 -->
  136. <div v-if="threeWorldLoadType == 'sonar'">
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. </template>
  142. <script setup lang="ts">
  143. import { onMounted, reactive, computed, watch, onUnmounted, nextTick, ref } from 'vue';
  144. import NavMenus from '../components/navMenus.vue';
  145. import StepTips from '../components/stepTips.vue';
  146. import { useRoute } from 'vue-router';
  147. import { getCourseInfo, getCourseChapterTree, studyReport, getCurrentChapter, updateProgress } from '@/api/student/trainChapter';
  148. import { studentTaskOptionPipDefectListByStudentId } from '@/api/student/studentTaskOptionPipDefect';
  149. import CourseChapter3d from '@/components/student/CourseChapter3d.vue';
  150. import CourseChapter3dView from '@/components/student/courseChapter3d/view.vue';
  151. import { getTrainDetail } from '@/api/student/trainList';
  152. import router from '@/router/index';
  153. import { courseChapter3dShow } from '@/stores/courseChapter3dShow.ts';
  154. import { threeWorld } from '@/stores/threeWorld.ts';
  155. import { uploadFileOss, getLiteMeta } from '../../../util/fileUtils';
  156. import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
  157. import { ElLoading } from 'element-plus';
  158. import { studentTaskPhoto } from '@/api/techer/taskStudentScore';
  159. import QvSimulationMain from "../../../components/QvSimulation/QvSimulationMain.vue";
  160. import CarSimulationMain from "../../../components/CarSimulation/CarSimulationMain.vue";
  161. import SonarSimulationMain from "../../../components/SonarSimulation/SonarSimulationMain.vue";
  162. const route = useRoute();
  163. const taskId = route.params.taskId as string;
  164. const studentTaskId = route.params.studentTaskId as string;
  165. const state: anyObj = reactive({
  166. hasRead: true,
  167. readingTime: 10,
  168. readHtml: '',
  169. showCnt: false,
  170. menuFold: false,
  171. menuListHeight: ``,
  172. tipsKey: 0,
  173. tipsMsg: '',
  174. tipsBtns: [],
  175. chapterTree: [],
  176. currentStep: 0,
  177. currentNode: 0,
  178. currentChapter: { type: 10 },
  179. activeSecondMenu: -1,
  180. lastChapterNum: -1,
  181. });
  182. // 地下车是否可以展示
  183. let CarSimulationMainLook = ref(true);
  184. // QV检测是否可以展示
  185. let QvSimulationMainLook = ref(false);
  186. // 声纳车是否可以展示
  187. let SonarSimulationMainLook = ref(false);
  188. /**
  189. * 之前CCTV的逻辑是固定了,这里是新增的
  190. * 用于控制 QV 检测 和 声纳车 以及后续可能新增的分类
  191. * 因为后面新增的,有自己的上一步,下一步UI了
  192. * 此时进入三维的上一步下一步再三维UI控制
  193. *
  194. * 所以外部流程控制的先隐藏,直到三维流程走完才显示
  195. */
  196. let stepTipsBool = ref(true);
  197. /**
  198. * 当前三维是否都完成指定任务
  199. * 如果完成任务了,则显示 stepTipsBool 的UI
  200. */
  201. let stepThreeWorldEndOk = ref(false);
  202. // 控制左侧UI .menu-box 是否展示
  203. let menuBoxOpen = ref(true);
  204. /**
  205. * 用于控制应该加载哪个三维场景
  206. * cctv cctv实训检测
  207. * qv qv实训检测
  208. * sonar 声纳实训检测
  209. *
  210. * 默认null ,则先不触发加载
  211. */
  212. let threeWorldLoadType : any = ref(null);
  213. // 控制QV三维里的逻辑重置回第一流程逻辑
  214. let processInitRestTimeQv = ref("0");
  215. let chapterQueue: Array<Array<anyObj>> = [];
  216. let maxChapterNum = 0;
  217. // 获取当前学生任务ID缺陷列表
  218. let studentTaskIdList: any = ref([]);
  219. // 用于控制3d场景什么时候加载显示
  220. let courseChapter3dViewBool = ref(false);
  221. const buildChapterQueue = (data: anyObj[], optionChapters: anyObj[], root = true): anyObj[] => {
  222. let chapters: anyObj[] = [];
  223. data?.forEach((item, idx) => {
  224. const children = item.children;
  225. //delete item.children;
  226. const opt = optionChapters.find((x) => x.chapterId == item.id);
  227. if (opt) item.durationStudyMin = opt.durationStudyMin;
  228. chapters.push(item);
  229. if (children && children.length > 0) {
  230. const childs = buildChapterQueue(children, optionChapters, false);
  231. chapters = chapters.concat(childs);
  232. }
  233. if (root) {
  234. chapterQueue[idx] = [...chapters];
  235. chapters = [];
  236. }
  237. });
  238. if (root) maxChapterNum = (chapterQueue.length - 1) * 100 + chapterQueue[chapterQueue.length - 1].length - 1;
  239. return chapters;
  240. };
  241. const menuSwitch = () => {
  242. state.menuFold = !state.menuFold;
  243. };
  244. const menuClick = (evt: Event, step: number, nodeId?: string) => {
  245. if ((evt.target as HTMLElement).classList.contains('el-step__title')) {
  246. let chapterNum = step * 100,
  247. node = 0;
  248. if (nodeId) {
  249. node = chapterQueue[step].findIndex((x) => x.id == nodeId);
  250. if (node > -1) chapterNum += node;
  251. }
  252. if (chapterNum <= state.lastChapterNum) {
  253. state.currentStep = step;
  254. state.currentNode = node;
  255. state.currentChapter = chapterQueue[step][node];
  256. } else {
  257. ElMessage({
  258. message: '该章节还未完成学习,请先按顺序完成学习!',
  259. type: 'warning',
  260. });
  261. }
  262. }
  263. evt.stopPropagation();
  264. };
  265. const hasReadClick = () => {
  266. state.hasRead = true;
  267. state.showCnt = true;
  268. setMenuListHeight();
  269. };
  270. const setMenuListHeight = () => {
  271. nextTick(() => {
  272. let height = document.querySelector('.menu-box')?.clientHeight ?? 0;
  273. if (height) state.menuListHeight = `calc(${height}px - 12rem)`;
  274. });
  275. };
  276. const countdown = (name: string, callback?: Function) => {
  277. if (state[name]) {
  278. const interval = setInterval(() => {
  279. state[name]--;
  280. if (state[name] == 0) {
  281. clearInterval(interval);
  282. if (callback) callback();
  283. }
  284. }, 1000);
  285. }
  286. };
  287. const tipsCountdown = computed(() => {
  288. let countdown = state.currentChapter.durationStudyMin ?? 0;
  289. const chapterNum = state.currentStep * 100 + state.currentNode;
  290. if (chapterNum <= state.lastChapterNum) countdown = 0;
  291. return countdown;
  292. });
  293. const htmlContent = computed(() => {
  294. if (state.currentChapter.type == 10) {
  295. return state.currentChapter.richText;
  296. } else if (state.currentChapter.type == 20) {
  297. return `<iframe src='${state.currentChapter.videoUrl}' width='100%' height='99%' scrolling='no' frameborder='0'><iframe>`;
  298. } else return '';
  299. });
  300. const prevStep = () => {
  301. if (state.currentNode == 0) {
  302. state.currentStep--;
  303. state.currentNode = chapterQueue[state.currentStep].length - 1;
  304. } else state.currentNode--;
  305. state.currentChapter = chapterQueue[state.currentStep][state.currentNode];
  306. };
  307. const nextStep = () => {
  308. updateStudyProgress();
  309. state.currentNode++;
  310. if (state.currentNode >= chapterQueue[state.currentStep].length) {
  311. state.currentStep++;
  312. state.currentNode = 0;
  313. }
  314. state.currentChapter = chapterQueue[state.currentStep][state.currentNode];
  315. };
  316. const completeStudy = () => {
  317. updateStudyProgress();
  318. router.push({ path: `/train/main/${taskId}` });
  319. };
  320. const updateStudyProgress = () => {
  321. //更新学习进度
  322. const num = state.currentStep * 100 + state.currentNode;
  323. if (num > state.lastChapterNum) {
  324. state.lastChapterNum = num;
  325. updateProgress(studentTaskId, state.currentChapter.id);
  326. }
  327. };
  328. const initTips = () => {
  329. state.tipsKey++;
  330. state.tipsMsg = state.currentChapter.tips;
  331. if (state.currentStep == 0 && state.currentNode == 0) {
  332. state.tipsBtns = [
  333. {
  334. name: '下一步',
  335. click: nextStep,
  336. attr: { type: 'primary' },
  337. },
  338. ];
  339. } else if (state.currentStep == chapterQueue.length - 1 && state.currentNode == chapterQueue[state.currentStep].length - 1) {
  340. // 这里是新的三维场景切换步骤的逻辑
  341. if (
  342. threeWorldLoadType.value == 'qv'
  343. || threeWorldLoadType.value == 'sonar'
  344. ) {
  345. state.tipsBtns = [
  346. // {
  347. // name: '上一步',
  348. // click: prevStep,
  349. // attr: { type: 'primary', plain: 'plain' },
  350. // },
  351. {
  352. name: '完成并前往编制报告',
  353. click: completeStudy,
  354. attr: { type: 'primary' },
  355. },
  356. ];
  357. } else {
  358. state.tipsBtns = [
  359. {
  360. name: '上一步',
  361. click: prevStep,
  362. attr: { type: 'primary', plain: 'plain' },
  363. },
  364. {
  365. name: '完成并前往编制报告',
  366. click: completeStudy,
  367. attr: { type: 'primary' },
  368. },
  369. ];
  370. }
  371. } else {
  372. state.tipsBtns = [
  373. {
  374. name: '上一步',
  375. click: prevStep,
  376. attr: { type: 'primary', plain: 'plain' },
  377. },
  378. {
  379. name: '下一步',
  380. click: nextStep,
  381. attr: { type: 'primary' },
  382. },
  383. ];
  384. }
  385. };
  386. let studyInterval: NodeJS.Timeout;
  387. onMounted(() => {
  388. threeWorld().loadSuccess = false;// CCTV实训检测三维场景逻辑
  389. courseChapter3dViewBool.value = false;
  390. //学习时间上报
  391. studyInterval = setInterval(() => {
  392. const type = state.currentChapter.type != 30 ? 1 : 2;
  393. studyReport(studentTaskId, type);
  394. }, 1000 * 100);
  395. //获取任务信息
  396. getTrainDetail(taskId).then((res) => {
  397. //获取课程信息
  398. getCourseInfo(res.data.data.courseId).then((res1) => {
  399. if (res1.data.data.description) {
  400. state.hasRead = false;
  401. state.readHtml = res1.data.data.description;
  402. countdown('readingTime');
  403. } else {
  404. state.hasRead = true;
  405. state.showCnt = true;
  406. setMenuListHeight();
  407. }
  408. window.addEventListener('resize', () => {
  409. setMenuListHeight();
  410. });
  411. });
  412. //获取课程树
  413. getCourseChapterTree(res.data.data.courseId).then((res2) => {
  414. state.chapterTree = res2.data.data;
  415. threeWorldLoadTypeEvent();
  416. buildChapterQueue(res2.data.data, res.data.data.optionChapters);
  417. //需先根据学习进度确定开始章节:currentStep和currentNode,默认第一章第一节
  418. //获取学习到的章节
  419. getCurrentChapter(studentTaskId).then((res3) => {
  420. const chapterId = res3.data.data;
  421. if (chapterId) {
  422. for (let i = 0; i < chapterQueue.length; i++) {
  423. const idx = chapterQueue[i].findIndex((x) => x.id == chapterId);
  424. if (idx > -1) {
  425. state.currentStep = i;
  426. state.currentNode = idx;
  427. break;
  428. }
  429. }
  430. state.lastChapterNum = state.currentStep * 100 + state.currentNode;
  431. }
  432. state.currentChapter = chapterQueue[state.currentStep][state.currentNode];
  433. initTips();
  434. });
  435. });
  436. // 根据学生id任务获取缺陷数据
  437. studentTaskOptionPipDefectListByStudentId(studentTaskId)
  438. .then((studentTaskOptionPipDefectListByStudentIdRes) => {
  439. studentTaskIdList.value = studentTaskOptionPipDefectListByStudentIdRes?.data?.data;
  440. courseChapter3dViewBool.value = true;
  441. // console.log(
  442. // "根据学生id任务获取缺陷数据 ===>",
  443. // studentTaskIdList.value,
  444. // studentTaskId
  445. // );
  446. })
  447. .catch((error) => {
  448. courseChapter3dViewBool.value = true;
  449. });
  450. });
  451. });
  452. onUnmounted(() => {
  453. if (studyInterval) clearInterval(studyInterval);
  454. });
  455. watch(
  456. () => state.currentChapter,
  457. (newVal) => {
  458. initTips();
  459. //滚动当前课程菜单至可视区域
  460. nextTick(() => {
  461. const target = document.querySelector('.el-step__description .is-process') as HTMLElement;
  462. target?.scrollIntoView({ behavior: 'smooth', block: 'center' });
  463. });
  464. //课程菜单默认显示二级,兼容课程树层级大于二级的情况
  465. if (state.currentNode == 0) state.activeSecondMenu = -1;
  466. else {
  467. const idx = (state.chapterTree[state.currentStep].children as anyObj[])?.findIndex((x) => x.id == newVal.id);
  468. if (idx > -1) state.activeSecondMenu = idx;
  469. }
  470. console.log(
  471. "更新到新的流程 state.currentChapter ===>",
  472. state.currentChapter.type,
  473. state.currentChapter.threeDimensionalConfig,
  474. state.currentChapter,
  475. );
  476. /**
  477. * 后面新增其他三维逻辑进行控制展示
  478. */
  479. if (
  480. threeWorldLoadType.value == 'qv'
  481. || threeWorldLoadType.value == 'sonar'
  482. ) {
  483. /**
  484. * 进入的如果是三维的时候,则隐藏上一步,下一步的UI
  485. * 使用的是三维里内置的上一步下一步的UI
  486. */
  487. if (state.currentChapter.type == 30) {
  488. stepTipsBool.value = false;
  489. }
  490. // 不是三维的时候,则显示上一步,下一步操作
  491. else if (state.currentChapter.type == 10) {
  492. stepTipsBool.value = true;
  493. menuBoxOpen.value = true;
  494. stepThreeWorldEndOk.value = false;
  495. }
  496. if (state.currentChapter.threeDimensionalConfig == 'QV仿真实验') {
  497. processInitRestTimeQv.value = "" + new Date().getTime();
  498. CarSimulationMainLook.value = true;
  499. QvSimulationMainLook.value = false;
  500. }
  501. }
  502. }
  503. );
  504. /**
  505. * // CCTV实训检测三维场景逻辑
  506. * 检测实训注意事项 知道了点击回调
  507. */
  508. courseChapter3dShow().show.showMatterGotItCallback = function () {
  509. // console.log(
  510. // "检测实训注意事项 知道了点击回调", res
  511. // );
  512. // 触发下一步逻辑
  513. nextStep();
  514. };
  515. /**
  516. * // CCTV实训检测三维场景逻辑
  517. * 监听弹出提示隐藏后来触发不同的逻辑
  518. */
  519. watch(
  520. () => courseChapter3dShow().show.showToastViewBool,
  521. (newVal, oldVal) => {
  522. if (courseChapter3dShow().show.showToastViewBool == true) {
  523. return;
  524. }
  525. // console.log(
  526. // " () => courseChapter3dShow().show.showToastState 1111111111 ",
  527. // courseChapter3dShow().show.showToastState
  528. // );
  529. switch (courseChapter3dShow().show.showToastState) {
  530. case '立即穿戴':
  531. // // 触发下一步逻辑
  532. // nextStep();
  533. break;
  534. }
  535. }
  536. );
  537. // 记录 tipsBtnsUpOpenEvent 事件是否显示上一步按钮
  538. let tipsBtnsUpOpenEventBool: Boolean = true;// CCTV实训检测三维场景逻辑
  539. /**
  540. * // CCTV实训检测三维场景逻辑
  541. * 上一步是否显示
  542. * open true - 显示, false - 隐藏
  543. */
  544. const tipsBtnsUpOpenEvent = (open: Boolean) => {
  545. // console.log("上一步是否显示", open);
  546. tipsBtnsUpOpenEventBool = open;
  547. tipsBtnsUpAndDownOpenEvent();
  548. };
  549. // 记录 tipsBtnsUpOpenEvent 事件是否显示下一步按钮
  550. let tipsBtnsDownOpenEventBool: Boolean = true;// CCTV实训检测三维场景逻辑
  551. /**
  552. * // CCTV实训检测三维场景逻辑
  553. * 下一步是否显示
  554. * open true - 显示, false - 隐藏
  555. */
  556. const tipsBtnsDownOpenEvent = (open: Boolean) => {
  557. // console.log("下一步是否显示", open);
  558. tipsBtnsDownOpenEventBool = open;
  559. tipsBtnsUpAndDownOpenEvent();
  560. };
  561. /**
  562. * // CCTV实训检测三维场景逻辑
  563. * 通过3d传来的事件来控制上一步下一步是否显示逻辑
  564. */
  565. const tipsBtnsUpAndDownOpenEvent = () => {
  566. let newArray = [];
  567. if (tipsBtnsUpOpenEventBool == true) {
  568. // newArray.push({
  569. // name: '上一步',
  570. // click: prevStep,
  571. // attr: { type: 'primary', plain: 'plain' },
  572. // });
  573. }
  574. if (tipsBtnsDownOpenEventBool == true) {
  575. newArray.push({
  576. name: '下一步',
  577. click: nextStep,
  578. attr: { type: 'primary' },
  579. });
  580. }
  581. // console.log(
  582. // " ========= state.tipsBtns ========= ",
  583. // state.tipsBtns
  584. // );
  585. // 如果出现 完成并前往编制报告 则不替换
  586. try {
  587. for (let i = 0; i < state.tipsBtns.length; i++) {
  588. let thisTipsBtns = state.tipsBtns[i];
  589. if (thisTipsBtns.name.indexOf('完成') >= 0) {
  590. return;
  591. }
  592. }
  593. } catch (e) {}
  594. state.tipsBtns = newArray;
  595. };
  596. // 操作帮助点击退出
  597. const showOperationHelpExitEvent = () => {// CCTV实训检测三维场景逻辑
  598. // 触发下一步逻辑
  599. nextStep();
  600. };
  601. /**
  602. * 车的完成下井实验
  603. */
  604. const carShutDownEvent = () => {// CCTV实训检测三维场景逻辑
  605. // 触发下一步逻辑
  606. nextStep();
  607. };
  608. /**
  609. * 车的相机截图
  610. * img base64位图片
  611. */
  612. const carCameraScreenshotEvent = (img: any) => {// CCTV实训检测三维场景逻辑
  613. // console.log(
  614. // "车的相机截图", img
  615. // );
  616. let loading = ElLoading.service({
  617. lock: true,
  618. text: '上传中,请稍后……',
  619. background: 'rgba(0, 0, 0, 0.7)',
  620. });
  621. // 将 base64为转换成,Blob格式
  622. let objBlob = dataURLtoBlob(img);
  623. // blob 转换成 file对象
  624. const file = new File([objBlob], 'example.png', { type: 'text/plain' });
  625. // 优先上传文件
  626. // @ts-ignore
  627. uploadFileOss(file, 'course/cover', null)
  628. .then(function (e: any) {
  629. // console.log(
  630. // "上传文件", e
  631. // );
  632. studentTaskPhoto({
  633. studentTaskId: studentTaskId,
  634. imageUrl: e,
  635. remark: '',
  636. })
  637. .then(function (studentTaskPhotoRes: any) {
  638. // console.log(
  639. // "上传文件成功", studentTaskPhotoRes
  640. // );
  641. loading.close();
  642. // ElMessage({
  643. // message: '截图上传完成',
  644. // type: 'success',
  645. // });
  646. if (threeWorldLoadType.value == "cctv") {
  647. courseChapter3dShow().show.showToastViewBool = false;
  648. courseChapter3dShow().show.showPromptEvent('截图上传完成', null, function (res: any) {
  649. // console.log("yes", res);
  650. });
  651. } else {
  652. ElMessage({
  653. message: '截图上传完成',
  654. type: 'success',
  655. });
  656. }
  657. })
  658. .catch(function (studentTaskPhotoResError: any) {
  659. // ElMessage({
  660. // message: '上传失败',
  661. // type: 'warning',
  662. // });
  663. loading.close();
  664. if (threeWorldLoadType.value == "cctv") {
  665. courseChapter3dShow().show.showToastViewBool = false;
  666. courseChapter3dShow().show.showPromptEvent('上传失败', null, function (res: any) {
  667. // console.log("yes", res);
  668. });
  669. } else {
  670. ElMessage({
  671. message: '上传失败',
  672. type: 'warning',
  673. });
  674. }
  675. });
  676. })
  677. .catch(function (e: any) {
  678. // ElMessage({
  679. // message: e || '上传失败',
  680. // type: 'warning',
  681. // });
  682. loading.close();
  683. if (threeWorldLoadType.value == "cctv") {
  684. courseChapter3dShow().show.showToastViewBool = false;
  685. courseChapter3dShow().show.showPromptEvent('上传失败', null, function (res: any) {
  686. // console.log("yes", res);
  687. });
  688. } else {
  689. ElMessage({
  690. message: '上传失败',
  691. type: 'warning',
  692. });
  693. }
  694. });
  695. };
  696. /**
  697. * 将base64转换为blob
  698. * @param dataurl base64位图片
  699. */
  700. const dataURLtoBlob = (dataurl: any) => {
  701. let arr = dataurl.split(','),
  702. mime = arr[0].match(/:(.*?);/)[1],
  703. bstr = atob(arr[1]),
  704. n = bstr.length,
  705. u8arr = new Uint8Array(n);
  706. while (n--) {
  707. u8arr[n] = bstr.charCodeAt(n);
  708. }
  709. return new Blob([u8arr], { type: mime });
  710. };
  711. /**
  712. * 控制三维场景类型,决定加载哪个
  713. */
  714. const threeWorldLoadTypeEvent = () => {
  715. // console.log(
  716. // "threeWorldLoadTypeEvent state.chapterTree ===>", state.chapterTree
  717. // );
  718. let newArray = getChapterTreeArray(state.chapterTree);
  719. // console.log("newArray ===>", newArray);
  720. for (let i = 0; i < newArray.length; i++) {
  721. let objNewArray = newArray[i];
  722. let threeDimensionalConfig = objNewArray["threeDimensionalConfig"];
  723. // console.log("objNewArray", objNewArray);
  724. // console.log("threeDimensionalConfig", threeDimensionalConfig);
  725. if (threeDimensionalConfig == "QV仿真实验") {
  726. threeWorldLoadType.value = "qv";
  727. return;
  728. }
  729. if (threeDimensionalConfig == "将检测机器人6寸右侧轮子安装到正确位置") {
  730. threeWorldLoadType.value = "cctv";
  731. return;
  732. }
  733. if (threeDimensionalConfig == "声纳仿真实验") {
  734. threeWorldLoadType.value = "sonar";
  735. return;
  736. }
  737. }
  738. }
  739. /**
  740. * 将 state.chapterTree 树形结构,转换成数组列表
  741. * array 当前遍历的数组,一开始传 state.chapterTree
  742. */
  743. const getChapterTreeArray = (array : any) => {
  744. if (array == null || array == undefined) {
  745. return [];
  746. }
  747. let retArray : any = [];
  748. for (let i = 0; i < array.length; i++) {
  749. let objArray = array[i];
  750. // console.log("objArray ===>", objArray);
  751. retArray.push(objArray);
  752. if (objArray['children'] != null && objArray['children'] != undefined) {
  753. let children = objArray['children'];
  754. let newArray = getChapterTreeArray(children);
  755. for (let newArrayI = 0; newArrayI < newArray.length; newArrayI++) {
  756. let objNewArray = newArray[newArrayI];
  757. retArray.push(objNewArray);
  758. }
  759. }
  760. }
  761. return retArray;
  762. }
  763. /**
  764. * QV穿完工作服回调
  765. * bool
  766. */
  767. const callbackChuanGongZuoFuQvEvent = (bool : any) => {
  768. // console.log( "穿完工作服回调 callbackChuanGongZuoFuQvEvent ===> ", bool );
  769. if (bool == true) {
  770. QvSimulationMainLook.value = true;
  771. CarSimulationMainLook.value = false;
  772. return;
  773. }
  774. QvSimulationMainLook.value = false;
  775. CarSimulationMainLook.value = true;
  776. }
  777. /**
  778. * QV仿真每次进入新逻辑
  779. */
  780. const callbackProcessUpdateQvEvent = (db : any) => {
  781. console.log( "callbackProcessUpdateQvEvent ===>", db );
  782. if (
  783. db == '连接WIFI'
  784. || db == '开控制终端(QV检测机器人)软件'
  785. || db == '按操作键控制仪器设备'
  786. || db == 'QV下井实验'
  787. ) {
  788. menuBoxOpen.value = false;
  789. } else {
  790. menuBoxOpen.value = true;
  791. }
  792. if (db == '已完成所有任务') {
  793. stepThreeWorldEndOk.value = true;
  794. } else {
  795. stepThreeWorldEndOk.value = false;
  796. }
  797. }
  798. /**
  799. * QV截图逻辑
  800. */
  801. const callbackScreenshotEventQv = (base64Str : any) => {
  802. // console.log(
  803. // "callbackScreenshotEventQv", base64Str
  804. // );
  805. carCameraScreenshotEvent(base64Str);
  806. }
  807. </script>
  808. <style lang="scss" scoped>
  809. .chapter-container {
  810. position: absolute;
  811. width: 100%;
  812. height: 100%;
  813. display: flex;
  814. justify-content: center;
  815. align-items: center;
  816. z-index: 1;
  817. .chapter-describe {
  818. position: relative;
  819. width: 70%;
  820. height: 90%;
  821. background-image: url(/src/assets/student/bg_describe.png);
  822. background-size: 100% 100%;
  823. &:before {
  824. content: '';
  825. position: absolute;
  826. top: 3%;
  827. left: 28%;
  828. height: 6%;
  829. width: 70%;
  830. background-image: url(/src/assets/student/training.png);
  831. background-size: auto 100%;
  832. background-repeat: no-repeat;
  833. }
  834. .describe-cnt {
  835. overflow: auto;
  836. box-sizing: border-box;
  837. position: absolute;
  838. inset: 10rem 0 10rem 6rem;
  839. padding-right: 4rem;
  840. }
  841. .describe-readed {
  842. position: absolute;
  843. bottom: 3rem;
  844. width: 100%;
  845. display: flex;
  846. justify-content: center;
  847. .readed-btn {
  848. width: 20rem;
  849. height: 5.4rem;
  850. line-height: 5.4rem;
  851. text-align: center;
  852. color: white;
  853. font-size: 2rem;
  854. border-radius: 0.8rem;
  855. background: linear-gradient(90deg, #6cd9e9 0%, #4b90dd 100%);
  856. cursor: pointer;
  857. &.reading {
  858. pointer-events: none;
  859. }
  860. }
  861. }
  862. }
  863. .chapter-box {
  864. width: 100%;
  865. height: 100%;
  866. }
  867. .chapter-content {
  868. position: absolute;
  869. width: 100%;
  870. height: 100%;
  871. .menu-box {
  872. position: absolute;
  873. top: 3.2rem;
  874. left: 2rem;
  875. width: 20rem;
  876. height: calc(100% - 18rem);
  877. transition-property: left;
  878. transition-duration: 0.6s;
  879. z-index: 2;
  880. &.fold {
  881. left: -20rem;
  882. .menu-switch {
  883. background-image: url(/src/assets/student/training/menu_switch1.png);
  884. }
  885. }
  886. .menu-switch {
  887. position: absolute;
  888. width: 2rem;
  889. height: 5rem;
  890. background-image: url(/src/assets/student/training/menu_switch.png);
  891. background-size: 100% 100%;
  892. right: -2rem;
  893. top: 50%;
  894. margin-top: -2.5rem;
  895. cursor: pointer;
  896. }
  897. .menu-title {
  898. height: 5rem;
  899. background-image: url(/src/assets/student/training/menu_title.png);
  900. background-size: 100% 100%;
  901. color: white;
  902. font-size: 2rem;
  903. &:before {
  904. content: '';
  905. display: inline-block;
  906. vertical-align: middle;
  907. height: 5rem;
  908. width: 8rem;
  909. background-image: url(/src/assets/student/training/menu_icon.png);
  910. background-size: auto 50%;
  911. background-repeat: no-repeat;
  912. background-position: center center;
  913. }
  914. }
  915. .menu-list {
  916. position: relative;
  917. min-height: 20rem;
  918. max-height: calc(100% - 6rem);
  919. margin-top: 1rem;
  920. padding: 3rem 0 3rem 2rem;
  921. background-image: url(/src/assets/student/training/menu_list.png);
  922. background-size: 100% 100%;
  923. box-sizing: border-box;
  924. --step-item-size: 2rem;
  925. --step-icon-size: 1.2rem;
  926. --step-line-left: 0.9rem;
  927. --step-border-width: 0.2rem;
  928. .step-icon {
  929. width: var(--step-item-size);
  930. height: var(--step-item-size);
  931. border-radius: 50%;
  932. border: var(--step-border-width) solid #5faaf4;
  933. box-sizing: border-box;
  934. transition: 0.15s ease-out;
  935. display: flex;
  936. justify-content: center;
  937. align-items: center;
  938. .step-icon-inner {
  939. width: var(--step-icon-size);
  940. height: var(--step-icon-size);
  941. background-size: 100% 100%;
  942. }
  943. }
  944. :deep(.el-step__head) {
  945. width: var(--step-item-size);
  946. .el-step__line {
  947. width: var(--step-border-width);
  948. top: var(--step-item-size);
  949. left: var(--step-line-left);
  950. background-color: #5faaf4;
  951. }
  952. .el-step__icon {
  953. width: unset;
  954. height: unset;
  955. background: transparent;
  956. display: flex;
  957. }
  958. &.is-process .step-icon-inner {
  959. background-image: url(/src/assets/student/training/step_process.png);
  960. }
  961. &.is-finish .step-icon-inner {
  962. background-image: url(/src/assets/student/training/step_finish.png);
  963. }
  964. }
  965. :deep(.el-step__main) {
  966. padding-left: 2rem;
  967. .el-step__title {
  968. font-size: var(--step-item-size);
  969. color: white;
  970. font-weight: unset;
  971. line-height: var(--step-item-size);
  972. cursor: pointer;
  973. &.is-process,
  974. &.is-finish {
  975. text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
  976. background: linear-gradient(180deg, #82ccdd 0%, #589cf2 100%);
  977. background-clip: text;
  978. -webkit-background-clip: text;
  979. -webkit-text-fill-color: transparent;
  980. }
  981. }
  982. .el-step__description {
  983. margin-left: -1.1rem;
  984. --step-item-size: 0.9rem;
  985. --step-icon-size: 0.6rem;
  986. .el-steps {
  987. margin: 0.8rem 0;
  988. }
  989. .el-step__head {
  990. width: var(--step-icon-size);
  991. &.is-process .step-icon-second {
  992. background-color: #5ff49b;
  993. }
  994. &.is-finish .step-icon-second {
  995. background-color: #5fa9f4;
  996. }
  997. }
  998. .el-step__icon {
  999. height: var(--step-item-size);
  1000. line-height: var(--step-item-size);
  1001. }
  1002. .step-icon-second {
  1003. width: var(--step-icon-size);
  1004. height: var(--step-icon-size);
  1005. border-radius: 50%;
  1006. box-sizing: border-box;
  1007. background-color: #ccc;
  1008. }
  1009. .el-step__line {
  1010. width: 0.1rem;
  1011. left: 0.25rem;
  1012. top: 0.6rem;
  1013. bottom: -0.4rem;
  1014. margin-bottom: unset !important;
  1015. &:after {
  1016. display: none;
  1017. }
  1018. }
  1019. .el-step:last-of-type .el-step__line {
  1020. display: none;
  1021. }
  1022. .el-step__main {
  1023. padding-left: 0.6rem;
  1024. }
  1025. .el-step__title {
  1026. font-size: var(--step-item-size);
  1027. &.is-process {
  1028. color: #5ff49b;
  1029. -webkit-text-fill-color: unset;
  1030. }
  1031. &.is-finish {
  1032. color: #5fa9f4;
  1033. -webkit-text-fill-color: unset;
  1034. }
  1035. }
  1036. }
  1037. }
  1038. :deep(.el-step:last-of-type) {
  1039. .el-step__line {
  1040. display: block;
  1041. margin-bottom: 1rem;
  1042. &:after {
  1043. content: '';
  1044. position: absolute;
  1045. border: 0.8rem solid transparent;
  1046. border-top-color: #5faaf4;
  1047. bottom: -1rem;
  1048. left: -0.7rem;
  1049. }
  1050. .el-step__line-inner {
  1051. transition-delay: 150ms;
  1052. border-width: 0px;
  1053. height: 0%;
  1054. }
  1055. }
  1056. }
  1057. }
  1058. }
  1059. .chapter-detail {
  1060. position: absolute;
  1061. top: 3.2rem;
  1062. left: 28rem;
  1063. width: 88rem;
  1064. height: 56rem;
  1065. max-height: calc(100% - 12rem);
  1066. background-image: url(/src/assets/student/training/window_bg.png);
  1067. background-size: 100% 100%;
  1068. .detail-title {
  1069. height: 18%;
  1070. font-size: 3rem;
  1071. text-align: center;
  1072. color: white;
  1073. display: flex;
  1074. justify-content: center;
  1075. align-items: center;
  1076. }
  1077. .detail-cnt {
  1078. height: 76%;
  1079. padding: 0 5rem;
  1080. box-sizing: border-box;
  1081. overflow: auto;
  1082. :deep(img) {
  1083. max-width: 100%;
  1084. }
  1085. }
  1086. }
  1087. }
  1088. }
  1089. </style>