Browse Source

阅读题预览

dusenyao 1 year ago
parent
commit
bb28325410
28 changed files with 331 additions and 138 deletions
  1. 7 6
      src/components/common/RichText.vue
  2. 1 1
      src/styles/mixin.scss
  3. 1 0
      src/styles/variables.scss
  4. 22 3
      src/views/exercise_questions/create/components/common/SelectQuestionType.vue
  5. 0 4
      src/views/exercise_questions/create/components/common/SoundRecord.vue
  6. 1 3
      src/views/exercise_questions/create/components/create.vue
  7. 3 2
      src/views/exercise_questions/create/components/exercises/DialogueQuestion.vue
  8. 8 2
      src/views/exercise_questions/create/components/exercises/FillQuestion.vue
  9. 13 2
      src/views/exercise_questions/create/components/exercises/JudgeQuestion.vue
  10. 7 1
      src/views/exercise_questions/create/components/exercises/MatchingQuestion.vue
  11. 81 22
      src/views/exercise_questions/create/components/exercises/ReadQuestion.vue
  12. 8 2
      src/views/exercise_questions/create/components/exercises/SelectQuestion.vue
  13. 2 2
      src/views/exercise_questions/create/index.vue
  14. 2 1
      src/views/exercise_questions/data/chinese.js
  15. 22 13
      src/views/exercise_questions/data/common.js
  16. 27 22
      src/views/exercise_questions/data/judge.js
  17. 12 1
      src/views/exercise_questions/data/read.js
  18. 32 28
      src/views/exercise_questions/data/select.js
  19. 7 7
      src/views/exercise_questions/preview/ChooseTonePreview.vue
  20. 1 1
      src/views/exercise_questions/preview/DialoguePreview.vue
  21. 2 2
      src/views/exercise_questions/preview/JudgePreview.vue
  22. 1 1
      src/views/exercise_questions/preview/ReadAloudPreview.vue
  23. 57 3
      src/views/exercise_questions/preview/ReadPreview.vue
  24. 2 2
      src/views/exercise_questions/preview/RepeatPreview.vue
  25. 1 1
      src/views/exercise_questions/preview/SelectPreview.vue
  26. 2 2
      src/views/exercise_questions/preview/SortPreview.vue
  27. 9 0
      src/views/exercise_questions/preview/components/PreviewMixin.js
  28. 0 4
      src/views/exercise_questions/preview/components/common/SoundRecordPreview.vue

+ 7 - 6
src/components/common/RichText.vue

@@ -61,7 +61,7 @@ export default {
     },
     height: {
       type: [Number, String],
-      default: 110,
+      default: 52,
     },
     isBorder: {
       type: Boolean,
@@ -91,9 +91,8 @@ export default {
         placeholder: this.placeholder,
         language: 'zh_CN',
         skin_url: `${process.env.BASE_URL}tinymce/skins/ui/oxide`,
-        // height: this.height,
         content_css: `${process.env.BASE_URL}tinymce/skins/content/index.css`,
-        min_height: 52,
+        min_height: this.height,
         width: '100%',
         autoresize_bottom_margin: 0,
         plugins: 'link lists image hr media autoresize ax_wordlimit',
@@ -326,9 +325,11 @@ export default {
     }
   }
 
-  :deep &.is-border + .tox.tox-tinymce {
-    border-width: 2px;
-    border-radius: 10px;
+  &.is-border {
+    :deep + .tox.tox-tinymce {
+      border: $border;
+      border-radius: 4px;
+    }
   }
 }
 </style>

+ 1 - 1
src/styles/mixin.scss

@@ -32,7 +32,7 @@
   .description {
     min-height: 48px;
     padding: 12px 24px;
-    background-color: #f9f8f9;
+    background-color: $content-color;
     border-radius: 16px;
   }
 

+ 1 - 0
src/styles/variables.scss

@@ -8,6 +8,7 @@ $font-color: #1d2129;
 $font-light-color: #4e5969;
 $fill-color: #f2f3f5;
 $text-color: #86909c;
+$content-color: #f9f8f9;
 $border-color: #e5e6eb;
 $border: 1px solid $border-color;
 $hanzi-writer-color: #de4444;

+ 22 - 3
src/views/exercise_questions/create/components/common/SelectQuestionType.vue

@@ -8,9 +8,10 @@
       @change="handleChange"
     />
     <div class="intelligent-recognition" @click="showRecognition"><SvgIcon icon-class="copy" :size="14" />智能识别</div>
-    <div class="save-tip">{{ saveDate }} 已自动保存</div>
+    <SvgIcon v-if="isChild" class-name="pointer" icon-class="delete" :size="14" @click="$emit('deleteQuestion')" />
+    <div v-else class="save-tip">{{ saveDate }} 已自动保存</div>
 
-    <IntelligentRecognition :visible.sync="dialogVisible" @recognition="recognition" />
+    <IntelligentRecognition :visible.sync="dialogVisible" @recognition="handleRecognition" />
   </div>
 </template>
 
@@ -30,12 +31,19 @@ export default {
       type: Array,
       required: true,
     },
+    isChild: {
+      type: Boolean,
+      default: false,
+    },
+    questionTypeOption: {
+      type: Array,
+      default: () => questionTypeOption,
+    },
   },
   data() {
     return {
       dialogVisible: false, // 智能识别弹窗
       typeArr: this.type,
-      questionTypeOption,
     };
   },
   computed: {
@@ -50,9 +58,20 @@ export default {
   },
   methods: {
     handleChange(val) {
+      if (this.isChild) {
+        return this.$emit('updateCurQuestionType', val);
+      }
       this.updateCurQuestionType(val);
     },
 
+    // 处理智能识别
+    handleRecognition(text) {
+      if (this.isChild) {
+        return this.$emit('recognition', text);
+      }
+      this.recognition(text);
+    },
+
     showRecognition() {
       this.dialogVisible = true;
     },

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

@@ -39,10 +39,6 @@ export default {
       type: String,
       default: '',
     },
-    itemIndex: {
-      type: Number,
-      default: null,
-    },
   },
   data() {
     return {

+ 1 - 3
src/views/exercise_questions/create/components/create.vue

@@ -174,9 +174,7 @@ export default {
      */
     dataConversion() {
       const data = this.$refs.exercise[0].data;
-      const { score, score_type, select_type } = data.property;
-      data.answer.score = score;
-      data.answer.score_type = score_type;
+      const { select_type } = data.property;
 
       return {
         question_id: this.indexList[this.curIndex].id,

+ 3 - 2
src/views/exercise_questions/create/components/exercises/DialogueQuestion.vue

@@ -176,10 +176,11 @@ export default {
         .map(({ content }) => {
           return content
             .filter(({ type }) => type === 'input')
-            .map(({ content, ...data }) => {
+            .map(({ content, mark }) => {
               return {
                 value: content,
-                ...data,
+                mark,
+                type: 'only_one',
               };
             });
         })

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

@@ -37,7 +37,7 @@
           :is-fill="true"
           :toolbar="false"
           :wordlimit-num="false"
-          placeholder="输入文"
+          placeholder="输入文"
           @showContentmenu="showContentmenu"
           @hideContentmenu="hideContentmenu"
         />
@@ -138,6 +138,12 @@ export default {
     UploadAudio,
   },
   mixins: [QuestionMixin],
+  props: {
+    externalData: {
+      type: Object,
+      default: () => JSON.parse(JSON.stringify(fillData)),
+    },
+  },
   data() {
     return {
       isShow: false,
@@ -145,7 +151,7 @@ export default {
         top: 0,
         left: 0,
       },
-      data: JSON.parse(JSON.stringify(fillData)),
+      data: this.externalData,
     };
   },
   created() {

+ 13 - 2
src/views/exercise_questions/create/components/exercises/JudgeQuestion.vue

@@ -129,7 +129,12 @@ import UploadAudio from '../common/UploadAudio.vue';
 import QuestionMixin from '../common/QuestionMixin.js';
 
 import { changeOptionType } from '@/views/exercise_questions/data/common';
-import { judgeData, option_type_list, option_type_value_list, getOption } from '@/views/exercise_questions/data/judge';
+import {
+  getJudgeData,
+  option_type_list,
+  option_type_value_list,
+  getOption,
+} from '@/views/exercise_questions/data/judge';
 
 export default {
   name: 'JudgeQuestion',
@@ -137,11 +142,17 @@ export default {
     UploadAudio,
   },
   mixins: [QuestionMixin],
+  props: {
+    externalData: {
+      type: Object,
+      default: () => getJudgeData(),
+    },
+  },
   data() {
     return {
       option_type_list,
       changeOptionType,
-      data: JSON.parse(JSON.stringify(judgeData)),
+      data: this.externalData,
     };
   },
   computed: {

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

@@ -120,10 +120,16 @@ import { columnNumberList, getOption, getMatchingDataTemplate } from '@/views/ex
 export default {
   name: 'MatchingQuestion',
   mixins: [QuestionMixin],
+  props: {
+    externalData: {
+      type: Object,
+      default: () => getMatchingDataTemplate(),
+    },
+  },
   data() {
     return {
       columnNumberList,
-      data: getMatchingDataTemplate(),
+      data: this.externalData,
     };
   },
   watch: {

+ 81 - 22
src/views/exercise_questions/create/components/exercises/ReadQuestion.vue

@@ -11,7 +11,12 @@
           placeholder="输入题干"
         />
 
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText
+          v-if="data.property.stem_type === stemTypeList[1].value"
+          v-model="data.stem"
+          :wordlimit-num="5000"
+          placeholder="输入题干"
+        />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -24,7 +29,25 @@
       </div>
 
       <div class="content">
-        <RichText v-model="data.article" placeholder="输入文章" />
+        <RichText v-model="data.article" :is-border="true" :height="130" placeholder="输入文章" />
+      </div>
+
+      <div class="footer">
+        <span class="add-option" @click="addQuestion">
+          <SvgIcon icon-class="add-circle" size="14" /><span>增加配题</span>
+        </span>
+      </div>
+
+      <div v-for="(item, i) in data.question_list" :key="i">
+        <SelectQuestionType
+          :question-type-option="questionTypeOption"
+          :is-child="true"
+          :type="exerciseTypeList[item.type]"
+          @updateCurQuestionType="updateCurQuestionType(i, $event)"
+          @deleteQuestion="deleteQuestion(i)"
+          @recognition="recognition(i, $event)"
+        />
+        <component :is="exerciseComponents[item.type]" ref="exercise" :external-data="item" />
       </div>
     </template>
 
@@ -78,46 +101,82 @@
         </el-form-item>
       </el-form>
     </template>
-    <template #footer>
-      <div class="footer">
-        <span class="add-option" @click="addOption">
-          <SvgIcon icon-class="add-circle" size="14" /> <span>增加配题</span>
-        </span>
-      </div>
-    </template>
   </QuestionBase>
 </template>
 
 <script>
-import QuestionMixin from '../common/QuestionMixin.js';
+import { readData, questionTypeOption, exerciseTypeList } from '@/views/exercise_questions/data/read';
+import { getSelectData } from '@/views/exercise_questions/data/select';
+import { getJudgeData } from '@/views/exercise_questions/data/judge';
+import { getMatchingDataTemplate } from '@/views/exercise_questions/data/matching';
+import { fillData } from '@/views/exercise_questions/data/fill';
+import { curry } from '@/utils';
 
-import { changeOptionType } from '@/views/exercise_questions/data/common';
-import { readData } from '@/views/exercise_questions/data/read';
+import QuestionMixin from '../common/QuestionMixin.js';
+import SelectQuestionType from '@/views/exercise_questions/create/components/common/SelectQuestionType.vue';
+import SelectQuestion from '@/views/exercise_questions/create/components/exercises/SelectQuestion.vue';
+import JudgeQuestion from '@/views/exercise_questions/create/components/exercises/JudgeQuestion.vue';
+import MatchingQuestion from '@/views/exercise_questions/create/components/exercises/MatchingQuestion.vue';
+import FillQuestion from '@/views/exercise_questions/create/components/exercises/FillQuestion.vue';
 
 export default {
   name: 'ReadQuestion',
+  components: {
+    SelectQuestionType,
+    SelectQuestion,
+    JudgeQuestion,
+    MatchingQuestion,
+    FillQuestion,
+  },
   mixins: [QuestionMixin],
   data() {
     return {
-      changeOptionType,
+      curry,
+      questionTypeOption,
+      exerciseTypeList,
       data: JSON.parse(JSON.stringify(readData)),
+      exerciseComponents: {
+        select: SelectQuestion,
+        judge: JudgeQuestion,
+        matching: MatchingQuestion,
+        fill: FillQuestion,
+      },
+      exerciseData: {
+        select: getSelectData(),
+        judge: getJudgeData(),
+        matching: getMatchingDataTemplate(),
+        fill: fillData,
+      },
     };
   },
   methods: {
-    addOption() {
-      this.data.question_list.push('');
+    addQuestion() {
+      this.data.question_list.push(JSON.parse(JSON.stringify(this.exerciseData['select'])));
+    },
+
+    deleteQuestion(i) {
+      this.data.question_list.splice(i, 1);
+    },
+
+    /**
+     * 智能识别
+     * @param {Number} i 题目索引
+     * @param {String} text 识别数据
+     */
+    recognition(i, text) {
+      this.$refs.exercise[i]?.recognition(text);
+    },
+
+    updateCurQuestionType(i, arr) {
+      let type = arr[arr.length - 1];
+      this.$set(this.data.question_list, i, JSON.parse(JSON.stringify(this.exerciseData[type])));
     },
   },
 };
 </script>
 
 <style lang="scss" scoped>
-.footer {
-  padding: 5px 16px;
-  margin: 16px 0;
-  text-align: center;
-  cursor: pointer;
-  background-color: #fff;
-  border-radius: 2px;
+.question-type {
+  margin-top: 16px;
 }
 </style>

+ 8 - 2
src/views/exercise_questions/create/components/exercises/SelectQuestion.vue

@@ -133,7 +133,7 @@ import UploadAudio from '../common/UploadAudio.vue';
 import QuestionMixin from '../common/QuestionMixin.js';
 
 import { selectTypeList, scoreTypeList, changeOptionType } from '@/views/exercise_questions/data/common';
-import { selectData, getOption } from '@/views/exercise_questions/data/select';
+import { getSelectData, getOption } from '@/views/exercise_questions/data/select';
 
 export default {
   name: 'SelectQuestion',
@@ -141,11 +141,17 @@ export default {
     UploadAudio,
   },
   mixins: [QuestionMixin],
+  props: {
+    externalData: {
+      type: Object,
+      default: () => getSelectData(),
+    },
+  },
   data() {
     return {
       selectTypeList,
       changeOptionType,
-      data: JSON.parse(JSON.stringify(selectData)),
+      data: this.externalData,
     };
   },
   methods: {

+ 2 - 2
src/views/exercise_questions/create/index.vue

@@ -15,7 +15,7 @@
         </ul>
       </div>
       <div class="list-operate">
-        <el-button type="primary">批量导入</el-button>
+        <!-- <el-button type="primary">批量导入</el-button> -->
         <el-button type="primary" @click="addQuestionToExercise('select', 'single')">新建</el-button>
       </div>
     </div>
@@ -319,7 +319,7 @@ export default {
       display: flex;
 
       > .el-button {
-        width: 50%;
+        flex: 1;
       }
     }
   }

+ 2 - 1
src/views/exercise_questions/data/chinese.js

@@ -32,7 +32,8 @@ export const audioGenerationMethodList = [
     label: '录音',
   },
 ];
-// 选择题数据模板
+
+// 汉字题数据模板
 export const chineseData = {
   type: 'chinese', // 题型
   stem: '', // 题干

+ 22 - 13
src/views/exercise_questions/data/common.js

@@ -50,20 +50,29 @@ export const exerciseNames = questionTypeOption
     return obj;
   }, {});
 
+/**
+ * 获取题型类型列表
+ * @param {Array} arr 题型类型选项
+ * @returns Object
+ */
+export function getExerciseTypeList(arr) {
+  return arr
+    .map(({ value, children }) => {
+      if (children) {
+        return children.map(({ value: children_value }) => {
+          return { [children_value]: [value, children_value] };
+        });
+      }
+      return { [value]: [value] };
+    })
+    .flat()
+    .reduce((obj, item) => {
+      return { ...obj, ...item };
+    }, {});
+}
+
 // 题型类型列表
-export const exerciseTypeList = questionTypeOption
-  .map(({ value, children }) => {
-    if (children) {
-      return children.map(({ value: children_value }) => {
-        return { [children_value]: [value, children_value] };
-      });
-    }
-    return { [value]: [value] };
-  })
-  .flat()
-  .reduce((obj, item) => {
-    return { ...obj, ...item };
-  }, {});
+export const exerciseTypeList = getExerciseTypeList(questionTypeOption);
 
 // 选项类型
 export const optionTypeList = [

+ 27 - 22
src/views/exercise_questions/data/judge.js

@@ -14,25 +14,30 @@ export function getOption(content = '') {
   return { content, mark: getRandomNumber() };
 }
 
-// 判断题数据模板
-export const judgeData = {
-  type: 'judge', // 题型
-  stem: '', // 题干
-  option_number_show_mode: optionTypeList[0].value, // 选项类型
-  option_list: [getOption(), getOption(), getOption()], // 选项
-  file_id_list: [], // 文件 id 列表
-  answer: { answer_list: [], score: 0, score_type: scoreTypeList[0].value }, // 答案
-  // 题型属性
-  property: {
-    stem_type: stemTypeList[0].value, // 题干类型
-    question_number: '1', // 题号
-    option_type_list: [option_type_list[0].value, option_type_list[1].value], // 选项类型列表
-    is_enable_listening: switchOption[0].value, // 是否听力
-    score: 1, // 分值
-    score_type: scoreTypeList[0].value, // 分值类型
-  },
-  // 其他属性
-  other: {
-    question_number_type: questionNumberTypeList[0].value, // 题号类型
-  },
-};
+/**
+ * 获取判断题数据模板(防止 mark 重复)
+ * @returns {object} 判断题数据模板
+ */
+export function getJudgeData() {
+  return {
+    type: 'judge', // 题型
+    stem: '', // 题干
+    option_number_show_mode: optionTypeList[0].value, // 选项类型
+    option_list: [getOption(), getOption(), getOption()], // 选项
+    file_id_list: [], // 文件 id 列表
+    answer: { answer_list: [], score: 0, score_type: scoreTypeList[0].value }, // 答案
+    // 题型属性
+    property: {
+      stem_type: stemTypeList[0].value, // 题干类型
+      question_number: '1', // 题号
+      option_type_list: [option_type_list[0].value, option_type_list[1].value], // 选项类型列表
+      is_enable_listening: switchOption[0].value, // 是否听力
+      score: 1, // 分值
+      score_type: scoreTypeList[0].value, // 分值类型
+    },
+    // 其他属性
+    other: {
+      question_number_type: questionNumberTypeList[0].value, // 题号类型
+    },
+  };
+}

+ 12 - 1
src/views/exercise_questions/data/read.js

@@ -1,4 +1,15 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList } from './common';
+import { stemTypeList, scoreTypeList, questionNumberTypeList, getExerciseTypeList } from './common';
+
+// 题型类型选项
+export const questionTypeOption = [
+  { label: '选择题', value: 'select' },
+  { label: '判断题', value: 'judge' },
+  { label: '填空题', value: 'fill' },
+  { label: '连线题', value: 'matching' },
+];
+
+// 题型类型列表
+export const exerciseTypeList = getExerciseTypeList(questionTypeOption);
 
 // 阅读题数据模板
 export const readData = {

+ 32 - 28
src/views/exercise_questions/data/select.js

@@ -12,31 +12,35 @@ export function getOption(content = '') {
   return { content, mark: getRandomNumber() };
 }
 
-// 选择题数据模板
-export const selectData = {
-  type: 'select', // 题型
-  stem: '', // 题干
-  option_number_show_mode: optionTypeList[0].value, // 选项类型
-  description: '', // 描述
-  option_list: [
-    { content: '', mark: getRandomNumber() },
-    { content: '', mark: getRandomNumber() },
-    { content: '', mark: getRandomNumber() },
-  ], // 选项
-  file_id_list: [], // 文件 id 列表
-  answer: { answer_list: [], score: 0, score_type: scoreTypeList[0].value }, // 答案
-  // 题型属性
-  property: {
-    stem_type: stemTypeList[0].value, // 题干类型
-    question_number: '1', // 题号
-    is_enable_description: switchOption[1].value, // 描述
-    select_type: selectTypeList[0].value, // 选择类型
-    is_enable_listening: switchOption[0].value, // 是否听力
-    score: 1, // 分值
-    score_type: scoreTypeList[0].value, // 分值类型
-  },
-  // 其他属性
-  other: {
-    question_number_type: questionNumberTypeList[0].value, // 题号类型
-  },
-};
+/**
+ * 获取选择题数据模板(防止 mark 重复)
+ */
+export function getSelectData() {
+  return {
+    type: 'select', // 题型
+    stem: '', // 题干
+    option_number_show_mode: optionTypeList[0].value, // 选项类型
+    description: '', // 描述
+    option_list: [
+      { content: '', mark: getRandomNumber() },
+      { content: '', mark: getRandomNumber() },
+      { content: '', mark: getRandomNumber() },
+    ], // 选项
+    file_id_list: [], // 文件 id 列表
+    answer: { answer_list: [], score: 0, score_type: scoreTypeList[0].value }, // 答案
+    // 题型属性
+    property: {
+      stem_type: stemTypeList[0].value, // 题干类型
+      question_number: '1', // 题号
+      is_enable_description: switchOption[1].value, // 描述
+      select_type: selectTypeList[0].value, // 选择类型
+      is_enable_listening: switchOption[0].value, // 是否听力
+      score: 1, // 分值
+      score_type: scoreTypeList[0].value, // 分值类型
+    },
+    // 其他属性
+    other: {
+      question_number_type: questionNumberTypeList[0].value, // 题号类型
+    },
+  };
+}

+ 7 - 7
src/views/exercise_questions/preview/ChooseTonePreview.vue

@@ -47,12 +47,12 @@
             con_preview[i].user_answer[con_preview[i].item_active_index].select_tone === value
               ? 'active'
               : data.property.answer_mode === 'label' &&
-                con_preview[i].user_answer[con_preview[i].item_active_index] &&
-                con_preview[i].user_answer[con_preview[i].item_active_index].select_tone === value &&
-                con_preview[i].user_answer[con_preview[i].item_active_index].select_letter === active_letter &&
-                select_item_index === i
-              ? 'active'
-              : '',
+                  con_preview[i].user_answer[con_preview[i].item_active_index] &&
+                  con_preview[i].user_answer[con_preview[i].item_active_index].select_tone === value &&
+                  con_preview[i].user_answer[con_preview[i].item_active_index].select_letter === active_letter &&
+                  select_item_index === i
+                ? 'active'
+                : '',
           ]"
           @click="chooseTone(con_preview[i], value, i)"
         >
@@ -279,7 +279,7 @@ export default {
       .option-content {
         padding: 12px 24px;
         color: #706f78;
-        background-color: #f9f8f9;
+        background-color: $content-color;
         border-radius: 40px;
       }
 

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

@@ -91,7 +91,7 @@ export default {
 
   .dialogue-wrapper {
     padding: 24px;
-    background-color: #f9f8f9;
+    background-color: $content-color;
     border-radius: 16px;
 
     ul {

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

@@ -88,7 +88,7 @@ export default {
         flex: 1;
         padding: 12px 24px;
         color: #706f78;
-        background-color: #f9f8f9;
+        background-color: $content-color;
         border-radius: 40px;
       }
 
@@ -105,7 +105,7 @@ export default {
           height: 48px;
           color: #000;
           cursor: pointer;
-          background-color: #f9f8f9;
+          background-color: $content-color;
           border-radius: 50%;
 
           &.active {

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

@@ -40,7 +40,7 @@ export default {
 
   .reference-answer {
     padding: 12px 24px;
-    background-color: #f9f8f9;
+    background-color: $content-color;
     border-radius: 8px;
   }
 }

+ 57 - 3
src/views/exercise_questions/preview/ReadPreview.vue

@@ -6,20 +6,48 @@
       <span v-html="sanitizeHTML(data.stem)"></span>
     </div>
     <div v-if="isEnable(data.property.is_enable_description)" class="description">{{ data.description }}</div>
+
+    <div class="container">
+      <div class="articel" v-html="sanitizeHTML(data.article)"></div>
+      <div class="question-list">
+        <component
+          :is="previewComponents[item.type]"
+          v-for="(item, i) in data.question_list"
+          :key="i"
+          class="preview"
+          :data="item"
+          @change="changeAnswer(i, item.type, $event)"
+        />
+      </div>
+    </div>
   </div>
 </template>
 
 <script>
 import PreviewMixin from './components/PreviewMixin';
+import SelectPreview from '@/views/exercise_questions/preview/SelectPreview.vue';
+import JudgePreview from '@/views/exercise_questions/preview/JudgePreview.vue';
+import MatchingPreview from '@/views/exercise_questions/preview/MatchingPreview.vue';
+import FillPreview from '../preview/FillPreview.vue';
 
 export default {
   name: 'ReadPreview',
   mixins: [PreviewMixin],
   data() {
-    return {};
+    return {
+      previewComponents: {
+        select: SelectPreview,
+        judge: JudgePreview,
+        matching: MatchingPreview,
+        fill: FillPreview,
+      },
+    };
+  },
+  methods: {
+    changeAnswer(i, type, answer) {
+      this.answer.answer_list[i] = { type, ...answer };
+    },
   },
-  created() {},
-  methods: {},
 };
 </script>
 
@@ -28,5 +56,31 @@ export default {
 
 .read-preview {
   @include preview;
+
+  .container {
+    display: flex;
+    column-gap: 24px;
+
+    .articel {
+      max-width: 488px;
+      padding: 24px 40px;
+      color: #2f3742;
+      background-color: $content-color;
+      border-radius: 16px;
+    }
+
+    .question-list {
+      display: flex;
+      flex: 1;
+      flex-direction: column;
+      row-gap: 24px;
+
+      .preview {
+        min-height: 0;
+        padding: 0;
+        margin: 0;
+      }
+    }
+  }
 }
 </style>

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

@@ -79,13 +79,13 @@ export default {
         max-width: 306px;
         padding: 8px 16px;
         color: #706f78;
-        background-color: #f9f8f9;
+        background-color: $content-color;
         border-radius: 40px;
       }
 
       .sound-box {
         padding: 4px;
-        background: #f9f8f9;
+        background: $content-color;
         border-radius: 40px;
       }
     }

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

@@ -77,7 +77,7 @@ export default {
       padding: 12px 24px;
       color: #706f78;
       cursor: pointer;
-      background-color: #f9f8f9;
+      background-color: $content-color;
       border-radius: 40px;
 
       .selectionbox {

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

@@ -142,7 +142,7 @@ export default {
       flex: 1;
       max-width: 800px;
       padding: 8px 16px;
-      background: #f9f8f9;
+      background: $content-color;
       border-radius: 4px;
 
       :deep p {
@@ -164,7 +164,7 @@ export default {
         display: block;
         width: max-content;
         padding: 8px 16px;
-        background: #f9f8f9;
+        background: $content-color;
         border-radius: 4px;
       }
 

+ 9 - 0
src/views/exercise_questions/preview/components/PreviewMixin.js

@@ -20,6 +20,15 @@ const PreviewMixin = {
       answer: { answer_list: [] }, // 答案
     };
   },
+  watch: {
+    answer: {
+      handler() {
+        this.$emit('change', this.answer);
+      },
+      deep: true,
+      immediate: true,
+    },
+  },
   methods: {
     /**
      * 获取答案

+ 0 - 4
src/views/exercise_questions/preview/components/common/SoundRecordPreview.vue

@@ -49,10 +49,6 @@ export default {
       type: String,
       default: '',
     },
-    itemIndex: {
-      type: Number,
-      default: null,
-    },
     type: {
       type: String,
       default: 'big',