trainChapter.vue 37 KB

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