ソースを参照

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

dusenyao 1 年間 前
コミット
9af73df6e6
23 ファイル変更1017 行追加158 行削除
  1. 1 1
      src/router/guard/index.js
  2. 6 0
      src/views/exercise_questions/create/components/create.vue
  3. 128 0
      src/views/exercise_questions/create/components/exercises/AnswerQuestion.vue
  4. 55 13
      src/views/exercise_questions/create/components/exercises/ChineseQuestion.vue
  5. 22 12
      src/views/exercise_questions/create/components/exercises/ChooseToneQuestion.vue
  6. 6 4
      src/views/exercise_questions/create/components/exercises/RepeatQuestion.vue
  7. 56 15
      src/views/exercise_questions/create/components/exercises/WordCardQuestion.vue
  8. 280 0
      src/views/exercise_questions/create/components/exercises/WritePictureQuestion.vue
  9. 0 10
      src/views/exercise_questions/create/components/exercises/WriteQuestion.vue
  10. 6 0
      src/views/exercise_questions/create/index.vue
  11. 26 0
      src/views/exercise_questions/data/answerQuestion.js
  12. 1 0
      src/views/exercise_questions/data/chinese.js
  13. 9 4
      src/views/exercise_questions/data/common.js
  14. 1 0
      src/views/exercise_questions/data/wordCard.js
  15. 0 1
      src/views/exercise_questions/data/write.js
  16. 35 0
      src/views/exercise_questions/data/writePicture.js
  17. 61 0
      src/views/exercise_questions/preview/AnswerQuestionPreview.vue
  18. 32 46
      src/views/exercise_questions/preview/ChinesePreview.vue
  19. 7 6
      src/views/exercise_questions/preview/ChooseTonePreview.vue
  20. 11 11
      src/views/exercise_questions/preview/TalkPictruePreview.vue
  21. 27 28
      src/views/exercise_questions/preview/WordCardPreview.vue
  22. 246 0
      src/views/exercise_questions/preview/WritePictruePreview.vue
  23. 1 7
      src/views/exercise_questions/preview/WritePreview.vue

+ 1 - 1
src/router/guard/index.js

@@ -4,7 +4,7 @@ import NProgress from 'nprogress';
 import 'nprogress/nprogress.css';
 NProgress.configure({ showSpinner: false });
 
-const whiteList = ['/login', '/EnterSys']; // 重定向白名单
+const whiteList = ['/login', '/EnterSys', '/open/share/exercise']; // 重定向白名单
 
 export function setupRouterGuard(router) {
   // 全局前置守卫

+ 6 - 0
src/views/exercise_questions/create/components/create.vue

@@ -59,6 +59,8 @@ import ListenSelectQuestion from './exercises/ListenSelectQuestion.vue';
 import ListenJudgeQuestion from './exercises/ListenJudgeQuestion.vue';
 import ListenFillQuestion from './exercises/ListenFillQuestion.vue';
 import WordCardQuestion from './exercises/WordCardQuestion.vue';
+import AnswerQuestion from './exercises/AnswerQuestion.vue';
+import WritePictureQuestion from './exercises/WritePictureQuestion.vue';
 
 export default {
   name: 'CreateMain',
@@ -81,6 +83,8 @@ export default {
     ListenJudgeQuestion,
     ListenFillQuestion,
     WordCardQuestion,
+    AnswerQuestion,
+    WritePictureQuestion,
   },
   provide() {
     return {
@@ -121,6 +125,8 @@ export default {
         listen_judge: ListenJudgeQuestion,
         listen_fill: ListenFillQuestion,
         word_card: WordCardQuestion,
+        answer_question: AnswerQuestion,
+        write_picture: WritePictureQuestion,
       },
     };
   },

+ 128 - 0
src/views/exercise_questions/create/components/exercises/AnswerQuestion.vue

@@ -0,0 +1,128 @@
+<template>
+  <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="输入题干" />
+
+        <el-input
+          v-show="isEnable(data.property.is_enable_description)"
+          v-model="data.description"
+          rows="3"
+          resize="none"
+          type="textarea"
+          placeholder="输入文段"
+        />
+        <UploadAudio :file-id="data.file_id_list?.[0]" @upload="upload" @deleteFile="deleteFile" />
+        <el-input
+          v-if="isEnable(data.property.is_enable_reference_answer)"
+          v-model="data.reference_answer"
+          type="textarea"
+          rows="3"
+          placeholder="输入参考答案"
+        />
+      </div>
+    </template>
+
+    <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>
+        <el-form-item label-width="45px">
+          <el-radio
+            v-for="{ value, label } in questionNumberTypeList"
+            :key="value"
+            v-model="data.other.question_number_type"
+            :label="value"
+          >
+            {{ label }}
+          </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"
+            :key="value"
+            v-model="data.property.is_enable_reference_answer"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+
+        <el-form-item label="分值">
+          <el-radio
+            v-for="{ value, label } in scoreTypeList"
+            :key="value"
+            v-model="data.property.score_type"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label-width="45px">
+          <el-input-number
+            v-model="data.property.score"
+            :min="0"
+            :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
+          />
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import QuestionMixin from '../common/QuestionMixin.js';
+import UploadAudio from '../common/UploadAudio.vue';
+
+import { answerQuestionData } from '@/views/exercise_questions/data/answerQuestion';
+
+export default {
+  name: 'ReadAloudQuestion',
+  components: { UploadAudio },
+  mixins: [QuestionMixin],
+  data() {
+    return {
+      data: JSON.parse(JSON.stringify(answerQuestionData)),
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+.stem {
+  border-bottom-width: 0 !important;
+}
+</style>

+ 55 - 13
src/views/exercise_questions/create/components/exercises/ChineseQuestion.vue

@@ -26,7 +26,7 @@
       <div class="content">
         <label class="title-little">题目:</label>
         <ul>
-          <li v-for="(item, i) in data.option_list" :key="i" class="content-item">
+          <li v-for="(item, i) in data.option_list" :key="i" class="content-item" v-loading="item.loadings">
             <span
               class="question-number"
               @dblclick="changeOptionType(data)"
@@ -39,6 +39,7 @@
               v-model="item.content"
               :maxlength="data.property.learn_type === 'dictation' ? null : 1"
               :placeholder="data.property.learn_type === 'dictation' ? '输入汉字或词汇' : '输入一个汉字'"
+              @blur="handleChineseStrokes(item)"
             />
             <el-input
               v-model="item.pinyin"
@@ -55,7 +56,9 @@
             />
             <div v-else-if="data.other.audio_generation_method === 'auto'" class="auto-matically">
               <AudioPlay :file-id="item.audio_file_id" theme-color="gray" v-if="item.audio_file_id" />
-              <span class="auto-btn" @click="handleMatically(item)">自动生成</span>
+              <span class="auto-btn" @click="handleMatically(item)" v-loading="item.loading">{{
+                item.audio_file_id ? '已生成' : '自动生成'
+              }}</span>
             </div>
             <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
             <el-input
@@ -212,25 +215,33 @@ export default {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
-      }).then(() => {
-        this.data.option_list.splice(i, 1);
-        this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
-      });
+      })
+        .then(() => {
+          this.data.option_list.splice(i, 1);
+          this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+        })
+        .catch(() => {});
     },
     // 自动生成音频
     handleMatically(item) {
       if (item.pinyin.trim()) {
+        this.$set(item, 'loading', true);
         let MethodName = 'tool-PinyinToVoiceFile';
         let data = {
           pinyin: item.pinyin.trim(),
         };
-        GetStaticResources(MethodName, data).then((res) => {
-          if (res.status === 1) {
-            this.data.file_id_list.splice(this.data.file_id_list.indexOf(item.file_id), 1);
-            item.audio_file_id = res.file_id;
-            this.data.file_id_list.push(res.file_id);
-          }
-        });
+        GetStaticResources(MethodName, data)
+          .then((res) => {
+            item.loading = false;
+            if (res.status === 1) {
+              this.data.file_id_list.splice(this.data.file_id_list.indexOf(item.file_id), 1);
+              item.audio_file_id = res.file_id;
+              this.data.file_id_list.push(res.file_id);
+            }
+          })
+          .catch(() => {
+            item.loading = false;
+          });
       }
     },
     // 改变类型
@@ -243,6 +254,37 @@ export default {
         });
       }
     },
+    // 生成汉字
+    handleChineseStrokes(item) {
+      if (item.content.trim()) {
+        this.$set(item, 'loadings', true);
+        let content_arr = item.content.trim().split('');
+        let content_arrs = [];
+        let content_arr_strokes = [];
+        content_arr.forEach((itemc) => {
+          if (itemc.trim()) {
+            content_arrs.push(itemc.trim());
+          }
+        });
+        content_arrs.forEach((itemc, indexc) => {
+          content_arr_strokes.push(null);
+          let MethodName = 'hz_resource_manager-GetHZStrokesContent';
+          let data = {
+            hz: itemc,
+          };
+          GetStaticResources(MethodName, data).then((res) => {
+            let obj = {
+              hz: itemc.trim(),
+              strokes: res,
+            };
+            content_arr_strokes[indexc] = obj;
+          });
+        });
+        item.loadings = false;
+
+        item.chinese_strokes = content_arr_strokes;
+      }
+    },
   },
 };
 </script>

+ 22 - 12
src/views/exercise_questions/create/components/exercises/ChooseToneQuestion.vue

@@ -42,7 +42,9 @@
             />
             <div v-else-if="data.other.audio_generation_method === 'auto'" class="auto-matically">
               <AudioPlay :file-id="item.audio_file_id" theme-color="gray" v-if="item.audio_file_id" />
-              <span class="auto-btn" @click="handleMatically(item)">自动生成</span>
+              <span class="auto-btn" @click="handleMatically(item)" v-loading="item.loading">{{
+                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)" />
@@ -187,14 +189,17 @@ export default {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
-      }).then(() => {
-        this.data.option_list.splice(i, 1);
-        this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
-      });
+      })
+        .then(() => {
+          this.data.option_list.splice(i, 1);
+          this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+        })
+        .catch(() => {});
     },
     // 自动生成音频
     handleMatically(item) {
       if (item.content.trim()) {
+        this.$set(item, 'loading', true);
         if (!this.matically_pinyin_obj[item.mark]) {
           this.handleItemAnswer(item);
         }
@@ -202,13 +207,18 @@ export default {
         let data = {
           pinyin: this.matically_pinyin_obj[item.mark],
         };
-        GetStaticResources(MethodName, data).then((res) => {
-          if (res.status === 1) {
-            this.data.file_id_list.splice(this.data.file_id_list.indexOf(item.file_id), 1);
-            item.audio_file_id = res.file_id;
-            this.data.file_id_list.push(res.file_id);
-          }
-        });
+        GetStaticResources(MethodName, data)
+          .then((res) => {
+            item.loading = false;
+            if (res.status === 1) {
+              this.data.file_id_list.splice(this.data.file_id_list.indexOf(item.file_id), 1);
+              item.audio_file_id = res.file_id;
+              this.data.file_id_list.push(res.file_id);
+            }
+          })
+          .catch(() => {
+            item.loading = false;
+          });
       }
     },
     handleReplaceTone(value, mark) {

+ 6 - 4
src/views/exercise_questions/create/components/exercises/RepeatQuestion.vue

@@ -164,10 +164,12 @@ export default {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
-      }).then(() => {
-        this.data.option_list.splice(i, 1);
-        this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
-      });
+      })
+        .then(() => {
+          this.data.option_list.splice(i, 1);
+          this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+        })
+        .catch(() => {});
     },
   },
 };

+ 56 - 15
src/views/exercise_questions/create/components/exercises/WordCardQuestion.vue

@@ -45,9 +45,9 @@
               :limit="1"
               ref="uploadDrag"
             ></UploadDrag>
-            <div class="word-card-item">
+            <div class="word-card-item" v-loading="item.loadings">
               <div class="word-card">
-                <el-input v-model="item.content" :placeholder="'输入汉字或词汇'" />
+                <el-input v-model="item.content" :placeholder="'输入汉字或词汇'" @blur="handleChineseStrokes(item)" />
                 <el-input v-model="item.pinyin" :placeholder="'拼音间用空格隔开'" />
                 <UploadAudio
                   v-if="data.other.audio_generation_method === 'upload'"
@@ -60,7 +60,9 @@
                 />
                 <div v-else-if="data.other.audio_generation_method === 'auto'" class="auto-matically">
                   <AudioPlay :file-id="item.audio_file_id" theme-color="gray" v-if="item.audio_file_id" />
-                  <span class="auto-btn" @click="handleMatically(item)">自动生成</span>
+                  <span class="auto-btn" @click="handleMatically(item)" v-loading="item.loading">{{
+                    item.audio_file_id ? '已生成' : '自动生成'
+                  }}</span>
                 </div>
                 <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
                 <span class="rate-box"><el-rate v-model="item.rate"></el-rate></span>
@@ -230,11 +232,13 @@ export default {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
-      }).then(() => {
-        this.data.option_list.splice(i, 1);
-        this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
-        this.data.file_id_list.splice(this.data.file_id_list.indexOf(pic_id), 1);
-      });
+      })
+        .then(() => {
+          this.data.option_list.splice(i, 1);
+          this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+          this.data.file_id_list.splice(this.data.file_id_list.indexOf(pic_id), 1);
+        })
+        .catch(() => {});
     },
     // 删除
     delectOptions(i, id) {
@@ -254,17 +258,23 @@ export default {
     // 自动生成音频
     handleMatically(item) {
       if (item.pinyin.trim()) {
+        this.$set(item, 'loading', true);
         let MethodName = 'tool-PinyinToVoiceFile';
         let data = {
           pinyin: item.pinyin.trim().split(' ').join(','),
         };
-        GetStaticResources(MethodName, data).then((res) => {
-          if (res.status === 1) {
-            this.data.file_id_list.splice(this.data.file_id_list.indexOf(item.file_id), 1);
-            item.audio_file_id = res.file_id;
-            this.data.file_id_list.push(res.file_id);
-          }
-        });
+        GetStaticResources(MethodName, data)
+          .then((res) => {
+            item.loading = false;
+            if (res.status === 1) {
+              this.data.file_id_list.splice(this.data.file_id_list.indexOf(item.file_id), 1);
+              item.audio_file_id = res.file_id;
+              this.data.file_id_list.push(res.file_id);
+            }
+          })
+          .catch(() => {
+            item.loading = false;
+          });
       }
     },
     fileUploadSuccess(file_id, file_url, index) {
@@ -272,6 +282,37 @@ export default {
       this.data.option_list[index].picture_file_id = file_id;
       this.$set(this.pic_list, file_id, file_url);
     },
+    // 生成汉字
+    handleChineseStrokes(item) {
+      if (item.content.trim()) {
+        this.$set(item, 'loadings', true);
+        let content_arr = item.content.trim().split('');
+        let content_arrs = [];
+        let content_arr_strokes = [];
+        content_arr.forEach((itemc) => {
+          if (itemc.trim()) {
+            content_arrs.push(itemc.trim());
+          }
+        });
+        content_arrs.forEach((itemc, indexc) => {
+          content_arr_strokes.push(null);
+          let MethodName = 'hz_resource_manager-GetHZStrokesContent';
+          let data = {
+            hz: itemc,
+          };
+          GetStaticResources(MethodName, data).then((res) => {
+            let obj = {
+              hz: itemc.trim(),
+              strokes: res,
+            };
+            content_arr_strokes[indexc] = obj;
+          });
+        });
+        item.loadings = false;
+
+        item.chinese_strokes = content_arr_strokes;
+      }
+    },
   },
 };
 </script>

+ 280 - 0
src/views/exercise_questions/create/components/exercises/WritePictureQuestion.vue

@@ -0,0 +1,280 @@
+<template>
+  <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="输入题干" />
+      </div>
+
+      <div class="content">
+        <div v-for="(item, index) in data.option_list" :key="index" class="content-item">
+          <div v-if="pic_list[item.picture_file_id]" class="content-pic">
+            <div class="item-left">
+              <el-image
+                style="width: 72px; height: 72px"
+                :src="pic_list[item.picture_file_id]"
+                :preview-src-list="[pic_list[item.picture_file_id]]"
+                fit="contain"
+              />
+              <button class="delete-btn" @click="delectOptions(index, item.picture_file_id)">
+                <i class="el-icon-delete"></i>删除
+              </button>
+            </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>
+            </div>
+          </div>
+        </div>
+        <UploadDrag @fileUploadSuccess="fileUploadSuccess" :limit="999" ref="uploadDrag"></UploadDrag>
+
+        <template v-if="isEnable(data.property.is_enable_description)">
+          <label class="title-little">题目要求:</label>
+          <el-input v-model="data.description" rows="3" resize="none" type="textarea" placeholder="输入题目要求" />
+        </template>
+        <label class="title-little">阅读材料:</label>
+        <RichText v-model="data.article" placeholder="输入阅读材料" />
+        <template v-if="isEnable(data.property.is_enable_sample_text)">
+          <el-divider class="write-divider" />
+          <label class="title-little">范文:</label>
+          <RichText v-model="data.sample_text" placeholder="输入范文" :wordlimit-num="5000" />
+          <p class="tips">多篇范文之间使用分割线(---)</p>
+        </template>
+      </div>
+    </template>
+
+    <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>
+        <el-form-item label-width="45px">
+          <el-radio
+            v-for="{ value, label } in questionNumberTypeList"
+            :key="value"
+            v-model="data.other.question_number_type"
+            :label="value"
+          >
+            {{ label }}
+          </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 scoreTypeList"
+            :key="value"
+            v-model="data.property.score_type"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label-width="45px">
+          <el-input-number
+            v-model="data.property.score"
+            :min="0"
+            :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
+          />
+        </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-form-item>
+        <el-form-item label="范文">
+          <el-radio
+            v-for="{ value, label } in switchOption"
+            :key="value"
+            v-model="data.property.is_enable_sample_text"
+            :label="value"
+          >
+            {{ label }}
+          </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_upload_accessory"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import QuestionMixin from '../common/QuestionMixin.js';
+
+import { writePictrueData, getOption } from '@/views/exercise_questions/data/writePicture';
+import { GetFileStoreInfo } from '@/api/app';
+import UploadDrag from '../common/UploadDrag.vue';
+
+export default {
+  name: 'TalkPicture',
+  mixins: [QuestionMixin],
+  components: { UploadDrag },
+  data() {
+    return {
+      data: JSON.parse(JSON.stringify(writePictrueData)),
+      pic_list: {},
+      is_first: true,
+    };
+  },
+  watch: {
+    'data.file_id_list': {
+      handler() {
+        if (this.is_first) {
+          this.handleData();
+        }
+      },
+      deep: true,
+    },
+  },
+  created() {},
+  mounted() {},
+  methods: {
+    // 初始化数据
+    handleData() {
+      this.data.file_id_list.forEach((item) => {
+        GetFileStoreInfo({ file_id: item }).then(({ file_id, file_url }) => {
+          this.$set(this.pic_list, file_id, file_url);
+        });
+      });
+      this.is_first = false;
+    },
+    // 删除
+    delectOptions(i, id) {
+      this.$confirm('是否删除该条全部信息?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          delete this.pic_list[id];
+          this.data.file_id_list.splice(this.data.file_id_list.indexOf(id), 1);
+          this.data.option_list.splice(i, 1);
+          this.$refs.uploadDrag.clearFiles();
+        })
+        .catch(() => {});
+    },
+    fileUploadSuccess(file_id, file_url) {
+      this.data.file_id_list.push(file_id);
+      this.data.option_list.push(getOption());
+      this.data.option_list[this.data.option_list.length - 1].picture_file_id = file_id;
+      this.$set(this.pic_list, file_id, file_url);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.content {
+  :deep .el-upload {
+    width: 100%;
+
+    &-dragger {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 90px;
+      font-size: 14px;
+
+      :first-child {
+        color: #000;
+      }
+
+      :last-child {
+        color: $text-color;
+      }
+    }
+  }
+
+  .content-item {
+    .content-pic {
+      display: flex;
+      column-gap: 8px;
+      margin-bottom: 24px;
+    }
+  }
+
+  .delete-btn {
+    width: 100%;
+    padding: 5px 8px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #f53f3f;
+    background: #fff4f4;
+    border: none;
+    border-radius: 4px;
+
+    .el-icon-delete {
+      margin-right: 8px;
+    }
+
+    &:hover {
+      color: #f53f3f;
+    }
+
+    &:focus {
+      outline: none;
+    }
+  }
+
+  .item-left {
+    width: 72px;
+  }
+
+  .item-right {
+    flex: 1;
+
+    .item-rich {
+      display: flex;
+
+      > label {
+        flex-shrink: 0;
+        width: 64px;
+        font-size: 14px;
+        line-height: 32px;
+        color: #4e5969;
+      }
+    }
+  }
+}
+</style>

+ 0 - 10
src/views/exercise_questions/create/components/exercises/WriteQuestion.vue

@@ -100,16 +100,6 @@
             {{ label }}
           </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_voice_answer"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
         <el-form-item label="上传附件">
           <el-radio
             v-for="{ value, label } in switchOption"

+ 6 - 0
src/views/exercise_questions/create/index.vue

@@ -89,6 +89,8 @@ import ListenSelectPreview from '../preview/ListenSelectPreview.vue';
 import ListenFillPreview from '../preview/ListenFillPreview.vue';
 import ListenJudgePreview from '../preview/ListenJudgePreview.vue';
 import WordCardPreview from '../preview/WordCardPreview.vue';
+import AnswerQuestionPreview from '../preview/AnswerQuestionPreview.vue';
+import WritePictruePreview from '../preview/WritePictruePreview.vue';
 
 export default {
   name: 'CreateExercise',
@@ -111,6 +113,8 @@ export default {
     ListenFillPreview,
     ListenJudgePreview,
     WordCardPreview,
+    AnswerQuestionPreview,
+    WritePictruePreview,
   },
   provide() {
     return {
@@ -151,6 +155,8 @@ export default {
         listen_fill: ListenFillPreview,
         listen_judge: ListenJudgePreview,
         word_card: WordCardPreview,
+        answer_question: AnswerQuestionPreview,
+        write_picture: WritePictruePreview,
       },
     };
   },

+ 26 - 0
src/views/exercise_questions/data/answerQuestion.js

@@ -0,0 +1,26 @@
+import { stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } from './common';
+
+// 朗读题数据模板
+export const answerQuestionData = {
+  type: 'answer_question', // 题型
+  stem: '', // 题干
+  description: '', // 描述
+  file_id_list: [], // 文件 id 列表
+  answer: {
+    score: 0,
+    score_type: scoreTypeList[0].value,
+  }, // 答案
+  // 题型属性
+  property: {
+    stem_type: stemTypeList[0].value, // 题干类型
+    question_number: '1', // 题号
+    is_enable_reference_answer: switchOption[0].value, // 是否开启参考答案
+    is_enable_description: switchOption[0].value, // 是否启用描述
+    score: 1, // 分值
+    score_type: scoreTypeList[0].value, // 分值类型
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
+};

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

@@ -9,6 +9,7 @@ export function getOption(content = '') {
     pinyin: '',
     definition: '',
     collocation: '',
+    chinese_strokes: [],
   };
 }
 // 汉字类型列表

+ 9 - 4
src/views/exercise_questions/data/common.js

@@ -22,6 +22,7 @@ export const questionTypeOption = [
       { label: '听说训练', value: 'repeat' },
       { label: '看图说话', value: 'talk_picture' },
       { label: '对话题', value: 'dialogue' },
+      { label: '回答问题', value: 'answer_question' },
     ],
   },
   {
@@ -42,12 +43,16 @@ export const questionTypeOption = [
     ],
   },
   {
-    value: 'read',
-    label: '阅读题',
+    value: 'writing',
+    label: '写作题',
+    children: [
+      { label: '基础写作', value: 'write' },
+      { label: '看图写作', value: 'write_picture' },
+    ],
   },
   {
-    value: 'write',
-    label: '写作题',
+    value: 'read',
+    label: '阅读题',
   },
 ];
 

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

@@ -11,6 +11,7 @@ export function getOption(content = '') {
     rate: null,
     example_sentence: ['', ''],
     picture_file_id: '',
+    chinese_strokes: [],
   };
 }
 

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

@@ -17,7 +17,6 @@ export const writeData = {
     score_type: scoreTypeList[0].value, // 分值类型
     word_num: 50, // 词数
     is_enable_sample_text: 'true', // 范文开启
-    is_enable_voice_answer: 'true', // 语音作答
     is_enable_upload_accessory: 'true', // 上传附件
   },
   // 其他属性

+ 35 - 0
src/views/exercise_questions/data/writePicture.js

@@ -0,0 +1,35 @@
+import { stemTypeList, scoreTypeList, questionNumberTypeList, switchOption } from './common';
+import { getRandomNumber } from '@/utils/index';
+export function getOption() {
+  return {
+    picture_title: '',
+    picture_info: '',
+    picture_file_id: '',
+    mark: getRandomNumber(),
+  };
+}
+export const writePictrueData = {
+  type: 'write_picture', // 题型
+  stem: '', // 题干
+  description: '',
+  article: '',
+  sample_text: '',
+  option_list: [], // 选项
+  file_id_list: [],
+  answer: { score: 0, score_type: scoreTypeList[0].value }, // 答案
+  // 题型属性
+  property: {
+    stem_type: stemTypeList[0].value, // 题干类型
+    question_number: '1', // 题号
+    score: 1, // 分值
+    is_enable_description: switchOption[1].value, // 描述
+    score_type: scoreTypeList[0].value, // 分值类型
+    word_num: 50, // 词数
+    is_enable_sample_text: 'true', // 范文开启
+    is_enable_upload_accessory: 'true', // 上传附件
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
+};

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

@@ -0,0 +1,61 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="answerquestion-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+
+    <div v-if="isEnable(data.property.is_enable_description)" class="description">{{ data.description }}</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].voice_file_id" />
+    <div v-if="isEnable(data.property.is_enable_reference_answer) && 1 === 2" class="reference-box">
+      <h5 class="reference-title">参考答案</h5>
+      <span class="reference-answer" v-html="sanitizeHTML(data.reference_answer)"></span>
+    </div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+import SoundRecordPreview from './components/common/SoundRecordPreview.vue';
+
+export default {
+  name: 'ReadAloudPreview',
+  components: {
+    SoundRecordPreview,
+  },
+  mixins: [PreviewMixin],
+  created() {
+    this.$set(this.answer.answer_list, 0, { voice_file_id: '' });
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.answerquestion-preview {
+  @include preview;
+
+  :deep .sound-record-wrapper {
+    justify-content: center;
+  }
+
+  .reference-box {
+    padding: 12px;
+    background: #f9f8f9;
+
+    .reference-title {
+      margin: 0 0 10px;
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 32px;
+      color: #4e5969;
+    }
+  }
+}
+</style>

+ 32 - 46
src/views/exercise_questions/preview/ChinesePreview.vue

@@ -10,7 +10,7 @@
     <!-- 笔画学习 -->
     <div :class="['words-box', 'words-box-' + data.property.learn_type]">
       <div v-for="(item, index) in data.option_list" :key="index" :class="['words-item']">
-        <template v-if="item.content && item.content.trim() && item.strokes">
+        <template v-if="item.content && item.content.trim() && item.chinese_strokes[0].strokes">
           <div
             v-if="data.property.learn_type !== 'dictation'"
             class="words-top"
@@ -31,7 +31,7 @@
               :play-storkes="true"
               :book-text="item.content"
               :target-div="'pre' + item.content + index"
-              :book-strokes="item.strokes"
+              :book-strokes="item.chinese_strokes[0].strokes"
               :class="['strock-chinese', 'border-right-none']"
             />
             <Strockred
@@ -41,7 +41,7 @@
               :hanzi-color="hanzi_color"
               :reset="true"
               :target-div="'write-praT' + item.content + itemI + Math.random().toString(36).substring(2, 10)"
-              :book-strokes="item.strokes"
+              :book-strokes="item.chinese_strokes[0].strokes"
               :class="[
                 'strock-chinese',
                 (itemI + 1) % writer_number_yuan !== 0 && itemI !== writer_number ? 'border-right-none' : '',
@@ -68,13 +68,13 @@
               :play-storkes="true"
               :book-text="item.content"
               :target-div="'pre' + item.content + index"
-              :book-strokes="item.strokes"
+              :book-strokes="item.chinese_strokes[0].strokes"
               :class="['strock-chinese']"
             />
             <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' : '']"
-                @click="freeWrite(items, index, indexs)"
+                @click="freeWrite(items, index, indexs, item.mark)"
               >
                 <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
                 <img
@@ -96,7 +96,7 @@
           <div class="words-dic-box">
             <div v-for="(itemc, indexc) in item.imgArr" :key="indexc" class="words-dic-item">
               <span class="pinyin">{{ item.pinyin_arr[indexc].pinyin_item }}</span>
-              <div :class="['strockplay-newWord']" @click="freeWrite(itemc, index, indexc)">
+              <div :class="['strockplay-newWord']" @click="freeWrite(itemc, index, indexc, item.mark)">
                 <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
                 <img
                   v-if="!play_status && itemc && itemc.strokes_image_url"
@@ -133,7 +133,6 @@ import PreviewMixin from './components/PreviewMixin';
 import Strockplayredline from './components/common/Strockplayredline.vue';
 import Strockred from './components/common/Strockred.vue';
 import FreewriteLettle from './components/common/FreewriteLettle.vue';
-import { GetStaticResources } from '@/api/app';
 
 export default {
   name: 'ChinesePreview',
@@ -149,9 +148,7 @@ export default {
       hanzi_color: '#404040', // 描红汉字底色
       writer_number_yuan: 19,
       writer_number: null, // 书写个数
-      answer_list: {
-        write_model: {},
-      }, // 用户答题数据
+      answer_list: [], // 用户答题数据
       if_free_show: false,
       free_img: [],
       active_index: null,
@@ -198,6 +195,7 @@ export default {
         '快',
         '素',
       ],
+      active_mark: '',
     };
   },
   watch: {
@@ -217,19 +215,7 @@ export default {
   methods: {
     // 初始化数据
     handleData() {
-      // if (
-      //   document.getElementsByClassName('preview-content') &&
-      //   document.getElementsByClassName('preview-content')[0] &&
-      //   !this.writer_number
-      // ) {
-      //   this.writer_number_yuan =
-      //     Math.floor((document.getElementsByClassName('preview-content')[0].clientWidth - 128) / 64) - 1;
-      // }
-      // if (this.data.property.learn_type === 'paint') {
-      //   this.writer_number = this.writer_number_yuan - 5;
-      // } else {
-      //   this.writer_number = this.writer_number_yuan;
-      // }
+      this.answer_list = [];
       this.writer_number_yuan = Math.floor(
         (document.getElementsByClassName('preview-content')[0].clientWidth - 128) / 64,
       );
@@ -247,37 +233,37 @@ export default {
             item.pinyin_arr.push(obj);
           });
           item.imgArr = arr;
-          this.answer_list.write_model[this.hz_data[index]] = arr;
+          // this.answer_list.write_model[this.hz_data[index]] = arr;
         } else if (item.content.trim()) {
-          let MethodName = 'hz_resource_manager-GetHZStrokesContent';
-          let data = {
-            hz: item.content.trim(),
-          };
-          GetStaticResources(MethodName, data).then((res) => {
-            this.$set(item, 'strokes', res);
-          });
-
           for (let i = 0; i < this.writer_number; i++) {
             arr.push(null);
           }
           item.imgArr = arr;
-          this.answer_list.write_model[item.content] = arr;
+          // this.answer_list.write_model[item.content] = arr;
         }
+        let obj = {
+          mark: item.mark,
+          strokes_content_list: arr,
+        };
+        this.answer_list.push(obj);
       });
+      console.log(this.data);
+      console.log(this.answer_list);
     },
     changePraShow() {
       this.if_free_show = false;
     },
-    closeIfFreeShow(data, rowIndex, colIndex) {
+    closeIfFreeShow(data, rowIndex, colIndex, mark) {
       this.data.option_list[rowIndex].imgArr[colIndex] = data;
       this.if_free_show = false;
-      this.freeWrite(data, rowIndex, colIndex);
+      this.freeWrite(data, rowIndex, colIndex, mark);
       this.$forceUpdate();
     },
-    freeWrite(imgUrl, index, indexs) {
+    freeWrite(imgUrl, index, indexs, mark) {
       this.if_free_show = true;
       this.active_index = index;
       this.active_col_index = indexs;
+      this.active_mark = mark;
       if (this.data.property.learn_type === 'dictation') {
         this.current_hz = this.hz_data[index];
       } else {
@@ -288,20 +274,20 @@ export default {
     // 删除记录
     deleteWriteRecord(rowIndex, colIndex, current_hz) {
       this.$set(this.data.option_list[rowIndex].imgArr, colIndex, {});
-      let answer = {
-        hz: current_hz,
-        strokes_content: '',
-        strokes_image_url: '',
-      };
-      this.changeCurQue(answer, colIndex);
+      this.changeCurQue(null, colIndex, this.active_mark);
       this.current_hz_data = null;
+      this.active_mark = '';
       this.$forceUpdate();
     },
-    changeCurQue(answer, colIndex) {
+    changeCurQue(answer, colIndex, mark) {
       if (answer) {
-        let write_model = this.answer_list.write_model;
-        let hz = answer.hz;
-        write_model[hz][colIndex] = answer;
+        let write_model = [];
+        this.answer_list.forEach((itema) => {
+          if (itema.mark === mark) {
+            write_model = itema.strokes_content_list;
+          }
+        });
+        write_model[colIndex] = answer;
       }
     },
   },

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

@@ -47,12 +47,12 @@
             con_preview[i].user_answer[con_preview[i].item_active_index].select_tone === value
               ? 'active'
               : data.property.answer_mode === 'label' &&
-                  con_preview[i].user_answer[con_preview[i].item_active_index] &&
-                  con_preview[i].user_answer[con_preview[i].item_active_index].select_tone === value &&
-                  con_preview[i].user_answer[con_preview[i].item_active_index].select_letter === active_letter &&
-                  select_item_index === i
-                ? 'active'
-                : '',
+                con_preview[i].user_answer[con_preview[i].item_active_index] &&
+                con_preview[i].user_answer[con_preview[i].item_active_index].select_tone === value &&
+                con_preview[i].user_answer[con_preview[i].item_active_index].select_letter === active_letter &&
+                select_item_index === i
+              ? 'active'
+              : '',
           ]"
           @click="chooseTone(con_preview[i], value, i)"
         >
@@ -158,6 +158,7 @@ export default {
         };
         this.con_preview.push(obj);
       });
+      console.log(this.con_preview);
     },
     handleReplaceTone(e) {
       this.$nextTick(() => {

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

@@ -13,7 +13,7 @@
           height="276px"
           :autoplay="false"
           indicator-position="none"
-          arrow="never"
+          arrow="always"
           @change="changeImg"
         >
           <el-carousel-item v-for="(item, index) in data.option_list" :key="index">
@@ -27,20 +27,13 @@
         </el-carousel>
         <h3 class="pic-title" v-html="sanitizeHTML(data.option_list[active_index].picture_title)"></h3>
         <p class="pic-info" v-html="sanitizeHTML(data.option_list[active_index].picture_info)"></p>
-      </div>
-      <div class="content-right">
-        <el-input
-          v-model="answer_list[active_index].value"
-          rows="12"
-          resize="none"
-          type="textarea"
-          placeholder="请输入"
-        />
+
         <template v-if="isEnable(data.property.is_enable_voice_answer)">
           <!-- 语音作答 -->
           <SoundRecordPreview :wav-blob.sync="answer_list[active_index].audio_file_id" />
         </template>
       </div>
+      <div class="content-right"></div>
     </div>
     <div v-if="isEnable(data.property.is_enable_reference_answer) && 1 === 2" class="reference-box">
       <h5 class="reference-title">参考答案</h5>
@@ -84,7 +77,6 @@ export default {
       this.data.option_list.forEach((item) => {
         let obj = {
           mark: item.mark,
-          value: '',
           audio_file_id: '',
         };
         this.answer_list.push(obj);
@@ -124,6 +116,10 @@ export default {
         opacity: 0.2;
       }
 
+      :deep .el-carousel__arrow:focus {
+        outline: none;
+      }
+
       .el-carousel__item--card.is-active {
         .el-image {
           background: #fff;
@@ -150,6 +146,10 @@ export default {
       }
     }
 
+    .sound-record-wrapper {
+      margin-top: 8px;
+    }
+
     &-right {
       flex: 1;
 

+ 27 - 28
src/views/exercise_questions/preview/WordCardPreview.vue

@@ -26,14 +26,14 @@
         </div>
         <template v-for="(item, index) in data.option_list">
           <div class="strock-box" :key="index" v-if="index === active_index">
-            <div class="strock-left" v-if="item.content_arr_strokes && item.content_arr_strokes.length > 0">
-              <template v-for="(items, indexs) in item.content_arr_strokes">
+            <div class="strock-left" v-if="item.chinese_strokes && item.chinese_strokes.length > 0">
+              <template v-for="(items, indexs) in item.chinese_strokes">
                 <Strockplayredline
                   :play-storkes="true"
                   :book-text="items.hz"
                   :target-div="'pre' + items.hz + indexs + active_index"
                   :book-strokes="items.strokes"
-                  :class="['strock-chinese', indexs !== item.content_arr_strokes.length - 1 ? 'border-right-none' : '']"
+                  :class="['strock-chinese', indexs !== item.chinese_strokes.length - 1 ? 'border-right-none' : '']"
                   :key="indexs"
                   v-if="items"
                 />
@@ -84,7 +84,7 @@
 <script>
 import { computeOptionMethods } from '@/views/exercise_questions/data/common';
 import PreviewMixin from './components/PreviewMixin';
-import { GetStaticResources, GetFileStoreInfo } from '@/api/app';
+import { GetFileStoreInfo } from '@/api/app';
 import SoundRecordPreview from './components/common/SoundRecordPreview.vue';
 import Strockplayredline from './components/common/Strockplayredline.vue';
 
@@ -103,7 +103,6 @@ export default {
   },
   watch: {},
   created() {
-    console.log(this.data);
     this.handleData();
   },
   mounted() {},
@@ -124,29 +123,29 @@ export default {
         };
         item.definition_preview = item.definition.split('\n');
         this.answer_list.push(obj);
-        let content_arr = item.content.trim().split('');
-        let content_arrs = [];
-        let content_arr_strokes = [];
-        content_arr.forEach((itemc) => {
-          if (itemc.trim()) {
-            content_arrs.push(itemc.trim());
-          }
-        });
-        content_arrs.forEach((itemc, indexc) => {
-          content_arr_strokes.push(null);
-          let MethodName = 'hz_resource_manager-GetHZStrokesContent';
-          let data = {
-            hz: itemc,
-          };
-          GetStaticResources(MethodName, data).then((res) => {
-            let obj = {
-              hz: itemc.trim(),
-              strokes: res,
-            };
-            content_arr_strokes[indexc] = obj;
-          });
-        });
-        item.content_arr_strokes = content_arr_strokes;
+        // let content_arr = item.content.trim().split('');
+        // let content_arrs = [];
+        // let content_arr_strokes = [];
+        // content_arr.forEach((itemc) => {
+        //   if (itemc.trim()) {
+        //     content_arrs.push(itemc.trim());
+        //   }
+        // });
+        // content_arrs.forEach((itemc, indexc) => {
+        //   content_arr_strokes.push(null);
+        //   let MethodName = 'hz_resource_manager-GetHZStrokesContent';
+        //   let data = {
+        //     hz: itemc,
+        //   };
+        //   GetStaticResources(MethodName, data).then((res) => {
+        //     let obj = {
+        //       hz: itemc.trim(),
+        //       strokes: res,
+        //     };
+        //     content_arr_strokes[indexc] = obj;
+        //   });
+        // });
+        // item.content_arr_strokes = content_arr_strokes;
       });
     },
   },

+ 246 - 0
src/views/exercise_questions/preview/WritePictruePreview.vue

@@ -0,0 +1,246 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="writepicture-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+    <div class="article-content" v-html="sanitizeHTML(data.article)"></div>
+    <div v-if="isEnable(data.property.is_enable_description)" class="description">
+      {{ data.description }}
+    </div>
+    <div class="content">
+      <div class="content-left">
+        <el-carousel
+          type="card"
+          height="276px"
+          :autoplay="false"
+          indicator-position="none"
+          arrow="always"
+          @change="changeImg"
+        >
+          <el-carousel-item v-for="(item, index) in data.option_list" :key="index">
+            <el-image
+              v-if="pic_list[item.picture_file_id]"
+              style="width: 370px; height: 276px"
+              :src="pic_list[item.picture_file_id]"
+              fit="contain"
+            />
+          </el-carousel-item>
+        </el-carousel>
+        <h3 class="pic-title" v-html="sanitizeHTML(data.option_list[active_index].picture_title)"></h3>
+        <p class="pic-info" v-html="sanitizeHTML(data.option_list[active_index].picture_info)"></p>
+      </div>
+      <div class="content-right">
+        <el-input
+          v-model="answer_list[active_index].value"
+          rows="12"
+          resize="none"
+          type="textarea"
+          placeholder="请输入"
+          :maxlength="data.property.word_num"
+          show-word-limit
+          @input="handleInput"
+        />
+      </div>
+    </div>
+    <template v-if="isEnable(data.property.is_enable_upload_accessory)">
+      <!-- 上传附件 -->
+      <UploadFiles
+        :fille-number="999"
+        file-type-name="文件"
+        :upload-type="'*'"
+        :file-id-list="answer_list[active_index].accessory_file_id"
+        upload-title="上传附件:"
+        @upload="handleUpload"
+        @deleteFile="handleDelete"
+      />
+    </template>
+    <template v-if="isEnable(data.property.is_enable_sample_text)">
+      <el-divider content-position="center"
+        ><span
+          :class="['sample-text', show_sample_text ? 'sample-show' : 'sample-hide']"
+          @click="show_sample_text = !show_sample_text"
+          >{{ show_sample_text ? '隐藏范文' : '查看范文' }}</span
+        ></el-divider
+      >
+      <div v-if="show_sample_text" class="article-content" v-html="sanitizeHTML(data.sample_text)"></div>
+    </template>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+import { GetFileStoreInfo } from '@/api/app';
+import SoundRecordPreview from './components/common/SoundRecordPreview.vue';
+import UploadFiles from './components/common/UploadFiles.vue';
+
+export default {
+  name: 'TalkPictruePreview',
+  components: {
+    SoundRecordPreview,
+    UploadFiles,
+  },
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      show_sample_text: false,
+      pic_list: {},
+      active_index: 0,
+      answer_list: [],
+    };
+  },
+  created() {
+    this.handleData();
+  },
+  methods: {
+    // 初始化数据
+    handleData() {
+      this.answer_list = [];
+      this.pic_list = {};
+      this.active_index = 0;
+      this.data.file_id_list.forEach((item) => {
+        GetFileStoreInfo({ file_id: item }).then(({ file_id, file_url }) => {
+          this.$set(this.pic_list, file_id, file_url);
+        });
+      });
+      this.data.option_list.forEach((item) => {
+        let obj = {
+          mark: item.mark,
+          value: '',
+          accessory_file_id: [], // 上传文件列表
+        };
+        this.answer_list.push(obj);
+      });
+    },
+    changeImg(index) {
+      this.active_index = index;
+    },
+    // 文件上传成功
+    handleUpload(fileId) {
+      this.user_answer[this.active_index].accessory_file_id.push(fileId);
+    },
+    // 删除文件
+    handleDelete(fileId) {
+      this.user_answer[this.active_index].accessory_file_id.splice(
+        this.user_answer.accessory_file_id.indexOf(fileId),
+        1,
+      );
+    },
+    handleInput(value) {
+      if (value.length >= this.data.property.word_num) {
+        this.$message.warning(`字数达到${value.length}字!`);
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.writepicture-preview {
+  @include preview;
+
+  :deep p {
+    margin: 0;
+  }
+
+  .content {
+    display: flex;
+    column-gap: 24px;
+
+    &-left {
+      flex-shrink: 0;
+      width: 478px;
+
+      :deep .el-carousel__item--card {
+        width: 77%;
+        margin-left: -13.5%;
+      }
+
+      .el-image {
+        opacity: 0.2;
+      }
+
+      .el-carousel__item--card.is-active {
+        .el-image {
+          background: #fff;
+          opacity: 1;
+        }
+      }
+
+      .pic-title {
+        margin: 8px 0 4px;
+        font-size: 12px;
+        font-weight: 600;
+        line-height: 20px;
+        color: #000;
+        word-break: break-word;
+      }
+
+      .pic-info {
+        margin: 0;
+        font-size: 12px;
+        font-weight: 400;
+        line-height: 20px;
+        color: #000;
+        word-break: break-word;
+      }
+    }
+
+    :deep .el-carousel__arrow:focus {
+      outline: none;
+    }
+
+    &-right {
+      flex: 1;
+
+      .el-textarea {
+        height: 276px;
+        margin-bottom: 16px;
+      }
+    }
+  }
+
+  .reference-box {
+    padding: 12px;
+    background: #f9f8f9;
+
+    .reference-title {
+      margin: 0 0 10px;
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 32px;
+      color: #4e5969;
+    }
+  }
+
+  .el-divider--horizontal {
+    margin: 12px 0;
+  }
+
+  .sample-text {
+    font-size: 14px;
+    font-weight: 400;
+    line-height: 30px;
+    color: #000;
+    cursor: pointer;
+
+    &.sample-show {
+      color: $light-main-color;
+    }
+  }
+
+  .article-content {
+    :deep p {
+      margin: 0;
+    }
+  }
+
+  :deep .el-textarea .el-input__count {
+    bottom: 15px;
+    background-color: #f2f3f5;
+  }
+}
+</style>

+ 1 - 7
src/views/exercise_questions/preview/WritePreview.vue

@@ -16,10 +16,7 @@
       show-word-limit
       @input="handleInput"
     />
-    <template v-if="isEnable(data.property.is_enable_voice_answer)">
-      <!-- 语音作答 -->
-      <SoundRecordPreview :wav-blob.sync="user_answer.voice_file_id" />
-    </template>
+
     <template v-if="isEnable(data.property.is_enable_upload_accessory)">
       <!-- 上传附件 -->
       <UploadFiles
@@ -46,14 +43,12 @@
 </template>
 
 <script>
-import SoundRecordPreview from './components/common/SoundRecordPreview.vue';
 import PreviewMixin from './components/PreviewMixin';
 import UploadFiles from './components/common/UploadFiles.vue';
 
 export default {
   name: 'WritePreview',
   components: {
-    SoundRecordPreview,
     UploadFiles,
   },
   mixins: [PreviewMixin],
@@ -62,7 +57,6 @@ export default {
       show_sample_text: false,
       user_answer: {
         text: '', // 用户文章
-        voice_file_id: '', // 录音内容
         accessory_file_id: [], // 上传文件列表
       },
     };