trainChapter.vue 32 KB

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