<template> <div class="RongYun" id="RongYun"> <div class="rong-container" id="rongContainer"> <div class="rong-action" id="rong-action"> <div class="textDiv2"> <div class="textTitle"> <span class="dote"></span> <span>接收用户:</span> </div> <div class="userList" v-if="allData.userList.length > 0"> <template v-for="(item, index) in allData.userList" :key="index"> <div class="user" v-if="item.phone != userId"> <div class="userInfoClass"> <span class="username">{{ item.name }}:</span> <div class="ceninfoBox"> <div class="userphone" v-if="item.phone"> {{ geTel(item.phone) }} </div> <div class="userphone userdept">{{ item.deptName }}</div> </div> </div> <div> <span style="margin-left: 10px" :class="'classType' + item.ClassType">{{ item.typeFont }}</span> </div> </div> </template> </div> </div> <div class="textDiv"> <span class="dote"></span> <div class="text"> <span>发起人:</span><span class="Rightvalue" @click="getToken()">{{ userinfo.userName }}</span> </div> </div> <div class="textDiv"> <span class="dote"></span> <div class="text"> <span>当前登录用户 ID:</span><span id="userId" class="Rightvalue" @click="getToken()">{{ userId + '_web' }}</span> </div> </div> <div class="textDiv"> <span class="dote"></span> <div class="text"> <span>通话类型:</span> <el-radio-group v-model="mediaType" style="display: flex; justify-content: center; align-items: center" @change="changeMediaType" > <el-radio :label="2">视频</el-radio> <el-radio :label="1">语音</el-radio> </el-radio-group> </div> </div> <div id="inviteList" class="textDiv"> <span class="dote"></span> <div class="text"> <span>选择邀请人:</span> <el-select v-model="Getidinvite" placeholder="选择被邀请人" filterable multiple collapse-tags :popper-append-to-body="true" popper-class="eloption" > <el-option v-for="dict in inviteuserIdsList" :key="dict.id" :label="dict.name" :value="dict.id"></el-option> </el-select> </div> </div> <div class="rong-login-box"> <div class="btn callBtn" id="call" @click="callHandler()"> <img src="/src/assets/images/rongYunImg/callic.png" alt="" /> 发起通话 </div> <div class="btn hunupBtn hangBtn" id="hunup" @click="hungup()"> <img src="/src/assets/images/rongYunImg/hangic.png" alt="" /> 挂断 </div> <div class="btn inviteBtn hunupBtn" id="invite" @click="inviteCall()">邀请</div> </div> </div> <div id="videoView" class="rong-video-box newbox"></div> </div> </div> </template> <script setup> import moment from 'moment'; import useUserStore from '@/store/modules/user'; import { ElMessageBox } from 'element-plus'; import bus from '@/utils/util'; import rongyunStore from '@/store/modules/rongyunStore'; import { listrcloudUnitUser, getUserStatus, rongyunGroupSave, groupJoinUsers, batchAdd, answer, hangUp, } from '@/views/RongyunCommunication/rongyunjs/rongyunApi.js'; import { findCurrentTreeObj } from '@/utils/ruoyi'; import { RCCallErrorCode } from '@rongcloud/plugin-call'; const appStore = useUserStore(); const useRongyunStore = rongyunStore(); const { proxy } = getCurrentInstance(); const inviteuserIdsList = ref([]); //被邀请人的列表 const Getidinvite = ref([]); const allData = reactive({ typeList: [ { label: '视频', value: 2, }, { label: '语音', value: 1, }, ], userId: appStore.userInfo.phonenumber, userName: '', mediaType: useRongyunStore.mediaType, userList: [], idList: [], userinfo: appStore.userInfo, unitArr: [], }); const { typeList, userId, userName, mediaType, userList, idList, userinfo, unitArr } = toRefs(allData); const props = defineProps({ ryUser: Object, callType: Number, }); const paramSave = ref({ groupId: undefined, //群聊Id promoterThridUserId: appStore.userInfo.phonenumber + '_web', //发起人id answerThridUserIds: '', //接收人id type: '', //通话类型 1是视频 2是语音 createBy: localStorage.getItem('userNo'), }); onMounted(() => { nextTick(() => { initGroupData(); }); bus.on('removeEle', e => { removeVideoEl(); }); }); onBeforeUnmount(() => { close(); }); const geTel = tel => { var reg = /^(\d{3})\d{4}(\d{4})$/; return tel.replace(reg, '$1****$2'); }; // 处理群呼数据 const initGroupData = () => { console.log('props.ryUser 处理群呼/单呼数据', props.ryUser); allData.userList = []; if (props.callType == 2) { let arr = [].concat(props.ryUser.userList); arr.forEach(v => { allData.userList.push({ name: v.name, phone: v.registerThridUserId, typeFont: null, ClassType: null, }); }); } else { allData.userList.push({ name: props.ryUser.name, phone: props.ryUser.phone, typeFont: null, ClassType: null, }); } console.log('allData.userList', allData.userList); }; const getDom = key => { return document.querySelector(key); }; const changeMediaType = value => { console.log('value', value); useRongyunStore.SET_MediaType(value); }; /** * CallSession 事件 */ const getCallSessionEvent = () => { return { onRinging: sender => { // 当远端用户已开始响铃, 该函数有 2 个参数: sender 是发送者,session 是通话实例。这时用户可以在业务层做响铃的 UI 展示。 // ; proxy.$modal.msgWarning(` ${sender.userId} 振铃`); allData.userList.forEach(element => { if (element.phone == sender.userId) { element.typeFont = '对方已响铃'; element.ClassType = 1; } }); }, onAccept: sender => { // 当远端用户已同意接听, 该函数有 2 个参数: sender 是发送者,session 是通话实例。这时用户可以把 UI 的‘响铃’变成‘通话中’。 proxy.$modal.msgWarning(` ${sender.userId} 已接听`); console.log('接听监听,', allData.userList, sender); allData.userList.forEach(element => { if (element.phone == sender.userId) { element.typeFont = '已接听'; element.ClassType = 2; element.answerType = 4; acceptApi(element); } }); }, onHungup: (sender, reason) => { // 当有远端用户挂断, 该函数有 3 个参数: sender 是发送者,reason 是挂断原因,session 是通话实例。这时用户可以在 UI 层提示‘xxx已挂断’。 proxy.$modal.msgWarning(` ${sender.userId} 已挂断`); console.log('挂断监听,', allData.userList, sender); console.log('挂断监听挂断原因,', reason); allData.userList.forEach(element => { if (element.phone == sender.userId) { element.typeFont = '已挂断'; element.answerType = reason; // 挂断原因转义成后端所需字段 // 1.未响铃 2.响铃未接听 3.响铃后挂断 4.已接听 // 响铃后挂断 // if (reason == 12) { // element.answerType = 3; // } // // 响铃未接听 // if (reason == 15) { // element.answerType = 2; // } // // 已接听 // if (reason == 13) { // element.answerType = 4; // } hungupApi(element); } }); // 群组中移除相应节点 const videoViewDom = getDom('#videoView'); const videoDom = getDom(`#video-${sender.userId}`); videoDom && videoViewDom.removeChild(videoDom); const itemdomList = document.getElementsByClassName('video-item'); if (itemdomList.length <= 1) { restore(); } }, onTrackReady: track => { // 当本端资源或远端资源已获取, 该函数有 2 个参数:track 是本端资源或远端资源, session 是通话实例。这时用户可以用拿到的 track 播放音频、视频。 setTimeout(() => { appendVideoEl(track); }, 1500); // 当本端资源或远端资源已获取, 该函数有 2 个参数:track 是本端资源或远端资源, session 是通话实例。这时用户可以用拿到的 track 播放音频、视频。 if (!track.isLocalTrack()) { proxy.$modal.msgWarning('通话已建立'); } }, onMemberModify: (sender, invitedUsers, session) => { console.log('触发邀请监听', sender, invitedUsers, session); console.log('触发邀请监听', allData.userList); // 群组通话中有其他人被邀请加入, 该函数有 3 个参数: sender 是发送者,invitedUsers 是被邀请的用户列表, session 是通话实例。这时用户可以在 UI 层提示‘xxx加入通话’。 }, onMediaModify: sender => { // 通话类型改变时触发, 该函数有3个参数: sender 是发送者,mediaType 通话类型, session 是通话实例。这时用户可以在 UI 层提示‘已降级成音频通话’。 }, onAudioMuteChange: muteUser => { // 对方静音后触发, 该函数有2个参数: muteUser 是已静音的用户, session 是通话实例。这时用户可以在 UI 层提示‘xxx已静音’。 proxy.$modal.msgWarning(` ${sender.userId} 已静音`); }, onVideoMuteChange: muteUser => { // 对方禁用视频后触发, 该函数有2个参数: muteUser 是已禁用视频的用户, session 是通话实例。这时用户可以在 UI 层提示‘xxx已禁用视频’。 proxy.$modal.msgWarning(` ${sender.userId} 已禁用视频`); }, }; }; /** * callSession 事件注册 */ const registerCallSessionEvent = session => { const events = getCallSessionEvent(); session.registerSessionListener(events); }; const callHandler = () => { console.log('..??', props); props.callType == 1 ? rongYunCall() : groupCall(); }; // 单呼 const call = () => { getDom('#call').style.display = 'none'; getDom('#hunup').style.display = 'block'; const events = getCallSessionEvent(); console.log('props.ryUser', props.ryUser); console.log('props.ryUser.phone2', props.ryUser.phone); // 这里是传给融云拨打电话的传递userid const params = { targetId: props.ryUser.phone, mediaType: useRongyunStore.mediaType, listener: events, }; useRongyunStore.callClient.call(params).then(({ code, session }) => { if (code === 10000) { registerCallSessionEvent(session); useRongyunStore.SET_CALLSESSION(session); saveOnAccept(); } else { proxy.$modal.msgWarning(`呼叫失败,错误原因:${code}`); } }); }; //单呼前判登录用户在线状态 const rongYunCall = async item => { console.log('allData.userId', allData.userId, appStore.userInfo); let params = { registerThridUserId: allData.userId + '_web', }; let res1 = await getUserStatus(params); let flag1 = null; if (res1.code == 200) { flag1 = JSON.parse(res1.data); } if (flag1 != 1) { ElMessageBox.confirm(`${appStore.userInfo.userName || '当前'}用户视频会商不在线,是否重新连接?, "提示"`, { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }) .then(() => { setTimeout(() => { bus.emit('rystatus'); getCallStatus(); }, 1500); }) .catch(() => {}); } else { setTimeout(() => { getCallStatus(); }, 1500); } }; //单呼前判对方用户在线状态 const getCallStatus = async () => { let params = { registerThridUserId: props.ryUser.phone, }; let res = await getUserStatus(params); let flag = null; if (res.code == 200) { flag = JSON.parse(res.data); } if (flag != 1) { return proxy.$modal.msgWarning(`${props.ryUser.name}用户视频会商不在线!`); } else { call(); } }; //群呼 const groupCall = () => { getDom('#call').style.display = 'none'; getDom('#hunup').style.display = 'block'; getDom('#invite').style.display = 'block'; getDom('#inviteList').style.visibility = 'visible'; console.log('allData.userList', allData.userList, props.ryUser); allData.userList.forEach(v => { v.typeFont = '已拨号'; v.ClassType = 0; v.answerType = 1; }); const events = getCallSessionEvent(); const params = { targetId: props.ryUser.groupId, userIds: props.ryUser.userIds.split(','), mediaType: useRongyunStore.mediaType, listener: events, }; console.log('param', params); useRongyunStore.callClient.callInGroup(params).then(({ code, session }) => { if (code === 10000) { registerCallSessionEvent(session); useRongyunStore.SET_CALLSESSION(session); saveOnAccept(); GetallOnline(); } else { const reason = code === RCCallErrorCode.NOT_IN_GROUP ? '当前用户未加入群组' : code; proxy.$modal.msgWarning(`呼叫失败,错误原因:${code}`); removeVideoEl(); } }); }; let endids = ref([]); // 邀请用户加入当前通话(仅限群组) const inviteCall = () => { if (Getidinvite.value && Getidinvite.value.length) { console.log('点击了邀请', Getidinvite.value, inviteuserIdsList.value); let ids = []; let GetuserList = []; Getidinvite.value.map(item => { inviteuserIdsList.value.map(p => { if (item == p.id) { GetuserList.push(p); } }); }); console.log('点击了邀请', GetuserList); GetuserList.map(item => { item.userTokenList.length && item.userTokenList.map(p => { if (p.isOnlion) { ids.push(p.registerThridUserId); } }); }); endids.value = ids; let params = { groupId: props.ryUser.groupId, ids: ids.join(','), }; groupJoinUsers(params).then(res => { console.log(res); console.log('点击了邀请', ids); useRongyunStore.callSession.invite(ids).then(({ code }) => { console.log(code); if (code === 10000) { proxy.$modal.msgSuccess(`邀请成功`); } else { proxy.$modal.msgWarning(`邀请失败,错误原因:${code}`); } }); }); } else { proxy.$modal.msgWarning(`请选择邀请人`); } }; // 批量新增融合通讯通话 const saveOnAccept = () => { console.log('批量新增', props.ryUser); paramSave.value.type = useRongyunStore.mediaType; paramSave.value.groupId = props.callType == 1 ? -1 : props.ryUser.groupId; paramSave.value.answerThridUserIds = props.ryUser.phone || props.ryUser.userIds; //接听人单呼就是phone userIds 两个 数据不一样 console.log('paramSave.value', paramSave.value, props.ryUser); batchAdd(paramSave.value).then(res => { if (res.code == 200) { console.log('批量新增融合通讯通话'); } }); }; // 接听后端接口 const acceptApi = item => { console.log('调取了接听接口', item); let params = { promoterThridUserId: appStore.userInfo.phonenumber + '_web', //发起人 当前登陆人 answerThridUserId: item.phone, //接听人 updateBy: localStorage.getItem('userNo'), //当前登录用户 }; answer(params).then(res => { console.log('调取了接听接口', res); }); }; // 挂断融合通讯通话 const hungupApi = item => { console.log('调取了挂断接口', item); let params = { promoterThridUserId: appStore.userInfo.phonenumber + '_web', //发起人 answerThridUserId: item.phone, //接听人 updateBy: localStorage.getItem('userNo'), answerType: item.answerType || null, }; hangUp(params).then(res => { console.log('调取了挂断接口', res); }); }; // 获取所有注册用户 const GetallOnline = item => { inviteuserIdsList.value = []; console.log('被邀请人', props.ryUser); // let havaUser = []; // props.ryUser.userIds.map(item => { // havaUser.push(item.split('_')[0]); // }); // console.log('被邀请人', havaUser); listrcloudUnitUser().then(res => { res.data.map(item => { if (item.isOnlion && !props.ryUser.userIds.includes(item.phone)) { inviteuserIdsList.value.push(item); } }); console.log('被邀请人', inviteuserIdsList.value, props.ryUser.userIds); }); }; /** * video 视图渲染 */ const appendVideoEl = async track => { const uid = track.getUserId(); let params = { phone: uid.split('_')[0], }; let mediaType = useRongyunStore.mediaType; let data = await listrcloudUnitUser(params); console.log('datar', data); if (data.code == 200) { const container = getDom('#videoView'); const node = document.createElement('div'); node.setAttribute('id', `video-${uid}`); if (allData.idList.findIndex(i => i == `video-${uid}`) == -1) { allData.idList.push(`video-${uid}`); } let videoTpl = null; if (mediaType == 1) { videoTpl = `<span class="video-user-id">${data.data[0].name}: ${uid} </span><span class="video-media-type"></span><video id="${uid}"></video>`; } else { videoTpl = `<span class="video-user-id">${data.data[0].name}: ${uid} </span><video id="${uid}"></video>`; } node.innerHTML = videoTpl; node.classList.add('video-item'); let length = allData.userList.length + endids.value.length; console.log('什么东西xxxxx', allData.userId, uid, props.callType, length); // 根据人数判断渲染不同的视图样式 if (allData.userId + '_web' == uid && length <= 2) { // 判断是当前用户 人数小于等于2人时给予单独样式 node.classList.add('first-child'); } if (length > 2 && length <= 4) { node.classList.add('video-item-4'); } if (length > 4 && length <= 8) { node.classList.add('video-item-8'); } if (track.isAudioTrack()) { if (mediaType == 1) { container.appendChild(node); } track.play(); } else { container.appendChild(node); const videoEl = document.getElementById(`${track.getUserId()}`); track.play(videoEl); } const targetElements = document.querySelectorAll('.video-item'); console.log('targetElements', targetElements); targetElements.forEach(target => { target.classList.remove('video-item-4'); target.classList.remove('video-item-8'); if (length > 2 && length <= 4) { target.classList.add('video-item-4'); } if (length > 4 && length <= 8) { target.classList.add('video-item-8'); } }); } }; /** * 挂断当前 callSession */ const hungup = () => { // console.log("1231------------------------", useRongyunStore.callSession); useRongyunStore.callSession.hungup().then(({ code }) => { if (code === 10000) { // proxy.$modal.msgWarning("挂断成功"); getDom('#call').style.display = 'block'; getDom('#hunup').style.display = 'none'; getDom('#invite').style.display = 'none'; getDom('#inviteList').style.visibility = 'hidden'; removeVideoEl(); restore(); } else { proxy.$modal.msgWarning(`挂断失败,错误原因:${code}`); } allData.userList.forEach(element => { element.typeFont = '已挂断'; element.ClassType = 3; element.answerType = code; hungupApi(element); }); }); }; const removeVideoEl = () => { getDom('#videoView').innerHTML = ''; getDom('#videoView').style = ''; getDom('#call').style.display = 'block'; getDom('#hunup').style.display = 'none'; getDom('#invite').style.display = 'none'; getDom('#inviteList').style.visibility = 'hidden'; allData.idList = []; }; const close = () => { if (useRongyunStore.callSession != null) { hungup(); } restore(); }; // 输出组件的方法,让外部组件可以调用 defineExpose({ close, }); // 还原 const restore = () => { let style = { width: '934px' }; getDom('#rong-action').style.display = 'block'; const dom = getDom('#RongYun'); const dom1 = getDom('#videoView'); getDom('#videoView').style = ''; allData.idList.forEach((item, index) => { let videodom = getDom(`#${item}`); if (videodom) { videodom.style = null; } const titledom = getDom(`#${item} .video-user-id`); if (titledom) { titledom.style = null; } }); }; </script> <style lang="scss"> @import '@/views/RongyunCommunication/rongyuncss/index.scss'; .rong-container { padding: 0 20px; display: flex; width: 100%; height: 100%; .rong-action { width: 35%; display: flex; flex-direction: column; margin-right: 20px; #inviteList { visibility: hidden; } .textDiv { min-height: 38px; line-height: 38px; display: flex; align-items: center; :deep(.el-radio) { color: #fff; } .dote { display: block; margin-right: 20px; width: 10px; height: 10px; background: #58fdff; border-radius: 10px; } .text { display: flex; flex: 1; justify-content: space-between; font-size: 16px; font-family: Alibaba PuHuiTi; font-weight: 500; color: #81b6d4; } .Rightvalue { max-width: 160px; overflow: hidden; //溢出隐藏 text-overflow: ellipsis; //超出显示省略号 white-space: nowrap; //强制文本在一行内显示 color: #fff; } } .textDiv2 { // min-height: 45px; // line-height: 45px; .dote { display: block; margin-right: 20px; width: 10px; height: 10px; background: #58fdff; border-radius: 10px; } .textTitle { height: 40px; line-height: 40px; display: flex; align-items: center; font-size: 16px; font-weight: 500; color: #81b6d4; } .userList { max-height: 220px; overflow-y: auto; .user { // background-color: #14306a; // padding: 0 10px; // margin-left: 20px; // border-bottom: 1px solid #0a0a0a; display: flex; justify-content: space-between; background: #093a73; align-items: center; border-radius: 1px; padding: 2px 8px; } .username { min-width: 52px; font-weight: 500; color: #13bfff; } .userphone { font-weight: bold; color: #ffffff; } .userInfoClass { display: flex; align-items: center; } .ceninfoBox { padding-left: 5px; .userdept { max-width: 140px; font-size: 12px; text-align: center; } } } } .rong-login-box { // margin-left: 20px; display: flex; width: 300px; height: 40px; align-items: center; margin-top: 50px; .btn { margin-right: 20px; background: #14306a; border-radius: 4px; padding: 6px 10px; font-size: 16px; font-family: Alibaba PuHuiTi; font-weight: 400; display: flex; justify-content: space-around; align-items: center; color: #fff; margin: 0 auto; cursor: pointer; img { width: 18px; height: 18px; } } .callBtn { background: #67c23a; } .hangBtn { background: #ff3a5a; } .inviteBtn { background: #158bd2; } .hunupBtn { display: none; } .acceptBtn { display: none; } } } .rong-video-box { width: 70%; height: 600px; display: flex; flex-wrap: wrap; overflow: auto; position: relative; .video-item { box-sizing: border-box; width: 100%; height: calc(100% - 10px); position: relative; .video-user-id { position: absolute; top: 5px; padding: 0 5px; border-radius: 2px; z-index: 100; font-size: 14px; color: #58fdff; display: inline-block; width: 100%; text-align: center; } .video-media-type { position: absolute; top: 30px; left: 0; display: block; width: 100%; height: calc(100% - 30px); background: url('@/assets/images/rongYunImg/yuyin_bg.png') no-repeat; background-size: 100% 100%; } video { width: 100%; height: 100%; } } .video-item-4 { width: 50%; height: calc(50% - 10px); } .video-item-8 { width: 25%; height: calc(50% - 10px); } .first-child { position: absolute; right: 0px; top: 0; width: 40%; height: 30%; z-index: 999; } } } .eloption { z-index: 20000 !important; } </style> <style lang="scss" scoped> .RongYun { width: 100%; height: calc(100% - 40px); position: relative; .closeIcon { position: absolute; right: 0px; top: -30px; &:hover { color: chocolate; } } } </style>