阿里-语音转文字开发联调,学习记录

/* eslint-disable */
<template>
  <div class="container">
    <div
      v-if="showCtrlProcessWaveStatus"
      style="height:100px;width:100%;box-sizing: border-box;display:inline-block;vertical-align:bottom"
      class="ctrlProcessWave"
    />
    <!-- 放一个 <audio ></audio> 播放器,标签名字大写,阻止uniapp里面乱编译 -->
    <AUDIO ref="LogAudioPlayer" :class="isPlay?'visible':'hidden'" style="width:100%" />
    <el-input
      v-model="text"
      :class="isShowBase64?'visible':'hidden'"
      type="textarea"
      placeholder="语音Base64内容"
      rows="10"
      show-word-limit
      readonly
    />
    <el-input
      v-model="identifyTexts"
      :class="isShowIdentifyText?'visible':'hidden'"
      type="textarea"
      rows="10"
      show-word-limit
    />
    <div class="buttonContainer">
      <el-button :class="isOpen?'hidden':'visible'" type="success" size="small" @click="openVoice">
        开始录音
      </el-button>
      <el-button :class="isOpen?'visible':'hidden'" type="primary" size="small" @click="closeVoice">
        {{ isLoading?'编译中...': `结束录音` }}
      </el-button>
      <el-button type="primary" size="small" @click="sureVoice">确定</el-button>
    </div>
  </div>
</template>

<script>
/* eslint-disable */
import * as api from '@/api/system/sluggish-sales-manage'
import Recorder from 'recorder-core'
import 'recorder-core/src/engine/mp3.js'
import 'recorder-core/src/engine/mp3-engine.js'
// import 'recorder-core/recorder.mp3.min.js'
import 'recorder-core/src/extensions/waveview'
import CryptoJS from 'crypto-js'

const APPID = 'xxxxxxxx'
const API_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
const API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

export default {
  props: {
    openStatus: {
      type: Boolean,
      default: false
    },
    playStatus: {
      type: Boolean,
      default: false
    },
    showCtrlProcessWaveStatus: {
      type: Boolean,
      default: false
    },
    showBase64Status: {
      type: Boolean,
      default: false
    },
    downStatus: {
      type: Boolean,
      default: false
    },
    identifyStatus: {
      type: Boolean,
      default: false
    },
    showIdentifyTextStatus: {
      type: Boolean,
      default: false
    },
    skc: {
      type: String,
      default: ''
    },
    skuInfoObj: {
      type: Object,
      default: {}
    }
  },
  data() {
    return {
      isOpen: this.openStatus,//是否正在录音
      isPlay: this.playStatus,//是否正在播放音频
      isShowCtrlProcessWave: this.showCtrlProcessWaveStatus,
      isShowBase64: this.showBase64Status,
      isDown: this.downStatus,
      isIdentify: this.identifyStatus,
      isShowIdentifyText: this.showIdentifyTextStatus,

      audio: null, // 音频
      text: null, // 存放转换后的文本内容(用于存储 Base64 编码的音频数据)
      identifyTexts: '', // 存放转换后的文本内容(用于存储 文件流 编码的音频数据)

      audios: [],
      base64s: [],
      type: 'mp3',
      bitRate: 16,
      sampleRate: 16000,

      duration: 0,
      durationTxt: '0',
      powerLevel: 0,

      logs: [],

      sendInterval: 0, // 发送间隔时长(毫秒),mp3 chunk数据会缓冲,当pcm的累积时长达到这个时长,就会传输发送。这个值在takeoffEncodeChunk实现下,使用0也不会有性能上的影响。
      realTimeSendTryTime: 0,
      realTimeSendTryNumber: null,
      realTimeSendTryBytesChunks: null,
      realTimeSendTryClearPrevBufferIdx: null,
      realTimeSendTryBuffers: null,
      transferUploadNumberMax: null,
      recLogLast: {},
      pcmData: [], // 用于保存 PCM 数据
      isLoading:false,

    }
  },
  mounted() {
    if (this.$props.openStatus) {
      this.openVoice()
    }
  },
  watch: {
    skuInfoObj: {
      handler(newVal, oldVal) {
        if (newVal) {
          console.log('newVal',newVal)
          this.identifyTexts=newVal.skcRemark
        }
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    sureVoice() {
      let t = this
      let req = {
        skcNo: t.skc,
        remark:t.identifyTexts
      }
      api.saveRemark(req).then(res => {
        this.$emit('closeRemarkDialog')
        return t.$message.success('保存成功')
      })
    },
    openVoice: function() {
      var This = this
      This.pcmData = []
      This.isLoading = false
      var rec = this.rec = Recorder({//初始化 Recorder 实例
        type: This.type,//mp3格式
        bitRate: +This.bitRate,//指定采样率hz
        sampleRate: +This.sampleRate,//比特率kbps
        //接收到录音数据时的回调函数
        onProcess: function (buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd) {
          //buffers=[[Int16,...],...]:缓冲的PCM数据块(16位小端LE),为从开始录音到现在的所有pcm片段,每次回调可能增加0-n个不定量的pcm片段
          //powerLevel:当前缓冲的音量级别0-100
          //duration:已缓冲时长
          //sampleRate(Worker)时,此采样率和设置的采样率相同,否则不一定相同)
          //newBufferIdx:本次回调新增的buffer起始索引
          //asyncEnd:fn() 如果onProcess是异步的(返回值为true时),处理完成时需要调用此回调,如果不是异步的请忽略此参数,此方法回调时必须是真异步(不能真异步时需用setTimeout包裹)
          This.realTimeOnProcessClear(buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd)// 实时数据处理,清理内存

          This.duration = duration
          This.durationTxt = This.formatMs(duration, 1)
          This.powerLevel = powerLevel
          // 将 PCM 数据添加到 pcmData 数组中
          for (var i = 0; i < buffers.length; i++) {
            if (buffers[i]) {
              This.pcmData.push(...buffers[i]);
            }
          }
          //录音的时候,绘制波形语音段
          if (This.isShowCtrlProcessWave) This.wave.input(buffers[buffers.length - 1], powerLevel, sampleRate)
        },

        //实时编码环境下接管编码器输出,当编码器实时编码出一块有效的二进制音频数据时实时回调此方法;参数为二进制的Uint8Array
        //当提供此回调方法时,将接管编码器的数据输出,编码器内部将放弃存储生成的音频数据;如果当前编码器或环境不支持实时编码处理,将在open时直接走fail逻辑
        //因此提供此回调后调用stop方法将无法获得有效的音频数据,因为编码器内没有音频数据,因此stop时返回的blob将是一个字节长度为0的blob
        //大部分录音格式编码器都支持实时编码(边录边转码),比如mp3格式:会实时的将编码出来的mp3片段通过此方法回调,所有的chunkBytes拼接到一起即为完整的mp3,此种拼接的结果比mock方法实时生成的音质更加,因为天然避免了首尾的静默
        //不支持实时编码的录音格式不可以提供此回调(wav格式不支持,因为wav文件头中需要提供文件最终长度),提供了将在open时直接走fail逻辑


        //用于处理编码的音频数据
        takeoffEncodeChunk: function (chunkBytes) {
          //chunkBytes:就是编码出来的音频数据片段,所有的chunkBytes拼接在一起即为完整音频

          //当你不断说话的时候,就会一直执行下方的逻辑,类似于setTimeOut

          // 大于等于60秒自动关闭
          if (This.duration >= 5 *60000) {
            This.closeVoice()
          } else {
            // 接管实时转码,推入实时处理(将编码的音频数据发送到 WebSocket 服务器进行实时语音识别)
            This.realTimeSendTry(chunkBytes, false)
          }
        }
      })
      rec.open(function() {
        This.reclog('已打开:' + This.type + ' ' + This.sampleRate + 'hz ' + This.bitRate + 'kbps', 2)
        This.recStart()
        This.isOpen = true

        if (This.isShowCtrlProcessWave) This.wave = Recorder.WaveView({ elem: '.ctrlProcessWave' })

        if (This.$refs && This.$refs.LogAudioPlayer) {
          var audio = This.$refs.LogAudioPlayer
          audio.pause()
          audio = null
          This.audio = audio
        }
        This.text = null
        This.identifyText = ''
        This.base64s = []
      }, function (msg, isUserNotAllow) {
        if (isUserNotAllow) {
          // 用户没有给予录音权限,或者没有可用的录音设备
          // 在这里添加你的提醒代码
          This.$message.error("无法录音:请检查你的麦克风设备是否正常,或者是否已经给予录音权限");
        } else {
          // 其他原因导致的打开录音设备失败
          This.reclog('打开失败:' + msg, 1);
          This.$message.error('打开失败:' + msg);
        }
      })
    },
    recStart: function() {
      if (!this.rec || !Recorder.IsOpen()) {
        this.reclog('未打开录音', 1)
        this.isOpen = false
        return
      }
      this.rec.start()
      this.realTimeSendTryReset()

      var set = this.rec.set
      this.reclog('录制中:' + set.type + ' ' + set.sampleRate + 'hz ' + set.bitRate + 'kbps')
    },
    closeVoice: function() {
      this.realTimeSendTry(null, true)
      this.rec.close()
    },
    recLast: function() {
      if (!this.recLogLast) {
        this.reclog('请先录音,然后停止后再播放', 1)
        return
      }
      if (this.isPlay) this.recplay(this.recLogLast.idx)
      if (this.isShowBase64) this.recdown64(this.recLogLast.idx)
      if (this.isDown) this.recdown(this.recLogLast.idx)
    },
    //播放音频
    recplay: function(idx) {
      var This = this
      var o = this.logs[this.logs.length - idx - 1]
      o.play = (o.play || 0) + 1
      var logmsg = function(msg) {
        o.playMsg = '<span style="color:green">' + o.play + '</span> ' + This.getTime() + ' ' + msg
      }
      logmsg('')

      var audio = this.$refs.LogAudioPlayer
      audio.controls = true
      if (!(audio.ended || audio.paused)) {
        audio.pause()
      }
      audio.onerror = function(e) {
        logmsg('<span style="color:red">播放失败[' + audio.error.code + ']' + audio.error.message + '</span>')
      }
      audio.src = (window.URL || window.webkitURL).createObjectURL(o.res.blob)
      audio.play()
      This.audio = audio
    },
    //下载 Base64 编码的音频数据
    recdown64: function(idx) {
      var This = this
      var o = this.logs[this.logs.length - idx - 1]
      var reader = new FileReader()
      reader.readAsDataURL(o.res.blob)
      reader.onloadend = function() {
        var base64 = (/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1]
        This.text = base64
      }
    },
    //下载音频文件
    recdown: function(idx) {
      var This = this
      var o = this.logs[this.logs.length - idx - 1]
      o.down = (o.down || 0) + 1

      o = o.res
      var name = 'rec-' + o.duration + 'ms-' + (o.rec.set.bitRate || '-') + 'kbps-' + (o.rec.set.sampleRate || '-') + 'hz.' + (o.rec.set.type || (/\w+$/.exec(o.blob.type) || [])[0] || 'unknown')
      var downA = document.createElement('A')
      downA.href = (window.URL || window.webkitURL).createObjectURL(o.blob)
      downA.download = name
      downA.click()
    },
    //毫秒数转换为时分秒,当 all为true时,会显示所有的时间单位,否则只显示非零的时间单位
    formatMs: function(ms, all) {
      var ss = ms % 1000
      ms = (ms - ss) / 1000
      var s = ms % 60
      ms = (ms - s) / 60
      var m = ms % 60
      ms = (ms - m) / 60
      var h = ms
      var t = (h ? h + ':' : '') +
                    (all || h + m ? ('0' + m).substr(-2) + ':' : '') +
                    (all || h + m + s ? ('0' + s).substr(-2) + '″' : '') +
                    ('00' + ss).substr(-3)
      return t
    },
    //获取当前时间,格式:HH:MM:SS
    getTime: function() {
      var now = new Date()
      var t = ('0' + now.getHours()).substr(-2) +
                    ':' + ('0' + now.getMinutes()).substr(-2) +
                    ':' + ('0' + now.getSeconds()).substr(-2)
      return t
    },
    //在录音过程中记录和打印日志
    reclog: function (msg, color, res) {
      //msg:日志消息
      //color:是用于在控制台输出时区分不同类型日志的颜色代码
      //res:包含音频数据和相关信息的对象
      //playMsg:播放消息
      //down:下载次数
      //down64Val:Base64编码的音频数据
      var obj = {
        idx: this.logs.length,
        msg: msg,
        color: color,
        res: res,

        playMsg: '',
        down: 0,
        down64Val: ''
      }
      if (res && res.blob) {//如果 res 对象存在并包含音频数据 blob
        this.recLogLast = obj//用于记录最后一次的音频数据
      }
      this.logs.splice(0, 0, obj)
    },
    realTimeSendTryReset() {
      this.realTimeSendTryTime = 0
    },
    realTimeOnProcessClear(buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd) {//清空,开始录音的时候,this.realTimeSendTryTime的值为0
      if (this.realTimeSendTryTime == 0) {
        this.realTimeSendTryTime = Date.now()
        this.realTimeSendTryNumber = 0
        this.transferUploadNumberMax = 0
        this.realTimeSendTryBytesChunks = []
        this.realTimeSendTryClearPrevBufferIdx = 0
        this.realTimeSendTryBuffers = []
      }

      // 清理PCM缓冲数据,最后完成录音时不能调用stop,因为数据已经被清掉了
      // 这里进行了延迟操作(必须要的操作),只清理上次到现在的buffer
      for (var i = this.realTimeSendTryClearPrevBufferIdx; i < newBufferIdx; i++) {
        buffers[i] = null
      }
      this.realTimeSendTryClearPrevBufferIdx = newBufferIdx

      // 备份一下方便后面生成wav
      for (var i = newBufferIdx; i < buffers.length; i++) {
        this.realTimeSendTryBuffers.push(buffers[i])
      }
    },
    realTimeSendTry(chunkBytes, isClose) {
      console.log('123')
      if (chunkBytes) { // 推入缓冲再说
        this.realTimeSendTryBytesChunks.push(chunkBytes)
      }

      var t1 = Date.now()
      if (!isClose && t1 - this.realTimeSendTryTime < this.sendInterval) {//边说边翻译,当说话间隔设置小于你设置的间隔,就不进行传输
        console.log('return掉了')
        return// 控制缓冲达到指定间隔才进行传输
      }
      this.realTimeSendTryTime = t1
      var number = ++this.realTimeSendTryNumber

      // 缓冲的chunk拼接成一个更长点的
      var len = 0
      for (var i = 0; i < this.realTimeSendTryBytesChunks.length; i++) {
        len += this.realTimeSendTryBytesChunks[i].length
      }
      var chunkData = new Uint8Array(len)
      for (var i = 0, idx = 0; i < this.realTimeSendTryBytesChunks.length; i++) {
        var chunk = this.realTimeSendTryBytesChunks[i]
        chunkData.set(chunk, idx)
        idx += chunk.length
      }
      this.realTimeSendTryBytesChunks = []

      // 推入传输
      var blob = null; var meta = {}
      if (chunkData.length > 0) { // 不是空的
        blob = new Blob([chunkData], { type: 'audio/' + this.type })
        meta = Recorder.mp3ReadMeta([chunkData.buffer], chunkData.length) || {}// 读取出这个片段信息
      }
      this.transferUpload(number
        , blob
        , meta.duration || 0
        , {
          set: {
            type: this.type,
            sampleRate: meta.sampleRate,
            bitRate: meta.bitRate
          }
        }
        , isClose
      )
      var recMock = Recorder({
        type: this.type,
        sampleRate: this.sampleRate,
        bitRate: this.bitRate
      })
      if (this.realTimeSendTryBuffers.length > 0) {
        var chunk = Recorder.SampleData(this.realTimeSendTryBuffers, this.sampleRate, this.sampleRate)
        recMock.mock(chunk.data, this.sampleRate)
        var This = this
        recMock.stop(function(blob, duration) {
          var item = {
            blob: blob,
            duration: duration,
            durationTxt: This.formatMs(duration),
            rec: recMock
          }
          This.audios.push(item)
        })
      }
      this.realTimeSendTryBuffers = []
    },
    //音频数据转换为 Base64 编码,并通过 WebSocket 发送到服务器
    transferUpload(number, blobOrNull, c, blobRec, isClose) {
      this.transferUploadNumberMax = Math.max(this.transferUploadNumberMax, number)
      var This = this
      if (blobOrNull) {
        var blob = blobOrNull

        //* ********发送:Base64文本发送***************
        var reader = new FileReader()
        reader.readAsDataURL(blob)
        reader.onloadend = function() {
          var base64 = (/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1]
          This.base64s.push(base64)
          //创建了一个 WebSocket
          This.connectWebSocket()
        }
      }
      if (isClose) {
        This.isLoading = true
         // 将 PCM 数据转换为 Blob 对象
        var pcmBlob = new Blob([new Int16Array(This.pcmData)], { type: 'audio/pcm' });

        // 创建 FormData 对象,并添加 Blob 对象
        const formData = new FormData();
        formData.append('file', pcmBlob);

        // 发送 FormData 对象到服务器
        api.voice(formData).then(res => {
          This.identifyTexts = This.identifyTexts + res.data
          setTimeout(() => {
            This.isOpen = false
          }, 200);
        }).catch(err => {
          console.log(6666,err);
        })
        // this.mergeAll()
      }
    },
    mergeAll() {
      var bitRate = this.bitRate;
      var idx = -1 + 1;
      var files = [];
      var This = this;
      var read = function() {
        var audios = This.audios;
        var audiosItem = audios[idx++];
        if (idx >= audios.length) {
          if (!files.length) {
            return;
          }

          var rec = audiosItem.rec;
          This.mp3Merge(files, bitRate, function(fileBytes, duration) {
            var blob = new Blob([fileBytes.buffer], { type: 'audio/' + This.type });
            This.reclog('已录制:', '', {
              blob: blob,
              duration: duration,
              durationTxt: This.formatMs(duration),
              rec: rec
            });
            // 创建 FormData 对象,并添加 Blob 对象
            const formData = new FormData();
            formData.append('file', blob);
            api.voice(formData).then(res => {
              // 处理响应
              // 创建隐藏的可下载链接
              var element = document.createElement('a');
              element.setAttribute('href', window.URL.createObjectURL(blob));
              element.setAttribute('download', 'response.mp3'); // or any other extension
              element.style.display = 'none';
              document.body.appendChild(element);
              element.click();
              document.body.removeChild(element);
            }).catch(err => {
              console.log(6666,err);
            });
            This.recLast();
          })
          This.audios = [];
          return;
        }
        var reader = new FileReader();
        reader.onloadend = function() {
          files.push(new Uint8Array(reader.result));
          read();
        }
        console.log('audiosItem.blob:', audiosItem.blob);
        reader.readAsArrayBuffer(audiosItem.blob);
      }
      read();
    },
    mp3Merge(fileBytesList, bitRate, True) {
      // 计算所有文件总长度
      var size = 0
      for (var i = 0; i < fileBytesList.length; i++) {
        size += fileBytesList[i].byteLength
      }
      // 全部直接拼接到一起
      var fileBytes = new Uint8Array(size)
      var pos = 0
      for (var i = 0; i < fileBytesList.length; i++) {
        var bytes = fileBytesList[i]
        fileBytes.set(bytes, pos)
        pos += bytes.byteLength
      }
      // 计算合并后的总时长
      var duration = Math.round(size * 8 / bitRate)
      True(fileBytes, duration)
    },

    // 连接websocket
    connectWebSocket() {
      if (!this.isIdentify) return
      console.log('连接websocket')
      return this.getWebSocketUrl().then(url => {
        if (this.webSocket && this.webSocket.readyState === this.webSocket.OPEN) {
          return
        } else {
          if ('WebSocket' in window) {
            console.log('创建WebSocket')
            this.webSocket = new WebSocket(url)
          } else if ('MozWebSocket' in window) {
            console.log('MozWebSocket')
            this.webSocket = new MozWebSocket(url)
          } else {
            return
          }
          //用于连接成功后发送数据
          this.webSocket.onopen = e => {
            this.webSocketSend()
          }
          //用于接收服务器返回的识别结果
          this.webSocket.onmessage = e => {
            this.result(e.data)
          }
          this.webSocket.onerror = e => {
          }
          this.webSocket.onclose = e => {
          }
        }
      })
    },
    getWebSocketUrl() {
      return new Promise((resolve) => {
        // 请求地址根据语种不同变化
        var url = 'wss://iat-api.xfyun.cn/v2/iat'
        var host = 'iat-api.xfyun.cn'
        var apiKey = API_KEY
        var apiSecret = API_SECRET
        var date = new Date().toGMTString()
        var algorithm = 'hmac-sha256'
        var headers = 'host date request-line'
        var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`
        var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)
        var signature = CryptoJS.enc.Base64.stringify(signatureSha)
        var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`
        var authorization = btoa(authorizationOrigin)
        url = `${url}?authorization=${authorization}&date=${date}&host=${host}`
        resolve(url)
      })
    },
    // 向webSocket发送数据
    webSocketSend() {
      console.log('webSocketSend:', JSON.stringify(this.webSocket))
      if (this.webSocket.readyState !== this.webSocket.OPEN) {
        return
      }
      if (this.base64s.length == 0) return
      this.identifyText = ''
      var index = 0
      var params
      params =
                    {
                      common: {
                        app_id: APPID
                      },
                      business: {
                        language: 'zh_cn', // 小语种可在控制台--语音听写(流式)--方言/语种处添加试用
                        domain: 'iat',
                        accent: 'mandarin' // 中文方言可在控制台--语音听写(流式)--方言/语种处添加试用
                      },
                      data: {
                        status: 0,
                        format: 'audio/L16;rate=16000',
                        encoding: 'lame',
                        audio: this.base64s[index]
                      }
                    }
      console.log('首帧', this.base64s.length, index)
      // console.log("首帧", JSON.stringify(params));
      this.webSocket.send(JSON.stringify(params))
      this.handlerInterval = setInterval(() => {
        // websocket未连接
        if (this.webSocket.readyState !== this.webSocket.OPEN) {
          console.log('websocket未连接:', this.base64s.length, index)
          this.closeVoice()
          clearInterval(this.handlerInterval)
          return
        }
        if (this.base64s.length === index) {
          params =
                            {
                              data: {
                                status: 2,
                                format: 'audio/L16;rate=16000',
                                encoding: 'lame',
                                audio: ''
                              }
                            }
          console.log('尾帧', this.base64s.length, index)
          // console.log("尾帧", JSON.stringify(params));
          this.webSocket.send(JSON.stringify(params))
          clearInterval(this.handlerInterval)
          return false
        }
        // 中间帧
        index++
        params =
                        {
                          data: {
                            status: 1,
                            format: 'audio/L16;rate=16000',
                            encoding: 'lame',
                            audio: this.base64s[index]
                          }
                        }
        console.log('中间帧', this.base64s.length, index)
        // console.log("中间帧",this.base64s.length,index, JSON.stringify(params));
        this.webSocket.send(JSON.stringify(params))
      }, 40)
    },
    result(resultData) {
      console.log('resultData:', resultData)
      // 识别结束
      const jsonData = JSON.parse(resultData)
      if (jsonData.data && jsonData.data.result) {
        const data = jsonData.data.result
        const status = jsonData.data.status
        let str = ''
        const ws = data.ws
        for (let i = 0; i < ws.length; i++) {
          str = str + ws[i].cw[0].w
        }
        console.log('识别的结果为' + this.isOpen + '===>' + status + ':', str)
        if (status === 0) {
          this.identifyText = str
        }
        if (status === 1) {
          this.identifyText += str
        }
        if (status === 2) {
          this.identifyText += str
        }
      }
      if (jsonData.code === 0 && jsonData.data.status === 2) {
        this.webSocket.close()
      }
      if (jsonData.code !== 0) {
        this.webSocket.close()
      }
    }
  }
}
</script>
<style lang="scss" scoped>
    .container {
        width: 100%;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        height: fit-content;

        .buttonContainer {
            margin-top: 20px;
            display: flex;
            width: 100%;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
        }
    }
    /* 隐藏 */
    .hidden {
        display: none;
    }

    /* 显示 */
    .visible {
        display: block;
    }

</style>



外部组件
 <!-- 语音转文字录入 -->
    <el-dialog title="单款分析" width="1000px" :visible.sync="txtRemainVisible">
      <voice :identify-status="true" :show-identify-text-status="true" :skc="skc" :sku-info-obj="skuInfoObj" @closeRemarkDialog="closeRemarkDialog" />
    </el-dialog>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值