Parcourir la source

静态的编辑和预览界面

dusenyao il y a 1 an
Parent
commit
379339abc1

+ 3 - 0
src/icons/svg/first-tone.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 2H12" stroke="#4E5969" stroke-width="2.5" stroke-linecap="round"/>
+</svg>

+ 3 - 0
src/icons/svg/fourth-tone.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 2L6.2948 5.86532C6.69013 6.22111 7.29479 6.20521 7.67087 5.82913L12 1.5" stroke="#4E5969" stroke-width="2.5" stroke-linecap="round"/>
+</svg>

+ 3 - 0
src/icons/svg/neutral-tone.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="6" cy="6" r="4.25" stroke="#4E5969" stroke-width="2.5"/>
+</svg>

+ 3 - 0
src/icons/svg/second-tone.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.75977 6.64941L10.2402 1.35022" stroke="#4E5969" stroke-width="2.5" stroke-linecap="round"/>
+</svg>

+ 3 - 0
src/icons/svg/third-tone.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.75977 1.35059L10.2402 6.64978" stroke="#4E5969" stroke-width="2.5" stroke-linecap="round"/>
+</svg>

+ 2 - 0
src/styles/mixin.scss

@@ -5,6 +5,7 @@
   display: flex;
   flex-direction: column;
   row-gap: 24px;
+  min-height: calc(100% - 32px);
   padding: 64px;
   margin: 16px 0;
   background-color: #fff;
@@ -29,6 +30,7 @@
 
   // 描述
   .description {
+    min-height: 48px;
     padding: 12px 24px;
     background-color: #f9f8f9;
     border-radius: 16px;

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

@@ -49,6 +49,9 @@ import FillQuestion from './exercises/FillQuestion.vue';
 import ReadAloudQuestion from './exercises/ReadAloudQuestion.vue';
 import ChineseQuestion from './exercises/ChineseQuestion.vue';
 import WriteQuestion from './exercises/WriteQuestion.vue';
+import DialogueQuestion from './exercises/DialogueQuestion.vue';
+import TalkPicture from './exercises/TalkPicture.vue';
+import ChooseToneQuestion from './exercises/ChooseToneQuestion.vue';
 
 export default {
   name: 'CreateMain',
@@ -61,6 +64,9 @@ export default {
     ReadAloudQuestion,
     ChineseQuestion,
     WriteQuestion,
+    DialogueQuestion,
+    TalkPicture,
+    ChooseToneQuestion,
   },
   provide() {
     return {
@@ -91,6 +97,9 @@ export default {
         read_aloud: ReadAloudQuestion,
         chinese: ChineseQuestion,
         write: WriteQuestion,
+        dialogue: DialogueQuestion,
+        talk_picture: TalkPicture,
+        choose_tone: ChooseToneQuestion,
       },
     };
   },
@@ -189,6 +198,7 @@ export default {
 <style lang="scss" scoped>
 .create {
   padding: 16px 24px;
+  overflow: hidden;
   background-color: $main-background-color;
 
   &-operate {

+ 189 - 0
src/views/exercise_questions/create/components/exercises/ChooseToneQuestion.vue

@@ -0,0 +1,189 @@
+<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="data.property.is_enable_description"
+          v-model="data.description"
+          rows="3"
+          resize="none"
+          type="textarea"
+          placeholder="输入描述"
+        />
+      </div>
+
+      <div class="content">
+        <label class="subtitle">内容</label>
+        <ul>
+          <li v-for="(item, i) in data.option_list" :key="i" class="content-item">
+            <span class="question-number" @dblclick="changeOptionType(data)">
+              {{ computedQuestionNumber(i, data.option_number_show_mode) }}.
+            </span>
+            <div class="option-content">
+              <RichText v-model="item.content" placeholder="输入内容" :inline="true" />
+            </div>
+            <UploadAudio :file-id="item.audio_file_id" />
+            <span v-for="({ img }, j) in toneList" :key="j" class="tone">
+              <SvgIcon :icon-class="img" />
+            </span>
+            <SvgIcon icon-class="delete" class="delete pointer" @click="deleteOption(i)" />
+          </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 v-model="data.property.score" type="number" />
+        </el-form-item>
+        <el-form-item label="类型">
+          <el-radio
+            v-for="{ value, label } in toneTypeList"
+            :key="value"
+            v-model="data.property.tone_type"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label="音频">
+          <el-radio
+            v-for="{ value, label } in audioGenerationMethodList"
+            :key="value"
+            v-model="data.property.audio_generation_method"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import QuestionMixin from '@/views/exercise_questions/create/components/common/QuestionMixin';
+import UploadAudio from '../common/UploadAudio.vue';
+
+import { changeOptionType } from '@/views/exercise_questions/data/common';
+import {
+  getOption,
+  ChooseToneData,
+  audioGenerationMethodList,
+  toneList,
+  toneTypeList,
+} from '@/views/exercise_questions/data/chooseTone';
+
+export default {
+  name: 'ChooseToneQuestion',
+  components: {
+    UploadAudio,
+  },
+  mixins: [QuestionMixin],
+  data() {
+    return {
+      toneList,
+      changeOptionType,
+      audioGenerationMethodList,
+      toneTypeList,
+      data: JSON.parse(JSON.stringify(ChooseToneData)),
+    };
+  },
+  methods: {
+    addOption() {
+      this.data.option_list.push(getOption());
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.content {
+  display: flex;
+  flex-direction: column;
+
+  .subtitle {
+    margin: 8px 0;
+    font-size: 14px;
+    color: #4e5969;
+  }
+
+  .content-item {
+    .upload-wrapper {
+      margin-top: 0;
+    }
+
+    .tone {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 32px;
+      height: 32px;
+      cursor: pointer;
+      background-color: $fill-color;
+    }
+  }
+}
+</style>
+@/views/exercise_questions/data/chooseTone

+ 143 - 0
src/views/exercise_questions/create/components/exercises/DialogueQuestion.vue

@@ -0,0 +1,143 @@
+<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="data.property.is_enable_description"
+          v-model="data.description"
+          rows="3"
+          resize="none"
+          type="textarea"
+          placeholder="输入描述"
+        />
+      </div>
+
+      <div class="content">
+        <span class="subtitle">输入对话:</span>
+        <el-input v-model="data.dialogue" type="textarea" rows="6" placeholder="输入对话" />
+      </div>
+
+      <div v-if="data.property.is_enable_reference_answer" class="content">
+        <span class="subtitle">参考答案:</span>
+        <el-input 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 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 v-model="data.property.score" type="number" />
+        </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"
+            :key="value"
+            v-model="data.property.is_enable_reference_answer"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import QuestionMixin from '../common/QuestionMixin.js';
+
+import { dialogueData } from '@/views/exercise_questions/data/dialogue';
+
+export default {
+  name: 'DialogueQuestion',
+  mixins: [QuestionMixin],
+  data() {
+    return {
+      data: JSON.parse(JSON.stringify(dialogueData)),
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+.content {
+  display: flex;
+  flex-direction: column;
+  row-gap: 8px;
+
+  .subtitle {
+    font-size: 14px;
+  }
+
+  + .content {
+    padding-top: 8px;
+    margin-top: 8px;
+    border-top: $border;
+  }
+}
+</style>

+ 67 - 2
src/views/exercise_questions/create/components/exercises/ReadAloudQuestion.vue

@@ -11,9 +11,70 @@
       />
 
       <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+
+      <div class="content">
+        <el-input
+          v-if="data.property.is_enable_reference_answer"
+          v-model="data.reference_answer"
+          type="textarea"
+          rows="3"
+          placeholder="输入参考答案"
+        />
+      </div>
     </template>
 
-    <template #property></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_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 v-model="data.property.score" type="number" />
+        </el-form-item>
+      </el-form>
+    </template>
   </QuestionBase>
 </template>
 
@@ -34,4 +95,8 @@ export default {
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.content {
+  margin-top: 8px;
+}
+</style>

+ 164 - 0
src/views/exercise_questions/create/components/exercises/TalkPicture.vue

@@ -0,0 +1,164 @@
+<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="data.property.is_enable_description"
+          v-model="data.description"
+          rows="3"
+          resize="none"
+          type="textarea"
+          placeholder="输入描述"
+        />
+      </div>
+
+      <div class="content">
+        <el-upload ref="upload" action="no" accept="image/*" drag :show-file-list="false">
+          <div>点击或拖拽图片到此上传</div>
+          <div>只有 jpg, png, gif 等格式文件可以上传,文件大小不得超过 5MB</div>
+        </el-upload>
+      </div>
+
+      <div v-if="data.property.is_enable_reference_answer" class="reference-answer">
+        <span class="subtitle">参考答案:</span>
+        <el-input 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 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 v-model="data.property.score" type="number" />
+        </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"
+            :key="value"
+            v-model="data.property.is_enable_reference_answer"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import QuestionMixin from '../common/QuestionMixin.js';
+
+import { talkPictrueData } from '@/views/exercise_questions/data/talkPicture';
+
+export default {
+  name: 'TalkPicture',
+  mixins: [QuestionMixin],
+  data() {
+    return {
+      data: JSON.parse(JSON.stringify(talkPictrueData)),
+    };
+  },
+  methods: {},
+};
+</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;
+      }
+    }
+  }
+}
+
+.reference-answer {
+  display: flex;
+  flex-direction: column;
+  row-gap: 8px;
+  margin-top: 8px;
+
+  .subtitle {
+    font-size: 14px;
+  }
+}
+</style>

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

@@ -31,7 +31,7 @@
       @createNewQuestion="createNewQuestion"
     />
 
-    <div class="preview">
+    <div class="preview" :style="{ minHeight: preview ? '300px' : 'auto' }">
       <div class="preview-header">
         <span class="quick-preview">快捷预览:</span>
         <div class="preview-right">
@@ -64,6 +64,11 @@ import JudgePreview from '@/views/exercise_questions/preview/JudgePreview.vue';
 import MatchingPreview from '@/views/exercise_questions/preview/MatchingPreview.vue';
 import ChinesePreview from '@/views/exercise_questions/preview/ChinesePreview.vue';
 import WritePreview from '../preview/WritePreview.vue';
+import FillPreview from '../preview/FillPreview.vue';
+import ReadAloudPreview from '../preview/ReadAloudPreview.vue';
+import DialoguePreview from '../preview/DialoguePreview.vue';
+import TalkPictruePreview from '../preview/TalkPictruePreview.vue';
+import ChooseTonePreview from '../preview/ChooseTonePreview.vue';
 
 export default {
   name: 'CreateExercise',
@@ -74,6 +79,11 @@ export default {
     MatchingPreview,
     ChinesePreview,
     WritePreview,
+    FillPreview,
+    ReadAloudPreview,
+    DialoguePreview,
+    TalkPictruePreview,
+    ChooseTonePreview,
   },
   provide() {
     return {
@@ -98,6 +108,11 @@ export default {
         matching: MatchingPreview,
         chinese: ChinesePreview,
         write: WritePreview,
+        fill: FillPreview,
+        read_aloud: ReadAloudPreview,
+        dialogue: DialoguePreview,
+        talk_picture: TalkPictruePreview,
+        choose_tone: ChooseTonePreview,
       },
     };
   },
@@ -198,7 +213,7 @@ export default {
     /**
      * 选择练习
      * @param {Number} index 练习索引
-     * @param {Object} data 练习数据
+     * @param {object} data 练习数据
      */
     selectExerciseItem(index, data) {
       if (index < 0 || index > this.index_list.length - 1) return;
@@ -332,7 +347,7 @@ export default {
 
     &-content {
       width: 100%;
-      max-height: calc(100% - 24px);
+      height: calc(100% - 24px);
       overflow: auto;
     }
   }

+ 58 - 0
src/views/exercise_questions/data/ChooseTone.js

@@ -0,0 +1,58 @@
+import { stemTypeList, questionNumberTypeList, scoreTypeList, optionTypeList } from './common';
+import { getRandomNumber } from '@/utils/index';
+
+export function getOption(content = '') {
+  return { content, mark: getRandomNumber(), audio_file_id: '', tone: '' };
+}
+
+export const toneList = [
+  { value: 'first', label: '一声', img: 'first-tone' },
+  { value: 'second', label: '二声', img: 'second-tone' },
+  { value: 'third', label: '三声', img: 'third-tone' },
+  { value: 'fourth', label: '四声', img: 'fourth-tone' },
+  { value: 'neutral', label: '轻声', img: 'neutral-tone' },
+];
+
+export const toneTypeList = [
+  { value: 'select', label: '选择声调' },
+  { value: 'dimension', label: '标注声调' },
+];
+
+export const audioGenerationMethodList = [
+  {
+    value: 'upload',
+    label: '上传',
+  },
+  {
+    value: 'auto',
+    label: '自动生成',
+  },
+  {
+    value: 'record',
+    label: '录音',
+  },
+];
+
+// 选择声调题数据模板
+export const ChooseToneData = {
+  type: 'choose_tone', // 题型
+  stem: '', // 题干
+  description: '', // 描述
+  option_number_show_mode: optionTypeList[0].value, // 选项类型
+  option_list: [getOption(), getOption(), getOption()], // 选项
+  answer: { score: 0, score_type: scoreTypeList[0].value, select_list: '' }, // 答案
+  // 题型属性
+  property: {
+    stem_type: stemTypeList[0].value, // 题干类型
+    question_number: 1, // 题号
+    is_enable_description: false, // 描述
+    tone_type: toneTypeList[0].value, // 音调类型
+    audio_generation_method: audioGenerationMethodList[0].value, // 音频生成方式
+    score: 1, // 分值
+    score_type: scoreTypeList[0].value, // 分值类型
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
+};

+ 13 - 12
src/views/exercise_questions/data/common.js

@@ -9,20 +9,21 @@ export const questionTypeOption = [
       { label: '选择题', value: 'select' },
       { label: '判断题', value: 'judge' },
       { label: '填空题', value: 'fill' },
-      { label: '排序题', value: 'sort' },
+      { label: '排序题', value: 'sort', disabled: true },
       { label: '连线题', value: 'matching' },
-      { label: '问答题', value: 'answer' },
-      { label: '填表题', value: 'table' },
+      { label: '问答题', value: 'answer', disabled: true },
+      { label: '填表题', value: 'table', disabled: true },
+      { label: '选择声调', value: 'choose_tone' },
     ],
   },
   {
     value: 'hear',
     label: '听力题',
     children: [
-      { label: '听后选择', value: 'hear_select' },
-      { label: '听后判断', value: 'hear_judge' },
-      { label: '听后填空', value: 'hear_fill' },
-      { label: '自定义听力题', value: 'hear_custom' },
+      { label: '听后选择', value: 'hear_select', disabled: true },
+      { label: '听后判断', value: 'hear_judge', disabled: true },
+      { label: '听后填空', value: 'hear_fill', disabled: true },
+      { label: '自定义听力题', value: 'hear_custom', disabled: true },
     ],
   },
   {
@@ -30,11 +31,11 @@ export const questionTypeOption = [
     label: '口语题',
     children: [
       { label: '朗读题', value: 'read_aloud' },
-      { label: '听后复述', value: 'repeat' },
-      { label: '回答问题', value: 'spoken_answer' },
-      { label: '看图说话', value: 'spoken_picture' },
-      { label: '对话练习', value: 'spoken_dialogue' },
-      { label: '自定义口语', value: 'spoken_custom' },
+      { label: '听后复述', value: 'repeat', disabled: true },
+      { label: '回答问题', value: 'spoken_answer', disabled: true },
+      { label: '看图说话', value: 'talk_picture' },
+      { label: '对话题', value: 'dialogue' },
+      { label: '自定义口语', value: 'spoken_custom', disabled: true },
     ],
   },
   {

+ 25 - 0
src/views/exercise_questions/data/dialogue.js

@@ -0,0 +1,25 @@
+import { stemTypeList, questionNumberTypeList, scoreTypeList } from './common';
+
+// 对话题数据模板
+export const dialogueData = {
+  type: 'dialogue', // 题型
+  stem: '', // 题干
+  reference_answer: '', // 参考答案
+  description: '', // 描述
+  dialogue: '', // 对话内容
+  answer: { score: 0, score_type: scoreTypeList[0].value }, // 答案
+  // 题型属性
+  property: {
+    stem_type: stemTypeList[0].value, // 题干类型
+    question_number: 1, // 题号
+    score: 1, // 分值
+    is_enable_description: false, // 描述
+    is_enable_voice_answer: true, // 语音作答
+    is_enable_reference_answer: true, // 参考答案
+    score_type: scoreTypeList[0].value, // 分值类型
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
+};

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

@@ -1,8 +1,10 @@
 import { stemTypeList, questionNumberTypeList, scoreTypeList } from './common';
 
+// 朗读题数据模板
 export const readAloudData = {
-  type: 'fill', // 题型
+  type: 'read_aloud', // 题型
   stem: '', // 题干
+  reference_answer: '', // 参考答案
   answer: { score: 0, score_type: scoreTypeList[0].value, reference_answer: '' }, // 答案
   // 题型属性
   property: {

+ 25 - 0
src/views/exercise_questions/data/talkPicture.js

@@ -0,0 +1,25 @@
+import { stemTypeList, scoreTypeList, questionNumberTypeList } from './common';
+
+// 看图说话数据模板
+export const talkPictrueData = {
+  type: 'talk_pictrue', // 题型
+  stem: '', // 题干
+  reference_answer: '', // 参考答案
+  description: '', // 描述
+  option_list: '', // 选项
+  answer: { score: 0, score_type: scoreTypeList[0].value }, // 答案
+  // 题型属性
+  property: {
+    stem_type: stemTypeList[0].value, // 题干类型
+    question_number: 1, // 题号
+    score: 1, // 分值
+    is_enable_description: false, // 描述
+    is_enable_voice_answer: true, // 语音作答
+    is_enable_reference_answer: true, // 参考答案
+    score_type: scoreTypeList[0].value, // 分值类型
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
+};

+ 33 - 0
src/views/exercise_questions/preview/ChooseTonePreview.vue

@@ -0,0 +1,33 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="choosetone-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+    <div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
+
+    <div class="option-list"></div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+
+export default {
+  name: 'ChooseTonePreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.choosetone-preview {
+  @include preview;
+}
+</style>

+ 36 - 0
src/views/exercise_questions/preview/DialoguePreview.vue

@@ -0,0 +1,36 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="dialogue-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+    <div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
+    <div class="dialogue">{{ data.dialogue }}</div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+
+export default {
+  name: 'DialoguePreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.dialogue-preview {
+  @include preview;
+
+  .dialogue {
+    white-space: pre-line;
+  }
+}
+</style>

+ 36 - 0
src/views/exercise_questions/preview/FillPreview.vue

@@ -0,0 +1,36 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="fill-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+    <div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
+    <AudioPlay
+      v-if="data.property.is_enable_listening && data.file_id_list.length > 0"
+      :file-id="data.file_id_list[0]"
+    />
+    <div>{{ data.model_essay }}</div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+
+export default {
+  name: 'FillPreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.fill-preview {
+  @include preview;
+}
+</style>

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

@@ -0,0 +1,31 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="readaloud-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+    <div>{{ data.reference_answer }}</div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+
+export default {
+  name: 'ReadAloudPreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.readaloud-preview {
+  @include preview;
+}
+</style>

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

@@ -0,0 +1,31 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="talkpictrue-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="sanitizeHTML(data.stem)"></span>
+    </div>
+    <div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+
+export default {
+  name: 'TalkPictruePreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.talkpictrue-preview {
+  @include preview;
+}
+</style>