首页 > 其他分享 >Vue3在线聊天室

Vue3在线聊天室

时间:2025-01-16 21:04:35浏览次数:3  
标签:hhr 聊天室 在线 Result Vue3 import com public uid

更多开源项目请关注我的gitee:乌鸦像写字台(关注公众号:寻川的AI工具库 免费得毕设必备软件以及详细项目运行文档) (he-haoran-hhh) - Gitee.com

在Layout.vue中添加菜单选项

<el-menu-item index="/home">首页</el-menu-item>
<el-menu-item index="/im">天农聊天室</el-menu-item>

在index.js中添加子路由

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'Layout',
      component: () => import('../layout/Layout.vue'),
      redirect:'/home', //保证一开始输入/页面就直接重定向到home页面
      children:[
        {
          path: 'home',
          name: 'Home',
          component: () => import('../views/HomeView.vue'),
        },
        {
          path: 'im',
          name: 'Im',
          component: () => import('../views/Im.vue'),
        },
        {
          path: 'personCenter',
          name: 'PersonCenter',
          component: () => import('../views/PersonCenter.vue')
        }
      ]
    }
  ]
})

创建Im.vue组件并写出主要格式

<template>
  <div class="im_main_box">
    聊天室
  </div>
</template>

<script setup>

</script>

<style>
  .im_main_box{
    width: 100%;
    background-color: white;
    margin: 10px auto;
    min-height: 100px;
  }
</style>

转到后端

websocket依赖

<!--webSocket-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

websocket 后端配置

WebSocketConfig.java
package com.hhr.friendback.common;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author: 何浩然
 * @date: 2023 - 02 - 20 09:56
 **/
@Configuration
@EnableWebSocket
public class WebSocketConfig {
    /**
     * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

Service层中的ServiceImpl(服务实现)

WebSocketServer.java
package com.hhr.friendback.service.impl;

import cn.hutool.core.lang.Dict;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hhr.friendback.entity.Im;
import com.hhr.friendback.entity.User;
import com.hhr.friendback.service.IImService;
import com.hhr.friendback.service.IUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: 何浩然
 * @date: 2023 - 02 - 20 10:11
 **/
@ServerEndpoint(value = "/imserver/{uid}")
@Component
public class WebSocketServer {
    private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    /**
     * 记录当前在线连接数
     */
    public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();

    @Resource
    IUserService userService;
    @Resource
    IImService imService;


    private static IUserService staticUserService;
    private static IImService staticImService;

    // 程序初始化的时候触发这个方法  赋值
    @PostConstruct
    public void setStaticUser() {
        staticUserService = userService;
        staticImService = imService;
    }

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("uid") String uid) {
        sessionMap.put(uid, session);
        log.info("有新用户加入,uid={}, 当前在线人数为:{}", uid, sessionMap.size());
        Dict dict = Dict.create().set("nums", sessionMap.size());
        sendAllMessage(JSONUtil.toJsonStr(dict));  // 后台发送消息给所有的客户端
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session, @PathParam("uid") String uid) {
        sessionMap.remove(uid);
        log.info("有一连接关闭,uid={}的用户session, 当前在线人数为:{}", uid, sessionMap.size());
        Dict dict = Dict.create().set("nums", sessionMap.size());
        sendAllMessage(JSONUtil.toJsonStr(dict));  // 后台发送消息给所有的客户端
    }

    /**
     * 收到客户端消息后调用的方法
     * 后台收到客户端发送过来的消息
     * onMessage 是一个消息的中转站
     * 接受 浏览器端 socket.send 发送过来的 json数据
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session fromSession, @PathParam("uid") String uid) throws JsonProcessingException {
        log.info("服务端收到用户uid={}的消息:{}", uid, message);
        // 处理msg
        // 存储数据库
        // 添加创建时间
        if (staticUserService == null) {
            return;
        }
        User user = staticUserService.getOne(new QueryWrapper<User>().eq("uid", uid));
        if (user == null) {
            log.error("获取用户信息失败,uid={}", uid);
            return;
        }

        Im im = Im.builder().uid(uid).username(user.getName()).avatar(user.getAvatar()).sign(user.getSign())
                .createTime(LocalDateTime.now()).text(message).build();
        // 存储数据到数据库
        staticImService.save(im);
        String jsonStr = new ObjectMapper().writeValueAsString(im);  // 处理后的消息体
        this.sendAllMessage(jsonStr);
        log.info("发送消息:{}", jsonStr);
    }

    @OnError
    public void one rror(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 服务端发送消息给除了自己的其他客户端
     */
    private void sendMessage(Session fromSession, String message) {
        sessionMap.values().forEach(session -> {
            if (fromSession != session) {
                log.info("服务端给客户端[{}]发送消息{}", session.getId(), message);
                try {
                    session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    log.error("服务端发送消息给客户端异常", e);
                }
            }
        });
    }

    /**
     * 服务端发送消息给所有客户端
     */
    private void sendAllMessage(String message) {
        try {
            for (Session session : sessionMap.values()) {
                log.info("服务端给客户端[{}]发送消息{}", session.getId(), message);
                session.getBasicRemote().sendText(message);
            }
        } catch (Exception e) {
            log.error("服务端发送消息给客户端失败", e);
        }
    }
}

消息存储到数据库中创建Im三层架构

ImController
package com.hhr.friendback.controller;

import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelWriter;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletOutputStream;
import java.net.URLEncoder;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.InputStream;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hhr.friendback.common.Result;
import org.springframework.web.multipart.MultipartFile;
import com.hhr.friendback.service.IImService;
import com.hhr.friendback.entity.Im;

import org.springframework.web.bind.annotation.RestController;

/**
* <p>
    *  前端控制器
    * </p>
*
* @author 何浩然
* @since 2023-02-23
*/
@RestController
@RequestMapping("/im")
public class ImController {

    @Resource
    private IImService imService;

    @PostMapping
    public Result save(@RequestBody Im im) {
        imService.save(im);
        return Result.success();
    }

    @PutMapping
    public Result update(@RequestBody Im im) {
        imService.updateById(im);
        return Result.success();
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        imService.removeById(id);
        return Result.success();
    }

    @PostMapping("/del/batch")
    public Result deleteBatch(@RequestBody List<Integer> ids) {
        imService.removeByIds(ids);
        return Result.success();
    }

    @GetMapping
    public Result findAll() {
        return Result.success(imService.list());
    }

    @GetMapping("/init/{limit}")
    public Result findAllInit(@PathVariable Integer limit) {
        List<Im> ims = imService.list(new QueryWrapper<Im>()
                                      .orderByDesc("id")
                                      .last("limit "+limit));
        return Result.success(ims.stream().sorted(Comparator.comparing(Im::getId)).collect(Collectors.toList()));
    }

    @GetMapping("/{id}")
    public Result findOne(@PathVariable Integer id) {
        return Result.success(imService.getById(id));
    }

    @GetMapping("/page")
    public Result findPage(@RequestParam(defaultValue = "") String name,
                           @RequestParam Integer pageNum,
                           @RequestParam Integer pageSize) {
        QueryWrapper<Im> queryWrapper = new QueryWrapper<Im>().orderByDesc("id");
        queryWrapper.like(!"".equals(name), "name", name);
        return Result.success(imService.page(new Page<>(pageNum, pageSize), queryWrapper));
    }

    /**
    * 导出接口
    */
    @GetMapping("/export")
    public void export(HttpServletResponse response) throws Exception {
        // 从数据库查询出所有的数据
        List<Im> list = imService.list();
        // 在内存操作,写出到浏览器
        ExcelWriter writer = ExcelUtil.getWriter(true);

        // 一次性写出list内的对象到excel,使用默认样式,强制输出标题
        writer.write(list, true);

        // 设置浏览器响应的格式
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
                                String fileName = URLEncoder.encode("Im信息表", "UTF-8");
                                response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");

                                ServletOutputStream out = response.getOutputStream();
                                writer.flush(out, true);
                                out.close();
                                writer.close();

                                }

                                /**
                                * excel 导入
                                * @param file
                                * @throws Exception
                                */
                                @PostMapping("/import")
                                public Result imp(MultipartFile file) throws Exception {
                                InputStream inputStream = file.getInputStream();
                                ExcelReader reader = ExcelUtil.getReader(inputStream);
                                // 通过 javabean的方式读取Excel内的对象,但是要求表头必须是英文,跟javabean的属性要对应起来
                                List<Im> list = reader.readAll(Im.class);

                                imService.saveBatch(list);
                                return Result.success();
                                }

                                }
IImService
package com.hhr.friendback.service;

import com.hhr.friendback.entity.Im;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author 何浩然
 * @since 2023-02-23
 */
public interface IImService extends IService<Im> {

}
IImServiceI
package com.hhr.friendback.service;

import com.hhr.friendback.entity.Im;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author 何浩然
 * @since 2023-02-23
 */
public interface IImService extends IService<Im> {

}
ServiceImpl
package com.hhr.friendback.service.impl;

import com.hhr.friendback.entity.Im;
import com.hhr.friendback.mapper.ImMapper;
import com.hhr.friendback.service.IImService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 何浩然
 * @since 2023-02-23
 */
@Service
public class ImServiceImpl extends ServiceImpl<ImMapper, Im> implements IImService {

}
ImMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hhr.friendback.mapper.ImMapper">

</mapper>

转到前端

Im.vue代码

<script setup>
  import {nextTick, onMounted, ref} from "vue";
  import V3Emoji from 'vue3-emoji'
  import 'vue3-emoji/dist/style.css'
  import {useUserStore} from "@/stores/user";
  import request from "@/utils/request";

  const messages = ref([])

  const userStore = useUserStore()
  const user = userStore.getUser

  const text = ref('')  // 聊天输入的内容
  const divRef = ref()   // 聊天框的引用

  // 页面滚动到最新位置的函数
  const scrollBottom = () => {
    nextTick(() => {   // 等到页面元素出来之后再去滚动
      divRef.value.scrollTop = divRef.value.scrollHeight
    })
  }

  // 页面加载完成触发此函数
  onMounted(() => {
    request.get("/im/init/10").then(res => {
      messages.value = res.data

      scrollBottom()
    })
  })

  const client = new WebSocket(`ws://localhost:9090/imserver/${user.uid}`)
  // 发送消息触发滚动条滚动
  const send = () => {
    if (client) {
      client.send(text.value)
    }
    text.value = ''  // 清空文本框
  }

  const optionsName = {
    'Smileys & Emotion': '笑脸&表情',
    'Food & Drink': '食物&饮料',
    'Animals & Nature': '动物&自然',
    'Travel & Places': '旅行&地点',
    'People & Body': '人物&身体',
    Objects: '物品',
    Symbols: '符号',
    Flags: '旗帜',
    Activities: '活动'
  }

  client.onopen = () => {
    console.log('open')
  }
  client.onclose = () => {  // 页面刷新的时候和后台websocket服务关闭的时候
    console.log('close')
  }
  client.onmessage = (msg) => {
    if (msg.data) {
      let json = JSON.parse(msg.data)
      if (json.uid && json.text) {  // 聊天消息
        messages.value.push(json)
        scrollBottom()  // 滚动页面到最底部
      }
    }
  }
</script>

<template>
  <div style="width: 80%; margin: 10px auto">

    <div ref="divRef" style="background-color: white; padding: 20px; border: 1px solid #ccc; border-radius: 10px; height: 400px; overflow-y: scroll;">
      <div v-for="item in messages" :key="item.id">
        <div style="display: flex; margin: 20px 0;" v-if="user.uid !== item.uid">
          <el-popover
            placement="top-start"
            :width="100"
            trigger="click"
            >
            <template #reference>
              <img :src="item.avatar" alt="" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px">
              </template>
<div style="line-height: 20px">
  <div style="font-size: 16px">{{ item.username }}</div>
  <div style="font-size: 12px;">{{ item.sign }}</div>
</div>
</el-popover>
  <!--          <div style="width: 50px; line-height: 30px; margin-left: 5px; color: #888; overflow: hidden; font-size: 14px">{{ item.username }}</div>-->
  <div style="line-height: 30px; background-color: aliceblue; padding: 0 10px; width:fit-content; border-radius: 10px">{{ item.text }}</div>
  </div>

  <div style="display: flex; justify-content: flex-end; margin: 20px 0;" v-else>
  <div style="line-height: 30px; background-color: lightyellow; padding: 0 10px; width:fit-content; border-radius: 10px;">{{ item.text }}</div>
  <el-popover
  placement="top-start"
  :width="100"
  trigger="hover"
    >
    <template #reference>
    <img :src="item.avatar" alt="" style="width: 30px; height: 30px; border-radius: 50%; margin-left: 10px">
    </template>
    <div style="line-height: 20px">
    <div style="font-size: 16px">{{ item.username }}</div>
    <div style="font-size: 12px;">{{ item.sign }}</div>
    </div>
    </el-popover>
    </div>
    </div>
    </div>

    <div style="margin: 10px 0; width: 100%">
    <V3Emoji default-select="recent" :recent="true" :options-name="optionsName" :keep="true"  :textArea="true" size="mid" v-model="text" />
    <div style="text-align: right"><el-button @click="send" type="primary">发送</el-button></div>
    </div>

    </div>
    </template>

表情包依赖安装

表情包依赖 GitHub - ADKcodeXD/Vue3-Emoji: 基于Vue3和emoji-data.json实现的表情选择组件

 "vue3-emoji": "^1.3.0"

  npm i vue3-emoji -S

标签:hhr,聊天室,在线,Result,Vue3,import,com,public,uid
From: https://blog.csdn.net/m0_68497879/article/details/145109222

相关文章

  • 基SpringBoot+Vue3的游乐园管理系统
    文章目录详细视频演示项目介绍技术介绍功能介绍核心代码系统效果图详细视频演示文章底部名片,获取项目的完整演示视频,免费解答技术疑问项目介绍  游乐园作为娱乐休闲的重要场所,其管理效率和服务质量直接影响到游客的满意度和游乐园的声誉。传统的游乐园管理方式......
  • 计算机毕业设计Springboot校园点餐系统 基于Spring Boot的高校在线订餐平台开发 Sprin
    计算机毕业设计Springboot校园点餐系统670075a9(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着校园生活的日益数字化,传统的食堂点餐方式已逐渐无法满足师生的快节奏生活需求。为了提升用餐体验,减少排队时间,同时为校园餐饮服务......
  • 基于Springboot的在线骑行网站
      博主介绍:java高级开发,从事互联网行业多年,熟悉各种主流语言,精通java、python、php、爬虫、web开发,已经做了多年的设计程序开发,开发过上千套设计程序,没有什么华丽的语言,只有实实在在的写点程序。......
  • vue3 实现标签拖拽排序 + curd
    ......
  • 【详细教程】如何下载超星学习通(学银在线)上面的视频课件资料ppt、pdf
    前言:很多同学都想知道超星学习通中课程资料怎么下载,但是超星学习通中某个课程的目录中展示的资料是不提供直接下载方式的,所以下面就教大家如何下载超星学习通目录中展示的视频课件资料,包括PPT和PDF。一、电脑登录超星学习通网页版,复制课程链接【https://i.chaoxing.com】二、将......
  • springboot基于Vue在线考试管理系统_3ogiiv49
    收藏关注不迷路!!......
  • vue3.0 keep-alive 缓存指定页面
    vue3.0keep-alive缓存指定页面**vue2.0和vue3.0keep-alive写法是有区别,不要太过于依赖AI**!!!!vue2的写法(不适用于vue3)<keep-alive><router-viewv-if="$route.meta.keepAlive"></router-view></keep-alive><router-viewv-if="!$route.met......
  • 计算机毕业设计Springboot“茶文化”网站 “茶韵在线”Spring Boot 网站开发 Spring B
    计算机毕业设计Springboot“茶文化”网站2p9kxpza(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着茶文化在全球范围内的影响力日益扩大,越来越多的人渴望深入了解这一古老而深邃的文化。为了满足这一需求,我们开发了“茶文化”网......
  • 计算机毕业设计Springboot毕业生线上招聘平台 基于Springboot的大学生就业招聘在线平
    计算机毕业设计Springboot毕业生线上招聘平台bvc1qz7p(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着互联网技术的飞速发展,传统的招聘方式已经无法满足现代求职者和企业的需求。越来越多的毕业生和企业开始寻求更高效、便捷的......
  • 【开源】基于SpringBoot框架在线考试系统(计算机毕业设计)+万字毕业论文 T207
    系统合集跳转源码获取链接点击主页更能获取海量源码10年计算机开发经验,主营业务:源码获取、项目二开、语音辅导、远程调试、毕业设计、课程设计、毕业论文、BUG修改一、系统环境运行环境:最好是javajdk1.8,我们在这个平台上运行的。其他版本理论上也可以。IDE环境......