├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── chat.png ├── default_head.jpg ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ └── chat │ │ ├── card.vue │ │ ├── chattitle.vue │ │ ├── list.vue │ │ ├── message.vue │ │ ├── toolbar.vue │ │ └── usertext.vue ├── main.js ├── router │ └── index.js ├── store │ └── index.js ├── utils │ ├── api.js │ ├── emoji.json │ ├── sockjs.js │ └── stomp.js └── views │ ├── admin │ ├── AdminLogin.vue │ ├── GroupChatRecord.vue │ ├── Home.vue │ ├── PrivateChatRecord.vue │ └── UserInfo.vue │ └── chat │ ├── ChatRoom.vue │ └── Login.vue └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目介绍 2 | 微言聊天室基于前后端分离,项目采用 SpringBoot+Vue 开发,当前项目是系统的Vue前端SPA工程 3 | 4 | 项目预览地址:http://www.javahai.top/index.html 5 | 6 | 前端工程源码地址:https://github.com/JustCoding-Hai/subtlechat-vue 7 | 8 | 后端工程源码地址:https://github.com/JustCoding-Hai/subtlechat 9 | 10 | 11 | ## 前端技术栈 12 | 1. Vue 13 | 2. ElementUI 14 | 3. axios 15 | 4. vue-router 16 | 5. Vuex 17 | 6. WebSocket 18 | 7. vue-cli4 19 | 20 | ## Project setup 21 | ``` 22 | npm install 23 | ``` 24 | 25 | ## 环境配置 26 | 指定包下载 27 | ``` 28 | #安装element-ui 29 | npm i element-ui -S 30 | #安装axios 31 | npm install axios 32 | #安装vuex 33 | npm install vuex --save 34 | #安装font-awesome 35 | npm install --save font-awesome 36 | #安装sass 37 | npm install sass-loader --save-dev 38 | cnpm install node-sass --save-dev 39 | ``` 40 | 41 | ## Compiles and hot-reloads for development 运行项目 42 | ``` 43 | npm run serve 44 | ``` 45 | 46 | ## Compiles and minifies for production打包项目 47 | ``` 48 | npm run build 49 | ``` 50 | 51 | ## 文档 52 | 53 | - [1.前端Vue的环境配置与全局方法封装](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/1.前端Vue的环境配置与全局方法封装) 54 | - [2.聊天页面组件引入与环境配置](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/2.聊天页面组件引入与环境配置) 55 | - [3.使用websocket实现群聊](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/3.使用websocket实现群聊) 56 | - [ 4.解决页面刷新vuex的state清空问题](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/4.解决页面刷新vuex的state清空问题) 57 | - [5.重启服务器自动跳转到登陆页](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/5.重启服务器自动跳转到登陆页) 58 | - [6.修改elementui的el-popover弹框的样式](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/6.修改elementui的el-popover弹框的样式) 59 | - [7.emoji表情发送](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/7.emoji表情发送) 60 | - [8.vue前端遇到的问题](https://github.com/JustCoding-Hai/subtlechat-vue/wiki/8.vue前端遇到的问题) 61 | 62 | ## 参考 63 | https://github.com/is-liyiwei/vue-Chat-demo 64 | 65 | ### Customize configuration 66 | See [Configuration Reference](https://cli.vuejs.org/config/). 67 | 68 | 69 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuechatroom", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.19.2", 11 | "core-js": "^3.6.5", 12 | "element-ui": "^2.13.2", 13 | "font-awesome": "^4.7.0", 14 | "vue": "^2.6.11", 15 | "vue-router": "^3.2.0", 16 | "vuex": "^3.4.0" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "~4.4.0", 20 | "@vue/cli-plugin-router": "~4.4.0", 21 | "@vue/cli-service": "~4.4.0", 22 | "node-sass": "^4.14.1", 23 | "sass-loader": "^8.0.2", 24 | "vue-template-compiler": "^2.6.11" 25 | }, 26 | "browserslist": [ 27 | "> 1%", 28 | "last 2 versions", 29 | "not dead" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /public/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustCoding-Hai/subtlechat-vue/b2586d728066422a32839d2aa0376925f8527380/public/chat.png -------------------------------------------------------------------------------- /public/default_head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustCoding-Hai/subtlechat-vue/b2586d728066422a32839d2aa0376925f8527380/public/default_head.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustCoding-Hai/subtlechat-vue/b2586d728066422a32839d2aa0376925f8527380/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 微言SubtleChat~ 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustCoding-Hai/subtlechat-vue/b2586d728066422a32839d2aa0376925f8527380/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/chat/card.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 59 | 66 | -------------------------------------------------------------------------------- /src/components/chat/chattitle.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /src/components/chat/list.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 82 | 83 | 130 | -------------------------------------------------------------------------------- /src/components/chat/message.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 113 | 114 | 213 | -------------------------------------------------------------------------------- /src/components/chat/toolbar.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 147 | 148 | 205 | 220 | -------------------------------------------------------------------------------- /src/components/chat/usertext.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 166 | 167 | 168 | 178 | 179 | 253 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import ElementUI from 'element-ui'; 5 | import 'element-ui/lib/theme-chalk/index.css'; 6 | import store from './store/index'; 7 | import 'font-awesome/css/font-awesome.min.css' 8 | 9 | /* 10 | 封装请求方法,供全局调用 11 | */ 12 | import {postKeyValueRequest} from "./utils/api"; 13 | import {postRequest} from "./utils/api"; 14 | import {getRequest} from "./utils/api"; 15 | import {putRequest} from "./utils/api"; 16 | import {deleteRequest} from "./utils/api"; 17 | 18 | Vue.prototype.postKeyValueRequest=postKeyValueRequest; 19 | Vue.prototype.postRequest=postRequest; 20 | Vue.prototype.getRequest=getRequest; 21 | Vue.prototype.putRequest=putRequest; 22 | Vue.prototype.deleteRequest=deleteRequest; 23 | 24 | /*路由前置守卫 25 | to:去哪,from:从哪来,调用next():通过本次路由请求*/ 26 | router.beforeEach((to,from,next)=>{ 27 | if (to.path=="/"||to.path=="/adminlogin"){//首页不需要请求菜单 28 | next(); 29 | }else if (to.path=="/home"&&!window.sessionStorage.getItem('admin')) { 30 | ElementUI.Message.error({message:"不具有访问权限!"}); 31 | next(from) 32 | } 33 | else{ 34 | if (window.sessionStorage.getItem('user')||window.sessionStorage.getItem('admin')){ //登录后才请求菜单 35 | next(); 36 | }else {//没登录就跳转到登陆页 37 | //如果先前写了请求路径(to中路径)则记录下来 38 | ElementUI.Message.error({message:"请登录后访问!"}); 39 | next('/?redirect='+to.path); 40 | } 41 | } 42 | }) 43 | 44 | Vue.config.productionTip = false 45 | Vue.use(ElementUI); 46 | 47 | new Vue({ 48 | router, 49 | store,//这里需要注意 50 | render: h => h(App) 51 | }).$mount('#app') 52 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Login from '../views/chat/Login' 4 | import ChatRoom from '../views/chat/ChatRoom' 5 | import AdminLogin from '../views/admin/AdminLogin' 6 | import Home from '../views/admin/Home' 7 | import UserInfo from '../views/admin/UserInfo' 8 | import GroupChatRecord from '../views/admin/GroupChatRecord' 9 | import PrivateChatRecord from '../views/admin/PrivateChatRecord' 10 | 11 | Vue.use(VueRouter) 12 | 13 | const routes = [ 14 | { 15 | path: '/', 16 | name: 'Login', 17 | component: Login, 18 | hidden:true 19 | }, 20 | { 21 | path:'/chatroom', 22 | name:'ChatRoom', 23 | component:ChatRoom, 24 | hidden:true 25 | }, 26 | { 27 | path:'/adminlogin', 28 | name:'AdminLogin', 29 | component:AdminLogin, 30 | hidden:true 31 | }, 32 | { 33 | path:'/home', 34 | name:'Home', 35 | component:Home, 36 | hidden:true 37 | }, 38 | { 39 | path:'/home', 40 | name:'用户管理', 41 | component:Home, 42 | iconCls:"fa fa-user", 43 | children:[{ 44 | path:'/userinfo', 45 | name:'用户信息管理', 46 | component:UserInfo, 47 | }] 48 | }, 49 | { 50 | path:'/home', 51 | name:'聊天记录管理', 52 | iconCls:'fa fa-book', 53 | component:Home, 54 | children:[ 55 | { 56 | path:'/groupChatRecord', 57 | name:'群聊记录管理', 58 | component:GroupChatRecord 59 | }, 60 | { 61 | path:'/privateChatRecord', 62 | name:'私聊记录管理', 63 | component:PrivateChatRecord 64 | } 65 | ] 66 | } 67 | 68 | 69 | 70 | // { 71 | // path: '/about', 72 | // name: 'About', 73 | // // route level code-splitting 74 | // // this generates a separate chunk (about.[hash].js) for this route 75 | // // which is lazy-loaded when the route is visited. 76 | // component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') 77 | // } 78 | ] 79 | //解决重复访问路由地址报错 80 | const originalPush = VueRouter.prototype.push; 81 | VueRouter.prototype.push = function push(location) { 82 | return originalPush.call(this, location).catch(err => err) 83 | }; 84 | 85 | const router = new VueRouter({ 86 | routes 87 | }) 88 | 89 | export default router 90 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import {getRequest, postRequest} from "../utils/api"; 4 | import SockJS from '../utils/sockjs' 5 | import '../utils/stomp' 6 | import { Notification } from 'element-ui'; 7 | 8 | Vue.use(Vuex) 9 | 10 | const now = new Date(); 11 | 12 | const store = new Vuex.Store({ 13 | state:sessionStorage.getItem('state') ? JSON.parse(sessionStorage.getItem('state')) :{ 14 | routes:[], 15 | sessions:{},//聊天记录 16 | users:[],//用户列表 17 | currentUser:null,//当前登录用户 18 | currentSession:{username:'群聊',nickname:'群聊'},//当前选中的用户,默认为群聊 19 | currentList:'群聊',//当前聊天窗口列表 20 | filterKey:'', 21 | stomp:null, 22 | isDot:{},//两用户之间是否有未读信息 23 | errorImgUrl:"http://39.108.169.57/group1/M00/00/00/J2ypOV7wJkyAAv1fAAANuXp4Wt8303.jpg",//错误提示图片 24 | shotHistory:{}//拍一拍的记录历史 25 | }, 26 | mutations:{ 27 | initRoutes(state,data){ 28 | state.routes=data; 29 | }, 30 | changeCurrentSession (state,currentSession) { 31 | //切换到当前用户就标识消息已读 32 | Vue.set(state.isDot,state.currentUser.username+"#"+currentSession.username,false); 33 | //更新当前选中的用户 34 | state.currentSession =currentSession; 35 | }, 36 | //修改当前聊天窗口列表 37 | changeCurrentList(state,currentList){ 38 | state.currentList=currentList; 39 | }, 40 | //保存群聊消息记录 41 | addGroupMessage(state,msg){ 42 | let message=state.sessions['群聊']; 43 | if (!message){ 44 | //state.sessions[state.currentHr.username+"#"+msg.to]=[]; 45 | Vue.set(state.sessions,'群聊',[]); 46 | } 47 | state.sessions['群聊'].push({ 48 | fromId:msg.fromId, 49 | fromName:msg.fromName, 50 | fromProfile:msg.fromProfile, 51 | content:msg.content, 52 | messageTypeId:msg.messageTypeId, 53 | createTime: msg.createTime, 54 | }) 55 | }, 56 | //保存单聊数据 57 | addMessage (state,msg) { 58 | let message=state.sessions[state.currentUser.username+"#"+msg.to]; 59 | if (!message){ 60 | //创建保存消息记录的数组 61 | Vue.set(state.sessions,state.currentUser.username+"#"+msg.to,[]); 62 | } 63 | state.sessions[state.currentUser.username+"#"+msg.to].push({ 64 | content:msg.content, 65 | date: new Date(), 66 | fromNickname:msg.fromNickname, 67 | messageTypeId:msg.messageTypeId, 68 | self:!msg.notSelf 69 | }) 70 | }, 71 | /** 72 | * 获取本地聊天记录,同步数据库的记录保存到localStorage中。 73 | * 不刷新情况下都是读取保存再localStorage中的记录 74 | * @param state 75 | * @constructor 76 | */ 77 | INIT_DATA (state) { 78 | //同步数据库中的群聊数据 79 | getRequest("/groupMsgContent/").then(resp=>{ 80 | if (resp){ 81 | Vue.set(state.sessions,'群聊',resp); 82 | } 83 | }) 84 | }, 85 | //保存系统所有用户 86 | INIT_USER(state,data){ 87 | state.users=data; 88 | }, 89 | //请求并保存所有系统用户 90 | GET_USERS(state){ 91 | getRequest("/chat/users").then(resp=>{ 92 | if (resp){ 93 | state.users=resp; 94 | } 95 | }) 96 | } 97 | }, 98 | actions:{ 99 | /** 100 | * 作用:初始化数据 101 | * action函数接受一个与store实例具有相同方法和属性的context对象 102 | * @param context 103 | */ 104 | initData (context) { 105 | //初始化聊天记录 106 | context.commit('INIT_DATA') 107 | //获取用户列表 108 | context.commit('GET_USERS') 109 | }, 110 | /** 111 | * 实现连接服务端连接与消息订阅 112 | * @param context 与store实例具有相同方法和属性的context对象 113 | */ 114 | connect(context){ 115 | //连接Stomp站点 116 | context.state.stomp=Stomp.over(new SockJS('/ws/ep')); 117 | context.state.stomp.connect({},success=>{ 118 | /** 119 | * 订阅系统广播通知消息 120 | */ 121 | context.state.stomp.subscribe("/topic/notification",msg=>{ 122 | //判断是否是系统广播通知 123 | Notification.info({ 124 | title: '系统消息', 125 | message: msg.body.substr(5), 126 | position:"top-right" 127 | }); 128 | //更新用户列表(的登录状态) 129 | context.commit('GET_USERS'); 130 | }); 131 | /** 132 | * 订阅群聊消息 133 | */ 134 | context.state.stomp.subscribe("/topic/greetings",msg=>{ 135 | //接收到的消息数据 136 | let receiveMsg=JSON.parse(msg.body); 137 | console.log("收到消息"+receiveMsg); 138 | //当前点击的聊天界面不是群聊,默认为消息未读 139 | if (context.state.currentSession.username!="群聊"){ 140 | Vue.set(context.state.isDot,context.state.currentUser.username+"#群聊",true); 141 | } 142 | //提交消息记录 143 | context.commit('addGroupMessage',receiveMsg); 144 | }); 145 | /** 146 | * 订阅机器人回复消息 147 | */ 148 | context.state.stomp.subscribe("/user/queue/robot",msg=>{ 149 | //接收到的消息 150 | let receiveMsg=JSON.parse(msg.body); 151 | //标记为机器人回复 152 | receiveMsg.notSelf=true; 153 | receiveMsg.to='机器人'; 154 | receiveMsg.messageTypeId=1; 155 | //添加到消息记录保存 156 | context.commit('addMessage',receiveMsg); 157 | }) 158 | /** 159 | * 订阅私人消息 160 | */ 161 | context.state.stomp.subscribe('/user/queue/chat',msg=>{ 162 | //接收到的消息数据 163 | let receiveMsg=JSON.parse(msg.body); 164 | //没有选中用户或选中用户不是发来消息的那一方 165 | if (!context.state.currentSession||receiveMsg.from!=context.state.currentSession.username){ 166 | Notification.info({ 167 | title:'【'+receiveMsg.fromNickname+'】发来一条消息', 168 | message:receiveMsg.content.length<8?receiveMsg.content:receiveMsg.content.substring(0,8)+"...", 169 | position:"bottom-right" 170 | }); 171 | //默认为消息未读 172 | Vue.set(context.state.isDot,context.state.currentUser.username+"#"+receiveMsg.from,true); 173 | } 174 | //标识这个消息不是自己发的 175 | receiveMsg.notSelf=true; 176 | //获取发送方 177 | receiveMsg.to=receiveMsg.from; 178 | //提交消息记录 179 | context.commit('addMessage',receiveMsg); 180 | }) 181 | },error=>{ 182 | Notification.info({ 183 | title: '系统消息', 184 | message: "无法与服务端建立连接,请尝试重新登陆系统~", 185 | position:"top-right" 186 | }); 187 | }) 188 | }, 189 | //与Websocket服务端断开连接 190 | disconnect(context){ 191 | if (context.state.stomp!=null) { 192 | context.state.stomp.disconnect(); 193 | console.log("关闭连接~"); 194 | } 195 | }, 196 | } 197 | }) 198 | 199 | /** 200 | * 监听state.sessions,有变化就重新保存到local Storage中chat-session中 201 | */ 202 | store.watch(function (state) { 203 | return state.sessions 204 | },function (val) { 205 | console.log('CHANGE: ', val); 206 | localStorage.setItem('chat-session', JSON.stringify(val)); 207 | },{ 208 | deep:true/*这个貌似是开启watch监测的判断,官方说明也比较模糊*/ 209 | }) 210 | 211 | 212 | export default store; 213 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {Message} from "element-ui"; 3 | import router from '../router' 4 | 5 | 6 | /*axios全局响应拦截*/ 7 | axios.interceptors.response.use(success=>{ 8 | if (success.status&&success.status==200&&success.data.status==500){//请求成功,但处理出现其他错误 9 | Message.error({message:success.data.msg}) 10 | return; 11 | } 12 | //请求成功且服务器处理无错误 13 | if (success.data.msg){ 14 | Message.success({message:success.data.msg}); 15 | } 16 | return success.data; 17 | },error => { 18 | if (error.response.status==504) {// 充当网关或代理的服务器,未及时从远端服务器获取请求 19 | Message.error({message:'找不到服务器'}) 20 | }else if(error.response.status==403){ //服务器理解请求客户端的请求,但是拒绝执行此请求 21 | Message.error({message:'权限不足,请联系管理员'}) 22 | }else if (error.response.status==401){//请求要求用户的身份认证 23 | Message.error({message:'尚未登录,请登录'}); 24 | router.replace("/");//跳转到登陆页 25 | }else if (error.response.status==404){ 26 | Message.error({message:'服务器无法根据客户端的请求找到资源'}) 27 | } else if (error.response.status==500){ 28 | Message.error({message:'服务器内部错误,无法完成请求'}) 29 | } else { 30 | if (error.response.data){ 31 | Message.error({message:error.response.data.msg}) 32 | } 33 | else { 34 | Message.error({message:'未知错误!'}) 35 | } 36 | } 37 | return; 38 | }) 39 | 40 | let base=''; 41 | 42 | /* 43 | 登录请求方法,与服务端Spring Security的登录接口对接 44 | */ 45 | export const postKeyValueRequest=(url,params)=>{ 46 | return axios({ 47 | method:'post', 48 | url:`${base}${url}`, 49 | data:params, 50 | transformRequest:[function (data) {//处理请求的数据格式 51 | //console.log(data); 52 | let ret=''; 53 | for (let i in data){ 54 | ret+=encodeURIComponent(i)+'='+encodeURIComponent(data[i])+'&' 55 | } 56 | // console.log(ret); 57 | return ret; 58 | }], 59 | headers:{ 60 | 'Content-Type':'application/x-www-form-urlencoded' 61 | } 62 | }); 63 | } 64 | /* 65 | 封装“增加”请求方法——post 66 | */ 67 | export const postRequest=(url,params)=>{ 68 | return axios({ 69 | method:'post', 70 | url:`${base}${url}`, 71 | data:params 72 | }); 73 | } 74 | /* 75 | 封装“修改”请求方法——put 76 | */ 77 | export const putRequest=(url,params)=>{ 78 | return axios({ 79 | method:'put', 80 | url:`${base}${url}`, 81 | data:params 82 | }); 83 | } 84 | /* 85 | 封装“查询”请求方法——get 86 | */ 87 | export const getRequest=(url,params)=>{ 88 | return axios({ 89 | method:'get', 90 | url:`${base}${url}`, 91 | data:params 92 | }); 93 | } 94 | /* 95 | 封装“删除”请求方法——get 96 | */ 97 | export const deleteRequest=(url,params)=>{ 98 | return axios({ 99 | method:'delete', 100 | url:`${base}${url}`, 101 | data:params 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /src/utils/stomp.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | 3 | /* 4 | Stomp Over WebSocket http://www.jmesnil.net/stomp-websocket/doc/ | Apache License V2.0 5 | 6 | Copyright (C) 2010-2013 [Jeff Mesnil](http://jmesnil.net/) 7 | Copyright (C) 2012 [FuseSource, Inc.](http://fusesource.com) 8 | */ 9 | 10 | (function() { 11 | var Byte, Client, Frame, Stomp, 12 | __hasProp = {}.hasOwnProperty, 13 | __slice = [].slice; 14 | 15 | Byte = { 16 | LF: '\x0A', 17 | NULL: '\x00' 18 | }; 19 | 20 | Frame = (function() { 21 | var unmarshallSingle; 22 | 23 | function Frame(command, headers, body) { 24 | this.command = command; 25 | this.headers = headers != null ? headers : {}; 26 | this.body = body != null ? body : ''; 27 | } 28 | 29 | Frame.prototype.toString = function() { 30 | var lines, name, skipContentLength, value, _ref; 31 | lines = [this.command]; 32 | skipContentLength = this.headers['content-length'] === false ? true : false; 33 | if (skipContentLength) { 34 | delete this.headers['content-length']; 35 | } 36 | _ref = this.headers; 37 | for (name in _ref) { 38 | if (!__hasProp.call(_ref, name)) continue; 39 | value = _ref[name]; 40 | lines.push("" + name + ":" + value); 41 | } 42 | if (this.body && !skipContentLength) { 43 | lines.push("content-length:" + (Frame.sizeOfUTF8(this.body))); 44 | } 45 | lines.push(Byte.LF + this.body); 46 | return lines.join(Byte.LF); 47 | }; 48 | 49 | Frame.sizeOfUTF8 = function(s) { 50 | if (s) { 51 | return encodeURI(s).match(/%..|./g).length; 52 | } else { 53 | return 0; 54 | } 55 | }; 56 | 57 | unmarshallSingle = function(data) { 58 | var body, chr, command, divider, headerLines, headers, i, idx, len, line, start, trim, _i, _j, _len, _ref, _ref1; 59 | divider = data.search(RegExp("" + Byte.LF + Byte.LF)); 60 | headerLines = data.substring(0, divider).split(Byte.LF); 61 | command = headerLines.shift(); 62 | headers = {}; 63 | trim = function(str) { 64 | return str.replace(/^\s+|\s+$/g, ''); 65 | }; 66 | _ref = headerLines.reverse(); 67 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 68 | line = _ref[_i]; 69 | idx = line.indexOf(':'); 70 | headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1)); 71 | } 72 | body = ''; 73 | start = divider + 2; 74 | if (headers['content-length']) { 75 | len = parseInt(headers['content-length']); 76 | body = ('' + data).substring(start, start + len); 77 | } else { 78 | chr = null; 79 | for (i = _j = start, _ref1 = data.length; start <= _ref1 ? _j < _ref1 : _j > _ref1; i = start <= _ref1 ? ++_j : --_j) { 80 | chr = data.charAt(i); 81 | if (chr === Byte.NULL) { 82 | break; 83 | } 84 | body += chr; 85 | } 86 | } 87 | return new Frame(command, headers, body); 88 | }; 89 | 90 | Frame.unmarshall = function(datas) { 91 | var frame, frames, last_frame, r; 92 | frames = datas.split(RegExp("" + Byte.NULL + Byte.LF + "*")); 93 | r = { 94 | frames: [], 95 | partial: '' 96 | }; 97 | r.frames = (function() { 98 | var _i, _len, _ref, _results; 99 | _ref = frames.slice(0, -1); 100 | _results = []; 101 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 102 | frame = _ref[_i]; 103 | _results.push(unmarshallSingle(frame)); 104 | } 105 | return _results; 106 | })(); 107 | last_frame = frames.slice(-1)[0]; 108 | if (last_frame === Byte.LF || (last_frame.search(RegExp("" + Byte.NULL + Byte.LF + "*$"))) !== -1) { 109 | r.frames.push(unmarshallSingle(last_frame)); 110 | } else { 111 | r.partial = last_frame; 112 | } 113 | return r; 114 | }; 115 | 116 | Frame.marshall = function(command, headers, body) { 117 | var frame; 118 | frame = new Frame(command, headers, body); 119 | return frame.toString() + Byte.NULL; 120 | }; 121 | 122 | return Frame; 123 | 124 | })(); 125 | 126 | Client = (function() { 127 | var now; 128 | 129 | function Client(ws) { 130 | this.ws = ws; 131 | this.ws.binaryType = "arraybuffer"; 132 | this.counter = 0; 133 | this.connected = false; 134 | this.heartbeat = { 135 | outgoing: 10000, 136 | incoming: 10000 137 | }; 138 | this.maxWebSocketFrameSize = 16 * 1024; 139 | this.subscriptions = {}; 140 | this.partialData = ''; 141 | } 142 | 143 | Client.prototype.debug = function(message) { 144 | var _ref; 145 | return typeof window !== "undefined" && window !== null ? (_ref = window.console) != null ? _ref.log(message) : void 0 : void 0; 146 | }; 147 | 148 | now = function() { 149 | if (Date.now) { 150 | return Date.now(); 151 | } else { 152 | return new Date().valueOf; 153 | } 154 | }; 155 | 156 | Client.prototype._transmit = function(command, headers, body) { 157 | var out; 158 | out = Frame.marshall(command, headers, body); 159 | if (typeof this.debug === "function") { 160 | this.debug(">>> " + out); 161 | } 162 | while (true) { 163 | if (out.length > this.maxWebSocketFrameSize) { 164 | this.ws.send(out.substring(0, this.maxWebSocketFrameSize)); 165 | out = out.substring(this.maxWebSocketFrameSize); 166 | if (typeof this.debug === "function") { 167 | this.debug("remaining = " + out.length); 168 | } 169 | } else { 170 | return this.ws.send(out); 171 | } 172 | } 173 | }; 174 | 175 | Client.prototype._setupHeartbeat = function(headers) { 176 | var serverIncoming, serverOutgoing, ttl, v, _ref, _ref1; 177 | if ((_ref = headers.version) !== Stomp.VERSIONS.V1_1 && _ref !== Stomp.VERSIONS.V1_2) { 178 | return; 179 | } 180 | _ref1 = (function() { 181 | var _i, _len, _ref1, _results; 182 | _ref1 = headers['heart-beat'].split(","); 183 | _results = []; 184 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 185 | v = _ref1[_i]; 186 | _results.push(parseInt(v)); 187 | } 188 | return _results; 189 | })(), serverOutgoing = _ref1[0], serverIncoming = _ref1[1]; 190 | if (!(this.heartbeat.outgoing === 0 || serverIncoming === 0)) { 191 | ttl = Math.max(this.heartbeat.outgoing, serverIncoming); 192 | if (typeof this.debug === "function") { 193 | this.debug("send PING every " + ttl + "ms"); 194 | } 195 | this.pinger = Stomp.setInterval(ttl, (function(_this) { 196 | return function() { 197 | _this.ws.send(Byte.LF); 198 | return typeof _this.debug === "function" ? _this.debug(">>> PING") : void 0; 199 | }; 200 | })(this)); 201 | } 202 | if (!(this.heartbeat.incoming === 0 || serverOutgoing === 0)) { 203 | ttl = Math.max(this.heartbeat.incoming, serverOutgoing); 204 | if (typeof this.debug === "function") { 205 | this.debug("check PONG every " + ttl + "ms"); 206 | } 207 | return this.ponger = Stomp.setInterval(ttl, (function(_this) { 208 | return function() { 209 | var delta; 210 | delta = now() - _this.serverActivity; 211 | if (delta > ttl * 2) { 212 | if (typeof _this.debug === "function") { 213 | _this.debug("did not receive server activity for the last " + delta + "ms"); 214 | } 215 | return _this.ws.close(); 216 | } 217 | }; 218 | })(this)); 219 | } 220 | }; 221 | 222 | Client.prototype._parseConnect = function() { 223 | var args, connectCallback, errorCallback, headers; 224 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 225 | headers = {}; 226 | switch (args.length) { 227 | case 2: 228 | headers = args[0], connectCallback = args[1]; 229 | break; 230 | case 3: 231 | if (args[1] instanceof Function) { 232 | headers = args[0], connectCallback = args[1], errorCallback = args[2]; 233 | } else { 234 | headers.login = args[0], headers.passcode = args[1], connectCallback = args[2]; 235 | } 236 | break; 237 | case 4: 238 | headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3]; 239 | break; 240 | default: 241 | headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3], headers.host = args[4]; 242 | } 243 | return [headers, connectCallback, errorCallback]; 244 | }; 245 | 246 | Client.prototype.connect = function() { 247 | var args, errorCallback, headers, out; 248 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 249 | out = this._parseConnect.apply(this, args); 250 | headers = out[0], this.connectCallback = out[1], errorCallback = out[2]; 251 | if (typeof this.debug === "function") { 252 | this.debug("Opening Web Socket..."); 253 | } 254 | this.ws.onmessage = (function(_this) { 255 | return function(evt) { 256 | var arr, c, client, data, frame, messageID, onreceive, subscription, unmarshalledData, _i, _len, _ref, _results; 257 | data = typeof ArrayBuffer !== 'undefined' && evt.data instanceof ArrayBuffer ? (arr = new Uint8Array(evt.data), typeof _this.debug === "function" ? _this.debug("--- got data length: " + arr.length) : void 0, ((function() { 258 | var _i, _len, _results; 259 | _results = []; 260 | for (_i = 0, _len = arr.length; _i < _len; _i++) { 261 | c = arr[_i]; 262 | _results.push(String.fromCharCode(c)); 263 | } 264 | return _results; 265 | })()).join('')) : evt.data; 266 | _this.serverActivity = now(); 267 | if (data === Byte.LF) { 268 | if (typeof _this.debug === "function") { 269 | _this.debug("<<< PONG"); 270 | } 271 | return; 272 | } 273 | if (typeof _this.debug === "function") { 274 | _this.debug("<<< " + data); 275 | } 276 | unmarshalledData = Frame.unmarshall(_this.partialData + data); 277 | _this.partialData = unmarshalledData.partial; 278 | _ref = unmarshalledData.frames; 279 | _results = []; 280 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 281 | frame = _ref[_i]; 282 | switch (frame.command) { 283 | case "CONNECTED": 284 | if (typeof _this.debug === "function") { 285 | _this.debug("connected to server " + frame.headers.server); 286 | } 287 | _this.connected = true; 288 | _this._setupHeartbeat(frame.headers); 289 | _results.push(typeof _this.connectCallback === "function" ? _this.connectCallback(frame) : void 0); 290 | break; 291 | case "MESSAGE": 292 | subscription = frame.headers.subscription; 293 | onreceive = _this.subscriptions[subscription] || _this.onreceive; 294 | if (onreceive) { 295 | client = _this; 296 | messageID = frame.headers["message-id"]; 297 | frame.ack = function(headers) { 298 | if (headers == null) { 299 | headers = {}; 300 | } 301 | return client.ack(messageID, subscription, headers); 302 | }; 303 | frame.nack = function(headers) { 304 | if (headers == null) { 305 | headers = {}; 306 | } 307 | return client.nack(messageID, subscription, headers); 308 | }; 309 | _results.push(onreceive(frame)); 310 | } else { 311 | _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled received MESSAGE: " + frame) : void 0); 312 | } 313 | break; 314 | case "RECEIPT": 315 | _results.push(typeof _this.onreceipt === "function" ? _this.onreceipt(frame) : void 0); 316 | break; 317 | case "ERROR": 318 | _results.push(typeof errorCallback === "function" ? errorCallback(frame) : void 0); 319 | break; 320 | default: 321 | _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled frame: " + frame) : void 0); 322 | } 323 | } 324 | return _results; 325 | }; 326 | })(this); 327 | this.ws.onclose = (function(_this) { 328 | return function() { 329 | var msg; 330 | msg = "Whoops! Lost connection to " + _this.ws.url; 331 | if (typeof _this.debug === "function") { 332 | _this.debug(msg); 333 | } 334 | _this._cleanUp(); 335 | return typeof errorCallback === "function" ? errorCallback(msg) : void 0; 336 | }; 337 | })(this); 338 | return this.ws.onopen = (function(_this) { 339 | return function() { 340 | if (typeof _this.debug === "function") { 341 | _this.debug('Web Socket Opened...'); 342 | } 343 | headers["accept-version"] = Stomp.VERSIONS.supportedVersions(); 344 | headers["heart-beat"] = [_this.heartbeat.outgoing, _this.heartbeat.incoming].join(','); 345 | return _this._transmit("CONNECT", headers); 346 | }; 347 | })(this); 348 | }; 349 | 350 | Client.prototype.disconnect = function(disconnectCallback, headers) { 351 | if (headers == null) { 352 | headers = {}; 353 | } 354 | this._transmit("DISCONNECT", headers); 355 | this.ws.onclose = null; 356 | this.ws.close(); 357 | this._cleanUp(); 358 | return typeof disconnectCallback === "function" ? disconnectCallback() : void 0; 359 | }; 360 | 361 | Client.prototype._cleanUp = function() { 362 | this.connected = false; 363 | if (this.pinger) { 364 | Stomp.clearInterval(this.pinger); 365 | } 366 | if (this.ponger) { 367 | return Stomp.clearInterval(this.ponger); 368 | } 369 | }; 370 | 371 | Client.prototype.send = function(destination, headers, body) { 372 | if (headers == null) { 373 | headers = {}; 374 | } 375 | if (body == null) { 376 | body = ''; 377 | } 378 | headers.destination = destination; 379 | return this._transmit("SEND", headers, body); 380 | }; 381 | 382 | Client.prototype.subscribe = function(destination, callback, headers) { 383 | var client; 384 | if (headers == null) { 385 | headers = {}; 386 | } 387 | if (!headers.id) { 388 | headers.id = "sub-" + this.counter++; 389 | } 390 | headers.destination = destination; 391 | this.subscriptions[headers.id] = callback; 392 | this._transmit("SUBSCRIBE", headers); 393 | client = this; 394 | return { 395 | id: headers.id, 396 | unsubscribe: function() { 397 | return client.unsubscribe(headers.id); 398 | } 399 | }; 400 | }; 401 | 402 | Client.prototype.unsubscribe = function(id) { 403 | delete this.subscriptions[id]; 404 | return this._transmit("UNSUBSCRIBE", { 405 | id: id 406 | }); 407 | }; 408 | 409 | Client.prototype.begin = function(transaction) { 410 | var client, txid; 411 | txid = transaction || "tx-" + this.counter++; 412 | this._transmit("BEGIN", { 413 | transaction: txid 414 | }); 415 | client = this; 416 | return { 417 | id: txid, 418 | commit: function() { 419 | return client.commit(txid); 420 | }, 421 | abort: function() { 422 | return client.abort(txid); 423 | } 424 | }; 425 | }; 426 | 427 | Client.prototype.commit = function(transaction) { 428 | return this._transmit("COMMIT", { 429 | transaction: transaction 430 | }); 431 | }; 432 | 433 | Client.prototype.abort = function(transaction) { 434 | return this._transmit("ABORT", { 435 | transaction: transaction 436 | }); 437 | }; 438 | 439 | Client.prototype.ack = function(messageID, subscription, headers) { 440 | if (headers == null) { 441 | headers = {}; 442 | } 443 | headers["message-id"] = messageID; 444 | headers.subscription = subscription; 445 | return this._transmit("ACK", headers); 446 | }; 447 | 448 | Client.prototype.nack = function(messageID, subscription, headers) { 449 | if (headers == null) { 450 | headers = {}; 451 | } 452 | headers["message-id"] = messageID; 453 | headers.subscription = subscription; 454 | return this._transmit("NACK", headers); 455 | }; 456 | 457 | return Client; 458 | 459 | })(); 460 | 461 | Stomp = { 462 | VERSIONS: { 463 | V1_0: '1.0', 464 | V1_1: '1.1', 465 | V1_2: '1.2', 466 | supportedVersions: function() { 467 | return '1.1,1.0'; 468 | } 469 | }, 470 | client: function(url, protocols) { 471 | var klass, ws; 472 | if (protocols == null) { 473 | protocols = ['v10.stomp', 'v11.stomp']; 474 | } 475 | klass = Stomp.WebSocketClass || WebSocket; 476 | ws = new klass(url, protocols); 477 | return new Client(ws); 478 | }, 479 | over: function(ws) { 480 | return new Client(ws); 481 | }, 482 | Frame: Frame 483 | }; 484 | 485 | if (typeof exports !== "undefined" && exports !== null) { 486 | exports.Stomp = Stomp; 487 | } 488 | 489 | if (typeof window !== "undefined" && window !== null) { 490 | Stomp.setInterval = function(interval, f) { 491 | return window.setInterval(f, interval); 492 | }; 493 | Stomp.clearInterval = function(id) { 494 | return window.clearInterval(id); 495 | }; 496 | window.Stomp = Stomp; 497 | } else if (!exports) { 498 | self.Stomp = Stomp; 499 | } 500 | 501 | }).call(this); 502 | -------------------------------------------------------------------------------- /src/views/admin/AdminLogin.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 98 | 99 | 102 | -------------------------------------------------------------------------------- /src/views/admin/GroupChatRecord.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 212 | 213 | 221 | -------------------------------------------------------------------------------- /src/views/admin/Home.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 86 | 87 | 124 | -------------------------------------------------------------------------------- /src/views/admin/PrivateChatRecord.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/admin/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 231 | 232 | 235 | -------------------------------------------------------------------------------- /src/views/chat/ChatRoom.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 61 | 62 | 91 | 92 | -------------------------------------------------------------------------------- /src/views/chat/Login.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 262 | 263 | 264 | 296 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | let proxyObj={}; 2 | proxyObj['/ws']={ 3 | ws:true, 4 | target:"ws://localhost:8082" 5 | }; 6 | proxyObj['/']={ 7 | ws:false, 8 | target:'http://localhost:8082', 9 | changeOrigin: true, 10 | pathRewrite:{ 11 | '^/':'' 12 | } 13 | } 14 | module.exports={ 15 | devServer:{ 16 | host:'localhost', 17 | port:8080, 18 | proxy:proxyObj 19 | } 20 | } 21 | --------------------------------------------------------------------------------