喊话器功能

 

 

 


<script setup lang="ts"> import { Modal, message } from 'ant-design-vue'; import Speech from 'speak-tts'; import { useStorage } from '@vueuse/core'; import { createErrorMsg } from '@/api/helper.ts'; const dialogRef2 = ref(); const voiceTypeTabs = ref([ { label: '实时喊话', value: '0', }, { label: '音频', value: '1', }, { label: '语言合成', value: '2', }, ]); const CURRENT_EQIUP: any = useStorage('CURRENT_EQUIP', {}, sessionStorage); const dockSn = computed(() => CURRENT_EQIUP.value.dock || ''); const activedVioce = ref('0'); const voiceSlider = ref<number>(100);// 音量 const voviceOpen = ref(false); // 音量弹窗是否开启 const speech = new Speech(); const voiceText = ref(''); function open() { // 清空和停止之前的内容播放 voiceText.value = ''; speech.cancel(); dialogRef2.value?.open(); } function close() { console.log('关闭'); } // 获取音频项目 const audio = ref<HTMLAudioElement>(); const audioUrl = ref(''); const audioIndex = ref(); // const fileList = ref([]); const queryParamsyp = ref({ currentPage: 1, pageSize: 5, }); const total: any = ref(0); const queryParamsyy = ref({ currentPage: 1, pageSize: 5, }); // 音频列表 const voiceList: any = ref([ // { // fileIndex: 0, // key: '2024/10/16/080bc94167ee4c1f935ab02e629a48c1.mp3', // fileName: '音频1.mp3', // msg: '成功', // url: 'https://www.teleuav.com:20000/api/file-service/file/view?key=1815209819035451393/2024/11/22/5d6246a09c154557b37e51f986180b31.mp3', // compressKey: null, // id: '1846381305490059265', // isPlay: false, // isEdit: true, // }, // { // fileIndex: 0, // key: '2024/10/16/99821753ff2c4d18b255f18bfe9d45f1.mp3', // fileName: '音频2.mp3', // msg: '成功', // url: 'https://www.teleuav.com:20000/api/file-service/file/view?key=1815209819035451393/2024/11/22/5d6246a09c154557b37e51f986180b31.mp3', // compressKey: null, // id: '1846448057976631297', // isPlay: false, // isEdit: true, // }, ]); function _postApiSpeakerPageQuery(vtype: any, currentPage: any, pageSize: any) { postApiSpeakerPageQuery({ currentPage, pageSize, dockSn: dockSn.value, contentType: vtype, // 0文本,1音频 }).then((res) => { console.log('列表111', res); if (res.success) { if (res.data && res.data !== null) { const d = res.data.records; d?.forEach((item) => { item.isEdit = true; }); voiceList.value = d; total.value = res.data.total; } } }); } function voiceTypeTabschange(e: any) { if (e.value === '1') { // 音频 _postApiSpeakerPageQuery(1, queryParamsyp.value.currentPage, queryParamsyp.value.pageSize); } else if (e.value === '2') { // 语言 _postApiSpeakerPageQuery(0, queryParamsyy.value.currentPage, queryParamsyy.value.pageSize); } } function onChangePageyp(current: number, pageSize: number) { queryParamsyp.value.currentPage = current; queryParamsyp.value.pageSize = pageSize; } function onChangePageyy(current: number, pageSize: number) { queryParamsyy.value.currentPage = current; queryParamsyy.value.pageSize = pageSize; } // function isAudioFile(file: any) { // const audioMimeTypes = [ // 'audio/mpeg', // 'audio/mp3', // 'audio/mp4', // 'audio/pmc', // 'audio/webm', // 'audio/ogg', // 'audio/wav', // 'audio/x-ms-wma', // 'audio/x-ms-wma', // 'audio/x-realaudio', // 'audio/vnd.rn-realaudio', // 'audio/x-pn-realaudio', // 'audio/x-wav', // ]; // return audioMimeTypes.includes(file.type); // } // 上传前对文件进行校验 // function beforeUpload(file: any) { // if (!isAudioFile(file)) { // message.warning('请上传音频格式的文件!'); // return false; // } // const fileExtension = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase(); // if (fileExtension !== 'zip') { // message.warning('请上传zip格式的文件!'); // return; // } // handleAudioUpload(file); // return false; // } // 音频保存 // function handleAudioUpload(file: any) { // console.log('file上传', file); // const res = { // code: 0, // msg: '操作成功', // hint: '', // data: { // fileIndex: 0, // key: '2024/10/16/080bc94167ee4c1f935ab02e629a48c1.mp3', // fileName: '080bc94167ee4c1f935ab02e629a48c1.mp3', // msg: '成功', // url: 'http://125.122.14.45:20000/api/file-service/file/view?key=2024/10/16/080bc94167ee4c1f935ab02e629a48c1.mp3', // compressKey: null, // id: '1846381305490059265', // }, // success: true, // }; // if (res.success) { // const json = res.data; // voiceList.value.push(json); // console.log('voiceList.value', voiceList.value); // } // postFileUpload({}, {}, file).then((res) => { // console.log('res上传',res) // { // "code": 0, // "msg": "操作成功", // "hint": "", // "data": { // "fileIndex": 0, // "key": "2024/10/16/080bc94167ee4c1f935ab02e629a48c1.mp3", // "fileName": "080bc94167ee4c1f935ab02e629a48c1.mp3", // "msg": "成功", // "url": "http://125.122.14.45:20000/api/file-service/file/view?key=2024/10/16/080bc94167ee4c1f935ab02e629a48c1.mp3", // "compressKey": null, // "id": "1846381305490059265" // }, // "success": true // } // if (res?.success) { // message.success('上传成功'); // const json = res.data; // voiceList.value.push(json); // } // }); // } // 播放当前项 function playItem(index: any, item: any) { console.log(item); audioIndex.value = index; audioUrl.value = item.contentUrl; // 获取当前播放音频 // 当前播放 其他停止 voiceList.value.forEach((item2) => { if (item2.id === item.id) { item2.isPlay = !item2.isPlay; if (item2.isPlay) { nextTick(() => { audio?.value?.play(); }); } } else { item2.isPlay = false; audio?.value?.pause(); } }); } // 音频是否播放完毕 function audioEnded() { voiceList.value[audioIndex.value].isPlay = false; } // 音频发送 function fasongItem(index: any, item: any) { Modal.confirm({ title: '提示', content: '确认要喊话吗?', okText: '确认', cancelText: '取消', async onOk() { postApiSpeakerShoutContent({ dockSn: item.dockSn, id: item.id, }).then((res) => { if (res.success) { message.success(res.msg); } else { message.error(res.msg); } }); }, onCancel() { }, }); } // 编辑音频名称 function editItem(item: any) { item.isEdit = !item.isEdit; if (item.isEdit) { postApiSpeakerUpdateContent({ dockSn: item.dockSn, fileName: item.fileName, id: item.id, }).then((res) => { if (res.success) { message.success(res.msg); _postApiSpeakerPageQuery(1, queryParamsyp.value.currentPage, queryParamsyp.value.pageSize); } else { message.error(res.msg); } }); } } // 删除当前音频 function deleteItem(index: any, item: any) { console.log('item', item); Modal.confirm({ title: '提示', content: '确认删除当前内容?', okText: '确认', cancelText: '取消', async onOk() { // 删除和停止播放 // voiceList.value.splice(index, 1); postApiSpeakerDelContent({ dockSn: item.dockSn, id: item.id, }).then((res) => { if (res.success) { if (index === audioIndex.value) { audio?.value?.pause(); } message.success(res.msg); if (item.contentType === 0) { _postApiSpeakerPageQuery(0, queryParamsyy.value.currentPage, queryParamsyy.value.pageSize); } else { _postApiSpeakerPageQuery(1, queryParamsyp.value.currentPage, queryParamsyp.value.pageSize); } } else { message.error(res.msg); } }); }, onCancel() { }, }); } // function removefile(file: any) { // console.log('file', file); // console.log(fileList, 123); // } /* 语言转换 */ const voiceisPlay = ref<boolean>(false); if (speech.hasBrowserSupport()) { // 检测浏览器是否支持,returns a boolean console.log('语音引擎加载成功'); } else { console.log('此浏览器不支持语音播报'); } speech.init({ volume: 1, // 音量0-1 lang: 'zh-CN', // 语言 rate: 1, // 语速1正常语速,2倍语速就写2 pitch: 1, // 音调 voice: 'Microsoft Yaoyao - Chinese (Simplified, PRC)', // 支持Microsoft Huihui - Chinese (Simplified, PRC),Microsoft Kangkang - Chinese (Simplified, PRC),Microsoft Yaoyao - Chinese (Simplified, PRC) listeners: { // 事件 onvoiceschanged: (voices: any) => { console.log('事件声音已更改', voices); }, }, }) .then((data: any) => { console.log('语音已准备好,声音可用', data); }) .catch((e: any) => { console.error('初始化时发生错误 : ', e); }); // 音频转换播放 function playvoice() { speech.resume(); speech.speak({ text: voiceText.value, // 这里使用文字或者i18n 都可以 看自己需求 // queue: true, listeners: { // 开始播放 onstart: () => { console.log('开始播放'); voiceisPlay.value = true; }, // 判断播放是否完毕 onend: () => { console.log('播放是否完毕'); voiceisPlay.value = false; }, // 恢复播放 onresume: () => { console.log('恢复播放'); voiceisPlay.value = true; speech.resume(); }, }, }) .then(() => { console.log('播放成功!'); }) .catch((e: any) => { console.error('发生错误:', e); }); } function stopvoice() { voiceisPlay.value = false; speech.pause(); } function deletevoice() { voiceText.value = ''; speech.cancel(); voiceisPlay.value = false; } // 内容变化 function textareachange() { speech.cancel(); voiceisPlay.value = false; } // 语言音频保存 function savevoice() { if (!voiceText.value) { createErrorMsg('请输入文本内容'); return; } const data = { dockSn: dockSn.value, contentType: 0, // 0-文本,1音频 contentUrl: voiceText.value, // 文本文字或喊话url // contentAuditionUrl: '' //音频喊话pcm格式的url }; postApiSpeakerAddContent(data).then((res) => { if (res.success) { message.success(res.msg); voiceText.value = ''; _postApiSpeakerPageQuery(0, queryParamsyy.value.currentPage, queryParamsyy.value.pageSize); } else { message.error(res.msg); } }); } // 设置音量 function openVoviceModal() { voviceOpen.value = true; // 查询喊话音量大小 getApiSpeakerGetVolume({ dockSn: dockSn.value, }).then((res) => { if (res.success) { if (res.data && res.data !== null) { voiceSlider.value = res.data; } } }); } // 更改音频音量 const changeVoice = function (e: any) { if (e) { voiceSlider.value = e; } }; // 音量保存 function handleOk() { const params = { dockSn: dockSn.value, volume: voiceSlider.value, }; postApiSpeakerUpdateVolume(params).then((res) => { if (res.success) { message.success(res.msg); voviceOpen.value = false; } else { message.error(res.msg); } }); } // 录音保存成功刷新列表 function recorderupdata() { _postApiSpeakerPageQuery(1, queryParamsyp.value.currentPage, queryParamsyp.value.pageSize); } defineExpose({ open, }); </script> <template> <Dialog ref="dialogRef2" :footer="false" :dialog-style="{ width: '25rem', top: '25%', right: '7rem', zIndex: 1049 }" title="喊话器" @close="close" > <div class="voice_container"> <!-- 设置音量 --> <div class="mt-[-28px]" @click="openVoviceModal"> <a-tooltip title="音量设置" placement="top"> <PubSvgIcon name="icon_set" class="cursor-pointer" :size="22" /> </a-tooltip> </div> <div class="relative h-12 px-4 flex items-center justify-center"> <TabBox v-model="activedVioce" :tabs="voiceTypeTabs" @change="voiceTypeTabschange" /> </div> <div v-if="activedVioce === '0'" class="w-full h-full flex items-center justify-center my-9" style="flex-direction: column;" > <!-- 实时喊话录音 --> <recorder /> </div> <!-- 音频 --> <div v-if="activedVioce === '1'"> <div v-if="voiceList && voiceList.length"> <div v-for="(item, index) in voiceList" :key="index" class="flex px-2 mb-3 h-8 line-clamp-1 bg-[#11253E] items-center mt-1" > <div class="grid-cols-3 whitespace-nowrap list_item_left"> <!-- <span>{{ item.fileName }}</span> --> <a-input v-model:value="item.fileName" :disabled="item.isEdit" :maxlength="10"> <template #suffix> <a-tooltip title="编辑" placement="top"> <PubSvgIcon name="icon_edit" class="cursor-pointer" :size="22" @click="editItem(item)" /> </a-tooltip> </template> </a-input> </div> <span class="flex items-center h-full"> <a-tooltip title="试听" placement="top"> <PubSvgIcon v-if="!item.isPlay" class="cursor-pointer" name="icon_play" size="1.5rem" color="#fff" @click="playItem(index, item)" /> <PubSvgIcon v-if="item.isPlay" class="cursor-pointer" name="icon_pause" size="1.5rem" color="#fff" @click="playItem(index, item)" /> </a-tooltip> <a-tooltip title="发送" placement="top"> <PubSvgIcon name="icon_fasong" size="1.5rem" color="#FE3B30" class="ml-1 mr-1 cursor-pointer" @click="fasongItem(index, item)" /> </a-tooltip> <a-tooltip title="删除" placement="top"> <PubSvgIcon name="icon_delete" class="cursor-pointer" size="1.5rem" color="#FE3B30" @click="deleteItem(index, item)" /> </a-tooltip> </span> </div> </div> <div v-if="!voiceList || voiceList.length <= 0" class="no-data"> 暂无数据 </div> <a-pagination v-model:current="queryParamsyp.currentPage" v-model:page-size="queryParamsyp.pageSize" size="small" class="text-center" :total="total" :page-size="5" :show-total="total => `共 ${total} 条`" @change="onChangePageyp" /> <div class="flex items-center justify-center"> <!-- <a-upload v-model:file-list="fileList" :before-upload="(file: any) => beforeUpload(file)" @remove="removefile" > <a-button type="primary" class="up-button"> 上传音频 </a-button> </a-upload> --> <recorderup @updata="recorderupdata" /> </div> </div> <!-- 语言合成 --> <div v-if="activedVioce === '2'" class="relative"> <div v-if="voiceList && voiceList.length"> <div v-for="(item, index) in voiceList" :key="index" class="flex px-2 mb-3 h-8 line-clamp-1 bg-[#11253E] items-center mt-1" > <div class="grid-cols-3 whitespace-nowrap list_item_left"> <span>{{ item.contentUrl }}</span> </div> <span class="flex items-center h-full"> <a-tooltip title="发送" placement="top"> <PubSvgIcon name="icon_fasong" class="cursor-pointer mr-1" size="1.5rem" color="#FE3B30" cursor-pointer @click="fasongItem(index, item)" /> </a-tooltip> <a-tooltip title="删除" placement="top"> <PubSvgIcon name="icon_delete" size="1.5rem" class="cursor-pointer" color="#FE3B30" @click="deleteItem(index, item)" /> </a-tooltip> </span> </div> </div> <div v-if="!voiceList || voiceList.length <= 0" class="no-data"> 暂无数据 </div> <a-pagination v-model:current="queryParamsyy.currentPage" v-model:page-size="queryParamsyy.pageSize" size="small" class="text-center" :total="total" :page-size="5" :show-total="total => `共 ${total} 条`" @change="onChangePageyy" /> <div class="relative mt-2"> <div class="absolute flex items-center z-30 right-2 top-2"> <PubSvgIcon v-if="!voiceisPlay" name="icon_play" class="cursor-pointer mr-1" size="1.5rem" color="#fff" @click="playvoice()" /> <PubSvgIcon v-else name="icon_pause" size="1.5rem" color="#fff" class="cursor-pointer mr-1" @click="stopvoice()" /> <a-tooltip title="清空" placement="top"> <PubSvgIcon name="icon_delete" size="1.5rem" color="#FE3B30" @click="deletevoice()" /> </a-tooltip> </div> <a-textarea v-model:value="voiceText" class="new-textarea" :rows="4" placeholder="请输入符合文本内容" show-count :maxlength="300" @change="textareachange" /> <div class="flex items-center justify-end mt-8"> <a-button type="primary" class="up-button" @click="savevoice"> 保存 </a-button> </div> </div> </div> <!-- 设置音量弹窗 --> <a-modal v-model:open="voviceOpen" class="new-modal" :mask-closable="false" style="top: 30%;z-index: 1049;" @ok="handleOk" > <div> <span class="w-10 font-size-5">音量设置</span> </div> <div class="mt-1 flex items-center"> <div class="flex-1"> <a-slider v-model:value="voiceSlider" class="blue-slider" :min="1" @change="changeVoice" /> </div> </div> </a-modal> <audio ref="audio" :src="audioUrl" @ended="audioEnded" /> </div> </Dialog> </template> <style lang="less" scoped> :deep(.modal-body) { margin: 0; } .voice_container { margin-top: -10px; margin-bottom: -10px; filter: none; .list_item_left { width: 90%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .up-button { background-image: linear-gradient(115deg, #3a83fe 10%, #0f63fe 100%); } :deep(.ant-slider-rail) { background-color: #757f89; } :deep(.ant-slider-track) { background: #1f6aff; } :deep(.ant-slider-handle::after) { background: #1f6aff; border: 1px solid #fff; box-shadow: none; } :deep(.ant-slider-handle:hover::after) { box-shadow: none; } :deep(.ant-input-textarea-show-count::after) { position: absolute; right: 8px; bottom: 6px; color: rgba(255, 255, 255, 0.45); white-space: nowrap; content: attr(data-count); } } .new-textarea { :deep(.ant-input) { padding: 10px 50px 10px 10px; } } //隐藏上传列表 :deep(.ant-upload-list) { display: none !important; } </style>
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { useStorage } from '@vueuse/core';

// 必须引入的核心
import Recorder from 'recorder-core';
import 'recorder-core/src/engine/mp3';
import 'recorder-core/src/engine/mp3-engine';
import 'recorder-core/src/engine/wav';
import 'recorder-core/src/engine/pcm';
import 'recorder-core/src/extensions/waveview';

const emit = defineEmits(['updata']);
const CURRENT_EQIUP: any = useStorage('CURRENT_EQUIP', {}, sessionStorage);
const dockSn = computed(() => CURRENT_EQIUP.value.dock || '');
let rec: any;
let recBlob: any;
let wave: any;
const playing = ref(false);
const recwave = ref(null);
// 打开录音
function recOpen() {
  playing.value = true;
  // 创建录音对象
  rec = Recorder({
    type: 'mp3', // 录音格式,可以换成wav等其他格式
    sampleRate: 16000, // 录音的采样率,越大细节越丰富越细腻
    bitRate: 16, // 录音的比特率,越大音质越好
    format: 'mp3',
    onProcess: (
      buffers: any,
      powerLevel: any,
      // bufferDuration: any,
      bufferSampleRate: any,
      // newBufferIdx: any,
      // asyncEnd: any,
    ) => {
      // 录音实时回调,大约1秒调用12次本回调
      // 可实时绘制波形,实时上传(发送)数据
      if (wave) {
        wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
      }
    },
  });
  if (!rec) {
    // alert('当前浏览器不支持录音功能!');
    return;
  }
  // 打开录音,获得权限
  rec.open(
    () => {
      console.log('录音已打开');
      if (recwave.value) {
        // 创建音频可视化图形绘制对象
        wave = Recorder.WaveView({ elem: recwave.value });
        setTimeout(recStart, 300);
      }
    },
    (msg: any, isUserNotAllow: any) => {
      // 用户拒绝了录音权限,或者浏览器不支持录音
      console.log(`${isUserNotAllow ? 'UserNotAllow,' : ''}无法录音:${msg}`);
    },
  );
}
// 开始录音
function recStart() {
  if (!rec) {
    console.error('未打开录音');
    return;
  }
  rec.start();
  console.log('已开始录音');
}
// 结束录音
const isEnd = ref(false);
function recStop() {
  playing.value = false;
  if (!rec) {
    console.error('未打开录音');
    return;
  }
  rec.stop(
    (blob: any, duration: any) => {
      if (blob) {
        // blob就是我们要的录音文件对象,可以上传,或者本地播放
        recBlob = blob;
        // 简单利用URL生成本地文件地址,此地址只能本地使用,比如赋值给audio.src进行播放,赋值给a.href然后a.click()进行下载(a需提供download="xxx.mp3"属性)
        const localUrl = (window.URL || window.webkitURL).createObjectURL(blob);
        console.log('录音成功', blob, localUrl, `时长:${duration}ms`);
        console.log('recBlob1111', recBlob);
        isEnd.value = true;
      }
      // upload(blob); // 把blob文件上传到服务器
      rec.close(); // 关闭录音,释放录音资源,当然可以不释放,后面可以连续调用start
      rec = null;
    },
    (err: any) => {
      console.error(`结束录音出错:${err}`);
      rec.close(); // 关闭录音,释放录音资源,当然可以不释放,后面可以连续调用start
      rec = null;
    },
  );
}
// 上传录音
const pcmurl: any = ref();
const mp3url: any = ref();

function submit() {
  if (recBlob) {
    const audioFilepcm = new File([recBlob], '.pcm', { type: 'audio/pcm' });
    const audioFilemp3 = new File([recBlob], '.mp3', { type: 'audio/mp3' });
    postFileUpload({}, {}, audioFilepcm).then((res) => {
      if (res.success) {
        if (res.data && res.data !== null) {
          if (res.data.url && res.data.url !== null) {
            pcmurl.value = res.data.url;
          }
        }
      }
      else {
        message.error(res.msg);
      }
    });
    setTimeout(() => {
      postFileUpload({}, {}, audioFilemp3).then((res) => {
        if (res.data && res.data !== null) {
          if (res.data.url && res.data.url !== null) {
            mp3url.value = res.data.url;
          }
        }
      });
    }, 500);
    // 上传成功才能添加
    setTimeout(() => {
      if (mp3url.value && pcmurl.value) {
        const data = {
          dockSn: dockSn.value,
          contentType: 1, // 0-文本,1音频
          contentUrl: mp3url.value, // 文本文字或喊话url
          contentAuditionUrl: pcmurl.value, // 音频喊话pcm格式的url
        };
        postApiSpeakerAddContent(data).then((res) => {
          if (res.success) {
            pcmurl.value = '';
            mp3url.value = '';
            playing.value = false;
            isEnd.value = false;
            emit('updata');
            message.success(res.msg);
          }
          else {
            message.error(res.msg);
          }
        });
      }
    }, 1500);
  }
}
// 本地播放录音
function recPlay() {
  // 本地播放录音试听,可以直接用URL把blob转换成本地播放地址,用audio进行播放
  const localUrl = URL.createObjectURL(recBlob);
  const audio = document.createElement('audio');
  audio.controls = true;
  document.body.appendChild(audio);
  audio.src = localUrl;
  audio.style.display = 'none';
  audio.play(); // 这样就能播放了
  // 注意不用了时需要revokeObjectURL,否则霸占内存
  setTimeout(() => {
    URL.revokeObjectURL(audio.src);
  }, 5000);
}
</script>

<template>
  <div class="flex items-center" style="flex-direction: column;">
    <div class="mt-2 mb-2">
      {{ playing ? '录音中' : '' }}
    </div>
    <div class="icon-box text-center mt-2">
      <div class="cursor-pointer w-full h-full flex items-center justify-center" :class="playing ? 'active' : ''">
        <PubSvgIcon v-if="!playing" name="icon_vovice" color="#fff" :size="24" />
        <PubSvgIcon v-else name="icon_vovice_active" color="#fff" :size="24" />
      </div>
    </div>
    <div class="flex mt-2">
      <a-button v-if="!playing" type="primary" class="mr-2" @click="recOpen">
        开始录音
      </a-button>
      <a-button v-if="playing" type="primary" class="mr-2" @click="recStop">
        结束录音
      </a-button>
      <a-button v-if="isEnd && !playing" type="primary" class="mr-2" @click="recPlay">
        本地试听
      </a-button>
      <a-button v-if="isEnd && !playing" type="primary" @click="submit">
        上传录音
      </a-button>
    </div>

    <!-- 波形绘制区域 -->
    <div v-show="playing" class="pt-8">
      <div style="display: inline-block; vertical-align: bottom;">
        <div ref="recwave" style="width: 10rem;height: 6rem;" />
      </div>
    </div>
  </div>
</template>

<style lang="less" scoped>
.icon-box {
  flex-direction: column;
  width: 68px;
  height: 68px;
  margin-bottom: 6px;
  background: url("@/assets/images/airportDetail/nav/nav-bg0.png") no-repeat center center;
  background-size: 100% 100%;

  .active {
    border-radius: 50%;
    box-shadow: 0 0 4px 0 #00abff;
  }
}
</style>
<!-- 录音 -->
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { useStorage } from '@vueuse/core';
// 必须引入的核心
import Recorder from 'recorder-core';
import 'recorder-core/src/engine/mp3';
import 'recorder-core/src/engine/mp3-engine';
import 'recorder-core/src/engine/wav';
import 'recorder-core/src/engine/pcm';
import 'recorder-core/src/extensions/waveview';

const CURRENT_EQIUP: any = useStorage('CURRENT_EQUIP', {}, sessionStorage);
const dockSn = computed(() => CURRENT_EQIUP.value.dock || '');
let rec: any;
let recBlob: any;
let wave: any;
const recwave = ref(null);
// 打开录音
function recOpen() {
  // 创建录音对象
  rec = Recorder({
    type: 'pcm', // 录音格式,可以换成wav等其他格式
    sampleRate: 16000, // 录音的采样率,越大细节越丰富越细腻
    bitRate: 16, // 录音的比特率,越大音质越好
    format: 'pcm',
    onProcess: (
      buffers: any,
      powerLevel: any,
      // bufferDuration: any,
      bufferSampleRate: any,
      // newBufferIdx: any,
      // asyncEnd: any,
    ) => {
      // 录音实时回调,大约1秒调用12次本回调
      // 可实时绘制波形,实时上传(发送)数据
      if (wave) {
        wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
      }
    },
  });
  if (!rec) {
    // alert('当前浏览器不支持录音功能!');
    return;
  }
  // 打开录音,获得权限
  rec.open(
    () => {
      // console.log('录音已打开');
      if (recwave.value) {
        // 创建音频可视化图形绘制对象
        wave = Recorder.WaveView({ elem: recwave.value });
        recStart();
        // setTimeout(recStart, 1500);
      }
    },
    (msg: any, isUserNotAllow: any) => {
      // 用户拒绝了录音权限,或者浏览器不支持录音
      console.log(`${isUserNotAllow ? 'UserNotAllow,' : ''}无法录音:${msg}`);
    },
  );
}
// 开始录音
function recStart() {
  if (!rec) {
    console.error('未打开录音');
    return;
  }
  rec.start();
  console.log('已开始录音');
}
// 结束录音
function recStop() {
  if (!rec) {
    console.error('未打开录音');
    return;
  }
  rec.stop(
    (blob: any, duration: any) => {
      // blob就是我们要的录音文件对象,可以上传,或者本地播放
      recBlob = blob;
      // 简单利用URL生成本地文件地址,此地址只能本地使用,比如赋值给audio.src进行播放,赋值给a.href然后a.click()进行下载(a需提供download="xxx.mp3"属性)
      const localUrl = (window.URL || window.webkitURL).createObjectURL(blob);
      console.log('录音成功', blob, localUrl, `时长:${duration}ms`);
      // console.log('recBlob1111', recBlob);
      rec.close(); // 关闭录音,释放录音资源,当然可以不释放,后面可以连续调用start
      rec = null;
    },
    (err: any) => {
      console.error(`结束录音出错:${err}`);
      rec.close(); // 关闭录音,释放录音资源,当然可以不释放,后面可以连续调用start
      rec = null;
    },
  );
}
// 上传录音
function submit() {
  if (recBlob) {
    const audioFile = new File([recBlob], '.pcm', { type: 'audio/pcm' });
    postFileUpload({}, {}, audioFile).then((res) => {
      if (res.success) {
        if (res.data && res.data !== null) {
          if (res.data.url && res.data.url !== null) {
            // 实时喊话
            postApiSpeakerShoutNow({
              dockSn: dockSn.value,
              contentUrl: res.data.url,
            }).then((res) => {
              if (res.success) {
                message.success('喊话成功');
              }
              else {
                message.error(res.msg);
              }
            });
          }
        }
      }
    });
  }
}
// 本地播放录音
// function recPlay() {
//   // 本地播放录音试听,可以直接用URL把blob转换成本地播放地址,用audio进行播放
//   const localUrl = URL.createObjectURL(recBlob);
//   const audio = document.createElement('audio');
//   audio.controls = true;
//   document.body.appendChild(audio);
//   audio.src = localUrl;
//   audio.style.display = 'none';
//   audio.play(); // 这样就能播放了
//   // 注意不用了时需要revokeObjectURL,否则霸占内存
//   setTimeout(() => {
//     URL.revokeObjectURL(audio.src);
//   }, 5000);
// }
const isvovice = ref(false);
// function changeVovice() {
//   isvovice.value = !isvovice.value;
//   if (isvovice.value) {
//     // 开始录音
//     recOpen();
//   }
//   else {
//     // 结束录音
//     recStop();
//     // 上传录音
//     setTimeout(() => {
//       submit();
//     }, 1000);
//   }
// }
const longPressActive = ref(false);
function emitMessage() {
  // 发送你的消息
  // console.log('长按了');
  isvovice.value = true;
  recOpen();
}

function startLongPress(event: any) {
  // 阻止默认上下文菜单出现
  event.preventDefault();
  longPressActive.value = true;
  const timeout = setTimeout(() => {
    emitMessage();
  }, 300); // 设置长按1秒触发
  // 存储setTimeout的引用,以便稍后清理
  event.target.timeout = timeout;
}

function endLongPress(event: any) {
  longPressActive.value = false;
  // 清理setTimeout的引用
  if (event.target.timeout) {
    clearTimeout(event.target.timeout);
  }
  // console.log('取消了');
  isvovice.value = false;
  recBlob = null;
  // 结束录音
  recStop();
  // 上传录音
  setTimeout(() => {
    submit();
  }, 1000);
}

onMounted(() => {
  // 监听全局mouseup事件以确保即使鼠标移出元素也能正确清理
  // document.addEventListener('mouseup', endLongPress);
});

onUnmounted(() => {
  // 清理事件监听器
  document.removeEventListener('mouseup', endLongPress);
});
</script>

<template>
  <!-- 长按触发 -->
  <div class="icon-box text-center" @mousedown="startLongPress" @mouseup="endLongPress">
    <div class="cursor-pointer w-full h-full flex items-center justify-center" :class="isvovice ? 'active' : ''">
      <PubSvgIcon v-if="!isvovice" name="icon_vovice" color="#fff" :size="24" />
      <PubSvgIcon v-else name="icon_vovice_active" color="#fff" :size="24" />
    </div>
    <div class="mt-2">
      {{ isvovice ? '喊话中' : '实时喊话' }}
    </div>
  </div>
  <!-- 点击触发 -->
  <!-- <div class="icon-box text-center" @click="changeVovice">
      <div class="cursor-pointer w-full h-full flex items-center justify-center" :class="isvovice ? 'active' : ''">
        <PubSvgIcon v-if="!isvovice" name="icon_vovice" color="#fff" :size="24" />
        <PubSvgIcon v-else name="icon_vovice_active" color="#fff" :size="24" />
      </div>
      <div class="mt-2">
        {{ isvovice ? '喊话中' : '实时喊话' }}
      </div>
    </div> -->
  <!-- 波形绘制区域 -->
  <div v-if="isvovice" class="pt-8">
    <div style="display: inline-block; vertical-align: bottom;">
      <div ref="recwave" style="width: 10rem;height: 6rem;" />
    </div>
  </div>
  <!-- <a-button type="primary" class="mr-2" @click="recOpen">
        开始录音
      </a-button>
      <a-button type="primary" class="mr-2" @click="recStop">
        结束录音
      </a-button>
      <a-button type="primary" @click="recPlay">
        本地试听
      </a-button>
      <a-button type="primary" class="mt-2" @click="submit">
        上传录音
      </a-button> -->
</template>

<style lang="less" scoped>
.icon-box {
  flex-direction: column;
  width: 68px;
  height: 68px;
  margin-bottom: 6px;
  background: url("@/assets/images/airportDetail/nav/nav-bg0.png") no-repeat center center;
  background-size: 100% 100%;

  .active {
    border-radius: 50%;
    box-shadow: 0 0 4px 0 #00abff;
  }
}
</style>

 

posted @ 2024-11-22 16:58  abcByme  阅读(4)  评论(0编辑  收藏  举报