<template> <div id="as"> <!-- <div class="record-page"> --> <div class="recordBtmBox" @click="Record()"> <!-- 四套状态 --> <!-- 默认状态,期望用户来进行点击 --> <img class="DongImg" v-show="process == 0" src="@/assets/images/voice/ceshi1.png" style="opacity: 0.3; bottom: 5px" alt="" /> <el-tooltip content="点击开启智能语音" effect="customized"> <Transition name="slideYY-fade"> <img v-show="process == 0" class="MKF" src="@/assets/images/voice/mkf_btn.png" alt="" /> </Transition> </el-tooltip> <!-- 声音获取状态,收录语音--> <img class="DongImg" v-show="process == 1" src="@/assets/images/voice/ceshi2.png" alt="" /> <!-- 智慧语音识别中的等待状态 --> <img class="DongImg" v-show="process == 2" src="@/assets/images/voice/ceshi3.png" alt="" /> <!-- 识别成功状态,展示识别结果,并且可以点击再次触发识别 --> <img class="DongImg" v-show="process == 3" src="@/assets/images/voice/ceshi1.png" alt="" /> <div class="FontBox">{{ nowword }}</div> </div> </div> </template> <script setup name="as"> import { ref, reactive, toRefs, onMounted } from "vue"; import lamejs from "lamejs"; const { proxy } = getCurrentInstance(); import Recorder from "js-audio-recorder"; import useUserStore from "@/store/modules/user"; const userStore = useUserStore(); const lockReconnect = ref(null); const timeoutnum = ref(null); import bus from "@/bus"; import { parseTime } from "@/utils/ruoyi"; const ComShowID = ref(true); //当前大屏的菜单id const props = defineProps({ ComShowID: { type: Number, }, }); watch( () => props.ComShowID, () => { ComShowID.value = props.ComShowID; }, { immediate: true } ); var recorder = new Recorder({ sampleBits: 16, // 采样位数,支持 8 或 16,默认是16 sampleRate: 11025, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000 numChannels: 1, // 声道,支持 1 或 2, 默认是1 // compiling: false,(0.x版本中生效,1.x增加中) // 是否边录边转换,默认是false }); // 功能3 // const shibieword = ref(""); const nowword = ref(""); //当前指令进度位置 const process = ref(0); //0 :未开始 1:录音中 2:识别中 3:识别结束 const data = reactive({ recordStatus: null, //录音进程的当前位置 recognizeWs: null, }); onMounted(() => { // 绑定事件-打印的是当前录音数据 initRecognizeWs(); // initWs1(); }); onBeforeUnmount(() => { data.recognizeWs && data.recognizeWs.close(); }); // 语音识别流程方法 const Record = () => { if (process.value == 0) { // 正常流程 // process.value = 1; // 录音中 startRecorder("begin"); } else if (process.value == 3) { // 语音识别结果展示,并在2秒后回到初始化状态 setTimeout(() => { process.value = 0; nowword.value = ""; }, 2000); } else { // 中途的瞎操作就不管了 } }; //初始化语音调度websocket服务 function initRecognizeWs() { if (data.recognizeWs) { data.recognizeWs.onclose(); } let wsuri; if (window.location.protocol.includes("https")) { //线上环境 wsuri = `wss://${window.location.host}/websocket/voiceWebsocket`; } else { //本地环境 // wsuri = `ws://192.168.20.145:13002/voiceWebsocket`; wsuri = `wss://server2.wh-nf.cn:8088/websocket/voiceWebsocket`; // wsuri = `wss://jingkai.wh-nf.cn:8986/voicesWebocket`; } data.recognizeWs = new WebSocket(wsuri); //连接建立 data.recognizeWs.onopen = function (e) { console.log("连接成功", e); }; //连接建立失败 data.recognizeWs.onerror = function (evt) { console.log("连接失败", evt); reconnect(); }; data.recognizeWs.onmessage = function (e) { if (e.data != "客户端连接成功") { let data = JSON.parse(e.data); let params = data.data; console.log(parseTime(new Date()), "Websocket接收值", data); console.log("接收的data内部的data", params); // // 表明进入了文字播报的转义功能 不走指令等功能 // if (wordbusinessSourceCode.value == data.type) { // // 将返回的type数据与文字播报的业务code进行精准匹配 如果匹配上了 才说明是发送的这条数据 // if (params.audioFilePath) { // wordaudioFilePath.value = pathToUrl(params.audioFilePath); // } // } if (params.recognitionState == 1) { // 将返回的type数据与语音指令的业务code进行精准匹配 如果匹配上了 才说明是发送的这条数据 // shibieword.value = params.recognitionResult; if (params.recognitionActionCode == "error") { nowword.value = `指令未识别,请您再说一遍`; setTimeout(() => { process.value = 0; nowword.value = ""; // Record(); }, 2000); } else { nowword.value = `成功识别语音,${params.recognitionResult}`; if (process.value == 2) { process.value = 3; Record(); } if (params.recognitionActionCode == "open") { // 打开的操作 // if (params.recognitionDataSourceCode == "pump" && params.recognitionDataId) { // // 例如是泵站的操作 具体业务书写..... recognitionDataId 对象的唯一id 泵站:泵站id 站点:站点id // // pumpdia.value = true; // } else if ("其它业务") { // } switch (params.recognitionDataSourceCode) { // 图层控制 case "YuShuiFenQu": // 先清空 bus.emit("clearAllLayer"); bus.emit("SetLayerShow", ["雨水分区"]); break; case "RanqiGuanWang": // 先清空 bus.emit("clearAllLayer"); bus.emit("SetLayerShow", ["燃气管网"]); break; case "fangZhenMap": bus.emit("SetLayerShow", ["仿真地图"]); break; case "buildingLayer": bus.emit("SetLayerShow", ["三维建筑"]); break; case "ranqiRisk": bus.emit("SetLayerShow", ["燃气风险评估"]); break; case "paishuiRisk": bus.emit("SetLayerShow", ["排水风险评估"]); break; // 专题切换 case "Menu_FXPL": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "排水", id: 3, }, MenuData2: { name: "防汛排涝", id: 7, }, num: 2, }); break; case "Menu_ZHSD": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "隧道", id: 5, }, MenuData2: null, num: 1, }); break; case "Menu_ZHQL": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "桥梁", id: 4, }, MenuData2: null, num: 1, }); break; case "Menu_ZHHM": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "智慧海绵", id: 6, }, MenuData2: null, num: 1, }); break; case "Menu_ZHPS": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "排水", id: 3, }, MenuData2: null, num: 1, }); break; case "Menu_ZHRQ": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "燃气", id: 2, }, MenuData2: null, num: 1, }); break; case "Menu_ZTGL": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "总体概览", id: 1, }, MenuData2: null, num: 1, }); break; case "Menu_JCFX": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "监测分析", id: 61, }, MenuData2: null, num: 1, }); break; case "Menu_JSPG": bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "建设评估", id: 62, }, MenuData2: null, num: 1, }); break; case "Open_XQHG": // 汛情回顾 // 只在排水专题触发 if (ComShowID.value == 7) { bus.emit("openUserDialog"); } else { if (userStore.ZhuanTiType == 1) { bus.emit("ChangeZhuanTiTu", { MenuData1: { name: "排水", id: 3, }, MenuData2: { name: "防汛排涝", id: 7, }, num: 2, }); setTimeout(() => { bus.emit("openUserDialog"); }, 1000); } } break; } } else if (params.recognitionActionCode == "close") { // 关闭的操作 } else if (params.recognitionActionCode == "detail") { // 查看的操作 } else { nowword.value = `指令未识别,请您再说一遍`; } } } } }; //关闭连接 data.recognizeWs.onclose = function (e) { console.log("断开连接"); }; //重新连接 function reconnect() { if (lockReconnect.value) { return; } lockReconnect.value = true; //没连接上会一直重连,设置延迟避免请求过多 timeoutnum.value && clearTimeout(timeoutnum.value); timeoutnum.value = setTimeout(() => { lockReconnect.value = false; }, 5000); } } // 结束录音并自动上传 function stopRecorderAndupload(val) { nowword.value = "智能识别中..."; console.log(`上传录音`, parseTime(new Date())); data.recordStatus = val; recorder.stop(); uploadaudioformwebSocket(); } /** * 录音的具体操作功能 * */ // 开始录音 function startRecorder(val) { data.recordStatus = val; // 获取麦克风权限 Recorder.getPermission().then( () => { // proxy.$modal.msgSuccess("获取权限成功,开始录音"); recorder.start().then(() => { process.value = 1; nowword.value = "开始录音"; console.log(`开始录音`, parseTime(new Date())); //5秒钟后语音识别结束,并进入到下一步骤 setTimeout(() => { process.value = 2; if (process.value == 2) { // 如果手动提前点击了结束进度到下一步,那么这就可以不用走了 stopRecorderAndupload("stop"); //这里面返回结果进行监听,做了处理process.value=3,且回调Record() } }, 5000); }); }, (error) => { proxy.$modal.msgError("请先允许该网页使用麦克风"); // console.log(`${error.name} : ${error.message}`); } ); } // 将获取到的音频文件上传到服务器[通过webSocket方式] function uploadaudioformwebSocket() { const mp3Blob = convertToMp3(recorder.getWAV()); // recorder.download(mp3Blob, "recorder", "mp3"); mp3ToBase64(mp3Blob).then((stream) => { // 下面发送数据 let parms = { createBy: userStore.userInfo.userName, voiceType: "mp3", data: stream, businessSourceCode: "dpyysb", }; setTimeout(() => { if (process.value == 2) { console.log("说明没有识别且死机", process.value); // 说明没有识别且死机 process.value = 0; nowword.value = ""; initRecognizeWs(); } }, 5000); data.recognizeWs.send(JSON.stringify(parms)); }); } // function convertToMp3(wavDataView) { // 获取wav头信息 const wav = lamejs.WavHeader.readHeader(wavDataView); // 此处其实可以不用去读wav头信息,毕竟有对应的config配置 const { channels, sampleRate } = wav; const mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128); // 获取左右通道数据 const result = recorder.getChannelData(); const buffer = []; const leftData = result.left && new Int16Array(result.left.buffer, 0, result.left.byteLength / 2); const rightData = result.right && new Int16Array(result.right.buffer, 0, result.right.byteLength / 2); const remaining = leftData.length + (rightData ? rightData.length : 0); const maxSamples = 1152; for (let i = 0; i < remaining; i += maxSamples) { const left = leftData.subarray(i, i + maxSamples); let right = null; let mp3buf = null; if (channels === 2) { right = rightData.subarray(i, i + maxSamples); mp3buf = mp3enc.encodeBuffer(left, right); } else { mp3buf = mp3enc.encodeBuffer(left); } if (mp3buf.length > 0) { buffer.push(mp3buf); } } const enc = mp3enc.flush(); if (enc.length > 0) { buffer.push(enc); } return new Blob(buffer, { type: "audio/mp3" }); } function mp3ToBase64(blob) { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = (e) => { resolve(e.target.result); }; fileReader.readAsDataURL(blob); fileReader.onerror = () => { reject(new Error("blobToBase64 error")); }; }); } function closedia() { nowword.value = "你好,请点击【开始录制】,进行语音录制!"; stopRecorder(); } // 结束录音 function stopRecorder(val) { nowword.value = "录音结束"; data.recordStatus = val; recorder.stop(); } </script> <style lang="scss" scoped> #as { position: absolute; bottom: 0px; left: 50%; z-index: 999; margin-left: -200px; .recordBtmBox { position: relative; z-index: 1000; width: 400px; height: 40px; cursor: pointer; .DongImg { position: absolute; width: 400px; height: 30px; left: 0; bottom: 0; } .MKF { position: absolute; width: 30px; height: 30px; left: 190px; top: 5px; z-index: 999; } .FontBox { width: 100%; height: 30px; position: absolute; left: 0; top: 0; text-align: center; line-height: 30px; color: #ffffff; } } /* 进入和离开动画可以使用不同 持续时间和速度曲线。 */ .slideYY-fade-enter-active { transition: all 0.3s ease-out; } .slideYY-fade-leave-active { transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1); } .slideYY-fade-enter-from, .slideYY-fade-leave-to { transform: translateY(-80px); opacity: 0; } } </style> <style> .el-popper.is-customized { /* Set padding to ensure the height is 32px */ padding: 6px 12px; background: linear-gradient(90deg, rgb(103, 226, 226), rgb(3, 168, 168)); color: rgb(255, 255, 255); } .el-popper.is-customized .el-popper__arrow::before { background: linear-gradient(90deg, rgb(103, 226, 199), rgb(3, 168, 168)); right: 0; color: rgb(255, 255, 255); } </style>