SpringBoot3+WebSocket+Vue3+TypeScript实现简易在线聊天室(附完整源码参考)

目录

一、效果展示与消息格式。

二、代码实现。

[(1)引入 websocket stater 起步依赖。](#(1)引入 websocket stater 起步依赖。)

[(2)编写 websocket 配置类。](#(2)编写 websocket 配置类。)

[(3)编写存储 HttpSession 配置类。](#(3)编写存储 HttpSession 配置类。)

[GetHttpSessionConfig 。](#GetHttpSessionConfig 。)

ChatEndPoint实体类。(服务端核心处理消息类)

(4)处理消息的自定义工具类。

(5)响应结果封装类。

三、前端代码。(核心页代码)

四、完整源代码。



  • 本篇博客就是代码实现。
  • 博主实现比较潦草且简易。搁置半个月才更新,实习上班心累,属于不想动,,,
  • 如果你喜欢,请点个赞hh。如果你更强,请继续完善它吧!

一、效果展示与消息格式。

  • 聊天登录。(未实现数据库登录,可以自己实现)

  • 首次进入。

  • 聊天1。

  • 聊天2。

  • 聊天3。

  • 系统广播。

  • 消息格式。
  1. 客户端 给 服务端:{"toName":"xxx","message":"hello"}。
  2. 服务端 给 客户端:(两种格式)

(1)系统消息格式:{"system":true,"fromName":null,"message":"xxxx","offline":true/false}。

(2)推送给某一个用户的消息格式:{"system":false,"fromName":"xxx","message":"xxxx","offline":true/false}。


  • Message实体类。
java 复制代码
package com.hyl.websocket.bean;

/**
 * 封装浏览器(客户端)给服务端发送的消息数据
 */
public class Message {
    private String toName;
    private String message;

    public Message(){

    }

    public Message(String toName, String message) {
        this.toName = toName;
        this.message = message;
    }

    public String getToName() {
        return toName;
    }

    public void setToName(String toName) {
        this.toName = toName;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
  • ResultMessage实体类。
java 复制代码
package com.hyl.websocket.bean;

/**
 * 封装服务端给浏览器(客户端)发送的消息数据
 */
public class ResultMessage {
    private boolean isSystem;
    private String fromName;
    private Object message; //系统消息可能是数组(用户名称数组)
    private boolean offline;  //是否为下线消息

    public ResultMessage() {
    }

    public ResultMessage(boolean isSystem, String fromName, Object message, boolean offline) {
        this.isSystem = isSystem;
        this.fromName = fromName;
        this.message = message;
        this.offline = offline;
    }

    public boolean isSystem() {
        return isSystem;
    }

    public void setSystem(boolean system) {
        isSystem = system;
    }

    public String getFromName() {
        return fromName;
    }

    public void setFromName(String fromName) {
        this.fromName = fromName;
    }

    public Object getMessage() {
        return message;
    }

    public void setMessage(Object message) {
        this.message = message;
    }

    public boolean isOffline() {
        return offline;
    }

    public void setOffline(boolean offline) {
        this.offline = offline;
    }

    @Override
    public String toString() {
        return "ResultMessage{" +
                "isSystem=" + isSystem +
                ", fromName='" + fromName + '\'' +
                ", message=" + message +
                ", offline=" + offline +
                '}';
    }
}

二、代码实现。

(1)引入 websocket stater 起步依赖。
XML 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.53</version>
        </dependency>

        <!--hutool工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>
(2)编写 websocket 配置类。
  • 主要作用:扫描并添加含有注解@ServerEndponit的Bean。

  • 代码。
java 复制代码
package com.hyl.config;

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

/**
 * 扫描@ServerEndpoint注解
 */
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
(3)编写存储 HttpSession 配置类。
  • 普通 Web 请求中,通过 HttpServletRequest.getSession() 即可简单获取HttpSession 。
  • WebSocket 协议是独立于 HTTP 的全双工协议。HttpSession 是 HTTP 层的会话,WebSocket 的 Endpoint 中无法直接获取 HttpSession ,所以要在 WebSocket 中获取 HttpSession ,必须在 HTTP 握手阶段(还未升级为 WebSocket 时) 保存 HttpSession 的引用。

  1. 自定义 Configurator 继承 ServerEndpointConfig.Configurator。重写modifyHandshake()方法,获取 HttpSession 对象并存储到配置对象中。
  2. 在注解 @ServerEndpoint 中引入的自定义配置器(Configurator)。
GetHttpSessionConfig 。
java 复制代码
package com.hyl.config;

import jakarta.servlet.http.HttpSession;
import jakarta.websocket.HandshakeResponse;
import jakarta.websocket.server.HandshakeRequest;
import jakarta.websocket.server.ServerEndpointConfig;

/**
 * HTTP握手阶段(还未升级为 WebSocket时)保存 HttpSession 的引用
 * ChatEndpoint就通过这个配置类获取HttpSession对象进行操作(@ServerEndpoint中引入)
 */
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator{
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        //获取HttpSession对象
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        //保存HttpSession对象到ServerEndpointConfig对象中
        //ChatEndpoint类中在onOpen方法中通过EndpointConfig对象获取
        sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }
}
ChatEndPoint实体类。(服务端核心处理消息类)
  • @ServerEndPoint注解(端点)引入配置类。
java 复制代码
package com.hyl.websocket;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.hyl.config.GetHttpSessionConfig;
import com.hyl.utils.MessageUtil;
import com.hyl.websocket.bean.Message;
import jakarta.servlet.http.HttpSession;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 针对每一个客户端都会创建一个Endpoint(端点)
 */
//设置访问路径
//引入HttpSession配置类
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {

    private static final Logger log = LoggerFactory.getLogger(ChatEndpoint.class);

    //保存当前所有在线用户,key为用户名,value为 Session 对象
    //static 共享 final 防止重新赋值 ConcurrentHashMap 线程安全
    private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();

    private HttpSession httpSession; //成员变量,方便当前用户继续使用

    /**
     * 建立websocket连接后,被调用
     * @param wsSession
     */
    @OnOpen
    public void onOpen(Session wsSession, EndpointConfig endpointConfig){  //注意这个是websocket的session
        //用户登录时,已经保存用户名,从HttpSession中获取用户名
        this.httpSession = (HttpSession) endpointConfig.getUserProperties().get(HttpSession.class.getName());
        String username = (String) this.httpSession.getAttribute("currentUser");
        if(ObjectUtil.isNotEmpty(username) && !onlineUsers.containsKey(username)){  //避免重复存储
            //存储当前用户以及对应的session进行存储
            onlineUsers.put(username, wsSession);
            //再将当前登录的所有用户以广播的方式进行推送
            String message = MessageUtil.getMessage(true, null, getAllFriendUsernames(), false);
            broadcastAllUsers(message);
        }
    }

    public Set getAllFriendUsernames(){
        Set<String> set = onlineUsers.keySet();
        return set;
    }

    /**
     * 广播方法
     * 系统消息格式:{"system":true,"fromName":null,"message":"xxx"}
     */
    private void broadcastAllUsers(String message){
        if(ObjectUtil.isEmpty(message)){
            return;
        }
        try{
            //遍历当前在线用户的map集合
            Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
            for (Map.Entry<String, Session> entry : entries) {
                //获取所有用户的session对象
                Session session = entry.getValue();
                if(session.isOpen()){
                    //获取同步消息发送的实例,发送文本消息
                    session.getBasicRemote().sendText(message);
                }else {
                    onlineUsers.remove(entry.getKey());
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            log.info("广播已发送,消息:{}", message);
        }
    }

    /**
     * 接收到浏览器(客户端)发送的数据时被调用,如:张三 -> 李四
     * @param message 发送的数据是JSON字符串 ,格式为:{"toName":"xxx","message":"xxx"}
     */
    @OnMessage
    public void onMessage(String message){
        //当前登录的用户
        String username = null;
        //当前用户发送的消息
        String messageText = null;
        //接收人用户名
        String toUsername = null;
        try {
            //将消息推送给指定用户
            //操作JSON字符串消息需先转换为对应的Message对象
            Message msg = JSON.parseObject(message, Message.class);
            //获取接收方用户名称
            toUsername = msg.getToName();
            //获取接收方需收到的消息
            messageText = msg.getMessage();
            //获取消息接收方用户对象的session对象发消息
            Session toUserSession = onlineUsers.get(toUsername);
            //封装发送的消息
            username = (String) this.httpSession.getAttribute("currentUser");
            String toUserMsg = MessageUtil.getMessage(false, username, messageText, false);
            toUserSession.getBasicRemote().sendText(toUserMsg);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            log.info("发送人:{},接收人:{},消息内容:{}", username, toUsername, messageText);
        }
    }

    /**
     * 断开websocket连接被调用
     * @param wsSession
     */
    @OnClose
    public void onClose(Session wsSession){  //注意这个是websocket的session
        //从在线用户map中剔除当前退出登录的用户
        String username = (String) this.httpSession.getAttribute("currentUser");
        if(ObjectUtil.isNotNull(username)){
            onlineUsers.remove(username);
            //通知其他用户,当前用户下线
            String leaveMessage = MessageUtil.getMessage(true, null, username + ",已下线",true);
            broadcastAllUsers(leaveMessage);
            //就是重新发送广播,当前在线用户的map的key
            String message = MessageUtil.getMessage(true, null, getAllFriendUsernames(),false);
            broadcastAllUsers(message);
        }
    }
}
(4)处理消息的自定义工具类。
  • MessageUtil。
java 复制代码
package com.hyl.utils;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.hyl.websocket.bean.ResultMessage;

/**
 * 封装json格式的消息工具类
 */
public class MessageUtil {

    /**
     *
     * @param isSystemMessage 是否是系统消息 广播是系统消息 私聊不是
     * @param fromName 谁发送的
     * @param message 具体消息内容
     * @param offline 是否离线消息
     * @return
     */
    public static String getMessage(boolean isSystemMessage, String fromName, Object message , boolean offline){
        try {
            ResultMessage resultMessage = new ResultMessage();
            resultMessage.setSystem(isSystemMessage);
            resultMessage.setMessage(message);
            //校验fromName是否为空 系统消息默认为null
            if(ObjectUtil.isNotNull(fromName)){
                resultMessage.setFromName(fromName);
            }
            if(offline){
                resultMessage.setOffline(true);
            }else {
                resultMessage.setOffline(false);
            }
            return JSON.toJSONString(resultMessage);
        }catch (Exception e){
            throw new RuntimeException("消息序列化失败", e);
        }
    }
}
(5)响应结果封装类。
java 复制代码
package com.hyl.common;

import java.io.Serializable;

public class ResponseData<T> implements Serializable {
    private String code;
    private String message;
    private T result;

    public ResponseData(){

    }

    public ResponseData(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public ResponseData(String code, String message, T result) {
        this.code = code;
        this.message = message;
        this.result = result;
    }

    //泛型方法:第一个T的作用域只限于当前方法
    public static <T> ResponseData<T> success() {
        ResponseData<T> resData  = new ResponseData<>();
        resData.setCode("200");
        resData.setMessage("success");
        return resData;
    }

    public static <T> ResponseData<T> success(T result){
        ResponseData<T> resData  = new ResponseData<>();
        resData.setCode("200");
        resData.setMessage("success");
        resData.setResult(result);
        return resData;
    }

    public static <T> ResponseData<T> fail(String message){
        ResponseData<T> resData  = new ResponseData<>();
        resData.setCode("500");  //后台服务器业务错误
        resData.setMessage(message);
        return resData;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getResult() {
        return result;
    }

    public void setResult(T result) {
        this.result = result;
    }
}
  • 实体类User。
java 复制代码
package com.hyl.bean;

/**
 * 用户实体类
 */
public class User {
    private Long userId;
    private String username;
    private String password;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "userId=" + userId +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

三、前端代码。(核心页代码)

  • 这里只放了vue页面代码。前端的其他组件或重要文件可以去源码查看。
  • 登录页。
html 复制代码
<template>
  <section class="login-container">
    <div class="login-box">
      <div class="login-box-left">
        <div>
          <img style="margin-top: 60px" src="../../public/meta.svg" alt=""></img>
        </div>
        <div>WebSocket <br/> Online Chat Room</div>
      </div>
      <div class="login-div">
        <div class="login-title">Login In Now</div>
        <div class="username">
          <n-input ref="uinput" type="text" v-model:value="usernameValue" maxlength="30" show-count round clearable placeholder="请输入用户名" size="large"></n-input>
        </div>
        <div class="password">
          <n-input ref="pinput" type="password" v-model:value="passwordValue" maxlength="30" show-count round clearable placeholder="请输入密码" show-password-on="mousedown" size="large"></n-input>
        </div>
        <div class="login-button">
          <n-button :loading="loading" strong secondary round type="success" size="large" @click="handleLogin">Click Login</n-button>
        </div>
      </div>
    </div>
  </section>
</template>

<script setup lang="ts">

import {ref} from 'vue';
import {login} from '@/api/login/api'
import router from "@/router";
import {useMessage,NInput } from 'naive-ui'

const usernameValue = ref<string | null>();
const passwordValue = ref<string | null>();
const uinput = ref<InstanceType<typeof NInput> | null>();
const pinput = ref<InstanceType<typeof NInput> | null>();
const message = useMessage();

//新增加载显示
const loading = ref<boolean>(false);

async function handleLogin() {
  loading.value = true;
  if(!usernameValue.value){
    message.warning('用户名不能为空!');
    uinput.value?.focus();
    loading.value = false;
    return;
  }
  if(!passwordValue.value){
    message.warning('密码不能为空!');
    pinput.value?.focus();
    loading.value = false;
    return;
  }

  try {
    const res = await login(usernameValue.value, passwordValue.value)
    if(res?.code === '200'){
      message.success('登录成功');
      router.replace('/home');
    }else {
      message.error(res?.message);
      usernameValue.value = '';
      passwordValue.value = '';
      uinput.value?.focus();
    }
  }catch (err) {
    console.log('登录请求异常:', err);
    message.error('网络异常,无法连接服务器');
  }finally {
    loading.value = false;
  }
}

</script>

<style scoped>

.login-container {
  height: 100vh;
  width: 100vw;
  background-image: url('@/assets/img/login_bg.jpg'); /*背景图片*/
  background-size: cover;  /*填充*/
  background-repeat: no-repeat;  /*不重复*/
  background-position: center;  /*居中*/
  background-attachment: fixed;  /*固定*/

  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;

}

.login-box {
  width: 800px;
  height: 600px;
  font-size: 28px;
  font-weight: 800;
  color: #ffffff;
  display: flex;
  justify-content: left;

  border-radius: 20px;
  box-shadow: 5px 5px 5px #888888, -5px -5px 5px #DDDDDD;
  overflow: hidden;
}

.login-box-left {
  height: 100%;
  width: 40%;
  background-color: #9ddd9d;
}

.login-div {
  height: 100%;
  width: 60%;
  background-color: white;
  display: flex;
  flex-direction: column;
  align-items: center;

  .login-title {
    margin-top: 150px;
    font-size: 30px;
    font-weight: 700;
    color: #777676;
  }

  .username {
    width: 350px;
    margin-top: 40px;
  }
  .password {
    width: 350px;
    margin-top: 20px;
  }
  .login-button {
    margin-top: 20px;
  }
}

</style>
  • 聊天主页。
html 复制代码
<template>
  <section class="message">
    <div class="message-box">
      <!--  头部  -->
      <div class="box-head">
        <div class="head-user">
          当前用户:{{ username }}
          <span class="status" v-if="isOnline">(在线)</span>
          <span class="status" v-else>(离线)</span>
        </div>
        <div class="head-chat" v-show="chatNameShow">正在和 {{toName}} 聊天</div>
      </div>

      <!--  主体内容   -->
      <div class="box-main">
        <!--  左部分    -->
        <div class="box-left">
          <!--  左上部分-未点击好友-空白聊天背景 -->
          <div class="left-nomessage" v-show="!isShowChat">
            <div class="nomessage-title">
              当前无聊天。请选择好友列表中的在线好友开始聊天吧!
            </div>
          </div>
          <!--   左上部分-消息聊天框   -->
          <div class="left-message" v-show="isShowChat">
            <div v-for="item in historyMessage">
              <!--左部分-对面消息-->
              <div class="msg-guest" v-if="item.fromName">
                <div class="msg-name">{{item.fromName}}</div>
                <div class="msg-content">
                  <img class="msg-img" src="../assets/img/avatar1.jpg" alt="">
                  <div class="message-1">{{item.message}}</div>
                </div>
              </div>
              <!--右边部分-自己消息-->
              <div class="msg-host" v-else>
                <div class="msg-name">{{username}}</div>
                <div class="msg-content">
                  <img class="msg-img" src="../assets/img/avatar2.jpg" alt="">
                  <div class="message-2">{{item.message}}</div>
                </div>
              </div>
            </div>
          </div>
          <!--   左下部分-消息发送框   -->
          <div class="left-input">
            <div class="input-wrapper">
              <n-input v-model:value="sendMessage.message" @keydown:enter="handleEnter" class="l-textarea" type="textarea" placeholder="在此输入文字信息,,," :autosize="{minRows: 3,maxRows: 5}"/>
              <n-button class="send-btn" type="success" size="small" @click="submit">
                发送
              </n-button>
            </div>
            <div class="send-tip">提示:Shift + Enter 换行</div>
          </div>
        </div>

        <!--   右部分   -->
        <div class="box-right">
          <!--   右上部分-好友列表   -->
          <div class="right-friend">
            <div class="right-title">
              <div>好友列表</div>
            </div>
            <div class="friend-list">
              <ul>
                <li v-for="item in friendList">
                  <div style="cursor: pointer" @click="showChat(item)">{{ item }}<span class="status"></span></div>
                </li>
              </ul>
            </div>
          </div>
          <!--   右下部分-广播列表(系统消息)   -->
          <div class="right-sys">
            <div class="right-title">
              <div>系统广播</div>
            </div>
            <div class="sys-msg">
              <ul>
                <li v-for="item in systemOfflineMessages">
                  {{item}}
                </li>
                <li v-for="item in systemMessages">
                  {{item}} 已上线
                </li>
              </ul>
            </div>
          </div>

        </div>
      </div>


    </div>
  </section>
</template>

<script setup lang="ts">

import {ref,onMounted} from "vue";
import {getCurrentUsername} from "@/api/home/api";
import {ChatMessage, ResponseData, SendMessage} from "@/api/type";
import { useMessage } from 'naive-ui'

const websocket = ref<WebSocket | null>();
const data = ref<ResponseData<string>>(); //初始化数据
const message = useMessage();

const username = ref<string | null>('');  // 当前用户
const isOnline = ref<boolean>(true);
const toName = ref<string | null>('');  // 聊天对象
const chatNameShow = ref<boolean>(false);  // 聊天对象是否显示

const friendList = ref<string[]>([]);  //在线好友列表
const systemOfflineMessages = ref<string[]>([]); //离线消息
const systemMessages = ref<string[]>([]);  //系统消息
const historyMessage = ref<ChatMessage[]>([]);  //聊天历史消息
const isShowChat = ref<boolean>(false);  //是否显示聊天框

/*message*/
const sendMessage = ref<SendMessage>({
  toName: '',
  message: '',
})

const handleEnter = (e: KeyboardEvent) =>{
  if(!e.shiftKey){
    e.preventDefault(); //阻止默认回车换行行为
    submit();
  }
}

function showChat (friend: string) {
  toName.value = friend;  // 聊天对象赋值

  //处理历史聊天信息
  const history = sessionStorage.getItem(toName.value);
  if(!history){
    historyMessage.value = [];
  } else {
    historyMessage.value = JSON.parse(history);  //反序列化获取历史聊天记录
  }

  isShowChat.value = true;  //渲染聊天
  chatNameShow.value = true;  //渲染正在和谁聊天
}

//发送信息
const submit = () =>{
  sendMessage.value.message = sendMessage.value.message.trim();
  if(!sendMessage.value.message){
    message.warning('请输入聊天内容');
    return;
  }
  //接收消息人
  sendMessage.value.toName = toName.value;
  //添加自己的一条信息
  historyMessage.value.push(JSON.parse(JSON.stringify(sendMessage.value)));
  //临时保存与该对象的聊天历史消息
  sessionStorage.setItem(toName.value, JSON.stringify(historyMessage.value))  //存字符串类型,序列化stringify,反序列化parse
  //向服务端发送信息
  websocket.value.send(JSON.stringify(sendMessage.value));
  sendMessage.value.message = '';
}

//onOpen
function onOpen() {
  isOnline.value = true;
}

const onClose = () => {
  isOnline.value = false;
}

//接收服务端发送的信息
const onMessage = (event: MessageEvent) =>{
  const dataString = event.data;  //服务端推送的信息
  console.log(dataString);
  const resJsonObj = JSON.parse(dataString);
  console.log(resJsonObj);
  //系统信息
  if(resJsonObj && resJsonObj.system){
    const names = resJsonObj.message;
    const offline = resJsonObj.offline;
    if(offline){
      systemOfflineMessages.value.push(resJsonObj.message);
    }else {
      for(let i=0; i<names.length; i++){
        if(names[i] !== username.value && !friendList.value.includes(names[i])){
          friendList.value.push(names[i]);
          systemMessages.value.push(names[i]);
        }
      }
    }
  }else {  //聊天信息
    //获取聊天记录
    const history = sessionStorage.getItem(resJsonObj.fromName);
    if(!history){
      historyMessage.value = [];
    }
    historyMessage.value.push(resJsonObj);
    sessionStorage.setItem(resJsonObj.fromName, JSON.stringify(historyMessage.value))
  }
}

//初始化
const init = async () => {
  await getCurrentUsername().then((res) =>{
    if(res.code === '200'){
      data.value = res;
      username.value = data.value.result;
    }
  }).catch((err)=>{
    console.log(err);
  });

  /*创建websocket对象*/
  websocket.value = new WebSocket('ws://localhost:8081/chat');

  websocket.value.onopen = onOpen;

  websocket.value.onmessage  = onMessage;

  websocket.value.onclose = onClose;

}

onMounted(()=>{
  init();
})

</script>

<style scoped>

.message {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

.message-box {
  width: 50%;
  height: 700px;
  border: 1px solid #afdaaf;
  border-radius: 5px;
  overflow: hidden; /* 防止内部元素溢出 */
  box-shadow: 0 2px 8px rgba(157, 221, 157, 0.3);
}

.box-head {
  background-color: #afeaaf;
  height: 60px;
  padding: 0 10px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start; /*子元素靠顶部对齐,防止拉伸挤压*/

  .head-user {
    font-size: 16px;
    font-weight: 600;
    margin-top: 5px;

    .status {
      font-size: 12px;
      font-weight: 500;
      margin-left: 5px;
      color: #0c8fe1;
    }
  }

  .head-chat {
    text-align: center;
    font-size: 18px;
    font-weight: 600;
  }
}

/*主体内容*/
.box-main {
  height: calc(100% - 60px);
  display: flex;

  .box-left {
    height: 100%;
    width: 70%;
    border-right: 1px solid #c2c3c5;

    display: flex;
    flex-direction: column;

    /*左侧无消息*/
    .left-nomessage {
      flex: 1;
      padding: 15px;
      overflow-y: auto;  /*内容过多下滑*/
      background-color: #fff;

      .nomessage-title {
        color: #eae5e5;
        font-size: 15px;
        font-weight: 500;
        line-height: 25px;
        background-color: #9e9c9c;
        padding: 5px 10px;
      }
    }

    /*左侧消息*/
    .left-message {
      flex: 1;
      padding: 15px;
      overflow-y: auto;  /*内容过多下滑*/
      background-color: #fff;

      .msg-guest {
        margin-bottom: 12px;
        padding: 0 5px;
        text-align: left;

        .msg-name {
          font-size: 14px;
          color: #000000;
          font-weight: 400;
          line-height: 25px;
        }

        .msg-content {
          display: flex;
          align-items: flex-start;

          .msg-img {
            width: 36px;
            height: 36px;
            border-radius: 5px;
          }
          .message-1 {
            margin-left: 10px;
            background-color: #f0f0f0;
            padding: 8px 12px;
            border-radius: 8px;
            max-width: 70%; /* 限制气泡最大宽度 */
          }

        }

      }

      .msg-host {
        margin-bottom: 12px;
        padding: 0 5px;
        text-align: right;

        .msg-name {
          font-size: 14px;
          color: #000000;
          font-weight: 400;
          line-height: 25px;
        }
        .msg-content {
          display: flex;
          align-items: flex-start;
          flex-direction: row-reverse;  /*反转*/

          .msg-img {
            width: 36px;
            height: 36px;
            border-radius: 5px;
            margin-left: 10px;
          }
          .message-2 {
            background-color: #beedc6;
            padding: 8px 12px;
            border-radius: 8px;
            max-width: 70%;
          }
        }
      }
    }

    /*左侧输入框*/
    .left-input {
      display: flex;
      flex-direction: column;
      padding: 12px 15px;
      border-top: 1px solid #c2c3c5;

      .input-wrapper {
        position: relative;
        flex: 1;

        .l-textarea {
          width: 100% !important; /*确保输入框占满容器*/
          padding-bottom: 50px !important; /*给下侧按钮预留空间*/
        }

        .send-btn {
          position: absolute;
          bottom: 8px;
          right: 8px;
          padding: 10px 30px;
        }

      }

      .send-tip {
        margin-top: 5px;
        color: #c2c3c5;
      }


    }
  }
  .box-right {
    width: 30%;
    height: 100%;
    /*处理子容器分布*/
    display: flex;
    flex-direction: column;

    .right-title {
      font-size: 16px;
      font-weight: 500;
      padding: 8px 10px;
      border-bottom: 1px solid #c2c3c5;
    }

    .right-friend {
      height: 40%;
      display: flex;
      flex-direction: column;
      overflow: hidden;

      .friend-list {
        flex: 1;
        overflow-y: auto;
        padding: 8px 10px;
      }

    }

    .right-sys {
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;
      .sys-msg {
        flex: 1;
        overflow-y: auto;
        padding: 8px 10px;
      }
    }

    .friend-list ul,.sys-msg ul {
      margin: 0;
      padding: 0;
      list-style: none;
    }


  }

}

</style>

四、完整源代码。

(1)gitee仓库源码地址:https://gitee.com/suisuipingan-hyl/online-chat-room

相关推荐
菜鸟plus+2 小时前
Captcha
java·开发语言
那个松鼠很眼熟w2 小时前
8.设计模式-两阶段终止(优雅停机)
java
聪明的笨猪猪2 小时前
Java 高并发多线程 “基础”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
北京耐用通信3 小时前
自动化通信谜团:耐达讯自动化Modbus RTU如何变身 Profibus连接触摸屏
人工智能·网络协议·自动化·信息与通信
惬意小西瓜3 小时前
3.java常用类知识点
java·开发语言·分类
YA3333 小时前
java设计模式五、适配器模式
java·设计模式·适配器模式
拂晓银砾3 小时前
EasyExcel 动态多级标题、合并单元格、修改单元格样式实现总结
java
玩毛线的包子3 小时前
Android Gradle学习(十)- java字节码指令集解读
java
华农第一蒟蒻3 小时前
谈谈跨域问题
java·后端·nginx·安全·okhttp·c5全栈