Selaa lähdekoodia

修改问题和填表题

dusenyao 1 vuosi sitten
vanhempi
commit
47dd3a1e96
24 muutettua tiedostoa jossa 745 lisäystä ja 564 poistoa
  1. 214 476
      package-lock.json
  2. 11 11
      package.json
  3. 1 2
      src/views/exercise_questions/create/components/common/SelectQuestionType.vue
  4. 2 2
      src/views/exercise_questions/create/components/create.vue
  5. 1 1
      src/views/exercise_questions/create/components/exercises/MatchingQuestion.vue
  6. 0 17
      src/views/exercise_questions/create/components/exercises/ReadAloudQuestion.vue
  7. 1 1
      src/views/exercise_questions/create/components/exercises/ReadQuestion.vue
  8. 44 3
      src/views/exercise_questions/create/components/exercises/SelectQuestion.vue
  9. 1 1
      src/views/exercise_questions/create/components/exercises/ShortAnswerQuestion.vue
  10. 136 23
      src/views/exercise_questions/create/components/exercises/TableFillQuestion.vue
  11. 2 0
      src/views/exercise_questions/data/PreviewQuestionTypeMixin.js
  12. 2 2
      src/views/exercise_questions/data/questionType.js
  13. 0 1
      src/views/exercise_questions/data/readAloud.js
  14. 2 2
      src/views/exercise_questions/data/tableFill.js
  15. 4 4
      src/views/exercise_questions/preview/FillPreview.vue
  16. 1 0
      src/views/exercise_questions/preview/ListenFillPreview.vue
  17. 1 0
      src/views/exercise_questions/preview/ListenJudgePreview.vue
  18. 0 6
      src/views/exercise_questions/preview/ReadAloudPreview.vue
  19. 1 1
      src/views/exercise_questions/preview/RepeatPreview.vue
  20. 36 7
      src/views/exercise_questions/preview/SelectPreview.vue
  21. 1 0
      src/views/exercise_questions/preview/ShortAnswerPreview.vue
  22. 280 0
      src/views/exercise_questions/preview/TableFillPreview.vue
  23. 3 3
      src/views/exercise_questions/preview/TalkPictruePreview.vue
  24. 1 1
      src/views/exercise_questions/preview/WordCardPreview.vue

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 214 - 476
package-lock.json


+ 11 - 11
package.json

@@ -10,7 +10,7 @@
   },
   "dependencies": {
     "@tinymce/tinymce-vue": "^3.2.8",
-    "axios": "^1.6.2",
+    "axios": "^1.6.3",
     "core-js": "^3.34.0",
     "dompurify": "^3.0.6",
     "element-ui": "^2.15.14",
@@ -27,9 +27,9 @@
     "vuex": "^3.6.2"
   },
   "devDependencies": {
-    "@babel/core": "^7.23.5",
+    "@babel/core": "^7.23.6",
     "@babel/eslint-parser": "^7.23.3",
-    "@rushstack/eslint-patch": "^1.6.0",
+    "@rushstack/eslint-patch": "^1.6.1",
     "@types/md5": "^2.3.5",
     "@vue/cli-plugin-babel": "~5.0.8",
     "@vue/cli-plugin-eslint": "~5.0.8",
@@ -37,24 +37,24 @@
     "@vue/eslint-config-prettier": "^8.0.0",
     "@vue/preload-webpack-plugin": "^2.0.0",
     "babel-plugin-dynamic-import-node": "^2.3.3",
-    "compression-webpack-plugin": "^6.1.1",
-    "eslint": "^8.55.0",
-    "eslint-plugin-prettier": "^5.0.1",
+    "compression-webpack-plugin": "^6.1.2",
+    "eslint": "^8.56.0",
+    "eslint-plugin-prettier": "^5.1.2",
     "eslint-plugin-vue": "^9.19.2",
     "patch-package": "^8.0.0",
     "postcss-html": "^1.5.0",
-    "prettier": "^3.1.0",
+    "prettier": "^3.1.1",
     "sass": "^1.69.5",
     "sass-loader": "^13.3.2",
     "stylelint": "^15.11.0",
     "stylelint-config-recess-order": "^4.4.0",
-    "stylelint-config-recommended-scss": "^13.1.0",
+    "stylelint-config-recommended-scss": "^14.0.0",
     "stylelint-config-recommended-vue": "^1.5.0",
-    "stylelint-config-standard-scss": "^11.1.0",
-    "stylelint-declaration-block-no-ignored-properties": "^2.7.0",
+    "stylelint-config-standard-scss": "^12.0.0",
+    "stylelint-declaration-block-no-ignored-properties": "^2.8.0",
     "stylelint-webpack-plugin": "^4.1.1",
     "svg-sprite-loader": "^6.0.11",
-    "svgo": "^3.0.5",
+    "svgo": "^3.1.0",
     "vue-template-compiler": "^2.6.14"
   },
   "engines": {

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

@@ -57,8 +57,7 @@ export default {
 <style lang="scss">
 .select-question-type {
   .el-cascader-menu__wrap {
-    // height: 300px;
-    height: 270px;
+    height: 300px;
   }
 }
 </style>

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

@@ -63,7 +63,7 @@ import AnswerQuestion from './exercises/AnswerQuestion.vue';
 import WritePictureQuestion from './exercises/WritePictureQuestion.vue';
 import ReplaceAnswerQuestion from './exercises/ReplaceAnswerQuestion.vue';
 import EssayQuestion from './exercises/EssayQuestion.vue';
-import FillFormQuestion from './exercises/FillFormQuestion.vue';
+import TableFillQuestion from './exercises/TableFillQuestion.vue';
 
 export default {
   name: 'CreateMain',
@@ -112,7 +112,7 @@ export default {
         write_picture: WritePictureQuestion,
         replace_answer: ReplaceAnswerQuestion,
         essay_question: EssayQuestion,
-        fill_form: FillFormQuestion,
+        table_fill: TableFillQuestion,
       },
     };
   },

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

@@ -53,7 +53,7 @@
             {{ label }}
           </el-radio>
         </el-form-item>
-        <el-form-item label="描述">
+        <el-form-item label="提示">
           <el-radio
             v-for="{ value, label } in switchOption"
             :key="value"

+ 0 - 17
src/views/exercise_questions/create/components/exercises/ReadAloudQuestion.vue

@@ -10,12 +10,6 @@
           @upload="upload"
           @deleteFile="deleteFile"
         />
-
-        <RichText
-          v-if="isEnable(data.property.is_enable_description)"
-          v-model="data.description"
-          placeholder="输入文段"
-        />
       </div>
     </template>
 
@@ -35,17 +29,6 @@
           </el-radio>
         </el-form-item>
 
-        <el-form-item label="描述">
-          <el-radio
-            v-for="{ value, label } in switchOption"
-            :key="value"
-            v-model="data.property.is_enable_description"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
-
         <el-form-item label="听力">
           <el-radio
             v-for="{ value, label } in switchOption"

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

@@ -58,7 +58,7 @@
             {{ label }}
           </el-radio>
         </el-form-item>
-        <el-form-item label="描述">
+        <el-form-item label="提示">
           <el-radio
             v-for="{ value, label } in switchOption"
             :key="value"

+ 44 - 3
src/views/exercise_questions/create/components/exercises/SelectQuestion.vue

@@ -21,15 +21,30 @@
             <div v-if="isEnable(data.property.is_option_subdivision)" class="option-list">
               <div v-for="li in item.data_list" :key="li.mark" class="option-content">
                 <span
-                  :class="['checkbox', { active: isAnswer(li.mark, item.mark) }]"
+                  v-if="data.property.select_type === selectTypeList[0].value"
+                  :class="['selectionbox', { active: isAnswer(li.mark, item.mark) }]"
                   @click="selectAnswer(li.mark, item.mark)"
                 ></span>
+                <span
+                  v-else
+                  :class="['checkbox', { active: isAnswer(li.mark, item.mark) }]"
+                  @click="selectAnswer(li.mark, item.mark)"
+                >
+                  <SvgIcon icon-class="check-mark" width="10" height="7" />
+                </span>
                 <RichText v-model="li.content" placeholder="输入内容" :inline="true" />
               </div>
             </div>
 
             <div v-else class="option-content">
-              <span :class="['checkbox', { active: isAnswer(item.mark) }]" @click="selectAnswer(item.mark)"></span>
+              <span
+                v-if="data.property.select_type === selectTypeList[0].value"
+                :class="['selectionbox', { active: isAnswer(item.mark) }]"
+                @click="selectAnswer(item.mark)"
+              ></span>
+              <span v-else :class="['checkbox', { active: isAnswer(item.mark) }]" @click="selectAnswer(item.mark)">
+                <SvgIcon icon-class="check-mark" width="10" height="7" />
+              </span>
               <RichText v-model="item.content" placeholder="输入内容" :inline="true" />
             </div>
             <SvgIcon icon-class="delete" class="delete pointer" @click="deleteOption(i)" />
@@ -269,7 +284,7 @@ export default {
   }
 
   .option-content {
-    .checkbox {
+    .selectionbox {
       display: inline-flex;
       align-items: center;
       justify-content: center;
@@ -291,6 +306,32 @@ export default {
         }
       }
     }
+
+    .checkbox {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 16px;
+      height: 16px;
+      margin-right: 8px;
+      cursor: pointer;
+      background-color: #fff;
+      border: 1px solid #dcdfe6;
+      border-radius: 2px;
+
+      .svg-icon {
+        display: none;
+      }
+
+      &.active {
+        color: #fff;
+        background-color: $light-main-color;
+
+        .svg-icon {
+          display: block;
+        }
+      }
+    }
   }
 }
 </style>

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

@@ -46,7 +46,7 @@
           </el-radio>
         </el-form-item>
 
-        <el-form-item label="描述">
+        <el-form-item label="提示">
           <el-radio
             v-for="{ value, label } in switchOption"
             :key="value"

+ 136 - 23
src/views/exercise_questions/create/components/exercises/FillFormQuestion.vue → src/views/exercise_questions/create/components/exercises/TableFillQuestion.vue

@@ -25,7 +25,7 @@
                 <div class="rich">
                   <RichText
                     ref="modelEssay"
-                    v-model="data.option_list[i - 1][j - 1].text"
+                    v-model="data.option_list[j - 1][i - 1].text"
                     :is-fill="true"
                     :toolbar="false"
                     :font-size="12"
@@ -107,15 +107,15 @@
 import QuestionMixin from '../common/QuestionMixin.js';
 
 import { getRandomNumber } from '@/utils';
-import { getFillFormData, getOption } from '@/views/exercise_questions/data/fillForm';
+import { getTableFillData, getOption } from '@/views/exercise_questions/data/tableFill.js';
 
 export default {
-  name: 'FillFormQuestion',
+  name: 'TableFillQuestion',
   mixins: [QuestionMixin],
   data() {
     return {
       isMask: false,
-      data: getFillFormData(),
+      data: getTableFillData(),
     };
   },
   computed: {
@@ -126,18 +126,17 @@ export default {
   watch: {
     'data.property.row_number': {
       handler(val) {
-        this.data.option_list = this.data.option_list.map((item) => {
-          const arr = [];
-          for (let i = 0; i < val; i++) {
-            arr.push(item[i] || getOption());
-          }
-          return arr;
-        });
+        if (val > this.data.option_list.length) {
+          this.data.option_list.push(Array.from({ length: this.data.property.column_number }, () => getOption()));
+        } else if (val < this.data.option_list.length) {
+          this.data.option_list.pop();
+        }
       },
       immediate: true,
     },
     'data.property.column_number': {
       handler(val) {
+        // 列数变化时,需要重新计算每列的宽度
         if (val > this.data.option_header_list.length) {
           let width = 0;
           this.data.option_header_list.forEach((item) => {
@@ -148,27 +147,35 @@ export default {
           });
           this.data.option_header_list.push({ mark: getRandomNumber(), text: '', width });
         } else if (val < this.data.option_header_list.length) {
+          let width = this.data.option_header_list[val].width;
           this.data.option_header_list.pop();
+          this.data.option_header_list.forEach((item) => {
+            item.width += width / val;
+          });
         }
 
-        if (val > this.data.option_list.length) {
-          this.data.option_list.push(Array.from({ length: this.data.property.row_number }, () => getOption()));
-        } else if (val < this.data.option_list.length) {
-          this.data.option_list.pop();
-        }
+        this.data.option_list = this.data.option_list.map((item) => {
+          const arr = [];
+          for (let i = 0; i < val; i++) {
+            arr.push(item[i] || getOption());
+          }
+          return arr;
+        });
       },
       immediate: true,
     },
     optionList: {
       handler(newVal, oldVal) {
         if (newVal.length === 0 || !oldVal) return;
-        for (let i = 0; i < newVal.length; i++) {
-          for (let j = 0; j < newVal[i].length; j++) {
-            if (newVal[i][j].text !== oldVal[i][j].text) {
-              console.log('text属性发生变化:', newVal[i][j].text);
-            }
-          }
-        }
+        if (newVal.length !== oldVal.length || newVal[0].length !== oldVal[0].length) return;
+        newVal.forEach((item, i) => {
+          item.forEach((li, j) => {
+            let newValue = newVal[i][j];
+            let oldValue = oldVal[i][j];
+            if (newValue.text === oldValue.text) return;
+            this.handleOptionChange(newValue, oldValue, i, j);
+          });
+        });
       },
       immediate: true,
       deep: true,
@@ -216,6 +223,112 @@ export default {
         width: `${this.data.property.form_width}px`,
       };
     },
+    /**
+     * 处理选项内容变化
+     * @param {object} newValue 新值
+     * @param {object} oldValue 旧值
+     * @param {number} i 选项列表的索引
+     * @param {number} j 选项列表中的索引
+     */
+    handleOptionChange(newValue, oldValue, i, j) {
+      let arr = newValue.text.split(/<p>(.*?)<\/p>/gi).filter((item) => item);
+      // 答案数组中的索引
+      let answerIndex = this.data.answer.answer_list.findIndex(({ mark }) => mark === newValue.mark);
+      if (arr.length === 0) {
+        if (answerIndex !== -1) {
+          this.data.answer.answer_list.splice(answerIndex, 1);
+        }
+        return;
+      }
+      let isStart = /^##/.test(arr[0]); // 是否以 ## 开头
+      if (isStart) {
+        this.handleSpecialCharacterStart(arr, answerIndex, newValue.mark, i, j);
+      } else {
+        let str = arr.join().replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '###$1$2###');
+        this.handleFill(str, answerIndex, newValue.mark, i, j);
+      }
+    },
+    /**
+     * 处理填空或其他情况
+     * @param {string} str 处理后的字符串
+     * @param {number} answerIndex 答案数组的索引
+     * @param {string} newValueMark 新值的标识
+     * @param {number} i 选项列表的索引
+     * @param {number} j 选项列表中的索引
+     */
+    handleFill(str, answerIndex, newValueMark, i, j) {
+      let _str = str;
+      let start = 0;
+      let index = 0;
+      let arr = [];
+      let matchNum = 0;
+      while (index !== -1) {
+        index = _str.indexOf('###', start);
+        if (index === -1) break;
+        matchNum += 1;
+        arr.push({ content: _str.slice(start, index), type: 'text', mark: '' });
+        if (matchNum % 2 === 0 && arr.length > 0) {
+          arr[arr.length - 1].type = 'input';
+          let mark = getRandomNumber();
+          arr[arr.length - 1].mark = mark;
+        }
+        start = index + 3;
+      }
+      let last = _str.slice(start);
+      if (last) {
+        arr.push({ content: last, type: 'text' });
+      }
+      let value_list = arr
+        .filter(({ type }) => type === 'input')
+        .map(({ content, mark }) => ({ value: content, mark }));
+      if (answerIndex === -1 && value_list.length > 0) {
+        this.data.answer.answer_list.push({
+          value_list,
+          mark: newValueMark,
+        });
+      } else if (answerIndex !== -1) {
+        this.data.answer.answer_list[answerIndex].value_list = value_list;
+      }
+      this.data.option_list[i][j].content_list = arr.map(({ content, type, mark }) => {
+        return {
+          type,
+          mark,
+          content: type === 'input' ? '' : content,
+        };
+      });
+    },
+    /**
+     * 处理特殊字符开头
+     * @param {array} arr 文本数组
+     * @param {number} answerIndex 答案数组的索引
+     * @param {string} newValueMark 新值的标识
+     * @param {number} i 选项列表的索引
+     * @param {number} j 选项列表中的索引
+     */
+    handleSpecialCharacterStart(arr, answerIndex, newValueMark, i, j) {
+      // 去除第一个元素的 ##
+      arr[0] = arr[0].slice(2);
+      let _arr = arr.map((item) => {
+        return item.replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '$1$2');
+      });
+      let mark = getRandomNumber();
+
+      if (answerIndex === -1) {
+        this.data.answer.answer_list.push({
+          value_list: [{ mark, value: _arr.join('') }],
+          mark: newValueMark,
+        });
+      } else {
+        this.data.answer.answer_list[answerIndex].value_list = [{ mark, value: _arr.join('') }];
+      }
+      this.data.option_list[i][j].content_list = [
+        {
+          type: 'fill',
+          mark,
+          content: '',
+        },
+      ];
+    },
   },
 };
 </script>

+ 2 - 0
src/views/exercise_questions/data/PreviewQuestionTypeMixin.js

@@ -21,6 +21,7 @@ import AnswerQuestionPreview from '../preview/AnswerQuestionPreview.vue';
 import WritePictruePreview from '../preview/WritePictruePreview.vue';
 import ReplaceAnswerPreview from '../preview/ReplaceAnswerPreview.vue';
 import EssayQuestionPreview from '../preview/EssayQuestionPreview.vue';
+import TableFillPreview from '../preview/TableFillPreview.vue';
 
 const PreviewQuestionTypeMixin = {
   data() {
@@ -48,6 +49,7 @@ const PreviewQuestionTypeMixin = {
         write_picture: WritePictruePreview,
         replace_answer: ReplaceAnswerPreview,
         essay_question: EssayQuestionPreview,
+        table_fill: TableFillPreview,
       },
     };
   },

+ 2 - 2
src/views/exercise_questions/data/questionType.js

@@ -19,7 +19,7 @@ import { wordCardData } from './wordCard';
 import { writeData } from './write';
 import { writePictrueData } from './writePicture';
 import { readData } from './read';
-import { getFillFormData } from './fillForm';
+import { getTableFillData } from './tableFill';
 
 // 题型源数据
 export const questionTypeDataOption = [
@@ -35,7 +35,7 @@ export const questionTypeDataOption = [
       { label: '连线题', value: 'matching', data: getMatchingDataTemplate() },
       { label: '选择声调', value: 'choose_tone', data: ChooseToneData },
       { label: '问答题', value: 'essay_question', data: essayQuestionData },
-      // { label: '填表题', value: 'fill_form', data: getFillFormData() },
+      { label: '填表题', value: 'table_fill', data: getTableFillData() },
     ],
   },
   {

+ 0 - 1
src/views/exercise_questions/data/readAloud.js

@@ -15,7 +15,6 @@ export const readAloudData = {
     stem_type: stemTypeList[1].value, // 题干类型
     question_number: '1', // 题号
     is_enable_listening: switchOption[0].value, // 是否开启听力
-    is_enable_description: switchOption[0].value, // 是否启用描述
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型
   },

+ 2 - 2
src/views/exercise_questions/data/fillForm.js → src/views/exercise_questions/data/tableFill.js

@@ -9,9 +9,9 @@ export function getOption() {
  * 获取填表题数据模板(防止 mark 重复)
  * @returns {object} 判断题数据模板
  */
-export function getFillFormData() {
+export function getTableFillData() {
   return {
-    type: 'fill_form', // 题型
+    type: 'table_fill', // 题型
     stem: '', // 题干
     option_header_list: [
       { mark: getRandomNumber(), text: '', width: 50 },

+ 4 - 4
src/views/exercise_questions/preview/FillPreview.vue

@@ -171,8 +171,8 @@ export default {
       if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
         return '';
       }
-      let selectOption = this.answer.answer_list.find((item) => item.mark === mark); // 是否已选中的选项
-      let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark); // 是否正确的选项
+      let selectOption = this.answer.answer_list.find((item) => item.mark === mark);
+      let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
       if (!selectOption) return '';
       let selectValue = selectOption.value;
       let answerValue = answerOption.value;
@@ -193,8 +193,8 @@ export default {
     },
     computedAnswerText(mark) {
       if (!this.isShowRightAnswer) return '';
-      let selectOption = this.answer.answer_list.find((item) => item.mark === mark); // 是否已选中的选项
-      let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark); // 是否正确的选项
+      let selectOption = this.answer.answer_list.find((item) => item.mark === mark);
+      let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
       if (!selectOption) return '';
       let selectValue = selectOption.value;
       let answerValue = answerOption.value;

+ 1 - 0
src/views/exercise_questions/preview/ListenFillPreview.vue

@@ -14,6 +14,7 @@
     <AudioPlay
       v-if="isEnable(data.property.is_enable_listening) && data.file_id_list.length > 0"
       :file-id="data.file_id_list[0]"
+      :show-slider="true"
     />
 
     <div class="fill-wrapper">

+ 1 - 0
src/views/exercise_questions/preview/ListenJudgePreview.vue

@@ -8,6 +8,7 @@
     <AudioPlay
       v-if="isEnable(data.property.is_enable_listening) && data.file_id_list.length > 0"
       :file-id="data.file_id_list[0]"
+      :show-slider="true"
     />
 
     <ul class="option-list">

+ 0 - 6
src/views/exercise_questions/preview/ReadAloudPreview.vue

@@ -12,12 +12,6 @@
       :show-slider="true"
     />
 
-    <div
-      v-if="isEnable(data.property.is_enable_description)"
-      class="description rich-text"
-      v-html="sanitizeHTML(data.description)"
-    ></div>
-
     <SoundRecordPreview :wav-blob.sync="answer.answer_list[0].voice_file_id" position="center" />
   </div>
 </template>

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

@@ -16,7 +16,7 @@
         <AudioPlay
           v-if="data.option_list[i] && data.option_list[i].audio_file_id"
           :file-id="data.option_list[i].audio_file_id"
-          :showSlider="true"
+          :show-slider="true"
         />
         <div
           v-if="sanitizeHTML(data.option_list[i].content)"

+ 36 - 7
src/views/exercise_questions/preview/SelectPreview.vue

@@ -21,7 +21,10 @@
           :class="['option-item', { active: isAnswer(mark, item.mark) }, ...computedAnswerClass(mark, item.mark)]"
           @click="selectAnswer(mark, item.mark)"
         >
-          <span class="selectionbox"></span>
+          <span v-if="data.property.select_type === selectTypeList[0].value" class="selectionbox"></span>
+          <span v-else class="checkbox">
+            <SvgIcon icon-class="check-mark" width="10" height="7" />
+          </span>
           <span class="content rich-text" v-html="sanitizeHTML(content)"></span>
         </li>
       </ul>
@@ -35,7 +38,11 @@
         :class="['option-item', { active: isAnswer(mark) }, ...computedAnswerClass(mark)]"
         @click="selectAnswer(mark)"
       >
-        <span class="selectionbox"></span>
+        {{ selectTypeList[0].type }}
+        <span v-if="data.property.select_type === selectTypeList[0].value" class="selectionbox"></span>
+        <span v-else class="checkbox">
+          <SvgIcon icon-class="check-mark" width="10" height="7" />
+        </span>
         <span class="serial-number">{{ computeOptionMethods[data.option_number_show_mode](i) }} </span>
         <span class="content rich-text" v-html="sanitizeHTML(content)"></span>
       </li>
@@ -53,6 +60,7 @@ export default {
   mixins: [PreviewMixin],
   data() {
     return {
+      selectTypeList,
       computeOptionMethods,
     };
   },
@@ -61,11 +69,6 @@ export default {
       return isEnable(this.data.property.is_option_subdivision);
     },
   },
-  created() {
-    if (isEnable(this.data.property.is_option_subdivision)) {
-      this.answer.answer_list = this.data.option_list.map(() => []);
-    }
-  },
   methods: {
     isAnswer(mark, parent_mark) {
       if (isEnable(this.data.property.is_option_subdivision)) {
@@ -168,6 +171,23 @@ export default {
         border-radius: 50%;
       }
 
+      .checkbox {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 14px;
+        height: 14px;
+        margin-right: 8px;
+        cursor: pointer;
+        background-color: #fff;
+        border: 1px solid #dcdfe6;
+        border-radius: 2px;
+
+        .svg-icon {
+          display: none;
+        }
+      }
+
       .serial-number {
         font-size: 16pt;
       }
@@ -195,6 +215,15 @@ export default {
           border-width: 4px;
         }
 
+        .checkbox {
+          color: #fff;
+          background-color: $light-main-color;
+
+          .svg-icon {
+            display: block;
+          }
+        }
+
         &.right {
           background-color: $content-color;
           border: 1px solid $right-color;

+ 1 - 0
src/views/exercise_questions/preview/ShortAnswerPreview.vue

@@ -11,6 +11,7 @@
     <AudioPlay
       v-if="isEnable(data.property.is_enable_listening) && data.file_id_list.length > 0"
       :file-id="data.file_id_list[0]"
+      :show-slider="true"
     />
 
     <el-input

+ 280 - 0
src/views/exercise_questions/preview/TableFillPreview.vue

@@ -0,0 +1,280 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="fill-form-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+
+    <div class="form-wrapper">
+      <div class="form" :style="{ width: `${data.property.form_width}px` }">
+        <div class="form-header">
+          <div
+            v-for="({ text, mark, width }, i) in data.option_header_list"
+            :key="mark"
+            :style="{ width: `${width}%` }"
+            class="header-item"
+          >
+            <span v-if="i === 0 && isEnable(data.property.is_enable_number_column)" class="serial-number">#</span>
+            <span>{{ text }}</span>
+          </div>
+        </div>
+        <div v-for="(item, i) in optionList" :key="i" class="form-content">
+          <div
+            v-for="({ mark, content_list }, j) in item"
+            :key="mark"
+            :style="{ width: `${data.option_header_list[i].width}%` }"
+            class="form-item"
+          >
+            <span v-if="j === 0 && isEnable(data.property.is_enable_number_column)" class="serial-number">
+              {{ i + 1 }}
+            </span>
+            <template v-for="(li, k) in content_list">
+              <span v-if="li.type === 'text'" :key="k">{{ li.content }}</span>
+              <template v-else-if="li.type === 'fill'">
+                <el-input
+                  :key="k"
+                  v-model="li.content"
+                  :class="['content_input', ...computedAnswerClass(mark, li.mark)]"
+                  placeholder="请输入"
+                  :style="[{ width: Math.max(80, li.content.length * 16) + 'px' }]"
+                />
+                <span
+                  v-show="computedAnswerText(mark, li.mark).length > 0"
+                  :key="`answer-${i}-${j}-${k}`"
+                  class="right-answer-content"
+                >
+                  {{ computedAnswerText(mark, li.mark) }}
+                </span>
+              </template>
+              <template v-else-if="li.type === 'input'">
+                <el-input
+                  :key="k"
+                  v-model="li.content"
+                  :class="['input', ...computedAnswerClass(mark, li.mark)]"
+                  :style="[{ width: Math.max(80, li.content.length * 16) + 'px' }]"
+                />
+                <span
+                  v-show="computedAnswerText(mark, li.mark).length > 0"
+                  :key="`answer-${i}-${j}-${k}`"
+                  class="right-answer-fill"
+                >
+                  {{ computedAnswerText(mark, li.mark) }}
+                </span>
+              </template>
+            </template>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+
+export default {
+  name: 'TableFillPreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      optionList: [],
+    };
+  },
+  watch: {
+    'data.option_list': {
+      handler(val) {
+        this.optionList = JSON.parse(JSON.stringify(val));
+      },
+      immediate: true,
+      deep: true,
+    },
+    optionList: {
+      handler(val) {
+        val.forEach((item) => {
+          item.forEach(({ mark, content_list }) => {
+            let value_list = [];
+            let answerIndex = this.answer.answer_list.findIndex((item) => item.mark === mark);
+            content_list.forEach((li) => {
+              if (['fill', 'input'].includes(li.type) && li.content.length > 0) {
+                value_list.push({
+                  mark: li.mark,
+                  value: li.content,
+                });
+              }
+            });
+            if (answerIndex === -1 && value_list.length <= 0) return;
+            if (answerIndex !== -1 && value_list.length <= 0) {
+              this.answer.answer_list.splice(answerIndex, 1);
+              return;
+            }
+            if (answerIndex !== -1) {
+              this.answer.answer_list[answerIndex].value_list = value_list;
+              return;
+            }
+            this.answer.answer_list.push({
+              mark,
+              value_list,
+            });
+          });
+        });
+      },
+      deep: true,
+    },
+  },
+  methods: {
+    /**
+     * 计算答题对错选项字体颜色
+     * @param {string} mark 选项标识
+     * @param {string} liMark 选项子标识
+     */
+    computedAnswerClass(mark, liMark) {
+      if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
+        return '';
+      }
+      let selectOption = this.answer.answer_list
+        .find((item) => item.mark === mark)
+        ?.value_list.find((item) => item.mark === liMark);
+      let answerOption = this.data.answer.answer_list
+        .find((item) => item.mark === mark)
+        ?.value_list.find((item) => item.mark === liMark);
+      if (!selectOption) return '';
+      let selectValue = selectOption.value;
+      let answerValue = answerOption.value;
+      let classList = [];
+      let isRight = selectValue === answerValue;
+
+      if (this.isJudgingRightWrong) {
+        isRight ? classList.push('right') : classList.push('wrong');
+      }
+
+      if (this.isShowRightAnswer && !isRight) {
+        classList.push('show-right-answer');
+      }
+      return classList;
+    },
+    /**
+     * 计算正确答案
+     * @param {string} mark 选项标识
+     * @param {string} liMark 选项子标识
+     */
+    computedAnswerText(mark, liMark) {
+      if (!this.isShowRightAnswer) return '';
+      // mark 对应答题选项
+      let selectOption = this.answer.answer_list
+        .find((item) => item.mark === mark)
+        ?.value_list.find((item) => item.mark === liMark);
+      // mark 对应正确答案选项
+      let answerOption = this.data.answer.answer_list
+        .find((item) => item.mark === mark)
+        ?.value_list.find((item) => item.mark === liMark);
+      if (!selectOption) return '';
+      let selectValue = selectOption.value;
+      let answerValue = answerOption.value;
+      let isRight = selectValue === answerValue;
+      if (isRight) return '';
+      return `(${answerValue})`;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.fill-form-preview {
+  @include preview;
+
+  .form-wrapper {
+    overflow: auto;
+
+    .form {
+      &-header {
+        display: flex;
+        font-weight: bold;
+        color: $main-color;
+        background-color: #eaeffb;
+
+        .header-item {
+          display: flex;
+          align-items: center;
+          padding: 8px 12px;
+        }
+      }
+
+      &-content {
+        display: flex;
+
+        .form-item {
+          display: flex;
+          align-items: center;
+          padding: 8px 12px;
+          border-bottom: 1px solid #eaeffb;
+
+          %input,
+          .el-input.content_input {
+            display: inline-flex;
+            align-items: center;
+            width: 120px;
+            margin: 0 2px;
+
+            &.right {
+              :deep input.el-input__inner {
+                color: $right-color;
+              }
+            }
+
+            &.wrong {
+              :deep input.el-input__inner {
+                color: $error-color;
+              }
+            }
+
+            & + .right-answer-fill {
+              position: relative;
+              left: -4px;
+              display: inline-block;
+              height: 32px;
+              line-height: 28px;
+              vertical-align: bottom;
+              border-bottom: 1px solid $font-color;
+            }
+
+            & + .right-answer-content {
+              position: relative;
+              left: -4px;
+              display: inline-block;
+              height: 32px;
+              line-height: 28px;
+              vertical-align: bottom;
+            }
+
+            :deep input.el-input__inner {
+              padding: 0;
+              font-size: 16px;
+              color: $font-color;
+              text-align: left;
+              background-color: #fff;
+              border-width: 0;
+              border-radius: 0;
+            }
+          }
+
+          .fill_input {
+            @extend %input;
+
+            :deep input.el-input__inner {
+              text-align: center;
+              border-bottom: 1px solid $font-color;
+            }
+          }
+        }
+      }
+
+      .serial-number {
+        padding-right: 16px;
+      }
+    }
+  }
+}
+</style>

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

@@ -18,13 +18,13 @@
           :src="pic_list[item.picture_file_id]"
           fit="cover"
         />
-        <div class="content-box" v-if="item.picture_info">
+        <div v-if="item.picture_info" class="content-box">
           <span class="option-number">{{ computeOptionMethods[data.option_number_show_mode](index) }} </span>
           <p class="pic-info rich-text" v-html="sanitizeHTML(item.picture_info)"></p>
         </div>
         <div class="content-box" style="align-items: center">
-          <span class="option-number" v-if="!item.picture_info"
-            >{{ computeOptionMethods[data.option_number_show_mode](index) }}
+          <span v-if="!item.picture_info" class="option-number">
+            {{ computeOptionMethods[data.option_number_show_mode](index) }}
           </span>
           <!-- 语音作答 -->
           <div v-if="isEnable(data.property.is_enable_voice_answer) && answer.answer_list[index]" class="sound-box">

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

@@ -25,7 +25,7 @@
       <div class="words-right">
         <template v-for="(item, index) in option_list">
           <div v-if="index === active_index" :key="index" class="strock-box">
-            <div class="pinyin-box" v-if="item.audio_file_id || item.pinyin">
+            <div v-if="item.audio_file_id || item.pinyin" class="pinyin-box">
               <AudioPlay v-if="item.audio_file_id" :file-id="item.audio_file_id" theme-color="white" />
               <span class="pinyin">{{ item.pinyin }}</span>
             </div>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä