index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. <!-- 学习课程 -->
  2. <template>
  3. <div class="learningCourse">
  4. <div class="page-container-top-max-big-box">
  5. <p class="top-1-p">开始学习</p>
  6. <p class="top-2-p" @click="backPage()">返回</p>
  7. </div>
  8. <div class="content-box">
  9. <div class="left-max-big-box">
  10. <coursewareComponent ref="coursewareComponent"></coursewareComponent>
  11. </div>
  12. <div class="right-max-big-box">
  13. <p class="title-p">{{newData.courseName}}</p>
  14. <div class="text-p" style="color:#666;">{{secondsToMinutes(newData.totalDuration)}}分钟丨{{newData.totalCreditHours}}学时丨{{newData.totalPoints}}积分</div>
  15. <div class="text-p">已学 {{newData.learnedCoursewareCount}} / 总 {{newData.totalCoursewareCount}} 课件</div>
  16. <div class="progress-box">
  17. <p class="progress-title-box">学习进度</p>
  18. <div class="progress-min-box">
  19. <el-progress :stroke-width="12" :percentage="newData.coursewares[0].progress?newData.coursewares[0].progress:0"></el-progress>
  20. </div>
  21. </div>
  22. <div class="learning-for-max-big-box scrollbar-box">
  23. <div class="for-max-big-box"
  24. v-for="(item,index) in newData.coursewares" :key="index">
  25. <div class="for-big-box" @click="checkLearningButton(index)"
  26. :class="checkLearningIndex == index ?'check-for-box':''">
  27. <p>{{item.coursewareName}}</p>
  28. <p>已学习:{{formatTime(item.learnedDuration)}} / {{formatTime(item.minLearningDuration)}}</p>
  29. <p :class="item.learnStatus ==2 ?'colorA':''">{{item.learnStatus==2?'已学完':'未学完'}}</p>
  30. </div>
  31. <p class="for-button" @click="finishStudying(index)"
  32. v-if="item.learnStatus != 2 && item.learnedDuration == item.minLearningDuration && checkLearningIndex == index">
  33. 若已完成学习,请点此确认
  34. </p>
  35. </div>
  36. </div>
  37. </div>
  38. </div>
  39. <el-dialog
  40. title="请完成安全验证"
  41. :visible.sync="dialogVisible"
  42. width="340px"
  43. :close-on-click-modal="false"
  44. :show-close="false"
  45. append-to-body
  46. center>
  47. <div class="verify-container">
  48. <VerifySlider
  49. v-if="dialogVisible"
  50. tips="请按住滑块,拖动到最后边"
  51. success-tips="验证通过"
  52. @success="onVerifySuccess" />
  53. </div>
  54. </el-dialog>
  55. </div>
  56. </template>
  57. <script>
  58. import StudyTimer from '@/utils/timerUtils/StudyTimer';
  59. import CountdownTimer from '@/utils/timerUtils/CountdownTimer';
  60. import { VerifySlider } from 'vue-verify-slider';
  61. import coursewareComponent from "@/views/safetyEducationExaminationNew/components/coursewareComponent.vue";
  62. import {
  63. examUserCourseLearningCoursewareList,examUserCoursewareLearningReportProgress,
  64. examUserCoursewareLearningConfirmCompletion,examUserCoursewareLearningRestart
  65. } from "@/api/safetyEducationExaminationNew/index";
  66. export default {
  67. name: 'index',
  68. components: {
  69. VerifySlider,
  70. coursewareComponent,
  71. },
  72. data() {
  73. return {
  74. examId:null,
  75. courseId:null,
  76. newData:{
  77. courseName:"",
  78. totalDuration:0,
  79. totalCreditHours:0,
  80. totalPoints:0,
  81. learnedCoursewareCount:0,
  82. totalCoursewareCount:0,
  83. progress:0,
  84. coursewares:[
  85. {
  86. coursewareName:'视频课件名称',
  87. listData2:1,
  88. learnedDuration:590,
  89. minLearningDuration:600,
  90. learnStatus:2,
  91. attachmentPath:'http://192.168.1.8/statics/bigFile/20260801/0731dbcb-622b-4468-ade2-e20820b122eb.mp4',
  92. attachmentType:'mp4',
  93. materialType:'1',
  94. articleContent:'',
  95. },
  96. {
  97. coursewareName:'docx课件名称',
  98. listData2:1,
  99. learnedDuration:600,
  100. minLearningDuration:600,
  101. learnStatus:0,
  102. attachmentPath:'http://192.168.1.8/statics/2026/08/01/b68f1b1d-99ea-44e0-94b5-4bad4ac3ff1f.docx',
  103. attachmentType:'.docx',
  104. materialType:'3',
  105. articleContent:'',
  106. },
  107. {
  108. coursewareName:'xlsx课件名称',
  109. listData2:1,
  110. learnedDuration:600,
  111. minLearningDuration:600,
  112. learnStatus:1,
  113. attachmentPath:'http://192.168.1.8/statics/2026/08/01/270eb79e-627b-4fdf-9f1a-397bd9784d39.xlsx',
  114. attachmentType:'.xlsx',
  115. materialType:'3',
  116. articleContent:'',
  117. },
  118. {
  119. coursewareName:'pdf课件名称',
  120. listData2:1,
  121. learnedDuration:600,
  122. minLearningDuration:600,
  123. learnStatus:1,
  124. attachmentPath:'http://192.168.1.8/statics/2026/08/01/b251bd69-120c-48d5-b662-c28e308cd61b.pdf',
  125. attachmentType:'.pdf',
  126. materialType:'3',
  127. articleContent:'',
  128. },
  129. {
  130. coursewareName:'富文本课件名称',
  131. listData2:1,
  132. learnedDuration:600,
  133. minLearningDuration:600,
  134. learnStatus:1,
  135. attachmentPath:'',
  136. attachmentType:'',
  137. materialType:'4',
  138. articleContent:"<p><img style=\"max-width: 100%;\" src=\"../statics/2026/08/01/e2788e82-745c-4b49-82e0-ebb6f2a30713.jpeg\" alt=\"1.jpeg\">&nbsp;</p>"
  139. },
  140. {
  141. coursewareName:'xlsx课件名称',
  142. listData2:1,
  143. learnedDuration:600,
  144. minLearningDuration:600,
  145. learnStatus:1,
  146. attachmentPath:'http://192.168.1.8/statics/2026/08/01/270eb79e-627b-4fdf-9f1a-397bd9784d39.xlsx',
  147. attachmentType:'.xlsx',
  148. materialType:'3',
  149. articleContent:'',
  150. },
  151. ],
  152. },
  153. //当前选中
  154. checkLearningIndex:0,
  155. //学习计时器
  156. studyTimer: null,
  157. //验证计时器
  158. countdownTimer:null,
  159. //验证器弹窗状态
  160. dialogVisible:false,
  161. //验证器弹窗间隔时间
  162. verificationTime:0,
  163. //心跳回报时间
  164. heartbeatValue:30,
  165. }
  166. },
  167. created() {
  168. },
  169. mounted() {
  170. this.initialization();
  171. },
  172. methods: {
  173. //初始化
  174. initialization(){
  175. this.$set(this,'courseId',this.$route.params.courseId)
  176. this.$set(this,'examId',this.$route.params.examId)
  177. this.$nextTick(()=>{
  178. this.examUserCourseLearningCoursewareList();
  179. })
  180. },
  181. //获取课程下的课件列表
  182. examUserCourseLearningCoursewareList(){
  183. examUserCourseLearningCoursewareList({ courseId:this.courseId,examId:this.examId }).then(response => {
  184. this.$set(this, 'newData', response.data)
  185. if(response.data.popupVerifyEnabled == 1){
  186. if (this.countdown) {
  187. this.countdown.clear();
  188. this.countdown = null;
  189. }
  190. this.$set(this,'verificationTime',this.accMul(response.data.popupIntervalMinutes,60));
  191. this.startCountdown();//验证码倒计时
  192. }
  193. this.initTimer();//学习计时器
  194. this.courseRendering(this.checkLearningIndex);//课件渲染
  195. })
  196. },
  197. //返回
  198. backPage() {
  199. this.$router.back()
  200. },
  201. //完成课件学习
  202. finishStudying(index){
  203. let self = this;
  204. this.$confirm('确认完成课件学习?', "警告", {
  205. confirmButtonText: "确认",
  206. cancelButtonText: "取消",
  207. type: "warning"
  208. }).then(function() {
  209. }).then(() => {
  210. self.examUserCoursewareLearningConfirmCompletion();
  211. }).catch(() => {});
  212. },
  213. //切换课件
  214. checkLearningButton(index){
  215. let self = this;
  216. if(this.newData.coursewares[this.checkLearningIndex].learnStatus == 2){
  217. self.$set(self,'checkLearningIndex',index);
  218. self.initTimer();
  219. self.courseRendering(index);
  220. }else{
  221. self.handleToggle();
  222. self.verificationHandleToggle();
  223. if(this.checkLearningIndex != index){
  224. this.$confirm('确认切换课件?', "警告", {
  225. confirmButtonText: "确认",
  226. cancelButtonText: "取消",
  227. type: "warning"
  228. }).then(function() {
  229. }).then(() => {
  230. self.$set(self,'checkLearningIndex',index);
  231. self.initTimer();
  232. self.verificationHandleToggle();
  233. self.courseRendering(index);
  234. }).catch(() => {
  235. self.handleToggle();
  236. self.verificationHandleToggle();
  237. });
  238. }
  239. }
  240. },
  241. //课件渲染
  242. courseRendering(index){
  243. let name = this.newData.coursewares[index].coursewareName;
  244. let url = '';
  245. let type = '';
  246. if(this.newData.coursewares[index].materialType == 1){
  247. //视频
  248. type = 'video'
  249. url = this.newData.coursewares[index].attachmentPath;
  250. }else if(this.newData.coursewares[index].materialType == 3){
  251. //文档
  252. if(this.newData.coursewares[index].attachmentType == '.docx'){
  253. type = 'docx'
  254. }else if(this.newData.coursewares[index].attachmentType == '.xlsx'){
  255. type = 'excel'
  256. }else if(this.newData.coursewares[index].attachmentType == '.pdf'){
  257. type = 'pdf'
  258. }
  259. url = this.newData.coursewares[index].attachmentPath;
  260. }else if(this.newData.coursewares[index].materialType == 4){
  261. //文章
  262. type = 'text'
  263. url = this.newData.coursewares[index].articleContent;
  264. }
  265. this.$refs['coursewareComponent'].initialize(name,url,type);
  266. },
  267. //学习完成
  268. examUserCoursewareLearningConfirmCompletion(){
  269. let self = this;
  270. let obj = {
  271. coursewareId:this.newData.coursewares[this.checkLearningIndex].coursewareId,
  272. courseId:this.courseId,
  273. examId:this.examId,
  274. }
  275. examUserCoursewareLearningConfirmCompletion(obj).then(response => {
  276. for(let i=0;i<self.newData.coursewares.length;i++){
  277. if(self.newData.coursewares[i].learnStatus!=2 && i!=this.checkLearningIndex){
  278. this.$set(this,'checkLearningIndex',i);
  279. break
  280. }
  281. }
  282. this.examUserCourseLearningCoursewareList();
  283. this.msgSuccess(response.message)
  284. })
  285. },
  286. //心跳接口
  287. examUserCoursewareLearningReportProgress(){
  288. let obj = {
  289. coursewareId:this.newData.coursewares[this.checkLearningIndex].coursewareId,
  290. courseId:this.courseId,
  291. examId:this.examId,
  292. duration:this.heartbeatValue,
  293. }
  294. examUserCoursewareLearningReportProgress(obj).then(response => {})
  295. },
  296. //倒计时时间换算
  297. formatTime(totalSeconds) {
  298. if (typeof totalSeconds !== 'number' || totalSeconds <= 0) {
  299. return '0秒';
  300. }
  301. const minutes = Math.floor(totalSeconds / 60);
  302. const seconds = totalSeconds % 60;
  303. if (minutes > 0 && seconds > 0) {
  304. return `${minutes}分${seconds}秒`;
  305. } else if (minutes > 0 && seconds === 0) {
  306. return `${minutes}分`;
  307. } else {
  308. return `${seconds}秒`;
  309. }
  310. },
  311. //展示时间换算
  312. secondsToMinutes(totalSeconds) {
  313. if (typeof totalSeconds !== 'number' || totalSeconds <= 0) {
  314. return 1;
  315. }
  316. return Math.ceil(totalSeconds / 60);
  317. },
  318. /*=================================== 验证器 ===================================*/
  319. onVerifySuccess() {
  320. this.$message.success('验证成功');
  321. this.$set(this,'dialogVisible',false);
  322. this.handleToggle();
  323. this.startCountdown();
  324. //如果是视频课件 则继续播放视频
  325. if(this.newData.coursewares[this.checkLearningIndex].materialType == 1){
  326. this.$refs['coursewareComponent'].resumeVideo();
  327. }
  328. },
  329. /*=================================== 学习定时器 ===================================*/
  330. // 初始化定时器
  331. initTimer() {
  332. let self = this;
  333. if(this.newData.coursewares[this.checkLearningIndex].learnedDuration == this.newData.coursewares[this.checkLearningIndex].minLearningDuration){
  334. return
  335. }
  336. this.studyTimer = new StudyTimer({
  337. initialSeconds: self.newData.coursewares[self.checkLearningIndex].learnedDuration,
  338. maxSeconds: self.newData.coursewares[self.checkLearningIndex].minLearningDuration,
  339. onTick: (elapsedSeconds) => {
  340. //每次计时
  341. this.$set(self.newData.coursewares[self.checkLearningIndex],'learnedDuration',elapsedSeconds);
  342. //心跳判断
  343. if (elapsedSeconds % this.heartbeatValue === 0) {
  344. //调用心跳方法
  345. this.examUserCoursewareLearningReportProgress();
  346. }
  347. },
  348. onMaxTimeReached: (elapsedSeconds) => {
  349. //达到最大值
  350. this.$set(self.newData.coursewares[self.checkLearningIndex],'learnedDuration',elapsedSeconds);
  351. //调用心跳方法
  352. this.examUserCoursewareLearningReportProgress();
  353. }
  354. });
  355. this.studyTimer.resume();
  356. },
  357. // 切换暂停/继续
  358. handleToggle() {
  359. if (!this.studyTimer) return;
  360. if (this.studyTimer.isRunning) {
  361. this.studyTimer.pause();
  362. } else {
  363. this.studyTimer.resume();
  364. }
  365. },
  366. // 清除定时器
  367. clearTimer() {
  368. if (this.studyTimer) {
  369. this.studyTimer.clear();
  370. this.studyTimer = null;
  371. }
  372. },
  373. /*=================================== 验证定时器 ===================================*/
  374. // 1. 启动倒计时
  375. startCountdown() {
  376. if(this.verificationTime == 0){
  377. return
  378. }
  379. this.countdown = new CountdownTimer({
  380. targetSeconds: this.verificationTime,
  381. // 达到目标值时触发的回调
  382. onTargetReached: (elapsedSeconds) => {
  383. //倒计时结束 开启弹窗
  384. this.handleToggle();
  385. this.verificationHandleToggle();
  386. this.$set(this,'dialogVisible',true);
  387. //如果是视频课件 则暂停播放视频
  388. if(this.newData.coursewares[this.checkLearningIndex].materialType == 1){
  389. this.$refs['coursewareComponent'].pauseVideo();
  390. }
  391. }
  392. });
  393. if (this.countdown) {
  394. this.countdown.start();
  395. }
  396. },
  397. // 2. 暂停/继续
  398. verificationHandleToggle() {
  399. if (!this.countdown) return;
  400. this.countdown.toggle();
  401. },
  402. // 3. 清除倒计时
  403. clearCountdown() {
  404. if (this.countdown) {
  405. this.countdown.clear();
  406. this.countdown = null;
  407. }
  408. },
  409. //乘法
  410. accMul(arg1,arg2){
  411. var m=0,s1=arg1.toString(),s2=arg2.toString();
  412. try{m+=s1.split(".")[1].length}catch(e){}
  413. try{m+=s2.split(".")[1].length}catch(e){}
  414. return Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m)
  415. },
  416. },
  417. beforeDestroy() {
  418. if (this.studyTimer) {
  419. this.studyTimer.destroy();
  420. this.studyTimer = null;
  421. }
  422. if (this.countdownTimer) {
  423. this.countdownTimer.destroy();
  424. this.countdownTimer = null;
  425. }
  426. },
  427. }
  428. </script>
  429. <style scoped lang="scss">
  430. .learningCourse {
  431. flex:1;
  432. display: flex;
  433. flex-direction: column;
  434. background:#FFFFFF;
  435. border-radius:10px;
  436. overflow: hidden;
  437. padding:0;
  438. box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
  439. margin:10px;
  440. ::v-deep .el-progress .el-progress__text{
  441. margin-left:10px;
  442. margin-top:1px;
  443. }
  444. .content-box{
  445. flex: 1;
  446. display: flex;
  447. overflow: hidden;
  448. .left-max-big-box{
  449. flex:1;
  450. display: flex;
  451. flex-direction: column;
  452. background-color: #dedede;
  453. margin:20px 20px;
  454. border-radius:10px;
  455. overflow: hidden;
  456. box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
  457. border:1px solid #dedede;
  458. }
  459. .right-max-big-box{
  460. padding:20px 0;
  461. width:400px;
  462. display: flex;
  463. flex-direction: column;
  464. overflow: hidden;
  465. border-left: 1px solid #dedede;
  466. .title-p{
  467. font-size:16px;
  468. font-weight:700;
  469. line-height:30px;
  470. padding:0 20px;
  471. }
  472. .text-p{
  473. font-size:14px;
  474. line-height:20px;
  475. margin-bottom:20px;
  476. padding:0 20px;
  477. }
  478. .progress-box{
  479. width:400px;
  480. height:20px;
  481. display: flex;
  482. padding:0 20px;
  483. .progress-title-box{
  484. font-size:14px;
  485. line-height:20px;
  486. font-weight:700;
  487. }
  488. .progress-min-box{
  489. flex:1;
  490. padding:0 0 0 20px;
  491. .el-progress{
  492. margin-top:1px;
  493. }
  494. }
  495. }
  496. .learning-for-max-big-box{
  497. flex:1;
  498. display: flex;
  499. flex-direction: column;
  500. padding:0 20px;
  501. .for-max-big-box{
  502. margin-top:40px;
  503. .for-big-box{
  504. cursor: pointer;
  505. border-radius:6px;
  506. border:1px solid #666;
  507. padding:15px 10px;
  508. position: relative;
  509. p{
  510. line-height:30px;
  511. }
  512. p:nth-child(1){
  513. font-size:16px;
  514. font-weight:700;
  515. /*单行省略号*/
  516. display:block;
  517. overflow:hidden;
  518. text-overflow:ellipsis;
  519. white-space:nowrap;
  520. }
  521. p:nth-child(2){
  522. font-size:14px;
  523. }
  524. p:nth-child(3){
  525. font-size:14px;
  526. position: absolute;
  527. bottom: 16px;
  528. right: 16px;
  529. color:#fff!important;
  530. background-color: #999;
  531. border-radius:4px;
  532. width:60px;
  533. line-height:24px;
  534. text-align: center;
  535. }
  536. .colorA{
  537. background-color: #0183fa!important;
  538. }
  539. }
  540. .for-button{
  541. margin-top:20px;
  542. background-color: #13CE66;
  543. color:#fff;
  544. text-align: center;
  545. line-height:30px;
  546. border-radius:4px;
  547. cursor: pointer;
  548. font-size:14px;
  549. }
  550. .check-for-box{
  551. background-color: rgba(1,131,250,0.1);
  552. border:1px solid #0183fa!important;
  553. p{
  554. color:#0183fa!important;
  555. }
  556. }
  557. }
  558. }
  559. }
  560. }
  561. }
  562. </style>