Browse Source

前端代码格式化(使用vscode默认格式)

master
xsx 1 year ago
parent
commit
d0e684907f
  1. 1
      .gitignore
  2. 6
      README.md
  3. 2
      im-web/src/App.vue
  4. 10
      im-web/src/api/camera.js
  5. 6
      im-web/src/api/date.js
  6. 6
      im-web/src/api/element.js
  7. 4
      im-web/src/api/emotion.js
  8. 2
      im-web/src/api/httpRequest.js
  9. 24
      im-web/src/api/messageType.js
  10. 36
      im-web/src/api/rtcGroupApi.js
  11. 16
      im-web/src/api/rtcPrivateApi.js
  12. 26
      im-web/src/api/webrtc.js
  13. 36
      im-web/src/api/wssocket.js
  14. 216
      im-web/src/components/chat/ChatAtBox.vue
  15. 1332
      im-web/src/components/chat/ChatBox.vue
  16. 18
      im-web/src/components/chat/ChatGroupMember.vue
  17. 20
      im-web/src/components/chat/ChatGroupReaded.vue
  18. 19
      im-web/src/components/chat/ChatGroupSide.vue
  19. 273
      im-web/src/components/chat/ChatHistory.vue
  20. 994
      im-web/src/components/chat/ChatInput.vue
  21. 360
      im-web/src/components/chat/ChatItem.vue
  22. 640
      im-web/src/components/chat/ChatMessageItem.vue
  23. 225
      im-web/src/components/chat/ChatRecord.vue
  24. 118
      im-web/src/components/common/Emotion.vue
  25. 169
      im-web/src/components/common/FileUpload.vue
  26. 102
      im-web/src/components/common/FullImage.vue
  27. 201
      im-web/src/components/common/HeadImage.vue
  28. 26
      im-web/src/components/common/Icp.vue
  29. 98
      im-web/src/components/common/RightMenu.vue
  30. 196
      im-web/src/components/common/UserInfo.vue
  31. 209
      im-web/src/components/friend/AddFriend.vue
  32. 214
      im-web/src/components/friend/FriendItem.vue
  33. 22
      im-web/src/components/group/AddGroupMember.vue
  34. 81
      im-web/src/components/group/GroupItem.vue
  35. 106
      im-web/src/components/group/GroupMember.vue
  36. 18
      im-web/src/components/group/GroupMemberItem.vue
  37. 214
      im-web/src/components/group/GroupMemberSelector.vue
  38. 184
      im-web/src/components/rtc/RtcGroupJoin.vue
  39. 36
      im-web/src/components/rtc/RtcGroupVideo.vue
  40. 185
      im-web/src/components/rtc/RtcPrivateAcceptor.vue
  41. 30
      im-web/src/components/rtc/RtcPrivateVideo.vue
  42. 14
      im-web/src/components/setting/Setting.vue
  43. 2
      im-web/src/main.js
  44. 70
      im-web/src/router/index.js
  45. 8
      im-web/src/store/chatStore.js
  46. 12
      im-web/src/store/configStore.js
  47. 46
      im-web/src/store/friendStore.js
  48. 4
      im-web/src/store/index.js
  49. 24
      im-web/src/store/uiStore.js
  50. 20
      im-web/src/store/userStore.js
  51. 136
      im-web/src/utils/directive/dialogDrag.js
  52. 10
      im-web/src/view/Chat.vue
  53. 320
      im-web/src/view/Friend.vue
  54. 34
      im-web/src/view/Group.vue
  55. 20
      im-web/src/view/Home.vue
  56. 245
      im-web/src/view/Login.vue
  57. 257
      im-web/src/view/Register.vue
  58. BIN
      截图/交流群2.png

1
.gitignore

@ -12,4 +12,3 @@
/im-web/dist/
/im-uniapp/node_modules/
/im-uniapp/package-lock.json
/im-uniapp/unpackage/

6
README.md

@ -122,9 +122,9 @@ https://www.yuque.com/u1475064/mufu2a/vn5u10ephxh9sau8
![输入图片说明](%E6%88%AA%E5%9B%BE/app/2.jpg)
#### 加入交流群
1群目前已满员,扫码进入2群:
![输入图片说明](%E6%88%AA%E5%9B%BE/%E4%BA%A4%E6%B5%81%E7%BE%A42.png)
群1: 741174521(已满)
群2: 937470451(已满)
群3:
欢迎进群与小伙们一起交流, **申请加群前请务必先star哦**

2
im-web/src/App.vue

@ -12,7 +12,6 @@ export default {
</script>
<style lang="scss">
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@ -22,5 +21,4 @@ export default {
color: var(--im-text-color);
font-family: var(--im-font-family);
}
</style>

10
im-web/src/api/camera.js

@ -5,13 +5,13 @@ class ImCamera {
}
}
ImCamera.prototype.isEnable = function() {
ImCamera.prototype.isEnable = function () {
return !!navigator && !!navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia;
}
ImCamera.prototype.openVideo = function() {
ImCamera.prototype.openVideo = function () {
return new Promise((resolve, reject) => {
if(this.stream){
if (this.stream) {
this.close()
}
let constraints = {
@ -38,7 +38,7 @@ ImCamera.prototype.openVideo = function() {
}
ImCamera.prototype.openAudio = function() {
ImCamera.prototype.openAudio = function () {
return new Promise((resolve, reject) => {
let constraints = {
video: false,
@ -61,7 +61,7 @@ ImCamera.prototype.openAudio = function() {
})
}
ImCamera.prototype.close = function() {
ImCamera.prototype.close = function () {
// 停止流
if (this.stream) {
this.stream.getTracks().forEach((track) => {

6
im-web/src/api/date.js

@ -20,8 +20,8 @@ let toTimeText = (timeStamp, simple) => {
} else {
//不属于今年
timeText = formatDateTime(dateTime);
if(simple){
timeText = timeText.substr(2,8);
if (simple) {
timeText = timeText.substr(2, 8);
}
}
return timeText;
@ -58,7 +58,7 @@ let formatDateTime = (date) => {
}
export{
export {
toTimeText,
isYestday,
isYear,

6
im-web/src/api/element.js

@ -16,14 +16,14 @@ let fixLeft = (e) => {
let setTitleTip = (tip) => {
let title = process.env.VUE_APP_NAME;
if(tip){
if (tip) {
title = `(${tip})${title}`;
}
document.title =title;
document.title = title;
}
export default{
export default {
fixTop,
fixLeft,
setTitleTip

4
im-web/src/api/emotion.js

@ -15,7 +15,7 @@ let transform = (content) => {
let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
if(idx==-1){
if (idx == -1) {
return emoText;
}
let url = require(`@/assets/emoji/${idx}.gif`);
@ -25,7 +25,7 @@ let textToImg = (emoText) => {
let textToUrl = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
if(idx==-1){
if (idx == -1) {
return "";
}
let url = require(`@/assets/emoji/${idx}.gif`);

2
im-web/src/api/httpRequest.js

@ -42,7 +42,7 @@ http.interceptors.response.use(async response => {
headers: {
refreshToken: refreshToken
}
}).catch(()=>{
}).catch(() => {
location.href = "/";
})
// 保存token

24
im-web/src/api/messageType.js

@ -1,32 +1,32 @@
// 是否普通消息
let isNormal = function(type){
return type>=0 && type < 10;
let isNormal = function (type) {
return type >= 0 && type < 10;
}
// 是否状态消息
let isStatus = function(type){
return type>=10 && type < 20;
let isStatus = function (type) {
return type >= 10 && type < 20;
}
// 是否提示消息
let isTip = function(type){
return type>=20 && type < 30;
let isTip = function (type) {
return type >= 20 && type < 30;
}
// 操作交互类消息
let isAction = function(type){
return type>=40 && type < 50;
let isAction = function (type) {
return type >= 40 && type < 50;
}
// 单人通话信令
let isRtcPrivate = function(type){
return type>=100 && type < 200;
let isRtcPrivate = function (type) {
return type >= 100 && type < 200;
}
// 多人通话信令
let isRtcGroup = function(type){
return type>=200 && type < 300;
let isRtcGroup = function (type) {
return type >= 200 && type < 300;
}

36
im-web/src/api/rtcGroupApi.js

@ -1,8 +1,8 @@
import http from './httpRequest.js'
class RtcGroupApi {}
class RtcGroupApi { }
RtcGroupApi.prototype.setup = function(groupId, userInfos) {
RtcGroupApi.prototype.setup = function (groupId, userInfos) {
let formData = {
groupId,
userInfos
@ -14,21 +14,21 @@ RtcGroupApi.prototype.setup = function(groupId, userInfos) {
})
}
RtcGroupApi.prototype.accept = function(groupId) {
RtcGroupApi.prototype.accept = function (groupId) {
return http({
url: '/webrtc/group/accept?groupId='+groupId,
url: '/webrtc/group/accept?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.reject = function(groupId) {
RtcGroupApi.prototype.reject = function (groupId) {
return http({
url: '/webrtc/group/reject?groupId='+groupId,
url: '/webrtc/group/reject?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.failed = function(groupId,reason) {
RtcGroupApi.prototype.failed = function (groupId, reason) {
let formData = {
groupId,
reason
@ -41,14 +41,14 @@ RtcGroupApi.prototype.failed = function(groupId,reason) {
}
RtcGroupApi.prototype.join = function(groupId) {
RtcGroupApi.prototype.join = function (groupId) {
return http({
url: '/webrtc/group/join?groupId='+groupId,
url: '/webrtc/group/join?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.invite = function(groupId, userInfos) {
RtcGroupApi.prototype.invite = function (groupId, userInfos) {
let formData = {
groupId,
userInfos
@ -61,7 +61,7 @@ RtcGroupApi.prototype.invite = function(groupId, userInfos) {
}
RtcGroupApi.prototype.offer = function(groupId, userId, offer) {
RtcGroupApi.prototype.offer = function (groupId, userId, offer) {
let formData = {
groupId,
userId,
@ -74,7 +74,7 @@ RtcGroupApi.prototype.offer = function(groupId, userId, offer) {
})
}
RtcGroupApi.prototype.answer = function(groupId, userId, answer) {
RtcGroupApi.prototype.answer = function (groupId, userId, answer) {
let formData = {
groupId,
userId,
@ -87,21 +87,21 @@ RtcGroupApi.prototype.answer = function(groupId, userId, answer) {
})
}
RtcGroupApi.prototype.quit = function(groupId) {
RtcGroupApi.prototype.quit = function (groupId) {
return http({
url: '/webrtc/group/quit?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.cancel = function(groupId) {
RtcGroupApi.prototype.cancel = function (groupId) {
return http({
url: '/webrtc/group/cancel?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
RtcGroupApi.prototype.candidate = function (groupId, userId, candidate) {
let formData = {
groupId,
userId,
@ -114,7 +114,7 @@ RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
})
}
RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) {
RtcGroupApi.prototype.device = function (groupId, isCamera, isMicroPhone) {
let formData = {
groupId,
isCamera,
@ -128,7 +128,7 @@ RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) {
}
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
RtcGroupApi.prototype.candidate = function (groupId, userId, candidate) {
let formData = {
groupId,
userId,
@ -141,7 +141,7 @@ RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
})
}
RtcGroupApi.prototype.heartbeat = function(groupId) {
RtcGroupApi.prototype.heartbeat = function (groupId) {
return http({
url: '/webrtc/group/heartbeat?groupId=' + groupId,
method: 'post'

16
im-web/src/api/rtcPrivateApi.js

@ -3,7 +3,7 @@ import http from './httpRequest.js'
class RtcPrivateApi {
}
RtcPrivateApi.prototype.call = function(uid, mode, offer) {
RtcPrivateApi.prototype.call = function (uid, mode, offer) {
return http({
url: `/webrtc/private/call?uid=${uid}&mode=${mode}`,
method: 'post',
@ -14,7 +14,7 @@ RtcPrivateApi.prototype.call = function(uid, mode, offer) {
})
}
RtcPrivateApi.prototype.accept = function(uid, answer) {
RtcPrivateApi.prototype.accept = function (uid, answer) {
return http({
url: `/webrtc/private/accept?uid=${uid}`,
method: 'post',
@ -26,35 +26,35 @@ RtcPrivateApi.prototype.accept = function(uid, answer) {
}
RtcPrivateApi.prototype.handup = function(uid) {
RtcPrivateApi.prototype.handup = function (uid) {
return http({
url: `/webrtc/private/handup?uid=${uid}`,
method: 'post'
})
}
RtcPrivateApi.prototype.cancel = function(uid) {
RtcPrivateApi.prototype.cancel = function (uid) {
return http({
url: `/webrtc/private/cancel?uid=${uid}`,
method: 'post'
})
}
RtcPrivateApi.prototype.reject = function(uid) {
RtcPrivateApi.prototype.reject = function (uid) {
return http({
url: `/webrtc/private/reject?uid=${uid}`,
method: 'post'
})
}
RtcPrivateApi.prototype.failed = function(uid, reason) {
RtcPrivateApi.prototype.failed = function (uid, reason) {
return http({
url: `/webrtc/private/failed?uid=${uid}&reason=${reason}`,
method: 'post'
})
}
RtcPrivateApi.prototype.sendCandidate = function(uid, candidate) {
RtcPrivateApi.prototype.sendCandidate = function (uid, candidate) {
return http({
url: `/webrtc/private/candidate?uid=${uid}`,
method: 'post',
@ -65,7 +65,7 @@ RtcPrivateApi.prototype.sendCandidate = function(uid, candidate) {
});
}
RtcPrivateApi.prototype.heartbeat = function(uid) {
RtcPrivateApi.prototype.heartbeat = function (uid) {
return http({
url: `/webrtc/private/heartbeat?uid=${uid}`,
method: 'post'

26
im-web/src/api/webrtc.js

@ -6,7 +6,7 @@ class ImWebRtc {
}
}
ImWebRtc.prototype.isEnable = function() {
ImWebRtc.prototype.isEnable = function () {
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window
.mozRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window
@ -16,11 +16,11 @@ ImWebRtc.prototype.isEnable = function() {
return !!window.RTCPeerConnection;
}
ImWebRtc.prototype.init = function(configuration) {
ImWebRtc.prototype.init = function (configuration) {
this.configuration = configuration;
}
ImWebRtc.prototype.setupPeerConnection = function(callback) {
ImWebRtc.prototype.setupPeerConnection = function (callback) {
this.peerConnection = new RTCPeerConnection(this.configuration);
this.peerConnection.ontrack = (e) => {
// 对方的视频流
@ -29,11 +29,11 @@ ImWebRtc.prototype.setupPeerConnection = function(callback) {
}
ImWebRtc.prototype.setStream = function(stream) {
if(this.stream){
ImWebRtc.prototype.setStream = function (stream) {
if (this.stream) {
this.peerConnection.removeStream(this.stream)
}
if(stream){
if (stream) {
stream.getTracks().forEach((track) => {
this.peerConnection.addTrack(track, stream);
});
@ -42,7 +42,7 @@ ImWebRtc.prototype.setStream = function(stream) {
}
ImWebRtc.prototype.onIcecandidate = function(callback) {
ImWebRtc.prototype.onIcecandidate = function (callback) {
this.peerConnection.onicecandidate = (event) => {
// 追踪到候选信息
if (event.candidate) {
@ -51,7 +51,7 @@ ImWebRtc.prototype.onIcecandidate = function(callback) {
}
}
ImWebRtc.prototype.onStateChange = function(callback) {
ImWebRtc.prototype.onStateChange = function (callback) {
// 监听连接状态
this.peerConnection.oniceconnectionstatechange = (event) => {
let state = event.target.iceConnectionState;
@ -60,7 +60,7 @@ ImWebRtc.prototype.onStateChange = function(callback) {
};
}
ImWebRtc.prototype.createOffer = function() {
ImWebRtc.prototype.createOffer = function () {
return new Promise((resolve, reject) => {
const offerParam = {};
offerParam.offerToRecieveAudio = 1;
@ -78,7 +78,7 @@ ImWebRtc.prototype.createOffer = function() {
}
ImWebRtc.prototype.createAnswer = function(offer) {
ImWebRtc.prototype.createAnswer = function (offer) {
return new Promise((resolve, reject) => {
// 设置远端的sdp
this.setRemoteDescription(offer);
@ -97,17 +97,17 @@ ImWebRtc.prototype.createAnswer = function(offer) {
});
}
ImWebRtc.prototype.setRemoteDescription = function(offer) {
ImWebRtc.prototype.setRemoteDescription = function (offer) {
// 设置对方的sdp信息
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
}
ImWebRtc.prototype.addIceCandidate = function(candidate) {
ImWebRtc.prototype.addIceCandidate = function (candidate) {
// 添加对方的候选人信息
this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
ImWebRtc.prototype.close = function(uid) {
ImWebRtc.prototype.close = function (uid) {
// 关闭RTC连接
if (this.peerConnection) {
this.peerConnection.close();

36
im-web/src/api/wssocket.js

@ -1,19 +1,19 @@
var websock = null;
let rec; //断线重连后,延迟5秒重新创建WebSocket连接 rec用来存储延迟请求的代码
let isConnect = false; //连接标识 避免重复连接
let connectCallBack= null;
let connectCallBack = null;
let messageCallBack = null;
let closeCallBack = null
let connect = (wsurl,accessToken) => {
let connect = (wsurl, accessToken) => {
try {
if (isConnect) {
return;
}
console.log("连接WebSocket");
websock = new WebSocket(wsurl);
websock.onmessage = function(e) {
websock.onmessage = function (e) {
let sendInfo = JSON.parse(e.data)
if (sendInfo.cmd == 0) {
heartCheck.start()
@ -25,16 +25,16 @@ let connect = (wsurl,accessToken) => {
heartCheck.reset();
} else {
// 其他消息转发出去
console.log("收到消息:",sendInfo);
console.log("收到消息:", sendInfo);
messageCallBack && messageCallBack(sendInfo.cmd, sendInfo.data)
}
}
websock.onclose = function(e) {
websock.onclose = function (e) {
console.log('WebSocket连接关闭')
isConnect = false; //断开后修改标识
closeCallBack && closeCallBack(e);
}
websock.onopen = function() {
websock.onopen = function () {
console.log("WebSocket连接成功");
isConnect = true;
// 发送登录命令
@ -48,27 +48,27 @@ let connect = (wsurl,accessToken) => {
}
// 连接发生错误的回调方法
websock.onerror = function() {
websock.onerror = function () {
console.log('WebSocket连接发生错误')
isConnect = false; //连接断开修改标识
reconnect(wsurl,accessToken);
reconnect(wsurl, accessToken);
}
} catch (e) {
console.log("尝试创建连接失败");
reconnect(wsurl,accessToken); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接
reconnect(wsurl, accessToken); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接
}
};
//定义重连函数
let reconnect = (wsurl,accessToken) => {
let reconnect = (wsurl, accessToken) => {
console.log("尝试重新连接");
if (isConnect){
if (isConnect) {
//如果已经连上就不在重连了
return;
}
rec && clearTimeout(rec);
rec = setTimeout(function() { // 延迟5秒重连 避免过多次过频繁请求重连
connect(wsurl,accessToken);
rec = setTimeout(function () { // 延迟5秒重连 避免过多次过频繁请求重连
connect(wsurl, accessToken);
}, 15000);
};
//设置关闭连接
@ -81,7 +81,7 @@ let close = (code) => {
let heartCheck = {
timeout: 5000, //每段时间发送一次心跳包 这里设置为20s
timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
start: function() {
start: function () {
if (isConnect) {
console.log('发送WebSocket心跳')
let heartBeat = {
@ -92,9 +92,9 @@ let heartCheck = {
}
},
reset: function() {
reset: function () {
clearTimeout(this.timeoutObj);
this.timeoutObj = setTimeout(function() {
this.timeoutObj = setTimeout(function () {
heartCheck.start();
}, this.timeout);
@ -110,12 +110,12 @@ let sendMessage = (agentData) => {
websock.send(JSON.stringify(agentData))
} else if (websock.readyState === websock.CONNECTING) {
// 若是 正在开启状态,则等待1s后重新调用
setTimeout(function() {
setTimeout(function () {
sendMessage(agentData)
}, 1000)
} else {
// 若未开启 ,则等待1s后重新调用
setTimeout(function() {
setTimeout(function () {
sendMessage(agentData)
}, 1000)
}

216
im-web/src/components/chat/ChatAtBox.vue

@ -1,131 +1,131 @@
<template>
<el-scrollbar v-show="show&&showMembers.length" ref="scrollBox" class="group-member-choose"
:style="{'left':pos.x+'px','top':pos.y-300+'px'}">
<div v-for="(member,idx) in showMembers" :key="member.id">
<chat-group-member :member="member" :height="40" :active='activeIdx==idx'
@click.native="onSelectMember(member)"></chat-group-member>
<el-scrollbar v-show="show && showMembers.length" ref="scrollBox" class="group-member-choose"
:style="{ 'left': pos.x + 'px', 'top': pos.y - 300 + 'px' }">
<div v-for="(member, idx) in showMembers" :key="member.id">
<chat-group-member :member="member" :height="40" :active='activeIdx == idx'
@click.native="onSelectMember(member)"></chat-group-member>
</div>
</el-scrollbar>
</template>
<script>
import ChatGroupMember from "./ChatGroupMember.vue";
export default {
name: "chatAtBox",
components: {
ChatGroupMember
import ChatGroupMember from "./ChatGroupMember.vue";
export default {
name: "chatAtBox",
components: {
ChatGroupMember
},
props: {
searchText: {
type: String,
default: ""
},
props: {
searchText: {
type: String,
default: ""
},
ownerId: {
type: Number,
ownerId: {
type: Number,
},
members: {
type: Array
}
},
data() {
return {
show: false,
pos: {
x: 0,
y: 0
},
members: {
type: Array
activeIdx: 0,
showMembers: []
};
},
methods: {
init() {
this.$refs.scrollBox.wrap.scrollTop = 0;
this.showMembers = [];
let userId = this.$store.state.userStore.userInfo.id;
let name = "全体成员";
if (this.ownerId == userId && name.startsWith(this.searchText)) {
this.showMembers.push({
userId: -1,
showNickName: name
})
}
this.members.forEach((m) => {
if (m.userId != userId && !m.quit && m.showNickName.startsWith(this.searchText)) {
this.showMembers.push(m);
}
})
this.activeIdx = this.showMembers.length > 0 ? 0 : -1;
},
data() {
return {
show: false,
pos: {
x: 0,
y: 0
},
activeIdx: 0,
showMembers: []
};
open(pos) {
this.show = true;
this.pos = pos;
this.init();
},
methods: {
init() {
this.$refs.scrollBox.wrap.scrollTop = 0;
this.showMembers = [];
let userId = this.$store.state.userStore.userInfo.id;
let name = "全体成员";
if (this.ownerId == userId && name.startsWith(this.searchText)) {
this.showMembers.push({
userId: -1,
showNickName: name
})
}
this.members.forEach((m) => {
if (m.userId != userId && !m.quit && m.showNickName.startsWith(this.searchText)) {
this.showMembers.push(m);
}
})
this.activeIdx = this.showMembers.length > 0 ? 0: -1;
},
open(pos) {
this.show = true;
this.pos = pos;
this.init();
},
close() {
this.show = false;
},
moveUp() {
if (this.activeIdx > 0) {
this.activeIdx--;
this.scrollToActive()
}
},
moveDown() {
if (this.activeIdx < this.showMembers.length - 1) {
this.activeIdx++;
this.scrollToActive()
}
},
select() {
if (this.activeIdx >= 0) {
this.onSelectMember(this.showMembers[this.activeIdx])
}
this.close();
},
scrollToActive() {
if (this.activeIdx * 35 - this.$refs.scrollBox.wrap.clientHeight > this.$refs.scrollBox.wrap.scrollTop) {
this.$refs.scrollBox.wrap.scrollTop += 140;
if (this.$refs.scrollBox.wrap.scrollTop > this.$refs.scrollBox.wrap.scrollHeight) {
this.$refs.scrollBox.wrap.scrollTop = this.$refs.scrollBox.wrap.scrollHeight
}
}
if (this.activeIdx * 35 < this.$refs.scrollBox.wrap.scrollTop) {
this.$refs.scrollBox.wrap.scrollTop -= 140;
if (this.$refs.scrollBox.wrap.scrollTop < 0) {
this.$refs.scrollBox.wrap.scrollTop = 0;
}
}
},
onSelectMember(member) {
this.$emit("select", member);
this.show = false;
close() {
this.show = false;
},
moveUp() {
if (this.activeIdx > 0) {
this.activeIdx--;
this.scrollToActive()
}
},
computed: {
isOwner() {
return this.$store.state.userStore.userInfo.id == this.ownerId;
moveDown() {
if (this.activeIdx < this.showMembers.length - 1) {
this.activeIdx++;
this.scrollToActive()
}
},
watch: {
searchText: {
handler(newText, oldText) {
this.init();
select() {
if (this.activeIdx >= 0) {
this.onSelectMember(this.showMembers[this.activeIdx])
}
this.close();
},
scrollToActive() {
if (this.activeIdx * 35 - this.$refs.scrollBox.wrap.clientHeight > this.$refs.scrollBox.wrap.scrollTop) {
this.$refs.scrollBox.wrap.scrollTop += 140;
if (this.$refs.scrollBox.wrap.scrollTop > this.$refs.scrollBox.wrap.scrollHeight) {
this.$refs.scrollBox.wrap.scrollTop = this.$refs.scrollBox.wrap.scrollHeight
}
}
if (this.activeIdx * 35 < this.$refs.scrollBox.wrap.scrollTop) {
this.$refs.scrollBox.wrap.scrollTop -= 140;
if (this.$refs.scrollBox.wrap.scrollTop < 0) {
this.$refs.scrollBox.wrap.scrollTop = 0;
}
}
},
onSelectMember(member) {
this.$emit("select", member);
this.show = false;
}
},
computed: {
isOwner() {
return this.$store.state.userStore.userInfo.id == this.ownerId;
}
},
watch: {
searchText: {
handler(newText, oldText) {
this.init();
}
}
}
}
</script>
<style scoped lang="scss">
.group-member-choose {
position: fixed;
width: 200px;
height: 300px;
//border: 1px solid #53a0e79c;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
}
.group-member-choose {
position: fixed;
width: 200px;
height: 300px;
//border: 1px solid #53a0e79c;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
}
</style>

1332
im-web/src/components/chat/ChatBox.vue

File diff suppressed because it is too large

18
im-web/src/components/chat/ChatGroupMember.vue

@ -1,9 +1,9 @@
<template>
<div class="chat-group-member" :class="active?'active':''" :style="{'height':height+'px'}">
<div class="chat-group-member" :class="active ? 'active' : ''" :style="{ 'height': height + 'px' }">
<div class="member-avatar">
<head-image :size="headImageSize" :name="member.showNickName" :url="member.headImage"> </head-image>
</div>
<div class="member-name" :style="{'line-height':height+'px'}">
<div class="member-name" :style="{ 'line-height': height + 'px' }">
<div>{{ member.showNickName }}</div>
</div>
</div>
@ -22,17 +22,17 @@ export default {
type: Object,
required: true
},
height:{
height: {
type: Number,
default: 50
},
active: {
type: Boolean,
default: false
}
active: {
type: Boolean,
default: false
}
},
computed:{
headImageSize(){
computed: {
headImageSize() {
return Math.ceil(this.height * 0.75)
}
}

20
im-web/src/components/chat/ChatGroupReaded.vue

@ -57,9 +57,9 @@ export default {
groupMembers: {
type: Array
},
msgInfo: {
type: Object
}
msgInfo: {
type: Object
}
},
methods: {
close() {
@ -100,16 +100,16 @@ export default {
//
if (userIds.find(userId => member.userId == userId)) {
this.readedMembers.push(member);
} else {
} else {
this.unreadMembers.push(member);
}
})
//
this.$store.commit("updateMessage", {
id: this.msgInfo.id,
groupId: this.msgInfo.groupId,
readedCount: this.readedMembers.length
})
//
this.$store.commit("updateMessage", {
id: this.msgInfo.id,
groupId: this.msgInfo.groupId,
readedCount: this.readedMembers.length
})
})
}
}

19
im-web/src/components/chat/ChatGroupSide.vue

@ -8,18 +8,16 @@
<div class="group-side-scrollbar">
<div v-show="!group.quit" class="group-side-member-list">
<div class="group-side-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember=true">
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember = true">
<i class="el-icon-plus"></i>
</div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers"
@reload="$emit('reload')"
@close="showAddGroupMember=false"></add-group-member>
@reload="$emit('reload')" @close="showAddGroupMember = false"></add-group-member>
</div>
<div v-for="(member) in groupMembers" :key="member.id">
<group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)"
:member="member"
:showDel="false"></group-member>
:member="member" :showDel="false"></group-member>
</div>
</div>
<el-divider v-if="!group.quit" content-position="center"></el-divider>
@ -34,16 +32,14 @@
<el-input v-model="group.notice" disabled type="textarea" maxlength="1024"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="group.remarkGroupName" :disabled="!editing"
maxlength="20"></el-input>
<el-input v-model="group.remarkGroupName" :disabled="!editing" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20"
></el-input>
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20"></el-input>
</el-form-item>
<div v-show="!group.quit" class="btn-group">
<el-button v-if="editing" type="success" @click="onSaveGroup()">保存</el-button>
<el-button v-if="!editing" type="primary" @click="editing=!editing">编辑</el-button>
<el-button v-if="!editing" type="primary" @click="editing = !editing">编辑</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
</div>
</el-form>
@ -217,7 +213,8 @@ export default {
}
}
.el-input__inner, .el-textarea__inner {
.el-input__inner,
.el-textarea__inner {
color: var(--im-text-color) !important;
}

273
im-web/src/components/chat/ChatHistory.vue

@ -1,12 +1,11 @@
<template>
<el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="onClose">
<div class="chat-history" v-loading="loading"
element-loading-text="拼命加载中">
<el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar" >
<el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="onClose">
<div class="chat-history" v-loading="loading" element-loading-text="拼命加载中">
<el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar">
<ul>
<li v-for="(msgInfo,idx) in messages" :key="idx">
<chat-message-item :mode="2" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
:msgInfo="msgInfo" :menu="false">
<li v-for="(msgInfo, idx) in messages" :key="idx">
<chat-message-item :mode="2" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" :msgInfo="msgInfo" :menu="false">
</chat-message-item>
</li>
</ul>
@ -16,155 +15,157 @@
</template>
<script>
import ChatMessageItem from './ChatMessageItem.vue';
import ChatMessageItem from './ChatMessageItem.vue';
export default {
name: 'chatHistory',
components: {
ChatMessageItem
export default {
name: 'chatHistory',
components: {
ChatMessageItem
},
props: {
visible: {
type: Boolean
},
props: {
visible: {
type: Boolean
},
chat: {
type: Object
},
friend: {
type: Object
},
group: {
type: Object
},
groupMembers: {
type: Array,
}
chat: {
type: Object
},
data() {
return {
page: 1,
size: 10,
messages: [],
loadAll: false,
loading: false,
lastScrollTime: new Date()
}
friend: {
type: Object
},
methods: {
onClose() {
this.page = 1;
this.messages = [];
this.loadAll = false;
this.$emit('close');
},
onScroll() {
let high = this.$refs.scrollbar.$refs.wrap.scrollTop; //
let timeDiff = new Date().getTime() - this.lastScrollTime.getTime();
if ( high < 30 && timeDiff>500) {
this.lastScrollTime = new Date();
this.loadMessages();
group: {
type: Object
},
groupMembers: {
type: Array,
}
},
data() {
return {
page: 1,
size: 10,
messages: [],
loadAll: false,
loading: false,
lastScrollTime: new Date()
}
},
methods: {
onClose() {
this.page = 1;
this.messages = [];
this.loadAll = false;
this.$emit('close');
},
onScroll() {
let high = this.$refs.scrollbar.$refs.wrap.scrollTop; //
let timeDiff = new Date().getTime() - this.lastScrollTime.getTime();
if (high < 30 && timeDiff > 500) {
this.lastScrollTime = new Date();
this.loadMessages();
}
},
loadMessages() {
if (this.loadAll) {
return this.$message.success("已到达顶部");
}
let param = {
page: this.page++,
size: this.size
}
if (this.chat.type == 'GROUP') {
param.groupId = this.group.id;
} else {
param.friendId = this.friend.id;
}
this.loading = true;
this.$http({
url: this.histroyAction,
method: 'get',
params: param
}).then(messages => {
messages.forEach(m => this.messages.unshift(m));
this.loading = false;
if (messages.length < this.size) {
this.loadAll = true;
}
},
loadMessages() {
if(this.loadAll){
return this.$message.success("已到达顶部");
}
let param = {
page: this.page++,
size: this.size
}
if (this.chat.type == 'GROUP') {
param.groupId = this.group.id;
} else {
param.friendId = this.friend.id;
}
this.loading = true;
this.$http({
url: this.histroyAction,
method: 'get',
params: param
}).then(messages => {
messages.forEach(m => this.messages.unshift(m));
this.loading = false;
if(messages.length <this.size){
this.loadAll = true;
}
this.refreshScrollPos();
}).catch(()=>{
this.loading = false;
})
},
showName(msgInfo) {
if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.showNickName : "";
} else {
return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName
}
},
headImage(msgInfo) {
if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.headImage : "";
} else {
return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
}
},
refreshScrollPos(){
let scrollWrap = this.$refs.scrollbar.$refs.wrap;
let scrollHeight = scrollWrap.scrollHeight;
let scrollTop = scrollWrap.scrollTop;
this.$nextTick(() => {
let offsetTop = scrollWrap.scrollHeight - scrollHeight;
scrollWrap.scrollTop = scrollTop + offsetTop;
//
if(scrollWrap.scrollHeight == scrollHeight){
this.loadMessages();
}
});
this.refreshScrollPos();
}).catch(() => {
this.loading = false;
})
},
showName(msgInfo) {
if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.showNickName : "";
} else {
return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName
}
},
computed: {
mine() {
return this.$store.state.userStore.userInfo;
},
histroyAction() {
return `/message/${this.chat.type.toLowerCase()}/history`;
headImage(msgInfo) {
if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.headImage : "";
} else {
return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
}
},
watch: {
visible: {
handler(newValue, oldValue) {
if (newValue) {
this.loadMessages();
this.$nextTick(() => {
document.getElementById('historyScrollbar').addEventListener("mousewheel", this.onScroll,true);
});
}
refreshScrollPos() {
let scrollWrap = this.$refs.scrollbar.$refs.wrap;
let scrollHeight = scrollWrap.scrollHeight;
let scrollTop = scrollWrap.scrollTop;
this.$nextTick(() => {
let offsetTop = scrollWrap.scrollHeight - scrollHeight;
scrollWrap.scrollTop = scrollTop + offsetTop;
//
if (scrollWrap.scrollHeight == scrollHeight) {
this.loadMessages();
}
});
}
},
computed: {
mine() {
return this.$store.state.userStore.userInfo;
},
histroyAction() {
return `/message/${this.chat.type.toLowerCase()}/history`;
}
},
watch: {
visible: {
handler(newValue, oldValue) {
if (newValue) {
this.loadMessages();
this.$nextTick(() => {
document.getElementById('historyScrollbar').addEventListener("mousewheel", this.onScroll, true);
});
}
}
}
}
}
</script>
<style lang="scss">
.chat-history {
display: flex;
height: 100%;
.chat-history {
display: flex;
height: 100%;
.chat-history-scrollbar {
flex: 1;
.el-scrollbar__thumb {
background-color: #555555;
}
ul {
padding: 20px;
.chat-history-scrollbar {
flex: 1;
li {
list-style-type: none;
}
.el-scrollbar__thumb {
background-color: #555555;
}
ul {
padding: 20px;
li {
list-style-type: none;
}
}
}
}
</style>

994
im-web/src/components/chat/ChatInput.vue

File diff suppressed because it is too large

360
im-web/src/components/chat/ChatItem.vue

@ -2,230 +2,228 @@
<div class="chat-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="chat-left">
<head-image :url="chat.headImage" :name="chat.showName" :size="42"
:id="chat.type=='PRIVATE'?chat.targetId:0" :isShowUserInfo="false"></head-image>
<div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div>
:id="chat.type == 'PRIVATE' ? chat.targetId : 0" :isShowUserInfo="false"></head-image>
<div v-show="chat.unreadCount > 0" class="unread-text">{{ chat.unreadCount }}</div>
</div>
<div class="chat-right">
<div class="chat-name">
<div class="chat-name-text">
<div>{{chat.showName}}</div>
<el-tag v-if="chat.type=='GROUP'" size="mini" effect="dark"></el-tag>
</div>
<div class="chat-time-text">{{showTime}}</div>
</div>
<div class="chat-name-text">
<div>{{ chat.showName }}</div>
<el-tag v-if="chat.type == 'GROUP'" size="mini" effect="dark"></el-tag>
</div>
<div class="chat-time-text">{{ showTime }}</div>
</div>
<div class="chat-content">
<div class="chat-at-text">{{atText}}</div>
<div class="chat-send-name" v-show="isShowSendName">{{chat.sendNickName+':&nbsp;'}}</div>
<div class="chat-at-text">{{ atText }}</div>
<div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div>
<div class="chat-content-text" v-html="$emo.transform(chat.lastContent)"></div>
</div>
</div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" @close="rightMenu.show=false"
@select="onSelectMenu"></right-menu>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
</div>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
import RightMenu from '../common/RightMenu.vue';
export default {
name: "chatItem",
components: {
HeadImage,
RightMenu
},
data() {
return {
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
},
items: [{
key: 'TOP',
name: '置顶',
icon: 'el-icon-top'
}, {
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
}]
}
import HeadImage from '../common/HeadImage.vue';
import RightMenu from '../common/RightMenu.vue';
export default {
name: "chatItem",
components: {
HeadImage,
RightMenu
},
data() {
return {
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
},
items: [{
key: 'TOP',
name: '置顶',
icon: 'el-icon-top'
}, {
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
}]
}
}
},
props: {
chat: {
type: Object
},
props: {
chat: {
type: Object
},
active: {
type: Boolean
},
index: {
type: Number
}
active: {
type: Boolean
},
index: {
type: Number
}
},
methods: {
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
methods: {
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
}
},
computed: {
isShowSendName() {
if (!this.chat.sendNickName) {
return false;
}
let size = this.chat.messages.length;
if (size == 0) {
return false;
}
//
let lastMsg = this.chat.messages[size - 1];
return this.$msgType.isNormal(lastMsg.type)
},
computed: {
isShowSendName() {
if (!this.chat.sendNickName) {
return false;
}
let size = this.chat.messages.length;
if (size == 0) {
return false;
}
//
let lastMsg = this.chat.messages[size - 1];
return this.$msgType.isNormal(lastMsg.type)
},
showTime() {
return this.$date.toTimeText(this.chat.lastSendTime, true)
},
atText() {
if (this.chat.atMe) {
return "[有人@我]"
} else if (this.chat.atAll) {
return "[@全体成员]"
}
return "";
showTime() {
return this.$date.toTimeText(this.chat.lastSendTime, true)
},
atText() {
if (this.chat.atMe) {
return "[有人@我]"
} else if (this.chat.atAll) {
return "[@全体成员]"
}
return "";
}
}
}
</script>
<style lang="scss">
.chat-item {
height: 50px;
display: flex;
.chat-item {
height: 50px;
display: flex;
position: relative;
padding: 5px 10px;
align-items: center;
background-color: var(--im-background);
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: var(--im-background-active);
}
&.active {
background-color: var(--im-background-active-dark);
}
.chat-left {
position: relative;
padding: 5px 10px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--im-background);
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: var(--im-background-active);
}
&.active {
background-color: var(--im-background-active-dark);
.unread-text {
position: absolute;
background-color: #f56c6c;
right: -4px;
top: -8px;
color: white;
border-radius: 30px;
padding: 1px 5px;
font-size: 10px;
text-align: center;
white-space: nowrap;
border: 1px solid #f1e5e5;
}
}
.chat-left {
position: relative;
display: flex;
justify-content: center;
align-items: center;
.unread-text {
position: absolute;
background-color: #f56c6c;
right: -4px;
top: -8px;
color: white;
border-radius: 30px;
padding: 1px 5px;
font-size: 10px;
text-align: center;
white-space: nowrap;
border: 1px solid #f1e5e5;
}
}
.chat-right {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 10px;
text-align: left;
overflow: hidden;
.chat-right {
flex: 1;
.chat-name {
display: flex;
flex-direction: column;
padding-left: 10px;
text-align: left;
overflow: hidden;
line-height: 20px;
height: 20px;
.chat-name {
.chat-name-text {
flex: 1;
display: flex;
line-height: 20px;
height: 20px;
.chat-name-text {
flex: 1;
display: flex;
align-items: center;
font-size: var(--im-font-size);
white-space: nowrap;
overflow: hidden;
.el-tag {
min-width: 22px;
text-align: center;
background-color: #2830d3;
border-radius: 10px;
border: 0;
height: 16px;
line-height: 16px;
font-size: 10px;
margin-left: 2px;
opacity: 0.8;
}
}
.chat-time-text {
font-size: var(--im-font-size-smaller);
text-align: right;
color: var(--im-text-color-light);
white-space: nowrap;
overflow: hidden;
padding-left: 10px;
align-items: center;
font-size: var(--im-font-size);
white-space: nowrap;
overflow: hidden;
.el-tag {
min-width: 22px;
text-align: center;
background-color: #2830d3;
border-radius: 10px;
border: 0;
height: 16px;
line-height: 16px;
font-size: 10px;
margin-left: 2px;
opacity: 0.8;
}
}
.chat-content {
display: flex;
line-height: 22px;
.chat-time-text {
font-size: var(--im-font-size-smaller);
text-align: right;
color: var(--im-text-color-light);
white-space: nowrap;
overflow: hidden;
padding-left: 10px;
}
}
.chat-at-text {
color: #c70b0b;
font-size: var(--im-font-size-smaller);
}
.chat-content {
display: flex;
line-height: 22px;
.chat-send-name {
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
.chat-at-text {
color: #c70b0b;
font-size: var(--im-font-size-smaller);
}
.chat-send-name {
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
.chat-content-text {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
img {
width: 20px !important;
height: 20px !important;
vertical-align: bottom;
}
.chat-content-text {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
img {
width: 20px !important;
height: 20px !important;
vertical-align: bottom;
}
}
}
}
}
</style>

640
im-web/src/components/chat/ChatMessageItem.vue

@ -51,21 +51,21 @@
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div>
<div class="chat-action chat-msg-text" v-if="isAction">
<span v-if="msgInfo.type==$enums.MESSAGE_TYPE.ACT_RT_VOICE" title="重新呼叫" @click="$emit('call')"
<span v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE" title="重新呼叫" @click="$emit('call')"
class="iconfont icon-chat-voice"></span>
<span v-if="msgInfo.type==$enums.MESSAGE_TYPE.ACT_RT_VIDEO" title="重新呼叫" @click="$emit('call')"
<span v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VIDEO" title="重新呼叫" @click="$emit('call')"
class="iconfont icon-chat-video"></span>
<span>{{msgInfo.content}}</span>
<span>{{ msgInfo.content }}</span>
</div>
<div class="chat-msg-status" v-if="!isAction">
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span>
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span>
</div>
<div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span>
<span v-else>{{msgInfo.readedCount}}人已读</span>
<span v-else>{{ msgInfo.readedCount }}人已读</span>
</div>
</div>
</div>
@ -77,399 +77,399 @@
</template>
<script>
import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue';
export default {
name: "messageItem",
components: {
HeadImage,
RightMenu,
ChatGroupReaded
import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue';
export default {
name: "messageItem",
components: {
HeadImage,
RightMenu,
ChatGroupReaded
},
props: {
mode: {
type: Number,
default: 1
},
props: {
mode: {
type: Number,
default: 1
},
mine: {
type: Boolean,
required: true
},
headImage: {
type: String,
required: true
},
showName: {
type: String,
required: true
},
msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array
},
menu: {
type: Boolean,
default: true
}
mine: {
type: Boolean,
required: true
},
data() {
return {
audioPlayState: 'STOP',
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
}
headImage: {
type: String,
required: true
},
showName: {
type: String,
required: true
},
msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array
},
menu: {
type: Boolean,
default: true
}
},
data() {
return {
audioPlayState: 'STOP',
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
}
}
}
},
methods: {
onSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
},
methods: {
onSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
},
showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if (imageUrl) {
this.$store.commit('showFullImageBox', imageUrl);
}
},
onPlayVoice() {
if (!this.audio) {
this.audio = new Audio();
}
this.audio.src = JSON.parse(this.msgInfo.content).url;
this.audio.play();
this.onPlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
},
onShowReadedBox() {
let rect = this.$refs.chatMsgBox.getBoundingClientRect();
this.$refs.chatGroupReadedBox.open(rect);
showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if (imageUrl) {
this.$store.commit('showFullImageBox', imageUrl);
}
},
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
},
data() {
return JSON.parse(this.msgInfo.content)
},
fileSize() {
let size = this.data.size;
if (size > 1024 * 1024) {
return Math.round(size / 1024 / 1024) + "M";
}
if (size > 1024) {
return Math.round(size / 1024) + "KB";
}
return size + "B";
},
menuItems() {
let items = [];
onPlayVoice() {
if (!this.audio) {
this.audio = new Audio();
}
this.audio.src = JSON.parse(this.msgInfo.content).url;
this.audio.play();
this.onPlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
},
onShowReadedBox() {
let rect = this.$refs.chatMsgBox.getBoundingClientRect();
this.$refs.chatGroupReadedBox.open(rect);
}
},
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
},
data() {
return JSON.parse(this.msgInfo.content)
},
fileSize() {
let size = this.data.size;
if (size > 1024 * 1024) {
return Math.round(size / 1024 / 1024) + "M";
}
if (size > 1024) {
return Math.round(size / 1024) + "KB";
}
return size + "B";
},
menuItems() {
let items = [];
items.push({
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
});
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
items.push({
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
key: 'RECALL',
name: '撤回',
icon: 'el-icon-refresh-left'
});
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
items.push({
key: 'RECALL',
name: '撤回',
icon: 'el-icon-refresh-left'
});
}
return items;
},
isAction() {
return this.$msgType.isAction(this.msgInfo.type);
},
isNormal() {
const type = this.msgInfo.type;
return this.$msgType.isNormal(type) || this.$msgType.isAction(type)
}
return items;
},
isAction() {
return this.$msgType.isAction(this.msgInfo.type);
},
isNormal() {
const type = this.msgInfo.type;
return this.$msgType.isNormal(type) || this.$msgType.isAction(type)
}
}
}
</script>
<style lang="scss">
.chat-msg-item {
.chat-msg-item {
.chat-msg-tip {
line-height: 50px;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
.chat-msg-tip {
line-height: 50px;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
.chat-msg-normal {
position: relative;
font-size: 0;
padding-left: 48px;
min-height: 50px;
margin-top: 10px;
.head-image {
position: absolute;
width: 40px;
height: 40px;
top: 0;
left: 0;
}
.chat-msg-normal {
position: relative;
font-size: 0;
padding-left: 48px;
min-height: 50px;
margin-top: 10px;
.chat-msg-content {
text-align: left;
.head-image {
position: absolute;
width: 40px;
height: 40px;
top: 0;
left: 0;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
.chat-msg-content {
text-align: left;
.chat-msg-top {
display: flex;
flex-wrap: nowrap;
color: var(--im-text-color-light);
font-size: var(--im-font-size);
line-height: 20px;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
span {
margin-right: 12px;
}
}
.chat-msg-top {
display: flex;
flex-wrap: nowrap;
color: var(--im-text-color-light);
.chat-msg-bottom {
display: inline-block;
padding-right: 300px;
padding-left: 5px;
.chat-msg-text {
display: block;
position: relative;
line-height: 26px;
//margin-top: 3px;
padding: 6px 10px;
background-color: var(--im-background);
border-radius: 10px;
font-size: var(--im-font-size);
line-height: 20px;
span {
margin-right: 12px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #eee transparent transparent;
overflow: hidden;
border-width: 10px;
}
}
.chat-msg-bottom {
display: inline-block;
padding-right: 300px;
padding-left: 5px;
.chat-msg-text {
display: block;
position: relative;
line-height: 26px;
//margin-top: 3px;
padding: 6px 10px;
background-color: var(--im-background);
border-radius: 10px;
font-size: var(--im-font-size);
text-align: left;
white-space: pre-wrap;
word-break: break-all;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #eee transparent transparent;
overflow: hidden;
border-width: 10px;
}
.chat-msg-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.send-image {
min-width: 200px;
min-height: 150px;
max-width: 400px;
max-height: 300px;
border-radius: 8px;
cursor: pointer;
}
.chat-msg-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.send-image {
min-width: 200px;
min-height: 150px;
max-width: 400px;
max-height: 300px;
border-radius: 8px;
cursor: pointer;
}
}
}
.chat-msg-file {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
cursor: pointer;
margin-bottom: 2px;
.chat-msg-file {
.chat-file-box {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
cursor: pointer;
margin-bottom: 2px;
.chat-file-box {
display: flex;
flex-wrap: nowrap;
align-items: center;
min-height: 60px;
box-shadow: var(--im-box-shadow-light);
border-radius: 4px;
padding: 10px 15px;
.chat-file-info {
flex: 1;
height: 100%;
text-align: left;
min-height: 60px;
box-shadow: var(--im-box-shadow-light);
border-radius: 4px;
padding: 10px 15px;
.chat-file-info {
flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
margin-right: 10px;
.chat-file-name {
display: inline-block;
min-width: 160px;
max-width: 220px;
font-size: 14px;
margin-right: 10px;
.chat-file-name {
display: inline-block;
min-width: 160px;
max-width: 220px;
font-size: 14px;
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.chat-file-size {
font-size: var(--im-font-size-smaller);
color: var(--im-text-color-light);
}
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.chat-file-icon {
font-size: 44px;
color: #d42e07;
.chat-file-size {
font-size: var(--im-font-size-smaller);
color: var(--im-text-color-light);
}
}
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
.chat-file-icon {
font-size: 44px;
color: #d42e07;
}
}
.chat-msg-voice {
font-size: 14px;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
audio {
height: 45px;
padding: 5px 0;
}
margin: 0 20px;
}
.chat-action {
display: flex;
align-items: center;
}
.iconfont {
cursor: pointer;
font-size: 22px;
padding-right: 8px;
}
.chat-msg-voice {
font-size: 14px;
cursor: pointer;
audio {
height: 45px;
padding: 5px 0;
}
}
.chat-msg-status {
display: block;
.chat-action {
display: flex;
align-items: center;
.chat-readed {
font-size: 12px;
color: var(--im-text-color-light);
}
.iconfont {
cursor: pointer;
font-size: 22px;
padding-right: 8px;
}
}
.chat-unread {
font-size: var(--im-font-size-smaller);
color: var(--im-color-danger);
}
.chat-msg-status {
display: block;
.chat-readed {
font-size: 12px;
color: var(--im-text-color-light);
}
.chat-receipt {
.chat-unread {
font-size: var(--im-font-size-smaller);
cursor: pointer;
color: var(--im-text-color-light);
.icon-ok {
font-size: 20px;
color: var(--im-color-sucess);
}
color: var(--im-color-danger);
}
}
.chat-at-user {
padding: 2px 5px;
border-radius: 3px;
cursor: pointer;
.chat-receipt {
font-size: var(--im-font-size-smaller);
cursor: pointer;
color: var(--im-text-color-light);
.icon-ok {
font-size: 20px;
color: var(--im-color-sucess);
}
}
.chat-at-user {
padding: 2px 5px;
border-radius: 3px;
cursor: pointer;
}
}
}
&.chat-msg-mine {
text-align: right;
padding-left: 0;
padding-right: 48px;
&.chat-msg-mine {
text-align: right;
padding-left: 0;
padding-right: 48px;
.head-image {
left: auto;
right: 0;
}
.head-image {
left: auto;
right: 0;
}
.chat-msg-content {
text-align: right;
.chat-msg-content {
text-align: right;
.chat-msg-top {
flex-direction: row-reverse;
.chat-msg-top {
flex-direction: row-reverse;
span {
margin-left: 12px;
margin-right: 0;
}
span {
margin-left: 12px;
margin-right: 0;
}
}
.chat-msg-bottom {
padding-left: 180px;
padding-right: 5px;
.chat-msg-bottom {
padding-left: 180px;
padding-right: 5px;
.chat-msg-text {
margin-left: 10px;
background-color: var(--im-color-primary-light-2);
color: #fff;
.chat-msg-text {
margin-left: 10px;
background-color: var(--im-color-primary-light-2);
color: #fff;
&:after {
left: auto;
right: -10px;
border-top-color: var(--im-color-primary-light-2);
}
&:after {
left: auto;
right: -10px;
border-top-color: var(--im-color-primary-light-2);
}
}
.chat-msg-image {
flex-direction: row-reverse;
}
.chat-msg-image {
flex-direction: row-reverse;
}
.chat-msg-file {
flex-direction: row-reverse;
}
.chat-msg-file {
flex-direction: row-reverse;
}
.chat-action {
flex-direction: row-reverse;
.chat-action {
flex-direction: row-reverse;
.iconfont {
transform: rotateY(180deg);
}
.iconfont {
transform: rotateY(180deg);
}
}
}
}
}
}
}
</style>

225
im-web/src/components/chat/ChatRecord.vue

@ -1,139 +1,138 @@
<template>
<el-dialog class="chat-record" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose">
<div v-show="mode=='RECORD'">
<div class="tip">{{stateTip}}</div>
<div>时长: {{state=='STOP'?0:parseInt(rc.duration)}}s</div>
<div v-show="mode == 'RECORD'">
<div class="tip">{{ stateTip }}</div>
<div>时长: {{ state == 'STOP' ? 0 : parseInt(rc.duration) }}s</div>
</div>
<audio v-show="mode=='PLAY'" :src="url" controls ref="audio" @ended="onStopAudio()"></audio>
<audio v-show="mode == 'PLAY'" :src="url" controls ref="audio" @ended="onStopAudio()"></audio>
<el-divider content-position="center"></el-divider>
<el-row class="btn-group">
<el-button round type="primary" v-show="state=='STOP'" @click="onStartRecord()">开始录音</el-button>
<el-button round type="warning" v-show="state=='RUNNING'" @click="onPauseRecord()">暂停录音</el-button>
<el-button round type="primary" v-show="state=='PAUSE'" @click="onResumeRecord()">继续录音</el-button>
<el-button round type="danger" v-show="state=='RUNNING'||state=='PAUSE'" @click="onCompleteRecord()">
<el-button round type="primary" v-show="state == 'STOP'" @click="onStartRecord()">开始录音</el-button>
<el-button round type="warning" v-show="state == 'RUNNING'" @click="onPauseRecord()">暂停录音</el-button>
<el-button round type="primary" v-show="state == 'PAUSE'" @click="onResumeRecord()">继续录音</el-button>
<el-button round type="danger" v-show="state == 'RUNNING' || state == 'PAUSE'" @click="onCompleteRecord()">
结束录音</el-button>
<el-button round type="success" v-show="state=='COMPLETE' && mode!='PLAY'" @click="onPlayAudio()">播放录音
<el-button round type="success" v-show="state == 'COMPLETE' && mode != 'PLAY'" @click="onPlayAudio()">播放录音
</el-button>
<el-button round type="warning" v-show="state=='COMPLETE' && mode=='PLAY'" @click="onStopAudio()">停止播放
<el-button round type="warning" v-show="state == 'COMPLETE' && mode == 'PLAY'" @click="onStopAudio()">停止播放
</el-button>
<el-button round type="primary" v-show="state=='COMPLETE'" @click="onRestartRecord()">重新录音</el-button>
<el-button round type="primary" v-show="state=='COMPLETE'" @click="onSendRecord()">立即发送</el-button>
<el-button round type="primary" v-show="state == 'COMPLETE'" @click="onRestartRecord()">重新录音</el-button>
<el-button round type="primary" v-show="state == 'COMPLETE'" @click="onSendRecord()">立即发送</el-button>
</el-row>
</el-dialog>
</template>
<script>
import Recorder from 'js-audio-recorder';
import Recorder from 'js-audio-recorder';
export default {
name: 'chatRecord',
props: {
visible: {
type: Boolean
}
},
data() {
return {
rc: new Recorder(),
audio: new Audio(),
state: 'STOP', // STOPRUNNINGPAUSECOMPLETE
stateTip: "未开始",
mode: 'RECORD', // RECORD PLAY
duration: 0,
url: ""
}
export default {
name: 'chatRecord',
props: {
visible: {
type: Boolean
}
},
data() {
return {
rc: new Recorder(),
audio: new Audio(),
state: 'STOP', // STOPRUNNINGPAUSECOMPLETE
stateTip: "未开始",
mode: 'RECORD', // RECORD PLAY
duration: 0,
url: ""
}
},
methods: {
onClose() {
//
this.rc.destroy();
this.rc = new Recorder();
this.audio.pause();
this.mode = 'RECORD';
this.state = 'STOP';
this.stateTip = '未开始';
this.$emit("close");
},
methods: {
onClose() {
//
this.rc.destroy();
this.rc = new Recorder();
this.audio.pause();
this.mode = 'RECORD';
this.state = 'STOP';
this.stateTip = '未开始';
this.$emit("close");
},
onStartRecord() {
this.rc.start().then((stream) => {
this.state = 'RUNNING';
this.stateTip = "正在录音...";
}).catch(error => {
this.$message.error(error);
});
},
onPauseRecord() {
this.rc.pause();
this.state = 'PAUSE';
this.stateTip = "已暂停录音";
},
onResumeRecord() {
this.rc.resume();
onStartRecord() {
this.rc.start().then((stream) => {
this.state = 'RUNNING';
this.stateTip = "正在录音...";
},
onCompleteRecord() {
this.rc.pause();
this.state = 'COMPLETE';
this.stateTip = "已结束录音";
},
onPlayAudio() {
let wav = this.rc.getWAVBlob();
let url = URL.createObjectURL(wav);
this.$refs.audio.src = url;
this.$refs.audio.play();
this.mode = 'PLAY';
},
onStopAudio() {
this.$refs.audio.pause();
this.mode = 'RECORD';
},
onRestartRecord() {
this.rc.destroy();
this.rc = new Recorder()
this.rc.start();
this.state = 'RUNNING';
this.mode = 'RECORD';
this.stateTip = "正在录音...";
},
onSendRecord() {
let wav = this.rc.getWAVBlob();
let name = new Date().getDate() + '.wav';
var formData = new window.FormData()
formData.append('file', wav, name);
this.$http({
url: '/file/upload',
data: formData,
method: 'post',
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((url) => {
let data = {
duration: parseInt(this.rc.duration),
url: url
}
this.$emit("send", data);
this.onClose();
})
}
}
}).catch(error => {
this.$message.error(error);
});
},
onPauseRecord() {
this.rc.pause();
this.state = 'PAUSE';
this.stateTip = "已暂停录音";
},
onResumeRecord() {
this.rc.resume();
this.state = 'RUNNING';
this.stateTip = "正在录音...";
},
onCompleteRecord() {
this.rc.pause();
this.state = 'COMPLETE';
this.stateTip = "已结束录音";
},
onPlayAudio() {
let wav = this.rc.getWAVBlob();
let url = URL.createObjectURL(wav);
this.$refs.audio.src = url;
this.$refs.audio.play();
this.mode = 'PLAY';
},
onStopAudio() {
this.$refs.audio.pause();
this.mode = 'RECORD';
},
onRestartRecord() {
this.rc.destroy();
this.rc = new Recorder()
this.rc.start();
this.state = 'RUNNING';
this.mode = 'RECORD';
this.stateTip = "正在录音...";
},
onSendRecord() {
let wav = this.rc.getWAVBlob();
let name = new Date().getDate() + '.wav';
var formData = new window.FormData()
formData.append('file', wav, name);
this.$http({
url: '/file/upload',
data: formData,
method: 'post',
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((url) => {
let data = {
duration: parseInt(this.rc.duration),
url: url
}
this.$emit("send", data);
this.onClose();
})
}
}
}
</script>
<style lang="scss">
.chat-record {
.chat-record {
.tip {
font-size: 18px;
}
.tip {
font-size: 18px;
}
.btn-group {
margin-bottom: 20px;
}
.btn-group {
margin-bottom: 20px;
}
}
</style>

118
im-web/src/components/common/Emotion.vue

@ -1,6 +1,6 @@
<template>
<div v-show="show" @click="close()">
<div class="emotion-box" :style="{'left':x+'px','top':y+'px'}">
<div v-show="show" @click="close()">
<div class="emotion-box" :style="{ 'left': x + 'px', 'top': y + 'px' }">
<el-scrollbar style="height: 220px">
<div class="emotion-item-list">
<div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i"
@ -13,73 +13,73 @@
</template>
<script>
export default {
name: "emotion",
data() {
return {
show: false,
pos: {
x: 0,
y: 0
}
export default {
name: "emotion",
data() {
return {
show: false,
pos: {
x: 0,
y: 0
}
}
},
methods: {
onClickEmo(emoText) {
let emotion = `#${emoText};`
this.$emit('emotion', emotion)
},
methods: {
onClickEmo(emoText) {
let emotion = `#${emoText};`
this.$emit('emotion', emotion)
},
open(pos) {
this.pos = pos;
this.show = true;
},
close() {
this.show = false;
}
open(pos) {
this.pos = pos;
this.show = true;
},
computed: {
x() {
return this.pos.x - 22;
},
y() {
return this.pos.y - 234;
}
close() {
this.show = false;
}
},
computed: {
x() {
return this.pos.x - 22;
},
y() {
return this.pos.y - 234;
}
}
}
</script>
<style scoped lang="scss">
.emotion-box {
position: fixed;
width: 372px;
box-sizing: border-box;
padding: 5px;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
.emotion-box {
position: fixed;
width: 372px;
box-sizing: border-box;
padding: 5px;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
.emotion-item-list {
display: flex;
flex-wrap: wrap;
.emotion-item-list {
display: flex;
flex-wrap: wrap;
.emotion-item {
text-align: center;
cursor: pointer;
padding: 2px;
.emotion-item {
text-align: center;
cursor: pointer;
padding: 2px;
}
}
//&:after {
// content: "";
// position: absolute;
// left: 185px;
// bottom: -30px;
// width: 0;
// height: 0;
// border-style: solid dashed dashed;
// border-color: #f5f5f5 transparent transparent;
// overflow: hidden;
// border-width: 15px;
//}
}
//&:after {
// content: "";
// position: absolute;
// left: 185px;
// bottom: -30px;
// width: 0;
// height: 0;
// border-style: solid dashed dashed;
// border-color: #f5f5f5 transparent transparent;
// overflow: hidden;
// border-width: 15px;
//}
}
</style>

169
im-web/src/components/common/FileUpload.vue

@ -1,103 +1,102 @@
<template>
<el-upload :action="'#'" :http-request="onFileUpload" :accept="fileTypes==null?'':fileTypes.join(',')" :show-file-list="false"
:disabled="disabled" :before-upload="beforeUpload" :multiple="true">
<el-upload :action="'#'" :http-request="onFileUpload" :accept="fileTypes == null ? '' : fileTypes.join(',')"
:show-file-list="false" :disabled="disabled" :before-upload="beforeUpload" :multiple="true">
<slot></slot>
</el-upload>
</template>
<script>
export default {
name: "fileUpload",
data() {
return {
loading: null,
uploadHeaders: {
"accessToken": sessionStorage.getItem('accessToken')
}
export default {
name: "fileUpload",
data() {
return {
loading: null,
uploadHeaders: {
"accessToken": sessionStorage.getItem('accessToken')
}
}
},
props: {
action: {
type: String,
required: false
},
props: {
action: {
type: String,
required: false
},
fileTypes: {
type: Array,
default: null
},
maxSize: {
type: Number,
default: null
},
showLoading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
fileTypes: {
type: Array,
default: null
},
methods: {
onFileUpload(file) {
//
if (this.showLoading) {
this.loading = this.$loading({
lock: true,
text: '正在上传...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
}
let formData = new FormData()
formData.append('file', file.file)
this.$http({
url: this.action,
data: formData,
method: 'post',
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((data) => {
this.$emit("success", data, file.file);
}).catch((e) => {
this.$emit("fail", e, file.file);
}).finally(() => {
this.loading && this.loading.close();
})
},
beforeUpload(file) {
//
if (this.fileTypes && this.fileTypes.length > 0) {
let fileType = file.type;
let t = this.fileTypes.find((t) => t.toLowerCase() === fileType);
if (t === undefined) {
this.$message.error(`文件格式错误,请上传以下格式的文件:${this.fileTypes.join("、")}`);
return false;
}
maxSize: {
type: Number,
default: null
},
showLoading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
onFileUpload(file) {
//
if (this.showLoading) {
this.loading = this.$loading({
lock: true,
text: '正在上传...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
}
let formData = new FormData()
formData.append('file', file.file)
this.$http({
url: this.action,
data: formData,
method: 'post',
headers: {
'Content-Type': 'multipart/form-data'
}
//
if (this.maxSize && file.size > this.maxSize) {
this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`);
}).then((data) => {
this.$emit("success", data, file.file);
}).catch((e) => {
this.$emit("fail", e, file.file);
}).finally(() => {
this.loading && this.loading.close();
})
},
beforeUpload(file) {
//
if (this.fileTypes && this.fileTypes.length > 0) {
let fileType = file.type;
let t = this.fileTypes.find((t) => t.toLowerCase() === fileType);
if (t === undefined) {
this.$message.error(`文件格式错误,请上传以下格式的文件:${this.fileTypes.join("、")}`);
return false;
}
this.$emit("before", file);
return true;
}
},
computed: {
fileSizeStr() {
if (this.maxSize > 1024 * 1024) {
return Math.round(this.maxSize / 1024 / 1024) + "M";
}
if (this.maxSize > 1024) {
return Math.round(this.maxSize / 1024) + "KB";
}
return this.maxSize + "B";
//
if (this.maxSize && file.size > this.maxSize) {
this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`);
return false;
}
this.$emit("before", file);
return true;
}
},
computed: {
fileSizeStr() {
if (this.maxSize > 1024 * 1024) {
return Math.round(this.maxSize / 1024 / 1024) + "M";
}
if (this.maxSize > 1024) {
return Math.round(this.maxSize / 1024) + "KB";
}
return this.maxSize + "B";
}
}
}
</script>
<style>
</style>
<style></style>

102
im-web/src/components/common/FullImage.vue

@ -9,70 +9,70 @@
</template>
<script>
export default {
name: "fullImage",
data() {
return {
fit: 'contain'
}
},
methods: {
onClose() {
this.$emit("close");
}
export default {
name: "fullImage",
data() {
return {
fit: 'contain'
}
},
methods: {
onClose() {
this.$emit("close");
}
},
props: {
visible: {
type: Boolean
},
props: {
visible: {
type: Boolean
},
url: {
type: String
}
url: {
type: String
}
}
}
</script>
<style lang="scss">
.full-image {
.full-image {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
bottom: 0;
right: 0;
.mask {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
bottom: 0;
right: 0;
.mask {
position: fixed;
width: 100%;
height: 100%;
background: black;
opacity: 0.5;
background: black;
opacity: 0.5;
}
}
.image-box {
position: relative;
width: 100%;
height: 100%;
.image-box {
position: relative;
width: 100%;
height: 100%;
img {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
}
img {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
}
}
.close {
position: fixed;
top: 10px;
right: 10px;
color: white;
font-size: 25px;
cursor: pointer;
}
.close {
position: fixed;
top: 10px;
right: 10px;
color: white;
font-size: 25px;
cursor: pointer;
}
}
</style>

201
im-web/src/components/common/HeadImage.vue

@ -1,8 +1,8 @@
<template>
<div class="head-image" @click="showUserInfo($event)" :style="{cursor : isShowUserInfo ? 'pointer': null}">
<div class="head-image" @click="showUserInfo($event)" :style="{ cursor: isShowUserInfo ? 'pointer' : null }">
<img class="avatar-image" v-show="url" :src="url" :style="avatarImageStyle" loading="lazy" />
<div class="avatar-text" v-show="!url" :style="avatarTextStyle">
{{name?.substring(0,2).toUpperCase()}}
{{ name?.substring(0, 2).toUpperCase() }}
</div>
<div v-show="online" class="online" title="用户当前在线"></div>
<slot></slot>
@ -10,121 +10,120 @@
</template>
<script>
export default {
name: "headImage",
data() {
return {
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
"#c9b455", "#326eb6"
]
}
export default {
name: "headImage",
data() {
return {
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
"#c9b455", "#326eb6"
]
}
},
props: {
id: {
type: Number
},
props: {
id: {
type: Number
},
size: {
type: Number,
default: 42
},
width: {
type: Number
},
height: {
type: Number
},
radius: {
type: String,
default: "50%"
},
url: {
type: String
},
name: {
type: String,
default: null
},
online: {
type: Boolean,
default: false
},
isShowUserInfo: {
type: Boolean,
default: true
}
size: {
type: Number,
default: 42
},
methods: {
showUserInfo(e) {
if (!this.isShowUserInfo) return;
if (this.id && this.id > 0) {
this.$http({
url: `/user/find/${this.id}`,
method: 'get'
}).then((user) => {
this.$store.commit("setUserInfoBoxPos", e);
this.$store.commit("showUserInfoBox", user);
})
}
}
width: {
type: Number
},
height: {
type: Number
},
computed: {
avatarImageStyle() {
let w = this.width ? this.width : this.size;
let h = this.height ? this.height : this.size;
return `width:${w}px; height:${h}px;
radius: {
type: String,
default: "50%"
},
url: {
type: String
},
name: {
type: String,
default: null
},
online: {
type: Boolean,
default: false
},
isShowUserInfo: {
type: Boolean,
default: true
}
},
methods: {
showUserInfo(e) {
if (!this.isShowUserInfo) return;
if (this.id && this.id > 0) {
this.$http({
url: `/user/find/${this.id}`,
method: 'get'
}).then((user) => {
this.$store.commit("setUserInfoBoxPos", e);
this.$store.commit("showUserInfoBox", user);
})
}
}
},
computed: {
avatarImageStyle() {
let w = this.width ? this.width : this.size;
let h = this.height ? this.height : this.size;
return `width:${w}px; height:${h}px;
border-radius: ${this.radius};`
},
avatarTextStyle() {
let w = this.width ? this.width : this.size;
let h = this.height ? this.height : this.size;
return `
},
avatarTextStyle() {
let w = this.width ? this.width : this.size;
let h = this.height ? this.height : this.size;
return `
width: ${w}px;height:${h}px;
background-color: ${this.name ? this.textColor : '#fff'};
font-size:${w*0.35}px;
font-size:${w * 0.35}px;
border-radius: ${this.radius};
`
},
textColor() {
let hash = 0;
for (var i = 0; i < this.name.length; i++) {
hash += this.name.charCodeAt(i);
}
return this.colors[hash % this.colors.length];
},
textColor() {
let hash = 0;
for (var i = 0; i < this.name.length; i++) {
hash += this.name.charCodeAt(i);
}
return this.colors[hash % this.colors.length];
}
}
}
</script>
<style scoped lang="scss">
.head-image {
position: relative;
//cursor: pointer;
.head-image {
position: relative;
//cursor: pointer;
.avatar-image {
position: relative;
overflow: hidden;
display: block;
}
.avatar-image {
position: relative;
overflow: hidden;
display: block;
}
.avatar-text {
color: white;
display: flex;
align-items: center;
justify-content: center;
//border: 1px solid #ccc;
//box-shadow: var(--im-box-shadow);
}
.avatar-text {
color: white;
display: flex;
align-items: center;
justify-content: center;
//border: 1px solid #ccc;
//box-shadow: var(--im-box-shadow);
}
.online {
position: absolute;
right: -5px;
bottom: 0;
width: 12px;
height: 12px;
background: limegreen;
border-radius: 50%;
border: 2px solid white;
}
.online {
position: absolute;
right: -5px;
bottom: 0;
width: 12px;
height: 12px;
background: limegreen;
border-radius: 50%;
border: 2px solid white;
}
}
</style>

26
im-web/src/components/common/Icp.vue

@ -1,7 +1,7 @@
<template>
<div class="icp">
<img class="icp-icon" src="../../assets/image/icp_logo.png">
<a target="_blank" href="https://beian.miit.gov.cn/">粤ICP备xxxx号-1</a>
<a target="_blank" href="https://beian.miit.gov.cn/">粤ICP备xxxx号-1</a>
</div>
</template>
@ -9,18 +9,18 @@
</script>
<style lang="scss">
.icp {
position: fixed;
text-align: center;
bottom: 20px;
margin: 0 auto;
width: 100%;
color: #5c6b77;
.icp {
position: fixed;
text-align: center;
bottom: 20px;
margin: 0 auto;
width: 100%;
color: #5c6b77;
.icp-icon {
width: 20px;
height: 20px;
vertical-align: bottom;
}
.icp-icon {
width: 20px;
height: 20px;
vertical-align: bottom;
}
}
</style>

98
im-web/src/components/common/RightMenu.vue

@ -1,11 +1,11 @@
<template>
<div class="right-menu-mask" @click="close()" @contextmenu.prevent="close()">
<div class="right-menu" :style="{'left':pos.x+'px','top':pos.y+'px'}">
<div class="right-menu" :style="{ 'left': pos.x + 'px', 'top': pos.y + 'px' }">
<el-menu text-color="#333333">
<el-menu-item v-for="(item) in items" :key="item.key" :title="item.name"
@click.native.stop="onSelectMenu(item)">
<!-- <span :class="item.icon"></span>-->
<span>{{item.name}}</span>
<!-- <span :class="item.icon"></span>-->
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</div>
@ -13,64 +13,64 @@
</template>
<script>
export default {
name: "rightMenu",
data() {
return {}
export default {
name: "rightMenu",
data() {
return {}
},
props: {
pos: {
type: Object
},
props: {
pos: {
type: Object
},
items: {
type: Array
}
items: {
type: Array
}
},
methods: {
close() {
this.$emit("close");
},
methods: {
close() {
this.$emit("close");
},
onSelectMenu(item) {
this.$emit("select", item);
this.close();
}
onSelectMenu(item) {
this.$emit("select", item);
this.close();
}
}
}
</script>
<style lang="scss">
.right-menu-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
}
.right-menu-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
}
.right-menu {
position: fixed;
border-radius: 8px;
overflow: hidden;
box-shadow: var(--im-box-shadow-light);
.right-menu {
position: fixed;
border-radius: 8px;
overflow: hidden;
box-shadow: var(--im-box-shadow-light);
.el-menu {
border-radius: 4px;
overflow: hidden;
.el-menu {
border-radius: 4px;
overflow: hidden;
.el-menu-item {
height: 36px;
line-height: 36px;
min-width: 100px;
text-align: left;
padding: 0 0 0 20px;
.el-menu-item {
height: 36px;
line-height: 36px;
min-width: 100px;
text-align: left;
padding: 0 0 0 20px;
&:hover {
background-color: var(--im-background-active);
}
&:hover {
background-color: var(--im-background-active);
}
}
}
}
</style>

196
im-web/src/components/common/UserInfo.vue

@ -1,11 +1,10 @@
<template>
<div class="user-info-mask" @click="$emit('close')">
<div class="user-info" :style="{left: pos.x+'px',top: pos.y+'px'}" @click.stop>
<div class="user-info" :style="{ left: pos.x + 'px', top: pos.y + 'px' }" @click.stop>
<div class="user-info-box">
<div class="avatar">
<head-image :name="user.nickName" :url="user.headImageThumb" :size="70"
:online="user.online" radius="10%"
@click.native="showFullImage()"> </head-image>
<head-image :name="user.nickName" :url="user.headImageThumb" :size="70" :online="user.online"
radius="10%" @click.native="showFullImage()"> </head-image>
</div>
<div>
<el-descriptions :column="1" :title="user.userName" class="user-info-items">
@ -15,7 +14,6 @@
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<el-divider content-position="center"></el-divider>
<div class="user-btn-group">
@ -27,123 +25,123 @@
</template>
<script>
import HeadImage from './HeadImage.vue'
import HeadImage from './HeadImage.vue'
export default {
name: "userInfo",
components: {
HeadImage
},
data() {
return {
export default {
name: "userInfo",
components: {
HeadImage
},
data() {
return {
}
}
},
props: {
user: {
type: Object
},
props: {
user: {
type: Object
},
pos: {
type: Object
pos: {
type: Object
}
},
methods: {
onSendMessage() {
let user = this.user;
let chat = {
type: 'PRIVATE',
targetId: user.id,
showName: user.nickName,
headImage: user.headImage,
};
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
if (this.$route.path != "/home/chat") {
this.$router.push("/home/chat");
}
this.$emit("close");
},
methods: {
onSendMessage() {
let user = this.user;
let chat = {
type: 'PRIVATE',
targetId: user.id,
showName: user.nickName,
headImage: user.headImage,
};
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
if (this.$route.path != "/home/chat") {
this.$router.push("/home/chat");
onAddFriend() {
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: this.user.id
}
this.$emit("close");
},
onAddFriend() {
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: this.user.id
}
}).then((data) => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id: this.user.id,
nickName: this.user.nickName,
headImage: this.user.headImageThumb,
online: this.user.online
}
this.$store.commit("addFriend", friend);
})
},
showFullImage() {
if (this.user.headImage) {
this.$store.commit("showFullImageBox", this.user.headImage);
}).then((data) => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id: this.user.id,
nickName: this.user.nickName,
headImage: this.user.headImageThumb,
online: this.user.online
}
}
this.$store.commit("addFriend", friend);
})
},
computed: {
isFriend() {
let friends = this.$store.state.friendStore.friends;
let friend = friends.find((f) => f.id == this.user.id);
return friend != undefined;
showFullImage() {
if (this.user.headImage) {
this.$store.commit("showFullImageBox", this.user.headImage);
}
}
},
computed: {
isFriend() {
let friends = this.$store.state.friendStore.friends;
let friend = friends.find((f) => f.id == this.user.id);
return friend != undefined;
}
}
}
</script>
<style lang="scss">
.user-info-mask {
background-color: rgba($color: #000000, $alpha: 0);
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.user-info-mask {
background-color: rgba($color: #000000, $alpha: 0);
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.user-info {
position: absolute;
width: 300px;
background-color: white;
box-shadow: var(--im-box-shadow);
border-radius: 4px;
padding: 15px;
.user-info {
position: absolute;
width: 300px;
background-color: white;
box-shadow: var(--im-box-shadow);
border-radius: 4px;
padding: 15px;
.user-info-box {
display: flex;
align-items: center;
.user-info-box {
display: flex;
align-items: center;
.user-info-items {
margin-left: 10px;
white-space: nowrap;
overflow: hidden;
.user-info-items {
margin-left: 10px;
white-space: nowrap;
overflow: hidden;
.el-descriptions__header {
margin-bottom: 5px;
}
.el-descriptions__header {
margin-bottom: 5px;
}
.el-descriptions__title {
font-size: 18px;
}
.el-descriptions__title {
font-size: 18px;
}
.el-descriptions-item__cell {
padding-bottom: 1px;
}
.el-descriptions-item__cell {
padding-bottom: 1px;
}
}
}
.el-divider--horizontal {
margin: 18px 0;
}
.el-divider--horizontal {
margin: 18px 0;
}
.user-btn-group {
text-align: center;
}
.user-btn-group {
text-align: center;
}
}
</style>

209
im-web/src/components/friend/AddFriend.vue

@ -1,29 +1,29 @@
<template>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="400px" :before-close="onClose" custom-class="add-friend-dialog">
<el-input placeholder="输入用户名或昵称按下enter搜索,最多展示20条" class="input-with-select" v-model="searchText" size="small" @keyup.enter.native="onSearch()">
<i class="el-icon-search el-input__icon" slot="suffix"
@click="onSearch()"> </i>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="400px" :before-close="onClose"
custom-class="add-friend-dialog">
<el-input placeholder="输入用户名或昵称按下enter搜索,最多展示20条" class="input-with-select" v-model="searchText" size="small"
@keyup.enter.native="onSearch()">
<i class="el-icon-search el-input__icon" slot="suffix" @click="onSearch()"> </i>
</el-input>
<el-scrollbar style="height:400px">
<div v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id">
<div class="item">
<div class="avatar">
<head-image :name="user.nickName"
:url="user.headImage"
:online="user.online"
></head-image>
<head-image :name="user.nickName" :url="user.headImage" :online="user.online"></head-image>
</div>
<div class="add-friend-text">
<div class="text-user-name">
<div>{{user.userName}}</div>
<div :class="user.online ? 'online-status online':'online-status'">{{ user.online?"[在线]":"[离线]"}}</div>
<div>{{ user.userName }}</div>
<div :class="user.online ? 'online-status online' : 'online-status'">{{
user.online ? "[在线]" :"[离线]"}}</div>
</div>
<div class="text-nick-name">
<div>昵称:{{user.nickName}}</div>
<div>昵称:{{ user.nickName }}</div>
</div>
</div>
<el-button type="success" size="mini" v-show="!isFriend(user.id)" @click="onAddFriend(user)">添加</el-button>
<el-button type="info" size="mini" v-show="isFriend(user.id)" plain disabled>已添加</el-button>
<el-button type="success" size="mini" v-show="!isFriend(user.id)"
@click="onAddFriend(user)">添加</el-button>
<el-button type="info" size="mini" v-show="isFriend(user.id)" plain disabled>已添加</el-button>
</div>
</div>
</el-scrollbar>
@ -31,111 +31,112 @@
</template>
<script>
import HeadImage from '../common/HeadImage.vue'
import HeadImage from '../common/HeadImage.vue'
export default {
name: "addFriend",
components:{HeadImage},
data() {
return {
users: [],
searchText: ""
}
export default {
name: "addFriend",
components: { HeadImage },
data() {
return {
users: [],
searchText: ""
}
},
props: {
dialogVisible: {
type: Boolean
}
},
methods: {
onClose() {
this.$emit("close");
},
props: {
dialogVisible: {
type: Boolean
onSearch() {
if (!this.searchText) {
this.users = [];
return;
}
this.$http({
url: "/user/findByName",
method: "get",
params: {
name: this.searchText
}
}).then((data) => {
this.users = data;
})
},
methods: {
onClose() {
this.$emit("close");
},
onSearch() {
if(!this.searchText){
this.users = [];
return;
}
this.$http({
url: "/user/findByName",
method: "get",
params: {
name: this.searchText
}
}).then((data) => {
this.users = data;
})
},
onAddFriend(user){
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: user.id
}
}).then((data) => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id:user.id,
nickName: user.nickName,
headImage: user.headImage,
online: user.online
}
this.$store.commit("addFriend",friend);
})
},
isFriend(userId){
let friends = this.$store.state.friendStore.friends;
let friend = friends.find((f)=> f.id==userId);
return friend != undefined;
}
onAddFriend(user) {
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: user.id
}
}).then((data) => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id: user.id,
nickName: user.nickName,
headImage: user.headImage,
online: user.online
}
this.$store.commit("addFriend", friend);
})
},
isFriend(userId) {
let friends = this.$store.state.friendStore.friends;
let friend = friends.find((f) => f.id == userId);
return friend != undefined;
}
}
}
</script>
<style lang="scss">
<style lang="scss">
.add-friend-dialog {
.item {
height: 65px;
display: flex;
position: relative;
padding-left: 15px;
align-items: center;
padding-right: 25px;
.item {
height: 65px;
display: flex;
position: relative;
padding-left: 15px;
align-items: center;
padding-right: 25px;
.add-friend-text {
margin-left: 15px;
flex: 3;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
.add-friend-text {
margin-left: 15px;
flex: 3;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
.text-user-name{
display: flex;
flex-direction: row;
font-weight: 600;
font-size: 16px;
line-height: 25px;
.text-user-name {
display: flex;
flex-direction: row;
font-weight: 600;
font-size: 16px;
line-height: 25px;
.online-status{
font-size: 12px;
font-weight: 600;
&.online{
color: #5fb878;
}
}
}
.online-status {
font-size: 12px;
font-weight: 600;
.text-nick-name{
display: flex;
flex-direction: row;
font-size: 12px;
line-height: 20px;
}
&.online {
color: #5fb878;
}
}
}
}
}
.text-nick-name {
display: flex;
flex-direction: row;
font-size: 12px;
line-height: 20px;
}
}
}
}
</style>

214
im-web/src/components/friend/FriendItem.vue

@ -5,141 +5,141 @@
</head-image>
</div>
<div class="friend-info">
<div class="friend-name">{{ friend.nickName}}</div>
<div class="friend-name">{{ friend.nickName }}</div>
<div class="friend-online">
<i class="el-icon-monitor online" v-show="friend.onlineWeb" title="电脑设备在线">
<span class="online-icon"></span>
</i>
<i class="el-icon-mobile-phone online" v-show="friend.onlineApp" title="移动设备在线">
<span class="online-icon"></span>
</i>
<i class="el-icon-monitor online" v-show="friend.onlineWeb" title="电脑设备在线">
<span class="online-icon"></span>
</i>
<i class="el-icon-mobile-phone online" v-show="friend.onlineApp" title="移动设备在线">
<span class="online-icon"></span>
</i>
</div>
</div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@close="rightMenu.show=false" @select="onSelectMenu"></right-menu>
@close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
<slot></slot>
</div>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
import RightMenu from "../common/RightMenu.vue";
import HeadImage from '../common/HeadImage.vue';
import RightMenu from "../common/RightMenu.vue";
export default {
name: "frinedItem",
components: {
HeadImage,
RightMenu
},
data() {
return {
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
},
items: [{
key: 'CHAT',
name: '发送消息',
icon: 'el-icon-chat-dot-round'
}, {
key: 'DELETE',
name: '删除好友',
icon: 'el-icon-delete'
}]
}
export default {
name: "frinedItem",
components: {
HeadImage,
RightMenu
},
data() {
return {
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
},
items: [{
key: 'CHAT',
name: '发送消息',
icon: 'el-icon-chat-dot-round'
}, {
key: 'DELETE',
name: '删除好友',
icon: 'el-icon-delete'
}]
}
}
},
methods: {
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
methods: {
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
}
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
}
},
computed: {
friend() {
return this.$store.state.friendStore.friends[this.index];
}
},
props: {
active: {
type: Boolean
},
computed:{
friend(){
return this.$store.state.friendStore.friends[this.index];
}
index: {
type: Number
},
props: {
active: {
type: Boolean
},
index: {
type: Number
},
menu: {
type: Boolean,
default: true
}
menu: {
type: Boolean,
default: true
}
}
}
</script>
<style scope lang="scss">
.friend-item {
height: 50px;
.friend-item {
height: 50px;
display: flex;
position: relative;
padding: 5px 10px;
align-items: center;
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: var(--im-background-active);
}
&.active {
background-color: var(--im-background-active-dark);
}
.friend-avatar {
display: flex;
position: relative;
padding: 5px 10px;
justify-content: center;
align-items: center;
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: var(--im-background-active);
}
}
&.active {
background-color: var(--im-background-active-dark);
}
.friend-info {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 10px;
text-align: left;
.friend-avatar {
display: flex;
justify-content: center;
align-items: center;
.friend-name {
font-size: var(--im-font-size);
white-space: nowrap;
overflow: hidden;
}
.friend-info {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 10px;
text-align: left;
.friend-name {
font-size: var(--im-font-size);
white-space: nowrap;
overflow: hidden;
.friend-online {
.online {
font-weight: bold;
padding-right: 2px;
font-size: 16px;
position: relative;
}
.friend-online {
.online {
font-weight: bold;
padding-right: 2px;
font-size: 16px;
position: relative;
}
.online-icon{
position: absolute;
right: 0;
bottom: 0;
width: 6px;
height: 6px;
background: limegreen;
border-radius: 50%;
border: 1px solid white;
}
.online-icon {
position: absolute;
right: 0;
bottom: 0;
width: 6px;
height: 6px;
background: limegreen;
border-radius: 50%;
border: 1px solid white;
}
}
}
}
</style>

22
im-web/src/components/group/AddGroupMember.vue

@ -8,12 +8,11 @@
</el-input>
</div>
<el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id">
<div v-for="(friend, index) in friends" :key="friend.id">
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index"
:active="false">
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index" :active="false">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox"
v-model="friend.isCheck" size="medium"></el-checkbox>
v-model="friend.isCheck" size="medium"></el-checkbox>
</friend-item>
</div>
</el-scrollbar>
@ -22,18 +21,18 @@
<div class="agm-r-box">
<div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div>
<el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index"
:active="false" @del="onRemoveFriend(friend,index)" :menu="false">
<div v-for="(friend, index) in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index" :active="false"
@del="onRemoveFriend(friend, index)" :menu="false">
</friend-item>
</div>
</el-scrollbar>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
<el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
</el-dialog>
</template>
@ -110,7 +109,7 @@ export default {
this.$store.state.friendStore.friends.forEach((f) => {
let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
.find((m) => m.userId == f.id);
if (m) {
//
friend.disabled = true;
@ -173,7 +172,6 @@ export default {
line-height: 40px;
text-indent: 6px;
color: var(--im-text-color-light)
}
}
}

81
im-web/src/components/group/GroupItem.vue

@ -4,60 +4,59 @@
<head-image :size="42" :name="group.showGroupName" :url="group.headImage"> </head-image>
</div>
<div class="group-name">
<div>{{group.showGroupName}}</div>
<div>{{ group.showGroupName }}</div>
</div>
</div>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
import HeadImage from '../common/HeadImage.vue';
export default {
name: "groupItem",
components: {
HeadImage
export default {
name: "groupItem",
components: {
HeadImage
},
data() {
return {}
},
props: {
group: {
type: Object
},
data() {
return {}
},
props: {
group: {
type: Object
},
active: {
type: Boolean
}
active: {
type: Boolean
}
}
}
</script>
<style lang="scss" >
.group-item {
height: 50px;
display: flex;
position: relative;
padding: 5px 10px;
align-items: center;
white-space: nowrap;
cursor: pointer;
<style lang="scss">
.group-item {
height: 50px;
display: flex;
position: relative;
padding: 5px 10px;
align-items: center;
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: var(--im-background-active);
}
&:hover {
background-color: var(--im-background-active);
}
&.active {
background-color: var(--im-background-active-dark);
}
&.active {
background-color: var(--im-background-active-dark);
}
.group-name {
padding-left: 10px;
height: 100%;
text-align: left;
line-height: 50px;
white-space: nowrap;
overflow: hidden;
font-size: var(--im-font-size);
}
.group-name {
padding-left: 10px;
height: 100%;
text-align: left;
line-height: 50px;
white-space: nowrap;
overflow: hidden;
font-size: var(--im-font-size);
}
}
</style>

106
im-web/src/components/group/GroupMember.vue

@ -1,70 +1,70 @@
<template>
<div class="group-member">
<head-image :id="member.userId" :name="member.showNickName"
:url="member.headImage" :size="38"
:online="member.online" >
<div v-if="showDel" @click.stop="onDelete()" class="btn-kick el-icon-error"></div>
<head-image :id="member.userId" :name="member.showNickName" :url="member.headImage" :size="38"
:online="member.online">
<div v-if="showDel" @click.stop="onDelete()" class="btn-kick el-icon-error"></div>
</head-image>
<div class="member-name">{{member.showNickName}}</div>
<div class="member-name">{{ member.showNickName }}</div>
</div>
</template>
<script>
import HeadImage from "../common/HeadImage.vue";
export default{
name: "groupMember",
components:{HeadImage},
data(){
return {};
import HeadImage from "../common/HeadImage.vue";
export default {
name: "groupMember",
components: { HeadImage },
data() {
return {};
},
props: {
member: {
type: Object,
required: true
},
props:{
member:{
type: Object,
required: true
},
showDel:{
type: Boolean,
default: false
}
},
methods:{
onDelete(){
this.$emit("del",this.member);
}
showDel: {
type: Boolean,
default: false
}
},
methods: {
onDelete() {
this.$emit("del", this.member);
}
}
}
</script>
<style lang="scss">
.group-member{
display: flex;
flex-direction: column;
align-items: center;
width: 50px;
.member-name {
font-size: 12px;
text-align: center;
width: 100%;
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow:ellipsis;
overflow:hidden
}
.group-member {
display: flex;
flex-direction: column;
align-items: center;
width: 50px;
.btn-kick {
display: none;
position: absolute;
right: -8px;
top: -8px;
color: darkred;
font-size: 20px;
cursor: pointer;
}
.member-name {
font-size: 12px;
text-align: center;
width: 100%;
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
&:hover .btn-kick{
display: block;
color: #ce1818;
}
.btn-kick {
display: none;
position: absolute;
right: -8px;
top: -8px;
color: darkred;
font-size: 20px;
cursor: pointer;
}
&:hover .btn-kick {
display: block;
color: #ce1818;
}
}
</style>

18
im-web/src/components/group/GroupMemberItem.vue

@ -1,13 +1,13 @@
<template>
<div class="group-member-item" :style="{'height':height+'px'}">
<div class="group-member-item" :style="{ 'height': height + 'px' }">
<div class="member-avatar">
<head-image :size="headImageSize" :name="member.showNickName"
:url="member.headImage" :online="member.online"> </head-image>
<head-image :size="headImageSize" :name="member.showNickName" :url="member.headImage"
:online="member.online"> </head-image>
</div>
<div class="member-name" :style="{'line-height':height+'px'}">
<div class="member-name" :style="{ 'line-height': height + 'px' }">
<div>{{ member.showNickName }}</div>
</div>
<slot></slot>
<slot></slot>
</div>
</template>
@ -24,13 +24,13 @@ export default {
type: Object,
required: true
},
height:{
height: {
type: Number,
default: 50
}
},
computed:{
headImageSize(){
computed: {
headImageSize() {
return Math.ceil(this.height * 0.75)
}
}
@ -55,7 +55,7 @@ export default {
}
.member-name {
flex:1;
flex: 1;
padding-left: 10px;
height: 100%;
text-align: left;

214
im-web/src/components/group/GroupMemberSelector.vue

@ -7,8 +7,8 @@
</el-input>
<el-scrollbar style="height:400px;">
<div v-for="m in members" :key="m.userId">
<group-member-item v-show="!m.quit&&m.showNickName.includes(searchText)"
:member="m" @click.native="onClickMember(m)">
<group-member-item v-show="!m.quit && m.showNickName.includes(searchText)" :member="m"
@click.native="onClickMember(m)">
<el-checkbox :disabled="m.locked" v-model="m.checked" @change="onChange(m)"
@click.native.stop=""></el-checkbox>
</group-member-item>
@ -17,7 +17,7 @@
</div>
<div class="arrow el-icon-d-arrow-right"></div>
<div class="right-box">
<div class="select-tip"> 已勾选{{checkedMembers.length}}位成员</div>
<div class="select-tip"> 已勾选{{ checkedMembers.length }}位成员</div>
<div class="checked-member-list">
<div v-for="m in members" :key="m.userId">
<group-member class="member-item" v-if="m.checked" :member="m"></group-member>
@ -33,132 +33,132 @@
</template>
<script>
import GroupMemberItem from './GroupMemberItem.vue';
import GroupMember from './GroupMember.vue';
import GroupMemberItem from './GroupMemberItem.vue';
import GroupMember from './GroupMember.vue';
export default {
name: "addGroupMember",
components: {
GroupMemberItem,
GroupMember
export default {
name: "addGroupMember",
components: {
GroupMemberItem,
GroupMember
},
data() {
return {
isShow: false,
searchText: "",
maxSize: -1,
members: []
}
},
props: {
groupId: {
type: Number
}
},
methods: {
open(maxSize, checkedIds, lockedIds) {
this.maxSize = maxSize;
this.isShow = true;
this.loadGroupMembers(checkedIds, lockedIds);
},
data() {
return {
isShow: false,
searchText: "",
maxSize: -1,
members: []
}
loadGroupMembers(checkedIds, lockedIds) {
this.$http({
url: `/group/members/${this.groupId}`,
method: 'get'
}).then((members) => {
members.forEach((m) => {
//
m.checked = checkedIds.indexOf(m.userId) >= 0;
m.locked = lockedIds.indexOf(m.userId) >= 0;
});
this.members = members;
});
},
props: {
groupId: {
type: Number
onClickMember(m) {
if (!m.locked) {
m.checked = !m.checked;
}
},
methods: {
open(maxSize, checkedIds, lockedIds) {
this.maxSize = maxSize;
this.isShow = true;
this.loadGroupMembers(checkedIds, lockedIds);
},
loadGroupMembers(checkedIds, lockedIds) {
this.$http({
url: `/group/members/${this.groupId}`,
method: 'get'
}).then((members) => {
members.forEach((m) => {
//
m.checked = checkedIds.indexOf(m.userId) >= 0;
m.locked = lockedIds.indexOf(m.userId) >= 0;
});
this.members = members;
});
},
onClickMember(m) {
if (!m.locked) {
m.checked = !m.checked;
}
if (this.checkedMembers.length > this.maxSize) {
this.$message.error(`最多选择${this.maxSize}位成员`)
m.checked = false;
}
},
onChange(m) {
if (this.checkedMembers.length > this.maxSize) {
this.$message.error(`最多选择${this.maxSize}位成员`)
m.checked = false;
}
},
ok() {
this.$emit("complete", this.checkedMembers);
this.isShow = false;
},
close() {
this.isShow = false;
if (this.checkedMembers.length > this.maxSize) {
this.$message.error(`最多选择${this.maxSize}位成员`)
m.checked = false;
}
},
computed: {
checkedMembers() {
let ids = [];
this.members.forEach((m) => {
if (m.checked) {
ids.push(m);
}
})
return ids;
onChange(m) {
if (this.checkedMembers.length > this.maxSize) {
this.$message.error(`最多选择${this.maxSize}位成员`)
m.checked = false;
}
},
ok() {
this.$emit("complete", this.checkedMembers);
this.isShow = false;
},
close() {
this.isShow = false;
}
},
computed: {
checkedMembers() {
let ids = [];
this.members.forEach((m) => {
if (m.checked) {
ids.push(m);
}
})
return ids;
}
}
}
</script>
<style lang="scss">
.group-member-selector {
display: flex;
.group-member-selector {
display: flex;
.left-box {
width: 48%;
overflow: hidden;
border: var(--im-border);
.left-box {
width: 48%;
overflow: hidden;
border: var(--im-border);
.el-input__inner {
border: none;
border-bottom: var(--im-border);
}
.el-input__inner {
border: none;
border-bottom: var(--im-border);
}
}
.arrow {
display: flex;
align-items: center;
font-size: 20px;
padding: 10px;
font-weight: 600;
color: var(--im-color-primary);
}
.arrow {
display: flex;
align-items: center;
font-size: 20px;
padding: 10px;
font-weight: 600;
color: var(--im-color-primary);
}
.right-box {
width: 48%;
border: var(--im-border);
.right-box {
width: 48%;
border: var(--im-border);
.select-tip {
text-align: left;
height: 40px;
line-height: 40px;
text-indent: 5px;
color: var(--im-text-color-light)
}
.select-tip {
text-align: left;
height: 40px;
line-height: 40px;
text-indent: 5px;
color: var(--im-text-color-light)
}
.checked-member-list {
padding: 10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
.checked-member-list {
padding: 10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
.member-item {
padding: 2px;
}
.member-item {
padding: 2px;
}
}
}
}
</style>

184
im-web/src/components/rtc/RtcGroupJoin.vue

@ -1,116 +1,118 @@
<template>
<el-dialog title="是否加入通话?" :visible.sync="isShow" width="400px">
<div class="rtc-group-join">
<div class="host-info">
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>
<div class="host-text">{{'发起人:'+rtcInfo.host.nickName}}</div>
<el-dialog title="是否加入通话?" :visible.sync="isShow" width="400px">
<div class="rtc-group-join">
<div class="host-info">
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>
<div class="host-text">{{ '发起人:' + rtcInfo.host.nickName }}</div>
</div>
<div class="users-info">
<div>{{ rtcInfo.userInfos.length + '人正在通话中' }}</div>
<div class="user-list">
<div class="user-item" v-for="user in rtcInfo.userInfos" :key="user.id">
<head-image :url="user.headImage" :name="user.nickName" :size="40">
</head-image>
</div>
</div>
</div>
</div>
<div class="users-info">
<div>{{rtcInfo.userInfos.length+'人正在通话中'}}</div>
<div class="user-list">
<div class="user-item" v-for="user in rtcInfo.userInfos" :key="user.id">
<head-image :url="user.headImage" :name="user.nickName" :size="40">
</head-image>
</div>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onCancel()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
<span slot="footer" class="dialog-footer">
<el-button @click="onCancel()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
</el-dialog>
</template>
<script>
import HeadImage from '@/components/common/HeadImage'
import HeadImage from '@/components/common/HeadImage'
export default{
name: "rtcGroupJoin",
components:{
HeadImage
},
data() {
return {
isShow: false,
rtcInfo: {
host:{},
userInfos:[]
}
export default {
name: "rtcGroupJoin",
components: {
HeadImage
},
data() {
return {
isShow: false,
rtcInfo: {
host: {},
userInfos: []
}
}
},
props: {
groupId: {
type: Number
}
},
methods: {
open(rtcInfo) {
this.rtcInfo = rtcInfo;
this.isShow = true;
},
props: {
groupId: {
type: Number
onOk() {
this.isShow = false;
let userInfos = this.rtcInfo.userInfos;
let mine = this.$store.state.userStore.userInfo;
if (!userInfos.find((user) => user.id == mine.id)) {
//
userInfos.push({
id: mine.id,
nickName: mine.nickName,
headImage: mine.headImageThumb,
isCamera: false,
isMicroPhone: true
})
}
},
methods: {
open(rtcInfo) {
this.rtcInfo = rtcInfo;
this.isShow = true;
},
onOk() {
this.isShow = false;
let userInfos = this.rtcInfo.userInfos;
let mine = this.$store.state.userStore.userInfo;
if(!userInfos.find((user)=>user.id==mine.id)){
//
userInfos.push({
id: mine.id,
nickName: mine.nickName,
headImage: mine.headImageThumb,
isCamera: false,
isMicroPhone: true
})
}
let rtcInfo = {
isHost: false,
groupId: this.groupId,
inviterId: mine.id,
userInfos: userInfos
}
// home.vue
this.$eventBus.$emit("openGroupVideo", rtcInfo);
},
onCancel(){
this.isShow = false;
let rtcInfo = {
isHost: false,
groupId: this.groupId,
inviterId: mine.id,
userInfos: userInfos
}
// home.vue
this.$eventBus.$emit("openGroupVideo", rtcInfo);
},
onCancel() {
this.isShow = false;
}
}
}
</script>
<style lang="scss" scoped>
.rtc-group-join {
height: 260px;
.rtc-group-join {
height: 260px;
padding: 10px;
.host-info {
display: flex;
flex-direction: column;
font-size: 16px;
padding: 10px;
.host-info {
display: flex;
flex-direction: column;
font-size: 16px;
padding: 10px;
height: 100px;
align-items: center;
height: 100px;
align-items: center;
.host-text{
margin-top: 5px;
}
.host-text {
margin-top: 5px;
}
}
.users-info {
font-size: 16px;
margin-top: 20px;
.users-info {
font-size: 16px;
margin-top: 20px;
.user-list {
display: flex;
padding: 5px 5px;
height: 90px;
flex-wrap: wrap;
justify-content: center;
.user-list {
display: flex;
padding: 5px 5px;
height: 90px;
flex-wrap: wrap;
justify-content: center;
.user-item{
padding: 2px;
}
.user-item {
padding: 2px;
}
}
}
}
</style>

36
im-web/src/components/rtc/RtcGroupVideo.vue

@ -1,6 +1,6 @@
<template>
<el-dialog v-dialogDrag top="5vh" title="语音通话" :close-on-click-modal="false" :close-on-press-escape="false"
:visible.sync="isShow" width="50%">
:visible.sync="isShow" width="50%">
<div class='rtc-group-video'>
<div style="padding-top:30px;font-weight: 600; text-align: center;font-size: 16px;">
多人音视频通话属于付费功能如有需要请联系作者购买商业版源码...
@ -19,27 +19,27 @@
</template>
<script>
export default {
name: "rtcGroupVideo",
data() {
return {
isShow: false
}
export default {
name: "rtcGroupVideo",
data() {
return {
isShow: false
}
},
methods: {
open() {
this.isShow = true;
},
methods: {
open() {
this.isShow = true;
},
onRTCMessage(){
//this.isShow = true;
}
onRTCMessage() {
//this.isShow = true;
}
}
}
</script>
<style lang="scss">
.rtc-group-video {
height: 300px;
background-color: #E8F2FF;
}
.rtc-group-video {
height: 300px;
background-color: #E8F2FF;
}
</style>

185
im-web/src/components/rtc/RtcPrivateAcceptor.vue

@ -1,8 +1,9 @@
<template>
<div class="rtc-private-acceptor">
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100" :isShowUserInfo="false"></head-image>
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"
:isShowUserInfo="false"></head-image>
<div class="acceptor-text">
{{tip}}
{{ tip }}
</div>
<div class="acceptor-btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="$emit('accept')" title="接受"></div>
@ -12,115 +13,115 @@
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
export default {
name: "rtcPrivateAcceptor",
components: {
HeadImage
},
data() {
return {}
},
props: {
mode:{
type: String
},
friend:{
type: Object
}
import HeadImage from '../common/HeadImage.vue';
export default {
name: "rtcPrivateAcceptor",
components: {
HeadImage
},
data() {
return {}
},
props: {
mode: {
type: String
},
computed: {
tip() {
let modeText = this.mode == "video" ? "视频" : "语音"
return `${this.friend.nickName} 请求和您进行${modeText}通话...`
}
friend: {
type: Object
}
},
computed: {
tip() {
let modeText = this.mode == "video" ? "视频" : "语音"
return `${this.friend.nickName} 请求和您进行${modeText}通话...`
}
}
}
</script>
<style scoped lang="scss">
.rtc-private-acceptor {
position: absolute;
.rtc-private-acceptor {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
right: 5px;
bottom: 5px;
width: 250px;
height: 250px;
padding: 20px;
background-color: #fff;
box-shadow: var(--im-box-shadow-dark);
border-radius: 4px;
.acceptor-text {
padding: 10px;
text-align: center;
font-size: 16px;
}
.acceptor-btn-group {
display: flex;
flex-direction: column;
align-items: center;
right: 5px;
bottom: 5px;
width: 250px;
height: 250px;
padding: 20px;
background-color: #fff;
box-shadow: var(--im-box-shadow-dark);
border-radius: 4px;
.acceptor-text {
padding: 10px;
text-align: center;
font-size: 16px;
}
justify-content: space-around;
margin-top: 20px;
width: 100%;
.icon {
font-size: 60px;
cursor: pointer;
border-radius: 50%;
&.accept {
color: green;
animation: anim 2s ease-in infinite, vibration 2s ease-in infinite;
@keyframes anim {
0% {
box-shadow: 0 1px 0 4px #ffffff;
}
.acceptor-btn-group {
display: flex;
justify-content: space-around;
margin-top: 20px;
width: 100%;
.icon {
font-size: 60px;
cursor: pointer;
border-radius: 50%;
&.accept {
color: green;
animation: anim 2s ease-in infinite, vibration 2s ease-in infinite;
@keyframes anim {
0% {
box-shadow: 0 1px 0 4px #ffffff;
}
10% {
box-shadow: 0 1px 0 8px rgba(255, 165, 0, 1);
}
25% {
box-shadow: 0 1px 0 12px rgba(255, 210, 128, 1), 0 1px 0 16px rgba(255, 201, 102, 1);
}
50% {
box-shadow: 0 2px 5px 10px rgba(255, 184, 51, 1), 0 2px 5px 23px rgba(248, 248, 255, 1);
}
10% {
box-shadow: 0 1px 0 8px rgba(255, 165, 0, 1);
}
@keyframes vibration {
0% {
transform: rotate(0deg);
}
25% {
box-shadow: 0 1px 0 12px rgba(255, 210, 128, 1), 0 1px 0 16px rgba(255, 201, 102, 1);
}
25% {
transform: rotate(20deg);
}
50% {
box-shadow: 0 2px 5px 10px rgba(255, 184, 51, 1), 0 2px 5px 23px rgba(248, 248, 255, 1);
}
}
50% {
transform: rotate(0deg);
}
@keyframes vibration {
0% {
transform: rotate(0deg);
}
75% {
transform: rotate(-15deg);
}
25% {
transform: rotate(20deg);
}
100% {
transform: rotate(0deg);
}
50% {
transform: rotate(0deg);
}
}
75% {
transform: rotate(-15deg);
}
&.reject {
color: red;
100% {
transform: rotate(0deg);
}
}
}
&.reject {
color: red;
}
}
}
}
</style>

30
im-web/src/components/rtc/RtcPrivateVideo.vue

@ -1,21 +1,13 @@
<template>
<div>
<el-dialog
v-dialogDrag
top="5vh"
custom-class="rtc-private-video-dialog"
:title="title"
:width="width"
:visible.sync="showRoom"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="onQuit">
<el-dialog v-dialogDrag top="5vh" custom-class="rtc-private-video-dialog" :title="title" :width="width"
:visible.sync="showRoom" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="onQuit">
<div class="rtc-private-video">
<div v-show="isVideo" class="rtc-video-box">
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.1)">
element-loading-background="rgba(0, 0, 0, 0.1)">
<head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName"
:url="friend.headImage" :isShowUserInfo="false" radius="0">
:url="friend.headImage" :isShowUserInfo="false" radius="0">
</head-image>
<video ref="remoteVideo" autoplay=""></video>
</div>
@ -24,20 +16,19 @@
</div>
</div>
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.1)">
element-loading-background="rgba(0, 0, 0, 0.1)">
<head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName"
:url="friend.headImage" :isShowUserInfo="false">
:url="friend.headImage" :isShowUserInfo="false">
<div class="rtc-voice-name">{{ friend.nickName }}</div>
</head-image>
</div>
<div class="rtc-control-bar">
<div title="取消" class="icon iconfont icon-phone-reject reject"
style="color: red;" @click="onQuit()"></div>
<div title="取消" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="onQuit()"></div>
</div>
</div>
</el-dialog>
<rtc-private-acceptor v-if="!isHost && isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept"
@reject="onReject"></rtc-private-acceptor>
@reject="onReject"></rtc-private-acceptor>
</div>
</template>
@ -185,8 +176,8 @@ export default {
onRTCMessage(msg) {
//
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
this.isClose) {
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
this.isClose) {
return;
}
// RTC
@ -504,5 +495,4 @@ export default {
}
}
}
</style>

14
im-web/src/components/setting/Setting.vue

@ -2,12 +2,8 @@
<el-dialog class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose">
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small">
<el-form-item label="头像" style="margin-bottom: 0 !important;">
<file-upload class="avatar-uploader"
:action="imageAction"
:showLoading="true"
:maxSize="maxSize"
@success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<file-upload class="avatar-uploader" :action="imageAction" :showLoading="true" :maxSize="maxSize"
@success="onUploadSuccess" :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
@ -30,9 +26,9 @@
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onSubmit()"> </el-button>
</span>
<el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onSubmit()"> </el-button>
</span>
</el-dialog>
</template>

2
im-web/src/main.js

@ -32,6 +32,6 @@ new Vue({
// 配置路由
router,
store,
render: h=>h(App)
render: h => h(App)
})

70
im-web/src/router/index.js

@ -9,41 +9,41 @@ 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: "Friend",
path: "/home/friend",
component: () => import("../view/Friend"),
},
{
name: "GROUP",
path: "/home/group",
component: () => import("../view/Group"),
}
]
}
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: "Friend",
path: "/home/friend",
component: () => import("../view/Friend"),
},
{
name: "GROUP",
path: "/home/group",
component: () => import("../view/Group"),
}
]
}
]
});

8
im-web/src/store/chatStore.js

@ -249,7 +249,7 @@ export default {
let chat = this.getters.findChatByFriend(friend.id);
// 更新会话中的群名和头像
if (chat && (chat.headImage != friend.headImageThumb ||
chat.showName != friend.nickName)) {
chat.showName != friend.nickName)) {
chat.headImage = friend.headImageThumb;
chat.showName = friend.nickName;
chat.stored = false;
@ -259,7 +259,7 @@ export default {
updateChatFromGroup(state, group) {
let chat = this.getters.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb ||
chat.showName != group.showGroupName)) {
chat.showName != group.showGroupName)) {
// 更新会话中的群名称和头像
chat.headImage = group.headImageThumb;
chat.showName = group.showGroupName;
@ -280,7 +280,7 @@ export default {
}
},
refreshChats(state) {
if(!cacheChats){
if (!cacheChats) {
return;
}
// 排序
@ -369,7 +369,7 @@ export default {
return state.loadingPrivateMsg || state.loadingGroupMsg
},
findChats: (state, getters) => () => {
if(cacheChats && getters.isLoading()){
if (cacheChats && getters.isLoading()) {
return cacheChats;
}
return state.chats;

12
im-web/src/store/configStore.js

@ -8,21 +8,21 @@ export default {
setConfig(state, config) {
state.webrtc = config.webrtc;
},
clear(state){
clear(state) {
state.webrtc = {};
}
},
actions:{
loadConfig(context){
actions: {
loadConfig(context) {
return new Promise((resolve, reject) => {
http({
url: '/system/config',
method: 'GET'
}).then((config) => {
console.log("系统配置",config)
context.commit("setConfig",config);
console.log("系统配置", config)
context.commit("setConfig", config);
resolve();
}).catch((res)=>{
}).catch((res) => {
reject(res);
});
})

46
im-web/src/store/friendStore.js

@ -1,5 +1,5 @@
import http from '../api/httpRequest.js'
import {TERMINAL_TYPE} from "../api/enums.js"
import { TERMINAL_TYPE } from "../api/enums.js"
export default {
@ -10,20 +10,20 @@ export default {
},
mutations: {
setFriends(state, friends) {
friends.forEach((f)=>{
friends.forEach((f) => {
f.online = false;
f.onlineWeb = false;
f.onlineApp = false;
})
state.friends = friends;
},
updateFriend(state,friend){
state.friends.forEach((f,index)=>{
if(f.id==friend.id){
updateFriend(state, friend) {
state.friends.forEach((f, index) => {
if (f.id == friend.id) {
// 拷贝属性
let online = state.friends[index].online;
Object.assign(state.friends[index], friend);
state.friends[index].online =online;
state.friends[index].online = online;
}
})
},
@ -39,45 +39,45 @@ export default {
addFriend(state, friend) {
state.friends.push(friend);
},
refreshOnlineStatus(state){
refreshOnlineStatus(state) {
let userIds = [];
if(state.friends.length ==0){
if (state.friends.length == 0) {
return;
}
state.friends.forEach((f)=>{userIds.push(f.id)});
state.friends.forEach((f) => { userIds.push(f.id) });
http({
url: '/user/terminal/online',
method: 'get',
params: {userIds: userIds.join(',')}
params: { userIds: userIds.join(',') }
}).then((onlineTerminals) => {
this.commit("setOnlineStatus",onlineTerminals);
this.commit("setOnlineStatus", onlineTerminals);
})
// 30s后重新拉取
state.timer && clearTimeout(state.timer);
state.timer = setTimeout(()=>{
state.timer = setTimeout(() => {
this.commit("refreshOnlineStatus");
},30000)
}, 30000)
},
setOnlineStatus(state,onlineTerminals){
state.friends.forEach((f)=>{
let userTerminal = onlineTerminals.find((o)=> f.id==o.userId);
if(userTerminal){
setOnlineStatus(state, onlineTerminals) {
state.friends.forEach((f) => {
let userTerminal = onlineTerminals.find((o) => f.id == o.userId);
if (userTerminal) {
f.online = true;
f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB)>=0
f.onlineApp = userTerminal.terminals.indexOf(TERMINAL_TYPE.APP)>=0
}else{
f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB) >= 0
f.onlineApp = userTerminal.terminals.indexOf(TERMINAL_TYPE.APP) >= 0
} else {
f.online = false;
f.onlineWeb = false;
f.onlineApp = false;
}
});
// 在线的在前面
state.friends.sort((f1,f2)=>{
if(f1.online&&!f2.online){
state.friends.sort((f1, f2) => {
if (f1.online && !f2.online) {
return -1;
}
if(f2.online&&!f1.online){
if (f2.online && !f1.online) {
return 1;
}
return 0;

4
im-web/src/store/index.js

@ -10,7 +10,7 @@ import uiStore from './uiStore.js';
Vue.use(Vuex)
export default new Vuex.Store({
modules: {chatStore,friendStore,userStore,groupStore,configStore,uiStore},
modules: { chatStore, friendStore, userStore, groupStore, configStore, uiStore },
state: {},
mutations: {
},
@ -25,7 +25,7 @@ export default new Vuex.Store({
return Promise.all(promises);
})
},
unload(context){
unload(context) {
context.commit("clear");
}
},

24
im-web/src/store/uiStore.js

@ -3,9 +3,9 @@ export default {
userInfo: { // 用户信息窗口
show: false,
user: {},
pos:{
x:0,
y:0
pos: {
x: 0,
y: 0
}
},
fullImage: { // 全屏大图
@ -14,25 +14,25 @@ export default {
}
},
mutations: {
showUserInfoBox(state,user){
showUserInfoBox(state, user) {
state.userInfo.show = true;
state.userInfo.user = user;
},
setUserInfoBoxPos(state,pos){
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
state.userInfo.pos.x = Math.min(pos.x,w-350);
state.userInfo.pos.y = Math.min(pos.y,h-200);
setUserInfoBoxPos(state, pos) {
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
state.userInfo.pos.x = Math.min(pos.x, w - 350);
state.userInfo.pos.y = Math.min(pos.y, h - 200);
},
closeUserInfoBox(state){
closeUserInfoBox(state) {
state.userInfo.show = false;
},
showFullImageBox(state,url){
showFullImageBox(state, url) {
state.fullImage.show = true;
state.fullImage.url = url;
},
closeFullImageBox(state){
closeFullImageBox(state) {
state.fullImage.show = false;
}
}

20
im-web/src/store/userStore.js

@ -1,5 +1,5 @@
import http from '../api/httpRequest.js'
import {RTC_STATE} from "../api/enums.js"
import { RTC_STATE } from "../api/enums.js"
export default {
state: {
@ -9,7 +9,7 @@ export default {
rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
}
},
@ -17,13 +17,13 @@ export default {
setUserInfo(state, userInfo) {
state.userInfo = userInfo
},
setRtcInfo(state, rtcInfo ){
state.rtcInfo = rtcInfo;
setRtcInfo(state, rtcInfo) {
state.rtcInfo = rtcInfo;
},
setRtcState(state,rtcState){
setRtcState(state, rtcState) {
state.rtcInfo.state = rtcState;
},
clear(state){
clear(state) {
state.userInfo = {};
state.rtcInfo = {
friend: {},
@ -32,16 +32,16 @@ export default {
};
}
},
actions:{
loadUser(context){
actions: {
loadUser(context) {
return new Promise((resolve, reject) => {
http({
url: '/user/self',
method: 'GET'
}).then((userInfo) => {
context.commit("setUserInfo",userInfo);
context.commit("setUserInfo", userInfo);
resolve();
}).catch((res)=>{
}).catch((res) => {
reject(res);
});
})

136
im-web/src/utils/directive/dialogDrag.js

@ -1,72 +1,72 @@
import Vue from 'vue'
 
// v-dialogDrag: 弹窗拖拽
Vue.directive('dialogDrag', {
  bind (el, binding, vnode, oldVnode) {
    const dialogHeaderEl = el.querySelector('.el-dialog__header')
    const dragDom = el.querySelector('.el-dialog')
    dialogHeaderEl.style.cursor = 'move'
 
    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
 
    dialogHeaderEl.onmousedown = (e) => {
      // 鼠标按下,计算当前元素距离可视区的距离
      const disX = e.clientX - dialogHeaderEl.offsetLeft
      const disY = e.clientY - dialogHeaderEl.offsetTop
      const screenWidth = document.body.clientWidth; // body当前宽度
      const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
      const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
      const dragDomheight = dragDom.offsetHeight; // 对话框高度
      const minDragDomLeft = dragDom.offsetLeft;
      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
      const minDragDomTop = dragDom.offsetTop;
      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
 
      // 获取到的值带px 正则匹配替换
      let styL, styT
 
      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
      if (sty.left.includes('%')) {
        styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
        styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
      } else {
        styL = +sty.left.replace(/\px/g, '')
        styT = +sty.top.replace(/\px/g, '')
      }
 
      document.onmousemove = function (e) {
        // 获取body的页面可视宽高
        // var clientHeight = document.documentElement.clientHeight || document.body.clientHeight
        // var clientWidth = document.documentElement.clientWidth || document.body.clientWidth
 
        // 通过事件委托,计算移动的距离
        var l = e.clientX - disX
        var t = e.clientY - disY
 
        // 边界处理
        if (-l > minDragDomLeft) {
          l = -minDragDomLeft;
        } else if (l > maxDragDomLeft) {
          l = maxDragDomLeft;
        }
        if (-t > minDragDomTop) {
          t = -minDragDomTop;
        } else if (t > maxDragDomTop) {
          t = maxDragDomTop;
        }
        // 移动当前元素
        dragDom.style.left = `${l + styL}px`
        dragDom.style.top = `${t + styT}px`
 
        // 将此时的位置传出去
        // binding.value({x:e.pageX,y:e.pageY})
      }
 
      document.onmouseup = function (e) {
        document.onmousemove = null
        document.onmouseup = null
      }
    }
  }
bind(el, binding, vnode, oldVnode) {
const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog')
dialogHeaderEl.style.cursor = 'move'
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
const screenWidth = document.body.clientWidth; // body当前宽度
const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
const dragDomheight = dragDom.offsetHeight; // 对话框高度
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
// 获取到的值带px 正则匹配替换
let styL, styT
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
} else {
styL = +sty.left.replace(/\px/g, '')
styT = +sty.top.replace(/\px/g, '')
}
document.onmousemove = function (e) {
// 获取body的页面可视宽高
// var clientHeight = document.documentElement.clientHeight || document.body.clientHeight
// var clientWidth = document.documentElement.clientWidth || document.body.clientWidth
// 通过事件委托,计算移动的距离
var l = e.clientX - disX
var t = e.clientY - disY
// 边界处理
if (-l > minDragDomLeft) {
l = -minDragDomLeft;
} else if (l > maxDragDomLeft) {
l = maxDragDomLeft;
}
if (-t > minDragDomTop) {
t = -minDragDomTop;
} else if (t > maxDragDomTop) {
t = maxDragDomTop;
}
// 移动当前元素
dragDom.style.left = `${l + styL}px`
dragDom.style.top = `${t + styT}px`
// 将此时的位置传出去
// binding.value({x:e.pageX,y:e.pageY})
}
document.onmouseup = function (e) {
document.onmousemove = null
document.onmouseup = null
}
}
}
})

10
im-web/src/view/Chat.vue

@ -7,13 +7,13 @@
</el-input>
</div>
<div class="chat-list-loading" v-if="loading" v-loading="true" element-loading-text="消息接收中..."
element-loading-spinner="el-icon-loading" element-loading-background="#F9F9F9" element-loading-size="24">
element-loading-spinner="el-icon-loading" element-loading-background="#F9F9F9" element-loading-size="24">
</div>
<el-scrollbar class="chat-list-items" v-else>
<div v-for="(chat,index) in chatStore.chats" :key="index">
<chat-item v-show="!chat.delete&&chat.showName.includes(searchText)" :chat="chat" :index="index"
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="chat === chatStore.activeChat"></chat-item>
<div v-for="(chat, index) in chatStore.chats" :key="index">
<chat-item v-show="!chat.delete && chat.showName.includes(searchText)" :chat="chat" :index="index"
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="chat === chatStore.activeChat"></chat-item>
</div>
</el-scrollbar>
</el-aside>

320
im-web/src/view/Friend.vue

@ -10,17 +10,17 @@
<add-friend :dialogVisible="showAddFriend" @close="onCloseAddFriend"></add-friend>
</div>
<el-scrollbar class="friend-list-items">
<div v-for="(friend,index) in $store.state.friendStore.friends" :key="index">
<div v-for="(friend, index) in $store.state.friendStore.friends" :key="index">
<friend-item v-show="friend.nickName.includes(searchText)" :index="index"
:active="friend === $store.state.friendStore.activeFriend" @chat="onSendMessage(friend)"
@delete="onDelItem(friend,index)" @click.native="onActiveItem(friend,index)">
@delete="onDelItem(friend, index)" @click.native="onActiveItem(friend, index)">
</friend-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="friend-box">
<div class="friend-header" v-show="userInfo.id">
{{userInfo.nickName}}
{{ userInfo.nickName }}
</div>
<div v-show="userInfo.id">
<div class="friend-detail">
@ -33,7 +33,7 @@
</el-descriptions-item>
<el-descriptions-item label="昵称">{{ userInfo.nickName }}
</el-descriptions-item>
<el-descriptions-item label="性别">{{ userInfo.sex==0?"男":"女" }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ userInfo.sex == 0 ? "男" : "女" }}</el-descriptions-item>
<el-descriptions-item label="签名">{{ userInfo.signature }}</el-descriptions-item>
</el-descriptions>
</div>
@ -43,7 +43,7 @@
<el-button v-show="!isFriend" icon="el-icon-plus" type="primary"
@click="onAddFriend(userInfo)">加为好友</el-button>
<el-button v-show="isFriend" icon="el-icon-delete" type="danger"
@click="onDelItem(userInfo,activeIdx)">删除好友</el-button>
@click="onDelItem(userInfo, activeIdx)">删除好友</el-button>
</div>
</div>
</div>
@ -55,188 +55,188 @@
</template>
<script>
import FriendItem from "../components/friend/FriendItem.vue";
import AddFriend from "../components/friend/AddFriend.vue";
import HeadImage from "../components/common/HeadImage.vue";
import FriendItem from "../components/friend/FriendItem.vue";
import AddFriend from "../components/friend/AddFriend.vue";
import HeadImage from "../components/common/HeadImage.vue";
export default {
name: "friend",
components: {
FriendItem,
AddFriend,
HeadImage
export default {
name: "friend",
components: {
FriendItem,
AddFriend,
HeadImage
},
data() {
return {
searchText: "",
showAddFriend: false,
activeIdx: -1,
userInfo: {}
}
},
methods: {
onShowAddFriend() {
this.showAddFriend = true;
},
data() {
return {
searchText: "",
showAddFriend: false,
activeIdx: -1,
userInfo: {}
}
onCloseAddFriend() {
this.showAddFriend = false;
},
methods: {
onShowAddFriend() {
this.showAddFriend = true;
},
onCloseAddFriend() {
this.showAddFriend = false;
},
onActiveItem(friend, idx) {
this.$store.commit("activeFriend", idx);
this.activeIdx = idx
this.loadUserInfo(friend, idx);
},
onDelItem(friend, idx) {
this.$confirm(`确认删除'${friend.nickName}',并清空聊天记录吗?`, '确认解除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/friend/delete/${friend.id}`,
method: 'delete'
}).then((data) => {
this.$message.success("删除好友成功");
this.$store.commit("removeFriend", idx);
this.$store.commit("removePrivateChat", friend.id);
})
})
},
onAddFriend(user) {
onActiveItem(friend, idx) {
this.$store.commit("activeFriend", idx);
this.activeIdx = idx
this.loadUserInfo(friend, idx);
},
onDelItem(friend, idx) {
this.$confirm(`确认删除'${friend.nickName}',并清空聊天记录吗?`, '确认解除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: user.id
}
url: `/friend/delete/${friend.id}`,
method: 'delete'
}).then((data) => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id: user.id,
nickName: user.nickName,
headImage: user.headImage,
online: user.online
}
this.$store.commit("addFriend", friend);
this.$message.success("删除好友成功");
this.$store.commit("removeFriend", idx);
this.$store.commit("removePrivateChat", friend.id);
})
},
onSendMessage(user) {
let chat = {
type: 'PRIVATE',
targetId: user.id,
showName: user.nickName,
headImage: user.headImageThumb,
};
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
this.$router.push("/home/chat");
},
showFullImage() {
if (this.userInfo.headImage) {
this.$store.commit('showFullImageBox', this.userInfo.headImage);
})
},
onAddFriend(user) {
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: user.id
}
},
updateFriendInfo(friend, user, index) {
// storestore
friend = JSON.parse(JSON.stringify(friend));
friend.headImage = user.headImageThumb;
friend.nickName = user.nickName;
this.$http({
url: "/friend/update",
method: "put",
data: friend
}).then(() => {
this.$store.commit("updateFriend", friend);
this.$store.commit("updateChatFromFriend", user);
})
},
loadUserInfo(friend, index) {
this.$http({
url: `/user/find/${friend.id}`,
method: 'get'
}).then((user) => {
this.userInfo = user;
//
if (user.headImageThumb != friend.headImage ||
user.nickName != friend.nickName) {
this.updateFriendInfo(friend, user, index)
}
})
}
}).then((data) => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id: user.id,
nickName: user.nickName,
headImage: user.headImage,
online: user.online
}
this.$store.commit("addFriend", friend);
})
},
computed: {
friendStore() {
return this.$store.state.friendStore;
},
isFriend() {
return this.friendStore.friends.find((f) => f.id == this.userInfo.id);
onSendMessage(user) {
let chat = {
type: 'PRIVATE',
targetId: user.id,
showName: user.nickName,
headImage: user.headImageThumb,
};
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
this.$router.push("/home/chat");
},
showFullImage() {
if (this.userInfo.headImage) {
this.$store.commit('showFullImageBox', this.userInfo.headImage);
}
},
updateFriendInfo(friend, user, index) {
// storestore
friend = JSON.parse(JSON.stringify(friend));
friend.headImage = user.headImageThumb;
friend.nickName = user.nickName;
this.$http({
url: "/friend/update",
method: "put",
data: friend
}).then(() => {
this.$store.commit("updateFriend", friend);
this.$store.commit("updateChatFromFriend", user);
})
},
loadUserInfo(friend, index) {
this.$http({
url: `/user/find/${friend.id}`,
method: 'get'
}).then((user) => {
this.userInfo = user;
//
if (user.headImageThumb != friend.headImage ||
user.nickName != friend.nickName) {
this.updateFriendInfo(friend, user, index)
}
})
}
},
computed: {
friendStore() {
return this.$store.state.friendStore;
},
isFriend() {
return this.friendStore.friends.find((f) => f.id == this.userInfo.id);
}
}
}
</script>
<style lang="scss">
.friend-page {
.friend-list-box {
display: flex;
flex-direction: column;
background: var(--im-background);
.friend-list-header {
height: 50px;
display: flex;
align-items: center;
padding: 0 8px;
.friend-page {
.friend-list-box {
display: flex;
flex-direction: column;
background: var(--im-background);
.add-btn {
padding: 5px !important;
margin: 5px;
font-size: 16px;
border-radius: 50%;
}
}
.friend-list-header {
height: 50px;
display: flex;
align-items: center;
padding: 0 8px;
.friend-list-items {
flex: 1;
.add-btn {
padding: 5px !important;
margin: 5px;
font-size: 16px;
border-radius: 50%;
}
}
.friend-box {
display: flex;
flex-direction: column;
.friend-list-items {
flex: 1;
}
}
.friend-header {
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
font-size: var(--im-font-size-larger);
border-bottom: var(--im-border);
box-sizing: border-box;
}
.friend-box {
display: flex;
flex-direction: column;
.friend-detail {
display: flex;
padding: 50px 80px 20px 80px;
text-align: center;
.friend-header {
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
font-size: var(--im-font-size-larger);
border-bottom: var(--im-border);
box-sizing: border-box;
}
.info-item {
margin-left: 20px;
background-color: #ffffff;
border: 1px #ddd solid;
}
.friend-detail {
display: flex;
padding: 50px 80px 20px 80px;
text-align: center;
.description {
padding: 20px 20px 0 20px;
}
.info-item {
margin-left: 20px;
background-color: #ffffff;
border: 1px #ddd solid;
}
.frient-btn-group {
text-align: left !important;
padding: 20px;
.description {
padding: 20px 20px 0 20px;
}
}
.frient-btn-group {
text-align: left !important;
padding: 20px;
}
}
}
</style>

34
im-web/src/view/Group.vue

@ -8,9 +8,9 @@
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button>
</div>
<el-scrollbar class="group-list-items">
<div v-for="(group,index) in groupStore.groups" :key="index">
<group-item v-show="!group.quit&&group.showGroupName.includes(searchText)" :group="group"
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)">
<div v-for="(group, index) in groupStore.groups" :key="index">
<group-item v-show="!group.quit && group.showGroupName.includes(searchText)" :group="group"
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group, index)">
</group-item>
</div>
</el-scrollbar>
@ -23,21 +23,20 @@
<div v-show="activeGroup.id">
<div class="group-info">
<div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
:showLoading="true" :maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" :showLoading="true"
:maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
<head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage"
:name="activeGroup.showGroupName" radius="10%">
:name="activeGroup.showGroupName" radius="10%">
</head-image>
<el-button class="send-btn" icon="el-icon-position" type="primary"
@click="onSendMessage()">发消息
<el-button class="send-btn" icon="el-icon-position" type="primary" @click="onSendMessage()">发消息
</el-button>
</div>
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small"
ref="groupForm">
ref="groupForm">
<el-form-item label="群聊名称" prop="name">
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
</el-form-item>
@ -46,15 +45,15 @@
</el-form-item>
<el-form-item label="群名备注">
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
maxlength="20"></el-input>
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="activeGroup.remarkNickName" maxlength="20"
:placeholder="$store.state.userStore.userInfo.nickName"></el-input>
:placeholder="$store.state.userStore.userInfo.nickName"></el-input>
</el-form-item>
<el-form-item label="群公告">
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3"
maxlength="1024" placeholder="群主未设置"></el-input>
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3" maxlength="1024"
placeholder="群主未设置"></el-input>
</el-form-item>
<div>
<el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button>
@ -68,16 +67,15 @@
<div class="group-member-list">
<div v-for="(member) in groupMembers" :key="member.id">
<group-member v-show="!member.quit" class="group-member" :member="member"
:showDel="isOwner && member.userId!=activeGroup.ownerId" @del="onKick"></group-member>
:showDel="isOwner && member.userId != activeGroup.ownerId" @del="onKick"></group-member>
</div>
<div class="group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
<i class="el-icon-plus"></i>
</div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id"
:members="groupMembers" @reload="loadGroupMembers"
@close="onCloseAddGroupMember"></add-group-member>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" :members="groupMembers"
@reload="loadGroupMembers" @close="onCloseAddGroupMember"></add-group-member>
</div>
</div>
</div>

20
im-web/src/view/Home.vue

@ -1,14 +1,12 @@
<template>
<div class="home-page">
<div class="app-container" :class="{fullscreen: isFullscreen}">
<div class="app-container" :class="{ fullscreen: isFullscreen }">
<div class="navi-bar">
<div class="navi-bar-box">
<div class="top">
<div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName"
:size="38"
:url="$store.state.userStore.userInfo.headImageThumb"
@click.native="showSettingDialog = true">
<head-image :name="$store.state.userStore.userInfo.nickName" :size="38"
:url="$store.state.userStore.userInfo.headImageThumb" @click.native="showSettingDialog = true">
</head-image>
</div>
@ -50,9 +48,9 @@
</div>
<setting :visible="showSettingDialog" @close="closeSetting()"></setting>
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user"
@close="$store.commit('closeUserInfoBox')"></user-info>
@close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
@close="$store.commit('closeFullImageBox')"></full-image>
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
<rtc-group-video ref="rtcGroupVideo"></rtc-group-video>
</div>
@ -133,7 +131,7 @@ export default {
// 线
this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
"accessToken"));
"accessToken"));
}
});
}).catch((e) => {
@ -206,7 +204,7 @@ export default {
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && this.$msgType.isNormal(msg.type) &&
msg.status != this.$enums.MESSAGE_STATUS.READED) {
msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
@ -266,7 +264,7 @@ export default {
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
msg.status != this.$enums.MESSAGE_STATUS.READED) {
msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
@ -509,6 +507,4 @@ export default {
text-align: center;
}
}
</style>

245
im-web/src/view/Login.vue

@ -4,7 +4,7 @@
<el-form class="login-form" :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px"
@keyup.enter.native="submitForm('loginForm')">
<div class="login-brand">
<img class="logo" src="../../public/logo.png"/>
<img class="logo" src="../../public/logo.png" />
<div>登陆盒子IM</div>
</div>
<el-form-item label="终端" prop="userName" v-show="false">
@ -13,7 +13,6 @@
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="loginForm.userName" autocomplete="off"
placeholder="用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password" autocomplete="off"
@ -34,145 +33,145 @@
</template>
<script>
import Icp from '../components/common/Icp.vue'
export default {
name: "login",
components: {
Icp
},
data() {
var checkUsername = (rule, value, callback) => {
if (!value) {
return callback(new Error('请输入用户名'));
}
callback();
};
var checkPassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
}
callback();
};
return {
loginForm: {
terminal: this.$enums.TERMINAL_TYPE.WEB,
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: "/login",
method: 'post',
data: this.loginForm
})
.then((data) => {
// cookie()
this.setCookie('username', this.loginForm.userName);
this.setCookie('password', this.loginForm.password);
// token
sessionStorage.setItem("accessToken", data.accessToken);
sessionStorage.setItem("refreshToken", data.refreshToken);
this.$message.success("登陆成功");
this.$router.push("/home/chat");
})
import Icp from '../components/common/Icp.vue'
export default {
name: "login",
components: {
Icp
},
data() {
var checkUsername = (rule, value, callback) => {
if (!value) {
return callback(new Error('请输入用户名'));
}
callback();
};
var checkPassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
}
callback();
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
};
return {
loginForm: {
terminal: this.$enums.TERMINAL_TYPE.WEB,
userName: '',
password: ''
},
getCookie(name) {
let reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
let arr = document.cookie.match(reg)
if (arr) {
return unescape(arr[2]);
}
return '';
},
setCookie(name, value) {
document.cookie = name + "=" + escape(value);
rules: {
userName: [{
validator: checkUsername,
trigger: 'blur'
}],
password: [{
validator: checkPassword,
trigger: 'blur'
}]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$http({
url: "/login",
method: 'post',
data: this.loginForm
})
.then((data) => {
// cookie()
this.setCookie('username', this.loginForm.userName);
this.setCookie('password', this.loginForm.password);
// token
sessionStorage.setItem("accessToken", data.accessToken);
sessionStorage.setItem("refreshToken", data.refreshToken);
this.$message.success("登陆成功");
this.$router.push("/home/chat");
})
}
});
},
mounted() {
this.loginForm.userName = this.getCookie("username");
// cookie便
this.loginForm.password = this.getCookie("password");
resetForm(formName) {
this.$refs[formName].resetFields();
},
getCookie(name) {
let reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
let arr = document.cookie.match(reg)
if (arr) {
return unescape(arr[2]);
}
return '';
},
setCookie(name, value) {
document.cookie = name + "=" + escape(value);
}
},
mounted() {
this.loginForm.userName = this.getCookie("username");
// cookie便
this.loginForm.password = this.getCookie("password");
}
}
</script>
<style scoped lang="scss">
.login-view {
width: 100%;
height: 100%;
background: #E8F2FF;
background-size: cover;
box-sizing: border-box;
.login-view {
width: 100%;
height: 100%;
background: #E8F2FF;
background-size: cover;
box-sizing: border-box;
.login-content {
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
padding: 10%;
.login-content {
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
padding: 10%;
.login-form {
height: 340px;
width: 400px;
padding: 30px;
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
border-radius: 3%;
overflow: hidden;
border: 1px solid #ccc;
.login-form {
height: 340px;
width: 400px;
padding: 30px;
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
border-radius: 3%;
overflow: hidden;
border: 1px solid #ccc;
.login-brand {
display: flex;
justify-content: center;
align-items: center;
line-height: 50px;
margin: 30px 0 40px 0;
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
text-align: center;
.login-brand {
display: flex;
justify-content: center;
align-items: center;
line-height: 50px;
margin: 30px 0 40px 0;
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
text-align: center;
.logo {
width: 30px;
height: 30px;
margin-right: 10px;
}
.logo {
width: 30px;
height: 30px;
margin-right: 10px;
}
}
.register {
display: flex;
flex-direction: row-reverse;
line-height: 40px;
text-align: left;
padding-left: 20px;
}
.register {
display: flex;
flex-direction: row-reverse;
line-height: 40px;
text-align: left;
padding-left: 20px;
}
}
}
}
</style>

257
im-web/src/view/Register.vue

@ -1,22 +1,27 @@
<template>
<el-container class="register-view">
<div>
<el-form :model="registerForm" status-icon :rules="rules" ref="registerForm" label-width="80px" class="web-ruleForm">
<el-form :model="registerForm" status-icon :rules="rules" ref="registerForm" label-width="80px"
class="web-ruleForm">
<div class="register-brand">
<img class="logo" src="../../public/logo.png"/>
<img class="logo" src="../../public/logo.png" />
<div>欢迎成为盒子IM的用户</div>
</div>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="registerForm.userName" autocomplete="off" placeholder="用户名(登录使用)"></el-input>
<el-input type="userName" v-model="registerForm.userName" autocomplete="off"
placeholder="用户名(登录使用)"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickName">
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off" placeholder="昵称"></el-input>
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off"
placeholder="昵称"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password" autocomplete="off" placeholder="密码"></el-input>
<el-input type="password" v-model="registerForm.password" autocomplete="off"
placeholder="密码"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off" placeholder="确认密码"></el-input>
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off"
placeholder="确认密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
@ -32,143 +37,139 @@
</template>
<script>
import Icp from '../components/common/Icp.vue'
export default {
name: "login",
components: {
Icp
},
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();
};
import Icp from '../components/common/Icp.vue'
export default {
name: "login",
components: {
Icp
},
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) => {
if (value === '') {
return callback(new Error('请输入密码'));
}
if (value != this.registerForm.password) {
return callback(new Error('两次密码输入不一致'));
}
callback();
};
var checkConfirmPassword = (rule, value, callback) => {
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: "/register",
method: 'post',
data: this.registerForm
})
.then((data) => {
this.$message.success("注册成功!");
})
}
});
return {
registerForm: {
userName: '',
nickName: '',
password: '',
confirmPassword: ''
},
resetForm(formName) {
this.$refs[formName].resetFields();
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: "/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;
align-items: center;
width: 100%;
height: 100%;
background: rgb(232, 242, 255);
.register-view {
position: fixed;
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
height: 100%;
background: rgb(232, 242, 255);
.web-ruleForm {
width: 500px;
height: 450px;
padding: 20px;
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
border-radius: 3px;
overflow: hidden;
border-radius: 3%;
.web-ruleForm {
width: 500px;
height: 450px;
padding: 20px;
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
border-radius: 3px;
overflow: hidden;
border-radius: 3%;
.register-brand {
display: flex;
justify-content: center;
align-items: center;
line-height: 50px;
margin: 20px 0 30px 0;
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
text-align: center;
text-transform: uppercase;
.register-brand {
display: flex;
justify-content: center;
align-items: center;
line-height: 50px;
margin: 20px 0 30px 0;
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
text-align: center;
text-transform: uppercase;
.logo {
width: 30px;
height: 30px;
margin-right: 10px;
}
.logo {
width: 30px;
height: 30px;
margin-right: 10px;
}
}
.to-login {
display: flex;
flex-direction: row-reverse;
line-height: 40px;
text-align: left;
padding-left: 20px;
}
.to-login {
display: flex;
flex-direction: row-reverse;
line-height: 40px;
text-align: left;
padding-left: 20px;
}
}
}
</style>

BIN
截图/交流群2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Loading…
Cancel
Save