项目 实现了登录注册与聊天功能

文本输入支持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;
    },

}

演示

选择文件
在这里插入图片描述
发送文件
在这里插入图片描述
点击后自动下载
在这里插入图片描述

Logo

前往低代码交流专区

更多推荐