mirror of
https://gitee.com/xiongmao1988/rax-medical.git
synced 2025-08-24 04:54:58 +08:00
commit: 二期,语音聊天功能(bs端实现)
This commit is contained in:
parent
3480ee2af7
commit
121ae430cb
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,6 @@
|
|||
import {createApp} from 'vue'
|
||||
|
||||
|
||||
import main from './main.vue'
|
||||
import router from './router'
|
||||
import {createPinia} from 'pinia'
|
||||
|
@ -7,7 +8,7 @@ import ElementPlus, {ElDialog} from 'element-plus';
|
|||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import TableAbility from '@/components/table-ability.vue'
|
||||
|
||||
|
||||
import './assets/font/all.css'
|
||||
import 'element-plus/dist/index.css';
|
||||
import './assets/css/global.scss';
|
||||
import './assets/font/iconfont.css';
|
||||
|
|
|
@ -204,7 +204,7 @@ export const useRemoteWsStore = defineStore("remoteWs", {
|
|||
patient.chatWS.onmessage = (e: any) => {
|
||||
if (e && e.data) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.msgType == "msg") {
|
||||
if (data.msgType == "msg" || data.msgType == "audio") {
|
||||
cb(e)
|
||||
} else {
|
||||
patient.chatWS.send(JSON.stringify({msgType: "heartbeat"}))
|
||||
|
@ -268,6 +268,28 @@ export const useRemoteWsStore = defineStore("remoteWs", {
|
|||
})
|
||||
}
|
||||
},
|
||||
// 发送语音消息
|
||||
sendAudio(name: string, id: string, date: string, audio: any, index: number, cb: any) {
|
||||
const patient: any = this.patient[name + id + date + index]
|
||||
if (patient) {
|
||||
const params = {
|
||||
patientName: name,
|
||||
idNum: id,
|
||||
date: date,
|
||||
content: audio,
|
||||
msgType: "audio"
|
||||
}
|
||||
patient.chatWS.send(JSON.stringify(params))
|
||||
cb({
|
||||
status: 0
|
||||
})
|
||||
} else {
|
||||
cb({
|
||||
status: 1,
|
||||
msg: "已断开连接"
|
||||
})
|
||||
}
|
||||
},
|
||||
disconnectChat(name: string, id: string, date: string, index: number) {
|
||||
const patient: any = this.patient[name + id + date + index]
|
||||
if (patient && patient.chatWS) {
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="right-box">
|
||||
<!--带视频组件的聊天框-->
|
||||
<!--<div class="video-box" @click="playPause">-->
|
||||
<!-- <div class="icon-box">-->
|
||||
<!-- <el-icon v-if="isVideoPlay">-->
|
||||
|
@ -80,16 +81,40 @@
|
|||
<!-- <source src="@/assets/medical.mp4" type="video/mp4"/>-->
|
||||
<!-- </video>-->
|
||||
<!--</div>-->
|
||||
<!--<div class="message-box">-->
|
||||
<!-- <ul ref="msgLog" class="message-log">-->
|
||||
<!-- <li v-for="(item, index) in mssageList" :key="'msg-log-' + index"-->
|
||||
<!-- :class="{ 'align-right': item.createUser == userInfo.account }">-->
|
||||
<!-- <span>{{ item.content }}</span>-->
|
||||
<!-- </li>-->
|
||||
<!-- </ul>-->
|
||||
<!-- <div class="send-box">-->
|
||||
<!-- <el-input v-model="msgVal" placeholder="请输入消息"/>-->
|
||||
<!-- <el-button color="#006080" @click="sendMsg">发送消息</el-button>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
<!-- 聊天框, 添加音频组件 -->
|
||||
<div class="message-box">
|
||||
<ul ref="msgLog" class="message-log">
|
||||
<li v-for="(item, index) in mssageList" :key="'msg-log-' + index"
|
||||
:class="{ 'align-right': item.createUser == userInfo.userInfo.username }">
|
||||
<span>{{ item.content }}</span>
|
||||
:class="{ 'align-right': item.createUser == userInfo.account }">
|
||||
<span v-if="item.msgType === 'msg'">{{ item.content }}</span>
|
||||
<audio v-if="item.msgType === 'audio'" controls>
|
||||
21312
|
||||
<source :src="item.content" type="audio/mpeg"/>
|
||||
您的浏览器不支持音频元素。
|
||||
</audio>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="send-box">
|
||||
<el-input v-model="msgVal" placeholder="请输入消息"/>
|
||||
<el-button color="#006080" @click="sendMsg">发送消息</el-button>
|
||||
<el-input style="width: 60%" v-model="msgVal" placeholder="请输入消息"/>
|
||||
<el-button style="color: #006080; width: 20%; margin-left: 10px;" @click="sendMsg">发送消息</el-button>
|
||||
<el-button style="color: #006080; width: 20%" class="mic-btn" @mousedown="startRecording" @mouseup="stopRecording"
|
||||
@mouseleave="stopRecording">录音</el-button>
|
||||
</div>
|
||||
<div v-if="isRecording" class="mic-icon">
|
||||
<i class="fa-solid fa-microphone"></i> <!-- 麦克风图标 -->
|
||||
正在录音... {{ remainingTime }} 秒剩余
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -235,8 +260,9 @@ const medicineCustom: any[] = [
|
|||
]
|
||||
const remoteWsStore = useRemoteWsStore()
|
||||
const currentRemote = ref(remoteWsStore.getRemoteTask()[remoteWsStore.getCurrentTaskIndex()])
|
||||
const userInfo = useUserStore()
|
||||
|
||||
const userInfoStore = useUserStore()
|
||||
const userInfo = ref(userInfoStore.getlogin())
|
||||
|
||||
const chartDom1 = ref(),
|
||||
chartDom2 = ref(),
|
||||
|
@ -250,18 +276,14 @@ const chartDom1 = ref(),
|
|||
const isPatientDialog = ref(false)
|
||||
const database = ref('')
|
||||
const databaseOptions = ref([] as { value: string, label: string }[])
|
||||
const messageSum = ref(10)
|
||||
const userName = ref(userInfo.userInfo.name)
|
||||
|
||||
const setDatabaseDialog = ref(false);
|
||||
const featureTable = ref([] as any[]);
|
||||
let chartNowData = reactive({ID: 0});
|
||||
const lungAlarm = ref(false); // 肺部警告
|
||||
const heartAlarm = ref(false); // 心脏警告
|
||||
const isAIDose = ref(0); // 是否AI给药
|
||||
const isVideoPlay = ref(false); // 视频是否播放
|
||||
const videoSrc = ref('https://www.runoob.com/try/demo_source/mov_bbb.mp4');
|
||||
const mssageList = ref([] as any);
|
||||
const msgVal = ref('');
|
||||
// const videoSrc = ref('https://www.runoob.com/try/demo_source/mov_bbb.mp4');
|
||||
const unusual = ref([] as any);
|
||||
const fixedTableData = ref([] as any[]);
|
||||
const varTableData = ref([] as any[]);
|
||||
|
@ -419,7 +441,7 @@ const subscribeVital = () => {
|
|||
chartDom3.value.updateChartData(data.vitalSignsList[0]);
|
||||
chartDom4.value.updateChartData(data.vitalSignsList[0]);
|
||||
isAIDose.value = data.flags.aiFlag === '1' ? 1 : 0;
|
||||
console.log('data >>>>>', data);
|
||||
// console.log('data >>>>>', data);
|
||||
if (!data.rateModTime) {
|
||||
updateMedicineTable(data.medicineList);
|
||||
return
|
||||
|
@ -718,6 +740,123 @@ function startAI() {
|
|||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* 聊天室
|
||||
*/
|
||||
const msgVal = ref('');
|
||||
const mssageList = ref([] as any);
|
||||
const isRecording = ref(false); // 用于跟踪录音状态
|
||||
const mediaRecorder = ref<MediaRecorder | null>(null);
|
||||
const audioChunks = ref<Blob[]>([]);
|
||||
const remainingTime = ref(10); // 初始化剩余时间为 10 秒
|
||||
|
||||
// 将 Blob 对象转换为 Base64 字符串
|
||||
const convertBlobToBase64 = (blob: Blob): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader(); // 创建 FileReader 实例
|
||||
|
||||
// 定义 onloadend 事件
|
||||
reader.onloadend = () => {
|
||||
// 检查 result 是否不为 null
|
||||
if (reader.result) {
|
||||
// const base64String = (reader.result as string).split(',')[1]; // 获取 Base64 部分
|
||||
const base64String = (reader.result as string); // 获取 Base64 部分
|
||||
resolve(base64String); // 返回 Base64 字符串
|
||||
} else {
|
||||
reject(new Error("读取 Base64 失败")); // 处理读取失败情况
|
||||
}
|
||||
};
|
||||
|
||||
// 定义错误处理
|
||||
reader.onerror = (error) => {
|
||||
reject(error); // 处理读取过程中可能出现的错误
|
||||
};
|
||||
|
||||
// 将 Blob 读取为数据 URL
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
// 开始录音
|
||||
const startRecording = async () => {
|
||||
if (isRecording.value) return; // 预防多次点击
|
||||
isRecording.value = true; // 设置录音状态为开启
|
||||
remainingTime.value = 10; // 重置剩余时间为 10 秒
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
|
||||
mediaRecorder.value = new MediaRecorder(stream);
|
||||
|
||||
// 监听数据可用事件
|
||||
mediaRecorder.value.ondataavailable = (event) => {
|
||||
console.log("录音中...");
|
||||
audioChunks.value.push(event.data);
|
||||
console.log("当前音频数据块:", event.data); // 这里打印每个数据块
|
||||
};
|
||||
|
||||
// 监听停止事件
|
||||
mediaRecorder.value.onstop = async () => {
|
||||
isRecording.value = false; // 停止录音状态
|
||||
|
||||
if (audioChunks.value.length === 0) {
|
||||
console.error("没有音频数据可用于创建 Blob");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 Blob 数据
|
||||
const audioBlob = new Blob(audioChunks.value, {type: 'audio/webm; codecs=opus'});
|
||||
|
||||
try {
|
||||
// 转换为 Base64
|
||||
const base64Audio = await convertBlobToBase64(audioBlob);
|
||||
console.log("转换后的 Base64 字符串:", base64Audio);
|
||||
|
||||
// 准备 WebSocket 消息
|
||||
const index = remoteWsStore.getCurrentTaskIndex();
|
||||
// 发送消息
|
||||
remoteWsStore.sendAudio(
|
||||
currentRemote.value.patient,
|
||||
currentRemote.value.patientId,
|
||||
currentRemote.value.date,
|
||||
base64Audio.replace(/\s+/g, ''), // 移除空白字符
|
||||
index,
|
||||
function (res: any) {
|
||||
if (res.code == 1) {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("转换为 Base64 失败:", error);
|
||||
}
|
||||
|
||||
audioChunks.value = []; // 可选: 重置音频块
|
||||
};
|
||||
|
||||
// 启动录音
|
||||
mediaRecorder.value.start();
|
||||
console.log("录音已开始"); // 别忘了在此处添加提示
|
||||
|
||||
// 设置最大录音时间为 10 秒,创建倒计时
|
||||
const timer = setInterval(() => {
|
||||
if (remainingTime.value > 0) {
|
||||
remainingTime.value--; // 每秒减少1
|
||||
} else {
|
||||
stopRecording(); // 达到0秒时自动停止
|
||||
console.log("录音时间到,已自动停止!");
|
||||
clearInterval(timer); // 清除倒计时
|
||||
}
|
||||
}, 1000); // 每秒执行一次
|
||||
};
|
||||
|
||||
// 停止录音
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorder.value) {
|
||||
mediaRecorder.value.stop();
|
||||
isRecording.value = false; // 设置录音状态为关闭
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1007,34 +1146,107 @@ function startAI() {
|
|||
}
|
||||
}
|
||||
|
||||
// v2带视频的msg-box
|
||||
//.message-box {
|
||||
// width: 100%;
|
||||
// // height: 270px;
|
||||
// height: 100%;
|
||||
// // margin-bottom: 5px;
|
||||
//
|
||||
// .message-log {
|
||||
// width: 100%;
|
||||
// height: calc(100% - 40px);
|
||||
// max-height: 109px;
|
||||
// padding: 16px 20px;
|
||||
// box-sizing: border-box;
|
||||
// border: 1px solid #c8c8c8;
|
||||
// background: #f8f8f8;
|
||||
// overflow-y: auto;
|
||||
//
|
||||
// li {
|
||||
// width: 100%;
|
||||
// font-size: 14px;
|
||||
// line-height: 1.6;
|
||||
// margin: 5px 0;
|
||||
//
|
||||
// &.align-right {
|
||||
// text-align: right;
|
||||
//
|
||||
// span {
|
||||
// background: $main-color;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// span {
|
||||
// display: inline-block;
|
||||
// max-width: 80%;
|
||||
// padding: 6px 8px;
|
||||
// box-sizing: border-box;
|
||||
// border-radius: 8px;
|
||||
// color: white;
|
||||
// letter-spacing: 1px;
|
||||
// background: #92b3c1;
|
||||
// text-align: left;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// .send-box {
|
||||
// width: 100%;
|
||||
// height: 40px;
|
||||
// display: flex;
|
||||
// justify-content: space-between;
|
||||
// align-items: flex-end;
|
||||
//
|
||||
// .el-input {
|
||||
// width: calc(100% - 110px);
|
||||
// height: 32px;
|
||||
//
|
||||
// :deep(.el-input__wrapper) {
|
||||
// background-color: #F2F3F5;
|
||||
// border-color: #C1C1C1;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// .el-button {
|
||||
// padding: 0;
|
||||
// width: 100px;
|
||||
// line-height: 30px;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
.message-box {
|
||||
width: 100%;
|
||||
// height: 270px;
|
||||
height: 300px;
|
||||
height: 100%;
|
||||
// margin-bottom: 5px;
|
||||
|
||||
.message-log {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
max-height: 109px;
|
||||
height: 100%;
|
||||
max-height: calc(100% - 35px);
|
||||
padding: 16px 20px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #c8c8c8;
|
||||
background: #f8f8f8;
|
||||
overflow-y: auto;
|
||||
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 5px 0;
|
||||
|
||||
|
||||
&.align-right {
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
background: $main-color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
span {
|
||||
|
@ -1045,6 +1257,7 @@ function startAI() {
|
|||
border-radius: 8px;
|
||||
color: white;
|
||||
letter-spacing: 1px;
|
||||
background: $main-color;
|
||||
background: #92b3c1;
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -1123,4 +1336,29 @@ function startAI() {
|
|||
}
|
||||
}
|
||||
|
||||
// 麦克风图标样式
|
||||
.send-box {
|
||||
position: relative; /* 设置相对定位用于绝对定位子元素的参考 */
|
||||
}
|
||||
|
||||
.mic-icon {
|
||||
position: fixed; /* 使用固定定位 */
|
||||
bottom: 20px; /* 距离底部20px */
|
||||
left: 50%; /* 水平居中 */
|
||||
transform: translateX(-50%); /* 使图标真正居中 */
|
||||
background-color: rgba(255, 255, 255, 0.9); /* 背景颜色 */
|
||||
border-radius: 5px; /* 圆角 */
|
||||
padding: 10px; /* 内边距 */
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* 阴影效果 */
|
||||
z-index: 1000; /* 确保图标在其他元素之上 */
|
||||
display: flex; /* 使用 flexbox 布局 */
|
||||
align-items: center; /* 垂直居中 */
|
||||
}
|
||||
|
||||
.mic-icon .fa-microphone {
|
||||
color: red; /* 麦克风图标颜色 */
|
||||
font-size: 80px; /* 调整图标大小 */
|
||||
margin-right: 5px; /* 图标与文本间的间距 */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue
Block a user