瀏覽代碼

参考答案与解析

dsy 1 天之前
父節點
當前提交
ef3e869bbf
共有 25 個文件被更改,包括 677 次插入14 次删除
  1. 二進制
      src/assets/component/answer-analysis-icon.png
  2. 二進制
      src/assets/component/answer-analysis.png
  3. 二進制
      src/assets/component/component-answer.png
  4. 二進制
      src/assets/component/component-correct.png
  5. 二進制
      src/assets/component/component-retry.png
  6. 二進制
      src/assets/component/correct-answer-icon.png
  7. 二進制
      src/assets/component/correct-answer.png
  8. 二進制
      src/assets/component/reference-answer-icon.png
  9. 二進制
      src/assets/component/reference-answer.png
  10. 3 3
      src/components/CommonPreview.vue
  11. 13 4
      src/views/book/courseware/create/components/CreateCanvas.vue
  12. 8 3
      src/views/book/courseware/create/components/base/common/UploadFile.vue
  13. 25 0
      src/views/book/courseware/create/components/common/AddAnswer.vue
  14. 231 0
      src/views/book/courseware/create/components/common/AnswerAnalysisList.vue
  15. 58 1
      src/views/book/courseware/create/components/common/ModuleMixin.js
  16. 10 0
      src/views/book/courseware/create/components/common/SettingMixin.js
  17. 11 0
      src/views/book/courseware/create/components/question/fill/Fill.vue
  18. 2 0
      src/views/book/courseware/create/components/question/fill/FillSetting.vue
  19. 12 1
      src/views/book/courseware/create/index.vue
  20. 22 0
      src/views/book/courseware/data/common.js
  21. 2 0
      src/views/book/courseware/data/fill.js
  22. 232 0
      src/views/book/courseware/preview/common/AnswerAnalysis.vue
  23. 2 2
      src/views/book/courseware/preview/components/common/AudioPlay.vue
  24. 4 0
      src/views/book/courseware/preview/components/common/PreviewMixin.js
  25. 42 0
      src/views/book/courseware/preview/components/fill/FillPreview.vue

二進制
src/assets/component/answer-analysis-icon.png


二進制
src/assets/component/answer-analysis.png


二進制
src/assets/component/component-answer.png


二進制
src/assets/component/component-correct.png


二進制
src/assets/component/component-retry.png


二進制
src/assets/component/correct-answer-icon.png


二進制
src/assets/component/correct-answer.png


二進制
src/assets/component/reference-answer-icon.png


二進制
src/assets/component/reference-answer.png


+ 3 - 3
src/components/CommonPreview.vue

@@ -1073,12 +1073,12 @@ export default {
     convertHtmlPreserveAttributes(html) {
       try {
         const parser = new DOMParser();
-        const doc = parser.parseFromString(html, 'text/html');
+        const doc = parser.parseFromString(html, 'text/html'); // 解析HTML字符串,返回Document对象
 
-        // 跳过这些标记内的转换
+        // 跳过 script 和 style 标签
         const skipTags = new Set(['SCRIPT', 'STYLE']);
 
-        const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null, false);
+        const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
         let node = walker.nextNode();
         while (node) {
           const parent = node.parentNode;

+ 13 - 4
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -204,6 +204,7 @@ export default {
       visibleFullTextSettings: false,
       book_unified_attrib: unified_attrib,
       title_list: [],
+      curComponentId: '', // 当前选中组件 id
     };
   },
   computed: {
@@ -533,15 +534,23 @@ export default {
     },
     /**
      * 显示设置
-     * @param {object} setting
-     * @param {string} type
-     * @param {string} id
-     * @param {object} params
+     * @param {object} setting 组件设置数据
+     * @param {string} type 组件类型
+     * @param {string} id 组件 id
+     * @param {object} params 组件参数
      */
     showSetting(setting, type, id, params = {}) {
+      this.curComponentId = id;
       this.$emit('showSetting', setting, type, id, params);
     },
     /**
+     * 组件中添加答案或解析
+     * @param {'answer'|'analysis'} type 类型
+     */
+    addAnswerAndAnalysis(type) {
+      this.findChildComponentByKey(`grid-${this.curComponentId}`)?.addAnswerAndAnalysis(type);
+    },
+    /**
      * 计算组件移动
      * @param {number} i 行
      * @param {number} j 列

+ 8 - 3
src/views/book/courseware/create/components/base/common/UploadFile.vue

@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="upload-file">
     <div class="file-area">
       <span class="label-text">{{ labelText }}</span>
       <div class="upload-box">
@@ -22,7 +22,7 @@
         <el-button size="small" type="primary" @click="useResource">使用资源</el-button>
       </div>
     </div>
-    <el-divider />
+    <el-divider v-if="showDivider" />
     <div class="upload-tip">{{ uploadTip }}</div>
     <ul v-if="fileList.length > 0" slot="file-list" class="file-list">
       <li v-for="(file, i) in fileList" :key="i">
@@ -38,7 +38,7 @@
             <span v-else>{{
               canEditName && file.file_id
                 ? content.file_info[file.file_id].xuhao + content.file_info[file.file_id].file_name
-                : file.file_name ?? file.name
+                : (file.file_name ?? file.name)
             }}</span>
             <!-- <span>({{ file.size }})</span> -->
           </span>
@@ -176,6 +176,11 @@ export default {
       type: Number,
       default: null,
     },
+    // 是否显示分割线
+    showDivider: {
+      type: Boolean,
+      default: true,
+    },
   },
   data() {
     return {

+ 25 - 0
src/views/book/courseware/create/components/common/AddAnswer.vue

@@ -0,0 +1,25 @@
+<template>
+  <div>
+    <el-divider />
+    <el-form-item label="附加功能">
+      <el-button type="primary" size="mini" @click="addAnswerAndAnalysis('answer')">添加参考答案</el-button>
+      <el-button type="primary" size="mini" @click="addAnswerAndAnalysis('analysis')">添加解析</el-button>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'AddAnswer',
+  inject: ['addAnswerAndAnalysis'],
+  data() {
+    return {};
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.el-button + .el-button {
+  margin-left: 5px;
+}
+</style>

+ 231 - 0
src/views/book/courseware/create/components/common/AnswerAnalysisList.vue

@@ -0,0 +1,231 @@
+<template>
+  <div class="answer-analysis-list-wrapper">
+    <el-divider content-position="left">参考答案与解析</el-divider>
+    <div v-for="(item, i) in answerList" :key="`answer-${i}`" class="answer-list">
+      <div class="answer">
+        <strong>
+          参考答案 {{ i + 1 }}
+          <i class="el-icon-delete answer-delete" @click="deleteAnswerAnalysis(i, 'answer')"></i>
+        </strong>
+        <RichText
+          ref="richText"
+          v-model="item.answer_rich_text"
+          toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
+          :font-size="unifiedAttrib?.font_size"
+          :font-family="unifiedAttrib?.font"
+        />
+        <UploadFile
+          label-text="音频"
+          type="audio"
+          :show-divider="false"
+          :file-list="item.answer_audio_list"
+          :file-id-list="item.answer_audio_id_list"
+          :accept-file-type="'.mp3,.wav,.acc,.wma'"
+          :index="i"
+          :indexs="0"
+          @updateFileList="updateAnswerFileList"
+        />
+        <UploadFile
+          label-text="图片"
+          type="picture"
+          :show-divider="false"
+          :file-list="item.answer_image_list"
+          :file-id-list="item.answer_image_id_list"
+          :accept-file-type="'.jpg,.png,.jpeg'"
+          :index="i"
+          :indexs="1"
+          @updateFileList="updateAnswerFileList"
+        />
+        <UploadFile
+          label-text="视频"
+          type="video"
+          :show-divider="false"
+          :file-list="item.answer_video_list"
+          :file-id-list="item.answer_video_id_list"
+          :accept-file-type="'.mp4'"
+          :index="i"
+          :indexs="2"
+          @updateFileList="updateAnswerFileList"
+        />
+      </div>
+    </div>
+    <div v-for="(item, j) in analysisList" :key="`analysis-${j}`" class="analysis-list">
+      <div class="analysis">
+        <strong>
+          解析 {{ j + 1 }}
+          <i class="el-icon-delete analysis-delete" @click="deleteAnswerAnalysis(j, 'analysis')"></i>
+        </strong>
+        <RichText
+          ref="richText"
+          v-model="item.analysis_rich_text"
+          toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
+          :font-size="unifiedAttrib?.font_size"
+          :font-family="unifiedAttrib?.font"
+        />
+        <UploadFile
+          label-text="音频"
+          type="audio"
+          :show-divider="false"
+          :file-list="item.analysis_audio_list"
+          :file-id-list="item.analysis_audio_id_list"
+          :accept-file-type="'.mp3,.wav,.acc,.wma'"
+          :index="j"
+          :indexs="3"
+          @updateFileList="updateAnalysisFileList"
+        />
+        <UploadFile
+          label-text="图片"
+          type="picture"
+          :show-divider="false"
+          :file-list="item.analysis_image_list"
+          :file-id-list="item.analysis_image_id_list"
+          :accept-file-type="'.jpg,.png,.jpeg'"
+          :index="j"
+          :indexs="4"
+          @updateFileList="updateAnalysisFileList"
+        />
+        <UploadFile
+          label-text="视频"
+          type="video"
+          :show-divider="false"
+          :file-list="item.analysis_video_list"
+          :file-id-list="item.analysis_video_id_list"
+          :accept-file-type="'.mp4'"
+          :index="j"
+          :indexs="5"
+          @updateFileList="updateAnalysisFileList"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import UploadFile from '@/views/book/courseware/create/components/base/common/UploadFile.vue';
+import RichText from '@/components/RichText.vue';
+
+export default {
+  name: 'AnswerAnalysisList',
+  components: {
+    UploadFile,
+    RichText,
+  },
+  props: {
+    answerList: {
+      type: Array,
+      required: true,
+    },
+    analysisList: {
+      type: Array,
+      required: true,
+    },
+    unifiedAttrib: {
+      type: Object,
+      required: true,
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {
+    updateAnswerFileList({ file_list, file_id_list }, index, indexs) {
+      this.$emit('updateAnswerAnalysisFileList', {
+        file_list,
+        file_id_list,
+        index,
+        indexs,
+        type: 'answer',
+      });
+    },
+    updateAnalysisFileList({ file_list, file_id_list }, index, indexs) {
+      this.$emit('updateAnswerAnalysisFileList', {
+        file_list,
+        file_id_list,
+        index,
+        indexs,
+        type: 'analysis',
+      });
+    },
+    /**
+     * 删除答案或解析
+     * @param {number} index 索引
+     * @param {'answer'|'analysis'} type 类型
+     */
+    deleteAnswerAnalysis(index, type) {
+      this.$confirm(`您确定删除该${type === 'answer' ? '答案' : '解析'}吗?`)
+        .then(() => {
+          this.$emit('deleteAnswerAnalysis', { index, type });
+        })
+        .catch(() => {});
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.answer-list {
+  width: 100%;
+
+  .answer {
+    margin-bottom: 16px;
+
+    strong {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+
+      .answer-delete {
+        cursor: pointer;
+      }
+    }
+
+    .rich-wrapper {
+      margin-bottom: 8px;
+    }
+
+    .upload-file {
+      margin-bottom: 8px;
+    }
+  }
+
+  & + .answer-list {
+    padding-top: 16px;
+    border-top: 1px solid #eaeaea;
+  }
+}
+
+.analysis-list {
+  width: 100%;
+
+  .analysis {
+    margin-bottom: 16px;
+
+    strong {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+
+      .analysis-delete {
+        cursor: pointer;
+      }
+    }
+
+    .rich-wrapper {
+      margin-bottom: 8px;
+    }
+
+    .upload-file {
+      margin-bottom: 8px;
+    }
+  }
+
+  & + .analysis-list {
+    padding-top: 16px;
+    border-top: 1px solid #eaeaea;
+  }
+}
+</style>

+ 58 - 1
src/views/book/courseware/create/components/common/ModuleMixin.js

@@ -4,7 +4,7 @@ import ModuleBase from './ModuleBase.vue';
 import RichText from '@/components/RichText.vue';
 import MultilingualFill from '@/views/book/components/MultilingualFill.vue';
 
-import { displayList, viewMethodList, isEnable } from '@/views/book/courseware/data/common';
+import { displayList, viewMethodList, isEnable, analysisData, answerData } from '@/views/book/courseware/data/common';
 import {
   ContentSaveCoursewareComponentContent,
   ContentGetCoursewareComponentContent,
@@ -23,6 +23,22 @@ const mixin = {
       },
       borderColorObj: Vue.observable({ value: this.borderColor }), // 边框颜色
       multilingualVisible: false, // 多语言弹窗
+      idNameOrder: [
+        'answer_audio_id_list',
+        'answer_image_id_list',
+        'answer_video_id_list',
+        'analysis_audio_id_list',
+        'analysis_image_id_list',
+        'analysis_video_id_list',
+      ],
+      fileNameOrder: [
+        'answer_audio_list',
+        'answer_image_list',
+        'answer_video_list',
+        'analysis_audio_list',
+        'analysis_image_list',
+        'analysis_video_list',
+      ],
     };
   },
   props: {
@@ -340,6 +356,47 @@ const mixin = {
         if (activeTextStyle) listItem.activeTextStyle = activeTextStyle;
       }
     },
+    /**
+     * 组件中添加答案与解析
+     * @param {'answer'|'analysis'} type 类型 answer 答案 analysis 解析
+     */
+    addAnswerAndAnalysis(type) {
+      if (type === 'answer') {
+        this.data.answer_list.push(structuredClone(answerData));
+      } else if (type === 'analysis') {
+        this.data.analysis_list.push(structuredClone(analysisData));
+      }
+    },
+    /**
+     * 更新答案或解析的文件列表
+     * @param {Array} file_list 文件列表
+     * @param {Array} file_id_list 文件ID列表
+     * @param {Number} index 答案解析索引
+     * @param {Number} indexs 文件类型索引 0-答案音频 1-答案图片 2-答案视频 3-解析音频 4-解析图片 5-解析视频
+     * @param {String} type 类型 answer 答案 analysis 解析
+     */
+    updateAnswerAnalysisFileList({ file_list, file_id_list, index, indexs, type }) {
+      if (type === 'answer') {
+        this.data.answer_list[index][this.idNameOrder[indexs]] = file_id_list;
+        this.data.answer_list[index][this.fileNameOrder[indexs]] = file_list;
+      } else if (type === 'analysis') {
+        this.data.analysis_list[index][this.idNameOrder[indexs]] = file_id_list;
+        this.data.analysis_list[index][this.fileNameOrder[indexs]] = file_list;
+      }
+    },
+    /**
+     * 删除答案或解析项
+     * @param {Object} param 参数对象
+     * @param {number} param.index 索引
+     * @param {'answer'|'analysis'} param.type 类型
+     */
+    deleteAnswerAnalysis({ index, type }) {
+      if (type === 'answer') {
+        this.data.answer_list.splice(index, 1);
+      } else if (type === 'analysis') {
+        this.data.analysis_list.splice(index, 1);
+      }
+    },
   },
 };
 

+ 10 - 0
src/views/book/courseware/create/components/common/SettingMixin.js

@@ -2,6 +2,7 @@ import { checkString, isEnable, pinyinPositionList } from '@/views/book/coursewa
 
 import SerialNumber from '@/views/book/courseware/create/components/common/SerialNumber.vue';
 import BackgroundSet from '@/views/book/courseware/create/components/common/BackgroundSet.vue';
+import AddAnswer from '@/views/book/courseware/create/components/common/AddAnswer.vue';
 
 const mixin = {
   data() {
@@ -15,6 +16,7 @@ const mixin = {
   components: {
     SerialNumber,
     BackgroundSet,
+    AddAnswer,
   },
   watch: {
     'property.serial_number': {
@@ -26,6 +28,7 @@ const mixin = {
   provide() {
     return {
       updateProperty: this.updateProperty,
+      addAnswerAndAnalysis: this.addAnswerAndAnalysis,
     };
   },
   methods: {
@@ -46,6 +49,13 @@ const mixin = {
     updateProperty(key, value) {
       this.$set(this.property, key, value);
     },
+    /**
+     * @description 添加答案与解析
+     * @param {'answer'|'analysis'} type 类型
+     */
+    addAnswerAndAnalysis(type) {
+      this.$emit('addAnswerAndAnalysis', type);
+    },
   },
 };
 

+ 11 - 0
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -76,6 +76,15 @@
         </div>
       </template>
 
+      <AnswerAnalysisList
+        v-if="data.answer_list.length > 0 || data.analysis_list.length > 0"
+        :answer-list="data.answer_list"
+        :analysis-list="data.analysis_list"
+        :unified-attrib="data.unified_attrib"
+        @updateAnswerAnalysisFileList="updateAnswerAnalysisFileList"
+        @deleteAnswerAnalysis="deleteAnswerAnalysis"
+      />
+
       <MultilingualFill
         :visible.sync="multilingualVisible"
         :text="data.content"
@@ -91,6 +100,7 @@ import ModuleMixin from '../../common/ModuleMixin';
 import SoundRecord from '@/views/book/courseware/create/components/question/fill/components/SoundRecord.vue';
 import UploadAudio from '@/views/book/courseware/create/components/question/fill/components/UploadAudio.vue';
 import PinyinText from '@/components/PinyinText.vue';
+import AnswerAnalysisList from '@/views/book/courseware/create/components/common/AnswerAnalysisList.vue';
 
 import { getFillData, arrangeTypeList, fillFontList, fillTypeList } from '@/views/book/courseware/data/fill';
 import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
@@ -103,6 +113,7 @@ export default {
     SoundRecord,
     UploadAudio,
     PinyinText,
+    AnswerAnalysisList,
   },
   mixins: [ModuleMixin],
   data() {

+ 2 - 0
src/views/book/courseware/create/components/question/fill/FillSetting.vue

@@ -90,6 +90,8 @@
           >句首大写</el-checkbox
         >
       </el-form-item>
+
+      <AddAnswer />
     </el-form>
   </div>
 </template>

+ 12 - 1
src/views/book/courseware/create/index.vue

@@ -35,7 +35,11 @@
 
     <div class="create-right">
       <div class="setting-tittle">设置</div>
-      <component :is="componentSettingList[curSettingType]" ref="setting" />
+      <component
+        :is="componentSettingList[curSettingType]"
+        ref="setting"
+        @addAnswerAndAnalysis="addAnswerAndAnalysis"
+      />
     </div>
 
     <SelectBackground :visible.sync="visible" @setBackgroundImage="setBackgroundImage" />
@@ -193,6 +197,13 @@ export default {
         this.$refs.setting.setSetting(setting, params);
       });
     },
+    /**
+     * 添加参考答案或解析
+     * @param {'answer'|'analysis'} type 类型
+     */
+    addAnswerAndAnalysis(type) {
+      this.$refs.createCanvas.addAnswerAndAnalysis(type);
+    },
     setBackgroundImage(...data) {
       this.$refs.createCanvas.setBackgroundImage(...data);
     },

+ 22 - 0
src/views/book/courseware/data/common.js

@@ -112,6 +112,28 @@ export const switchOption = [
   },
 ];
 
+// 参考答案数据模板
+export const answerData = {
+  answer_rich_text: '', // 参考答案富文本
+  answer_audio_list: [], // 参考答案音频上传列表
+  answer_audio_id_list: [], // 参考答案音频ID列表
+  answer_video_list: [], // 参考答案视频上传列表
+  answer_video_id_list: [], // 参考答案视频ID列表
+  answer_image_list: [], // 参考答案图片上传列表
+  answer_image_id_list: [], // 参考答案图片ID列表
+};
+
+// 解析数据模板
+export const analysisData = {
+  analysis_rich_text: '', // 解析富文本
+  analysis_audio_list: [], // 解析音频上传列表
+  analysis_audio_id_list: [], // 解析音频ID列表
+  analysis_video_list: [], // 解析视频上传列表
+  analysis_video_id_list: [], // 解析视频ID列表
+  analysis_image_list: [], // 解析图片上传列表
+  analysis_image_id_list: [], // 解析图片ID列表
+};
+
 /**
  * 是否开启
  * @param {String} value 值

+ 2 - 0
src/views/book/courseware/data/fill.js

@@ -88,5 +88,7 @@ export function getFillData() {
       node_list: [{ name: '横排中文填空组件' }],
     },
     multilingual: [], // 多语言
+    answer_list: [], // 答案列表
+    analysis_list: [], // 解析列表
   };
 }

+ 232 - 0
src/views/book/courseware/preview/common/AnswerAnalysis.vue

@@ -0,0 +1,232 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <el-dialog
+    custom-class="answer-analysis-dialog"
+    :visible="visible"
+    width="65vw"
+    :close-on-click-modal="false"
+    :before-close="handleClose"
+  >
+    <!-- 正确答案 -->
+    <div class="right-answer">
+      <slot name="right-answer"></slot>
+    </div>
+
+    <!-- 参考答案 -->
+    <template v-if="answerList.length > 0">
+      <div v-for="(item, i) in answerList" :key="`answer-${i}`" class="answer-list">
+        <div class="answer">
+          <div class="answer-title">
+            <span class="answer-title-icon"></span>
+            <span class="answer-title-image"></span>
+          </div>
+          <div class="rich-text" v-html="item.answer_rich_text"></div>
+          <div v-if="item.answer_audio_list.length > 0" class="answer-audio-list">
+            <AudioPlay
+              v-for="file in item.answer_audio_list"
+              :key="file.file_id"
+              :file-id="file.file_id"
+              :file-name="file.file_name.slice(0, file.file_name.lastIndexOf('.'))"
+              :audio-index="i"
+            />
+          </div>
+          <div v-if="item.answer_image_list.length > 0" class="answer-image-list">
+            <img
+              v-for="file in item.answer_image_list"
+              :key="file.file_id"
+              :src="file.file_url"
+              :alt="file.file_name"
+            />
+          </div>
+          <div v-if="item.answer_video_list.length > 0" class="answer-video-list">
+            <VideoPlay
+              v-for="(file, j) in item.answer_video_list"
+              :key="file.file_id"
+              view-size="small"
+              :file-id="file.file_id"
+              :cur-video-index="j"
+            />
+          </div>
+        </div>
+      </div>
+    </template>
+
+    <!-- 解析 -->
+    <template v-if="analysisList.length > 0">
+      <div v-for="(item, i) in analysisList" :key="`analysis-${i}`" class="analysis-list">
+        <div class="analysis">
+          <div class="analysis-title">
+            <span class="analysis-title-icon"></span>
+            <span class="analysis-title-image"></span>
+          </div>
+          <div class="rich-text" v-html="item.analysis_rich_text"></div>
+          <div v-if="item.analysis_audio_list.length > 0" class="analysis-audio-list">
+            <AudioPlay
+              v-for="file in item.analysis_audio_list"
+              :key="file.file_id"
+              :file-id="file.file_id"
+              :file-name="file.file_name.slice(0, file.file_name.lastIndexOf('.'))"
+              :audio-index="i"
+            />
+          </div>
+          <div v-if="item.analysis_image_list.length > 0" class="analysis-image-list">
+            <img
+              v-for="file in item.analysis_image_list"
+              :key="file.file_id"
+              :src="file.file_url"
+              :alt="file.file_name"
+            />
+          </div>
+          <div v-if="item.analysis_video_list.length > 0" class="analysis-video-list">
+            <VideoPlay
+              v-for="(file, j) in item.analysis_video_list"
+              :key="file.file_id"
+              view-size="small"
+              :file-id="file.file_id"
+              :cur-video-index="j"
+            />
+          </div>
+        </div>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+import AudioPlay from '@/views/book/courseware/preview/components/common/AudioPlay.vue';
+import VideoPlay from '@/views/book/courseware/preview/components/common/VideoPlay.vue';
+
+export default {
+  name: 'AnswerAnalysis',
+  components: { AudioPlay, VideoPlay },
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    answerList: {
+      type: Array,
+      required: true,
+    },
+    analysisList: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {
+    handleClose() {
+      this.$emit('update:visible', false);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.answer-list {
+  width: 100%;
+
+  .answer {
+    display: flex;
+    flex-direction: column;
+    row-gap: 12px;
+    margin-bottom: 16px;
+
+    strong {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+    }
+
+    &-title {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+      margin-bottom: 8px;
+
+      &-icon {
+        width: 32px;
+        height: 32px;
+        background: url('@/assets/component/reference-answer-icon.png') no-repeat center/contain;
+      }
+
+      &-image {
+        width: 211px;
+        height: 32px;
+        background: url('@/assets/component/reference-answer.png') no-repeat center/contain;
+      }
+    }
+
+    .answer-image-list {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+    }
+  }
+
+  & + .analysis-list {
+    padding-top: 24px;
+    margin-top: 24px;
+    border-top: $border;
+  }
+}
+
+.analysis-list {
+  width: 100%;
+
+  .analysis {
+    display: flex;
+    flex-direction: column;
+    row-gap: 12px;
+    margin-bottom: 16px;
+
+    strong {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+    }
+
+    .analysis {
+      &-title {
+        display: flex;
+        column-gap: 8px;
+        align-items: center;
+        margin-bottom: 8px;
+
+        &-icon {
+          width: 16px;
+          height: 16px;
+          background: url('@/assets/component/answer-analysis-icon.png') no-repeat center/contain;
+        }
+
+        &-image {
+          width: 191px;
+          height: 32px;
+          background: url('@/assets/component/answer-analysis.png') no-repeat center/contain;
+        }
+      }
+
+      .analysis-image-list {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.answer-analysis-dialog {
+  .el-dialog__body {
+    max-height: 75vh;
+    overflow: auto;
+  }
+}
+</style>

+ 2 - 2
src/views/book/courseware/preview/components/common/AudioPlay.vue

@@ -13,7 +13,7 @@
             @change="changeCurrentTime"
           />
           <span class="audio-time">{{ audio_allTime }}</span>
-          <el-select v-model="audio.playbackRate" @change="onSpeedChange" style="width: 100px">
+          <el-select v-model="audio.playbackRate" style="width: 100px" @change="onSpeedChange">
             <el-option
               v-for="speed in playbackRateList.split(' ')"
               :key="speed"
@@ -34,7 +34,7 @@
       <div v-else class="independent-big">
         <div v-if="fileNameDisplay == 'true'" class="audio-name">{{ audioIndex + 1 }}. {{ fileName }}</div>
         <div class="slider-area">
-          <SvgIcon :icon-class="iconClass" size="18" :color="topicColor" @click="playAudio" style="cursor: pointer" />
+          <SvgIcon :icon-class="iconClass" size="18" :color="topicColor" style="cursor: pointer" @click="playAudio" />
           <span class="audio-time">{{ secondFormatConversion(audio.current_time) }}</span>
           <el-slider
             v-model="play_value"

+ 4 - 0
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -17,6 +17,7 @@ const mixin = {
       isShowParse: false, // 是否显示解析
       isEnable,
       loader: false,
+      visibleAnswerAnalysis: false,
     };
   },
   inject: ['getLang', 'getChinese', 'convertText', 'getTitleList'],
@@ -186,6 +187,9 @@ const mixin = {
         ...borderData,
       };
     },
+    showAnswerAnalysis() {
+      this.visibleAnswerAnalysis = true;
+    },
   },
 };
 

+ 42 - 0
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -105,7 +105,18 @@
       </div>
     </div>
 
+    <div class="operation">
+      <div class="button retry"></div>
+      <div class="button correct"></div>
+      <div class="button answer" @click="showAnswerAnalysis"></div>
+    </div>
+
     <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+    />
   </div>
 </template>
 
@@ -123,6 +134,7 @@ import AudioFill from './components/AudioFillPlay.vue';
 import SoundRecord from '../../common/SoundRecord.vue';
 import SoundRecordBox from '@/views/book/courseware/preview/components/record_input/SoundRecord.vue';
 import WriteDialog from './components/WriteDialog.vue';
+import AnswerAnalysis from '../../common/AnswerAnalysis.vue';
 
 export default {
   name: 'FillPreview',
@@ -131,6 +143,7 @@ export default {
     SoundRecord,
     SoundRecordBox,
     WriteDialog,
+    AnswerAnalysis,
   },
   mixins: [PreviewMixin],
   data() {
@@ -470,6 +483,35 @@ export default {
       width: 100px;
     }
   }
+
+  .operation {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 8px;
+
+    .button {
+      width: 90px;
+      height: 40px;
+      cursor: pointer;
+      border-radius: 5px;
+
+      & + .button {
+        margin-left: 24px;
+      }
+
+      &.retry {
+        background: url('@/assets/component/component-retry.png') no-repeat center;
+      }
+
+      &.correct {
+        background: url('@/assets/component/component-correct.png') no-repeat center;
+      }
+
+      &.answer {
+        background: url('@/assets/component/component-answer.png') no-repeat center;
+      }
+    }
+  }
 }
 </style>