更多开源项目请关注我的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