dedsudiyu 22 hours ago
parent
commit
b43a9c26b4

+ 1 - 0
package.json

@@ -86,6 +86,7 @@
     "vue-router": "3.4.9",
     "vue-simple-uploader": "^0.7.6",
     "vue-ueditor-wrap": "^2.5.6",
+    "vue-verify-slider": "^2.0.0",
     "vue-video-player": "^5.0.2",
     "vuedraggable": "2.24.3",
     "vuex": "3.6.0",

+ 5 - 0
src/router/index.js

@@ -54,26 +54,31 @@ export const constantRoutes = [
     ]
   },
   {
+    name:'learningCourse',
     path: '/learningCourse',//学习课程
     component: (resolve) => require(['@/views/safetyEducationExaminationNew/featurePage/learningCourse/index.vue'], resolve),
     hidden: true
   },
   {
+    name:'learningMaterials',
     path: '/learningMaterials',//学习课件
     component: (resolve) => require(['@/views/safetyEducationExaminationNew/featurePage/learningMaterials/index.vue'], resolve),
     hidden: true
   },
   {
+    name:'startTheExam',
     path: '/startTheExam',//开始考试
     component: (resolve) => require(['@/views/safetyEducationExaminationNew/featurePage/startTheExam/index.vue'], resolve),
     hidden: true
   },
   {
+    name:'mockExam',
     path: '/mockExam',//模拟考试
     component: (resolve) => require(['@/views/safetyEducationExaminationNew/featurePage/mockExam/index.vue'], resolve),
     hidden: true
   },
   {
+    name:'startPracticing',
     path: '/startPracticing',//开始练习
     component: (resolve) => require(['@/views/safetyEducationExaminationNew/featurePage/startPracticing/index.vue'], resolve),
     hidden: true

+ 154 - 0
src/utils/timerUtils/CountdownTimer.js

@@ -0,0 +1,154 @@
+/**
+ * 倒计时触发器工具类 (终极修复版)
+ */
+export default class CountdownTimer {
+  constructor(options) {
+    this.targetSeconds = options.targetSeconds || 0;
+    this.onTargetReached = options.onTargetReached || (() => {});
+
+    this.elapsedTime = 0;
+    this.isRunning = false;
+    this.isTriggered = false;
+    this.isPageVisible = true;
+    this.isManualPaused = false;
+    this.timerId = null;
+    this.startTimeStamp = 0;
+
+    this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
+    document.addEventListener('visibilitychange', this._handleVisibilityChange);
+  }
+
+  getElapsedSeconds() {
+    return this.elapsedTime;
+  }
+
+  /**
+   * 内部核心:启动定时器 (仅处理定时器引擎,不处理状态守卫)
+   */
+  _startEngine() {
+    this.startTimeStamp = Date.now() - this.elapsedTime * 1000;
+    this.timerId = setInterval(() => {
+      const now = Date.now();
+      const rawElapsed = (now - this.startTimeStamp) / 1000;
+      const newElapsed = Math.floor(rawElapsed);
+
+      if (rawElapsed >= this.targetSeconds) {
+        this.elapsedTime = this.targetSeconds;
+        this._trigger();
+      } else {
+        this.elapsedTime = newElapsed;
+      }
+    }, 1000);
+  }
+
+  /**
+   * 内部核心:停止定时器 (仅处理定时器引擎)
+   */
+  _stopEngine() {
+    clearInterval(this.timerId);
+    this.timerId = null;
+  }
+
+  /**
+   * 启动/继续计时
+   */
+  start() {
+    if (this.isTriggered || this.isRunning) return;
+    if (!this.isManualPaused && !this.isPageVisible) return;
+
+    this.isRunning = true;
+    this.isManualPaused = false;
+    this._startEngine();
+  }
+
+  /**
+   * 手动暂停计时
+   */
+  pause() {
+    if (!this.isRunning) return;
+    this.isRunning = false;
+    this.isManualPaused = true;
+    this._stopEngine();
+  }
+
+  /**
+   * ⚠️ 终极修复:切换暂停/继续状态(二合一)
+   * 直接操作底层状态,绕过 start/pause 的守卫逻辑,确保 100% 生效
+   */
+  toggle() {
+    if (this.isTriggered) return;
+
+    if (this.isRunning) {
+      // 强制暂停
+      this.isRunning = false;
+      this.isManualPaused = true;
+      this._stopEngine();
+    } else {
+      // 强制继续
+      this.isRunning = true;
+      this.isManualPaused = false;
+      this._startEngine();
+    }
+  }
+
+  resetAndStart(newOptions) {
+    this._stopEngine();
+    this.isRunning = false;
+    this.isManualPaused = false;
+
+    document.removeEventListener('visibilitychange', this._handleVisibilityChange);
+
+    this.targetSeconds = newOptions.targetSeconds || 0;
+    this.onTargetReached = newOptions.onTargetReached || (() => {});
+
+    this.elapsedTime = 0;
+    this.isTriggered = false;
+    this.isPageVisible = true;
+
+    document.addEventListener('visibilitychange', this._handleVisibilityChange);
+
+    this.isRunning = true;
+    this._startEngine();
+  }
+
+  clear() {
+    this._stopEngine();
+    this.isRunning = false;
+    this.isManualPaused = false;
+    this.elapsedTime = 0;
+    this.isTriggered = false;
+
+    document.removeEventListener('visibilitychange', this._handleVisibilityChange);
+    this.onTargetReached = null;
+  }
+
+  _trigger() {
+    this._stopEngine();
+    this.isRunning = false;
+    this.isManualPaused = false;
+    this.isTriggered = true;
+    this.onTargetReached(this.elapsedTime);
+  }
+
+  _handleVisibilityChange() {
+    if (this.isTriggered) return;
+
+    if (document.hidden) {
+      this.isPageVisible = false;
+      if (this.isRunning) {
+        this.isRunning = false;
+        this._stopEngine();
+      }
+    } else {
+      this.isPageVisible = true;
+      if (!this.isManualPaused && !this.isRunning) {
+        this.isRunning = true;
+        this._startEngine();
+      }
+    }
+  }
+
+  destroy() {
+    this.clear();
+  }
+}

+ 151 - 0
src/utils/timerUtils/StudyTimer.js

@@ -0,0 +1,151 @@
+/**
+ * 学习考试系统 - 纯逻辑定时器工具类
+ */
+export default class StudyTimer {
+  /**
+   * @param {Object} options 配置项
+   * @param {Number} options.initialSeconds - 计时起点(秒)
+   * @param {Number} options.maxSeconds - 最大时长(秒)
+   * @param {Function} options.onTick - 每秒回调,返回当前总耗时秒数
+   * @param {Function} options.onMaxTimeReached - 达到最大时长回调
+   */
+  constructor(options) {
+    this.initialSeconds = options.initialSeconds || 0;
+    this.maxSeconds = options.maxSeconds || Infinity;
+    this.onTick = options.onTick || (() => {});
+    this.onMaxTimeReached = options.onMaxTimeReached || (() => {});
+
+    // 内部状态
+    this.elapsedTime = this.initialSeconds;
+    this.isRunning = false;
+    this.isFinished = false;
+    this.isPageVisible = true; // 页面是否可见
+    this.timerId = null;
+    this.startTimeStamp = 0;
+
+    // 绑定切屏事件,确保 this 指向正确且方便后续移除
+    this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
+    document.addEventListener('visibilitychange', this._handleVisibilityChange);
+  }
+
+  /**
+   * 获取当前已耗时的秒数
+   */
+  getElapsedSeconds() {
+    return this.elapsedTime;
+  }
+
+  /**
+   * 启动/继续计时
+   */
+  resume() {
+    if (this.isRunning || this.isFinished || !this.isPageVisible) return;
+
+    this.isRunning = true;
+    // 核心:基于当前时间和已消耗时间,反推基准时间戳
+    this.startTimeStamp = Date.now() - this.elapsedTime * 1000;
+
+    this.timerId = setInterval(() => {
+      const now = Date.now();
+      const rawElapsed = (now - this.startTimeStamp) / 1000; // 获取精确的毫秒级秒数
+      const newElapsed = Math.floor(rawElapsed); // 向下取整
+
+      // ⚠️ 核心修复:强制兜底逻辑
+      // 只要实际耗时达到最大值,直接强制赋值,确保绝对能触发 maxSeconds
+      if (rawElapsed >= this.maxSeconds) {
+        this.elapsedTime = this.maxSeconds;
+        this._finish();
+      } else {
+        this.elapsedTime = newElapsed;
+        this.onTick(this.elapsedTime);
+      }
+    }, 1000);
+  }
+
+  /**
+   * 暂停计时(手动暂停 或 切屏暂停 均调用此方法)
+   */
+  pause() {
+    if (!this.isRunning) return;
+    this.isRunning = false;
+    clearInterval(this.timerId);
+    this.timerId = null;
+  }
+
+  /**
+   * ⚠️ 新增:重置并启动(用于多目标切换)
+   * @param {Object} newOptions - 新的配置项(同 constructor 的参数)
+   */
+  resetAndStart(newOptions) {
+    // 1. 停止当前计时
+    this.pause();
+
+    // 2. 移除旧的切屏监听(防止重复绑定)
+    document.removeEventListener('visibilitychange', this._handleVisibilityChange);
+
+    // 3. 更新配置和内部状态
+    this.initialSeconds = newOptions.initialSeconds || 0;
+    this.maxSeconds = newOptions.maxSeconds || Infinity;
+    this.onTick = newOptions.onTick || (() => {});
+    this.onMaxTimeReached = newOptions.onMaxTimeReached || (() => {});
+
+    this.elapsedTime = this.initialSeconds;
+    this.isFinished = false;
+    this.isPageVisible = true; // 重置页面可见性状态
+
+    // 4. 重新绑定切屏监听
+    document.addEventListener('visibilitychange', this._handleVisibilityChange);
+
+    // 5. 立即启动新计时
+    this.resume();
+  }
+
+  /**
+   * 彻底清除定时器并重置状态
+   */
+  clear() {
+    // 1. 停止计时
+    this.pause();
+    // 2. 重置状态
+    this.elapsedTime = this.initialSeconds;
+    this.isFinished = false;
+    // 3. 移除切屏监听
+    document.removeEventListener('visibilitychange', this._handleVisibilityChange);
+    // 4. 清空回调,帮助垃圾回收
+    this.onTick = null;
+    this.onMaxTimeReached = null;
+  }
+
+  /**
+   * 内部方法:达到最大值
+   */
+  _finish() {
+    this.pause();
+    this.isFinished = true;
+    this.onMaxTimeReached(this.elapsedTime);
+  }
+
+  /**
+   * 内部方法:切屏监听
+   */
+  _handleVisibilityChange() {
+    if (this.isFinished) return;
+
+    if (document.hidden) {
+      // 切屏:强制暂停
+      this.isPageVisible = false;
+      this.pause();
+    } else {
+      // 切回:自动恢复
+      this.isPageVisible = true;
+      this.resume();
+    }
+  }
+
+  /**
+   * 销毁方法(语义上用于组件卸载,内部调用 clear)
+   */
+  destroy() {
+    this.clear();
+  }
+}

+ 205 - 0
src/views/safetyEducationExaminationNew/components/coursewareComponent.vue

@@ -0,0 +1,205 @@
+<!-- 课件展示 -->
+<template>
+  <div class="coursewareComponent-addPage">
+    <div class="max-img-box" ref="viewBox" v-if="showType">
+      <transition name="fade" mode="in-out">
+        <vue-office-docx
+          v-if="officeType === 'docx'&&!officeNullType"
+          :src="lookUrl"
+          @rendered="renderedHandler"
+          @error="errorHandler"
+        />
+        <vue-office-excel
+          v-if="officeType === 'excel'&&!officeNullType"
+          :src="lookUrl"
+          @rendered="renderedHandler"
+          @error="errorHandler"
+        />
+        <vue-office-pdf
+          v-if="officeType === 'pdf'&&!officeNullType"
+          :src="lookUrl"
+          @rendered="renderedHandler"
+          @error="errorHandler"/>
+        <p v-if="officeNullType" class="office-null-text">加载失败...</p>
+        <video v-if="officeType === 'video'" ref="videoPlayer"
+               class="video-box" :src="lookUrl" autoplay  controls muted></video>
+        <div  v-if="officeType === 'text'" class="rich-text-box" v-html="lookUrl"></div>
+      </transition>
+    </div>
+  </div>
+</template>
+
+<script>
+  //引入VueOfficeDocx组件
+  import VueOfficeDocx from '@vue-office/docx'
+  import '@vue-office/docx/lib/index.css'
+  //引入VueOfficeExcel组件
+  import VueOfficeExcel from '@vue-office/excel'
+  import '@vue-office/excel/lib/index.css'
+  //引入VueOfficePdf组件
+  import VueOfficePdf from '@vue-office/pdf'
+  export default {
+    name: 'addPage',
+    components: {
+      VueOfficeDocx,
+      VueOfficeExcel,
+      VueOfficePdf,
+    },
+    props: {
+      coursewareComponentPropsData: {}
+    },
+    data() {
+      return {
+        showType:false,
+        officeNullType:false,
+        //文档类型
+        officeType:null,
+        //文档名称
+        lookName:"",
+        //文档地址
+        lookUrl:"",
+        currentPage: 1,
+        totalPages: 1,
+        currentScale: 1.0 // 初始缩放比例
+      }
+    },
+    created() {
+
+    },
+    mounted() {
+
+    },
+    methods: {
+      //初始化
+      initialize(name,url,type){
+        let self = this;
+        this.$set(this,'officeNullType',true);
+        this.$set(this,'showType',false);
+        this.$nextTick(()=>{
+          if(name&&url&&type){
+            this.$set(this,'officeType',type);
+            this.$set(this,'lookName',name);
+            this.$set(this,'lookUrl',url);
+            this.$set(this,'officeNullType',false);
+            this.$set(this,'showType',true);
+            // if(type == 'video'){
+            //   this.$nextTick(()=>{
+            //     const video = this.$refs.videoPlayer;
+            //     if (video) {
+            //       video.muted = false;
+            //       video.play().catch(err => {
+            //         console.warn('自动播放被拦截:', err);
+            //       });
+            //     }
+            //   })
+            // }
+          }else{
+            this.msgError('文件参数异常')
+          }
+        })
+      },
+      // 1. 暂停播放
+      pauseVideo() {
+        const player = this.$refs.videoPlayer;
+        if (player) {
+          player.pause();
+        }
+      },
+      // 2. 继续播放方法(如果已播完则不继续)
+      resumeVideo() {
+        const player = this.$refs.videoPlayer;
+        if (player) {
+          // 核心判断:如果视频已经播放结束(ended 为 true),则直接 return,不执行播放
+          if (player.ended) {
+            console.log('视频已播放完毕,不再继续');
+            return;
+          }
+          player.play();
+        }
+      },
+    }
+  }
+</script>
+
+<style scoped lang="scss">
+  .coursewareComponent-addPage {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+
+    .max-img-box{
+      flex:1;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+      position: relative;
+      .office-null-text{
+        line-height:700px;
+        text-align: center;
+        color:#dedede;
+        font-size:16px;
+      }
+      .video-box{
+        width:1460px;
+        height:759px;
+      }
+      .rich-text-box {
+        padding:20px 100px;
+        flex:1;
+        background-color: #fff;
+        line-height: 1.6; /* 基础行高 */
+        word-break: break-word; /* 防止长单词或长链接撑破布局 */
+      }
+
+      /* 穿透样式,控制富文本内部的标签 */
+      .rich-text-box ::v-deep img {
+        max-width: 100% !important; /* 防止图片超出容器 */
+        height: auto !important;
+        display: block;
+        margin: 10px auto;
+      }
+
+      .rich-text-box ::v-deep table {
+        width: 100%;
+        border-collapse: collapse;
+      }
+
+      .rich-text-box ::v-deep p {
+        margin-bottom: 10px;
+      }
+    }
+
+    //滚动条样式
+    .max-img-box::-webkit-scrollbar{
+      width: 6px;     /*高宽分别对应横竖滚动条的尺寸*/
+      height: 6px;
+    }
+    .max-img-box::-webkit-scrollbar-thumb{
+      border-radius: 5px;
+      background: #D3D7D4;
+    }
+    .max-img-box::-webkit-scrollbar-track{
+      -webkit-box-shadow: inset 0 0 5px #FFFFFF;
+      border-radius: 0;
+      background: #FFFFFF;
+    }
+    //滚动条样式
+    ::v-deep .ofd-footer{
+      padding:10px 0;
+    }
+    ::v-deep .ofd-body::-webkit-scrollbar{
+      width: 6px;     /*高宽分别对应横竖滚动条的尺寸*/
+      height: 6px;
+    }
+    ::v-deep .ofd-body::-webkit-scrollbar-thumb{
+      border-radius: 5px;
+      background: #D3D7D4;
+    }
+    ::v-deep .ofd-body::-webkit-scrollbar-track{
+      -webkit-box-shadow: inset 0 0 5px #FFFFFF;
+      border-radius: 0;
+      background: #FFFFFF;
+    }
+  }
+</style>

+ 445 - 5
src/views/safetyEducationExaminationNew/featurePage/learningCourse/index.vue

@@ -5,24 +5,164 @@
       <p class="top-1-p">开始学习</p>
       <p class="top-2-p" @click="backPage()">返回</p>
     </div>
-    <div class="content-box scrollbar-box">
-
+    <div class="content-box">
+      <div class="left-max-big-box">
+        <coursewareComponent ref="coursewareComponent"></coursewareComponent>
+      </div>
+      <div class="right-max-big-box">
+        <p class="title-p">{{learningData.data1}}</p>
+        <div class="text-p" style="color:#666;">{{learningData.data2}}分钟丨{{learningData.data3}}学时丨{{learningData.data4}}积分</div>
+        <div class="text-p">已学 {{learningData.data5}} / 总 {{learningData.data6}} 课件</div>
+        <div class="progress-box">
+          <p class="progress-title-box">学习进度</p>
+          <div class="progress-min-box">
+            <el-progress :stroke-width="12" :percentage="learningData.data7"></el-progress>
+          </div>
+        </div>
+        <div class="learning-for-max-big-box scrollbar-box">
+          <div class="for-max-big-box"
+               v-for="(item,index) in learningData.dataList" :key="index">
+            <div class="for-big-box" @click="checkLearningButton(index)"
+                 :class="checkLearningIndex == index ?'check-for-box':''">
+              <p>{{item.listData1}}</p>
+              <p>已学习:{{formatTime(item.listData3)}} / {{formatTime(item.listData4)}}</p>
+              <p :class="item.listData5 ==1 ?'colorA':''">{{item.listData5==1?'已学完':'未学完'}}</p>
+            </div>
+            <p class="for-button" @click="finishStudying(index)"
+               v-if="item.listData5 == 0 && item.listData3 == item.listData4 && checkLearningIndex == index">
+              若已完成学习,请点此确认
+            </p>
+          </div>
+        </div>
+      </div>
     </div>
+    <el-dialog
+      title="请完成安全验证"
+      :visible.sync="dialogVisible"
+      width="340px"
+      :close-on-click-modal="false"
+      :show-close="false"
+      append-to-body
+      center>
+      <div class="verify-container">
+        <VerifySlider
+          v-if="dialogVisible"
+          tips="请按住滑块,拖动到最后边"
+          success-tips="验证通过"
+          @success="onVerifySuccess" />
+      </div>
+    </el-dialog>
   </div>
 </template>
 <script>
+  import StudyTimer from '@/utils/timerUtils/StudyTimer';
+  import CountdownTimer from '@/utils/timerUtils/CountdownTimer';
+  import { VerifySlider } from 'vue-verify-slider';
+  import coursewareComponent from "@/views/safetyEducationExaminationNew/components/coursewareComponent.vue";
   export default {
     name: 'index',
+    components: {
+      VerifySlider,
+      coursewareComponent,
+    },
     data() {
       return {
-
+        learningData:{
+          data1:"课程名称",
+          data2:"20",
+          data3:"1",
+          data4:"3",
+          data5:"0",
+          data6:"2",
+          data7:20,
+          dataList:[
+            {
+              listData1:'视频课件名称',
+              listData2:1,
+              listData3:590,
+              listData4:600,
+              listData5:0,
+              listData6:'http://192.168.1.8/statics/bigFile/20260801/0731dbcb-622b-4468-ade2-e20820b122eb.mp4',
+              listData7:'mp4',
+              materialType:'1',
+              articleContent:'',
+            },
+            {
+              listData1:'docx课件名称',
+              listData2:1,
+              listData3:600,
+              listData4:600,
+              listData5:0,
+              listData6:'http://192.168.1.8/statics/2026/08/01/b68f1b1d-99ea-44e0-94b5-4bad4ac3ff1f.docx',
+              listData7:'.docx',
+              materialType:'3',
+              articleContent:'',
+            },
+            {
+              listData1:'xlsx课件名称',
+              listData2:1,
+              listData3:600,
+              listData4:600,
+              listData5:1,
+              listData6:'http://192.168.1.8/statics/2026/08/01/270eb79e-627b-4fdf-9f1a-397bd9784d39.xlsx',
+              listData7:'.xlsx',
+              materialType:'3',
+              articleContent:'',
+            },
+            {
+              listData1:'pdf课件名称',
+              listData2:1,
+              listData3:600,
+              listData4:600,
+              listData5:1,
+              listData6:'http://192.168.1.8/statics/2026/08/01/b251bd69-120c-48d5-b662-c28e308cd61b.pdf',
+              listData7:'.pdf',
+              materialType:'3',
+              articleContent:'',
+            },
+            {
+              listData1:'富文本课件名称',
+              listData2:1,
+              listData3:600,
+              listData4:600,
+              listData5:1,
+              listData6:'',
+              listData7:'',
+              materialType:'4',
+              articleContent:"<p><img style=\"max-width: 100%;\" src=\"../statics/2026/08/01/e2788e82-745c-4b49-82e0-ebb6f2a30713.jpeg\" alt=\"1.jpeg\">&nbsp;</p>"
+            },
+            {
+              listData1:'xlsx课件名称',
+              listData2:1,
+              listData3:600,
+              listData4:600,
+              listData5:1,
+              listData6:'http://192.168.1.8/statics/2026/08/01/270eb79e-627b-4fdf-9f1a-397bd9784d39.xlsx',
+              listData7:'.xlsx',
+              materialType:'3',
+              articleContent:'',
+            },
+          ],
+        },
+        //当前选中
+        checkLearningIndex:0,
+        //学习计时器
+        studyTimer: null,
+        //验证计时器
+        countdownTimer:null,
+        //验证器弹窗状态
+        dialogVisible:false,
+        //验证器弹窗间隔时间
+        verificationTime:120,
+        //心跳回报时间
+        heartbeatValue:30,
       }
     },
     created() {
 
     },
     mounted() {
-
+      this.initialization();
     },
     methods: {
       /*
@@ -35,11 +175,194 @@
       })
       this.$route.params.title
       */
+      //初始化
+      initialization(){
+
+        this.initTimer();
+        this.startCountdown();
+        this.courseRendering(0);
+      },
       //返回
       backPage() {
         this.$router.back()
       },
-    }
+      //完成课件学习
+      finishStudying(index){
+        let self = this;
+        this.$confirm('确认完成课件学习?', "警告", {
+          confirmButtonText: "确认",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+        }).then(() => {
+
+        }).catch(() => {});
+      },
+      //切换课件
+      checkLearningButton(index){
+        let self = this;
+        self.handleToggle();
+        self.verificationHandleToggle();
+        if(this.checkLearningIndex != index){
+          this.$confirm('确认切换课件?', "警告", {
+            confirmButtonText: "确认",
+            cancelButtonText: "取消",
+            type: "warning"
+          }).then(function() {
+          }).then(() => {
+            self.$set(self,'checkLearningIndex',index);
+            self.initTimer();
+            self.verificationHandleToggle();
+            self.courseRendering(index);
+          }).catch(() => {
+            self.handleToggle();
+            self.verificationHandleToggle();
+          });
+        }
+      },
+      //课件渲染
+      courseRendering(index){
+        let name = this.learningData.dataList[index].listData1;
+        let url = '';
+        let type = '';
+        if(this.learningData.dataList[index].materialType == 1){
+          //视频
+          type = 'video'
+          url = this.learningData.dataList[index].listData6;
+        }else if(this.learningData.dataList[index].materialType == 3){
+          //文档
+          if(this.learningData.dataList[index].listData7 == '.docx'){
+            type = 'docx'
+          }else if(this.learningData.dataList[index].listData7 == '.xlsx'){
+            type = 'excel'
+          }else if(this.learningData.dataList[index].listData7 == '.pdf'){
+            type = 'pdf'
+          }
+          url = this.learningData.dataList[index].listData6;
+        }else if(this.learningData.dataList[index].materialType == 4){
+          //文章
+          type = 'text'
+          url = this.learningData.dataList[index].articleContent;
+        }
+        this.$refs['coursewareComponent'].initialize(name,url,type);
+      },
+      //时间换算
+      formatTime(totalSeconds) {
+        if (typeof totalSeconds !== 'number' || totalSeconds <= 0) {
+          return '0秒';
+        }
+        const minutes = Math.floor(totalSeconds / 60);
+        const seconds = totalSeconds % 60;
+        if (minutes > 0 && seconds > 0) {
+          return `${minutes}分${seconds}秒`;
+        } else if (minutes > 0 && seconds === 0) {
+          return `${minutes}分`;
+        } else {
+          return `${seconds}秒`;
+        }
+      },
+      /*=================================== 验证器 ===================================*/
+      onVerifySuccess() {
+        this.$message.success('验证成功');
+        this.$set(this,'dialogVisible',false);
+        this.handleToggle();
+        this.startCountdown();
+        //如果是视频课件 则继续播放视频
+        if(this.learningData.dataList[this.checkLearningIndex].materialType == 1){
+          this.$refs['coursewareComponent'].resumeVideo();
+        }
+      },
+      /*=================================== 学习定时器 ===================================*/
+      // 初始化定时器
+      initTimer() {
+        let self = this;
+        if(this.learningData.dataList[this.checkLearningIndex].listData3 == this.learningData.dataList[this.checkLearningIndex].listData4){
+          return
+        }
+        this.studyTimer = new StudyTimer({
+          initialSeconds: self.learningData.dataList[self.checkLearningIndex].listData3,
+          maxSeconds: self.learningData.dataList[self.checkLearningIndex].listData4,
+          onTick: (elapsedSeconds) => {
+            //每次计时
+            this.$set(self.learningData.dataList[self.checkLearningIndex],'listData3',elapsedSeconds);
+            //心跳判断
+            if (elapsedSeconds % this.heartbeatValue === 0) {
+              //调用心跳方法
+
+            }
+          },
+          onMaxTimeReached: (elapsedSeconds) => {
+            //达到最大值
+            this.$set(self.learningData.dataList[self.checkLearningIndex],'listData3',elapsedSeconds);
+            //调用心跳方法
+
+          }
+        });
+        this.studyTimer.resume();
+      },
+      // 切换暂停/继续
+      handleToggle() {
+        if (!this.studyTimer) return;
+        if (this.studyTimer.isRunning) {
+          this.studyTimer.pause();
+        } else {
+          this.studyTimer.resume();
+        }
+      },
+      // 清除定时器
+      clearTimer() {
+        if (this.studyTimer) {
+          this.studyTimer.clear();
+          this.studyTimer = null;
+        }
+      },
+      /*=================================== 验证定时器 ===================================*/
+      // 1. 启动倒计时
+      startCountdown() {
+        if(this.verificationTime == 0){
+          return
+        }
+        this.countdown = new CountdownTimer({
+          targetSeconds: this.verificationTime,
+          // 达到目标值时触发的回调
+          onTargetReached: (elapsedSeconds) => {
+            //倒计时结束 开启弹窗
+            this.handleToggle();
+            this.verificationHandleToggle();
+            this.$set(this,'dialogVisible',true);
+            //如果是视频课件 则暂停播放视频
+            if(this.learningData.dataList[this.checkLearningIndex].materialType == 1){
+              this.$refs['coursewareComponent'].pauseVideo();
+            }
+          }
+        });
+        if (this.countdown) {
+          this.countdown.start();
+        }
+      },
+      // 2. 暂停/继续
+      verificationHandleToggle() {
+        if (!this.countdown) return;
+        this.countdown.toggle();
+      },
+      // 3. 清除倒计时
+      clearCountdown() {
+        if (this.countdown) {
+          this.countdown.clear();
+          this.countdown = null;
+        }
+      },
+    },
+    beforeDestroy() {
+      if (this.studyTimer) {
+        this.studyTimer.destroy();
+        this.studyTimer = null;
+      }
+      if (this.countdownTimer) {
+        this.countdownTimer.destroy();
+        this.countdownTimer = null;
+      }
+    },
   }
 </script>
 <style scoped lang="scss">
@@ -53,5 +376,122 @@
     padding:0;
     box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
     margin:10px;
+    .content-box{
+      flex: 1;
+      display: flex;
+      overflow: hidden;
+      .left-max-big-box{
+        flex:1;
+        display: flex;
+        flex-direction: column;
+        background-color: #dedede;
+        margin:20px 20px;
+        border-radius:10px;
+        overflow: hidden;
+        box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
+        border:1px solid #dedede;
+      }
+      .right-max-big-box{
+        padding:20px 0;
+        width:400px;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        border-left: 1px solid #dedede;
+        .title-p{
+          font-size:16px;
+          font-weight:700;
+          line-height:30px;
+          padding:0 20px;
+        }
+        .text-p{
+          font-size:14px;
+          line-height:20px;
+          margin-bottom:20px;
+          padding:0 20px;
+        }
+        .progress-box{
+          width:400px;
+          height:20px;
+          display: flex;
+          padding:0 20px;
+          .progress-title-box{
+            font-size:14px;
+            line-height:20px;
+            font-weight:700;
+          }
+          .progress-min-box{
+            flex:1;
+            padding:0 0 0 20px;
+            .el-progress{
+              margin-top:1px;
+            }
+          }
+        }
+        .learning-for-max-big-box{
+          flex:1;
+          display: flex;
+          flex-direction: column;
+          padding:0 20px;
+          .for-max-big-box{
+            margin-top:40px;
+            .for-big-box{
+              cursor: pointer;
+              border-radius:6px;
+              border:1px solid #666;
+              padding:15px 10px;
+              position: relative;
+              p{
+                line-height:30px;
+              }
+              p:nth-child(1){
+                font-size:16px;
+                font-weight:700;
+                /*单行省略号*/
+                display:block;
+                overflow:hidden;
+                text-overflow:ellipsis;
+                white-space:nowrap;
+              }
+              p:nth-child(2){
+                font-size:14px;
+              }
+              p:nth-child(3){
+                font-size:14px;
+                position: absolute;
+                bottom: 16px;
+                right: 16px;
+                color:#fff!important;
+                background-color: #999;
+                border-radius:4px;
+                width:60px;
+                line-height:24px;
+                text-align: center;
+              }
+              .colorA{
+                background-color: #0183fa!important;
+              }
+            }
+            .for-button{
+              margin-top:20px;
+              background-color: #13CE66;
+              color:#fff;
+              text-align: center;
+              line-height:30px;
+              border-radius:4px;
+              cursor: pointer;
+              font-size:14px;
+            }
+            .check-for-box{
+              background-color: rgba(1,131,250,0.1);
+              border:1px solid #0183fa!important;
+              p{
+                color:#0183fa!important;
+              }
+            }
+          }
+        }
+      }
+    }
   }
 </style>

+ 225 - 1
src/views/safetyEducationExaminationNew/featurePage/learningMaterials/index.vue

@@ -5,8 +5,36 @@
       <p class="top-1-p">开始学习</p>
       <p class="top-2-p" @click="backPage()">返回</p>
     </div>
-    <div class="content-box scrollbar-box">
+    <div class="content-box">
+      <div class="left-max-big-box">
 
+      </div>
+      <div class="right-max-big-box">
+        <p class="title-p">{{learningData.data1}}</p>
+        <div class="text-p" style="color:#666;">{{learningData.data2}}分钟丨{{learningData.data3}}学时丨{{learningData.data4}}积分</div>
+        <div class="text-p">已学 {{learningData.data5}} / 总 {{learningData.data6}} 课件</div>
+        <div class="progress-box">
+          <p class="progress-title-box">学习进度</p>
+          <div class="progress-min-box">
+            <el-progress :stroke-width="12" :percentage="learningData.data7"></el-progress>
+          </div>
+        </div>
+        <div class="learning-for-max-big-box scrollbar-box">
+          <div class="for-max-big-box"
+               v-for="(item,index) in learningData.dataList" :key="index">
+            <div class="for-big-box" @click="checkLearningButton(index)"
+                 :class="checkLearningIndex == index ?'check-for-box':''">
+              <p>{{item.listData1}}</p>
+              <p>已学习:{{formatTime(item.listData3)}} / {{formatTime(item.listData4)}}</p>
+              <p :class="item.listData5 ==1 ?'colorA':''">{{item.listData5==1?'已学完':'未学完'}}</p>
+            </div>
+            <p class="for-button" @click="finishStudying(index)"
+               v-if="item.listData5 == 0 && item.listData3 == item.listData4 && checkLearningIndex == index">
+              若已完成学习,请点此确认
+            </p>
+          </div>
+        </div>
+      </div>
     </div>
   </div>
 </template>
@@ -15,6 +43,39 @@
     name: 'index',
     data() {
       return {
+        learningData:{
+          data1:"课程名称",
+          data2:"20",
+          data3:"1",
+          data4:"3",
+          data5:"0",
+          data6:"2",
+          data7:"20",
+          dataList:[
+            {
+              listData1:'课件名称',
+              listData2:1,
+              listData3:320,
+              listData4:600,
+              listData5:0,
+            },
+            {
+              listData1:'课件名称',
+              listData2:1,
+              listData3:600,
+              listData4:600,
+              listData5:0,
+            },
+            {
+              listData1:'课件名称',
+              listData2:1,
+              listData3:600,
+              listData4:600,
+              listData5:1,
+            },
+          ],
+        },
+        checkLearningIndex:0,
 
       }
     },
@@ -25,10 +86,61 @@
 
     },
     methods: {
+      /*
+      this.$router.push({
+        name: 'Detail',
+        params: {
+          id: 1,
+          title: '消息标题'
+        }
+      })
+      this.$route.params.title
+      */
       //返回
       backPage() {
         this.$router.back()
       },
+      //完成课件学习
+      finishStudying(index){
+        let self = this;
+        this.$confirm('确认完成课件学习?', "警告", {
+          confirmButtonText: "确认",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+        }).then(() => {
+
+        }).catch(() => {});
+      },
+      //切换课件
+      checkLearningButton(index){
+        let self = this;
+        if(this.checkLearningIndex != index){
+          this.$confirm('确认切换课件?', "警告", {
+            confirmButtonText: "确认",
+            cancelButtonText: "取消",
+            type: "warning"
+          }).then(function() {
+          }).then(() => {
+            self.$set(self,'checkLearningIndex',index);
+          }).catch(() => {});
+        }
+      },
+      //时间换算
+      formatTime(totalSeconds) {
+        if (typeof totalSeconds !== 'number' || totalSeconds <= 0) {
+          return '0秒';
+        }
+        const minutes = Math.floor(totalSeconds / 60);
+        const seconds = totalSeconds % 60;
+        if (minutes > 0 && seconds > 0) {
+          return `${minutes}分${seconds}秒`;
+        } else if (minutes > 0 && seconds === 0) {
+          return `${minutes}分`;
+        } else {
+          return `${seconds}秒`;
+        }
+      }
     }
   }
 </script>
@@ -43,5 +155,117 @@
     padding:0;
     box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
     margin:10px;
+    .content-box{
+      flex: 1;
+      display: flex;
+      overflow: hidden;
+      .left-max-big-box{
+        flex:1;
+        background-color: #dedede;
+        margin:40px 20px;
+        border-radius:10px;
+        overflow: hidden;
+      }
+      .right-max-big-box{
+        padding:40px 0;
+        width:400px;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        .title-p{
+          font-size:16px;
+          font-weight:700;
+          line-height:30px;
+          padding:0 20px;
+        }
+        .text-p{
+          font-size:14px;
+          line-height:20px;
+          margin-bottom:20px;
+          padding:0 20px;
+        }
+        .progress-box{
+          width:400px;
+          height:20px;
+          display: flex;
+          padding:0 20px;
+          .progress-title-box{
+            font-size:14px;
+            line-height:20px;
+            font-weight:700;
+          }
+          .progress-min-box{
+            flex:1;
+            padding:0 0 0 20px;
+            .el-progress{
+              margin-top:1px;
+            }
+          }
+        }
+        .learning-for-max-big-box{
+          flex:1;
+          display: flex;
+          flex-direction: column;
+          padding:0 20px;
+          .for-max-big-box{
+            margin-top:40px;
+            .for-big-box{
+              cursor: pointer;
+              border-radius:6px;
+              border:1px solid #666;
+              padding:15px 10px;
+              position: relative;
+              p{
+                line-height:30px;
+              }
+              p:nth-child(1){
+                font-size:16px;
+                font-weight:700;
+                /*单行省略号*/
+                display:block;
+                overflow:hidden;
+                text-overflow:ellipsis;
+                white-space:nowrap;
+              }
+              p:nth-child(2){
+                font-size:14px;
+              }
+              p:nth-child(3){
+                font-size:14px;
+                position: absolute;
+                bottom: 16px;
+                right: 16px;
+                color:#fff!important;
+                background-color: #999;
+                border-radius:4px;
+                width:60px;
+                line-height:24px;
+                text-align: center;
+              }
+              .colorA{
+                background-color: #0183fa!important;
+              }
+            }
+            .for-button{
+              margin-top:20px;
+              background-color: #13CE66;
+              color:#fff;
+              text-align: center;
+              line-height:30px;
+              border-radius:4px;
+              cursor: pointer;
+              font-size:14px;
+            }
+            .check-for-box{
+              background-color: rgba(1,131,250,0.1);
+              border:1px solid #0183fa!important;
+              p{
+                color:#0183fa!important;
+              }
+            }
+          }
+        }
+      }
+    }
   }
 </style>

+ 500 - 8
src/views/safetyEducationExaminationNew/featurePage/startTheExam/index.vue

@@ -5,30 +5,183 @@
       <p class="top-1-p">开始考试</p>
       <p class="top-2-p" @click="backPage()">返回</p>
     </div>
-    <div class="content-box scrollbar-box">
-
+    <div class="content-box">
+      <!-- 左侧面板 -->
+      <div class="left-panel">
+        <div class="exam-title">{{ examInfo.examTypeName }}</div>
+        <div class="exam-subtitle">总{{ examInfo.questionCount }}题 / 共{{ examInfo.totalScore }}分</div>
+        <div class="exam-name-box scrollbar-box">{{ examInfo.examName }}</div>
+        <div class="question-group-max-box">
+          <!-- 题目导航 -->
+          <div v-for="group in questionTypeGroups" :key="group.type" class="question-group">
+            <div class="group-header">
+              <span class="group-title">{{ group.questionTypeName }}</span>
+              <span class="group-count">{{ group.questionCount }}题 / 共{{ group.totalScore }}分</span>
+            </div>
+            <div class="question-nums">
+              <div
+                v-for="(item,index) in group.questions"
+                :key="item.questionNo"
+                :class="['num-item', getNumClass(item), currentIndex === item.questionNo ? 'num-active' : '']"
+                @click="selectQuestion(item)">
+                {{ item.questionNo }}
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="left-time-box">
+          <p>剩余时间</p>
+          <p>29分钟50秒</p>
+        </div>
+      </div>
+      <!-- 右侧内容区:只展示当前选中题目 -->
+      <div class="right-panel" v-if="currentQuestion">
+        <div class="question-card scrollbar-box">
+          <!-- 题目头部 -->
+          <div class="question-header">
+            <div class="question-index-box">{{ currentQuestion.questionNo }}</div>
+            <div class="question-type-score">{{ currentQuestion.questionTypeName }}({{ currentQuestion.questionScore }}分)</div>
+            <div class="question-stars">
+              <i
+                v-for="s in 4"
+                :key="s"
+                :class="['star-icon', s <= currentQuestion.difficulty ? 'star-active' : 'star-empty']"
+              >★</i>
+            </div>
+          </div>
+          <!-- 题目内容(富文本) -->
+          <div class="question-content" v-html="currentQuestion.questionContent"></div>
+          <!-- 单选题选项/多选题选项 -->
+          <div class="options-list">
+            <div v-for="(minItem,minIndex) in currentQuestion.options"
+                 :key="minIndex" class="option-item"
+                 :class="isValueInList(minItem.optionLabel,currentQuestion.correctAnswerTags)?'option-selected':''">
+            <span class="option-radio">
+              <i v-if="isValueInList(minItem.optionLabel,currentQuestion.correctAnswerTags)" class="el-icon-check radio-checked"></i>
+              <i v-else class="radio-empty"></i>
+            </span>
+              <span class="option-key" v-if="currentQuestion.questionType == '1' || currentQuestion.questionType == '2'">{{ minItem.optionLabel }}</span>
+              <span class="option-text" v-html="minItem.optionContent"></span>
+            </div>
+          </div>
+        </div>
+        <div class="question-button-box">
+          <p class="previous-question" :class="buttonIndex!=0?'check-button':''">上一题</p>
+          <p class="null-p"></p>
+          <p class="submit-exam">交卷</p>
+          <p class="null-p"></p>
+          <p class="next-question" :class="buttonIndex!=maxIndex?'check-button':''">下一题</p>
+        </div>
+      </div>
     </div>
   </div>
 </template>
 <script>
+  import {
+    examElExamAttemptReviewSidebar, examElExamAttemptReviewQuestion
+  } from '@/api/safetyEducationExaminationNew/index'
   export default {
     name: 'index',
     data() {
       return {
-
+        newData:{},
+        currentIndex: null,
+        examInfo: {
+          examTypeName: '',
+          examName: '',
+          questionCount: 0,
+          totalScore: 0,
+          passScore: 0,
+          earnedScore: 0,
+          durationText: '',
+          passStatus: null,
+        },
+        questionTypeGroups: [],
+        questionCache: {},
+        currentQuestion: null,
+        // 右侧加载中状态
+        questionLoading: false,
+        mockQuestions: {},
+        //按钮状态
+        maxIndex:10,
+        buttonIndex:0,
       }
     },
-    created() {
-
-    },
+    computed: {},
+    created() {},
     mounted() {
-
+      this.initialize()
     },
     methods: {
-      //返回
+      initialize() {
+        console.log('this.$route.params',this.$route.params)
+        if(this.$route.params){
+          this.$set(this,'newData',this.$route.params);
+        }else{
+          this.$router.back()
+          return
+        }
+        this.$nextTick(()=>{
+          //左侧数据
+          examElExamAttemptReviewSidebar({attemptId:this.newData.attemptId}).then(response => {
+            this.$set(this,'examInfo',{
+              examTypeName: response.data.examTypeName?response.data.examTypeName:'',
+              examName: response.data.examName?response.data.examName:'',
+              questionCount: response.data.questionCount?response.data.questionCount:0,
+              totalScore: response.data.totalScore?response.data.totalScore:0,
+              passScore: response.data.passScore?response.data.passScore:0,
+              earnedScore: response.data.earnedScore?response.data.earnedScore:0,
+              durationText: response.data.durationText?response.data.durationText:'',
+              passStatus: response.data.passStatus?response.data.passStatus:null,
+            });
+            this.$set(this,'questionTypeGroups',response.data.questionTypeGroups);
+            // 默认选中第一题(假数据直接从 data 里读取,无需接口)
+            const firstGroup = this.questionTypeGroups[0]
+            if (firstGroup && firstGroup.questions.length) {
+              this.$set(this, 'currentIndex', firstGroup.questions[0].questionNo);
+              this.examElExamAttemptReviewQuestion(firstGroup.questions[0].snapshotQuestionId);
+            }
+          });
+        })
+      },
+      //右侧数据
+      examElExamAttemptReviewQuestion(id){
+        examElExamAttemptReviewQuestion({attemptId:this.newData.attemptId,snapshotQuestionId:id}).then(response => {
+          this.$set(this,'currentQuestion',response.data);
+        });
+      },
       backPage() {
         this.$router.back()
       },
+      // 点击左侧题号:切换当前题目,按需从接口加载详情
+      selectQuestion(item) {
+        this.$set(this, 'currentIndex', item.questionNo);
+        this.examElExamAttemptReviewQuestion(item.snapshotQuestionId);
+      },
+      // 判断选项是否被选中
+      isSelectedOption(q, key) {
+        if (!q.userAnswer) return false
+        return q.userAnswer.split(',').includes(key)
+      },
+      // 根据左侧题目列表的 status 字段获取题号样式
+      // status: '1' → 绿色,'0' → 红色,'null' → 灰色
+      getNumClass(item) {
+        if (item.isCorrect === 1) return 'num-correct'
+        if (item.isCorrect === 0) return 'num-wrong'
+        return 'num-unanswered'
+      },
+      //匹配用户选择
+      isValueInList(target, list) {
+        if (!Array.isArray(list)) return false;
+        return list.some(item => item === target);
+      },
+      //返回用户选择答案
+      arrayToString(list) {
+        // 防御性编程:如果传入的不是数组,返回空字符串防止报错
+        if (!Array.isArray(list)) return '';
+
+        return list.join(',');
+      },
     }
   }
 </script>
@@ -43,5 +196,344 @@
     padding:0;
     box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
     margin:10px;
+    .content-box{
+      flex: 1;
+      display: flex;
+      overflow: hidden;
+      background: #f5f5f5;
+
+      /* ========== 左侧面板 ========== */
+      .left-panel {
+        width: 400px;
+        min-width: 400px;
+        background: #fff;
+        display: flex;
+        flex-direction: column;
+        overflow-y: auto;
+        padding: 20px 16px;
+        border-right: 1px solid #e8e8e8;
+
+        &::-webkit-scrollbar { width: 4px; }
+        &::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
+
+        .exam-title {
+          font-size: 16px;
+          font-weight: 700;
+          color: #1890ff;
+          margin-bottom: 10px;
+        }
+
+        .exam-subtitle {
+          font-size: 13px;
+          color: #666;
+          margin-bottom: 16px;
+        }
+
+        .exam-name-box {
+          font-size: 14px;
+          font-weight: 700;
+          color: #333;
+          margin-bottom: 12px;
+          line-height: 1.6;
+        }
+        .question-group-max-box{
+          flex:1;
+        }
+        .exam-info-box {
+          background: #f5f5f5;
+          border-radius: 6px;
+          padding: 12px;
+          margin-bottom: 20px;
+
+          .info-item {
+            font-size: 13px;
+            color: #333;
+            margin-bottom: 6px;
+            line-height: 1.5;
+            &:last-child { margin-bottom: 0; }
+
+            .label { font-weight: 500; }
+            .result-pass { color: #52c41a; font-weight: 700; }
+            .result-fail { color: #faad14; font-weight: 700; }
+          }
+        }
+        .question-group {
+          margin-bottom: 16px;
+
+          .group-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 8px;
+
+            .group-title { font-size: 14px; font-weight: 600; color: #333; }
+            .group-count { font-size: 12px; color: #999; }
+          }
+
+          .question-nums {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 6px;
+
+            .num-item {
+              width: 36px;
+              height: 36px;
+              border-radius: 4px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              font-size: 13px;
+              font-weight: 600;
+              cursor: pointer;
+              position: relative;
+              color: #fff;
+              transition: opacity 0.2s;
+
+              &:hover { opacity: 0.85; }
+
+              &.num-correct { background: #52c41a; }
+              &.num-wrong { background: #ff4d4f; }
+              &.num-unanswered { background: #999; color: #fff; }
+              &.num-active {
+                outline: 3px solid #1890ff;
+                outline-offset: 1px;
+              }
+            }
+          }
+        }
+        .left-time-box{
+          padding-top:20px;
+          display: flex;
+          p{
+            line-height:30px;
+          }
+          p:nth-child(1){
+            flex:4;
+            color:#666;
+            text-align: right;
+            padding:0 10px;
+            font-size:16px;
+            font-weight:700;
+          }
+          p:nth-child(2){
+            flex:5;
+            color:#EC808D;
+            padding:0 10px;
+            font-size:16px;
+            font-weight:700;
+          }
+        }
+      }
+
+      /* ========== 右侧内容区 ========== */
+      .right-panel {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        padding: 20px;
+
+        &::-webkit-scrollbar { width: 6px; }
+        &::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
+
+        .question-card {
+          flex: 1;
+          display: flex;
+          flex-direction: column;
+          background: #fff;
+          border-radius: 8px;
+          padding: 20px;
+
+          .question-header {
+            display: flex;
+            align-items: center;
+            margin-bottom: 12px;
+
+            .question-index-box {
+              width: 28px;
+              height: 28px;
+              background: #1890ff;
+              color: #fff;
+              border-radius: 4px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              font-size: 13px;
+              font-weight: 700;
+              margin-right: 10px;
+              flex-shrink: 0;
+            }
+
+            .question-type-score {
+              font-size: 15px;
+              font-weight: 600;
+              color: #333;
+              flex: 1;
+            }
+
+            .question-stars {
+              display: flex;
+              gap: 2px;
+
+              .star-icon {
+                font-size: 18px;
+                font-style: normal;
+                &.star-active { color: #fadb14; }
+                &.star-empty { color: #d9d9d9; }
+              }
+            }
+          }
+
+          .question-content {
+            font-size: 14px;
+            font-weight: 600;
+            color: #333;
+            margin-bottom: 16px;
+            line-height: 1.6;
+          }
+
+          .options-list {
+            display: flex;
+            flex-direction: column;
+            gap: 8px;
+            margin-bottom: 16px;
+
+            .option-item {
+              display: flex;
+              align-items: center;
+              border: 1px solid #e8e8e8;
+              border-radius: 6px;
+              padding: 10px 14px;
+              cursor: default;
+              transition: border-color 0.2s;
+              &.option-selected {
+                border-color: #1890ff;
+                background: #e6f7ff;
+
+                .option-key { color: #1890ff; }
+                .option-text { color: #1890ff; }
+              }
+
+              .option-radio {
+                width: 16px;
+                height: 16px;
+                border-radius: 50%;
+                border: 1px solid #d9d9d9;
+                margin-right: 10px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                flex-shrink: 0;
+
+                .radio-checked { color: #1890ff; font-size: 12px; }
+                .radio-empty { display: block; width: 8px; height: 8px; }
+              }
+
+              .option-checkbox {
+                margin-right: 10px;
+                flex-shrink: 0;
+              }
+
+              .option-key {
+                font-size: 14px;
+                font-weight: 600;
+                color: #666;
+                margin-right: 8px;
+                width: 16px;
+                flex-shrink: 0;
+              }
+
+              .option-text {
+                font-size: 14px;
+                color: #333;
+                flex: 1;
+              }
+            }
+          }
+
+          .answer-result-box {
+            background: #f5f5f5;
+            border-radius: 6px;
+            padding: 14px 16px;
+
+            .answer-status {
+              margin-bottom: 10px;
+              font-size: 14px;
+              font-weight: 600;
+
+              .status-correct { color: #52c41a; }
+              .status-wrong { color: #ff4d4f; }
+            }
+
+            .answer-detail {
+              .answer-row {
+                font-size: 14px;
+                color: #333;
+                margin-bottom: 10px;
+
+                .answer-label { font-weight: 600; }
+              }
+
+              .analysis-title {
+                font-size: 14px;
+                font-weight: 600;
+                color: #333;
+                margin-bottom: 6px;
+              }
+
+              .analysis-content {
+                font-size: 13px;
+                /*color: #fa8c16;*/
+                line-height: 1.7;
+              }
+            }
+          }
+        }
+        .question-button-box{
+          height:34px;
+          display: flex;
+          margin-top:20px;
+          .null-p{
+            flex:1;
+          }
+          .previous-question{
+            width:100px;
+            line-height:34px;
+            height:34px;
+            border-radius:6px;
+            background-color: #999;
+            color:#fff;
+            text-align: center;
+            margin:0 0 0 120px;
+            cursor: pointer;
+          }
+          .next-question{
+            width:100px;
+            line-height:34px;
+            height:34px;
+            border-radius:6px;
+            background-color: #999;
+            color:#fff;
+            text-align: center;
+            margin:0 120px 0 0;
+            cursor: pointer;
+          }
+          .submit-exam{
+            width:140px;
+            line-height:34px;
+            height:34px;
+            border-radius:6px;
+            background-color: #0183fa;
+            color:#fff;
+            text-align: center;
+            cursor: pointer;
+          }
+          .check-button{
+            background-color: #0183fa;
+            color:#fff;
+          }
+        }
+      }
+    }
   }
 </style>

+ 18 - 9
src/views/safetyEducationExaminationNew/publicConfiguration/certificateTemplate/addPage.vue

@@ -23,14 +23,21 @@
             </el-select>
           </el-form-item>
           <el-form-item label="颁发单位" prop="deptId">
-            <el-select v-model="newData.deptId" placeholder="请选择颁发单位" style="width: 500px">
-              <el-option
-                v-for="dict in optionDeptList"
-                :key="dict.deptId"
-                :label="dict.deptName"
-                :value="dict.deptId"
-              />
-            </el-select>
+            <el-cascader
+              style="width: 500px"
+              v-model="newData.deptId"
+              :options="optionDeptList"
+              :props="{
+                emitPath:false,
+                checkStrictly: true,
+                value: 'deptId',
+                label: 'deptName',
+                children: 'child',
+              }"
+              :show-all-levels="false"
+              filterable
+              placeholder="请选择颁发单位"
+            ></el-cascader>
           </el-form-item>
           <el-form-item label="证书模板" prop="templateWordPath">
             <el-upload
@@ -235,7 +242,9 @@
       //学院列表
       systemDeptCurrentDept(){
         systemDeptCurrentDept({}).then(response => {
-          this.$set(this,'optionDeptList',response.data);
+          const list = response.data || [];
+          this.formatTreeData(list);
+          this.$set(this,'optionDeptList',list);
         });
       },
     },

+ 8 - 4
src/views/safetyEducationExaminationNew/safetyTest/onlineExam/index.vue

@@ -168,7 +168,13 @@
       tableButton(type, item) {
         let self = this
         if (type == 1) {
-          //模拟练习
+          this.$router.push({
+            name: 'startTheExam',
+            params: {
+              attemptId: '2081995162915893249',
+              type: '1'
+            }
+          })
           if(item.conditionType == 1){
 
           }else if(item.conditionType == 2){
@@ -176,9 +182,7 @@
           }else if(item.conditionType == 3){
 
           }else if(item.conditionType == 4){
-
-          }else if(item.conditionType == 5){
-
+            //开始考试
           }
 
         } else if (type == 6) {