浏览代码

写作 用户录音公共组件

natasha 1 年之前
父节点
当前提交
59c1005733

二进制
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 - 11
src/views/exercise_questions/create/components/exercises/WriteQuestion.vue

@@ -29,7 +29,7 @@
         <template v-if="data.property.is_enable_sample_text">
           <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>
@@ -116,16 +116,6 @@
             {{ label }}
           </el-radio>
         </el-form-item>
-        <el-form-item label="参考范文">
-          <el-radio
-            v-for="{ value, label } in switchOption"
-            :key="value"
-            v-model="data.property.is_enable_model_essay"
-            :label="value"
-          >
-            {{ label }}
-          </el-radio>
-        </el-form-item>
       </el-form>
     </template>
   </QuestionBase>

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

@@ -19,7 +19,6 @@ export const writeData = {
     is_enable_sample_text: true, // 范文开启
     is_enable_voice_answer: true, // 语音作答
     is_enable_upload: true, // 上传附件
-    is_enable_model_essay: true, // 参考范文
   },
   // 其他属性
   other: {

+ 47 - 2
src/views/exercise_questions/preview/WritePreview.vue

@@ -7,7 +7,27 @@
     </div>
     <div class="article-content" 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">
+    <el-input
+      v-model="user_answer.article"
+      rows="3"
+      type="textarea"
+      placeholder="请输入内容"
+      :maxlength="data.property.word_num"
+      show-word-limit
+    />
+    <template v-if="data.property.is_enable_voice_answer">
+      <!-- 语音作答 -->
+      <SoundRecordPreview
+        :wav-blob="user_answer.audio_wav"
+        :record-time="user_answer.audio_wav_time"
+        @deleteWav="deleteWav"
+        @updataWav="updataWav"
+      />
+    </template>
+    <template v-if="data.property.is_enable_upload">
+      <!-- 上传附件 -->
+    </template>
+    <template v-if="data.property.is_enable_sample_text">
       <el-divider content-position="center"
         ><span
           :class="['sample-text', show_sample_text ? 'sample-show' : 'sample-hide']"
@@ -21,20 +41,41 @@
 </template>
 
 <script>
+import SoundRecordPreview from './components/common/SoundRecordPreview.vue';
 import PreviewMixin from './components/PreviewMixin';
 
 export default {
   name: 'WritePreview',
+  components: {
+    SoundRecordPreview,
+  },
   mixins: [PreviewMixin],
   data() {
     return {
       show_sample_text: false,
+      user_answer: {
+        article: '', // 用户文章
+        audio_wav: '', // 录音内容
+        audio_wav_time: 0, // 录音时间
+        file_list: [], // 上传文件列表
+      },
     };
   },
   created() {
     console.log(this.data);
   },
-  methods: {},
+  methods: {
+    // 清除录音
+    deleteWav() {
+      this.user_answer.audio_wav = '';
+      this.user_answer.audio_wav_time = 0;
+    },
+    // 更新录音内容和时间
+    updataWav(wav, time) {
+      this.user_answer.audio_wav = wav;
+      this.user_answer.audio_wav_time = time;
+    },
+  },
 };
 </script>
 
@@ -65,5 +106,9 @@ export default {
       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>