###########################################################################################################################################################
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
app.js
const express = require("express");
const app = express();
const { Server } = require("socket.io");
const io = new Server(3000, {
cors: {
origin: ["http://192.168.1.92:8080"],
},
});
const userList = []; // 私聊数据表
const rooms = {}; // 群聊房间列表
io.on("connection", (socket) => {
// 多人聊天精简版
// 开始监听前端的socket请求连接(前端每次执行一次io()方法就就会发起一次socket请求)
console.log("开始了");
// 接收客户端发来的数据
socket.on("joinRoom", (data) => {
const { username, room } = JSON.parse(data);
socket.join(room);
if (!rooms[room]) {
rooms[room] = [];
}
const user = { id: socket.id, username: username };
rooms[room].push(user);
console.log(`${username} joined room ${room}`);
const userList = rooms[room].map((user) => user.username);
io.to(room).emit("userList", JSON.stringify(userList));
});
socket.on("chat message", (data) => {
const obj = JSON.parse(data);
console.log("Received message:", obj.username, obj.mes);
// 将消息发送给房间内的其他用户
let roomUsers = rooms[obj.room];
if (!Array.isArray(roomUsers)) {
roomUsers = [];
}
roomUsers.forEach((user) => {
if (user.id !== socket.id) {
io.to(user.id).emit(
"receiveMessage",
JSON.stringify({ username: obj.username, mes: obj.mes })
);
}
});
});
// 私人聊天版本
// console.log('connection',socket.id);
const username = socket.handshake.query.username;
if (!username) return;
const userInfo = userList.find((user) => user.username === username);
if (userInfo) {
userInfo.id = socket.id;
} else {
userList.push({
id: socket.id,
username,
});
// 监听用户断开连接事件,将用户从用户列表中移除
socket.on("disconnect", () => {
const index = userList.findIndex((user) => user.id === socket.id);
if (index !== -1) {
userList.splice(index, 1);
}
//多人聊天断开socket请求
console.log("user disconnect --- 退出了了天", username);
io.emit("tuichu", {
msg: `用户${username}推出了聊天界面,选择某用户聊条吧...`,
});
Object.keys(rooms).forEach((room) => {
const index = rooms[room].findIndex((user) => user.id === socket.id);
if (index > -1) {
rooms[room].splice(index, 1);
const userList = rooms[room].map((user) => user.username);
io.to(room).emit("userList", JSON.stringify(userList));
}
});
});
}
// console.log(userList)
io.emit("online", {
userList,
});
socket.on("send", ({ fromUsername, targetId, msg }) => {
// console.log(fromUsername,targetId,msg);
// console.log(io.sockets.sockets.get(targetId));
const targetSocket = io.sockets.sockets.get(targetId);
const toUser = userList.find((user) => user.id === targetId);
if (targetSocket) {
targetSocket.emit("receive", {
fromUsername,
toUsername: toUser.username,
msg,
dateTime: new Date().getTime(),
});
} else {
console.log("找不到目标用户的 socket");
io.emit("duankai", {
msg: "可能你或(对方)刷新了页面找不到目标用户,请重新选择目标用户,在进行交谈",
});
}
});
});
app.listen(8000, (err) => {
console.log("服务器启动成功");
});
home.vue
<template>
<div class="home">
<h1 style="color: red;">欢迎来到银熊联盟聊天部</h1>
<button style="width: 200px; height: 40px;" @click="router.push('/about?username=q1')">用户01</button><br />
<hr />
<button style="width: 200px; height: 40px;" @click="router.push('/about?username=q2')">用户02</button><br />
<hr />
<button style="width: 200px; height: 40px;" @click="router.push('/about?username=q3')">用户03</button><br />
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<style>
.home {
text-align: center;
margin: auto;
}
</style>
about.vue
<template>
<div>
<h1 style="color: red;">欢迎来到银熊联盟聊天部</h1>
<h2 style="color: fuchsia;">有朋自远方来,不亦说乎!</h2>
<div class="box-liao">
<h3>以下是用户,点击和某个用户聊天,一对一私密聊天,不会被第三者极道</h3>
<ul>
<template v-for="userInfo of state.userList" :key="userInfo.id">
<li v-if="userInfo.username === state.username">
{{ userInfo.username }}
</li>
<li v-else>
<a href="javascript:;" @click="selectUser(userInfo)">{{ userInfo.username }}</a>
</li>
</template>
</ul>
</div>
<div v-if="state.targetUser">
<h3>你将要和{{ state.targetUser.username }}用户进行交流,请准备...</h3>
<input type="text" placeholder="说点什么吧 ..." v-model="state.msgText" @keyup.enter="sendMessage">
<button @click="sendMessage">send</button>
</div>
<div class="box-xx" ref="messageContainer">
<span style="color: blueviolet;font-size: 22px;">你是{{ state.username }}用户,请保持不变,感谢</span>
<ul>
<li v-for="(item, index) of messageList" :key="index"
:class="[item.fromUsername === state.username ? 'message-right' : 'message-left']">
<p><span style="color: red;font-size: 20px;;">{{ item.fromUsername }}</span> : {{
dayjs(item.dateTime).format('YYYY-MM-DD HH:mm:ss') }}</p>
<p>{{ item.msg }}</p>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { io } from "socket.io-client";
import { useRouter } from "vue-router";
import { reactive, computed, ref, nextTick } from "vue";
import dayjs from 'dayjs'
const router = useRouter()
console.log(router);
const state = reactive({
username: router.currentRoute.value.query.username,
userList: [],
targetUser: null,
msgText: '',
messageBox: {}
})
const messageContainer = ref(null);
const messageList = computed(() => {
return (state.messageBox[state.username] && state.targetUser) ?
state.messageBox[state.username].filter(item => {
return item.fromUsername === state.targetUser.username ||
item.toUsername === state.targetUser.username
}) : []
})
const socket = io("http://192.168.1.92:3000", {
query: {
username: state.username
}
})
const selectUser = (userInfo) => {
// console.log(userInfo);
state.targetUser = userInfo
console.log('userInfo', userInfo);
}
const sendMessage = () => {
// 如果msgText中没有数据就不会触发此事件机制
if (!state.msgText.length) return;
// 清除两端的空格
const message = state.msgText.trim();
// 判断两端是否有空格,并处理
if (message) {
appendMessage({
fromUsername: state.username,
toUsername: state.targetUser.username,
msg: state.msgText,
dateTime: new Date().getTime()
})
// 用户的个人信息
socket.emit('send', {
fromUsername: state.username,
targetId: state.targetUser.id,
msg: state.msgText
})
// 清空聊天框的信息
state.msgText = ''
// 滚动到底部
nextTick(() => {
scrollToBottom();
});
}
}
// 置底
const scrollToBottom = () => {
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
}
};
socket.on('online', (data) => {
console.log('online', data);
state.userList = data.userList
})
socket.on('receive', (data) => {
console.log('receive', data);
appendMessage(data)
// 滚动到底部
nextTick(() => {
scrollToBottom();
});
})
socket.on('duankai', (data) => {
console.log('duankai', data);
alert(data.msg)
// appendMessage(data)
})
socket.on('tuichu', (data) => {
console.log('tuichu', data);
alert(data.msg)
// appendMessage(data)
})
socket.on('error', (err) => {
console.log(err);
})
socket.on('disconnect', (data) => {
// 用户断开连接时执行的操作
socket.emit('disconnect'); // 向服务器发送断开连接的消息
});
function appendMessage(data) {
!state.messageBox[state.username] && (state.messageBox[state.username] = [])
state.messageBox[state.username].push(data)
}
</script>
<style scoped>
.message-left {
text-align: left;
background-color: #eaeaea;
padding: 5px;
margin-bottom: 10px;
}
.message-right {
text-align: right;
background-color: #eaeaea;
padding: 5px;
margin-bottom: 10px;
}
.box-xx {
height: 500px;
/* 设置容器高度 */
overflow-y: auto;
/* 启用垂直滚动 */
}
.box-liao ul {
list-style: none;
display: flex;
justify-content: flex-start;
}
.box-liao ul li {
padding: 10px;
vertical-align: middle;
display: flex;
justify-content: center;
align-items: center;
}
.box-liao ul li a {
text-decoration: none;
color: red;
font-size: 22px;
height: 30px;
line-height: 30px;
}
</style>
chat.vue
<script setup>
import { ref, onMounted, nextTick } from 'vue';
import io from 'socket.io-client';
const username = ref('');
const socket = io('http://192.168.1.92:3000');
const val = ref('');
const messages = ref([]);
const isUsernameConfirmed = ref(false);
const room = ref('');
const userList = ref([]);
const messageContainer = ref(null);
const confirmUsername = () => {
if (!username.value) {
alert('请输入用户名');
return;
}
isUsernameConfirmed.value = true;
};
const joinRoom = () => {
if (room.value.trim().length === 0) {
return; // 禁止加入空房间
}
if (!room.value) {
alert('请输入房间号');
return;
}
socket.emit('joinRoom', JSON.stringify({ username: username.value, room: room.value }));
};
const sendMessage = () => {
if (!val.value) {
alert('请输入消息内容');
return;
}
const obj = {
username: username.value,
mes: val.value,
room: room.value, // 将房间号也一起发送到服务器
};
socket.emit('chat message', JSON.stringify(obj));
messages.value.push({
username: username.value,
mes: val.value,
type: 'sent',
});
val.value = ''; // 重置输入框的值
// 滚动到底部
nextTick(() => {
scrollToBottom();
});
};
const scrollToBottom = () => {
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
}
};
onMounted(() => {
scrollToBottom();
socket.on('userList', (data) => {
console.log(data);
userList.value = JSON.parse(data);
});
socket.on('receiveMessage', function (data) {
const message = JSON.parse(data);
console.log(message);
if (message.username === username.value) {
// 不渲染自己发送的消息
return;
}
messages.value.push({
username: message.username,
mes: message.mes,
type: 'received',
});
// 滚动到底部
nextTick(() => {
scrollToBottom();
});
});
});
</script>
<template>
<div>
<div v-if="!isUsernameConfirmed">
<input v-model="username" placeholder="请输入聊天名称..." @keyup.enter="confirmUsername" />
<button @click="confirmUsername">确定</button>
</div>
<div v-else>
<input v-model="room" placeholder="请输入房间号" @keyup.enter="joinRoom" />
<button @click="joinRoom">加入房间</button>
<div>房间成员列表:</div>
<ul>
<li v-for="user in userList" :key="user">{{ user }}</li>
</ul>
<div class="message-container" ref="messageContainer">
<ul>
<li v-for="message in messages" :key="message.content"
:style="{ textAlign: message.type === 'sent' ? 'right' : 'left', color: message.type === 'sent' ? 'blue' : 'red' }">
<span v-if="message.type === 'received'" class="nick">{{ message.username }}:</span>
{{ message.mes }}
</li>
</ul>
</div>
<input id="m" autocomplete="off" v-model="val" @keyup.enter="sendMessage" :disabled="!room" /><button
@click="sendMessage">Send</button>
</div>
</div>
</template>
<style scoped>
ul#messages {
list-style-type: none;
margin: 0;
padding: 0;
}
li {
margin: 5px 0;
}
.nick {
font-weight: bold;
}
.message-container {
height: 300px;
/* 设置容器高度 */
overflow-y: auto;
/* 启用垂直滚动 */
}
</style>