仿新版QQ的聊天小软件

news2024/10/24 9:17:54

仿新版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实现,或者别的消息推送方式实现

源码下载

在线聊天组件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2210949.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MQ快速入门【详细】个人笔记 讲解通俗易懂

1.同步通讯和异步通讯 同步通讯&#xff1a;如果举个例子来说&#xff0c;同步通讯就像是两个人在打电话&#xff0c;一方说的话&#xff0c;能够立马传给另一方&#xff0c;消息的时效性非常高&#xff0c;但是相对的&#xff0c;只能是给一个人通讯&#xff0c;如果这个时候&…

React (三) 创建安装脚手架,类组件与函数式组件;生命周期;父子通信props;插槽;非父子通信Context

文章目录 一、脚手架的创建与安装1. 认识脚手架2. 安装脚手架3. 创建react项目4. 项目结构 二、从0编写三、组件化开发1. 什么是组件化开发2. 类组件3. render函数4. 函数式组件 四、生命周期1. 挂载Mount2. 更新Update3. 卸载Unmount4. 不常用的生命周期 五、父子组件通信1. 父…

数据结构之队列(python)

华子目录 1.队列存储结构1.1队列基本介绍1.2队列的实现方式 2.顺序队列2.1顺序队列的介绍2.2顺序队列的简单实现2.3代码实现 3.链式队列和基本操作3.1链式队列数据入队3.2链式队列数据出队3.3队列的链式表示和实现 1.队列存储结构 1.1队列基本介绍 队列的两端都"开口&qu…

springcloud之服务集群注册与发现 Eureka

前言 1&#xff1a;对于能提供完整领域服务接口功能的RPC而言&#xff0c;例如&#xff1b;gRPC、Thrift、Dubbo等&#xff0c;服务的注册与发现都是核心功能中非常重要的一环&#xff0c;使得微服务得到统一管理。 2&#xff1a;在分布式领域中有个著名的CAP理论&#xff1b;…

Vue3 + Element plus 实现切换el-radio前二次确认

Vue3 Element plus 实现切换el-radio前二次确认 场景&#xff1a;点击切换el-radio之前判断当前内容是否有改变&#xff0c;如有改变弹窗提示切换el-radio将销毁操作&#xff0c;弹窗二次确认是否切换 问题&#xff1a; el-radio 没有提供类似于beforeUpdate这样的钩子去处理这…

探索极简计算的新边界:从Uxn虚拟机看未来编程生态

越来越多的开发者追求复杂度和功能性的极致,然而,有一个小众的编程社区选择了截然不同的道路——极简主义。Uxn虚拟机便是这一思潮的代表之一。它通过简洁的指令集和有限的硬件资源模拟,试图打造一种可以在多种设备上运行的便携性编程环境。 与主流的重型操作系统和复杂…

HTB:Legacy[WriteUP]

目录 连接至HTB服务器并启动靶机 1.How many TCP ports are open on Legacy? 2.What is the 2008 CVE ID for a vulnerability in SMB that allows for remote code execution? 3.What is the name of the Metasploit module that exploits CVE-2008-4250? 4.When expl…

VS+QT 自定义插件变成动态库加载及使用

一、前言 有个界面需要重复使用某个自定义的控件&#xff0c;希望自定义控件能够像动态库文件那样&#xff0c;添加引用lib就能使用&#xff0c;经过多次太坑后&#xff0c;总结如下 二、实现方式 ① 新建项目&#xff0c;选择"Qt Designer Custom Widget" 创建自定…

Springboot从入门到起飞-【day01】

个人主页→VON 收录专栏→Springboot从入门到起飞 一、前言 经过了近两个月的沉淀开始了新专栏的学习&#xff0c;经过深思熟虑还是决定重新学习java&#xff0c;因为基础部分东西太多太乱就不进行逐一的更新了&#xff0c;等到学完了一同进行更新。 二、Springboot简要概述 …

kafka消息队列核心内容及常见问题

目录 1. 使用消息队列的目的&#xff08;优点与缺点&#xff09; 2. 常见各消息队列对比 3. kafka介绍 3.1 kafka简介 3.2 kafka特点 3.3 kafka系统架构 3.4 设置数据可靠性 3.4.1 Topic 分区副本 3.4.2 消息确认机制 4. 常见问题&#xff08;面试题&#xff09; 4.…

Springboot 接入 WebSocket 实战

Springboot 接入 WebSocket 实战 前言&#xff1a; WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。 简单理解&#xff1a; 1&#xff0c;常见开发过程中我们知道 Http协议&#xff0c;客户端…

LED显示器闪烁故障原因及解决方法

随着电子显示屏在各行各业的广泛应用&#xff0c;LED显示器因其高亮度、节能和灵活的宣传功能&#xff0c;成为了宣传推广的重要工具。然而&#xff0c;LED显示器在使用过程中有时会出现闪烁的现象&#xff0c;这不仅影响了显示效果&#xff0c;还可能影响用户体验。针对这一问…

【layui】多文件上传组件实现

插件预览效果&#xff1a; 需要引入layui的脚本文件layui.js和样式文件layui.css html代码&#xff1a; <div class"layui-input-block"><div class"layui-upload-list"><table class"layui-table"><colgroup><col…

18936 手串

### 思路 1. **输入处理**&#xff1a;读取输入的n, m, c&#xff0c;以及每个串珠的颜色信息。 2. **颜色位置记录**&#xff1a;使用一个字典来记录每种颜色出现的位置。 3. **检查颜色分布**&#xff1a;遍历每种颜色&#xff0c;检查其在任意连续m个串珠中是否出现超过一次…

【Flask】Flask数据库

【Flask】Flask数据库 1.概述2.使用Flask-SQLAlchemy管理数据库3.定义模型4.关系5.数据库操作创建表插入行修改行删除行查询行 1.概述 大多数的数据库引擎都有对应的 Python 包&#xff0c;包括开源包和商业包。Flask 并不限制你使用何种类型的数据库包&#xff0c;因此可以根…

Java体系中的泛型

1. 泛型 一般的类和方法&#xff0c;只能够使用基本类型&#xff0c;要么是自定义的类&#xff0c;如果要编写可以应用于多种数据类型的代码&#xff0c;这种刻板的限制对代码的约束就会很大&#xff0c;那么如何实现可应用于多种数据类型的代码&#xff0c;而不局限于单一一种…

第5篇:DDOS病毒----应急响应之Linux实战篇

现象描述 某服务器网络资源异常,感染该木马病毒的服务器会占用网络带宽&#xff0c;甚至影响网络业务正常应用。 系统分析 针对日志服务器病毒事件排查情况&#xff1a; 在开机启动项/etc/rc.d/rc.local发现可疑的sh.sh脚本&#xff0c;进一步跟踪sh.sh脚本,这是一个检测病毒…

C++从入门到起飞之——AVL树 全方位剖析!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C从入门到起飞 &#x1f516;克心守己&#xff0c;律己则安 目录 1. AVL的概念 2. AVL树的实现 2.1 AVL树的结构 2.2 AVL树的插⼊ >AVL树插⼊⼀个值的⼤概过程 &…

Rocky linux 修改ip地址, rocky服务器修改静态地址, rocky虚拟机修改ip

1. 更新yum yum update 2. 安装ifconfig yum install net-tools 3. 修改配置 vi /etc/NetworkManager/system-connections/ens33.nmconnection 将ipv4内容修改如下&#xff1a; # 自动改为手动 methodmanual # 网关为vm ware 查看网关地址 address你想改为的ip/24,网关 #dns不…

Qml 分组动画(二) 动画嵌套(自学笔记)

分组动画嵌套示例&#xff0c;直接看效果&#xff0c; 做一个踢足球的示例 下面两个Rectangle 制作渐变的天空和大地 下面这个Rectangle 用于放置足球图片&#xff0c; 由于足球图片直接从网上下载的 没有找到合适大小的图片 &#xff0c;所以用 一个矩形框作限制&#xff0c;…