feat: init commit
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/myplayer_icon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Myplayer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" style="grid-template-columns: 1fr"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5261
package-lock.json
generated
Normal file
5261
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "myplayer_vue",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"dplayer": "^1.27.1",
|
||||||
|
"element-plus": "^2.9.4",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"hls.js": "^1.5.20",
|
||||||
|
"idb": "^8.0.2",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"video.js": "^8.21.0",
|
||||||
|
"videojs-vtt.js": "^0.15.5",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-axios": "^3.5.2",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"vite": "^6.0.11",
|
||||||
|
"vite-plugin-vue-devtools": "^7.7.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
proxy.js
Normal file
62
proxy.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { URL } from 'url';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
// 允许跨域请求
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通用代理路由
|
||||||
|
app.get('/proxy', async (req, res) => {
|
||||||
|
console.log('Received Range header:', req.headers.range);
|
||||||
|
// 获取目标 URL
|
||||||
|
const targetUrl = req.query.url;
|
||||||
|
console.log('Fetching data from:', targetUrl);
|
||||||
|
if (!targetUrl) {
|
||||||
|
return res.status(400).send('URL parameter is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置请求头
|
||||||
|
const headers = {};
|
||||||
|
if (req.headers.range) {
|
||||||
|
headers['Range'] = req.headers.range; // 转发 Range 请求头
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向目标 URL 发起请求
|
||||||
|
const response = await fetch(targetUrl, { headers });
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
if (key !== 'content-encoding') { // 避免某些头信息导致问题
|
||||||
|
res.setHeader(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 特殊处理 Transfer-Encoding 和 Content-Length
|
||||||
|
if (response.headers.get('transfer-encoding') === 'chunked') {
|
||||||
|
res.removeHeader('Content-Length');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将响应体直接流式传输给客户端
|
||||||
|
response.body.pipe(res, { end: true });
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
response.body.on('error', (err) => {
|
||||||
|
console.error('Error during data transfer:', err);
|
||||||
|
res.status(500).send('Error during data transfer');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
res.status(500).send('Error fetching data');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Proxy server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
BIN
public/myplayer_icon.ico
Normal file
BIN
public/myplayer_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
86
src/assets/base.css
Normal file
86
src/assets/base.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
BIN
src/assets/defaultavatar.jpg
Normal file
BIN
src/assets/defaultavatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
35
src/assets/main.css
Normal file
35
src/assets/main.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@import './base.css';
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
/* margin: 0 auto; */
|
||||||
|
/* padding: 2rem; */
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
transition: 0.4s;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
/* background-color: hsla(160, 100%, 37%, 0.2); */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: grid;
|
||||||
|
/* grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0 2rem; */
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/assets/微信二维码.png
Normal file
BIN
src/assets/微信二维码.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
BIN
src/assets/微信收款码.png
Normal file
BIN
src/assets/微信收款码.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
src/assets/支付宝收款码.jpg
Normal file
BIN
src/assets/支付宝收款码.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
44
src/components/GlobalMessageButton.vue
Normal file
44
src/components/GlobalMessageButton.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<router-link to="/home/message">
|
||||||
|
<el-button
|
||||||
|
class="global-message-button"
|
||||||
|
@click="readMessage" >
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
<div class="messagepoint" v-if="messagePoint.hasNewMessage"></div>
|
||||||
|
</el-button>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { messagePointStore } from '@/store/store';
|
||||||
|
import { Message } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const messagePoint = messagePointStore();
|
||||||
|
|
||||||
|
const readMessage = () => {
|
||||||
|
messagePoint.hasNewMessage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.global-message-button {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagepoint{
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom : 0;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
162
src/components/navBar.vue
Normal file
162
src/components/navBar.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="navbar-left">
|
||||||
|
<router-link to="/home" class="navbar-logo">
|
||||||
|
<img :src="logo" alt="MyPlayer" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-right">
|
||||||
|
<router-link to="/home/search">搜索</router-link>
|
||||||
|
<router-link to="/home/friends">好友</router-link>
|
||||||
|
<router-link to="/home/group">群聊</router-link>
|
||||||
|
<router-link to="/home/playroom">Playroom</router-link>
|
||||||
|
<div class="more-dropdown" @click="toggleDropdown" @blur="closeDropdown">
|
||||||
|
更多
|
||||||
|
<ul v-if="isDropdownOpen" class="dropdown-content">
|
||||||
|
<li>设置</li>
|
||||||
|
<li @click="aboutUs">关于我们</li>
|
||||||
|
<li>帮助</li>
|
||||||
|
<li @click="logout">退出登录</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, initCustomFormatter } from 'vue';
|
||||||
|
import logo from '../assets/logo.png';
|
||||||
|
import { onlineSocketStore } from '@/store/Online';
|
||||||
|
import router from '@/router';
|
||||||
|
import { voiceStore } from '@/store/Voice';
|
||||||
|
import { groupMessageStore } from '@/store/message';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
|
||||||
|
const userInfo = userInfoStore()
|
||||||
|
const voice = voiceStore()
|
||||||
|
const socket = onlineSocketStore()
|
||||||
|
const groupMessage = groupMessageStore()
|
||||||
|
|
||||||
|
|
||||||
|
const isDropdownOpen = ref(false); // 控制下拉框的显示与隐藏
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
isDropdownOpen.value = !isDropdownOpen.value; // 切换下拉框的显示状态
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
isDropdownOpen.value = false; // 关闭下拉框
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听全局点击事件
|
||||||
|
const handleGlobalClick = (event) => {
|
||||||
|
// 检查点击是否发生在下拉框外部
|
||||||
|
if (!event.target.closest('.more-dropdown')) {
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleGlobalClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleGlobalClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//退出登录逻辑
|
||||||
|
const logout = () => {
|
||||||
|
//保存群聊消息
|
||||||
|
groupMessage.saveMessagesHistory(socket.u_id,groupMessage.g_id)
|
||||||
|
// 清除用户全局信息
|
||||||
|
userInfo.clearUserInfo();
|
||||||
|
localStorage.clear();
|
||||||
|
// 断开websocket链接
|
||||||
|
voice.disconnect();
|
||||||
|
socket.disconnect();
|
||||||
|
// 跳转到登录页面
|
||||||
|
window.location.href = '/';
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const aboutUs = () => {
|
||||||
|
router.push('/home/about')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #60605e;
|
||||||
|
color: #fff;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-left,
|
||||||
|
.navbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10%;
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-logo img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a {
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a.router-link-active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-dropdown {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
position: absolute;
|
||||||
|
width: 100px;
|
||||||
|
margin-top: 22px;
|
||||||
|
right: -50px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #444444;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 5px;
|
||||||
|
z-index: 1000;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content a {
|
||||||
|
display: block;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content li {
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content li:hover {
|
||||||
|
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
153
src/components/phonePanel.vue
Normal file
153
src/components/phonePanel.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<el-button class="telegram-btn" @click="showTelegramPanel">
|
||||||
|
<el-icon>
|
||||||
|
<Mic />
|
||||||
|
</el-icon>
|
||||||
|
<div class="telegramTag"></div>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<div class="voice" v-if="onCall.panel">
|
||||||
|
<div>
|
||||||
|
<div class="profilebox">
|
||||||
|
<img :src="onCall.target.avatar" alt="头像">
|
||||||
|
</div>
|
||||||
|
<div class="infobox">
|
||||||
|
<p>{{ onCall.target.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="buttonsbox">
|
||||||
|
<button @click="answer" v-if="!onCall.from">
|
||||||
|
接听
|
||||||
|
</button>
|
||||||
|
<button @click="hangup">
|
||||||
|
挂断
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button @click="playRemoteAudio">没有声音?试试我</el-button>
|
||||||
|
</div>
|
||||||
|
<audio ref="remoteAudio" autoplay></audio>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
import { voiceStore } from '@/store/Voice';
|
||||||
|
import { onCallStore } from '@/store/VoiceTarget';
|
||||||
|
import { Mic } from '@element-plus/icons-vue';
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
|
const voice = voiceStore();
|
||||||
|
const onCall = onCallStore();
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
|
||||||
|
const localAudio = ref(null);
|
||||||
|
const remoteAudio = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onCall.setRemoteElement(remoteAudio.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 清理音频流和元素
|
||||||
|
if (localAudio.value.srcObject) {
|
||||||
|
localAudio.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
if (remoteAudio.value.srcObject) {
|
||||||
|
remoteAudio.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const answer = () => {
|
||||||
|
console.log("接听");
|
||||||
|
onCall.callingOff();
|
||||||
|
onCall.fromOn();
|
||||||
|
voice.pickup(userinfo.user.u_id, onCall.target.u_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hangup = () => {
|
||||||
|
voice.hangup();
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTelegramPanel = () => {
|
||||||
|
onCall.panel = !onCall.panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playRemoteAudio = () => {
|
||||||
|
const audio = document.getElementById("remoteAudio");
|
||||||
|
if (audio) {
|
||||||
|
audio.play().catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.voice {
|
||||||
|
position: fixed;
|
||||||
|
top: 210px;
|
||||||
|
right: 100px;
|
||||||
|
width: 300px;
|
||||||
|
height: 400px;
|
||||||
|
background-color: #d7d7d7;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilebox {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilebox img {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #646464;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsbox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsbox button {
|
||||||
|
margin: 0 20px 0 20px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 45%;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegramTag {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f00;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
72
src/components/userProfile.vue
Normal file
72
src/components/userProfile.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<router-link to="/home/user" v-if="userinfo.user.u_avatar" class="user-profile-link">
|
||||||
|
<div class="user-profile">
|
||||||
|
<img :src="userinfo.user.u_avatar" alt="User Avatar" />
|
||||||
|
<div :class="['status-dot', statusClass]"></div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
|
||||||
|
const isLoggedIn = ref(true); // 假设用户已登录
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
return isLoggedIn.value ? 'online' : 'offline';
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-profile-link {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
/* 去掉链接的下划线 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 500;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online {
|
||||||
|
background-color: green;
|
||||||
|
/* 在线状态 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline {
|
||||||
|
background-color: red;
|
||||||
|
/* 离线状态 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/components/videoPlayer.vue
Normal file
95
src/components/videoPlayer.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="videoRef" class="dplayer-container"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
|
import DPlayer from 'dplayer';
|
||||||
|
import Hls from 'hls.js';
|
||||||
|
|
||||||
|
const videoRef = ref(null);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
autoplay: { type: Boolean, default: false },
|
||||||
|
videoUrl: { type: String, required: true },
|
||||||
|
danmaku: { type: Object, default: () => ({}) }
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const playerOptions = {
|
||||||
|
container: videoRef.value,
|
||||||
|
autoplay: props.autoplay,
|
||||||
|
video: {
|
||||||
|
url: `/proxy?url=${encodeURIComponent(props.videoUrl)}`,
|
||||||
|
type: 'customHls',
|
||||||
|
customType: {
|
||||||
|
customHls: function (video, player) {
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls();
|
||||||
|
hls.loadSource(video.src);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.config.maxBufferLength = 60; // 设置最大缓冲时间为60秒
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
danmaku: props.danmaku
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`/proxy?url=${encodeURIComponent(props.videoUrl)}`)
|
||||||
|
const player = new DPlayer(playerOptions);
|
||||||
|
|
||||||
|
player.on('play', () => {
|
||||||
|
console.log("播放...")
|
||||||
|
})
|
||||||
|
player.on('pause', () => {
|
||||||
|
console.log("暂停...")
|
||||||
|
})
|
||||||
|
player.on('ended', () => {
|
||||||
|
console.log("结束...")
|
||||||
|
})
|
||||||
|
player.on('error', () => {
|
||||||
|
console.log("出错...")
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.videoUrl, (newUrl) => {
|
||||||
|
if (player) {
|
||||||
|
console.log("切换视频...")
|
||||||
|
player.switchVideo({
|
||||||
|
url: `/proxy?url=${encodeURIComponent(newUrl)}`,
|
||||||
|
type: 'customHls',
|
||||||
|
customType: {
|
||||||
|
customHls: function (video, player) {
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls();
|
||||||
|
hls.loadSource(video.src);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 销毁时清理播放器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
player.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dplayer-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
src/functions/groupHistoryMessage.js
Normal file
66
src/functions/groupHistoryMessage.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { openDB } from 'idb';
|
||||||
|
|
||||||
|
const DB_NAME = 'myplayer';
|
||||||
|
const STORE_NAME = 'groupHistoryMessages';
|
||||||
|
|
||||||
|
// 打开数据库
|
||||||
|
const getDB = async () => {
|
||||||
|
return openDB(DB_NAME, 3, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
db.createObjectStore(STORE_NAME); // 使用 userId 作为 key
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 存储消息(追加到历史记录中)
|
||||||
|
const saveGroupMessages = async (key, messages) => {
|
||||||
|
const db = await getDB();
|
||||||
|
try {
|
||||||
|
// 读取现有的历史消息
|
||||||
|
const existingMessages = await loadGroupMessages(key);
|
||||||
|
|
||||||
|
// 确保 existingMessages 是一个数组
|
||||||
|
const newMessages = Array.isArray(existingMessages) ? existingMessages : [];
|
||||||
|
|
||||||
|
// 将当前消息追加到历史消息中
|
||||||
|
const updatedMessages = [...newMessages, ...messages]; // 将新消息追加到历史消息数组中
|
||||||
|
|
||||||
|
// 保存更新后的历史消息
|
||||||
|
await db.put(STORE_NAME, updatedMessages, key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取消息
|
||||||
|
const loadGroupMessages = async (key) => {
|
||||||
|
const db = await getDB();
|
||||||
|
try {
|
||||||
|
const messages = await db.get(STORE_NAME, key);
|
||||||
|
return messages || []; // 如果没有消息,返回空数组
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading messages:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除消息
|
||||||
|
const deleteGroupMessages = async (key,messages) => {
|
||||||
|
const db = await getDB();
|
||||||
|
try {
|
||||||
|
if(messages.length === 0){
|
||||||
|
console.log('删除后没有消息了');
|
||||||
|
await db.delete(STORE_NAME, key);
|
||||||
|
}else{
|
||||||
|
console.log('删除后还有消息')
|
||||||
|
console.log(messages)
|
||||||
|
await db.put(STORE_NAME, messages, key);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { saveGroupMessages, loadGroupMessages, deleteGroupMessages };
|
||||||
95
src/functions/historyMessages.js
Normal file
95
src/functions/historyMessages.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { openDB } from 'idb';
|
||||||
|
|
||||||
|
const DB_NAME = 'myplayer';
|
||||||
|
const STORE_NAME = 'historyMessages';
|
||||||
|
|
||||||
|
//初始化数据库
|
||||||
|
const initDB = async () => {
|
||||||
|
try {
|
||||||
|
// 打开数据库,如果不存在则会创建一个新的数据库
|
||||||
|
const db = await openDB('myplayer', 3, {
|
||||||
|
// 数据库升级回调
|
||||||
|
upgrade(db) {
|
||||||
|
console.log('数据库升级中...');
|
||||||
|
// 创建对象存储 groupHistoryMessages
|
||||||
|
if (!db.objectStoreNames.contains('groupHistoryMessages')) {
|
||||||
|
db.createObjectStore('groupHistoryMessages');
|
||||||
|
}
|
||||||
|
// 创建对象存储 historyMessages
|
||||||
|
if (!db.objectStoreNames.contains('historyMessages')) {
|
||||||
|
db.createObjectStore('historyMessages');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('数据库初始化成功!');
|
||||||
|
// 在这里可以对数据库进行其他操作,例如读取或写入数据
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化 IndexedDB 时发生错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 打开数据库
|
||||||
|
const getDB = async () => {
|
||||||
|
return openDB(DB_NAME, 3, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
db.createObjectStore(STORE_NAME); // 使用 userId 作为 key
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 存储消息(追加到历史记录中)
|
||||||
|
const saveMessages = async (userId, messages) => {
|
||||||
|
const db = await getDB();
|
||||||
|
try {
|
||||||
|
// 读取现有的历史消息
|
||||||
|
const existingMessages = await loadMessages(userId);
|
||||||
|
|
||||||
|
// 确保 existingMessages 是一个数组
|
||||||
|
const newMessages = Array.isArray(existingMessages) ? existingMessages : [];
|
||||||
|
|
||||||
|
// 将当前消息追加到历史消息中
|
||||||
|
const updatedMessages = [...newMessages, ...messages]; // 将新消息追加到历史消息数组中
|
||||||
|
|
||||||
|
// 保存更新后的历史消息
|
||||||
|
await db.put(STORE_NAME, updatedMessages, userId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取消息
|
||||||
|
const loadMessages = async (userId) => {
|
||||||
|
const db = await getDB();
|
||||||
|
try {
|
||||||
|
const messages = await db.get(STORE_NAME, userId);
|
||||||
|
return messages || []; // 如果没有消息,返回空数组
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading messages:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除消息
|
||||||
|
const deleteMessages = async (userId,messages) => {
|
||||||
|
const db = await getDB();
|
||||||
|
try {
|
||||||
|
if(messages.length === 0){
|
||||||
|
console.log('删除后没有消息了');
|
||||||
|
await db.delete(STORE_NAME, userId);
|
||||||
|
}else{
|
||||||
|
console.log('删除后还有消息')
|
||||||
|
console.log(messages)
|
||||||
|
await db.put(STORE_NAME, messages, userId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { saveMessages, loadMessages, deleteMessages, initDB };
|
||||||
26
src/functions/user.js
Normal file
26
src/functions/user.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
|
||||||
|
export const getUserInfo = async () => {
|
||||||
|
console.log("getUserInfojs was used!")
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/user/getuserinfo', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
userinfo.user.u_id = response.data.data.u_id
|
||||||
|
userinfo.user.u_name = response.data.data.u_name
|
||||||
|
userinfo.user.u_avatar = response.data.data.u_avatar
|
||||||
|
userinfo.user.u_introduction = response.data.data.u_introduction
|
||||||
|
console.log(userinfo.user)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
34
src/main.js
Normal file
34
src/main.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
pinia.use(piniaPluginPersistedstate)
|
||||||
|
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(pinia)
|
||||||
|
|
||||||
|
// imoort element-plus
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
//import axios
|
||||||
|
import axios from 'axios'
|
||||||
|
import VueAxios from 'vue-axios'
|
||||||
|
app.use(VueAxios, axios)
|
||||||
|
|
||||||
|
app.provide('axios', app.config.globalProperties.axios)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
89
src/router/index.js
Normal file
89
src/router/index.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('../views/login/login.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('../views/home/home.vue'),
|
||||||
|
redirect: '/home/',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'homeDefault',
|
||||||
|
component: () => import('../views/home/homeDefault.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'friends',
|
||||||
|
name: 'Friends',
|
||||||
|
component: () => import('../views/home/friends.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'playroom',
|
||||||
|
name: 'Playroom',
|
||||||
|
component: () => import('../views/home/playroom.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user',
|
||||||
|
name: 'User',
|
||||||
|
component: () => import('../views/user/user.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'search',
|
||||||
|
name: 'Search',
|
||||||
|
component: () => import('../views/home/search.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'message',
|
||||||
|
name: 'Message',
|
||||||
|
component: () => import('../views/home/message.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'setting',
|
||||||
|
name: 'Setting',
|
||||||
|
component: () => import('../views/home/setting.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'group',
|
||||||
|
name:'Group',
|
||||||
|
component:()=>import('../views/home/group.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'about',
|
||||||
|
name:'About',
|
||||||
|
component:()=>import('../views/home/about.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'register',
|
||||||
|
component: () => import('../views/login/register.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reset-password',
|
||||||
|
name: 'resetPassword',
|
||||||
|
component: () => import('../views/login/resetPassword.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/init',
|
||||||
|
name: 'init',
|
||||||
|
component: () => import('../views/login/init.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/room',
|
||||||
|
name: 'room',
|
||||||
|
component: () => import('../views/room/room.vue'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
189
src/socket/onlineSocket.js
Normal file
189
src/socket/onlineSocket.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
import { userInfoStore } from "@/store/store";
|
||||||
|
import { messagePointStore } from "@/store/store";
|
||||||
|
import {
|
||||||
|
messageStore,
|
||||||
|
messageSignStore,
|
||||||
|
groupMessageStore,
|
||||||
|
} from "@/store/message";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
// userinfo 实例
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
// message 实例
|
||||||
|
const message = messageStore();
|
||||||
|
// groupMessage 实例
|
||||||
|
const groupMessage = groupMessageStore();
|
||||||
|
// messageSignStoregn 实例
|
||||||
|
const messageSign = messageSignStore();
|
||||||
|
// WebSocket 实例
|
||||||
|
const socket = ref(null);
|
||||||
|
//messagePoint 实例
|
||||||
|
const messagePoint = messagePointStore();
|
||||||
|
|
||||||
|
const isManualClose = ref(false);
|
||||||
|
|
||||||
|
const reconnectScheduled = ref(false);
|
||||||
|
|
||||||
|
const retryCount = ref(0);
|
||||||
|
|
||||||
|
export const getRetryCount = () => {
|
||||||
|
return retryCount.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addRetryCount = () => {
|
||||||
|
retryCount.value = retryCount.value + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetRetryCount = () => {
|
||||||
|
retryCount.value = 0;
|
||||||
|
};
|
||||||
|
export const setReconnectScheduled = (value) => {
|
||||||
|
reconnectScheduled.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getReconnectScheduled = () => {
|
||||||
|
return reconnectScheduled.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setIsManualClose = (value) => {
|
||||||
|
isManualClose.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsManualClose = () => {
|
||||||
|
return isManualClose.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接WebSocket
|
||||||
|
export const connectWebSocket = () => {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
|
const host = window.location.host;
|
||||||
|
const socketUrl = `${protocol}${host}/online?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`;
|
||||||
|
|
||||||
|
// const socketUrl = `ws://localhost:8080/online?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`;
|
||||||
|
if (socket.value && socket.value.readyState !== WebSocket.CLOSED) {
|
||||||
|
console.log("还在重连中...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const retrytime = getRetryCount();
|
||||||
|
if (retrytime >= 10) {
|
||||||
|
console.log("重连失败,请稍后再试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(retrytime);
|
||||||
|
socket.value = new WebSocket(socketUrl);
|
||||||
|
|
||||||
|
socket.value.onopen = (event) => {
|
||||||
|
console.log("WebSocket连接已建立", event);
|
||||||
|
setReconnectScheduled(false);
|
||||||
|
setIsManualClose(false);
|
||||||
|
resetRetryCount();
|
||||||
|
};
|
||||||
|
|
||||||
|
//处理消息逻辑
|
||||||
|
socket.value.onmessage = (event) => {
|
||||||
|
console.log("从服务器收到消息:", event.data);
|
||||||
|
try {
|
||||||
|
const MessageData = JSON.parse(event.data);
|
||||||
|
//是否为上下线消息(三类消息)
|
||||||
|
if (!MessageData.system) {
|
||||||
|
//是否为新消息
|
||||||
|
if (MessageData.message) {
|
||||||
|
messagePoint.hasNewMessage = true;
|
||||||
|
if (MessageData.group) {
|
||||||
|
//四类消息,群聊消息
|
||||||
|
console.log("有新群消息");
|
||||||
|
groupMessage.addMessage(MessageData);
|
||||||
|
console.log(groupMessage.messages);
|
||||||
|
messageSign.addSign({
|
||||||
|
sender_name: MessageData.sender_name,
|
||||||
|
g_id: MessageData.g_id,
|
||||||
|
g_name: MessageData.g_name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//一类消息,私聊消息
|
||||||
|
console.log("有新消息");
|
||||||
|
message.addMessage(MessageData);
|
||||||
|
console.log(message.messages);
|
||||||
|
messageSign.addSign({
|
||||||
|
sender: MessageData.sender,
|
||||||
|
sender_name: MessageData.sender_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (MessageData.engaged) {
|
||||||
|
// 五类消息,账号重复登录逻辑
|
||||||
|
setIsManualClose(true);
|
||||||
|
disconnectWebSocket();
|
||||||
|
ElMessage("账号重复登录,请注意密码泄露");
|
||||||
|
console.log("账号重复登陆");
|
||||||
|
window.location.replace("/");
|
||||||
|
} else {
|
||||||
|
//三类消息,指示用户上下线
|
||||||
|
if (MessageData.status === "online")
|
||||||
|
ElMessage("用户:" + MessageData.u_name + "上线");
|
||||||
|
else {
|
||||||
|
ElMessage("用户:" + MessageData.u_name + "下线");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析 JSON 失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.value.onerror = (error) => {
|
||||||
|
console.error("WebSocket发生错误:", error);
|
||||||
|
setReconnectScheduled(true);
|
||||||
|
socket.value.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.value.onclose = (event) => {
|
||||||
|
if (!getIsManualClose()) {
|
||||||
|
if (getReconnectScheduled()) {
|
||||||
|
socket.value = null;
|
||||||
|
addRetryCount();
|
||||||
|
setTimeout(reConnectWebSocket, 5000);
|
||||||
|
setReconnectScheduled(false);
|
||||||
|
} else {
|
||||||
|
console.log("websocket因为浏览器省电设置断开");
|
||||||
|
console.log("WebSocket连接已关闭", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 断开WebSocket连接
|
||||||
|
export const disconnectWebSocket = () => {
|
||||||
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
|
socket.value.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重连机制
|
||||||
|
export const reConnectWebSocket = () => {
|
||||||
|
connectWebSocket();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
export const sendMessage = (message) => {
|
||||||
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
|
socket.value.send(message);
|
||||||
|
} else {
|
||||||
|
console.warn("WebSocket未连接,无法发送消息");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//没有错误的重连,只是浏览器在后台断开了连接
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
} else {
|
||||||
|
if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) {
|
||||||
|
if (getReconnectScheduled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reConnectWebSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
0
src/socket/roomSocket.js
Normal file
0
src/socket/roomSocket.js
Normal file
287
src/socket/voiceSocket.js
Normal file
287
src/socket/voiceSocket.js
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
import { userInfoStore } from "@/store/store";
|
||||||
|
import { onCallStore } from "@/store/VoiceTarget";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
// userinfo 实例
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
// oncall 实例
|
||||||
|
const oncall = onCallStore();
|
||||||
|
// WebSocket 实例
|
||||||
|
const socket = ref(null);
|
||||||
|
|
||||||
|
const isManualClose = ref(false);
|
||||||
|
const reconnectScheduled = ref(false);
|
||||||
|
const retryCount = ref(0);
|
||||||
|
|
||||||
|
const getRetryCount = () => {
|
||||||
|
return retryCount.value;
|
||||||
|
};
|
||||||
|
const addRetryCount = () => {
|
||||||
|
retryCount.value = retryCount.value + 1;
|
||||||
|
};
|
||||||
|
const resetRetryCount = () => {
|
||||||
|
retryCount.value = 0;
|
||||||
|
};
|
||||||
|
const setReconnectScheduled = (value) => {
|
||||||
|
reconnectScheduled.value = value;
|
||||||
|
};
|
||||||
|
const getReconnectScheduled = () => {
|
||||||
|
return reconnectScheduled.value;
|
||||||
|
};
|
||||||
|
const setIsManualClose = (value) => {
|
||||||
|
isManualClose.value = value;
|
||||||
|
};
|
||||||
|
const getIsManualClose = () => {
|
||||||
|
return isManualClose.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const iceserver = {
|
||||||
|
iceServers: [
|
||||||
|
{
|
||||||
|
// urls: "stun:stun.l.google.com:19302"
|
||||||
|
urls: "stun:8.134.92.199:3478",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: "turn:8.134.92.199:3478",
|
||||||
|
username: "test",
|
||||||
|
credential: "123456",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接WebSocket
|
||||||
|
export const connectVoicesocket = () => {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
|
const host = window.location.host;
|
||||||
|
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) {
|
||||||
|
console.log("还在重连中...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const retrytime = getRetryCount();
|
||||||
|
if (retrytime >= 10) {
|
||||||
|
console.log("重连失败,请稍后再试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(retrytime);
|
||||||
|
socket.value = new WebSocket(socketUrl);
|
||||||
|
|
||||||
|
socket.value.onopen = (event) => {
|
||||||
|
console.log("Voice连接已建立", event);
|
||||||
|
setReconnectScheduled(false);
|
||||||
|
setIsManualClose(false);
|
||||||
|
resetRetryCount();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理消息逻辑
|
||||||
|
socket.value.onmessage = async (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === "status") {
|
||||||
|
ElMessage.error("对方已离线");
|
||||||
|
oncall.panelOff();
|
||||||
|
oncall.callingOff();
|
||||||
|
} else if (data.type === "incomingcall") {
|
||||||
|
// 通话请求处理
|
||||||
|
oncall.setTarget(data.from, data.from_name, data.from_avatar);
|
||||||
|
oncall.panelOn();
|
||||||
|
oncall.callingOn();
|
||||||
|
oncall.fromOff();
|
||||||
|
oncall.statusOff();
|
||||||
|
} else if (data.type === "pickup") {
|
||||||
|
ElMessage.success("对方已接听,等待连接中...");
|
||||||
|
try {
|
||||||
|
await initRTCconnection();
|
||||||
|
RTCpeerConnection.value.onicecandidate = (event) => {
|
||||||
|
if (event.candidate) {
|
||||||
|
console.log("发送candidate", event.candidate);
|
||||||
|
const message = {
|
||||||
|
type: "single",
|
||||||
|
content: "candidate",
|
||||||
|
from: userinfo.user.u_id,
|
||||||
|
to: oncall.target.u_id,
|
||||||
|
candidate: event.candidate,
|
||||||
|
};
|
||||||
|
sendMessage(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RTCpeerConnection.value.ontrack = (event) => {
|
||||||
|
console.log("收到对方音频流", event);
|
||||||
|
oncall.setRemoteStream(event.streams[0]);
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.log("webRTC初始化失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const offer = await RTCpeerConnection.value.createOffer({
|
||||||
|
offerToReceiveAudio: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await RTCpeerConnection.value.setLocalDescription(offer);
|
||||||
|
|
||||||
|
// 发送offer
|
||||||
|
const message = {
|
||||||
|
type: "single",
|
||||||
|
content: "offer",
|
||||||
|
from: userinfo.user.u_id,
|
||||||
|
to: oncall.target.u_id,
|
||||||
|
offer: offer,
|
||||||
|
};
|
||||||
|
sendMessage(JSON.stringify(message));
|
||||||
|
} else if (data.type === "hangup") {
|
||||||
|
ElMessage.success("对方已挂断");
|
||||||
|
oncall.panelOff();
|
||||||
|
oncall.callingOff();
|
||||||
|
oncall.fromOff();
|
||||||
|
oncall.statusOff();
|
||||||
|
RTCpeerConnection.value.close();
|
||||||
|
RTCpeerConnection.value = null;
|
||||||
|
oncall.resetStream();
|
||||||
|
} else if (data.content === "offer") {
|
||||||
|
ElMessage.success("收到对方offer");
|
||||||
|
console.log("收到对方offer", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initRTCconnection();
|
||||||
|
|
||||||
|
RTCpeerConnection.value.onicecandidate = (event) => {
|
||||||
|
if (event.candidate) {
|
||||||
|
console.log("发送candidate", event.candidate);
|
||||||
|
const message = {
|
||||||
|
type: "single",
|
||||||
|
content: "candidate",
|
||||||
|
from: userinfo.user.u_id,
|
||||||
|
to: oncall.target.u_id,
|
||||||
|
candidate: event.candidate,
|
||||||
|
};
|
||||||
|
sendMessage(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RTCpeerConnection.value.ontrack = (event) => {
|
||||||
|
console.log("收到对方音频流", event);
|
||||||
|
oncall.setRemoteStream(event.streams[0]);
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.log("webRTC初始化失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
await RTCpeerConnection.value.setRemoteDescription(data.offer);
|
||||||
|
const answer = await RTCpeerConnection.value.createAnswer();
|
||||||
|
await RTCpeerConnection.value.setLocalDescription(answer);
|
||||||
|
|
||||||
|
// 发送answer
|
||||||
|
const message = {
|
||||||
|
type: "single",
|
||||||
|
content: "answer",
|
||||||
|
from: userinfo.user.u_id,
|
||||||
|
to: oncall.target.u_id,
|
||||||
|
answer: answer,
|
||||||
|
};
|
||||||
|
sendMessage(JSON.stringify(message));
|
||||||
|
} else if (data.content === "answer") {
|
||||||
|
ElMessage.success("对方已接受");
|
||||||
|
console.log("对方已接受", data);
|
||||||
|
await RTCpeerConnection.value.setRemoteDescription(data.answer);
|
||||||
|
} else if (data.content === "candidate") {
|
||||||
|
console.log("收到candidate", data);
|
||||||
|
await RTCpeerConnection.value.addIceCandidate(data.candidate);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("VoiceSocket消息格式错误", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.value.onerror = (error) => {
|
||||||
|
console.error("Voicesocket发生错误:", error);
|
||||||
|
setReconnectScheduled(true);
|
||||||
|
socket.value.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.value.onclose = (event) => {
|
||||||
|
if (!getIsManualClose()) {
|
||||||
|
if (getReconnectScheduled()) {
|
||||||
|
socket.value = null;
|
||||||
|
addRetryCount();
|
||||||
|
setTimeout(reConnectVoicesocket, 5000);
|
||||||
|
setReconnectScheduled(false);
|
||||||
|
} else {
|
||||||
|
console.log("Voicesocket因为浏览器省电设置断开");
|
||||||
|
console.log("Voicesocket连接已关闭", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 断开Voicesocket连接
|
||||||
|
export const disconnectVoicesocket = () => {
|
||||||
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
|
socket.value.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重连机制
|
||||||
|
export const reConnectVoicesocket = () => {
|
||||||
|
connectVoicesocket();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
export const sendMessage = (message) => {
|
||||||
|
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
|
||||||
|
socket.value.send(message);
|
||||||
|
} else {
|
||||||
|
console.warn("Voicesocket未连接,无法发送消息");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//没有错误的重连,只是浏览器在后台断开了连接
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
} else {
|
||||||
|
if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) {
|
||||||
|
if (getReconnectScheduled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reConnectVoicesocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//WebRTC连接相关代码
|
||||||
|
const RTCpeerConnection = ref(null);
|
||||||
|
|
||||||
|
export const getlocalStream = async () => {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
console.log("获取本地音频流成功", stream);
|
||||||
|
return stream;
|
||||||
|
// // 获取音频和视频轨道
|
||||||
|
// const audioTrack = stream.getAudioTracks()[0];
|
||||||
|
// // 将轨道添加到 RTCPeerConnection
|
||||||
|
// peerConnection.addTrack(audioTrack, stream);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initRTCconnection = async () => {
|
||||||
|
RTCpeerConnection.value = new RTCPeerConnection(iceserver);
|
||||||
|
const stream = await getlocalStream();
|
||||||
|
RTCpeerConnection.value.addTrack(stream.getAudioTracks()[0], stream);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hangup = () => {
|
||||||
|
oncall.panelOff();
|
||||||
|
oncall.callingOff();
|
||||||
|
oncall.fromOff();
|
||||||
|
oncall.statusOff();
|
||||||
|
oncall.resetStream();
|
||||||
|
RTCpeerConnection.value.close();
|
||||||
|
RTCpeerConnection.value = null;
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
type: "hangup",
|
||||||
|
from: userinfo.user.u_id,
|
||||||
|
to: oncall.target.u_id,
|
||||||
|
};
|
||||||
|
sendMessage(JSON.stringify(message));
|
||||||
|
};
|
||||||
37
src/store/Online.js
Normal file
37
src/store/Online.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { connectWebSocket, disconnectWebSocket, sendMessage, setIsManualClose } from '@/socket/onlineSocket'
|
||||||
|
import { messageStore } from './message'
|
||||||
|
|
||||||
|
const message = messageStore()
|
||||||
|
|
||||||
|
export const onlineSocketStore = defineStore('onlineSocket', {
|
||||||
|
state: () => ({
|
||||||
|
isConnected: false,
|
||||||
|
hasGotMessage: false,
|
||||||
|
u_id: ''
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
connect(u_id) {
|
||||||
|
this.u_id = u_id;
|
||||||
|
if (this.isConnected === true) return
|
||||||
|
connectWebSocket();
|
||||||
|
this.isConnected = true;
|
||||||
|
if (!this.hasGotMessage) {
|
||||||
|
message.loadMessagesHistory(this.u_id)
|
||||||
|
this.hasGotMessage = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disconnect() {
|
||||||
|
setIsManualClose(true);
|
||||||
|
disconnectWebSocket();
|
||||||
|
this.isConnected = false;
|
||||||
|
if (this.hasGotMessage) {
|
||||||
|
message.saveMessagesHistory(this.u_id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
send(message) {
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
50
src/store/Voice.js
Normal file
50
src/store/Voice.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { connectVoicesocket, disconnectVoicesocket, sendMessage, hangup } from "@/socket/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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
93
src/store/VoiceTarget.js
Normal file
93
src/store/VoiceTarget.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const onCallStore = defineStore("onCall", {
|
||||||
|
state: () => ({
|
||||||
|
target: {
|
||||||
|
u_id: "",
|
||||||
|
name: "",
|
||||||
|
avatar: ""
|
||||||
|
},
|
||||||
|
panel: false,
|
||||||
|
calling: false,
|
||||||
|
from: false,
|
||||||
|
status: false,
|
||||||
|
localstream: {
|
||||||
|
audioStream: null,
|
||||||
|
audioElement: null
|
||||||
|
},
|
||||||
|
remotestream: {
|
||||||
|
audioStream: null,
|
||||||
|
audioElement: null
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setTarget(u_id, u_name, u_avatar) {
|
||||||
|
this.target = {
|
||||||
|
u_id: u_id,
|
||||||
|
name: u_name,
|
||||||
|
avatar: u_avatar
|
||||||
|
}
|
||||||
|
},
|
||||||
|
panelOn() {
|
||||||
|
this.panel = true
|
||||||
|
},
|
||||||
|
panelOff() {
|
||||||
|
this.panel = false
|
||||||
|
},
|
||||||
|
callingOn() {
|
||||||
|
this.calling = true
|
||||||
|
},
|
||||||
|
callingOff() {
|
||||||
|
this.calling = false
|
||||||
|
},
|
||||||
|
fromOn() {
|
||||||
|
this.from = true
|
||||||
|
},
|
||||||
|
fromOff() {
|
||||||
|
this.from = false
|
||||||
|
},
|
||||||
|
statusOn() {
|
||||||
|
this.status = true
|
||||||
|
},
|
||||||
|
statusOff() {
|
||||||
|
this.status = false
|
||||||
|
},
|
||||||
|
setLocalStream(stream) {
|
||||||
|
this.localstream.audioStream = stream;
|
||||||
|
if (this.localstream.audioStream) {
|
||||||
|
this.localstream.audioElement.srcObject = stream;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setLocalElement(element) {
|
||||||
|
this.localstream.audioElement = element;
|
||||||
|
if (this.localstream.audioStream) {
|
||||||
|
this.localstream.audioElement.srcObject = this.localstream.audioStream;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
186
src/store/message.js
Normal file
186
src/store/message.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import {
|
||||||
|
saveMessages,
|
||||||
|
loadMessages,
|
||||||
|
deleteMessages,
|
||||||
|
} from "@/functions/historyMessages";
|
||||||
|
import {
|
||||||
|
saveGroupMessages,
|
||||||
|
loadGroupMessages,
|
||||||
|
deleteGroupMessages,
|
||||||
|
} from "@/functions/groupHistoryMessage";
|
||||||
|
import { toRaw } from "vue";
|
||||||
|
|
||||||
|
export const messageStore = defineStore("messageStore", {
|
||||||
|
// 定义一个响应式数组来存储聊天消息
|
||||||
|
state: () => ({
|
||||||
|
historymessages: [],
|
||||||
|
messages: [],
|
||||||
|
sender: "", //选择的聊天用户id
|
||||||
|
target: "", //用户自己的id
|
||||||
|
corresponding: [],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 定义操作消息的函数
|
||||||
|
actions: {
|
||||||
|
// 添加消息到数组
|
||||||
|
addMessage(message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空所有消息
|
||||||
|
clearMessages() {
|
||||||
|
this.messages = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
setCorresponding() {
|
||||||
|
// 过滤出当前聊天中的消息
|
||||||
|
this.corresponding = this.messages.filter(
|
||||||
|
(msg) =>
|
||||||
|
(msg.sender === this.sender && msg.target === this.target) ||
|
||||||
|
(msg.sender === this.target && msg.target === this.sender)
|
||||||
|
);
|
||||||
|
const historymessages = this.historymessages.filter(
|
||||||
|
(msg) =>
|
||||||
|
(msg.sender === this.sender && msg.target === this.target) ||
|
||||||
|
(msg.sender === this.target && msg.target === this.sender)
|
||||||
|
);
|
||||||
|
this.corresponding = [...historymessages, ...this.corresponding];
|
||||||
|
},
|
||||||
|
// 清楚当前登录的聊天数据,保存到本地
|
||||||
|
async saveMessagesHistory(u_id) {
|
||||||
|
const messages = toRaw(this.messages);
|
||||||
|
await saveMessages(u_id, messages);
|
||||||
|
},
|
||||||
|
// 加载本地聊天数据
|
||||||
|
async loadMessagesHistory(u_id) {
|
||||||
|
try {
|
||||||
|
this.historymessages = await loadMessages(u_id);
|
||||||
|
|
||||||
|
// 确保历史消息是数组类型
|
||||||
|
if (!Array.isArray(this.historymessages)) {
|
||||||
|
console.error("历史消息数据无效:", this.historymessages);
|
||||||
|
this.historymessages = []; // 如果数据无效,设置为空数组
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("加载历史消息时出错" + error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteMessagesHistory(u_id, f_id) {
|
||||||
|
this.historymessages = this.historymessages.filter(
|
||||||
|
(msg) =>
|
||||||
|
!(
|
||||||
|
(msg.sender === f_id && msg.target === u_id) ||
|
||||||
|
(msg.sender === u_id && msg.target === f_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.messages = this.messages.filter(
|
||||||
|
(msg) =>
|
||||||
|
!(
|
||||||
|
(msg.sender === f_id && msg.target === u_id) ||
|
||||||
|
(msg.sender === u_id && msg.target === f_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (f_id === this.sender) {
|
||||||
|
console.log("清除对应聊天数据展示栈");
|
||||||
|
this.corresponding = [];
|
||||||
|
}
|
||||||
|
const messages = JSON.parse(JSON.stringify(toRaw(this.historymessages)));
|
||||||
|
try {
|
||||||
|
await deleteMessages(u_id, messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("删除历史消息时出错" + error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 定义计算属性(可选)
|
||||||
|
getters: {
|
||||||
|
// 获取消息数量
|
||||||
|
messageCount: (state) => state.messages.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const messageSignStore = defineStore("messageSignStore", {
|
||||||
|
state: () => ({
|
||||||
|
sign: [],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
addSign(sign) {
|
||||||
|
// 检查是否已经存在相同的值
|
||||||
|
const exists = this.sign.some(
|
||||||
|
(item) =>
|
||||||
|
item.sender === sign.sender && item.sender_name === sign.sender_name
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
this.sign.push(sign);
|
||||||
|
} else {
|
||||||
|
console.warn(`Sign "${sign}" already exists and will not be added.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearSign() {
|
||||||
|
this.sign = [];
|
||||||
|
},
|
||||||
|
removeSign(sign) {
|
||||||
|
// 找到相同值的索引
|
||||||
|
const index = this.sign.indexOf(sign);
|
||||||
|
if (index !== -1) {
|
||||||
|
// 如果存在,删除该值
|
||||||
|
this.sign.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
console.warn(`Sign "${sign}" not found.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const groupMessageStore = defineStore("groupMessageStore", {
|
||||||
|
state: () => ({
|
||||||
|
g_id: "",
|
||||||
|
historymessages: [],
|
||||||
|
messages: [],
|
||||||
|
corresponding: [],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
addMessage(message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
},
|
||||||
|
clearMessages() {
|
||||||
|
this.messages = [];
|
||||||
|
},
|
||||||
|
async initMessages() {
|
||||||
|
this.historymessages = [...this.historymessages, ...this.messages];
|
||||||
|
this.messages = [];
|
||||||
|
},
|
||||||
|
async getHistoryMessages(u_id, g_id) {
|
||||||
|
this.g_id = g_id;
|
||||||
|
const key = `${u_id}-${g_id}`;
|
||||||
|
try {
|
||||||
|
this.historymessages = await loadGroupMessages(key);
|
||||||
|
console.log("缓存到历史消息:");
|
||||||
|
console.log(this.historymessages);
|
||||||
|
// 确保历史消息是数组类型
|
||||||
|
if (!Array.isArray(this.historymessages)) {
|
||||||
|
console.error("历史消息数据无效:", this.historymessages);
|
||||||
|
this.historymessages = []; // 如果数据无效,设置为空数组
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("加载历史消息时出错" + error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveMessagesHistory(u_id, g_id) {
|
||||||
|
const key = `${u_id}-${g_id}`;
|
||||||
|
console.log(key);
|
||||||
|
const messages = toRaw(this.messages);
|
||||||
|
this.messages = [];
|
||||||
|
this.historymessages = [];
|
||||||
|
await saveGroupMessages(key, messages);
|
||||||
|
},
|
||||||
|
async deleteMessagesHistory(u_id, g_id) {
|
||||||
|
const key = `${u_id}-${g_id}`;
|
||||||
|
this.historymessages = [];
|
||||||
|
this.messages = [];
|
||||||
|
await deleteGroupMessages(key, []);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
15
src/store/room.js
Normal file
15
src/store/room.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const roomStore = defineStore("room",{
|
||||||
|
state: () => ({
|
||||||
|
r_id: '',
|
||||||
|
r_name: '',
|
||||||
|
r_avatar: '',
|
||||||
|
inroomTag: '',
|
||||||
|
currentURL: '',
|
||||||
|
}),
|
||||||
|
actions:{
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
50
src/store/store.js
Normal file
50
src/store/store.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { ref, reactive } from "vue";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const userInfoStore = defineStore(
|
||||||
|
"userInfo",
|
||||||
|
() => {
|
||||||
|
const token = ref(null);
|
||||||
|
|
||||||
|
const user = reactive({
|
||||||
|
u_id: "",
|
||||||
|
u_name: "",
|
||||||
|
u_avatar: "@/assets/defaultavatar.jpg",
|
||||||
|
u_account: "",
|
||||||
|
u_introduction: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearUserInfo = () => {
|
||||||
|
// 清除 token
|
||||||
|
token.value = null;
|
||||||
|
|
||||||
|
// 清除 user 对象中的所有属性
|
||||||
|
user.u_id = "";
|
||||||
|
user.u_name = "";
|
||||||
|
user.u_avatar = "@/assets/defaultavatar.jpg"; // 重置为默认头像路径
|
||||||
|
user.u_account = "";
|
||||||
|
user.u_introduction = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
clearUserInfo,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
persist: {
|
||||||
|
key: "userInfo",
|
||||||
|
storage: localStorage,
|
||||||
|
paths: ["token", "user"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const messagePointStore = defineStore("hasNewMessage", () => {
|
||||||
|
const hasNewMessage = ref(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasNewMessage,
|
||||||
|
};
|
||||||
|
});
|
||||||
1
src/views/admin/admin.vue
Normal file
1
src/views/admin/admin.vue
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<template></template>
|
||||||
33
src/views/home/about.vue
Normal file
33
src/views/home/about.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="6">
|
||||||
|
<h1>关于我们</h1>
|
||||||
|
<p>前端是我</p>
|
||||||
|
<p>后端是我</p>
|
||||||
|
<p>测试是我</p>
|
||||||
|
<p>运维是我</p>
|
||||||
|
<p>UI设计是我</p>
|
||||||
|
<p>项目管理是我</p>
|
||||||
|
<p>架构设计是我</p>
|
||||||
|
<p>数据库设计还是我</p>
|
||||||
|
<p>感谢使用与支持</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="9">
|
||||||
|
可以添加微信询问详情
|
||||||
|
<img class="QRcode" src="@/assets/微信二维码.png" alt="candlelight_official">
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="9">
|
||||||
|
<img class="QRcode" src="@/assets/微信收款码.png" alt="" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="9">
|
||||||
|
<img class="QRcode" src="@/assets/支付宝收款码.jpg" alt="" />
|
||||||
|
</el-col> -->
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.QRcode {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
441
src/views/home/friends.vue
Normal file
441
src/views/home/friends.vue
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<el-row class="container">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-button type="primary" @click="dialogVisibleFriendManage = true">好友管理</el-button>
|
||||||
|
<el-input v-model="tempsearch" :prefix-icon="Search" style="width: 90%;" clearable></el-input>
|
||||||
|
<el-scrollbar style="height: 500px;width: 90%;">
|
||||||
|
<ul class="infinitelist" style="overflow: auto">
|
||||||
|
<li v-for="(item) in tempsearchResult" :key="item.u_id" class="infinitelistitem"
|
||||||
|
@click="switchTemplate(item.u_id, item.u_name, item.u_avatar)"
|
||||||
|
:class="{ 'selected': selectedFriendId === item.u_id }">
|
||||||
|
<div class="user-profile">
|
||||||
|
<img :src="item.u_avatar" alt="User Avatar" />
|
||||||
|
<div :class="['status-dot', statusClass]"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: inline-block;">{{ item.u_name }}#{{ item.u_id }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="18">
|
||||||
|
<el-row class="messagebox">
|
||||||
|
<el-scrollbar ref="scrollbarRef" style="width: 100%;height: 100%;">
|
||||||
|
<!-- 聊天消息指示框 头像、时间、内容 -->
|
||||||
|
<div v-for="(item) in messagebox" class="message-item">
|
||||||
|
<div
|
||||||
|
:class="{ 'message-item-profile': true, 'left': item.sender !== userinfo.user.u_id, 'right': item.sender === userinfo.user.u_id }">
|
||||||
|
<img :src="item.sender !== userinfo.user.u_id ? oppositeAvatar : userinfo.user.u_avatar"
|
||||||
|
alt="User Avatar" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="{ 'message-item-content': true, 'left': item.sender !== userinfo.user.u_id, 'right': item.sender === userinfo.user.u_id }">
|
||||||
|
{{ item.content }}</div>
|
||||||
|
<div
|
||||||
|
:class="{ 'message-item-time': true, 'left': item.sender === userinfo.user.u_id, 'right': item.sender !== userinfo.user.u_id }">
|
||||||
|
{{ item.time }}</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-input class="inputbox" v-model="inputValue" :placeholder="placeholder" @keyup.enter="handleEnter" clearable
|
||||||
|
:disabled="inputDisabled"></el-input>
|
||||||
|
<el-button class="call" :disabled="inputDisabled" @click="confirmCall()">语音通话</el-button>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 确认通话弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisibleCallConfirm" title="确认通话对象" width="30%" :before-close="handleClose">
|
||||||
|
<el-row style="height: 80px;align-items: center;">
|
||||||
|
<div class="user-profile">
|
||||||
|
<img :src="oppositeAvatar" alt="User Avatar" />
|
||||||
|
</div>
|
||||||
|
<div>{{ oppositeName }}</div>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-button type="primary" @click="handleConfirmCall()" style="margin: auto;">确认</el-button>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 好友管理弹窗 -->
|
||||||
|
<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-table :data="friendSearchResult" style="width: 100%;height: 400px;">
|
||||||
|
<el-table-column prop="u_avatar" label="" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- 使用 el-avatar 组件显示头像 -->
|
||||||
|
<el-avatar :src="friendSearchResult[scope.$index].u_avatar" size="large" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="u_name" label="姓名" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="u_id" label="id" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="u_introduction" label="个性签名" width="200"></el-table-column>
|
||||||
|
<el-table-column>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="deleteChatHisory(scope.row.u_id)">删除聊天记录</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="deleteFriend(scope.row.u_id)">删除好友</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick, watch, computed } from 'vue'
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onlineSocketStore } from '@/store/Online';
|
||||||
|
import { messageStore } from '@/store/message';
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
import { voiceStore } from '@/store/Voice';
|
||||||
|
import { onCallStore } from '@/store/VoiceTarget';
|
||||||
|
|
||||||
|
const socket = onlineSocketStore()
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
const message = messageStore()
|
||||||
|
const voice = voiceStore()
|
||||||
|
const oncall = onCallStore()
|
||||||
|
|
||||||
|
const messagebox = ref([])
|
||||||
|
const scrollbarRef = ref(null)
|
||||||
|
const dialogVisibleFriendManage = ref(false)
|
||||||
|
const dialogVisibleCallConfirm = ref(false)
|
||||||
|
|
||||||
|
const tempsearch = ref('')
|
||||||
|
const inputValue = ref('')
|
||||||
|
const statusClass = ref('online')
|
||||||
|
const inputDisabled = ref(true)
|
||||||
|
const placeholder = ref('请选择聊天对象')
|
||||||
|
const oppositeId = ref('')
|
||||||
|
const oppositeName = ref('')
|
||||||
|
const oppositeAvatar = ref('')
|
||||||
|
|
||||||
|
const friends = ref([])
|
||||||
|
const friendSearch = ref('')
|
||||||
|
const selectedFriendId = ref('')
|
||||||
|
|
||||||
|
const tempsearchResult = computed(() => {
|
||||||
|
if (!tempsearch.value)
|
||||||
|
return friends.value
|
||||||
|
friends.value.filter(item => item.u_name.includes(tempsearch.value))
|
||||||
|
})
|
||||||
|
|
||||||
|
const friendSearchResult = computed(() => {
|
||||||
|
if (!friendSearch.value)
|
||||||
|
return friends.value
|
||||||
|
else
|
||||||
|
return friends.value.filter(item => item.u_name.includes(friendSearch.value))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
//获取好友信息
|
||||||
|
const getFriends = async () => {
|
||||||
|
const response = await axios.get('/api/friend/getfriends', { headers: { 'Authorization': userinfo.token } })
|
||||||
|
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
friends.value = response.data.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
// axios(({
|
||||||
|
// headers: {
|
||||||
|
// 'Authorization': userinfo.token
|
||||||
|
// },
|
||||||
|
// method: 'GET',
|
||||||
|
// url: '/api/friend/getfriends'
|
||||||
|
// })).then((response) => {
|
||||||
|
// if (response.data.code === 200) {
|
||||||
|
// friends.value = response.data.data
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// ElMessage.error(response.data.msg)
|
||||||
|
// }
|
||||||
|
// }).catch((error) => {
|
||||||
|
// ElMessage.error('获取好友信息失败,请检查网络连接' + error)
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//切换聊天对象
|
||||||
|
const switchTemplate = (u_id, u_name, u_avatar) => {
|
||||||
|
message.sender = u_id
|
||||||
|
oppositeId.value = u_id
|
||||||
|
oppositeName.value = u_name
|
||||||
|
oppositeAvatar.value = u_avatar
|
||||||
|
message.setCorresponding()
|
||||||
|
messagebox.value = message.corresponding
|
||||||
|
inputDisabled.value = false
|
||||||
|
placeholder.value = '请输入消息内容'
|
||||||
|
scrollToBottom()
|
||||||
|
selectedFriendId.value = u_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义回车键的处理函数
|
||||||
|
const handleEnter = () => {
|
||||||
|
if (inputValue.value === '') {
|
||||||
|
ElMessage.error('消息不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送一类消息给目标用户
|
||||||
|
const msg = {
|
||||||
|
group: false,
|
||||||
|
message: true,
|
||||||
|
system: false,
|
||||||
|
sender: userinfo.user.u_id,
|
||||||
|
sender_name: userinfo.user.u_name,
|
||||||
|
target: message.sender,//sender为选中聊天的用户id
|
||||||
|
content: inputValue.value,
|
||||||
|
time: new Date().toLocaleString()
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.send(JSON.stringify(msg))
|
||||||
|
message.addMessage(msg)
|
||||||
|
inputValue.value = ''
|
||||||
|
scrollToBottom()
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const scrollbar = scrollbarRef.value?.$el; // 获取 el-scrollbar 的根元素
|
||||||
|
if (scrollbar) {
|
||||||
|
const scrollWrap = scrollbar.querySelector(".el-scrollbar__wrap"); // 找到滚动区域
|
||||||
|
if (scrollWrap) {
|
||||||
|
scrollWrap.scrollTop = scrollWrap.scrollHeight; // 滚动到底部
|
||||||
|
} else {
|
||||||
|
console.error("滚动容器未找到,请检查 el-scrollbar 的 DOM 结构");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听消息
|
||||||
|
watch(() => message.messages.length, (newLength, oldLength) => {
|
||||||
|
const msg = message.messages[newLength - 1]
|
||||||
|
if (newLength > oldLength && (msg.sender === message.sender && msg.target === message.target) || (msg.sender === message.target && msg.target === message.sender)) {
|
||||||
|
messagebox.value.push(msg)
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteFriend = (u_id) => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/friend/deletefriend',
|
||||||
|
data: {
|
||||||
|
u_id: u_id
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
inputValue.value = ''
|
||||||
|
inputDisabled.value = true
|
||||||
|
placeholder.value = '请选择聊天对象'
|
||||||
|
oppositeAvatar.value = ''
|
||||||
|
ElMessage.success('删除好友成功')
|
||||||
|
getFriends()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteChatHisory = (u_id) => {
|
||||||
|
try {
|
||||||
|
if (u_id === message.sender) messagebox.value = []
|
||||||
|
message.deleteMessagesHistory(userinfo.user.u_id, u_id)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('删除聊天记录失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmCall = () => {
|
||||||
|
dialogVisibleCallConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmCall = () => {
|
||||||
|
dialogVisibleCallConfirm.value = false
|
||||||
|
oncall.setTarget(oppositeId.value, oppositeName.value, oppositeAvatar.value)
|
||||||
|
oncall.panelOn()
|
||||||
|
oncall.callingOn()
|
||||||
|
oncall.fromOn()
|
||||||
|
voice.startCall(userinfo.user.u_id, userinfo.user.u_name, userinfo.user.u_avatar, oppositeId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getFriends()
|
||||||
|
await nextTick()
|
||||||
|
console.log(friends.value)
|
||||||
|
console.log(message.sender)
|
||||||
|
if (message.sender !== '' || message.sender !== null) {
|
||||||
|
console.log("切换聊天对象")
|
||||||
|
const foundUser = friends.value.find((friend) => friend.u_id === message.sender);
|
||||||
|
switchTemplate(message.sender, foundUser.u_name, foundUser.u_avatar)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
height: 70%;
|
||||||
|
margin-top: 4%;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infinitelist {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infinitelistitem {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infinitelistitem.selected {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
/* 灰色背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox {
|
||||||
|
height: 400px;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputbox {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-link {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
/* 去掉链接的下划线 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
position: relative;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
right: -5px;
|
||||||
|
z-index: 1;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online {
|
||||||
|
background-color: green;
|
||||||
|
/* 在线状态 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline {
|
||||||
|
background-color: red;
|
||||||
|
/* 离线状态 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
padding: 10px;
|
||||||
|
height: 80px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile.left {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile.right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-content {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-content.left {
|
||||||
|
left: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-content.right {
|
||||||
|
right: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-time {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 23px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-time.left {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-time.right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call {
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
565
src/views/home/group.vue
Normal file
565
src/views/home/group.vue
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<h2 style="display: inline-block;">
|
||||||
|
群聊
|
||||||
|
</h2>
|
||||||
|
<el-button style="display: inline-block;margin-left: 50px;margin-bottom: 3px;"
|
||||||
|
@click="joinDialogVisible = true">加入群聊</el-button>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-input v-model="tempsearch" :prefix-icon="Search" style="width: 90%;margin-left: 5px;"
|
||||||
|
clearable></el-input>
|
||||||
|
<el-scrollbar class="groups">
|
||||||
|
<div v-for="(item) in tempsearchResult" class="group"
|
||||||
|
@click="switchGroup(item.g_id, item.g_name, item.g_avatar, item.g_note)"
|
||||||
|
:class="{ 'selected': selectedGroupId === item.g_id }">
|
||||||
|
<div class="groupAvatar">
|
||||||
|
<img :src="item.g_avatar" alt="群头像">
|
||||||
|
</div>
|
||||||
|
<div class="groupName">
|
||||||
|
<el-text truncated>{{ item.g_name }}</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="15">
|
||||||
|
<!-- 显示当前选择的群聊信息 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="groupInfo">
|
||||||
|
<el-text size="large">{{ selectedGroupName }}</el-text>
|
||||||
|
<el-button class="groupInfoBtn" @click="createDialogVisible = true">创建群聊</el-button>
|
||||||
|
<el-button class="groupInfoBtn" style="right: 200px;" v-if="selected"
|
||||||
|
@click="groupManagementVisible = true">群管理</el-button>
|
||||||
|
<el-button class="groupInfoBtn" style="right: 110px;" v-if="selected"
|
||||||
|
@click="leaveGroup">离开群聊</el-button>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- 消息显示框 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-scrollbar ref="scrollbarRef" class="messagebox" style="height: 450px;">
|
||||||
|
<div v-for="(item) in messagebox" class="message-item">
|
||||||
|
<div
|
||||||
|
:class="{ 'message-item-profile': true, 'left': item.sender !== userinfo.user.u_id, 'right': item.sender === userinfo.user.u_id }">
|
||||||
|
<img :src="getAvatar(item.sender)" alt="User Avatar" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="{ 'message-item-content': true, 'left': item.sender !== userinfo.user.u_id, 'right': item.sender === userinfo.user.u_id }">
|
||||||
|
{{ item.content }}</div>
|
||||||
|
<div
|
||||||
|
:class="{ 'message-item-time': true, 'left': item.sender === userinfo.user.u_id, 'right': item.sender !== userinfo.user.u_id }">
|
||||||
|
{{ item.time }}</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="inputBox">
|
||||||
|
<el-input v-model="message" placeholder="请输入消息内容" @keyup.enter="sendMessage" clearable
|
||||||
|
style="width: 80%;" :disabled="inputDisabled"></el-input>
|
||||||
|
<el-button type="primary" @click="sendMessage" style="margin-left: 5px;"
|
||||||
|
:disabled="inputDisabled">发送</el-button>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog v-model="createDialogVisible" title="创建群聊">
|
||||||
|
<el-input v-model="groupName" placeholder="请输入群聊名称" clearable style="width: 80%;"></el-input>
|
||||||
|
<el-button type="primary" @click="createGroup"
|
||||||
|
style="position: absolute;bottom: 10px;right: 10px;">创建</el-button>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="groupManagementVisible" title="群管理">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<div class="groupAvatar">
|
||||||
|
<img :src="selectedGroupAvatar" alt="群头像">
|
||||||
|
</div>
|
||||||
|
<div class="groupName">
|
||||||
|
<el-text truncated>{{ selectedGroupName }}#{{ selectedGroupId }}</el-text>
|
||||||
|
</div>
|
||||||
|
<div class="groupNote">
|
||||||
|
<el-text truncated>{{ selectedGroupNote }}</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-text size="large">成员管理</el-text>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-input v-model="tempsearchmember" clearable :prefix-icon="Search"></el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="memberSearchResult" style="width: 100%;height: 300px;">
|
||||||
|
<el-table-column prop="m_avatar" label="" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- 使用 el-avatar 组件显示头像 -->
|
||||||
|
<el-avatar :src="memberSearchResult[scope.$index].m_avatar" size="large" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="m_name" label="姓名" width="125"></el-table-column>
|
||||||
|
<el-table-column prop="m_id" label="id" width="125"></el-table-column>
|
||||||
|
<el-table-column>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="addFriend(scope.row.m_id)">添加好友</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="kick(scope.row.m_id)">踢出</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="joinDialogVisible" title="加入群聊">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-input v-model="searchGroup" placeholder="请输入群聊名称" clearable style="width: 80%;"
|
||||||
|
:prefix-icon="Search" @keyup.enter="searchGroups"></el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="groupSearchResult" style="height: 300px;">
|
||||||
|
<el-table-column prop="g_avatar" label="" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-avatar :src="groupSearchResult[scope.$index].g_avatar" size="large" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="g_name" label="姓名" width="125"></el-table-column>
|
||||||
|
<el-table-column prop="g_id" label="id" width="125"></el-table-column>
|
||||||
|
<el-table-column prop="identify" label="是否需要验证" width="125"></el-table-column>
|
||||||
|
<el-table-column>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="joinGroup(scope.row.g_id)">加入</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { groupMessageStore } from '@/store/message'
|
||||||
|
import { onMounted, ref, computed, nextTick, watch } from 'vue'
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onlineSocketStore } from '@/store/Online'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
|
||||||
|
const socket = onlineSocketStore();
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
const groupMessage = groupMessageStore();
|
||||||
|
|
||||||
|
const groupManagementVisible = ref(false)
|
||||||
|
const createDialogVisible = ref(false)
|
||||||
|
const joinDialogVisible = ref(false)
|
||||||
|
|
||||||
|
const inputDisabled = ref(true)
|
||||||
|
const selected = ref(false)
|
||||||
|
const selectedGroupId = ref('')
|
||||||
|
const selectedGroupName = ref('请选择群聊')
|
||||||
|
const selectedGroupAvatar = ref('')
|
||||||
|
const selectedGroupNote = ref('')
|
||||||
|
const groupName = ref('default')
|
||||||
|
const currentRole = ref('')
|
||||||
|
const tempsearchmember = ref('')
|
||||||
|
|
||||||
|
const searchGroup = ref('')
|
||||||
|
const groupSearchResult = ref([])
|
||||||
|
|
||||||
|
|
||||||
|
const memberSearchResult = computed(() => {
|
||||||
|
if (!tempsearchmember.value)
|
||||||
|
return groupsMember.value
|
||||||
|
else
|
||||||
|
return groupsMember.value.filter(item => item.m_name.includes(tempsearchmember.value))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupsMember = ref([
|
||||||
|
|
||||||
|
]);
|
||||||
|
const groups = ref([
|
||||||
|
// {
|
||||||
|
// g_id: '1',
|
||||||
|
// g_name: '我的群',
|
||||||
|
// g_avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// g_id: '2',
|
||||||
|
// g_name: '我的好友群',
|
||||||
|
// g_avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// g_id: '3',
|
||||||
|
// g_name: '我的家人群',
|
||||||
|
// g_avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
|
||||||
|
// },
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
const messagebox = ref([])
|
||||||
|
const message = ref('')
|
||||||
|
const tempsearch = ref('')
|
||||||
|
const tempsearchResult = computed(() => {
|
||||||
|
if (!tempsearch.value)
|
||||||
|
return groups.value
|
||||||
|
else
|
||||||
|
return groups.value.filter(item => item.g_name.includes(tempsearch.value))
|
||||||
|
})
|
||||||
|
|
||||||
|
const scrollbarRef = ref(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const scrollbar = scrollbarRef.value?.$el; // 获取 el-scrollbar 的根元素
|
||||||
|
if (scrollbar) {
|
||||||
|
const scrollWrap = scrollbar.querySelector(".el-scrollbar__wrap"); // 找到滚动区域
|
||||||
|
if (scrollWrap) {
|
||||||
|
scrollWrap.scrollTop = scrollWrap.scrollHeight; // 滚动到底部
|
||||||
|
} else {
|
||||||
|
console.error("滚动容器未找到,请检查 el-scrollbar 的 DOM 结构");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroupMembers = async (g_id) => {
|
||||||
|
axios({
|
||||||
|
url: '/api/group/getmember/' + g_id,
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
groupsMember.value = response.data.data
|
||||||
|
currentRole.value = response.data.data.find(item => item.m_id === userinfo.user.u_id).role
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const switchGroup = async (g_id, g_name, g_avatar, g_note) => {
|
||||||
|
groupMessage.g_id = g_id
|
||||||
|
inputDisabled.value = false
|
||||||
|
getGroupMembers(g_id)
|
||||||
|
if (selectedGroupId.value) {
|
||||||
|
groupMessage.saveMessagesHistory(userinfo.user.u_id, selectedGroupId.value)
|
||||||
|
messagebox.value = []
|
||||||
|
console.log(messagebox.value)
|
||||||
|
}
|
||||||
|
await groupMessage.getHistoryMessages(userinfo.user.u_id, g_id)
|
||||||
|
await groupMessage.initMessages()
|
||||||
|
messagebox.value = groupMessage.historymessages
|
||||||
|
selectedGroupId.value = g_id
|
||||||
|
selectedGroupName.value = g_name
|
||||||
|
selectedGroupAvatar.value = g_avatar
|
||||||
|
selectedGroupNote.value = g_note
|
||||||
|
selected.value = true
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (message.value === '') {
|
||||||
|
ElMessage.error('消息不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送四类消息给目标用户
|
||||||
|
const msg = {
|
||||||
|
group: true,
|
||||||
|
message: true,
|
||||||
|
system: false,
|
||||||
|
sender: userinfo.user.u_id,
|
||||||
|
sender_name: userinfo.user.u_name,
|
||||||
|
target: selectedGroupId.value,
|
||||||
|
target_name: selectedGroupName.value,
|
||||||
|
content: message.value,
|
||||||
|
time: new Date().toLocaleString()
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.send(JSON.stringify(msg))
|
||||||
|
groupMessage.addMessage(msg)
|
||||||
|
message.value = ''
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAvatar = (u_id) => {
|
||||||
|
// 假设 groupsMember 是一个响应式变量,包含多个对象
|
||||||
|
const member = groupsMember.value.find(member => member.u_id === u_id);
|
||||||
|
return member ? member.u_avatar : null; // 如果找到匹配项,返回 u_avatar,否则返回 null
|
||||||
|
};
|
||||||
|
// 监听消息
|
||||||
|
watch(() => groupMessage.messages.length, (newLength) => {
|
||||||
|
const msg = groupMessage.messages[newLength - 1]
|
||||||
|
messagebox.value.push(msg)
|
||||||
|
scrollToBottom()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getGroups = async () => {
|
||||||
|
const response = await axios.get('/api/group/getgroups', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
groups.value = response.data.data
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createGroup = async () => {
|
||||||
|
if (groupName.value === '') {
|
||||||
|
ElMessage.error('群聊名称不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const response = await axios.post('/api/group/create', {
|
||||||
|
g_name: groupName.value
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
createDialogVisible.value = false
|
||||||
|
getGroups()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchGroups = () => {
|
||||||
|
axios({
|
||||||
|
url: '/api/group/search',
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
g_name: searchGroup.value
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
groupSearchResult.value = response.data.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinGroup = (g_id) => {
|
||||||
|
axios({
|
||||||
|
url: '/api/group/joingroup',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
g_id: g_id,
|
||||||
|
u_id: userinfo.user.u_id,
|
||||||
|
identify: 0
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('加入成功')
|
||||||
|
joinDialogVisible.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
getGroups()
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaveGroup = async () => {
|
||||||
|
const response = await axios.post('/api/group/leave', {
|
||||||
|
params: {
|
||||||
|
g_id: selectedGroupId.value
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('离开成功')
|
||||||
|
selected.value = false
|
||||||
|
selectedGroupId.value = ''
|
||||||
|
selectedGroupName.valuemessa = '请选择群聊'
|
||||||
|
messagebox.value = []
|
||||||
|
groupMessage.clearMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setNote = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getGroups()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.groups {
|
||||||
|
width: 80%;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
width: 90%;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #d3d3d3;
|
||||||
|
margin: 5px 5px 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group.selected {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
/* 灰色背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupAvatar {
|
||||||
|
width: 30%;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupAvatar img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: -7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupNote {
|
||||||
|
width: 70%;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupname {
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox {
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
padding: 10px;
|
||||||
|
height: 80px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile.left {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-profile.right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-content {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-content.left {
|
||||||
|
left: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-content.right {
|
||||||
|
right: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-time {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 23px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-time.left {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-time.right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupInfo {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupInfoBtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
88
src/views/home/home.vue
Normal file
88
src/views/home/home.vue
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<div class="home-container">
|
||||||
|
|
||||||
|
|
||||||
|
<div id="mainContent" class="main-content">
|
||||||
|
<!-- 左侧主空间 -->
|
||||||
|
<div class="left-content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 语音面板 -->
|
||||||
|
<phonePanel />
|
||||||
|
|
||||||
|
<!-- 消息按钮 -->
|
||||||
|
<GlobalMessageButton />
|
||||||
|
|
||||||
|
<!-- 右下角用户头像框 -->
|
||||||
|
<UserProfile />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, watchEffect } from 'vue';
|
||||||
|
import Navbar from '../../components/navBar.vue';
|
||||||
|
import UserProfile from '../../components/userProfile.vue';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
import { onlineSocketStore } from '@/store/Online';
|
||||||
|
import { getUserInfo } from '@/functions/user';
|
||||||
|
import GlobalMessageButton from '@/components/GlobalMessageButton.vue';
|
||||||
|
import { messageStore } from '@/store/message';
|
||||||
|
import phonePanel from '@/components/phonePanel.vue';
|
||||||
|
import { voiceStore } from '@/store/Voice';
|
||||||
|
import { initDB } from '@/functions/historyMessages';
|
||||||
|
|
||||||
|
const voice = voiceStore();
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
const socket = onlineSocketStore();
|
||||||
|
const message = messageStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getUserInfo()
|
||||||
|
|
||||||
|
// 使用 watchEffect 监听 u_id 是否为空
|
||||||
|
watchEffect(() => {
|
||||||
|
if (userinfo.user.u_id) { // 如果 u_id 不为空
|
||||||
|
console.log('User ID is available:', userinfo.user.u_id);
|
||||||
|
socket.connect(userinfo.user.u_id); // 建立 WebSocket 连接
|
||||||
|
message.target = userinfo.user.u_id; // 设置消息发送者为当前用户 ID
|
||||||
|
|
||||||
|
voice.connect(userinfo.user.u_id); // 建立语音通话连接
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
initDB(); // 初始化历史消息数据库
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-container {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 72px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
src/views/home/homeDefault.vue
Normal file
23
src/views/home/homeDefault.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-default">
|
||||||
|
<h1>欢迎使用Myplayer!</h1>
|
||||||
|
<h2>当前版本v0.2.0测试版</h2>
|
||||||
|
<p>v0.1.0 websocket实时聊天实装</p>
|
||||||
|
<p>v0.1.1 面板更新,websocket重连机制增加</p>
|
||||||
|
<p>v0.1.2 修复bug,群聊功能性实现,webRTC点对点语言聊天实装</p>
|
||||||
|
<p>v0.2.0 修复若干bug,完善了部分ui和逻辑</p>
|
||||||
|
<p>预期开发计划:1、播放器相关开发 2、ui重绘 3、群聊功能完善......</p>
|
||||||
|
<p>总之还有好多事,慢慢写吧!</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-default {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
266
src/views/home/message.vue
Normal file
266
src/views/home/message.vue
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<template>
|
||||||
|
<div class="box">
|
||||||
|
<el-button @click="allRead">全部已读</el-button>
|
||||||
|
<el-row style="height: 30%;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<span style="font-size: 30px;">收到的邀请</span>
|
||||||
|
<el-scrollbar style="height: 300px;width: 900px;">
|
||||||
|
<div class="inviteItem">
|
||||||
|
<div v-for="item in friendInviting" :key="item.inviter">
|
||||||
|
<div class="inviteAvatar">
|
||||||
|
<img :src="item.inviter_avatar" alt="莫得">
|
||||||
|
</div>
|
||||||
|
<div style="width: 700px;display: inline-block;">
|
||||||
|
<span style="font-size: 20px;">用户:{{ item.inviter_name }} 邀请您成为好友</span>
|
||||||
|
</div>
|
||||||
|
<el-button style="width: 50px;" @click="acceptFriend(item.inviter)">接受</el-button>
|
||||||
|
<el-button style="width: 50px;" @click="rejectFriend(item.inviter)">拒绝</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inviteItem">
|
||||||
|
<div v-for="item in roomInviting" :key="item.inviter">
|
||||||
|
<div class="inviteAvatar">
|
||||||
|
<img :src="item.inviter_avatar" alt="莫得">
|
||||||
|
</div>
|
||||||
|
<div style="width: 700px;display: inline-block;">
|
||||||
|
<span style="font-size: 20px;">{{ item.inviter_name }} 邀请您加入房间:{{ item.room }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button style="width: 50px;" @click="acceptRoom(item.inviter, item.room)">接受</el-button>
|
||||||
|
<el-button style="width: 50px;" @click="rejectRoom(item.inviter, item.room)">拒绝</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="height: 40%;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<span style="font-size: 30px;">收到的消息</span>
|
||||||
|
<el-scrollbar style="height: 300px;">
|
||||||
|
<div class="inviteItem">
|
||||||
|
<div v-for="item in messageSign.sign" :key="item">
|
||||||
|
<div v-if="!item.target_name" style="width: 700px;display: inline-block;"
|
||||||
|
@click="directionToFriend(item)">
|
||||||
|
<span style="font-size: 20px;">{{ item.sender_name }} 发来了消息</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.target_name" style="width: 700px;display: inline-block;"
|
||||||
|
@click="directionToGroup(item.target_id)">
|
||||||
|
<span style="font-size: 20px;">{{ item.sender_name }} 在群组 {{ item.target_name }}
|
||||||
|
有消息</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { userInfoStore, messagePointStore } from '@/store/store';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { messageStore, messageSignStore } from '@/store/message';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
|
const message = messageStore()
|
||||||
|
const messageSign = messageSignStore()
|
||||||
|
|
||||||
|
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
const messagePoint = messagePointStore()
|
||||||
|
|
||||||
|
const allInviting = ref([])
|
||||||
|
|
||||||
|
const friendInviting = computed(() => allInviting.value.filter(item => item.room == null || item.room == ''))
|
||||||
|
const roomInviting = computed(() => allInviting.value.filter(item => item.room != null && item.room != ''))
|
||||||
|
|
||||||
|
const allRead = () => {
|
||||||
|
messagePoint.hasNewMessage = false
|
||||||
|
messageSign.clearSign()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllInviting = () => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/inviting/getinvitings'
|
||||||
|
}).then(res => {
|
||||||
|
allInviting.value = res.data.data
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptFriend = (inviter) => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/inviting/passinviting',
|
||||||
|
data: {
|
||||||
|
inviter: inviter,
|
||||||
|
target: userinfo.user.u_id
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('接受成功');
|
||||||
|
// 从 allInviting 中删除当前邀请项
|
||||||
|
const index = allInviting.value.findIndex(item => item.inviter === inviter && item.room === null);
|
||||||
|
console.log(index)
|
||||||
|
if (index !== -1) {
|
||||||
|
allInviting.value.splice(index, 1); // 修改 allInviting,computed 属性会自动更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
ElMessage.error('系统错误,请稍后再试')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const rejectFriend = (inviter) => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/inviting/refuseinviting',
|
||||||
|
data: {
|
||||||
|
inviter: inviter,
|
||||||
|
target: userinfo.user.u_id
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('拒绝成功');
|
||||||
|
// 从 allInviting 中删除当前邀请项
|
||||||
|
const index = allInviting.value.findIndex(item => item.inviter === inviter && item.room === null);
|
||||||
|
console.log(index)
|
||||||
|
if (index !== -1) {
|
||||||
|
allInviting.value.splice(index, 1); // 修改 allInviting,computed 属性会自动更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ElMessage.error('系统错误,请稍后再试')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptRoom = (inviter, room) => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/inviting/passinviting',
|
||||||
|
data: {
|
||||||
|
inviter: inviter,
|
||||||
|
target: userinfo.user.u_id,
|
||||||
|
room: room
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('拒绝成功')
|
||||||
|
// 从 allInviting 中删除当前邀请项
|
||||||
|
const index = allInviting.value.findIndex(item => item.inviter === inviter && item.room === room);
|
||||||
|
console.log(index)
|
||||||
|
if (index !== -1) {
|
||||||
|
allInviting.value.splice(index, 1); // 修改 allInviting,computed 属性会自动更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ElMessage.error('系统错误,请稍后再试')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const rejectRoom = (inviter, room) => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/inviting/refuseinviting',
|
||||||
|
data: {
|
||||||
|
inviter: inviter,
|
||||||
|
target: userinfo.user.u_id,
|
||||||
|
room: room
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('拒绝成功')
|
||||||
|
// 从 allInviting 中删除当前邀请项
|
||||||
|
const index = allInviting.value.findIndex(item => item.inviter === inviter && item.room === room);
|
||||||
|
console.log(index)
|
||||||
|
if (index !== -1) {
|
||||||
|
allInviting.value.splice(index, 1); // 修改 allInviting,computed 属性会自动更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ElMessage.error('系统错误,请稍后再试')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const directionToFriend = (item) => {
|
||||||
|
// 跳转到好友聊天页面
|
||||||
|
message.sender = item.sender
|
||||||
|
messageSign.removeSign(item)
|
||||||
|
router.push('/home/friends')
|
||||||
|
}
|
||||||
|
|
||||||
|
const directionToGroup = (id) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getAllInviting()
|
||||||
|
console.log(messageSign.sign)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.inviteAvatar {
|
||||||
|
display: inline-block;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inviteAvatar img {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inviteItem {
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
width: 100%;
|
||||||
|
height: 650px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
src/views/home/playroom.vue
Normal file
142
src/views/home/playroom.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<el-button @Click="createWindow = true">创建房间</el-button>
|
||||||
|
|
||||||
|
<el-table :data="rooms">
|
||||||
|
<el-table-column label="" prop="r_avatar" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- 使用 el-avatar 组件显示头像 -->
|
||||||
|
<el-avatar :src="rooms[scope.$index].r_avatar" size="large" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="房间名" prop="r_name"></el-table-column>
|
||||||
|
<el-table-column label="房间ID" prop="r_id"></el-table-column>
|
||||||
|
<el-table-column label="角色" prop="role"></el-table-column>
|
||||||
|
<el-table-column label="简介" prop="r_introduction"></el-table-column>
|
||||||
|
<el-table-column label="">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="goToRoom(scope.row.r_id)">进入房间</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<el-dialog v-model="createWindow" title="创建房间" style="width: 400px;height: 300px;">
|
||||||
|
<el-form ref="form" :model="formData" label-width="80px" @submit.prevent="createRoom">
|
||||||
|
<el-form-item label="房间名" prop="roomName">
|
||||||
|
<el-input v-model="formData.roomName" placeholder="请输入房间名" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" native-type="submit">创建</el-button>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { roomStore } from '@/store/room';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
const roominfo = roomStore()
|
||||||
|
|
||||||
|
const createWindow = ref(false)
|
||||||
|
const formData = reactive({
|
||||||
|
roomName: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rooms = ref([])
|
||||||
|
|
||||||
|
const goToRoom = (r_id) => {
|
||||||
|
console.log(r_id)
|
||||||
|
roominfo.r_id = r_id
|
||||||
|
// 跳转到房间页面
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
const targetUrl = `${baseUrl}/room`; // 替换为你的目标路由
|
||||||
|
window.open(targetUrl, "room");
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRoom = async () => {
|
||||||
|
if (!formData.roomName || formData.roomName.trim() === '') {
|
||||||
|
ElMessage.error('房间名不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/room/create',{
|
||||||
|
r_name: formData.roomName
|
||||||
|
},{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
console.log(formData.roomName + '创建成功')
|
||||||
|
} else {
|
||||||
|
console.log(response.data.msg)
|
||||||
|
}
|
||||||
|
formData.roomName = ''
|
||||||
|
createWindow.value = false
|
||||||
|
getRooms()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRooms = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/room/getrooms',{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
console.log(response.data.data)
|
||||||
|
rooms.value = response.data.data
|
||||||
|
} else {
|
||||||
|
console.log(response.data.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getRooms()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 800px;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
top: 50px;
|
||||||
|
left: 100px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
186
src/views/home/search.vue
Normal file
186
src/views/home/search.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<div class="box">
|
||||||
|
<el-row style="height: 50%;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<h1>搜索好友</h1>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-input v-model="inputValue" style="width: 80%" size="large" placeholder="Please Input" clearable
|
||||||
|
:prefix-icon="Search" @keyup.enter="searchFriend" />
|
||||||
|
<el-button class="search" size="large" @click="searchFriend">搜索</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="friendSearchResult" style="width: 100%;height: 200px;">
|
||||||
|
<el-table-column prop="u_avatar" label="" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- 使用 el-avatar 组件显示头像 -->
|
||||||
|
<el-avatar :src="friendSearchResult[scope.$index].u_avatar" size="large" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="u_name" label="姓名" width="125"></el-table-column>
|
||||||
|
<el-table-column prop="u_id" label="id" width="125"></el-table-column>
|
||||||
|
<el-table-column prop="u_introduction" label="个性签名" width="200"></el-table-column>
|
||||||
|
<el-table-column>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="addFriend(scope.row.u_id)">添加好友</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="height: 50%;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<h1>搜索playRoom</h1>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-input v-model="inputValue1" style="width: 80%" size="large" placeholder="Please Input" clearable
|
||||||
|
:prefix-icon="Search" @keyup.enter="searchPlayRoom" />
|
||||||
|
<el-button class="search" size="large" @click="searchPlayRoom">搜索</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="roomSearchResult" style="width: 100%;height: 200px;">
|
||||||
|
<el-table-column prop="r_avatar" label="" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- 使用 el-avatar 组件显示头像 -->
|
||||||
|
<el-avatar :src="roomSearchResult[scope.$index].r_avatar" size="large" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="r_name" label="姓名" width="125"></el-table-column>
|
||||||
|
<el-table-column prop="r_id" label="id" width="125"></el-table-column>
|
||||||
|
<el-table-column prop="r_introduction" label="个性签名" width="200"></el-table-column>
|
||||||
|
<el-table-column>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="joinRoom(scope.row.r_id)">加入房间</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
import { onlineSocketStore } from '@/store/Online'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const socket = onlineSocketStore()
|
||||||
|
const inputValue = ref('')
|
||||||
|
const inputValue1 = ref('')
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
|
||||||
|
|
||||||
|
const friendSearchResult = ref([])
|
||||||
|
const roomSearchResult = ref([])
|
||||||
|
|
||||||
|
// 定义回车键的处理函数
|
||||||
|
const searchFriend = () => {
|
||||||
|
console.log(inputValue.value);
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/friend/searchuser',
|
||||||
|
params: {
|
||||||
|
u_name: inputValue.value
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
friendSearchResult.value = response.data.data
|
||||||
|
console.log(friendSearchResult.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
//搜索playRoom
|
||||||
|
const searchPlayRoom = () => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/room/search',
|
||||||
|
params: {
|
||||||
|
r_name: inputValue1.value
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
roomSearchResult.value = response.data.data
|
||||||
|
console.log(roomSearchResult.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
inputValue1.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//添加好友逻辑
|
||||||
|
const addFriend = (u_id) => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/inviting/sendinviting',
|
||||||
|
data: {
|
||||||
|
inviter: userinfo.user.u_id,
|
||||||
|
target: u_id
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('已发送申请,等待对方处理')
|
||||||
|
const message = {
|
||||||
|
message: false,
|
||||||
|
system: false,
|
||||||
|
target: u_id,
|
||||||
|
sender_name: userinfo.user.u_name
|
||||||
|
}
|
||||||
|
socket.send(JSON.stringify(message))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.error('发送失败,请稍后再试 msg:' + response.data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//加入房间申请逻辑
|
||||||
|
const joinRoom = (r_id) => {
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
},
|
||||||
|
url: '/api/inviting/sendinviting',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
inviter: userinfo.user.u_id,
|
||||||
|
target: userinfo.user.u_id,
|
||||||
|
room: r_id
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success("请求发送成功,请耐心等待审核")
|
||||||
|
}
|
||||||
|
else if (response.data.code === 500) {
|
||||||
|
ElMessage.error("请勿重复发送!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.box {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/views/home/setting.vue
Normal file
1
src/views/home/setting.vue
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<template></template>
|
||||||
149
src/views/login/init.vue
Normal file
149
src/views/login/init.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="init-container">
|
||||||
|
<h1>用户初始化</h1>
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<!-- 上传头像 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="avatar">上传头像:</label>
|
||||||
|
<input type="file" id="avatar" @change="handleAvatarChange" accept="image/*" />
|
||||||
|
<div v-if="avatarPreview" class="avatar-preview">
|
||||||
|
<img :src="avatarPreview" alt="头像预览" />
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="uploadAvatar">确认上传</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置用户名 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">用户名:</label>
|
||||||
|
<input type="text" id="username" v-model="username" required pattern="^[a-zA-Z0-9\u4e00-\u9fa5_-]{2,8}$"
|
||||||
|
title="用户名必须为2到8个字符,只能包含字母、数字、汉字、下划线(_)和短横线(-)" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置个性签名 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="signature">个性签名:</label>
|
||||||
|
<input type="text" id="signature" v-model="signature" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit">完成初始化</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import defaultavatar from '../../assets/defaultavatar.jpg';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const avatar = ref(null); // 存储上传的头像文件
|
||||||
|
const avatarPreview = ref(null); // 头像预览
|
||||||
|
const username = ref(''); // 用户名
|
||||||
|
const signature = ref(''); // 个性签名
|
||||||
|
|
||||||
|
//设置默认头像
|
||||||
|
avatarPreview.value = defaultavatar;
|
||||||
|
//设置默认姓名
|
||||||
|
username.value = "default";
|
||||||
|
|
||||||
|
// 处理头像预览并将文件保存到变量
|
||||||
|
const handleAvatarChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
avatar.value = file;
|
||||||
|
avatarPreview.value = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//上传到文件到服务器
|
||||||
|
const uploadAvatar = () => {
|
||||||
|
const formdata = new FormData();
|
||||||
|
formdata.append('avatar', avatar.value)
|
||||||
|
formdata.append('id', userinfo.user.u_id)
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/avatar/upload',
|
||||||
|
data: formdata
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
ElMessage.error(response.data.msg || '上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
ElMessage.error('上传失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!username.value) {
|
||||||
|
ElMessage.error('用户名不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
u_name: username.value,
|
||||||
|
u_introduction: signature.value
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/init', data,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('初始化成功');
|
||||||
|
router.replace('/home');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '初始化失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化失败:', error);
|
||||||
|
ElMessage.error('初始化失败');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.init-container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
src/views/login/login.vue
Normal file
117
src/views/login/login.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>登录</h2>
|
||||||
|
<form @submit.prevent="handleLogin">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="account">邮箱</label>
|
||||||
|
<input type="text" id="account" v-model="account" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input type="password" id="password" v-model="password" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit">登录</button>
|
||||||
|
<router-link to="/register" class="link">注册</router-link>
|
||||||
|
<router-link to="/reset-password" class="link">忘记密码?</router-link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { userInfoStore } from '@/store/store.js';
|
||||||
|
import SHA256 from 'crypto-js/sha256';
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
const account = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const encryptedPassword = ref('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
encryptedPassword.value = SHA256(password.value).toString();
|
||||||
|
const response = await axios.post('/api/login', {
|
||||||
|
u_account: account.value,
|
||||||
|
u_password: encryptedPassword.value,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
userinfo.token = response.data.data;
|
||||||
|
userinfo.user.u_account = account.value;
|
||||||
|
console.log(userinfo.user.u_account);
|
||||||
|
router.replace({ name: 'home' });
|
||||||
|
} else {
|
||||||
|
alert('账号或密码错误!' + response.data.msg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
alert('Login failed. Please try again later.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
220
src/views/login/register.vue
Normal file
220
src/views/login/register.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<div class="register-container">
|
||||||
|
<h2>Register</h2>
|
||||||
|
<form @submit.prevent="handleRegister">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="u_account">邮箱</label>
|
||||||
|
<input type="u_account" id="u_account" v-model="u_account" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="u_password">密码</label>
|
||||||
|
<input type="u_password" id="u_password" v-model="u_password" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm-u_password">确认密码</label>
|
||||||
|
<input type="u_password" id="confirm-u_password" v-model="confirmPassword" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="verification-code">验证码</label>
|
||||||
|
<input type="text" id="verification-code" v-model="verificationCode" required />
|
||||||
|
<button type="button" :disabled="isCountingDown" @click="sendVerificationCode">
|
||||||
|
{{ isCountingDown ? `${countdownTime} s` : 'Get Code' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit">验证并注册</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||||
|
import { userInfoStore } from '@/store/store.js';
|
||||||
|
import SHA256 from 'crypto-js/sha256';
|
||||||
|
|
||||||
|
const u_account = ref('');
|
||||||
|
const u_password = ref('');
|
||||||
|
const encryptedPassword = ref('');
|
||||||
|
const confirmPassword = ref('');
|
||||||
|
const verificationCode = ref('');
|
||||||
|
const countdownTime = ref(60);
|
||||||
|
const isCountingDown = ref(false);
|
||||||
|
const codeId = ref('');
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 定义邮箱验证的正则表达式
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
|
|
||||||
|
// 使用 computed 属性来验证邮箱格式
|
||||||
|
const isEmailValid = computed(() => {
|
||||||
|
return emailRegex.test(u_account.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendVerificationCode = () => {
|
||||||
|
if (isCountingDown.value) return;
|
||||||
|
console.log(isEmailValid.value)
|
||||||
|
if (!isEmailValid.value) {
|
||||||
|
ElMessage.error("请输入正确的邮箱格式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(u_account.value)
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: "post",
|
||||||
|
url: "/api/code/sendcode",
|
||||||
|
data: JSON.stringify({ u_account: u_account.value })
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data && response.data.code !== 200) {
|
||||||
|
ElMessageBox.alert("注册失败,请重新填写 msg:" + (response.data.msg || '未知错误'), '注册失败');
|
||||||
|
} else {
|
||||||
|
startCountdown();
|
||||||
|
codeId.value = response.data.data;
|
||||||
|
console.log(codeId);
|
||||||
|
ElMessageBox.alert('发送成功,请耐心等待', '请求成功');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("请求失败:", error);
|
||||||
|
ElMessageBox.alert("请求失败,请稍后再试", '网络错误');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const startCountdown = () => {
|
||||||
|
isCountingDown.value = true;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
countdownTime.value--;
|
||||||
|
if (countdownTime.value <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
isCountingDown.value = false;
|
||||||
|
countdownTime.value = 60;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注册逻辑
|
||||||
|
const handleRegister = () => {
|
||||||
|
//验证验证码是否正确
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: "post",
|
||||||
|
url: "/api/code/verifycode",
|
||||||
|
data: JSON.stringify({
|
||||||
|
v_id: codeId.value,
|
||||||
|
code: verificationCode.value
|
||||||
|
})
|
||||||
|
}).then((response) => {
|
||||||
|
console.log(verificationCode)
|
||||||
|
if (response.data.code === 210)
|
||||||
|
ElMessageBox.alert("验证码过期")
|
||||||
|
else if (response.data.code === 211)
|
||||||
|
ElMessageBox.alert("验证码错误")
|
||||||
|
else {
|
||||||
|
//验证成功,提交注册表单
|
||||||
|
if (u_password.value !== confirmPassword.value) {
|
||||||
|
ElMessageBox.alert("两次密码不一致", '注册失败')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
encryptedPassword.value = SHA256(u_password.value).toString();
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/register',
|
||||||
|
data: JSON.stringify({
|
||||||
|
u_account: u_account.value,
|
||||||
|
u_password: encryptedPassword.value
|
||||||
|
})
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 212) ElMessageBox.alert("该账号已注册", '注册失败')
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/login',
|
||||||
|
data: JSON.stringify({
|
||||||
|
u_account: u_account.value,
|
||||||
|
u_password: encryptedPassword.value
|
||||||
|
})
|
||||||
|
}).then((response) => {
|
||||||
|
userinfo.token = response.data.data;
|
||||||
|
userinfo.user.u_account = u_account.value;
|
||||||
|
ElMessage.success("注册成功,正在跳转");
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("登录失败:", error);
|
||||||
|
ElMessageBox.alert("登录失败,请稍后再试", '网络错误');
|
||||||
|
});
|
||||||
|
router.push({ name: 'init' }).catch(err => {
|
||||||
|
console.error("Routing error:", err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch((error) => {
|
||||||
|
ElMessageBox.alert("错误")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.register-container {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
146
src/views/login/resetPassword.vue
Normal file
146
src/views/login/resetPassword.vue
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<div class="reset-password-container">
|
||||||
|
<h2>Reset Password</h2>
|
||||||
|
<form @submit.prevent="handleResetPassword">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" v-model="email" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="verification-code">Verification Code</label>
|
||||||
|
<input type="text" id="verification-code" v-model="verificationCode" required />
|
||||||
|
<button type="button" :disabled="isCountingDown" @click="sendVerificationCode">
|
||||||
|
{{ isCountingDown ? `${countdownTime} s` : 'Get Code' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-password">New Password</label>
|
||||||
|
<input type="password" id="new-password" v-model="newPassword" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm-new-password">Confirm New Password</label>
|
||||||
|
<input type="password" id="confirm-new-password" v-model="confirmNewPassword" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit">Reset Password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const verificationCode = ref('');
|
||||||
|
const newPassword = ref('');
|
||||||
|
const confirmNewPassword = ref('');
|
||||||
|
const countdownTime = ref(60);
|
||||||
|
const isCountingDown = ref(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendVerificationCode = () => {
|
||||||
|
if (isCountingDown.value) return;
|
||||||
|
|
||||||
|
axios.post('/api/send-verification-code', { email: email.value })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data.success) {
|
||||||
|
alert('Verification code sent successfully!');
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
alert('Failed to send verification code. Please try again.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error sending verification code:', error);
|
||||||
|
alert('Failed to send verification code. Please try again.');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const startCountdown = () => {
|
||||||
|
isCountingDown.value = true;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
countdownTime.value--;
|
||||||
|
if (countdownTime.value <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
isCountingDown.value = false;
|
||||||
|
countdownTime.value = 60;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置密码逻辑
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
if (newPassword.value !== confirmNewPassword.value) {
|
||||||
|
alert('Passwords do not match.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/reset-password', {
|
||||||
|
email: email.value,
|
||||||
|
verificationCode: verificationCode.value,
|
||||||
|
newPassword: newPassword.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
alert('Password reset successful!');
|
||||||
|
router.push({ name: 'Login' });
|
||||||
|
} else {
|
||||||
|
alert('Password reset failed. Please try again.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Password reset error:', error);
|
||||||
|
alert('Password reset failed. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reset-password-container {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
559
src/views/room/room.vue
Normal file
559
src/views/room/room.vue
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
<template>
|
||||||
|
<div class="goback">
|
||||||
|
<el-button type="primary" @click="goback">返回</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="maincontent">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-row>
|
||||||
|
<!-- 播放器信息 -->
|
||||||
|
<div class="roominfo">
|
||||||
|
<div class="roombasedata">
|
||||||
|
<div class="room-profile">
|
||||||
|
<img :src="curruentRoomInfo.r_avatar" alt="User Avatar" />
|
||||||
|
</div>
|
||||||
|
<el-text class="roomname" size="large" truncated>{{ curruentRoomInfo.r_name }}</el-text>
|
||||||
|
<el-button type="primary" @click="drawer = true">房间管理</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<!-- 成员列表 -->
|
||||||
|
<el-scrollbar style="height: 300px;width: 90%;">
|
||||||
|
<ul class="memberlist" style="overflow: auto">
|
||||||
|
<li v-for="(item) in members" :key="item.u_id" class="memberlistitem">
|
||||||
|
<div class="user-profile">
|
||||||
|
<img :src="item.u_avatar" alt="User Avatar" />
|
||||||
|
<div :class="['status-dot', item.status]"></div>
|
||||||
|
</div>
|
||||||
|
<el-text style="width: 100%;" truncated>{{ item.u_name }}#{{ item.u_id }}</el-text>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<video-player :autoplay="false" :videoUrl="videoUrl" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-row style="height: 50%">
|
||||||
|
<!-- 语音大厅 -->
|
||||||
|
<el-col :span="24" class="voice-room">
|
||||||
|
<!-- <audio-player :audioUrl="audioUrl" /> -->
|
||||||
|
<el-row>
|
||||||
|
<!-- 加入的语言用户 -->
|
||||||
|
<el-text size="large">语言聊天室:</el-text>
|
||||||
|
<el-button type="success" round style="margin-left: 10px;" @click="joinVoiceRoom"
|
||||||
|
v-if="!invoice">加入</el-button>
|
||||||
|
<el-button type="info" round style="margin-left: 10px;" @click="leaveVoiceRoom"
|
||||||
|
v-if="invoice">离开</el-button>
|
||||||
|
<el-scrollbar style="height: 150px;width: 100%;">
|
||||||
|
<div class="invoice">
|
||||||
|
<div v-for="(item) in membersInVoice" class="user-profile-invoice">
|
||||||
|
<img :src="item.u_avatar" alt="User Avatar" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="height: 50%">
|
||||||
|
<el-col :span="24">
|
||||||
|
<!-- 弹幕显示区域 -->
|
||||||
|
<el-scroller class="comments">
|
||||||
|
<div v-for="comment in roomMessages" :key="comment.id" class="comment">
|
||||||
|
{{ comment.u_name }}:
|
||||||
|
{{ comment.content }}
|
||||||
|
</div>
|
||||||
|
</el-scroller>
|
||||||
|
<!-- 发送弹幕输入框 -->
|
||||||
|
<el-input v-model="newComment" placeholder="发送弹幕" style="height: 20%;margin-left: 3px;"
|
||||||
|
@keyup.enter="sendComment" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-drawer v-model="drawer" title="房间管理" direction="rtl" :before-close="handleClose">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div style="position: absolute;right: 0;">
|
||||||
|
<h3>权限等级:</h3>
|
||||||
|
<span v-if="role === 0">房主</span>
|
||||||
|
<span v-else-if="role === 1">管理员</span>
|
||||||
|
<span v-else-if="role === 2">用户</span>
|
||||||
|
<span v-else>未知权限等级</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="updateProfile">
|
||||||
|
<div class="profile">
|
||||||
|
<img :src="avatarPreview" alt="头像">
|
||||||
|
</div>
|
||||||
|
<el-col :span="12" v-if="role === 1 || role === 0">
|
||||||
|
<input type="file" id="avatar" @change="handleAvatarChange" accept="image/*" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12" v-if="role === 1 || role === 0">
|
||||||
|
<el-button class="re-profile" @click="uploadAvatar">修改头像</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<span>推荐头像参数:<br>
|
||||||
|
分辨率:1051*1051 96dpi <br>
|
||||||
|
格式:jpg、png <br>
|
||||||
|
大小:≤5M
|
||||||
|
</span>
|
||||||
|
</el-col>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="16"> <span style="font-size: large;"> 房间ID:</span>{{ curruentRoomInfo.r_id }}</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-button @click="getInvitingCode">生成邀请码</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-input v-model="curruentRoomInfo.currentURL" disabled />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-button @click="dialogVisibleVideo = true">替换视频流</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- 邀请码生成窗口 -->
|
||||||
|
<el-dialog title="生成邀请码" v-model="dialogVisibleCode">
|
||||||
|
<h1>{{ invitingCode }}</h1>
|
||||||
|
<div>邀请码五分钟有效</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 设置/替换视频流 -->
|
||||||
|
<el-dialog title="设置/替换视频流" v-model="dialogVisibleVideo">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="20">
|
||||||
|
<el-input v-model="curruentRoomInfo.currentURL" placeholder="请输入视频流地址"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-button>测试</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-button type="primary" @click="replaceURL" style="margin-left: 330px;margin-top: 40px;">确定替换</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, watchEffect } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
import videoPlayer from '@/components/videoPlayer.vue';
|
||||||
|
import { roomStore } from '@/store/room';
|
||||||
|
//import audioPlayer from '@/components/audioPlayer.vue'; // 假设你有一个音频播放组件
|
||||||
|
|
||||||
|
const curruentRoomInfo = roomStore();
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
|
||||||
|
const dialogVisibleCode = ref(false)
|
||||||
|
const dialogVisibleVideo = ref(false)
|
||||||
|
|
||||||
|
const invoice = ref(false)
|
||||||
|
const drawer = ref(false);
|
||||||
|
const role = ref(null);
|
||||||
|
const invitingCode = ref('666666')
|
||||||
|
|
||||||
|
const currentURL = ref('https://www.5dm.link/api/dd.php?vid=ccccxhndnys1&cid=ccccxhndnys1&xid=0&pid=55293&tid=1742788904&t=616d5131b6ade51a0e20814466b13515&ext=.mp4')
|
||||||
|
const avatar = ref(null)
|
||||||
|
const avatarPreview = ref('')
|
||||||
|
const roomMessages = ref([
|
||||||
|
{
|
||||||
|
u_name: 'system',
|
||||||
|
content: '欢迎来到弹幕聊天室'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const members = ref([
|
||||||
|
{
|
||||||
|
u_id: 'U000001',
|
||||||
|
u_name: '1',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000002',
|
||||||
|
u_name: '2',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000003',
|
||||||
|
u_name: '3',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'offline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000004',
|
||||||
|
u_name: '4',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000003',
|
||||||
|
u_name: '3',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const membersInVoice = ref([
|
||||||
|
{
|
||||||
|
u_id: 'U000001',
|
||||||
|
u_name: '1',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000002',
|
||||||
|
u_name: '2',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000003',
|
||||||
|
u_name: '3',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'offline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000004',
|
||||||
|
u_name: '4',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u_id: 'U000009',
|
||||||
|
u_name: '3',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
const videoUrl = ref('https://www.5dm.link/api/dd.php?vid=ccccxhndnys1&cid=ccccxhndnys1&xid=0&pid=55293&tid=1742788904&t=616d5131b6ade51a0e20814466b13515&ext=.mp4'); // 替换为你的视频流地址
|
||||||
|
const audioUrl = ref('https://example.com/audio/stream'); // 替换为你的音频流地址
|
||||||
|
|
||||||
|
const danmakuOptions = ref({
|
||||||
|
id: 'your-danmaku-id',
|
||||||
|
api: 'https://api.prprpr.me/dplayer/', // 弹幕 API 地址
|
||||||
|
token: 'your-token', // 弹幕 API 的 token
|
||||||
|
maximum: 1000, // 最大弹幕数量
|
||||||
|
addition: ['https://api.prprpr.me/dplayer/v3/bilibili?aid=123456'], // 额外弹幕数据
|
||||||
|
user: 'your-username',
|
||||||
|
bottom: '15%', // 弹幕底部距离
|
||||||
|
unlimited: true // 是否允许无限制弹幕
|
||||||
|
});
|
||||||
|
|
||||||
|
const comments = ref([]); // 存储弹幕评论
|
||||||
|
const newComment = ref(''); // 新弹幕内容
|
||||||
|
|
||||||
|
// 模拟发送弹幕
|
||||||
|
const sendComment = () => {
|
||||||
|
if (newComment.value.trim() !== '') {
|
||||||
|
roomMessages.value.push({
|
||||||
|
u_name: 'you',
|
||||||
|
content: newComment.value.trim()
|
||||||
|
});
|
||||||
|
newComment.value = ''; // 清空输入框
|
||||||
|
} else {
|
||||||
|
ElMessage.error('弹幕内容不能为空')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoomInfo = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAvatarChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
avatar.value = file;
|
||||||
|
avatarPreview.value = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//上传到服务器
|
||||||
|
const uploadAvatar = () => {
|
||||||
|
if (!avatar.value) {
|
||||||
|
ElMessage('请选择头像')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const formdata = new FormData();
|
||||||
|
formdata.append('file', avatar.value)
|
||||||
|
formdata.append('r_id', curruentRoomInfo.r_id)
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/avatar/upload',
|
||||||
|
data: formdata
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
ElMessage.error(response.data.msg || '上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getUserInfo()
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
ElMessage.error('上传失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInvitingCode = () => {
|
||||||
|
dialogVisibleCode.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceURL = () => {
|
||||||
|
currentURL.value = curruentRoomInfo.currentURL;
|
||||||
|
dialogVisibleVideo.value = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinVoiceRoom = () => {
|
||||||
|
membersInVoice.value.push({
|
||||||
|
u_id: 'U000009',
|
||||||
|
u_name: '3',
|
||||||
|
u_avatar: 'https://merlin.xin/avatars/avatar',
|
||||||
|
status: 'online'
|
||||||
|
})
|
||||||
|
invoice.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaveVoiceRoom = () => {
|
||||||
|
membersInVoice.value = membersInVoice.value.filter(item => item.u_id !== 'U000009')
|
||||||
|
invoice.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const goback = () => {
|
||||||
|
//window.location.href = '/home';
|
||||||
|
window.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getRoomInfo()
|
||||||
|
|
||||||
|
//测试代码
|
||||||
|
curruentRoomInfo.r_id = '123456';
|
||||||
|
curruentRoomInfo.r_name = '弹幕聊天室';
|
||||||
|
curruentRoomInfo.currentURL = 'https://www.5dm.link/api/dd.php?vid=ccccxhndnys1&cid=ccccxhndnys1&xid=0&pid=55293&tid=1742788904&t=616d5131b6ade51a0e20814466b13515&ext=.mp4';
|
||||||
|
curruentRoomInfo.r_avatar = 'https://merlin.xin/avatars/avatar';
|
||||||
|
role.value = 0;
|
||||||
|
avatarPreview.value = curruentRoomInfo.r_avatar;
|
||||||
|
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (curruentRoomInfo.r_id !== '' && curruentRoomInfo.r_id !== null) { // 如果 r_id 不为空
|
||||||
|
console.log('Room ID is available:', curruentRoomInfo.r_id);
|
||||||
|
//页面需要加载的逻辑
|
||||||
|
|
||||||
|
//头像预览容器赋值
|
||||||
|
avatarPreview.value = curruentRoomInfo.r_avatar;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.goback {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
top: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 80vh;
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .maincontent {} */
|
||||||
|
|
||||||
|
.roominfo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roombasedata {
|
||||||
|
height: 30%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.room-profile {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomname {
|
||||||
|
padding: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
color: white;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.updateProfile {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
top: 50px;
|
||||||
|
left: 100px;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memberlist {
|
||||||
|
list-style: none;
|
||||||
|
padding: 5px;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memberlist .memberlistitem {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
right: -5px;
|
||||||
|
z-index: 5;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online {
|
||||||
|
background-color: green;
|
||||||
|
/* 在线状态 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline {
|
||||||
|
background-color: rgb(78, 78, 78);
|
||||||
|
/* 离线状态 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-invoice {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-invoice img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-room {
|
||||||
|
width: 100%;
|
||||||
|
height: 99%;
|
||||||
|
background-color: #daf2f1;
|
||||||
|
margin-left: 3px;
|
||||||
|
border-radius: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
background-color: #939393;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 5%;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
496
src/views/user/user.vue
Normal file
496
src/views/user/user.vue
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="updateProfile">
|
||||||
|
<div class="profile">
|
||||||
|
<img :src="avatarPreview" alt="头像">
|
||||||
|
</div>
|
||||||
|
<el-col span="12">
|
||||||
|
<input type="file" id="avatar" @change="handleAvatarChange" accept="image/*" />
|
||||||
|
</el-col>
|
||||||
|
<el-col span="12">
|
||||||
|
<el-button class="re-profile" @click="uploadAvatar">修改头像</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col span="12">
|
||||||
|
<span>推荐头像参数:<br>
|
||||||
|
分辨率:1051*1051 96dpi <br>
|
||||||
|
格式:jpg、png <br>
|
||||||
|
大小:≤1M
|
||||||
|
</span>
|
||||||
|
</el-col>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-row>
|
||||||
|
<span style="width: 80%;">用户名:{{ userinfo.user.u_name }}</span>
|
||||||
|
<el-button @click="dialogVisibleChangeName = true">修改名字</el-button>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<span style="width: 80%;">账号:{{ userinfo.user.u_account }}</span>
|
||||||
|
<el-button @click="dialogVisibleChangeAccount = true">修改账号</el-button>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<span style="width: 80%;">个性签名:{{ userinfo.user.u_introduction }}</span>
|
||||||
|
<el-button @click="dialogVisibleChangeIntro = true">修改简介</el-button>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<span style="width: 80%;">密码: *********************</span>
|
||||||
|
<el-button @click="dialogVisibleChangePassword = true">修改密码</el-button>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- 修改名字弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisibleChangeName" title="修改名字" width="500">
|
||||||
|
<el-input v-model="newname" autocomplete="off" />
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisibleChangeName = false">算了</el-button>
|
||||||
|
<el-button type="primary" @click="changeName">
|
||||||
|
改!
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 修改个签弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisibleChangeIntro" title="修改个签" width="500">
|
||||||
|
<el-input v-model="newintro" autocomplete="off" size="large" />
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisibleChangeIntro = false">算了</el-button>
|
||||||
|
<el-button type="primary" @click="changeIntro">
|
||||||
|
改!
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 修改邮箱弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisibleChangeAccount" title="修改邮箱" width="500">
|
||||||
|
<el-row style="margin-bottom: 10px;">
|
||||||
|
<el-col span="6">当前邮箱:</el-col>
|
||||||
|
<el-col span="18">{{ userinfo.user.u_account }}</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form ref="formRef" :model="formData" :rules="rules">
|
||||||
|
<el-form-item label="新邮箱" prop="newaccount">
|
||||||
|
<el-col span="24">
|
||||||
|
<el-input v-model="formData.newaccount" autocomplete="off" placeholder="请输入邮箱地址"
|
||||||
|
style="width: 300px;" />
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="验证码" prop="code">
|
||||||
|
<el-col span="18">
|
||||||
|
<el-input v-model="formData.code" autocomplete="off" placeholder="请输入验证码" style="width: 225px;" />
|
||||||
|
</el-col>
|
||||||
|
<el-col span="6">
|
||||||
|
<button class="codebutton" type="button" :disabled="isCountingDown" @click="sendVerificationCode">
|
||||||
|
{{ isCountingDown ? `${countdownTime} s` : 'Get Code' }}
|
||||||
|
</button>
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisibleChangeAccount = false">算了</el-button>
|
||||||
|
<el-button type="primary" @click="changeAccount">
|
||||||
|
改!
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 修改密码弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisibleChangePassword" title="修改密码" width="500">
|
||||||
|
<el-form ref="formRedPassword" :model="password" :rules="passwordRules">
|
||||||
|
<el-form-item label="原密码" prop="oldpassword">
|
||||||
|
<el-col span="24">
|
||||||
|
<el-input v-model="password.oldpassword" autocomplete="off" placeholder="请输入原密码"
|
||||||
|
show-password="true" style="width: 300px;" />
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="password">
|
||||||
|
<el-col span="24">
|
||||||
|
<el-input v-model="password.password" autocomplete="off" placeholder="请输入新密码" show-password="true"
|
||||||
|
style="width: 300px;" />
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-col span="24">
|
||||||
|
<el-input v-model="password.confirmPassword" autocomplete="off" placeholder="确认密码"
|
||||||
|
show-password="true" style="width: 300px;" />
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisibleChangePassword = false">算了</el-button>
|
||||||
|
<el-button type="primary" @click="changePassword">
|
||||||
|
改!
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { getUserInfo } from '@/functions/user';
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
console.log(userinfo.user.u_account);
|
||||||
|
|
||||||
|
|
||||||
|
const dialogVisibleChangeName = ref(false)
|
||||||
|
const dialogVisibleChangeIntro = ref(false)
|
||||||
|
const dialogVisibleChangeAccount = ref(false)
|
||||||
|
const dialogVisibleChangePassword = ref(false)
|
||||||
|
const isCountingDown = ref(false);
|
||||||
|
const countdownTime = ref(60);
|
||||||
|
|
||||||
|
|
||||||
|
const newname = ref('')
|
||||||
|
const newintro = ref('')
|
||||||
|
|
||||||
|
const formRef = ref(null); // 表单引用
|
||||||
|
const formData = ref({
|
||||||
|
newaccount: '',
|
||||||
|
v_id: '',
|
||||||
|
code: '' // 初始化表单数据
|
||||||
|
});
|
||||||
|
const formRedPassword = ref(null); // 密码表单引用
|
||||||
|
const password = ref({
|
||||||
|
oldpassword: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
}); // 密码输入框
|
||||||
|
|
||||||
|
// 定义验证规则
|
||||||
|
const rules = {
|
||||||
|
newaccount: [
|
||||||
|
{ required: true, message: '请输入账号', trigger: 'blur' }, // 非空验证
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] } // 邮箱格式验证
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{ required: true, message: '请输入验证码', trigger: 'blur' }, // 非空验证
|
||||||
|
]
|
||||||
|
};
|
||||||
|
// 定义密码验证规则
|
||||||
|
const passwordRules = {
|
||||||
|
oldpassword: [
|
||||||
|
{ required: true, message: '请输入原密码', trigger: 'blur' }, // 非空验证
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' }, // 非空验证
|
||||||
|
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }, // 密码长度验证
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请确认密码', trigger: 'blur' }, // 非空验证
|
||||||
|
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }, // 密码长度验证
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//修改头像部分
|
||||||
|
//预览头像
|
||||||
|
const avatarPreview = ref(userinfo.user.u_avatar);
|
||||||
|
const avatar = ref(null); // 存储上传的头像文件
|
||||||
|
|
||||||
|
|
||||||
|
// 处理头像预览并将文件保存到变量
|
||||||
|
const handleAvatarChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
avatar.value = file;
|
||||||
|
avatarPreview.value = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//上传到服务器
|
||||||
|
const uploadAvatar = () => {
|
||||||
|
if(!avatar.value){
|
||||||
|
ElMessage('请选择头像')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const formdata = new FormData();
|
||||||
|
formdata.append('file', avatar.value)
|
||||||
|
formdata.append('id', userinfo.user.u_id)
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/avatar/upload',
|
||||||
|
data: formdata
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
ElMessage.error(response.data.msg || '上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getUserInfo()
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
ElMessage.error('上传失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const changeName = async () => {
|
||||||
|
|
||||||
|
const response = await axios.post('/api/user/updatename', {
|
||||||
|
u_name: newname.value
|
||||||
|
}
|
||||||
|
, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
userinfo.user.u_name = newname.value
|
||||||
|
newname.value = ''
|
||||||
|
dialogVisibleChangeName.value = false
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeIntro = async () => {
|
||||||
|
console.log(newintro.value);
|
||||||
|
const response = await axios.post('/api/user/updateintroduction', {
|
||||||
|
u_introduction: newintro.value
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': userinfo.token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
userinfo.user.u_introduction = newintro.value
|
||||||
|
newintro.value = ''
|
||||||
|
dialogVisibleChangeIntro.value = false
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendVerificationCode = () => {
|
||||||
|
formRef.value.validateField('newaccount', (isValid) => {
|
||||||
|
console.log(isValid);
|
||||||
|
if (isValid) {
|
||||||
|
// 如果邮箱验证通过
|
||||||
|
ElMessage.success('邮箱格式正确,正在发送验证码...');
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: "post",
|
||||||
|
url: "/api/code/sendcode",
|
||||||
|
data: JSON.stringify({ u_account: formData.value.newaccount })
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data && response.data.code !== 200) {
|
||||||
|
ElMessageBox.alert("注册失败,请重新填写 msg:" + (response.data.msg || '未知错误'), '注册失败');
|
||||||
|
} else {
|
||||||
|
startCountdown();
|
||||||
|
formData.value.v_id = response.data.data;
|
||||||
|
console.log(formData.value.v_id);
|
||||||
|
ElMessageBox.alert('发送成功,请耐心等待', '请求成功');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("请求失败:", error);
|
||||||
|
ElMessageBox.alert("请求失败,请稍后再试", '网络错误');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果邮箱验证失败
|
||||||
|
ElMessage.error('请输入有效的邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const startCountdown = () => {
|
||||||
|
isCountingDown.value = true;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
countdownTime.value--;
|
||||||
|
if (countdownTime.value <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
isCountingDown.value = false;
|
||||||
|
countdownTime.value = 60;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 验证验证码,并进行邮箱更改
|
||||||
|
const changeAccount = async () => {
|
||||||
|
|
||||||
|
// 验证新邮箱字段
|
||||||
|
formRef.value.validateField(['newaccount', 'code'], (errorMessage) => {
|
||||||
|
console.log(errorMessage);
|
||||||
|
if (errorMessage) {
|
||||||
|
// 如果验证通过(errorMessage为空)
|
||||||
|
ElMessage.success('正在修改邮箱...');
|
||||||
|
// 验证验证码
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: "post",
|
||||||
|
url: "/api/code/verifycode",
|
||||||
|
data: {
|
||||||
|
v_id: formData.value.v_id,
|
||||||
|
code: formData.value.code
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
ElMessage.error(response.data.msg || '验证码错误');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 验证码验证通过,发起修改邮箱的请求
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: "post",
|
||||||
|
url: "/api/user/updateaccount",
|
||||||
|
data: {
|
||||||
|
u_account: formData.value.newaccount
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
ElMessage.error(response.data.msg || '修改失败');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ElMessage.success('邮箱修改成功');
|
||||||
|
userinfo.user.u_account = formData.value.newaccount; // 更新当前邮箱
|
||||||
|
formRef.value.resetFields(); // 重置表单数据
|
||||||
|
formRef.value.clearValidate(); // 清除验证错误
|
||||||
|
dialogVisibleChangeAccount.value = false; // 关闭弹窗
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果验证失败(errorMessage不为空)
|
||||||
|
ElMessage.error('请输入正确的验证码');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
const changePassword = async () => {
|
||||||
|
// 验证密码字段
|
||||||
|
formRedPassword.value.validateField(['oldpassword', 'password', 'confirmPassword'], (errorMessage) => {
|
||||||
|
console.log(errorMessage);
|
||||||
|
if (password.value.password !== password.value.confirmPassword) {
|
||||||
|
ElMessage.error('两次密码输入不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.value.password === password.value.oldpassword) {
|
||||||
|
ElMessage.error('密码一样你改密码呢');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMessage) {
|
||||||
|
// 如果验证通过(errorMessage为空)
|
||||||
|
ElMessage.success('正在修改密码...');
|
||||||
|
// 发送修改密码请求
|
||||||
|
axios({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': userinfo.token
|
||||||
|
},
|
||||||
|
method: "post",
|
||||||
|
url: "/api/user/updatepassword",
|
||||||
|
data: password.value
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
ElMessage.error(response.data.msg || '修改失败');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ElMessage.success('密码修改成功');
|
||||||
|
formRedPassword.value.resetFields(); // 重置表单数据
|
||||||
|
formRedPassword.value.clearValidate(); // 清除验证错误
|
||||||
|
dialogVisibleChangePassword.value = false; // 关闭弹窗
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果验证失败(errorMessage不为空)
|
||||||
|
ElMessage.error('请输入正确的密码');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.updateProfile {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
top: 50px;
|
||||||
|
left: 100px;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.re-profile {
|
||||||
|
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-row {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codebutton {
|
||||||
|
width: 75px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codebutton:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codebutton:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
vite.config.js
Normal file
59
vite.config.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/src',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080', // 后端服务器地址
|
||||||
|
changeOrigin: true, // 允许跨域
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,去掉 /api 前缀
|
||||||
|
},
|
||||||
|
'/proxy': {
|
||||||
|
target: 'http://localhost:3000', // 代理服务器的地址
|
||||||
|
changeOrigin: true, // 必须设置为 true,才能避免跨域问题
|
||||||
|
// rewrite: (path) => path.replace(/^\/proxy/, ''), // 重写路径,去掉 /api 前缀
|
||||||
|
|
||||||
|
},
|
||||||
|
'/online': {
|
||||||
|
target: 'ws://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
'/voice': {
|
||||||
|
target: 'ws://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// server: {
|
||||||
|
// https:{
|
||||||
|
// key: fs.readFileSync('./cert/merlin.xin.key'),
|
||||||
|
// cert: fs.readFileSync('./cert/merlin.xin.pem'),
|
||||||
|
// },
|
||||||
|
// proxy: {
|
||||||
|
// '/api': {
|
||||||
|
// target: 'https://localhost:8443', // 后端服务器地址
|
||||||
|
// changeOrigin: true, // 允许跨域
|
||||||
|
// rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,去掉 /api 前缀
|
||||||
|
// },
|
||||||
|
// '/online':{
|
||||||
|
// target:'wss://localhost:8443/online',
|
||||||
|
// changeOrigin:true,
|
||||||
|
// ws:true,
|
||||||
|
// },
|
||||||
|
// '/voice':{
|
||||||
|
// target:'wss://localhost:8443/voice',
|
||||||
|
// changeOrigin:true,
|
||||||
|
// ws:true,
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user