学习SpringBoot+Vue前后端分离项目,原项目GitHub地址,项目作者江雨一点雨博客。
聊天界面选用github开源项目is-liyiwei/vue-Chat-demo,克隆到本地仓库,
安装sass-loader、node-sass
npm install sass-loader --save-dev //安装到dev环境下
npm install node-sass --save-dev
Home.vue中在el-dropdown上添加聊天功能按钮
<div>
<el-button icon="el-icon-bell" type="text" style="margin-right: 8px;color: #000000;" size="normal" @click="goChat"></el-button>
<el-dropdown class="userInfo" @command="commandHandler">
<span class="el-dropdown-link">
{{user.name}}<i><img :src="user.userface" alt=""></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
<el-dropdown-item command="setting">设置</el-dropdown-item>
<el-dropdown-item command="logout" divided>注销登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
methods中添加goChat
goChat() {
this.$router.push("/chat");
}
创建views/chat/FriendChat.vue,并在router中添加,将上面克隆到仓库的项目中的App.vue拷贝到FriendChat.vue
import FriendChat from './views/chat/FriendChat.vue'
children:[
{
path: '/chat',
name: '在线聊天',
component: FriendChat,
hidden:true
}
]
<template>
<div id="app">
<div class="sidebar">
<card></card>
<list></list>
</div>
<div class="main">
<message></message>
<usertext></usertext>
</div>
</div>
</template>
<script>
import card from '../../components/chat/card.vue'
import list from '../../components/chat/list.vue'
import message from '../../components/chat/message.vue'
import usertext from '../../components/chat/usertext.vue'
export default {
//可改可不改,原为app
name: 'FriendChat',
data () {
return {
}
},
mounted:function() {
this.$store.dispatch('initData');
},
components:{
card,
list,
message,
usertext
}
}
</script>
<style lang="scss" scoped>
#app {
margin: 20px auto;
width: 800px;
height: 600px;
overflow: hidden;
border-radius: 10px;
.sidebar, .main {
height: 100%;
}
.sidebar {
float: left;
color: #f4f4f4;
background-color: #2e3238;
width: 200px;
}
.main {
position: relative;
overflow: hidden;
background-color: #eee;
}
}
</style>
创建components/chat,将仓库中的四个组件拷贝进来
动态信息获取
ChatController
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
HrService hrService;
@GetMapping("/hrs")
public List<Hr> getAllHrs() {
return hrService.getAllHrsExceptCurrentHr();
}
}
HrService
public List<Hr> getAllHrsExceptCurrentHr() {
return hrMapper.getAllHrsExceptCurrentHr(HrUtils.getCurrentHr().getId());
}
HrMapper
public List<Hr> getAllHrsExceptCurrentHr() {
return hrMapper.getAllHrsExceptCurrentHr(HrUtils.getCurrentHr().getId());
}
HrMapper.xml
<select id="getAllHrsExceptCurrentHr" resultMap="BaseResultMap">
select * from hr where id !=#{id};
</select>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
vhr-web/config/WebSocketConfig
@Configuration
//开启消息代理
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//设置"/ws/ep"为endPoint,开启SockJs
//setAllowedOrigins不限访问源,解决跨域问题
registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//设置"/queue"消息代理
registry.enableSimpleBroker("/queue");
}
}
model/ChatMsg
public class ChatMsg {
private String from; //从哪儿来
private String to;//到哪儿去
private String content;//消息内容
private Date date;//发送消息的日期
private String fromNickname;//昵称
//省略getter、setter
}
controller/WsController
@Controller
public class WsController {
@Autowired
SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/ws/chat")
public void handleMsg(Authentication authentication, ChatMsg chatMsg) {
//当前用户
Hr hr = (Hr) authentication.getPrincipal();
chatMsg.setFrom(hr.getUsername());
chatMsg.setFromNickname(hr.getName());
chatMsg.setDate(new Date());
//getTO获取发送给谁,"/queue/chat"路径,发送内容
simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
}
vue.config.js
proxyObj['/ws'] = {
ws: true,
target: "ws://localhost:8081"
};
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
//消息提醒
import { Notification } from 'element-ui';
import {getRequest} from "../utils/api";
//导入新的文件
import '../utils/stomp'
import '../utils/sockjs'
Vue.use(Vuex)
const now = new Date();
const store = new Vuex.Store({
state: {
routes: [],
sessions: {},
hrs: [],
currentSession: null,
currentHr: JSON.parse(window.sessionStorage.getItem("user")),
filterKey: '',
stomp: null,
isDot: {}
},
mutations: {
INIT_CURRENTHR(state, hr) {
state.currentHr = hr;
},
initRoutes(state, data) {
state.routes = data;
},
changeCurrentSession(state, currentSession) {
//Vue.set( target, key, value )
//target:要更改的数据源(可以是对象或者数组)
//key:要更改的具体数据
//value :重新赋的值
//这样做可以在key发生改动的时候页面自动刷新,在这里就是接受消息时页面刷新
Vue.set(state.isDot, state.currentHr.username + '#' + currentSession.username, false);
state.currentSession = currentSession;
},
//发送消息
addMessage(state, msg) {
let mss = state.sessions[state.currentHr.username + '#' + msg.to];
if (!mss) {
Vue.set(state.sessions, state.currentHr.username + '#' + msg.to, []);
}
state.sessions[state.currentHr.username + '#' + msg.to].push({
content: msg.content,
date: new Date(),
self: !msg.notSelf
})
},
INIT_DATA(state) {
//浏览器本地的历史聊天记录可以在这里完成
let data = localStorage.getItem('vue-chat-session');
if (data) {
state.sessions = JSON.parse(data);
}
},
INIT_HR(state, data) {
state.hrs = data;
}
},
actions: {
//接收消息
connect(context) {
context.state.stomp = Stomp.over(new SockJS('/ws/ep'));
//两种回调,成功的回调
context.state.stomp.connect({}, success => {
//注意必须加上/user
context.state.stomp.subscribe('/user/queue/chat', msg => {
let receiveMsg = JSON.parse(msg.body);
//接受新消息时判断是否为当前聊天对象发送,不是则发送消息提醒
//包括没选中任何一人进行聊天时,可以发送提醒消息
if (!context.state.currentSession || receiveMsg.from != context.state.currentSession.username) {
Notification.info({
title: '【' + receiveMsg.fromNickname + '】发来一条消息',
message: receiveMsg.content.length > 10 ? receiveMsg.content.substr(0, 10) : receiveMsg.content,
position: 'bottom-right'
})
Vue.set(context.state.isDot, context.state.currentHr.username + '#' + receiveMsg.from, true);
}
receiveMsg.notSelf = true;
receiveMsg.to = receiveMsg.from;
context.commit('addMessage', receiveMsg);
})
//失败的回调
}, error => {
})
},
initData(context) {
context.commit('INIT_DATA')
getRequest("/chat/hrs").then(resp => {
if (resp) {
context.commit('INIT_HR', resp);
}
})
}
}
})
store.watch(function (state) {
return state.sessions
}, function (val) {
localStorage.setItem('vue-chat-session', JSON.stringify(val));
}, {
deep: true/*这个貌似是开启watch监测的判断,官方说明也比较模糊*/
})
card.vue
<template>
<div id="card">
<header>
<img class="avatar" v-bind:src="user.userface" v-bind:alt="user.name">
<p class="name">{{user.name}}</p>
</header>
<footer>
<input class="search" type="text" v-model="$store.state.filterKey" placeholder="search user...">
</footer>
</div>
</template>
<script>
export default {
name: 'card',
data () {
return {
user: JSON.parse(window.sessionStorage.getItem("user"))
}
}
}
</script>
<style lang="scss" scoped>
#card {
padding: 12px;
.avatar{
width: 40px;
height: 40px;
vertical-align: middle;/*这个是图片和文字居中对齐*/
}
.name {
display: inline-block;
padding: 10px;
margin-bottom: 15px;
font-size: 16px;
}
.search {
background: #26292E;
height: 30px;
line-height: 30px;
padding: 0 10px;
border: 1px solid #3a3a3a;
border-radius: 4px;
outline: none;/*鼠标点击后不会出现蓝色边框*/
color: #FFF;
}
}
</style>
list.vue
<template>
<div id="list">
<ul style="padding-left: 0px">
//首先判断currentSesssion是否存在,存在就判断用户名是否相等否则不激活
<li v-for="item in hrs" :class="{ active: currentSession?item.username === currentSession.username:false}"
v-on:click="changeCurrentSession(item)">
<!-- :class="[item.id === currentSession ? 'active':'']" -->
<img class="avatar" :src="item.userface" :alt="item.name">
//新消息提示小红点
<el-badge :is-dot="isDot[user.username+'#'+item.username]"><p class="name">{{item.name}}</p></el-badge>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'list',
data() {
return {
user:JSON.parse(window.sessionStorage.getItem("user"))
}
},
computed: mapState([
'hrs',
'isDot',
'currentSession'
]),
methods: {
changeCurrentSession (currentSession) {
this.$store.commit('changeCurrentSession', currentSession)
}
}
}
</script>
<style lang="scss" scoped>
#list {
li {
padding: 16px 15px;
border-bottom: 1px solid #292C33;
cursor: pointer;
list-style: none;
&:hover {
background-color: rgba(255, 255, 255, 0.03);
}
}
li.active { /*注意这个是.不是冒号:*/
background-color: rgba(255, 255, 255, 0.1);
}
.avatar {
border-radius: 2px;
width: 30px;
height: 30px;
vertical-align: middle;
}
.name {
display: inline-block;
margin-left: 15px;
margin-top: 0px;
margin-bottom: 0px;
}
}
</style>
message.vue
<template>
<div id="message" v-scroll-bottom="sessions">
<ul v-if="currentSession">
<li v-for="entry in sessions[user.username+'#'+currentSession.username]">
<p class="time">
<span>{{entry.date | time}}</span>
</p>
<div class="main" :class="{self:entry.self}">
<img class="avatar" :src="entry.self ? user.userface : currentSession.userface" alt="">
<p class="text">{{entry.content}}</p>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'message',
data() {
return {
user: JSON.parse(window.sessionStorage.getItem("user"))
}
},
computed: mapState([
'sessions',
'currentSession'
]),
filters: {
time(date) {
if (date) {
date = new Date(date);
}
return `${date.getHours()}:${date.getMinutes()}`;
}
},
directives: {/*这个是vue的自定义指令,官方文档有详细说明*/
// 发送消息后滚动到底部,这里无法使用原作者的方法,也未找到合理的方法解决,暂用setTimeout的方法模拟
'scroll-bottom'(el) {
//console.log(el.scrollTop);
setTimeout(function () {
el.scrollTop += 9999;
}, 1)
}
}
}
</script>
<style lang="scss" scoped>
#message {
padding: 15px;
max-height: 68%;
overflow-y: scroll;
ul {
list-style-type: none;
padding-left: 0px;
li {
margin-bottom: 15px;
}
}
.time {
text-align: center;
margin: 7px 0;
> span {
display: inline-block;
padding: 0 18px;
font-size: 12px;
color: #FFF;
background-color: #dcdcdc;
border-radius: 2px;
}
}
.main {
.avatar {
float: left;
margin: 0 10px 0 0;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #fafafa;
border-radius: 4px;
line-height: 30px;
}
}
.self {
text-align: right;
.avatar {
float: right;
margin: 0 0 0 10px;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #b2e281;
border-radius: 4px;
line-height: 30px;
}
}
}
</style>
usertext.vue
<template>
<div id="uesrtext">
<textarea placeholder="按 Ctrl + Enter 发送" v-model="content" v-on:keyup="addMessage"></textarea>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'uesrtext',
data() {
return {
content: ''
}
},
computed: mapState([
'sessions',
'currentSession'
]),
methods: {
addMessage(e) {
if (e.ctrlKey && e.keyCode === 13 && this.content.length) {
let msgObj = new Object();
msgObj.to = this.currentSession.username;
msgObj.content = this.content;
this.$store.state.stomp.send('/ws/chat', {}, JSON.stringify(msgObj));
this.$store.commit('addMessage', msgObj);
this.content = '';
}
}
}
}
</script>
<style lang="scss" scoped>
#uesrtext {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
height: 30%;
border-top: solid 1px #DDD;
> textarea {
padding: 10px;
width: 100%;
height: 100%;
border: none;
outline: none;
}
}
</style>