喊话器功能
<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>