Browse Source

调整修改 exam 模块

reghao 1 week ago
parent
commit
cb948683bc

+ 48 - 64
src/api/exam.js

@@ -1,142 +1,126 @@
 import { delete0, get, post } from '@/utils/request'
 
 const examApi = {
-  // *********************************************************************************************************************
   // 科目接口
-  getSubject: '/api/content/exam/subject',
-  getSubjectKV: '/api/content/exam/subject/kv',
-  getSubjectCascade: '/api/content/exam/subject/cascade',
-  // *********************************************************************************************************************
+  examSubject: '/api/content/exam/subject',
   // 试题接口
-  getQuestionType: '/api/content/exam/question/type',
-  getQuestion: '/api/content/exam/question',
-  addQuestion: '/api/content/exam/question',
-  // *********************************************************************************************************************
+  examQuestion: '/api/content/exam/question',
   // 试卷接口
-  examPaperApi: '/api/content/exam/paper',
-  getPaper: '/api/content/exam/paper',
-  addPaper: '/api/content/exam/paper',
-  getPaperDetail: '/api/content/exam/paper/detail',
-  getPaperKeyValue: '/api/content/exam/paper/kv',
-  // *********************************************************************************************************************
-  // 考试接口
-  examList: '/api/content/exam/eval/list',
-  startExam: '/api/content/exam/eval/start',
-  cacheUserAnswer: '/api/content/exam/eval/cache',
-  submitExam: '/api/content/exam/eval/submit',
-  // *********************************************************************************************************************
+  examPaper: '/api/content/exam/paper',
+  // 测评管理接口
+  examMgr: '/api/content/exam/mgr',
+  // 测评接口
+  examEval: '/api/content/exam/eval',
   // 阅卷接口
-  getExamMarkList: '/api/content/exam/mark/list',
-  getMarkView: '/api/content/exam/mark/view',
-  submitExamMark: '/api/content/exam/mark/submit'
+  examMark: '/api/content/exam/mark'
 }
 
 // *********************************************************************************************************************
 // 科目接口
 export function getSubject() {
-  return get(examApi.getSubject)
+  return get(examApi.examSubject)
 }
 
 export function getSubjectKV() {
-  return get(examApi.getSubjectKV)
+  return get(examApi.examSubject + '/kv')
 }
 
 export function getSubjectCascade() {
-  return get(examApi.getSubjectCascade)
+  return get(examApi.examSubject + '/cascade')
 }
 
 // *********************************************************************************************************************
 // 试题接口
 export function getQuestionType() {
-  return get(examApi.getQuestionType)
+  return get(examApi.examQuestion + '/type')
 }
 
 export function getQuestions(queryParams) {
-  return get(examApi.getQuestion, queryParams)
+  return get(examApi.examQuestion + '/list', queryParams)
 }
 
 export function deleteQuestion(questionId) {
-  return delete0(examApi.getQuestion + '/' + questionId)
+  return delete0(examApi.examQuestion + '/' + questionId)
 }
 
 export function getQuestion(questionId) {
-  return get(examApi.getQuestion + '/' + questionId)
+  return get(examApi.examQuestion + '/' + questionId)
 }
 
 export function addQuestion(jsonData) {
-  return post(examApi.addQuestion, jsonData)
+  return post(examApi.examQuestion, jsonData)
 }
 
 // *********************************************************************************************************************
 // 试卷接口
-export function getPapers(queryParams) {
-  return get(examApi.examPaperApi, queryParams)
+export function addPaper(jsonData) {
+  return post(examApi.examPaper, jsonData)
 }
 
-export function getPaper(queryParams) {
-  return get(examApi.examPaperApi + '/detail', queryParams)
+export function deletePaper(paperId) {
+  return delete0(examApi.examPaper + '/' + paperId)
 }
 
-export function getPaperKeyValues() {
-  return get(examApi.getPaperKeyValue)
+export function getPapers(queryParams) {
+  return get(examApi.examPaper, queryParams)
 }
 
-export function addPaper(jsonData) {
-  return post(examApi.addPaper, jsonData)
+export function getPaper(queryParams) {
+  return get(examApi.examPaper + '/detail', queryParams)
 }
 
-export function deletePaper(paperId) {
-  return delete0(examApi.addPaper + '/' + paperId)
+// *********************************************************************************************************************
+// 测评管理接口
+export function createExamEval(jsonData) {
+  return get(examApi.examMgr + '/create', jsonData)
 }
 
 // *********************************************************************************************************************
-// 考试接口
+// 测评接口
 export function getExamList() {
-  return get(examApi.examList)
+  return get(examApi.examEval + '/list')
 }
 
 export function getExamInfo(queryParams) {
-  return get(examApi.startExam, queryParams)
+  return get(examApi.examEval + '/start', queryParams)
 }
 
-export function getPaperQuestions(paperId) {
-  return get('/api/content/exam/paper/' + paperId + '/question')
+export function cacheUserAnswer(jsonData) {
+  return post(examApi.examEval + '/cache', jsonData)
 }
 
-export function submitExam(jsonData) {
-  return post(examApi.submitExam, jsonData)
+export function getCachedUserAnswer(queryParams) {
+  return get(examApi.examEval + '/cache', queryParams)
 }
 
-export function cacheUserAnswer(jsonData) {
-  return post(examApi.cacheUserAnswer, jsonData)
+export function submitExam(jsonData) {
+  return post(examApi.examEval + '/submit', jsonData)
 }
 
-export function getCachedUserAnswer(queryParams) {
-  return get(examApi.cacheUserAnswer, queryParams)
+export function getExamResult(queryParams) {
+  return get(examApi.examEval + '/result', queryParams)
 }
 
 // *********************************************************************************************************************
 // 阅卷接口
-export function getExamMarkList(params) {
-  return get(examApi.getExamMarkList, params)
+export function getMarkPapers() {
+  return get(examApi.examMark + '/paper')
 }
 
-export function getMarkView(queryParams) {
-  return get(examApi.getMarkView, queryParams)
+export function getExamMarkList(queryParams) {
+  return get(examApi.examMark + '/list', queryParams)
 }
 
-export function submitMarkResult(jsonData) {
-  return post(examApi.submitExamMark, jsonData)
+export function getMarkView(queryParams) {
+  return get(examApi.examMark + '/view', queryParams)
 }
 
-// *********************************************************************************************************************
-// 考试结果接口
-export function getResultView(paperId) {
-  return get('/api/content/exam/result/view/' + paperId)
+export function submitMarkResult(jsonData) {
+  return post(examApi.examMark + '/submit', jsonData)
 }
 
 // *********************************************************************************************************************
-// 考试数据接口
+// 测评数据接口
 export function getExamCount() {
   return get('/api/content/exam/statistic/count')
 }

+ 8 - 9
src/router/exam.js

@@ -7,14 +7,13 @@ const ExamSubject = () => import('views/exam/ExamSubject')
 const ExamQuestion = () => import('views/exam/ExamQuestion')
 const ExamQuestionAdd = () => import('views/exam/ExamQuestionAdd')
 const ExamPaperList = () => import('views/exam/ExamPaperList')
-const ExamPaperAdd = () => import('views/exam/ExamPaperAdd')
 const ExamPaperAssembly = () => import('views/exam/ExamPaperAssembly')
 const ExamEvalList = () => import('views/exam/ExamEvalList')
-const ExamCard = () => import('views/exam/ExamCard')
 const ExamScoreIndex = () => import('views/exam/ExamScoreIndex')
 const ExamMarkIndex = () => import('views/exam/ExamMarkIndex')
 const ExamPaperPreview = () => import('views/exam/ExamPaperPreview')
 const ExamPaperMark = () => import('views/exam/ExamPaperMark')
+const ExamPaperResult = () => import('views/exam/ExamPaperResult')
 
 export default {
   path: '/exam',
@@ -28,12 +27,6 @@ export default {
       component: ExamDashboard,
       meta: { needAuth: true }
     },
-    {
-      path: '/exam/start/:paperId',
-      name: 'ExamCard',
-      component: ExamCard,
-      meta: { needAuth: true }
-    },
     {
       path: '/exam/score',
       name: 'ExamScoreIndex',
@@ -72,7 +65,7 @@ export default {
     },
     {
       path: '/exam/paper/add',
-      name: 'ExamPaperAdd',
+      name: 'ExamPaperAssembly',
       component: ExamPaperAssembly,
       meta: { needAuth: true }
     },
@@ -82,6 +75,12 @@ export default {
       component: ExamEvalList,
       meta: { needAuth: true }
     },
+    {
+      path: '/exam/eval/result',
+      name: 'ExamPaperResult',
+      component: ExamPaperResult,
+      meta: { needAuth: true }
+    },
     {
       path: '/exam/statistics',
       name: 'ExamDashboard',

+ 2 - 2
src/views/exam/Exam.vue

@@ -21,7 +21,7 @@
           <el-submenu index="/exam/admin">
             <template slot="title">
               <i class="el-icon-user" />
-              <span slot="title">考试管理</span>
+              <span slot="title">测评管理</span>
             </template>
             <el-menu-item-group>
               <el-menu-item index="/exam/subject">
@@ -108,7 +108,7 @@ export default {
     }
   },
   created() {
-    document.title = '考试系统'
+    document.title = '测评系统'
     this.user = getAuthedUser()
   }
 }

+ 0 - 815
src/views/exam/ExamCard.vue

@@ -1,815 +0,0 @@
-<template>
-  <el-container v-if="show">
-    <el-header style="margin-top: 60px">
-      <el-row>
-        <el-col :span="18" :offset="3" style="border-bottom: 1px solid #f5f5f5">
-          <span class="startExam">开始考试</span>
-          <span class="examTitle">距离考试结束还有:</span>
-          <span style="color: red;font-size: 18px;">{{ duration | timeFormat }}</span>
-          <el-button
-            type="warning"
-            round
-            style="background-color: #ffd550;float: right;color: black;font-weight: 800"
-            @click="uploadExamToAdmin"
-          >提交试卷
-          </el-button>
-        </el-col>
-      </el-row>
-    </el-header>
-
-    <el-main>
-      <el-row>
-        <el-col :span="18" :offset="3">
-          <el-col :span="16">
-            <el-card style="min-height: 500px">
-              <!-- 题目信息 -->
-              <div>
-                <span v-if="questionList[curIndex].questionType === 1">【单选题】</span>
-                <span v-else-if="questionList[curIndex].questionType === 2">【多选题】</span>
-                <span v-else-if="questionList[curIndex].questionType === 3">【不定项选择题】</span>
-                <span v-else-if="questionList[curIndex].questionType === 4">【判断题】</span>
-                <span v-else-if="questionList[curIndex].questionType === 5">【填空题】</span>
-                <span v-else-if="questionList[curIndex].questionType === 6">【问答题】</span>
-                <span v-else-if="questionList[curIndex].questionType === 7">【理解题】</span>
-                <span v-else>【综合题】</span>
-                <br>
-                <br>
-                <i class="num">{{ curIndex + 1 }}</i>
-                <span v-html="questionList[curIndex].questionContent" />
-              </div>
-              <!--题目中的配图-->
-              <img
-                v-for="url in questionList[curIndex].images"
-                :src="url"
-                title="点击查看大图"
-                alt="题目图片"
-                style="width: 100px;height: 100px;cursor: pointer"
-                @click="showBigImg(url)"
-              >
-
-              <!-- 单选和判断题候选答案列表 -->
-              <div
-                v-show="questionList[curIndex].questionType === 1
-                  || questionList[curIndex].questionType === 4"
-                style="margin-top: 25px"
-              >
-                <div class="el-radio-group">
-                  <label
-                    v-for="(item,index) in questionList[curIndex].answer"
-                    :class="index === userAnswer[curIndex] ? 'active' : ''"
-                    @click="checkSingleAnswer(index)"
-                  >
-                    <span>{{ optionName[index] + '、' + item.answer }}</span>
-                    <img
-                      v-for="i2 in item.images"
-                      v-if="item.images !== null"
-                      style="position: absolute;left:100%;top:50%;transform: translateY(-50%);width: 40px;height: 40px;float: right;cursor: pointer;"
-                      title="点击查看大图"
-                      :src="i2"
-                      alt=""
-                      @mouseover="showBigImg(i2)"
-                    >
-                  </label>
-                </div>
-              </div>
-
-              <!-- 多选和不定项选择题的候选答案列表 -->
-              <div
-                v-show="questionList[curIndex].questionType === 2
-                  || questionList[curIndex].questionType === 3"
-                style="margin-top: 25px"
-              >
-                <div class="el-radio-group">
-                  <label
-                    v-for="(item,index) in questionList[curIndex].answer"
-                    :class="(userAnswer[curIndex]+'').indexOf(index+'') !== -1? 'active' : ''"
-                    @click="selectedMultipleAnswer(index)"
-                  >
-                    <span>{{ optionName[index] + '、' + item.answer }}</span>
-                    <img
-                      v-for="i2 in item.images"
-                      v-if="item.images !== null"
-                      style="position: absolute;left:100%;top:50%;transform: translateY(-50%);
-                  width: 40px;height: 40px;float: right;cursor: pointer;"
-                      title="点击查看大图"
-                      :src="i2"
-                      alt=""
-                      @mouseover="showBigImg(i2)"
-                    >
-                  </label>
-                </div>
-              </div>
-
-              <!-- 填空题和问答题的回答区 -->
-              <div
-                v-show="questionList[curIndex].questionType === 5
-                  || questionList[curIndex].questionType === 6"
-                style="margin-top: 25px"
-              >
-                <el-input
-                  v-model="userAnswer[curIndex]"
-                  type="textarea"
-                  :rows="8"
-                  placeholder="请输入答案"
-                />
-              </div>
-
-              <!-- 综合题的回答区 -->
-              <div
-                v-show="questionList[curIndex].questionType === 8"
-                style="margin-top: 25px"
-              >
-                <el-input
-                  v-model="userAnswer[curIndex]"
-                  type="textarea"
-                  :rows="8"
-                  placeholder="请输入答案"
-                />
-              </div>
-
-              <!-- 上一题和下一题按钮 -->
-              <div style="margin-top: 25px">
-                <el-button type="primary" icon="el-icon-back" :disabled="curIndex<1" @click="curIndex--">上一题</el-button>
-                <el-button
-                  type="primary"
-                  icon="el-icon-right"
-                  :disabled="curIndex>=questionList.length-1"
-                  @click="curIndex++"
-                >下一题
-                </el-button>
-              </div>
-            </el-card>
-          </el-col>
-          <!-- 答题卡 -->
-          <el-col :span="7" :offset="1">
-            <el-card>
-              <div>
-                <p style="font-size: 18px;">答题卡</p>
-                <div style="margin-top: 25px">
-                  <span
-                    style="background-color: rgb(238,238,238);padding: 5px 10px 5px 10px;margin-left: 15px"
-                  >未作答</span>
-                  <span
-                    style="background-color: rgb(87,148,247);color: white;padding: 5px 10px 5px 10px;margin-left: 15px"
-                  >已作答</span>
-                </div>
-              </div>
-
-              <!-- 单选的答题卡 -->
-              <div style="margin-top: 25px">
-                <p style="font-size: 18px;">单选题</p>
-                <el-button
-                  v-for="item in questionList.length"
-                  v-show="questionList[item-1].questionType === 1"
-                  :key="item"
-                  style="margin-top: 10px;margin-left: 15px"
-                  size="mini"
-                  :class="questionList[item-1].questionType === 1 && userAnswer[item-1] !== undefined ?
-                    'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
-                  @click="curIndex = item-1"
-                >{{ item }}
-                </el-button>
-              </div>
-
-              <!-- 多选的答题卡 -->
-              <div style="margin-top: 25px">
-                <p style="font-size: 18px;">多选题</p>
-                <el-button
-                  v-for="item in questionList.length"
-                  v-show="questionList[item-1].questionType === 2"
-                  :key="item"
-                  style="margin-top: 10px;margin-left: 15px"
-                  size="mini"
-                  :class="questionList[item-1].questionType === 2 && userAnswer[item-1] !== undefined ?
-                    'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
-                  @click="curIndex = item-1"
-                >{{ item }}
-                </el-button>
-              </div>
-              <!-- 不定项选择题的答题卡 -->
-              <div style="margin-top: 25px">
-                <p style="font-size: 18px;">不定项选择题</p>
-                <el-button
-                  v-for="item in questionList.length"
-                  v-show="questionList[item-1].questionType === 3"
-                  :key="item"
-                  style="margin-top: 10px;margin-left: 15px"
-                  size="mini"
-                  :class="questionList[item-1].questionType === 3 && userAnswer[item-1] !== undefined ?
-                    'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
-                  @click="curIndex = item-1"
-                >{{ item }}
-                </el-button>
-              </div>
-
-              <!-- 判断题的答题卡 -->
-              <div style="margin-top: 25px">
-                <p style="font-size: 18px;">判断题</p>
-                <el-button
-                  v-for="item in questionList.length"
-                  v-show="questionList[item-1].questionType === 4"
-                  :key="item"
-                  style="margin-top: 10px;margin-left: 15px"
-                  size="mini"
-                  :class="questionList[item-1].questionType === 4 && userAnswer[item-1] !== undefined ?
-                    'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
-                  @click="curIndex = item-1"
-                >{{ item }}
-                </el-button>
-              </div>
-
-              <!-- 填空题的答题卡 -->
-              <div style="margin-top: 25px">
-                <p style="font-size: 18px;">填空题</p>
-                <el-button
-                  v-for="item in questionList.length"
-                  v-show="questionList[item-1].questionType === 5"
-                  :key="item"
-                  style="margin-top: 10px;margin-left: 15px"
-                  size="mini"
-                  :class="questionList[item-1].questionType === 5 && userAnswer[item-1] !== undefined ?
-                    'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
-                  @click="curIndex = item-1"
-                >{{ item }}
-                </el-button>
-              </div>
-
-              <!-- 问答题的答题卡 -->
-              <div style="margin-top: 25px">
-                <p style="font-size: 18px;">问答题</p>
-                <el-button
-                  v-for="item in questionList.length"
-                  v-show="questionList[item-1].questionType === 6"
-                  :key="item"
-                  style="margin-top: 10px;margin-left: 15px"
-                  size="mini"
-                  :class="questionList[item-1].questionType === 6 && userAnswer[item-1] !== undefined ?
-                    'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
-                  @click="curIndex = item-1"
-                >{{ item }}
-                </el-button>
-              </div>
-
-              <!-- 综合题的答题卡 -->
-              <div style="margin-top: 25px">
-                <p style="font-size: 18px;">综合题</p>
-                <el-button
-                  v-for="item in questionList.length"
-                  v-show="questionList[item-1].questionType === 8"
-                  :key="item"
-                  style="margin-top: 10px;margin-left: 15px"
-                  size="mini"
-                  :class="questionList[item-1].questionType === 8 && userAnswer[item-1] !== undefined ?
-                    'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
-                  @click="curIndex = item-1"
-                >{{ item }}
-                </el-button>
-              </div>
-            </el-card>
-          </el-col>
-        </el-col>
-      </el-row>
-      <video
-        id="video"
-        muted="muted"
-        style="float:right;position: fixed;top: 80%;left: 85%"
-        width="200px"
-        height="200px"
-        autoplay="autoplay"
-      />
-      <canvas id="canvas" hidden width="200px" height="200px" />
-    </el-main>
-    <!--图片回显-->
-    <el-dialog :visible.sync="bigImgDialog" @close="bigImgDialog = false">
-      <img style="width: 100%" :src="bigImgUrl">
-    </el-dialog>
-  </el-container>
-</template>
-
-<script>
-import { submitExam, getExamInfo, getPaperQuestions } from '@/api/exam'
-
-export default {
-  name: 'ExamCard',
-  filters: {
-    ellipsis(value) {
-      if (!value) return ''
-      const max = 50
-      if (value.length > max) {
-        return value.slice(0, max) + '...'
-      }
-      return value
-    },
-    timeFormat(time) {
-      // 分钟
-      var minute = time / 60
-      var minutes = parseInt(minute)
-
-      if (minutes < 10) {
-        minutes = '0' + minutes
-      }
-
-      // 秒
-      var second = time % 60
-      var seconds = Math.round(second)
-      if (seconds < 10) {
-        seconds = '0' + seconds
-      }
-      return `${minutes}:${seconds}`
-    }
-  },
-  data() {
-    return {
-      // 当前考试的信息
-      examInfo: {},
-      // 当前的考试题目
-      questionList: [
-        {
-          questionType: ''
-        }
-      ],
-      // 当前题目的索引值
-      curIndex: 0,
-      // 控制大图的对话框
-      bigImgDialog: false,
-      // 当前要展示的大图的url
-      bigImgUrl: '',
-      // 用户选择的答案
-      userAnswer: [],
-      // 页面数据加载
-      loading: {},
-      // 页面绘制是否开始
-      show: false,
-      // 答案的选项名abcd数据
-      optionName: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
-      // 考试总时长
-      duration: 0,
-      // 摄像头对象
-      mediaStreamTrack: null,
-      // 诚信照片的url
-      takePhotoUrl: [],
-      // 摄像头是否开启
-      cameraOn: false
-    }
-  },
-  watch: {
-    // 监控考试的剩余时间
-    async duration(newVal) {
-      const examDuration = {
-        duration: newVal,
-        timestamp: Date.now()
-      }
-      localStorage.setItem('examDuration' + this.examInfo.examId, JSON.stringify(examDuration))
-
-      // 摄像头数据
-      const constraints = {
-        video: {
-          width: 200,
-          height: 200
-        },
-        audio: false
-      }
-      // 通过调用摄像头判断用户是否中途关闭摄像头
-      /* const promise = navigator.mediaDevices.getUserMedia(constraints)
-      promise.catch((back) => {
-        this.cameraOn = false
-      })*/
-      if (!this.cameraOn) { // 如果摄像头未开启,就再次调用开启
-        this.getCamera()
-      }
-
-      // 考试时间结束, 自动提交试卷
-      if (newVal < 1) {
-        /* if (this.cameraOn) {
-          // 结束的时候拍照上传一张
-          await this.takePhoto()
-          this.closeCamera()
-        }*/
-
-        this.submitUserPayload(true)
-      }
-    }
-  },
-  created() {
-    this.getExamInfo()
-    // 页面数据加载的等待状态栏
-    this.loading = this.$loading({
-      body: true,
-      lock: true,
-      text: '数据拼命加载中,(*╹▽╹*)',
-      spinner: 'el-icon-loading'
-    })
-    // 开启摄像头
-    window.onload = () => {
-      setTimeout(() => {
-        this.getCamera()
-      }, 1000)
-
-      // 生成3次时间点截图
-      const times = []
-      for (let i = 0; i < 2; i++) {
-        times.push(Math.ceil(Math.random() * this.duration * 1000))
-      }
-      times.push(10000)
-      // 一次考试最多3次随机的诚信截图
-      times.forEach(item => {
-        window.setTimeout(() => {
-          this.takePhoto()
-        }, item)
-      })
-    }
-  },
-  mounted() {
-    // 关闭浏览器窗口的时候移除 localstorage的时长
-    var userAgent = navigator.userAgent // 取得浏览器的userAgent字符串
-    var isOpera = userAgent.indexOf('Opera') > -1 // 判断是否Opera浏览器
-    var isIE = userAgent.indexOf('compatible') > -1 && userAgent.indexOf('MSIE') > -1 && !isOpera // 判断是否IE浏览器
-    var isIE11 = userAgent.indexOf('rv:11.0') > -1 // 判断是否是IE11浏览器
-    var isEdge = userAgent.indexOf('Edge') > -1 && !isIE // 判断是否IE的Edge浏览器
-    if (!isIE && !isEdge && !isIE11) { // 兼容chrome和firefox
-      var _beforeUnload_time = 0; var _gap_time = 0
-      var is_fireFox = navigator.userAgent.indexOf('Firefox') > -1// 是否是火狐浏览器
-      window.onunload = function() {
-        _gap_time = new Date().getTime() - _beforeUnload_time
-        if (_gap_time <= 5) {
-          localStorage.removeItem('examDuration' + this.examInfo.examId)
-        } else { // 谷歌浏览器刷新
-        }
-      }
-      window.onbeforeunload = function() {
-        _beforeUnload_time = new Date().getTime()
-        if (is_fireFox) { // 火狐关闭执行
-
-        } else { // 火狐浏览器刷新
-        }
-      }
-    }
-  },
-  methods: {
-    renderByMathjax() {
-      // tinymce 的 mathjax 插件生成的 latex 格式公式放在 className 为 math-tex 的 span 标签内
-      const className = 'math-tex'
-      this.$nextTick(function() {
-        if (this.MathJax.isMathjaxConfig) {
-          this.MathJax.initMathjaxConfig()
-        }
-        this.MathJax.MathQueue1(className)
-      })
-    },
-    // 查询当前考试的信息
-    getExamInfo() {
-      const paperId = this.$route.params.paperId
-      getExamInfo(paperId).then((resp) => {
-        if (resp.code === 0) {
-          this.examInfo = resp.data
-          // 设置定时(秒)
-          try {
-            const examDuration = JSON.parse(localStorage.getItem('examDuration' + this.examInfo.examId) || '{}')
-            if (examDuration.duration === 0 || Date.now() >= (examDuration.timestamp ||
-              Date.now()) + (examDuration.duration * 1000 || Date.now())) {
-              localStorage.removeItem('examDuration' + this.examInfo.examId)
-            }
-            this.duration = Math.min(JSON.parse(
-              localStorage.getItem('examDuration' + this.examInfo.examId) || '{}').duration ||
-              this.examInfo.examDuration * 60, this.examInfo.examDuration * 60)
-          } catch (e) {
-            localStorage.removeItem('examDuration' + this.examInfo.examId)
-          }
-
-          // 考试剩余时间定时器
-          this.timer = window.setInterval(() => {
-            if (this.duration > 0) this.duration--
-          }, 1000)
-          var paperId = this.examInfo.examId
-          this.getQuestionInfo1(paperId)
-        }
-      })
-    },
-    // 查询考试的题目信息
-    async getQuestionInfo1(paperId) {
-      await getPaperQuestions(paperId).then(resp => {
-        if (resp.code === 0) {
-          this.questionList = resp.data || []
-          // 重置问题的顺序 单选 多选 判断 简答
-          this.questionList = this.questionList.sort(function(a, b) {
-            return a.questionType - b.questionType
-          })
-
-          this.renderByMathjax()
-        }
-      })
-      this.loading.close()
-      this.show = true
-    },
-    // 点击展示高清大图
-    showBigImg(url) {
-      this.bigImgUrl = url
-      this.bigImgDialog = true
-    },
-    // ****************************************************************************************************************
-    // 检验单选题的用户选择的答案
-    checkSingleAnswer(index) {
-      this.$set(this.userAnswer, this.curIndex, index)
-    },
-    // 多选题用户的答案选中
-    selectedMultipleAnswer(index) {
-      if (this.userAnswer[this.curIndex] === undefined) { // 当前是多选的第一个答案
-        this.$set(this.userAnswer, this.curIndex, index)
-      } else if (String(this.userAnswer[this.curIndex]).split(',').includes(index + '')) { // 取消选中
-        const newArr = []
-        String(this.userAnswer[this.curIndex]).split(',').forEach(item => {
-          if (item !== '' + index) newArr.push(item)
-        })
-        if (newArr.length === 0) {
-          this.$set(this.userAnswer, this.curIndex, undefined)
-        } else {
-          this.$set(this.userAnswer, this.curIndex, newArr.join(','))
-          // 答案格式化顺序DBAC -> ABCD
-          this.userAnswer[this.curIndex] = String(this.userAnswer[this.curIndex]).split(',').sort(function(a, b) {
-            return a - b
-          }).join(',')
-        }
-      } else if (!((this.userAnswer[this.curIndex] + '').split(',').includes(index + ''))) { // 第n个答案
-        this.$set(this.userAnswer, this.curIndex, this.userAnswer[this.curIndex] += ',' + index)
-        // 答案格式化顺序DBAC -> ABCD
-        this.userAnswer[this.curIndex] = String(this.userAnswer[this.curIndex]).split(',').sort(function(a, b) {
-          return a - b
-        }).join(',')
-      }
-    },
-    // ****************************************************************************************************************
-    // 调用摄像头
-    getCamera() {
-      const constraints = {
-        video: {
-          width: 200,
-          height: 200
-        },
-        audio: false
-      }
-      // 获得video摄像头
-      const video = document.getElementById('video')
-      /* const promise = navigator.mediaDevices.getUserMedia(constraints)
-      promise.then((mediaStream) => {
-        this.mediaStreamTrack = typeof mediaStream.stop === 'function' ? mediaStream : mediaStream.getTracks()[1]
-        video.srcObject = mediaStream
-        video.play()
-        this.cameraOn = true
-      }).catch((back) => {
-        this.$message({
-          duration: 1500,
-          message: '请开启摄像头权限o(╥﹏╥)o!',
-          type: 'error'
-        })
-      })*/
-    },
-    // 拍照
-    async takePhoto() {
-      if (this.cameraOn) { // 摄像头是否开启 开启了才执行上传信用图片
-        // 获得Canvas对象
-        const video = document.getElementById('video')
-        const canvas = document.getElementById('canvas')
-        const ctx = canvas.getContext('2d')
-        ctx.drawImage(video, 0, 0, 200, 200)
-        // toDataURL  ---  可传入'image/png'---默认, 'image/jpeg'
-        const img = document.getElementById('canvas').toDataURL()
-
-        // 构造post的form表单
-        const formData = new FormData()
-        // convertBase64UrlToBlob函数是将base64编码转换为Blob
-        formData.append('file', this.base64ToFile(img, 'examTakePhoto.png'))
-        // 上传阿里云OSS
-        /* await ossUtils.uploadImage(formData).then((resp) => {
-          if (resp.code === 0) this.takePhotoUrl.push(resp.data)
-        })*/
-      }
-    },
-    // 关闭摄像头
-    closeCamera() {
-      const stream = document.getElementById('video').srcObject
-      const tracks = stream.getTracks()
-      tracks.forEach(function(track) {
-        track.stop()
-      })
-      document.getElementById('video').srcObject = null
-    },
-    // 将摄像头截图的base64串转化为file提交后台
-    base64ToFile(urlData, fileName) {
-      const arr = urlData.split(',')
-      const mime = arr[0].match(/:(.*?);/)[1]
-      const bytes = atob(arr[1]) // 解码base64
-      let n = bytes.length
-      const ia = new Uint8Array(n)
-      while (n--) {
-        ia[n] = bytes.charCodeAt(n)
-      }
-      return new File([ia], fileName, { type: mime })
-    },
-    // ****************************************************************************************************************
-    // 上传用户考试信息进入后台
-    async uploadExamToAdmin() {
-      if (this.cameraOn) await this.takePhoto()// 结束的时候拍照上传一张
-      // 正则
-      var reg = new RegExp('-', 'g')
-      // 去掉用户输入的非法分割符号(-),保证后端接受数据处理不报错
-      this.userAnswer.forEach((item, index) => {
-        // 简答题答案处理
-        if (this.questionList[index].questionType === 5 ||
-          this.questionList[index].questionType === 6 ||
-          this.questionList[index].questionType === 7) {
-          this.userAnswer[index] = item.replace(reg, ' ')
-        }
-      })
-
-      // 标记题目是否全部做完
-      let flag = true
-      for (let i = 0; i < this.userAnswer.length; i++) { // 检测用户是否题目全部做完
-        if (this.userAnswer[i] === undefined) {
-          flag = false
-        }
-      }
-      // 如果用户所有答案的数组长度小于题目长度,这个时候也要将标志位置为false
-      if (this.userAnswer.length < this.questionList.length) {
-        flag = false
-      }
-
-      // 题目未做完
-      if (!flag) {
-        this.$confirm('当前试题暂未做完, 是否继续提交o(╥﹏╥)o ?', 'Tips', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning'
-        }).then(() => {
-          this.submitUserPayload(false)
-        }).catch(() => {
-          this.$notify({
-            title: 'Tips',
-            message: '继续加油! *^▽^*',
-            type: 'success',
-            duration: 2000
-          })
-        })
-      } else { // 当前题目做完了
-        if (this.cameraOn) {
-          // 结束的时候拍照上传一张
-          await this.takePhoto()
-          this.closeCamera()
-        }
-
-        this.submitUserPayload(false)
-      }
-    },
-    submitUserPayload(autoSubmit) {
-      var msg = '考试结束了捏 *^▽^*'
-      if (autoSubmit) {
-        msg = '考试时间结束,已为您自动提交 *^▽^*'
-      }
-
-      const userAnswers = []
-      console.log(this.questionList)
-      console.log(this.userAnswer)
-      this.questionList.forEach((item, index) => {
-        let answer
-        if (this.userAnswer[index] === undefined) {
-          answer = '-'
-        } else {
-          answer = this.userAnswer[index]
-        }
-        userAnswers.push({
-          pos: item.pos,
-          questionId: item.questionId,
-          answer: answer
-        })
-      })
-
-      const payload = {}
-      payload.paperId = parseInt(this.$route.params.paperId)
-      payload.answers = userAnswers
-      console.log(payload)
-
-      const userPayload = {}
-      userPayload.questionIds = []
-      userPayload.userAnswers = this.userAnswer.join('-')
-      this.questionList.forEach((item, index) => {
-        userPayload.questionIds.push(item.questionId)
-        // 当前数据不完整,用户回答不完整(我们自动补充空答案,防止业务出错)
-        if (index > this.userAnswer.length) {
-          userPayload.userAnswers += ' -'
-        }
-      })
-
-      // 如果所有题目全部未答
-      if (userPayload.userAnswers === '') {
-        this.questionList.forEach(item => {
-          userPayload.userAnswers += ' -'
-        })
-        userPayload.userAnswers.split(0, userPayload.userAnswers.length - 1)
-      }
-      userPayload.examId = parseInt(this.$route.params.paperId)
-      userPayload.questionIds = userPayload.questionIds.join(',')
-      userPayload.creditImgUrl = this.takePhotoUrl.join(',')
-      submitExam(payload).then((resp) => {
-        if (resp.code === 0) {
-          this.$notify({
-            title: 'Tips',
-            message: msg,
-            type: 'success',
-            duration: 2000
-          })
-          const resultId = resp.data
-          this.$router.push('/exam/result/' + resultId)
-        }
-      }).catch(error => {
-        this.$notify({
-          title: '提示',
-          message: error.message,
-          type: 'error',
-          duration: 3000
-        })
-      })
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-* {
-  font-weight: 800;
-}
-
-.el-container {
-  width: 100%;
-  height: 100%;
-}
-
-.startExam {
-  color: #160f58;
-  border-bottom: 4px solid #ffd550;
-  font-size: 18px;
-  font-weight: 700;
-  padding-bottom: 10px
-}
-
-.examTitle {
-  font-size: 18px;
-  color: #cbcacf;
-  margin-left: 20px;
-  font-weight: 700;
-}
-
-.el-radio-group label {
-  display: block;
-  width: 400px;
-  padding: 48px 20px 10px 20px;
-  border-radius: 4px;
-  border: 1px solid #dcdfe6;
-  margin-bottom: 10px;
-  cursor: pointer;
-  position: relative;
-
-  span {
-    position: absolute;
-    top: 50%;
-    transform: translateY(-50%);
-    font-size: 16px;
-  }
-}
-
-.el-radio-group label:hover {
-  background-color: rgb(245, 247, 250);
-}
-
-/*当前选中的答案*/
-.active {
-  border: 1px solid #1f90ff !important;
-  opacity: .5;
-}
-
-/*做过的题目的高亮颜色*/
-.done {
-  background-color: rgb(87, 148, 247);
-}
-
-/*未做题目的高亮颜色*/
-.noAnswer {
-  background-color: rgb(238, 238, 238);
-}
-
-/*当前在做的题目高亮的颜色*/
-.orange {
-  background-color: rgb(255, 213, 80);
-}
-
-.num {
-  display: inline-block;
-  background: url('../../assets/img/examTitle.png') no-repeat 95%;
-  background-size: contain;
-  height: 37px;
-  width: 37px;
-  line-height: 30px;
-  color: #fff;
-  font-size: 20px;
-  text-align: center;
-  margin-right: 15px;
-}
-</style>

+ 5 - 9
src/views/exam/ExamDashboard.vue

@@ -14,12 +14,13 @@
             <el-step title="科目" icon="el-icon-s-grid" />
             <el-step title="试题" icon="el-icon-tickets" />
             <el-step title="试卷" icon="el-icon-notebook-2" />
+            <el-step title="测评" icon="el-icon-notebook-2" />
           </el-steps>
           <el-steps :active="0" finish-status="success" simple style="margin-top: 20px">
-            <el-step title="试卷列表" icon="el-icon-files" />
-            <el-step title="试卷测评" icon="el-icon-loading" />
-            <el-step title="试卷批改" icon="el-icon-edit" />
-            <el-step title="试卷结果" icon="el-icon-s-data" />
+            <el-step title="测评列表" icon="el-icon-files" />
+            <el-step title="测评试卷" icon="el-icon-loading" />
+            <el-step title="测评阅卷" icon="el-icon-edit" />
+            <el-step title="测评结果" icon="el-icon-s-data" />
           </el-steps>
         </div>
       </el-card>
@@ -44,9 +45,4 @@ export default {
 </script>
 
 <style scoped>
-.movie-list {
-  padding-top: 5px;
-  padding-left: 5px;
-  padding-right: 5px;
-}
 </style>

+ 43 - 51
src/views/exam/ExamMarkIndex.vue

@@ -3,17 +3,17 @@
     <el-header height="220">
       <el-row>
         <el-select
-          v-model="queryInfo.subjectId"
+          v-model="queryParams.examId"
           clearable
-          placeholder="请选择试卷"
+          placeholder="请选择测评"
           style="margin-left: 5px"
           @change="paperChange"
         >
           <el-option
-            v-for="item in allPapers"
-            :key="item.key"
-            :label="item.value"
-            :value="item.key"
+            v-for="item in examList"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
           />
         </el-select>
       </el-row>
@@ -32,21 +32,23 @@
         />
         <el-table-column
           prop="name"
-          label="试卷名称"
-          width="150"
+          label="测评名"
+        />
+        <el-table-column
+          prop="name"
+          label="试卷名"
         />
         <el-table-column
           prop="examTime"
-          label="考试时间"
-          width="150"
+          label="测评时间"
         />
         <el-table-column
           prop="totalScore"
           label="考试总分"
         />
         <el-table-column
-          prop="student"
-          label="参考学生"
+          prop="evalUser"
+          label="测评用户"
         />
         <el-table-column
           fixed="right"
@@ -57,7 +59,7 @@
             <el-button
               size="mini"
               type="warning"
-              @click="markPaper(scope.$index, scope.row)"
+              @click="markPaper(scope.row)"
             >去批改</el-button>
           </template>
         </el-table-column>
@@ -78,7 +80,7 @@
 </template>
 
 <script>
-import { getExamMarkList, getPaperKeyValues } from '@/api/exam'
+import { getExamMarkList, getMarkPapers } from '@/api/exam'
 
 export default {
   name: 'ExamMarkIndex',
@@ -91,75 +93,65 @@ export default {
       totalSize: 0,
       dataList: [],
       // **********************************************************************
-      queryInfo: {
+      queryParams: {
+        examId: '',
         pageNumber: 1,
-        pageSize: 10,
-        paperId: null,
-        marked: false
+        pageSize: 10
       },
-      allPapers: [],
-      allSubject: []
+      examList: []
     }
   },
   created() {
     document.title = '阅卷管理'
-    this.getData(this.queryInfo)
+    this.getData(this.queryParams)
     this.getPapers()
   },
   methods: {
     handleCurrentChange(pageNumber) {
       this.currentPage = pageNumber
-      this.queryInfo.pageNumber = this.currentPage
-      this.queryInfo.pageSize = this.pageSize
-      this.getData(this.queryInfo)
+      this.queryParams.pageNumber = this.currentPage
+      this.queryParams.pageSize = this.pageSize
+      this.getData(this.queryParams)
       // 回到顶部
       scrollTo(0, 0)
     },
-    getData(queryInfo) {
-      getExamMarkList(queryInfo).then(resp => {
+    getData(queryParams) {
+      getExamMarkList(queryParams).then(resp => {
         if (resp.code === 0) {
           this.dataList = resp.data.list
           this.totalSize = resp.data.totalSize
         } else {
-          this.$notify({
-            title: '提示',
-            message: resp.msg,
-            type: 'warning',
-            duration: 3000
-          })
+          this.$message.warning(resp.msg)
         }
       }).catch(error => {
-        this.$notify({
-          title: '提示',
-          message: error.message,
-          type: 'error',
-          duration: 3000
-        })
+        this.$message.error(error.message)
       })
     },
     getPapers() {
-      getPaperKeyValues().then(resp => {
+      getMarkPapers().then(resp => {
         if (resp.code === 0) {
-          this.allPapers = resp.data
+          this.examList = resp.data
+          this.getData(this.queryParams)
+        } else {
+          this.$message.warning(resp.msg)
         }
       })
     },
     // 试卷变化
     paperChange(val) {
-      this.queryInfo.paperId = val
-      this.queryInfo.pageNumber = this.currentPage
-      this.queryInfo.pageSize = this.pageSize
-      this.getData(this.queryInfo)
+      this.queryParams.examId = val
+      this.queryParams.pageNumber = this.currentPage
+      this.queryParams.pageSize = this.pageSize
+      this.getData(this.queryParams)
     },
-    markPaper(index, row) {
-      const paperId = row.paperId
-      const userId = row.userId
+    markPaper(row) {
+      const examId = row.examId
+      const evalUser = row.evalUser
       const routeUrl = this.$router.resolve({
-        path: '/exam/paper/preview',
+        path: '/exam/paper/mark',
         query: {
-          paperId: paperId,
-          viewType: 3,
-          userId: userId
+          examId: examId,
+          userId: evalUser
         }
       })
       window.open(routeUrl.href, '_blank')

+ 0 - 731
src/views/exam/ExamPaperAdd.vue

@@ -1,731 +0,0 @@
-<template>
-  <el-container>
-    <el-header height="220">
-      <el-steps :active="curStep" simple>
-        <el-step title="组卷配置" icon="el-icon-edit" />
-        <el-step title="考试权限" icon="el-icon-lock" />
-        <el-step title="补充配置" icon="el-icon-setting" />
-      </el-steps>
-
-      <el-button v-show="curStep !== 1" style="margin-top: 10px" @click="curStep--">
-        上一步
-      </el-button>
-      <el-button v-show="curStep !== 3" style="float:right;margin-top: 10px" type="primary" @click="curStep++">
-        下一步
-      </el-button>
-      <el-button v-show="curStep === 3" style="float:right;margin-top: 10px" type="primary" @click="createExamPaper">
-        创建试卷
-      </el-button>
-    </el-header>
-    <el-main>
-      <!--设置试题信息-->
-      <el-card v-show="curStep === 1">
-        <el-radio-group
-          v-model="makeModel"
-          size="medium"
-          @change="makeModelChange"
-        >
-          <el-radio :label="1" border>自由组卷</el-radio>
-          <el-radio :label="2" border>题库组卷</el-radio>
-        </el-radio-group>
-        <span v-show="makeModel === 1" style="float: right;color: red;font-weight: bold">
-          {{ '试卷总分:' + sumTotalScore }}
-        </span>
-        <!-- 自由组卷内容-->
-        <div v-show="makeModel === 1" style="margin-top: 25px">
-          <el-button icon="el-icon-plus" size="mini" @click="showAddDialog">添加试题</el-button>
-          <el-table :data="addExamQuestion1" border style="margin-top: 10px">
-            <el-table-column type="index" label="顺序" />
-            <el-table-column label="题目内容" align="center">
-              <template slot-scope="scope">
-                {{ scope.row.content.substr(0, 20) }}
-              </template>
-            </el-table-column>
-            <el-table-column
-              align="center"
-              label="题目类型"
-              prop="type"
-            />
-            <el-table-column label="单题分数" align="center">
-              <template slot-scope="scope">
-                <div v-if="scope.row.typeCode !== 8">
-                  <el-input-number v-model="scope.row.score" :min="1" :max="20" style="margin-left: 5px" />
-                </div>
-                <div v-else>
-                  <el-button icon="el-icon-plus" size="mini" @click="showGroupDialog(scope.row.questionId)">
-                    设置组合题分数
-                  </el-button>
-                  <span>{{scope.row.score}}</span>
-                </div>
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" width="80" align="center">
-              <template slot-scope="scope">
-                <el-button
-                  type="danger"
-                  icon="el-icon-delete"
-                  circle
-                  @click="delQuestion(scope.row.questionId)"
-                />
-              </template>
-            </el-table-column>
-          </el-table>
-        </div>
-        <!-- 题库组卷内容-->
-        <div v-show="makeModel === 2" style="margin-top: 25px">
-          <el-button icon="el-icon-plus" size="mini" @click="addBank">添加题库</el-button>
-          <!--存放题目的信息-->
-          <el-table :data="addExamQuestion2" border style="margin-top: 10px">
-            <el-table-column label="题库" width="155" align="center">
-              <template slot-scope="scope">
-                <el-select
-                  v-model="scope.row.bankName"
-                  clearable
-                  placeholder="请选择题库"
-                  style="margin-left: 5px"
-                >
-                  <el-option
-                    v-for="item in bankList"
-                    :key="item.bankId"
-                    :label="item.bankName"
-                    :value="item.bankName"
-                  />
-                </el-select>
-              </template>
-            </el-table-column>
-
-            <el-table-column label="单选题分数" align="center">
-              <template slot-scope="scope">
-                <el-input v-model="scope.row.singleScore" style="margin-left: 5px" />
-              </template>
-            </el-table-column>
-            <el-table-column label="多选题分数" align="center">
-              <template slot-scope="scope">
-                <el-input v-model="scope.row.multipleScore" style="margin-left: 5px" />
-              </template>
-            </el-table-column>
-            <el-table-column label="判断题分数" align="center">
-              <template slot-scope="scope">
-                <el-input v-model="scope.row.judgeScore" style="margin-left: 5px" />
-              </template>
-            </el-table-column>
-            <el-table-column label="简答题分数" align="center">
-              <template slot-scope="scope">
-                <el-input v-model="scope.row.shortScore" style="margin-left: 5px" />
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" width="80" align="center">
-              <template slot-scope="scope">
-                <el-button type="danger" icon="el-icon-delete" circle @click="delBank(scope.row.bankId)" />
-              </template>
-            </el-table-column>
-          </el-table>
-        </div>
-      </el-card>
-
-      <!--设置考试权限-->
-      <el-card v-show="curStep === 2">
-        <el-radio-group v-model="examAuthority" size="medium">
-          <el-radio :label="1" border>完全公开</el-radio>
-          <el-radio :label="2" border>需要密码</el-radio>
-        </el-radio-group>
-
-        <el-alert
-          style="margin-top: 15px"
-          :title="examAuthority === 1? '开放的,任何人都可以进行考试!' : '半开放的,知道密码的人员才可以考试!'"
-          type="warning"
-        />
-
-        <el-input
-          v-show="examAuthority === 2"
-          v-model="examPassword"
-          style="margin-top: 15px;width: 20%"
-          type="password"
-          show-password
-          placeholder="输入考试密码"
-        />
-      </el-card>
-
-      <!--设置考试信息-->
-      <el-card v-show="curStep === 3">
-        <el-form ref="examInfoForm" :model="examInfo" :rules="examInfoRules" label-width="100px">
-          <el-form-item label="考试名称" prop="examName">
-            <el-input v-model="examInfo.examName" />
-          </el-form-item>
-          <el-form-item label="考试描述" prop="examDesc">
-            <el-input v-model="examInfo.examDesc" />
-          </el-form-item>
-          <el-form-item v-show="makeModel === 1" label="总分数" prop="totalScore">
-            <el-input-number :value="sumTotalScore" :disabled="true" />
-          </el-form-item>
-          <el-form-item label="及格分数" prop="passScore">
-            <el-input-number v-model="examInfo.passScore" :min="1" :max="parseInt(sumTotalScore)" />
-          </el-form-item>
-          <el-form-item label="考试时长(分钟)" prop="examDuration">
-            <el-input-number v-model="examInfo.examDuration" :min="1" :max="120" />
-          </el-form-item>
-          <el-form-item label="考试开始时间" prop="startTime">
-            <el-date-picker
-              v-model="examInfo.startTime"
-              style="margin-left: 5px"
-              type="datetime"
-              placeholder="考试开始时间"
-            />
-          </el-form-item>
-          <el-form-item label="考试结束时间" prop="endTime">
-            <el-date-picker
-              v-model="examInfo.endTime"
-              style="margin-left: 5px"
-              type="datetime"
-              placeholder="考试结束时间"
-            />
-          </el-form-item>
-        </el-form>
-      </el-card>
-    </el-main>
-
-    <el-dialog title="添加试卷试题" :visible.sync="showQuestionDialog" width="80%" center>
-      <el-row>
-        <el-select
-          v-model="queryInfo.subjectId"
-          clearable
-          placeholder="请选择科目"
-          style="margin-left: 5px"
-          @change="subjectChange"
-        >
-          <el-option
-            v-for="item in allSubject"
-            :key="item.label"
-            :label="item.label"
-            :value="item.value"
-          />
-        </el-select>
-        <el-select v-model="queryInfo.questionType" clearable placeholder="请选择试题类型" @change="typeChange">
-          <el-option
-            v-for="item in allType"
-            :key="item.label"
-            :label="item.label"
-            :value="item.value"
-          />
-        </el-select>
-        <el-input
-          v-model="queryInfo.questionContent"
-          placeholder="题目内容"
-          style="margin-left: 5px;width: 220px"
-          prefix-icon="el-icon-search"
-          @blur="getQuestionInfo"
-        />
-        <el-button type="primary" style="float: right" @click="addQuToFree">确认{{ selectedTable.length }}项</el-button>
-      </el-row>
-      <el-table
-        ref="questionTable"
-        v-loading="loading"
-        highlight-current-row
-        :border="true"
-        :data="dataList"
-        tooltip-effect="dark"
-        style="width: 100%;margin-top: 25px;"
-        @selection-change="handleTableSectionChange"
-      >
-        <el-table-column
-          align="center"
-          type="selection"
-          width="55"
-        />
-        <el-table-column
-          align="center"
-          label="题目类型"
-          prop="type"
-        />
-        <el-table-column align="center" label="题目内容">
-          <template slot-scope="scope">
-            <span class="quContent">{{ scope.row.content.substr(0, 20) }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column
-          align="center"
-          label="难度"
-          prop="level"
-        />
-        <el-table-column
-          align="center"
-          prop="subject"
-          label="所属科目"
-        />
-        <el-table-column
-          align="center"
-          prop="createBy"
-          label="创建人"
-        />
-
-        <el-table-column
-          align="center"
-          label="创建时间"
-        >
-          <template slot-scope="scope">
-            {{ scope.row.createAt }}
-          </template>
-        </el-table-column>
-      </el-table>
-      <!--分页-->
-      <el-pagination
-        style="margin-top: 25px"
-        layout="total, sizes, prev, pager, next, jumper"
-        :page-sizes="[10, 20, 50]"
-        :page-size="pageSize"
-        :current-page="currentPage"
-        :total="totalSize"
-        @current-change="handleCurrentChange"
-        @prev-click="handleCurrentChange"
-        @next-click="handleCurrentChange"
-      />
-    </el-dialog>
-    <el-dialog title="组合题分数设置" :visible.sync="showGroupQuestionDialog" width="80%" center>
-      <el-button icon="el-icon-plus" size="mini" @click="confirmChildScore">确定</el-button>
-      <el-table
-        highlight-current-row
-        :border="true"
-        :data="groupQuestionList"
-        tooltip-effect="dark"
-        style="width: 100%;margin-top: 25px;"
-      >
-        <el-table-column
-          align="center"
-          type="selection"
-          width="55"
-        />
-        <el-table-column
-          align="center"
-          label="题目类型"
-          prop="type"
-        />
-        <el-table-column align="center" label="题目内容">
-          <template slot-scope="scope">
-            <span class="quContent">{{ scope.row.content.substr(0, 20) }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column
-          align="center"
-          label="单题分数"
-          prop="score"
-        >
-          <template slot-scope="scope">
-            <el-input-number v-model="scope.row.score" :min="1" :max="20" style="margin-left: 5px" />
-          </template>
-        </el-table-column>
-        <el-table-column
-          align="center"
-          prop="subject"
-          label="所属科目"
-        />
-      </el-table>
-    </el-dialog>
-  </el-container>
-</template>
-
-<script>
-import { getQuestions, getQuestionType, getSubjectKV, addPaper, getQuestion } from '@/api/exam'
-import { validFormAndInvoke } from '@/utils/util'
-
-export default {
-  name: 'ExamPaperAdd',
-  props: ['tagInfo'],
-  data() {
-    return {
-      currentPage: 1,
-      pageSize: 20,
-      totalSize: 0,
-      dataList: [],
-      // 查询题目的参数
-      queryInfo: {
-        subjectId: null,
-        type: null,
-        level: null,
-        pageNumber: 1,
-        pageSize: 10
-      },
-      // 所有题库信息
-      allSubject: [
-      ],
-      bankList: [
-         {
-           bankId: 1,
-           bankName: '默认题库'
-         }
-      ],
-      allType: [],
-      // 当前的步骤
-      curStep: 1,
-      // 组卷模式
-      makeModel: 1,
-      // 添加考试题目信息(makeModel = 1的时候)
-      addExamQuestion1: [],
-      // 添加考试题目信息(makeModel = 2 的时候)
-      addExamQuestion2: [],
-      // 所有题目的对话框
-      groupQuestionId: null,
-      showQuestionDialog: false,
-      showGroupQuestionDialog: false,
-      groupQuestionList: [],
-      // 对话框中题目表格的加载
-      loading: true,
-      // 所有题目的信息
-      questionInfo: [],
-      // 所有题目的对话框中表格被选中
-      selectedTable: [],
-      // 所有题目总数
-      total: 0,
-      // 考试权限(1公开, 2密码)
-      examAuthority: 1,
-      // 考试密码(权限为2时的密码)
-      examPassword: '',
-      // 补充的考试信息
-      examInfo: {
-        examId: null,
-        examDesc: null,
-        passScore: 0,
-        examDuration: 0,
-        startTime: null,
-        endTime: null
-      },
-      // 补充的考试信息的表单验证
-      examInfoRules: {
-        examName: [
-          {
-            required: true,
-            message: '请输入考试名称',
-            trigger: 'blur'
-          }
-        ],
-        passScore: [
-          {
-            required: true,
-            message: '请输入通过分数',
-            trigger: 'blur'
-          }
-        ],
-        examDuration: [
-          {
-            required: true,
-            message: '请输入考试时长',
-            trigger: 'blur'
-          }
-        ]
-      }
-    }
-  },
-  computed: {
-    // 计算总分
-    sumTotalScore() {
-      if (this.makeModel === 1) {
-        let score = 0
-        this.addExamQuestion1.forEach(item => {
-          score += parseInt(item.score)
-        })
-        return score
-      }
-    }
-  },
-  created() {
-    document.title = '组卷管理'
-    // 一创建就改变头部的面包屑
-    this.$emit('giveChildChangeBreakInfo', '添加考试', '添加考试')
-    // this.createTagsInParent()
-    this.getSubjects()
-    this.getQuestionTypes()
-  },
-  methods: {
-    // 向父组件中添加头部的tags标签
-    createTagsInParent() {
-      let flag = false
-      this.tagInfo.map(item => {
-        // 如果tags全部符合
-        if (item.name === '添加考试' && item.url === this.$route.path) {
-          flag = true
-        } else if (item.name === '添加考试' && item.url !== this.$route.path) {
-          this.$emit('updateTagInfo', '添加考试', this.$route.path)
-          flag = true
-        }
-      })
-      if (!flag) this.$emit('giveChildAddTag', '添加考试', this.$route.path)
-    },
-    // 获取所有的题库信息
-    getSubjects() {
-      getSubjectKV().then((resp) => {
-        if (resp.code === 0) {
-          this.allSubject = resp.data
-        } else {
-          this.$notify({
-            title: 'Tips',
-            message: resp.message,
-            type: 'error',
-            duration: 2000
-          })
-        }
-      })
-    },
-    getQuestionTypes() {
-      getQuestionType().then((resp) => {
-        if (resp.code === 0) {
-          this.allType = resp.data
-        } else {
-          this.$notify({
-            title: 'Tips',
-            message: resp.message,
-            type: 'error',
-            duration: 2000
-          })
-        }
-      })
-    },
-    // 删除当前需要去除的题库
-    delBank(bankId) {
-      this.addExamQuestion2.forEach((item, index) => {
-        if (item.bankId === bankId) this.addExamQuestion2.splice(index, 1)
-      })
-    },
-    // 添加题库组卷中的题库
-    addBank() {
-      this.addExamQuestion2.push({
-        'bankId': 1,
-        'bankName': '默认题库',
-        'singleScore': 1,
-        'multipleScore': 1,
-        'judgeScore': 1,
-        'shortScore': 1
-      })
-    },
-    // 自由组卷时添加试题
-    showAddDialog() {
-      this.showQuestionDialog = true
-      this.queryInfo.subjectId = null
-      this.queryInfo.type = null
-      this.queryInfo.level = null
-      this.queryInfo.pageNumber = this.currentPage
-      this.queryInfo.pageSize = this.pageSize
-      this.getQuestionInfo()
-    },
-    // 自由组卷时删除试题
-    delQuestion(questionId) {
-      this.addExamQuestion1.forEach((item, index) => {
-        if (item.questionId === questionId) this.addExamQuestion1.splice(index, 1)
-      })
-    },
-    // 题目类型变化
-    typeChange(val) {
-      this.queryInfo.type = val
-      this.queryInfo.pageNumber = this.currentPage
-      this.queryInfo.pageSize = this.pageSize
-      this.getQuestionInfo()
-    },
-    // 科目变化
-    subjectChange(val) {
-      this.queryInfo.subjectId = val
-      this.queryInfo.pageNumber = this.currentPage
-      this.queryInfo.pageSize = this.pageSize
-      this.getQuestionInfo()
-    },
-    // 获取题目信息
-    getQuestionInfo() {
-      this.dataList = []
-      this.loading = true
-      getQuestions(this.queryInfo).then(resp => {
-        if (resp.code === 0) {
-          this.dataList = resp.data.list
-          this.totalSize = resp.data.totalSize
-          this.loading = false
-        } else {
-          this.$notify({
-            title: '提示',
-            message: resp.msg,
-            type: 'warning',
-            duration: 3000
-          })
-        }
-      }).catch(error => {
-        this.$notify({
-          title: '提示',
-          message: error.message,
-          type: 'error',
-          duration: 3000
-        })
-      })
-    },
-    // 处理表格被选中
-    handleTableSectionChange(val) {
-      this.selectedTable = val
-    },
-    // 分页页面大小改变
-    handleSizeChange(val) {
-      this.queryInfo.pageSize = val
-      this.getQuestionInfo()
-    },
-    // 分页插件的页数
-    handleCurrentChange(val) {
-      this.queryInfo.pageNumber = val
-      this.getQuestionInfo()
-    },
-    // 自由组卷中选中的题目添加进去
-    addQuToFree() {
-      this.selectedTable.forEach(item => {
-        // 不存在有当前题目
-        if (!this.addExamQuestion1.some(i2 => { return i2.questionId === item.questionId })) {
-          this.addExamQuestion1.push({
-            'pos': this.addExamQuestion1.length + 1,
-            'questionId': item.questionId,
-            'content': item.content,
-            'type': item.type,
-            'typeCode': item.typeCode,
-            'score': 1,
-            'children': []
-          })
-        }
-      })
-      this.showQuestionDialog = false
-    },
-    // 组卷模式变化
-    makeModelChange() {
-      this.$confirm('此操作将丢失当前组卷数据, 是否继续?', 'Tips', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        this.makeModel === 1 ? this.addExamQuestion1 = [] : this.addExamQuestion2 = []
-      }).catch(() => {
-      })
-    },
-    // 创建试卷
-    createExamPaper() {
-      validFormAndInvoke(this.$refs['examInfoForm'], () => {
-        if ((this.makeModel === 1 && this.addExamQuestion1.length === 0) ||
-          (this.makeModel === 2 && this.addExamQuestion2.length === 0)) {
-          this.$message.error('试卷还没有添加试题')
-          return false
-        }
-
-        // 构造数据对象(考试信息)
-        const exam = this.examInfo
-        exam.totalScore = this.sumTotalScore
-        exam.status = 1
-        // 权限id设置
-        exam.examAuthority = this.examAuthority
-        if (this.examAuthority === 2) { // 考试密码
-          if (this.examPassword === '') { // 当前用户选择了需要密码权限,但是密码为空
-            this.$message.error('当前权限为需要密码,但是密码为空')
-            return false
-          }
-          exam.examPassword = this.examPassword
-        }
-
-        if (this.makeModel === 1 && !this.addExamQuestion1.some(item => item.bankId === '')) {
-          var questions = []
-          this.addExamQuestion1.forEach(item => {
-            questions.push({
-              pos: item.pos,
-              questionId: item.questionId,
-              type: item.typeCode,
-              score: item.score,
-              children: item.children
-            })
-          })
-          exam.paperQuestions = questions
-          this.addExamPaper(exam)
-        } else if (this.makeModel === 2) {
-          const bankNames = []
-          this.addExamQuestion2.forEach(item => bankNames.push(item.bankName))
-          exam.bankNames = bankNames.join(',')
-          exam.singleScore = this.addExamQuestion2[0].singleScore
-          exam.multipleScore = this.addExamQuestion2[0].multipleScore
-          exam.judgeScore = this.addExamQuestion2[0].judgeScore
-          exam.shortScore = this.addExamQuestion2[0].shortScore
-          this.addExamPaper(exam)
-        } else {
-          this.$message.error('请检查考试规则设置是否完整')
-        }
-      }, '请检查考试规则设置是否完整')
-    },
-    addExamPaper(examInfo) {
-      addPaper(examInfo).then(resp => {
-        if (resp.code === 0) {
-          this.$notify({
-            title: '提示',
-            message: '试卷已创建',
-            type: 'warning',
-            duration: 3000
-          })
-          this.$router.push('/exam/paper')
-        } else {
-          this.$notify({
-            title: '提示',
-            message: resp.msg,
-            type: 'warning',
-            duration: 3000
-          })
-        }
-      }).catch(error => {
-        this.$notify({
-          title: '提示',
-          message: error.message,
-          type: 'error',
-          duration: 3000
-        })
-      })
-    },
-    showGroupDialog(questionId) {
-      this.groupQuestionId = questionId
-      this.showGroupQuestionDialog = true
-      getQuestion(questionId).then(resp => {
-        if (resp.code === 0) {
-          this.groupQuestionList = resp.data.children
-          for (const item of this.groupQuestionList) {
-            item.score = 0
-          }
-        }
-      })
-    },
-    confirmChildScore() {
-      this.showGroupQuestionDialog = false
-      var childIds = []
-      var score = 0
-      for (const item of this.groupQuestionList) {
-        score += item.score
-        childIds.push({
-          pid: this.groupQuestionId,
-          questionId: item.questionId,
-          score: item.score
-        })
-      }
-
-      for (const item of this.addExamQuestion1) {
-        if (item.questionId === this.groupQuestionId) {
-          item.score = score
-          item.children = childIds
-        }
-      }
-    }
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-.el-container {
-  width: 100%;
-  height: 100%;
-}
-
-.el-container {
-  animation: leftMoveIn .7s ease-in;
-}
-
-@keyframes leftMoveIn {
-  0% {
-    transform: translateX(-100%);
-    opacity: 0;
-  }
-  100% {
-    transform: translateX(0%);
-    opacity: 1;
-  }
-}
-</style>

+ 35 - 39
src/views/exam/ExamPaperMark.vue

@@ -294,17 +294,15 @@
 </template>
 
 <script>
-import {getMarkView, submitMarkResult} from '@/api/exam'
+import { getMarkView, submitMarkResult } from '@/api/exam'
 
 export default {
   name: 'ExamPaperMark',
   data() {
     return {
       queryParams: {
-        examId: 0,
-        userId: 0
+        resultId: 0
       },
-      remainTime: 0,
       timeLoading: false,
       currentActiveId: null,
 
@@ -316,24 +314,14 @@ export default {
       sectionsList: []
     }
   },
-  computed: {
-    formatRemainTime() {
-      const hours = Math.floor(this.remainTime / 3600).toString().padStart(2, '0')
-      const mins = Math.floor((this.remainTime % 3600) / 60).toString().padStart(2, '0')
-      const secs = Math.floor(this.remainTime % 60).toString().padStart(2, '0')
-      return `${hours}:${mins}:${secs}`
-    }
-  },
   created() {
     document.title = '阅卷'
-    const examId = this.$route.query.examId
-    const userId = this.$route.query.userId || 0
-    if (!examId) {
+    const resultId = this.$route.query.resultId
+    if (!resultId) {
       this.$message.error('参数缺失,无法加载阅卷页面!')
       return
     }
-    this.queryParams.examId = examId
-    this.queryParams.userId = userId
+    this.queryParams.resultId = resultId
     this.loadPaperData()
   },
   methods: {
@@ -346,8 +334,6 @@ export default {
         }
 
         const examData = resp.data
-        this.remainTime = examData.duration
-
         const remoteData = examData.paperDetail
         this.paperInfo = {
           paperId: remoteData.paperId,
@@ -365,33 +351,39 @@ export default {
           const flatQuestions = []
 
           const ProcessedQuestions = sec.questions.map(q => {
-            // 标准化 options
             if (q.answers && !q.options) {
               q.options = q.answers
             }
 
+            // --- 【优化核心:对普通题型的 userAnswer 进行规范化解包】 ---
+            let normalizedAnswer = ''
+            if (q.userAnswer && Array.isArray(q.userAnswer)) {
+              if (q.typeCode === 2) {
+                // 多选题:保留数组,并转换为字符串数组方便比对
+                normalizedAnswer = q.userAnswer.map(String)
+              } else {
+                // 单选、填空、问答题:解包数组,取出第一个元素,若无则为空字符串
+                normalizedAnswer = q.userAnswer.length > 0 ? q.userAnswer[0] : ''
+              }
+            }
+            localAnswers[q.questionId] = normalizedAnswer
+            localReviews[q.questionId] = ''
+
             // 处理普通非材料题
             if (q.typeCode !== 5) {
               q.number = globalQuestionIndex++
               flatQuestions.push({ questionId: q.questionId, number: q.number, typeCode: q.typeCode })
 
-              // 还原考生提交上来的真实数据
-              localAnswers[q.questionId] = q.userAnswer !== undefined ? q.userAnswer : ''
-              localReviews[q.questionId] = ''
-
-              // 如果是选择题 (1-单选, 2-多选),系统可自动给出判定分数
+              // 自动判分逻辑(基于规范化后的数据)
               if (q.typeCode === 1) {
                 const correctOpt = (q.options || []).find(o => o.correct)
-                const isRight = correctOpt && String(correctOpt.pos) === String(q.userAnswer)
+                const isRight = correctOpt && String(correctOpt.pos) === String(normalizedAnswer)
                 localScores[q.questionId] = isRight ? q.score : 0
               } else if (q.typeCode === 2) {
-                // 假设多选题 userAnswer 是形如 [2, 4] 的数组或逗号分隔串
-                const userArr = Array.isArray(q.userAnswer) ? q.userAnswer.map(String) : []
                 const correctArr = (q.options || []).filter(o => o.correct).map(o => String(o.pos))
-                const isRight = userArr.length === correctArr.length && userArr.every(v => correctArr.includes(v))
+                const isRight = normalizedAnswer.length === correctArr.length && normalizedAnswer.every(v => correctArr.includes(v))
                 localScores[q.questionId] = isRight ? q.score : 0
               } else {
-                // 主观题(填空/问答)默认初始化为满分或0分,等候老师用 el-input-number 细扣
                 localScores[q.questionId] = 0
               }
             } else if (q.typeCode === 5 && q.children) {
@@ -403,17 +395,25 @@ export default {
                 subQ.number = globalQuestionIndex++
                 flatQuestions.push({ questionId: subQ.questionId, number: subQ.number, typeCode: subQ.typeCode })
 
-                localAnswers[subQ.questionId] = subQ.userAnswer !== undefined ? subQ.userAnswer : ''
+                // --- 【子题目的 userAnswer 同样规范化解包】 ---
+                let subNormalizedAnswer = ''
+                if (subQ.userAnswer && Array.isArray(subQ.userAnswer)) {
+                  if (subQ.typeCode === 2) {
+                    subNormalizedAnswer = subQ.userAnswer.map(String)
+                  } else {
+                    subNormalizedAnswer = subQ.userAnswer.length > 0 ? subQ.userAnswer[0] : ''
+                  }
+                }
+                localAnswers[subQ.questionId] = subNormalizedAnswer
                 localReviews[subQ.questionId] = ''
 
                 // 选择子题自动判分
                 if (subQ.typeCode === 1) {
                   const correctOpt = (subQ.options || []).find(o => o.correct)
-                  localScores[subQ.questionId] = (correctOpt && String(correctOpt.pos) === String(subQ.userAnswer)) ? subQ.score : 0
+                  localScores[subQ.questionId] = (correctOpt && String(correctOpt.pos) === String(subNormalizedAnswer)) ? subQ.score : 0
                 } else if (subQ.typeCode === 2) {
-                  const userArr = Array.isArray(subQ.userAnswer) ? subQ.userAnswer.map(String) : []
                   const correctArr = (subQ.options || []).filter(o => o.correct).map(o => String(o.pos))
-                  const isRight = userArr.length === correctArr.length && userArr.every(v => correctArr.includes(v))
+                  const isRight = subNormalizedAnswer.length === correctArr.length && subNormalizedAnswer.every(v => correctArr.includes(v))
                   localScores[subQ.questionId] = isRight ? subQ.score : 0
                 } else {
                   localScores[subQ.questionId] = 0
@@ -441,8 +441,6 @@ export default {
       }
     },
 
-    handleAnswerChange(qId) {},
-
     // 侧边栏辅助:检查该题是否分配了分数
     checkIsGraded(qId) {
       return this.markScores[qId] !== undefined && this.markScores[qId] !== null
@@ -478,9 +476,7 @@ export default {
         type: 'success'
       }).then(() => {
         const jsonData = {}
-        jsonData.examId = 1
-        jsonData.resultId = 1
-        jsonData.userId = 1
+        jsonData.resultId = this.queryParams.resultId
         jsonData.markResults = gradeResults
         submitMarkResult(jsonData).then(resp => {
           if (resp.code === 0) {

+ 653 - 0
src/views/exam/ExamPaperResult.vue

@@ -0,0 +1,653 @@
+<template>
+  <el-container class="exam-container">
+    <!-- 左侧:答题卡/得分卡(使用 sticky 粘性定位,使其在全局滚动时固定在左侧) -->
+    <el-aside width="280px" class="exam-aside">
+      <div class="aside-card answer-card">
+        <h4 class="card-title">答题得分卡</h4>
+        <div v-for="(section, sIndex) in sectionsList" :key="sIndex" class="card-group">
+          <div class="group-label">{{ section.sectionName }}</div>
+          <div class="number-grid">
+            <span
+              v-for="q in section.flatQuestions"
+              :key="q.questionId"
+              class="number-item"
+              :class="getScoreClass(q)"
+              @click="scrollToQuestion(q.questionId)"
+            >
+              {{ q.number }}
+            </span>
+          </div>
+        </div>
+        <!-- 侧边栏底部简易比例说明 -->
+        <div class="score-legend">
+          <span class="legend-item"><i class="dot full" /> 满分</span>
+          <span class="legend-item"><i class="dot part" /> 部分得分</span>
+          <span class="legend-item"><i class="dot zero" /> 错/零分</span>
+        </div>
+      </div>
+    </el-aside>
+
+    <!-- 右侧:试卷题目及判分详情 -->
+    <el-main class="exam-main">
+      <div class="paper-header">
+        <h2 class="paper-title">{{ paperInfo.title }}</h2>
+        <div class="paper-meta">
+          <span>考试科目:{{ paperInfo.subjectName || '未指定' }}</span>
+          <span class="total-score-banner">
+            客观/主观题得分总览请见各题下方
+          </span>
+        </div>
+      </div>
+
+      <div v-for="(section, sIndex) in sectionsList" :key="sIndex" class="question-group-section">
+        <h3 class="group-header-title">
+          {{ section.sectionName }} <span class="group-desc">({{ section.sectionHint }})</span>
+        </h3>
+
+        <div
+          v-for="q in section.questions"
+          :id="'q-' + q.questionId"
+          :key="q.questionId"
+          class="question-card"
+          :class="{ 'is-active': currentActiveId === q.questionId }"
+          @mouseenter="currentActiveId = q.questionId"
+        >
+          <!-- 1. 单选题 -->
+          <template v-if="q.typeCode === 1">
+            <div class="question-stem">
+              <span class="q-number">{{ q.number }}.</span>
+              <span class="q-score-highlight">({{ q.score }}分)</span>
+              <span class="q-text" v-html="formatStem(q.content)" />
+              <el-tag size="mini" type="info">单选</el-tag>
+            </div>
+            <div class="question-body">
+              <div class="options-display-list">
+                <div
+                  v-for="opt in q.answers"
+                  :key="opt.pos"
+                  class="mark-option-item"
+                  :class="{
+                    'opt-correct': opt.correct,
+                    'opt-user-selected': (q.userAnswer || []).includes(opt.pos),
+                    'opt-user-wrong': (q.userAnswer || []).includes(opt.pos) && !opt.correct
+                  }"
+                >
+                  <i v-if="opt.correct" class="el-icon-success text-success" />
+                  <i v-else-if="(q.userAnswer || []).includes(opt.pos)" class="el-icon-error text-danger" />
+                  <i v-else class="el-icon-circle-check text-muted" />
+                  <span class="opt-content">{{ opt.content }}</span>
+                  <el-tag v-if="opt.correct" size="mini" type="success" effect="dark" class="mini-tag">正确答案</el-tag>
+                  <el-tag v-if="(q.userAnswer || []).includes(opt.pos)" size="mini" type="primary" effect="plain" class="mini-tag">考生选择</el-tag>
+                </div>
+              </div>
+              <div class="result-display-bar">
+                <div class="score-tag-wrapper">
+                  结果判定:
+                  <el-tag :type="q.markScore === q.score ? 'success' : 'danger'" effect="dark">
+                    得分:{{ q.markScore }} 分 / 满分 {{ q.score }} 分
+                  </el-tag>
+                </div>
+              </div>
+            </div>
+          </template>
+
+          <!-- 2. 多选题 -->
+          <template v-else-if="q.typeCode === 2">
+            <div class="question-stem">
+              <span class="q-number">{{ q.number }}.</span>
+              <span class="q-score-highlight">({{ q.score }}分)</span>
+              <span class="q-text" v-html="formatStem(q.content)" />
+              <el-tag type="warning" size="mini" class="type-tag">多选</el-tag>
+            </div>
+            <div class="question-body">
+              <div class="options-display-list">
+                <div
+                  v-for="opt in q.answers"
+                  :key="opt.pos"
+                  class="mark-option-item"
+                  :class="{
+                    'opt-correct': opt.correct,
+                    'opt-user-selected': (q.userAnswer || []).includes(opt.pos),
+                    'opt-user-wrong': (q.userAnswer || []).includes(opt.pos) && !opt.correct
+                  }"
+                >
+                  <i v-if="opt.correct" class="el-icon-success text-success" />
+                  <i v-else-if="(q.userAnswer || []).includes(opt.pos)" class="el-icon-error text-danger" />
+                  <i v-else class="el-icon-circle-check text-muted" />
+                  <span class="opt-content">{{ opt.content }}</span>
+                  <el-tag v-if="opt.correct" size="mini" type="success" effect="dark" class="mini-tag">正确答案</el-tag>
+                  <el-tag v-if="(q.userAnswer || []).includes(opt.pos)" size="mini" type="primary" effect="plain" class="mini-tag">考生选择</el-tag>
+                </div>
+              </div>
+              <div class="result-display-bar">
+                <div class="score-tag-wrapper">
+                  结果判定:
+                  <el-tag :type="q.markScore === q.score ? 'success' : (q.markScore > 0 ? 'warning' : 'danger')" effect="dark">
+                    得分:{{ q.markScore }} 分 / 满分 {{ q.score }} 分
+                  </el-tag>
+                </div>
+              </div>
+            </div>
+          </template>
+
+          <!-- 3. 填空题 -->
+          <template v-else-if="q.typeCode === 3">
+            <div class="question-stem">
+              <span class="q-number">{{ q.number }}.</span>
+              <span class="q-score-highlight">({{ q.score }}分)</span>
+              <span class="q-text" v-html="formatStem(q.content)" />
+              <el-tag size="mini" type="primary">填空</el-tag>
+            </div>
+            <div class="question-body subjective-mark-box">
+              <el-row :gutter="20" class="compare-row">
+                <el-col :span="12">
+                  <div class="answer-block user-answer-block">
+                    <div class="block-title"><i class="el-icon-user" /> 考生作答情况</div>
+                    <div class="block-content font-code">
+                      {{ (q.userAnswer && q.userAnswer[0]) || '(未作答)' }}
+                    </div>
+                  </div>
+                </el-col>
+                <el-col :span="12">
+                  <div class="answer-block standard-answer-block">
+                    <div class="block-title"><i class="el-icon-key" /> 参考答案</div>
+                    <div class="block-content font-code">
+                      <div v-for="(opt, oIdx) in q.answers" :key="oIdx">
+                        <span class="text-success font-bold">{{ opt.content }}</span>
+                      </div>
+                    </div>
+                  </div>
+                </el-col>
+              </el-row>
+              <!-- 纯文本展示结果与评语 -->
+              <div class="result-display-bar display-column">
+                <div class="score-display">
+                  最终得分:<span class="score-num" :class="q.markScore > 0 ? 'score-win' : 'score-loss'">{{ q.markScore }}</span> / {{ q.score }} 分
+                </div>
+                <div v-if="q.markReview" class="review-display">
+                  <i class="el-icon-chat-dot-round" /> <strong>评语:</strong>{{ q.markReview }}
+                </div>
+              </div>
+            </div>
+          </template>
+
+          <!-- 4. 问答题 -->
+          <template v-else-if="q.typeCode === 4">
+            <div class="question-stem">
+              <span class="q-number">{{ q.number }}.</span>
+              <span class="q-score-highlight">({{ q.score }}分)</span>
+              <span class="q-text" v-html="formatStem(q.content)" />
+              <el-tag size="mini" type="purple">问答</el-tag>
+            </div>
+            <div class="question-body subjective-mark-box">
+              <div class="answer-block user-answer-block mb-15">
+                <div class="block-title"><i class="el-icon-user" /> 考生答案</div>
+                <div class="block-content text-pre-wrap">{{ (q.userAnswer && q.userAnswer[0]) || '(空白未填写)' }}</div>
+              </div>
+              <div class="answer-block standard-answer-block mb-15">
+                <div class="block-title"><i class="el-icon-key" /> 参考答案</div>
+                <div class="block-content text-pre-wrap">{{ q.answers && q.answers[0] ? q.answers[0].content : '暂无详尽解析' }}</div>
+              </div>
+              <!-- 纯文本展示结果与评语 -->
+              <div class="result-display-bar display-column">
+                <div class="score-display">
+                  最终得分:<span class="score-num" :class="q.markScore > 0 ? 'score-win' : 'score-loss'">{{ q.markScore }}</span> / {{ q.score }} 分
+                </div>
+                <div v-if="q.markReview" class="review-display">
+                  <i class="el-icon-chat-dot-round" /> <strong>评语:</strong>{{ q.markReview }}
+                </div>
+              </div>
+            </div>
+          </template>
+
+          <!-- 5. 组合题 -->
+          <template v-else-if="q.typeCode === 5">
+            <div class="question-stem material-stem">
+              <span class="material-tag">【组合材料】</span>
+              <div class="q-text" style="white-space: pre-wrap;">{{ q.content }}</div>
+            </div>
+
+            <div class="sub-questions-container">
+              <div v-for="subQ in q.children" :key="subQ.questionId" class="sub-question-item">
+                <div class="question-stem sub-stem">
+                  <span class="q-number">{{ subQ.number }}.</span>
+                  <span class="q-score-highlight">({{ subQ.score }}分)</span>
+                  <span class="q-text">{{ subQ.content }}</span>
+                  <el-tag size="mini" effect="plain" type="info" style="margin-left:5px;">{{ subQ.typeStr }}</el-tag>
+                </div>
+
+                <div class="question-body">
+                  <!-- 组合题 - 选择题型子题 -->
+                  <template v-if="subQ.typeCode === 1 || subQ.typeCode === 2">
+                    <div class="options-display-list">
+                      <div
+                        v-for="opt in subQ.answers"
+                        :key="opt.pos"
+                        class="mark-option-item"
+                        :class="{
+                          'opt-correct': opt.correct,
+                          'opt-user-selected': (subQ.userAnswer || []).includes(opt.pos),
+                          'opt-user-wrong': (subQ.userAnswer || []).includes(opt.pos) && !opt.correct
+                        }"
+                      >
+                        <i v-if="opt.correct" class="el-icon-success text-success" />
+                        <i v-else-if="(subQ.userAnswer || []).includes(opt.pos)" class="el-icon-error text-danger" />
+                        <i v-else class="el-icon-circle-check text-muted" />
+                        <span class="opt-content">{{ opt.content }}</span>
+                      </div>
+                    </div>
+                    <div class="result-display-bar">
+                      <div class="score-tag-wrapper">
+                        结果判定:
+                        <el-tag :type="subQ.markScore === subQ.score ? 'success' : (subQ.markScore > 0 ? 'warning' : 'danger')" size="mini" effect="dark">
+                          得分:{{ subQ.markScore }} 分 / 满分 {{ subQ.score }} 分
+                        </el-tag>
+                      </div>
+                    </div>
+                  </template>
+
+                  <!-- 组合题 - 主观题型子题 (填空/问答) -->
+                  <template v-else-if="subQ.typeCode === 3 || subQ.typeCode === 4">
+                    <el-row :gutter="20" class="compare-row mb-10">
+                      <el-col :span="12">
+                        <div class="answer-block user-answer-block">
+                          <div class="block-title">考生答案</div>
+                          <div class="block-content text-pre-wrap">{{ (subQ.userAnswer && subQ.userAnswer[0]) || '(未作答)' }}</div>
+                        </div>
+                      </el-col>
+                      <el-col :span="12">
+                        <div class="answer-block standard-answer-block">
+                          <div class="block-title">参考答案</div>
+                          <div class="block-content text-pre-wrap">
+                            <template v-if="subQ.typeCode === 3">
+                              <div v-for="(opt, oIdx) in subQ.answers" :key="oIdx">{{ opt.content }}</div>
+                            </template>
+                            <template v-else>
+                              {{ subQ.options && subQ.options[0] ? subQ.options[0].content : '无解析' }}
+                            </template>
+                          </div>
+                        </div>
+                      </el-col>
+                    </el-row>
+                    <div class="result-display-bar display-column">
+                      <div class="score-display">
+                        最终得分:<span class="score-num" :class="subQ.markScore > 0 ? 'score-win' : 'score-loss'">{{ subQ.markScore }}</span> / {{ subQ.score }} 分
+                      </div>
+                      <div v-if="subQ.markReview" class="review-display">
+                        <i class="el-icon-chat-dot-round" /> <strong>评语:</strong>{{ subQ.markReview }}
+                      </div>
+                    </div>
+                  </template>
+                </div>
+              </div>
+            </div>
+          </template>
+
+        </div>
+      </div>
+    </el-main>
+  </el-container>
+</template>
+
+<script>
+import { getExamResult } from '@/api/exam'
+
+export default {
+  name: 'ExamResult',
+  data() {
+    return {
+      queryParams: {
+        resultId: 0
+      },
+      currentActiveId: null,
+      paperInfo: {},
+      sectionsList: []
+    }
+  },
+  created() {
+    document.title = '试卷结果'
+    const resultId = this.$route.query.resultId
+    if (!resultId) {
+      this.$message.error('参数缺失,无法加载试卷结果页面!')
+      return
+    }
+    this.queryParams.resultId = resultId
+    this.loadPaperData()
+  },
+  methods: {
+    async loadPaperData() {
+      const response = await getExamResult(this.queryParams)
+      const paperDetail = response.data.paperDetail || {}
+      this.paperInfo = {
+        title: paperDetail.title,
+        subjectName: paperDetail.subjectName
+      }
+
+      let questionNumberCounter = 1
+      this.sectionsList = (paperDetail.sections || []).map(section => {
+        const flatQuestions = []
+
+        const processedQuestions = (section.questions || []).map(q => {
+          q.number = questionNumberCounter++
+
+          if (q.typeCode === 5 && q.children) {
+            q.children.forEach((child, index) => {
+              child.number = `${q.number}.${index + 1}`
+              flatQuestions.push({
+                questionId: child.questionId,
+                number: child.number,
+                markScore: child.markScore,
+                score: child.score
+              })
+            })
+          } else {
+            flatQuestions.push({
+              questionId: q.questionId,
+              number: q.number,
+              markScore: q.markScore,
+              score: q.score
+            })
+          }
+          return q
+        })
+
+        return {
+          ...section,
+          questions: processedQuestions,
+          flatQuestions: flatQuestions
+        }
+      })
+    },
+
+    getScoreClass(q) {
+      const classes = []
+      if (this.currentActiveId === q.questionId) {
+        classes.push('is-active')
+      }
+
+      if (q.markScore === q.score) {
+        classes.push('score-full')
+      } else if (q.markScore > 0 && q.markScore < q.score) {
+        classes.push('score-part')
+      } else {
+        classes.push('score-zero')
+      }
+      return classes
+    },
+
+    scrollToQuestion(id) {
+      this.currentActiveId = id
+      const el = document.getElementById('q-' + id)
+      if (el) {
+        // 全局滚动条模式下,靠最外层的 window 或 html/body 滚动
+        el.scrollIntoView({ behavior: 'smooth', block: 'center' })
+      }
+    },
+
+    formatStem(content) {
+      if (!content) return ''
+      return content.replace(/\n/g, '<br/>')
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* 核心改动:移除了 height: 100vh 限制,允许被内容撑高,交由最外层产生全局滚动条 */
+.exam-container {
+  min-height: 100vh;
+  background-color: #f5f7fa;
+  display: flex;
+  align-items: flex-start; /* 关键:防止 Aside 默认拉伸填满不正常的高度 */
+}
+
+/* 核心改动:左侧答题卡启用 position: sticky。这样在页面整体向下滚动时,答题卡能悬浮固定在视口顶部 */
+.exam-aside {
+  background: #fff;
+  border-right: 1px solid #e6ebf5;
+  padding: 15px;
+  position: sticky;
+  top: 20px; /* 距离浏览器视口顶部的悬浮间距 */
+  max-height: calc(100vh - 40px); /* 限制左侧自身最大高度防止其超出视口 */
+  overflow-y: auto; /* 若答题卡题号过多,左侧内部可自适应微调滚动 */
+}
+
+.aside-card {
+  height: 100%;
+}
+.card-title {
+  font-size: 16px;
+  margin-bottom: 15px;
+  color: #303133;
+  border-left: 4px solid #409eff;
+  padding-left: 8px;
+}
+.card-group {
+  margin-bottom: 20px;
+}
+.group-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 10px;
+}
+.number-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.number-item {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 4px;
+  font-size: 12px;
+  cursor: pointer;
+  border: 1px solid #dcdfe6;
+  transition: all 0.2s;
+}
+
+.score-full {
+  background-color: #f0f9eb;
+  border-color: #c2e7b0;
+  color: #67c23a;
+}
+.score-part {
+  background-color: #fdf6ec;
+  border-color: #f5dab1;
+  color: #e6a23c;
+}
+.score-zero {
+  background-color: #fef0f0;
+  border-color: #fde2e2;
+  color: #f56c6c;
+}
+.number-item.is-active {
+  box-shadow: 0 0 0 2px #409eff inset;
+  font-weight: bold;
+}
+
+.score-legend {
+  margin-top: 30px;
+  border-top: 1px dashed #e6ebf5;
+  padding-top: 15px;
+  font-size: 12px;
+  color: #606266;
+}
+.legend-item {
+  display: inline-flex;
+  align-items: center;
+  margin-right: 15px;
+}
+.dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: 4px;
+}
+.dot.full { background: #67c23a; }
+.dot.part { background: #e6a23c; }
+.dot.zero { background: #f56c6c; }
+
+/* 核心改动:移除 overflow-y: auto,交给全局。由 flex: 1 自动分配右侧空间 */
+.exam-main {
+  padding: 20px 40px;
+  flex: 1;
+}
+
+.paper-header {
+  background: #fff;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
+}
+.paper-title {
+  margin: 0 0 10px 0;
+  font-size: 22px;
+}
+.paper-meta {
+  color: #909399;
+  font-size: 14px;
+  display: flex;
+  justify-content: space-between;
+}
+.question-group-section {
+  margin-top: 30px;
+}
+.group-header-title {
+  font-size: 16px;
+  color: #303133;
+  margin-bottom: 15px;
+}
+.group-desc {
+  font-weight: normal;
+  color: #909399;
+}
+.question-card {
+  background: #fff;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+  border: 1px solid #efeef0;
+  transition: all 0.2s;
+}
+.question-card.is-active {
+  border-color: #c0c4cc;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+}
+.question-stem {
+  font-size: 15px;
+  line-height: 1.6;
+  margin-bottom: 15px;
+  color: #303133;
+}
+.q-number {
+  font-weight: bold;
+  margin-right: 5px;
+}
+.q-score-highlight {
+  color: #409eff;
+  font-weight: 500;
+}
+.mark-option-item {
+  padding: 10px 15px;
+  border: 1px solid #f2f6fc;
+  background-color: #fafafa;
+  border-radius: 4px;
+  margin-bottom: 8px;
+  display: flex;
+  align-items: center;
+}
+.opt-correct {
+  background-color: #f0f9eb;
+  border-color: #c2e7b0;
+}
+.opt-user-selected {
+  border-left: 4px solid #409eff;
+}
+.opt-user-wrong {
+  background-color: #fef0f0;
+  border-color: #fde2e2;
+}
+.opt-content {
+  margin-left: 10px;
+  flex: 1;
+}
+.mini-tag {
+  margin-left: 8px;
+}
+
+.result-display-bar {
+  margin-top: 15px;
+  padding: 12px 15px;
+  background-color: #f8fafc;
+  border-radius: 6px;
+  border-left: 4px solid #909399;
+}
+.result-display-bar.display-column {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.score-display {
+  font-size: 14px;
+  color: #606266;
+}
+.score-num {
+  font-size: 18px;
+  font-weight: bold;
+}
+.score-win { color: #67c23a; }
+.score-loss { color: #f56c6c; }
+
+.review-display {
+  font-size: 13px;
+  color: #e6a23c;
+  background: #fffdf9;
+  padding: 8px 12px;
+  border-radius: 4px;
+  border: 1px dashed #f5dab1;
+}
+
+.answer-block {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  padding: 12px;
+  height: 100%;
+}
+.user-answer-block { background-color: #fafafa; }
+.standard-answer-block { background-color: #f4f4f5; border-color: #dcdfe6; }
+.block-title {
+  font-size: 13px;
+  font-weight: bold;
+  margin-bottom: 8px;
+  color: #606266;
+}
+.block-content {
+  font-size: 14px;
+  color: #303133;
+}
+.text-pre-wrap { white-space: pre-wrap; }
+.mb-15 { margin-bottom: 15px; }
+
+.material-stem {
+  background-color: #f4f4f5;
+  padding: 15px;
+  border-radius: 6px;
+  border-left: 4px solid #909399;
+}
+.material-tag {
+  font-weight: bold;
+  color: #909399;
+}
+.sub-questions-container {
+  margin-top: 15px;
+  padding-left: 20px;
+  border-left: 2px dashed #e4e7ed;
+}
+.sub-question-item {
+  margin-bottom: 25px;
+}
+</style>

+ 3 - 3
src/views/exam/ExamQuestion.vue

@@ -274,7 +274,7 @@
         <el-button type="primary" @click="updateQuestion">确 定</el-button>
       </div>
     </el-dialog>
-    <question-preview-dialog ref="previewDialog" />
+    <exam-question-preview ref="previewDialog" />
     <el-dialog
       title="加入题库"
       :visible.sync="addTableVisible"
@@ -340,7 +340,7 @@
 </template>
 
 <script>
-import QuestionPreviewDialog from './QuestionPreviewDialog.vue'
+import ExamQuestionPreview from './ExamQuestionPreview.vue'
 import {
   getSubjectKV,
   addQuestion,
@@ -351,7 +351,7 @@ import { validFormAndInvoke } from '@/utils/util'
 
 export default {
   name: 'ExamQuestionList',
-  components: { QuestionPreviewDialog },
+  components: { ExamQuestionPreview: ExamQuestionPreview },
   data() {
     return {
       radio: '',

+ 1 - 1
src/views/exam/QuestionPreviewDialog.vue → src/views/exam/ExamQuestionPreview.vue

@@ -78,7 +78,7 @@
 
 <script>
 export default {
-  name: 'QuestionPreviewDialog',
+  name: 'ExamQuestionPreview',
   data() {
     return {
       visible: false,