仿新版QQ的聊天小软件
- 文章说明
- 核心源码
- 效果展示
- 源码下载
文章说明
新开一个聊天组件的项目的想法主要来源于想学习一下消息队列的使用,后来在书写界面和一些功能模块时,又想到可以抽离出来,分别写几篇文章,主要介绍扫码登陆、消息存储和展示、消息推送等;这篇文章主要是展示该聊天小软件的简单使用效果
目前该组件尚处于初步阶段,在传输效率、安全性方面没有进行很多涉及,所以仅作为学习使用
目前该软件主要包含以下功能:
1、添加好友
2、发送消息(文字消息、表情包、图片、文件)
3、消息的在线推送
目前待完善的几个模块如下:
1、扫码登陆(目前只是写了扫码登陆的界面,并没有设计安卓端的APP,进行匹配登录)
2、客户端消息存储(可以考虑将消息存储在客户端本地,如IndexDB中,但显然的,有一定的安全性问题;不过针对于图片、文件、表情包类型的聊天记录,备份一份存储在客户端是个不错的选择,可以较好的降低服务器压力)
核心源码
左侧消息列表展示代码
<script setup>
import {defineExpose, defineProps, onBeforeMount, reactive} from "vue";
import {fillImagePrefix, formatDate, getRequest, message} from "@/util";
const props = defineProps({
setReceiverUserInfo: {
type: Function,
required: true,
},
});
const data = reactive({
chatList: [],
originChatList: [],
currentChatId: 0,
});
onBeforeMount(() => {
getRelList();
});
function getRelList() {
const user = JSON.parse(localStorage.getItem("user"));
getRequest("/rel/list", {
userId: user.userId,
}).then(async (res) => {
if (res.data.code === 200) {
const relList = res.data.data;
for (let i = 0; i < relList.length; i++) {
const chatItem = {
id: i + 1,
username: "",
nickname: "",
avatarUrl: "",
time: "",
content: "",
friendUserId: relList[i].friendUserId
};
await getUserInfo(chatItem, relList[i]);
await getChatLogInfo(chatItem, relList[i]);
data.chatList.push(chatItem);
data.originChatList.push(chatItem);
}
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
});
}
async function getChatLogInfo(chatItem, relItem) {
const user = JSON.parse(localStorage.getItem("user"));
const res = await getRequest("/chat-log/getLastChatLog", {
userId: user.userId,
friendUserId: relItem.friendUserId,
});
if (res.data.code === 200) {
if (res.data.data) {
chatItem.time = formatDate(new Date(res.data.data["createTime"]));
const chatLogId = res.data.data.chatLogId;
await getChatLogContentInfo(chatItem, chatLogId);
}
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
}
async function getChatLogContentInfo(chatItem, chatLogId) {
const res = await getRequest("/chat-log-content/getContent", {
chatLogId: chatLogId,
});
if (res.data.code === 200) {
chatItem.content = res.data.data.chatLogContent;
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
}
async function getUserInfo(chatItem, relItem) {
const res = await getRequest("/user/getUserInfo", {
userId: relItem["friendUserId"],
});
if (res.data.code === 200) {
chatItem.username = res.data.data.username;
chatItem.nickname = res.data.data.nickname;
chatItem.avatarUrl = fillImagePrefix(res.data.data.avatarUrl);
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
}
function setFriendUserId(item) {
data.currentChatId = item.id;
props.setReceiverUserInfo(item);
}
function filterChatList(searchNickname, receiverUserId) {
data.chatList = [];
for (let i = 0; i < data.originChatList.length; i++) {
if (data.originChatList[i].nickname.indexOf(searchNickname) > -1) {
data.chatList.push(data.originChatList[i]);
}
}
for (let i = 0; i < data.chatList.length; i++) {
if (data.chatList[i].friendUserId === receiverUserId) {
return true;
}
}
data.currentChatId = 0;
return false;
}
function addChatItem(newChatItem) {
newChatItem.id = data.originChatList.length + 1;
newChatItem.avatarUrl = fillImagePrefix(newChatItem.avatarUrl);
data.chatList.push(newChatItem);
data.originChatList.push(newChatItem);
}
defineExpose({
getChatLogInfo,
filterChatList,
addChatItem
});
</script>
<template>
<div class="container">
<template v-for="item in data.chatList" :key="item.id">
<div :class="data.currentChatId === item.id ? ' active-chat' : ''" class="chat-item"
@dblclick="setFriendUserId(item)">
<div style="display: flex; padding: 10px; height: 70px; align-items: center">
<div style="margin-right: 10px; display: flex; align-items: center; justify-content: center">
<img :src="item.avatarUrl" alt=""/>
</div>
<div style="flex: 1; overflow: hidden; margin-top: -5px">
<div style="display: flex; align-items: center">
<p class="nickname">{{ item.nickname }}</p>
<p class="time">{{ item.time }}</p>
</div>
<div class="content">{{ item.content }}</div>
</div>
</div>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
background-color: #ffffff;
.chat-item {
cursor: default;
user-select: none;
&:hover {
background-color: #f5f5f5;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
}
.nickname {
flex: 1;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.time {
width: fit-content;
font-size: 10px;
color: #a39999;
margin-left: 5px;
}
.content {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 12px;
color: #a39999;
margin-top: 5px;
}
}
.active-chat {
background-color: #ccebff;
&:hover {
background-color: #ccebff;
}
}
}
</style>
消息输入区域代码
<script setup>
import {defineProps, reactive} from "vue";
import {message, postRequest, TEXT} from "@/util";
const props = defineProps({
receiverUserId: {
type: Number,
required: true,
},
updateStatusAfterSendMessage: {
type: Function,
required: true,
}
});
const data = reactive({
text: "",
});
function sendText() {
const user = JSON.parse(localStorage.getItem("user"));
postRequest("/chat-log/send", null, {
chatLogType: TEXT,
senderUserId: user.userId,
receiverUserId: props.receiverUserId,
}).then((res) => {
if (res.data.code === 200) {
const chatLog = res.data.data;
saveContent(chatLog.chatLogId);
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
});
}
function saveContent(chatLogId) {
postRequest("/chat-log-content/saveContent", null, {
chatLogId: chatLogId,
chatLogContent: data.text,
}).then((res) => {
if (res.data.code === 200) {
const chatLogItem = {
chatLogId: chatLogId,
chatLogType: TEXT,
chatLogContent: data.text,
};
props.updateStatusAfterSendMessage(chatLogItem);
document.getElementById("text").textContent = "";
data.text = "";
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
});
}
function getContent(e) {
data.text = e.target.textContent;
}
</script>
<template>
<div class="input-container">
<div class="tool-container">
<div class="tool-item">
<i class="iconfont icon-emoji"></i>
</div>
<div class="tool-item">
<i class="iconfont icon-file"></i>
</div>
<div class="tool-item">
<i class="iconfont icon-image"></i>
</div>
</div>
<div class="input-elem-container">
<div contenteditable="true" @input="getContent($event)" id="text"></div>
</div>
<div class="send-btn-container">
<button @click="sendText">发送</button>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "@/css/iconfont.css";
.input-container {
width: 100%;
height: 100%;
background-color: transparent;
border-top: 1px solid #dbe4f5;
display: flex;
flex-direction: column;
.tool-container {
height: 30px;
width: 100%;
.tool-item {
float: left;
margin: 3px 10px;
.iconfont::before {
font-size: 24px;
}
.iconfont:hover {
color: crimson;
cursor: default;
}
}
}
.input-elem-container {
flex: 1;
width: 100%;
overflow: auto;
&::-webkit-scrollbar {
height: 6px;
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #d0d5db;
border-radius: 6px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
div {
border: none;
outline: none;
background-color: transparent;
font-size: 16px;
word-break: break-all;
padding-left: 10px;
width: 100%;
height: 100%;
}
}
.send-btn-container {
height: 50px;
width: 100%;
position: relative;
button {
width: fit-content;
height: 30px;
line-height: 30px;
text-align: center;
border: none;
outline: none;
background-color: #0099ff;
color: white;
padding: 0 15px;
border-radius: 5px;
position: absolute;
bottom: 10px;
right: 20px;
font-size: 14px;
&:hover {
background-color: #0093f5;
}
&:active {
background-color: #0086e0;
}
}
}
}
</style>
消息列表展示代码
<script setup>
import {defineExpose, defineProps, onBeforeMount, reactive, watch} from "vue";
import {fillImagePrefix, formatDate, getRequest, message} from "@/util";
const props = defineProps({
receiverUserInfo: {
type: Object,
required: true,
},
});
const data = reactive({
chatLogList: [],
});
onBeforeMount(() => {
getChatLogList();
});
function getChatLogList() {
const user = JSON.parse(localStorage.getItem("user"));
getRequest("/chat-log/getChatLogList", {
userId: user.userId,
friendUserId: props.receiverUserInfo.userId,
}).then(async (res) => {
if (res.data.code === 200) {
data.chatLogList = [];
const chatLogList = res.data.data;
for (let i = 0; i < chatLogList.length; i++) {
chatLogList[i].createTime = formatDate(new Date(chatLogList[i].createTime));
data.chatLogList.push(chatLogList[i]);
}
for (let i = 0; i < data.chatLogList.length; i++) {
if (data.chatLogList[i].self) {
data.chatLogList[i].avatarUrl = fillImagePrefix(user.avatarUrl);
} else {
data.chatLogList[i].avatarUrl = props.receiverUserInfo.avatarUrl;
}
data.chatLogList[i].chatLogContent = await getChatLogContentInfo(data.chatLogList[i].chatLogId);
}
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
});
}
async function getChatLogContentInfo(chatLogId) {
const res = await getRequest("/chat-log-content/getContent", {
chatLogId: chatLogId,
});
if (res.data.code === 200) {
return res.data.data.chatLogContent;
} else if (res.data.code === 500) {
message(res.data.msg, "error");
}
return "";
}
watch(() => props.receiverUserInfo.userId, () => {
getChatLogList();
});
function updateChatLogList(chatLogItem) {
const user = JSON.parse(localStorage.getItem("user"));
data.chatLogList.push({
chatLogId: chatLogItem.chatLogId,
chatLogType: chatLogItem.chatLogType,
chatLogContent: chatLogItem.chatLogContent,
senderUserId: user.userId,
receiverUserId: props.receiverUserInfo.userId,
createTime: formatDate(new Date()),
self: true,
avatarUrl: fillImagePrefix(user.avatarUrl)
});
}
defineExpose({
updateChatLogList,
});
</script>
<template>
<div class="chat-log-container">
<div class="chat-log-list">
<template v-for="item in data.chatLogList" :key="item.chatLogId">
<div class="chat-log-item" v-if="!item.self">
<img alt="" :src="item.avatarUrl"/>
<span>{{ item.chatLogContent }}</span>
</div>
<div class="self-chat-log-item" v-if="item.self">
<span>{{ item.chatLogContent }}</span>
<img alt="" :src="item.avatarUrl"/>
</div>
</template>
</div>
</div>
</template>
<style lang="scss" scoped>
.chat-log-container {
width: 100%;
height: 100%;
background-color: transparent;
.chat-log-list {
font-size: 14px;
.chat-log-item, .self-chat-log-item {
width: 100%;
min-height: 30px;
padding: 10px;
display: flex;
img {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 10px;
}
span {
display: inline-flex;
align-items: center;
background-color: #ffffff;
border-radius: 10px;
color: #000000;
max-width: calc(100% - 150px);
padding: 10px;
line-height: 1.8;
}
}
.self-chat-log-item {
justify-content: right;
span {
background-color: #0099ff;
color: #ffffff;
}
}
}
}
</style>
效果展示
登录页面
注册页面
扫码登陆页面(暂未完成手机端的扫码登陆模块)
已登录页面
添加好友页面
聊天消息列表
还有不少功能未完善,等后续逐步完善,包括:发送图片、表情包、文件消息,以及此类消息的展示,还未添加个人信息页面,还有聊天消息的上拉懒加载功能还未完善,以及消息推送功能还未完善,后续可考虑采用web-socket实现,或者别的消息推送方式实现
源码下载
在线聊天组件