<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>