Browse Source

!123 发布3.1.0版本

Merge pull request !123 from blue/v_3.0.0
master
blue 1 year ago
committed by Gitee
parent
commit
b7d202795f
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 1
      .gitignore
  2. 6
      README.md
  3. 1
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  4. 2
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java
  5. 70
      im-uniapp/App.vue
  6. 6
      im-uniapp/common/date.js
  7. 52
      im-uniapp/common/enums.js
  8. 24
      im-uniapp/common/messageType.js
  9. 8
      im-uniapp/common/recorder-h5.js
  10. 4
      im-uniapp/common/request.js
  11. 10
      im-uniapp/common/wssocket.js
  12. 40
      im-uniapp/components/bar/arrow-bar.vue
  13. 17
      im-uniapp/components/bar/bar-group.vue
  14. 68
      im-uniapp/components/bar/btn-bar.vue
  15. 61
      im-uniapp/components/bar/switch-bar.vue
  16. 52
      im-uniapp/components/chat-at-box/chat-at-box.vue
  17. 21
      im-uniapp/components/chat-group-readed/chat-group-readed.vue
  18. 70
      im-uniapp/components/chat-item/chat-item.vue
  19. 144
      im-uniapp/components/chat-message-item/chat-message-item.vue
  20. 50
      im-uniapp/components/chat-record/chat-record.vue
  21. 11
      im-uniapp/components/file-upload/file-upload.vue
  22. 34
      im-uniapp/components/friend-item/friend-item.vue
  23. 36
      im-uniapp/components/group-item/group-item.vue
  24. 18
      im-uniapp/components/group-member-selector/group-member-selector.vue
  25. 17
      im-uniapp/components/group-rtc-join/group-rtc-join.vue
  26. 51
      im-uniapp/components/head-image/head-image.vue
  27. 21
      im-uniapp/components/image-upload/image-upload.vue
  28. 20
      im-uniapp/components/loading/loading.vue
  29. 121
      im-uniapp/components/long-press-menu/long-press-menu.vue
  30. 119
      im-uniapp/components/nav-bar/nav-bar.vue
  31. 121
      im-uniapp/components/pop-menu/pop-menu.vue
  32. 60
      im-uniapp/im-var.scss
  33. 141
      im-uniapp/im.scss
  34. 13
      im-uniapp/main.js
  35. 167
      im-uniapp/manifest.json
  36. 63
      im-uniapp/pages.json
  37. 110
      im-uniapp/pages/chat/chat-box.vue
  38. 11
      im-uniapp/pages/chat/chat-group-video.vue
  39. 27
      im-uniapp/pages/chat/chat-private-video.vue
  40. 58
      im-uniapp/pages/chat/chat.vue
  41. 106
      im-uniapp/pages/common/user-info.vue
  42. 38
      im-uniapp/pages/friend/friend-add.vue
  43. 81
      im-uniapp/pages/friend/friend.vue
  44. 37
      im-uniapp/pages/group/group-edit.vue
  45. 110
      im-uniapp/pages/group/group-info.vue
  46. 47
      im-uniapp/pages/group/group-invite.vue
  47. 46
      im-uniapp/pages/group/group-member.vue
  48. 37
      im-uniapp/pages/group/group.vue
  49. 20
      im-uniapp/pages/login/login.vue
  50. 54
      im-uniapp/pages/mine/mine-edit.vue
  51. 33
      im-uniapp/pages/mine/mine-password.vue
  52. 98
      im-uniapp/pages/mine/mine.vue
  53. 20
      im-uniapp/pages/register/register.vue
  54. BIN
      im-uniapp/static/logo/logo.png
  55. BIN
      im-uniapp/static/tarbar/mine.png
  56. BIN
      im-uniapp/static/tarbar/mine_active.png
  57. 26
      im-uniapp/store/chatStore.js
  58. 2
      im-uniapp/store/userStore.js
  59. 46
      im-uniapp/uni.scss
  60. BIN
      im-uniapp/unpackage/res/icons/1024x1024.png
  61. BIN
      im-uniapp/unpackage/res/icons/120x120.png
  62. BIN
      im-uniapp/unpackage/res/icons/144x144.png
  63. BIN
      im-uniapp/unpackage/res/icons/152x152.png
  64. BIN
      im-uniapp/unpackage/res/icons/167x167.png
  65. BIN
      im-uniapp/unpackage/res/icons/180x180.png
  66. BIN
      im-uniapp/unpackage/res/icons/192x192.png
  67. BIN
      im-uniapp/unpackage/res/icons/20x20.png
  68. BIN
      im-uniapp/unpackage/res/icons/29x29.png
  69. BIN
      im-uniapp/unpackage/res/icons/40x40.png
  70. BIN
      im-uniapp/unpackage/res/icons/58x58.png
  71. BIN
      im-uniapp/unpackage/res/icons/60x60.png
  72. BIN
      im-uniapp/unpackage/res/icons/72x72.png
  73. BIN
      im-uniapp/unpackage/res/icons/76x76.png
  74. BIN
      im-uniapp/unpackage/res/icons/80x80.png
  75. BIN
      im-uniapp/unpackage/res/icons/87x87.png
  76. BIN
      im-uniapp/unpackage/res/icons/96x96.png
  77. 2
      im-uniapp/vite.config.js
  78. 24
      im-web/package.json
  79. BIN
      im-web/public/logo.png
  80. 99
      im-web/src/App.vue
  81. 15
      im-web/src/api/camera.js
  82. 6
      im-web/src/api/date.js
  83. 6
      im-web/src/api/element.js
  84. 6
      im-web/src/api/emotion.js
  85. 2
      im-web/src/api/httpRequest.js
  86. 24
      im-web/src/api/messageType.js
  87. 36
      im-web/src/api/rtcGroupApi.js
  88. 16
      im-web/src/api/rtcPrivateApi.js
  89. 26
      im-web/src/api/webrtc.js
  90. 36
      im-web/src/api/wssocket.js
  91. BIN
      im-web/src/assets/image/online_app.png
  92. BIN
      im-web/src/assets/image/online_web.png
  93. 112
      im-web/src/assets/style/element.scss
  94. 43
      im-web/src/assets/style/global.css
  95. 91
      im-web/src/assets/style/im.scss
  96. 6
      im-web/src/assets/style/thems.scss
  97. 28
      im-web/src/components/chat/ChatAtBox.vue
  98. 126
      im-web/src/components/chat/ChatBox.vue
  99. 23
      im-web/src/components/chat/ChatGroupMember.vue
  100. 4
      im-web/src/components/chat/ChatGroupReaded.vue

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: 1012017031
欢迎进群与小伙们一起交流, **申请加群前请务必先star哦** 欢迎进群与小伙们一起交流, **申请加群前请务必先star哦**

1
im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java

@ -65,6 +65,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
member.setGroupId(group.getId()); member.setGroupId(group.getId());
member.setUserId(user.getId()); member.setUserId(user.getId());
member.setHeadImage(user.getHeadImageThumb()); member.setHeadImage(user.getHeadImageThumb());
member.setUserNickName(user.getNickName());
member.setRemarkNickName(vo.getRemarkNickName()); member.setRemarkNickName(vo.getRemarkNickName());
member.setRemarkGroupName(vo.getRemarkGroupName()); member.setRemarkGroupName(vo.getRemarkGroupName());
groupMemberService.save(member); groupMemberService.save(member);

2
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java

@ -314,7 +314,7 @@ public class WebrtcPrivateServiceImpl implements WebrtcPrivateService {
sendMessage.setSender(new IMUserInfo(rtcSession.getCallerId(), rtcSession.getCallerTerminal())); sendMessage.setSender(new IMUserInfo(rtcSession.getCallerId(), rtcSession.getCallerTerminal()));
sendMessage.setRecvId(rtcSession.getCallerId()); sendMessage.setRecvId(rtcSession.getCallerId());
sendMessage.setSendToSelf(false); sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false); sendMessage.setSendResult(true);
sendMessage.setData(messageInfo); sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage); imClient.sendPrivateMessage(sendMessage);
// 推给接听方 // 推给接听方

70
im-uniapp/App.vue

@ -1,12 +1,12 @@
<script> <script>
import App from './App' import App from './App'
import http from './common/request'; import http from './common/request';
import * as msgType from './common/messageType'; import * as msgType from './common/messageType';
import * as enums from './common/enums'; import * as enums from './common/enums';
import * as wsApi from './common/wssocket'; import * as wsApi from './common/wssocket';
import UNI_APP from '@/.env.js' import UNI_APP from '@/.env.js'
export default { export default {
data() { data() {
return { return {
isExit: false, // 退 isExit: false, // 退
@ -79,7 +79,7 @@
return Promise.all(promises); return Promise.all(promises);
}) })
}, },
unloadStore(){ unloadStore() {
this.friendStore.clear(); this.friendStore.clear();
this.groupStore.clear(); this.groupStore.clear();
this.chatStore.clear(); this.chatStore.clear();
@ -378,31 +378,53 @@
// #endif // #endif
}) })
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@/uni_modules/uview-plus/index.scss"; @import "@/uni_modules/uview-plus/index.scss";
@import url('./static/icon/iconfont.css'); @import "@/im.scss";
@import url('./static/icon/iconfont.css');
// #ifdef H5 // #ifdef H5
uni-page-head { uni-page-head {
display: none; // h5 display: none; // h5
} }
// #endif
// #endif
.tab-page { .tab-page {
position: relative;
display: flex;
flex-direction: column;
// #ifdef H5 // #ifdef H5
height: calc(100vh - 50px); // h5100vh height: calc(100vh - 50px - $im-nav-bar-height); // h5100vh
top: $im-nav-bar-height;
// #endif // #endif
// #ifndef H5 // #ifndef H5
height: calc(100vh); height: calc(100vh - var(--status-bar-height) - $im-nav-bar-height); // app
top: calc($im-nav-bar-height + var(--status-bar-height));
// #endif // #endif
background-color: #f8f8f8; color: $im-text-color;
} background-color: $im-bg;
font-size: $im-font-size;
font-family: $font-family;
}
.page { .page {
height: calc(100vh); position: relative;
background-color: #f8f8f8; // #ifdef H5
} height: calc(100vh - $im-nav-bar-height); // app
top: $im-nav-bar-height;
// #endif
// #ifndef H5
height: calc(100vh - var(--status-bar-height) - $im-nav-bar-height); // app
top: calc($im-nav-bar-height + var(--status-bar-height));
// #endif
color: $im-text-color;
background-color: $im-bg;
font-size: $im-font-size;
font-family: $font-family;
}
</style> </style>

6
im-uniapp/common/date.js

@ -20,8 +20,8 @@ let toTimeText = (timeStamp, simple) => {
} 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,

52
im-uniapp/common/enums.js

@ -2,18 +2,18 @@
const MESSAGE_TYPE = { const MESSAGE_TYPE = {
TEXT: 0, TEXT: 0,
IMAGE: 1, IMAGE: 1,
FILE:2, FILE: 2,
AUDIO:3, AUDIO: 3,
VIDEO:4, VIDEO: 4,
RECALL:10, RECALL: 10,
READED:11, READED: 11,
RECEIPT:12, RECEIPT: 12,
TIP_TIME:20, TIP_TIME: 20,
TIP_TEXT:21, TIP_TEXT: 21,
LOADING:30, LOADING: 30,
ACT_RT_VOICE:40, ACT_RT_VOICE: 40,
ACT_RT_VIDEO:41, ACT_RT_VIDEO: 41,
USER_BANNED:50, USER_BANNED: 50,
RTC_CALL_VOICE: 100, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101, RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,
@ -22,18 +22,18 @@ const MESSAGE_TYPE = {
RTC_FAILED: 105, RTC_FAILED: 105,
RTC_HANDUP: 106, RTC_HANDUP: 106,
RTC_CANDIDATE: 107, RTC_CANDIDATE: 107,
RTC_GROUP_SETUP:200, RTC_GROUP_SETUP: 200,
RTC_GROUP_ACCEPT:201, RTC_GROUP_ACCEPT: 201,
RTC_GROUP_REJECT:202, RTC_GROUP_REJECT: 202,
RTC_GROUP_FAILED:203, RTC_GROUP_FAILED: 203,
RTC_GROUP_CANCEL:204, RTC_GROUP_CANCEL: 204,
RTC_GROUP_QUIT:205, RTC_GROUP_QUIT: 205,
RTC_GROUP_INVITE:206, RTC_GROUP_INVITE: 206,
RTC_GROUP_JOIN:207, RTC_GROUP_JOIN: 207,
RTC_GROUP_OFFER:208, RTC_GROUP_OFFER: 208,
RTC_GROUP_ANSWER:209, RTC_GROUP_ANSWER: 209,
RTC_GROUP_CANDIDATE:210, RTC_GROUP_CANDIDATE: 210,
RTC_GROUP_DEVICE:211 RTC_GROUP_DEVICE: 211
} }
const USER_STATE = { const USER_STATE = {
@ -50,8 +50,8 @@ const TERMINAL_TYPE = {
const MESSAGE_STATUS = { const MESSAGE_STATUS = {
UNSEND: 0, UNSEND: 0,
SENDED: 1, SENDED: 1,
RECALL:2, RECALL: 2,
READED:3 READED: 3
} }
export { export {

24
im-uniapp/common/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;
} }

8
im-uniapp/common/recorder-h5.js

@ -3,7 +3,7 @@ import UNI_APP from '@/.env.js';
let rc = null; let rc = null;
let start = () => { let start = () => {
if(rc != null){ if (rc != null) {
close(); close();
} }
rc = new Recorder(); rc = new Recorder();
@ -22,7 +22,7 @@ let close = () => {
let upload = () => { let upload = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const wavBlob = rc.getWAVBlob(); const wavBlob = rc.getWAVBlob();
const newbolb = new Blob([wavBlob], { type: 'audio/wav'}) const newbolb = new Blob([wavBlob], { type: 'audio/wav' })
const name = new Date().getDate() + '.wav'; const name = new Date().getDate() + '.wav';
const file = new File([newbolb], name) const file = new File([newbolb], name)
uni.uploadFile({ uni.uploadFile({
@ -34,10 +34,10 @@ let upload = () => {
name: 'file', name: 'file',
success: (res) => { success: (res) => {
let r = JSON.parse(res.data); let r = JSON.parse(res.data);
if(r.code != 200){ if (r.code != 200) {
console.log(res) console.log(res)
reject(r.message); reject(r.message);
}else { } else {
const data = { const data = {
duration: parseInt(rc.duration), duration: parseInt(rc.duration),
url: r.data url: r.data

4
im-uniapp/common/request.js

@ -11,7 +11,7 @@ const request = (options) => {
if (loginInfo) { if (loginInfo) {
header.accessToken = loginInfo.accessToken; header.accessToken = loginInfo.accessToken;
} }
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
uni.request({ uni.request({
url: UNI_APP.BASE_URL + options.url, url: UNI_APP.BASE_URL + options.url,
method: options.method || 'GET', method: options.method || 'GET',
@ -72,7 +72,7 @@ const request = (options) => {
const reqRefreshToken = (loginInfo) => { const reqRefreshToken = (loginInfo) => {
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
uni.request({ uni.request({
method: 'PUT', method: 'PUT',
url: UNI_APP.BASE_URL + '/refreshToken', url: UNI_APP.BASE_URL + '/refreshToken',

10
im-uniapp/common/wssocket.js

@ -55,7 +55,7 @@ let init = () => {
console.log(e) console.log(e)
isConnect = false; isConnect = false;
// APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连 // APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连
closeCallBack && closeCallBack({code: 1006}); closeCallBack && closeCallBack({ code: 1006 });
}) })
}; };
@ -92,7 +92,7 @@ let reconnect = (wsurl, accessToken) => {
let timeDiff = new Date().getTime() - lastConnectTime.getTime() let timeDiff = new Date().getTime() - lastConnectTime.getTime()
let delay = timeDiff < 10000 ? 10000 - timeDiff : 0; let delay = timeDiff < 10000 ? 10000 - timeDiff : 0;
rec && clearTimeout(rec); rec && clearTimeout(rec);
rec = setTimeout(function() { rec = setTimeout(function () {
connect(wsurl, accessToken); connect(wsurl, accessToken);
}, delay); }, delay);
}; };
@ -119,7 +119,7 @@ let close = (code) => {
var heartCheck = { var heartCheck = {
timeout: 10000, //每段时间发送一次心跳包 这里设置为30s timeout: 10000, //每段时间发送一次心跳包 这里设置为30s
timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象) timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
start: function() { start: function () {
if (isConnect) { if (isConnect) {
console.log('发送WebSocket心跳') console.log('发送WebSocket心跳')
let heartBeat = { let heartBeat = {
@ -134,9 +134,9 @@ var 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);
} }

40
im-uniapp/components/bar/arrow-bar.vue

@ -0,0 +1,40 @@
<template>
<view class="arrow-bar">
<text class="title">{{ title }}</text>
<uni-icons class="arrow" type="right" size="16"></uni-icons>
</view>
</template>
<script>
export default {
name: "arrow-bar",
props: {
title: {
type: String,
required: true
}
},
}
</script>
<style lang="scss" scoped>
.arrow-bar {
width: 100%;
height: 90rpx;
font-size: 30rpx;
color: black;
margin-top: 5rpx;
background-color: white;
line-height: 90rpx;
display: flex;
.title {
flex: 1;
margin-left: 40rpx;
}
.arrow {
margin-right: 40rpx;
}
}
</style>

17
im-uniapp/components/bar/bar-group.vue

@ -0,0 +1,17 @@
<template>
<view class="bar-group">
<slot></slot>
</view>
</template>
<script>
export default {
name: "bar-group"
}
</script>
<style lang="scss" scoped>
.bar-group {
margin: 20rpx 0;
}
</style>

68
im-uniapp/components/bar/btn-bar.vue

@ -0,0 +1,68 @@
<template>
<view class="btn-bar" :style="style">
<text v-if="icon" class="icon iconfont" :class="icon"></text>
<text class="title">{{ title }}</text>
</view>
</template>
<script>
export default {
name: "btn-bar",
props: {
title: {
type: String,
required: true
},
icon: {
type: String,
required: false
},
type: {
type: String,
default: "normal"
},
color: {
type: String,
default: "#000"
}
},
computed: {
style() {
let color = "#000";
switch (this.type) {
case 'danger':
color = "#f14747";
break;
case 'primary':
color = "#35567f";
break;
}
return `color: ${color};`
}
}
}
</script>
<style lang="scss" scoped>
.btn-bar {
width: 100%;
height: 100rpx;
margin-top: 5rpx;
background-color: white;
line-height: 100rpx;
text-align: center;
display: flex;
justify-content: center;
.icon {
font-size: 40rpx;
font-weight: 600;
margin-right: 10rpx;
}
.title {
font-size: 32rpx;
font-weight: 600;
}
}
</style>

61
im-uniapp/components/bar/switch-bar.vue

@ -0,0 +1,61 @@
<template>
<view class="switch-bar">
<text class="title">{{ title }}</text>
<switch class="switch" :checked="checked" color="#18bc37" @change="onChange"></switch>
</view>
</template>
<script>
export default {
name: "switch-bar",
props: {
title: {
type: String,
required: true
},
checked: {
type: Boolean,
default: false
}
},
data() {
return {
value: this.checked
}
},
methods: {
onChange(e) {
this.value = true;
setTimeout(() => {
this.value = false;
}, 100)
//this.value = false;
this.$emit('change', e);
}
}
}
</script>
<style lang="scss" scoped>
.switch-bar {
width: 100%;
height: 100rpx;
font-size: 34rpx;
color: black;
margin-top: 5rpx;
background-color: white;
line-height: 100rpx;
display: flex;
.title {
flex: 1;
margin-left: 40rpx;
}
.switch {
margin-right: 40rpx;
}
}
</style>

52
im-uniapp/components/chat-at-box/chat-at-box.vue

@ -4,13 +4,13 @@
<view class="chat-at-top"> <view class="chat-at-top">
<view class="chat-at-tip"> 选择要提醒的人</view> <view class="chat-at-tip"> 选择要提醒的人</view>
<button class="chat-at-btn" type="warn" size="mini" @click="onClean()">清空 </button> <button class="chat-at-btn" type="warn" size="mini" @click="onClean()">清空 </button>
<button class="chat-at-btn" type="primary" size="mini" @click="onOk()">确定({{atUserIds.length}}) <button class="chat-at-btn" type="primary" size="mini" @click="onOk()">确定({{ atUserIds.length }})
</button> </button>
</view> </view>
<scroll-view v-show="atUserIds.length>0" scroll-x="true" scroll-left="120"> <scroll-view v-show="atUserIds.length > 0" scroll-x="true" scroll-left="120">
<view class="at-user-items"> <view class="at-user-items">
<view v-for="m in showMembers" v-show="m.checked" class="at-user-item"> <view v-for="m in showMembers" v-show="m.checked" class="at-user-item" :key="m.userId">
<head-image :name="m.showNickName" :url="m.headImage" :size="60"></head-image> <head-image :name="m.showNickName" :url="m.headImage" size="mini"></head-image>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@ -19,15 +19,14 @@
</view> </view>
<view class="member-items"> <view class="member-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="m in showMembers" v-show="m.showNickName.includes(searchText)" <view v-for="m in showMembers" v-show="m.showNickName.includes(searchText)" :key="m.userId">
:key="m.userId"> <view class="member-item" :class="{ checked: m.checked }" @click="onSwitchChecked(m)">
<view class="member-item" @click="onSwitchChecked(m)">
<head-image :name="m.showNickName" :online="m.online" :url="m.headImage" <head-image :name="m.showNickName" :online="m.online" :url="m.headImage"
:size="90"></head-image> size="small"></head-image>
<view class="member-name">{{ m.showNickName}}</view> <view class="member-name">{{ m.showNickName }}</view>
<view class="member-checked"> <!-- <view class="member-checked">-->
<radio :checked="m.checked" @click.stop="onSwitchChecked(m)" /> <!-- <radio :checked="m.checked" @click.stop="onSwitchChecked(m)" />-->
</view> <!-- </view>-->
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@ -37,7 +36,7 @@
</template> </template>
<script> <script>
export default { export default {
name: "chat-at-box", name: "chat-at-box",
props: { props: {
ownerId: { ownerId: {
@ -50,21 +49,21 @@
data() { data() {
return { return {
searchText: "", searchText: "",
showMembers:[] showMembers: []
}; };
}, },
methods: { methods: {
init(atUserIds) { init(atUserIds) {
this.showMembers = []; this.showMembers = [];
let userId = this.userStore.userInfo.id; let userId = this.userStore.userInfo.id;
if(this.ownerId == userId){ if (this.ownerId == userId) {
this.showMembers.push({ this.showMembers.push({
userId:-1, userId: -1,
showNickName: "全体成员" showNickName: "全体成员"
}) })
} }
this.members.forEach((m) => { this.members.forEach((m) => {
if(!m.quit && m.userId != userId){ if (!m.quit && m.userId != userId) {
m.checked = atUserIds.indexOf(m.userId) >= 0; m.checked = atUserIds.indexOf(m.userId) >= 0;
this.showMembers.push(m); this.showMembers.push(m);
} }
@ -101,19 +100,18 @@
return ids; return ids;
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-at-box { .chat-at-box {
position: relative; position: relative;
border: #dddddd solid 1rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: white; background-color: white;
padding: 10rpx; padding: 10rpx;
border-radius: 15rpx; //border-radius: 15rpx;
.chat-at-top { .chat-at-top {
display: flex; display: flex;
@ -149,19 +147,23 @@
overflow: hidden; overflow: hidden;
.member-item { .member-item {
height: 120rpx; height: 110rpx;
display: flex; display: flex;
position: relative; position: relative;
padding: 0 30rpx; padding: 0 30rpx;
align-items: center; align-items: center;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
margin-bottom: 1px;
&.checked {
background-color: $im-color-primary-light-9;
}
.member-name { .member-name {
flex: 1; flex: 1;
padding-left: 20rpx; padding-left: 20rpx;
font-size: 30rpx; font-size: $im-font-size;
font-weight: 600;
line-height: 60rpx; line-height: 60rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -172,5 +174,5 @@
height: 800rpx; height: 800rpx;
} }
} }
} }
</style> </style>

21
im-uniapp/components/chat-group-readed/chat-group-readed.vue

@ -2,7 +2,8 @@
<uni-popup ref="popup" type="bottom"> <uni-popup ref="popup" type="bottom">
<view class="chat-group-readed"> <view class="chat-group-readed">
<view class="uni-padding-wrap uni-common-mt"> <view class="uni-padding-wrap uni-common-mt">
<uni-segmented-control :current="current" :values="items" style-type="button" active-color="#587ff0" @clickItem="onClickItem"/> <uni-segmented-control :current="current" :values="items" style-type="button"
@clickItem="onClickItem" />
</view> </view>
<view class="content"> <view class="content">
<view v-if="current === 0"> <view v-if="current === 0">
@ -11,7 +12,7 @@
<view class="member-item"> <view class="member-item">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage" <head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
:size="90"></head-image> :size="90"></head-image>
<view class="member-name">{{ m.aliasName}}</view> <view class="member-name">{{ m.aliasName }}</view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@ -22,7 +23,7 @@
<view class="member-item"> <view class="member-item">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage" <head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
:size="90"></head-image> :size="90"></head-image>
<view class="member-name">{{ m.aliasName}}</view> <view class="member-name">{{ m.aliasName }}</view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@ -33,7 +34,7 @@
</template> </template>
<script> <script>
export default { export default {
name: "chat-group-readed", name: "chat-group-readed",
data() { data() {
return { return {
@ -86,22 +87,22 @@
}) })
}) })
}, },
onClickItem(e){ onClickItem(e) {
this.current = e.currentIndex; this.current = e.currentIndex;
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-group-readed { .chat-group-readed {
position: relative; position: relative;
border: #dddddd solid 1rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: white; background-color: white;
padding: 10rpx; padding: 10rpx;
border-radius: 15rpx;
//border-radius: 15rpx;
.scroll-bar { .scroll-bar {
height: 800rpx; height: 800rpx;
} }
@ -127,5 +128,5 @@
} }
} }
</style> </style>

70
im-uniapp/components/chat-item/chat-item.vue

@ -1,30 +1,30 @@
<template> <template>
<view class="chat-item" :class="active?'active':''"> <view class="chat-item" :class="active ? 'active' : ''">
<!--rich-text中的表情包会屏蔽事件所以这里用一个遮罩层捕获点击事件 --> <!--rich-text中的表情包会屏蔽事件所以这里用一个遮罩层捕获点击事件 -->
<view class="mask" @tap="showChatBox()"></view> <view class="mask" @tap="showChatBox()"></view>
<view class="left"> <view class="left">
<head-image :url="chat.headImage" :name="chat.showName" :size="90"></head-image> <head-image :url="chat.headImage" :name="chat.showName"></head-image>
</view> </view>
<view class="chat-right"> <view class="chat-right">
<view class="chat-name"> <view class="chat-name">
<view class="chat-name-text"> <view class="chat-name-text">
<view>{{chat.showName}}</view> <view>{{ chat.showName }}</view>
<uni-tag v-if="chat.type=='GROUP'" circle text="群" size="small"></uni-tag> <uni-tag v-if="chat.type == 'GROUP'" circle text="群" size="small" type="primary"></uni-tag>
</view> </view>
<view class="chat-time">{{$date.toTimeText(chat.lastSendTime,true)}}</view> <view class="chat-time">{{ $date.toTimeText(chat.lastSendTime, true) }}</view>
</view> </view>
<view class="chat-content"> <view class="chat-content">
<view class="chat-at-text">{{atText}}</view> <view class="chat-at-text">{{ atText }}</view>
<view class="chat-send-name" v-if="isShowSendName">{{chat.sendNickName+':&nbsp;'}}</view> <view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view>
<rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent)"></rich-text> <rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent)"></rich-text>
<uni-badge v-if="chat.unreadCount>0" size="small" :max-num="99" :text="chat.unreadCount" /> <uni-badge v-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "chatItem", name: "chatItem",
data() { data() {
return {} return {}
@ -71,33 +71,34 @@
return ""; return "";
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.chat-item { .chat-item {
height: 100rpx; height: 96rpx;
display: flex; display: flex;
margin-bottom: 2rpx; margin-bottom: 2rpx;
position: relative; position: relative;
padding: 10rpx 20rpx; padding: 18rpx 20rpx;
align-items: center; align-items: center;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
background-color: #f5f6ff; background-color: $im-bg-active;
} }
&.active { &.active {
background-color: #f5f6ff; background-color: $im-bg-active;
} }
.mask { .mask {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
left: 0;
right: 0;
z-index: 99; z-index: 99;
} }
@ -108,27 +109,24 @@
align-items: center; align-items: center;
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
} }
.chat-right { .chat-right {
height: 100%;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
padding-left: 20rpx; padding-left: 20rpx;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
.chat-name { .chat-name {
display: flex; display: flex;
line-height: 44rpx;
height: 44rpx;
.chat-name-text { .chat-name-text {
flex: 1; flex: 1;
font-size: 30rpx; font-size: $im-font-size-large;
font-weight: 600;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@ -138,19 +136,15 @@
text-align: center; text-align: center;
margin-left: 5rpx; margin-left: 5rpx;
border: 0; border: 0;
height: 30rpx;
line-height: 30rpx;
font-size: 20rpx;
padding: 1px 5px; padding: 1px 5px;
background-color: #de1c1c; //opacity: 0.8;
opacity: 0.8;
} }
} }
.chat-time { .chat-time {
font-size: 26rpx; font-size: $im-font-size-smaller-extra;
color: $im-text-color-lighter;
text-align: right; text-align: right;
color: #888888;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
@ -158,27 +152,31 @@
.chat-content { .chat-content {
display: flex; display: flex;
line-height: 60rpx; font-size: $im-font-size-smaller;
height: 60rpx; color: $im-text-color-lighter;
padding-top: 8rpx;
.chat-at-text { .chat-at-text {
color: #c70b0b; color: $im-color-danger;
font-size: 24rpx;
} }
.chat-send-name { .chat-send-name {
font-size: 26rpx; font-size: $im-font-size-smaller;
} }
.chat-content-text { .chat-content-text {
flex: 1; flex: 1;
font-size: 28rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
}
img {
width: 40rpx !important;
height: 40rpx !important;
} }
} }
}
} }
}
</style> </style>

144
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -1,78 +1,78 @@
<template> <template>
<view class="chat-msg-item"> <view class="chat-msg-item">
<view class="chat-msg-tip" <view class="chat-msg-tip"
v-if="msgInfo.type==$enums.MESSAGE_TYPE.RECALL||msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT"> v-if="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{msgInfo.content}} {{ msgInfo.content }}
</view> </view>
<view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME"> <view class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{$date.toTimeText(msgInfo.sendTime)}} {{ $date.toTimeText(msgInfo.sendTime) }}
</view> </view>
<view class="chat-msg-normal" v-if="isNormal" :class="{'chat-msg-mine':msgInfo.selfSend}"> <view class="chat-msg-normal" v-if="isNormal" :class="{ 'chat-msg-mine': msgInfo.selfSend }">
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage" <head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
:name="showName" :size="80"></head-image> :name="showName" size="small"></head-image>
<view class="chat-msg-content"> <view class="chat-msg-content">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top"> <view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<text>{{showName}}</text> <text>{{ showName }}</text>
</view> </view>
<view class="chat-msg-bottom"> <view class="chat-msg-bottom">
<view v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT"> <view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<pop-menu :items="menuItems" @select="onSelectMenu"> <long-press-menu :items="menuItems" @select="onSelectMenu">
<rich-text class="chat-msg-text" :nodes="$emo.transform(msgInfo.content)"></rich-text> <rich-text class="chat-msg-text" :nodes="$emo.transform(msgInfo.content)"></rich-text>
</pop-menu> </long-press-menu>
</view> </view>
<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE"> <view class="chat-msg-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<pop-menu :items="menuItems" @select="onSelectMenu"> <long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="img-load-box"> <view class="img-load-box">
<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl" <image class="send-image" mode="widthFix" :src="JSON.parse(msgInfo.content).thumbUrl"
lazy-load="true" @click.stop="onShowFullImage()"> lazy-load="true" @click.stop="onShowFullImage()">
</image> </image>
<loading v-if="loading"></loading> <loading v-if="loading"></loading>
</view> </view>
</pop-menu> </long-press-menu>
<text title="发送失败" v-if="loadFail" @click="onSendFail" <text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text> class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
<view class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE"> <view class="chat-msg-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<pop-menu :items="menuItems" @select="onSelectMenu"> <long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="chat-file-box"> <view class="chat-file-box">
<view class="chat-file-info"> <view class="chat-file-info">
<uni-link class="chat-file-name" :text="data.name" showUnderLine="true" <uni-link class="chat-file-name" :text="data.name" showUnderLine="true"
color="#007BFF" :href="data.url"></uni-link> color="#007BFF" :href="data.url"></uni-link>
<view class="chat-file-size">{{fileSize}}</view> <view class="chat-file-size">{{ fileSize }}</view>
</view> </view>
<view class="chat-file-icon iconfont icon-file"></view> <view class="chat-file-icon iconfont icon-file"></view>
<loading v-if="loading"></loading> <loading v-if="loading"></loading>
</view> </view>
</pop-menu> </long-press-menu>
<text title="发送失败" v-if="loadFail" @click="onSendFail" <text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text> class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
<pop-menu v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" :items="menuItems" @select="onSelectMenu"> <long-press-menu v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" :items="menuItems" @select="onSelectMenu">
<view class="chat-msg-audio chat-msg-text" @click="onPlayAudio()"> <view class="chat-msg-audio chat-msg-text" @click="onPlayAudio()">
<text class="iconfont icon-voice-play"></text> <text class="iconfont icon-voice-play"></text>
<text class="chat-audio-text">{{JSON.parse(msgInfo.content).duration+'"'}}</text> <text class="chat-audio-text">{{ JSON.parse(msgInfo.content).duration + '"' }}</text>
<text v-if="audioPlayState=='PAUSE'" class="iconfont icon-play"></text> <text v-if="audioPlayState == 'PAUSE'" class="iconfont icon-play"></text>
<text v-if="audioPlayState=='PLAYING'" class="iconfont icon-pause"></text> <text v-if="audioPlayState == 'PLAYING'" class="iconfont icon-pause"></text>
</view> </view>
</pop-menu> </long-press-menu>
<pop-menu v-if="isAction" :items="menuItems" @select="onSelectMenu"> <long-press-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">
<view class="chat-realtime chat-msg-text" @click="$emit('call')"> <view class="chat-realtime chat-msg-text" @click="$emit('call')">
<text v-if="msgInfo.type==$enums.MESSAGE_TYPE.ACT_RT_VOICE" <text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE"
class="iconfont icon-chat-voice"></text> class="iconfont icon-chat-voice"></text>
<text v-if="msgInfo.type==$enums.MESSAGE_TYPE.ACT_RT_VIDEO" <text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VIDEO"
class="iconfont icon-chat-video"></text> class="iconfont icon-chat-video"></text>
<text>{{msgInfo.content}}</text> <text>{{ msgInfo.content }}</text>
</view> </view>
</pop-menu> </long-press-menu>
<view class="chat-msg-status" v-if="!isAction"> <view class="chat-msg-status" v-if="!isAction">
<text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId <text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text> && msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId <text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text> && msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</text>
</view> </view>
<view class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox"> <view class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text> <text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
<text v-else>{{msgInfo.readedCount}}人已读</text> <text v-else>{{ msgInfo.readedCount }}人已读</text>
</view> </view>
</view> </view>
</view> </view>
@ -83,7 +83,7 @@
</template> </template>
<script> <script>
export default { export default {
name: "chat-message-item", name: "chat-message-item",
props: { props: {
headImage: { headImage: {
@ -220,25 +220,24 @@
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.chat-msg-item { .chat-msg-item {
padding: 2rpx 20rpx; padding: 2rpx 20rpx;
.chat-msg-tip { .chat-msg-tip {
line-height: 60rpx; line-height: 60rpx;
text-align: center; text-align: center;
color: #555; color: $im-text-color-lighter;
font-size: 24rpx; font-size: $im-font-size-smaller-extra;
padding: 10rpx; padding: 10rpx;
} }
.chat-msg-normal { .chat-msg-normal {
position: relative; position: relative;
font-size: 0; margin-bottom: 22rpx;
margin-bottom: 15rpx;
padding-left: 110rpx; padding-left: 110rpx;
min-height: 80rpx; min-height: 80rpx;
@ -254,10 +253,9 @@
.chat-msg-top { .chat-msg-top {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
color: #333; color: $im-text-color-lighter;
font-size: 24rpx; font-size: $im-font-size-smaller;
line-height: 24rpx; line-height: $im-font-size-smaller;
} }
.chat-msg-bottom { .chat-msg-bottom {
@ -266,13 +264,13 @@
.chat-msg-text { .chat-msg-text {
position: relative; position: relative;
line-height: 60rpx; line-height: 1.6;
margin-top: 10rpx; margin-top: 10rpx;
padding: 8rpx 20rpx; padding: 16rpx 24rpx;
background-color: #eee; background-color: $im-bg;
border-radius: 20rpx; border-radius: 20rpx;
color: #333; color: $im-text-color;
font-size: 30rpx; font-size: $im-font-size;
text-align: left; text-align: left;
display: block; display: block;
word-break: break-all; word-break: break-all;
@ -287,9 +285,10 @@
width: 6rpx; width: 6rpx;
height: 6rpx; height: 6rpx;
border-style: solid dashed dashed; border-style: solid dashed dashed;
border-color: #eee transparent transparent; border-color: $im-bg transparent transparent;
overflow: hidden; overflow: hidden;
border-width: 18rpx; border-width: 18rpx;
//box-shadow: $im-box-shadow-dark;
} }
} }
@ -305,18 +304,16 @@
.send-image { .send-image {
min-width: 200rpx; min-width: 200rpx;
min-height: 200rpx; max-width: 420rpx;
max-width: 400rpx;
max-height: 400rpx;
border: 8rpx solid #ebebf5;
cursor: pointer; cursor: pointer;
border-radius: 4px;
} }
} }
.send-fail { .send-fail {
color: #e60c0c; color: $im-color-danger;
font-size: 30px; font-size: $im-font-size;
cursor: pointer; cursor: pointer;
margin: 0 20px; margin: 0 20px;
} }
@ -334,12 +331,10 @@
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
min-height: 80px; min-height: 60px;
border: #dddddd solid 1px; border-radius: 4px;
border-radius: 10rpx;
background-color: #eeeeee;
padding: 10px 15px; padding: 10px 15px;
box-shadow: 2px 2px 2px #c0c0c0; box-shadow: $im-box-shadow-dark;
.chat-file-info { .chat-file-info {
flex: 1; flex: 1;
@ -349,7 +344,6 @@
width: 300rpx; width: 300rpx;
.chat-file-name { .chat-file-name {
font-size: 16px;
font-weight: 600; font-weight: 600;
margin-bottom: 15px; margin-bottom: 15px;
word-break: break-all; word-break: break-all;
@ -380,7 +374,7 @@
} }
.icon-voice-play { .icon-voice-play {
font-size: 20px; font-size: 18px;
padding-right: 8px; padding-right: 8px;
} }
} }
@ -396,29 +390,29 @@
} }
.chat-msg-status { .chat-msg-status {
display: block; line-height: $im-font-size-smaller-extra;
font-size: $im-font-size-smaller-extra;
padding-top: 2rpx;
.chat-readed { .chat-readed {
font-size: 12px; display: block;
color: #888; padding-top: 2rpx;
font-weight: 600; color: $im-text-color-lighter;
} }
.chat-unread { .chat-unread {
font-size: 12px; color: $im-color-danger;
color: #f23c0f;
font-weight: 600;
} }
} }
.chat-receipt { .chat-receipt {
font-size: 13px; font-size: $im-font-size-smaller;
color: darkblue; color: $im-text-color-lighter;
font-weight: 600; font-weight: 600;
.icon-ok { .icon-ok {
font-size: 20px; font-size: 20px;
color: #329432; color: $im-color-success;
} }
} }
} }
@ -444,13 +438,13 @@
.chat-msg-text { .chat-msg-text {
margin-left: 10px; margin-left: 10px;
background-color: #587ff0; background-color: $im-color-primary-light-2;
color: #fff; color: #fff;
&:after { &:after {
left: auto; left: auto;
right: -10px; right: -9px;
border-top-color: #587ff0; border-top-color: $im-color-primary-light-2;
} }
} }
@ -489,5 +483,5 @@
} }
} }
} }
</style> </style>

50
im-uniapp/components/chat-record/chat-record.vue

@ -1,8 +1,8 @@
<template> <template>
<view class="chat-record"> <view class="chat-record">
<view class="chat-record-bar" id="chat-record-bar" :style="recordBarStyle" @click.stop="" <view class="chat-record-bar" :class="{ recording: recording }" id="chat-record-bar" @click.stop=""
@touchstart.prevent="onStartRecord" @touchmove.prevent="onTouchMove" @touchend.prevent="onEndRecord"> @touchstart.prevent="onStartRecord" @touchmove.prevent="onTouchMove" @touchend.prevent="onEndRecord">
{{recording?'正在录音':'长按 说话'}}</view> {{ recording ? '正在录音' : '长按 说话' }}</view>
<view v-if="recording" class="chat-record-window" :style="recordWindowStyle"> <view v-if="recording" class="chat-record-window" :style="recordWindowStyle">
<view class="rc-wave"> <view class="rc-wave">
<text class="note" style="--d: 0"></text> <text class="note" style="--d: 0"></text>
@ -13,18 +13,18 @@
<text class="note" style="--d: 5"></text> <text class="note" style="--d: 5"></text>
<text class="note" style="--d: 6"></text> <text class="note" style="--d: 6"></text>
</view> </view>
<view class="rc-tip">{{recordTip}}</view> <view class="rc-tip">{{ recordTip }}</view>
<view class="cancel-btn" @click="onCancel"> <view class="cancel-btn" @click="onCancel">
<uni-icons :class="moveToCancel?'red':'black'" type="clear" :size="moveToCancel?45:40"></uni-icons> <uni-icons :class="moveToCancel ? 'red' : 'black'" type="clear" :size="moveToCancel ? 45 : 40"></uni-icons>
</view> </view>
<view class="opt-tip" :class="moveToCancel?'red':'black'">{{moveToCancel? '松手取消':'松手发送,上划取消'}}</view> <view class="opt-tip" :class="moveToCancel ? 'red' : 'black'">{{ moveToCancel ? '松手取消' : '松手发送,上划取消' }}</view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "chat-record", name: "chat-record",
data() { data() {
return { return {
@ -130,25 +130,19 @@
const bottom = windowHeight - this.recordBarTop + 12; const bottom = windowHeight - this.recordBarTop + 12;
return `bottom:${bottom}px;` return `bottom:${bottom}px;`
}, },
recordBarStyle() {
const bgColor = this.recording ? "royalblue" : "white";
const textColor = this.recording ? "white" : "black";
return `background-color:${bgColor};
color:${textColor};`
},
recordTip() { recordTip() {
if (this.druation > 50) { if (this.druation > 50) {
return `${60-this.druation}s后将停止录音`; return `${60 - this.druation}s后将停止录音`;
} }
return `录音时长:${this.druation}s`; return `录音时长:${this.druation}s`;
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-record { .chat-record {
.rc-wave { .rc-wave {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
@ -157,7 +151,7 @@
height: 80rpx; height: 80rpx;
.note { .note {
background: linear-gradient(to top, #395ff3 0%, #89aff3 100%); background: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
width: 4px; width: 4px;
height: 50%; height: 50%;
border-radius: 5rpx; border-radius: 5rpx;
@ -167,19 +161,19 @@
@keyframes loading { @keyframes loading {
0% { 0% {
background-image: linear-gradient(to right, #395ff3 0%, #89aff3 100%); background-image: linear-gradient(to right, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
height: 20%; height: 20%;
border-radius: 5rpx; border-radius: 5rpx;
} }
50% { 50% {
background-image: linear-gradient(to top, #395ff3 0%, #a9cff3 100%); background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
height: 80%; height: 80%;
border-radius: 5rpx; border-radius: 5rpx;
} }
100% { 100% {
background-image: linear-gradient(to top, #395ff3 0%, #a9cff3 100%); background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
height: 20%; height: 20%;
border-radius: 5rpx; border-radius: 5rpx;
} }
@ -192,13 +186,19 @@
margin: 10rpx; margin: 10rpx;
border-radius: 10rpx; border-radius: 10rpx;
text-align: center; text-align: center;
box-shadow: $im-box-shadow;
&.recording {
background-color: $im-color-primary;
color: #fff;
}
} }
.chat-record-window { .chat-record-window {
position: fixed; position: fixed;
left: 0; left: 0;
right: 0;
height: 360rpx; height: 360rpx;
width: 100%;
background-color: rgba(255, 255, 255, 0.95); background-color: rgba(255, 255, 255, 0.95);
padding: 30rpx; padding: 30rpx;
@ -211,7 +211,8 @@
.rc-tip { .rc-tip {
text-align: center; text-align: center;
font-size: 30rpx; font-size: $im-font-size-small;
color: $im-text-color-light;
margin-top: 20rpx; margin-top: 20rpx;
} }
@ -229,12 +230,9 @@
} }
.red { .red {
color: red !important; color: $im-color-danger !important;
} }
.black {
color: gray;
}
}
} }
}
</style> </style>

11
im-uniapp/components/file-upload/file-upload.vue

@ -8,9 +8,9 @@
</template> </template>
<script> <script>
import UNI_APP from '@/.env.js'; import UNI_APP from '@/.env.js';
export default { export default {
name: "file-upload", name: "file-upload",
data() { data() {
return { return {
@ -74,7 +74,7 @@
return; return;
} }
files.forEach((file, name) => { files.forEach((file, name) => {
if(!this.fileMap.has(file.path)){ if (!this.fileMap.has(file.path)) {
this.onBefore && this.onBefore(file) this.onBefore && this.onBefore(file)
this.fileMap.set(file.path, file); this.fileMap.set(file.path, file);
} }
@ -110,8 +110,7 @@
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

34
im-uniapp/components/friend-item/friend-item.vue

@ -1,21 +1,18 @@
<template> <template>
<view class="friend-item" @click="showFriendInfo()"> <view class="friend-item" @click="showFriendInfo()">
<head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage" <head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage" size="small"></head-image>
:size="90"></head-image>
<view class="friend-info"> <view class="friend-info">
<view class="friend-name">{{ friend.nickName}}</view> <view class="friend-name">{{ friend.nickName }}</view>
<view class="friend-online"> <view class="friend-online">
<image v-show="friend.onlineWeb" class="online" src="/static/image/online_web.png" <image v-show="friend.onlineWeb" class="online" src="/static/image/online_web.png" title="电脑设备在线" />
title="电脑设备在线" /> <image v-show="friend.onlineApp" class="online" src="/static/image/online_app.png" title="移动设备在线" />
<image v-show="friend.onlineApp" class="online" src="/static/image/online_app.png"
title="移动设备在线" />
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "frined-item", name: "frined-item",
data() { data() {
return {} return {}
@ -32,12 +29,12 @@
type: Object type: Object
} }
} }
} }
</script> </script>
<style scope lang="scss"> <style scope lang="scss">
.friend-item { .friend-item {
height: 100rpx; height: 90rpx;
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 1rpx;
position: relative; position: relative;
@ -48,7 +45,7 @@
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
background-color: #f5f6ff; background-color: $im-bg;
} }
.friend-info { .friend-info {
@ -59,21 +56,20 @@
text-align: left; text-align: left;
.friend-name { .friend-name {
font-size: 30rpx; font-size: $im-font-size;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.friend-online { .friend-online {
margin-top: 4rpx;
.online { .online {
padding-right: 4rpx; padding-right: 4rpx;
width: 32rpx; width: 24rpx;
height: 32rpx; height: 24rpx;
}
} }
} }
} }
}
</style> </style>

36
im-uniapp/components/group-item/group-item.vue

@ -1,23 +1,22 @@
<template> <template>
<view class="group-item" @click="showGroupInfo()"> <view class="group-item" @click="showGroupInfo()">
<head-image :name="group.showGroupName" <head-image :name="group.showGroupName" :url="group.headImage" size="small"></head-image>
:url="group.headImage" :size="90"></head-image>
<view class="group-name"> <view class="group-name">
<view>{{ group.showGroupName}}</view> <view>{{ group.showGroupName }}</view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "group-item", name: "group-item",
data() { data() {
return {} return {}
}, },
methods:{ methods: {
showGroupInfo(){ showGroupInfo() {
uni.navigateTo({ uni.navigateTo({
url:"/pages/group/group-info?id="+this.group.id url: "/pages/group/group-info?id=" + this.group.id
}) })
}, },
}, },
@ -26,31 +25,34 @@
type: Object type: Object
} }
} }
} }
</script> </script>
<style scope lang="scss"> <style scope lang="scss">
.group-item { .group-item {
height: 100rpx; height: 90rpx;
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 2rpx;
position: relative; position: relative;
padding: 10rpx; padding: 18rpx 20rpx;
padding-left: 20rpx;
align-items: center; align-items: center;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
background-color: #f5f6ff; background-color: $im-bg-active;
}
&.active {
background-color: $im-bg-active;
} }
.group-name { .group-name {
font-size: 32rpx; font-size: $im-font-size;
padding-left: 20rpx; padding-left: 20rpx;
font-weight: 600;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
} }
</style> </style>

18
im-uniapp/components/group-member-selector/group-member-selector.vue

@ -4,12 +4,12 @@
<view class="top-bar"> <view class="top-bar">
<view class="top-tip">选择成员</view> <view class="top-tip">选择成员</view>
<button class="top-btn" type="warn" size="mini" @click="onClean()">清空 </button> <button class="top-btn" type="warn" size="mini" @click="onClean()">清空 </button>
<button class="top-btn" type="primary" size="mini" @click="onOk()">确定({{checkedIds.length}}) <button class="top-btn" type="primary" size="mini" @click="onOk()">确定({{ checkedIds.length }})
</button> </button>
</view> </view>
<scroll-view v-show="checkedIds.length>0" scroll-x="true" scroll-left="120"> <scroll-view v-show="checkedIds.length > 0" scroll-x="true" scroll-left="120">
<view class="checked-users"> <view class="checked-users">
<view v-for="m in members" v-show="m.checked" class="user-item"> <view v-for="m in members" v-show="m.checked" class="user-item" :key="m.userId">
<head-image :name="m.showNickName" :url="m.headImage" :size="60"></head-image> <head-image :name="m.showNickName" :url="m.headImage" :size="60"></head-image>
</view> </view>
</view> </view>
@ -23,7 +23,7 @@
<view class="member-item" @click="onSwitchChecked(m)"> <view class="member-item" @click="onSwitchChecked(m)">
<head-image :name="m.showNickName" :online="m.online" :url="m.headImage" <head-image :name="m.showNickName" :online="m.online" :url="m.headImage"
:size="90"></head-image> :size="90"></head-image>
<view class="member-name">{{ m.showNickName}}</view> <view class="member-name">{{ m.showNickName }}</view>
<view class="member-checked"> <view class="member-checked">
<radio :checked="m.checked" :disabled="m.locked" @click.stop="onSwitchChecked(m)" /> <radio :checked="m.checked" :disabled="m.locked" @click.stop="onSwitchChecked(m)" />
</view> </view>
@ -37,7 +37,7 @@
<script> <script>
export default { export default {
name: "chat-group-member-choose", name: "chat-group-member-choose",
props: { props: {
members: { members: {
@ -102,14 +102,13 @@
return ids; return ids;
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-group-member-choose { .chat-group-member-choose {
position: relative; position: relative;
border: #dddddd solid 1rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: white; background-color: white;
@ -136,6 +135,7 @@
align-items: center; align-items: center;
height: 90rpx; height: 90rpx;
padding: 0 30rpx; padding: 0 30rpx;
.user-item { .user-item {
padding: 3rpx; padding: 3rpx;
} }
@ -170,5 +170,5 @@
height: 800rpx; height: 800rpx;
} }
} }
} }
</style> </style>

17
im-uniapp/components/group-rtc-join/group-rtc-join.vue

@ -1,17 +1,16 @@
<template> <template>
<uni-popup ref="popup" type="center"> <uni-popup ref="popup" type="center">
<uni-popup-dialog mode="base" :duration="2000" title="是否加入通话?" confirmText="加入" <uni-popup-dialog mode="base" :duration="2000" title="是否加入通话?" confirmText="加入" @confirm="onOk">
@confirm="onOk">
<div class="group-rtc-join"> <div class="group-rtc-join">
<div class="host-info"> <div class="host-info">
<div>发起人</div> <div>发起人</div>
<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> </div>
<div class="user-info"> <div class="user-info">
<div>{{rtcInfo.userInfos.length+'人正在通话中'}}</div> <div>{{ rtcInfo.userInfos.length + '人正在通话中' }}</div>
<scroll-view scroll-x="true" scroll-left="120"> <scroll-view scroll-x="true" scroll-left="120">
<view class="user-list"> <view class="user-list">
<view v-for="user in rtcInfo.userInfos" class="user-item"> <view v-for="user in rtcInfo.userInfos" class="user-item" :key="user.id">
<head-image :name="user.nickName" :url="user.headImage" :size="80"></head-image> <head-image :name="user.nickName" :url="user.headImage" :size="80"></head-image>
</view> </view>
</view> </view>
@ -23,7 +22,7 @@
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
rtcInfo: {} rtcInfo: {}
@ -43,7 +42,7 @@
let users = this.rtcInfo.userInfos; let users = this.rtcInfo.userInfos;
let mine = this.userStore.userInfo; let mine = this.userStore.userInfo;
// //
if(!users.find((user)=>user.id==mine.id)){ if (!users.find((user) => user.id == mine.id)) {
users.push({ users.push({
id: mine.id, id: mine.id,
nickName: mine.nickName, nickName: mine.nickName,
@ -59,11 +58,11 @@
}) })
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.group-rtc-join { .group-rtc-join {
width: 100%; width: 100%;
.host-info { .host-info {
@ -85,5 +84,5 @@
padding: 3rpx; padding: 3rpx;
} }
} }
} }
</style> </style>

51
im-uniapp/components/head-image/head-image.vue

@ -1,9 +1,9 @@
<template> <template>
<view class="head-image" @click="showUserInfo($event)" :title="name"> <view class="head-image" @click="showUserInfo($event)" :title="name">
<image class="avatar-image" v-if="url" :src="url" <image class="avatar-image" v-if="url" :src="url" :style="avatarImageStyle" lazy-load="true"
:style="avatarImageStyle" lazy-load="true" mode="aspectFill"/> mode="aspectFill" />
<view class="avatar-text" v-if="!url" :style="avatarTextStyle"> <view class="avatar-text" v-if="!url" :style="avatarTextStyle">
{{name.substring(0,1).toUpperCase()}} {{ name?.substring(0, 1).toUpperCase() }}
</view> </view>
<view v-if="online" class="online" title="用户当前在线"> <view v-if="online" class="online" title="用户当前在线">
</view> </view>
@ -11,7 +11,7 @@
</template> </template>
<script> <script>
export default { export default {
name: "head-image", name: "head-image",
data() { data() {
return { return {
@ -24,40 +24,55 @@
type: Number type: Number
}, },
size: { size: {
type: Number, type: [Number, String],
default: 20 default: 'default'
}, },
url: { url: {
type: String type: String
}, },
name: { name: {
type: String, type: String,
default: "?" default: null
}, },
online: { online: {
type: Boolean, type: Boolean,
default: false default: false
} },
}, },
methods: { methods: {
showUserInfo(e) { showUserInfo(e) {
if (this.id && this.id > 0) { if (this.id && this.id > 0) {
uni.navigateTo({ uni.navigateTo({
url: "/pages/common/user-info?id="+this.id url: "/pages/common/user-info?id=" + this.id
}) })
} }
} }
}, },
computed: { computed: {
_size() {
if (typeof this.size === 'number') {
return this.size;
} else if (typeof this.size === 'string') {
return {
'default': 96,
'small': 84,
'smaller': 72,
'mini': 60,
'minier': 48,
'lage': 108,
'lager': 120,
}[this.size]
}
},
avatarImageStyle() { avatarImageStyle() {
return `width:${this.size}rpx; return `width:${this._size}rpx;
height:${this.size}rpx;` height:${this._size}rpx;`
}, },
avatarTextStyle() { avatarTextStyle() {
return `width: ${this.size}rpx; return `width: ${this._size}rpx;
height:${this.size}rpx; height:${this._size}rpx;
background-color:${this.textColor}; background-color:${this.name ? this.textColor : '#fff'};
font-size:${this.size*0.5}rpx; font-size:${this._size * 0.5}rpx;
` `
}, },
textColor() { textColor() {
@ -68,11 +83,11 @@
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;
@ -101,5 +116,5 @@
border-radius: 50%; border-radius: 50%;
border: 6rpx solid white; border: 6rpx solid white;
} }
} }
</style> </style>

21
im-uniapp/components/image-upload/image-upload.vue

@ -5,9 +5,9 @@
</template> </template>
<script> <script>
import UNI_APP from '@/.env.js' import UNI_APP from '@/.env.js'
export default { export default {
name: "image-upload", name: "image-upload",
data() { data() {
return { return {
@ -17,15 +17,15 @@
} }
}, },
props: { props: {
maxCount:{ maxCount: {
type: Number, type: Number,
default: 1 default: 1
}, },
maxSize: { maxSize: {
type: Number, type: Number,
default: 5*1024*1024 default: 5 * 1024 * 1024
}, },
sourceType:{ sourceType: {
type: String, type: String,
default: 'album' default: 'album'
}, },
@ -50,7 +50,7 @@
sizeType: ['original'], //original compressed sizeType: ['original'], //original compressed
success: (res) => { success: (res) => {
res.tempFiles.forEach((file) => { res.tempFiles.forEach((file) => {
console.log("文件:",file) console.log("文件:", file)
if (!this.onBefore || this.onBefore(file)) { if (!this.onBefore || this.onBefore(file)) {
// //
this.uploadImage(file); this.uploadImage(file);
@ -69,13 +69,13 @@
name: 'file', name: 'file',
success: (res) => { success: (res) => {
let data = JSON.parse(res.data); let data = JSON.parse(res.data);
if(data.code != 200){ if (data.code != 200) {
uni.showToast({ uni.showToast({
icon: "none", icon: "none",
title: data.message, title: data.message,
}) })
this.onError && this.onError(file, data); this.onError && this.onError(file, data);
}else{ } else {
this.onSuccess && this.onSuccess(file, data); this.onSuccess && this.onSuccess(file, data);
} }
}, },
@ -86,8 +86,7 @@
}) })
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

20
im-uniapp/components/loading/loading.vue

@ -6,10 +6,8 @@
</template> </template>
<script> <script>
import {
computed export default {
} from "vue"
export default {
data() { data() {
return {} return {}
}, },
@ -31,11 +29,11 @@
return this.mask ? "background: rgba(0, 0, 0, 0.3);" : ""; return this.mask ? "background: rgba(0, 0, 0, 0.3);" : "";
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.loading-box { .loading-box {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: absolute; position: absolute;
@ -45,14 +43,14 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.rotate { .rotate {
animation: rotate 2s ease-in-out infinite; animation: rotate 2s ease-in-out infinite;
} }
@keyframes rotate { @keyframes rotate {
from { from {
transform: rotate(0deg) transform: rotate(0deg)
} }
@ -60,5 +58,5 @@
to { to {
transform: rotate(360deg) transform: rotate(360deg)
} }
} }
</style> </style>

121
im-uniapp/components/long-press-menu/long-press-menu.vue

@ -0,0 +1,121 @@
<template>
<view>
<view @longpress.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd">
<slot></slot>
</view>
<view v-if="isShowMenu" class="menu-mask" @touchstart="onClose()" @contextmenu.prevent=""></view>
<view v-if="isShowMenu" class="menu" :style="menuStyle">
<view class="menu-item" v-for="(item) in items" :key="item.key" @click.prevent="onSelectMenu(item)">
<text :style="itemStyle(item)"> {{ item.name }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: "long-press-menu",
data() {
return {
isShowMenu: false,
isTouchMove: false,
style: ""
}
},
props: {
items: {
type: Array
}
},
methods: {
onLongPress(e) {
if (this.isTouchMove) {
//
return;
}
uni.getSystemInfo({
success: (res) => {
let touches = e.touches[0];
let style = "";
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
if (touches.clientY > (res.windowHeight / 2)) {
style = `bottom:${res.windowHeight - touches.clientY}px;`;
} else {
style = `top:${touches.clientY}px;`;
}
if (touches.clientX > (res.windowWidth / 2)) {
style += `right:${res.windowWidth - touches.clientX}px;`;
} else {
style += `left:${touches.clientX}px;`;
}
this.menuStyle = style;
//
this.$nextTick(() => {
this.isShowMenu = true;
});
}
})
},
onTouchMove() {
this.onClose();
this.isTouchMove = true;
},
onTouchEnd() {
this.isTouchMove = false;
},
onSelectMenu(item) {
this.$emit("select", item);
this.isShowMenu = false;
},
onClose() {
this.isShowMenu = false;
},
itemStyle(item) {
if (item.color) {
return `color:${item.color};`
}
return `color:#000;`;
}
}
}
</script>
<style lang="scss" scoped>
.menu-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.menu {
position: fixed;
border-radius: 4px;
overflow: hidden;
background-color: #fff;
z-index: 1000;
box-shadow: $im-box-shadow-dark;
.menu-item {
height: 28px;
min-width: 120rpx;
line-height: 28px;
font-size: $im-font-size-small;
display: flex;
padding: 6px 20px;
justify-content: flex-start;
&:hover {
background: $im-bg-active;
}
.menu-icon {
margin-right: 10rpx;
}
}
}
</style>

119
im-uniapp/components/nav-bar/nav-bar.vue

@ -0,0 +1,119 @@
<template>
<view class="im-nav-bar">
<!-- #ifndef H5 -->
<view style="height: var(--status-bar-height)"></view>
<!-- #endif -->
<view class="im-nav-bar-content">
<view class="back" @click="handleBackClick" v-if="back">
<uni-icons type="back" :size="iconFontSize"></uni-icons>
</view>
<view class="title" v-if="title">
<slot></slot>
</view>
<view class="btn">
<uni-icons class="btn-item" v-if="search" type="search" :size="iconFontSize"
@click="$emit('search')"></uni-icons>
<uni-icons class="btn-item" v-if="add" type="plusempty" :size="iconFontSize" @click="$emit('add')"></uni-icons>
<uni-icons class="btn-item" v-if="more" type="more-filled" :size="iconFontSize"
@click="$emit('more')"></uni-icons>
</view>
</view>
</view>
</template>
<script>
export default {
name: "nav-bar",
props: {
back: {
type: Boolean,
default: false
},
title: {
type: Boolean,
default: true
},
search: {
type: Boolean,
default: false
},
add: {
type: Boolean,
default: false
},
more: {
type: Boolean,
default: false
},
iconFontSize: {
type: Number,
default: 24
}
},
data() {
return {}
},
computed: {
height() {
}
},
methods: {
handleBackClick() {
uni.navigateBack({
delta: 1
})
}
}
}
</script>
<style scoped lang="scss">
.im-nav-bar {
background-color: #fff;
//background-color: $im-bg;
position: fixed;
top: 0;
width: 100%;
color: $im-text-color;
border-bottom: 1px solid $im-border-light;
font-size: $im-font-size-large;
z-index: 99;
.im-nav-bar-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
height: $im-nav-bar-height;
.title {}
.back {
position: absolute;
left: 0;
height: 100%;
display: flex;
align-items: center;
padding: 12px;
font-size: 22px;
box-sizing: border-box;
}
.btn {
position: absolute;
right: 0;
height: 100%;
display: flex;
padding: 12px;
align-items: center;
box-sizing: border-box;
.btn-item {
margin-left: 8px;
}
}
}
}
</style>

121
im-uniapp/components/pop-menu/pop-menu.vue

@ -1,121 +0,0 @@
<template>
<view>
<view @longpress.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd">
<slot></slot>
</view>
<view v-if="isShowMenu" class="pop-menu" @touchstart="onClose()" @contextmenu.prevent=""></view>
<view v-if="isShowMenu" class="menu" :style="menuStyle">
<view class="menu-item" v-for="(item) in items" :key="item.key" @click.prevent="onSelectMenu(item)">
<uni-icons class="menu-icon" :type="item.icon" :style="itemStyle(item)" size="22"></uni-icons>
<text :style="itemStyle(item)"> {{item.name}}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: "pop-menu",
data() {
return {
isShowMenu : false,
isTouchMove: false,
style : ""
}
},
props: {
items: {
type: Array
}
},
methods: {
onLongPress(e){
if(this.isTouchMove){
//
return;
}
uni.getSystemInfo({
success: (res) => {
let touches = e.touches[0];
let style = "";
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
if (touches.clientY > (res.windowHeight / 2)) {
style = `bottom:${res.windowHeight-touches.clientY}px;`;
} else {
style = `top:${touches.clientY}px;`;
}
if (touches.clientX > (res.windowWidth / 2)) {
style += `right:${res.windowWidth-touches.clientX}px;`;
} else {
style += `left:${touches.clientX}px;`;
}
this.menuStyle = style;
//
this.$nextTick(() => {
this.isShowMenu = true;
});
}
})
},
onTouchMove(){
this.onClose();
this.isTouchMove = true;
},
onTouchEnd(){
this.isTouchMove = false;
},
onSelectMenu(item) {
this.$emit("select", item);
this.isShowMenu = false;
},
onClose() {
this.isShowMenu = false;
},
itemStyle(item){
if(item.color){
return `color:${item.color};`
}
return `color:#4f76e6;`;
}
}
}
</script>
<style lang="scss" scoped>
.pop-menu {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-color: #333;
z-index: 999;
opacity: 0.5;
}
.menu {
position: fixed;
border: 1px solid #b4b4b4;
border-radius: 7px;
overflow: hidden;
background-color: #f5f6ff;
z-index: 1000;
.menu-item {
height: 25px;
min-width: 150rpx;
line-height: 25px;
font-size: 18px;
display: flex;
padding: 10px;
justify-content: center;
border-bottom: 1px solid #d0d0d8;
.menu-icon {
margin-right: 10rpx;
}
}
}
</style>

60
im-uniapp/im-var.scss

@ -0,0 +1,60 @@
// 颜色
$im-color-primary: #3e45d7;
$im-color-primary-light-1: mix(#fff, $im-color-primary, 10%);
$im-color-primary-light-2: mix(#fff, $im-color-primary, 20%);
$im-color-primary-light-3: mix(#fff, $im-color-primary, 30%);
$im-color-primary-light-4: mix(#fff, $im-color-primary, 40%);
$im-color-primary-light-5: mix(#fff, $im-color-primary, 50%);
$im-color-primary-light-6: mix(#fff, $im-color-primary, 60%);
$im-color-primary-light-7: mix(#fff, $im-color-primary, 70%);
$im-color-primary-light-8: mix(#fff, $im-color-primary, 80%);
$im-color-primary-light-9: mix(#fff, $im-color-primary, 90%);
$im-color-primary-dark-1: mix(#000, $im-color-primary, 10%);
$im-color-primary-dark-2: mix(#000, $im-color-primary, 20%);
$im-color-primary-dark-3: mix(#000, $im-color-primary, 30%);
$im-color-primary-dark-4: mix(#000, $im-color-primary, 40%);
$im-color-success: #18bc37;
$im-color-warning: #f3a73f;
$im-color-danger: #e43d33;
$im-color-info: #8f939c;
// 文字颜色
$im-text-color: #000000;
$im-text-color-light: #6a6a6a;
$im-text-color-lighter: #909399;
$im-text-color-lighter-extra: #c7c7c7;
// 边框颜色
$im-border: #F0F0F0;
$im-border-light: #EDEDED;
$im-border-lighter: #DCDCDC;
$im-border-lighter-extra: #B9B9B9;
// 文字大小
$im-font-size: 30rpx;
$im-font-size-small: 28rpx;
$im-font-size-smaller: 26rpx;
$im-font-size-smaller-extra: 24rpx;
$im-font-size-large: 32rpx;
$im-font-size-larger: 34rpx;
$im-font-size-larger-extra: 36rpx;
// 阴影
$im-box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
$im-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
$im-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, .12);
$im-box-shadow-dark: 0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5);
// 背景
$im-bg: #f7f7f7;
$im-bg-active: #f1f1f1;
// 标题
$im-title-size: 26px;
$im-title-size-1: 22px;
$im-title-size-2: 18px;
$font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
$im-nav-bar-height: 50px;

141
im-uniapp/im.scss

@ -0,0 +1,141 @@
/** 原生button样式 **/
uni-button {
font-size: $im-font-size !important;
}
uni-button[type='primary'] {
color: #fff !important;
background-color: $im-color-primary !important;
}
uni-button[type='primary'][plain] {
color: $im-color-primary !important;
border: 1px solid $im-color-primary;
background-color: transparent;
}
uni-button[type='warn'] {
color: #fff !important;
background-color: $im-color-danger !important;
}
uni-button[type='warn'][plain] {
color: $im-color-danger !important;
border: 1px solid $im-color-danger !important;
background-color: transparent !important;
}
uni-button[size='mini'] {
font-size: $im-font-size-smaller !important;
}
.button-hover[type='primary'] {
color: #fff !important;
background-color: $im-color-primary-dark-1 !important;
}
/** uni-ui input激活后边框、图标颜色 **/
.uni-easyinput__content.is-focused:not(.is-input-error-border) {
border-color: $im-color-primary-light-2 !important;
.content-clear-icon {
color: $im-color-primary-light-2 !important;
}
}
/** 底部导航 **/
.uni-tabbar-bottom .uni-tabbar {
box-shadow: $im-box-shadow;
}
.uni-tabbar-border {
display: none;
}
.segmented-control {
border-color: $im-color-primary !important;
.segmented-control__item--button {
border-color: $im-color-primary !important;
}
.segmented-control__item--button--active {
background-color: $im-color-primary !important;
.segmented-control__text{
color: #fff !important;
}
}
.segmented-control__text{
color: $im-color-primary !important;
}
}
.uni-radio-input {
//border-color: $im-color-primary !important;
//background-color: $im-color-primary !important;
}
.uni-section__content-title {
font-size: $im-font-size !important;
color: $im-text-color-light;
}
.uni-forms-item__label {
color: $im-text-color;
font-size: $im-font-size !important;
}
.uni-forms-item {
//margin-bottom: 8px !important;
}
.uni-easyinput__content-input {
font-size: $im-font-size !important;
}
.uni-easyinput__placeholder-class {
color: $im-text-color-lighter;
font-size: $im-font-size !important;;
}
.uni-easyinput__content-textarea {
font-size: $im-font-size !important;;
}
.uni-input-input:disabled {
color: $im-text-color-light;
}
.uni-forms-item.is-direction-top .uni-forms-item__label {
padding: 0 !important;
}
.uni-data-checklist .checklist-group .checklist-box .checklist-content .checklist-text {
font-size: $im-font-size !important;
}
.uni-card .uni-card__content {
color: unset !important;
padding: 10px 0 !important;
}
.uni-tag-text--small{
font-size: 10px !important;
font-weight: bolder !important;
}
.nav-bar {
height: 100rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
background-color: white;
border-bottom: 1px solid $im-border;
.nav-search {
flex: 1;
}
.nav-add {
cursor: pointer;
}
}
.bottom-btn {
margin: 40rpx 40rpx;
uni-button + uni-button {
margin-top: 20rpx;
}
}

13
im-uniapp/main.js

@ -13,6 +13,10 @@ import useFriendStore from '@/store/friendStore.js'
import useGroupStore from '@/store/groupStore.js' import useGroupStore from '@/store/groupStore.js'
import useConfigStore from '@/store/configStore.js' import useConfigStore from '@/store/configStore.js'
import useUserStore from '@/store/userStore.js' import useUserStore from '@/store/userStore.js'
import barGroup from '@/components/bar/bar-group'
import arrowBar from '@/components/bar/arrow-bar'
import btnBar from '@/components/bar/btn-bar'
import switchBar from '@/components/bar/switch-bar'
//import VConsole from 'vconsole' //import VConsole from 'vconsole'
//new VConsole(); //new VConsole();
@ -23,13 +27,14 @@ import * as recorder from './common/recorder-h5';
// #ifndef H5 // #ifndef H5
import * as recorder from './common/recorder-app'; import * as recorder from './common/recorder-app';
// #endif // #endif
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
app.use(uviewPlus); app.use(uviewPlus);
app.use(pinia.createPinia()); app.use(pinia.createPinia());
app.component('bar-group', barGroup);
app.component('arrow-bar', arrowBar);
app.component('btn-bar', btnBar);
app.component('switch-bar', switchBar);
app.config.globalProperties.$http = request; app.config.globalProperties.$http = request;
app.config.globalProperties.$wsApi = socketApi; app.config.globalProperties.$wsApi = socketApi;
app.config.globalProperties.$msgType = messageType; app.config.globalProperties.$msgType = messageType;
@ -38,7 +43,7 @@ export function createApp() {
app.config.globalProperties.$date = date; app.config.globalProperties.$date = date;
app.config.globalProperties.$rc = recorder; app.config.globalProperties.$rc = recorder;
// 初始化时再挂载store对象 // 初始化时再挂载store对象
app.config.globalProperties.$mountStore = ()=>{ app.config.globalProperties.$mountStore = () => {
app.config.globalProperties.chatStore = useChatStore(); app.config.globalProperties.chatStore = useChatStore();
app.config.globalProperties.friendStore = useFriendStore(); app.config.globalProperties.friendStore = useFriendStore();
app.config.globalProperties.groupStore = useGroupStore(); app.config.globalProperties.groupStore = useGroupStore();

167
im-uniapp/manifest.json

@ -1,32 +1,32 @@
{ {
"name" : "盒子IM", "name": "盒子IM",
"appid" : "__UNI__69DD57A", "appid": "__UNI__69DD57A",
"description" : "", "description": "",
"versionName" : "3.0.0", "versionName": "3.1.0",
"versionCode" : 300, "versionCode": 3100,
"transformPx" : false, "transformPx": false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus": {
"usingComponents" : true, "usingComponents": true,
"nvueStyleCompiler" : "uni-app", "nvueStyleCompiler": "uni-app",
"compilerVersion" : 3, "compilerVersion": 3,
"splashscreen" : { "splashscreen": {
"alwaysShowBeforeRender" : true, "alwaysShowBeforeRender": true,
"waiting" : true, "waiting": true,
"autoclose" : true, "autoclose": true,
"delay" : 0 "delay": 0
}, },
/* */ /* */
"modules" : { "modules": {
"Camera" : {}, "Camera": {},
"Record" : {}, "Record": {},
"Bluetooth" : {} "Bluetooth": {}
}, },
/* */ /* */
"distribute" : { "distribute": {
/* android */ /* android */
"android" : { "android": {
"permissions" : [ "permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>", "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
@ -45,87 +45,90 @@
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />", "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />" "<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />"
], ],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ], "abiFilters": [
"minSdkVersion" : 21 "armeabi-v7a",
"arm64-v8a",
"x86"
],
"minSdkVersion": 21
}, },
/* ios */ /* ios */
"ios" : { "ios": {
"dSYMs" : false, "dSYMs": false,
"privacyDescription" : { "privacyDescription": {
"NSMicrophoneUsageDescription" : "", "NSMicrophoneUsageDescription": "",
"NSCameraUsageDescription" : "" "NSCameraUsageDescription": ""
}, },
"idfa" : false "idfa": false
}, },
/* SDK */ /* SDK */
"sdkConfigs" : { "sdkConfigs": {
"ad" : {}, "ad": {},
"speech" : {} "speech": {}
}, },
"icons" : { "icons": {
"android" : { "android": {
"xhdpi" : "unpackage/res/icons/96x96.png", "xhdpi": "unpackage/res/icons/96x96.png",
"hdpi" : "unpackage/res/icons/72x72.png", "hdpi": "unpackage/res/icons/72x72.png",
"xxhdpi" : "unpackage/res/icons/144x144.png", "xxhdpi": "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png" "xxxhdpi": "unpackage/res/icons/192x192.png"
}, },
"ios" : { "ios": {
"appstore" : "unpackage/res/icons/1024x1024.png", "appstore": "unpackage/res/icons/1024x1024.png",
"ipad" : { "ipad": {
"app" : "unpackage/res/icons/76x76.png", "app": "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png", "app@2x": "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png", "notification": "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png", "notification@2x": "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png", "proapp@2x": "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png", "settings": "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png", "settings@2x": "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png", "spotlight": "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png" "spotlight@2x": "unpackage/res/icons/80x80.png"
}, },
"iphone" : { "iphone": {
"app@2x" : "unpackage/res/icons/120x120.png", "app@2x": "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png", "app@3x": "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png", "notification@2x": "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png", "notification@3x": "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png", "settings@2x": "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png", "settings@3x": "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png", "spotlight@2x": "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png" "spotlight@3x": "unpackage/res/icons/120x120.png"
} }
} }
} }
} }
}, },
/* */ /* */
"quickapp" : {}, "quickapp": {},
/* */ /* */
"mp-weixin" : { "mp-weixin": {
"appid" : "wxda94f40bfad0262c", "appid": "wxda94f40bfad0262c",
"libVersion" : "latest", "libVersion": "latest",
"setting" : { "setting": {
"urlCheck" : false "urlCheck": false
}, },
"usingComponents" : true "usingComponents": true
}, },
"mp-alipay" : { "mp-alipay": {
"usingComponents" : true "usingComponents": true
}, },
"mp-baidu" : { "mp-baidu": {
"usingComponents" : true "usingComponents": true
}, },
"mp-toutiao" : { "mp-toutiao": {
"usingComponents" : true "usingComponents": true
}, },
"uniStatistics" : { "uniStatistics": {
"enable" : false "enable": false
}, },
"vueVersion" : "3", "vueVersion": "3",
"h5" : { "h5": {
"title" : "盒子IM", "title": "盒子IM",
"router" : { "router": {
"base" : "/h5/" "base": "/h5/"
} }
} }
} }
/* ios *//* SDK */ /* ios */ /* SDK */

63
im-uniapp/pages.json

@ -7,59 +7,76 @@
"^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue" "^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue"
} }
}, },
"pages": [{ "pages": [
{
"path": "pages/login/login" "path": "pages/login/login"
}, { },
"path" : "pages/register/register" {
},{ "path": "pages/register/register"
},
{
"path": "pages/chat/chat" "path": "pages/chat/chat"
}, { },
{
"path": "pages/friend/friend" "path": "pages/friend/friend"
}, { },
{
"path": "pages/group/group" "path": "pages/group/group"
}, { },
{
"path": "pages/mine/mine" "path": "pages/mine/mine"
}, { },
{
"path": "pages/common/user-info" "path": "pages/common/user-info"
}, { },
{
"path": "pages/chat/chat-box" "path": "pages/chat/chat-box"
},{ },
{
"path": "pages/chat/chat-private-video" "path": "pages/chat/chat-private-video"
},{ },
{
"path": "pages/chat/chat-group-video" "path": "pages/chat/chat-group-video"
}, { },
{
"path": "pages/friend/friend-add" "path": "pages/friend/friend-add"
}, { },
{
"path": "pages/group/group-info" "path": "pages/group/group-info"
}, { },
{
"path": "pages/group/group-edit" "path": "pages/group/group-edit"
}, { },
{
"path": "pages/group/group-invite" "path": "pages/group/group-invite"
}, { },
{
"path": "pages/group/group-member" "path": "pages/group/group-member"
}, { },
{
"path": "pages/mine/mine-edit" "path": "pages/mine/mine-edit"
},{ },
{
"path": "pages/mine/mine-password" "path": "pages/mine/mine-password"
} }
], ],
"globalStyle": { "globalStyle": {
"navigationBarTitleText": "盒子IM", "navigationBarTitleText": "盒子IM",
"navigationBarTextStyle": "black", "navigationStyle": "custom",
"navigationBarBackgroundColor": "#F0F0F0", "navigationBarBackgroundColor": "#f7f7f7",
"backgroundColor": "#fdfdfd" "backgroundColor": "#f7f7f7"
}, },
"tabBar": { "tabBar": {
"color": "#000000", "color": "#000000",
"selectedColor": "#587ff0", "selectedColor": "#587ff0",
"borderStyle": "black", "borderStyle": "black",
"backgroundColor": "#ffffff", "backgroundColor": "#ffffff",
"list": [{ "list": [
{
"pagePath": "pages/chat/chat", "pagePath": "pages/chat/chat",
"iconPath": "static/tarbar/chat.png", "iconPath": "static/tarbar/chat.png",
"selectedIconPath": "static/tarbar/chat_active.png", "selectedIconPath": "static/tarbar/chat_active.png",
"text": "消息" "text": "消息"
}, },
{ {
"pagePath": "pages/friend/friend", "pagePath": "pages/friend/friend",

110
im-uniapp/pages/chat/chat-box.vue

@ -1,27 +1,24 @@
<template> <template>
<view class="page chat-box"> <view class="page chat-box">
<view class="header"> <nav-bar back more @more="onShowMore">{{ title }}</nav-bar>
<text class="title">{{title}}</text> <view class="chat-msg" @click="switchChatTabBox('none', true)">
<uni-icons class="btn-side right" type="more-filled" size="30" @click="onShowMore()"></uni-icons>
</view>
<view class="chat-msg" @click="switchChatTabBox('none',true)">
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop" <scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-'+scrollMsgIdx"> :scroll-into-view="'chat-item-' + scrollMsgIdx">
<view v-if="chat" v-for="(msgInfo,idx) in chat.messages" :key="idx"> <view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)" <chat-message-item v-if="idx >= showMinIdx" :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @copy="onCopyMessage" :showName="showName(msgInfo)" @recall="onRecallMessage" @copy="onCopyMessage"
@delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" @delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile"
:id="'chat-item-'+idx" :msgInfo="msgInfo" :groupMembers="groupMembers"> :id="'chat-item-' + idx" :msgInfo="msgInfo" :groupMembers="groupMembers">
</chat-message-item> </chat-message-item>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<view v-if="atUserIds.length>0" class="chat-at-bar" @click="openAtBox()"> <view v-if="atUserIds.length > 0" class="chat-at-bar" @click="openAtBox()">
<view class="iconfont icon-at">:&nbsp;</view> <view class="iconfont icon-at">:&nbsp;</view>
<scroll-view v-if="atUserIds.length>0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120"> <scroll-view v-if="atUserIds.length > 0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">
<view class="chat-at-items"> <view class="chat-at-items">
<view v-for="m in atUserItems" class="chat-at-item"> <view v-for="m in atUserItems" class="chat-at-item">
<head-image :name="m.showNickName" :url="m.headImage" :size="50"></head-image> <head-image :name="m.showNickName" :url="m.headImage" size="minier"></head-image>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@ -32,19 +29,20 @@
<chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record> <chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record>
<view v-else class="send-text"> <view v-else class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false" <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt?'[回执消息]':''" :adjust-position="false" @confirm="sendTextMessage()" :placeholder="isReceipt ? '[回执消息]' : ''" :adjust-position="false" @confirm="sendTextMessage()"
@keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold @keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold
:hold-keyboard="true"></textarea> :hold-keyboard="true"></textarea>
</view> </view>
<view v-if="chat && chat.type=='GROUP'" class="iconfont icon-at" @click="openAtBox()"></view> <view v-if="chat && chat.type == 'GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view> <view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view>
<view v-if="sendText==''" class="iconfont icon-add" @click="onShowToolsChatTab()"> <view v-if="sendText == ''" class="iconfont icon-add" @click="onShowToolsChatTab()">
</view> </view>
<button v-if="sendText!=''||atUserIds.length>0" class="btn-send" type="primary" <button v-if="sendText != '' || atUserIds.length > 0" class="btn-send" type="primary"
@touchend.prevent="sendTextMessage()" size="mini">发送</button> @touchend.prevent="sendTextMessage()" size="mini">发送</button>
</view> </view>
<view class="chat-tab-bar" v-show="chatTabBox!='none' || (showKeyBoard && !isH5) " :style="{height:`${keyboardHeight}px`}"> <view class="chat-tab-bar" v-show="chatTabBox != 'none' || (showKeyBoard && !isH5)"
:style="{ height: `${keyboardHeight}px` }">
<view v-if="chatTabBox == 'tools'" class="chat-tools"> <view v-if="chatTabBox == 'tools'" class="chat-tools">
<view class="chat-tools-item"> <view class="chat-tools-item">
<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore" <image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
@ -74,7 +72,7 @@
<view class="tool-name">语音消息</view> <view class="tool-name">语音消息</view>
</view> </view>
<view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="switchReceipt()"> <view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="switchReceipt()">
<view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view> <view class="tool-icon iconfont icon-receipt" :class="isReceipt ? 'active' : ''"></view>
<view class="tool-name">回执消息</view> <view class="tool-name">回执消息</view>
</view> </view>
<!-- #ifndef MP-WEIXIN --> <!-- #ifndef MP-WEIXIN -->
@ -93,7 +91,7 @@
</view> </view>
<!-- #endif --> <!-- #endif -->
</view> </view>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true"> <scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true">
<view class="emotion-item-list"> <view class="emotion-item-list">
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)" <image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit" v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
@ -115,9 +113,9 @@
</template> </template>
<script> <script>
import UNI_APP from '@/.env.js'; import UNI_APP from '@/.env.js';
export default { export default {
data() { data() {
return { return {
chat: {}, chat: {},
@ -710,7 +708,7 @@
} }
}, },
watch: { watch: {
messageSize: function(newSize, oldSize) { messageSize: function (newSize, oldSize) {
// //
if (newSize > oldSize) { if (newSize > oldSize) {
let pages = getCurrentPages(); let pages = getCurrentPages();
@ -762,13 +760,12 @@
this.needScrollToBottom = false; this.needScrollToBottom = false;
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-box { .chat-box {
position: relative; position: relative;
border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -778,16 +775,15 @@
align-items: center; align-items: center;
height: 60rpx; height: 60rpx;
padding: 5px; padding: 5px;
background-color: #f8f8f8; background-color: #f9f9f9;
line-height: 50px; line-height: 50px;
font-size: 36rpx; font-size: $im-font-size-large;
border: #dddddd solid 1px; box-shadow: $im-box-shadow-lighter;
z-index: 1;
.btn-side { .btn-side {
position: absolute; position: absolute;
line-height: 60rpx; line-height: 60rpx;
font-size: 28rpx;
cursor: pointer; cursor: pointer;
&.right { &.right {
@ -799,7 +795,6 @@
.chat-msg { .chat-msg {
flex: 1; flex: 1;
padding: 0; padding: 0;
border: #dddddd solid 1px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: white; background-color: white;
@ -813,12 +808,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 10rpx; padding: 0 10rpx;
border: #dddddd solid 1px;
.icon-at { .icon-at {
font-size: 35rpx; font-size: $im-font-size-larger;
color: darkblue; color: $im-color-primary;
font-weight: 600; font-weight: bold;
} }
.chat-at-scroll-box { .chat-at-scroll-box {
@ -838,33 +832,38 @@
} }
$icon-color: rgba(0, 0, 0, 0.88);
.send-bar { .send-bar {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10rpx; padding: 10rpx;
margin-bottom: 10rpx; //margin-bottom: 10rpx;
border: #dddddd solid 1px; border-top: $im-border solid 1px;
background-color: #f7f8fd; background-color: $im-bg;
height: 80rpx; height: 80rpx;
//box-shadow: $im-box-shadow-lighter;
z-index: 1;
.iconfont { .iconfont {
font-size: 68rpx; font-size: 60rpx;
margin: 6rpx; margin: 0 10rpx;
color: $icon-color;
} }
.chat-record { .chat-record {
flex: 1; flex: 1;
} }
.send-text { .send-text {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 20rpx; padding: 14rpx 20rpx;
background-color: #fff; background-color: #fff;
border-radius: 20rpx; border-radius: 8rpx;
font-size: 30rpx; font-size: $im-font-size;
box-sizing: border-box; box-sizing: border-box;
margin: 0 10rpx;
.send-text-area { .send-text-area {
width: 100%; width: 100%;
@ -880,28 +879,30 @@
.chat-tab-bar { .chat-tab-bar {
height: 500rpx; height: 500rpx;
padding: 20rpx; padding: 20rpx;
background-color: #f8f8f8; background-color: $im-bg;
.chat-tools { .chat-tools {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding-top: 20rpx;
.chat-tools-item { .chat-tools-item {
width: 140rpx; width: 25%;
padding: 16rpx; padding: 16rpx;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.tool-icon { .tool-icon {
padding: 28rpx; padding: 26rpx;
font-size: 60rpx; font-size: 54rpx;
border-radius: 20%; border-radius: 20%;
background-color: white; background-color: white;
color: black; color: $icon-color;
&.active { &:active {
background-color: #ddd; background-color: $im-bg-active;
} }
} }
@ -919,10 +920,11 @@
.emotion-item-list { .emotion-item-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between;
.emotion-item { .emotion-item {
width: 40px; width: 34px;
height: 40px; height: 34px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
padding: 6px; padding: 6px;
@ -931,5 +933,5 @@
} }
} }
} }
</style> </style>

11
im-uniapp/pages/chat/chat-group-video.vue

@ -3,8 +3,8 @@
</template> </template>
<script> <script>
import UNI_APP from '@/.env.js' import UNI_APP from '@/.env.js'
export default { export default {
data() { data() {
return { return {
url: "", url: "",
@ -104,11 +104,11 @@
onUnload() { onUnload() {
uni.$off('WS_RTC_GROUP') uni.$off('WS_RTC_GROUP')
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-group-video { .chat-group-video {
.header { .header {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -119,7 +119,6 @@
line-height: 50px; line-height: 50px;
font-size: 40rpx; font-size: 40rpx;
font-weight: 600; font-weight: 600;
border: #dddddd solid 1px;
.btn-side { .btn-side {
position: absolute; position: absolute;
@ -136,5 +135,5 @@
} }
} }
} }
} }
</style> </style>

27
im-uniapp/pages/chat/chat-private-video.vue

@ -3,8 +3,8 @@
</template> </template>
<script> <script>
import UNI_APP from '@/.env.js' import UNI_APP from '@/.env.js'
export default { export default {
data() { data() {
return { return {
url: "", url: "",
@ -56,19 +56,19 @@
this.wv = document.getElementById('chat-video-wv').contentWindow this.wv = document.getElementById('chat-video-wv').contentWindow
// #endif // #endif
}, },
initUrl(){ initUrl() {
this.url = "/hybrid/html/rtc-private/index.html"; this.url = "/hybrid/html/rtc-private/index.html";
this.url += "?mode="+this.mode; this.url += "?mode=" + this.mode;
this.url += "&isHost="+this.isHost; this.url += "&isHost=" + this.isHost;
this.url += "&baseUrl="+UNI_APP.BASE_URL; this.url += "&baseUrl=" + UNI_APP.BASE_URL;
this.url += "&loginInfo="+JSON.stringify(uni.getStorageSync("loginInfo")); this.url += "&loginInfo=" + JSON.stringify(uni.getStorageSync("loginInfo"));
this.url += "&userInfo="+JSON.stringify(this.userStore.userInfo); this.url += "&userInfo=" + JSON.stringify(this.userStore.userInfo);
this.url += "&friend="+JSON.stringify(this.friend); this.url += "&friend=" + JSON.stringify(this.friend);
this.url += "&config=" + JSON.stringify(this.configStore.webrtc); this.url += "&config=" + JSON.stringify(this.configStore.webrtc);
}, },
}, },
onBackPress() { onBackPress() {
this.sendMessageToWebView("NAV_BACK",{}) this.sendMessageToWebView("NAV_BACK", {})
}, },
onLoad(options) { onLoad(options) {
uni.$on('WS_RTC_PRIVATE', msg => { uni.$on('WS_RTC_PRIVATE', msg => {
@ -92,12 +92,9 @@
onUnload() { onUnload() {
uni.$off('WS_RTC_PRIVATE') uni.$off('WS_RTC_PRIVATE')
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-web-view {}
.chat-web-view{
}
</style> </style>

58
im-uniapp/pages/chat/chat.vue

@ -1,36 +1,35 @@
<template> <template>
<view class="tab-page"> <view class="tab-page">
<nav-bar search @search="showSearch = !showSearch">消息</nav-bar>
<view v-if="loading" class="chat-loading"> <view v-if="loading" class="chat-loading">
<loading :size="50" :mask="false"> <loading :size="50" :mask="false">
<view>消息接收中...</view> <view>消息接收中...</view>
</loading> </loading>
</view> </view>
<view class="nav-bar"> <view class="nav-bar" v-if="showSearch">
<view class="nav-search"> <view class="nav-search">
<uni-search-bar radius="100" v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar> <uni-search-bar radius="100" v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar>
</view> </view>
</view> </view>
<view class="chat-tip" v-if="!loading && chatStore.chats.length==0"> <view class="chat-tip" v-if="!loading && chatStore.chats.length == 0">
温馨提示您现在还没有任何聊天消息快跟您的好友发起聊天吧~ 温馨提示您现在还没有任何聊天消息快跟您的好友发起聊天吧~
</view> </view>
<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">
<view v-for="(chat,index) in chatStore.chats" :key="index"> <view v-for="(chat, index) in chatStore.chats" :key="index">
<pop-menu v-if="isShowChat(chat)" :items="menu.items" <long-press-menu v-if="isShowChat(chat)" :items="menu.items" @select="onSelectMenu($event, index)">
@select="onSelectMenu($event,index)"> <chat-item :chat="chat" :index="index" :active="menu.chatIdx == index"></chat-item>
<chat-item :chat="chat" :index="index" </long-press-menu>
:active="menu.chatIdx==index"></chat-item>
</pop-menu>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</template> </template>
<script> <script>
import useChatStore from '@/store/chatStore.js'
export default { export default {
data() { data() {
return { return {
showSearch: false,
searchText: "", searchText: "",
menu: { menu: {
show: false, show: false,
@ -53,7 +52,7 @@
} }
}, },
methods: { methods: {
onSelectMenu(item,chatIdx) { onSelectMenu(item, chatIdx) {
switch (item.key) { switch (item.key) {
case 'DELETE': case 'DELETE':
this.removeChat(chatIdx); this.removeChat(chatIdx);
@ -72,8 +71,8 @@
moveToTop(chatIdx) { moveToTop(chatIdx) {
this.chatStore.moveTop(chatIdx); this.chatStore.moveTop(chatIdx);
}, },
isShowChat(chat){ isShowChat(chat) {
if(chat.delete){ if (chat.delete) {
return false; return false;
} }
return !this.searchText || chat.showName.includes(this.searchText) return !this.searchText || chat.showName.includes(this.searchText)
@ -87,7 +86,7 @@
} else { } else {
uni.removeTabBarBadge({ uni.removeTabBarBadge({
index: 0, index: 0,
complete: () => {} complete: () => { }
}) })
} }
} }
@ -114,40 +113,22 @@
onShow() { onShow() {
this.refreshUnreadBadge(); this.refreshUnreadBadge();
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.tab-page { .tab-page {
position: relative; position: relative;
border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.nav-bar {
padding: 2rpx 20rpx;
display: flex;
align-items: center;
background-color: white;
border-bottom: 1px solid #ddd;
height: 110rpx;
.nav-search {
flex: 1;
height: 110rpx;
}
}
.chat-tip { .chat-tip {
position: absolute; position: absolute;
top: 400rpx; top: 400rpx;
padding: 50rpx; padding: 50rpx;
line-height: 50rpx; line-height: 50rpx;
text-align: left; text-align: center;
color: darkblue; color: $im-text-color-lighter;
font-size: 30rpx;
} }
.chat-loading { .chat-loading {
@ -158,11 +139,10 @@
position: fixed; position: fixed;
top: 0; top: 0;
z-index: 999; z-index: 999;
color: blue; color: $im-text-color-lighter;
.loading-box { .loading-box {
position: relative; position: relative;
} }
} }
@ -170,5 +150,5 @@
flex: 1; flex: 1;
height: 100%; height: 100%;
} }
} }
</style> </style>

106
im-uniapp/pages/common/user-info.vue

@ -1,47 +1,60 @@
<template> <template>
<view class="page user-info"> <view class="page user-info">
<nav-bar back>用户信息</nav-bar>
<uni-card :is-shadow="false" is-full :border="false">
<view class="content"> <view class="content">
<head-image :name="userInfo.nickName" :url="userInfo.headImageThumb" <head-image :name="userInfo.nickName" :url="userInfo.headImageThumb" :size="160"
:size="160" @click="onShowFullImage()"></head-image> @click="onShowFullImage()"></head-image>
<view class="info-item"> <view class="info-item">
<view class="info-primary"> <view class="info-primary">
<text class="info-username"> <text class="info-username">
{{userInfo.userName}} {{ userInfo.userName }}
</text> </text>
<uni-icons v-show="userInfo.sex==0" class="sex-boy" type="person-filled" size="20" <text v-show="userInfo.sex == 0" class="iconfont icon-man" color="darkblue"></text>
color="darkblue"></uni-icons> <text v-show="userInfo.sex == 1" class="iconfont icon-girl" color="darkred"></text>
<uni-icons v-show="userInfo.sex==1" class="sex-girl" type="person-filled" size="20"
color="darkred"></uni-icons>
</view> </view>
<text> <view class="info-text">
昵称 {{userInfo.nickName}} <text class="label-text">
昵称:
</text> </text>
<text> <text class="content-text">
签名 {{userInfo.signature}} {{ userInfo.nickName }}
</text>
</view>
<view class="info-text">
<view>
<text class="label-text">
签名:
</text>
<text class="content-text">
{{ userInfo.signature }}
</text> </text>
</view> </view>
</view> </view>
<view class="line"></view>
<view class="btn-group">
<button class="btn" v-show="isFriend" type="primary" @click="onSendMessage()">发消息</button>
<button class="btn" v-show="!isFriend" type="primary" @click="onAddFriend()">加为好友</button>
<button class="btn" v-show="isFriend" type="warn" @click="onDelFriend()">删除好友</button>
</view> </view>
</view> </view>
</uni-card>
<bar-group>
<btn-bar v-show="isFriend" type="primary" title="发送消息" @click="onSendMessage()">
</btn-bar>
<btn-bar v-show="!isFriend" type="primary" title="加为好友" @click="onAddFriend()"></btn-bar>
<btn-bar v-show="isFriend" type="danger" title="删除好友" @click="onDelFriend()"></btn-bar>
</bar-group>
</view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
userInfo: {} userInfo: {}
} }
}, },
methods: { methods: {
onShowFullImage(){ onShowFullImage() {
let imageUrl = this.userInfo.headImage; let imageUrl = this.userInfo.headImage;
if(imageUrl){ if (imageUrl) {
uni.previewImage({ uni.previewImage({
urls: [imageUrl] urls: [imageUrl]
}) })
@ -57,7 +70,7 @@
this.chatStore.openChat(chat); this.chatStore.openChat(chat);
let chatIdx = this.chatStore.findChatIdx(chat); let chatIdx = this.chatStore.findChatIdx(chat);
uni.navigateTo({ uni.navigateTo({
url:"/pages/chat/chat-box?chatIdx=" + chatIdx url: "/pages/chat/chat-box?chatIdx=" + chatIdx
}) })
}, },
onAddFriend() { onAddFriend() {
@ -78,12 +91,12 @@
}) })
}) })
}, },
onDelFriend(){ onDelFriend() {
uni.showModal({ uni.showModal({
title: "确认删除", title: "确认删除",
content: `确认删除 '${this.userInfo.nickName}',并删除聊天记录吗?`, content: `确认删除 '${this.userInfo.nickName}',并删除聊天记录吗?`,
success: (res)=> { success: (res) => {
if(res.cancel) if (res.cancel)
return; return;
this.$http({ this.$http({
url: `/friend/delete/${this.userInfo.id}`, url: `/friend/delete/${this.userInfo.id}`,
@ -115,7 +128,7 @@
this.chatStore.updateChatFromFriend(this.userInfo); this.chatStore.updateChatFromFriend(this.userInfo);
}) })
}, },
loadUserInfo(id){ loadUserInfo(id) {
this.$http({ this.$http({
url: "/user/find/" + id, url: "/user/find/" + id,
method: 'GET' method: 'GET'
@ -133,7 +146,7 @@
isFriend() { isFriend() {
return !!this.friendInfo; return !!this.friendInfo;
}, },
friendInfo(){ friendInfo() {
let friends = this.friendStore.friends; let friends = this.friendStore.friends;
let friend = friends.find((f) => f.id == this.userInfo.id); let friend = friends.find((f) => f.id == this.userInfo.id);
return friend; return friend;
@ -143,11 +156,11 @@
// //
this.loadUserInfo(options.id); this.loadUserInfo(options.id);
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.user-info { .user-info {
.content { .content {
height: 200rpx; height: 200rpx;
display: flex; display: flex;
@ -162,31 +175,44 @@
padding-left: 40rpx; padding-left: 40rpx;
flex: 1; flex: 1;
.info-text {
line-height: 1.5;
//margin-bottom: 10rpx;
}
.label-text {
font-size: $im-font-size-small;
color: $im-text-color-light;
}
.content-text {
font-size: $im-font-size-small;
color: $im-text-color-light;
}
.info-primary { .info-primary {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 10rpx;
.info-username { .info-username {
font-size: 40rpx; font-size: $im-font-size-large;
font-weight: 600; font-weight: 600;
} }
}
}
}
.line { .icon-man {
margin: 20rpx; color: $im-text-color;
border-bottom: 1px solid #aaaaaa; font-size: $im-font-size-large;
padding-left: 10rpx;
} }
.btn-group { .icon-girl {
margin: 100rpx; color: darkred;
.btn{
margin-top: 20rpx;
} }
} }
} }
}
}
</style> </style>

38
im-uniapp/pages/friend/friend-add.vue

@ -1,17 +1,19 @@
<template> <template>
<view class="page friend-add"> <view class="page friend-add">
<view class="search-bar"> <nav-bar back>添加好友</nav-bar>
<uni-search-bar v-model="searchText" radius="100" :focus="true" @confirm="onSearch()" @cancel="onCancel()" <view class="nav-bar">
placeholder="用户名/昵称"></uni-search-bar> <view class="nav-search">
<uni-search-bar v-model="searchText" radius="100" :focus="true" @confirm="onSearch()"
@cancel="onCancel()" placeholder="用户名/昵称"></uni-search-bar>
</view>
</view> </view>
<view class="user-items"> <view class="user-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="(user) in users" :key="user.id" v-show="user.id != userStore.userInfo.id"> <view v-for="(user) in users" :key="user.id" v-show="user.id != userStore.userInfo.id">
<view class="user-item"> <view class="user-item">
<head-image :id="user.id" :name="user.nickName" <head-image :id="user.id" :name="user.nickName" :online="user.online"
:online="user.online" :url="user.headImage" :url="user.headImage"></head-image>
:size="100"></head-image> <view class="user-name">{{ user.nickName }}</view>
<view class="user-name">{{ user.nickName}}</view>
<view class="user-btns"> <view class="user-btns">
<button type="primary" v-show="!isFriend(user.id)" size="mini" <button type="primary" v-show="!isFriend(user.id)" size="mini"
@click.stop="onAddFriend(user)">加为好友</button> @click.stop="onAddFriend(user)">加为好友</button>
@ -25,7 +27,7 @@
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
searchText: "", searchText: "",
@ -73,38 +75,34 @@
return !!friend; return !!friend;
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.friend-add { .friend-add {
position: relative; position: relative;
border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.search-bar { .user-items {
background: white;
}
.user-items{
position: relative; position: relative;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
.user-item { .user-item {
height: 120rpx; height: 120rpx;
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 1rpx;
position: relative; position: relative;
padding: 0 30rpx ; padding: 0 30rpx;
align-items: center; align-items: center;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
.user-name { .user-name {
flex:1; flex: 1;
padding-left: 20rpx; padding-left: 20rpx;
font-size: 30rpx; font-size: $im-font-size;
font-weight: 600;
line-height: 60rpx; line-height: 60rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -116,5 +114,5 @@
} }
} }
} }
</style> </style>

81
im-uniapp/pages/friend/friend.vue

@ -1,39 +1,38 @@
<template> <template>
<view class="tab-page friend"> <view class="tab-page friend">
<view class="nav-bar"> <nav-bar add search @add="onAddNewFriends" @search="showSearch = !showSearch">好友</nav-bar>
<view class="nav-bar" v-if="showSearch">
<view class="nav-search"> <view class="nav-search">
<uni-search-bar v-model="searchText" radius="100" cancelButton="none" placeholder="点击搜索好友"></uni-search-bar> <uni-search-bar v-model="searchText" radius="100" cancelButton="none"
</view> placeholder="点击搜索好友"></uni-search-bar>
<view class="nav-add" @click="onAddNewFriends()">
<uni-icons type="personadd" size="35"></uni-icons>
</view> </view>
</view> </view>
<view class="friend-tip" v-if="friends.length==0"> <view class="friend-tip" v-if="friends.length == 0">
温馨提示您现在还没有任何好友快点击右上方'+'按钮添加好友吧~ 温馨提示您现在还没有任何好友快点击右上方'+'按钮添加好友吧~
</view> </view>
<view class="friend-items" v-else> <view class="friend-items" v-else>
<up-index-list :index-list="friendIdx" > <up-index-list :index-list="friendIdx">
<template v-for="(friends,i) in friendGroups"> <template v-for="(friends, i) in friendGroups">
<up-index-item> <up-index-item>
<up-index-anchor :text="friendIdx[i]=='*'?'在线':friendIdx[i]" <up-index-anchor :text="friendIdx[i] == '*' ? '在线' : friendIdx[i]"></up-index-anchor>
bgColor="#f2f3fd"></up-index-anchor> <view v-for="(friend, idx) in friends" :key="idx">
<view v-for="(friend,idx) in friends" :key="idx">
<friend-item :friend="friend"></friend-item> <friend-item :friend="friend"></friend-item>
</view> </view>
</up-index-item> </up-index-item>
</template> </template>
</up-index-list> </up-index-list>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import { pinyin } from 'pinyin-pro'; import { pinyin } from 'pinyin-pro';
export default { export default {
data() { data() {
return { return {
showSearch: false,
searchText: '' searchText: ''
} }
}, },
@ -60,11 +59,11 @@
friends() { friends() {
return this.friendStore.friends; return this.friendStore.friends;
}, },
friendGroupMap(){ friendGroupMap() {
// //
let groupMap = new Map(); let groupMap = new Map();
this.friends.forEach((f) => { this.friends.forEach((f) => {
if(this.searchText && !f.nickName.includes(this.searchText)){ if (this.searchText && !f.nickName.includes(this.searchText)) {
return; return;
} }
let letter = this.firstLetter(f.nickName).toUpperCase(); let letter = this.firstLetter(f.nickName).toUpperCase();
@ -93,53 +92,53 @@
groupMap = new Map(arrayObj.map(i => [i[0], i[1]])); groupMap = new Map(arrayObj.map(i => [i[0], i[1]]));
return groupMap; return groupMap;
}, },
friendIdx(){ friendIdx() {
return Array.from(this.friendGroupMap.keys()); return Array.from(this.friendGroupMap.keys());
}, },
friendGroups(){ friendGroups() {
return Array.from(this.friendGroupMap.values()); return Array.from(this.friendGroupMap.values());
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.friend { .friend {
position: relative; position: relative;
border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.friend-tip { :deep(.u-index-anchor) {
position: absolute; height: 60rpx !important;
top: 400rpx; background-color: unset !important;
padding: 50rpx; border-bottom: none !important;
text-align: center;
line-height: 50rpx;
text-align: left;
color: darkblue;
font-size: 30rpx;
} }
.nav-bar { :deep(.u-index-anchor__text) {
padding: 2rpx 10rpx; color: $im-text-color !important;
display: flex; }
align-items: center;
background-color: white;
.nav-search { :deep(.u-index-list__letter__item) {
flex: 1; width: 48rpx !important;
height: 48rpx !important;
} }
.nav-add { :deep(.u-index-list__letter__item__index) {
cursor: pointer; font-size: $im-font-size-small !important;
} }
.friend-tip {
position: absolute;
top: 400rpx;
padding: 50rpx;
text-align: center;
line-height: 50rpx;
color: $im-text-color-lighter;
} }
.friend-items { .friend-items {
flex: 1; flex: 1;
padding: 0; padding: 0;
border: #dddddd solid 1px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@ -147,5 +146,5 @@
height: 100%; height: 100%;
} }
} }
} }
</style> </style>

37
im-uniapp/pages/group/group-edit.vue

@ -1,33 +1,37 @@
<template> <template>
<view v-if="userStore.userInfo.type == 1" class="page group-edit"> <view v-if="userStore.userInfo.type == 1" class="page group-edit">
<nav-bar back>修改群资料</nav-bar>
<uni-card :is-shadow="false" is-full :border="false">
<uni-forms ref="form" :modelValue="group" :rules="rules" validate-trigger="bind" label-position="top" <uni-forms ref="form" :modelValue="group" :rules="rules" validate-trigger="bind" label-position="top"
label-width="100%"> label-width="100%">
<uni-forms-item label="群聊头像:" name="headImage"> <uni-forms-item name="headImage" class="avatar">
<image-upload v-if="isOwner" :onSuccess="onUnloadImageSuccess"> <image-upload v-if="isOwner" :onSuccess="onUnloadImageSuccess">
<image :src="group.headImageThumb" class="group-image"></image> <image :src="group.headImageThumb" class="group-image"></image>
</image-upload> </image-upload>
<head-image v-if="!isOwner" :name="group.showGroupName" <head-image v-if="!isOwner" :name="group.showGroupName" :url="group.headImageThumb"
:url="group.headImageThumb" :size="200"></head-image> :size="200"></head-image>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="群聊名称:" name="name" :required="true"> <uni-forms-item label="群聊名称" name="name" :required="true">
<uni-easyinput type="text" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称" /> <uni-easyinput type="text" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称" />
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="群聊备注:" name="remarkGroupName"> <uni-forms-item label="群聊备注" name="remarkGroupName">
<uni-easyinput v-model="group.remarkGroupName" type="text" :placeholder="group.name" /> <uni-easyinput v-model="group.remarkGroupName" type="text" :placeholder="group.name" />
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="我在本群的昵称:" name="remarkNickName"> <uni-forms-item label="我在本群的昵称" name="remarkNickName">
<uni-easyinput v-model="group.remarkNickName" type="text" :placeholder="userStore.userInfo.nickName" /> <uni-easyinput v-model="group.remarkNickName" type="text"
:placeholder="userStore.userInfo.nickName" />
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="群公告:" name="notice"> <uni-forms-item label="群公告" name="notice">
<uni-easyinput type="textarea" v-model="group.notice" :disabled="!isOwner" placeholder="请输入群公告" /> <uni-easyinput type="textarea" v-model="group.notice" :disabled="!isOwner" placeholder="请输入群公告" />
</uni-forms-item> </uni-forms-item>
</uni-forms> </uni-forms>
<button type="primary" @click="submit()">提交</button> </uni-card>
<button class="bottom-btn" type="primary" @click="submit()">提交</button>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
group: {}, group: {},
@ -42,7 +46,6 @@
} }
} }
}, },
methods: { methods: {
submit() { submit() {
if (this.group.id) { if (this.group.id) {
@ -133,12 +136,12 @@
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.group-edit { .group-edit {
padding: 20rpx; //padding: 20rpx;
.group-image { .group-image {
width: 200rpx; width: 200rpx;
@ -146,5 +149,9 @@
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 5%; border-radius: 5%;
} }
} }
.avatar {
margin-top: -30px;
}
</style> </style>

110
im-uniapp/pages/group/group-info.vue

@ -1,68 +1,69 @@
<template> <template>
<view v-if="userStore.userInfo.type == 1" class="page group-info"> <view v-if="userStore.userInfo.type == 1" class="page group-info">
<nav-bar back>群聊信息</nav-bar>
<view v-if="!group.quit" class="group-members"> <view v-if="!group.quit" class="group-members">
<view class="member-items"> <view class="member-items">
<view v-for="(member,idx) in groupMembers" :key="idx"> <view v-for="(member, idx) in groupMembers" :key="idx">
<view class="member-item" v-if="idx<9"> <view class="member-item" v-if="idx < 9">
<head-image :id="member.userId" :name="member.showNickName" :url="member.headImage" <head-image :id="member.userId" :name="member.showNickName" :url="member.headImage" size="small"
:size="100" :online="member.online" ></head-image> :online="member.online"></head-image>
<view class="member-name"> <view class="member-name">
<text>{{member.showNickName}}</text> <text>{{ member.showNickName }}</text>
</view> </view>
</view> </view>
</view> </view>
<view class="invite-btn" @click="onInviteMember()"> <view class="invite-btn" @click="onInviteMember()">
<uni-icons type="plusempty" size="28" color="#888888"></uni-icons> <uni-icons type="plusempty" size="20" color="#888888"></uni-icons>
</view> </view>
</view> </view>
<view class="member-more" @click="onShowMoreMmeber()">{{`查看全部群成员${groupMembers.length}`}}></view> <view class="member-more" @click="onShowMoreMmeber()">{{ `查看全部群成员${groupMembers.length}` }}></view>
</view> </view>
<view class="group-detail"> <view class="group-detail">
<uni-section title="群聊名称:" titleFontSize="14px">
<uni-section title="群聊名称">
<template v-slot:right> <template v-slot:right>
<text class="detail-text">{{group.name}}</text> <text class="detail-text">{{ group.name }}</text>
</template> </template>
</uni-section> </uni-section>
<uni-section title="群主:" titleFontSize="14px"> <uni-section title="群主">
<template v-slot:right> <template v-slot:right>
<text class="detail-text">{{ownerName}}</text> <text class="detail-text">{{ ownerName }}</text>
</template> </template>
</uni-section> </uni-section>
<uni-section title="群名备注">
<uni-section title="群名备注:" titleFontSize="14px">
<template v-slot:right> <template v-slot:right>
<text class="detail-text"> {{group.remarkGroupName}}</text> <text class="detail-text"> {{ group.remarkGroupName }}</text>
</template> </template>
</uni-section> </uni-section>
<uni-section title="我在本群的昵称:" titleFontSize="14px"> <uni-section title="我在本群的昵称">
<template v-slot:right> <template v-slot:right>
<text class="detail-text"> {{group.showNickName}}</text> <text class="detail-text"> {{ group.showNickName }}</text>
</template> </template>
</uni-section> </uni-section>
<uni-section v-if="group.notice" title="群公告:" titleFontSize="14px"> <uni-section v-if="group.notice" title="群公告">
<uni-notice-bar :text="group.notice" /> <uni-notice-bar :text="group.notice" />
</uni-section> </uni-section>
<view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view> <view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view>
</view> </view>
<view v-if="!group.quit" class="btn-group"> <bar-group v-if="!group.quit">
<button class="btn" type="primary" @click="onSendMessage()">发消息</button> <btn-bar type="primary" title="发送消息" @click="onSendMessage()"></btn-bar>
<button class="btn" v-show="!isOwner" type="warn" @click="onQuitGroup()">退出群聊</button> <btn-bar v-if="!isOwner" type="danger" title="退出群聊" @click="onQuitGroup()"></btn-bar>
<button class="btn" v-show="isOwner" type="warn" @click="onDissolveGroup()">解散群聊</button> <btn-bar v-if="isOwner" type="danger" title="解散群聊" @click="onDissolveGroup()"></btn-bar>
</view> </bar-group>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
groupId: null, groupId: null,
group:{}, group: {},
groupMembers: [] groupMembers: []
} }
}, },
methods: { methods: {
onFocusSearch() {}, onFocusSearch() { },
onInviteMember() { onInviteMember() {
uni.navigateTo({ uni.navigateTo({
url: `/pages/group/group-invite?id=${this.groupId}` url: `/pages/group/group-invite?id=${this.groupId}`
@ -107,13 +108,13 @@
content: `您已退出群聊'${this.group.name}'`, content: `您已退出群聊'${this.group.name}'`,
showCancel: false, showCancel: false,
success: () => { success: () => {
setTimeout(()=>{ setTimeout(() => {
uni.switchTab({ uni.switchTab({
url:"/pages/group/group" url: "/pages/group/group"
}); });
this.groupStore.removeGroup(this.groupId); this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this.groupId); this.chatStore.removeGroupChat(this.groupId);
},100) }, 100)
} }
}) })
}); });
@ -136,13 +137,13 @@
content: `群聊'${this.group.name}'已解散`, content: `群聊'${this.group.name}'已解散`,
showCancel: false, showCancel: false,
success: () => { success: () => {
setTimeout(()=>{ setTimeout(() => {
uni.switchTab({ uni.switchTab({
url:"/pages/group/group" url: "/pages/group/group"
}); });
this.groupStore.removeGroup(this.groupId); this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this.groupId); this.chatStore.removeGroupChat(this.groupId);
},100) }, 100)
} }
}) })
}); });
@ -190,11 +191,11 @@
this.loadGroupMembers(options.id) this.loadGroupMembers(options.id)
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.group-info { .group-info {
.group-members { .group-members {
padding: 30rpx; padding: 30rpx;
background: white; background: white;
@ -211,16 +212,16 @@
position: relative; position: relative;
align-items: center; align-items: center;
padding-right: 5px; padding-right: 5px;
background-color: #fafafa;
white-space: nowrap; white-space: nowrap;
.member-name { .member-name {
width: 100%; width: 100%;
flex: 1; flex: 1;
font-size: 14px;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
padding-top: 8rpx;
font-size: $im-font-size-smaller;
} }
} }
@ -228,53 +229,38 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100rpx; width: 86rpx;
height: 100rpx; height: 86rpx;
margin: 10rpx; margin: 10rpx;
border: #686868 dashed 2px; border: $im-border solid 2rpx;
border-radius: 10%; border-radius: 10%;
} }
} }
.member-more { .member-more {
padding-top: 30rpx; padding-top: 24rpx;
text-align: center; text-align: center;
font-size: 32rpx; font-size: $im-font-size-small;
color: #333; color: $im-text-color-lighter;
} }
} }
.group-detail { .group-detail {
margin-top: 30rpx; margin-top: 30rpx;
padding: 30rpx; padding: 20rpx 20rpx;
background: white; background: white;
.detail-text{ .detail-text {
font-size: 28rpx; font-size: $im-font-size;
font-weight: 600;
} }
.group-edit { .group-edit {
padding-top: 30rpx; padding-top: 30rpx;
text-align: center; text-align: center;
font-size: 32rpx; font-size: $im-font-size-small;
color: #333; color: $im-text-color-lighter;
}
} }
.btn-group {
margin-top: 30rpx;
padding: 30rpx;
background: white;
.btn {
margin: 10rpx;
}
}
} }
}
</style> </style>

47
im-uniapp/pages/group/group-invite.vue

@ -1,33 +1,31 @@
<template> <template>
<view class="page group-invite"> <view class="page group-invite">
<view class="search-bar"> <view class="nav-bar">
<uni-search-bar v-model="searchText" radius="100" cancelButton="none" placeholder="输入好友昵称搜索"></uni-search-bar> <view class="nav-search">
<uni-search-bar v-model="searchText" radius="100" cancelButton="none"
placeholder="输入好友昵称搜索"></uni-search-bar>
</view>
</view> </view>
<view class="friend-items"> <view class="friend-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="friend in friendItems" v-show="!searchText || friend.nickName.includes(searchText)" <view v-for="friend in friendItems" v-show="!searchText || friend.nickName.includes(searchText)"
:key="friend.id"> :key="friend.id">
<view class="friend-item" @click="onSwitchChecked(friend)"> <view class="friend-item" @click="onSwitchChecked(friend)"
<head-image :name="friend.nickName" :class="{ checked: friend.checked, disabled: friend.disabled }">
:online="friend.online" :url="friend.headImage" <head-image :name="friend.nickName" :online="friend.online"
:size="100"></head-image> :url="friend.headImage"></head-image>
<view class="friend-name">{{ friend.nickName }}</view>
<view class="friend-name">{{ friend.nickName}}</view>
<view class="friend-checked">
<radio :checked="friend.checked" :disabled="friend.disabled" @click.stop="onSwitchChecked(friend)"/>
</view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<view> <button class="bottom-btn" type="primary" :disabled="inviteSize == 0"
<button type="primary" :disabled="inviteSize==0" @click="onInviteFriends()">邀请({{inviteSize}}) </button> @click="onInviteFriends()">邀请({{ inviteSize }}) </button>
</view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
groupId: null, groupId: null,
@ -116,13 +114,12 @@
this.groupId = options.id; this.groupId = options.id;
this.loadGroupMembers(options.id); this.loadGroupMembers(options.id);
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.group-invite { .group-invite {
position: relative; position: relative;
border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -136,13 +133,21 @@
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 1rpx;
position: relative; position: relative;
padding: 0 30rpx ; padding: 0 30rpx;
align-items: center; align-items: center;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
&.disabled {
background-color: $im-bg-active !important;
}
&.checked {
background-color: $im-color-primary-light-9;
}
.friend-name { .friend-name {
flex:1; flex: 1;
padding-left: 20rpx; padding-left: 20rpx;
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
@ -156,5 +161,5 @@
height: 100%; height: 100%;
} }
} }
} }
</style> </style>

46
im-uniapp/pages/group/group-member.vue

@ -1,29 +1,27 @@
<template> <template>
<view class="page group-member"> <view class="page group-member">
<view class="search-bar"> <nav-bar back>群成员</nav-bar>
<uni-search-bar v-model="searchText" radius="100" cancelButton="none" placeholder="输入昵称搜索"></uni-search-bar> <view class="nav-bar">
<view class="nav-search">
<uni-search-bar v-model="searchText" radius="100" cancelButton="none"
placeholder="输入昵称搜索"></uni-search-bar>
</view>
</view> </view>
<view class="member-items"> <view class="member-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="(member,idx) in groupMembers" <view v-for="(member, idx) in groupMembers"
v-show="!searchText || member.showNickName.includes(searchText)" :key="idx"> v-show="!searchText || member.showNickName.includes(searchText)" :key="idx">
<view class="member-item" @click="onShowUserInfo(member.userId)"> <view class="member-item" @click="onShowUserInfo(member.userId)">
<head-image :name="member.showNickName" <head-image :name="member.showNickName" :online="member.online"
:online="member.online" :url="member.headImage" :url="member.headImage"></head-image>
:size="100"></head-image> <view class="member-name">{{ member.showNickName }}
<view class="member-name">{{ member.showNickName}} <uni-tag v-if="member.userId == group.ownerId" text="群主" size="small" circle type="error">
<uni-tag v-if="member.userId==group.ownerId"
text="群主" size="small" circle
custom-style="background-color: #e30a0a;">
</uni-tag> </uni-tag>
<uni-tag v-if="member.userId==userStore.userInfo.id" <uni-tag v-if="member.userId == userStore.userInfo.id" text="我" size="small" circle></uni-tag>
text="我" size="small" circle></uni-tag>
</view> </view>
<view class="member-kick"> <view class="member-kick">
<button type="warn" plain v-show="isOwner && !isSelf(member.userId)" size="mini" <button type="warn" plain v-show="isOwner && !isSelf(member.userId)" size="mini"
@click.stop="onKickOut(member,idx)">移出群聊</button> @click.stop="onKickOut(member, idx)">移出群聊</button>
</view> </view>
</view> </view>
</view> </view>
@ -33,7 +31,7 @@
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
isModify: false, isModify: false,
@ -106,13 +104,12 @@
prevPage.$vm.loadGroupMembers(); prevPage.$vm.loadGroupMembers();
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.group-member { .group-member {
position: relative; position: relative;
border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -126,7 +123,7 @@
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 1rpx;
position: relative; position: relative;
padding: 0 30rpx ; padding: 0 30rpx;
align-items: center; align-items: center;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
@ -134,11 +131,10 @@
.member-name { .member-name {
display: flex; display: flex;
align-items: center; align-items: center;
flex:1; flex: 1;
padding-left: 20rpx; padding-left: 20rpx;
font-size: 30rpx; font-size: $im-font-size;
font-weight: 600; line-height: $im-font-size * 2;
line-height: 60rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -158,5 +154,5 @@
height: 100%; height: 100%;
} }
} }
} }
</style> </style>

37
im-uniapp/pages/group/group.vue

@ -1,21 +1,19 @@
<template> <template>
<view class="tab-page group"> <view class="tab-page group">
<view class="nav-bar"> <nav-bar add search @add="onCreateNewGroup" @search="showSearch = !showSearch">群聊</nav-bar>
<view class="nav-bar" v-if="showSearch">
<view class="nav-search"> <view class="nav-search">
<uni-search-bar v-model="searchText" cancelButton="none" radius="100" <uni-search-bar v-model="searchText" cancelButton="none" radius="100"
placeholder="点击搜索群聊"></uni-search-bar> placeholder="点击搜索群聊"></uni-search-bar>
</view> </view>
<view class="nav-add" @click="onCreateNewGroup()">
<uni-icons type="personadd" size="35"></uni-icons>
</view> </view>
</view> <view class="group-tip" v-if="groupStore.groups.length == 0">
<view class="group-tip" v-if="groupStore.groups.length==0">
温馨提示您现在还没有加入任何群聊点击右上方'+'按钮可以创建群聊哦~ 温馨提示您现在还没有加入任何群聊点击右上方'+'按钮可以创建群聊哦~
</view> </view>
<view class="group-items" v-else> <view class="group-items" v-else>
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="group in groupStore.groups" :key="group.id"> <view v-for="group in groupStore.groups" :key="group.id">
<group-item v-if="!group.quit&&group.showGroupName.includes(searchText)" <group-item v-if="!group.quit && group.showGroupName.includes(searchText)"
:group="group"></group-item> :group="group"></group-item>
</view> </view>
</scroll-view> </scroll-view>
@ -24,9 +22,10 @@
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
showSearch: false,
searchText: "" searchText: ""
} }
}, },
@ -40,32 +39,15 @@
}) })
} }
} }
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.group { .group {
position: relative; position: relative;
border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.nav-bar {
padding: 2rpx 10rpx;
display: flex;
align-items: center;
background-color: white;
.nav-search {
flex: 1;
}
.nav-add {
cursor: pointer;
}
}
.group-tip { .group-tip {
position: absolute; position: absolute;
top: 400rpx; top: 400rpx;
@ -79,7 +61,6 @@
.group-items { .group-items {
flex: 1; flex: 1;
padding: 0; padding: 0;
border: #dddddd solid 1px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@ -87,5 +68,5 @@
height: 100%; height: 100%;
} }
} }
} }
</style> </style>

20
im-uniapp/pages/login/login.vue

@ -1,5 +1,5 @@
<template> <template>
<view class="page login"> <view class="login">
<view class="title">欢迎登录</view> <view class="title">欢迎登录</view>
<uni-forms class="form" :modelValue="loginForm" :rules="rules" validate-trigger="bind"> <uni-forms class="form" :modelValue="loginForm" :rules="rules" validate-trigger="bind">
<uni-forms-item name="userName"> <uni-forms-item name="userName">
@ -17,7 +17,7 @@
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
loginForm: { loginForm: {
@ -66,18 +66,18 @@
this.loginForm.userName = uni.getStorageSync("userName"); this.loginForm.userName = uni.getStorageSync("userName");
this.loginForm.password = uni.getStorageSync("password"); this.loginForm.password = uni.getStorageSync("password");
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.login { .login {
.title { .title {
padding-top: 150rpx; padding-top: 150rpx;
padding-bottom: 50rpx; padding-bottom: 50rpx;
color: royalblue; color: $im-color-primary;
text-align: center; text-align: center;
font-size: 60rpx; font-size: 24px;
font-weight: 600; font-weight: bold;
} }
.form { .form {
@ -93,9 +93,9 @@
position: fixed; position: fixed;
width: 100%; width: 100%;
bottom: 100rpx; bottom: 100rpx;
color: royalblue; color: $im-color-primary;
text-align: center; text-align: center;
font-size: 32rpx; font-size: $im-font-size;
}
} }
}
</style> </style>

54
im-uniapp/pages/mine/mine-edit.vue

@ -1,61 +1,61 @@
<template> <template>
<view class="page mine-edit"> <view class="page mine-edit">
<uni-forms ref="form" :modelValue="userInfo" label-position="top" <nav-bar back>修改我的信息</nav-bar>
label-width="100%"> <uni-card :is-shadow="false" is-full :border="false">
<uni-forms-item label="头像:" name="headImage"> <uni-forms ref="form" :modelValue="userInfo" label-position="top" label-width="100%">
<uni-forms-item name="headImage" class="avatar">
<image-upload :onSuccess="onUnloadImageSuccess"> <image-upload :onSuccess="onUnloadImageSuccess">
<image :src="userInfo.headImageThumb" class="head-image"></image> <image :src="userInfo.headImageThumb" class="head-image"></image>
</image-upload> </image-upload>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="用户名:" name="userName"> <uni-forms-item label="用户名" name="userName">
<uni-easyinput type="text" v-model="userInfo.userName" :disabled="true" /> <uni-easyinput type="text" v-model="userInfo.userName" :disabled="true" />
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="昵称:" name="nickName"> <uni-forms-item label="昵称" name="nickName">
<uni-easyinput v-model="userInfo.nickName" type="text" :placeholder="userInfo.userName" /> <uni-easyinput v-model="userInfo.nickName" type="text" :placeholder="userInfo.userName" />
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="性别:" name="sex"> <uni-forms-item label="性别" name="sex">
<radio-group @change="onSexchange"> <uni-data-checkbox v-model="userInfo.sex"
<label class="radio"><radio value="0" :checked="userInfo.sex==0" /></label> :localdata="[{ text: '男', value: 0 }, { text: '女', value: 1 }]"></uni-data-checkbox>
<label class="radio"><radio value="1" :checked="userInfo.sex==1" /> </label>
</radio-group>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="签名:" name="signature"> <uni-forms-item label="签名" name="signature">
<uni-easyinput type="textarea" v-model="userInfo.signature" placeholder="编辑个性标签,展示我的独特态度" /> <uni-easyinput type="textarea" v-model="userInfo.signature" placeholder="编辑个性标签,展示我的独特态度" />
</uni-forms-item> </uni-forms-item>
</uni-forms> </uni-forms>
<button type="primary" @click="onSubmit()">提交</button> </uni-card>
<button type="primary" class="bottom-btn" @click="onSubmit()">提交</button>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
userInfo: {} userInfo: {}
} }
}, },
methods:{ methods: {
onSexchange(e){ onSexchange(e) {
this.userInfo.sex=e.detail.value; this.userInfo.sex = e.detail.value;
}, },
onUnloadImageSuccess(file, res) { onUnloadImageSuccess(file, res) {
this.userInfo.headImage = res.data.originUrl; this.userInfo.headImage = res.data.originUrl;
this.userInfo.headImageThumb = res.data.thumbUrl; this.userInfo.headImageThumb = res.data.thumbUrl;
}, },
onSubmit(){ onSubmit() {
this.$http({ this.$http({
url: "/user/update", url: "/user/update",
method: "PUT", method: "PUT",
data: this.userInfo data: this.userInfo
}).then(()=>{ }).then(() => {
this.userStore.setUserInfo(this.userInfo); this.userStore.setUserInfo(this.userInfo);
uni.showToast({ uni.showToast({
title:"修改成功", title: "修改成功",
icon: 'none' icon: 'none'
}); });
setTimeout(()=>{ setTimeout(() => {
uni.navigateBack(); uni.navigateBack();
},1000); }, 1000);
}) })
} }
}, },
@ -64,16 +64,18 @@
let mine = this.userStore.userInfo; let mine = this.userStore.userInfo;
this.userInfo = JSON.parse(JSON.stringify(mine)); this.userInfo = JSON.parse(JSON.stringify(mine));
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.mine-edit { .mine-edit {
padding: 20rpx;
.head-image { .head-image {
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;
} }
} }
.avatar {
margin-top: -30px;
}
</style> </style>

33
im-uniapp/pages/mine/mine-password.vue

@ -1,23 +1,25 @@
<template> <template>
<view class="page mine-password"> <view class="page mine-password">
<uni-forms ref="form" :modelValue="formData" label-position="top" label-width="100%" > <nav-bar back>修改密码</nav-bar>
<uni-forms-item label="原密码:" name="oldPassword"> <uni-card :is-shadow="false" is-full :border="false">
<uni-forms ref="form" :modelValue="formData" label-position="top" label-width="100%">
<uni-forms-item label="原密码" name="oldPassword">
<uni-easyinput type="password" v-model="formData.oldPassword" /> <uni-easyinput type="password" v-model="formData.oldPassword" />
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="新密码:" name="newPassword"> <uni-forms-item label="新密码" name="newPassword">
<uni-easyinput type="password" v-model="formData.newPassword" /> <uni-easyinput type="password" v-model="formData.newPassword" />
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="确认密码:" name="confirmPassword"> <uni-forms-item label="确认密码" name="confirmPassword">
<uni-easyinput type="password" v-model="formData.confirmPassword" /> <uni-easyinput type="password" v-model="formData.confirmPassword" />
</uni-forms-item> </uni-forms-item>
<button type="primary" @click="onSubmit()">提交</button>
</uni-forms> </uni-forms>
</uni-card>
<button class="bottom-btn" type="primary" @click="onSubmit()">提交</button>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
formData: { formData: {
@ -37,7 +39,7 @@
required: true, required: true,
errorMessage: '请输入新密码', errorMessage: '请输入新密码',
}, { }, {
validateFunction: function(rule, value, data, callback) { validateFunction: function (rule, value, data, callback) {
if (data.confirmPassword != data.newPassword) { if (data.confirmPassword != data.newPassword) {
callback("两次输入的密码不一致"); callback("两次输入的密码不一致");
} }
@ -53,7 +55,7 @@
required: true, required: true,
errorMessage: '请输入确认密码', errorMessage: '请输入确认密码',
}, { }, {
validateFunction: function(rule, value, data, callback) { validateFunction: function (rule, value, data, callback) {
if (data.confirmPassword != data.newPassword) { if (data.confirmPassword != data.newPassword) {
callback("两次输入的密码不一致"); callback("两次输入的密码不一致");
} }
@ -78,9 +80,9 @@
title: "修改密码成功", title: "修改密码成功",
icon: 'none' icon: 'none'
}) })
setTimeout(()=>{ setTimeout(() => {
uni.navigateBack(); uni.navigateBack();
},1000); }, 1000);
}) })
}).catch(err => { }).catch(err => {
console.log('表单错误信息:', err); console.log('表单错误信息:', err);
@ -93,12 +95,7 @@
this.$refs.form.setRules(this.rules) this.$refs.form.setRules(this.rules)
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss"></style>
.mine-password {
padding: 20rpx;
}
</style>

98
im-uniapp/pages/mine/mine.vue

@ -1,38 +1,52 @@
<template> <template>
<view class="page mine"> <view class="page mine">
<nav-bar>我的</nav-bar>
<uni-card :is-shadow="false" is-full :border="false">
<view class="content" @click="onModifyInfo()"> <view class="content" @click="onModifyInfo()">
<head-image :name="userInfo.nickName" <head-image :name="userInfo.nickName" :url="userInfo.headImage" :size="160"></head-image>
:url="userInfo.headImage"
:size="160"></head-image>
<view class="info-item"> <view class="info-item">
<view class="info-primary"> <view class="info-primary">
<text class="info-username"> <text class="info-username">
{{userInfo.userName}} {{ userInfo.userName }}
</text> </text>
<text v-show="userInfo.sex==0" class="iconfont icon-man" <text v-show="userInfo.sex == 0" class="iconfont icon-man" color="darkblue"></text>
color="darkblue"></text> <text v-show="userInfo.sex == 1" class="iconfont icon-girl" color="darkred"></text>
<text v-show="userInfo.sex==1" class="iconfont icon-girl"
color="darkred"></text>
</view> </view>
<text> <view class="info-text">
昵称 {{userInfo.nickName}} <text class="label-text">
昵称:
</text> </text>
<text> <text class="content-text">
签名 {{userInfo.signature}} {{ userInfo.nickName }}
</text> </text>
</view> </view>
<view class="info-arrow">></view> <view class="info-text">
<view>
<text class="label-text">
签名:
</text>
<text class="content-text">
{{ userInfo.signature }}
</text>
</view>
</view>
</view>
<view class="info-arrow">
</view> </view>
<view class="line"></view>
<view class="btn-group">
<button class="btn" type="primary" @click="onModifyPassword()">修改密码</button>
<button class="btn" type="warn" @click="onQuit()">退出</button>
</view> </view>
</uni-card>
<bar-group>
<arrow-bar title="修改密码" @click="onModifyPassword()"></arrow-bar>
</bar-group>
<bar-group>
<btn-bar title="退出登陆" type="danger" @click="onQuit()"></btn-bar>
</bar-group>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return {} return {}
}, },
@ -66,13 +80,13 @@
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.mine { .mine {
.content { .content {
height: 200rpx; //height: 200rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@ -85,16 +99,36 @@
padding-left: 40rpx; padding-left: 40rpx;
flex: 1; flex: 1;
.info-text {
line-height: 1.5;
//margin-bottom: 10rpx;
}
.label-text {
font-size: $im-font-size-small;
color: $im-text-color-light;
}
.content-text {
font-size: $im-font-size-small;
color: $im-text-color-light;
}
.info-primary { .info-primary {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 10rpx;
.info-username { .info-username {
font-size: 40rpx; font-size: $im-font-size-large;
font-weight: 600; font-weight: 600;
} }
.icon-man { .icon-man {
color: darkblue; color: $im-text-color;
font-size: $im-font-size-large;
padding-left: 10rpx;
} }
.icon-girl { .icon-girl {
@ -106,22 +140,10 @@
.info-arrow { .info-arrow {
width: 50rpx; width: 50rpx;
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; position: relative;
} left: 30rpx;
} }
.line {
margin: 20rpx;
border-bottom: 1px solid #aaaaaa;
} }
.btn-group { }
margin: 100rpx;
.btn {
margin-top: 20rpx;
}
}
}
</style> </style>

20
im-uniapp/pages/register/register.vue

@ -14,16 +14,16 @@
<uni-forms-item name="corfirmPassword" label="确认密码"> <uni-forms-item name="corfirmPassword" label="确认密码">
<uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" /> <uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" />
</uni-forms-item> </uni-forms-item>
<button class="btn-submit" @click="submit" type="warn">注册并登陆</button> <button class="btn-submit" @click="submit" type="primary">注册并登陆</button>
</uni-forms> </uni-forms>
<navigator class="nav-login" url="/pages/login/login" > <navigator class="nav-login" url="/pages/login/login">
返回登陆页面 返回登陆页面
</navigator> </navigator>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
dataForm: { dataForm: {
@ -70,6 +70,7 @@
}, },
methods: { methods: {
submit() { submit() {
this.$refs.form.validate().then(() => {
this.$http({ this.$http({
url: '/register', url: '/register',
data: this.dataForm, data: this.dataForm,
@ -81,6 +82,7 @@
}) })
this.login(); this.login();
}) })
})
}, },
login() { login() {
const loginForm = { const loginForm = {
@ -106,17 +108,17 @@
}) })
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.register { .register {
.title { .title {
padding-top: 150rpx; padding-top: 150rpx;
padding-bottom: 50rpx; padding-bottom: 50rpx;
color: royalblue; color: $im-color-primary;
text-align: center; text-align: center;
font-size: 60rpx; font-size: 24px;
font-weight: 600; font-weight: 600;
} }
@ -133,9 +135,9 @@
position: fixed; position: fixed;
width: 100%; width: 100%;
bottom: 100rpx; bottom: 100rpx;
color: royalblue; color: $im-color-primary;
text-align: center; text-align: center;
font-size: 32rpx; font-size: 32rpx;
} }
} }
</style> </style>

BIN
im-uniapp/static/logo/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 238 KiB

BIN
im-uniapp/static/tarbar/mine.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
im-uniapp/static/tarbar/mine_active.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

26
im-uniapp/store/chatStore.js

@ -99,7 +99,7 @@ export default defineStore('chatStore', {
} }
} }
}) })
if(!chat.stored){ if (!chat.stored) {
this.saveToStorage(); this.saveToStorage();
} }
}, },
@ -293,7 +293,7 @@ export default defineStore('chatStore', {
} }
}, },
refreshChats() { refreshChats() {
if(!cacheChats){ if (!cacheChats) {
return; return;
} }
// 排序 // 排序
@ -316,17 +316,17 @@ export default defineStore('chatStore', {
let key = "chats-app-" + userId; let key = "chats-app-" + userId;
let chatKeys = []; let chatKeys = [];
// 按会话为单位存储,只存储有改动的会话 // 按会话为单位存储,只存储有改动的会话
this.chats.forEach((chat)=>{ this.chats.forEach((chat) => {
let chatKey = `${key}-${chat.type}-${chat.targetId}` let chatKey = `${key}-${chat.type}-${chat.targetId}`
if(!chat.stored){ if (!chat.stored) {
if(chat.delete){ if (chat.delete) {
uni.removeStorageSync(chatKey); uni.removeStorageSync(chatKey);
}else{ } else {
uni.setStorageSync(chatKey,chat); uni.setStorageSync(chatKey, chat);
} }
chat.stored = true; chat.stored = true;
} }
if(!chat.delete){ if (!chat.delete) {
chatKeys.push(chatKey); chatKeys.push(chatKey);
} }
}) })
@ -353,13 +353,13 @@ export default defineStore('chatStore', {
let userStore = useUserStore(); let userStore = useUserStore();
let userId = userStore.userInfo.id; let userId = userStore.userInfo.id;
let chatsData = uni.getStorageSync("chats-app-" + userId) let chatsData = uni.getStorageSync("chats-app-" + userId)
if(chatsData){ if (chatsData) {
if(chatsData.chatKeys){ if (chatsData.chatKeys) {
let time = new Date().getTime(); let time = new Date().getTime();
chatsData.chats = []; chatsData.chats = [];
chatsData.chatKeys.forEach(key=>{ chatsData.chatKeys.forEach(key => {
let chat = uni.getStorageSync(key); let chat = uni.getStorageSync(key);
if(chat){ if (chat) {
chatsData.chats.push(chat); chatsData.chats.push(chat);
} }
}) })
@ -375,7 +375,7 @@ export default defineStore('chatStore', {
return state.loadingPrivateMsg || state.loadingGroupMsg return state.loadingPrivateMsg || state.loadingGroupMsg
}, },
curChats: (state) => { curChats: (state) => {
if(cacheChats && state.isLoading()){ if (cacheChats && state.isLoading()) {
return cacheChats; return cacheChats;
} }
return state.chats; return state.chats;

2
im-uniapp/store/userStore.js

@ -14,7 +14,7 @@ export default defineStore('userStore', {
clear() { clear() {
this.userInfo = {}; this.userInfo = {};
}, },
loadUser(context) { loadUser() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http({ http({
url: '/user/self', url: '/user/self',

46
im-uniapp/uni.scss

@ -1,5 +1,51 @@
@import '@/uni_modules/uview-plus/theme.scss'; @import '@/uni_modules/uview-plus/theme.scss';
@import '@/uni_modules/uni-scss/variables.scss'; @import '@/uni_modules/uni-scss/variables.scss';
@import '@/im-var.scss';
// 修改uni-ui主题
$uni-primary: $im-color-primary;
$uni-success: $im-color-success;
$uni-warning: $im-color-warning;
$uni-error: $im-color-danger;
$uni-info: $im-color-info;
// $uni-main-color: #3a3a3a; // 主要文字
// $uni-base-color: #6a6a6a; // 常规文字
// $uni-secondary-color: #909399; // 次要文字
// $uni-extra-color: #c7c7c7; // 辅助说明
// // 边框颜色
// $uni-border-1: #F0F0F0;
// $uni-border-2: #EDEDED;
// $uni-border-3: #DCDCDC;
// $uni-border-4: #B9B9B9;
// // 常规色
// $uni-black: #000000;
// $uni-white: #ffffff;
// $uni-transparent: rgba($color: #000000, $alpha: 0);
// // 背景色
// $uni-bg-color: #f7f7f7;
// /* 水平间距 */
// $uni-spacing-sm: 8px;
// $uni-spacing-base: 15px;
// $uni-spacing-lg: 30px;
// // 阴影
// $uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5);
// $uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2);
// $uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5);
// // 蒙版
// $uni-mask: rgba($color: #000000, $alpha: 0.4);
/**************************************************/
/* 行为相关颜色 */ /* 行为相关颜色 */
$uni-color-primary: #007aff; $uni-color-primary: #007aff;
$uni-color-success: #4cd964; $uni-color-success: #4cd964;

BIN
im-uniapp/unpackage/res/icons/1024x1024.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
im-uniapp/unpackage/res/icons/120x120.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
im-uniapp/unpackage/res/icons/144x144.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
im-uniapp/unpackage/res/icons/152x152.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
im-uniapp/unpackage/res/icons/167x167.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
im-uniapp/unpackage/res/icons/180x180.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
im-uniapp/unpackage/res/icons/192x192.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
im-uniapp/unpackage/res/icons/20x20.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

BIN
im-uniapp/unpackage/res/icons/29x29.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
im-uniapp/unpackage/res/icons/40x40.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
im-uniapp/unpackage/res/icons/58x58.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
im-uniapp/unpackage/res/icons/60x60.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
im-uniapp/unpackage/res/icons/72x72.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
im-uniapp/unpackage/res/icons/76x76.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
im-uniapp/unpackage/res/icons/80x80.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
im-uniapp/unpackage/res/icons/87x87.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
im-uniapp/unpackage/res/icons/96x96.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

2
im-uniapp/vite.config.js

@ -1,7 +1,5 @@
import { defineConfig } from "vite" import { defineConfig } from "vite"
import uni from "@dcloudio/vite-plugin-uni"; import uni from "@dcloudio/vite-plugin-uni";
const path = require('path')
const fs = require('fs')
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
uni() uni()

24
im-web/package.json

@ -8,18 +8,18 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^1.1.3", "axios": "1.7.7",
"core-js": "^3.6.5", "core-js": "3.38.1",
"element-ui": "^2.15.10", "element-ui": "2.15.14",
"js-audio-recorder": "^1.0.7", "js-audio-recorder": "1.0.7",
"localforage": "^1.10.0", "localforage": "1.10.0",
"sass": "^1.47.0", "sass": "1.32.12",
"sass-loader": "^10.1.1", "sass-loader": "10.1.1",
"vue": "^2.6.11", "vue": "2.7.16",
"vue-axios": "^3.5.0", "vue-axios": "3.5.2",
"vue-router": "^3.3.3", "vue-router": "3.6.5",
"vuex": "^3.6.2", "vuex": "3.6.2",
"vuex-persist": "^3.1.3" "vuex-persist": "3.1.3"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.12", "@vue/cli-plugin-babel": "~4.5.12",

BIN
im-web/public/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 141 KiB

99
im-web/src/App.vue

@ -5,111 +5,20 @@
</template> </template>
<script> <script>
export default { export default {
name: 'App', name: 'App',
components: {} components: {}
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import './assets/style/global.css';
#app { #app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
} color: var(--im-text-color);
font-family: var(--im-font-family);
.el-message {
z-index: 99999999 !important;
}
.el-scrollbar__thumb {
background-color: #A0A8AF !important;
}
.el-dialog {
border-radius: 8px !important;
overflow: hidden !important;
}
.el-dialog__header {
background-color: #5870e6 !important;
}
.el-dialog__title {
color: #f8f8f8 !important;
}
.el-dialog__close {
color: white !important;
font-size: 20px;
}
.el-checkbox__inner {
border-radius: 50% !important;
}
.el-input__inner {
border-radius: 5px !important;
border: #587FF0 1px solid !important;
}
.el-textarea__inner{
border-radius: 5px !important;
border: #587FF0 1px solid !important;
}
.el-icon-search {
color:#587FF0 !important;
}
.el-button--primary {
background-color: #687Ff0 !important;
border-color: #687Ff0 !important;
}
.el-button--success {
background-color: #4cae1b !important;
border-color: #4cae1b !important;
}
.el-button--danger {
background-color: #ea4949 !important;
border-color: #ea4949 !important;
}
.el-button {
padding: 8px 15px !important;
}
.el-checkbox {
display: flex;
align-items: center;
//
.el-checkbox__inner {
width: 20px;
height: 20px;
//
&::after {
height: 12px;
left: 7px;
}
}
//
.el-checkbox__input.is-checked+.el-checkbox__label {
color: #333333;
}
.el-checkbox__label {
line-height: 20px;
padding-left: 8px;
}
} }
</style> </style>

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

@ -5,20 +5,17 @@ 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 = {
video: { video: true,
with: window.screen.width,
height: window.screen.height
},
audio: { audio: {
echoCancellation: true, //音频开启回音消除 echoCancellation: true, //音频开启回音消除
noiseSuppression: true // 开启降噪 noiseSuppression: true // 开启降噪
@ -41,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,
@ -64,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) => {

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

@ -20,8 +20,8 @@ let toTimeText = (timeStamp, simple) => {
} 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,

6
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

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

@ -15,17 +15,17 @@ 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`);
return `<img src="${url}" style="width:35px;height:35px;vertical-align:bottom;"/>` return `<img src="${url}" style="width:32px;height:32px;vertical-align:bottom;"/>`
} }
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();

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

@ -1,19 +1,19 @@
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()
@ -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)
} }

BIN
im-web/src/assets/image/online_app.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

BIN
im-web/src/assets/image/online_web.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

112
im-web/src/assets/style/element.scss

@ -0,0 +1,112 @@
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// 文字
$--font-family: Microsoft YaHei, 'Avenir', Helvetica, Arial, sans-serif;
@import "thems";
@import "~element-ui/packages/theme-chalk/src/index";
.el-message {
z-index: 99999999 !important;
background: #fff !important;
box-shadow: 0 4px 12px 0 rgb(0 0 0 / 15%);
border: none !important;
min-width: unset !important;
border-radius: 3px !important;
padding: 14px 18px 14px 16px !important;
.el-message__content {
color: #000 !important;
}
}
.el-scrollbar__thumb {
background-color: #A0A8AF !important;
}
.el-dialog__title {
font-size: var(--im-font-size-larger);
color: var(--im-text-color);
}
.el-dialog__header {
padding: 12px 18px !important;
}
.el-dialog__headerbtn {
top: 15px;
right: 20px;
font-size: 18px;
}
.el-checkbox__inner {
border-radius: 50% !important;
}
.el-button--success {
//background-color: #688758 !important;
//border-color: #4cae1b !important;
}
.el-button--danger {
//background-color: #ea4949 !important;
//border-color: #ea4949 !important;
}
.el-button {
padding: 8px 15px !important;
}
.el-checkbox {
display: flex;
align-items: center;
//修改选中框的大小
.el-checkbox__inner {
width: 16px;
height: 16px;
//修改选中框中的对勾的大小和位置
&::after {
height: 7px;
left: 5px;
top: 2px;
}
}
// 修改点击文字颜色不变
.el-checkbox__input.is-checked + .el-checkbox__label {
color: #333333;
}
.el-checkbox__label {
line-height: 20px;
padding-left: 8px;
}
}
.el-form-item {
margin-bottom: 15px !important;
}
.el-input--small {
font-size: $--font-size-base;
}
.el-input__inner {
padding: 0 10px;
}
.el-textarea__inner {
padding: 5px 10px;
font-family: $--font-family;
}
.el-tag--mini {
height: 18px;
padding: 0 2px;
line-height: 16px;
border-radius: 2px;
}

43
im-web/src/assets/style/global.css

@ -1,43 +0,0 @@
@charset "UTF-8";
html {
height: 100%;
overflow: hidden;
}
body {
height: 100%;
margin: 0;
overflow: hidden;
}
section {
height: 100%;
}
.el-dialog__body{
padding: 10px 15px !important;
}
::-webkit-scrollbar {
width: 6px;
height: 1px;
}
::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 2px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: #535353;
}
::-webkit-scrollbar-track {
/*滚动条里面轨道*/
-webkit-box-shadow: inset 0 0 5px transparent;
border-radius: 2px;
background: #ededed;
}
/*# sourceMappingURL=v-im.cssss.map */

91
im-web/src/assets/style/im.scss

@ -0,0 +1,91 @@
@charset "UTF-8";
@import "element";
// im全局样式变量
:root {
// 主色
--im-color-primary: #{$--color-primary};
--im-color-primary-light-1: #{$--color-primary-light-1};
--im-color-primary-light-2: #{$--color-primary-light-2};
--im-color-primary-light-3: #{$--color-primary-light-3};
--im-color-primary-light-4: #{$--color-primary-light-4};
--im-color-primary-light-5: #{$--color-primary-light-5};
--im-color-primary-light-6: #{$--color-primary-light-6};
--im-color-primary-light-7: #{$--color-primary-light-7};
--im-color-primary-light-8: #{$--color-primary-light-8};
--im-color-primary-light-9: #{$--color-primary-light-9};
--im-color-sucess: #{$--color-success};
--im-color-warning: #{$--color-warning};
--im-color-danger: #{$--color-danger};
--im-color-info: #{$--color-info};
// 文字颜色
--im-text-color: #{$--color-text-regular};
--im-text-color-light: #999999;
--im-text-color-lighter: #C0C4CC;
// 文字大小
--im-font-size: #{$--font-size-base};
--im-font-size-small: #{$--font-size-small};
--im-font-size-smaller: #{$--font-size-extra-small};
--im-font-size-large: #{$--font-size-medium};
--im-font-size-larger: #{$--font-size-large};
--im-font-family: #{$--font-family};
// 边框颜色
--im-border: 1px solid #EBEEF5;
// 阴影
--im-box-shadow: #{$--box-shadow-base};
--im-box-shadow-light: #{$--box-shadow-light};
--im-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, .12);
--im-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, .08), 0px 12px 32px rgba(0, 0, 0, .12), 0px 8px 16px -8px rgba(0, 0, 0, .16);
// 背景色
--im-background: #F3F3F3;
--im-background-active: #F1F1F1;
--im-background-active-dark: #E9E9E9;
}
html {
height: 100%;
overflow: hidden;
}
body {
height: 100%;
margin: 0;
overflow: hidden;
}
section {
height: 100%;
}
.el-dialog__body {
padding: 10px 20px !important;
}
// 滚动条样式
::-webkit-scrollbar {
width: 8px;
height: 1px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background: hsla(0, 0%, 73%, .5);
}
::-webkit-scrollbar-track {
border-radius: 4px;
}
.search-input {
.el-input__inner {
border: unset !important;
}
}

6
im-web/src/assets/style/thems.scss

@ -0,0 +1,6 @@
// 主题色
$--color-primary: #2830d3;
//$--color-primary: #687ff0;
//$--color-primary: #096bff;
$--font-size-base: 14px;
$--color-text-regular: #000000;

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

@ -1,16 +1,16 @@
<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
@ -55,7 +55,7 @@
this.showMembers.push(m); this.showMembers.push(m);
} }
}) })
this.activeIdx = this.showMembers.length > 0 ? 0: -1; this.activeIdx = this.showMembers.length > 0 ? 0 : -1;
}, },
open(pos) { open(pos) {
this.show = true; this.show = true;
@ -115,17 +115,17 @@
} }
} }
} }
</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: #f5f5f5; background-color: #fff;
box-shadow: 0px 0px 10px #ccc; box-shadow: var(--im-box-shadow);
} }
</style> </style>

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

@ -1,7 +1,7 @@
<template> <template>
<div class="chat-box" @click="closeRefBox()" @mousemove="readedMessage()"> <div class="chat-box" @click="closeRefBox()" @mousemove="readedMessage()">
<el-container> <el-container>
<el-header height="56px"> <el-header height="50px">
<span>{{ title }}</span> <span>{{ title }}</span>
<span title="群聊信息" v-show="this.chat.type == 'GROUP'" class="btn-side el-icon-more" <span title="群聊信息" v-show="this.chat.type == 'GROUP'" class="btn-side el-icon-more"
@click="showSide = !showSide"></span> @click="showSide = !showSide"></span>
@ -23,7 +23,7 @@
</ul> </ul>
</div> </div>
</el-main> </el-main>
<el-footer height="240px" class="im-chat-footer"> <el-footer height="220px" class="im-chat-footer">
<div class="chat-tool-bar"> <div class="chat-tool-bar">
<div title="表情" class="icon iconfont icon-emoji" ref="emotion" <div title="表情" class="icon iconfont icon-emoji" ref="emotion"
@click.stop="showEmotionBox()"> @click.stop="showEmotionBox()">
@ -61,12 +61,13 @@
<ChatInput :ownerId="group.ownerId" ref="chatInputEditor" :group-members="groupMembers" <ChatInput :ownerId="group.ownerId" ref="chatInputEditor" :group-members="groupMembers"
@submit="sendMessage" /> @submit="sendMessage" />
<div class="send-btn-area"> <div class="send-btn-area">
<el-button type="primary" size="small" @click="notifySend()">发送</el-button> <el-button type="primary" icon="el-icon-s-promotion"
@click="notifySend()">发送</el-button>
</div> </div>
</div> </div>
</el-footer> </el-footer>
</el-container> </el-container>
<el-aside class="chat-group-side-box" width="300px" v-if="showSide"> <el-aside class="chat-group-side-box" width="260px" v-if="showSide">
<chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)"> <chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)">
</chat-group-side> </chat-group-side>
</el-aside> </el-aside>
@ -83,19 +84,19 @@
</template> </template>
<script> <script>
import ChatGroupSide from "./ChatGroupSide.vue"; import ChatGroupSide from "./ChatGroupSide.vue";
import ChatMessageItem from "./ChatMessageItem.vue"; import ChatMessageItem from "./ChatMessageItem.vue";
import FileUpload from "../common/FileUpload.vue"; import FileUpload from "../common/FileUpload.vue";
import Emotion from "../common/Emotion.vue"; import Emotion from "../common/Emotion.vue";
import ChatRecord from "./ChatRecord.vue"; import ChatRecord from "./ChatRecord.vue";
import ChatHistory from "./ChatHistory.vue"; import ChatHistory from "./ChatHistory.vue";
import ChatAtBox from "./ChatAtBox.vue" import ChatAtBox from "./ChatAtBox.vue"
import GroupMemberSelector from "../group/GroupMemberSelector.vue" import GroupMemberSelector from "../group/GroupMemberSelector.vue"
import RtcGroupJoin from "../rtc/RtcGroupJoin.vue" import RtcGroupJoin from "../rtc/RtcGroupJoin.vue"
import ChatInput from "./ChatInput"; import ChatInput from "./ChatInput";
export default { export default {
name: "chatPrivate", name: "chatPrivate",
components: { components: {
ChatInput, ChatInput,
@ -129,7 +130,7 @@
lockMessage: false, // lockMessage: false, //
showMinIdx: 0, // showMinIdx showMinIdx: 0, // showMinIdx
reqQueue: [], reqQueue: [],
isSending : false isSending: false
} }
}, },
methods: { methods: {
@ -369,7 +370,7 @@
let msg = fullList[i]; let msg = fullList[i];
switch (msg.type) { switch (msg.type) {
case "text": case "text":
await this.sendTextMessage(sendText + msg.content,msg.atUserIds); await this.sendTextMessage(sendText + msg.content, msg.atUserIds);
break; break;
case "image": case "image":
await this.sendImageMessage(msg.content.file); await this.sendImageMessage(msg.content.file);
@ -382,7 +383,7 @@
} }
}, },
sendImageMessage(file) { sendImageMessage(file) {
return new Promise((resolve,reject)=>{ return new Promise((resolve, reject) => {
this.onImageBefore(file); this.onImageBefore(file);
let formData = new FormData() let formData = new FormData()
formData.append('file', file) formData.append('file', file)
@ -401,8 +402,8 @@
this.scrollToBottom(); this.scrollToBottom();
}); });
}, },
sendTextMessage(sendText,atUserIds) { sendTextMessage(sendText, atUserIds) {
return new Promise((resolve,reject)=>{ return new Promise((resolve, reject) => {
if (!sendText.trim()) { if (!sendText.trim()) {
reject(); reject();
} }
@ -432,7 +433,7 @@
}); });
}, },
sendFileMessage(file) { sendFileMessage(file) {
return new Promise((resolve,reject)=>{ return new Promise((resolve, reject) => {
let check = this.$refs.fileUpload.beforeUpload(file); let check = this.$refs.fileUpload.beforeUpload(file);
if (check) { if (check) {
this.$refs.fileUpload.onFileUpload({ file }); this.$refs.fileUpload.onFileUpload({ file });
@ -481,7 +482,7 @@
this.$http({ this.$http({
url: url, url: url,
method: 'put' method: 'put'
}).then(() => {}) }).then(() => { })
}, },
loadReaded(fId) { loadReaded(fId) {
this.$http({ this.$http({
@ -561,14 +562,14 @@
this.placeholder = "聊点什么吧~"; this.placeholder = "聊点什么吧~";
} }
}, },
sendMessageRequest(msgInfo){ sendMessageRequest(msgInfo) {
return new Promise((resolve,reject)=>{ return new Promise((resolve, reject) => {
// "" // ""
this.reqQueue.push({msgInfo,resolve,reject}); this.reqQueue.push({ msgInfo, resolve, reject });
this.processReqQueue(); this.processReqQueue();
}) })
}, },
processReqQueue(){ processReqQueue() {
if (this.reqQueue.length && !this.isSending) { if (this.reqQueue.length && !this.isSending) {
this.isSending = true; this.isSending = true;
const reqData = this.reqQueue.shift(); const reqData = this.reqQueue.shift();
@ -576,11 +577,11 @@
url: this.messageAction, url: this.messageAction,
method: 'post', method: 'post',
data: reqData.msgInfo data: reqData.msgInfo
}).then((res)=>{ }).then((res) => {
reqData.resolve(res) reqData.resolve(res)
}).catch((e)=>{ }).catch((e) => {
reqData.reject(e) reqData.reject(e)
}).finally(()=>{ }).finally(() => {
this.isSending = false; this.isSending = false;
// //
this.processReqQueue(); this.processReqQueue();
@ -660,36 +661,37 @@
let div = document.getElementById("chatScrollBox"); let div = document.getElementById("chatScrollBox");
div.addEventListener('scroll', this.onScroll) div.addEventListener('scroll', this.onScroll)
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.chat-box { .chat-box {
position: relative; position: relative;
width: 100%; width: 100%;
background: #f8f8f8; background: #fff;
border: #dddddd solid 1px;
.el-header { .el-header {
padding: 3px; display: flex;
background-color: white; justify-content: space-between;
padding: 0 12px;
line-height: 50px; line-height: 50px;
font-size: 20px; font-size: var(--im-font-size-larger);
font-weight: 600; border-bottom: var(--im-border);
border-bottom: 1px #ddd solid;
.btn-side { .btn-side {
position: absolute; position: absolute;
right: 20px; right: 20px;
line-height: 50px; line-height: 50px;
font-size: 25px; font-size: 20px;
cursor: pointer; cursor: pointer;
color: var(--im-text-color-light);
} }
} }
.im-chat-main { .im-chat-main {
padding: 0; padding: 0;
background-color: white; background-color: #fff;
.im-chat-box { .im-chat-box {
>ul { >ul {
@ -711,36 +713,34 @@
display: flex; display: flex;
position: relative; position: relative;
width: 100%; width: 100%;
height: 40px; height: 36px;
text-align: left; text-align: left;
box-sizing: border-box; box-sizing: border-box;
border-top: #ccc solid 1px; border-top: var(--im-border);
padding: 2px; padding: 4px 2px 2px 8px;
background-color: #f8faff;
>div { >div {
font-size: 22px; font-size: 22px;
cursor: pointer; cursor: pointer;
color: black;
line-height: 30px; line-height: 30px;
width: 30px; width: 30px;
height: 30px; height: 30px;
text-align: center; text-align: center;
border-radius: 3px; border-radius: 2px;
margin: 3px 5px; margin-right: 8px;
color: #0f46ae; color: #999;
&:hover { transition: 0.3s;
font-weight: 600;
color: #042259;
}
&.chat-tool-active { &.chat-tool-active {
color: white; font-weight: 600;
background-color: #195ee2; color: var(--im-color-primary);
background-color: #ddd;
} }
} }
>div:hover {
color: #333;
}
} }
.send-content-area { .send-content-area {
@ -757,7 +757,6 @@
flex: 1; flex: 1;
resize: none; resize: none;
font-size: 16px; font-size: 16px;
color: black;
outline: none; outline: none;
text-align: left; text-align: left;
@ -820,15 +819,16 @@
.send-btn-area { .send-btn-area {
padding: 10px; padding: 10px;
position: absolute; position: absolute;
bottom: 0; bottom: 4px;
right: 0; right: 6px;
} }
} }
} }
.chat-group-side-box { .chat-group-side-box {
border: #dddddd solid 1px; border-left: var(--im-border);
animation: rtl-drawer-in .3s 1ms; //animation: rtl-drawer-in .3s 1ms;
}
} }
}
</style> </style>

23
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,7 +22,7 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
height:{ height: {
type: Number, type: Number,
default: 50 default: 50
}, },
@ -31,8 +31,8 @@ export default {
default: false default: false
} }
}, },
computed:{ computed: {
headImageSize(){ headImageSize() {
return Math.ceil(this.height * 0.75) return Math.ceil(this.height * 0.75)
} }
} }
@ -42,30 +42,19 @@ export default {
<style lang="scss"> <style lang="scss">
.chat-group-member { .chat-group-member {
display: flex; display: flex;
margin-bottom: 1px;
position: relative; position: relative;
padding: 0 5px; padding: 0 5px;
align-items: center; align-items: center;
background-color: #fafafa;
white-space: nowrap; white-space: nowrap;
box-sizing: border-box; box-sizing: border-box;
&:hover {
background-color: #F8FAFF;
}
&.active {
background-color: #E8F2FF;
}
.member-name { .member-name {
padding-left: 10px; padding-left: 10px;
height: 100%; height: 100%;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
font-size: 14px; font-size: var(--im-font-size);
font-weight: 600;
} }
} }
</style> </style>

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

@ -130,17 +130,13 @@ export default {
.chat-group-readed { .chat-group-readed {
position: fixed; position: fixed;
box-shadow: 0px 0px 10px #ccc;
width: 300px; width: 300px;
background-color: #fafafa;
border-radius: 8px;
.scroll-box { .scroll-box {
height: 400px; height: 400px;
} }
.arrow-left { .arrow-left {
position: absolute; position: absolute;
left: -15px; left: -15px;
width: 0; width: 0;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save