dusenyao 11 hónapja
szülő
commit
3dc0b72857

+ 19 - 0
src/api/exercise.js

@@ -125,6 +125,25 @@ export function PageQueryExerciseUserAnswerRecordList(data) {
 }
 
 /**
+ * 得到练习题题目用户答题统计表
+ * @param {object} data
+ * @param {string} data.share_record_id 分享记录id
+ */
+export function GetExerciseQuestionUserAnswerStatList(data) {
+  return http.post(`/TeachingServer/ExerciseComInfoQuery/GetExerciseQuestionUserAnswerStatList`, data);
+}
+
+/**
+ * 得到练习题题目答题用户列表
+ * @param {object} data
+ * @param {string} data.share_record_id 分享记录id
+ * @param {string} data.question_id 题目id
+ */
+export function GetExerciseQuestionAnswerUserList(data) {
+  return http.post(`/TeachingServer/ExerciseComInfoQuery/GetExerciseQuestionAnswerUserList`, data);
+}
+
+/**
  * 得到我的课程列表(教师)
  */
 export function GetMyCourseList_Teacher(data) {

+ 17 - 1
src/router/modules/exercise.js

@@ -48,4 +48,20 @@ const AnswerRecordList = {
   ],
 };
 
-export default [ExerciseCreatePage, AnswerPage, AnswerRecordList];
+/**
+ * 练习题题目答题用户列表
+ */
+const ExerciseAnswerUserList = {
+  path: '/exercise_answer_user_list',
+  component: ANSWER,
+  redirect: 'ExerciseAnswerUserList',
+  children: [
+    {
+      path: '/exercise_answer_user_list',
+      name: 'ExerciseAnswerUserList',
+      component: () => import('@/views/home/recovery/ExerciseAnswerUserList.vue'),
+    },
+  ],
+};
+
+export default [ExerciseCreatePage, AnswerPage, AnswerRecordList, ExerciseAnswerUserList];

+ 1 - 1
src/views/exercise_questions/answer/index.vue

@@ -260,7 +260,7 @@ export default {
       secondFormatConversion,
       isTeacher: this.$store.getters.isTeacher, // 是否是教师
       user_answer_record_info: {}, // 当前用户的答题记录信息
-      correct_answer_show_mode: 1,
+      correct_answer_show_mode: 1, // 正确答案显示模式
       scoreTypeList, // 分数类型列表
       // 分享记录信息
       share_record: {

+ 2 - 2
src/views/exercise_questions/create/components/common/SoundRecord.vue

@@ -18,7 +18,7 @@
       >
       <SvgIcon icon-class="delete-back-line" :class="['delete-btn', wavBlob ? '' : 'not-url']" @click="deleteWav" />
     </template>
-    <div v-else @click="microphone" class="sound-microphone">
+    <div v-else class="sound-microphone" @click="microphone">
       <img v-if="microphoneStatus" :src="require('../../../../../assets/record-ing.png')" class="voice-play" />
       <SvgIcon v-else icon-class="mic-line" class="record" />
       <span class="auto-btn">录制音频</span>
@@ -174,7 +174,7 @@ export default {
   align-items: center;
   width: 200px;
   padding: 5px 12px;
-  background: #f2f3f5;
+  background: $fill-color;
   border-radius: 2px;
 
   .audio-play-btn {

+ 1 - 1
src/views/exercise_questions/create/components/exercises/ChooseToneQuestion.vue

@@ -429,7 +429,7 @@ export default {
   .subtitle {
     margin: 8px 0;
     font-size: 14px;
-    color: #4e5969;
+    color: $font-light-color;
   }
 
   .content-item {

+ 1 - 1
src/views/exercise_questions/create/components/exercises/FillQuestion.vue

@@ -246,7 +246,7 @@ export default {
   .subtitle {
     margin: 8px 0;
     font-size: 14px;
-    color: #4e5969;
+    color: $font-light-color;
   }
 
   .correct-answer {

+ 1 - 1
src/views/exercise_questions/create/components/exercises/ListenFillQuestion.vue

@@ -309,7 +309,7 @@ export default {
   .subtitle {
     margin: 8px 0;
     font-size: 14px;
-    color: #4e5969;
+    color: $font-light-color;
   }
 
   .correct-answer {

+ 1 - 1
src/views/exercise_questions/create/components/exercises/TalkPictureQuestion.vue

@@ -274,7 +274,7 @@ export default {
         width: 64px;
         font-size: 14px;
         line-height: 32px;
-        color: #4e5969;
+        color: $font-light-color;
       }
 
       :deep .rich-wrapper {

+ 1 - 1
src/views/exercise_questions/create/components/exercises/WritePictureQuestion.vue

@@ -274,7 +274,7 @@ export default {
         width: 64px;
         font-size: 14px;
         line-height: 32px;
-        color: #4e5969;
+        color: $font-light-color;
       }
 
       :deep .rich-wrapper {

+ 1 - 1
src/views/exercise_questions/create/components/exercises/WriteQuestion.vue

@@ -157,7 +157,7 @@ export default {
 
     :deep .el-input-number__increase,
     .el-input-number__decrease {
-      background-color: #f2f3f5;
+      background-color: $fill-color;
     }
 
     .el-input input.el-input__inner {

+ 1 - 1
src/views/exercise_questions/preview/AnswerQuestionPreview.vue

@@ -61,7 +61,7 @@ export default {
       font-size: 14px;
       font-weight: 400;
       line-height: 32px;
-      color: #4e5969;
+      color: $font-light-color;
     }
   }
 }

+ 2 - 2
src/views/exercise_questions/preview/EssayQuestionPreview.vue

@@ -116,7 +116,7 @@ export default {
 
   :deep .el-textarea .el-input__count {
     font-size: 14px;
-    background-color: #f2f3f5;
+    background-color: $fill-color;
   }
 
   .reference-box {
@@ -128,7 +128,7 @@ export default {
       font-size: 14px;
       font-weight: 400;
       line-height: 32px;
-      color: #4e5969;
+      color: $font-light-color;
     }
   }
 }

+ 1 - 1
src/views/exercise_questions/preview/ReplaceAnswerPreview.vue

@@ -236,7 +236,7 @@ export default {
       font-size: 14px;
       font-weight: 400;
       line-height: 32px;
-      color: #4e5969;
+      color: $font-light-color;
     }
 
     :deep p {

+ 1 - 1
src/views/exercise_questions/preview/TalkPictruePreview.vue

@@ -187,7 +187,7 @@ export default {
       font-size: 14px;
       font-weight: 400;
       line-height: 32px;
-      color: #4e5969;
+      color: $font-light-color;
     }
   }
 

+ 1 - 1
src/views/exercise_questions/preview/WordDictationPreview.vue

@@ -470,7 +470,7 @@ export default {
       font-size: 14px;
       font-weight: 400;
       line-height: 32px;
-      color: #4e5969;
+      color: $font-light-color;
     }
   }
 }

+ 2 - 2
src/views/exercise_questions/preview/WritePictruePreview.vue

@@ -201,7 +201,7 @@ export default {
       font-size: 14px;
       font-weight: 400;
       line-height: 32px;
-      color: #4e5969;
+      color: $font-light-color;
     }
   }
 
@@ -239,7 +239,7 @@ export default {
   :deep .el-textarea .el-input__count {
     bottom: 5px;
     font-size: 14px;
-    background-color: #f2f3f5;
+    background-color: $fill-color;
   }
 }
 

+ 1 - 1
src/views/exercise_questions/preview/WritePreview.vue

@@ -151,7 +151,7 @@ export default {
 
   :deep .el-textarea .el-input__count {
     font-size: 14px;
-    background-color: #f2f3f5;
+    background-color: $fill-color;
   }
 }
 </style>

+ 192 - 36
src/views/home/recovery/AnswerData.vue

@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/no-v-html -->
 <template>
   <div class="answer">
     <div class="title">{{ share_record_info.share_record_name }}</div>
@@ -46,47 +47,112 @@
       </div>
     </div>
 
-    <el-table :data="answer_record_list" height="100%">
-      <el-table-column prop="index" label="序号" width="70">
-        <template slot-scope="{ $index }">{{ $index + 1 }}</template>
-      </el-table-column>
-      <el-table-column prop="user_real_name" label="用户" width="280" />
-      <el-table-column prop="finish_time" label="完成时间" width="180" />
-      <el-table-column prop="answer_duration" label="耗时" width="180">
-        <template slot-scope="{ row }">
-          <span>{{ row.finish_time ? secondFormatConversion(row.answer_duration, 'chinese') : '' }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="right_count" label="正确" width="160">
-        <template slot-scope="{ row }">
-          <span>{{ row.finish_time ? row.right_count : '' }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="error_count" label="错误" width="160">
-        <template slot-scope="{ row }">
-          <span>{{ row.finish_time ? row.error_count : '' }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="正确率">
-        <template slot-scope="{ row }">
-          <span>{{ row.finish_time ? row.right_percent + '%' : '' }}</span>
-        </template>
-      </el-table-column>
-
-      <el-table-column prop="operation" label="操作" fixed="right" width="200">
-        <template slot-scope="{ row }" v-if="row.finish_time">
-          <span class="link" @click="viewUserAnswerRecordLis(row.exercise_share_record_id, row.id)">查看</span>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <PaginationPage ref="pagination" :total="total" @getList="pageQueryExerciseUserAnswerRecordList" />
+    <ul class="statistics-type-list">
+      <li
+        v-for="{ label, value } in statisticsList"
+        :key="value"
+        :class="[{ active: value === curStatisticsType }]"
+        @click="changeStatistics(value)"
+      >
+        {{ label }}
+      </li>
+    </ul>
+
+    <template v-if="curStatisticsType === statisticsList[0].value">
+      <el-table :data="question_user_answer_stat_list" height="100%">
+        <el-table-column prop="index" label="题号" width="70">
+          <template slot-scope="{ row }">{{ row.question_number }}</template>
+        </el-table-column>
+        <el-table-column prop="name" label="名称" width="130" />
+        <el-table-column label="主客观" width="90">
+          <template slot-scope="{ row }">{{ row.is_objective === 'true' ? '客观题' : '主观题' }}</template>
+        </el-table-column>
+        <el-table-column label="题干" width="600">
+          <div slot-scope="{ row }" class="rich-text" v-html="sanitizeHTML(row.stem)"></div>
+        </el-table-column>
+        <el-table-column label="回答正确" width="100">
+          <template slot-scope="{ row }">
+            <template v-if="row.answer_right_person_name_list.length > 0">
+              <el-popover placement="bottom" width="270" trigger="hover">
+                <ul class="name-list">
+                  <li v-for="(name, i) in row.answer_right_person_name_list" :key="i">{{ name }}</li>
+                </ul>
+                <span slot="reference">{{ row.answer_right_person_count }}</span>
+              </el-popover>
+            </template>
+            <span v-else>{{ row.answer_right_person_count }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="回答错误" width="100">
+          <template slot-scope="{ row }">
+            <template v-if="row.answer_error_person_name_list.length > 0">
+              <el-popover placement="bottom" width="270" trigger="hover">
+                <ul class="name-list">
+                  <li v-for="(name, i) in row.answer_error_person_name_list" :key="i">{{ name }}</li>
+                </ul>
+                <span slot="reference">{{ row.answer_error_person_count }}</span>
+              </el-popover>
+            </template>
+            <span v-else>{{ row.answer_error_person_count }} </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="正确率">
+          <template slot-scope="{ row }">
+            {{ row.right_percent }}% ({{ row.answer_right_person_count }}/{{ row.answer_person_count }})</template
+          >
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" width="100">
+          <template slot-scope="{ row }">
+            <span class="link" @click="viewExerciseQuestion(row.question_id, row.exercise_id)">查看</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </template>
+
+    <template v-else-if="curStatisticsType === statisticsList[1].value">
+      <el-table :data="answer_record_list" height="100%">
+        <el-table-column prop="index" label="序号" width="70">
+          <template slot-scope="{ $index }">{{ $index + 1 }}</template>
+        </el-table-column>
+        <el-table-column prop="user_real_name" label="用户" width="280" />
+        <el-table-column prop="finish_time" label="完成时间" width="180" />
+        <el-table-column prop="answer_duration" label="耗时" width="180">
+          <template slot-scope="{ row }">
+            <span>{{ row.finish_time ? secondFormatConversion(row.answer_duration, 'chinese') : '' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="right_count" label="正确" width="160">
+          <template slot-scope="{ row }">
+            <span>{{ row.finish_time ? row.right_count : '' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="error_count" label="错误" width="160">
+          <template slot-scope="{ row }">
+            <span>{{ row.finish_time ? row.error_count : '' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="正确率">
+          <template slot-scope="{ row }">
+            <span>{{ row.finish_time ? row.right_percent + '%' : '' }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="operation" label="操作" fixed="right" width="200">
+          <template v-if="row.finish_time" slot-scope="{ row }">
+            <span class="link" @click="viewUserAnswerRecordLis(row.exercise_share_record_id, row.id)">查看</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <PaginationPage ref="pagination" :total="total" @getList="pageQueryExerciseUserAnswerRecordList" />
+    </template>
   </div>
 </template>
 
 <script>
-import { PageQueryExerciseUserAnswerRecordList } from '@/api/exercise';
+import { PageQueryExerciseUserAnswerRecordList, GetExerciseQuestionUserAnswerStatList } from '@/api/exercise';
 import { secondFormatConversion } from '@/utils/transform';
+import DOMPurify from 'dompurify';
 
 import PaginationPage from '@/components/common/PaginationPage.vue';
 
@@ -114,6 +180,14 @@ export default {
         { label: '未完成', value: 1 },
       ],
       secondFormatConversion,
+      curStatisticsType: 'question', // 当前统计类型
+      // 统计类型列表
+      statisticsList: [
+        { label: '按题统计', value: 'question' },
+        { label: '按人统计', value: 'person' },
+      ],
+      // 题目用户答题统计列表
+      question_user_answer_stat_list: [],
     };
   },
   computed: {
@@ -126,6 +200,9 @@ export default {
       return { hour, minute, second };
     },
   },
+  created() {
+    this.getExerciseQuestionUserAnswerStatList();
+  },
   methods: {
     getPageList() {
       this.$refs.pagination.getList();
@@ -146,6 +223,20 @@ export default {
         },
       });
     },
+    /**
+     * 查看练习题题目答题用户列表
+     */
+    viewExerciseQuestion(question_id, exercise_id) {
+      this.$router.push({
+        path: '/exercise_answer_user_list',
+        query: {
+          search_exercise_id: this.searchData.exercise_id,
+          exercise_id,
+          share_record_id: this.searchData.share_record_id,
+          question_id,
+        },
+      });
+    },
     pageQueryExerciseUserAnswerRecordList(data) {
       PageQueryExerciseUserAnswerRecordList({ ...data, ...this.searchData })
         .then(({ total_count, share_record_info, sum_info, answer_record_list }) => {
@@ -156,6 +247,33 @@ export default {
         })
         .catch(() => {});
     },
+    changeStatistics(value) {
+      this.curStatisticsType = value;
+      if (value === 'question') {
+        this.getExerciseQuestionUserAnswerStatList();
+      } else {
+        this.$nextTick(() => {
+          this.getPageList();
+        });
+      }
+    },
+    getExerciseQuestionUserAnswerStatList() {
+      GetExerciseQuestionUserAnswerStatList({
+        share_record_id: this.searchData.share_record_id,
+      }).then(({ share_record_info, sum_info, question_user_answer_stat_list }) => {
+        this.question_user_answer_stat_list = question_user_answer_stat_list;
+        this.share_record_info = share_record_info;
+        this.sum_info = sum_info;
+      });
+    },
+    /**
+     * 过滤 html,防止 xss 攻击
+     * @param {string} html 需要过滤的html
+     * @returns {string} 过滤后的html
+     */
+    sanitizeHTML(html) {
+      return DOMPurify.sanitize(html);
+    },
   },
 };
 </script>
@@ -215,5 +333,43 @@ export default {
       }
     }
   }
+
+  .statistics-type-list {
+    display: flex;
+    column-gap: 12px;
+    font-size: 14px;
+
+    li {
+      padding: 5px 12px;
+      color: $font-light-color;
+      cursor: pointer;
+
+      &.active {
+        color: $main-color;
+        background-color: $fill-color;
+        border-radius: 40px;
+      }
+    }
+  }
+
+  .rich-text {
+    @include rich-text(12pt);
+
+    :deep p {
+      margin: 0;
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.el-popover {
+  padding: 8px;
+
+  .name-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
 }
 </style>

+ 595 - 0
src/views/home/recovery/ExerciseAnswerUserList.vue

@@ -0,0 +1,595 @@
+<template>
+  <div class="exercise-answer">
+    <div class="user-list">
+      <span class="title">回答正确</span>
+      <ul>
+        <li
+          v-for="{ user_id, user_image_url, user_real_name, answer_record_id } in answer_right_person_list"
+          :key="user_id"
+          :class="['user-item', { active: user_id === curUserId }]"
+          @click="selectUser(user_id, answer_record_id)"
+        >
+          <el-avatar :size="24" :src="user_image_url" />
+          <span>{{ user_real_name }}</span>
+        </li>
+      </ul>
+      <span class="title">回答错误</span>
+      <ul>
+        <li
+          v-for="{ user_id, user_image_url, user_real_name, answer_record_id } in answer_error_person_list"
+          :key="user_id"
+          :class="['user-item', { active: user_id === curUserId }]"
+          @click="selectUser(user_id, answer_record_id)"
+        >
+          <el-avatar :size="24" :src="user_image_url" />
+          <span>{{ user_real_name }}</span>
+        </li>
+      </ul>
+    </div>
+
+    <!-- 练习题题目 -->
+    <div v-loading="loading" class="question-container">
+      <div class="answer-wrapper">
+        <header class="header">
+          <div class="back round" @click="goBack">
+            <i class="el-icon-arrow-left"></i>
+            <span>返回</span>
+          </div>
+          <div v-if="is_objective" class="user-answer-info">
+            <template v-if="user_answer.answer_status === 1">
+              <span class="answer-status right"><SvgIcon :size="10" icon-class="check-mark" />回答正确</span>
+            </template>
+            <template v-else-if="user_answer.answer_status === 2">
+              <span class="answer-status error"><SvgIcon :size="10" icon-class="cross" />回答错误</span>
+            </template>
+          </div>
+          <div class="question-info">
+            <el-popover :width="200" :disabled="true" trigger="click" popper-class="question-wrapper">
+              <div slot="reference" :style="{ backgroundColor: '#E9E8EA' }" class="round question-index">
+                <SvgIcon icon-class="list" />
+                <span>{{ curQuestionIndex + 1 }} / {{ questionList.length }}</span>
+                <span>{{ getExerciseName('cur') }}</span>
+              </div>
+            </el-popover>
+          </div>
+        </header>
+
+        <main class="main">
+          <template v-for="({ id }, i) in questionList">
+            <component
+              :is="curQuestionPage"
+              v-if="i === curQuestionIndex"
+              :key="id"
+              ref="exercise"
+              :data="currentQuestion"
+            />
+          </template>
+        </main>
+
+        <footer class="footer">
+          <el-popover v-model="isPopover" placement="top-start" trigger="click">
+            <!-- 教师填写批注 -->
+            <div class="annotations-container">
+              <div class="title">增加批注</div>
+              <div v-if="currentQuestion.type === 'read'" class="read-score">
+                <div v-for="(item, i) in remark.child_question_remark_list" :key="i">
+                  <span>小题 {{ i + 1 }} 分数:</span>
+                  <span v-if="isEnable(item?.is_objective)">得分 {{ item.score }}</span>
+                  <el-input-number v-else v-model="item.score" :min="0" :max="item.score_question" :step="1" />
+                </div>
+              </div>
+              <div v-else-if="!is_objective" class="score">
+                <span>分数</span>
+                <el-input-number
+                  v-model="remark.score"
+                  :min="0"
+                  :max="question.score"
+                  :step="question.score_type === scoreTypeList[0].value ? 1 : 0.1"
+                />
+              </div>
+              <el-input v-model="remark.remark" type="textarea" rows="6" resize="none" class="remark" />
+              <div>图片/视频</div>
+
+              <el-upload action="no" accept="video/*,image/*" :show-file-list="false" :http-request="upload">
+                <div class="upload">
+                  <i class="el-icon-plus avatar-uploader-icon"></i>
+                  <span>Upload</span>
+                </div>
+              </el-upload>
+              <ul class="file-list">
+                <li v-for="({ file_name, file_id }, i) in remark.file_list" :key="file_id" @click="removeFile(i)">
+                  <span>{{ file_name }}</span>
+                  <SvgIcon icon-class="delete" />
+                </li>
+              </ul>
+
+              <div class="popover-footer">
+                <el-button @click="isPopover = false">取消</el-button>
+                <el-button type="primary" @click="fillQuestionAnswerRemark">确定</el-button>
+              </div>
+            </div>
+
+            <div slot="reference" :class="['annotations', { has: isEnable(remark.is_remarked) && !isTeacher }]">
+              <template v-if="isEnable(remark.is_remarked) && !isTeacher">
+                <span>有一条教师批注</span>
+              </template>
+              <template v-else><i class="el-icon-plus"></i><span>批注</span></template>
+            </div>
+          </el-popover>
+        </footer>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  GetExerciseQuestionAnswerUserList,
+  GetExerciseQuestionIndexList,
+  GetQuestionInfo_AnswerRecord,
+  FillQuestionAnswerRemark,
+} from '@/api/exercise';
+import { exerciseNames } from '@/views/exercise_questions/data/questionType';
+import { scoreTypeList } from '@/views/exercise_questions/data/common';
+import { fileUpload } from '@/api/app';
+
+import PreviewQuestionTypeMixin from '@/views/exercise_questions/data/PreviewQuestionTypeMixin';
+
+export default {
+  name: 'ExerciseAnswerUserList',
+  mixins: [PreviewQuestionTypeMixin],
+  data() {
+    const { share_record_id, question_id, exercise_id, search_exercise_id } = this.$route.query;
+
+    return {
+      share_record_id,
+      question_id,
+      exercise_id, // 练习id
+      search_exercise_id, // 搜索练习id
+      exerciseNames,
+      scoreTypeList,
+      isTeacher: this.$store.getters.isTeacher, // 是否是教师
+      curUserId: '', // 当前用户id
+      curAnswerRecordId: '', // 当前答题记录id
+      answer_right_person_list: [],
+      answer_error_person_list: [],
+      questionList: [], // 题目列表
+      curQuestionPage: '', // 当前问题页面
+      curQuestionIndex: -1, // 当前题目索引
+      currentQuestion: {}, // 当前题目
+      is_objective: false, // 是否客观题
+      remark: {
+        is_remarked: 'false',
+        score: 0,
+        remark: '',
+        remark_person_image_url: '',
+        remark_person_name: '',
+        remark_time: '',
+        file_list: [],
+        child_question_remark_list: [], // 子题批注列表
+      }, // 批注
+      question: {
+        score: 1,
+        score_item: 1,
+        score_type: 'aggregate',
+      }, // 题目信息
+      // 用户答案
+      user_answer: {
+        answer_status: 0,
+      },
+      loading: false,
+      user_answer_record_info: {}, // 当前用户的答题记录信息
+      isPopover: false,
+    };
+  },
+  watch: {
+    curAnswerRecordId: {
+      handler() {
+        this.getQuestionInfo_AnswerRecord();
+      },
+    },
+  },
+  created() {
+    GetExerciseQuestionAnswerUserList({
+      share_record_id: this.share_record_id,
+      question_id: this.question_id,
+    }).then(({ answer_right_person_list, answer_error_person_list }) => {
+      this.answer_right_person_list = answer_right_person_list;
+      this.answer_error_person_list = answer_error_person_list;
+      if (answer_right_person_list.length > 0 || answer_error_person_list.length > 0) {
+        const userList = answer_right_person_list.length > 0 ? answer_right_person_list : answer_error_person_list;
+        this.selectUser(userList[0].user_id, userList[0].answer_record_id);
+      }
+    });
+    this.init();
+  },
+  methods: {
+    init() {
+      this.getExerciseQuestionIndexList();
+    },
+    // 得到练习的题目索引列表
+    getExerciseQuestionIndexList() {
+      GetExerciseQuestionIndexList({ exercise_id: this.exercise_id }).then(({ index_list }) => {
+        this.questionList = index_list.map((item) => ({
+          ...item,
+          isFill: true,
+        }));
+        this.curQuestionIndex = this.questionList.findIndex((item) => item.id === this.question_id);
+      });
+    },
+    // 得到答题记录题目信息
+    getQuestionInfo_AnswerRecord() {
+      if (this.questionList.length === 0) return;
+      GetQuestionInfo_AnswerRecord({
+        answer_record_id: this.curAnswerRecordId,
+        question_id: this.question_id,
+      }).then(({ question, user_answer: { is_fill_answer, content, is_objective, answer_status }, remark }) => {
+        if (question.type === 'read') {
+          let question_list = JSON.parse(question.content)?.question_list ?? [];
+          let child_question_remark_list = question_list
+            .map(({ id }) => {
+              return remark.child_question_remark_list.find((item) => item.question_id === id);
+            })
+            .filter((item) => item);
+          remark.child_question_remark_list = child_question_remark_list;
+        }
+        // 批注
+        this.remark = remark;
+
+        this.question = question;
+        // 题目内容
+        if (question.content) {
+          this.currentQuestion = JSON.parse(question.content);
+          this.curQuestionPage =
+            this.questionList.length === 0 || this.curQuestionIndex < 0
+              ? ''
+              : this.previewComponents[this.questionList[this.curQuestionIndex].type];
+        }
+        this.is_objective = is_objective === 'true';
+        this.user_answer.answer_status = answer_status;
+
+        // 如果已经填写过答案,直接显示答案
+        if (is_fill_answer === 'true') {
+          this.$nextTick().then(() => {
+            this.$refs.exercise?.[0].showAnswer(true, true, content.length > 0 ? JSON.parse(content) : null, true);
+          });
+        }
+      });
+    },
+    upload(file) {
+      fileUpload('Mid', file).then(({ file_info_list }) => {
+        if (file_info_list.length > 0) {
+          const { file_id, file_url, file_name } = file_info_list[0];
+          this.remark.file_list.push({ file_id, file_url, file_name });
+        }
+      });
+    },
+    // 填写批注
+    fillQuestionAnswerRemark() {
+      FillQuestionAnswerRemark({
+        answer_record_id: this.curAnswerRecordId,
+        question_id: this.question_id,
+        file_id_list: this.remark.file_list.map(({ file_id }) => file_id),
+        child_question_remark_list: this.remark.child_question_remark_list,
+        score: this.remark.score,
+        remark: this.remark.remark,
+      }).then(() => {
+        this.$message.success('批注成功');
+        this.isPopover = false;
+      });
+    },
+    goBack() {
+      this.$router.push({
+        path: '/answer_data',
+        query: { share_record_id: this.share_record_id, exercise_id: this.search_exercise_id },
+      });
+    },
+    getExerciseName(type, question_type, additional_type) {
+      if (this.questionList.length <= 0) return;
+      if (type === 'cur') {
+        if (this.curQuestionIndex < 0) return '';
+        let { type: _type, additional_type: _additional_type } = this.questionList[this.curQuestionIndex];
+        if (_type === 'select') {
+          return _additional_type === 'single' ? '单选题' : '多选题';
+        }
+        return this.exerciseNames[_type];
+      }
+
+      if (type === 'list') {
+        if (question_type === 'select') {
+          return additional_type === 'single' ? '单选题' : '多选题';
+        }
+        return this.exerciseNames[question_type];
+      }
+    },
+    selectUser(user_id, answer_record_id) {
+      this.curUserId = user_id;
+      this.curAnswerRecordId = answer_record_id;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.exercise-answer {
+  display: flex;
+  height: 100%;
+
+  .user-list {
+    width: 200px;
+    height: 100%;
+    background-color: #fff;
+
+    .title {
+      padding: 8px 8px 0;
+      font-size: 12px;
+      color: #999;
+    }
+
+    ul {
+      .user-item {
+        display: flex;
+        gap: 8px;
+        align-items: center;
+        padding: 8px;
+        font-size: 14px;
+        cursor: pointer;
+
+        &.active {
+          background-color: #e7eeff;
+        }
+      }
+    }
+  }
+
+  .question-container {
+    flex: 1;
+    height: 100%;
+    padding: 16px;
+    background-color: #f9f8f9;
+
+    .answer-wrapper {
+      display: flex;
+      flex-direction: column;
+      row-gap: 16px;
+      max-width: 1200px;
+      min-height: 100%;
+      padding: 16px;
+      margin: 0 auto;
+      background-color: #fff;
+      border-radius: 24px;
+      box-shadow: 0 6px 30px 5px #0000000d;
+
+      .header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        height: 38px;
+        font-size: 14px;
+
+        .back {
+          cursor: pointer;
+        }
+
+        .user-answer-info {
+          .answer-status {
+            display: flex;
+            column-gap: 8px;
+            align-items: center;
+            padding: 8px 16px;
+            color: #fff;
+            border-radius: 40px;
+
+            &.right {
+              background-color: #3acb85;
+            }
+
+            &.error {
+              background-color: #e65656;
+            }
+          }
+        }
+
+        .question-info {
+          display: flex;
+          column-gap: 12px;
+
+          .question-index {
+            cursor: pointer;
+          }
+        }
+      }
+
+      .main {
+        flex: 1;
+      }
+
+      .footer {
+        position: relative;
+        display: flex;
+        align-items: center;
+
+        .annotations {
+          display: flex;
+          column-gap: 8px;
+          align-items: center;
+          padding: 7px 16px;
+          font-size: 14px;
+          cursor: pointer;
+          background-color: $fill-color;
+          border-radius: 20px;
+
+          &.has {
+            color: $danger-color;
+            background-color: #ffece8;
+          }
+        }
+
+        .el-button {
+          padding: 9px 40px;
+        }
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.answer-container {
+  padding: 0 !important;
+}
+
+.el-popover {
+  display: flex;
+  flex-direction: column;
+  row-gap: 8px;
+  padding: 8px 8px 14px;
+  font-size: 14px;
+
+  %read-score {
+    display: flex;
+    flex-direction: column;
+    row-gap: 8px;
+
+    > div {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+      color: #000;
+
+      :first-child {
+        color: #999;
+      }
+
+      .el-input-number {
+        flex: 1;
+      }
+    }
+  }
+
+  .remark-container {
+    display: flex;
+    flex-direction: column;
+    row-gap: 8px;
+
+    .remark-info {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+
+      .remark-name {
+        flex: 1;
+      }
+
+      .remark-time {
+        font-size: 12px;
+        color: #999;
+      }
+    }
+
+    .file {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+
+      .el-image {
+        width: 80px;
+        height: 80px;
+        background-color: #d9d9d9;
+      }
+    }
+
+    .read-score {
+      @extend %read-score;
+    }
+  }
+
+  .annotations-container {
+    display: flex;
+    flex-direction: column;
+    row-gap: 8px;
+
+    .title {
+      color: #000;
+    }
+
+    .read-score {
+      @extend %read-score;
+    }
+
+    .score {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+
+      :first-child {
+        color: #999;
+      }
+    }
+
+    .remark {
+      width: 350px;
+    }
+
+    .upload {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: space-around;
+      width: 80px;
+      height: 80px;
+      padding: 8px;
+      background-color: $fill-color;
+      border: 1px solid $border-color;
+    }
+
+    .file-list {
+      display: flex;
+      flex-direction: column;
+      row-gap: 4px;
+
+      > li {
+        display: flex;
+        column-gap: 4px;
+
+        :first-child {
+          flex: 1;
+        }
+
+        :last-child {
+          cursor: pointer;
+        }
+      }
+    }
+
+    .popover-footer {
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+}
+
+.question-wrapper {
+  max-height: 60vh;
+  padding: 8px;
+  overflow: auto;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px 0 #00000040;
+
+  .question-list {
+    li {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+      padding: 8px 16px;
+      cursor: pointer;
+
+      &.active {
+        color: $main-color;
+        background-color: #f4f8ff;
+        border-radius: 2px;
+      }
+    }
+  }
+}
+</style>