Browse Source

Merge branch 'master' of http://60.205.254.193:3000/GCLS/GCLS_Page_Exercise

dusenyao 1 year ago
parent
commit
c98eeadd4d
31 changed files with 672 additions and 462 deletions
  1. 121 3
      src/views/exercise_questions/create/components/common/AudioPlay.vue
  2. 2 24
      src/views/exercise_questions/create/components/exercises/AnswerQuestion.vue
  3. 10 38
      src/views/exercise_questions/create/components/exercises/ChineseQuestion.vue
  4. 13 25
      src/views/exercise_questions/create/components/exercises/ChooseToneQuestion.vue
  5. 1 20
      src/views/exercise_questions/create/components/exercises/EssayQuestion.vue
  6. 122 22
      src/views/exercise_questions/create/components/exercises/RepeatQuestion.vue
  7. 38 27
      src/views/exercise_questions/create/components/exercises/ReplaceAnswerQuestion.vue
  8. 1 20
      src/views/exercise_questions/create/components/exercises/SortQuestion.vue
  9. 3 24
      src/views/exercise_questions/create/components/exercises/TalkPictureQuestion.vue
  10. 31 33
      src/views/exercise_questions/create/components/exercises/WordCardQuestion.vue
  11. 4 23
      src/views/exercise_questions/create/components/exercises/WritePictureQuestion.vue
  12. 1 20
      src/views/exercise_questions/create/components/exercises/WriteQuestion.vue
  13. 1 3
      src/views/exercise_questions/data/answerQuestion.js
  14. 2 3
      src/views/exercise_questions/data/chinese.js
  15. 1 2
      src/views/exercise_questions/data/chooseTone.js
  16. 1 2
      src/views/exercise_questions/data/essayQuestion.js
  17. 17 2
      src/views/exercise_questions/data/repeat.js
  18. 2 2
      src/views/exercise_questions/data/replaceAnswer.js
  19. 1 2
      src/views/exercise_questions/data/sort.js
  20. 2 3
      src/views/exercise_questions/data/talkPicture.js
  21. 2 3
      src/views/exercise_questions/data/wordCard.js
  22. 1 2
      src/views/exercise_questions/data/write.js
  23. 1 2
      src/views/exercise_questions/data/writePicture.js
  24. 0 3
      src/views/exercise_questions/preview/AnswerQuestionPreview.vue
  25. 49 43
      src/views/exercise_questions/preview/ChinesePreview.vue
  26. 7 5
      src/views/exercise_questions/preview/ChooseTonePreview.vue
  27. 6 4
      src/views/exercise_questions/preview/RepeatPreview.vue
  28. 11 2
      src/views/exercise_questions/preview/ReplaceAnswerPreview.vue
  29. 0 1
      src/views/exercise_questions/preview/TalkPictruePreview.vue
  30. 47 58
      src/views/exercise_questions/preview/WordCardPreview.vue
  31. 174 41
      src/views/exercise_questions/preview/components/common/SoundRecordPreview.vue

+ 121 - 3
src/views/exercise_questions/create/components/common/AudioPlay.vue

@@ -1,6 +1,13 @@
 <template>
   <div class="audio-wrapper">
-    <span :class="[url ? 'audio-play' : 'audio-play not-url']" @click="playAudio">
+    <span
+      :class="[url ? 'audio-play' : 'audio-play not-url']"
+      :style="{
+        padding: showSlider ? '4px 16px' : '',
+        width: showSlider ? 'auto' : '',
+      }"
+      @click="playAudio"
+    >
       <SvgIcon v-if="audio.paused" :size="audio.paused ? 20 : 14" :icon-class="iconClass" />
       <img
         v-else
@@ -11,13 +18,34 @@
         "
         class="voice-play"
       />
+
+      <template v-if="showSlider">
+        <span class="audio-time">{{ secondFormatConversion(audio.current_time) }}</span>
+        <el-slider
+          v-model="play_value"
+          class="audio-slider"
+          :format-tooltip="formatProcessToolTip"
+          @change="changeCurrentTime"
+        />
+        <span class="audio-time">{{ audio_allTime }}</span>
+      </template>
     </span>
-    <audio :id="fileId" :ref="fileId" :src="url" preload="metadata"></audio>
+
+    <audio
+      :id="fileId"
+      :ref="fileId"
+      :src="url"
+      preload="metadata"
+      @loadedmetadata="onLoadedmetadata"
+      @timeupdate="onTimeupdate"
+      @canplaythrough="oncanplaythrough"
+    ></audio>
   </div>
 </template>
 
 <script>
 import { GetFileURLMap } from '@/api/app';
+import { secondFormatConversion } from '@/utils/transform';
 
 export default {
   name: 'AudioPlay',
@@ -30,13 +58,27 @@ export default {
       type: String,
       default: '',
     },
+    showSlider: {
+      type: Boolean,
+      default: false,
+    },
   },
   data() {
     return {
+      secondFormatConversion,
       url: '',
       audio: {
         paused: true,
+        playing: false,
+        // 音频当前播放时长
+        current_time: 0,
+        // 音频最大播放时长
+        max_time: 0,
+        isPlaying: false,
+        loading: false,
       },
+      play_value: 0,
+      audio_allTime: null, // 展示总时间
     };
   },
   computed: {
@@ -85,6 +127,50 @@ export default {
       }
       audio.paused ? audio.play() : audio.pause();
     },
+    // 进度条格式化toolTip
+    formatProcessToolTip(index) {
+      let indexs = parseInt((this.audio.max_time / 100) * index);
+      return secondFormatConversion(indexs);
+    },
+    // 点击 拖拽播放音频
+    changeCurrentTime(value) {
+      let audioId = this.fileId;
+      this.$refs[audioId].play();
+      this.audio.playing = true;
+      this.$refs[audioId].currentTime = parseInt((value / 100) * this.audio.max_time);
+    },
+    // 音频加载完之后
+    onLoadedmetadata(res) {
+      this.audio.max_time = parseInt(res.target.duration);
+      this.audio_allTime = secondFormatConversion(this.audio.max_time);
+    },
+    // 当音频当前时间改变后,进度条也要改变
+    onTimeupdate(res) {
+      let audioId = this.fileId;
+      this.audio.current_time = res.target.currentTime;
+      this.play_value = (this.audio.current_time / this.audio.max_time) * 100;
+      if (this.audio.current_time * 1000 > this.ed) {
+        this.$refs[audioId].pause();
+      }
+    },
+    onTimeupdateTime(res, playFlag) {
+      if (!res && res !== 0) return;
+      let audioId = this.audioId;
+      this.$refs[audioId].currentTime = res;
+      this.play_value = (res / this.audio.max_time) * 100;
+      if (playFlag) {
+        let audio = document.getElementsByTagName('audio');
+        audio.forEach((item) => {
+          if (item.id !== audioId) {
+            item.pause();
+          }
+        });
+        this.$refs[audioId].play();
+      }
+    },
+    oncanplaythrough() {
+      this.audio.loading = false;
+    },
   },
 };
 </script>
@@ -93,6 +179,7 @@ export default {
 .audio-wrapper {
   .audio-play {
     display: flex;
+    column-gap: 8px;
     align-items: center;
     justify-content: center;
     width: 40px;
@@ -100,7 +187,7 @@ export default {
     color: #fff;
     cursor: pointer;
     background-color: $main-color;
-    border-radius: 50%;
+    border-radius: 20px;
 
     &.not-url {
       color: #a1a1a1;
@@ -111,6 +198,37 @@ export default {
       width: 20px;
       height: 20px;
     }
+
+    .audio-time {
+      min-width: 35px;
+      font-size: 12px;
+      font-weight: 400;
+      line-height: 20px;
+    }
+
+    .audio-slider {
+      width: 82px;
+
+      :deep .el-slider__runway {
+        height: 4px;
+        background-color: #1853c6;
+      }
+
+      :deep .el-slider__button {
+        width: 4px;
+        height: 4px;
+        border: none;
+      }
+
+      :deep .el-slider__bar {
+        height: 4px;
+        background-color: #fff;
+      }
+
+      :deep .el-slider__button-wrapper {
+        top: -16px;
+      }
+    }
   }
 }
 </style>

+ 2 - 24
src/views/exercise_questions/create/components/exercises/AnswerQuestion.vue

@@ -2,18 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
-
-        <UploadAudio :file-id="data.file_id_list?.[0]" @upload="upload" @deleteFile="deleteFile" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
         <el-input
           v-if="isEnable(data.property.is_enable_reference_answer)"
           v-model="data.reference_answer"
@@ -26,16 +15,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -85,13 +64,12 @@
 
 <script>
 import QuestionMixin from '../common/QuestionMixin.js';
-import UploadAudio from '../common/UploadAudio.vue';
 
 import { answerQuestionData } from '@/views/exercise_questions/data/answerQuestion';
 
 export default {
   name: 'AnswerQuestion',
-  components: { UploadAudio },
+  components: {},
   mixins: [QuestionMixin],
   data() {
     return {

+ 10 - 38
src/views/exercise_questions/create/components/exercises/ChineseQuestion.vue

@@ -2,16 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -40,16 +31,12 @@
             >
               {{ computedQuestionNumber(i, data.option_number_show_mode) }}
             </span>
-            <el-input
-              v-model="item.content"
-              :maxlength="data.property.learn_type === 'dictation' ? null : 1"
-              :placeholder="data.property.learn_type === 'dictation' ? '输入汉字或词汇' : '输入一个汉字'"
-              @blur="handleChineseStrokes(item, i)"
-            />
+            <el-input v-model="item.content" :placeholder="'输入汉字或词汇'" @blur="handleChineseStrokes(item, i)" />
             <el-input
               v-model="item.pinyin"
               :placeholder="data.property.learn_type === 'dictation' ? '拼音间用空格隔开' : '输入拼音'"
               @blur="handleSplitPy(item)"
+              @change="changePinyin(item)"
             />
             <UploadAudio
               v-if="data.other.audio_generation_method === 'upload'"
@@ -93,16 +80,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -149,12 +126,11 @@
             :key="value"
             v-model="data.property.learn_type"
             :label="value"
-            @change="handleChangeType"
           >
             {{ label }}
           </el-radio>
         </el-form-item>
-        <el-form-item v-if="data.property.learn_type !== 'dictation'" label="田字格">
+        <el-form-item v-if="data.property.learn_type !== 'dictation'" label="田字格">
           <el-input
             v-model="data.property.tian_number"
             type="number"
@@ -299,16 +275,6 @@ export default {
           });
       }
     },
-    // 改变类型
-    handleChangeType() {
-      if (this.data.property.learn_type !== 'dictation') {
-        this.data.option_list.forEach((item) => {
-          if (item.content.trim()) {
-            item.content = item.content.substring(0, 1);
-          }
-        });
-      }
-    },
     // 生成汉字
     handleChineseStrokes(item, i) {
       if (item.content.trim()) {
@@ -351,6 +317,12 @@ export default {
         item.pinyin_item_list.push(obj);
       });
     },
+    // 修改拼音
+    changePinyin(item) {
+      if (this.data.other.audio_generation_method === 'auto') {
+        item.audio_file_id = '';
+      }
+    },
   },
 };
 </script>

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

@@ -2,16 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -34,6 +25,7 @@
               v-model="item.content"
               placeholder="拼音间用空格隔开,如:ni3 ha3o"
               @blur="handleItemAnswer(item)"
+              @change="changePinyin(item)"
             />
             <UploadAudio
               v-if="data.other.audio_generation_method === 'upload'"
@@ -68,16 +60,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -296,7 +278,7 @@ export default {
       let content_preview = '';
       this.res_arr = [];
       this.$set(this.matically_pinyin_obj, item.mark, []);
-      content_arr.forEach((items) => {
+      content_arr.forEach((items, index) => {
         let items_trim = items.trim();
         if (items_trim) {
           let items_yuan = JSON.parse(JSON.stringify(items_trim)).replace(/0|1|2|3|4/, '');
@@ -304,10 +286,10 @@ export default {
           if (this.data.property.answer_mode === 'select') {
             // 如果是选择声调 把声调放在拼音后面
             // select_item += `${items_yuan + items_trim.substring(indexs, indexs + 1)} `;
-            select_item += `${items_trim.substring(indexs, indexs + 1)} `;
+            select_item += `${items_trim.substring(indexs, indexs + 1)}${index === content_arr.length - 1 ? '' : ' '}`;
           } else if (this.data.property.answer_mode === 'label') {
             // 如果是标注声调 把声调放在对应字母后面
-            select_item += `${items_trim} `;
+            select_item += `${items_trim}${index === content_arr.length - 1 ? '' : ' '}`;
           }
           content_preview += `${items_yuan} `;
           this.handleReplaceTone(items_yuan + items_trim.substring(indexs, indexs + 1), item.mark);
@@ -316,11 +298,11 @@ export default {
       if (index === -1) {
         let obj = {
           mark: item.mark,
-          value: select_item.trim().split(' '),
+          value: select_item.split(' '),
         };
         this.data.answer.answer_list.push(obj);
       } else {
-        this.data.answer.answer_list[index].value = select_item.trim().split(' ');
+        this.data.answer.answer_list[index].value = select_item.split(' ');
       }
       item.content_view = content_preview.trim().split(' ');
       // item.matically_pinyin = matically_pinyin.trim().split(' ').join(',');
@@ -331,6 +313,12 @@ export default {
         this.handleItemAnswer(item);
       });
     },
+    // 修改拼音
+    changePinyin(item) {
+      if (this.data.other.audio_generation_method === 'auto') {
+        item.audio_file_id = '';
+      }
+    },
   },
 };
 </script>

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

@@ -2,16 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -33,16 +24,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>

+ 122 - 22
src/views/exercise_questions/create/components/exercises/RepeatQuestion.vue

@@ -3,16 +3,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -32,9 +23,10 @@
               {{ computedQuestionNumber(i, data.option_number_show_mode) }}
             </span>
             <div class="option-content">
-              <RichText v-model="item.content" placeholder="输入内容" :inline="true" />
+              <RichText v-model="item.content" :class="'repeat' + i" placeholder="输入内容" :inline="true" />
             </div>
             <UploadAudio
+              v-if="data.other.audio_generation_method === 'upload'"
               :key="item.audio_file_id || i"
               :file-id="item.audio_file_id"
               :item-index="i"
@@ -42,6 +34,16 @@
               @upload="uploads"
               @deleteFile="deleteFiles"
             />
+            <div v-else-if="data.other.audio_generation_method === 'auto'" class="auto-matically">
+              <AudioPlay v-if="item.audio_file_id" :file-id="item.audio_file_id" theme-color="gray" />
+              <span
+                v-loading="loading_list[i] ? loading_list[i].loading : false"
+                class="auto-btn"
+                @click="handleMatically(item, i)"
+                >{{ item.audio_file_id ? '已生成' : '自动生成' }}</span
+              >
+            </div>
+            <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
             <SvgIcon icon-class="delete" class="delete pointer" @click="deleteOption(i, item.audio_file_id)" />
           </li>
         </ul>
@@ -56,16 +58,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -106,6 +98,16 @@
             :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
           />
         </el-form-item>
+        <el-form-item label="音频">
+          <el-radio
+            v-for="{ value, label } in audioGenerationMethodList"
+            :key="value"
+            v-model="data.other.audio_generation_method"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
       </el-form>
     </template>
   </QuestionBase>
@@ -114,23 +116,53 @@
 <script>
 import UploadAudio from '../common/UploadAudio.vue';
 import QuestionMixin from '../common/QuestionMixin.js';
+import SoundRecord from '../common/SoundRecord.vue';
 
 import { selectTypeList, changeOptionType } from '@/views/exercise_questions/data/common';
-import { repeatData, getOption } from '@/views/exercise_questions/data/repeat';
+import { repeatData, getOption, audioGenerationMethodList } from '@/views/exercise_questions/data/repeat';
+import { GetStaticResources } from '@/api/app';
 
 export default {
   name: 'RepeatQuestion',
   components: {
     UploadAudio,
+    SoundRecord,
   },
   mixins: [QuestionMixin],
   data() {
     return {
       selectTypeList,
+      audioGenerationMethodList,
       changeOptionType,
       data: JSON.parse(JSON.stringify(repeatData)),
+      loading_list: [
+        {
+          loading: false,
+        },
+        {
+          loading: false,
+        },
+        {
+          loading: false,
+        },
+      ],
     };
   },
+  watch: {
+    'data.option_list.length': {
+      handler(val) {
+        this.loading_list = [];
+        for (let i = 0; i < val; i++) {
+          let obj = {
+            loading: false,
+          };
+          this.loading_list.push(obj);
+        }
+      },
+      deep: true,
+      immediate: true,
+    },
+  },
   methods: {
     /**
      * 智能识别
@@ -168,9 +200,35 @@ export default {
         .then(() => {
           this.data.option_list.splice(i, 1);
           this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+          this.loading_list.splice(i, 1);
         })
         .catch(() => {});
     },
+    // 自动生成音频
+    handleMatically(item, i) {
+      if (
+        document.getElementsByClassName(`repeat${i}`) &&
+        document.getElementsByClassName(`repeat${i}`)[0] &&
+        document.getElementsByClassName(`repeat${i}`)[0].innerText
+      ) {
+        this.loading_list[i].loading = true;
+        let MethodName = 'tool-PinyinToVoiceFile';
+        let data = {
+          pinyin: document.getElementsByClassName(`repeat${i}`)[0].innerText.trim().split(' ').join(','),
+        };
+        GetStaticResources(MethodName, data)
+          .then((res) => {
+            this.loading_list[i].loading = false;
+            if (res.status === 1) {
+              item.audio_file_id = res.file_id;
+              this.data.file_id_list.push(res.file_id);
+            }
+          })
+          .catch(() => {
+            this.loading_list[i].loading = false;
+          });
+      }
+    },
   },
 };
 </script>
@@ -187,5 +245,47 @@ export default {
     text-overflow: ellipsis;
     white-space: nowrap;
   }
+
+  .auto-matically {
+    display: flex;
+    flex-shrink: 0;
+    align-items: center;
+    width: 233px;
+    padding: 5px 12px;
+    background-color: $fill-color;
+    border-radius: 2px;
+
+    .audio-wrapper {
+      margin-right: 12px;
+
+      :deep .audio-play {
+        width: 16px;
+        height: 16px;
+        color: #000;
+        background-color: initial;
+      }
+
+      :deep .audio-play.not-url {
+        color: #a1a1a1;
+      }
+
+      :deep .voice-play {
+        width: 16px;
+        height: 16px;
+      }
+    }
+
+    .auto-btn {
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 22px;
+      color: #1d2129;
+      cursor: pointer;
+    }
+  }
+
+  .delete {
+    flex-shrink: 0;
+  }
 }
 </style>

+ 38 - 27
src/views/exercise_questions/create/components/exercises/ReplaceAnswerQuestion.vue

@@ -3,16 +3,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -37,16 +28,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -94,7 +75,17 @@
             :step="1"
             class="word-num-input"
             :precision="0"
-            @change="handleChangeRows"
+            @change="handleChangeRows('row')"
+          />
+        </el-form-item>
+        <el-form-item label="列数">
+          <el-input-number
+            v-model="data.property.column_count"
+            :min="1"
+            :step="1"
+            class="word-num-input"
+            :precision="0"
+            @change="handleChangeRows('column')"
           />
         </el-form-item>
       </el-form>
@@ -104,6 +95,7 @@
 
 <script>
 import QuestionMixin from '../common/QuestionMixin.js';
+import { getRandomNumber } from '@/utils/index';
 
 import { selectTypeList, changeOptionType } from '@/views/exercise_questions/data/common';
 import { replaceAnswerData, getOption } from '@/views/exercise_questions/data/replaceAnswer';
@@ -139,13 +131,32 @@ export default {
       this.data.option_list.push(getOption());
     },
     // 修改行数
-    handleChangeRows(val) {
-      if (this.data.option_list.length >= val) {
-        this.data.option_list = this.data.option_list.splice(0, val);
+    handleChangeRows(type) {
+      let row_count = this.data.property.row_count;
+      let column_count = this.data.property.column_count;
+      if (type === 'row') {
+        if (this.data.option_list.length >= row_count) {
+          this.data.option_list = this.data.option_list.splice(0, row_count);
+        } else {
+          let length = row_count - this.data.option_list.length;
+          for (let i = 0; i < length; i++) {
+            let table_item = [];
+            for (let j = 0; j < column_count; j++) {
+              table_item.push({ content: '', mark: getRandomNumber() });
+            }
+            this.data.option_list.push(table_item);
+          }
+        }
+      } else if (this.data.option_list[0].length >= column_count) {
+        this.data.option_list.forEach((item, index) => {
+          this.data.option_list[index] = item.splice(0, column_count);
+        });
       } else {
-        let length = val - this.data.option_list.length;
-        for (let i = 0; i < length; i++) {
-          this.addOption();
+        let length = column_count - this.data.option_list[0].length;
+        for (let i = 0; i < row_count; i++) {
+          for (let j = 0; j < length; j++) {
+            this.data.option_list[i].push({ content: '', mark: getRandomNumber() });
+          }
         }
       }
     },

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

@@ -3,16 +3,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -44,16 +35,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>

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

@@ -2,16 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -39,10 +30,6 @@
             </div>
             <div class="item-right">
               <div class="item-rich">
-                <label class="">图片标题</label>
-                <RichText v-model="item.picture_title" placeholder="输入图片标题" />
-              </div>
-              <div class="item-rich">
                 <label class="">图片信息</label>
                 <RichText v-model="item.picture_info" placeholder="输入图片信息" />
               </div>
@@ -52,6 +39,7 @@
               </div>
             </div>
           </template>
+          <el-divider />
         </div>
         <UploadDrag ref="uploadDrag" :limit="999" @fileUploadSuccess="fileUploadSuccess" />
       </div>
@@ -59,16 +47,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -225,6 +203,7 @@ export default {
 
   .content-item {
     display: flex;
+    flex-wrap: wrap;
     column-gap: 8px;
     margin-bottom: 24px;
   }

+ 31 - 33
src/views/exercise_questions/create/components/exercises/WordCardQuestion.vue

@@ -2,16 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
 
         <el-input
           v-show="isEnable(data.property.is_enable_description)"
@@ -46,7 +37,7 @@
                   :placeholder="'输入汉字或词汇'"
                   @blur="handleChineseStrokes(item, i)"
                 />
-                <el-input v-model="item.pinyin" :placeholder="'拼音间用空格隔开'" />
+                <el-input v-model="item.pinyin" :placeholder="'拼音间用空格隔开'" @change="changePinyin(item)" />
                 <UploadAudio
                   v-if="data.other.audio_generation_method === 'upload'"
                   :key="item.audio_file_id || i"
@@ -66,8 +57,8 @@
                   >
                 </div>
                 <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
-                <span class="rate-box"><el-rate v-model="item.rate" /></span>
                 <el-input v-model="item.definition" placeholder="输入释义" type="textarea" :rows="1" />
+                <el-input v-model="item.collocation" placeholder="输入搭配" />
               </div>
               <SvgIcon
                 icon-class="delete"
@@ -82,7 +73,11 @@
                   {{ computedQuestionNumber(indexs, data.option_number_show_mode) }}
                 </span>
                 <el-input v-model="item.example_sentence[indexs]" placeholder="输入例句" />
+                <SvgIcon icon-class="delete" class="delete pointer" @click="deleteSentence(item, indexs)" />
               </div>
+              <span class="add-option" @click="addSentence(item)">
+                <SvgIcon icon-class="add-circle" size="14" /> <span>增加例句</span>
+              </span>
             </div>
           </li>
         </ul>
@@ -96,16 +91,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -256,7 +241,7 @@ export default {
     },
     // 删除小题
     deleteOption(i, file_id, pic_id) {
-      this.$confirm('是否删除?', '提示', {
+      this.$confirm('是否删除该条内容?', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
@@ -271,7 +256,7 @@ export default {
     },
     // 删除
     delectOptions(i, id) {
-      this.$confirm('是否删除?', '提示', {
+      this.$confirm('是否删除该图片?', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
@@ -284,6 +269,18 @@ export default {
         })
         .catch(() => {});
     },
+    // 删除例句
+    deleteSentence(item, index) {
+      this.$confirm('是否删除该条例句?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          item.example_sentence.splice(index, 1);
+        })
+        .catch(() => {});
+    },
     // 自动生成音频
     handleMatically(item, i) {
       if (item.pinyin.trim()) {
@@ -341,6 +338,16 @@ export default {
         item.hz_strokes_list = content_arr_strokes;
       }
     },
+    // 增加例句
+    addSentence(item) {
+      item.example_sentence.push('');
+    },
+    // 修改拼音
+    changePinyin(item) {
+      if (this.data.other.audio_generation_method === 'auto') {
+        item.audio_file_id = '';
+      }
+    },
   },
 };
 </script>
@@ -394,15 +401,6 @@ export default {
       .el-input {
         flex: 1;
       }
-
-      .rate-box {
-        width: 128px;
-        height: 32px;
-        padding: 6px 0;
-        text-align: center;
-        background: #f2f3f5;
-        border-radius: 2px;
-      }
     }
 
     .example-sentence-box {

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

@@ -2,16 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
       </div>
 
       <div class="content">
@@ -40,7 +31,7 @@
             </div>
           </div>
         </div>
-        <UploadDrag @fileUploadSuccess="fileUploadSuccess" :limit="999" ref="uploadDrag"></UploadDrag>
+        <UploadDrag ref="uploadDrag" :limit="999" @fileUploadSuccess="fileUploadSuccess" />
 
         <label class="title-little">阅读材料:</label>
         <RichText v-model="data.article" placeholder="输入阅读材料" />
@@ -59,16 +50,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>
@@ -110,7 +91,7 @@
           />
         </el-form-item>
         <el-form-item label="字数">
-          <el-input-number :min="1" :step="10" v-model="data.property.word_num" class="word-num-input" :precision="0" />
+          <el-input-number v-model="data.property.word_num" :min="1" :step="10" class="word-num-input" :precision="0" />
         </el-form-item>
         <el-form-item label="范文">
           <el-radio
@@ -146,8 +127,8 @@ import UploadDrag from '../common/UploadDrag.vue';
 
 export default {
   name: 'WritePictureQuestion',
-  mixins: [QuestionMixin],
   components: { UploadDrag },
+  mixins: [QuestionMixin],
   data() {
     return {
       data: JSON.parse(JSON.stringify(writePictrueData)),

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

@@ -2,16 +2,7 @@
   <QuestionBase>
     <template #content>
       <div class="stem">
-        <el-input
-          v-if="data.property.stem_type === stemTypeList[0].value"
-          v-model="data.stem"
-          rows="3"
-          resize="none"
-          type="textarea"
-          placeholder="输入题干"
-        />
-
-        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <RichText v-model="data.stem" placeholder="输入题干" />
       </div>
 
       <div class="content">
@@ -34,16 +25,6 @@
 
     <template #property>
       <el-form :model="data.property">
-        <el-form-item label="题干">
-          <el-radio
-            v-for="{ value, label } in stemTypeList"
-            :key="value"
-            v-model="data.property.stem_type"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="题号">
           <el-input v-model="data.property.question_number" />
         </el-form-item>

+ 1 - 3
src/views/exercise_questions/data/answerQuestion.js

@@ -1,10 +1,9 @@
-import { stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } from './common';
+import { questionNumberTypeList, scoreTypeList, switchOption } from './common';
 
 // 口语表达数据模板
 export const answerQuestionData = {
   type: 'answer_question', // 题型
   stem: '', // 题干
-  file_id_list: [], // 文件 id 列表
   reference_answer: '', // 参考答案
   answer: {
     score: 1,
@@ -12,7 +11,6 @@ export const answerQuestionData = {
   }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_reference_answer: switchOption[0].value, // 是否开启参考答案
     score: 1, // 分值

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

@@ -1,4 +1,4 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList, optionTypeList } from './common';
+import { scoreTypeList, questionNumberTypeList, optionTypeList } from './common';
 import { getRandomNumber } from '@/utils/index';
 
 export function getOption(content = '') {
@@ -47,13 +47,12 @@ export const chineseData = {
   file_id_list: [],
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_description: 'false', // 描述
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型
     learn_type: learnTypeList[0].value,
-    tian_number: 8, // 田字格
+    tian_number: 8, // 田字格
   },
   // 其他属性
   other: {

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

@@ -1,4 +1,4 @@
-import { stemTypeList, questionNumberTypeList, scoreTypeList, optionTypeList } from './common';
+import { questionNumberTypeList, scoreTypeList, optionTypeList } from './common';
 import { getRandomNumber } from '@/utils/index';
 
 export function getOption(content = '') {
@@ -49,7 +49,6 @@ export const ChooseToneData = {
   answer: { score: 1, score_type: scoreTypeList[0].value, answer_list: [] }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_description: 'false', // 描述
     answer_mode: toneTypeList[0].value, // 音调类型

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

@@ -1,4 +1,4 @@
-import { stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } from './common';
+import { questionNumberTypeList, scoreTypeList, switchOption } from './common';
 
 // 问答题数据模板
 export const essayQuestionData = {
@@ -13,7 +13,6 @@ export const essayQuestionData = {
   }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_description: switchOption[0].value, // 描述
     is_enable_reference_answer: switchOption[0].value, // 是否开启参考答案

+ 17 - 2
src/views/exercise_questions/data/repeat.js

@@ -1,9 +1,24 @@
-import { optionTypeList, stemTypeList, scoreTypeList, questionNumberTypeList } from './common';
+import { optionTypeList, scoreTypeList, questionNumberTypeList } from './common';
 import { getRandomNumber } from '@/utils/index';
 
 export function getOption(content = '') {
   return { content, mark: getRandomNumber(), audio_file_id: '' };
 }
+// 音频生成方式类型
+export const audioGenerationMethodList = [
+  {
+    value: 'upload',
+    label: '上传',
+  },
+  {
+    value: 'auto',
+    label: '自动生成',
+  },
+  {
+    value: 'record',
+    label: '录音',
+  },
+];
 
 // 听后训练数据模板
 export const repeatData = {
@@ -20,7 +35,6 @@ export const repeatData = {
   answer: { score: 1, score_type: scoreTypeList[0].value }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_description: 'false', // 描述
     score: 1, // 分值
@@ -29,5 +43,6 @@ export const repeatData = {
   // 其他属性
   other: {
     question_number_type: questionNumberTypeList[0].value, // 题号类型
+    audio_generation_method: audioGenerationMethodList[0].value, // 音频生成方式
   },
 };

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

@@ -1,4 +1,4 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList } from './common';
+import { scoreTypeList, questionNumberTypeList } from './common';
 import { getRandomNumber } from '@/utils/index';
 
 export function getOption(content = '') {
@@ -20,12 +20,12 @@ export const replaceAnswerData = {
   answer: { score: 1, score_type: scoreTypeList[0].value }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_description: 'false', // 描述
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型
     row_count: 4,
+    column_count: 4,
   },
   // 其他属性
   other: {

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

@@ -1,4 +1,4 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList, switchOption } from './common';
+import { scoreTypeList, questionNumberTypeList, switchOption } from './common';
 import { getRandomNumber } from '@/utils/index';
 
 export function getOption(content = '') {
@@ -21,7 +21,6 @@ export function getSortDataTemplate() {
     answer: { answer_list, score: 1, score_type: scoreTypeList[0].value }, // 答案
     // 题型属性
     property: {
-      stem_type: stemTypeList[0].value, // 题干类型
       question_number: '1', // 题号
       is_enable_description: switchOption[1].value, // 描述
       score: 1, // 分值

+ 2 - 3
src/views/exercise_questions/data/talkPicture.js

@@ -1,7 +1,7 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList, switchOption } from './common';
+import { scoreTypeList, questionNumberTypeList, switchOption } from './common';
 import { getRandomNumber } from '@/utils/index';
 export function getOption() {
-  return { picture_title: '', picture_info: '', reference_answer: '', picture_file_id: '', mark: getRandomNumber() };
+  return { picture_info: '', reference_answer: '', picture_file_id: '', mark: getRandomNumber() };
 }
 // 看图说话数据模板
 export const talkPictrueData = {
@@ -13,7 +13,6 @@ export const talkPictrueData = {
   answer: { score: 1, score_type: scoreTypeList[0].value }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     score: 1, // 分值
     is_enable_description: switchOption[1].value, // 描述

+ 2 - 3
src/views/exercise_questions/data/wordCard.js

@@ -1,4 +1,4 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList, optionTypeList } from './common';
+import { scoreTypeList, questionNumberTypeList, optionTypeList } from './common';
 import { getRandomNumber } from '@/utils/index';
 
 export function getOption(content = '') {
@@ -8,7 +8,7 @@ export function getOption(content = '') {
     audio_file_id: '',
     pinyin: '',
     definition: '',
-    rate: null,
+    collocation: '',
     example_sentence: ['', ''],
     picture_file_id: '',
     hz_strokes_list: [],
@@ -42,7 +42,6 @@ export const wordCardData = {
   file_id_list: [],
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_description: 'false', // 描述
     score: 1, // 分值

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

@@ -1,4 +1,4 @@
-import { optionTypeList, stemTypeList, scoreTypeList, questionNumberTypeList } from './common';
+import { optionTypeList, scoreTypeList, questionNumberTypeList } from './common';
 // 数据模板
 export const writeData = {
   type: 'write', // 题型
@@ -11,7 +11,6 @@ export const writeData = {
   answer: { score: 1, score_type: scoreTypeList[0].value }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_description: 'false', // 描述
     score: 1, // 分值

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

@@ -1,4 +1,4 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList, switchOption } from './common';
+import { scoreTypeList, questionNumberTypeList, switchOption } from './common';
 import { getRandomNumber } from '@/utils/index';
 export function getOption() {
   return {
@@ -19,7 +19,6 @@ export const writePictrueData = {
   answer: { score: 1, score_type: scoreTypeList[0].value }, // 答案
   // 题型属性
   property: {
-    stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     score: 1, // 分值
     is_enable_description: switchOption[1].value, // 描述

+ 0 - 3
src/views/exercise_questions/preview/AnswerQuestionPreview.vue

@@ -5,9 +5,6 @@
       <span class="question-number">{{ data.property.question_number }}.</span>
       <span v-html="sanitizeHTML(data.stem)"></span>
     </div>
-
-    <AudioPlay v-if="data.file_id_list.length > 0" :file-id="data.file_id_list[0]" />
-
     <SoundRecordPreview :wav-blob.sync="answer.answer_list[0].audio_file_id" :disabled="disabled" position="center" />
     <div v-if="isEnable(data.property.is_enable_reference_answer) && isShowRightAnswer" class="reference-box">
       <h5 class="reference-title">参考答案</h5>

+ 49 - 43
src/views/exercise_questions/preview/ChinesePreview.vue

@@ -17,10 +17,15 @@
             v-if="data.property.learn_type !== 'dictation'"
             class="words-top"
             :style="{
-              width: 64 * (writer_number + 1 > writer_number_yuan ? writer_number_yuan : writer_number + 1) + 'px',
+              width:
+                64 *
+                  ((writer_number + 1) * item.hz_strokes_list.length > writer_number_yuan
+                    ? writer_number_yuan
+                    : (writer_number + 1) * item.hz_strokes_list.length) +
+                'px',
             }"
           >
-            <div class="words-left">
+            <div class="words-left" :style="{ width: '64' * item.hz_strokes_list.length + 'px' }">
               <AudioPlay :file-id="item.audio_file_id" theme-color="gray" />
               <span class="pinyin">{{ item.pinyin }}</span>
             </div>
@@ -29,50 +34,49 @@
           </div>
           <div v-if="data.property.learn_type === 'paint'" class="card-box">
             <!-- 描红 -->
-            <Strockplayredline
-              :play-storkes="true"
-              :book-text="item.content"
-              :target-div="'pre' + item.content + index"
-              :book-strokes="item.hz_strokes_list[0].strokes"
-              :class="['strock-chinese', 'border-right-none']"
-            />
-            <Strockred
-              v-for="itemI in writer_number"
-              :key="itemI + data.property.learn_type"
-              :book-text="item.content"
-              :hanzi-color="hanzi_color"
-              :reset="true"
-              :target-div="'write-praT' + item.content + itemI + Math.random().toString(36).substring(2, 10)"
-              :book-strokes="item.hz_strokes_list[0].strokes"
-              :class="[
-                'strock-chinese',
-                (itemI + 1) % writer_number_yuan !== 0 && itemI !== writer_number ? 'border-right-none' : '',
-              ]"
-            />
-            <!-- <div
-              v-for="(items, indexs) in item.imgArr"
-              :key="indexs"
-              :class="['strockplay-newWord border-left-none']"
-              @click="freeWrite(items, index, indexs)"
-            >
-              <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
-              <img
-                v-if="!play_status && items && items.strokes_image"
-                class="hanzi-writer-img"
-                :src="items.strokes_image"
-                alt=""
+            <template v-for="(items, indexs) in item.hz_strokes_list">
+              <Strockplayredline
+                :key="indexs"
+                :play-storkes="true"
+                :book-text="item.content"
+                :target-div="'pre' + item.content + index + indexs"
+                :book-strokes="items.strokes"
+                :class="['strock-chinese', 'border-right-none']"
               />
-            </div> -->
+            </template>
+
+            <div v-for="itemI in writer_number" :key="itemI + data.property.learn_type + index" style="display: flex">
+              <Strockred
+                v-for="(items, indexs) in item.hz_strokes_list"
+                :key="indexs"
+                :book-text="item.content"
+                :hanzi-color="hanzi_color"
+                :reset="true"
+                :target-div="'write-praT' + item.content + itemI + Math.random().toString(36).substring(2, 10)"
+                :book-strokes="items.strokes"
+                :class="[
+                  'strock-chinese',
+                  ((item.hz_strokes_list.length * (itemI + 1) + indexs - 1) % writer_number_yuan !== 0 &&
+                    itemI !== writer_number) ||
+                  (itemI === writer_number && indexs !== item.hz_strokes_list.length - 1)
+                    ? 'border-right-none'
+                    : '',
+                ]"
+              />
+            </div>
           </div>
           <div v-else-if="data.property.learn_type === 'write'" class="card-box">
             <!-- 书写 -->
-            <Strockplayredline
-              :play-storkes="true"
-              :book-text="item.content"
-              :target-div="'pre' + item.content + index"
-              :book-strokes="item.hz_strokes_list[0].strokes"
-              :class="['strock-chinese']"
-            />
+            <template v-for="(items, indexs) in item.hz_strokes_list">
+              <Strockplayredline
+                :key="'write' + indexs"
+                :play-storkes="true"
+                :book-text="item.content"
+                :target-div="'pre' + item.content + index + indexs"
+                :book-strokes="items.strokes"
+                :class="['strock-chinese', indexs !== item.hz_strokes_list.length - 1 ? 'border-right-none' : '']"
+              />
+            </template>
             <div v-for="(items, indexs) in item.imgArr" :key="indexs" class="con-box">
               <div
                 :class="['strockplay-newWord', (indexs + 1) % writer_number_yuan !== 0 ? 'border-left-none' : '']"
@@ -263,7 +267,9 @@ export default {
           }
         } else if (item.content.trim()) {
           for (let i = 0; i < this.writer_number; i++) {
-            arr.push(null);
+            item.hz_strokes_list.forEach(() => {
+              arr.push(null);
+            });
           }
           if (this.isJudgingRightWrong) {
             item.imgArr = this.answer.answer_list[index].strokes_content_list;

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

@@ -115,7 +115,7 @@ export default {
         { value: '2', label: '二声', img: 'second-tone' },
         { value: '3', label: '三声', img: 'third-tone' },
         { value: '4', label: '四声', img: 'fourth-tone' },
-        { value: '0', label: '轻声', img: 'neutral-tone' },
+        // { value: '0', label: '轻声', img: 'neutral-tone' },
       ],
       con_preview: [],
       tone_data: [
@@ -198,13 +198,13 @@ export default {
         let con_arr = JSON.parse(JSON.stringify(item.content_view));
         let user_answer = [];
         let user_submit = []; // 用户提交答案
-        con_arr.forEach(() => {
+        con_arr.forEach((items) => {
           user_answer.push({
             select_tone: null,
             select_letter: '',
             select_index: '',
           });
-          user_submit.push('');
+          user_submit.push(this.data.property.answer_mode === 'label' ? items : '');
         });
         let obj = {
           item_con: con_arr,
@@ -255,7 +255,7 @@ export default {
               });
               if (this.data.property.answer_mode === 'label') {
                 let number_index = e.search(/0|1|2|3|4/) + 1;
-                arr[index] = str.trim() + e.substring(number_index);
+                arr[index] = str.trim() + (number_index === 0 ? '' : e.substring(number_index));
               } else {
                 arr[index] = str.trim();
               }
@@ -399,7 +399,9 @@ export default {
             } else {
               user_select[indexs] = items;
             }
-            user_answer[indexs].right_answer = this.data.answer.answer_list[index].value[indexs].match(/0|1|2|3|4/)[0];
+            user_answer[indexs].right_answer = this.data.answer.answer_list[index].value[indexs].match(/0|1|2|3|4/)
+              ? this.data.answer.answer_list[index].value[indexs].match(/0|1|2|3|4/)[0]
+              : '';
             user_answer[indexs].right_index = this.data.answer.answer_list[index].value[indexs].search(/0|1|2|3|4/) - 1;
           } else {
             this.handleReplaceTone(

+ 6 - 4
src/views/exercise_questions/preview/RepeatPreview.vue

@@ -9,7 +9,10 @@
     <div class="option-list">
       <li v-for="(item, i) in answer.answer_list" :key="i" :class="['option-item']">
         <span>{{ computeOptionMethods[data.option_number_show_mode](i) }} </span>
-        <AudioPlay v-if="data.option_list[i].audio_file_id" :file-id="data.option_list[i].audio_file_id" />
+        <AudioPlay
+          v-if="data.option_list[i] && data.option_list[i].audio_file_id"
+          :file-id="data.option_list[i].audio_file_id"
+        />
         <div
           v-if="sanitizeHTML(data.option_list[i].content)"
           class="option-content"
@@ -56,6 +59,7 @@ export default {
     // 初始化数据
     handleData() {
       if (!this.isJudgingRightWrong) {
+        this.answer.answer_list = [];
         this.data.option_list.forEach((item) => {
           let obj = {
             mark: item.mark,
@@ -88,9 +92,7 @@ export default {
       margin-bottom: 16px;
 
       .option-content {
-        flex: 1;
-
-        // max-width: 306px;
+        width: 480px;
         padding: 8px 16px;
         color: #706f78;
         background-color: $content-color;

+ 11 - 2
src/views/exercise_questions/preview/ReplaceAnswerPreview.vue

@@ -85,7 +85,10 @@ export default {
           },
         ];
       }
-      let option_lists = [[], [], [], []];
+      let option_lists = [];
+      this.data.option_list[0].forEach(() => {
+        option_lists.push([]);
+      });
       this.data.option_list.forEach((item) => {
         item.forEach((items, indexs) => {
           if (items.content) {
@@ -145,6 +148,8 @@ export default {
 
   .option-list {
     display: flex;
+    flex-wrap: wrap;
+    align-items: center;
     justify-content: center;
     width: max-content;
     margin: 0 auto;
@@ -199,7 +204,11 @@ export default {
     }
   }
 
-  .sound-record-wrapper {
+  .sound-record-preview {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    width: 280px;
     margin-left: 80px;
   }
 }

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

@@ -14,7 +14,6 @@
           :src="pic_list[item.picture_file_id]"
           fit="contain"
         />
-        <h3 class="pic-title" v-html="sanitizeHTML(item.picture_title)"></h3>
         <p class="pic-info" v-html="sanitizeHTML(item.picture_info)"></p>
         <!-- 语音作答 -->
         <div v-if="isEnable(data.property.is_enable_voice_answer) && answer.answer_list[index]" class="sound-box">

+ 47 - 58
src/views/exercise_questions/preview/WordCardPreview.vue

@@ -19,62 +19,47 @@
         fit="contain"
       />
       <div class="words-right">
-        <div :class="['words-item']">
-          <label
-            v-for="(item, index) in option_list"
-            :key="index"
-            :class="[active_index === index ? 'active' : '']"
-            @click="active_index = index"
-            >{{ item.content }}</label
-          >
-        </div>
         <template v-for="(item, index) in option_list">
-          <div class="strock-box" :key="index" v-if="index === active_index">
-            <div class="strock-left" v-if="item.hz_strokes_list && item.hz_strokes_list.length > 0">
+          <div v-if="index === active_index" :key="index" class="strock-box">
+            <div 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>
+            <div v-if="item.hz_strokes_list && item.hz_strokes_list.length > 0" class="strock-left">
               <template v-for="(items, indexs) in item.hz_strokes_list">
                 <Strockplayredline
+                  v-if="items"
+                  :key="indexs"
                   :play-storkes="true"
                   :book-text="items.hz"
                   :target-div="'pre' + items.hz + indexs + active_index"
                   :book-strokes="items.strokes"
                   :class="['strock-chinese', indexs !== item.hz_strokes_list.length - 1 ? 'border-right-none' : '']"
-                  :key="indexs"
-                  v-if="items"
                 />
               </template>
             </div>
-            <div class="strock-right">
-              <el-rate v-model="item.rate" disabled text-color="#ff9900"> </el-rate>
-              <div class="pinyin-box">
-                <AudioPlay :file-id="item.audio_file_id" theme-color="white" v-if="item.audio_file_id" />
-                <span class="pinyin">{{ item.pinyin }}</span>
-              </div>
-            </div>
           </div>
         </template>
-        <el-divider></el-divider>
-        <div class="content-box" v-if="option_list[active_index]">
-          <p class="definition" v-for="(itemd, indexd) in option_list[active_index].definition_preview" :key="indexd">
+        <el-divider />
+        <div v-if="option_list[active_index]" class="content-box">
+          <span v-if="option_list[active_index].definition_preview.length > 0" class="tips">释义:</span>
+          <p v-for="(itemd, indexd) in option_list[active_index].definition_preview" :key="indexd" class="definition">
             {{ itemd }}
           </p>
-          <span
-            class="tips"
-            v-if="
-              option_list[active_index].example_sentence &&
-              (option_list[active_index].example_sentence[0].trim() ||
-                option_list[active_index].example_sentence[1].trim())
-            "
-            >例句:</span
-          >
+          <span v-if="option_list[active_index].collocation" class="tips">搭配:</span>
+          <p v-if="option_list[active_index].collocation" class="definition">
+            {{ option_list[active_index].collocation }}
+          </p>
+          <span v-if="option_list[active_index].example_sentence.length > 0" class="tips">例句:</span>
           <template v-for="(iteme, indexe) in option_list[active_index].example_sentence">
-            <p class="example-sentence" :key="indexe + iteme.trim()" v-if="iteme.trim()">
+            <p v-if="iteme.trim()" :key="indexe + iteme.trim()" class="example-sentence">
               <span>{{ computeOptionMethods[data.option_number_show_mode](indexe) }} </span>
               <span>{{ iteme }}</span>
             </p>
           </template>
         </div>
-        <el-divider></el-divider>
-        <div class="sound-box" v-if="answer.answer_list[active_index]">
+        <el-divider />
+        <div v-if="answer.answer_list[active_index]" class="sound-box">
           <SoundRecordPreview
             :wav-blob.sync="answer.answer_list[active_index].audio_file_id"
             :type="'small'"
@@ -82,6 +67,16 @@
           />
         </div>
       </div>
+
+      <div v-if="option_list.length > 1" :class="['words-item']">
+        <label
+          v-for="(item, index) in option_list"
+          :key="index"
+          :class="[active_index === index ? 'active' : '']"
+          @click="active_index = index"
+          >{{ item.content }}</label
+        >
+      </div>
     </div>
   </div>
 </template>
@@ -164,25 +159,25 @@ export default {
 
     .words-right {
       flex: 1;
+    }
 
-      .words-item {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 16px;
+    .words-item {
+      width: 120px;
 
-        label {
-          padding: 8px 16px;
-          font-size: 16px;
-          font-weight: 400;
-          line-height: 24px;
-          color: #fff;
-          cursor: pointer;
-          background: rgba(48, 110, 255, 30%);
-          border-radius: 20px;
+      label {
+        display: block;
+        padding: 4px 16px;
+        margin-bottom: 4px;
+        font-size: 16px;
+        font-weight: 400;
+        line-height: 24px;
+        color: rgba(0, 0, 0, 30%);
+        cursor: pointer;
+        border-radius: 20px;
 
-          &.active {
-            background: rgba(48, 110, 255, 100%);
-          }
+        &.active {
+          color: #fff;
+          background: rgba(48, 110, 255, 100%);
         }
       }
     }
@@ -203,12 +198,6 @@ export default {
     }
   }
 
-  .strock-box {
-    display: flex;
-    column-gap: 16px;
-    margin-top: 24px;
-  }
-
   .strock-left {
     display: flex;
   }
@@ -219,7 +208,7 @@ export default {
     align-items: center;
     width: max-content;
     padding: 4px 8px;
-    margin-top: 10px;
+    margin-bottom: 10px;
     background: rgba(47, 111, 236, 100%);
     border-radius: 40px;
 

+ 174 - 41
src/views/exercise_questions/preview/components/common/SoundRecordPreview.vue

@@ -1,38 +1,66 @@
 <template>
-  <div :class="['sound-record-wrapper', 'sound-record-wrapper-' + type]" :style="{ justifyContent: position }">
-    <div class="sound-item sound-item-luyin">
-      <img
-        v-if="microphoneStatus"
-        :src="require('../../../../../assets/record-ing-hasBg.png')"
-        class="voice-play"
-        @click="microphone"
+  <div class="sound-record-preview">
+    <div v-if="type === 'big' && file_url" class="audio-slider-box audio-slider-box-big">
+      <span class="audio-time">{{ secondFormatConversion(audio.current_time) }}</span>
+      <el-slider
+        v-model="play_value"
+        class="audio-slider"
+        :format-tooltip="formatProcessToolTip"
+        @change="changeCurrentTime"
       />
-      <span v-else class="sound-item-span" @click="microphone">
-        <SvgIcon icon-class="mic-line" :size="iconSize" class="record" />
-      </span>
-
-      <label v-if="type === 'big'" :class="['record-time', microphoneStatus ? 'record-ing' : '']">
-        {{ audio.paused ? '' : '-' }}{{ secondFormatConversion(recordTimes) }}
-      </label>
+      <span class="audio-time">{{ audio_allTime }}</span>
     </div>
-    <div v-if="type === 'small'" class="sound-item">
-      <label :class="['record-time', microphoneStatus ? 'record-ing' : '']">
+    <div :class="['sound-record-wrapper', 'sound-record-wrapper-' + type]" :style="{ justifyContent: position }">
+      <div class="sound-item sound-item-luyin">
+        <img
+          v-if="microphoneStatus"
+          :src="require('../../../../../assets/record-ing-hasBg.png')"
+          class="voice-play"
+          @click="microphone"
+        />
+        <span v-else class="sound-item-span" @click="microphone">
+          <SvgIcon icon-class="mic-line" :size="iconSize" class="record" />
+        </span>
+
+        <label v-if="type === 'big'" :class="['record-time', microphoneStatus ? 'record-ing' : '']">
+          {{ microphoneStatus ? secondFormatConversion(recordTimes) : '录音' }}
+        </label>
+      </div>
+      <div v-if="type === 'small' && file_url" class="sound-item audio-slider-box">
+        <span class="audio-time">{{ secondFormatConversion(audio.current_time) }}</span>
+        <el-slider
+          v-model="play_value"
+          class="audio-slider"
+          :format-tooltip="formatProcessToolTip"
+          @change="changeCurrentTime"
+        />
+        <span class="audio-time">{{ audio_allTime }}</span>
+      </div>
+
+      <label v-if="type === 'small' && !file_url" :class="['record-time', microphoneStatus ? 'record-ing' : '']">
         {{ audio.paused ? '' : '-' }}{{ secondFormatConversion(recordTimes) }}
       </label>
+      <div class="sound-item">
+        <span :class="['sound-item-span sound-item-btn', wavBlob ? '' : 'not-url']" @click="playMicrophone">
+          <SvgIcon :icon-class="iconClass" :size="iconSize" :class="['audio-play-btn']" />
+        </span>
+        <label v-if="type === 'big'" class="tips">回放</label>
+      </div>
+      <div class="sound-item">
+        <span :class="['sound-item-span sound-item-btn', wavBlob ? '' : 'not-url']" @click="delectWav">
+          <SvgIcon icon-class="delete-back-line" :size="iconSize" :class="['delete-btn']" />
+        </span>
+        <label v-if="type === 'big'" class="tips">删除</label>
+      </div>
     </div>
-    <div class="sound-item">
-      <span :class="['sound-item-span sound-item-btn', wavBlob ? '' : 'not-url']" @click="playMicrophone">
-        <SvgIcon :icon-class="iconClass" :size="iconSize" :class="['audio-play-btn']" />
-      </span>
-      <label v-if="type === 'big'" class="tips">回放</label>
-    </div>
-    <div class="sound-item">
-      <span :class="['sound-item-span sound-item-btn', wavBlob ? '' : 'not-url']" @click="delectWav">
-        <SvgIcon icon-class="delete-back-line" :size="iconSize" :class="['delete-btn']" />
-      </span>
-      <label v-if="type === 'big'" class="tips">删除</label>
-    </div>
-    <audio ref="audio" :src="file_url" preload="metadata"></audio>
+    <audio
+      :ref="wavBlob"
+      :src="file_url"
+      preload="metadata"
+      @loadedmetadata="onLoadedmetadata"
+      @timeupdate="onTimeupdate"
+      @canplaythrough="oncanplaythrough"
+    ></audio>
   </div>
 </template>
 
@@ -74,13 +102,22 @@ export default {
       timer: null, // 计时器
       microphoneStatus: false, // 是否录音
       hasMicro: '', // 录音后的样式class
-      audio: {
-        paused: true,
-      },
       playtime: 0, // 播放时间
       recordTimes: 0,
       file_url: '',
       recordTime: 0,
+      audio: {
+        paused: true,
+        playing: false,
+        // 音频当前播放时长
+        current_time: 0,
+        // 音频最大播放时长
+        max_time: 0,
+        isPlaying: false,
+        loading: false,
+      },
+      play_value: 0,
+      audio_allTime: null, // 展示总时间
     };
   },
   computed: {
@@ -110,13 +147,13 @@ export default {
     },
   },
   mounted() {
-    this.$refs.audio.addEventListener('ended', () => {
+    this.$refs[this.wavBlob].addEventListener('ended', () => {
       this.audio.paused = true;
     });
-    this.$refs.audio.addEventListener('pause', () => {
+    this.$refs[this.wavBlob].addEventListener('pause', () => {
       this.audio.paused = true;
     });
-    this.$refs.audio.addEventListener('play', () => {
+    this.$refs[this.wavBlob].addEventListener('play', () => {
       this.audio.paused = false;
     });
   },
@@ -128,7 +165,7 @@ export default {
           this.hasMicro = 'active';
           // this.$refs.audio.pause();
           // this.$refs.audio.load();
-          this.$refs.audio.play();
+          this.$refs[this.wavBlob].play();
           if (this.recordTimes === 0) {
             this.recordTimes = JSON.parse(JSON.stringify(this.recordTime));
             this.playtime = 0;
@@ -145,7 +182,7 @@ export default {
             }
           }, 1000);
         } else {
-          this.$refs.audio.pause();
+          this.$refs[this.wavBlob].pause();
           this.hasMicro = 'normal';
           clearInterval(this.timer);
         }
@@ -154,7 +191,7 @@ export default {
     // 开始录音
     microphone() {
       if (!this.audio.paused) {
-        this.$refs.audio.pause();
+        this.$refs[this.wavBlob].pause();
         this.audio.paused = true;
       }
       if (this.disabled) return;
@@ -194,7 +231,7 @@ export default {
     },
     // 删除录音
     delectWav() {
-      this.$refs.audio.pause();
+      this.$refs[this.wavBlob].pause();
       this.hasMicro = '';
       this.microphoneStatus = false;
       this.playtime = 0;
@@ -202,6 +239,50 @@ export default {
       clearInterval(this.timer);
       this.$emit('update:wavBlob', '');
     },
+    // 进度条格式化toolTip
+    formatProcessToolTip(index) {
+      let indexs = parseInt((this.audio.max_time / 100) * index);
+      return secondFormatConversion(indexs);
+    },
+    // 点击 拖拽播放音频
+    changeCurrentTime(value) {
+      let audioId = this.wavBlob;
+      this.$refs[audioId].play();
+      this.audio.playing = true;
+      this.$refs[audioId].currentTime = parseInt((value / 100) * this.audio.max_time);
+    },
+    // 音频加载完之后
+    onLoadedmetadata(res) {
+      this.audio.max_time = parseInt(res.target.duration);
+      this.audio_allTime = secondFormatConversion(this.audio.max_time);
+    },
+    // 当音频当前时间改变后,进度条也要改变
+    onTimeupdate(res) {
+      let audioId = this.wavBlob;
+      this.audio.current_time = res.target.currentTime;
+      this.play_value = (this.audio.current_time / this.audio.max_time) * 100;
+      if (this.audio.current_time * 1000 > this.ed) {
+        this.$refs[audioId].pause();
+      }
+    },
+    onTimeupdateTime(res, playFlag) {
+      if (!res && res !== 0) return;
+      let audioId = this.wavBlob;
+      this.$refs[audioId].currentTime = res;
+      this.play_value = (res / this.audio.max_time) * 100;
+      if (playFlag) {
+        let audio = document.getElementsByTagName('audio');
+        audio.forEach((item) => {
+          if (item.id !== audioId) {
+            item.pause();
+          }
+        });
+        this.$refs[audioId].play();
+      }
+    },
+    oncanplaythrough() {
+      this.audio.loading = false;
+    },
   },
 };
 </script>
@@ -259,9 +340,8 @@ export default {
     display: block;
     margin-top: 4px;
     font-size: 12px;
-    font-weight: 500;
     line-height: 20px;
-    color: #000;
+    color: rgba(0, 0, 0, 50%);
   }
 
   &-small {
@@ -287,4 +367,57 @@ export default {
     }
   }
 }
+
+.audio-slider-box {
+  display: flex;
+  column-gap: 8px;
+  align-items: center;
+
+  .audio-time {
+    min-width: 35px;
+    font-size: 12px;
+    font-weight: 400;
+    line-height: 20px;
+  }
+
+  .audio-slider {
+    width: 82px;
+    height: 32px;
+
+    :deep .el-slider__runway {
+      height: 4px;
+      margin: 14px 0;
+      background-color: #dadada;
+    }
+
+    :deep .el-slider__button {
+      width: 4px;
+      height: 4px;
+      background-color: #000;
+      border: none;
+    }
+
+    :deep .el-slider__bar {
+      height: 4px;
+      background-color: #000;
+    }
+
+    :deep .el-slider__button-wrapper {
+      top: -16px;
+    }
+  }
+
+  &-big {
+    width: 280px;
+    height: 40px;
+    padding: 4px 12px;
+    margin: 16px auto;
+    background-color: #ededed;
+    border-radius: 28px;
+
+    .audio-slider {
+      width: 178px;
+    }
+  }
+}
 </style>