index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  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.progress?newData.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 ==1 ?'colorB':'')">{{item.learnStatus==2?'已学完':(item.learnStatus==1?'学习中':'未学完')}}</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. //当前选中
  87. checkLearningIndex:0,
  88. //学习计时器
  89. studyTimer: null,
  90. //验证计时器
  91. countdownTimer:null,
  92. //验证器弹窗状态
  93. dialogVisible:false,
  94. //验证器弹窗间隔时间
  95. verificationTime:0,
  96. //心跳回报时间
  97. heartbeatValue:30,
  98. }
  99. },
  100. created() {
  101. },
  102. mounted() {
  103. this.initialization();
  104. },
  105. methods: {
  106. //初始化
  107. initialization(){
  108. this.$set(this,'courseId',this.$route.query.courseId)
  109. this.$set(this,'examId',this.$route.query.examId)
  110. this.$nextTick(()=>{
  111. this.examUserCourseLearningCoursewareList();
  112. })
  113. },
  114. //获取课程下的课件列表
  115. examUserCourseLearningCoursewareList(){
  116. examUserCourseLearningCoursewareList({ courseId:this.courseId,examId:this.examId }).then(response => {
  117. this.$set(this, 'newData', response.data)
  118. if(response.data.popupVerifyEnabled == 1){
  119. if (this.countdown) {
  120. this.countdown.clear();
  121. this.countdown = null;
  122. }
  123. this.$set(this,'verificationTime',this.accMul(response.data.popupIntervalMinutes,60));
  124. this.startCountdown();//验证码倒计时
  125. }
  126. this.initTimer();//学习计时器
  127. this.courseRendering(this.checkLearningIndex);//课件渲染
  128. })
  129. },
  130. //返回
  131. backPage() {
  132. this.$router.back()
  133. },
  134. //完成课件学习
  135. finishStudying(index){
  136. let self = this;
  137. this.$confirm('确认完成课件学习?', "提示", {
  138. confirmButtonText: "确认",
  139. cancelButtonText: "取消",
  140. type: "warning"
  141. }).then(function() {
  142. }).then(() => {
  143. self.examUserCoursewareLearningConfirmCompletion();
  144. }).catch(() => {});
  145. },
  146. //切换课件
  147. checkLearningButton(index){
  148. let self = this;
  149. if(this.newData.coursewares[this.checkLearningIndex].learnStatus == 2){
  150. self.$set(self,'checkLearningIndex',index);
  151. self.initTimer();
  152. self.courseRendering(index);
  153. }else{
  154. self.handleToggle();
  155. self.verificationHandleToggle();
  156. if(this.checkLearningIndex != index){
  157. this.$confirm('确认切换课件?', "提示", {
  158. confirmButtonText: "确认",
  159. cancelButtonText: "取消",
  160. type: "warning"
  161. }).then(function() {
  162. }).then(() => {
  163. self.$set(self,'checkLearningIndex',index);
  164. self.initTimer();
  165. self.verificationHandleToggle();
  166. self.courseRendering(index);
  167. }).catch(() => {
  168. self.handleToggle();
  169. self.verificationHandleToggle();
  170. });
  171. }
  172. }
  173. },
  174. //课件渲染
  175. courseRendering(index){
  176. let name = this.newData.coursewares[index].coursewareName;
  177. let url = '';
  178. let type = '';
  179. if(this.newData.coursewares[index].materialType == 1){
  180. //视频
  181. type = 'video'
  182. url = this.newData.coursewares[index].attachmentPath;
  183. }else if(this.newData.coursewares[index].materialType == 3){
  184. //文档
  185. if(this.newData.coursewares[index].attachmentType == '.docx'){
  186. type = 'docx'
  187. }else if(this.newData.coursewares[index].attachmentType == '.xlsx'){
  188. type = 'excel'
  189. }else if(this.newData.coursewares[index].attachmentType == '.pdf'){
  190. type = 'pdf'
  191. }
  192. url = this.newData.coursewares[index].attachmentPath;
  193. }else if(this.newData.coursewares[index].materialType == 4){
  194. //文章
  195. type = 'text'
  196. url = this.newData.coursewares[index].articleContent;
  197. }
  198. this.$refs['coursewareComponent'].initialize(name,url,type);
  199. if(this.newData.coursewares[this.checkLearningIndex].learnStatus == 0){
  200. this.$set(this.newData.coursewares[this.checkLearningIndex],'learnStatus',1);
  201. }
  202. },
  203. //学习完成
  204. examUserCoursewareLearningConfirmCompletion(){
  205. let self = this;
  206. let obj = {
  207. coursewareId:this.newData.coursewares[this.checkLearningIndex].coursewareId,
  208. courseId:this.courseId,
  209. examId:this.examId,
  210. }
  211. examUserCoursewareLearningConfirmCompletion(obj).then(response => {
  212. for(let i=0;i<self.newData.coursewares.length;i++){
  213. if(self.newData.coursewares[i].learnStatus!=2 && i!=this.checkLearningIndex){
  214. this.$set(this,'checkLearningIndex',i);
  215. break
  216. }
  217. }
  218. this.examUserCourseLearningCoursewareList();
  219. this.msgSuccess(response.message)
  220. })
  221. },
  222. //心跳接口
  223. examUserCoursewareLearningReportProgress(){
  224. let obj = {
  225. coursewareId:this.newData.coursewares[this.checkLearningIndex].coursewareId,
  226. courseId:this.courseId,
  227. examId:this.examId,
  228. duration:this.heartbeatValue,
  229. }
  230. examUserCoursewareLearningReportProgress(obj).then(response => {})
  231. },
  232. //倒计时时间换算
  233. formatTime(totalSeconds) {
  234. if (typeof totalSeconds !== 'number' || totalSeconds <= 0) {
  235. return '0秒';
  236. }
  237. const minutes = Math.floor(totalSeconds / 60);
  238. const seconds = totalSeconds % 60;
  239. if (minutes > 0 && seconds > 0) {
  240. return `${minutes}分${seconds}秒`;
  241. } else if (minutes > 0 && seconds === 0) {
  242. return `${minutes}分`;
  243. } else {
  244. return `${seconds}秒`;
  245. }
  246. },
  247. //展示时间换算
  248. secondsToMinutes(totalSeconds) {
  249. if (typeof totalSeconds !== 'number' || totalSeconds <= 0) {
  250. return 1;
  251. }
  252. return Math.ceil(totalSeconds / 60);
  253. },
  254. /*=================================== 验证器 ===================================*/
  255. onVerifySuccess() {
  256. this.$message.success('验证成功');
  257. this.$set(this,'dialogVisible',false);
  258. this.handleToggle();
  259. this.startCountdown();
  260. //如果是视频课件 则继续播放视频
  261. if(this.newData.coursewares[this.checkLearningIndex].materialType == 1){
  262. this.$refs['coursewareComponent'].resumeVideo();
  263. }
  264. },
  265. /*=================================== 学习定时器 ===================================*/
  266. // 初始化定时器
  267. initTimer() {
  268. let self = this;
  269. if(this.newData.coursewares[this.checkLearningIndex].learnedDuration == this.newData.coursewares[this.checkLearningIndex].minLearningDuration){
  270. return
  271. }
  272. this.studyTimer = new StudyTimer({
  273. initialSeconds: self.newData.coursewares[self.checkLearningIndex].learnedDuration,
  274. maxSeconds: self.newData.coursewares[self.checkLearningIndex].minLearningDuration,
  275. onTick: (elapsedSeconds) => {
  276. //每次计时
  277. this.$set(self.newData.coursewares[self.checkLearningIndex],'learnedDuration',elapsedSeconds);
  278. //心跳判断
  279. if (elapsedSeconds % this.heartbeatValue === 0) {
  280. //调用心跳方法
  281. this.examUserCoursewareLearningReportProgress();
  282. }
  283. },
  284. onMaxTimeReached: (elapsedSeconds) => {
  285. //达到最大值
  286. this.$set(self.newData.coursewares[self.checkLearningIndex],'learnedDuration',elapsedSeconds);
  287. //调用心跳方法
  288. this.examUserCoursewareLearningReportProgress();
  289. }
  290. });
  291. this.studyTimer.resume();
  292. },
  293. // 切换暂停/继续
  294. handleToggle() {
  295. if (!this.studyTimer) return;
  296. if (this.studyTimer.isRunning) {
  297. this.studyTimer.pause();
  298. } else {
  299. this.studyTimer.resume();
  300. }
  301. },
  302. // 清除定时器
  303. clearTimer() {
  304. if (this.studyTimer) {
  305. this.studyTimer.clear();
  306. this.studyTimer = null;
  307. }
  308. },
  309. /*=================================== 验证定时器 ===================================*/
  310. // 1. 启动倒计时
  311. startCountdown() {
  312. if(this.verificationTime == 0){
  313. return
  314. }
  315. this.countdown = new CountdownTimer({
  316. targetSeconds: this.verificationTime,
  317. // 达到目标值时触发的回调
  318. onTargetReached: (elapsedSeconds) => {
  319. //倒计时结束 开启弹窗
  320. this.handleToggle();
  321. this.verificationHandleToggle();
  322. this.$set(this,'dialogVisible',true);
  323. //如果是视频课件 则暂停播放视频
  324. if(this.newData.coursewares[this.checkLearningIndex].materialType == 1){
  325. this.$refs['coursewareComponent'].pauseVideo();
  326. }
  327. }
  328. });
  329. if (this.countdown) {
  330. this.countdown.start();
  331. }
  332. },
  333. // 2. 暂停/继续
  334. verificationHandleToggle() {
  335. if (!this.countdown) return;
  336. this.countdown.toggle();
  337. },
  338. // 3. 清除倒计时
  339. clearCountdown() {
  340. if (this.countdown) {
  341. this.countdown.clear();
  342. this.countdown = null;
  343. }
  344. },
  345. //乘法
  346. accMul(arg1,arg2){
  347. var m=0,s1=arg1.toString(),s2=arg2.toString();
  348. try{m+=s1.split(".")[1].length}catch(e){}
  349. try{m+=s2.split(".")[1].length}catch(e){}
  350. return Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m)
  351. },
  352. },
  353. beforeDestroy() {
  354. if (this.studyTimer) {
  355. this.studyTimer.destroy();
  356. this.studyTimer = null;
  357. }
  358. if (this.countdownTimer) {
  359. this.countdownTimer.destroy();
  360. this.countdownTimer = null;
  361. }
  362. },
  363. }
  364. </script>
  365. <style scoped lang="scss">
  366. .learningCourse {
  367. flex:1;
  368. display: flex;
  369. flex-direction: column;
  370. background:#FFFFFF;
  371. border-radius:10px;
  372. overflow: hidden;
  373. padding:0;
  374. box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
  375. margin:10px;
  376. ::v-deep .el-progress .el-progress__text{
  377. margin-left:10px;
  378. margin-top:1px;
  379. }
  380. .content-box{
  381. flex: 1;
  382. display: flex;
  383. overflow: hidden;
  384. .left-max-big-box{
  385. flex:1;
  386. display: flex;
  387. flex-direction: column;
  388. background-color: #dedede;
  389. margin:20px 20px;
  390. border-radius:10px;
  391. overflow: hidden;
  392. box-shadow: 0 2px 8px rgba(37, 50, 74, 0.12);
  393. border:1px solid #dedede;
  394. }
  395. .right-max-big-box{
  396. padding:20px 0;
  397. width:400px;
  398. display: flex;
  399. flex-direction: column;
  400. overflow: hidden;
  401. border-left: 1px solid #dedede;
  402. .title-p{
  403. font-size:16px;
  404. font-weight:700;
  405. line-height:30px;
  406. padding:0 20px;
  407. }
  408. .text-p{
  409. font-size:14px;
  410. line-height:20px;
  411. margin-bottom:20px;
  412. padding:0 20px;
  413. }
  414. .progress-box{
  415. width:400px;
  416. height:20px;
  417. display: flex;
  418. padding:0 20px;
  419. .progress-title-box{
  420. font-size:14px;
  421. line-height:20px;
  422. font-weight:700;
  423. }
  424. .progress-min-box{
  425. flex:1;
  426. padding:0 0 0 20px;
  427. .el-progress{
  428. margin-top:1px;
  429. }
  430. }
  431. }
  432. .learning-for-max-big-box{
  433. flex:1;
  434. display: flex;
  435. flex-direction: column;
  436. padding:0 20px;
  437. .for-max-big-box{
  438. margin-top:40px;
  439. .for-big-box{
  440. cursor: pointer;
  441. border-radius:6px;
  442. border:1px solid #666;
  443. padding:15px 10px;
  444. position: relative;
  445. p{
  446. line-height:30px;
  447. }
  448. p:nth-child(1){
  449. font-size:16px;
  450. font-weight:700;
  451. /*单行省略号*/
  452. display:block;
  453. overflow:hidden;
  454. text-overflow:ellipsis;
  455. white-space:nowrap;
  456. }
  457. p:nth-child(2){
  458. font-size:14px;
  459. }
  460. p:nth-child(3){
  461. font-size:14px;
  462. position: absolute;
  463. bottom: 16px;
  464. right: 16px;
  465. color:#fff;
  466. background-color: #999;
  467. border-radius:4px;
  468. width:60px;
  469. line-height:24px;
  470. text-align: center;
  471. }
  472. .colorA{
  473. background-color: #0183fa!important;
  474. color:#fff!important;
  475. }
  476. .colorB{
  477. background-color: #fff!important;
  478. border:1px solid #0183fa!important;
  479. color:#0183fa!important;
  480. }
  481. }
  482. .for-button{
  483. margin-top:20px;
  484. background-color: #13CE66;
  485. color:#fff;
  486. text-align: center;
  487. line-height:30px;
  488. border-radius:4px;
  489. cursor: pointer;
  490. font-size:14px;
  491. }
  492. .check-for-box{
  493. background-color: rgba(1,131,250,0.1);
  494. border:1px solid #0183fa!important;
  495. p{
  496. color:#0183fa!important;
  497. }
  498. }
  499. }
  500. }
  501. }
  502. }
  503. }
  504. </style>