|
@@ -0,0 +1,334 @@
|
|
|
+<template>
|
|
|
+ <el-dialog
|
|
|
+ class="audio-record-dialog"
|
|
|
+ :visible.sync="dialogVisible"
|
|
|
+ width="390px"
|
|
|
+ title="语音提醒"
|
|
|
+ :close-on-press-escape="false"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ append-to-body
|
|
|
+ @open="initData"
|
|
|
+ >
|
|
|
+ <div class="record-time" v-if="recordStep !== 2">
|
|
|
+ <p>{{ recordTimeStr }}</p>
|
|
|
+ <p>(最长2分钟)</p>
|
|
|
+ </div>
|
|
|
+ <div class="record-audio-play" v-else>
|
|
|
+ <div class="audio-player">
|
|
|
+ <div
|
|
|
+ :class="['audio-player-play', { 'player-playing': isPlaying }]"
|
|
|
+ @click="play"
|
|
|
+ ></div>
|
|
|
+ <div class="audio-player-progress">
|
|
|
+ <el-progress
|
|
|
+ :percentage="audioPlayProgress"
|
|
|
+ :show-text="false"
|
|
|
+ :stroke-width="8"
|
|
|
+ status="success"
|
|
|
+ ></el-progress>
|
|
|
+ <p class="audio-player-tips">点击试听录音</p>
|
|
|
+ </div>
|
|
|
+ <div class="audio-player-time">{{ audioCurPlayTime }}</div>
|
|
|
+ </div>
|
|
|
+ <audio
|
|
|
+ :src="audioUrl"
|
|
|
+ class="audio-audio"
|
|
|
+ ref="AudioAudio"
|
|
|
+ @timeupdate="audioCurrentTimeChange"
|
|
|
+ @ended="audioEnded"
|
|
|
+ ></audio>
|
|
|
+ </div>
|
|
|
+ <div class="record-footer" slot="footer">
|
|
|
+ <el-button type="primary" v-if="recordStep === 0" @click="start"
|
|
|
+ >开始录音</el-button
|
|
|
+ >
|
|
|
+ <el-button type="primary" v-if="recordStep === 1" @click="finish"
|
|
|
+ >完成录音</el-button
|
|
|
+ >
|
|
|
+
|
|
|
+ <el-button v-if="recordStep === 2" @click="restart">重新录音</el-button>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ v-if="recordStep === 2"
|
|
|
+ @click="confirm"
|
|
|
+ :disabled="isSubmit"
|
|
|
+ >确认录音</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { sendAudioWarningMsg } from "@/api/invigilation";
|
|
|
+import AudioRecord from "./audioRecord";
|
|
|
+
|
|
|
+function recordTimeToText(timeNumber) {
|
|
|
+ const MINUTE_TIME = 60 * 1000;
|
|
|
+ const SECOND_TIME = 1000;
|
|
|
+ let [minute, second] = [0, 0, 0, 0];
|
|
|
+ let residueTime = timeNumber;
|
|
|
+
|
|
|
+ if (residueTime >= MINUTE_TIME) {
|
|
|
+ minute = Math.floor(residueTime / MINUTE_TIME);
|
|
|
+ residueTime -= minute * MINUTE_TIME;
|
|
|
+ }
|
|
|
+ if (residueTime >= SECOND_TIME) {
|
|
|
+ second = Math.round(residueTime / SECOND_TIME);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [minute, second]
|
|
|
+ .map((item) => ("00" + item).substr(("" + item).length))
|
|
|
+ .join(":");
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "audio-record-dialog",
|
|
|
+ props: {
|
|
|
+ recordId: {
|
|
|
+ type: String,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ dialogVisible: false,
|
|
|
+ isSubmit: false,
|
|
|
+ recordStep: 0, // 0:未开始,1:开始录音,2:录音结束
|
|
|
+ recordReady: false,
|
|
|
+ recordLimitTime: 2 * 60 * 1000,
|
|
|
+ recordStartTime: 0,
|
|
|
+ recordEndTime: 0,
|
|
|
+ recordTime: 0,
|
|
|
+ recordTimeStr: "00:00",
|
|
|
+ audioUrl: "",
|
|
|
+ isPlaying: false,
|
|
|
+ audioPlayProgress: 0,
|
|
|
+ audioCurPlayTime: "00:00",
|
|
|
+ audioRecord: null,
|
|
|
+ audioBlob: null,
|
|
|
+ setT: null,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ initData() {
|
|
|
+ this.isSubmit = false;
|
|
|
+ this.recordStep = 0;
|
|
|
+ this.recordStartTime = 0;
|
|
|
+ this.recordEndTime = 0;
|
|
|
+ this.recordTime = 0;
|
|
|
+ this.recordTimeStr = "00:00";
|
|
|
+ this.audioUrl = "";
|
|
|
+ this.audioPlayProgress = 0;
|
|
|
+ this.audioCurPlayTime = "00:00";
|
|
|
+ this.audioBlob = null;
|
|
|
+ this.setT = null;
|
|
|
+ },
|
|
|
+ cancel() {
|
|
|
+ this.dialogVisible = false;
|
|
|
+ },
|
|
|
+ open() {
|
|
|
+ this.dialogVisible = true;
|
|
|
+ },
|
|
|
+ // audio play
|
|
|
+ play() {
|
|
|
+ if (this.$refs.AudioAudio.ended) {
|
|
|
+ this.$refs.AudioAudio.currentTime = 0;
|
|
|
+ this.$refs.AudioAudio.play();
|
|
|
+ this.isPlaying = true;
|
|
|
+ this.audioPlayProgress = 0;
|
|
|
+ this.audioCurPlayTime = "00:00";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.$refs.AudioAudio.paused) {
|
|
|
+ this.$refs.AudioAudio.play();
|
|
|
+ this.isPlaying = true;
|
|
|
+ } else {
|
|
|
+ this.$refs.AudioAudio.pause();
|
|
|
+ this.isPlaying = false;
|
|
|
+ }
|
|
|
+ console.dir(this.$refs.AudioAudio);
|
|
|
+ },
|
|
|
+ audioCurrentTimeChange() {
|
|
|
+ const { duration, currentTime } = this.$refs.AudioAudio;
|
|
|
+ const audioPlayProgress = (100 * currentTime) / duration;
|
|
|
+ this.audioPlayProgress = audioPlayProgress
|
|
|
+ ? audioPlayProgress > 100
|
|
|
+ ? 100
|
|
|
+ : audioPlayProgress
|
|
|
+ : 0;
|
|
|
+ this.audioCurPlayTime = recordTimeToText(currentTime * 1000);
|
|
|
+ },
|
|
|
+ audioEnded() {
|
|
|
+ this.isPlaying = false;
|
|
|
+ },
|
|
|
+ // audio record
|
|
|
+ updateRecordTime() {
|
|
|
+ this.recordTime = Date.now() - this.recordStartTime;
|
|
|
+ this.recordTimeStr = recordTimeToText(this.recordTime);
|
|
|
+ },
|
|
|
+ start() {
|
|
|
+ this.audioRecord = new AudioRecord({
|
|
|
+ onaudioprocess: this.updateRecordTime,
|
|
|
+ });
|
|
|
+ this.audioRecord.init((err) => {
|
|
|
+ if (err) {
|
|
|
+ this.recordReady = false;
|
|
|
+ this.$message.error({ message: err.msg, duration: 3000 });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.recordStartTime = Date.now();
|
|
|
+ this.audioRecord.start();
|
|
|
+ this.recordStep = 1;
|
|
|
+ this.setT = setTimeout(() => {
|
|
|
+ this.finish();
|
|
|
+ }, this.recordLimitTime);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ finish() {
|
|
|
+ if (this.setT) clearTimeout(this.setT);
|
|
|
+
|
|
|
+ this.audioBlob = this.audioRecord.getAudioBlob();
|
|
|
+ console.log(this.audioBlob);
|
|
|
+ this.audioUrl = window.URL.createObjectURL(this.audioBlob);
|
|
|
+ this.updateRecordTime();
|
|
|
+ this.recordStep = 2;
|
|
|
+ this.audioRecord.recover();
|
|
|
+ console.log(this.recordTime);
|
|
|
+ console.log(this.recordTimeStr);
|
|
|
+ },
|
|
|
+ restart() {
|
|
|
+ this.initData();
|
|
|
+ this.start();
|
|
|
+ },
|
|
|
+ async confirm() {
|
|
|
+ if (this.isSubmit) return;
|
|
|
+ this.isSubmit = true;
|
|
|
+
|
|
|
+ const fdata = new FormData();
|
|
|
+ fdata.append("content", this.audioBlob);
|
|
|
+ fdata.append("type", "audio");
|
|
|
+ fdata.append("examRecordId", this.recordId);
|
|
|
+ const result = await sendAudioWarningMsg(fdata).catch(() => {});
|
|
|
+
|
|
|
+ this.isSubmit = false;
|
|
|
+ if (!result) return;
|
|
|
+ this.$emit("modified");
|
|
|
+ this.cancel();
|
|
|
+ },
|
|
|
+ },
|
|
|
+ beforeDestroy() {},
|
|
|
+};
|
|
|
+
|
|
|
+// 参考文件:
|
|
|
+// https://blog.csdn.net/qq_42986378/article/details/81872679
|
|
|
+// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
|
|
+// https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext
|
|
|
+// https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext/createMediaStreamSource
|
|
|
+// https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext/createScriptProcessor
|
|
|
+// https://www.cnblogs.com/hustskyking/p/javascript-array.html
|
|
|
+// https://www.barretlee.com/blog/2014/02/20/cb-webAudio-listen/
|
|
|
+// https://www.barretlee.com/blog/2014/02/22/cb-webAudio-show-audio/
|
|
|
+
|
|
|
+// method
|
|
|
+// navigator.mediaDevices.getUserMedia => stream
|
|
|
+// new AudioContext() => context
|
|
|
+// context.createMediaStreamSource(stream) => audioInput
|
|
|
+// context.createScriptProcessor(4096, 1, 1) => recorder
|
|
|
+
|
|
|
+// start => () => {
|
|
|
+// audioInput.connect(recorder);
|
|
|
+// recorder.connect(context.destination);
|
|
|
+// }
|
|
|
+
|
|
|
+// recording
|
|
|
+// recorder.onaudioprocess => () => {audioData.push(e.inputBuffer.getChannelData(0))}
|
|
|
+
|
|
|
+// stop => () => {
|
|
|
+// recorder.disconnect()
|
|
|
+// }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+.record-time {
|
|
|
+ padding: 30px;
|
|
|
+ text-align: center;
|
|
|
+ > p:first-child {
|
|
|
+ font-size: 40px;
|
|
|
+ line-height: 1.1;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #202b4b;
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+ > p:last-child {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #8c94ac;
|
|
|
+ line-height: 20px;
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+.record-audio-play {
|
|
|
+ padding: 30px 0;
|
|
|
+
|
|
|
+ .audio-audio {
|
|
|
+ visibility: hidden;
|
|
|
+ }
|
|
|
+}
|
|
|
+.audio-player {
|
|
|
+ height: 64px;
|
|
|
+ padding: 16px 25px;
|
|
|
+ background: #f0f4f9;
|
|
|
+ border-radius: 10px;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &-play {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ float: left;
|
|
|
+ background-image: url("../../../../assets/icon-play.png");
|
|
|
+ background-size: 100% 100%;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &.player-playing {
|
|
|
+ background-image: url("../../../../assets/icon-play-act.png");
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ background-image: url("../../../../assets/icon-play-act.png");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &-progress {
|
|
|
+ margin-left: 40px;
|
|
|
+ margin-right: 40px;
|
|
|
+ padding-top: 12px;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+ &-tips {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 24px;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #8c94ac;
|
|
|
+ line-height: 14px;
|
|
|
+ }
|
|
|
+ &-time {
|
|
|
+ position: absolute;
|
|
|
+ right: 25px;
|
|
|
+ top: 23px;
|
|
|
+ height: 18px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #202b4b;
|
|
|
+ line-height: 18px;
|
|
|
+ }
|
|
|
+ .el-progress-bar__outer {
|
|
|
+ background: #dbe1e7;
|
|
|
+ }
|
|
|
+ .el-progress.is-success .el-progress-bar__inner {
|
|
|
+ background: #1cd1a1;
|
|
|
+ }
|
|
|
+}
|
|
|
+.record-footer {
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+</style>
|