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. 18
      im-web/src/api/date.js
  6. 8
      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. 42
      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. 275
      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. 28
      im-web/src/components/common/Icp.vue
  29. 98
      im-web/src/components/common/RightMenu.vue
  30. 198
      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. 192
      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. 16
      im-web/src/store/configStore.js
  47. 50
      im-web/src/store/friendStore.js
  48. 4
      im-web/src/store/index.js
  49. 26
      im-web/src/store/uiStore.js
  50. 24
      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. 249
      im-web/src/view/Login.vue
  57. 265
      im-web/src/view/Register.vue
  58. BIN
      截图/交流群2.png

1
.gitignore

@ -12,4 +12,3 @@
/im-web/dist/ /im-web/dist/
/im-uniapp/node_modules/ /im-uniapp/node_modules/
/im-uniapp/package-lock.json /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) ![输入图片说明](%E6%88%AA%E5%9B%BE/app/2.jpg)
#### 加入交流群 #### 加入交流群
1群目前已满员,扫码进入2群: 群1: 741174521(已满)
群2: 937470451(已满)
![输入图片说明](%E6%88%AA%E5%9B%BE/%E4%BA%A4%E6%B5%81%E7%BE%A42.png) 群3:
欢迎进群与小伙们一起交流, **申请加群前请务必先star哦** 欢迎进群与小伙们一起交流, **申请加群前请务必先star哦**

2
im-web/src/App.vue

@ -12,7 +12,6 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
#app { #app {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -22,5 +21,4 @@ export default {
color: var(--im-text-color); color: var(--im-text-color);
font-family: var(--im-font-family); font-family: var(--im-font-family);
} }
</style> </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; return !!navigator && !!navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia;
} }
ImCamera.prototype.openVideo = function() { ImCamera.prototype.openVideo = function () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(this.stream){ if (this.stream) {
this.close() this.close()
} }
let constraints = { let constraints = {
@ -38,7 +38,7 @@ ImCamera.prototype.openVideo = function() {
} }
ImCamera.prototype.openAudio = function() { ImCamera.prototype.openAudio = function () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let constraints = { let constraints = {
video: false, video: false,
@ -61,7 +61,7 @@ ImCamera.prototype.openAudio = function() {
}) })
} }
ImCamera.prototype.close = function() { ImCamera.prototype.close = function () {
// 停止流 // 停止流
if (this.stream) { if (this.stream) {
this.stream.getTracks().forEach((track) => { this.stream.getTracks().forEach((track) => {

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

@ -5,23 +5,23 @@ let toTimeText = (timeStamp, simple) => {
var timeText = ''; var timeText = '';
if (timeDiff <= 60000) { //一分钟内 if (timeDiff <= 60000) { //一分钟内
timeText = '刚刚'; timeText = '刚刚';
} else if (timeDiff > 60000 && timeDiff < 3600000) { } else if (timeDiff > 60000 && timeDiff < 3600000) {
//1小时内 //1小时内
timeText = Math.floor(timeDiff / 60000) + '分钟前'; timeText = Math.floor(timeDiff / 60000) + '分钟前';
} else if (timeDiff >= 3600000 && timeDiff < 86400000 && !isYestday(dateTime)) { } else if (timeDiff >= 3600000 && timeDiff < 86400000 && !isYestday(dateTime)) {
//今日 //今日
timeText = formatDateTime(dateTime).substr(11, 5); timeText = formatDateTime(dateTime).substr(11, 5);
} else if (isYestday(dateTime)) { } else if (isYestday(dateTime)) {
//昨天 //昨天
timeText = '昨天' + formatDateTime(dateTime).substr(11, 5); timeText = '昨天' + formatDateTime(dateTime).substr(11, 5);
} else if (isYear(dateTime)) { } else if (isYear(dateTime)) {
//今年 //今年
timeText = formatDateTime(dateTime).substr(5, simple ? 5 : 14); timeText = formatDateTime(dateTime).substr(5, simple ? 5 : 14);
} else { } else {
//不属于今年 //不属于今年
timeText = formatDateTime(dateTime); timeText = formatDateTime(dateTime);
if(simple){ if (simple) {
timeText = timeText.substr(2,8); timeText = timeText.substr(2, 8);
} }
} }
return timeText; return timeText;
@ -58,7 +58,7 @@ let formatDateTime = (date) => {
} }
export{ export {
toTimeText, toTimeText,
isYestday, isYestday,
isYear, isYear,

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

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

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

@ -15,7 +15,7 @@ let transform = (content) => {
let textToImg = (emoText) => { let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if(idx==-1){ if (idx == -1) {
return emoText; return emoText;
} }
let url = require(`@/assets/emoji/${idx}.gif`); let url = require(`@/assets/emoji/${idx}.gif`);
@ -25,7 +25,7 @@ let textToImg = (emoText) => {
let textToUrl = (emoText) => { let textToUrl = (emoText) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if(idx==-1){ if (idx == -1) {
return ""; return "";
} }
let url = require(`@/assets/emoji/${idx}.gif`); 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: { headers: {
refreshToken: refreshToken refreshToken: refreshToken
} }
}).catch(()=>{ }).catch(() => {
location.href = "/"; location.href = "/";
}) })
// 保存token // 保存token

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

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

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

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

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

@ -3,7 +3,7 @@ import http from './httpRequest.js'
class RtcPrivateApi { class RtcPrivateApi {
} }
RtcPrivateApi.prototype.call = function(uid, mode, offer) { RtcPrivateApi.prototype.call = function (uid, mode, offer) {
return http({ return http({
url: `/webrtc/private/call?uid=${uid}&mode=${mode}`, url: `/webrtc/private/call?uid=${uid}&mode=${mode}`,
method: 'post', 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({ return http({
url: `/webrtc/private/accept?uid=${uid}`, url: `/webrtc/private/accept?uid=${uid}`,
method: 'post', method: 'post',
@ -26,35 +26,35 @@ RtcPrivateApi.prototype.accept = function(uid, answer) {
} }
RtcPrivateApi.prototype.handup = function(uid) { RtcPrivateApi.prototype.handup = function (uid) {
return http({ return http({
url: `/webrtc/private/handup?uid=${uid}`, url: `/webrtc/private/handup?uid=${uid}`,
method: 'post' method: 'post'
}) })
} }
RtcPrivateApi.prototype.cancel = function(uid) { RtcPrivateApi.prototype.cancel = function (uid) {
return http({ return http({
url: `/webrtc/private/cancel?uid=${uid}`, url: `/webrtc/private/cancel?uid=${uid}`,
method: 'post' method: 'post'
}) })
} }
RtcPrivateApi.prototype.reject = function(uid) { RtcPrivateApi.prototype.reject = function (uid) {
return http({ return http({
url: `/webrtc/private/reject?uid=${uid}`, url: `/webrtc/private/reject?uid=${uid}`,
method: 'post' method: 'post'
}) })
} }
RtcPrivateApi.prototype.failed = function(uid, reason) { RtcPrivateApi.prototype.failed = function (uid, reason) {
return http({ return http({
url: `/webrtc/private/failed?uid=${uid}&reason=${reason}`, url: `/webrtc/private/failed?uid=${uid}&reason=${reason}`,
method: 'post' method: 'post'
}) })
} }
RtcPrivateApi.prototype.sendCandidate = function(uid, candidate) { RtcPrivateApi.prototype.sendCandidate = function (uid, candidate) {
return http({ return http({
url: `/webrtc/private/candidate?uid=${uid}`, url: `/webrtc/private/candidate?uid=${uid}`,
method: 'post', method: 'post',
@ -65,7 +65,7 @@ RtcPrivateApi.prototype.sendCandidate = function(uid, candidate) {
}); });
} }
RtcPrivateApi.prototype.heartbeat = function(uid) { RtcPrivateApi.prototype.heartbeat = function (uid) {
return http({ return http({
url: `/webrtc/private/heartbeat?uid=${uid}`, url: `/webrtc/private/heartbeat?uid=${uid}`,
method: 'post' 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 window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window
.mozRTCPeerConnection; .mozRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window
@ -16,11 +16,11 @@ ImWebRtc.prototype.isEnable = function() {
return !!window.RTCPeerConnection; return !!window.RTCPeerConnection;
} }
ImWebRtc.prototype.init = function(configuration) { ImWebRtc.prototype.init = function (configuration) {
this.configuration = configuration; this.configuration = configuration;
} }
ImWebRtc.prototype.setupPeerConnection = function(callback) { ImWebRtc.prototype.setupPeerConnection = function (callback) {
this.peerConnection = new RTCPeerConnection(this.configuration); this.peerConnection = new RTCPeerConnection(this.configuration);
this.peerConnection.ontrack = (e) => { this.peerConnection.ontrack = (e) => {
// 对方的视频流 // 对方的视频流
@ -29,11 +29,11 @@ ImWebRtc.prototype.setupPeerConnection = function(callback) {
} }
ImWebRtc.prototype.setStream = function(stream) { ImWebRtc.prototype.setStream = function (stream) {
if(this.stream){ if (this.stream) {
this.peerConnection.removeStream(this.stream) this.peerConnection.removeStream(this.stream)
} }
if(stream){ if (stream) {
stream.getTracks().forEach((track) => { stream.getTracks().forEach((track) => {
this.peerConnection.addTrack(track, stream); 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) => { this.peerConnection.onicecandidate = (event) => {
// 追踪到候选信息 // 追踪到候选信息
if (event.candidate) { 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) => { this.peerConnection.oniceconnectionstatechange = (event) => {
let state = event.target.iceConnectionState; 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) => { return new Promise((resolve, reject) => {
const offerParam = {}; const offerParam = {};
offerParam.offerToRecieveAudio = 1; 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) => { return new Promise((resolve, reject) => {
// 设置远端的sdp // 设置远端的sdp
this.setRemoteDescription(offer); this.setRemoteDescription(offer);
@ -97,17 +97,17 @@ ImWebRtc.prototype.createAnswer = function(offer) {
}); });
} }
ImWebRtc.prototype.setRemoteDescription = function(offer) { ImWebRtc.prototype.setRemoteDescription = function (offer) {
// 设置对方的sdp信息 // 设置对方的sdp信息
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
} }
ImWebRtc.prototype.addIceCandidate = function(candidate) { ImWebRtc.prototype.addIceCandidate = function (candidate) {
// 添加对方的候选人信息 // 添加对方的候选人信息
this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
} }
ImWebRtc.prototype.close = function(uid) { ImWebRtc.prototype.close = function (uid) {
// 关闭RTC连接 // 关闭RTC连接
if (this.peerConnection) { if (this.peerConnection) {
this.peerConnection.close(); this.peerConnection.close();

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

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

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

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

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

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

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

@ -8,18 +8,16 @@
<div class="group-side-scrollbar"> <div class="group-side-scrollbar">
<div v-show="!group.quit" class="group-side-member-list"> <div v-show="!group.quit" class="group-side-member-list">
<div class="group-side-invite"> <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> <i class="el-icon-plus"></i>
</div> </div>
<div class="invite-member-text">邀请</div> <div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers" <add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers"
@reload="$emit('reload')" @reload="$emit('reload')" @close="showAddGroupMember = false"></add-group-member>
@close="showAddGroupMember=false"></add-group-member>
</div> </div>
<div v-for="(member) in groupMembers" :key="member.id"> <div v-for="(member) in groupMembers" :key="member.id">
<group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)" <group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)"
:member="member" :member="member" :showDel="false"></group-member>
:showDel="false"></group-member>
</div> </div>
</div> </div>
<el-divider v-if="!group.quit" content-position="center"></el-divider> <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-input v-model="group.notice" disabled type="textarea" maxlength="1024"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="备注"> <el-form-item label="备注">
<el-input v-model="group.remarkGroupName" :disabled="!editing" <el-input v-model="group.remarkGroupName" :disabled="!editing" maxlength="20"></el-input>
maxlength="20"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="我在本群的昵称"> <el-form-item label="我在本群的昵称">
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20" <el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20"></el-input>
></el-input>
</el-form-item> </el-form-item>
<div v-show="!group.quit" class="btn-group"> <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="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> <el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
</div> </div>
</el-form> </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; color: var(--im-text-color) !important;
} }

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

@ -1,12 +1,11 @@
<template> <template>
<el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="onClose"> <el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="onClose">
<div class="chat-history" v-loading="loading" <div class="chat-history" v-loading="loading" element-loading-text="拼命加载中">
element-loading-text="拼命加载中"> <el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar">
<el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar" >
<ul> <ul>
<li v-for="(msgInfo,idx) in messages" :key="idx"> <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)" <chat-message-item :mode="2" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
:msgInfo="msgInfo" :menu="false"> :showName="showName(msgInfo)" :msgInfo="msgInfo" :menu="false">
</chat-message-item> </chat-message-item>
</li> </li>
</ul> </ul>
@ -16,155 +15,157 @@
</template> </template>
<script> <script>
import ChatMessageItem from './ChatMessageItem.vue'; import ChatMessageItem from './ChatMessageItem.vue';
export default { export default {
name: 'chatHistory', name: 'chatHistory',
components: { components: {
ChatMessageItem ChatMessageItem
},
props: {
visible: {
type: Boolean
}, },
props: { chat: {
visible: { type: Object
type: Boolean },
}, friend: {
chat: { type: Object
type: Object },
}, group: {
friend: { type: Object
type: Object },
}, groupMembers: {
group: { type: Array,
type: Object }
}, },
groupMembers: { data() {
type: Array, 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');
}, },
data() { onScroll() {
return { let high = this.$refs.scrollbar.$refs.wrap.scrollTop; //
page: 1, let timeDiff = new Date().getTime() - this.lastScrollTime.getTime();
size: 10, if (high < 30 && timeDiff > 500) {
messages: [], this.lastScrollTime = new Date();
loadAll: false, this.loadMessages();
loading: false,
lastScrollTime: new Date()
} }
}, },
methods: { loadMessages() {
onClose() { if (this.loadAll) {
this.page = 1; return this.$message.success("已到达顶部");
this.messages = []; }
this.loadAll = false; let param = {
this.$emit('close'); page: this.page++,
}, size: this.size
onScroll() { }
let high = this.$refs.scrollbar.$refs.wrap.scrollTop; // if (this.chat.type == 'GROUP') {
let timeDiff = new Date().getTime() - this.lastScrollTime.getTime(); param.groupId = this.group.id;
if ( high < 30 && timeDiff>500) { } else {
this.lastScrollTime = new Date(); param.friendId = this.friend.id;
this.loadMessages(); }
this.loading = true;
} this.$http({
}, url: this.histroyAction,
loadMessages() { method: 'get',
if(this.loadAll){ params: param
return this.$message.success("已到达顶部"); }).then(messages => {
} messages.forEach(m => this.messages.unshift(m));
let param = { this.loading = false;
page: this.page++, if (messages.length < this.size) {
size: this.size this.loadAll = true;
}
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
} }
}, this.refreshScrollPos();
refreshScrollPos(){ }).catch(() => {
let scrollWrap = this.$refs.scrollbar.$refs.wrap; this.loading = false;
let scrollHeight = scrollWrap.scrollHeight; })
let scrollTop = scrollWrap.scrollTop; },
this.$nextTick(() => { showName(msgInfo) {
let offsetTop = scrollWrap.scrollHeight - scrollHeight; if (this.chat.type == 'GROUP') {
scrollWrap.scrollTop = scrollTop + offsetTop; let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
// return member ? member.showNickName : "";
if(scrollWrap.scrollHeight == scrollHeight){ } else {
this.loadMessages(); return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName
}
});
} }
}, },
computed: { headImage(msgInfo) {
mine() { if (this.chat.type == 'GROUP') {
return this.$store.state.userStore.userInfo; let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
}, return member ? member.headImage : "";
histroyAction() { } else {
return `/message/${this.chat.type.toLowerCase()}/history`; return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
} }
}, },
watch: { refreshScrollPos() {
visible: { let scrollWrap = this.$refs.scrollbar.$refs.wrap;
handler(newValue, oldValue) { let scrollHeight = scrollWrap.scrollHeight;
if (newValue) { let scrollTop = scrollWrap.scrollTop;
this.loadMessages(); this.$nextTick(() => {
this.$nextTick(() => { let offsetTop = scrollWrap.scrollHeight - scrollHeight;
document.getElementById('historyScrollbar').addEventListener("mousewheel", this.onScroll,true); 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> </script>
<style lang="scss"> <style lang="scss">
.chat-history { .chat-history {
display: flex; display: flex;
height: 100%; height: 100%;
.chat-history-scrollbar {
flex: 1;
.el-scrollbar__thumb {
background-color: #555555;
}
ul {
padding: 20px;
li { .chat-history-scrollbar {
list-style-type: none; flex: 1;
}
.el-scrollbar__thumb {
background-color: #555555;
}
ul {
padding: 20px;
li {
list-style-type: none;
} }
} }
} }
}
</style> </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-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="chat-left"> <div class="chat-left">
<head-image :url="chat.headImage" :name="chat.showName" :size="42" <head-image :url="chat.headImage" :name="chat.showName" :size="42"
:id="chat.type=='PRIVATE'?chat.targetId:0" :isShowUserInfo="false"></head-image> :id="chat.type == 'PRIVATE' ? chat.targetId : 0" :isShowUserInfo="false"></head-image>
<div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div> <div v-show="chat.unreadCount > 0" class="unread-text">{{ chat.unreadCount }}</div>
</div> </div>
<div class="chat-right"> <div class="chat-right">
<div class="chat-name"> <div class="chat-name">
<div class="chat-name-text"> <div class="chat-name-text">
<div>{{chat.showName}}</div> <div>{{ chat.showName }}</div>
<el-tag v-if="chat.type=='GROUP'" size="mini" effect="dark"></el-tag> <el-tag v-if="chat.type == 'GROUP'" size="mini" effect="dark"></el-tag>
</div> </div>
<div class="chat-time-text">{{ showTime }}</div>
<div class="chat-time-text">{{showTime}}</div> </div>
</div>
<div class="chat-content"> <div class="chat-content">
<div class="chat-at-text">{{atText}}</div> <div class="chat-at-text">{{ atText }}</div>
<div class="chat-send-name" v-show="isShowSendName">{{chat.sendNickName+':&nbsp;'}}</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 class="chat-content-text" v-html="$emo.transform(chat.lastContent)"></div>
</div> </div>
</div> </div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" @close="rightMenu.show=false" <right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@select="onSelectMenu"></right-menu> @close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
</div> </div>
</template> </template>
<script> <script>
import HeadImage from '../common/HeadImage.vue'; import HeadImage from '../common/HeadImage.vue';
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
export default { export default {
name: "chatItem", name: "chatItem",
components: { components: {
HeadImage, HeadImage,
RightMenu RightMenu
}, },
data() { data() {
return { return {
rightMenu: { rightMenu: {
show: false, show: false,
pos: { pos: {
x: 0, x: 0,
y: 0 y: 0
}, },
items: [{ items: [{
key: 'TOP', key: 'TOP',
name: '置顶', name: '置顶',
icon: 'el-icon-top' icon: 'el-icon-top'
}, { }, {
key: 'DELETE', key: 'DELETE',
name: '删除', name: '删除',
icon: 'el-icon-delete' icon: 'el-icon-delete'
}] }]
}
} }
}
},
props: {
chat: {
type: Object
}, },
props: { active: {
chat: { type: Boolean
type: Object },
}, index: {
active: { type: Number
type: Boolean }
}, },
index: { methods: {
type: Number showRightMenu(e) {
} this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
}, },
methods: { onSelectMenu(item) {
showRightMenu(e) { this.$emit(item.key.toLowerCase(), this.msgInfo);
this.rightMenu.pos = { }
x: e.x, },
y: e.y computed: {
}; isShowSendName() {
this.rightMenu.show = "true"; if (!this.chat.sendNickName) {
}, return false;
onSelectMenu(item) { }
this.$emit(item.key.toLowerCase(), this.msgInfo); 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: { showTime() {
isShowSendName() { return this.$date.toTimeText(this.chat.lastSendTime, true)
if (!this.chat.sendNickName) { },
return false; atText() {
} if (this.chat.atMe) {
let size = this.chat.messages.length; return "[有人@我]"
if (size == 0) { } else if (this.chat.atAll) {
return false; return "[@全体成员]"
}
//
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 "";
} }
return "";
} }
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.chat-item { .chat-item {
height: 50px; height: 50px;
display: flex; 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; position: relative;
padding: 5px 10px; display: flex;
justify-content: center;
align-items: center; align-items: center;
background-color: var(--im-background);
white-space: nowrap;
cursor: pointer;
&:hover { .unread-text {
background-color: var(--im-background-active); position: absolute;
} background-color: #f56c6c;
right: -4px;
&.active { top: -8px;
background-color: var(--im-background-active-dark); 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 { .chat-name {
flex: 1;
display: flex; display: flex;
flex-direction: column; line-height: 20px;
padding-left: 10px; height: 20px;
text-align: left;
overflow: hidden;
.chat-name { .chat-name-text {
flex: 1;
display: flex; display: flex;
line-height: 20px; align-items: center;
height: 20px; font-size: var(--im-font-size);
white-space: nowrap;
.chat-name-text { overflow: hidden;
flex: 1;
display: flex; .el-tag {
align-items: center; min-width: 22px;
font-size: var(--im-font-size); text-align: center;
white-space: nowrap; background-color: #2830d3;
overflow: hidden; border-radius: 10px;
border: 0;
.el-tag { height: 16px;
min-width: 22px; line-height: 16px;
text-align: center; font-size: 10px;
background-color: #2830d3; margin-left: 2px;
border-radius: 10px; opacity: 0.8;
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;
} }
} }
.chat-content { .chat-time-text {
display: flex; font-size: var(--im-font-size-smaller);
line-height: 22px; text-align: right;
color: var(--im-text-color-light);
white-space: nowrap;
overflow: hidden;
padding-left: 10px;
}
}
.chat-at-text { .chat-content {
color: #c70b0b; display: flex;
font-size: var(--im-font-size-smaller); line-height: 22px;
}
.chat-send-name { .chat-at-text {
font-size: var(--im-font-size-small); color: #c70b0b;
color: var(--im-text-color-light); 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 { .chat-content-text {
width: 20px !important; flex: 1;
height: 20px !important; white-space: nowrap;
vertical-align: bottom; 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> </style>

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

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

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

@ -1,139 +1,138 @@
<template> <template>
<el-dialog class="chat-record" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose"> <el-dialog class="chat-record" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose">
<div v-show="mode=='RECORD'"> <div v-show="mode == 'RECORD'">
<div class="tip">{{stateTip}}</div> <div class="tip">{{ stateTip }}</div>
<div>时长: {{state=='STOP'?0:parseInt(rc.duration)}}s</div> <div>时长: {{ state == 'STOP' ? 0 : parseInt(rc.duration) }}s</div>
</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-divider content-position="center"></el-divider>
<el-row class="btn-group"> <el-row class="btn-group">
<el-button round type="primary" v-show="state=='STOP'" @click="onStartRecord()">开始录音</el-button> <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="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="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="danger" v-show="state == 'RUNNING' || state == 'PAUSE'" @click="onCompleteRecord()">
结束录音</el-button> 结束录音</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>
<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>
<el-button round type="primary" v-show="state=='COMPLETE'" @click="onRestartRecord()">重新录音</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="onSendRecord()">立即发送</el-button>
</el-row> </el-row>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import Recorder from 'js-audio-recorder'; import Recorder from 'js-audio-recorder';
export default { export default {
name: 'chatRecord', name: 'chatRecord',
props: { props: {
visible: { visible: {
type: Boolean type: Boolean
} }
}, },
data() { data() {
return { return {
rc: new Recorder(), rc: new Recorder(),
audio: new Audio(), audio: new Audio(),
state: 'STOP', // STOPRUNNINGPAUSECOMPLETE state: 'STOP', // STOPRUNNINGPAUSECOMPLETE
stateTip: "未开始", stateTip: "未开始",
mode: 'RECORD', // RECORD PLAY mode: 'RECORD', // RECORD PLAY
duration: 0, duration: 0,
url: "" 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: { onStartRecord() {
onClose() { this.rc.start().then((stream) => {
//
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();
this.state = 'RUNNING'; this.state = 'RUNNING';
this.stateTip = "正在录音..."; this.stateTip = "正在录音...";
}, }).catch(error => {
onCompleteRecord() { this.$message.error(error);
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();
})
}
}
},
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> </script>
<style lang="scss"> <style lang="scss">
.chat-record { .chat-record {
.tip { .tip {
font-size: 18px; font-size: 18px;
} }
.btn-group { .btn-group {
margin-bottom: 20px; margin-bottom: 20px;
}
} }
}
</style> </style>

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

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

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

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

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

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

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

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

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

@ -1,7 +1,7 @@
<template> <template>
<div class="icp"> <div class="icp">
<img class="icp-icon" src="../../assets/image/icp_logo.png"> <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> </div>
</template> </template>
@ -9,18 +9,18 @@
</script> </script>
<style lang="scss"> <style lang="scss">
.icp { .icp {
position: fixed; position: fixed;
text-align: center; text-align: center;
bottom: 20px; bottom: 20px;
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
color: #5c6b77; color: #5c6b77;
.icp-icon { .icp-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
vertical-align: bottom; vertical-align: bottom;
}
} }
}
</style> </style>

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

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

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

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

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

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

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

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

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

@ -8,12 +8,11 @@
</el-input> </el-input>
</div> </div>
<el-scrollbar style="height:400px;"> <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" <friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index" @click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index" :active="false">
:active="false">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" <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> </friend-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
@ -22,18 +21,18 @@
<div class="agm-r-box"> <div class="agm-r-box">
<div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div> <div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div>
<el-scrollbar style="height:400px;"> <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-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index" <friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index" :active="false"
:active="false" @del="onRemoveFriend(friend,index)" :menu="false"> @del="onRemoveFriend(friend, index)" :menu="false">
</friend-item> </friend-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
</div> </div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button> <el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button> <el-button type="primary" @click="onOk()"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</template> </template>
@ -110,7 +109,7 @@ export default {
this.$store.state.friendStore.friends.forEach((f) => { this.$store.state.friendStore.friends.forEach((f) => {
let friend = JSON.parse(JSON.stringify(f)) let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit) let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id); .find((m) => m.userId == f.id);
if (m) { if (m) {
// //
friend.disabled = true; friend.disabled = true;
@ -173,7 +172,6 @@ export default {
line-height: 40px; line-height: 40px;
text-indent: 6px; text-indent: 6px;
color: var(--im-text-color-light) 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> <head-image :size="42" :name="group.showGroupName" :url="group.headImage"> </head-image>
</div> </div>
<div class="group-name"> <div class="group-name">
<div>{{group.showGroupName}}</div> <div>{{ group.showGroupName }}</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import HeadImage from '../common/HeadImage.vue'; import HeadImage from '../common/HeadImage.vue';
export default { export default {
name: "groupItem", name: "groupItem",
components: { components: {
HeadImage HeadImage
},
data() {
return {}
},
props: {
group: {
type: Object
}, },
data() { active: {
return {} type: Boolean
},
props: {
group: {
type: Object
},
active: {
type: Boolean
}
} }
} }
}
</script> </script>
<style lang="scss" > <style lang="scss">
.group-item { .group-item {
height: 50px; height: 50px;
display: flex; display: flex;
position: relative; position: relative;
padding: 5px 10px; padding: 5px 10px;
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: var(--im-background-active); background-color: var(--im-background-active);
} }
&.active { &.active {
background-color: var(--im-background-active-dark); background-color: var(--im-background-active-dark);
} }
.group-name { .group-name {
padding-left: 10px; padding-left: 10px;
height: 100%; height: 100%;
text-align: left; text-align: left;
line-height: 50px; line-height: 50px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
font-size: var(--im-font-size); font-size: var(--im-font-size);
}
} }
}
</style> </style>

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,21 +1,13 @@
<template> <template>
<div> <div>
<el-dialog <el-dialog v-dialogDrag top="5vh" custom-class="rtc-private-video-dialog" :title="title" :width="width"
v-dialogDrag :visible.sync="showRoom" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="onQuit">
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 class="rtc-private-video">
<div v-show="isVideo" class="rtc-video-box"> <div v-show="isVideo" class="rtc-video-box">
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..." <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" <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> </head-image>
<video ref="remoteVideo" autoplay=""></video> <video ref="remoteVideo" autoplay=""></video>
</div> </div>
@ -24,20 +16,19 @@
</div> </div>
</div> </div>
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..." <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" <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> <div class="rtc-voice-name">{{ friend.nickName }}</div>
</head-image> </head-image>
</div> </div>
<div class="rtc-control-bar"> <div class="rtc-control-bar">
<div title="取消" class="icon iconfont icon-phone-reject reject" <div title="取消" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="onQuit()"></div>
style="color: red;" @click="onQuit()"></div>
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
<rtc-private-acceptor v-if="!isHost && isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept" <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> </div>
</template> </template>
@ -185,8 +176,8 @@ export default {
onRTCMessage(msg) { onRTCMessage(msg) {
// //
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE && if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO && msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
this.isClose) { this.isClose) {
return; return;
} }
// RTC // RTC
@ -504,5 +495,4 @@ export default {
} }
} }
} }
</style> </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-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 :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small">
<el-form-item label="头像" style="margin-bottom: 0 !important;"> <el-form-item label="头像" style="margin-bottom: 0 !important;">
<file-upload class="avatar-uploader" <file-upload class="avatar-uploader" :action="imageAction" :showLoading="true" :maxSize="maxSize"
:action="imageAction" @success="onUploadSuccess" :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
: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"> <img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload> </file-upload>
@ -30,9 +26,9 @@
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button> <el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onSubmit()"> </el-button> <el-button type="primary" @click="onSubmit()"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</template> </template>

2
im-web/src/main.js

@ -32,6 +32,6 @@ new Vue({
// 配置路由 // 配置路由
router, router,
store, 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({ export default new VueRouter({
routes: [{ routes: [{
path: "/", path: "/",
redirect: "/login" redirect: "/login"
}, },
{ {
name: "Login", name: "Login",
path: '/login', path: '/login',
component: Login component: Login
}, },
{ {
name: "Register", name: "Register",
path: '/register', path: '/register',
component: Register component: Register
}, },
{ {
name: "Home", name: "Home",
path: '/home', path: '/home',
component: Home, component: Home,
children:[ children: [
{ {
name: "Chat", name: "Chat",
path: "/home/chat", path: "/home/chat",
component: () => import("../view/Chat"), component: () => import("../view/Chat"),
}, },
{ {
name: "Friend", name: "Friend",
path: "/home/friend", path: "/home/friend",
component: () => import("../view/Friend"), component: () => import("../view/Friend"),
}, },
{ {
name: "GROUP", name: "GROUP",
path: "/home/group", path: "/home/group",
component: () => import("../view/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); let chat = this.getters.findChatByFriend(friend.id);
// 更新会话中的群名和头像 // 更新会话中的群名和头像
if (chat && (chat.headImage != friend.headImageThumb || if (chat && (chat.headImage != friend.headImageThumb ||
chat.showName != friend.nickName)) { chat.showName != friend.nickName)) {
chat.headImage = friend.headImageThumb; chat.headImage = friend.headImageThumb;
chat.showName = friend.nickName; chat.showName = friend.nickName;
chat.stored = false; chat.stored = false;
@ -259,7 +259,7 @@ export default {
updateChatFromGroup(state, group) { updateChatFromGroup(state, group) {
let chat = this.getters.findChatByGroup(group.id); let chat = this.getters.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb || if (chat && (chat.headImage != group.headImageThumb ||
chat.showName != group.showGroupName)) { chat.showName != group.showGroupName)) {
// 更新会话中的群名称和头像 // 更新会话中的群名称和头像
chat.headImage = group.headImageThumb; chat.headImage = group.headImageThumb;
chat.showName = group.showGroupName; chat.showName = group.showGroupName;
@ -280,7 +280,7 @@ export default {
} }
}, },
refreshChats(state) { refreshChats(state) {
if(!cacheChats){ if (!cacheChats) {
return; return;
} }
// 排序 // 排序
@ -369,7 +369,7 @@ export default {
return state.loadingPrivateMsg || state.loadingGroupMsg return state.loadingPrivateMsg || state.loadingGroupMsg
}, },
findChats: (state, getters) => () => { findChats: (state, getters) => () => {
if(cacheChats && getters.isLoading()){ if (cacheChats && getters.isLoading()) {
return cacheChats; return cacheChats;
} }
return state.chats; return state.chats;

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

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

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

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

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

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

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

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

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

@ -1,15 +1,15 @@
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
import {RTC_STATE} from "../api/enums.js" import { RTC_STATE } from "../api/enums.js"
export default { export default {
state: { state: {
userInfo: { userInfo: {
}, },
rtcInfo: { rtcInfo: {
friend: {}, // 好友信息 friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音 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) { setUserInfo(state, userInfo) {
state.userInfo = userInfo state.userInfo = userInfo
}, },
setRtcInfo(state, rtcInfo ){ setRtcInfo(state, rtcInfo) {
state.rtcInfo = rtcInfo; state.rtcInfo = rtcInfo;
}, },
setRtcState(state,rtcState){ setRtcState(state, rtcState) {
state.rtcInfo.state = rtcState; state.rtcInfo.state = rtcState;
}, },
clear(state){ clear(state) {
state.userInfo = {}; state.userInfo = {};
state.rtcInfo = { state.rtcInfo = {
friend: {}, friend: {},
@ -32,16 +32,16 @@ export default {
}; };
} }
}, },
actions:{ actions: {
loadUser(context){ loadUser(context) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http({ http({
url: '/user/self', url: '/user/self',
method: 'GET' method: 'GET'
}).then((userInfo) => { }).then((userInfo) => {
context.commit("setUserInfo",userInfo); context.commit("setUserInfo", userInfo);
resolve(); resolve();
}).catch((res)=>{ }).catch((res) => {
reject(res); reject(res);
}); });
}) })

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

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

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

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

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

@ -10,17 +10,17 @@
<add-friend :dialogVisible="showAddFriend" @close="onCloseAddFriend"></add-friend> <add-friend :dialogVisible="showAddFriend" @close="onCloseAddFriend"></add-friend>
</div> </div>
<el-scrollbar class="friend-list-items"> <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" <friend-item v-show="friend.nickName.includes(searchText)" :index="index"
:active="friend === $store.state.friendStore.activeFriend" @chat="onSendMessage(friend)" :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> </friend-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
<el-container class="friend-box"> <el-container class="friend-box">
<div class="friend-header" v-show="userInfo.id"> <div class="friend-header" v-show="userInfo.id">
{{userInfo.nickName}} {{ userInfo.nickName }}
</div> </div>
<div v-show="userInfo.id"> <div v-show="userInfo.id">
<div class="friend-detail"> <div class="friend-detail">
@ -33,7 +33,7 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="昵称">{{ userInfo.nickName }} <el-descriptions-item label="昵称">{{ userInfo.nickName }}
</el-descriptions-item> </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-item label="签名">{{ userInfo.signature }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
@ -43,7 +43,7 @@
<el-button v-show="!isFriend" icon="el-icon-plus" type="primary" <el-button v-show="!isFriend" icon="el-icon-plus" type="primary"
@click="onAddFriend(userInfo)">加为好友</el-button> @click="onAddFriend(userInfo)">加为好友</el-button>
<el-button v-show="isFriend" icon="el-icon-delete" type="danger" <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> </div>
</div> </div>
@ -55,188 +55,188 @@
</template> </template>
<script> <script>
import FriendItem from "../components/friend/FriendItem.vue"; import FriendItem from "../components/friend/FriendItem.vue";
import AddFriend from "../components/friend/AddFriend.vue"; import AddFriend from "../components/friend/AddFriend.vue";
import HeadImage from "../components/common/HeadImage.vue"; import HeadImage from "../components/common/HeadImage.vue";
export default { export default {
name: "friend", name: "friend",
components: { components: {
FriendItem, FriendItem,
AddFriend, AddFriend,
HeadImage HeadImage
},
data() {
return {
searchText: "",
showAddFriend: false,
activeIdx: -1,
userInfo: {}
}
},
methods: {
onShowAddFriend() {
this.showAddFriend = true;
}, },
data() { onCloseAddFriend() {
return { this.showAddFriend = false;
searchText: "",
showAddFriend: false,
activeIdx: -1,
userInfo: {}
}
}, },
methods: { onActiveItem(friend, idx) {
onShowAddFriend() { this.$store.commit("activeFriend", idx);
this.showAddFriend = true; this.activeIdx = idx
}, this.loadUserInfo(friend, idx);
onCloseAddFriend() { },
this.showAddFriend = false; onDelItem(friend, idx) {
}, this.$confirm(`确认删除'${friend.nickName}',并清空聊天记录吗?`, '确认解除?', {
onActiveItem(friend, idx) { confirmButtonText: '确定',
this.$store.commit("activeFriend", idx); cancelButtonText: '取消',
this.activeIdx = idx type: 'warning'
this.loadUserInfo(friend, idx); }).then(() => {
},
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) {
this.$http({ this.$http({
url: "/friend/add", url: `/friend/delete/${friend.id}`,
method: "post", method: 'delete'
params: {
friendId: user.id
}
}).then((data) => { }).then((data) => {
this.$message.success("添加成功,对方已成为您的好友"); this.$message.success("删除好友成功");
let friend = { this.$store.commit("removeFriend", idx);
id: user.id, this.$store.commit("removePrivateChat", friend.id);
nickName: user.nickName,
headImage: user.headImage,
online: user.online
}
this.$store.commit("addFriend", friend);
}) })
}, })
onSendMessage(user) { },
let chat = { onAddFriend(user) {
type: 'PRIVATE', this.$http({
targetId: user.id, url: "/friend/add",
showName: user.nickName, method: "post",
headImage: user.headImageThumb, params: {
}; friendId: user.id
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);
} }
}, }).then((data) => {
updateFriendInfo(friend, user, index) { this.$message.success("添加成功,对方已成为您的好友");
// storestore let friend = {
friend = JSON.parse(JSON.stringify(friend)); id: user.id,
friend.headImage = user.headImageThumb; nickName: user.nickName,
friend.nickName = user.nickName; headImage: user.headImage,
this.$http({ online: user.online
url: "/friend/update", }
method: "put", this.$store.commit("addFriend", friend);
data: friend })
}).then(() => { },
this.$store.commit("updateFriend", friend); onSendMessage(user) {
this.$store.commit("updateChatFromFriend", user); let chat = {
}) type: 'PRIVATE',
}, targetId: user.id,
loadUserInfo(friend, index) { showName: user.nickName,
this.$http({ headImage: user.headImageThumb,
url: `/user/find/${friend.id}`, };
method: 'get' this.$store.commit("openChat", chat);
}).then((user) => { this.$store.commit("activeChat", 0);
this.userInfo = user; this.$router.push("/home/chat");
//
if (user.headImageThumb != friend.headImage ||
user.nickName != friend.nickName) {
this.updateFriendInfo(friend, user, index)
}
})
}
}, },
computed: { showFullImage() {
friendStore() { if (this.userInfo.headImage) {
return this.$store.state.friendStore; this.$store.commit('showFullImageBox', this.userInfo.headImage);
},
isFriend() {
return this.friendStore.friends.find((f) => f.id == this.userInfo.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)
}
})
}
},
computed: {
friendStore() {
return this.$store.state.friendStore;
},
isFriend() {
return this.friendStore.friends.find((f) => f.id == this.userInfo.id);
} }
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.friend-page { .friend-page {
.friend-list-box { .friend-list-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--im-background); background: var(--im-background);
.friend-list-header { .friend-list-header {
height: 50px; height: 50px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 8px; padding: 0 8px;
.add-btn { .add-btn {
padding: 5px !important; padding: 5px !important;
margin: 5px; margin: 5px;
font-size: 16px; font-size: 16px;
border-radius: 50%; border-radius: 50%;
}
} }
}
.friend-list-items { .friend-list-items {
flex: 1; flex: 1;
}
} }
}
.friend-box { .friend-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.friend-header { .friend-header {
height: 50px; height: 50px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 12px; padding: 0 12px;
font-size: var(--im-font-size-larger); font-size: var(--im-font-size-larger);
border-bottom: var(--im-border); border-bottom: var(--im-border);
box-sizing: border-box; box-sizing: border-box;
} }
.friend-detail { .friend-detail {
display: flex; display: flex;
padding: 50px 80px 20px 80px; padding: 50px 80px 20px 80px;
text-align: center; text-align: center;
.info-item {
margin-left: 20px;
background-color: #ffffff;
border: 1px #ddd solid;
}
.description { .info-item {
padding: 20px 20px 0 20px; margin-left: 20px;
} background-color: #ffffff;
border: 1px #ddd solid;
} }
.frient-btn-group { .description {
text-align: left !important; padding: 20px 20px 0 20px;
padding: 20px;
} }
} }
.frient-btn-group {
text-align: left !important;
padding: 20px;
}
} }
}
</style> </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> <el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button>
</div> </div>
<el-scrollbar class="group-list-items"> <el-scrollbar class="group-list-items">
<div v-for="(group,index) in groupStore.groups" :key="index"> <div v-for="(group, index) in groupStore.groups" :key="index">
<group-item v-show="!group.quit&&group.showGroupName.includes(searchText)" :group="group" <group-item v-show="!group.quit && group.showGroupName.includes(searchText)" :group="group"
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)"> :active="group === groupStore.activeGroup" @click.native="onActiveItem(group, index)">
</group-item> </group-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
@ -23,21 +23,20 @@
<div v-show="activeGroup.id"> <div v-show="activeGroup.id">
<div class="group-info"> <div class="group-info">
<div> <div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" <file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" :showLoading="true"
:showLoading="true" :maxSize="maxSize" @success="onUploadSuccess" :maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar"> <img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload> </file-upload>
<head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage" <head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage"
:name="activeGroup.showGroupName" radius="10%"> :name="activeGroup.showGroupName" radius="10%">
</head-image> </head-image>
<el-button class="send-btn" icon="el-icon-position" type="primary" <el-button class="send-btn" icon="el-icon-position" type="primary" @click="onSendMessage()">发消息
@click="onSendMessage()">发消息
</el-button> </el-button>
</div> </div>
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small" <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-form-item label="群聊名称" prop="name">
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input> <el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
</el-form-item> </el-form-item>
@ -46,15 +45,15 @@
</el-form-item> </el-form-item>
<el-form-item label="群名备注"> <el-form-item label="群名备注">
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name" <el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
maxlength="20"></el-input> maxlength="20"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="我在本群的昵称"> <el-form-item label="我在本群的昵称">
<el-input v-model="activeGroup.remarkNickName" maxlength="20" <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>
<el-form-item label="群公告"> <el-form-item label="群公告">
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3" <el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3" maxlength="1024"
maxlength="1024" placeholder="群主未设置"></el-input> placeholder="群主未设置"></el-input>
</el-form-item> </el-form-item>
<div> <div>
<el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button> <el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button>
@ -68,16 +67,15 @@
<div class="group-member-list"> <div class="group-member-list">
<div v-for="(member) in groupMembers" :key="member.id"> <div v-for="(member) in groupMembers" :key="member.id">
<group-member v-show="!member.quit" class="group-member" :member="member" <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>
<div class="group-invite"> <div class="group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()"> <div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
<i class="el-icon-plus"></i> <i class="el-icon-plus"></i>
</div> </div>
<div class="invite-member-text">邀请</div> <div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" <add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" :members="groupMembers"
:members="groupMembers" @reload="loadGroupMembers" @reload="loadGroupMembers" @close="onCloseAddGroupMember"></add-group-member>
@close="onCloseAddGroupMember"></add-group-member>
</div> </div>
</div> </div>
</div> </div>

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

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

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

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

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

BIN
截图/交流群2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Loading…
Cancel
Save