diff --git a/im-ui/.gitignore b/im-ui/.gitignore new file mode 100644 index 0000000..0b0371a --- /dev/null +++ b/im-ui/.gitignore @@ -0,0 +1,26 @@ +.DS_Store +node_modules +/dist + + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# +package-lock.json diff --git a/im-ui/README.md b/im-ui/README.md new file mode 100644 index 0000000..b29f472 --- /dev/null +++ b/im-ui/README.md @@ -0,0 +1,24 @@ +# web + +## Project setup +``` +npm install +``` + +### Compiles and hot-reloads for development +``` +npm run serve +``` + +### Compiles and minifies for production +``` +npm run build +``` + +### Lints and fixes files +``` +npm run lint +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/im-ui/babel.config.js b/im-ui/babel.config.js new file mode 100644 index 0000000..a673655 --- /dev/null +++ b/im-ui/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/im-ui/package.json b/im-ui/package.json new file mode 100644 index 0000000..a4c049a --- /dev/null +++ b/im-ui/package.json @@ -0,0 +1,60 @@ +{ + "name": "web", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "axios": "^1.1.3", + "core-js": "^3.6.5", + "element-ui": "^2.15.10", + "sass": "^1.47.0", + "sass-loader": "^7.3.1", + "vue": "^2.6.11", + "vue-axios": "^3.5.0", + "vue-router": "^3.3.3", + "vuex": "^3.6.2", + "vuex-persist": "^3.1.3" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.5.12", + "@vue/cli-plugin-eslint": "~4.5.12", + "@vue/cli-service": "~4.5.12", + "babel-eslint": "^10.1.0", + "eslint": "^6.7.2", + "eslint-plugin-vue": "^6.2.2", + "vue-template-compiler": "^2.6.11" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/essential", + "eslint:recommended" + ], + "parserOptions": { + "parser": "babel-eslint" + }, + "rules": { + "no-mixed-spaces-and-tabs": 0, + "generator-star-spacing": "off", + "no-tabs": "off", + "no-unused-vars": "off", + "no-unused-labels": "off", + "no-console": "off", + "vue/no-unused-components": "off", + "no-irregular-whitespace": "off", + "no-debugger": "off" + } + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ] +} diff --git a/im-ui/public/favicon.ico b/im-ui/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/im-ui/public/favicon.ico differ diff --git a/im-ui/public/index.html b/im-ui/public/index.html new file mode 100644 index 0000000..d2c9d76 --- /dev/null +++ b/im-ui/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ + + diff --git a/im-ui/src/App.vue b/im-ui/src/App.vue new file mode 100644 index 0000000..55ef82f --- /dev/null +++ b/im-ui/src/App.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/im-ui/src/api/wssocket.js b/im-ui/src/api/wssocket.js new file mode 100644 index 0000000..8cb13b0 --- /dev/null +++ b/im-ui/src/api/wssocket.js @@ -0,0 +1,154 @@ +var websock = null; +let rec; //断线重连后,延迟5秒重新创建WebSocket连接 rec用来存储延迟请求的代码 +let isConnect = false; //连接标识 避免重复连接 +let isCompleteConnect = false; //完全连接标识(接收到心跳) +let wsurl = ""; +let $store = null; +let messageCallBack = null; +let openCallBack = null; + + +let createWebSocket = (url, store) => { + $store = store; + wsurl = url; + initWebSocket(); +}; + +let initWebSocket = () => { + try { + console.log("初始化WebSocket"); + isCompleteConnect = false; + websock = new WebSocket(wsurl); + websock.onmessage = function(e) { + let msg = JSON.parse(decodeUnicode(e.data)) + if (msg.cmd == 0) { + if(!isCompleteConnect){ + // 第一次上传心跳成功才算连接完成 + isCompleteConnect = true; + openCallBack && openCallBack(); + } + heartCheck.reset(); + } else { + // 其他消息转发出去 + messageCallBack && messageCallBack(JSON.parse(e.data)) + } + } + websock.onclose = function(e) { + console.log('WebSocket连接关闭') + isConnect = false; //断开后修改标识 + reConnect(); + } + websock.onopen = function() { + console.log("WebSocket连接成功"); + isConnect = true; + heartCheck.start() + } + + // 连接发生错误的回调方法 + websock.onerror = function() { + console.log('WebSocket连接发生错误') + isConnect = false; //连接断开修改标识 + reConnect(); + } + } catch (e) { + console.log("尝试创建连接失败"); + reConnect(); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接 + } +}; + +//定义重连函数 +let reConnect = () => { + console.log("尝试重新连接"); + if (isConnect) return; //如果已经连上就不在重连了 + rec && clearTimeout(rec); + rec = setTimeout(function() { // 延迟5秒重连 避免过多次过频繁请求重连 + initWebSocket(wsurl); + }, 5000); +}; +//设置关闭连接 +let closeWebSocket = () => { + websock.close(); +}; +//心跳设置 +var heartCheck = { + timeout: 5000, //每段时间发送一次心跳包 这里设置为20s + timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象) + start: function() { + if(isConnect){ + console.log('发送WebSocket心跳') + let heartBeat = { + cmd: 0, + data: { + userId: $store.state.userStore.userInfo.id + } + }; + websock.send(JSON.stringify(heartBeat)) + } + + }, + + reset: function(){ + clearTimeout(this.timeoutObj); + this.timeoutObj = setTimeout(function() { + heartCheck.start(); + }, this.timeout); + + } +}; + + + +// 实际调用的方法 +function sendMessage(agentData) { + // console.log(globalCallback) + if (websock.readyState === websock.OPEN) { + // 若是ws开启状态 + websock.send(JSON.stringify(agentData)) + } else if (websock.readyState === websock.CONNECTING) { + // 若是 正在开启状态,则等待1s后重新调用 + setTimeout(function() { + sendMessage(agentData) + }, 1000) + } else { + // 若未开启 ,则等待1s后重新调用 + setTimeout(function() { + sendMessage(agentData) + }, 1000) + } +} + + +function onmessage(callback) { + messageCallBack = callback; +} + + +function onopen(callback) { + openCallBack = callback; + if (isCompleteConnect) { + openCallBack(); + } +} + + +function decodeUnicode(str) { + str = str.replace(/\\/g, "%"); + //转换中文 + str = unescape(str); + //将其他受影响的转换回原来 + str = str.replace(/%/g, "\\"); + //对网址的链接进行处理 + str = str.replace(/\\/g, ""); + return str; + +} + + +// 将方法暴露出去 +export { + createWebSocket, + closeWebSocket, + sendMessage, + onmessage, + onopen +} diff --git a/im-ui/src/assets/default_head.png b/im-ui/src/assets/default_head.png new file mode 100644 index 0000000..5f6e18a Binary files /dev/null and b/im-ui/src/assets/default_head.png differ diff --git a/im-ui/src/assets/logo.png b/im-ui/src/assets/logo.png new file mode 100644 index 0000000..f3d2503 Binary files /dev/null and b/im-ui/src/assets/logo.png differ diff --git a/im-ui/src/assets/style/global.css b/im-ui/src/assets/style/global.css new file mode 100644 index 0000000..5e3cd46 --- /dev/null +++ b/im-ui/src/assets/style/global.css @@ -0,0 +1,41 @@ +@charset "UTF-8"; + +html { + height: 100%; + overflow: hidden; + +} + +body { + height: 100%; + margin: 0; + overflow: hidden; + +} + +section { + height: 100%; +} + + + +::-webkit-scrollbar { + width: 6px; + height: 1px; +} + +::-webkit-scrollbar-thumb { + /*滚动条里面小方块*/ + border-radius: 2px; + -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); + background: #535353; +} + +::-webkit-scrollbar-track { + /*滚动条里面轨道*/ + -webkit-box-shadow: inset 0 0 5px transparent; + border-radius: 2px; + background: #ededed; +} + +/*# sourceMappingURL=v-im.cssss.map */ diff --git a/im-ui/src/common/globalApi.js b/im-ui/src/common/globalApi.js new file mode 100644 index 0000000..85dfa93 --- /dev/null +++ b/im-ui/src/common/globalApi.js @@ -0,0 +1,15 @@ +import globalVariable from './globalInfo'; + +function appendToken(url){ + console.log(url); + if(url.indexOf('?')==-1){ + url+="?" + } + url += `access_token=${globalVariable.token}`; + return url; +} + + +export default{ + appendToken +} \ No newline at end of file diff --git a/im-ui/src/common/globalInfo.js b/im-ui/src/common/globalInfo.js new file mode 100644 index 0000000..2c62493 --- /dev/null +++ b/im-ui/src/common/globalInfo.js @@ -0,0 +1,5 @@ +const token = ''; + +export default { + token +} diff --git a/im-ui/src/components/AddFriends.vue b/im-ui/src/components/AddFriends.vue new file mode 100644 index 0000000..9dbbd6b --- /dev/null +++ b/im-ui/src/components/AddFriends.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/im-ui/src/components/ChatItem.vue b/im-ui/src/components/ChatItem.vue new file mode 100644 index 0000000..0d0b50f --- /dev/null +++ b/im-ui/src/components/ChatItem.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/im-ui/src/components/ChatTime.vue b/im-ui/src/components/ChatTime.vue new file mode 100644 index 0000000..60ec149 --- /dev/null +++ b/im-ui/src/components/ChatTime.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/im-ui/src/components/FriendsItem.vue b/im-ui/src/components/FriendsItem.vue new file mode 100644 index 0000000..3194296 --- /dev/null +++ b/im-ui/src/components/FriendsItem.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/im-ui/src/components/HeadImage.vue b/im-ui/src/components/HeadImage.vue new file mode 100644 index 0000000..eebc188 --- /dev/null +++ b/im-ui/src/components/HeadImage.vue @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file diff --git a/im-ui/src/main.js b/im-ui/src/main.js new file mode 100644 index 0000000..8a9008f --- /dev/null +++ b/im-ui/src/main.js @@ -0,0 +1,33 @@ +import Vue from 'vue' +import App from './App' +import router from './router' // 自动扫描index.js +import axios from 'axios' +import VueAxios from 'vue-axios' +import ElementUI from 'element-ui'; +import 'element-ui/lib/theme-chalk/index.css'; +import httpRequest from './utils/httpRequest'; + +import globalApi from './common/globalApi'; +import globalInfo from './common/globalInfo'; +import * as socketApi from './api/wssocket' ; +import store from './store'; + +console.log(store); +Vue.use(ElementUI); + +// 挂载全局 + +Vue.prototype.$wsApi = socketApi; +Vue.prototype.$http = httpRequest // http请求方法 +Vue.prototype.globalApi = globalApi; // 注册全局方法 +Vue.prototype.globalInfo = globalInfo; // 注册全局变量 + +Vue.config.productionTip = false + +new Vue({ + el: '#app', + // 配置路由 + router, + store, + render: h=>h(App) +}) diff --git a/im-ui/src/router/index.js b/im-ui/src/router/index.js new file mode 100644 index 0000000..79e99f8 --- /dev/null +++ b/im-ui/src/router/index.js @@ -0,0 +1,44 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' +import Login from '../view/Login' +import Register from '../view/Register' +import Home from '../view/Home' +// 安装路由 +Vue.use(VueRouter); + +// 配置导出路由 +export default new VueRouter({ + routes: [{ + path: "/", + redirect: "/login" + }, + { + name: "Login", + path: '/login', + component: Login + }, + { + name: "Register", + path: '/register', + component: Register + }, + { + name: "Home", + path: '/home', + component: Home, + children:[ + { + name: "Chat", + path: "/home/chat", + component: () => import("../view/Chat"), + }, + { + name: "Friends", + path: "/home/friends", + component: () => import("../view/Friends"), + }, + ] + } + ] + +}); diff --git a/im-ui/src/store/chatStore.js b/im-ui/src/store/chatStore.js new file mode 100644 index 0000000..62852e9 --- /dev/null +++ b/im-ui/src/store/chatStore.js @@ -0,0 +1,78 @@ +import httpRequest from '../utils/httpRequest.js' + + +export default { + + state: { + activeIndex: -1, + chats: [] + }, + + mutations: { + openChat(state,chatInfo){ + let chat = null; + for(let i in state.chats){ + if(state.chats[i].targetId === chatInfo.targetId){ + + chat = state.chats[i]; + // 放置头部 + state.chats.splice(i,1); + state.chats.unshift(chat); + break; + } + } + // 创建会话 + if (chat == null) { + chat = { + targetId: chatInfo.targetId, + type: chatInfo.type, + showName: chatInfo.showName, + headImage: chatInfo.headImage, + lastContent: "", + lastSendTime: new Date().getTime(), + unreadCount: 0, + messages: [], + }; + state.chats.unshift(chat); + } + + }, + activeChat(state,idx){ + state.activeIndex = idx; + state.chats[idx].unreadCount=0; + }, + removeChat(state,idx){ + state.chats.splice(idx, 1); + if(state.activeIndex >= state.chats.length){ + state.activeIndex = state.chats.length-1; + } + }, + insertMessage(state, msgInfo) { + let targetId = msgInfo.selfSend?msgInfo.recvUserId:msgInfo.sendUserId; + let chat = state.chats.find((chat)=>chat.targetId==targetId); + + chat.lastContent = msgInfo.content; + chat.lastSendTime = msgInfo.sendTime; + chat.messages.push(msgInfo); + // 如果不是当前会话,未读加1 + if(state.activeIndex == -1 || state.chats[state.activeIndex].targetId != targetId){ + chat.unreadCount++; + } + }, + setChatUserInfo(state, userInfo){ + for(let i in state.chats){ + if(state.chats[i].targetId == userInfo.id){ + state.chats[i].headImage = userInfo.headImage; + state.chats[i].showName = userInfo.nickName; + break; + } + } + }, + resetChatStore(state){ + console.log("清空store") + state.activeIndex = -1; + state.chats = []; + } + }, + +} \ No newline at end of file diff --git a/im-ui/src/store/friendsStore.js b/im-ui/src/store/friendsStore.js new file mode 100644 index 0000000..f06fe70 --- /dev/null +++ b/im-ui/src/store/friendsStore.js @@ -0,0 +1,88 @@ +import httpRequest from '../utils/httpRequest.js' + +export default { + + state: { + friendsList: [], + activeIndex: -1, + activeUserInfo: {}, + timer: null + }, + mutations: { + initFriendsStore(state, userInfo) { + httpRequest({ + url: '/api/friends/list', + method: 'get' + }).then((friendsList) => { + this.commit("setFriendsList",friendsList); + this.commit("refreshOnlineStatus"); + }) + }, + setActiveUserInfo(state, userInfo){ + state.activeUserInfo = userInfo; + }, + setFriendsList(state, friendsList) { + state.friendsList = friendsList; + }, + activeFriends(state, index) { + state.activeIndex = index; + httpRequest({ + url: `/api/user/find/${state.friendsList[index].friendId}`, + method: 'get' + }).then((userInfo) => { + this.commit("setActiveUserInfo",userInfo); + }) + }, + removeFriends(state, index) { + state.friendsList.splice(index, 1); + }, + addFriends(state, friendsInfo) { + state.friendsList.push(friendsInfo); + }, + refreshOnlineStatus(state){ + let userIds = []; + state.friendsList.forEach((f)=>{userIds.push(f.friendId)}); + httpRequest({ + url: '/api/user/online', + method: 'get', + params: {userIds: userIds.join(',')} + }).then((onlineIds) => { + this.commit("setOnlineStatus",onlineIds); + }) + + // 30s后重新拉取 + clearTimeout(state.timer); + state.timer = setTimeout(()=>{ + this.commit("refreshOnlineStatus"); + },30000) + }, + setOnlineStatus(state,onlineIds){ + state.friendsList.forEach((f)=>{ + let onlineFriend = onlineIds.find((id)=> f.friendId==id); + f.online = onlineFriend != undefined; + console.log(f.friendNickName+":"+f.online); + }); + + let activeFriend = state.friendsList[state.activeIndex]; + state.friendsList.sort((f1,f2)=>{ + if(f1.online&&!f2.online){ + return -1; + } + if(f2.online&&!f1.online){ + return 1; + } + return 0; + }); + + // 重新排序后,activeIndex指向的好友可能会变化,需要重新指定 + if(state.activeIndex >=0){ + state.friendsList.forEach((f,i)=>{ + if(f.friendId == activeFriend.friendId){ + state.activeIndex = i; + } + }) + } + } + + } +} diff --git a/im-ui/src/store/index.js b/im-ui/src/store/index.js new file mode 100644 index 0000000..b01db4c --- /dev/null +++ b/im-ui/src/store/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import chatStore from './chatStore.js'; +import friendsStore from './friendsStore.js'; +import userStore from './userStore.js'; +import VuexPersistence from 'vuex-persist' + + +const vuexLocal = new VuexPersistence({ + storage: window.localStorage, + modules: ["userStore","chatStore"] +}) + +Vue.use(Vuex) + +export default new Vuex.Store({ + modules: {chatStore,friendsStore,userStore}, + state: { + userInfo: {} + }, + plugins: [vuexLocal.plugin], + mutations: { + initStore(state){ + + this.commit("initFriendsStore"); + } + + }, + strict: process.env.NODE_ENV !== 'production' +}) diff --git a/im-ui/src/store/userStore.js b/im-ui/src/store/userStore.js new file mode 100644 index 0000000..4ad45bd --- /dev/null +++ b/im-ui/src/store/userStore.js @@ -0,0 +1,18 @@ +export default { + + state: { + userInfo: {} + }, + + mutations: { + setUserInfo(state, userInfo) { + // 切换用户后,清理缓存 + if(userInfo.id != state.userInfo.id){ + console.log("用户切换") + this.commit("resetChatStore"); + } + state.userInfo = userInfo; + } + } + +} \ No newline at end of file diff --git a/im-ui/src/utils/httpRequest.js b/im-ui/src/utils/httpRequest.js new file mode 100644 index 0000000..b03e4fa --- /dev/null +++ b/im-ui/src/utils/httpRequest.js @@ -0,0 +1,90 @@ +import axios from 'axios' +import router from '@/router' +import qs from 'qs' +import merge from 'lodash/merge' +import { + Message +} from 'element-ui' + +const http = axios.create({ + timeout: 1000 * 30, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } +}) + +/** + * 请求拦截 + */ +http.interceptors.request.use(config => { + // todo 请求头带上token + return config +}, error => { + return Promise.reject(error) +}) + +/** + * 响应拦截 + */ +http.interceptors.response.use(response => { + if (response.data.code == 200) { + return response.data.data; + } else { + Message({ + message: response.data.message, + type: 'error', + duration: 1500, + customClass: 'element-error-message-zindex' + }) + + if (response.data.code == 401) { + router.replace("/login"); + } + return Promise.reject(response.data) + } +}, error => { + switch (error.response.status) { + case 400: + Message({ + message: error.response.data, + type: 'error', + duration: 1500, + customClass: 'element-error-message-zindex' + }) + break + case 401: + router.replace("/login"); + break + case 405: + Message({ + message: 'http请求方式有误', + type: 'error', + duration: 1500, + customClass: 'element-error-message-zindex' + }) + break + case 404: + case 500: + Message({ + message: '服务器出了点小差,请稍后再试', + type: 'error', + duration: 1500, + customClass: 'element-error-message-zindex' + }) + break + case 501: + Message({ + message: '服务器不支持当前请求所需要的某个功能', + type: 'error', + duration: 1500, + customClass: 'element-error-message-zindex' + }) + break + } + + return Promise.reject(error) +}) + + +export default http diff --git a/im-ui/src/view/Chat.vue b/im-ui/src/view/Chat.vue new file mode 100644 index 0000000..ea196d0 --- /dev/null +++ b/im-ui/src/view/Chat.vue @@ -0,0 +1,321 @@ + + + + + diff --git a/im-ui/src/view/Friends.vue b/im-ui/src/view/Friends.vue new file mode 100644 index 0000000..ba78274 --- /dev/null +++ b/im-ui/src/view/Friends.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/im-ui/src/view/Home.vue b/im-ui/src/view/Home.vue new file mode 100644 index 0000000..db395ed --- /dev/null +++ b/im-ui/src/view/Home.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/im-ui/src/view/Login.vue b/im-ui/src/view/Login.vue new file mode 100644 index 0000000..59300cb --- /dev/null +++ b/im-ui/src/view/Login.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/im-ui/src/view/Register.vue b/im-ui/src/view/Register.vue new file mode 100644 index 0000000..a51d805 --- /dev/null +++ b/im-ui/src/view/Register.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/im-ui/vue.config.js b/im-ui/vue.config.js new file mode 100644 index 0000000..a991c1f --- /dev/null +++ b/im-ui/vue.config.js @@ -0,0 +1,16 @@ +module.exports = { + + devServer: { + proxy: { + '/api': { + target: 'http://127.0.0.1:8888', + changeOrigin: true, + ws: false, + pathRewrite: { + '^/api': '' + } + } + } + } + +}