31 changed files with 2178 additions and 0 deletions
@ -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 |
|||
@ -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/). |
|||
@ -0,0 +1,5 @@ |
|||
module.exports = { |
|||
presets: [ |
|||
'@vue/cli-plugin-babel/preset' |
|||
] |
|||
} |
|||
@ -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" |
|||
] |
|||
} |
|||
|
After Width: | Height: | Size: 4.2 KiB |
@ -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> |
|||
@ -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> |
|||
@ -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 |
|||
} |
|||
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@ -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 */ |
|||
@ -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 |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
const token = ''; |
|||
|
|||
export default { |
|||
token |
|||
} |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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) |
|||
}) |
|||
@ -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"), |
|||
}, |
|||
] |
|||
} |
|||
] |
|||
|
|||
}); |
|||
@ -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 = []; |
|||
} |
|||
}, |
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -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' |
|||
}) |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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 |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -0,0 +1,16 @@ |
|||
module.exports = { |
|||
|
|||
devServer: { |
|||
proxy: { |
|||
'/api': { |
|||
target: 'http://127.0.0.1:8888', |
|||
changeOrigin: true, |
|||
ws: false, |
|||
pathRewrite: { |
|||
'^/api': '' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue