Parcourir la source

Merge branch 'lhd'

natasha il y a 1 an
Parent
commit
30e27b98eb

+ 64 - 0
src/views/exercise_questions/create/components/common/UploadDrag.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="upload-wrapper">
+    <el-upload
+      ref="upload"
+      action="no"
+      accept=".jpg,.png,.gif"
+      drag
+      :show-file-list="false"
+      :before-upload="beforeUpload"
+      :http-request="upload"
+      :on-exceed="handleExceed"
+      :limit="limit"
+    >
+      <div>点击或拖拽图片到此上传</div>
+      <div>只有 jpg, png, gif 等格式文件可以上传,文件大小不得超过 5MB</div>
+    </el-upload>
+  </div>
+</template>
+
+<script>
+import { fileUpload } from '@/api/app';
+
+export default {
+  name: 'UploadAudio',
+  props: {
+    limit: {
+      type: Number,
+      default: 1,
+    },
+    itemIndex: {
+      type: Number,
+      default: null,
+    },
+  },
+  data() {
+    return {};
+  },
+  watch: {},
+  methods: {
+    beforeUpload(file) {
+      // 可以用来限制文件大小
+      if (file.size > 5 * 1024 * 1024) {
+        this.$message.warning('上传图片大小不能超过5M');
+        return false; // 必须返回false
+      }
+    },
+    upload(file) {
+      fileUpload('Mid', file, { isGlobalprogress: true }).then(({ file_info_list }) => {
+        if (file_info_list.length > 0) {
+          const { file_id, file_url } = file_info_list[0];
+          this.$emit('fileUploadSuccess', file_id, file_url, this.itemIndex);
+        }
+      });
+    },
+    handleExceed(files, fileList) {
+      this.$message.warning(
+        `当前限制选择 ${this.limit ? this.limit : 1} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${
+          files.length + fileList.length
+        } 个文件`,
+      );
+    },
+  },
+};
+</script>

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

@@ -58,6 +58,7 @@ import SortQuestion from './exercises/SortQuestion.vue';
 import ListenSelectQuestion from './exercises/ListenSelectQuestion.vue';
 import ListenJudgeQuestion from './exercises/ListenJudgeQuestion.vue';
 import ListenFillQuestion from './exercises/ListenFillQuestion.vue';
+import WordCardQuestion from './exercises/WordCardQuestion.vue';
 
 export default {
   name: 'CreateMain',
@@ -79,6 +80,7 @@ export default {
     ListenSelectQuestion,
     ListenJudgeQuestion,
     ListenFillQuestion,
+    WordCardQuestion,
   },
   provide() {
     return {
@@ -118,6 +120,7 @@ export default {
         listen_select: ListenSelectQuestion,
         listen_judge: ListenJudgeQuestion,
         listen_fill: ListenFillQuestion,
+        word_card: WordCardQuestion,
       },
     };
   },

+ 9 - 3
src/views/exercise_questions/create/components/exercises/ChineseQuestion.vue

@@ -54,7 +54,7 @@
               @deleteFile="deleteFiles"
             />
             <div v-else-if="data.other.audio_generation_method === 'auto'" class="auto-matically">
-              <AudioPlay :file-id="item.audio_file_id" theme-color="gray" />
+              <AudioPlay :file-id="item.audio_file_id" theme-color="gray" v-if="item.audio_file_id" />
               <span class="auto-btn" @click="handleMatically(item)">自动生成</span>
             </div>
             <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
@@ -208,8 +208,14 @@ export default {
     },
     // 删除小题
     deleteOption(i, file_id) {
-      this.data.option_list.splice(i, 1);
-      this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+      this.$confirm('是否删除?', '提示', {
+        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);
+      });
     },
     // 自动生成音频
     handleMatically(item) {

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

@@ -41,7 +41,7 @@
               @deleteFile="deleteFiles"
             />
             <div v-else-if="data.other.audio_generation_method === 'auto'" class="auto-matically">
-              <AudioPlay :file-id="item.audio_file_id" theme-color="gray" />
+              <AudioPlay :file-id="item.audio_file_id" theme-color="gray" v-if="item.audio_file_id" />
               <span class="auto-btn" @click="handleMatically(item)">自动生成</span>
             </div>
             <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
@@ -168,6 +168,7 @@ export default {
       res_arr: [],
     };
   },
+  created() {},
   methods: {
     addOption() {
       this.data.option_list.push(getOption());
@@ -182,12 +183,21 @@ export default {
     },
     // 删除小题
     deleteOption(i, file_id) {
-      this.data.option_list.splice(i, 1);
-      this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+      this.$confirm('是否删除?', '提示', {
+        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);
+      });
     },
     // 自动生成音频
     handleMatically(item) {
       if (item.content.trim()) {
+        if (!this.matically_pinyin_obj[item.mark]) {
+          this.handleItemAnswer(item);
+        }
         let MethodName = 'tool-PinyinToVoiceFile';
         let data = {
           pinyin: this.matically_pinyin_obj[item.mark],

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

@@ -160,8 +160,14 @@ export default {
     },
     // 删除小题
     deleteOption(i, file_id) {
-      this.data.option_list.splice(i, 1);
-      this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+      this.$confirm('是否删除?', '提示', {
+        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);
+      });
     },
   },
 };

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

@@ -53,19 +53,7 @@
             </div>
           </template>
         </div>
-        <el-upload
-          ref="upload"
-          action="no"
-          accept=".jpg,.png,.gif"
-          drag
-          :show-file-list="false"
-          :before-upload="beforeUpload"
-          :http-request="upload"
-          :on-exceed="handleExceed"
-        >
-          <div>点击或拖拽图片到此上传</div>
-          <div>只有 jpg, png, gif 等格式文件可以上传,文件大小不得超过 5MB</div>
-        </el-upload>
+        <UploadDrag @fileUploadSuccess="fileUploadSuccess" :limit="999" ref="uploadDrag"></UploadDrag>
       </div>
     </template>
 
@@ -150,11 +138,13 @@
 import QuestionMixin from '../common/QuestionMixin.js';
 
 import { talkPictrueData, getOption } from '@/views/exercise_questions/data/talkPicture';
-import { fileUpload, GetFileStoreInfo } from '@/api/app';
+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(talkPictrueData)),
@@ -195,33 +185,15 @@ export default {
           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(() => {});
     },
-    beforeUpload(file) {
-      // 可以用来限制文件大小
-      if (file.size > 5 * 1024 * 1024) {
-        this.$message.warning('上传图片大小不能超过5M');
-        return false; // 必须返回false
-      }
-    },
-    upload(file) {
-      fileUpload('Mid', file, { isGlobalprogress: true }).then(({ file_info_list }) => {
-        if (file_info_list.length > 0) {
-          const { file_id, file_url } = file_info_list[0];
-          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);
-        }
-      });
-    },
-    handleExceed(files, fileList) {
-      this.$message.warning(
-        `当前限制选择 ${this.filleNumber ? this.filleNumber : 1} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${
-          files.length + fileList.length
-        } 个文件`,
-      );
+    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);
     },
   },
 };

+ 414 - 0
src/views/exercise_questions/create/components/exercises/WordCardQuestion.vue

@@ -0,0 +1,414 @@
+<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="输入描述"
+        />
+      </div>
+
+      <div class="content">
+        <label class="title-little">字词:</label>
+        <ul>
+          <li v-for="(item, i) in data.option_list" :key="i" class="content-word-card">
+            <div class="item-left" v-if="item.picture_file_id">
+              <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"
+              />
+              <el-button size="small" class="delete-btn" @click="delectOptions(i, item.picture_file_id)">
+                <i class="el-icon-delete"></i>删除
+              </el-button>
+            </div>
+            <UploadDrag
+              v-else
+              @fileUploadSuccess="fileUploadSuccess"
+              :itemIndex="i"
+              :limit="1"
+              ref="uploadDrag"
+            ></UploadDrag>
+            <div class="word-card-item">
+              <div class="word-card">
+                <el-input v-model="item.content" :placeholder="'输入汉字或词汇'" />
+                <el-input v-model="item.pinyin" :placeholder="'拼音间用空格隔开'" />
+                <UploadAudio
+                  v-if="data.other.audio_generation_method === 'upload'"
+                  :key="item.audio_file_id || i"
+                  :file-id="item.audio_file_id"
+                  :item-index="i"
+                  :show-upload="!item.audio_file_id"
+                  @upload="uploads"
+                  @deleteFile="deleteFiles"
+                />
+                <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>
+                </div>
+                <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
+                <span class="rate-box"><el-rate v-model="item.rate"></el-rate></span>
+                <el-input v-model="item.definition" placeholder="输入释义" type="textarea" :rows="1" />
+              </div>
+              <SvgIcon
+                icon-class="delete"
+                class="delete pointer"
+                @click="deleteOption(i, item.audio_file_id, item.picture_file_id)"
+              />
+            </div>
+
+            <div class="example-sentence-box">
+              <div class="example-sentence" v-for="(items, indexs) in item.example_sentence" :key="indexs">
+                <span class="question-number" @dblclick="changeOptionType(data)" title="双击切换序号类型">
+                  {{ computedQuestionNumber(indexs, data.option_number_show_mode) }}
+                </span>
+                <el-input v-model="item.example_sentence[indexs]" placeholder="输入例句" />
+              </div>
+            </div>
+          </li>
+        </ul>
+      </div>
+      <div class="footer">
+        <span class="add-option" @click="addOption">
+          <SvgIcon icon-class="add-circle" size="14" /> <span>增加字词</span>
+        </span>
+      </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-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>
+</template>
+
+<script>
+import QuestionMixin from '../common/QuestionMixin.js';
+import UploadAudio from '../common/UploadAudio.vue';
+import SoundRecord from '../common/SoundRecord.vue';
+import { GetStaticResources } from '@/api/app';
+import { GetFileStoreInfo } from '@/api/app';
+import { changeOptionType, handleInputNumber } from '@/views/exercise_questions/data/common';
+import UploadDrag from '../common/UploadDrag.vue';
+
+import {
+  wordCardData,
+  learnTypeList,
+  audioGenerationMethodList,
+  getOption,
+} from '@/views/exercise_questions/data/wordCard';
+
+export default {
+  name: 'WordCardQuestion',
+  components: {
+    UploadAudio,
+    SoundRecord,
+    UploadDrag,
+  },
+  mixins: [QuestionMixin],
+  data() {
+    return {
+      learnTypeList,
+      audioGenerationMethodList,
+      data: JSON.parse(JSON.stringify(wordCardData)),
+      changeOptionType,
+      handleInputNumber,
+      pic_list: {},
+      is_first: true,
+    };
+  },
+  watch: {
+    'data.file_id_list': {
+      handler() {
+        if (this.is_first) {
+          this.handleData();
+        }
+      },
+      deep: true,
+    },
+  },
+  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;
+    },
+    addOption() {
+      this.data.option_list.push(getOption());
+    },
+    uploads(file_id, index) {
+      this.data.option_list[index].audio_file_id = file_id;
+      this.data.file_id_list.push(file_id);
+    },
+    deleteFiles(file_id, itemIndex) {
+      this.data.option_list[itemIndex].audio_file_id = '';
+      this.data.file_id_list.splice(this.data.file_id_list.indexOf(file_id), 1);
+    },
+    // 删除小题
+    deleteOption(i, file_id, pic_id) {
+      this.$confirm('是否删除?', '提示', {
+        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);
+      });
+    },
+    // 删除
+    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[i].picture_file_id = '';
+          this.$refs.uploadDrag.clearFiles();
+        })
+        .catch(() => {});
+    },
+    // 自动生成音频
+    handleMatically(item) {
+      if (item.pinyin.trim()) {
+        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);
+          }
+        });
+      }
+    },
+    fileUploadSuccess(file_id, file_url, index) {
+      this.data.file_id_list.push(file_id);
+      this.data.option_list[index].picture_file_id = file_id;
+      this.$set(this.pic_list, file_id, file_url);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.content {
+  display: flex;
+  flex-direction: column;
+
+  :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-word-card {
+    .item-left {
+      display: flex;
+      column-gap: 8px;
+      align-items: flex-end;
+    }
+
+    .word-card-item {
+      display: flex;
+      column-gap: 8px;
+      margin: 16px 0;
+    }
+
+    .word-card {
+      display: flex;
+      flex: 1;
+      flex-wrap: wrap;
+      gap: 8px 4px;
+
+      .el-input {
+        flex: 1;
+      }
+
+      .rate-box {
+        width: 128px;
+        height: 32px;
+        padding: 6px 0;
+        text-align: center;
+        background: #f2f3f5;
+        border-radius: 2px;
+      }
+    }
+
+    .example-sentence-box {
+      .example-sentence {
+        display: flex;
+        column-gap: 8px;
+        margin-bottom: 8px;
+
+        .question-number {
+          min-width: 40px;
+          height: 32px;
+          padding: 4px 0;
+          color: $text-color;
+          text-align: center;
+          cursor: pointer;
+          background-color: $fill-color;
+          border-radius: 2px;
+        }
+      }
+    }
+
+    .upload-wrapper {
+      margin-top: 0;
+    }
+
+    :deep .file-name {
+      width: 205px;
+      overflow: hidden;
+      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;
+      width: 16px;
+      height: 16px;
+    }
+  }
+}
+</style>

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

@@ -86,6 +86,7 @@ import SortPreview from '../preview/SortPreview.vue';
 import ListenSelectPreview from '../preview/ListenSelectPreview.vue';
 import ListenFillPreview from '../preview/ListenFillPreview.vue';
 import ListenJudgePreview from '../preview/ListenJudgePreview.vue';
+import WordCardPreview from '../preview/WordCardPreview.vue';
 
 export default {
   name: 'CreateExercise',
@@ -107,6 +108,7 @@ export default {
     ListenSelectPreview,
     ListenFillPreview,
     ListenJudgePreview,
+    WordCardPreview,
   },
   provide() {
     return {
@@ -146,6 +148,7 @@ export default {
         listen_select: ListenSelectPreview,
         listen_fill: ListenFillPreview,
         listen_judge: ListenJudgePreview,
+        word_card: WordCardPreview,
       },
     };
   },

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

@@ -39,7 +39,7 @@ export const chineseData = {
   type: 'chinese', // 题型
   stem: '', // 题干
   description: '', // 描述
-  option_number_show_mode: optionTypeList[0].value, // 选项类型
+  option_number_show_mode: optionTypeList[1].value, // 选项类型
   answer: { score: 0, score_type: scoreTypeList[0].value }, // 答案
   option_list: [getOption(), getOption(), getOption()], // 选项
   file_id_list: [],

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

@@ -34,6 +34,14 @@ export const questionTypeOption = [
     ],
   },
   {
+    value: 'character',
+    label: '汉字题',
+    children: [
+      { label: '汉字练习', value: 'chinese' },
+      { label: '字词卡片', value: 'word_card' },
+    ],
+  },
+  {
     value: 'read',
     label: '阅读题',
   },
@@ -41,10 +49,6 @@ export const questionTypeOption = [
     value: 'write',
     label: '写作题',
   },
-  {
-    value: 'chinese',
-    label: '汉字题',
-  },
 ];
 
 // 练习名称

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

@@ -0,0 +1,55 @@
+import { stemTypeList, scoreTypeList, questionNumberTypeList, optionTypeList } from './common';
+import { getRandomNumber } from '@/utils/index';
+
+export function getOption(content = '') {
+  return {
+    content,
+    mark: getRandomNumber(),
+    audio_file_id: '',
+    pinyin: '',
+    definition: '',
+    rate: null,
+    example_sentence: ['', ''],
+    picture_file_id: '',
+  };
+}
+
+// 音频生成方式类型
+export const audioGenerationMethodList = [
+  {
+    value: 'upload',
+    label: '上传',
+  },
+  {
+    value: 'auto',
+    label: '自动生成',
+  },
+  {
+    value: 'record',
+    label: '录音',
+  },
+];
+
+// 汉字题数据模板
+export const wordCardData = {
+  type: 'word_card', // 题型
+  stem: '', // 题干
+  description: '', // 描述
+  option_number_show_mode: optionTypeList[1].value, // 选项类型
+  answer: { score: 0, score_type: scoreTypeList[0].value }, // 答案
+  option_list: [getOption()], // 选项
+  file_id_list: [],
+  // 题型属性
+  property: {
+    stem_type: stemTypeList[0].value, // 题干类型
+    question_number: '1', // 题号
+    is_enable_description: 'false', // 描述
+    score: 1, // 分值
+    score_type: scoreTypeList[0].value, // 分值类型
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+    audio_generation_method: audioGenerationMethodList[0].value, // 音频生成方式
+  },
+};

+ 275 - 0
src/views/exercise_questions/preview/WordCardPreview.vue

@@ -0,0 +1,275 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="word-card-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>
+
+    <!-- 笔画学习 -->
+    <div :class="['words-box']">
+      <el-image
+        v-if="pic_list[data.option_list[active_index].picture_file_id]"
+        :src="pic_list[data.option_list[active_index].picture_file_id]"
+        fit="contain"
+      />
+      <div class="words-right">
+        <div :class="['words-item']">
+          <label
+            v-for="(item, index) in data.option_list"
+            :key="index"
+            :class="[active_index === index ? 'active' : '']"
+            @click="active_index = index"
+            >{{ item.content }}</label
+          >
+        </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">
+                <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' : '']"
+                  :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">
+          <p
+            class="definition"
+            v-for="(itemd, indexd) in data.option_list[active_index].definition_preview"
+            :key="indexd"
+          >
+            {{ itemd }}
+          </p>
+          <span
+            class="tips"
+            v-if="
+              data.option_list[active_index].example_sentence[0].trim() ||
+              data.option_list[active_index].example_sentence[1].trim()
+            "
+            >例句:</span
+          >
+          <template v-for="(iteme, indexe) in data.option_list[active_index].example_sentence">
+            <p class="example-sentence" :key="indexe + iteme.trim()" v-if="iteme.trim()">
+              <span>{{ computeOptionMethods[data.option_number_show_mode](indexe) }} </span>
+              <span>{{ iteme }}</span>
+            </p>
+          </template>
+        </div>
+        <el-divider></el-divider>
+        <div class="sound-box">
+          <SoundRecordPreview :wav-blob.sync="answer_list[active_index].audio_file_id" :type="'small'" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { computeOptionMethods } from '@/views/exercise_questions/data/common';
+import PreviewMixin from './components/PreviewMixin';
+import { GetStaticResources, GetFileStoreInfo } from '@/api/app';
+import SoundRecordPreview from './components/common/SoundRecordPreview.vue';
+import Strockplayredline from './components/common/Strockplayredline.vue';
+
+export default {
+  name: 'WordCardPreview',
+  components: { SoundRecordPreview, Strockplayredline },
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      computeOptionMethods,
+      hanzi_color: '#404040', // 描红汉字底色
+      pic_list: {},
+      answer_list: [], // 用户答题数据
+      active_index: 0,
+    };
+  },
+  watch: {},
+  created() {
+    console.log(this.data);
+    this.handleData();
+  },
+  mounted() {},
+  methods: {
+    // 初始化数据
+    handleData() {
+      this.answer_list = [];
+      this.pic_list = {};
+      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,
+          audio_file_id: '',
+        };
+        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;
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.word-card-preview {
+  @include preview;
+
+  .words-box {
+    display: flex;
+    column-gap: 24px;
+
+    .el-image {
+      flex-shrink: 0;
+      width: 346px;
+      height: 346px;
+    }
+
+    .words-right {
+      flex: 1;
+
+      .words-item {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 16px;
+
+        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;
+
+          &.active {
+            background: rgba(48, 110, 255, 100%);
+          }
+        }
+      }
+    }
+
+    .pinyin {
+      font-family: 'League';
+      font-size: 16px;
+      font-weight: 500;
+      color: #fff;
+    }
+
+    .strock-chinese {
+      border: 1px solid #e81b1b;
+    }
+
+    .border-right-none {
+      border-right: none;
+    }
+  }
+
+  .strock-box {
+    display: flex;
+    column-gap: 16px;
+    margin-top: 24px;
+  }
+
+  .strock-left {
+    display: flex;
+  }
+
+  .pinyin-box {
+    display: flex;
+    gap: 4px;
+    align-items: center;
+    width: max-content;
+    padding: 4px 8px;
+    margin-top: 10px;
+    background: rgba(47, 111, 236, 100%);
+    border-radius: 40px;
+
+    :deep .audio-play {
+      width: auto;
+      height: 24px;
+      background: none;
+    }
+  }
+
+  .definition {
+    margin: 0 0 8px;
+    font-family: 'PingFang SC';
+    font-size: 16px;
+    line-height: 24px;
+    color: #000;
+  }
+
+  .tips {
+    display: block;
+    margin-bottom: 8px;
+    font-size: 14px;
+    font-weight: 400;
+    line-height: 22px;
+    color: rgba(0, 0, 0, 40%);
+  }
+
+  .example-sentence {
+    margin: 0;
+    font-size: 14px;
+    line-height: 22px;
+    color: #000;
+  }
+
+  .sound-box {
+    width: max-content;
+    padding: 4px;
+    background: $content-color;
+    border-radius: 40px;
+  }
+
+  .el-divider {
+    margin: 16px 0;
+  }
+}
+</style>