571
社区成员
发帖
与我相关
我的任务
分享前端使用 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
// ......
}
| id | username | password | |
|---|---|---|---|
| 用户1 | - | - | - |
| 用户2 | - | - | - |
| …… | - | - | - |
| user_id | friend_id | |
|---|---|---|
| 好友关系1 | - | - |
| 好友关系1 | - | - |
| …… | - | - |
主要使用 element-plus 的控件、css 的视图解析、 vue-router 的路由跳转、axios 的通信方法、websocket 发送聊天消息
(以下源代码省略 css 部分)
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)
})
}
export const createWebSocket = path => {
if(typeof(WebSocket) === 'undefined')
alert('您的浏览器不支持socket')
let socket = new WebSocket(path)
socket.onopen = () => {
console.log('websocket 连接成功!')
}
return socket
}





控件部分:
<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>
主视图:

菜单:
添加好友:


删除好友:

控件部分:
<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>

控件部分:
<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>

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>
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);
}
}
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;
}
}
使用 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