trainChapter.vue 34 KB

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