在线体验地址
需要用邮箱注册一个账号
在线链接
目前实现的功能
1、在线聊天(群聊)
2、实时监控成员状态
3、历史聊天,下拉加载
4、有新消息,自动滚动到最新消息,如果自己在查看历史记录,不会强行滚动
后续计划新增功能
感兴趣的可以先关注下,后续空了会挨个实现
- 聊天室基本信息设置,成员列表与状态,消息展示优化
- 撤回消息与未读提醒
- 发送消息类型丰富,表情与文字混合发送, 图片与文件发送
- 直接录制语音与发送
- 在线语音
- 在线视频
- 好友系统(单聊)
exprees部分
创建几个表来记录相关数据
- 下面这些都不是必须的,如果你只是生成一个临时聊天室,这些表也可以直接用个对象直接保存
AuthorInfo 成员详细信息
ImRoom 聊天房间
ImRoomSys 聊天信息
ImRoomMember 聊天成员
简单启动一个服务器,并创建基本的事件监听
socket.js
const express = require("express");
const app = express(); //创建网站服务器
const server = require("http").createServer(app);
const io = require('socket.io')(server,{ cors: true });
module.exports = {
app,
io,
express,
server,
};
具体的事件
const { io } = require("../../tool/socket.js");
const { AuthorInfo } = require("../../mod/author/author_info");
const { ImRoom } = require("../../mod/game/im_room.js");
const { ImRoomSys } = require("../../mod/game/im_room_sys.js");
const { ImRoomMember } = require("../../mod/game/im_room_member.js");
const { Game } = require("../../mod/game/game.js");
const { GameList } = require("../../mod/game/game_list.js");
let allSocket = {};
// 监听客户端的连接
io.on("connection", function (socket) {
allSocket[socket.id] = socket;
// 监听用户掉线
socket.on("disconnect", async () => {
// 更新用户状态
let user = await ImRoomMember.findOneAndUpdate(
{ socket_id: socket.id },
{ status: "2" }
);
if (user) {
delete allSocket[user.im_room_id];
// 向房间的用户同步信息
sendMsgToRoom(user.im_room_id);
}
});
// 监听加入房间
socket.on("join_room", async (data) => {
if (!global.isObject(data)) {
resMsg("加入房间参数错误", 400);
return;
}
let { user_id, room_id } = data;
if (!user_id) {
resMsg("用户id不能为空", 400);
return;
}
let user = await AuthorInfo.findOne({ _id: user_id });
if (!user) {
resMsg("用户不存在", 400);
return;
}
if (!room_id) {
resMsg("房间id不能为空", 400);
return;
}
let room = await ImRoom.findOne({ _id: room_id, status: "1" });
if (!room) {
resMsg("房间不存在", 400);
return;
}
let { max, status } = room;
if (+status !== 1) {
resMsg("房间未开放", 300);
return;
}
// 查找所有加入该房间,并且状态为在线的用户
let members = await ImRoomMember.find({
im_room_id: room_id,
status: 1,
}).countDocuments();
if (members >= max) {
resMsg("房间已满", 300);
return;
}
// 查找用户是否yin'jin
let oldUser = await ImRoomMember.findOne({
im_room_id: room_id,
author_id: user_id,
});
if (!oldUser) {
let res = await new ImRoomMember({
im_room_id: room_id,
author_id: user_id,
author_type: 2,
created_time: getCurrentTimer(),
updated_time: getCurrentTimer(),
status: 1,
socket_id: socket.id,
}).save();
if (!res) {
resMsg("加入房间失败", 400);
return;
}
} else {
await ImRoomMember.updateOne(
{ im_room_id: room_id, author_id: user_id },
{ socket_id: socket.id, status: 1 }
);
}
// 房间信息改变,向房间内所有在线用户推送房间信息
sendMsgToRoom(room_id);
});
// 主动推出登录
socket.on("live_room", async (data) => {
let { room_id, user_id } = data;
// 更新用户状态
let user = await ImRoomMember.findOneAndUpdate(
{ im_room_id: room_id, author_id: user_id },
{ status: "2" }
);
if (user) {
delete allSocket[user.socket_id];
// 向房间的用户同步信息
sendMsgToRoom(room_id);
}
});
// 发送消息
socket.on("send_msg", async (data) => {
if (!global.isObject(data)) return;
let { room_id, author_id, content } = data;
// 判断用户是否存在
if (!author_id) {
resMsg("用户id不能为空", 400);
return;
}
let user = await AuthorInfo.findOne({ _id: author_id });
if (!user) {
resMsg("用户id不能为空", 400);
return;
}
// 判断房间是否存在
if (!room_id) {
resMsg("房间id不能为空", 400);
return;
}
let room = await ImRoom({ _id: room_id, status: "1" });
if (!room) {
resMsg("房间未开放", 400);
return;
}
if (!content) {
resMsg("消息内容不能为空", 400);
return;
}
// 保存消息
let params = {
im_room_id: room_id,
author_id: author_id,
content: content,
created_time: getCurrentTimer(),
updated_time: getCurrentTimer(),
};
let room_sys = await new ImRoomSys(params).save();
if (!room_sys) {
resMsg("保存消息失败", 400);
return;
}
// 找出对应的成员信息
let userinfo = await AuthorInfo.findOne(
{ _id: author_id },
{
username: 1,
header_img: 1,
}
);
if (!userinfo) {
resMsg("用户信息不存在", 400);
return;
}
room_sys.author_id = userinfo;
sendMsgToRoom(room_id, room_sys);
});
// 向一个房间内的所有在线用户推送房间的基本信息
async function sendMsgToRoom(room_id, row = null) {
if (!room_id) return;
let members = await ImRoomMember.find(
{
im_room_id: room_id,
status: 1,
},
{ socket_id: 1 }
);
if (!members || members.length === 0) return;
let sockets = members.map((item) => item.socket_id);
// 查出房间的基本信息
// 额外查在线人数
let room = (await ImRoom.findOne({ _id: room_id, status: "1" })) || {};
let roomMembers = await ImRoomMember.find(
{ im_room_id: room_id },
{ author_id: 1, status: 1 }
)
.populate("author_id", "username")
.exec();
// 查找出当前房间的总消息数
let roomSysCount = await ImRoomSys.find({im_room_id: room_id}).countDocuments() || 0
sockets.forEach((item) => {
let socket = allSocket[item];
let res = {
data: room,
roomMembers,
roomSysCount,
msg: "房间信息已更新",
};
if (global.isObject(row)) {
res.content = row;
}
if (socket) {
resMsg(res, 200, "room_baseinfo", socket);
}
});
}
// 获取当前时间戳
function getCurrentTimer() {
return Date.now();
}
// 统一返回消息
function resMsg(msg, code = 400, name = "err", _socket) {
let obj = {
code,
};
if (code === 200) {
obj.msg = "操作成功";
obj.data = msg;
} else {
obj.msg = msg;
}
socket = _socket ? _socket : socket;
socket.emit(name, obj);
}
});
前端部分
需要用到下面这个插件
https://cdn.socket.io/3.1.2/socket.io.js
具体代码
index.vue
<template>
<div class="flex-wrap">
<!-- 用户个人信息 -->
<leftUserInfo v-if="isShowLeft" />
<!-- 聊天列表 -->
<centerList v-if="isShowCenter" :chatRoomList="chatInfo.chatList" />
<!-- 具体聊天信息 -->
<rightChat
v-loading="isLoading"
ref="rightChatRef"
v-if="isShowRight"
:isLoading="isLoading"
:socket="socket"
:chatInfo="chatInfo"
@loadPrev="loadPrev"
@postComment="postComment"
/>
</div>
</template>
<script>
import { baseURL } from '@/plugins/config.js'
import { get_room, get_chat_list } from '@/api/data.js'
const plugins = [
{
js: 'https://cdn.socket.io/3.1.2/socket.io.js',
},
]
import leftUserInfo from '@/views/blog/im/leftUserInfo.vue'
import centerList from '@/views/blog/im/centerList.vue'
import rightChat from '@/views/blog/im/rightChat.vue'
export default {
components: {
leftUserInfo,
centerList,
rightChat,
},
props: {
isShowLeft: {
type: Boolean,
default: true,
},
isShowCenter: {
type: Boolean,
default: false,
},
isShowRight: {
type: Boolean,
default: true,
},
},
data() {
return {
isUserActive:false,
isLoadMore:false,
isLoading: true,
socket: {},
chatInfo: {
roomInfo: {
roomMembers: [],
im_name: '',
},
chatList: [],
},
pages: {
page: 1,
limit: 10,
total: 0,
},
}
},
computed: {
...Vuex.mapState(['userdata', 'userTags']),
...Vuex.mapGetters(['isAdmin']),
islogin() {
return this.isLogin()
},
room_id() {
return this.chatInfo?.roomInfo?._id || ''
},
baseParams() {
return {
user_id: this.userdata._id,
room_id: this.room_id,
}
},
},
created() {
this.getIndexDBJS(plugins).finally((res) => {
this.init()
})
},
beforeDestroy() {
this.delPageScript(plugins)
this.socket.emit('live_room', this.baseParams)
},
methods: {
// 主动发送消息
postComment(val = false){
this.isUserActive = val
},
// 拉取上一页
loadPrev() {
let {room_id} = this
if(this.isLoadMore) return
let { page, limit, total } = this.pages
if (this.chatInfo.chatList.length >= total) return
// 找出第一条的id
let chat_id = this.chatInfo.chatList[0]?._id || ''
if (!chat_id) return
this.isLoadMore = true
get_chat_list({ room_id,chat_id,limit }).then(res=>{
if (res.data && this.isArrayLength(res.data.data)) {
let { data } = res.data
this.chatInfo.chatList = [...data.reverse(),...this.chatInfo.chatList]
this.$refs.rightChatRef.reloadScrollPostion()
}
}).catch(()=>{}).finally(()=>{
this.isLoadMore = false
})
},
scrollToBottom() {
let { rightChatRef } = this.$refs
if (rightChatRef) {
rightChatRef.scrollToBottom(this.isUserActive)
}
},
async init() {
// 获取房间信息
let res = await get_room({ type: 1 }).catch(() => {})
if (!res.data && !this.isArrayLength(res.data.data)) return
Object.assign(this.chatInfo.roomInfo, { _id: res.data.data[0]._id })
let { room_id } = this
if (!room_id) return
// 拉取最近的10条聊天记录
let syss = await get_chat_list({ room_id })
if (syss.data && this.isArrayLength(syss.data.data)) {
// 倒序
this.chatInfo.chatList = syss.data.data.reverse()
}
// 连接io
this.socket = io.connect(baseURL)
// 加入房间
this.socket.emit('join_room', { room_id, user_id: this.userdata._id })
// 监听错误事件
this.socket.on('err', (err) => {
console.log('err', err)
})
// 监听房间基本信息
this.socket.on('room_baseinfo', (res) => {
console.log('room_baseinfo', res.data)
if (res.data) {
let { data, content, roomMembers,roomSysCount } = res.data
Object.assign(this.chatInfo.roomInfo, data, {
roomMembers,
})
this.pages.total = roomSysCount
if (content) {
this.chatInfo.chatList.push(content)
}
// 将房间消息滚动到底部
this.scrollToBottom()
this.isLoading = false
}
})
},
},
}
</script>
<style lang="scss" scoped></style>
rightChat.vue
<template>
<div class="flex-1 flex-column-wrap">
<!-- 聊天室信息 -->
<div class="flex-justify-between flex-wrap flex-center-wrap h-80 p-l-20 p-r-20 b-b-1">
<div>
{{ chatInfo.roomInfo.im_name || '默认聊天室' }}
<span v-if="onLine"> ({{ onLine }}) </span>
</div>
<i class="el-icon-s-tools f-24"></i>
</div>
<!-- 聊天信息列表 -->
<div
:class="[
'room-container p-l-20 p-r-20 b-b-1 p-t-10 p-b-10',
pageClass,
isLoading ? 'op-0' : '',
]"
@scroll="scrollEvent"
>
<template v-if="chatInfo.chatList.length">
<chatItem
:class="[index !== 0 ? 'm-t-20' : '']"
v-for="(item, index) in chatInfo.chatList"
:key="item._id"
:row="item"
:roomMembers="chatInfo.roomInfo.roomMembers"
></chatItem
></template>
<div v-else>暂无</div>
<div class="tip-new" v-if="false">有新消息</div>
</div>
<!-- 底部发送消息区域 -->
<div class="im-send-continer h-100 p-l-20 flex-center-wrap p-r-20">
<kl-emoji ref="pushCommentRef" type="2" @postComment="postComment" />
</div>
</div>
</template>
<script>
import chatItem from '@/views/blog/im/chatItem.vue'
export default {
components: {
chatItem,
},
props: {
isLoading: {
type: Boolean,
default: true,
},
socket: {
type: Object,
default: () => {
return {}
},
},
chatInfo: {
type: Object,
default: () => {
return {
roomInfo: {
roomMembers: [],
},
chatList: [],
}
},
},
},
data() {
return {
pageClass: this.createId(),
isBottom: true,
}
},
computed: {
...Vuex.mapState(['userdata']),
onLine() {
let count = 0
this.chatInfo.roomInfo.roomMembers.forEach((item) => {
if (item.status == 1) {
count++
}
})
return count
},
},
methods: {
// 重新定位
async reloadScrollPostion() {
let el = document.querySelector(`.${this.pageClass}`)
if (el) {
let oldHeight = el.scrollHeight
await this.$nextTick()
let newHeight = document.querySelector(`.${this.pageClass}`).scrollHeight
// 计算出滚动的高度
let scrollHeight = newHeight - oldHeight
// 滚动到原来的位置
el.scrollTop = scrollHeight
}
},
// 监听滚动,判断用户是否在底部
scrollEvent(e) {
let el = $(`.${this.pageClass}`)
if (el) {
this.isBottom = el.scrollTop() + el.innerHeight() >= el[0].scrollHeight - 50
// 判断是否触顶,加载上一页
if (el.scrollTop() <= 50) {
this.$emit('loadPrev')
}
}
},
// 滚动规则: 1、第一次进入 2、新消息来之前,用户就是停在底部 3、用户主动发送了消息
async scrollToBottom(val) {
if (!this.isBottom && !val) return
await this.$nextTick()
let el = $(`.${this.pageClass}`)
if (el) {
el.scrollTop(el[0].scrollHeight)
}
this.$emit('postComment',false)
},
postComment(content) {
this.$emit('postComment',true)
this.socket.emit('send_msg', {
room_id: this.chatInfo.roomInfo._id,
author_id: this.userdata._id,
content,
})
},
},
}
</script>
<style lang="scss" scoped>
.room-container {
height: calc(100vh - 80px - 100px);
overflow-y: auto;
}
.b-b-1 {
border-bottom: 1px solid #aaa;
}
</style>
如果有道友有需要的也可以私聊我,我看到会回的