Browse Source

1.0.0版本(ui)

master
xie.bx 3 years ago
parent
commit
965990d12f
  1. 26
      im-ui/.gitignore
  2. 24
      im-ui/README.md
  3. 5
      im-ui/babel.config.js
  4. 60
      im-ui/package.json
  5. BIN
      im-ui/public/favicon.ico
  6. 17
      im-ui/public/index.html
  7. 30
      im-ui/src/App.vue
  8. 154
      im-ui/src/api/wssocket.js
  9. BIN
      im-ui/src/assets/default_head.png
  10. BIN
      im-ui/src/assets/logo.png
  11. 41
      im-ui/src/assets/style/global.css
  12. 15
      im-ui/src/common/globalApi.js
  13. 5
      im-ui/src/common/globalInfo.js
  14. 118
      im-ui/src/components/AddFriends.vue
  15. 164
      im-ui/src/components/ChatItem.vue
  16. 43
      im-ui/src/components/ChatTime.vue
  17. 114
      im-ui/src/components/FriendsItem.vue
  18. 35
      im-ui/src/components/HeadImage.vue
  19. 33
      im-ui/src/main.js
  20. 44
      im-ui/src/router/index.js
  21. 78
      im-ui/src/store/chatStore.js
  22. 88
      im-ui/src/store/friendsStore.js
  23. 30
      im-ui/src/store/index.js
  24. 18
      im-ui/src/store/userStore.js
  25. 90
      im-ui/src/utils/httpRequest.js
  26. 321
      im-ui/src/view/Chat.vue
  27. 163
      im-ui/src/view/Friends.vue
  28. 155
      im-ui/src/view/Home.vue
  29. 130
      im-ui/src/view/Login.vue
  30. 161
      im-ui/src/view/Register.vue
  31. 16
      im-ui/vue.config.js

26
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

24
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/).

5
im-ui/babel.config.js

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

60
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"
]
}

BIN
im-ui/public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
im-ui/public/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

30
im-ui/src/App.vue

@ -0,0 +1,30 @@
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
<style>
@import './assets/style/global.css';
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
position: absolute;
height: 100%;
width: 100%;
}
</style>

154
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
}

BIN
im-ui/src/assets/default_head.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
im-ui/src/assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

41
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 */

15
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
}

5
im-ui/src/common/globalInfo.js

@ -0,0 +1,5 @@
const token = '';
export default {
token
}

118
im-ui/src/components/AddFriends.vue

@ -0,0 +1,118 @@
<template>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="500px" :before-close="onClose">
<el-input width="200px" placeholder="搜索好友" class="input-with-select" v-model="searchText" @keyup.enter.native="onSearch()">
<el-button slot="append" icon="el-icon-search" @click="onSearch()"></el-button>
</el-input>
<el-scrollbar style="height:600px">
<div v-for="(userInfo) in users" :key="userInfo.id">
<div class="item">
<div class="avatar">
<head-image :url="userInfo.headImage"></head-image>
</div>
<div class="add-friend-text">
<div>{{userInfo.nickName}}</div>
<div :class="userInfo.online ? 'online-status online':'online-status'">{{ userInfo.online?"[在线]":"[离线]"}}</div>
</div>
<el-button type="success" v-show="!isFriend(userInfo.id)" plain @click="onAddFriends(userInfo)">添加</el-button>
<el-button type="info" v-show="isFriend(userInfo.id)" plain disabled>已添加</el-button>
</div>
</div>
</el-scrollbar>
</el-dialog>
</template>
<script>
import HeadImage from './HeadImage.vue'
export default {
name: "addFriends",
components:{HeadImage},
data() {
return {
users: [],
searchText: ""
}
},
props: {
dialogVisible: {
type: Boolean
}
},
methods: {
onClose() {
this.$emit("close");
},
onSearch() {
this.$http({
url: "/api/user/findByNickName",
method: "get",
params: {
nickName: this.searchText
}
}).then((data) => {
this.users = data;
})
},
onAddFriends(userInfo){
this.$http({
url: "/api/friends/add",
method: "post",
params: {
friendId: userInfo.id
}
}).then((data) => {
this.$store.commit("")
this.$message.success("添加成功,对方已成为您的好友");
let friendsInfo = {
friendId:userInfo.id,
friendNickName: userInfo.nickName,
friendHeadImage: userInfo.headImage,
online: userInfo.online
}
this.$store.commit("addFriends",friendsInfo);
})
},
isFriend(userId){
let friendList = this.$store.state.friendsStore.friendsList;
let friend = friendList.find((f)=> f.friendId==userId);
return friend != undefined;
}
},
mounted() {
this.onSearch();
}
}
</script>
<style scoped lang="scss">
.item {
height: 80px;
display: flex;
position: relative;
padding-left: 15px;
align-items: center;
padding-right: 25px;
.add-friend-text {
margin-left: 15px;
line-height: 80px;
flex: 3;
display: flex;
flex-direction: row;
height: 100%;
flex-shrink: 0;
overflow: hidden;
.online-status{
font-size: 12px;
font-weight: 600;
&.online{
color: #5fb878;
}
}
}
}
</style>

164
im-ui/src/components/ChatItem.vue

@ -0,0 +1,164 @@
<template>
<div class="item" :class="active ? 'active' : ''">
<div class="left">
<head-image :url="chat.headImage" :size="40">
</head-image>
<div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div>
</div>
<div class="mid">
<div>{{ chat.showName}}</div>
<div class="msg-text">{{chat.lastContent}}</div>
</div>
<div class="right ">
<div @click.stop="onClickClose()"><i class="el-icon-close close" style="border: none; font-size: 20px;color: black;" title="关闭"></i></div>
<div class="msg-time">
<chat-time :time="chat.lastSendTime"></chat-time>
</div>
</div>
</div>
</template>
<script>
import ChatTime from "./ChatTime.vue";
import HeadImage from './HeadImage.vue';
export default {
name: "chatItem",
components: {
ChatTime,
HeadImage
},
data() {
return {}
},
props: {
chat: {
type: Object
},
active: {
type: Boolean
},
index: {
type: Number
}
},
methods: {
onClickClose(){
this.$emit("del");
}
}
}
</script>
<style scode lang="scss">
.item {
height: 65px;
display: flex;
margin-bottom: 1px;
position: relative;
padding-left: 15px;
align-items: center;
padding-right: 5px;
background-color: #eeeeee;
&:hover {
background-color: #dddddd;
}
&.active {
background-color: #cccccc;
}
&:hover {
.close {
display: block !important;
}
}
.left {
position: relative;
display: flex;
width: 45px;
height: 45px;
.unread-text {
position: absolute;
background-color: #f56c6c;
right: -8px;
top: -8px;
color: white;
border-radius: 30px;
padding: 0 5px;
font-size: 10px;
text-align: center;
white-space: nowrap;
border: 1px solid #f1e5e5;
}
}
.mid {
margin-left: 15px;
flex: 3;
display: flex;
flex-direction: column;
height: 100%;
flex-shrink: 0;
overflow: hidden;
&>div {
display: flex;
justify-content: flex-start;
align-items: center;
flex: 1;
}
.msg-text {
font-size: 14px;
color: #888888;
white-space: nowrap;
}
}
.right {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-end;
height: 100%;
flex-shrink: 0;
overflow: hidden;
&>div {
display: flex;
justify-content: flex-start;
align-items: center;
flex: 1;
}
.close {
width: 1.5rem;
height: 1.5rem;
right: 0;
top: 1rem;
cursor: pointer;
display: none;
}
.msg-time {
font-size: 14px;
color: #888888;
white-space: nowrap;
}
}
}
.active {
background-color: #eeeeee;
}
</style>

43
im-ui/src/components/ChatTime.vue

@ -0,0 +1,43 @@
<template>
<span>{{formatDate}}</span>
</template>
<script>
export default {
name: "chatTime",
data() {
return {}
},
props: {
time: {
type: Number
}
},
computed:{
formatDate(){
console.log(this.time);
let time = new Date(this.time);
let strtime = "";
let curTime = new Date();
let dayDiff =curTime.getDate() - time.getDate() ;
if (time.getDate() === new Date().getDate()) {
strtime = time.getHours() < 9 ? "0" + time.getHours() : time.getHours();
strtime += ":"
strtime += time.getMinutes() < 9 ? "0" + time.getMinutes() : time.getMinutes();
} else if (dayDiff === 1) {
strtime = "昨天";
} else if (dayDiff < 7) {
strtime = `${dayDiff}天前`;
} else {
strtime = time.getMonth()+1+"月"+time.getDate()+"日";
}
console.log(strtime);
return strtime;
}
}
}
</script>
<style>
</style>

114
im-ui/src/components/FriendsItem.vue

@ -0,0 +1,114 @@
<template>
<div class="item" :class="active ? 'active' : ''">
<div class="avatar">
<head-image :src="friendsInfo.friendHeadImage" :size="40"> </head-image>
</div>
<div class="text">
<div>{{ friendsInfo.friendNickName}}</div>
<div :class="online ? 'online-status online':'online-status'">{{ online?"[在线]":"[离线]"}}</div>
</div>
<div class="close" @click.stop="$emit('del',friendsInfo,index)">
<i class="el-icon-close" style="border: none; font-size: 20px;color: black;" title="添加好友"></i>
</div>
</div>
</template>
<script>
import HeadImage from './HeadImage.vue';
export default {
name: "frinedsItem",
components: {HeadImage},
data() {
return {
}
},
props: {
friendsInfo: {
type: Object
},
active:{
type: Boolean
},
index:{
type: Number
}
},
computed:{
online(){
return this.$store.state.friendsStore.friendsList[this.index].online;
}
}
}
</script>
<style scope lang="scss">
.item {
height: 65px;
display: flex;
margin-bottom: 1px;
position: relative;
padding-left: 15px;
align-items: center;
padding-right: 5px;
background-color: #eeeeee;
&:hover {
background-color: #dddddd;
}
&.active{
background-color: #cccccc;
}
.close {
width: 1.5rem;
height: 1.5rem;
right: 10px;
top: 1.825rem;
cursor: pointer;
display: none;
}
&:hover {
.close {
display: block;
}
}
.avatar {
display: flex;
justify-content: center;
align-items: center;
width: 45px;
height: 45px;
}
.text {
margin-left: 15px;
flex: 3;
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
flex-shrink: 0;
overflow: hidden;
&>div {
display: flex;
justify-content: flex-start;
}
.online-status{
font-size: 12px;
font-weight: 600;
&.online{
color: #5fb878;
}
}
}
}
.active {
background-color: #eeeeee;
}
</style>

35
im-ui/src/components/HeadImage.vue

@ -0,0 +1,35 @@
<template>
<div class='img-box'>
<img src="../assets/default_head.png" style="width: 100%;height: 100%;cursor: pointer;" />
</div>
</template>
<script>
export default {
name: "headImage",
data() {
return {}
},
props: {
size: {
type: Number,
default: 50
},
url:{
type: String,
default: '../assets/default_head.png'
}
}
}
</script>
<style scoped>
.img-box {
width: 100%;
height: 100%;
display: inline-block;
border-radius: 3px;
background-color: #c0c4cc;
overflow: hidden;
}
</style>

33
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)
})

44
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"),
},
]
}
]
});

78
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 = [];
}
},
}

88
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;
}
})
}
}
}
}

30
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'
})

18
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;
}
}
}

90
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

321
im-ui/src/view/Chat.vue

@ -0,0 +1,321 @@
<template>
<el-container>
<el-aside width="250px" class="l-chat-box">
<el-header height="60px">
<el-row>
<el-input width="200px" placeholder="搜索" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</el-row>
</el-header>
<el-main>
<div v-for="(chat,index) in $store.state.chatStore.chats" :key="chat.targetId">
<chat-item :chat="chat" :index="index" @click.native="onClickItem(index)" @del="onDelItem(chat,index)"
:active="index === $store.state.chatStore.activeIndex"></chat-item>
</div>
</el-main>
</el-aside>
<el-container class="r-chat-box">
<el-header height="60px">
{{titleName}}
</el-header>
<el-main class="im-chat-main" id="chatScrollBox">
<div class="im-chat-box">
<ul>
<li v-for="item in messages" :key="item.id"
:class="{ 'im-chat-mine': item.sendUserId == $store.state.userStore.userInfo.id }">
<div class="head-image">
<head-image :url="headImage" ></head-image>
</div>
<div class="im-msg-content">
<div class="im-msg-top">
<span>{{showName(item)}}</span>
<chat-time :time="item.sendTime"></chat-time>
</div>
<div class="im-msg-bottom">
<span class="im-msg-text">{{item.content}}</span>
</div>
</div>
</li>
</ul>
</div>
</el-main>
<el-footer height="150px" class="im-chat-footer">
<textarea v-model="messageContent" ref="sendBox" class="textarea" @keyup.enter="onSendMessage()"></textarea>
<div class="im-chat-send">
<el-button type="primary" @click="onSendMessage()">发送</el-button>
</div>
</el-footer>
</el-container>
</el-container>
</template>
<script>
import ChatItem from "../components/ChatItem.vue";
import ChatTime from "../components/ChatTime.vue";
import HeadImage from "../components/HeadImage.vue";
export default {
name: "chat",
components: {
ChatItem,
ChatTime,
HeadImage
},
data() {
return {
searchText: "",
messageContent: ""
}
},
methods: {
onClickItem(index) {
this.$store.commit("activeChat", index);
},
onSendMessage() {
let msgInfo = {
recvUserId: this.$store.state.chatStore.chats[this.$store.state.chatStore.activeIndex].targetId,
content: this.messageContent,
type: 0
}
this.$http({
url: '/api/message/single/send',
method: 'post',
data: msgInfo
}).then((data) => {
this.$message.success("发送成功");
this.messageContent = "";
msgInfo.sendTime = new Date().getTime();
msgInfo.sendUserId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true;
this.$store.commit("insertMessage", msgInfo);
console.log(this.$refs.sendBox)
//
this.$refs.sendBox.focus();
//
this.$nextTick(() => {
const div = document.getElementById("chatScrollBox");
div.scrollTop = div.scrollHeight;
});
})
},
onDelItem(chat,index){
this.$store.commit("removeChat",index);
},
showName(item) {
if (item.sendUserId == this.$store.state.userStore.userInfo.id) {
return this.$store.state.userStore.userInfo.nickName;
} else {
let index = this.$store.state.chatStore.activeIndex;
let chats = this.$store.state.chatStore.chats
return chats[index].showName;
}
}
},
computed: {
messages() {
let index = this.$store.state.chatStore.activeIndex;
let chats = this.$store.state.chatStore.chats
if (index >= 0 && chats.length > 0) {
return chats[index].messages;
}
return [];
},
titleName(){
let index = this.$store.state.chatStore.activeIndex;
let chats = this.$store.state.chatStore.chats
if(index>=0 && chats.length > 0){
let chats = this.$store.state.chatStore.chats;
return chats[index].showName;
}
return "";
},
headImage(){
let index = this.$store.state.chatStore.activeIndex;
let chats = this.$store.state.chatStore.chats
if(index>=0 && chats.length > 0){
let chats = this.$store.state.chatStore.chats;
return chats[index].headImage;
}
return "";
}
}
}
</script>
<style scoped lang="scss">
.el-container {
.l-chat-box {
border: #dddddd solid 1px;
background: #eeeeee;
width: 3rem;
.el-header {
padding: 5px;
background-color: white;
line-height: 50px;
}
.el-main {
padding: 0
}
}
.r-chat-box {
background: white;
border: #dddddd solid 1px;
.el-header {
padding: 5px;
background-color: white;
line-height: 50px;
}
.im-chat-main {
padding: 0;
border: #dddddd solid 1px;
.im-chat-box {
ul {
padding: 10px;
li {
position: relative;
font-size: 0;
margin-bottom: 10px;
padding-left: 60px;
min-height: 68px;
.head-image {
position: absolute;
width: 40px;
height: 40px;
top: 0;
left: 0;
}
.im-msg-content {
display: flex;
flex-direction: column;
.im-msg-top {
display: flex;
flex-wrap: nowrap;
color: #333;
font-size: 14px;
line-height: 20px;
span {
margin-right: 12px;
}
}
.im-msg-bottom {
text-align: left;
.im-msg-text {
position: relative;
line-height: 22px;
margin-top: 10px;
padding: 10px;
background-color: #eeeeee;
border-radius: 3px;
color: #333;
display: inline-block;
font-size: 14px;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #eeeeee transparent transparent;
overflow: hidden;
border-width: 10px;
}
}
}
}
}
}
.im-chat-mine {
text-align: right;
padding-left: 0;
padding-right: 60px;
.head-image {
left: auto;
right: 0;
}
.im-msg-content {
.im-msg-top {
flex-direction: row-reverse;
span {
margin-left: 12px;
margin-right: 0;
}
}
.im-msg-bottom {
text-align: right;
.im-msg-text {
margin-left: 10px;
background-color: #5fb878;
color: #fff;
display: inline-block;
vertical-align: top;
font-size: 14px;
&:after {
left: auto;
right: -10px;
border-top-color: #5fb878;
}
}
}
}
.message-info {
right: 60px !important;
display: inline-block;
}
}
}
}
.im-chat-footer {
display: flex;
flex-direction: column;
padding: 0;
textarea {
box-sizing: border-box;
padding: 5px;
width: 100%;
flex: 1;
resize: none;
background-color: #f8f8f8 !important;
outline-color: rgba(83, 160, 231, 0.61);
}
.im-chat-send {
text-align: right;
padding: 7px;
}
}
}
}
</style>

163
im-ui/src/view/Friends.vue

@ -0,0 +1,163 @@
<template>
<el-container>
<el-aside width="250px" class="l-friend-box">
<el-header height="60px">
<el-row>
<el-col :span="19">
<el-input width="200px" placeholder="搜索好友" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="3">
<el-button plain icon="el-icon-plus" style="border: none; font-size: 20px;color: black;"
title="添加好友" @click="onShowAddFriends"></el-button>
</el-col>
</el-row>
<add-friends :dialogVisible="showAddFriend" @close="onCloseAddFriends" @add="onAddFriend()">
</add-friends>
</el-header>
<el-main>
<div v-for="(friendsInfo,index) in $store.state.friendsStore.friendsList" :key="friendsInfo.id">
<friends-item v-show="friendsInfo.friendNickName.startsWith(searchText)" :friendsInfo="friendsInfo"
:index="index" :active="index === $store.state.friendsStore.activeIndex"
@del="onDelItem(friendsInfo,index)" @click.native="onClickItem(friendsInfo,index)">
</friends-item>
</div>
</el-main>
</el-aside>
<el-container class="r-friend-box">
<div v-show="$store.state.friendsStore.activeIndex>=0">
<div class="user-detail">
<div class="detail-head-image">
<head-image :url="$store.state.friendsStore.activeUserInfo.headImage" ></head-image>
</div>
<div class="info-item">
<el-descriptions title="好友信息" class="description" :column="1">
<el-descriptions-item label="用户名">{{ $store.state.friendsStore.activeUserInfo.userName }}
</el-descriptions-item>
<el-descriptions-item label="昵称">{{ $store.state.friendsStore.activeUserInfo.nickName }}
</el-descriptions-item>
<el-descriptions-item label="备注">好基友</el-descriptions-item>
<el-descriptions-item label="签名">世界这么大我想去看看</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div class="btn-group">
<el-button class="send-btn" @click="onSend()">发消息</el-button>
</div>
</div>
</el-container>
</el-container>
</template>
<script>
import FriendsItem from "../components/FriendsItem.vue";
import AddFriends from "../components/AddFriends.vue";
import HeadImage from "../components/HeadImage.vue";
export default {
name: "friends",
components: {
FriendsItem,
AddFriends,
HeadImage
},
data() {
return {
searchText: "",
showAddFriend: false
}
},
methods: {
onShowAddFriends() {
this.showAddFriend = true;
},
onCloseAddFriends() {
this.showAddFriend = false;
},
onClickItem(friendsInfo, index) {
this.$store.commit("activeFriends", index);
},
onDelItem(friendsInfo, index) {
this.$http({
url: '/api/friends/delete',
method: 'delete',
params: {
friendId: friendsInfo.friendId
}
}).then((data) => {
this.$message.success("删除好友成功");
this.$store.commit("removeFriends", index);
})
},
onSend() {
let userInfo = this.$store.state.friendsStore.activeUserInfo
let chatInfo = {
type: 'single',
targetId: userInfo.id,
showName: userInfo.nickName,
headImage: userInfo.headImage,
};
this.$store.commit("openChat", chatInfo);
this.$store.commit("activeChat", 0);
this.$router.push("/home/chat");
}
}
}
</script>
<style scoped lang="scss">
.el-container {
.l-friend-box {
border: #dddddd solid 1px;
background: #eeeeee;
.el-header {
padding: 5px;
background-color: white;
line-height: 50px;
}
.el-main {
padding: 0;
}
}
.r-friend-box {
.user-detail {
width: 100%;
display: flex;
padding: 50px 10px 10px 50px;
text-align: center;
justify-content: space-around;
.detail-head-image {
width: 200px;
height: 200px;
}
.info-item {
width: 400px;
height: 200px;
background-color: #ffffff;
}
.description {
padding: 20px 20px 0px 20px;
}
}
.btn-group {
text-align: left !important;
padding-left: 100px;
}
}
}
</style>

155
im-ui/src/view/Home.vue

@ -0,0 +1,155 @@
<template>
<el-container>
<el-aside width="80px" class="navi-bar">
<div class="user-head-image" @click="onClickHeadImage">
<head-image :src="$store.state.userStore.userInfo.headImage" > </head-image>
</div>
<el-menu background-color="#333333" text-color="#ddd" style="margin-top: 30px;" >
<el-menu-item title="聊天">
<router-link v-bind:to="'/home/chat'">
<span class="el-icon-chat-dot-round"></span>
</router-link>
</el-menu-item >
<el-menu-item title="好友" >
<router-link v-bind:to="'/home/friends'">
<span class="el-icon-user"></span>
</router-link>
</el-menu-item>
<el-menu-item title="设置" index="/group">
<span class="el-icon-setting"></span>
</el-menu-item>
</el-menu>
<div class="exit-box" @click="onExit()" title="退出">
<span class="el-icon-circle-close"></span>
</div>
</el-aside>
<el-main class="content-box">
<router-view></router-view>
</el-main>
</el-container>
</template>
<script>
import HeadImage from '../components/HeadImage.vue';
export default {
components:{HeadImage},
methods: {
init(userInfo){
this.$store.commit("setUserInfo", userInfo);
this.$store.commit("initStore");
console.log("socket");
this.$wsApi.createWebSocket("ws://localhost:8878/im",this.$store);
this.$wsApi.onopen(()=>{
this.pullUnreadMessage();
});
this.$wsApi.onmessage((e)=>{
console.log(e);
if(e.cmd==1){
//
this.handleSingleMessage(e.data);
}
})
},
pullUnreadMessage(){
this.$http({
url: "/api/message/single/pullUnreadMessage",
method: 'post'
})
},
handleSingleMessage(msg){
//
let f = this.$store.state.friendsStore.friendsList.find((f)=>f.friendId==msg.sendUserId);
let chatInfo = {
type: 'single',
targetId: f.friendId,
showName: f.friendNickName,
headImage: f.friendHeadImage
};
//
this.$store.commit("openChat",chatInfo);
//
this.$store.commit("insertMessage",msg);
},
onExit(){
this.$http({
url: "/api/logout",
method: 'get'
}).then(()=>{
this.$router.push("/login");
})
},
onClickHeadImage(){
this.$message.success(JSON.stringify(this.$store.state.userStore.userInfo));
}
},
mounted() {
this.$http({
url: "/api/user/self",
methods: 'get'
}).then((userInfo) => {
this.init(userInfo);
})
},
unmounted(){
this.$wsApi.closeWebSocket();
}
}
</script>
<style scoped lang="scss">
.navi-bar {
background: #333333;
padding: 10px;
padding-top: 50px;
.user-head-image{
position: relative;
width: 50px;
height: 50px;
}
.el-menu {
border: none;
flex: 1;
.el-menu-item {
margin-top: 20px;
.router-link-exact-active span{
color: white !important;
}
span {
font-size: 24px !important;
color: #aaaaaa;
&:hover{
color: white !important;
}
}
}
}
.exit-box {
position: absolute;
width: 60px;
bottom: 40px;
color: #aaaaaa;
font-size: 24px;
text-align: center;
cursor: pointer;
&:hover{
color: white !important;
}
}
}
.content-box {
padding: 0;
background-color: #E9EEF3;
color: #333;
text-align: center;
}
</style>

130
im-ui/src/view/Login.vue

@ -0,0 +1,130 @@
<template>
<div class="login-view">
<el-form :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px" class="web-ruleForm">
<div class="login-brand">欢迎登陆fly-chat</div>
<el-form-item label="用户名" prop="username">
<el-input type="username" v-model="loginForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')">登陆</el-button>
<el-button @click="resetForm('loginForm')">清空</el-button>
</el-form-item>
<div class="register">
<router-link to="/register">没有账号,前往注册</router-link>
</div>
</el-form>
</div>
</template>
<script>
export default {
name: "login",
data() {
var checkUsername = (rule, value, callback) => {
console.log("checkUsername");
if (!value) {
return callback(new Error('请输入用户名'));
}
callback();
};
var checkPassword = (rule, value, callback) => {
console.log("checkPassword");
if (value === '') {
callback(new Error('请输入密码'));
}
callback();
};
return {
loginForm: {
username: '',
password: ''
},
rules: {
username: [{
validator: checkUsername,
trigger: 'blur'
}],
password: [{
validator: checkPassword,
trigger: 'blur'
}]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$http({
url: "/api/login",
method: 'post',
params: this.loginForm
})
.then((data) => {
this.$message.success("登陆成功");
this.$router.push("/home/chat");
})
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style scoped lang="scss">
.login-view {
position: relative;
display: flex;
justify-content: space-around;
width: 100%;
height: 100%;
background: linear-gradient(#65807a, #182e3c);
background-size: cover;
.web-ruleForm {
height: 340px;
padding: 20px;
margin-top: 150px ;
background: rgba(255,255,255,.75);
box-shadow: 0px 0px 1px #ccc;
border-radius: 5px;
overflow: hidden;
.login-brand {
line-height: 50px;
margin: 30px 0 40px 0;
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
}
.register {
display: flex;
flex-direction: row-reverse;
line-height: 40px;
text-align: left;
padding-left: 20px;
}
}
}
</style>

161
im-ui/src/view/Register.vue

@ -0,0 +1,161 @@
<template>
<el-container class="register-view">
<div>
<el-form :model="registerForm" status-icon :rules="rules" ref="registerForm" label-width="80px" class="web-ruleForm">
<div class="register-brand">欢迎注册成为FLY CHAT用户</div>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="registerForm.userName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickName">
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
<el-button @click="resetForm('registerForm')">清空</el-button>
</el-form-item>
<div class="to-login">
<router-link to="/login">已有账号,前往登录</router-link>
</div>
</el-form>
</div>
</el-container>
</template>
<script>
export default {
name: "login",
data() {
var checkUserName = (rule, value, callback) => {
if (!value) {
return callback(new Error('请输入用户名'));
}
callback();
};
var checkNickName = (rule, value, callback) => {
if (!value) {
return callback(new Error('请输入昵称'));
}
callback();
};
var checkPassword = (rule, value, callback) => {
if (value === '') {
return callback(new Error('请输入密码'));
}
callback();
};
var checkConfirmPassword = (rule, value, callback) => {
console.log("checkConfirmPassword");
if (value === '') {
return callback(new Error('请输入密码'));
}
if (value != this.registerForm.password) {
return callback(new Error('两次密码输入不一致'));
}
callback();
};
return {
registerForm: {
userName: '',
nickName: '',
password: '',
confirmPassword: ''
},
rules: {
userName: [{
validator: checkUserName,
trigger: 'blur'
}],
nickName: [{
validator: checkNickName,
trigger: 'blur'
}],
password: [{
validator: checkPassword,
trigger: 'blur'
}],
confirmPassword: [{
validator: checkConfirmPassword,
trigger: 'blur'
}]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$http({
url: "/api/register",
method: 'post',
data: this.registerForm
})
.then((data) => {
this.$message.success("注册成功!");
})
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style scoped lang="scss">
.register-view {
position: fixed;
display: flex;
justify-content: space-around;
width: 100%;
height: 100%;
background: #466368;
background: linear-gradient(#65807a, #182e3c);
background-size: cover;
-webkit-user-select: none;
background-size: cover;
.web-ruleForm {
width: 500px;
height: 430px;
padding: 20px;
margin-top: 100px ;
background: rgba(255,255,255,.75);
box-shadow: 0px 0px 1px #ccc;
border-radius: 3px;
overflow: hidden;
.register-brand {
line-height: 50px;
margin: 20px 0 30px 0;
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
}
.to-login {
display: flex;
flex-direction: row-reverse;
line-height: 40px;
text-align: left;
padding-left: 20px;
}
}
}
</style>

16
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': ''
}
}
}
}
}
Loading…
Cancel
Save