2 Commits 66963cbdef ... 964ebb0655

Author SHA1 Message Date
  dsy 964ebb0655 Merge branch 'master' of http://60.205.254.193:3000/GCLS/eep_page 1 month ago
  dsy c8b28365c2 填空增加多种填写方式 1 month ago

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

@@ -78,7 +78,9 @@ const mixin = {
     if (!this.data?.mind_map?.node_list?.[0]?.id) {
       this.data.mind_map = this.data.mind_map ?? {};
       this.data.mind_map.node_list = this.data.mind_map.node_list ?? [{}];
-      this.data.mind_map.node_list[0].id = this.id;
+      if (this.data.mind_map.node_list.length > 0) {
+        this.data.mind_map.node_list[0].id = this.id;
+      }
     }
   },
   methods: {

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

@@ -81,6 +81,21 @@ export default {
   watch: {
     'data.property.arrange_type': 'handleMindMap',
     'data.property.fill_font': 'handleMindMap',
+    'data.vocabulary': {
+      handler(val) {
+        if (!val) return;
+        this.data.word_list = val
+          .split(/[\n\r]+/)
+          .map((item) => item.split(' ').filter((s) => s))
+          .flat()
+          .map((content) => {
+            return {
+              content,
+              mark: getRandomNumber(),
+            };
+          });
+      },
+    },
   },
   methods: {
     // 识别文本
@@ -115,6 +130,7 @@ export default {
         arr.push({ content: _str.slice(start, index), type: 'text' });
         if (matchNum % 2 === 0 && arr.length > 0) {
           arr[arr.length - 1].type = 'input';
+          arr[arr.length - 1].audio_answer_list = [];
           let mark = getRandomNumber();
           arr[arr.length - 1].mark = mark;
           let content = arr[arr.length - 1].content;

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

@@ -70,6 +70,7 @@ export function getFillData() {
     record_list: [],
     model_essay: [],
     vocabulary: '', // 用于选词的词汇
+    word_list: [], // 选词列表
     answer: {
       answer_list: [],
       reference_answer: '',

+ 1 - 2
src/views/book/courseware/data/write.js

@@ -81,8 +81,7 @@ export function getWriteData() {
     // 内容中包含的文件列表,
     file_list: [],
     mind_map: {
-      node_list: [
-      ], // 思维导图数据
+      node_list: [{ name: '书写组件' }], // 思维导图数据
     },
     answer: {
       answer_list: [],

+ 127 - 36
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -6,7 +6,7 @@
     <div class="main" :style="getMainStyle()">
       <AudioFill :file-id="data.audio_file_id" />
       <div class="fill-wrapper">
-        <p v-for="(item, i) in data.model_essay" :key="i">
+        <p v-for="(item, i) in modelEssay" :key="i">
           <template v-for="(li, j) in item">
             <span v-if="li.type === 'text'" :key="j" v-html="sanitizeHTML(li.content)"></span>
             <template v-if="li.type === 'input'">
@@ -23,15 +23,20 @@
               <template v-else-if="data.property.fill_type === fillTypeList[1].value">
                 <el-popover :key="j" placement="top" trigger="click">
                   <div class="word-list">
-                    <span v-for="(word, index) in wordList" :key="index" class="word-item" @click="li.content = word">
-                      {{ word }}
+                    <span
+                      v-for="{ content, mark } in data.word_list"
+                      :key="mark"
+                      class="word-item"
+                      @click="handleSelectWord(content, mark, li)"
+                    >
+                      {{ content }}
                     </span>
                   </div>
 
                   <el-input
                     slot="reference"
                     v-model="li.content"
-                    :disabled="true"
+                    :readonly="true"
                     :class="[data.property.fill_font, ...computedAnswerClass(li.mark)]"
                     class="pinyin"
                     :style="[{ width: Math.max(80, li.content.length * 21.3) + 'px' }]"
@@ -39,12 +44,30 @@
                 </el-popover>
               </template>
 
-              <template v-else-if="data.property.fill_type === fillTypeList[2].value"></template>
+              <template v-else-if="data.property.fill_type === fillTypeList[2].value">
+                <span :key="j" class="write-click" @click="handleWriteClick(li.mark)">
+                  <img
+                    v-show="li.write_base64"
+                    style="background-color: #f4f4f4"
+                    :src="li.write_base64"
+                    alt="write-show"
+                  />
+                </span>
+              </template>
 
               <template v-else-if="data.property.fill_type === fillTypeList[3].value">
-                <!-- TODO -->
-                <!-- <SoundRecord :key="j" :wav-blob.sync="li.audio_file_id" /> -->
+                <SoundRecordBox
+                  ref="record"
+                  :key="j"
+                  type="mini"
+                  :many-times="false"
+                  class="record-box"
+                  :answer-record-list="data.audio_answer_list"
+                  :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
+                  @handleWav="handleMiniWav($event, li.mark)"
+                />
               </template>
+
               <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${j}`" class="right-answer">
                 {{ computedAnswerText(li.mark) }}
               </span>
@@ -62,6 +85,8 @@
         @handleWav="handleWav"
       />
     </div>
+
+    <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
   </div>
 </template>
 
@@ -77,19 +102,26 @@ import {
 import PreviewMixin from '../common/PreviewMixin';
 import AudioFill from './components/AudioFillPlay.vue';
 import SoundRecord from '../../common/SoundRecord.vue';
+import SoundRecordBox from '@/views/book/courseware/preview/components/record_input/SoundRecord.vue';
+import WriteDialog from './components/WriteDialog.vue';
 
 export default {
   name: 'FillPreview',
   components: {
     AudioFill,
     SoundRecord,
+    SoundRecordBox,
+    WriteDialog,
   },
   mixins: [PreviewMixin],
   data() {
     return {
       data: getFillData(),
       fillTypeList,
-      wordList: [],
+      modelEssay: [],
+      selectedWordList: [], // 用于存储选中的词汇
+      writeVisible: false,
+      writeMark: '',
     };
   },
   computed: {
@@ -100,15 +132,19 @@ export default {
   watch: {
     'data.model_essay': {
       handler(list) {
+        if (!list || !Array.isArray(list)) return;
+
+        this.modelEssay = JSON.parse(JSON.stringify(list));
         this.answer.answer_list = list
           .map((item) => {
             return item
-              .map(({ type, content, mark }) => {
+              .map(({ type, content, audio_answer_list, mark }) => {
                 if (type === 'input') {
                   return {
                     value: content,
-                    audio_file_id: '',
                     mark,
+                    audio_answer_list,
+                    write_base64: '',
                   };
                 }
               })
@@ -117,22 +153,13 @@ export default {
           .flat();
       },
       deep: true,
-    },
-    'data.vocabulary': {
-      handler(val) {
-        if (!val) return;
-        this.wordList = val
-          .split(/[\n\r]+/)
-          .map((item) => item.split(' ').filter((s) => s))
-          .flat();
-      },
       immediate: true,
     },
     isJudgingRightWrong(val) {
       if (!val) return;
 
       this.answer.answer_list.forEach(({ mark, value }) => {
-        this.data.model_essay.forEach((item) => {
+        this.modelEssay.forEach((item) => {
           item.forEach((li) => {
             if (li.mark === mark) {
               li.content = value;
@@ -147,26 +174,55 @@ export default {
       this.answer.record_list = val;
     },
   },
-  created() {
-    this.answer.answer_list = this.data.model_essay
-      .map((item) => {
-        return item
-          .map(({ type, content, mark }) => {
-            if (type === 'input') {
-              return {
-                value: content,
-                mark,
-              };
-            }
-          })
-          .filter((item) => item);
-      })
-      .flat();
-  },
   methods: {
     handleWav(data) {
       this.data.record_list = data;
     },
+    /**
+     * 处理小音频录音
+     * @param {Object} data 音频数据
+     * @param {String} mark 选项标识
+     */
+    handleMiniWav(data, mark) {
+      if (!data || !mark) return;
+      for (const item of this.modelEssay) {
+        const li = item.find((li) => li?.mark === mark);
+        if (li) {
+          this.$set(li, 'audio_answer_list', data);
+          break;
+        }
+      }
+    },
+    /**
+     * 处理选中词汇
+     * @param {String} content 选中的词汇内容
+     * @param {String} mark 选项标识
+     * @param {Object} li 当前输入框对象
+     */
+    handleSelectWord(content, mark, li) {
+      if (!content || !mark || !li) return;
+
+      li.content = content;
+      this.selectedWordList.push(mark);
+    },
+    /**
+     * 处理书写区确认
+     * @param {String} data 书写区数据
+     */
+    handleWriteConfirm(data) {
+      if (!data) return;
+      for (const item of this.modelEssay) {
+        const li = item.find((li) => li?.mark === this.writeMark);
+        if (li) {
+          this.$set(li, 'write_base64', data);
+          break;
+        }
+      }
+    },
+    handleWriteClick(mark) {
+      this.writeVisible = true;
+      this.writeMark = mark;
+    },
     getMainStyle() {
       const isRow = this.data.property.arrange_type === arrangeTypeList[0].value;
       const isFront = this.data.property.audio_position === audioPositionList[0].value;
@@ -253,6 +309,28 @@ export default {
       margin: 0;
     }
 
+    .record-box {
+      display: inline-flex;
+      align-items: center;
+      background-color: #fff;
+      border-bottom: 1px solid $font-color;
+    }
+
+    .write-click {
+      display: inline-block;
+      width: 104px;
+      height: 32px;
+      padding: 0 4px;
+      vertical-align: bottom;
+      cursor: pointer;
+      border-bottom: 1px solid $font-color;
+
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
     .el-input {
       display: inline-flex;
       align-items: center;
@@ -316,3 +394,16 @@ export default {
   }
 }
 </style>
+
+<style lang="scss" scoped>
+.word-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+
+  .word-item {
+    cursor: pointer;
+  }
+}
+</style>

+ 83 - 0
src/views/book/courseware/preview/components/fill/components/WriteDialog.vue

@@ -0,0 +1,83 @@
+<template>
+  <el-dialog :visible="visible" title="手写区" width="560px" :close-on-click-modal="false" @close="dialogClose">
+    <div class="esign-wrapper">
+      <VueEsign
+        ref="esign"
+        :width="540"
+        :height="180"
+        :is-crop="isCrop"
+        :line-width="lineWidth"
+        :line-color="lineColor"
+        :bg-color.sync="bgColor"
+      />
+      <i class="el-icon-check" @click="handleGenerate"></i>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import VueEsign from 'vue-esign';
+
+export default {
+  name: 'WriteDialog',
+  components: {
+    VueEsign,
+  },
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      lineWidth: 2,
+      lineColor: '#000000',
+      bgColor: '#F4F4F4',
+      isCrop: false,
+    };
+  },
+  watch: {
+    visible(val) {
+      if (!val) {
+        this.$refs.esign.reset();
+        this.$nextTick(() => {
+          this.bgColor = '#F4F4F4';
+        });
+      }
+    },
+  },
+  methods: {
+    dialogClose() {
+      this.$emit('update:visible', false);
+      this.$emit('close');
+    },
+    handleGenerate() {
+      this.$refs.esign
+        .generate({ format: 'png', quality: 0.8 })
+        .then((data) => {
+          this.$emit('confirm', data);
+          this.dialogClose();
+        })
+        .catch((err) => {
+          console.error(err);
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.esign-wrapper {
+  position: relative;
+
+  i {
+    position: absolute;
+    right: 6px;
+    bottom: 6px;
+    padding: 6px;
+    cursor: pointer;
+    background-color: #e5e6eb;
+  }
+}
+</style>

+ 0 - 1
src/views/book/courseware/preview/components/record_input/RecordInputPreview.vue

@@ -23,7 +23,6 @@
           :many-times="isEnable(data.is_enable_manyTimes)"
           class="record-box"
           :answer-record-list="data.answer.answer_list.answer_record_list"
-          :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
           @handleWav="handleWav"
         />
       </div>

+ 5 - 5
src/views/book/courseware/preview/components/write_base/WriteBasePreview.vue

@@ -23,9 +23,8 @@
       </div>
       <template v-if="data.write_base64[0]">
         <h2>展示区</h2>
-        <img style="background-color: #f7f8fa"
-:src="data.write_base64[0]" alt=""
-      /></template>
+        <img style="background-color: #f7f8fa" :src="data.write_base64[0]" alt="write-show" />
+      </template>
     </div>
   </div>
 </template>
@@ -57,9 +56,10 @@ export default {
   },
   methods: {
     handleReset() {
-      this.$refs.esign.$el.style.backgroundColor = '#f7f8fa';
       this.$refs.esign.reset();
-      this.$refs.esign.$el.style.backgroundColor = '#f7f8fa';
+      this.$nextTick(() => {
+        this.bgColor = '#f7f8fa';
+      });
     },
     handleGenerate() {
       this.$refs.esign