Browse Source

写作 用户录音公共组件

natasha 1 year ago
parent
commit
59c1005733

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 - 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>