Vue3在线聊天室

更多开源项目请关注我的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 onError(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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员寻川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值