chore: refact webRTC relative logic and update npm module
This commit is contained in:
4
.env_template
Normal file
4
.env_template
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
STUN_URL=''
|
||||||
|
TURN_URL=''
|
||||||
|
TURN_USERNAME=''
|
||||||
|
TURN_CREDENTIAL=''
|
||||||
64
.gitignore
vendored
64
.gitignore
vendored
@@ -1,30 +1,34 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
/cypress/videos/
|
/cypress/videos/
|
||||||
/cypress/screenshots/
|
/cypress/screenshots/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
|
||||||
|
# dev environment variables
|
||||||
|
.env
|
||||||
9178
package-lock.json
generated
9178
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
72
package.json
72
package.json
@@ -1,34 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "myplayer_vue",
|
"name": "myplayer_vue",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"proxy": "node proxy.js"
|
"proxy": "node proxy.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dplayer": "^1.27.1",
|
"dotenv": "^17.3.1",
|
||||||
"element-plus": "^2.9.4",
|
"dplayer": "^1.27.1",
|
||||||
"express": "^4.21.2",
|
"element-plus": "^2.9.4",
|
||||||
"hls.js": "^1.5.20",
|
"express": "^4.21.2",
|
||||||
"idb": "^8.0.2",
|
"hls.js": "^1.5.20",
|
||||||
"node-fetch": "^3.3.2",
|
"idb": "^8.0.2",
|
||||||
"pinia": "^3.0.1",
|
"node-fetch": "^3.3.2",
|
||||||
"pinia-plugin-persistedstate": "^4.2.0",
|
"pinia": "^3.0.1",
|
||||||
"video.js": "^8.21.0",
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
"videojs-vtt.js": "^0.15.5",
|
"video.js": "^8.21.0",
|
||||||
"vue": "^3.5.13",
|
"videojs-vtt.js": "^0.15.5",
|
||||||
"vue-axios": "^3.5.2",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-axios": "^3.5.2",
|
||||||
},
|
"vue-router": "^4.5.0"
|
||||||
"devDependencies": {
|
},
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"vite-plugin-vue-devtools": "^7.7.1"
|
"vite": "^6.0.11",
|
||||||
}
|
"vite-plugin-vue-devtools": "^7.7.1"
|
||||||
}
|
},
|
||||||
|
"overrides": {
|
||||||
|
"axios": "^1.13.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,162 +1,159 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="navbar-left">
|
<div class="navbar-left">
|
||||||
<router-link to="/home" class="navbar-logo">
|
<router-link to="/home" class="navbar-logo">
|
||||||
<img :src="logo" alt="MyPlayer" />
|
<img :src="logo" alt="MyPlayer" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-right">
|
<div class="navbar-right">
|
||||||
<router-link to="/home/search">搜索</router-link>
|
<router-link to="/home/search">搜索</router-link>
|
||||||
<router-link to="/home/friends">好友</router-link>
|
<router-link to="/home/friends">好友</router-link>
|
||||||
<router-link to="/home/group">群聊</router-link>
|
<router-link to="/home/group">群聊</router-link>
|
||||||
<router-link to="/home/playroom">Playroom</router-link>
|
<router-link to="/home/playroom">Playroom</router-link>
|
||||||
<div class="more-dropdown" @click="toggleDropdown" @blur="closeDropdown">
|
<div class="more-dropdown" @click="toggleDropdown" @blur="closeDropdown">
|
||||||
更多
|
更多
|
||||||
<ul v-if="isDropdownOpen" class="dropdown-content">
|
<ul v-if="isDropdownOpen" class="dropdown-content">
|
||||||
<li>设置</li>
|
<li>设置</li>
|
||||||
<li @click="aboutUs">关于我们</li>
|
<li @click="aboutUs">关于我们</li>
|
||||||
<li>帮助</li>
|
<li>帮助</li>
|
||||||
<li @click="logout">退出登录</li>
|
<li @click="logout">退出登录</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, initCustomFormatter } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import logo from '../assets/logo.png';
|
import logo from '../assets/logo.png';
|
||||||
import { onlineSocketStore } from '@/store/Online';
|
import { onlineSocketStore } from '@/store/Online';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { voiceStore } from '@/store/Voice';
|
import { groupMessageStore } from '@/store/message.ts';
|
||||||
import { groupMessageStore } from '@/store/message.ts';
|
import { userInfoStore } from '@/store/user';
|
||||||
import { userInfoStore } from '@/store/user';
|
|
||||||
|
const userInfo = userInfoStore()
|
||||||
const userInfo = userInfoStore()
|
const socket = onlineSocketStore()
|
||||||
const voice = voiceStore()
|
const groupMessage = groupMessageStore()
|
||||||
const socket = onlineSocketStore()
|
|
||||||
const groupMessage = groupMessageStore()
|
|
||||||
|
const isDropdownOpen = ref(false); // 控制下拉框的显示与隐藏
|
||||||
|
|
||||||
const isDropdownOpen = ref(false); // 控制下拉框的显示与隐藏
|
const toggleDropdown = () => {
|
||||||
|
isDropdownOpen.value = !isDropdownOpen.value; // 切换下拉框的显示状态
|
||||||
const toggleDropdown = () => {
|
};
|
||||||
isDropdownOpen.value = !isDropdownOpen.value; // 切换下拉框的显示状态
|
|
||||||
};
|
const closeDropdown = () => {
|
||||||
|
isDropdownOpen.value = false; // 关闭下拉框
|
||||||
const closeDropdown = () => {
|
};
|
||||||
isDropdownOpen.value = false; // 关闭下拉框
|
|
||||||
};
|
// 监听全局点击事件
|
||||||
|
const handleGlobalClick = (event) => {
|
||||||
// 监听全局点击事件
|
// 检查点击是否发生在下拉框外部
|
||||||
const handleGlobalClick = (event) => {
|
if (!event.target.closest('.more-dropdown')) {
|
||||||
// 检查点击是否发生在下拉框外部
|
closeDropdown();
|
||||||
if (!event.target.closest('.more-dropdown')) {
|
}
|
||||||
closeDropdown();
|
};
|
||||||
}
|
|
||||||
};
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleGlobalClick);
|
||||||
onMounted(() => {
|
});
|
||||||
document.addEventListener('click', handleGlobalClick);
|
|
||||||
});
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleGlobalClick);
|
||||||
onUnmounted(() => {
|
});
|
||||||
document.removeEventListener('click', handleGlobalClick);
|
|
||||||
});
|
|
||||||
|
//退出登录逻辑
|
||||||
|
const logout = () => {
|
||||||
//退出登录逻辑
|
//保存群聊消息
|
||||||
const logout = () => {
|
groupMessage.saveMessagesHistory(socket.u_id,groupMessage.g_id)
|
||||||
//保存群聊消息
|
// 清除用户全局信息
|
||||||
groupMessage.saveMessagesHistory(socket.u_id,groupMessage.g_id)
|
userInfo.clearUserInfo();
|
||||||
// 清除用户全局信息
|
localStorage.clear();
|
||||||
userInfo.clearUserInfo();
|
// 断开websocket链接
|
||||||
localStorage.clear();
|
socket.disconnect();
|
||||||
// 断开websocket链接
|
// 跳转到登录页面
|
||||||
voice.disconnect();
|
window.location.href = '/';
|
||||||
socket.disconnect();
|
|
||||||
// 跳转到登录页面
|
};
|
||||||
window.location.href = '/';
|
|
||||||
|
const aboutUs = () => {
|
||||||
};
|
router.push('/home/about')
|
||||||
|
}
|
||||||
const aboutUs = () => {
|
|
||||||
router.push('/home/about')
|
|
||||||
}
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
</script>
|
.navbar {
|
||||||
|
display: flex;
|
||||||
<style scoped>
|
justify-content: space-between;
|
||||||
.navbar {
|
align-items: center;
|
||||||
display: flex;
|
position: fixed;
|
||||||
justify-content: space-between;
|
top: 0;
|
||||||
align-items: center;
|
left: 0;
|
||||||
position: fixed;
|
width: 100%;
|
||||||
top: 0;
|
padding: 10px 20px;
|
||||||
left: 0;
|
background-color: #60605e;
|
||||||
width: 100%;
|
color: #fff;
|
||||||
padding: 10px 20px;
|
z-index: 1000;
|
||||||
background-color: #60605e;
|
}
|
||||||
color: #fff;
|
|
||||||
z-index: 1000;
|
.navbar-left,
|
||||||
}
|
.navbar-right {
|
||||||
|
display: flex;
|
||||||
.navbar-left,
|
align-items: center;
|
||||||
.navbar-right {
|
margin-right: 10%;
|
||||||
display: flex;
|
margin-left: 10%;
|
||||||
align-items: center;
|
}
|
||||||
margin-right: 10%;
|
|
||||||
margin-left: 10%;
|
.navbar-logo img {
|
||||||
}
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
.navbar-logo img {
|
}
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
.navbar a {
|
||||||
}
|
color: #fff;
|
||||||
|
margin: 0 10px;
|
||||||
.navbar a {
|
text-decoration: none;
|
||||||
color: #fff;
|
}
|
||||||
margin: 0 10px;
|
|
||||||
text-decoration: none;
|
.navbar a.router-link-active {
|
||||||
}
|
font-weight: bold;
|
||||||
|
}
|
||||||
.navbar a.router-link-active {
|
|
||||||
font-weight: bold;
|
.more-dropdown {
|
||||||
}
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
.more-dropdown {
|
}
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
.dropdown-content {
|
||||||
}
|
position: absolute;
|
||||||
|
width: 100px;
|
||||||
.dropdown-content {
|
margin-top: 22px;
|
||||||
position: absolute;
|
right: -50px;
|
||||||
width: 100px;
|
color: #fff;
|
||||||
margin-top: 22px;
|
background-color: #444444;
|
||||||
right: -50px;
|
border: 1px solid #000000;
|
||||||
color: #fff;
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
background-color: #444444;
|
padding: 5px;
|
||||||
border: 1px solid #000000;
|
z-index: 1000;
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
list-style-type: none;
|
||||||
padding: 5px;
|
}
|
||||||
z-index: 1000;
|
|
||||||
list-style-type: none;
|
.dropdown-content a {
|
||||||
}
|
display: block;
|
||||||
|
margin: 5px 0;
|
||||||
.dropdown-content a {
|
}
|
||||||
display: block;
|
|
||||||
margin: 5px 0;
|
.dropdown-content li {
|
||||||
}
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
.dropdown-content li {
|
}
|
||||||
height: 40px;
|
|
||||||
padding: 10px;
|
.dropdown-content li:hover {
|
||||||
}
|
|
||||||
|
background-color: #ffffff;
|
||||||
.dropdown-content li:hover {
|
color: #000000;
|
||||||
|
}
|
||||||
background-color: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,153 +1,152 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-button class="telegram-btn" @click="showTelegramPanel">
|
<el-button class="telegram-btn" @click="showTelegramPanel">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Mic />
|
<Mic />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<div class="telegramTag"></div>
|
<div class="telegramTag"></div>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<div class="voice" v-if="onCall.panel">
|
<div class="voice" v-if="onCall.panel">
|
||||||
<div>
|
<div>
|
||||||
<div class="profilebox">
|
<div class="profilebox">
|
||||||
<img :src="onCall.target.avatar" alt="头像">
|
<img :src="onCall.target.avatar" alt="头像">
|
||||||
</div>
|
</div>
|
||||||
<div class="infobox">
|
<div class="infobox">
|
||||||
<p>{{ onCall.target.name }}</p>
|
<p>{{ onCall.target.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttonsbox">
|
<div class="buttonsbox">
|
||||||
<button @click="answer" v-if="!onCall.from">
|
<button @click="answer" v-if="!onCall.from">
|
||||||
接听
|
接听
|
||||||
</button>
|
</button>
|
||||||
<button @click="hangup">
|
<button @click="denyCall(userinfo.user.id, onCall.target.u_id)" v-if="!onCall.from">
|
||||||
挂断
|
拒绝
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button @click="hangupCall(userinfo.user.id, onCall.target.u_id)" v-if="onCall.from">
|
||||||
</div>
|
挂断
|
||||||
<el-button @click="playRemoteAudio">没有声音?试试我</el-button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<audio ref="remoteAudio" autoplay></audio>
|
</div>
|
||||||
|
<el-button @click="playRemoteAudio">没有声音?试试我</el-button>
|
||||||
</template>
|
</div>
|
||||||
<script setup>
|
<audio ref="remoteAudio" id="remoteAudio" autoplay playsinline></audio>
|
||||||
import { userInfoStore } from '@/store/store';
|
|
||||||
import { voiceStore } from '@/store/Voice';
|
</template>
|
||||||
import { onCallStore } from '@/store/VoiceTarget';
|
<script setup>
|
||||||
import { Mic } from '@element-plus/icons-vue';
|
import { userInfoStore } from '@/store/user';
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
import { onCallStore } from '@/store/VoiceTarget';
|
||||||
|
import { Mic } from '@element-plus/icons-vue';
|
||||||
const voice = voiceStore();
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||||
const onCall = onCallStore();
|
import { denyCall, hangupCall,sendOffer } from '@/store/Voice.ts';
|
||||||
const userinfo = userInfoStore()
|
|
||||||
|
const onCall = onCallStore();
|
||||||
const localAudio = ref(null);
|
const userinfo = userInfoStore()
|
||||||
const remoteAudio = ref(null);
|
|
||||||
|
const localAudio = ref(null);
|
||||||
onMounted(() => {
|
const remoteAudio = ref(null);
|
||||||
onCall.setRemoteElement(remoteAudio.value);
|
|
||||||
});
|
onMounted(() => {
|
||||||
|
onCall.setRemoteElement(remoteAudio.value);
|
||||||
onBeforeUnmount(() => {
|
});
|
||||||
// 清理音频流和元素
|
|
||||||
if (localAudio.value.srcObject) {
|
onBeforeUnmount(() => {
|
||||||
localAudio.value.srcObject.getTracks().forEach(track => track.stop());
|
// 清理音频流和元素
|
||||||
}
|
if (localAudio.value && localAudio.value.srcObject) {
|
||||||
if (remoteAudio.value.srcObject) {
|
localAudio.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
remoteAudio.value.srcObject.getTracks().forEach(track => track.stop());
|
}
|
||||||
}
|
if (remoteAudio.value && remoteAudio.value.srcObject) {
|
||||||
})
|
remoteAudio.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
const answer = () => {
|
})
|
||||||
console.log("接听");
|
|
||||||
onCall.callingOff();
|
const answer = async () => {
|
||||||
onCall.fromOn();
|
console.log("接听");
|
||||||
voice.pickup(userinfo.user.u_id, onCall.target.u_id)
|
onCall.callingOff();
|
||||||
}
|
onCall.fromOn();
|
||||||
|
// voice.pickup(userinfo.user.id, onCall.target.u_id)
|
||||||
const hangup = () => {
|
await sendOffer(userinfo.user.id, onCall.target.u_id);
|
||||||
voice.hangup();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const showTelegramPanel = () => {
|
const showTelegramPanel = () => {
|
||||||
onCall.panel = !onCall.panel;
|
onCall.panel = !onCall.panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playRemoteAudio = () => {
|
const playRemoteAudio = () => {
|
||||||
const audio = document.getElementById("remoteAudio");
|
const audio = remoteAudio.value;
|
||||||
if (audio) {
|
if (!audio) return;
|
||||||
audio.play().catch(err => {
|
audio.play().catch(err => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
</script>
|
<style>
|
||||||
<style>
|
.voice {
|
||||||
.voice {
|
position: fixed;
|
||||||
position: fixed;
|
top: 210px;
|
||||||
top: 210px;
|
right: 100px;
|
||||||
right: 100px;
|
width: 300px;
|
||||||
width: 300px;
|
height: 400px;
|
||||||
height: 400px;
|
background-color: #d7d7d7;
|
||||||
background-color: #d7d7d7;
|
z-index: 999;
|
||||||
z-index: 999;
|
}
|
||||||
}
|
|
||||||
|
.profilebox {
|
||||||
.profilebox {
|
width: 100%;
|
||||||
width: 100%;
|
height: 150px;
|
||||||
height: 150px;
|
display: flex;
|
||||||
display: flex;
|
justify-content: center;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
align-items: center;
|
}
|
||||||
}
|
|
||||||
|
.profilebox img {
|
||||||
.profilebox img {
|
width: 100px;
|
||||||
width: 100px;
|
height: 100px;
|
||||||
height: 100px;
|
border-radius: 50%;
|
||||||
border-radius: 50%;
|
background-color: #fff;
|
||||||
background-color: #fff;
|
}
|
||||||
}
|
|
||||||
|
.infobox {
|
||||||
.infobox {
|
width: 100%;
|
||||||
width: 100%;
|
height: 50px;
|
||||||
height: 50px;
|
display: flex;
|
||||||
display: flex;
|
justify-content: center;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
align-items: center;
|
font-size: 18px;
|
||||||
font-size: 18px;
|
color: #646464;
|
||||||
color: #646464;
|
}
|
||||||
}
|
|
||||||
|
.buttonsbox {
|
||||||
.buttonsbox {
|
width: 100%;
|
||||||
width: 100%;
|
height: 100px;
|
||||||
height: 100px;
|
display: flex;
|
||||||
display: flex;
|
justify-content: center;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
align-items: center;
|
}
|
||||||
}
|
|
||||||
|
.buttonsbox button {
|
||||||
.buttonsbox button {
|
margin: 0 20px 0 20px;
|
||||||
margin: 0 20px 0 20px;
|
width: 50px;
|
||||||
width: 50px;
|
height: 50px;
|
||||||
height: 50px;
|
border-radius: 50%;
|
||||||
border-radius: 50%;
|
}
|
||||||
}
|
|
||||||
|
.telegram-btn {
|
||||||
.telegram-btn {
|
position: fixed;
|
||||||
position: fixed;
|
top: 45%;
|
||||||
top: 45%;
|
right: 0;
|
||||||
right: 0;
|
z-index: 1000;
|
||||||
z-index: 1000;
|
}
|
||||||
}
|
|
||||||
|
.telegramTag {
|
||||||
.telegramTag {
|
position: absolute;
|
||||||
position: absolute;
|
right: 0;
|
||||||
right: 0;
|
bottom: 0;
|
||||||
bottom: 0;
|
width: 10px;
|
||||||
width: 10px;
|
height: 10px;
|
||||||
height: 10px;
|
border-radius: 50%;
|
||||||
border-radius: 50%;
|
background-color: #f00;
|
||||||
background-color: #f00;
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,72 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link to="/home/user" v-if="userinfo.user.u_avatar" class="user-profile-link">
|
<router-link to="/home/user" v-if="userinfo.user.u_avatar" class="user-profile-link">
|
||||||
<div class="user-profile">
|
<div class="user-profile">
|
||||||
<img :src="userinfo.user.u_avatar" alt="User Avatar" />
|
<img :src="userinfo.user.u_avatar" alt="User Avatar" />
|
||||||
<div :class="['status-dot', statusClass]"></div>
|
<div :class="['status-dot', statusClass]"></div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { userInfoStore } from '@/store/store';
|
import { userInfoStore } from '@/store/user.ts';
|
||||||
|
|
||||||
const userinfo = userInfoStore();
|
const userinfo = userInfoStore();
|
||||||
|
|
||||||
const isLoggedIn = ref(true); // 假设用户已登录
|
const isLoggedIn = ref(true); // 假设用户已登录
|
||||||
|
|
||||||
const statusClass = computed(() => {
|
const statusClass = computed(() => {
|
||||||
return isLoggedIn.value ? 'online' : 'offline';
|
return isLoggedIn.value ? 'online' : 'offline';
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.user-profile-link {
|
.user-profile-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
/* 去掉链接的下划线 */
|
/* 去掉链接的下划线 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile {
|
.user-profile {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 500;
|
z-index: 500;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile img {
|
.user-profile img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.online {
|
.online {
|
||||||
background-color: green;
|
background-color: green;
|
||||||
/* 在线状态 */
|
/* 在线状态 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.offline {
|
.offline {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
/* 离线状态 */
|
/* 离线状态 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,37 +1,42 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { connectWebSocket, disconnectWebSocket, sendMessage, setIsManualClose } from '@/websocket/onlineSocket'
|
import { connectWebSocket, disconnectWebSocket, sendMessage, setIsManualClose } from '@/websocket/onlineSocket'
|
||||||
import { messageStore } from './message'
|
import { messageStore } from './message'
|
||||||
|
|
||||||
const message = messageStore()
|
const message = messageStore()
|
||||||
|
|
||||||
export const onlineSocketStore = defineStore('onlineSocket', {
|
export const onlineSocketStore = defineStore('onlineSocket', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
hasGotMessage: false,
|
hasGotMessage: false,
|
||||||
id: ''
|
id: ''
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
connect(id) {
|
connect(id: number) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
if (this.isConnected === true) return
|
if (this.isConnected === true) return
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
if (!this.hasGotMessage) {
|
if (!this.hasGotMessage) {
|
||||||
message.loadMessagesHistory(this.id)
|
message.loadMessagesHistory(this.id)
|
||||||
this.hasGotMessage = true
|
this.hasGotMessage = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disconnect() {
|
disconnect() {
|
||||||
setIsManualClose(true);
|
setIsManualClose(true);
|
||||||
disconnectWebSocket();
|
disconnectWebSocket();
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
if (this.hasGotMessage) {
|
if (this.hasGotMessage) {
|
||||||
message.saveMessagesHistory(this.id)
|
message.saveMessagesHistory(this.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
send(message) {
|
send(message: any) {
|
||||||
sendMessage(JSON.stringify(message));
|
try {
|
||||||
}
|
sendMessage(message);
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error('Failed to stringify message:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { connectVoicesocket, disconnectVoicesocket, sendMessage, hangup } from "@/websocket/voiceSocket";
|
|
||||||
import { defineStore } from "pinia";
|
|
||||||
|
|
||||||
export const voiceStore = defineStore("voice", {
|
|
||||||
state: () => ({
|
|
||||||
isConnected: false
|
|
||||||
}),
|
|
||||||
actions: {
|
|
||||||
connect() {
|
|
||||||
connectVoicesocket();
|
|
||||||
this.isConnected = true;
|
|
||||||
},
|
|
||||||
disconnect() {
|
|
||||||
disconnectVoicesocket();
|
|
||||||
this.isConnected = false;
|
|
||||||
},
|
|
||||||
startCall(from, from_name, from_avatar, to) {
|
|
||||||
if (this.isConnected) {
|
|
||||||
const message = {
|
|
||||||
type: "incomingcall",
|
|
||||||
from: from,
|
|
||||||
from_name: from_name,
|
|
||||||
from_avatar: from_avatar,
|
|
||||||
to: to
|
|
||||||
}
|
|
||||||
sendMessage(JSON.stringify(message))
|
|
||||||
} else {
|
|
||||||
console.log("voice socket is not connected")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pickup(from,to){
|
|
||||||
if (this.isConnected) {
|
|
||||||
const message ={
|
|
||||||
type: "pickup",
|
|
||||||
from: from,
|
|
||||||
to: to
|
|
||||||
}
|
|
||||||
sendMessage(JSON.stringify(message))
|
|
||||||
} else {
|
|
||||||
console.log("voice socket is not connected")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hangup(){
|
|
||||||
hangup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
|
||||||
235
src/store/Voice.ts
Normal file
235
src/store/Voice.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { sendMessage } from "@/websocket/onlineSocket";
|
||||||
|
import { onCallStore } from "@/store/VoiceTarget";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const iceserver = {
|
||||||
|
iceServers: [
|
||||||
|
{
|
||||||
|
urls: process.env.STUN_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: process.env.TURN_URL,
|
||||||
|
username: process.env.TURN_USERNAME,
|
||||||
|
credential: process.env.TURN_CREDENTIAL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface message {
|
||||||
|
cmd: "VOICE_ICE_CANDIDATE" | "VOICE_SDP_OFFER" | "VOICE_SDP_ANSWER",
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
content: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
const oncall = onCallStore();
|
||||||
|
|
||||||
|
const RTCpeerConnection = ref(null);
|
||||||
|
|
||||||
|
const localstream = ref(null);
|
||||||
|
const remotestream = ref<MediaStream | null>(null);
|
||||||
|
|
||||||
|
// 本地候选:为了避免“绑定 onicecandidate 太晚”导致丢失,统一先缓存,等 remoteDescription 就绪后再发送
|
||||||
|
const pendingLocalCandidates = ref<any[]>([]);
|
||||||
|
// 远端候选:避免远端 candidate 早到但 remoteDescription 未 set 导致 addIceCandidate 失败
|
||||||
|
const pendingRemoteCandidates = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 当前会话对端信息(用于发送 ICE 时带上 from/to)
|
||||||
|
const currentFrom = ref<number | null>(null);
|
||||||
|
const currentTo = ref<number | null>(null);
|
||||||
|
|
||||||
|
const canSendIceNow = () => {
|
||||||
|
const pc: any = RTCpeerConnection.value;
|
||||||
|
return (
|
||||||
|
!!pc &&
|
||||||
|
!!pc.localDescription &&
|
||||||
|
!!pc.remoteDescription &&
|
||||||
|
currentFrom.value !== null &&
|
||||||
|
currentTo.value !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushLocalCandidatesIfReady = () => {
|
||||||
|
if (!canSendIceNow()) return;
|
||||||
|
if (pendingLocalCandidates.value.length === 0) return;
|
||||||
|
|
||||||
|
pendingLocalCandidates.value.forEach((candidate) => {
|
||||||
|
const message: message = {
|
||||||
|
cmd: "VOICE_ICE_CANDIDATE",
|
||||||
|
from: currentFrom.value as number,
|
||||||
|
to: currentTo.value as number,
|
||||||
|
content: candidate,
|
||||||
|
};
|
||||||
|
sendMessage(message);
|
||||||
|
});
|
||||||
|
pendingLocalCandidates.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushRemoteCandidatesIfReady = async () => {
|
||||||
|
const pc: any = RTCpeerConnection.value;
|
||||||
|
if (!pc || !pc.remoteDescription) return;
|
||||||
|
if (pendingRemoteCandidates.value.length === 0) return;
|
||||||
|
|
||||||
|
for (const c of pendingRemoteCandidates.value) {
|
||||||
|
await pc.addIceCandidate(new RTCIceCandidate(c));
|
||||||
|
}
|
||||||
|
pendingRemoteCandidates.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getlocalStream = async () => {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
console.log("获取本地音频流成功");
|
||||||
|
return stream;
|
||||||
|
// // 获取音频和视频轨道
|
||||||
|
// const audioTrack = stream.getAudioTracks()[0];
|
||||||
|
// // 将轨道添加到 RTCPeerConnection
|
||||||
|
// peerConnection.addTrack(audioTrack, stream);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeLocalStream = (stream: MediaStream) => {
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initRTCconnection = async () => {
|
||||||
|
RTCpeerConnection.value = new RTCPeerConnection(iceserver);
|
||||||
|
localstream.value = await getlocalStream();
|
||||||
|
RTCpeerConnection.value.addTrack(localstream.value.getAudioTracks()[0], localstream.value);
|
||||||
|
|
||||||
|
// 必须尽早绑定,否则可能在 setLocalDescription 后就把 candidate 产完了
|
||||||
|
RTCpeerConnection.value.onicecandidate = (event: { candidate: any }) => {
|
||||||
|
if (!event.candidate) return;
|
||||||
|
pendingLocalCandidates.value.push(event.candidate);
|
||||||
|
flushLocalCandidatesIfReady();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 远端音频:收到 track 后把 MediaStream 绑定到 UI 的 <audio>
|
||||||
|
RTCpeerConnection.value.ontrack = (event: RTCTrackEvent) => {
|
||||||
|
// 优先用浏览器提供的 streams[0];否则手动聚合 track
|
||||||
|
let stream = event.streams && event.streams[0] ? event.streams[0] : null;
|
||||||
|
if (!stream) {
|
||||||
|
if (!remotestream.value) remotestream.value = new MediaStream();
|
||||||
|
if (event.track) remotestream.value.addTrack(event.track);
|
||||||
|
stream = remotestream.value;
|
||||||
|
}
|
||||||
|
if (stream) {
|
||||||
|
oncall.setRemoteStream(stream);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendOffer = async (from: number,to: number) => {
|
||||||
|
if (!RTCpeerConnection.value) {
|
||||||
|
await initRTCconnection();
|
||||||
|
};
|
||||||
|
currentFrom.value = from;
|
||||||
|
currentTo.value = to;
|
||||||
|
const offer = await RTCpeerConnection.value.createOffer();
|
||||||
|
await RTCpeerConnection.value.setLocalDescription(offer);
|
||||||
|
const message: message = {
|
||||||
|
cmd: "VOICE_SDP_OFFER",
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
content: offer
|
||||||
|
}
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleOffer = async (offer: any, from: number, to: number) => {
|
||||||
|
if (!RTCpeerConnection.value) {
|
||||||
|
await initRTCconnection();
|
||||||
|
}
|
||||||
|
// 这里是“我收到对方 offer”,因此我这端的 from/to 应该是 (to -> from)
|
||||||
|
currentFrom.value = to;
|
||||||
|
currentTo.value = from;
|
||||||
|
await RTCpeerConnection.value.setRemoteDescription(new RTCSessionDescription(offer));
|
||||||
|
await flushRemoteCandidatesIfReady();
|
||||||
|
const answer = await RTCpeerConnection.value.createAnswer();
|
||||||
|
await RTCpeerConnection.value.setLocalDescription(answer);
|
||||||
|
const message: message = {
|
||||||
|
cmd: "VOICE_SDP_ANSWER",
|
||||||
|
from: to,
|
||||||
|
to: from,
|
||||||
|
content: answer
|
||||||
|
}
|
||||||
|
sendMessage(message);
|
||||||
|
// remoteDescription + localDescription 都已具备,允许发送本地候选
|
||||||
|
flushLocalCandidatesIfReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleAnswer = async (answer: any, from: number, to: number) => {
|
||||||
|
if (!RTCpeerConnection.value)
|
||||||
|
{
|
||||||
|
console.error("connection lost");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 这里是“我收到对方 answer”,因此我这端的 from/to 应该是 (to -> from)
|
||||||
|
currentFrom.value = to;
|
||||||
|
currentTo.value = from;
|
||||||
|
await RTCpeerConnection.value.setRemoteDescription(new RTCSessionDescription(answer));
|
||||||
|
await flushRemoteCandidatesIfReady();
|
||||||
|
flushLocalCandidatesIfReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleCandidate = async (candidate: any) => {
|
||||||
|
if (!RTCpeerConnection.value) return;
|
||||||
|
// candidate 可能早到:remoteDescription 未 set 时直接 add 会失败,先缓存
|
||||||
|
if (!RTCpeerConnection.value.remoteDescription) {
|
||||||
|
pendingRemoteCandidates.value.push(candidate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await RTCpeerConnection.value.addIceCandidate(new RTCIceCandidate(candidate));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hangupCall = (from: number, to: number) => {
|
||||||
|
if(from === null || to === null) return;
|
||||||
|
if (RTCpeerConnection.value) {
|
||||||
|
RTCpeerConnection.value.close();
|
||||||
|
RTCpeerConnection.value = null;
|
||||||
|
const msg = {
|
||||||
|
cmd: "VOICE_CALL_END",
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
time: new Date().toLocaleString()
|
||||||
|
}
|
||||||
|
sendMessage(msg);
|
||||||
|
oncall.clear();
|
||||||
|
closeLocalStream(localstream.value);
|
||||||
|
remotestream.value = null;
|
||||||
|
pendingLocalCandidates.value = [];
|
||||||
|
pendingRemoteCandidates.value = [];
|
||||||
|
currentFrom.value = null;
|
||||||
|
currentTo.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const denyCall = (from: number, to: number) => {
|
||||||
|
if(from === null || to === null) return;
|
||||||
|
const msg = {
|
||||||
|
cmd: "VOICE_CALL_DENY",
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
time: new Date().toLocaleString()
|
||||||
|
}
|
||||||
|
sendMessage(msg);
|
||||||
|
oncall.clear();
|
||||||
|
remotestream.value = null;
|
||||||
|
pendingLocalCandidates.value = [];
|
||||||
|
pendingRemoteCandidates.value = [];
|
||||||
|
currentFrom.value = null;
|
||||||
|
currentTo.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const closeConnection = () => {
|
||||||
|
if (RTCpeerConnection.value) {
|
||||||
|
RTCpeerConnection.value.close();
|
||||||
|
RTCpeerConnection.value = null;
|
||||||
|
}
|
||||||
|
closeLocalStream(localstream.value);
|
||||||
|
remotestream.value = null;
|
||||||
|
pendingLocalCandidates.value = [];
|
||||||
|
pendingRemoteCandidates.value = [];
|
||||||
|
currentFrom.value = null;
|
||||||
|
currentTo.value = null;
|
||||||
|
}
|
||||||
@@ -1,93 +1,115 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const onCallStore = defineStore("onCall", {
|
export const onCallStore = defineStore("onCall", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
target: {
|
target: {
|
||||||
u_id: "",
|
u_id: "",
|
||||||
name: "",
|
name: "",
|
||||||
avatar: ""
|
avatar: ""
|
||||||
},
|
},
|
||||||
panel: false,
|
panel: false,
|
||||||
calling: false,
|
calling: false,
|
||||||
from: false,
|
from: false,
|
||||||
status: false,
|
status: false,
|
||||||
localstream: {
|
localstream: {
|
||||||
audioStream: null,
|
audioStream: null,
|
||||||
audioElement: null
|
audioElement: null
|
||||||
},
|
},
|
||||||
remotestream: {
|
remotestream: {
|
||||||
audioStream: null,
|
audioStream: null,
|
||||||
audioElement: null
|
audioElement: null
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setTarget(u_id, u_name, u_avatar) {
|
getTarget() {
|
||||||
this.target = {
|
return this.target;
|
||||||
u_id: u_id,
|
},
|
||||||
name: u_name,
|
setTarget(u_id:number, u_name:string, u_avatar:string) {
|
||||||
avatar: u_avatar
|
this.target = {
|
||||||
}
|
u_id: u_id,
|
||||||
},
|
name: u_name,
|
||||||
panelOn() {
|
avatar: u_avatar
|
||||||
this.panel = true
|
}
|
||||||
},
|
},
|
||||||
panelOff() {
|
panelOn() {
|
||||||
this.panel = false
|
this.panel = true
|
||||||
},
|
},
|
||||||
callingOn() {
|
panelOff() {
|
||||||
this.calling = true
|
this.panel = false
|
||||||
},
|
},
|
||||||
callingOff() {
|
callingOn() {
|
||||||
this.calling = false
|
this.calling = true
|
||||||
},
|
},
|
||||||
fromOn() {
|
callingOff() {
|
||||||
this.from = true
|
this.calling = false
|
||||||
},
|
},
|
||||||
fromOff() {
|
fromOn() {
|
||||||
this.from = false
|
this.from = true
|
||||||
},
|
},
|
||||||
statusOn() {
|
fromOff() {
|
||||||
this.status = true
|
this.from = false
|
||||||
},
|
},
|
||||||
statusOff() {
|
statusOn() {
|
||||||
this.status = false
|
this.status = true
|
||||||
},
|
},
|
||||||
setLocalStream(stream) {
|
statusOff() {
|
||||||
this.localstream.audioStream = stream;
|
this.status = false
|
||||||
if (this.localstream.audioStream) {
|
},
|
||||||
this.localstream.audioElement.srcObject = stream;
|
clear(){
|
||||||
}
|
this.target = {
|
||||||
},
|
u_id: "",
|
||||||
setLocalElement(element) {
|
name: "",
|
||||||
this.localstream.audioElement = element;
|
avatar: ""
|
||||||
if (this.localstream.audioStream) {
|
}
|
||||||
this.localstream.audioElement.srcObject = this.localstream.audioStream;
|
this.panel = false;
|
||||||
}
|
this.calling = false;
|
||||||
},
|
this.from = false;
|
||||||
setRemoteStream(stream) {
|
this.status = false;
|
||||||
this.remotestream.audioStream = stream;
|
this.localstream = {
|
||||||
if (this.remotestream.audioStream) {
|
audioStream: null,
|
||||||
this.remotestream.audioElement.srcObject = stream;
|
audioElement: null
|
||||||
}
|
}
|
||||||
},
|
this.remotestream = {
|
||||||
setRemoteElement(element) {
|
audioStream: null,
|
||||||
this.remotestream.audioElement = element;
|
audioElement: null
|
||||||
if (this.remotestream.audioStream) {
|
}
|
||||||
this.remotestream.audioElement.srcObject = this.remotestream.audioStream;
|
},
|
||||||
}
|
setLocalStream(stream) {
|
||||||
},
|
this.localstream.audioStream = stream;
|
||||||
resetStream() {
|
if (this.localstream.audioStream) {
|
||||||
// 停止本地媒体流
|
this.localstream.audioElement.srcObject = stream;
|
||||||
if (this.localstream.audioStream) {
|
}
|
||||||
this.localstream.audioStream.getTracks().forEach(track => track.stop());
|
},
|
||||||
this.localstream.audioStream = null;
|
setLocalElement(element) {
|
||||||
}
|
this.localstream.audioElement = element;
|
||||||
|
if (this.localstream.audioStream) {
|
||||||
// 停止远程媒体流
|
this.localstream.audioElement.srcObject = this.localstream.audioStream;
|
||||||
if (this.remotestream.videoStream) {
|
}
|
||||||
this.remotestream.videoStream.getTracks().forEach(track => track.stop());
|
},
|
||||||
this.remotestream.videoStream = null;
|
setRemoteStream(stream) {
|
||||||
}
|
this.remotestream.audioStream = stream;
|
||||||
}
|
if (this.remotestream.audioStream) {
|
||||||
}
|
this.remotestream.audioElement.srcObject = stream;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setRemoteElement(element) {
|
||||||
|
this.remotestream.audioElement = element;
|
||||||
|
if (this.remotestream.audioStream) {
|
||||||
|
this.remotestream.audioElement.srcObject = this.remotestream.audioStream;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetStream() {
|
||||||
|
// 停止本地媒体流
|
||||||
|
if (this.localstream.audioStream) {
|
||||||
|
this.localstream.audioStream.getTracks().forEach(track => track.stop());
|
||||||
|
this.localstream.audioStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止远程媒体流
|
||||||
|
if (this.remotestream.videoStream) {
|
||||||
|
this.remotestream.videoStream.getTracks().forEach(track => track.stop());
|
||||||
|
this.remotestream.videoStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -1,402 +1,410 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-row class="container">
|
<el-row class="container">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-button type="primary" @click="dialogVisibleFriendManage = true">好友管理</el-button>
|
<el-button type="primary" @click="dialogVisibleFriendManage = true">好友管理</el-button>
|
||||||
<el-input v-model="tempsearch" :prefix-icon="Search" style="width: 90%;" clearable></el-input>
|
<el-input v-model="tempsearch" :prefix-icon="Search" style="width: 90%;" clearable></el-input>
|
||||||
<el-scrollbar style="height: 500px;width: 90%;">
|
<el-scrollbar style="height: 500px;width: 90%;">
|
||||||
<ul class="infinitelist" style="overflow: auto">
|
<ul class="infinitelist" style="overflow: auto">
|
||||||
<li v-for="(item) in tempsearchResult" :key="item.id" class="infinitelistitem"
|
<li v-for="(item) in tempsearchResult" :key="item.id" class="infinitelistitem"
|
||||||
@click="switchTemplate(item.id, item.u_name, item.u_avatar)"
|
@click="switchTemplate(item.id, item.u_name, item.u_avatar)"
|
||||||
:class="{ 'selected': selectedFriendId === item.id }">
|
:class="{ 'selected': selectedFriendId === item.id }">
|
||||||
<div class="user-profile">
|
<div class="user-profile">
|
||||||
<img :src="item.u_avatar" alt="User Avatar" />
|
<img :src="item.u_avatar" alt="User Avatar" />
|
||||||
<div :class="['status-dot', statusClass]"></div>
|
<div :class="['status-dot', statusClass]"></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: inline-block;">{{ item.u_name }}#{{ item.u_id }}</div>
|
<div style="display: inline-block;">{{ item.u_name }}#{{ item.u_id }}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="18">
|
<el-col :span="18">
|
||||||
<el-row class="messagebox">
|
<el-row class="messagebox">
|
||||||
<el-scrollbar ref="scrollbarRef" style="width: 100%;height: 100%;">
|
<el-scrollbar ref="scrollbarRef" style="width: 100%;height: 100%;">
|
||||||
<!-- 聊天消息指示框 头像、时间、内容 -->
|
<!-- 聊天消息指示框 头像、时间、内容 -->
|
||||||
<div v-for="(item) in messagebox" class="message-item">
|
<div v-for="(item) in messagebox" class="message-item">
|
||||||
<div
|
<div
|
||||||
:class="{ 'message-item-profile': true, 'left': item.from !== userinfo.user.id, 'right': item.from === userinfo.user.id }">
|
:class="{ 'message-item-profile': true, 'left': item.from !== userinfo.user.id, 'right': item.from === userinfo.user.id }">
|
||||||
<img :src="item.from !== userinfo.user.id ? oppositeAvatar : userinfo.user.u_avatar"
|
<img :src="item.from !== userinfo.user.id ? oppositeAvatar : userinfo.user.u_avatar"
|
||||||
alt="User Avatar" />
|
alt="User Avatar" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="{ 'message-item-content': true, 'left': item.from !== userinfo.user.id, 'right': item.from === userinfo.user.id }">
|
:class="{ 'message-item-content': true, 'left': item.from !== userinfo.user.id, 'right': item.from === userinfo.user.id }">
|
||||||
{{ item.content }}</div>
|
{{ item.content }}</div>
|
||||||
<div
|
<div
|
||||||
:class="{ 'message-item-time': true, 'left': item.from === userinfo.user.id, 'right': item.from !== userinfo.user.id }">
|
:class="{ 'message-item-time': true, 'left': item.from === userinfo.user.id, 'right': item.from !== userinfo.user.id }">
|
||||||
{{ item.time }}</div>
|
{{ item.time }}</div>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-input class="inputbox" v-model="inputValue" :placeholder="placeholder" @keyup.enter="handleEnter" clearable
|
<el-input class="inputbox" v-model="inputValue" :placeholder="placeholder" @keyup.enter="handleEnter" clearable
|
||||||
:disabled="inputDisabled"></el-input>
|
:disabled="inputDisabled"></el-input>
|
||||||
<el-button class="call" :disabled="inputDisabled" @click="confirmCall()">语音通话</el-button>
|
<el-button class="call" :disabled="inputDisabled" @click="confirmCall()">语音通话</el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 确认通话弹窗 -->
|
<!-- 确认通话弹窗 -->
|
||||||
<el-dialog v-model="dialogVisibleCallConfirm" title="确认通话对象" width="30%" :before-close="handleClose">
|
<el-dialog v-model="dialogVisibleCallConfirm" title="确认通话对象" width="30%" :before-close="handleClose">
|
||||||
<el-row style="height: 80px;align-items: center;">
|
<el-row style="height: 80px;align-items: center;">
|
||||||
<div class="user-profile">
|
<div class="user-profile">
|
||||||
<img :src="oppositeAvatar" alt="User Avatar" />
|
<img :src="oppositeAvatar" alt="User Avatar" />
|
||||||
</div>
|
</div>
|
||||||
<div>{{ oppositeName }}</div>
|
<div>{{ oppositeName }}</div>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-button type="primary" @click="handleConfirmCall()" style="margin: auto;">确认</el-button>
|
<el-button type="primary" @click="handleConfirmCall()" style="margin: auto;">确认</el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 好友管理弹窗 -->
|
<!-- 好友管理弹窗 -->
|
||||||
<el-dialog v-model="dialogVisibleFriendManage" title="好友管理" style="width: 850px;height: 500px;">
|
<el-dialog v-model="dialogVisibleFriendManage" title="好友管理" style="width: 850px;height: 500px;">
|
||||||
<el-input v-model="friendSearch" placeholder="搜索好友" style="margin-bottom: 10px;width: 100%;"></el-input>
|
<el-input v-model="friendSearch" placeholder="搜索好友" style="margin-bottom: 10px;width: 100%;"></el-input>
|
||||||
<el-table :data="friendSearchResult" style="width: 100%;height: 400px;">
|
<el-table :data="friendSearchResult" style="width: 100%;height: 400px;">
|
||||||
<el-table-column prop="u_avatar" label="" width="100">
|
<el-table-column prop="u_avatar" label="" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<!-- 使用 el-avatar 组件显示头像 -->
|
<!-- 使用 el-avatar 组件显示头像 -->
|
||||||
<el-avatar :src="friendSearchResult[scope.$index].u_avatar" size="large" />
|
<el-avatar :src="friendSearchResult[scope.$index].u_avatar" size="large" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="u_name" label="姓名" width="100"></el-table-column>
|
<el-table-column prop="u_name" label="姓名" width="100"></el-table-column>
|
||||||
<!-- <el-table-column prop="id" label="id" width="0"></el-table-column> -->
|
<!-- <el-table-column prop="id" label="id" width="0"></el-table-column> -->
|
||||||
<el-table-column prop="u_id" label="u_id" width="100"></el-table-column>
|
<el-table-column prop="u_id" label="u_id" width="100"></el-table-column>
|
||||||
<el-table-column prop="u_introduction" label="个性签名" width="200"></el-table-column>
|
<el-table-column prop="u_introduction" label="个性签名" width="200"></el-table-column>
|
||||||
<el-table-column>
|
<el-table-column>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button @click="deleteChatHisory(scope.row.u_id)">删除聊天记录</el-button>
|
<el-button @click="deleteChatHisory(scope.row.u_id)">删除聊天记录</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column>
|
<el-table-column>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button @click="deletefriend(scope.row.id)">删除好友</el-button>
|
<el-button @click="deletefriend(scope.row.id)">删除好友</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, nextTick, watch, computed } from 'vue'
|
import { ref, onMounted, nextTick, watch, computed } from 'vue'
|
||||||
import { userInfoStore } from '@/store/user';
|
import { userInfoStore } from '@/store/user';
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { onlineSocketStore } from '@/store/Online';
|
import { sendMessage } from '@/websocket/onlineSocket';
|
||||||
import { messageStore } from '@/store/message.ts';
|
import { messageStore } from '@/store/message.ts';
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { voiceStore } from '@/store/Voice';
|
import { onCallStore } from '@/store/VoiceTarget.ts';
|
||||||
import { onCallStore } from '@/store/VoiceTarget';
|
import { getFriends, deleteFriend } from '@/api/friend';
|
||||||
import { getFriends, deleteFriend } from '@/api/friend';
|
|
||||||
|
|
||||||
const socket = onlineSocketStore()
|
const userinfo = userInfoStore()
|
||||||
const userinfo = userInfoStore()
|
const message = messageStore()
|
||||||
const message = messageStore()
|
const oncall = onCallStore()
|
||||||
const voice = voiceStore()
|
|
||||||
const oncall = onCallStore()
|
const messagebox = ref([])
|
||||||
|
const scrollbarRef = ref(null)
|
||||||
const messagebox = ref([])
|
const dialogVisibleFriendManage = ref(false)
|
||||||
const scrollbarRef = ref(null)
|
const dialogVisibleCallConfirm = ref(false)
|
||||||
const dialogVisibleFriendManage = ref(false)
|
|
||||||
const dialogVisibleCallConfirm = ref(false)
|
const tempsearch = ref('')
|
||||||
|
const inputValue = ref('')
|
||||||
const tempsearch = ref('')
|
const statusClass = ref('online')
|
||||||
const inputValue = ref('')
|
const inputDisabled = ref(true)
|
||||||
const statusClass = ref('online')
|
const placeholder = ref('请选择聊天对象')
|
||||||
const inputDisabled = ref(true)
|
const oppositeId = ref('')
|
||||||
const placeholder = ref('请选择聊天对象')
|
const oppositeName = ref('')
|
||||||
const oppositeId = ref('')
|
const oppositeAvatar = ref('')
|
||||||
const oppositeName = ref('')
|
|
||||||
const oppositeAvatar = ref('')
|
const friends = ref([])
|
||||||
|
const friendSearch = ref('')
|
||||||
const friends = ref([])
|
const selectedFriendId = ref('')
|
||||||
const friendSearch = ref('')
|
|
||||||
const selectedFriendId = ref('')
|
const tempsearchResult = computed(() => {
|
||||||
|
if (!tempsearch.value)
|
||||||
const tempsearchResult = computed(() => {
|
return friends.value
|
||||||
if (!tempsearch.value)
|
else
|
||||||
return friends.value
|
return friends.value.filter(item => item.u_name.includes(tempsearch.value))
|
||||||
friends.value.filter(item => item.u_name.includes(tempsearch.value))
|
})
|
||||||
})
|
|
||||||
|
const friendSearchResult = computed(() => {
|
||||||
const friendSearchResult = computed(() => {
|
if (!friendSearch.value)
|
||||||
if (!friendSearch.value)
|
return friends.value
|
||||||
return friends.value
|
else
|
||||||
else
|
return friends.value.filter(item => item.u_name.includes(friendSearch.value))
|
||||||
return friends.value.filter(item => item.u_name.includes(friendSearch.value))
|
|
||||||
|
})
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//切换聊天对象
|
||||||
//切换聊天对象
|
const switchTemplate = (id, u_name, u_avatar) => {
|
||||||
const switchTemplate = (id, u_name, u_avatar) => {
|
message.from = id
|
||||||
message.from = id
|
oppositeId.value = id
|
||||||
oppositeId.value = id
|
oppositeName.value = u_name
|
||||||
oppositeName.value = u_name
|
oppositeAvatar.value = u_avatar
|
||||||
oppositeAvatar.value = u_avatar
|
message.setCorresponding()
|
||||||
message.setCorresponding()
|
messagebox.value = message.corresponding
|
||||||
messagebox.value = message.corresponding
|
inputDisabled.value = false
|
||||||
inputDisabled.value = false
|
placeholder.value = '请输入消息内容'
|
||||||
placeholder.value = '请输入消息内容'
|
scrollToBottom()
|
||||||
scrollToBottom()
|
selectedFriendId.value = id
|
||||||
selectedFriendId.value = id
|
console.log(message.corresponding)
|
||||||
console.log(message.corresponding)
|
}
|
||||||
}
|
|
||||||
|
// 定义回车键的处理函数
|
||||||
// 定义回车键的处理函数
|
const handleEnter = () => {
|
||||||
const handleEnter = () => {
|
if (inputValue.value === '') {
|
||||||
if (inputValue.value === '') {
|
ElMessage.error('消息不能为空')
|
||||||
ElMessage.error('消息不能为空')
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// 发送一类消息给目标用户
|
||||||
// 发送一类消息给目标用户
|
const msg = {
|
||||||
const msg = {
|
cmd: "MESSAGE",
|
||||||
cmd: "MESSAGE",
|
from: userinfo.user.id,
|
||||||
from: userinfo.user.id,
|
to: message.from,
|
||||||
to: message.from,
|
content: inputValue.value,
|
||||||
content: inputValue.value,
|
time: new Date().toLocaleString()
|
||||||
time: new Date().toLocaleString()
|
};
|
||||||
};
|
|
||||||
|
sendMessage(msg);
|
||||||
socket.send(msg)
|
message.addMessage(msg)
|
||||||
message.addMessage(msg)
|
inputValue.value = ''
|
||||||
inputValue.value = ''
|
scrollToBottom()
|
||||||
scrollToBottom()
|
};
|
||||||
};
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
const scrollToBottom = () => {
|
nextTick(() => {
|
||||||
nextTick(() => {
|
const scrollbar = scrollbarRef.value?.$el; // 获取 el-scrollbar 的根元素
|
||||||
const scrollbar = scrollbarRef.value?.$el; // 获取 el-scrollbar 的根元素
|
if (scrollbar) {
|
||||||
if (scrollbar) {
|
const scrollWrap = scrollbar.querySelector(".el-scrollbar__wrap"); // 找到滚动区域
|
||||||
const scrollWrap = scrollbar.querySelector(".el-scrollbar__wrap"); // 找到滚动区域
|
if (scrollWrap) {
|
||||||
if (scrollWrap) {
|
scrollWrap.scrollTop = scrollWrap.scrollHeight; // 滚动到底部
|
||||||
scrollWrap.scrollTop = scrollWrap.scrollHeight; // 滚动到底部
|
} else {
|
||||||
} else {
|
console.error("滚动容器未找到,请检查 el-scrollbar 的 DOM 结构");
|
||||||
console.error("滚动容器未找到,请检查 el-scrollbar 的 DOM 结构");
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
};
|
||||||
};
|
|
||||||
|
// 监听消息
|
||||||
// 监听消息
|
watch(() => message.messages.length, (newLength, oldLength) => {
|
||||||
watch(() => message.messages.length, (newLength, oldLength) => {
|
const msg = message.messages[newLength - 1]
|
||||||
const msg = message.messages[newLength - 1]
|
console.log("监听到新消息:", msg)
|
||||||
console.log("监听到新消息:", msg)
|
if (newLength > oldLength && (msg.from === message.from && msg.to === message.to) || (msg.from === message.to && msg.to === message.from)) {
|
||||||
if (newLength > oldLength && (msg.from === message.from && msg.to === message.to) || (msg.from === message.to && msg.to === message.from)) {
|
messagebox.value.push(msg)
|
||||||
messagebox.value.push(msg)
|
scrollToBottom()
|
||||||
scrollToBottom()
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
|
const deletefriend = (id) => {
|
||||||
const deletefriend = (id) => {
|
if(deleteFriend(id)){
|
||||||
if(deleteFriend(id)){
|
inputValue.value = ''
|
||||||
inputValue.value = ''
|
inputDisabled.value = true
|
||||||
inputDisabled.value = true
|
placeholder.value = '请选择聊天对象'
|
||||||
placeholder.value = '请选择聊天对象'
|
oppositeAvatar.value = ''
|
||||||
oppositeAvatar.value = ''
|
ElMessage.success('删除好友成功')
|
||||||
ElMessage.success('删除好友成功')
|
friends.value = getFriends()
|
||||||
friends.value = getFriends()
|
return
|
||||||
return
|
}else{
|
||||||
}else{
|
ElMessage.error('删除好友失败')
|
||||||
ElMessage.error('删除好友失败')
|
return
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const deleteChatHisory = (u_id) => {
|
||||||
const deleteChatHisory = (u_id) => {
|
try {
|
||||||
try {
|
if (u_id === message.sender) messagebox.value = []
|
||||||
if (u_id === message.sender) messagebox.value = []
|
message.deleteMessagesHistory(userinfo.user.u_id, u_id)
|
||||||
message.deleteMessagesHistory(userinfo.user.u_id, u_id)
|
} catch (e) {
|
||||||
} catch (e) {
|
ElMessage.error('删除聊天记录失败')
|
||||||
ElMessage.error('删除聊天记录失败')
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const confirmCall = () => {
|
||||||
const confirmCall = () => {
|
dialogVisibleCallConfirm.value = true
|
||||||
dialogVisibleCallConfirm.value = true
|
}
|
||||||
}
|
|
||||||
|
const handleConfirmCall = () => {
|
||||||
const handleConfirmCall = () => {
|
dialogVisibleCallConfirm.value = false
|
||||||
dialogVisibleCallConfirm.value = false
|
oncall.setTarget(oppositeId.value, oppositeName.value, oppositeAvatar.value)
|
||||||
oncall.setTarget(oppositeId.value, oppositeName.value, oppositeAvatar.value)
|
oncall.panelOn()
|
||||||
oncall.panelOn()
|
oncall.callingOn()
|
||||||
oncall.callingOn()
|
oncall.fromOn()
|
||||||
oncall.fromOn()
|
// voice.startCall(userinfo.user.u_id, userinfo.user.u_name, userinfo.user.u_avatar, oppositeId.value)
|
||||||
voice.startCall(userinfo.user.u_id, userinfo.user.u_name, userinfo.user.u_avatar, oppositeId.value)
|
const msg = {
|
||||||
}
|
cmd: "VOICE_CALL_REQUEST",
|
||||||
|
from: userinfo.user.id,
|
||||||
|
from_name: userinfo.user.u_name,
|
||||||
onMounted(async () => {
|
from_avatar: userinfo.user.u_avatar,
|
||||||
friends.value = await getFriends()
|
to: oppositeId.value,
|
||||||
await nextTick()
|
time: new Date().toLocaleString()
|
||||||
console.log(friends.value)
|
}
|
||||||
console.log(message.from)
|
sendMessage(msg);
|
||||||
if (message.from !== '' || message.from !== null) {
|
}
|
||||||
console.log("切换聊天对象")
|
|
||||||
const foundUser = friends.value.find((friend) => friend.id === message.from);
|
|
||||||
switchTemplate(message.from, foundUser.u_name, foundUser.u_avatar)
|
onMounted(async () => {
|
||||||
}
|
friends.value = await getFriends()
|
||||||
})
|
await nextTick()
|
||||||
|
console.log(friends.value)
|
||||||
</script>
|
console.log(message.from)
|
||||||
|
if (message.from !== '' || message.from !== null) {
|
||||||
<style>
|
console.log("切换聊天对象")
|
||||||
.container {
|
const foundUser = friends.value.find((friend) => friend.id === message.from);
|
||||||
height: 70%;
|
switchTemplate(message.from, foundUser.u_name, foundUser.u_avatar)
|
||||||
margin-top: 4%;
|
}
|
||||||
width: 80%;
|
})
|
||||||
}
|
|
||||||
|
</script>
|
||||||
.infinitelist {
|
|
||||||
list-style: none;
|
<style>
|
||||||
padding: 0;
|
.container {
|
||||||
margin: 0;
|
height: 70%;
|
||||||
width: 100%;
|
margin-top: 4%;
|
||||||
}
|
width: 80%;
|
||||||
|
}
|
||||||
.infinitelistitem {
|
|
||||||
padding: 10px;
|
.infinitelist {
|
||||||
width: 100%;
|
list-style: none;
|
||||||
}
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
.infinitelistitem.selected {
|
width: 100%;
|
||||||
background-color: #f0f0f0;
|
}
|
||||||
/* 灰色背景 */
|
|
||||||
}
|
.infinitelistitem {
|
||||||
|
padding: 10px;
|
||||||
.messagebox {
|
width: 100%;
|
||||||
height: 400px;
|
}
|
||||||
border: 1px solid #cccccc;
|
|
||||||
border-radius: 20px;
|
.infinitelistitem.selected {
|
||||||
}
|
background-color: #f0f0f0;
|
||||||
|
/* 灰色背景 */
|
||||||
.inputbox {
|
}
|
||||||
margin-top: 10px;
|
|
||||||
margin-left: 10px;
|
.messagebox {
|
||||||
width: 80%;
|
height: 400px;
|
||||||
}
|
border: 1px solid #cccccc;
|
||||||
|
border-radius: 20px;
|
||||||
.user-profile-link {
|
}
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
.inputbox {
|
||||||
/* 去掉链接的下划线 */
|
margin-top: 10px;
|
||||||
}
|
margin-left: 10px;
|
||||||
|
width: 80%;
|
||||||
.user-profile {
|
}
|
||||||
position: relative;
|
|
||||||
width: 50px;
|
.user-profile-link {
|
||||||
height: 50px;
|
display: inline-block;
|
||||||
border-radius: 50%;
|
text-decoration: none;
|
||||||
background-color: #fff;
|
/* 去掉链接的下划线 */
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
}
|
||||||
|
|
||||||
}
|
.user-profile {
|
||||||
|
position: relative;
|
||||||
.user-profile img {
|
width: 50px;
|
||||||
width: 100%;
|
height: 50px;
|
||||||
height: 100%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
background-color: #fff;
|
||||||
border-radius: 50%;
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
}
|
|
||||||
|
}
|
||||||
.status-dot {
|
|
||||||
position: absolute;
|
.user-profile img {
|
||||||
bottom: -5px;
|
width: 100%;
|
||||||
right: -5px;
|
height: 100%;
|
||||||
z-index: 1;
|
object-fit: cover;
|
||||||
width: 20px;
|
border-radius: 50%;
|
||||||
height: 20px;
|
}
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid white;
|
.status-dot {
|
||||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
position: absolute;
|
||||||
}
|
bottom: -5px;
|
||||||
|
right: -5px;
|
||||||
.online {
|
z-index: 1;
|
||||||
background-color: green;
|
width: 20px;
|
||||||
/* 在线状态 */
|
height: 20px;
|
||||||
}
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
.offline {
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
background-color: red;
|
}
|
||||||
/* 离线状态 */
|
|
||||||
}
|
.online {
|
||||||
|
background-color: green;
|
||||||
.message-item {
|
/* 在线状态 */
|
||||||
padding: 10px;
|
}
|
||||||
height: 80px;
|
|
||||||
width: 100%;
|
.offline {
|
||||||
position: relative;
|
background-color: red;
|
||||||
}
|
/* 离线状态 */
|
||||||
|
}
|
||||||
.message-item-profile {
|
|
||||||
position: absolute;
|
.message-item {
|
||||||
width: 40px;
|
padding: 10px;
|
||||||
height: 40px;
|
height: 80px;
|
||||||
border-radius: 50%;
|
width: 100%;
|
||||||
background-color: #fff;
|
position: relative;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
}
|
||||||
display: inline-block;
|
|
||||||
}
|
.message-item-profile {
|
||||||
|
position: absolute;
|
||||||
.message-item-profile img {
|
width: 40px;
|
||||||
width: 100%;
|
height: 40px;
|
||||||
height: 100%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
background-color: #fff;
|
||||||
border-radius: 50%;
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
}
|
display: inline-block;
|
||||||
|
}
|
||||||
.message-item-profile.left {
|
|
||||||
left: 20px;
|
.message-item-profile img {
|
||||||
}
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
.message-item-profile.right {
|
object-fit: cover;
|
||||||
right: 20px;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item-content {
|
.message-item-profile.left {
|
||||||
position: absolute;
|
left: 20px;
|
||||||
display: inline-block;
|
}
|
||||||
bottom: 28px;
|
|
||||||
}
|
.message-item-profile.right {
|
||||||
|
right: 20px;
|
||||||
.message-item-content.left {
|
}
|
||||||
left: 90px;
|
|
||||||
}
|
.message-item-content {
|
||||||
|
position: absolute;
|
||||||
.message-item-content.right {
|
display: inline-block;
|
||||||
right: 90px;
|
bottom: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item-time {
|
.message-item-content.left {
|
||||||
position: absolute;
|
left: 90px;
|
||||||
bottom: 23px;
|
}
|
||||||
height: 20px;
|
|
||||||
text-align: left;
|
.message-item-content.right {
|
||||||
font-size: small;
|
right: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item-time.left {
|
.message-item-time {
|
||||||
left: 20px;
|
position: absolute;
|
||||||
}
|
bottom: 23px;
|
||||||
|
height: 20px;
|
||||||
.message-item-time.right {
|
text-align: left;
|
||||||
right: 20px;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call {
|
.message-item-time.left {
|
||||||
margin: auto;
|
left: 20px;
|
||||||
margin-top: 10px;
|
}
|
||||||
}
|
|
||||||
|
.message-item-time.right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call {
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,88 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 顶部导航栏 -->
|
<!-- 顶部导航栏 -->
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
|
|
||||||
|
|
||||||
<div id="mainContent" class="main-content">
|
<div id="mainContent" class="main-content">
|
||||||
<!-- 左侧主空间 -->
|
<!-- 左侧主空间 -->
|
||||||
<div class="left-content">
|
<div class="left-content">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 语音面板 -->
|
<!-- 语音面板 -->
|
||||||
<phonePanel />
|
<phonePanel />
|
||||||
|
|
||||||
<!-- 消息按钮 -->
|
<!-- 消息按钮 -->
|
||||||
<GlobalMessageButton />
|
<GlobalMessageButton />
|
||||||
|
|
||||||
<!-- 右下角用户头像框 -->
|
<!-- 右下角用户头像框 -->
|
||||||
<UserProfile />
|
<UserProfile />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, watchEffect } from 'vue';
|
import { onMounted, watchEffect } from 'vue';
|
||||||
import Navbar from '../../components/navBar.vue';
|
import Navbar from '../../components/navBar.vue';
|
||||||
import UserProfile from '../../components/userProfile.vue';
|
import UserProfile from '../../components/userProfile.vue';
|
||||||
import { userInfoStore } from '@/store/user';
|
import { userInfoStore } from '@/store/user';
|
||||||
import { onlineSocketStore } from '@/store/Online';
|
import { onlineSocketStore } from '@/store/Online.ts';
|
||||||
import { getUserInfo } from '@/api/user';
|
import { getUserInfo } from '@/api/user';
|
||||||
import GlobalMessageButton from '@/components/GlobalMessageButton.vue';
|
import GlobalMessageButton from '@/components/GlobalMessageButton.vue';
|
||||||
import { messageStore } from '@/store/message.ts';
|
import { messageStore } from '@/store/message.ts';
|
||||||
import phonePanel from '@/components/phonePanel.vue';
|
import phonePanel from '@/components/phonePanel.vue';
|
||||||
import { voiceStore } from '@/store/Voice';
|
import { initDB } from '@/functions/historyMessages';
|
||||||
import { initDB } from '@/functions/historyMessages';
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
const voice = voiceStore();
|
const socket = onlineSocketStore();
|
||||||
|
const message = messageStore();
|
||||||
const userinfo = userInfoStore();
|
|
||||||
const socket = onlineSocketStore();
|
onMounted(() => {
|
||||||
const message = messageStore();
|
getUserInfo()
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用 watchEffect 监听 u_id 是否为空
|
||||||
getUserInfo()
|
watchEffect(() => {
|
||||||
|
if (userinfo.user.u_id) { // 如果 u_id 不为空
|
||||||
// 使用 watchEffect 监听 u_id 是否为空
|
console.log('User ID is available:', userinfo.user.u_id);
|
||||||
watchEffect(() => {
|
socket.connect(userinfo.user.id); // 建立 WebSocket 连接
|
||||||
if (userinfo.user.u_id) { // 如果 u_id 不为空
|
message.to = userinfo.user.id; // 设置消息发送者为当前用户 ID
|
||||||
console.log('User ID is available:', userinfo.user.u_id);
|
|
||||||
socket.connect(userinfo.user.id); // 建立 WebSocket 连接
|
// voice.connect(userinfo.user.u_id); // 建立语音通话连接
|
||||||
message.to = userinfo.user.id; // 设置消息发送者为当前用户 ID
|
}
|
||||||
|
});
|
||||||
// voice.connect(userinfo.user.u_id); // 建立语音通话连接
|
|
||||||
}
|
initDB(); // 初始化历史消息数据库
|
||||||
});
|
})
|
||||||
|
|
||||||
initDB(); // 初始化历史消息数据库
|
|
||||||
})
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
</script>
|
.home-container {
|
||||||
|
position: relative;
|
||||||
<style scoped>
|
top: 0px;
|
||||||
.home-container {
|
display: flex;
|
||||||
position: relative;
|
flex-direction: column;
|
||||||
top: 0px;
|
height: 100vh;
|
||||||
display: flex;
|
width: 100vw;
|
||||||
flex-direction: column;
|
}
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
.main-content {
|
||||||
}
|
position: relative;
|
||||||
|
left: 0;
|
||||||
.main-content {
|
display: flex;
|
||||||
position: relative;
|
flex: 1;
|
||||||
left: 0;
|
margin-top: 72px;
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
}
|
||||||
margin-top: 72px;
|
|
||||||
|
.left-content {
|
||||||
}
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
.left-content {
|
}
|
||||||
flex: 1;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,191 +1,218 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { userInfoStore } from "@/store/user";
|
import { userInfoStore } from "@/store/user";
|
||||||
import { messageStore } from "@/store/message";
|
import { messageStore } from "@/store/message";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { messageSignStore } from "@/store/message_sign";
|
import { messageSignStore } from "@/store/message_sign";
|
||||||
import { groupMessageStore } from "@/store/group_message";
|
import { groupMessageStore } from "@/store/group_message";
|
||||||
|
import { onCallStore } from "@/store/VoiceTarget";
|
||||||
// userinfo 实例
|
import { handleOffer, handleAnswer, closeConnection, handleCandidate } from "@/store/Voice.ts";
|
||||||
const userinfo = userInfoStore();
|
|
||||||
// message 实例
|
// userinfo 实例
|
||||||
const message = messageStore();
|
const userinfo = userInfoStore();
|
||||||
// groupMessage 实例
|
// message 实例
|
||||||
const groupMessage = groupMessageStore();
|
const message = messageStore();
|
||||||
// messageSignStoregn 实例
|
// groupMessage 实例
|
||||||
// const messageSign = messageSignStore();
|
const groupMessage = groupMessageStore();
|
||||||
|
// oncall 实例
|
||||||
// WebSocket 实例
|
const oncall = onCallStore();
|
||||||
const socket = ref(null);
|
// messageSignStoregn 实例
|
||||||
const messageSign = messageSignStore();
|
// const messageSign = messageSignStore();
|
||||||
|
|
||||||
const isManualClose = ref(false);
|
// WebSocket 实例
|
||||||
|
const socket = ref(null);
|
||||||
const reconnectScheduled = ref(false);
|
const messageSign = messageSignStore();
|
||||||
|
|
||||||
const retryCount = ref(0);
|
const isManualClose = ref(false);
|
||||||
|
|
||||||
export const getRetryCount = () => {
|
const reconnectScheduled = ref(false);
|
||||||
return retryCount.value;
|
|
||||||
};
|
const retryCount = ref(0);
|
||||||
|
|
||||||
export const addRetryCount = () => {
|
|
||||||
retryCount.value = retryCount.value + 1;
|
export const getRetryCount = () => {
|
||||||
};
|
return retryCount.value;
|
||||||
|
};
|
||||||
export const resetRetryCount = () => {
|
|
||||||
retryCount.value = 0;
|
export const addRetryCount = () => {
|
||||||
};
|
retryCount.value = retryCount.value + 1;
|
||||||
export const setReconnectScheduled = (value) => {
|
};
|
||||||
reconnectScheduled.value = value;
|
|
||||||
};
|
export const resetRetryCount = () => {
|
||||||
|
retryCount.value = 0;
|
||||||
export const getReconnectScheduled = () => {
|
};
|
||||||
return reconnectScheduled.value;
|
export const setReconnectScheduled = (value) => {
|
||||||
};
|
reconnectScheduled.value = value;
|
||||||
|
};
|
||||||
export const setIsManualClose = (value) => {
|
|
||||||
isManualClose.value = value;
|
export const getReconnectScheduled = () => {
|
||||||
};
|
return reconnectScheduled.value;
|
||||||
|
};
|
||||||
export const getIsManualClose = () => {
|
|
||||||
return isManualClose.value;
|
export const setIsManualClose = (value) => {
|
||||||
};
|
isManualClose.value = value;
|
||||||
|
};
|
||||||
// 连接WebSocket
|
|
||||||
export const connectWebSocket = () => {
|
export const getIsManualClose = () => {
|
||||||
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
return isManualClose.value;
|
||||||
const host = window.location.host;
|
};
|
||||||
const socketUrl = `${protocol}${host}/ws/online?name=${userinfo.user.u_name}`;
|
|
||||||
|
// 连接WebSocket
|
||||||
// const socketUrl = `ws://localhost:8080/online?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`;
|
export const connectWebSocket = () => {
|
||||||
if (socket.value && socket.value.readyState !== WebSocket.CLOSED) {
|
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
console.log("还在重连中...");
|
const host = window.location.host;
|
||||||
return;
|
const socketUrl = `${protocol}${host}/ws/online?name=${userinfo.user.u_name}`;
|
||||||
}
|
|
||||||
const retrytime = getRetryCount();
|
// const socketUrl = `ws://localhost:8080/online?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`;
|
||||||
if (retrytime >= 10) {
|
if (socket.value && socket.value.readyState !== WebSocket.CLOSED) {
|
||||||
console.log("重连失败,请稍后再试");
|
console.log("还在重连中...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(retrytime);
|
const retrytime = getRetryCount();
|
||||||
socket.value = new WebSocket(socketUrl, "token-"+ userinfo.token);
|
if (retrytime >= 10) {
|
||||||
|
console.log("重连失败,请稍后再试");
|
||||||
socket.value.onopen = (event) => {
|
return;
|
||||||
console.log("WebSocket连接已建立", event);
|
}
|
||||||
setReconnectScheduled(false);
|
console.log(retrytime);
|
||||||
setIsManualClose(false);
|
socket.value = new WebSocket(socketUrl, "token-"+ userinfo.token);
|
||||||
resetRetryCount();
|
|
||||||
};
|
socket.value.onopen = (event) => {
|
||||||
|
console.log("WebSocket连接已建立", event);
|
||||||
//处理消息逻辑
|
setReconnectScheduled(false);
|
||||||
socket.value.onmessage = (event) => {
|
setIsManualClose(false);
|
||||||
console.log("从服务器收到消息:", event.data);
|
resetRetryCount();
|
||||||
try{
|
};
|
||||||
const MessageData = JSON.parse(event.data);
|
|
||||||
const cmd = MessageData.cmd;
|
//处理消息逻辑
|
||||||
switch(cmd){
|
socket.value.onmessage = async (event) => {
|
||||||
case "MESSAGE":
|
console.log("从服务器收到消息:", event.data);
|
||||||
message.addMessage(MessageData);
|
try{
|
||||||
// TODO:需要使用u_name或者u_id进行消息标记
|
const MessageData = JSON.parse(event.data);
|
||||||
// messageSign.setMessageSign(true);
|
const cmd = MessageData.cmd;
|
||||||
// ElMessage.info("您有一条新的消息");
|
switch(cmd){
|
||||||
break;
|
case "MESSAGE":
|
||||||
case "PERSONAL_NOTIFY":
|
message.addMessage(MessageData);
|
||||||
messageSign.setMessageSign(true);
|
// TODO:需要使用u_name或者u_id进行消息标记
|
||||||
ElMessage.info("您有一条新的邀请消息");
|
// messageSign.setMessageSign(true);
|
||||||
break;
|
// ElMessage.info("您有一条新的消息");
|
||||||
case "GROUP_MESSAGE":
|
break;
|
||||||
groupMessage.recieveMessage(userinfo.user.id, MessageData);
|
case "PERSONAL_NOTIFY":
|
||||||
|
messageSign.setMessageSign(true);
|
||||||
// TODO: 需要使用groupId进行消息标记
|
ElMessage.info("您有一条新的邀请消息");
|
||||||
break;
|
break;
|
||||||
case "VIDEO_SYNC":
|
case "GROUP_MESSAGE":
|
||||||
console.log("视频同步消息", MessageData);
|
groupMessage.recieveMessage(userinfo.user.id, MessageData);
|
||||||
break;
|
|
||||||
|
// TODO: 需要使用groupId进行消息标记
|
||||||
// 语音通话相关:
|
break;
|
||||||
case "VOICE_CALL_REQUEST":
|
case "VIDEO_SYNC":
|
||||||
ElMessage.info("您有一个新的语音通话请求");
|
console.log("视频同步消息", MessageData);
|
||||||
break;
|
break;
|
||||||
case "VOICE_CALL_RESPONSE":
|
|
||||||
console.log("收到语音通话响应消息");
|
// 语音通话相关:
|
||||||
break;
|
case "VOICE_CALL_REQUEST":
|
||||||
case "VOICE_CALL_DENY":
|
ElMessage.info("您有一个新的语音通话请求");
|
||||||
ElMessage.info("对方拒绝了您的语音通话请求");
|
oncall.setTarget(MessageData.from, MessageData.from_name, MessageData.from_avatar);
|
||||||
break;
|
oncall.panelOn();
|
||||||
case "VOICE_CALL_END":
|
oncall.callingOn();
|
||||||
ElMessage.info("语音通话已结束");
|
oncall.fromOff();
|
||||||
break;
|
oncall.statusOff();
|
||||||
case "VOICE_ICE_CANDIDATE":
|
break;
|
||||||
console.log("收到新的ICE候选", MessageData.candidate);
|
case "VOICE_CALL_RESPONSE":
|
||||||
break;
|
console.log("收到语音通话响应消息");
|
||||||
case "VOICE_SDP_OFFER":
|
//直接跳转到icecandidate阶段,不再接收额外的响应消息
|
||||||
console.log("收到SDP Offer", MessageData.sdp);
|
break;
|
||||||
break;
|
case "VOICE_CALL_DENY":
|
||||||
case "VOICE_SDP_ANSWER":
|
ElMessage.info("对方拒绝了您的语音通话请求");
|
||||||
console.log("收到SDP Answer", MessageData.sdp);
|
oncall.clear();
|
||||||
break;
|
break;
|
||||||
|
case "VOICE_CALL_END":
|
||||||
}
|
ElMessage.info("语音通话已结束");
|
||||||
}catch(error){
|
closeConnection();
|
||||||
console.error("解析 JSON 失败:", error);
|
oncall.clear();
|
||||||
}
|
break;
|
||||||
|
case "VOICE_ICE_CANDIDATE":
|
||||||
};
|
// stage3 : 收到ICE候选,添加到RTC连接中
|
||||||
|
console.log("收到新的ICE候选");
|
||||||
socket.value.onerror = (error) => {
|
await handleCandidate(MessageData.content);
|
||||||
console.error("WebSocket发生错误:", error);
|
break;
|
||||||
// console.log(error);
|
case "VOICE_SDP_OFFER":
|
||||||
setReconnectScheduled(true);
|
// stage1 :收到offer,发送answer
|
||||||
socket.value.close();
|
console.log("收到SDP Offer");
|
||||||
};
|
await handleOffer(MessageData.content, MessageData.from, MessageData.to);
|
||||||
|
break;
|
||||||
socket.value.onclose = (event) => {
|
case "VOICE_SDP_ANSWER":
|
||||||
message.saveMessagesHistory(userinfo.user.id);
|
// stage2 : 收到answer,设置远端SDP
|
||||||
if (!getIsManualClose()) {
|
console.log("收到SDP Answer");
|
||||||
if (getReconnectScheduled()) {
|
await handleAnswer(MessageData.content, MessageData.from, MessageData.to);
|
||||||
socket.value = null;
|
break;
|
||||||
addRetryCount();
|
|
||||||
setTimeout(reConnectWebSocket, 5000);
|
}
|
||||||
setReconnectScheduled(false);
|
}catch(error){
|
||||||
} else {
|
console.error("解析 JSON 失败:", error);
|
||||||
// console.log("websocket因为浏览器省电设置断开");
|
}
|
||||||
console.log("WebSocket连接已关闭", event);
|
|
||||||
}
|
};
|
||||||
}
|
|
||||||
};
|
socket.value.onerror = (error) => {
|
||||||
};
|
console.error("WebSocket发生错误:", error);
|
||||||
|
// console.log(error);
|
||||||
// 断开WebSocket连接
|
setReconnectScheduled(true);
|
||||||
export const disconnectWebSocket = () => {
|
socket.value.close();
|
||||||
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
};
|
||||||
socket.value.close();
|
|
||||||
}
|
socket.value.onclose = (event) => {
|
||||||
};
|
message.saveMessagesHistory(userinfo.user.id);
|
||||||
|
if (!getIsManualClose()) {
|
||||||
// 重连机制
|
if (getReconnectScheduled()) {
|
||||||
export const reConnectWebSocket = () => {
|
socket.value = null;
|
||||||
connectWebSocket();
|
addRetryCount();
|
||||||
};
|
setTimeout(reConnectWebSocket, 5000);
|
||||||
|
setReconnectScheduled(false);
|
||||||
// 发送消息
|
} else {
|
||||||
export const sendMessage = (message) => {
|
// console.log("websocket因为浏览器省电设置断开");
|
||||||
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
console.log("WebSocket连接已关闭", event);
|
||||||
socket.value.send(message);
|
}
|
||||||
} else {
|
}
|
||||||
console.warn("WebSocket未连接,无法发送消息");
|
};
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
// 断开WebSocket连接
|
||||||
//没有错误的重连,只是浏览器在后台断开了连接
|
export const disconnectWebSocket = () => {
|
||||||
document.addEventListener("visibilitychange", () => {
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
if (document.hidden) {
|
socket.value.close();
|
||||||
} else {
|
}
|
||||||
if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) {
|
};
|
||||||
if (getReconnectScheduled()) {
|
|
||||||
return;
|
// 重连机制
|
||||||
}
|
export const reConnectWebSocket = () => {
|
||||||
reConnectWebSocket();
|
connectWebSocket();
|
||||||
}
|
};
|
||||||
}
|
|
||||||
});
|
// 发送消息
|
||||||
|
export const sendMessage = (message) => {
|
||||||
|
try {
|
||||||
|
const jsonmessage = JSON.stringify(message);
|
||||||
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
|
socket.value.send(jsonmessage);
|
||||||
|
} else {
|
||||||
|
console.warn("WebSocket未连接,无法发送消息");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Failed to stringify message:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
//没有错误的重连,只是浏览器在后台断开了连接
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
} else {
|
||||||
|
if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) {
|
||||||
|
if (getReconnectScheduled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reConnectWebSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,287 +1,286 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { userInfoStore } from "@/store/user";
|
import { userInfoStore } from "@/store/user";
|
||||||
import { onCallStore } from "@/store/VoiceTarget";
|
import { onCallStore } from "@/store/VoiceTarget";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
// userinfo 实例
|
// userinfo 实例
|
||||||
const userinfo = userInfoStore();
|
const userinfo = userInfoStore();
|
||||||
// oncall 实例
|
// oncall 实例
|
||||||
const oncall = onCallStore();
|
const oncall = onCallStore();
|
||||||
// WebSocket 实例
|
// WebSocket 实例
|
||||||
const socket = ref(null);
|
const socket = ref(null);
|
||||||
|
|
||||||
const isManualClose = ref(false);
|
const isManualClose = ref(false);
|
||||||
const reconnectScheduled = ref(false);
|
const reconnectScheduled = ref(false);
|
||||||
const retryCount = ref(0);
|
const retryCount = ref(0);
|
||||||
|
|
||||||
const getRetryCount = () => {
|
const getRetryCount = () => {
|
||||||
return retryCount.value;
|
return retryCount.value;
|
||||||
};
|
};
|
||||||
const addRetryCount = () => {
|
const addRetryCount = () => {
|
||||||
retryCount.value = retryCount.value + 1;
|
retryCount.value = retryCount.value + 1;
|
||||||
};
|
};
|
||||||
const resetRetryCount = () => {
|
const resetRetryCount = () => {
|
||||||
retryCount.value = 0;
|
retryCount.value = 0;
|
||||||
};
|
};
|
||||||
const setReconnectScheduled = (value) => {
|
const setReconnectScheduled = (value) => {
|
||||||
reconnectScheduled.value = value;
|
reconnectScheduled.value = value;
|
||||||
};
|
};
|
||||||
const getReconnectScheduled = () => {
|
const getReconnectScheduled = () => {
|
||||||
return reconnectScheduled.value;
|
return reconnectScheduled.value;
|
||||||
};
|
};
|
||||||
const setIsManualClose = (value) => {
|
const setIsManualClose = (value) => {
|
||||||
isManualClose.value = value;
|
isManualClose.value = value;
|
||||||
};
|
};
|
||||||
const getIsManualClose = () => {
|
const getIsManualClose = () => {
|
||||||
return isManualClose.value;
|
return isManualClose.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const iceserver = {
|
const iceserver = {
|
||||||
iceServers: [
|
iceServers: [
|
||||||
{
|
{
|
||||||
// urls: "stun:stun.l.google.com:19302"
|
urls: process.env.STUN_URL,
|
||||||
urls: "stun:8.134.92.199:3478",
|
},
|
||||||
},
|
{
|
||||||
{
|
urls: process.env.TURN_URL,
|
||||||
urls: "turn:8.134.92.199:3478",
|
username: process.env.TURN_USERNAME,
|
||||||
username: "test",
|
credential: process.env.TURN_CREDENTIAL,
|
||||||
credential: "123456",
|
},
|
||||||
},
|
],
|
||||||
],
|
};
|
||||||
};
|
|
||||||
|
// 连接WebSocket
|
||||||
// 连接WebSocket
|
export const connectVoicesocket = () => {
|
||||||
export const connectVoicesocket = () => {
|
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
const host = window.location.host;
|
||||||
const host = window.location.host;
|
const socketUrl = `${protocol}${host}/voice?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`;
|
||||||
const socketUrl = `${protocol}${host}/voice?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`;
|
|
||||||
|
if (socket.value && socket.value.readyState !== WebSocket.CLOSED) {
|
||||||
if (socket.value && socket.value.readyState !== WebSocket.CLOSED) {
|
console.log("还在重连中...");
|
||||||
console.log("还在重连中...");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const retrytime = getRetryCount();
|
||||||
const retrytime = getRetryCount();
|
if (retrytime >= 10) {
|
||||||
if (retrytime >= 10) {
|
console.log("重连失败,请稍后再试");
|
||||||
console.log("重连失败,请稍后再试");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
console.log(retrytime);
|
||||||
console.log(retrytime);
|
socket.value = new WebSocket(socketUrl);
|
||||||
socket.value = new WebSocket(socketUrl);
|
|
||||||
|
socket.value.onopen = (event) => {
|
||||||
socket.value.onopen = (event) => {
|
console.log("Voice连接已建立", event);
|
||||||
console.log("Voice连接已建立", event);
|
setReconnectScheduled(false);
|
||||||
setReconnectScheduled(false);
|
setIsManualClose(false);
|
||||||
setIsManualClose(false);
|
resetRetryCount();
|
||||||
resetRetryCount();
|
};
|
||||||
};
|
|
||||||
|
// 处理消息逻辑
|
||||||
// 处理消息逻辑
|
socket.value.onmessage = async (event) => {
|
||||||
socket.value.onmessage = async (event) => {
|
try {
|
||||||
try {
|
const data = JSON.parse(event.data);
|
||||||
const data = JSON.parse(event.data);
|
if (data.type === "status") {
|
||||||
if (data.type === "status") {
|
ElMessage.error("对方已离线");
|
||||||
ElMessage.error("对方已离线");
|
oncall.panelOff();
|
||||||
oncall.panelOff();
|
oncall.callingOff();
|
||||||
oncall.callingOff();
|
} else if (data.type === "incomingcall") {
|
||||||
} else if (data.type === "incomingcall") {
|
// 通话请求处理
|
||||||
// 通话请求处理
|
oncall.setTarget(data.from, data.from_name, data.from_avatar);
|
||||||
oncall.setTarget(data.from, data.from_name, data.from_avatar);
|
oncall.panelOn();
|
||||||
oncall.panelOn();
|
oncall.callingOn();
|
||||||
oncall.callingOn();
|
oncall.fromOff();
|
||||||
oncall.fromOff();
|
oncall.statusOff();
|
||||||
oncall.statusOff();
|
} else if (data.type === "pickup") {
|
||||||
} else if (data.type === "pickup") {
|
ElMessage.success("对方已接听,等待连接中...");
|
||||||
ElMessage.success("对方已接听,等待连接中...");
|
try {
|
||||||
try {
|
await initRTCconnection();
|
||||||
await initRTCconnection();
|
RTCpeerConnection.value.onicecandidate = (event) => {
|
||||||
RTCpeerConnection.value.onicecandidate = (event) => {
|
if (event.candidate) {
|
||||||
if (event.candidate) {
|
console.log("发送candidate", event.candidate);
|
||||||
console.log("发送candidate", event.candidate);
|
const message = {
|
||||||
const message = {
|
type: "single",
|
||||||
type: "single",
|
content: "candidate",
|
||||||
content: "candidate",
|
from: userinfo.user.u_id,
|
||||||
from: userinfo.user.u_id,
|
to: oncall.target.u_id,
|
||||||
to: oncall.target.u_id,
|
candidate: event.candidate,
|
||||||
candidate: event.candidate,
|
};
|
||||||
};
|
sendMessage(JSON.stringify(message));
|
||||||
sendMessage(JSON.stringify(message));
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
RTCpeerConnection.value.ontrack = (event) => {
|
||||||
RTCpeerConnection.value.ontrack = (event) => {
|
console.log("收到对方音频流", event);
|
||||||
console.log("收到对方音频流", event);
|
oncall.setRemoteStream(event.streams[0]);
|
||||||
oncall.setRemoteStream(event.streams[0]);
|
};
|
||||||
};
|
} catch (e) {
|
||||||
} catch (e) {
|
console.log("webRTC初始化失败", e);
|
||||||
console.log("webRTC初始化失败", e);
|
}
|
||||||
}
|
|
||||||
|
const offer = await RTCpeerConnection.value.createOffer({
|
||||||
const offer = await RTCpeerConnection.value.createOffer({
|
offerToReceiveAudio: 1,
|
||||||
offerToReceiveAudio: 1,
|
});
|
||||||
});
|
|
||||||
|
await RTCpeerConnection.value.setLocalDescription(offer);
|
||||||
await RTCpeerConnection.value.setLocalDescription(offer);
|
|
||||||
|
// 发送offer
|
||||||
// 发送offer
|
const message = {
|
||||||
const message = {
|
type: "single",
|
||||||
type: "single",
|
content: "offer",
|
||||||
content: "offer",
|
from: userinfo.user.u_id,
|
||||||
from: userinfo.user.u_id,
|
to: oncall.target.u_id,
|
||||||
to: oncall.target.u_id,
|
offer: offer,
|
||||||
offer: offer,
|
};
|
||||||
};
|
sendMessage(JSON.stringify(message));
|
||||||
sendMessage(JSON.stringify(message));
|
} else if (data.type === "hangup") {
|
||||||
} else if (data.type === "hangup") {
|
ElMessage.success("对方已挂断");
|
||||||
ElMessage.success("对方已挂断");
|
oncall.panelOff();
|
||||||
oncall.panelOff();
|
oncall.callingOff();
|
||||||
oncall.callingOff();
|
oncall.fromOff();
|
||||||
oncall.fromOff();
|
oncall.statusOff();
|
||||||
oncall.statusOff();
|
RTCpeerConnection.value.close();
|
||||||
RTCpeerConnection.value.close();
|
RTCpeerConnection.value = null;
|
||||||
RTCpeerConnection.value = null;
|
oncall.resetStream();
|
||||||
oncall.resetStream();
|
} else if (data.content === "offer") {
|
||||||
} else if (data.content === "offer") {
|
ElMessage.success("收到对方offer");
|
||||||
ElMessage.success("收到对方offer");
|
console.log("收到对方offer", data);
|
||||||
console.log("收到对方offer", data);
|
|
||||||
|
try {
|
||||||
try {
|
await initRTCconnection();
|
||||||
await initRTCconnection();
|
|
||||||
|
RTCpeerConnection.value.onicecandidate = (event) => {
|
||||||
RTCpeerConnection.value.onicecandidate = (event) => {
|
if (event.candidate) {
|
||||||
if (event.candidate) {
|
console.log("发送candidate", event.candidate);
|
||||||
console.log("发送candidate", event.candidate);
|
const message = {
|
||||||
const message = {
|
type: "single",
|
||||||
type: "single",
|
content: "candidate",
|
||||||
content: "candidate",
|
from: userinfo.user.u_id,
|
||||||
from: userinfo.user.u_id,
|
to: oncall.target.u_id,
|
||||||
to: oncall.target.u_id,
|
candidate: event.candidate,
|
||||||
candidate: event.candidate,
|
};
|
||||||
};
|
sendMessage(JSON.stringify(message));
|
||||||
sendMessage(JSON.stringify(message));
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
RTCpeerConnection.value.ontrack = (event) => {
|
||||||
RTCpeerConnection.value.ontrack = (event) => {
|
console.log("收到对方音频流", event);
|
||||||
console.log("收到对方音频流", event);
|
oncall.setRemoteStream(event.streams[0]);
|
||||||
oncall.setRemoteStream(event.streams[0]);
|
};
|
||||||
};
|
} catch (e) {
|
||||||
} catch (e) {
|
console.log("webRTC初始化失败", e);
|
||||||
console.log("webRTC初始化失败", e);
|
}
|
||||||
}
|
|
||||||
|
await RTCpeerConnection.value.setRemoteDescription(data.offer);
|
||||||
await RTCpeerConnection.value.setRemoteDescription(data.offer);
|
const answer = await RTCpeerConnection.value.createAnswer();
|
||||||
const answer = await RTCpeerConnection.value.createAnswer();
|
await RTCpeerConnection.value.setLocalDescription(answer);
|
||||||
await RTCpeerConnection.value.setLocalDescription(answer);
|
|
||||||
|
// 发送answer
|
||||||
// 发送answer
|
const message = {
|
||||||
const message = {
|
type: "single",
|
||||||
type: "single",
|
content: "answer",
|
||||||
content: "answer",
|
from: userinfo.user.u_id,
|
||||||
from: userinfo.user.u_id,
|
to: oncall.target.u_id,
|
||||||
to: oncall.target.u_id,
|
answer: answer,
|
||||||
answer: answer,
|
};
|
||||||
};
|
sendMessage(JSON.stringify(message));
|
||||||
sendMessage(JSON.stringify(message));
|
} else if (data.content === "answer") {
|
||||||
} else if (data.content === "answer") {
|
ElMessage.success("对方已接受");
|
||||||
ElMessage.success("对方已接受");
|
console.log("对方已接受", data);
|
||||||
console.log("对方已接受", data);
|
await RTCpeerConnection.value.setRemoteDescription(data.answer);
|
||||||
await RTCpeerConnection.value.setRemoteDescription(data.answer);
|
} else if (data.content === "candidate") {
|
||||||
} else if (data.content === "candidate") {
|
console.log("收到candidate", data);
|
||||||
console.log("收到candidate", data);
|
await RTCpeerConnection.value.addIceCandidate(data.candidate);
|
||||||
await RTCpeerConnection.value.addIceCandidate(data.candidate);
|
}
|
||||||
}
|
} catch (e) {
|
||||||
} catch (e) {
|
console.log("VoiceSocket消息格式错误", e);
|
||||||
console.log("VoiceSocket消息格式错误", e);
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
socket.value.onerror = (error) => {
|
||||||
socket.value.onerror = (error) => {
|
console.error("Voicesocket发生错误:", error);
|
||||||
console.error("Voicesocket发生错误:", error);
|
setReconnectScheduled(true);
|
||||||
setReconnectScheduled(true);
|
socket.value.close();
|
||||||
socket.value.close();
|
};
|
||||||
};
|
|
||||||
|
socket.value.onclose = (event) => {
|
||||||
socket.value.onclose = (event) => {
|
if (!getIsManualClose()) {
|
||||||
if (!getIsManualClose()) {
|
if (getReconnectScheduled()) {
|
||||||
if (getReconnectScheduled()) {
|
socket.value = null;
|
||||||
socket.value = null;
|
addRetryCount();
|
||||||
addRetryCount();
|
setTimeout(reConnectVoicesocket, 5000);
|
||||||
setTimeout(reConnectVoicesocket, 5000);
|
setReconnectScheduled(false);
|
||||||
setReconnectScheduled(false);
|
} else {
|
||||||
} else {
|
console.log("Voicesocket因为浏览器省电设置断开");
|
||||||
console.log("Voicesocket因为浏览器省电设置断开");
|
console.log("Voicesocket连接已关闭", event);
|
||||||
console.log("Voicesocket连接已关闭", event);
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
// 断开Voicesocket连接
|
||||||
// 断开Voicesocket连接
|
export const disconnectVoicesocket = () => {
|
||||||
export const disconnectVoicesocket = () => {
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
socket.value.close();
|
||||||
socket.value.close();
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
// 重连机制
|
||||||
// 重连机制
|
export const reConnectVoicesocket = () => {
|
||||||
export const reConnectVoicesocket = () => {
|
connectVoicesocket();
|
||||||
connectVoicesocket();
|
};
|
||||||
};
|
|
||||||
|
// 发送消息
|
||||||
// 发送消息
|
export const sendMessage = (message) => {
|
||||||
export const sendMessage = (message) => {
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
socket.value.send(message);
|
||||||
socket.value.send(message);
|
} else {
|
||||||
} else {
|
console.warn("Voicesocket未连接,无法发送消息");
|
||||||
console.warn("Voicesocket未连接,无法发送消息");
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
//没有错误的重连,只是浏览器在后台断开了连接
|
||||||
//没有错误的重连,只是浏览器在后台断开了连接
|
document.addEventListener("visibilitychange", () => {
|
||||||
document.addEventListener("visibilitychange", () => {
|
if (document.hidden) {
|
||||||
if (document.hidden) {
|
} else {
|
||||||
} else {
|
if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) {
|
||||||
if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) {
|
if (getReconnectScheduled()) {
|
||||||
if (getReconnectScheduled()) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
reConnectVoicesocket();
|
||||||
reConnectVoicesocket();
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
//WebRTC连接相关代码
|
||||||
//WebRTC连接相关代码
|
export const RTCpeerConnection = ref(null);
|
||||||
const RTCpeerConnection = ref(null);
|
|
||||||
|
const getlocalStream = async () => {
|
||||||
export const getlocalStream = async () => {
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
console.log("获取本地音频流成功", stream);
|
||||||
console.log("获取本地音频流成功", stream);
|
return stream;
|
||||||
return stream;
|
// // 获取音频和视频轨道
|
||||||
// // 获取音频和视频轨道
|
// const audioTrack = stream.getAudioTracks()[0];
|
||||||
// const audioTrack = stream.getAudioTracks()[0];
|
// // 将轨道添加到 RTCPeerConnection
|
||||||
// // 将轨道添加到 RTCPeerConnection
|
// peerConnection.addTrack(audioTrack, stream);
|
||||||
// peerConnection.addTrack(audioTrack, stream);
|
};
|
||||||
};
|
|
||||||
|
const initRTCconnection = async () => {
|
||||||
const initRTCconnection = async () => {
|
RTCpeerConnection.value = new RTCPeerConnection(iceserver);
|
||||||
RTCpeerConnection.value = new RTCPeerConnection(iceserver);
|
const stream = await getlocalStream();
|
||||||
const stream = await getlocalStream();
|
RTCpeerConnection.value.addTrack(stream.getAudioTracks()[0], stream);
|
||||||
RTCpeerConnection.value.addTrack(stream.getAudioTracks()[0], stream);
|
};
|
||||||
};
|
|
||||||
|
export const hangup = () => {
|
||||||
export const hangup = () => {
|
oncall.panelOff();
|
||||||
oncall.panelOff();
|
oncall.callingOff();
|
||||||
oncall.callingOff();
|
oncall.fromOff();
|
||||||
oncall.fromOff();
|
oncall.statusOff();
|
||||||
oncall.statusOff();
|
oncall.resetStream();
|
||||||
oncall.resetStream();
|
RTCpeerConnection.value.close();
|
||||||
RTCpeerConnection.value.close();
|
RTCpeerConnection.value = null;
|
||||||
RTCpeerConnection.value = null;
|
|
||||||
|
const message = {
|
||||||
const message = {
|
type: "hangup",
|
||||||
type: "hangup",
|
from: userinfo.user.u_id,
|
||||||
from: userinfo.user.u_id,
|
to: oncall.target.u_id,
|
||||||
to: oncall.target.u_id,
|
};
|
||||||
};
|
sendMessage(JSON.stringify(message));
|
||||||
sendMessage(JSON.stringify(message));
|
};
|
||||||
};
|
|
||||||
131
vite.config.js
131
vite.config.js
@@ -1,61 +1,70 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
dotenv.config();
|
||||||
resolve: {
|
|
||||||
alias: {
|
export default defineConfig({
|
||||||
'@': '/src',
|
plugins: [vue()],
|
||||||
}
|
resolve: {
|
||||||
},
|
alias: {
|
||||||
server: {
|
'@': '/src',
|
||||||
proxy: {
|
}
|
||||||
'/api': {
|
},
|
||||||
target: 'http://localhost:8080', // 后端服务器地址
|
server: {
|
||||||
changeOrigin: true, // 允许跨域
|
proxy: {
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,去掉 /api 前缀
|
'/api': {
|
||||||
},
|
target: 'http://localhost:8080', // 后端服务器地址
|
||||||
'/proxy': {
|
changeOrigin: true, // 允许跨域
|
||||||
target: 'http://localhost:3000', // 代理服务器的地址
|
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,去掉 /api 前缀
|
||||||
changeOrigin: true, // 必须设置为 true,才能避免跨域问题
|
},
|
||||||
// rewrite: (path) => path.replace(/^\/proxy/, ''), // 重写路径,去掉 /api 前缀
|
'/proxy': {
|
||||||
|
target: 'http://localhost:3000', // 代理服务器的地址
|
||||||
},
|
changeOrigin: true, // 必须设置为 true,才能避免跨域问题
|
||||||
'/ws': {
|
// rewrite: (path) => path.replace(/^\/proxy/, ''), // 重写路径,去掉 /api 前缀
|
||||||
target: 'ws://localhost:8080',
|
|
||||||
changeOrigin: true,
|
},
|
||||||
ws: true,
|
'/ws': {
|
||||||
},
|
target: 'ws://localhost:8080',
|
||||||
// '/ws': {
|
changeOrigin: true,
|
||||||
// target: 'ws://localhost:8080',
|
ws: true,
|
||||||
// changeOrigin: true,
|
},
|
||||||
// ws: true,
|
// '/ws': {
|
||||||
// }
|
// target: 'ws://localhost:8080',
|
||||||
},
|
// changeOrigin: true,
|
||||||
host: '0.0.0.0',
|
// ws: true,
|
||||||
port: 5173,
|
// }
|
||||||
},
|
},
|
||||||
// server: {
|
host: '0.0.0.0',
|
||||||
// https:{
|
port: 5173,
|
||||||
// key: fs.readFileSync('./cert/merlin.xin.key'),
|
},
|
||||||
// cert: fs.readFileSync('./cert/merlin.xin.pem'),
|
// server: {
|
||||||
// },
|
// https:{
|
||||||
// proxy: {
|
// key: fs.readFileSync('./cert/merlin.xin.key'),
|
||||||
// '/api': {
|
// cert: fs.readFileSync('./cert/merlin.xin.pem'),
|
||||||
// target: 'https://localhost:8443', // 后端服务器地址
|
// },
|
||||||
// changeOrigin: true, // 允许跨域
|
// proxy: {
|
||||||
// rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,去掉 /api 前缀
|
// '/api': {
|
||||||
// },
|
// target: 'https://localhost:8443', // 后端服务器地址
|
||||||
// '/online':{
|
// changeOrigin: true, // 允许跨域
|
||||||
// target:'wss://localhost:8443/online',
|
// rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,去掉 /api 前缀
|
||||||
// changeOrigin:true,
|
// },
|
||||||
// ws:true,
|
// '/online':{
|
||||||
// },
|
// target:'wss://localhost:8443/online',
|
||||||
// '/voice':{
|
// changeOrigin:true,
|
||||||
// target:'wss://localhost:8443/voice',
|
// ws:true,
|
||||||
// changeOrigin:true,
|
// },
|
||||||
// ws:true,
|
// '/voice':{
|
||||||
// }
|
// target:'wss://localhost:8443/voice',
|
||||||
// },
|
// changeOrigin:true,
|
||||||
// },
|
// ws:true,
|
||||||
});
|
// }
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
define: {
|
||||||
|
'process.env.STUN_URL': JSON.stringify(process.env.STUN_URL),
|
||||||
|
'process.env.TURN_URL': JSON.stringify(process.env.TURN_URL),
|
||||||
|
'process.env.TURN_USERNAME': JSON.stringify(process.env.TURN_USERNAME),
|
||||||
|
'process.env.TURN_CREDENTIAL': JSON.stringify(process.env.TURN_CREDENTIAL),
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user