项目 实现了登录注册与聊天功能
文本输入支持markdown语法,实现代码高亮
后端项目编写
1.新建一个空项目
1.2 在空项目中新建两个模块
1.2.1 webscoket模块
该模块实现聊天室相关代码
选择版本和依赖
1.2.2 webServer模块
该模块实现登录、注册(邮箱验证码注册)、上传图片
版本与依赖
后端项目创建好后如下:
2.主要代码编写
2.1 webServer模块代码编写
2.1.2 注册功能
注册功能和其他依赖参考这篇文章:
基于spring boot的邮箱验证码注册功能 - 后端代码
注册功能测试
2.1.2 登录接口
在UserService与其实现类中添加登录代码
/**
* 用户登录
* @param user 用户数据
* @return 返回用户信息(用户名与id)
*/
Result login(UserRegisterRequest user);
@Override
public Result login(UserRegisterRequest user) {
if(StringUtils.isAnyBlank(user.getUserAccount(),user.getUserPassword())){
return Result.error("用户名或密码为空");
}
user.setUserPassword(DigestUtils.md5DigestAsHex((SALT + user.getUserPassword())
.getBytes())) ;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUseraccount,user.getUserAccount());
queryWrapper.eq(User::getUserpassword,user.getUserPassword());
User flag = getOne(queryWrapper);
if (null == flag) {
return Result.error("密码错误");
}
UserVo userVo = new UserVo();
userVo.setUsername(flag.getUseraccount());
userVo.setId(flag.getId());
return Result.ResultOk(userVo);
}
UserController中添加登录请求
@PostMapping("/login")
public Result userLogin(@RequestBody UserRegisterRequest user){
log.info("登录请求");
if (null == user) return Result.error("参数不能为空");
return userService.login(user);
}
测试
2.1.3 文件上传接口
因为我前端输入框是markdown格式的
Markdown 文件的图片以下两种形式保存:
外部链接形式
可以通过指定一个外部链接的方式来引用网络上的图片。示例如下:
![image.png](https://zyqaq-blog.oss-cn-chengdu.aliyuncs.com/2023/05/16/de14b04d259d40848a1a9af977b2b226.png)
本地文件形式
可以将图片文件保存在本地,并通过相对路径或绝对路径的方式引用。示例如下:
![image-20230516125050769](assets/image-20230516125050769.png)
这里我们只能采用第一种
引入阿里云oss依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
编写路径生成工具类
public class PathUtils {
public static String generateFilePath(String fileName){
//根据日期生成路径 2022/1/15/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
String datePath = sdf.format(new Date());
//uuid作为文件名
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//后缀和文件后缀一致
int index = fileName.lastIndexOf(".");
// test.jpg -> .jpg
String fileType = fileName.substring(index);
return new StringBuilder().append(datePath).append(uuid).append(fileType).toString();
}
}
配置文件配置好阿里云oss的参数
oss:
endpoint: oss-cn-chengdu.aliyuncs.com
keyid: LTAI5tDwkMpEN267sGWBDB2q
keysecret: sIVcsIMjXc4rZoDwjhxU5mFs4QDgz9
bucketname: zyqaq-blog
编写FileController、FileService、FileServiceImpl 代码
@RestController
public class FileController {
@Autowired
private FileService fileService;
@PostMapping("/upload")
public Result uploadImg(@RequestParam("img") MultipartFile multipartFile) {
System.out.println("上传图片");
return fileService.uploadImg(multipartFile);
}
}
public interface FileService {
Result uploadImg(MultipartFile img);
}
@Service
@Data
@ConfigurationProperties(prefix = "oss")
public class FileServiceImpl implements FileService {
String endpoint;
String keyid;
String keysecret;
String bucketname;
@Override
public Result uploadImg(MultipartFile img) {
String originalFilename = img.getOriginalFilename();
String filePath = PathUtils.generateFilePath(originalFilename);
String url = upload(img, filePath);// 2099/2/3/wqeqeqe.png
System.out.println(url);
return Result.ResultOk(url);
}
public String upload(MultipartFile file, String filePath) {
try {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, keyid, keysecret);
// 上传文件流。
InputStream inputStream = file.getInputStream();
// String fileName = file.getOriginalFilename();
//生成随机唯一值,使用uuid,添加到文件名称里面,不会导致重名
// String uuid = UUID.randomUUID().toString().replaceAll("-","");
// fileName = uuid+fileName;
//调用方法实现上传
ossClient.putObject(bucketname, filePath, inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//上传之后文件路径
// https://yygh-cccwm.oss-cn-shenzhen.aliyuncs.com/01.jpg
String url = "https://" + bucketname + "." + endpoint + "/" + filePath;
//返回
return url;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
测试
2.2 webscoketServeer模块代码编写
引入fastjson依赖(将前端发来的信息转换成对象)
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
工具类
全局响应类与json工具类
@Data
public class Result<T> implements Serializable {
private Integer code;
private String id;
private String msg;
private T data;
public Result() {
}
public static Result error(CodeEnum enums){
Result result = new Result();
result.setCode(enums.getCode());
result.setMsg(enums.getMsg());
return result;
}
public static Result ok(Object data){
Result result = new Result();
result.setData(data);
result.setCode(CodeEnum.SUCCESS.getCode());
result.setMsg(CodeEnum.SUCCESS.getMsg());
return result;
}
public static Result set(CodeEnum enums,String id,Object data){
Result result = new Result();
result.setCode(enums.getCode());
result.setMsg(enums.getMsg());
result.setData(data);
result.setId(id);
return result;
}
}
public class JsonUtils {
public static String toJson(Object object) throws Exception {
return JSON.toJSONString(object);
}
public static <T> T parse(String string, Class<T> resultClass) {
return JSON.parseObject(string, resultClass);
}
}
枚举类(消息类型)
public enum CodeEnum {
// 成功
SERVER_TO(0,"首次连接,推送消息"),
SESSION_ID(1,"连接id"),
MESSAGE(2,"消息"),
ONLINE_USERS(3,"在线用户"),
NOTICE(4,"公告"),
NOT_USERNAME(401,"没有用户名"),
SUCCESS(200,"操作成功");
int code;
String msg;
CodeEnum(int code, String errorMessage) {
this.code = code;
this.msg = errorMessage;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
实体类
// 返回前端的消息类
@Data
public class Message {
private long id ;
private String sender ;
private String time;
private String text;
}
// 公告
@Data
public class Notice {
private String gg;
private String time;
}
// 用户
@Data
public class User {
private long id;
private String username;
}
// 接收前端传来的消息类
@Data
public class userMsg {
private User user;
private String messageInput;
}
套接字处理类
编写WebSocketHandler的实现类ChatWebSocketHandler
该类处理websocket请求
@Slf4j
public class ChatWebSocketHandler implements WebSocketHandler {
// 存放 sessionId 与 session
private static Map<String,WebSocketSession> SESSIONS = new ConcurrentHashMap<>();
// 在线用户列表
private static List<User> ONLINE_USERS = new ArrayList<>();
// 消息列表
private static List <Message> msgList = new ArrayList<>();
// 公告
private static Notice notice = new Notice();
/**
* WebSocket 连接建立后调用的方法,通常用于处理连接建立后的业务逻辑。
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("WebSocket 连接已打开:" + session.getId());
// 获取请求路径 判断是否携带用户名
String uri = session.getUri().toString();
// ws://localhost:8080/username=zxwy&id=1
log.info(uri);
// 获取id 与 用户名
String id = uri.substring(uri.lastIndexOf('=')+1);
uri = uri.substring(0,uri.lastIndexOf('&'));
String username = uri.substring(uri.lastIndexOf('=')+1);
if ("".equals(username)){
session.sendMessage(new TextMessage(JsonUtils.toJson(Result.error(CodeEnum.NOT_USERNAME))));
return;
}
User user = new User();
user.setUsername(username);
user.setId(Integer.valueOf(id));
// 判断当前用户是否已经连接过
List<User> onlineUser = ONLINE_USERS.stream()
.filter(tmp -> tmp.getId()==user.getId())
.collect(Collectors.toList());
// 如果存在相同用户已经登录 删除之前登录的session并关闭
if (onlineUser.size() != 0){
delSessionById(onlineUser.get(0).getId());
}
SESSIONS.put(session.getId(),session);
// 将用户添加到在线列表
ONLINE_USERS.add(user);
session.getAttributes().put(session.getId(),user);
session.getAttributes().put("sessionId",session.getId());
// 将连接id推送给前端
session.sendMessage(new TextMessage(JsonUtils.toJson(Result.set(CodeEnum.SESSION_ID,session.getId(),null))));
// 推送在线列表
pushOnlineUser();
// 推送公告
pushNotice(session);
// 首次连接推送所有消息
session.sendMessage(new TextMessage(JsonUtils.toJson(Result.set(CodeEnum.SERVER_TO,null,msgList))));
}
/**
* handleTextMessage: 处理接收到的文本消息。
* @param session
* @param message 前端发送的消息
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
log.info("收到 WebSocket 消息:" + message.getPayload().toString());
Result result = JsonUtils.parse(message.getPayload().toString(),Result.class);
userMsg userMsg = JsonUtils.parse(result.getData().toString(), userMsg.class);
String username = userMsg.getUser().getUsername();
long id = userMsg.getUser().getId();
if (username == null || "".equals(username)){
session.sendMessage(new TextMessage(JsonUtils.toJson(Result.error(CodeEnum.NOT_USERNAME))));
}
String mtext = userMsg.getMessageInput();
// 指令 清空消息
if (mtext.substring(0,1).equals("$")){
if (mtext.equals("$clear")&&id==1){
msgList.removeAll(msgList);
broadcast(JsonUtils.toJson(Result.set(CodeEnum.SERVER_TO,null,msgList)));
return;
}
// 指令 发送公告
if (mtext.substring(0,3).equals("$gg")&&id==1){
notice.setTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")));
notice.setGg(mtext.substring(3));
broadcast(JsonUtils.toJson(Result.set(CodeEnum.NOTICE,null,notice)));
return;
}
}
// 普通消息
Message msg = new Message();
msg.setId(msgList.size());
msg.setSender(username);
msg.setTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")));
msg.setText(mtext);
// 广播消息给所有连接的客户端
msgList.add(msg);
if (msgList.size()==60)
msgList.remove(0);
broadcast(JsonUtils.toJson(Result.set(CodeEnum.MESSAGE,session.getId(),msg)));
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.info("WebSocket 连接错误:" + session.getId() + ", " + exception.getMessage());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("WebSocket 连接已关闭:" + session.getId());
// 移除session id与websocket连接的映射关系
User user = (User) session.getAttributes().get(session.getId());
ONLINE_USERS.remove(user);
String sessionId = (String) session.getAttributes().get("sessionId");
if (sessionId != null) {
SESSIONS.remove(sessionId);
session.close();
}
pushOnlineUser();
}
@Override
public boolean supportsPartialMessages() {
return false;
}
private void broadcast(String message) throws IOException {
Set<Map.Entry<String, WebSocketSession>> entries = SESSIONS.entrySet();
for (Map.Entry<String, WebSocketSession> sessions : entries) {
if(sessions.getValue().isOpen()){
sessions.getValue().sendMessage(new TextMessage(message));
}
}
}
// 推送在线列表
private void pushOnlineUser() throws Exception{
broadcast(JsonUtils.toJson(Result.set(CodeEnum.ONLINE_USERS,null,ONLINE_USERS)));
}
// 推送公告
private void pushNotice(WebSocketSession session) throws Exception{
session.sendMessage(new TextMessage(JsonUtils.toJson
(Result.set(CodeEnum.NOTICE,null,notice))));
}
private void delSessionById(long id) throws Exception{
Set<Map.Entry<String, WebSocketSession>> entries = SESSIONS.entrySet();
for (Map.Entry<String, WebSocketSession> sessions : entries) {
User user = (User)sessions.getValue().getAttributes().get(sessions.getValue().getId());
if (user.getId()==id){
String sessionId = (String) sessions.getValue().getAttributes().get("sessionId");
if (sessionId != null) {
SESSIONS.remove(sessionId);
sessions.getValue().close();
}
}
}
}
}
前端代码编写
1.输入命令创建项目
vue create chatroom
编辑vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 加上这条
lintOnSave: false,
})
修改package.json
"serve": "vue-cli-service serve --port 9999",// 防止和后端端口冲突
2.代码编写
根据以下目录结构创建文件
src
- pages
-login.vue
-register.vue
-chatroom.vue
- router
- router.js
- store
-store.js
路由编写
router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 定义路由规则
const routes = [
{
path: '/',
redirect: '/login'
},
{ path: '/chatroom', component: ()=>import('../pages/chatroom.vue') },
{ path: '/login', component: ()=>import('../pages/login.vue') },
{ path: '/register', component: ()=>import('../pages/register.vue') },
]
// 创建 router 实例
const router = new VueRouter({
mode: 'history', // 路由模式
routes // 路由规则
})
// 导出 router 实例
export default router
2.1 登录注册页面
安装依赖
npm install axios
npm install element-ui
npm install vue-router
npm install vuex
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/router' // 导入 router 实例
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import axios from 'axios';
// 配置axios请求的根路径
axios.defaults.baseURL = 'http://localhost:8888';
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router, // 注册 router 实例
render: h => h(App)
}).$mount('#app')
app.vue
<template>
<div id="app">
<router-view></router-view> <!-- 显示路由视图 -->
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
store.js
实现login.vue与chatroom.vue的数据共享
import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex);
export default new Vuex.Store({
state:{
user:{
username:'',
id:''
}
},
mutations:{
setUsername(state,user){
state.user=user
}
}
})
register.vue
<template>
<div>
<div id="zc">
<h1>注册</h1>
</div>
<div class="register">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户名" prop="userAccount">
<el-input v-model="form.userAccount"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="code">
<el-row>
<el-col :span="16">
<el-input v-model="form.code"></el-input>
</el-col>
<el-col :span="8">
<el-button @click="getCode">获取验证码</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="密码" prop="userPassword">
<el-input type="userPassword" v-model="form.userPassword"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="checkPassword">
<el-input type="userPassword" v-model="form.checkPassword"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="register">注册</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
form: {
userAccount: '',
email: '',
code: '',
userPassword: '',
checkPassword: ''
},
rules: {
userAccount: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
userPassword: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
checkPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.form.userPassword) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
}
},
methods: {
register() {
axios.post('/user/register', {
userAccount: this.form.userAccount,
email: this.form.email,
userPassword: this.form.userPassword,
checkPassword: this.form.checkPassword,
code: this.form.code
})
.then(res => {
// 处理注册成功的逻辑
if (res.data.code===200){
console.log("注册成功")
this.$router.push('/login');
}
})
},
getCode() {
axios.post('/mail',{
to: this.form.email
}).then(res =>{
if (res.data.code == 200){
console.log("验证码发送成功")
this.$message({
message: '验证码发送成功',
type: 'success'
});
}else {
this.$message.error('验证码发送失败');
}
})
}
}
}
</script>
<style>
.register{
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#zc{
justify-content: center; /* 水平居中 */
position: absolute;
left: 50%;
top: 10%;
}
</style>
login.vue
<template>
<div>
<div id="bt">
<h1>登录</h1>
</div >
<div class="login">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户名" prop="userAccount">
<el-input v-model="form.userAccount"></el-input>
</el-form-item>
<el-form-item label="密码" prop="userPassword">
<el-input type="password" v-model="form.userPassword"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="login">登录</el-button>
<el-button type="primary">
<router-link to="/register" style="text-decoration: none;color: white">注册
</router-link></el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
form: {
userAccount: '',
userPassword: ''
},
rules: {
userAccount: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
userPassword: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
}
},
methods: {
login() {
this.$message({
message: '请等待',
type: 'success'
});
axios.post('/user/login', {
userAccount: this.form.userAccount,
userPassword: this.form.userPassword,
}).then(res => {
// 处理注册成功的逻辑
if (res.data.code == 200){
console.log("登录成功")
this.$message({
message: '登录成功',
type: 'success'
});
console.log(res.data.data)
this.$store.commit('setUsername',res.data.data)
console.log(this.$store.state.username)
this.$router.push('/chatroom');
}
})
}
}
}
</script>
<style>
.login{
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#bt{
justify-content: center; /* 水平居中 */
position: absolute;
left: 50%;
top: 20%;
}
</style>
2.2 聊天室页面
安装依赖
npm install markdown-it
npm install mavon-editor
npm install marked
npm install highlight.js
npm install github-markdown-css
chatroom.vue
<template>
<div class="chatroom">
<el-row>
<el-col :span="6">
<el-card class="chatroom-users">
<div class="chatroom-users-header">在线用户</div>
<div class="chatroom-users-body">
<div class="chatroom-user" v-for="user in users" :key="user.id" @click="sengById(user)">{{user.username}}</div>
</div>
</el-card>
<el-card class="chatroom-users">
<div class="chatroom-users-header" >公告</div>
<div id="gg">
<div class="preview" v-html="show(notice.gg)"/>
</div>
</el-card>
</el-col>
<el-col :span="16">
<el-card class="chatroom-message">
<div class="chatroom-message-header">{{ roomName }}</div>
<div class="chatroom-message-body" id="message-box">
<div class="chatroom-message-item" v-for="message in messages" :key="message.id">
<div class="chatroom-message-sender">{{ message.sender }}</div>
<div class="chatroom-message-time">{{ message.time }}</div>
<div class="chatroom-message-text">
<div class="preview" v-html="show(message.text)"/>
</div>
</div>
</div>
</el-card>
<div class="chatroom-input">
<mavon-editor id="edit" ref="myEditor" v-model="tomsg.messageInput"
defaultOpen="edit"
:toolbars="toolbars"
@imgAdd="addImg" />
<el-button type="primary" @click="sendMessage">发送</el-button>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
window.onbeforeunload = function (e) {
return e;
};
import MarkdownIt from 'markdown-it'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
import axios from 'axios';
import { marked } from 'marked'
import hljs from 'highlight.js' // 代码块高亮
import 'highlight.js/styles/github.css' // 代码块高亮样式
import 'github-markdown-css' // 整体 markdown 样式
export default {
components: {
'mavon-editor': mavonEditor.mavonEditor,
},
data() {
return {
// 输入框工具类
toolbars :{
preview: true, // 预览
},
// 公告
notice:{
gg:"",
time:""
},
// 发送消息
tomsg: {
user:{
username:'',
id:''
},
messageInput: ''
},
// 收到消息
sendMsg: {
code: "",
id: "",
msg: "",
data: {}
},
//TODO 暂时用来判断是否需要跳转到消息最下面(以后用用户id判断)
//存放sessionId 以后用来实现私聊,私聊对象的sessionId(或用户id)
id: "",
roomName: "聊天室",
messages: [
/*{ id: 1, sender: "张三", time: "10:30", text: "大家好啊!" },*/
],
users: [
{
id:'',username:''
}
],
};
},
created() {
if (!mavonEditor.markdownIt) {
mavonEditor.markdownIt = new MarkdownIt();
}
this.tomsg.user = this.$store.state.user
this.websocket = new WebSocket('ws://localhost:8080/chatroom?username='
+ this.tomsg.user.username+"&id="+this.tomsg.user.id);
this.websocket.addEventListener('open', this.onOpen);
// 监听 WebSocket 消息事件
this.websocket.addEventListener('message', this.onMessage);
// 监听 WebSocket 连接关闭事件
this.websocket.addEventListener('close', this.onClose);
// 监听 WebSocket 连接错误事件
this.websocket.addEventListener('error', this.onError);
},
mounted() {
},
methods: {
uploadImg(img) {
const formData = new FormData()
formData.append('img', img)
return axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(response => {
console.log('上传图片', response.data);
return response.data.data
}).catch(error => {
throw new Error(error.message)
})
},
// 绑定@imgAdd event
addImg(pos, file) {
console.log("pos",pos)
// 第一步.将图片上传到服务器.
this.uploadImg(file).then(response => {
// TODO 图片能成功上传,但是这里转成url有问题
this.$refs.myEditor.$img2Url(pos, response)
}).catch(error => {
this.$message.error(error.msg)
})
},
show(text){
if (text === '' || text === undefined) return
return marked(text, {
highlight: function (code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
});
},
// 私聊 暂不实现
sengById(user){
alert("id:"+user.id+"\t用户名:"+user.username)
},
onOpen(event) {
console.log('WebSocket 连接已打开', event);
},
onMessage(event) {
console.log('收到 WebSocket 消息', event);
const temp = JSON.parse(event.data)
console.log('temp', temp);
if (temp.code === 401) {
this.$message.error('登录过时请重新登录');
this.$router.push('/login');
return
} else if (temp.code === 1) {
console.log('返回id',temp.id);
this.id = temp.id
} else if (temp.code === 2) {
console.log(temp.data.text );
const newMessage = temp.data;
newMessage.id = this.messages.length + 1;
this.messages.push(newMessage)
} else if (temp.code === 0) {
this.messages = temp.data
} else if (temp.code === 3) {
console.log('返回在线列表');
this.users = temp.data
} else if (temp.code === 4){
console.log('公告');
this.notice = temp.data
}
console.log('temp',temp.id);
console.log('this',this.id);
if (temp.id === this.id) {
setTimeout(() => {
this.moveHuaLun()
}, 20);
}
},
onClose(event) {
console.log('WebSocket 连接已关闭', event);
this.$message.error('WebSocket 连接已关闭');
this.$router.push('/login');
},
onError(event) {
console.error('WebSocket 连接错误', event);
},
sendMessage() {
if (this.tomsg.messageInput.trim() === "") {
return;
}
this.sendMsg.code = 2;
this.sendMsg.data = this.tomsg;
// 发送消息到 WebSocket 服务器
this.websocket.send(JSON.stringify(this.sendMsg));
this.tomsg.messageInput = "";
},
moveHuaLun() {
//获取消息框元素
const messageBox = document.getElementById("message-box");
if (messageBox) {
//将滚动条滚动到消息框底部
messageBox.scrollTop = messageBox.scrollHeight;
} else {
console.error("Element with ID 'message-box' not found.");
}
}
},
beforeUnmount() {
// 关闭 WebSocket 连接
this.websocket.close();
},
};
</script>
<style>
#edit{
width: 1200px;
resize: none; /* 禁止拖动 */
}
#gg{
margin-top: 30px;
height: 100%;
}
.chatroom {
margin-left: 5%;
margin-right: 5%;
margin-top: 2%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.chatroom-message {
height: 500px;
margin-bottom: 10px;
}
.chatroom-message-header {
font-weight: bold;
margin-bottom: 5px;
font-size: 18px;
}
.chatroom-message-body {
max-height: 460px; /* 最大高度为400像素 */
overflow-y: auto; /* 显示垂直滚动条,只有在内容溢出时才显示 */
height: calc(100% - 30px);
overflow-y: auto;
}
.chatroom-message-item {
margin-bottom: 10px;
}
.chatroom-message-sender {
font-weight: bold;
margin-right: 5px;
}
.chatroom-message-time {
font-weight: normal;
}
.chatroom-message-text {
word-break: break-all;
border: 1px solid black;
background-color: aliceblue;
zoom: 0.9;
padding: 5px;
}
.chatroom-input {
display: flex;
margin-top: 20px;
justify-content: space-between;
}
.chatroom-users {
max-height: 400px; /* 最大高度为400像素 */
max-width: 260px;
overflow-y: auto; /* 显示垂直滚动条,只有在内容溢出时才显示 */
height: 100%;
}
.chatroom-users-header {
/* 指定文本的粗细程度 */
width: 100px;
font-weight: bold;
margin-bottom: 5px;
font-size: 18px;
}
.chatroom-users-body {
height: calc(100% - 30px);
overflow-y: auto;
}
.chatroom-user {
margin-bottom: 5px;
cursor: pointer;
}
</style>
项目演示
启动项目
注册
成功登录
发送代码
发送图片
聊天室功能扩展
5.18
添加了发送文件的功能,文件大小限制在5mb,以链接的方式展示在聊天框
代码修改
后端
在FileController中添加以下代码:
@PostMapping("/upload/file")
public Result uploadFile(@RequestParam("file") MultipartFile multipartFile) {
System.out.println("上传文件");
return fileService.uploadFile(multipartFile);
}
在FileService中添加以下代码:
Result uploadFile(MultipartFile multipartFile);
@Override
public Result uploadFile(MultipartFile multipartFile) {
String fileName = multipartFile.getOriginalFilename();
fileName = fileName.replaceAll(" ", "")
// 文件名中的+号一定要替换
.replaceAll("\\+","-");
// String fileName = file.getOriginalFilename();
//生成随机唯一值,使用uuid,添加到文件名称里面,不会导致重名
String uuid = UUID.randomUUID()
.toString()
.replaceAll("-", "");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
String datePath = sdf.format(new Date());
fileName = datePath + uuid + "-" + fileName;
String url = upload(multipartFile, fileName);
return Result.ResultOk(url);
}
在application.yml中添加以下配置:
spring:
multipart:
max-file-size: 5MB
max-request-size: 5MB
前端
修改chatroom.vue
在公告下面添加文件发送组件
<el-card class="chatroom-users">
<div>
<div class="chatroom-users-header">文件发送</div>
<div>
<label for="upload-file" class="custom-upload">
<input id="upload-file" type="file" @change="handleFileChange" ref="fileInput" />
</label>
<div v-if="selectedFileName" class="selected-file">{{ selectedFileName }}</div>
<el-button v-if="selectedFileName" type="primary" plain @click="clearFile">清除</el-button>
</div>
<div id="wj">
<el-button type="primary" plain @click="uploadFile">发送文件</el-button>
</div>
</div>
</el-card>
添加两个属性
data() {
return {
selectedFileName: '' ,// 存储已选择的文件名
file: null, // 选择的文件
}
}
推荐3个函数
methods: {
uploadFile() {
if (this.file){
const formData = new FormData()
formData.append('file', this.file)
return axios.post('/upload/file', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(response => {
this.tomsg.messageInput = response.data.data;
this.sendMessage()
this.$message.success("文件发送成功")
this.clearFile()
}).catch(error => {
throw new Error(error.message)
})
}else {
this.$message.error('文件为空');
}
},
clearFile() {
this.selectedFileName = '';
// 清除文件输入框的值
this.$refs.fileInput.value = '';
this.file =null
},
handleFileChange(event) {
this.file = event.target.files[0];
if (this.file && this.file.size > 5 * 1024 * 1024){
this.$message.error('文件大小超过限制,建议小于5mb');
this.file = null
return
}
this.selectedFileName = this.file.name;
},
}
演示
选择文件
发送文件
点击后自动下载