Quellcode durchsuchen

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

dusenyao vor 1 Jahr
Ursprung
Commit
cbf6f61d28

BIN
src/assets/record-ing-hasBg.png


+ 3 - 0
src/icons/svg/pause.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75098 6C8.75098 5.58579 8.41519 5.25 8.00098 5.25C7.58676 5.25 7.25098 5.58579 7.25098 6V18C7.25098 18.4142 7.58676 18.75 8.00098 18.75C8.41519 18.75 8.75098 18.4142 8.75098 18V6ZM16.749 6C16.749 5.58579 16.4132 5.25 15.999 5.25C15.5848 5.25 15.249 5.58579 15.249 6V18C15.249 18.4142 15.5848 18.75 15.999 18.75C16.4132 18.75 16.749 18.4142 16.749 18V6Z" fill="currentColor"/>
+</svg>

+ 5 - 0
src/icons/svg/play-fill.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="play-fill">
+<path id="Vector" d="M19.376 12.4153L8.77735 19.4811C8.54759 19.6343 8.23715 19.5722 8.08397 19.3424C8.02922 19.2603 8 19.1638 8 19.0651V4.93359C8 4.65745 8.22386 4.43359 8.5 4.43359C8.59871 4.43359 8.69522 4.46281 8.77735 4.51757L19.376 11.5833C19.6057 11.7365 19.6678 12.0469 19.5146 12.2767C19.478 12.3316 19.4309 12.3787 19.376 12.4153Z" fill="currentColor"/>
+</g>
+</svg>

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

@@ -14,7 +14,7 @@
         <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-show="data.property.is_enable_description === 'true'"
           v-model="data.description"
           rows="3"
           resize="none"

+ 4 - 14
src/views/exercise_questions/create/components/exercises/WriteQuestion.vue

@@ -14,7 +14,7 @@
         <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-show="data.property.is_enable_description === 'true'"
           v-model="data.description"
           rows="3"
           resize="none"
@@ -26,10 +26,10 @@
       <div class="content">
         <label class="title-little">文章:</label>
         <RichText v-model="data.article" placeholder="输入文章" />
-        <template v-if="data.property.is_enable_sample_text">
+        <template v-if="data.property.is_enable_sample_text === 'true'">
           <el-divider class="write-divider" />
           <label class="title-little">范文:</label>
-          <RichText v-model="data.sample_text" placeholder="输入范文" :word-limit="5000" />
+          <RichText v-model="data.sample_text" placeholder="输入范文" :wordlimit-num="5000" />
           <p class="tips">多篇范文之间使用分割线(---)</p>
         </template>
       </div>
@@ -110,17 +110,7 @@
           <el-radio
             v-for="{ value, label } in switchOption"
             :key="value"
-            v-model="data.property.is_enable_upload"
-            :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_model_essay"
+            v-model="data.property.is_enable_upload_accessory"
             :label="value"
           >
             {{ label }}

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

@@ -53,7 +53,7 @@ export const ChooseToneData = {
   property: {
     stem_type: stemTypeList[0].value, // 题干类型
     question_number: 1, // 题号
-    is_enable_description: false, // 描述
+    is_enable_description: 'false', // 描述
     answer_mode: toneTypeList[0].value, // 音调类型
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型

+ 2 - 2
src/views/exercise_questions/data/common.js

@@ -122,8 +122,8 @@ export const selectTypeList = [
 
 // 开关选项
 export const switchOption = [
-  { value: true, label: '开启' },
-  { value: false, label: '关闭' },
+  { value: 'true', label: '开启' },
+  { value: 'false', label: '关闭' },
 ];
 
 // 题号类型

+ 4 - 5
src/views/exercise_questions/data/write.js

@@ -12,14 +12,13 @@ export const writeData = {
   property: {
     stem_type: stemTypeList[0].value, // 题干类型
     question_number: 1, // 题号
-    is_enable_description: false, // 描述
+    is_enable_description: 'false', // 描述
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型
     word_num: 50, // 词数
-    is_enable_sample_text: true, // 范文开启
-    is_enable_voice_answer: true, // 语音作答
-    is_enable_upload: true, // 上传附件
-    is_enable_model_essay: true, // 参考范文
+    is_enable_sample_text: 'true', // 范文开启
+    is_enable_voice_answer: 'true', // 语音作答
+    is_enable_upload_accessory: 'true', // 上传附件
   },
   // 其他属性
   other: {

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

@@ -5,7 +5,7 @@
       <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 v-if="data.property.is_enable_description === 'true'" class="description">{{ data.description }}</div>
 
     <div class="option-list">
       <li v-for="(item, i) in data.option_list" :key="i" :class="['option-item']">
@@ -43,9 +43,11 @@
           :class="[
             'tone',
             data.property.answer_mode === 'select' &&
+            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
               ? '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
@@ -130,12 +132,13 @@ export default {
     handleData() {
       this.con_preview = [];
       this.data.option_list.forEach((item) => {
-        let con_arr = item.content_view;
+        let con_arr = JSON.parse(JSON.stringify(item.content_view));
         let user_answer = [];
         con_arr.forEach(() => {
           user_answer.push({
             select_tone: null,
             select_letter: '',
+            select_index: '',
           });
         });
         let obj = {
@@ -245,6 +248,7 @@ export default {
     },
     handleSelectItemTone(i, indexc, indexi, itemi) {
       this.con_preview[i].item_active_index = indexc;
+      this.con_preview[i].user_answer[indexc].select_index = indexi;
       this.active_index_str = `${i}-${indexc}-${indexi}`;
       this.active_letter = itemi;
       this.active_letter_index = indexi;

+ 59 - 5
src/views/exercise_questions/preview/WritePreview.vue

@@ -5,9 +5,30 @@
       <span class="question-number">{{ data.property.question_number }}.</span>
       <span v-html="sanitizeHTML(data.stem)"></span>
     </div>
-    <div v-html="sanitizeHTML(data.article)"></div>
-    <div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
-    <template v-if="data.property.is_enable_model_essay">
+    <div class="article-content" v-html="sanitizeHTML(data.article)"></div>
+    <div v-if="data.property.is_enable_description === 'true'" class="description">{{ data.description }}</div>
+    <el-input
+      v-model="user_answer.text"
+      rows="3"
+      type="textarea"
+      placeholder="请输入内容"
+      :maxlength="data.property.word_num"
+      show-word-limit
+    />
+    <template v-if="data.property.is_enable_voice_answer === 'true'">
+      <!-- 语音作答 -->
+      <SoundRecordPreview
+        :wav-blob="user_answer.voice_file_id"
+        :record-time="user_answer.audio_wav_time"
+        @deleteWav="deleteWav"
+        @updataWav="updataWav"
+      />
+    </template>
+    <template v-if="data.property.is_enable_upload_accessory === 'true'">
+      <!-- 上传附件 -->
+      <UploadFiles />
+    </template>
+    <template v-if="data.property.is_enable_sample_text === 'true'">
       <el-divider content-position="center"
         ><span
           :class="['sample-text', show_sample_text ? 'sample-show' : 'sample-hide']"
@@ -15,26 +36,49 @@
           >{{ show_sample_text ? '隐藏范文' : '查看范文' }}</span
         ></el-divider
       >
-      <div v-if="show_sample_text" v-html="sanitizeHTML(data.sample_text)"></div>
+      <div v-if="show_sample_text" class="article-content" v-html="sanitizeHTML(data.sample_text)"></div>
     </template>
   </div>
 </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],
   data() {
     return {
       show_sample_text: false,
+      user_answer: {
+        text: '', // 用户文章
+        voice_file_id: '', // 录音内容
+        audio_wav_time: 0, // 录音时间
+        accessory_file_id: [], // 上传文件列表
+      },
     };
   },
   created() {
     console.log(this.data);
   },
-  methods: {},
+  methods: {
+    // 清除录音
+    deleteWav() {
+      this.user_answer.voice_file_id = '';
+      this.user_answer.audio_wav_time = 0;
+    },
+    // 更新录音内容和时间
+    updataWav(wav, time) {
+      this.user_answer.voice_file_id = wav;
+      this.user_answer.audio_wav_time = time;
+    },
+  },
 };
 </script>
 
@@ -59,5 +103,15 @@ export default {
       color: #306eff;
     }
   }
+
+  .article-content {
+    :deep p {
+      margin: 0;
+    }
+  }
+
+  :deep .el-textarea .el-input__count {
+    background-color: #f2f3f5;
+  }
 }
 </style>

+ 228 - 0
src/views/exercise_questions/preview/components/common/SoundRecordPreview.vue

@@ -0,0 +1,228 @@
+<template>
+  <div class="sound-record-wrapper">
+    <div class="sound-item sound-item-luyin">
+      <img
+        v-if="microphoneStatus"
+        :src="require('../../../../../assets/record-ing-hasBg.png')"
+        class="voice-play"
+        @click="microphone"
+      />
+      <span v-else class="sound-item-span" @click="microphone">
+        <SvgIcon icon-class="mic-line" :size="24" class="record" />
+      </span>
+
+      <label :class="['record-time', microphoneStatus ? 'record-ing' : '']">{{ handleDateTime(recordTimes) }}</label>
+    </div>
+    <div class="sound-item">
+      <span :class="['sound-item-span', wavBlob ? '' : 'not-url']" @click="playMicrophone">
+        <SvgIcon :icon-class="iconClass" :size="24" :class="['audio-play-btn']" />
+      </span>
+      <label class="tips">回放</label>
+    </div>
+    <div class="sound-item">
+      <span :class="['sound-item-span', wavBlob ? '' : 'not-url']" @click="delectWav">
+        <SvgIcon icon-class="delete-back-line" :size="24" :class="['delete-btn']" />
+      </span>
+      <label class="tips">删除</label>
+    </div>
+
+    <audio ref="audio" :src="wavBlob" preload="metadata"></audio>
+  </div>
+</template>
+
+<script>
+import Recorder from 'js-audio-recorder'; // 录音插件
+export default {
+  name: 'SoundRecordPreview',
+  props: {
+    wavBlob: {
+      type: String,
+      default: '',
+    },
+    recordTime: {
+      type: Number,
+      default: 0,
+    },
+  },
+  data() {
+    return {
+      recorder: new Recorder({
+        sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
+        sampleRate: 16000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000
+        numChannels: 1, // 声道,支持 1 或 2, 默认是1
+      }),
+      timer: null, // 计时器
+      microphoneStatus: false, // 是否录音
+      hasMicro: '', // 录音后的样式class
+      audio: {
+        paused: true,
+      },
+      playtime: 0, // 播放时间
+      recordTimes: JSON.parse(JSON.stringify(this.recordTime)),
+    };
+  },
+  computed: {
+    iconClass() {
+      return this.audio.paused ? 'play-fill' : 'pause';
+    },
+  },
+  watch: {},
+  mounted() {
+    this.$refs.audio.addEventListener('ended', () => {
+      this.audio.paused = true;
+    });
+    this.$refs.audio.addEventListener('pause', () => {
+      this.audio.paused = true;
+    });
+    this.$refs.audio.addEventListener('play', () => {
+      this.audio.paused = false;
+    });
+  },
+  methods: {
+    playMicrophone() {
+      if (this.wavBlob) {
+        let totalTime = JSON.parse(JSON.stringify(this.recordTime));
+        if (this.audio.paused) {
+          this.hasMicro = 'active';
+          // this.$refs.audio.pause();
+          // this.$refs.audio.load();
+          this.$refs.audio.play();
+          if (this.recordTimes === 0) {
+            this.recordTimes = JSON.parse(JSON.stringify(this.recordTime));
+            this.playtime = 0;
+          }
+          clearInterval(this.timer);
+          this.timer = setInterval(() => {
+            if (this.playtime < totalTime) {
+              this.playtime += 1;
+              this.recordTimes = totalTime - this.playtime;
+            } else {
+              this.playtime = 0;
+              this.recordTimes = JSON.parse(JSON.stringify(this.recordTime));
+              clearInterval(this.timer);
+            }
+          }, 1000);
+        } else {
+          this.$refs.audio.pause();
+          this.hasMicro = 'normal';
+          clearInterval(this.timer);
+        }
+      }
+    },
+    // 格式化录音时长
+    handleDateTime(time) {
+      let times = 0;
+      if (parseInt(time / 60) < 10) {
+        times = `${`0${parseInt(time / 60)}`.substring(`0${parseInt(time / 60)}`.length - 2)}:${`0${
+          time % 60
+        }`.substring(`0${time % 60}`.length - 2)}`;
+      } else {
+        times = `${parseInt(time / 60)}:${`0${time % 60}`.substring(`0${time % 60}`.length - 2)}`;
+      }
+      return times;
+    },
+    // 开始录音
+    microphone() {
+      if (!this.audio.paused) {
+        this.$refs.audio.pause();
+        this.audio.paused = true;
+      }
+      if (this.microphoneStatus) {
+        this.hasMicro = 'normal';
+        this.recorder.stop();
+        clearInterval(this.timer);
+        let tolTime = this.recorder.duration; // 录音总时长
+        // 录音结束,获取取录音数据
+        let wav = this.recorder.getWAVBlob(); // 获取 WAV 数据
+        this.microphoneStatus = false;
+        let reader = new window.FileReader();
+        reader.readAsDataURL(wav);
+        reader.onloadend = () => {
+          this.$emit('updataWav', reader.result, Math.floor(tolTime));
+        };
+      } else {
+        this.hasMicro = '';
+        this.$emit('updataWav', '', 0);
+        // 开始录音
+        this.recorder.start();
+        this.microphoneStatus = true;
+        this.recordTimes = 0;
+        clearInterval(this.timer);
+        this.timer = setInterval(() => {
+          this.recordTimes += 1;
+        }, 1000);
+      }
+    },
+    // 删除录音
+    delectWav() {
+      this.$refs.audio.pause();
+      this.hasMicro = '';
+      this.microphoneStatus = false;
+      this.playtime = 0;
+      this.recordTimes = 0;
+      clearInterval(this.timer);
+      this.$emit('deleteWav');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.sound-record-wrapper {
+  display: flex;
+  align-items: center;
+
+  .sound-item {
+    margin-right: 16px;
+    text-align: center;
+
+    .sound-item-span {
+      display: block;
+      width: 48px;
+      height: 48px;
+      padding: 12px;
+      font-size: 0;
+      cursor: pointer;
+      background: #ededed;
+      border-radius: 80px;
+
+      &.not-url {
+        color: #a1a1a1;
+        cursor: not-allowed;
+      }
+    }
+
+    &-luyin {
+      .sound-item-span {
+        color: #fff;
+        background-color: #7346d3;
+      }
+
+      .voice-play {
+        display: block;
+        width: 48px;
+        height: 48px;
+        cursor: pointer;
+      }
+    }
+
+    .tips {
+      display: block;
+      margin-top: 4px;
+      font-size: 12px;
+      font-weight: 400;
+      line-height: 20px;
+      color: rgba($color: #000, $alpha: 50%);
+    }
+  }
+
+  .record-time {
+    display: block;
+    margin-top: 4px;
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 20px;
+    color: #000;
+  }
+}
+</style>

+ 125 - 0
src/views/exercise_questions/preview/components/common/UploadFiles.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="upload-wrapper">
+    <el-upload
+      ref="upload"
+      :limit="1"
+      action="no"
+      accept="audio/*"
+      :show-file-list="false"
+      :before-upload="beforeUpload"
+      :http-request="upload"
+    >
+      <div class="upload-audio">
+        <SvgIcon icon-class="upload" />
+        <span>上传音频</span>
+      </div>
+    </el-upload>
+    <div v-show="file_url.length > 0" class="file-wrapper">
+      <div class="file-name">{{ file_name }}</div>
+      <SvgIcon icon-class="delete" class-name="delete pointer" @click="deleteFile" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { fileUpload, GetFileStoreInfo } from '@/api/app';
+
+export default {
+  name: 'UploadFiles',
+  props: {
+    fileId: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      file_id: '',
+      file_url: '',
+      file_name: '',
+    };
+  },
+  watch: {
+    fileId: {
+      handler(val) {
+        if (!val) return;
+        GetFileStoreInfo({ file_id: val }).then(({ file_id, file_url, file_name }) => {
+          this.file_id = file_id;
+          this.file_url = file_url;
+          this.file_name = file_name;
+        });
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    beforeUpload(file) {
+      if (this.file_id.length > 0) {
+        this.$message.warning('只能上传一个音频文件');
+        return false;
+      }
+      const fileName = file.name;
+      const suffix = fileName.slice(fileName.lastIndexOf('.') + 1, fileName.length).toLowerCase();
+      if (!['mp3', 'wav', 'aac', 'm4a'].includes(suffix)) {
+        this.$message.warning('音频格式不正确');
+        return false;
+      }
+    },
+    upload(file) {
+      fileUpload('Mid', file).then(({ file_info_list }) => {
+        if (file_info_list.length > 0) {
+          const { file_id, file_name, file_url } = file_info_list[0];
+          this.file_id = file_id;
+          this.file_url = file_url;
+          this.file_name = file_name;
+          this.$emit('upload', file_id);
+        }
+      });
+    },
+    deleteFile() {
+      this.$confirm('是否删除当前音频文件?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          this.$emit('deleteFile', this.file_id);
+          this.file_id = '';
+          this.file_url = '';
+          this.file_name = '';
+          this.$refs.upload.clearFiles();
+        })
+        .catch(() => {});
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.upload-wrapper {
+  display: flex;
+  column-gap: 12px;
+  align-items: center;
+  margin-top: 8px;
+
+  .upload-audio {
+    display: flex;
+    column-gap: 12px;
+    align-items: center;
+    width: 233px;
+    padding: 4px 12px;
+    background-color: $fill-color;
+  }
+
+  .file-wrapper {
+    display: flex;
+    column-gap: 12px;
+    align-items: center;
+
+    .file-name {
+      padding: 4px 12px;
+      background-color: $fill-color;
+    }
+  }
+}
+</style>