123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- <template>
- <view class="sound-record-wrapper">
- <template v-if="type === 'big'">
- <view class="progress-box" v-if="file_url&&wavBlob">
- <text> {{ currentTime }} </text>
- <slider v-model="sliderValue" activeColor="#000000" block-size="12" @change="onSliderChange" />
- <text> {{ duration }} </text>
- </view>
- <view class="sound-item-box">
- <view class="sound-item">
- <view class="sound-item-svg" @click="handlePlay">
- <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
- <use :xlink:href="playing ? '#icon-stop' : '#icon-triangle'"></use>
- </svg>
- </view>
- <text class="sound-item-text" :style="{'color':wavBlob?'#1D1D1D':'#8C8C8C'}">回放</text>
- </view>
- <view v-if="viewPopup" class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
- <view class="sound-item-svg sound-item-microphone" @click="handleStart">
- <svg fill="#ffffff" :style="{'width':iconSize,'height':iconSize}">
- <use xlink:href="#icon-microphone"></use>
- </svg>
- </view>
- <text class="sound-item-text">录音</text>
- </view>
- <view v-else class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
- <view v-if="microphoneStatus" class="sound-item-img" @click="microphone">
- <img class="voice-play" src="static/record-ing-hasBg.png" />
- </view>
- <view v-else class="sound-item-svg sound-item-microphone" @click="microphone">
- <svg fill="#ffffff" :style="{'width':iconSize,'height':iconSize}">
- <use xlink:href="#icon-microphone"></use>
- </svg>
- </view>
- <text class="sound-item-text">
- {{ microphoneStatus ? secondFormatConversion(recordTimes) : '录音' }}
- </text>
- </view>
- <view class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
- <view class="sound-item-svg" :style="{'background-color':type === 'big'?'#f9f8f9':'#ffffff'}"
- @click="deleteWav">
- <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
- <use xlink:href="#icon-deletefile"></use>
- </svg>
- </view>
- <text v-if="type === 'big'" class="sound-item-text">删除</text>
- </view>
- </view>
- </template>
- <template v-else>
- <view class="sound-item-box">
- <view class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
- <view class="sound-item-svg sound-item-microphone" @click="handleStart">
- <svg fill="#ffffff" :style="{'width':iconSize,'height':iconSize}">
- <use xlink:href="#icon-microphone"></use>
- </svg>
- </view>
- </view>
- <view class="sound-item">
- <view class="progress-box" v-if="file_url&&wavBlob">
- <text> {{ currentTime }} </text>
- <slider v-model="sliderValue" activeColor="#000000" block-size="12" @change="onSliderChange" />
- <text> {{ duration }} </text>
- </view>
- <text class="sound-item-text" v-else>
- {{ secondFormatConversion(recordTimes) }}
- </text>
- </view>
- <view class="sound-item">
- <view class="sound-item-svg" style="background-color:#ffffff" @click="handlePlay">
- <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
- <use :xlink:href="playing ? '#icon-stop' : '#icon-triangle'"></use>
- </svg>
- </view>
- </view>
- <view class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
- <view class="sound-item-svg" :style="{'background-color':type === 'big'?'#f9f8f9':'#ffffff'}"
- @click="deleteWav">
- <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
- <use xlink:href="#icon-deletefile"></use>
- </svg>
- </view>
- </view>
- </view>
- </template>
- <uni-popup v-show="viewPopup" ref="popup" :mask-click="false">
- <view class="canvas-area">
- <text class="canvas-title">录音中...
- <text>{{ secondFormatConversion(recordTimes) }}</text>
- </text>
- <canvas canvas-id="recordCanvas" id="recordCanvas" type="2d" ref="record"></canvas>
- <view class="sound-item-svg" @click="handleStop">
- <svg fill="#ffffff">
- <use xlink:href="#icon-audio-stop"></use>
- </svg>
- </view>
- </view>
- </uni-popup>
- </view>
- </template>
- <script>
- import Recorder from 'js-audio-recorder'; // 录音插件
- import {
- GetStaticResources,
- GetFileStoreInfo
- } from '@/api/api.js';
- import {
- secondFormatConversion
- } from '@/utils/transform';
- export default {
- name: 'sound-record',
- props: {
- wavBlob: {
- type: String,
- default: '',
- },
- itemIndex: {
- type: Number,
- default: null,
- },
- type: {
- type: String,
- default: 'big',
- },
- notOnTheCurPage: {
- type: Boolean,
- default: false,
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- viewPopup: {
- type: Boolean,
- default: true,
- }
- },
- data() {
- return {
- secondFormatConversion,
- microphoneStatus: false, //是否在录音
- recorder: new Recorder({
- sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
- sampleRate: 16000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值
- numChannels: 1, // 声道,支持 1 或 2, 默认是1
- }),
- timer: null, // 计时器
- recordTimes: 0,
- playtime: 0, //播放时间
- drawRecordId: null,
- audio: uni.createInnerAudioContext(),
- playing: false,
- file_url: '',
- duration: '00:00', //音频时长
- currentTime: '00:00', //当前时长
- sliderValue: 0,
- };
- },
- computed: {
- iconSize() {
- return this.type === 'big' ? 24 : 16;
- },
- },
- watch: {
- wavBlob: {
- handler(val) {
- if (!val) return;
- this.sliderValue = 0;
- GetFileStoreInfo({
- file_id: val
- }).then(({
- file_url,
- media_duration
- }) => {
- this.file_url = file_url;
- this.recordTime = media_duration;
- this.recordTimes = JSON.parse(JSON.parse(media_duration));
- this.audio.src = file_url;
- this.audio.onCanplay(() => {
- if (this.audio.duration !== 0) {
- clearInterval(this.intervalID);
- this.duration = this.secondFormatConversion(this.audio.duration) // 总时长
- this.currentTime = this.secondFormatConversion(this.audio.currentTime) // 当前时长
- }
- })
- this.audio.onTimeUpdate(() => {
- this.currentTime = this.secondFormatConversion(this.audio.currentTime);
- if (!this.dragging) { // 避免拖动滑块时自动更新
- this.sliderValue = (this.audio.currentTime / this.audio.duration) * 100;
- }
- })
- });
- },
- immediate: true,
- },
- },
- methods: {
- //不弹窗录音
- microphone() {
- if (this.disabled) return;
- if (this.microphoneStatus) {
- this.recorder.stop();
- this.sliderValue = 0;
- clearInterval(this.timer); //清除计时器
- this.microphoneStatus = false;
- // 录音结束,获取录音数据
- let wav = this.recorder.getWAVBlob(); // 获取 WAV 数据
- let reader = new window.FileReader();
- reader.readAsDataURL(wav);
- reader.onloadend = () => {
- let MethodName = 'file_store_manager-SaveFileByteBase64Text';
- let data = {
- base64_text: reader.result.replace('data:audio/wav;base64,', ''),
- file_suffix_name: 'mp3',
- };
- GetStaticResources(MethodName, data).then((res) => {
- if (res.status === 1) {
- this.$emit('update:wavBlob', res.file_id);
- this.$emit('setAudioId', res.file_id);
- this.audio.src = res.file_url;
- // console.log('文件', res);
- }
- });
- };
- } else {
- this.$emit('update:wavBlob', '');
- //获取录音权限
- Recorder.getPermission().then(() => {
- // 开始录音
- this.recorder.start().then(() => {
- this.microphoneStatus = true;
- this.sliderValue = 0;
- this.recordTimes = 0;
- clearInterval(this.timer);
- this.timer = setInterval(() => {
- this.recordTimes += 1;
- // console.log(this.recordTimes);
- }, 1000);
- // console.log('开始录音');
- })
- }, (error) => {
- console.log(`${error.name} : ${error.message}`)
- })
- }
- },
- // 开始录音
- handleStart() {
- if (this.disabled) return;
- //获取录音权限
- Recorder.getPermission().then(() => {
- this.recorder.start().then(() => {
- this.microphoneStatus = true;
- this.sliderValue = 0;
- //打开录音弹窗
- if (this.notOnTheCurPage) {
- uni.$emit('openOrClosePopup', true);
- } else {
- this.$refs.popup.open('center');
- }
- this.drawRecord();
- this.recordTimes = 0;
- clearInterval(this.timer);
- this.timer = setInterval(() => {
- this.recordTimes += 1;
- }, 1000);
- // console.log('开始录音');
- })
- }, (error) => {
- console.log(`${error.name} : ${error.message}`)
- })
- },
- // 停止录音
- handleStop() {
- this.recorder.stop();
- this.sliderValue = 0;
- //取消录音动画
- this.drawRecordId && cancelAnimationFrame(this.drawRecordId);
- this.drawRecordId = null;
- clearInterval(this.timer); //清除计时器
- this.microphoneStatus = false;
- //关闭录音弹窗
- if (!this.notOnTheCurPage) {
- this.$refs.popup.close();
- }
- // 录音结束,获取录音数据
- let wav = this.recorder.getWAVBlob(); // 获取 WAV 数据
- let reader = new window.FileReader();
- reader.readAsDataURL(wav);
- reader.onloadend = () => {
- let MethodName = 'file_store_manager-SaveFileByteBase64Text';
- let data = {
- base64_text: reader.result.replace('data:audio/wav;base64,', ''),
- file_suffix_name: 'mp3',
- };
- GetStaticResources(MethodName, data).then((res) => {
- if (res.status === 1) {
- this.$emit('update:wavBlob', res.file_id);
- this.$emit('setAudioId', res.file_id);
- this.audio.src = res.file_url;
- // console.log('文件', res);
- }
- });
- };
- },
- // 播放录音
- handlePlay() {
- if (this.wavBlob) {
- let totalTime = JSON.parse(JSON.stringify(this.recordTime));
- if (this.playing) {
- this.audio.pause();
- clearInterval(this.timer);
- } else {
- this.audio.play();
- clearInterval(this.timer);
- // 启动定时器,每秒更新一次进度条位置
- this.timer = setInterval(() => {
- if (this.audio.currentTime > 0 && this.audio.currentTime < this.audio.duration) {
- this.sliderValue = (this.audio.currentTime / this.audio.duration) * 100;
- } else {
- this.playing = !this.playing;
- clearInterval(this.timer);
- }
- }, 1000);
- }
- this.playing = !this.playing;
- }
- },
- // 删除录音
- deleteWav() {
- if (this.disabled) return;
- this.audio.pause();
- this.playing = false;
- this.microphoneStatus = false;
- this.playtime = 0;
- this.recordTimes = 0;
- this.file_url = '';
- clearInterval(this.timer);
- this.$emit('update:wavBlob', '');
- this.$emit('setAudioId', '');
- },
- // 录音波浪图
- drawRecord() {
- // 用requestAnimationFrame稳定60fps绘制
- this.drawRecordId = requestAnimationFrame(this.drawRecord);
- this.drawWave({
- dataArray: this.recorder.getRecordAnalyseData(),
- });
- },
- // 绘制波形图
- drawWave({
- dataArray
- }) {
- const ctx = uni.createCanvasContext('recordCanvas');
- const bufferLength = dataArray.length;
- ctx.rect(0, 0, 200, 64); //绘制矩形
- ctx.setFillStyle("#000000"); // 填充背景色
- // 设定波形,绘制颜色
- ctx.setLineWidth(2);
- ctx.setStrokeStyle("#ffffff");
- ctx.beginPath();
- var sliceWidth = (200 * 1.0) / bufferLength; // 一个点占多少位置,共有bufferLength个点要绘制
- var x = 0; // 绘制点的x轴位置
- for (var i = 0; i < bufferLength; i++) {
- var v = dataArray[i] / 128.0;
- var y = (v * 64) / 2;
- if (i === 0) {
- // 第一个点
- ctx.moveTo(x, y);
- } else {
- // 剩余的点
- ctx.lineTo(x, y);
- }
- // 依次平移,绘制所有点
- x += sliceWidth;
- }
- ctx.lineTo(200, 64 / 2);
- ctx.stroke();
- ctx.draw();
- },
- onSliderChange(event) {
- const value = event.detail.value;
- if (this.audio && this.audio.duration > 0) {
- // 验证 value 是否是有效的百分比值(0-100之间)
- if (isNaN(value) || value < 0 || value > 100) {
- // console.error("Invalid value:", value);
- return;
- }
- // 计算新的位置
- const newPosition = (value / 100) * this.audio.duration;
- // 验证 newPosition 是否是有效的数字,不是 NaN,并且在有效范围内
- if (!isNaN(newPosition) && newPosition >= 0 && newPosition <= this.audio.duration) {
- // 设置音频播放位置
- this.audio.seek(newPosition);
- if (!this.dragging) { // 避免拖动滑块时自动更新
- this.sliderValue = value;
- }
- } else {
- console.error("Invalid newPosition:", newPosition);
- }
- }
- },
- }
- };
- </script>
- <style lang="scss" scoped>
- .sound-record-wrapper {
- display: flex;
- flex-direction: column;
- margin-top: 32rpx;
- .progress-box {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background-color: $uni-bg-color-grey;
- padding: 8rpx 16rpx;
- border-radius: 80rpx;
- font-size: 24rpx;
- column-gap: 26rpx;
- margin-bottom: 32rpx;
- slider {
- flex: 1;
- margin: 0;
- }
- }
- .sound-item-box {
- display: flex;
- align-items: center;
- justify-content: space-between;
- column-gap: 8rpx;
- .sound-item {
- text-align: center;
- .progress-box {
- margin: 0;
- slider {
- width: 100rpx;
- flex: 1;
- margin: 0;
- }
- }
- .sound-item-svg {
- padding: 20rpx;
- border-radius: 160rpx;
- background-color: $uni-bg-color-grey;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .sound-item-microphone {
- background-color: #7346d3;
- }
- .sound-item-img {
- display: flex;
- .voice-play {
- width: 84rpx;
- height: 84rpx;
- }
- }
- .sound-item-text {
- font-size: 24rpx;
- line-height: 40rpx;
- }
- }
- }
- .canvas-area {
- width: 308rpx;
- height: 312rpx;
- text-align: center;
- background-color: rgba(0, 0, 0, 0.8);
- border-radius: 32rpx;
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- padding: 48rpx 32rpx;
- z-index: 111;
- .canvas-title {
- color: #ffffff;
- font-size: 28rpx;
- }
- canvas {
- width: 300rpx;
- height: 92rpx;
- margin: 46rpx 0;
- }
- .sound-item-svg {
- width: 96rpx;
- height: 96rpx;
- border-radius: 160rpx;
- background-color: #7346d3;
- display: flex;
- justify-content: center;
- align-items: center;
- margin: 0 auto;
- svg {
- width: 44rpx;
- height: 44rpx;
- }
- }
- }
- }
- </style>
|