浏览代码

字词卡片

natasha 1 年之前
父节点
当前提交
5bc7cfdd68

+ 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,
       },
     };
   },

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

@@ -0,0 +1,308 @@
+<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">
+            <UploadDrag @fileUploadSuccess="fileUploadSuccess" :limit="1" ref="uploadDrag"></UploadDrag>
+            <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" />
+                <span class="auto-btn" @click="handleMatically(item)">自动生成</span>
+              </div>
+              <SoundRecord v-else :wav-blob.sync="item.audio_file_id" />
+              <span><el-rate v-model="item.rate"></el-rate></span>
+              <el-input v-model="item.definition" placeholder="输入释义" type="textarea" />
+            </div>
+
+            <SvgIcon icon-class="delete" class="delete pointer" @click="deleteOption(i, item.audio_file_id)" />
+            <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 { 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: {},
+    };
+  },
+  methods: {
+    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) {
+      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.pinyin.trim()) {
+        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);
+          }
+        });
+      }
+    },
+    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 {
+  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 {
+    .word-card {
+      display: flex;
+      flex-wrap: wrap;
+      column-gap: 4px;
+
+      .el-input {
+        width: 255px;
+      }
+    }
+
+    .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

@@ -75,6 +75,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',
@@ -96,6 +97,7 @@ export default {
     ListenSelectPreview,
     ListenFillPreview,
     ListenJudgePreview,
+    WordCardPreview,
   },
   provide() {
     return {
@@ -133,6 +135,7 @@ export default {
         listen_select: ListenSelectPreview,
         listen_fill: ListenFillPreview,
         listen_judge: ListenJudgePreview,
+        word_card: WordCardPreview,
       },
     };
   },

+ 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, // 音频生成方式
+  },
+};

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

@@ -0,0 +1,252 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="chinese-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', 'words-box-' + data.property.learn_type]">
+      <div v-for="(item, index) in data.option_list" :key="index" :class="['words-item']"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { computeOptionMethods } from '@/views/exercise_questions/data/common';
+import PreviewMixin from './components/PreviewMixin';
+import { GetStaticResources } from '@/api/app';
+
+export default {
+  name: 'WordCardPreview',
+  components: {},
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      computeOptionMethods,
+      hanzi_color: '#404040', // 描红汉字底色
+      writer_number_yuan: 19,
+      writer_number: null, // 书写个数
+      answer_list: {
+        write_model: {},
+      }, // 用户答题数据
+    };
+  },
+  watch: {},
+  created() {
+    console.log(this.data);
+    this.handleData();
+  },
+  mounted() {},
+  methods: {
+    // 初始化数据
+    handleData() {
+      this.data.option_list.forEach((item, index) => {});
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.chinese-preview {
+  @include preview;
+
+  .words-box {
+    .words-item {
+      // display: flex;
+      // flex-wrap: wrap;
+      min-width: 64px;
+      margin-bottom: 24px;
+    }
+
+    .words-top {
+      display: flex;
+      width: 100%;
+      min-height: 30px;
+      border: 1px solid #e81b1b;
+
+      .words-left {
+        box-sizing: border-box;
+        display: flex;
+        column-gap: 4px;
+        align-items: center;
+        justify-content: center;
+        width: 64px;
+        margin-right: 12px;
+        border-right: 1px solid #e81b1b;
+      }
+
+      .words-right {
+        padding: 8px 4px;
+        margin: 0;
+        font-size: 14px;
+        line-height: 14px;
+        color: #000;
+      }
+    }
+
+    .audio-wrapper {
+      height: 16px;
+
+      :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;
+      }
+    }
+
+    .words-info {
+      display: flex;
+      column-gap: 4px;
+      align-items: center;
+      justify-content: center;
+      width: max-content;
+      padding: 5px 16px;
+      margin: 0 auto 8px;
+      font-size: 14px;
+      line-height: 22px;
+      color: #fff;
+      background: #165dff;
+      border-radius: 20px;
+
+      .pinyin {
+        font-size: 14px;
+        color: #fff;
+      }
+
+      .audio-wrapper {
+        :deep .audio-play {
+          color: #fff;
+        }
+
+        :deep .audio-play.not-url {
+          color: #a1a1a1;
+        }
+      }
+    }
+
+    .card-box {
+      display: flex;
+      flex-wrap: wrap;
+    }
+
+    .pinyin {
+      font-family: 'League';
+      font-size: 12px;
+      font-weight: 500;
+      color: #000;
+    }
+
+    .strock-chinese {
+      border: 1px solid #e81b1b;
+      border-top: none;
+    }
+
+    .strockplay-newWord {
+      position: relative;
+      box-sizing: border-box;
+      flex-shrink: 0;
+      width: 64px;
+      height: 64px;
+      border: 1px solid #e81b1b;
+      border-top: none;
+
+      .character-target-bg,
+      .hanzi-writer-img {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        color: #dedede;
+      }
+
+      .hanzi-writer-img {
+        z-index: 1;
+      }
+    }
+
+    .border-left-none {
+      border-left: none;
+    }
+
+    .border-right-none {
+      border-right: none;
+    }
+
+    &-learn {
+      display: flex;
+      column-gap: 24px;
+
+      .words-item {
+        display: block;
+      }
+    }
+  }
+
+  .words-box-dictation {
+    display: flex;
+    flex-wrap: wrap;
+    column-gap: 24px;
+
+    .card-box {
+      display: block;
+    }
+
+    .words-dic-box {
+      display: flex;
+      column-gap: 6px;
+      width: max-content;
+      margin: 0 auto;
+    }
+
+    .words-dic-item {
+      text-align: center;
+
+      .pinyin {
+        line-height: 30px;
+      }
+    }
+  }
+
+  .words-dic-item {
+    .strockplay-newWord {
+      border-top: 1px solid #e81b1b;
+    }
+  }
+
+  .practiceBox {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 101;
+    box-sizing: border-box;
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    overflow-y: auto;
+    background: rgba(0, 0, 0, 19%);
+
+    &.practice-box-strock {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding-top: 0;
+    }
+  }
+}
+</style>