vue + element-plus + ssm 实现群聊聊天室

沉默之剑s 2022-01-18 22:52:40

设计思路

前端使用 vue3 + element-plus 框架画出界面,并使用 axios 与后端通信

后端使用 ssm 框架,数据库使用本地单机的 mysql

数据结构

public class User {
    private Integer id;                    // 账号
    private String username;               // 昵称
    private String password;                //用户民
    private List<Integer> friendIds;        // 好友id 集合
    private List<String> friendNames;       // 好友昵称 集合
    
    // getter、setter
    // ......
}

数据库关系表设计

user 表

 idusernamepassword
用户1---
用户2---
……---

 

relation 表

 user_idfriend_id
好友关系1--
好友关系1--
……--

 

前端

主要使用 element-plus 的控件、css 的视图解析、 vue-router 的路由跳转、axios 的通信方法、websocket 发送聊天消息

(以下源代码省略 css 部分)

封装 axios、websocket

  • 调用 axios 部分 —— axios.ts
import router from "../router/index";
import axios from 'axios';
import qs from 'qs';
import { ElMessage } from 'element-plus'

// 弹出错误消息
export const showErrorMsg = (message) => {
    ElMessage({
        type: 'error',
        message: message
    })
}

// 弹出成功消息
export const showSuccessMsg = (message) => {
    ElMessage({
        type: 'success',
        message: message
    })
}

// 提交登录的表单
export const submitLoginForm = (id: any, password: any) => {
    console.log('submit!')

    //通过 axios 访问后端验证登录信息
    axios.post('/api/login', qs.stringify({
        id: id,
        password: password,
    })).then((res) => {
        console.log(res.data)
        if(res.data === null)
            showErrorMsg('登录失败')
        else {
            showSuccessMsg('登录成功')
            console.log(res.data)

            localStorage.setItem(id.toString(), qs.stringify(res.data))
            router.push({
                path: '\main',
                query: {
                    id: id
                }
            })
        }
    })
}

export const submitLogupForm = (id: any, username: any, password: any) => {
    console.log('submit!')
    axios.post('/api/register', qs.stringify({
        id: id,
        username: username,
        password: password,
    })).then((res) => {
        console.log(res.data)
        if(res.data === false) {
            showErrorMsg('注册失败(账号已注册)!')
            localStorage.setItem('upres', 'false')    // 保存登录结果
        }
        else {
            showSuccessMsg( '注册成功!')
            localStorage.setItem('upres', 'true')    //保存登录结果
        }
    })
}


export const delFriendById = (uid: any, fid: any) => {
    axios.post('/api/delFriendById', qs.stringify({
        uid: uid,
        fid: fid,
    }))
        .then((res) => {
            if(res.data === true) {
                showSuccessMsg("删除成功")
                localStorage.setItem('delres', 'true')
            }
            else {
                showErrorMsg("删除失败")
                localStorage.setItem('delres', 'false')
            }
        })
}

// 通过用户与好友的 id 来删除好友关系
export const addFriendById = (uid: any, fid: any) => {
    // console.log(uid)
    // console.log(fid)
    axios.post('/api/addFriendById', qs.stringify({
        uid: uid,
        fid: fid,
    }))
    .then((res) => {
        localStorage.setItem('addres', res.data)
    })
}

// 发送消息
export const sendMsg = (message, uid) => {
    axios.post('/api/sendMsg',  qs.stringify({
        content: message,
        uid: uid
    })).then((res) => {
        if(res.data == true)
            console.log('发送消息成功:' + message);
        else {
            console.log('发送失败')
            showErrorMsg('发送失败')
        }
    })
}

// 退出账号
export const doExit = (uid) => {
    axios.post('/api/exit', qs.stringify(uid)).then((res) => {
        console.log(res.data)
    })
}
  • 封装 websocket 的初始化 —— websocket.ts
export const createWebSocket = path => {
    if(typeof(WebSocket) === 'undefined')
        alert('您的浏览器不支持socket')
    let socket = new WebSocket(path)
    socket.onopen = () => {
        console.log('websocket 连接成功!')
    }
    return socket
}

 

登录/注册

界面预览

 

 

 

Login.vue 源码分析

控件部分:

<template>
  <div id="box">
  <div id="login">
    <span id="title">欢迎登录</span>
    <el-tabs id="tabs" v-model="pageName" @tab-click="handleClick" stretch="true">
      <el-tab-pane label="登录" name="signIn">
        <el-form class="el-form" ref="FormRef"
                 label-width="120px" :model="loginForm">
          <el-form-item label="账号" >
            <el-input v-model="loginForm.id" prop="id" class="el-input" autofocus="true" type="input" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item label="密码" >
            <el-input v-model="loginForm.password" prop="password" class="el-input" type="password" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button class="el-button" @click="handleLoginIn" type="primary" >登录</el-button>
            <!--        <el-button class="el-button" >注册</el-button>-->
          </el-form-item>
        </el-form>
      </el-tab-pane>
      <el-tab-pane label="注册" name="signUp">
        <el-form class="el-form" ref="FormRef"
                 label-width="120px" :model="logupForm">
          <el-form-item label="账号" >
            <el-input v-model="logupForm.id" prop="id" class="el-input" autofocus="true" type="input" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item label="昵称" >
            <el-input v-model="logupForm.username" prop="username" class="el-input" autofocus="true" type="input" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item label="密码" >
            <el-input v-model="logupForm.password" prop="password" class="el-input" type="password" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item label="确认" >
            <el-input v-model="logupForm.checkpass" prop="checkpass" class="el-input" type="password" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button class="el-button" type="primary" @click="handleLogUp">注册</el-button>
          </el-form-item>
        </el-form>
      </el-tab-pane>
    </el-tabs>

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

功能函数部分:

<script lang="ts" setup>
import { ref, reactive } from 'vue'
import type { ElForm} from 'element-plus'
import {useRouter} from 'vue-router'
import {submitLoginForm, submitLogupForm} from "@/utils/axios"

const FormRef = ref<InstanceType<typeof ElForm>>()
// 获取 router
const router = useRouter();
// 初始化 登录/注册 导航标签的默认值
let pageName = ref('signIn')

// 验证输入值是否是空值
const validateIsNull = (value: any) => {
  if(value === ''){
    alert('输入存在空值')
    return false
  }
  return true
}

// 检查输入的确认密码与密码是否一致
const validateCheckPass = (value1: any, value2: any) => {
  if(validateIsNull(value1) === false){
    return false;
  }else if(value1 != value2) {
    alert('两次输入密码不一致')
    return false;
  }
  return true;
}

// 登录表单数据  使用 reacttive 使其为响应式数据
const loginForm = reactive({
  id: '',
  password: '',
})

// 注册表单数据
const logupForm = reactive({
  id: '',
  username: '',
  password: '',
  checkpass: ''
})

// 处理登录按钮的点击
const handleLoginIn = () => {
  if(validateIsNull(loginForm.id) === false || validateIsNull(loginForm.password) === false)
      return
  submitLoginForm(loginForm.id, loginForm.password)
}

// 处理注册按钮的点击
const handleLogUp = () => {
  if(validateIsNull(logupForm.id) === false || validateIsNull(logupForm.username) === false
      || validateCheckPass(logupForm.password, logupForm.checkpass) === false) {
    console.log('注册失败')
  }else {
    console.log(logupForm)
    submitLogupForm(logupForm.id, logupForm.username, logupForm.password)
    if (localStorage.getItem('upres') === 'true')
      pageName = 'signUp'
  }
}
</script>

主界面

界面预览

主视图:

菜单:                                                                  

    

添加好友:

 

 

删除好友:

 Main.vue 源码分析

控件部分:

<template>
  <div class="background">
  <div id="box">
    <span id="title" style="font-family: 'Microsoft YaHei';font-weight: bold;font-size: x-large">好友列表</span>
    <el-row style="margin-top: 30px">
      <el-dropdown style="font-size: medium; background-color: white; border-radius: 5px; opacity: 0.6"
                   @command="(command)=>{if(command === 'exit'){exit()}}">
        <span class="el-dropdown-link">
          {{curUser.username}} <el-icon class="el-icon--right"><arrow-down /></el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item>账号:{{curUser.id}}</el-dropdown-item>
            <el-dropdown-item command="exit">注销登录</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
      <el-input v-model="newfid" type="input" placeholder="ID" style="width: 120px; margin-left: 200px" autocomplete="off"></el-input>
      <el-button class = "topBtn" @click="handleAdd(newfid)" size="small">添加好友</el-button>
      <el-button class = "topBtn" id="chatBtn" type="primary" @click="goChat">一键群聊</el-button>
      <br/>
    </el-row>
    <el-table id="list" :data="tableData" :key="Math.random()" style="width: 100%; font-size: larger">
      <el-table-column label="账号" width="300">
        <template #default="scope">
          <div style="display: flex; align-items: center">
            <el-icon><user /></el-icon>
            <span style="margin-left: 10px; font-size: small">{{ scope.row.id }}</span>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="昵称" width="300">
        <template #default="scope">
          <el-popover effect="light" trigger="hover" placement="top">
            <template #default>
              <p>name: {{ scope.row.name }}</p>
            </template>
            <template #reference>
              <div class="name-wrapper">
                <el-tag>{{ scope.row.name }}</el-tag>
              </div>
            </template>
          </el-popover>
        </template>
      </el-table-column>
      <el-table-column label="管理">
        <template #default="scope">
          <el-button
              size="small"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
  </div>
</template>

 功能函数部分:

<script lang="ts" setup>
import {ref, getCurrentInstance} from 'vue'
import app from '../main'
import { User, ArrowDown } from '@element-plus/icons-vue'
import {useRouter, useRoute} from "vue-router";
import {delFriendById, doExit, showErrorMsg, showSuccessMsg} from "@/utils/axios";
import {addFriendById} from "@/utils/axios";
import qs from "qs";
import {ElMessage} from "element-plus";

interface User {
  id: number
  name: string
}
const tableData: User[] = []

// 获取路由
const router = useRouter()
const route = useRoute()
// 获取上下文实例
const {ctx: _this}: any = getCurrentInstance()
// 与空间数据绑定
const newfid = ref()

// 从上个路由中获取用户信息
let curUser = qs.parse(localStorage.getItem(route.query.id.toString()))
if(curUser.friendIds != null) {
  for (let i in Object.keys(curUser.friendIds)) {
    tableData.push({
      id: curUser.friendIds[i],
      name: curUser.friendNames[i],
    })
  }
}

// 处理删除按钮的点击
const handleDelete =  (index: number, row: User) => {
  console.log(index, row)
  let uid = curUser.id, fid = row.id
  delFriendById(uid, fid)
  if(localStorage.getItem('delres') === 'true')
    tableData.splice(index, 1)
  console.log(tableData)

  //此处需要强制刷新,因为 el-from 不会随着数据变化自动更新视图
  _this.$forceUpdate()
  localStorage.removeItem('delres')
}

// 转到聊天页面
const goChat = ()=> {
  let routeUrl = router.resolve({
    path: '/chat',
    query: {
      id: curUser.id,
      name: curUser.username
    },
  })
  window.open(routeUrl.href, '_blank')
}

// 处理添加好友按钮的点击
const handleAdd = (fid) => {
  if(fid == null || fid == '')
    showErrorMsg('输入不能为空')
  else {
    console.log(curUser.id)
    console.log(fid)
    addFriendById(curUser.id, fid)
    let newname = localStorage.getItem('addres')
    console.log(newname)

    if ((newname != null) && (newname.length > 2)) {
      tableData.push({
        id: fid,
        name: newname
      })
      showSuccessMsg('添加成功')
      
      //此处需要强制刷新,因为 el-from 不会随着数据变化自动更新视图
      _this.$forceUpdate()
    }
    else if(newname === '-1')
      showErrorMsg('添加失败(已存在此好友)')
    else if(newname === '-2')
      showErrorMsg('添加失败(不存在此人)')
    else
      showErrorMsg('添加失败(未知错误)')
    localStorage.removeItem('addres')
  }
}

// 退出登录
const exit = () => {
  doExit(curUser.id)
  router.push('/')
}

</script>

聊天界面

预览

 Chat.vue 源码分析

控件部分:

<template>
  <el-container class="background">
    <el-header>
    </el-header>
    <el-main style="height: 550px; padding-left: 465px">
      <el-row>
        <div id="historyMsg">
          <el-scrollbar>
            <el-row id="msg" :key="Math.random()" v-for="msg in msgList" v-html="msg"></el-row>
          </el-scrollbar>
        </div>
      </el-row>
      <el-row id="inputMsg">
        <el-input rows="10" v-model="message" resize="none"
                  type="textarea" @keyup.enter.native="enterkeyDown(message, uid, $event)" autofocus="true"></el-input>
      </el-row>
    </el-main>

    <el-footer>
      <el-button id="backBtn" @click="goBack">关闭</el-button>
      <el-button id="sendBtn" type="primary" @click="handleMsg(message, uid)">发送</el-button>
    </el-footer>
  </el-container>
</template>

功能函数部分:

<script lang="ts" setup>
import {getCurrentInstance,reactive, ref} from 'vue'
import {createWebSocket} from "@/utils/websocket";
import {useRoute} from "vue-router";
import {sendMsg, showErrorMsg} from "@/utils/axios";

// 绑定的输入消息
let message = ref('')

//路由对象
const route = useRoute()

//上下文实例对象
const {ctx: _this}: any = getCurrentInstance()

//必须使用reactive,使其为响应式数据,否则视图不会随数据更新
const msgList = reactive([])

//获取上个路由保存的用户id
const uid = route.query.id

//获取 websocket 对象
const socket = createWebSocket('ws://localhost:8080/webSocketServer')

// 监听消息  
socket.onmessage = (evt) => {
    msgList.push(evt.data)
    console.log(msgList)
  }

  // 处理发送按钮点击
  const handleMsg = (userMsg, userId) => {
    if(userMsg === '' || userMsg === null){
      showErrorMsg('不能发送空消息!')
    }else {
      sendMsg(userMsg, userId)
      message = ''
      // _this.$forceUpdate()
    }
  }

  // 处理关闭按钮点击
  const goBack = () => {
    window.close()
  }

  // 监听键盘的回车键
  const enterkeyDown = (userMsg, userId, evt) => {
    if(evt.keyCode == 13){
      if(!evt.metaKey){
        evt.preventDefault()
        handleMsg(userMsg, userId)
      }
    }
  }
</script>

后端

Mapper

userMapper:

<mapper namespace="org.lff.ssm.mapper.UserMapper">

    <select id="getAllUsers" resultType="User">
        SELECT * FROM user;
    </select>
    <select id="getUserById" resultType="User">
        SELECT * FROM user WHERE id=#{id}
    </select>
    <insert id="addUser" parameterType="User">
        INSERT INTO user(id, name, password) VALUES(#{id}, #{username}, #{password});
    </insert>

</mapper>

 relationMapper:

 

    <insert id="addRelation" parameterType="Integer">
        INSERT INTO relation (user_id, friend_id) VALUES(#{id1}, #{id2});
    </insert>

    <select id="getAllFriendIds" resultType="Integer">
        SELECT friend_id FROM relation WHERE user_id=#{userId}
        UNION
        SELECT user_id FROM relation WHERE friend_id=#{userId};
    </select>

    <delete id="delRelationById" >
        DELETE FROM relation WHERE (user_id=#{id1} AND friend_id=#{id2}) OR (user_id=#{id2} AND friend_id=#{id1});
    </delete>

Service

UserService

@Service
public class UserService {
    @Autowired
    UserMapper userMapper;
    
    //获取所有用户
    public List<User> getAllUsers(){
        return userMapper.getAllUsers();
    }
    
    //获取指定用户
    public User getUserById(Integer id){
        return userMapper.getUserById(id);
    }
    
    // 添加用户
    public Integer addUser(User user){
        return userMapper.addUser(user);
    }

}

RelationService

@Service
public class RelationService {
    @Autowired
    private RelationMapper relationMapper;

    // 添加好友关系
    public Boolean addRelation(Integer id1, Integer id2){
        return relationMapper.addRelation(id1, id2);
    }

    // 获取所有好友id
    public List<Integer> getAllFriendIds(User user){
        return relationMapper.getAllFriendIds(user.getId());
    }

    //删除好友关系
    public Boolean delFriendById(Integer id1, Integer id2){
        return relationMapper.delRelationById(id1, id2);
    }

}

Controller

LoginController:

@Controller
public class LoginController {
    @Autowired
    private UserService userService;
    @Autowired
    private RelationService relationService;

    private Map<Integer, User> users = new HashMap<>();

    @PostMapping(value = "/login")
    @ResponseBody
    public User login(User user, Model model, HttpSession session){
        // 查询数据库对应的用户信息
        User query_user = userService.getUserById(user.getId());
        // 判断登录信息是否正确
        if(query_user == null || !query_user.getPassword().equals(user.getPassword())){
            return null;
        }
        // 查询并设置好友关系
        query_user.setFriendIds(relationService.getAllFriendIds(query_user));
        query_user.setFriendNames(getNamesByIds(query_user.getFriendIds()));
        
        // 将此用户保存到已登录用户map中
        users.put(query_user.getId(), query_user);
        // 将此用户保存到 session 缓存中
        session.setAttribute(query_user.getId().toString(), query_user);
        return query_user;
    }


    //注册用户
    @RequestMapping("/register")
    @ResponseBody
    public Boolean register(User user){
        if(userService.getUserById(user.getId()) == null)
            if(userService.addUser(user) > 0)
                return true;
        return false;
    }

    // 通过 id 获取指定用户
    @PostMapping("/getUserById")
    @ResponseBody
    public User getUserById(String uid){
        return users.get(Integer.parseInt(uid));
    }
    

    /**
     * 通过 id 得到 name
     * @param ids
     * @return
     */
    List<String> getNamesByIds(List<Integer> ids){
        List<String> names = new ArrayList<>();
        for(Integer id : ids){
            names.add(userService.getUserById(id).getUsername());
        }
        return names;
    }
}

HomeController:

@Controller
public class HomeController {
    @Autowired
    private RelationService relationService;
    @Autowired
    private UserService userService;
    private Map<Integer, User> users = new HashMap<>();

    //添加好友
    @PostMapping("/addFriendById")
    @ResponseBody
    public String addFriendById( Integer uid, @RequestParam Integer fid, HttpSession session) {
        User curUser = (User) session.getAttribute(uid.toString());
        Integer id1 = curUser.getId(), id2 = fid;
        if(curUser.getFriendIds() != null && curUser.getFriendIds().contains(id2))
            return "-1";    // 已存在此好友
        User fu = userService.getUserById(id2);
        if(fu == null)
            return "-2";    // 不存在此人
        if(relationService.addRelation(id1, id2)) {
            curUser.addFriend(id1, fu.getUsername());
            return fu.getUsername();    // 添加成功,返回其用户名
        }
        return "0";     // 未知原因添加失败
    }

    //删除好友
    @PostMapping("/delFriendById")
    @ResponseBody
    public Boolean delFriendById(Integer uid, @RequestParam Integer fid, HttpSession session){
        User curUser = (User) session.getAttribute(uid.toString());
        if(curUser != null) {
            relationService.delFriendById(curUser.getId(), fid);
            curUser.delFriend(fid, userService.getUserById(fid).getUsername());
            session.setAttribute(curUser.getId().toString(), curUser);
            return true;
        }
        return false;
    }

    // 退出登录 清除缓存
    @PostMapping("/exit")
    public void exit(Integer uid, HttpSession session){
        session.removeAttribute(uid.toString());
        users.remove(uid);
    }

}

ChatController:

@Controller
public class ChatController {
    @Autowired
    private MsgSocketHandler msgSocketHandler;
    private static final String SUBSCRIBE_MESSAGE_URI = "/chat.message";
    private Map<Integer, User> users;

    // 发送消息
    @PostMapping("/sendMsg")
    @ResponseBody
    public Boolean sendMsg(Integer uid, String content, HttpSession session){
        User u = (User)session.getAttribute(uid.toString());
        if(u == null)
            return false;
        String message = u.getUsername() + ":" + content;
        System.out.println(message);
        //调用 websocketHandler 处理消息
        msgSocketHandler.sendMessageToUsers(new TextMessage(message));
        return true;
    }

}

spring-websocket

使用 sprong-websocket 首先需要安装第三方依赖:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>5.3.13</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>5.3.13</version>
        </dependency>

然后需要自己定义三个类:

MsgSocketHandler:

@Component
public class MsgSocketHandler extends TextWebSocketHandler {
    private static ConcurrentHashMap<String, WebSocketSession> users = new ConcurrentHashMap<>();
    private static Logger logger = LoggerFactory.getLogger(MsgSocketHandler.class);

    // 连接后需要做的事情
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 保存用户
        users.put(session.getId(), session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 处理消息
        session.sendMessage(new TextMessage(message.getPayload()));
        System.out.println("服务器收到消息:" + message);

    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 连接关闭后清除缓存
        if(users.get(session.getId()) != null)
            users.remove(session.getId());
    }


    // 向所有用户发送消息
    public void sendMessageToUsers(TextMessage message){
        for(WebSocketSession userSession : users.values()){
            try {
                if (userSession.isOpen())
                    userSession.sendMessage(message);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

}

 

MyInterceptor:

public class MyInterceptor extends HttpSessionHandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        return super.beforeHandshake(request, response, wsHandler, attributes);
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
        super.afterHandshake(request, response, wsHandler, ex);
    }
}

 WebSocketConfig

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private MsgSocketHandler msgSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 配置webSocket服务端接口、Handler、拦截器
        registry.addHandler(msgSocketHandler, "/webSocketServer")
                .addInterceptors(new MyInterceptor()).setAllowedOrigins("*");
        registry.addHandler(msgSocketHandler,
                "/sockjs/webSocketServer")
                .addInterceptors(new MyInterceptor())
                .setAllowedOrigins("*")
                .withSockJS();
    }
}

作者:NP261

...全文
1796 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧