Browse Source

!147 增加回到消息底部功能

Merge pull request !147 from blue/v_3.0.0
master
blue 10 months ago
committed by Gitee
parent
commit
8f2be03c8d
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 4
      im-platform/src/main/java/com/bx/implatform/dto/RegisterDTO.java
  2. 17
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  3. 12
      im-uniapp/components/chat-at-box/chat-at-box.vue
  4. 95
      im-uniapp/components/chat-message-item/chat-message-item.vue
  5. 19
      im-uniapp/components/friend-item/friend-item.vue
  6. 29
      im-uniapp/im.scss
  7. 16
      im-uniapp/pages.json
  8. 82
      im-uniapp/pages/chat/chat-box.vue
  9. 37
      im-uniapp/pages/group/group-invite.vue
  10. 6
      im-uniapp/pages/mine/mine-password.vue
  11. 8
      im-uniapp/pages/register/register.vue
  12. 0
      im-uniapp/static/tab/chat.png
  13. BIN
      im-uniapp/static/tab/chat_active.png
  14. 0
      im-uniapp/static/tab/friend.png
  15. BIN
      im-uniapp/static/tab/friend_active.png
  16. 0
      im-uniapp/static/tab/group.png
  17. BIN
      im-uniapp/static/tab/group_active.png
  18. BIN
      im-uniapp/static/tab/mine.png
  19. BIN
      im-uniapp/static/tab/mine_active.png
  20. BIN
      im-uniapp/static/tarbar/chat_active.png
  21. BIN
      im-uniapp/static/tarbar/friend_active.png
  22. BIN
      im-uniapp/static/tarbar/group_active.png
  23. BIN
      im-uniapp/static/tarbar/mine.png
  24. BIN
      im-uniapp/static/tarbar/mine_active.png
  25. 6
      im-web/src/components/chat/ChatAtBox.vue
  26. 168
      im-web/src/components/chat/ChatBox.vue
  27. 6
      im-web/src/components/chat/ChatGroupMember.vue
  28. 2
      im-web/src/components/chat/ChatGroupReaded.vue
  29. 13
      im-web/src/components/chat/ChatGroupSide.vue
  30. 6
      im-web/src/components/chat/ChatHistory.vue
  31. 20
      im-web/src/components/chat/ChatInput.vue
  32. 17
      im-web/src/components/chat/ChatItem.vue
  33. 71
      im-web/src/components/chat/ChatMessageItem.vue
  34. 5
      im-web/src/components/chat/ChatRecord.vue
  35. 18
      im-web/src/components/common/Emotion.vue
  36. 2
      im-web/src/components/common/FullImage.vue
  37. 1
      im-web/src/components/common/HeadImage.vue
  38. 2
      im-web/src/components/common/Icp.vue
  39. 2
      im-web/src/components/common/RightMenu.vue
  40. 13
      im-web/src/components/common/UserInfo.vue
  41. 12
      im-web/src/components/friend/AddFriend.vue
  42. 311
      im-web/src/components/group/AddGroupMember.vue
  43. 2
      im-web/src/components/group/GroupItem.vue
  44. 2
      im-web/src/components/group/GroupMember.vue
  45. 2
      im-web/src/components/group/GroupMemberItem.vue
  46. 8
      im-web/src/components/group/GroupMemberSelector.vue
  47. 2
      im-web/src/components/rtc/RtcGroupJoin.vue
  48. 2
      im-web/src/components/rtc/RtcGroupVideo.vue
  49. 4
      im-web/src/components/rtc/RtcPrivateAcceptor.vue
  50. 2
      im-web/src/components/rtc/RtcPrivateVideo.vue
  51. 6
      im-web/src/components/setting/Setting.vue
  52. 28
      im-web/src/view/Chat.vue
  53. 34
      im-web/src/view/Friend.vue
  54. 165
      im-web/src/view/Group.vue
  55. 9
      im-web/src/view/Home.vue
  56. 12
      im-web/src/view/Login.vue
  57. 22
      im-web/src/view/Register.vue

4
im-platform/src/main/java/com/bx/implatform/dto/RegisterDTO.java

@ -9,7 +9,7 @@ import org.hibernate.validator.constraints.Length;
@Schema(description = "用户注册DTO")
public class RegisterDTO {
@Length(max = 64, message = "用户名不能大于64字符")
@Length(max = 20, message = "用户名不能大于20字符")
@NotEmpty(message = "用户名不可为空")
@Schema(description = "用户名")
private String userName;
@ -19,7 +19,7 @@ public class RegisterDTO {
@Schema(description = "用户密码")
private String password;
@Length(max = 64, message = "昵称不能大于64字符")
@Length(max = 20, message = "昵称不能大于20字符")
@NotEmpty(message = "用户昵称不可为空")
@Schema(description = "用户昵称")
private String nickName;

17
im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java

@ -24,6 +24,7 @@ import com.bx.implatform.service.UserService;
import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.util.SensitiveFilterUtil;
import com.bx.implatform.vo.LoginVO;
import com.bx.implatform.vo.OnlineTerminalVO;
import com.bx.implatform.vo.UserVO;
@ -46,6 +47,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
private final FriendService friendService;
private final JwtProperties jwtProperties;
private final IMClient imClient;
private final SensitiveFilterUtil sensitiveFilterUtil;
@Override
public LoginVO login(LoginDTO dto) {
@ -108,6 +110,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
@Override
public void register(RegisterDTO dto) {
User user = this.findUserByUserName(dto.getUserName());
if(!dto.getUserName().equals(sensitiveFilterUtil.filter(dto.getUserName()))){
throw new GlobalException("用户名包含敏感字符");
}
if(!dto.getNickName().equals(sensitiveFilterUtil.filter(dto.getNickName()))){
throw new GlobalException("昵称包含敏感字符");
}
if (!Objects.isNull(user)) {
throw new GlobalException(ResultCode.USERNAME_ALREADY_REGISTER);
}
@ -140,14 +148,19 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
@Override
public void update(UserVO vo) {
UserSession session = SessionContext.getSession();
if(!vo.getNickName().equals(sensitiveFilterUtil.filter(vo.getNickName()))){
throw new GlobalException("昵称包含敏感字符");
}
if(!vo.getSignature().equals(sensitiveFilterUtil.filter(vo.getSignature()))){
throw new GlobalException("签名内容包含敏感字符");
}
if (!session.getUserId().equals(vo.getId())) {
throw new GlobalException("不允许修改其他用户的信息!");
throw new GlobalException("不允许修改其他用户的信息");
}
User user = this.getById(vo.getId());
if (Objects.isNull(user)) {
throw new GlobalException("用户不存在");
}
if (!user.getNickName().equals(vo.getNickName()) || !user.getHeadImageThumb().equals(vo.getHeadImageThumb())) {
// 更新好友昵称和头像
LambdaUpdateWrapper<Friend> wrapper1 = Wrappers.lambdaUpdate();

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

@ -20,10 +20,12 @@
<view class="member-items">
<virtual-scroller :items="memberItems">
<template v-slot="{ item }">
<view class="member-item" :class="{ checked: item.checked }" @click="onSwitchChecked(item)">
<view class="member-item" @click="onSwitchChecked(item)">
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
size="small"></head-image>
<view class="member-name">{{ item.showNickName }}</view>
<radio :checked="item.checked" :disabled="item.locked"
@click.stop="onSwitchChecked(item)" />
</view>
</template>
</virtual-scroller>
@ -152,11 +154,11 @@ export default {
background-color: white;
white-space: nowrap;
margin-bottom: 1px;
&.checked {
background-color: $im-color-primary-light-9;
&:hover {
background-color: $im-bg-active;
}
.member-name {
flex: 1;
padding-left: 20rpx;

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

@ -1,31 +1,31 @@
<template>
<view class="chat-msg-item">
<view class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
<view class="chat-message-item">
<view class="message-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{ msgInfo.content }}
</view>
<view class="chat-msg-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
<view class="message-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{ $date.toTimeText(msgInfo.sendTime) }}
</view>
<view class="chat-msg-normal" v-else-if="isNormal" :class="{ 'chat-msg-mine': msgInfo.selfSend }">
<view class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': msgInfo.selfSend }">
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
:name="showName" size="small"></head-image>
<view class="chat-msg-content">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<view class="content">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="top">
<text>{{ showName }}</text>
</view>
<view class="chat-msg-bottom">
<view class="bottom">
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<!-- rich-text支持显示表情但是不支持点击a标签 -->
<rich-text v-if="$emo.containEmoji(msgInfo.content)" class="chat-msg-text"
<rich-text v-if="$emo.containEmoji(msgInfo.content)" class="message-text"
:nodes="nodesText"></rich-text>
<!-- up-parse支持点击a标签,但安卓打包后表情无法显示,原因未知 -->
<up-parse v-else class="chat-msg-text" :showImgMenu="false" :content="nodesText"></up-parse>
<up-parse v-else class="message-text" :showImgMenu="false" :content="nodesText"></up-parse>
</long-press-menu>
</view>
<view class="chat-msg-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<view class="message-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="img-load-box">
<view class="image-box">
<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl"
lazy-load="true" @click.stop="onShowFullImage()">
</image>
@ -35,15 +35,15 @@
<text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view>
<view class="chat-msg-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<view class="message-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="chat-file-box">
<view class="chat-file-info">
<uni-link class="chat-file-name" :text="data.name" showUnderLine="true"
<view class="file-box">
<view class="file-info">
<uni-link class="file-name" :text="data.name" showUnderLine="true"
color="#007BFF" :href="data.url"></uni-link>
<view class="chat-file-size">{{ fileSize }}</view>
<view class="file-size">{{ fileSize }}</view>
</view>
<view class="chat-file-icon iconfont icon-file"></view>
<view class="file-icon iconfont icon-file"></view>
<loading v-if="loading"></loading>
</view>
</long-press-menu>
@ -52,7 +52,7 @@
</view>
<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="message-audio message-text" @click="onPlayAudio()">
<text class="iconfont icon-voice-play"></text>
<text class="chat-audio-text">{{ JSON.parse(msgInfo.content).duration + '"' }}</text>
<text v-if="audioPlayState == 'PAUSE'" class="iconfont icon-play"></text>
@ -60,7 +60,7 @@
</view>
</long-press-menu>
<long-press-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">
<view class="chat-realtime chat-msg-text" @click="$emit('call')">
<view class="chat-realtime message-text" @click="$emit('call')">
<text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE"
class="iconfont icon-chat-voice"></text>
<text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VIDEO"
@ -68,7 +68,7 @@
<text>{{ msgInfo.content }}</text>
</view>
</long-press-menu>
<view class="chat-msg-status" v-if="!isAction">
<view class="message-status" v-if="!isAction">
<text class="chat-readed" v-if="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-if="msgInfo.selfSend && !msgInfo.groupId
@ -82,7 +82,6 @@
</view>
</view>
<chat-group-readed ref="chatGroupReaded" :groupMembers="groupMembers" :msgInfo="msgInfo"></chat-group-readed>
</view>
</template>
@ -241,15 +240,14 @@ export default {
return this.$emo.transform(text, 'emoji-normal')
}
}
}
</script>
<style scoped lang="scss">
.chat-msg-item {
.chat-message-item {
padding: 2rpx 20rpx;
.chat-msg-tip {
.message-tip {
line-height: 60rpx;
text-align: center;
color: $im-text-color-lighter;
@ -257,7 +255,7 @@ export default {
padding: 10rpx;
}
.chat-msg-normal {
.message-normal {
position: relative;
margin-bottom: 22rpx;
padding-left: 110rpx;
@ -269,10 +267,10 @@ export default {
left: 0;
}
.chat-msg-content {
.content {
text-align: left;
.chat-msg-top {
.top {
display: flex;
flex-wrap: nowrap;
color: $im-text-color-lighter;
@ -281,12 +279,12 @@ export default {
height: $im-font-size-smaller;
}
.chat-msg-bottom {
.bottom {
display: inline-block;
padding-right: 80rpx;
margin-top: 5rpx;
.chat-msg-text {
.message-text {
position: relative;
line-height: 1.6;
margin-top: 10rpx;
@ -300,7 +298,6 @@ export default {
word-break: break-all;
white-space: pre-line;
&:after {
content: "";
position: absolute;
@ -315,14 +312,13 @@ export default {
}
}
.chat-msg-image {
.message-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.img-load-box {
.image-box {
position: relative;
.send-image {
@ -334,7 +330,6 @@ export default {
}
}
.send-fail {
color: $im-color-danger;
font-size: $im-font-size;
@ -343,14 +338,14 @@ export default {
}
}
.chat-msg-file {
.message-file {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
cursor: pointer;
.chat-file-box {
.file-box {
position: relative;
display: flex;
flex-wrap: nowrap;
@ -360,21 +355,21 @@ export default {
padding: 10px 15px;
box-shadow: $im-box-shadow-dark;
.chat-file-info {
.file-info {
flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
width: 300rpx;
.chat-file-name {
.file-name {
font-weight: 600;
margin-bottom: 15px;
word-break: break-all;
}
}
.chat-file-icon {
.file-icon {
font-size: 80rpx;
color: #d42e07;
}
@ -386,10 +381,9 @@ export default {
cursor: pointer;
margin: 0 20rpx;
}
}
.chat-msg-audio {
.message-audio {
display: flex;
align-items: center;
@ -413,7 +407,7 @@ export default {
}
}
.chat-msg-status {
.message-status {
line-height: $im-font-size-smaller-extra;
font-size: $im-font-size-smaller-extra;
padding-top: 2rpx;
@ -442,8 +436,7 @@ export default {
}
}
&.chat-msg-mine {
&.message-mine {
text-align: right;
padding-left: 0;
padding-right: 110rpx;
@ -453,14 +446,14 @@ export default {
right: 0;
}
.chat-msg-content {
.content {
text-align: right;
.chat-msg-bottom {
.bottom {
padding-left: 80rpx;
padding-right: 0;
.chat-msg-text {
.message-text {
margin-left: 10px;
background-color: $im-color-primary-light-2;
color: #fff;
@ -472,15 +465,15 @@ export default {
}
}
.chat-msg-image {
.message-image {
flex-direction: row-reverse;
}
.chat-msg-file {
.message-file {
flex-direction: row-reverse;
}
.chat-msg-audio {
.message-audio {
flex-direction: row-reverse;
.chat-audio-text {
@ -501,11 +494,9 @@ export default {
transform: rotateY(180deg);
}
}
}
}
}
}
}
</style>

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

@ -7,6 +7,7 @@
<image v-show="friend.onlineWeb" class="online" src="/static/image/online_web.png" title="电脑设备在线" />
<image v-show="friend.onlineApp" class="online" src="/static/image/online_app.png" title="移动设备在线" />
</view>
<slot></slot>
</view>
</view>
</template>
@ -19,14 +20,20 @@ export default {
},
methods: {
showFriendInfo() {
uni.navigateTo({
url: "/pages/common/user-info?id=" + this.friend.id
})
},
if (this.detail) {
uni.navigateTo({
url: "/pages/common/user-info?id=" + this.friend.id
})
}
}
},
props: {
friend: {
type: Object
},
detail: {
type: Boolean,
default: true
}
}
}
@ -51,11 +58,11 @@ export default {
.friend-info {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 20rpx;
text-align: left;
.friend-name {
flex: 1;
font-size: $im-font-size;
white-space: nowrap;
overflow: hidden;
@ -72,4 +79,4 @@ export default {
}
}
}
</style>
</style>

29
im-uniapp/im.scss

@ -102,9 +102,34 @@ button[size='mini'] {
}
}
.uni-radio-input svg{
border-color: white !important;
background-color: $im-color-primary !important;
}
.uni-radio-input svg {
background-color: $im-color-primary !important;
border-color: $im-color-primary !important;
background-clip: content-box !important;
box-sizing: border-box;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.7)!important;
}
.uni-radio-input svg path{
fill: $im-color-primary !important;
}
.uni-radio-input {
//border-color: $im-color-primary !important;
//background-color: $im-color-primary !important;
background-color: white !important;
border-color: $im-color-primary !important;
}
.uni-radio-input-disabled {
background-color: rgb(225, 225, 225) !important;
border-color: rgb(209, 209, 209) !important;
opacity: 0.5;
}
.uni-section__content-title {

16
im-uniapp/pages.json

@ -78,26 +78,26 @@
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/chat/chat",
"iconPath": "static/tarbar/chat.png",
"selectedIconPath": "static/tarbar/chat_active.png",
"iconPath": "static/tab/chat.png",
"selectedIconPath": "static/tab/chat_active.png",
"text": "消息"
},
{
"pagePath": "pages/friend/friend",
"iconPath": "static/tarbar/friend.png",
"selectedIconPath": "static/tarbar/friend_active.png",
"iconPath": "static/tab/friend.png",
"selectedIconPath": "static/tab/friend_active.png",
"text": "好友"
},
{
"pagePath": "pages/group/group",
"iconPath": "static/tarbar/group.png",
"selectedIconPath": "static/tarbar/group_active.png",
"iconPath": "static/tab/group.png",
"selectedIconPath": "static/tab/group_active.png",
"text": "群聊"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tarbar/mine.png",
"selectedIconPath": "static/tarbar/mine_active.png",
"iconPath": "static/tab/mine.png",
"selectedIconPath": "static/tab/mine_active.png",
"text": "我的"
}
]

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

@ -1,8 +1,8 @@
<template>
<view class="page chat-box">
<view class="page chat-box" id="chatBox">
<nav-bar back more @more="onShowMore">{{ title }}</nav-bar>
<view class="chat-main-box" :style="{height: chatMainHeight+'px'}">
<view class="chat-msg" @click="switchChatTabBox('none')">
<view class="chat-message" @click="switchChatTabBox('none')">
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-' + scrollMsgIdx">
<view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
@ -15,9 +15,12 @@
</chat-message-item>
</view>
</scroll-view>
<view v-if="!isInBottom" class="scroll-to-bottom" @click="onClickToBottom">
{{ newMessageSize > 0 ? newMessageSize+'条新消息' :'回到底部'}}
</view>
</view>
<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">
<view class="chat-at-items">
<view v-for="m in atUserItems" class="chat-at-item" :key="m.userId">
@ -140,7 +143,9 @@ export default {
isEmpty: true, //
isFocus: false, //
isReadOnly: false, //
playingAudio: null //
playingAudio: null, //
isInBottom: true, //
newMessageSize: 0 //
}
},
methods: {
@ -561,17 +566,32 @@ export default {
}
});
},
onClickToBottom() {
this.scrollToBottom();
//
// 100s
setTimeout(() => {
this.isInBottom = true;
this.newMessageSize = 0;
}, 100)
},
onScrollToTop() {
if (this.showMinIdx == 0) {
console.log("消息已滚动到顶部")
return;
console.log("onScrollToTop")
if (this.showMinIdx > 0) {
// #ifndef H5
//
this.scrollToMsgIdx(this.showMinIdx);
// #endif
// 20
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
}
// #ifndef H5
//
this.scrollToMsgIdx(this.showMinIdx);
// #endif
// 20
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
//
this.isInBottom = false;
},
onScrollToBottom(e) {
//
this.isInBottom = true;
this.newMessageSize = 0;
},
onShowMore() {
if (this.chat.type == "GROUP") {
@ -639,7 +659,6 @@ export default {
method: 'PUT'
}).then(() => {
this.chatStore.resetUnreadCount(this.chat)
this.scrollToBottom();
})
},
loadGroup(groupId) {
@ -809,7 +828,7 @@ export default {
if (window.visualViewport && uni.getSystemInfoSync().platform == 'ios') {
keyboardHeight = this.initHeight - window.visualViewport.height;
}
console.log("resizeListener:",window.visualViewport.height)
console.log("resizeListener:", window.visualViewport.height)
this.isShowKeyBoard = keyboardHeight > 150;
if (this.isShowKeyBoard) {
this.keyboardHeight = keyboardHeight;
@ -907,12 +926,12 @@ export default {
messageSize: function(newSize, oldSize) {
//
if (newSize > oldSize) {
let pages = getCurrentPages();
let curPage = pages[pages.length - 1].route;
if (curPage == "pages/chat/chat-box") {
if (this.isInBottom) {
// ,
this.scrollToBottom();
} else {
this.needScrollToBottom = true;
//
this.newMessageSize++;
}
}
},
@ -949,12 +968,14 @@ export default {
//
this.$nextTick(() => {
this.windowHeight = uni.getSystemInfoSync().windowHeight;
this.reCalChatMainHeight()
// ios h5:
this.reCalChatMainHeight();
this.scrollToBottom();
// #ifdef H5
this.initHeight = window.innerHeight;
document.body.addEventListener('touchmove', function(e) {
e.preventDefault();
// iosh5:
const chatBox = document.getElementById('chatBox')
chatBox.addEventListener('touchmove', e => {
e.preventDefault()
}, { passive: false });
// #endif
});
@ -1014,7 +1035,7 @@ export default {
flex-direction: column;
z-index: 2;
.chat-msg {
.chat-message {
flex: 1;
padding: 0;
overflow: hidden;
@ -1024,6 +1045,19 @@ export default {
.scroll-box {
height: 100%;
}
.scroll-to-bottom {
position: absolute;
right: 30rpx;
bottom: 30rpx;
font-size: $im-font-size;
color: $im-color-primary;
font-weight: 600;
background: white;
padding: 10rpx 30rpx;
border-radius: 25rpx;
box-shadow: $im-box-shadow-dark;
}
}
.chat-at-bar {

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

@ -1,5 +1,6 @@
<template>
<view class="page group-invite">
<nav-bar back>邀请</nav-bar>
<view class="nav-bar">
<view class="nav-search">
<uni-search-bar v-model="searchText" radius="100" cancelButton="none"
@ -7,20 +8,18 @@
</view>
</view>
<view class="friend-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="friend in friendItems" :key="friend.id">
<view v-show="!searchText || friend.nickName.includes(searchText)" class="friend-item"
@click="onSwitchChecked(friend)"
:class="{ checked: friend.checked, disabled: friend.disabled }">
<head-image :name="friend.nickName" :online="friend.online"
:url="friend.headImage"></head-image>
<view class="friend-name">{{ friend.nickName }}</view>
</view>
</view>
</scroll-view>
<virtual-scroller height="100%" :items="showFriends">
<template v-slot="{ item }">
<friend-item :friend="item" :detail="false" @tap="onSwitchChecked(item)">
<radio @click.stop="onSwitchChecked(item)" :disabled="item.disabled" :checked="item.checked" />
</friend-item>
</template>
</virtual-scroller>
</view>
<view class="btn-bar">
<button class="btn" type="primary" :disabled="inviteSize == 0"
@click="onInviteFriends()">邀请({{ inviteSize }}) </button>
</view>
<button class="bottom-btn" type="primary" :disabled="inviteSize == 0"
@click="onInviteFriends()">邀请({{ inviteSize }}) </button>
</view>
</template>
@ -108,6 +107,9 @@ export default {
computed: {
inviteSize() {
return this.friendItems.filter(f => !f.disabled && f.checked).length;
},
showFriends() {
return this.friendItems.filter(f => f.nickName.includes(this.searchText))
}
},
onLoad(options) {
@ -161,5 +163,14 @@ export default {
height: 100%;
}
}
.btn-bar {
position: fixed;
bottom: 0;
background: $im-bg;
padding: 30rpx;
box-sizing: border-box;
width: 100%;
}
}
</style>

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

@ -4,13 +4,13 @@
<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" maxlength="20"/>
</uni-forms-item>
<uni-forms-item label="新密码" name="newPassword">
<uni-easyinput type="password" v-model="formData.newPassword" />
<uni-easyinput type="password" v-model="formData.newPassword" maxlength="20"/>
</uni-forms-item>
<uni-forms-item label="确认密码" name="confirmPassword">
<uni-easyinput type="password" v-model="formData.confirmPassword" />
<uni-easyinput type="password" v-model="formData.confirmPassword" maxlength="20"/>
</uni-forms-item>
</uni-forms>
</uni-card>

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

@ -4,16 +4,16 @@
<view class="form">
<uni-forms ref="form" :modelValue="dataForm" :rules="rules" validate-trigger="bind" label-width="80px">
<uni-forms-item name="userName" label="用户名">
<uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" />
<uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" maxlength="20"/>
</uni-forms-item>
<uni-forms-item name="nickName" label="昵称">
<uni-easyinput type="text" v-model="dataForm.nickName" placeholder="昵称" />
<uni-easyinput type="text" v-model="dataForm.nickName" placeholder="昵称" maxlength="20"/>
</uni-forms-item>
<uni-forms-item name="password" label="密码">
<uni-easyinput type="password" v-model="dataForm.password" placeholder="密码" />
<uni-easyinput type="password" v-model="dataForm.password" placeholder="密码" maxlength="20"/>
</uni-forms-item>
<uni-forms-item name="corfirmPassword" label="确认密码">
<uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" />
<uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" maxlength="20"/>
</uni-forms-item>
<button class="btn-submit" @click="submit" type="primary">注册并登录</button>
</uni-forms>

0
im-uniapp/static/tarbar/chat.png → im-uniapp/static/tab/chat.png

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
im-uniapp/static/tab/chat_active.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

0
im-uniapp/static/tarbar/friend.png → im-uniapp/static/tab/friend.png

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
im-uniapp/static/tab/friend_active.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

0
im-uniapp/static/tarbar/group.png → im-uniapp/static/tab/group.png

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
im-uniapp/static/tab/group_active.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

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

@ -1,5 +1,5 @@
<template>
<el-scrollbar v-show="show && showMembers.length" ref="scrollBox" class="group-member-choose"
<el-scrollbar v-show="show && showMembers.length" ref="scrollBox" class="chat-at-box"
:style="{ 'left': pos.x + 'px', 'top': pos.y - 300 + 'px' }">
<div v-for="(member, idx) in showMembers" :key="member.id">
<chat-group-member :member="member" :height="40" :active='activeIdx == idx'
@ -123,12 +123,10 @@ export default {
</script>
<style scoped lang="scss">
.group-member-choose {
.chat-at-box {
position: fixed;
width: 200px;
height: 300px;
//border: 1px solid #53a0e79c;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
}

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

@ -3,8 +3,7 @@
<el-container>
<el-header height="50px">
<span>{{ title }}</span>
<span title="群聊信息" v-show="isGroup" class="btn-side el-icon-more"
@click="showSide = !showSide"></span>
<span title="群聊信息" v-show="isGroup" class="btn-side el-icon-more" @click="showSide = !showSide"></span>
</el-header>
<el-main style="padding: 0;">
<el-container>
@ -23,6 +22,9 @@
</ul>
</div>
</el-main>
<div v-if="!isInBottom" class="scroll-to-bottom" @click="scrollToBottom">
{{ newMessageSize > 0 ? newMessageSize + '条新消息' : '回到底部' }}
</div>
<el-footer height="220px" class="im-chat-footer">
<div class="chat-tool-bar">
<div title="表情" class="icon iconfont icon-emoji" ref="emotion"
@ -68,7 +70,7 @@
</div>
</el-footer>
</el-container>
<el-aside class="chat-group-side-box" width="320px" v-if="showSide">
<el-aside class="side-box" width="320px" v-if="showSide">
<chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)">
</chat-group-side>
</el-aside>
@ -130,8 +132,10 @@ export default {
showHistory: false, //
lockMessage: false, //
showMinIdx: 0, // showMinIdx
reqQueue: [],
isSending: false
reqQueue: [], //
isSending: false, //
isInBottom: false, //
newMessageSize: 0 //
}
},
methods: {
@ -277,6 +281,12 @@ export default {
if (scrollTop < 30) { // ,
// 20
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
this.isInBottom = false;
}
//
if (scrollTop + scrollElement.clientHeight >= scrollElement.scrollHeight - 30) {
this.isInBottom = true;
this.newMessageSize = 0;
}
},
showEmotionBox() {
@ -321,7 +331,7 @@ export default {
//
let ids = [this.mine.id];
let maxChannel = this.$store.state.configStore.webrtc.maxChannel;
this.$refs.rtcSel.open(maxChannel, ids, ids,[]);
this.$refs.rtcSel.open(maxChannel, ids, ids, []);
},
onInviteOk(members) {
if (members.length < 2) {
@ -399,9 +409,7 @@ export default {
return;
}
let sendText = this.isReceipt ? "【回执消息】" : "";
let promiseList = [];
for (let i = 0; i < fullList.length; i++) {
let msg = fullList[i];
fullList.forEach(async msg => {
switch (msg.type) {
case "text":
await this.sendTextMessage(sendText + msg.content, msg.atUserIds);
@ -413,8 +421,7 @@ export default {
await this.sendFileMessage(msg.content.file);
break;
}
}
})
},
sendImageMessage(file) {
return new Promise((resolve, reject) => {
@ -702,6 +709,9 @@ export default {
handler(newChat, oldChat) {
if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
newChat.targetId != oldChat.targetId)) {
this.userInfo = {}
this.group = {};
this.groupMembers = [];
if (this.chat.type == "GROUP") {
this.loadGroup(this.chat.targetId);
} else {
@ -730,8 +740,13 @@ export default {
messageSize: {
handler(newSize, oldSize) {
if (newSize > oldSize) {
//
this.scrollToBottom();
if (this.isInBottom) {
//
this.scrollToBottom();
} else {
//
this.newMessageSize++;
}
}
}
}
@ -743,7 +758,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-box {
position: relative;
width: 100%;
@ -768,79 +783,98 @@ export default {
}
}
.im-chat-main {
padding: 0;
background-color: #fff;
.content-box {
position: relative;
.im-chat-main {
padding: 0;
background-color: #fff;
.im-chat-box {
>ul {
padding: 0 20px;
.im-chat-box {
>ul {
padding: 0 20px;
li {
list-style-type: none;
li {
list-style-type: none;
}
}
}
}
}
.im-chat-footer {
display: flex;
flex-direction: column;
padding: 0;
.scroll-to-bottom {
text-align: right;
position: absolute;
right: 20px;
bottom: 230px;
color: var(--im-color-primary);
font-size: var(--im-font-size);
font-weight: 600;
background: #eee;
padding: 5px 15px;
border-radius: 15px;
cursor: pointer;
z-index: 99;
box-shadow: var(--im-box-shadow-light);
}
.chat-tool-bar {
.im-chat-footer {
display: flex;
position: relative;
width: 100%;
height: 36px;
text-align: left;
box-sizing: border-box;
border-top: var(--im-border);
padding: 4px 2px 2px 8px;
flex-direction: column;
padding: 0;
.chat-tool-bar {
display: flex;
position: relative;
width: 100%;
height: 36px;
text-align: left;
box-sizing: border-box;
border-top: var(--im-border);
padding: 4px 2px 2px 8px;
>div {
font-size: 22px;
cursor: pointer;
line-height: 30px;
width: 30px;
height: 30px;
text-align: center;
border-radius: 2px;
margin-right: 8px;
color: #999;
transition: 0.3s;
>div {
font-size: 22px;
cursor: pointer;
line-height: 30px;
width: 30px;
height: 30px;
text-align: center;
border-radius: 2px;
margin-right: 8px;
color: #999;
transition: 0.3s;
&.chat-tool-active {
font-weight: 600;
color: var(--im-color-primary);
background-color: #ddd;
&.chat-tool-active {
font-weight: 600;
color: var(--im-color-primary);
background-color: #ddd;
}
}
}
>div:hover {
color: #333;
>div:hover {
color: #333;
}
}
}
.send-content-area {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
background-color: white !important;
.send-content-area {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
background-color: white !important;
.send-btn-area {
padding: 10px;
position: absolute;
bottom: 4px;
right: 6px;
.send-btn-area {
padding: 10px;
position: absolute;
bottom: 4px;
right: 6px;
}
}
}
}
.chat-group-side-box {
.side-box {
border-left: var(--im-border);
//animation: rtl-drawer-in .3s 1ms;
}
}

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

@ -3,7 +3,7 @@
<div class="member-avatar">
<head-image :size="headImageSize" :name="member.showNickName" :url="member.headImage"> </head-image>
</div>
<div class="member-name" :style="{ 'line-height': height + 'px' }">
<div class="name" :style="{ 'line-height': height + 'px' }">
<div>{{ member.showNickName }}</div>
</div>
</div>
@ -39,7 +39,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-group-member {
display: flex;
position: relative;
@ -52,7 +52,7 @@ export default {
background: #E1EAF7;
}
.member-name {
.name {
padding-left: 10px;
height: 100%;
text-align: left;

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

@ -122,7 +122,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-group-readed-mask {
position: fixed;
left: 0;

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

@ -1,6 +1,6 @@
<template>
<div class="chat-group-side">
<div v-show="!group.quit" class="group-side-search">
<div v-show="!group.quit" class="search">
<el-input placeholder="搜索群成员" v-model="searchText" size="small">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
@ -29,7 +29,7 @@
</div>
</el-scrollbar>
<el-divider v-if="!group.quit" content-position="center"></el-divider>
<el-form labelPosition="top" class="group-side-form" :model="group" size="small">
<el-form labelPosition="top" class="form" :model="group" size="small">
<el-form-item label="群聊名称">
<el-input v-model="group.name" disabled maxlength="20"></el-input>
</el-form-item>
@ -179,15 +179,14 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-group-side {
position: relative;
.group-side-search {
.search {
padding: 10px;
}
.el-divider--horizontal {
margin: 0;
}
@ -208,7 +207,6 @@ export default {
margin-left: 5px;
}
.member-tools {
display: flex;
flex-direction: column;
@ -243,7 +241,7 @@ export default {
}
}
.group-side-form {
.form {
text-align: left;
padding: 10px;
height: 30%;
@ -272,6 +270,5 @@ export default {
margin-top: 12px;
}
}
}
</style>

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

@ -1,7 +1,7 @@
<template>
<el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="onClose">
<div class="chat-history" v-loading="loading" element-loading-text="拼命加载中">
<el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar">
<el-scrollbar class="scroll-box" ref="scrollbar" id="historyScrollbar">
<ul>
<li v-for="(msgInfo, idx) in messages" :key="idx">
<chat-message-item :mode="2" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
@ -147,12 +147,12 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-history {
display: flex;
height: 100%;
.chat-history-scrollbar {
.scroll-box {
flex: 1;
.el-scrollbar__thumb {

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

@ -1,8 +1,9 @@
<template>
<div class="chat-input-area">
<div :class="['edit-chat-container', isEmpty ? '' : 'not-empty']" contenteditable="true" @paste.prevent="onPaste"
@keydown="onKeydown" @compositionstart="compositionFlag = true" @compositionend="onCompositionEnd"
@input="onEditorInput" @mousedown="onMousedown" ref="content" @blur="onBlur">
<div :class="['edit-chat-container', isEmpty ? '' : 'not-empty']" contenteditable="true"
@paste.prevent="onPaste" @keydown="onKeydown" @compositionstart="compositionFlag = true"
@compositionend="onCompositionEnd" @input="onEditorInput" @mousedown="onMousedown" ref="content"
@blur="onBlur">
</div>
<chat-at-box @select="onAtSelect" :search-text="atSearchText" ref="atBox" :ownerId="ownerId"
:members="groupMembers"></chat-at-box>
@ -45,7 +46,7 @@ export default {
range.deleteContents();
}
//
if (txt && typeof (txt) == 'string') {
if (txt && typeof(txt) == 'string') {
let textNode = document.createTextNode(txt);
range.insertNode(textNode)
range.collapse();
@ -234,7 +235,7 @@ export default {
},
onBlur(e) {
if(!this.atIng){
if (!this.atIng) {
this.updateRange();
}
},
@ -342,6 +343,7 @@ export default {
this.empty();
this.imageList = [];
this.fileList = [];
this.$refs.atBox.close();
},
empty() {
this.$refs.content.innerHTML = "";
@ -366,13 +368,13 @@ export default {
this.updateRange();
},
html2Escape(strHtml) {
return strHtml.replace(/[<>&"]/g, function (c) {
return strHtml.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
}[c];
} [c];
});
},
submit() {
@ -440,7 +442,7 @@ export default {
if (node.dataset.id) {
tempText += node.innerHTML;
atUserIds.push(node.dataset.id)
} else if(node.outerHtml) {
} else if (node.outerHtml) {
tempText += node.outerHtml;
}
}
@ -554,4 +556,4 @@ export default {
}
}
</style>
</style>

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

@ -9,7 +9,7 @@
<div class="chat-name">
<div class="chat-name-text">
<div>{{ chat.showName }}</div>
<el-tag v-if="chat.type == 'GROUP'" size="mini" effect="dark"></el-tag>
<el-tag v-if="chat.type == 'GROUP'" size="mini" ></el-tag>
</div>
<div class="chat-time-text">{{ showTime }}</div>
</div>
@ -53,9 +53,6 @@ export default {
},
active: {
type: Boolean
},
index: {
type: Number
}
},
methods: {
@ -94,7 +91,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-item {
height: 50px;
display: flex;
@ -159,15 +156,9 @@ export default {
.el-tag {
min-width: 22px;
text-align: center;
background-color: #2830d3;
border-radius: 10px;
border: 0;
height: 16px;
line-height: 16px;
font-size: 10px;
margin-left: 2px;
opacity: 0.8;
}
}
@ -194,8 +185,7 @@ export default {
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
.chat-content-text {
flex: 1;
white-space: nowrap;
@ -203,7 +193,6 @@ export default {
text-overflow: ellipsis;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
}

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

@ -1,29 +1,28 @@
<template>
<div class="chat-msg-item">
<div class="chat-msg-tip"
v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
<div class="chat-message-item">
<div class="message-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{ msgInfo.content }}
</div>
<div class="chat-msg-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
<div class="message-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{ $date.toTimeText(msgInfo.sendTime) }}
</div>
<div class="chat-msg-normal" v-else-if="isNormal" :class="{ 'chat-msg-mine': mine }">
<div class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': mine }">
<div class="head-image">
<head-image :name="showName" :size="38" :url="headImage" :id="msgInfo.sendId"></head-image>
</div>
<div class="chat-msg-content">
<div v-show="mode == 1 && msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<div class="content">
<div v-show="mode == 1 && msgInfo.groupId && !msgInfo.selfSend" class="message-top">
<span>{{ showName }}</span>
</div>
<div v-show="mode == 2" class="chat-msg-top">
<div v-show="mode == 2" class="message-top">
<span>{{ showName }}</span>
<span>{{ $date.toTimeText(msgInfo.sendTime) }}</span>
</div>
<div class="chat-msg-bottom" @contextmenu.prevent="showRightMenu($event)">
<div class="message-bottom" @contextmenu.prevent="showRightMenu($event)">
<div ref="chatMsgBox">
<span class="chat-msg-text" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT"
<span class="message-text" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT"
v-html="htmlText"></span>
<div class="chat-msg-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<div class="message-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<div class="img-load-box" v-loading="loading" element-loading-text="上传中.."
element-loading-background="rgba(0, 0, 0, 0.4)">
<img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl"
@ -32,7 +31,7 @@
<span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span>
</div>
<div class="chat-msg-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<div class="message-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<div class="chat-file-box" v-loading="loading">
<div class="chat-file-info">
<el-link class="chat-file-name" :underline="true" target="_blank" type="primary"
@ -47,17 +46,17 @@
class="send-fail el-icon-warning"></span>
</div>
</div>
<div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<div class="message-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div>
<div class="chat-action chat-msg-text" v-if="isAction">
<div class="chat-action message-text" v-if="isAction">
<span v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE" title="重新呼叫"
@click="$emit('call')" class="iconfont icon-chat-voice"></span>
<span v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VIDEO" title="重新呼叫"
@click="$emit('call')" class="iconfont icon-chat-video"></span>
<span>{{ msgInfo.content }}</span>
</div>
<div class="chat-msg-status" v-if="!isAction">
<div class="message-status" v-if="!isAction">
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
@ -195,22 +194,22 @@ export default {
htmlText() {
let color = this.msgInfo.selfSend ? 'white' : '';
let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color)
return this.$emo.transform(text,'emoji-normal')
return this.$emo.transform(text, 'emoji-normal')
}
}
}
</script>
<style lang="scss">
.chat-msg-item {
<style lang="scss" scoped>
.chat-message-item {
.chat-msg-tip {
.message-tip {
line-height: 50px;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
.chat-msg-normal {
.message-normal {
position: relative;
font-size: 0;
padding-left: 48px;
@ -225,7 +224,7 @@ export default {
left: 0;
}
.chat-msg-content {
.content {
text-align: left;
.send-fail {
@ -235,7 +234,7 @@ export default {
margin: 0 20px;
}
.chat-msg-top {
.message-top {
display: flex;
flex-wrap: nowrap;
color: var(--im-text-color-light);
@ -247,13 +246,13 @@ export default {
}
}
.chat-msg-bottom {
.message-bottom {
display: inline-block;
padding-right: 300px;
padding-left: 5px;
.chat-msg-text {
display: block;
.message-text {
display: inline-block;
position: relative;
line-height: 26px;
//margin-top: 3px;
@ -279,7 +278,7 @@ export default {
}
}
.chat-msg-image {
.message-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
@ -296,7 +295,7 @@ export default {
}
.chat-msg-file {
.message-file {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
@ -351,7 +350,7 @@ export default {
}
.chat-msg-voice {
.message-voice {
font-size: 14px;
cursor: pointer;
@ -372,7 +371,7 @@ export default {
}
}
.chat-msg-status {
.message-status {
display: block;
.chat-readed {
@ -406,7 +405,7 @@ export default {
}
&.chat-msg-mine {
&.message-mine {
text-align: right;
padding-left: 0;
padding-right: 48px;
@ -416,10 +415,10 @@ export default {
right: 0;
}
.chat-msg-content {
.content {
text-align: right;
.chat-msg-top {
.message-top {
flex-direction: row-reverse;
span {
@ -428,11 +427,11 @@ export default {
}
}
.chat-msg-bottom {
.message-bottom {
padding-left: 180px;
padding-right: 5px;
.chat-msg-text {
.message-text {
margin-left: 10px;
background-color: var(--im-color-primary-light-2);
color: #fff;
@ -444,11 +443,11 @@ export default {
}
}
.chat-msg-image {
.message-image {
flex-direction: row-reverse;
}
.chat-msg-file {
.message-file {
flex-direction: row-reverse;
}

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

@ -1,5 +1,5 @@
<template>
<el-dialog class="chat-record" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose">
<el-dialog v-dialogDrag class="chat-record" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose">
<div v-show="mode == 'RECORD'">
<div class="tip">{{ stateTip }}</div>
<div>时长: {{ state == 'STOP' ? 0 : parseInt(rc.duration) }}s</div>
@ -120,11 +120,10 @@ export default {
})
}
}
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-record {
.tip {

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

@ -2,7 +2,7 @@
<div v-show="show" @click="close()">
<div class="emotion-box" :style="{ 'left': x + 'px', 'top': y + 'px' }">
<el-scrollbar style="height: 220px">
<div class="emotion-item-list">
<div class="emotion-items">
<div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i"
@click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText,'emoji-large')">
</div>
@ -53,11 +53,10 @@ export default {
width: 372px;
box-sizing: border-box;
padding: 5px;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
.emotion-item-list {
.emotion-items {
display: flex;
flex-wrap: wrap;
@ -68,18 +67,5 @@ export default {
}
}
//&:after {
// content: "";
// position: absolute;
// left: 185px;
// bottom: -30px;
// width: 0;
// height: 0;
// border-style: solid dashed dashed;
// border-color: #f5f5f5 transparent transparent;
// overflow: hidden;
// border-width: 15px;
//}
}
</style>

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

@ -32,7 +32,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.full-image {
position: fixed;
width: 100%;

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

@ -100,7 +100,6 @@ export default {
<style scoped lang="scss">
.head-image {
position: relative;
//cursor: pointer;
.avatar-image {
position: relative;

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

@ -8,7 +8,7 @@
<script>
</script>
<style lang="scss">
<style lang="scss" scoped>
.icp {
position: fixed;
text-align: center;

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

@ -41,7 +41,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.right-menu-mask {
position: fixed;
left: 0;

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

@ -20,7 +20,7 @@
</div>
</div>
<el-divider content-position="center"></el-divider>
<div class="user-btn-group">
<div class="btn-group">
<el-button v-if="isFriend" type="primary" @click="onSendMessage()">发消息</el-button>
<el-button v-else type="primary" @click="onAddFriend()">加为好友</el-button>
</div>
@ -99,7 +99,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.user-info-mask {
background-color: rgba($color: #f4f4f4, $alpha: 0);
position: fixed;
@ -144,23 +144,16 @@ export default {
font-size: var(--im-font-size);
margin-top: 5px;
word-break: break-all;
}
}
}
.el-divider--horizontal {
margin: 18px 0;
}
.user-btn-group {
.btn-group {
text-align: center;
.wait-text {
font-size: 14px;
color: var(--im-text-color-light);
}
}
}
</style>

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

@ -1,6 +1,6 @@
<template>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="400px" :before-close="onClose"
custom-class="add-friend-dialog">
<el-dialog v-dialogDrag title="添加好友" :visible.sync="dialogVisible" width="400px" :before-close="onClose"
custom-class="add-friend">
<el-input placeholder="输入用户名或昵称按下enter搜索,最多展示20条" class="input-with-select" v-model="searchText" size="small"
@keyup.enter.native="onSearch()">
<i class="el-icon-search el-input__icon" slot="suffix" @click="onSearch()"> </i>
@ -11,7 +11,7 @@
<div class="avatar">
<head-image :name="user.nickName" :url="user.headImage" :online="user.online"></head-image>
</div>
<div class="add-friend-text">
<div class="friend-info">
<div class="nick-name">
<div>{{ user.nickName }}</div>
<div :class="user.online ? 'online-status online' : 'online-status'">{{
@ -93,8 +93,8 @@ export default {
}
</script>
<style lang="scss">
.add-friend-dialog {
<style lang="scss" scoped>
.add-friend {
.item {
height: 65px;
display: flex;
@ -103,7 +103,7 @@ export default {
align-items: center;
padding-right: 25px;
.add-friend-text {
.friend-info {
margin-left: 15px;
flex: 3;
display: flex;

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

@ -1,174 +1,173 @@
<template>
<el-dialog title="邀请好友" :visible.sync="show" width="620px" :before-close="close">
<div class="agm-container">
<div class="agm-l-box">
<div class="search">
<el-input placeholder="搜索好友" v-model="searchText" size="small">
<i class="el-icon-search el-input__icon" slot="suffix"> </i>
</el-input>
</div>
<el-scrollbar style="height:400px;">
<div v-for="friend in friends" :key="friend.id">
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :active="false">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox"
v-model="friend.isCheck" size="medium"></el-checkbox>
</friend-item>
</div>
</el-scrollbar>
</div>
<div class="agm-arrow el-icon-d-arrow-right"></div>
<div class="agm-r-box">
<div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div>
<el-scrollbar style="height:400px;">
<div v-for="friend in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :active="false"
@del="onRemoveFriend(friend)" :menu="false">
</friend-item>
</div>
</el-scrollbar>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="close()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
</el-dialog>
<el-dialog v-dialogDrag title="邀请好友" :visible.sync="show" width="620px" :before-close="close">
<div class="add-group-member">
<div class="left-box">
<div class="search">
<el-input placeholder="搜索好友" v-model="searchText" size="small">
<i class="el-icon-search el-input__icon" slot="suffix"> </i>
</el-input>
</div>
<el-scrollbar style="height:400px;">
<div v-for="friend in friends" :key="friend.id">
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :active="false">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="checkbox"
v-model="friend.isCheck" size="medium"></el-checkbox>
</friend-item>
</div>
</el-scrollbar>
</div>
<div class="arrow el-icon-d-arrow-right"></div>
<div class="right-box">
<div class="tip"> 已勾选{{ checkCount }}位好友</div>
<el-scrollbar style="height:400px;">
<div v-for="friend in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :active="false"
@del="onRemoveFriend(friend)" :menu="false">
</friend-item>
</div>
</el-scrollbar>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="close()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
</el-dialog>
</template>
<script>
import FriendItem from '../friend/FriendItem.vue';
export default {
name: "addGroupMember",
components: {
FriendItem
},
data() {
return {
show: false,
searchText: "",
friends: []
}
},
methods: {
open() {
this.show = true;
this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => {
if (f.deleted) {
return;
}
let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
if (m) {
//
friend.disabled = true;
friend.isCheck = true
} else {
friend.disabled = false;
friend.isCheck = false;
}
this.friends.push(friend);
})
},
close() {
this.show = false;
},
onOk() {
let inviteVO = {
groupId: this.groupId,
friendIds: []
}
this.friends.forEach((f) => {
if (f.isCheck && !f.disabled) {
inviteVO.friendIds.push(f.id);
}
})
if (inviteVO.friendIds.length > 0) {
this.$http({
url: "/group/invite",
method: 'post',
data: inviteVO
}).then(() => {
this.$message.success("邀请成功");
this.$emit("reload");
this.close()
})
}
},
onRemoveFriend(friend) {
friend.isCheck = false;
},
onSwitchCheck(friend) {
if (!friend.disabled) {
friend.isCheck = !friend.isCheck
}
}
},
props: {
groupId: {
type: Number
},
members: {
type: Array
}
},
computed: {
checkCount() {
return this.friends.filter((f) => f.isCheck && !f.disabled).length;
}
}
name: "addGroupMember",
components: {
FriendItem
},
data() {
return {
show: false,
searchText: "",
friends: []
}
},
methods: {
open() {
this.show = true;
this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => {
if (f.deleted) {
return;
}
let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
if (m) {
//
friend.disabled = true;
friend.isCheck = true
} else {
friend.disabled = false;
friend.isCheck = false;
}
this.friends.push(friend);
})
},
close() {
this.show = false;
},
onOk() {
let inviteVO = {
groupId: this.groupId,
friendIds: []
}
this.friends.forEach((f) => {
if (f.isCheck && !f.disabled) {
inviteVO.friendIds.push(f.id);
}
})
if (inviteVO.friendIds.length > 0) {
this.$http({
url: "/group/invite",
method: 'post',
data: inviteVO
}).then(() => {
this.$message.success("邀请成功");
this.$emit("reload");
this.close()
})
}
},
onRemoveFriend(friend) {
friend.isCheck = false;
},
onSwitchCheck(friend) {
if (!friend.disabled) {
friend.isCheck = !friend.isCheck
}
}
},
props: {
groupId: {
type: Number
},
members: {
type: Array
}
},
computed: {
checkCount() {
return this.friends.filter((f) => f.isCheck && !f.disabled).length;
}
}
}
</script>
<style lang="scss">
.agm-container {
display: flex;
<style lang="scss" scoped>
.add-group-member {
display: flex;
.agm-l-box {
flex: 1;
overflow: hidden;
border: var(--im-border);
.left-box {
flex: 1;
overflow: hidden;
border: var(--im-border);
.search {
height: 40px;
display: flex;
align-items: center;
.search {
height: 40px;
display: flex;
align-items: center;
.el-input__inner {
border: unset;
border-bottom: var(--im-border);
}
.el-input__inner {
border: unset;
border-bottom: var(--im-border);
}
}
}
.checkbox {
margin-right: 20px;
}
}
.agm-friend-checkbox {
margin-right: 20px;
}
}
.arrow {
display: flex;
align-items: center;
font-size: 18px;
padding: 10px;
font-weight: 600;
color: var(--im-color-primary);
}
.agm-arrow {
display: flex;
align-items: center;
font-size: 18px;
padding: 10px;
font-weight: 600;
color: var(--im-color-primary);
}
.right-box {
flex: 1;
border: var(--im-border);
.agm-r-box {
flex: 1;
border: var(--im-border);
.agm-select-tip {
text-align: left;
height: 40px;
line-height: 40px;
text-indent: 6px;
color: var(--im-text-color-light)
}
}
.tip {
text-align: left;
height: 40px;
line-height: 40px;
text-indent: 10px;
color: var(--im-text-color-light)
}
}
}
</style>
</style>

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

@ -31,7 +31,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.group-item {
height: 50px;
display: flex;

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

@ -26,7 +26,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.group-member {
display: flex;
flex-direction: column;

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

@ -37,7 +37,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.group-member-item {
display: flex;
position: relative;

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

@ -1,5 +1,5 @@
<template>
<el-dialog :title="title" :visible.sync="isShow" width="700px">
<el-dialog v-dialogDrag :title="title" :visible.sync="isShow" width="700px">
<div class="group-member-selector">
<div class="left-box">
<el-input placeholder="搜索" v-model="searchText">
@ -19,7 +19,7 @@
<div class="right-box">
<div class="select-tip"> 已勾选{{ checkedMembers.length }}位成员</div>
<el-scrollbar class="scroll-box">
<div class="checked-member-list">
<div class="member-items">
<div v-for="m in members" :key="m.userId">
<group-member class="member-item" v-if="m.checked" :member="m"></group-member>
</div>
@ -123,7 +123,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.group-member-selector {
display: flex;
@ -163,7 +163,7 @@ export default {
color: var(--im-text-color-light)
}
.checked-member-list {
.member-items {
padding: 10px;
display: flex;
flex-direction: row;

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

@ -1,5 +1,5 @@
<template>
<el-dialog title="是否加入通话?" :visible.sync="isShow" width="400px">
<el-dialog v-dialogDrag title="是否加入通话?" :visible.sync="isShow" width="400px">
<div class="rtc-group-join">
<div class="host-info">
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>

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

@ -37,7 +37,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.rtc-group-video {
height: 300px;
background-color: #E8F2FF;

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

@ -5,7 +5,7 @@
<div class="acceptor-text">
{{ tip }}
</div>
<div class="acceptor-btn-group">
<div class="btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="$emit('accept')" title="接受"></div>
<div class="icon iconfont icon-phone-reject reject" @click="$emit('reject')" title="拒绝"></div>
</div>
@ -61,7 +61,7 @@ export default {
font-size: 16px;
}
.acceptor-btn-group {
.btn-group {
display: flex;
justify-content: space-around;
margin-top: 20px;

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

@ -420,7 +420,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.rtc-private-video {
position: relative;

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

@ -1,5 +1,5 @@
<template>
<el-dialog class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose">
<el-dialog v-dialogDrag class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose">
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small">
<el-form-item label="头像" style="margin-bottom: 0 !important;">
<file-upload class="avatar-uploader" :action="imageAction" :showLoading="true" :maxSize="maxSize"
@ -13,7 +13,7 @@
<el-input disabled v-model="userInfo.userName" autocomplete="off" size="small"></el-input>
</el-form-item>
<el-form-item prop="nickName" label="昵称">
<el-input v-model="userInfo.nickName" autocomplete="off" size="small"></el-input>
<el-input v-model="userInfo.nickName" autocomplete="off" size="small" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="userInfo.sex">
@ -101,7 +101,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.setting {
.el-form {
padding: 10px 0 0 10px;

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

@ -1,23 +1,23 @@
<template>
<el-container class="chat-page">
<el-aside width="260px" class="chat-list-box">
<div class="chat-list-header">
<el-aside width="260px" class="aside">
<div class="header">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
</div>
<div class="chat-list-loading" v-if="loading" v-loading="true" element-loading-text="消息接收中..."
<div class="chat-loading" v-if="loading" v-loading="true" element-loading-text="消息接收中..."
element-loading-spinner="el-icon-loading" element-loading-background="#F9F9F9" element-loading-size="24">
</div>
<el-scrollbar class="chat-list-items" v-else>
<el-scrollbar class="chat-items" v-else>
<div v-for="(chat, index) in chatStore.chats" :key="index">
<chat-item v-show="!chat.delete && chat.showName && chat.showName.includes(searchText)" :chat="chat" :index="index"
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
<chat-item v-show="!chat.delete && chat.showName && chat.showName.includes(searchText)" :chat="chat"
:index="index" @click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="chat === chatStore.activeChat"></chat-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="chat-box">
<el-container>
<chat-box v-if="chatStore.activeChat" :chat="chatStore.activeChat"></chat-box>
</el-container>
</el-container>
@ -63,21 +63,21 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.chat-page {
.chat-list-box {
.aside {
display: flex;
flex-direction: column;
background: var(--im-background);
.chat-list-header {
.header {
height: 50px;
display: flex;
align-items: center;
padding: 0 8px;
}
.chat-list-loading {
.chat-loading {
height: 50px;
background-color: #eee;
@ -89,13 +89,9 @@ export default {
.el-loading-text {
color: var(--im-text-color-light);
}
.chat-loading-box {
height: 100%;
}
}
.chat-list-items {
.chat-items {
flex: 1;
}
}

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

@ -1,7 +1,7 @@
<template>
<el-container class="friend-page">
<el-aside width="260px" class="friend-list-box">
<div class="friend-list-header">
<el-aside width="260px" class="aside">
<div class="header">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
@ -9,9 +9,9 @@
@click="onShowAddFriend()"></el-button>
<add-friend :dialogVisible="showAddFriend" @close="onCloseAddFriend"></add-friend>
</div>
<el-scrollbar class="friend-list-items">
<el-scrollbar class="friend-items">
<div v-for="(friends, i) in friendValues" :key="i">
<div class="index-title">{{ friendKeys[i] }}</div>
<div class="letter">{{ friendKeys[i] }}</div>
<div v-for="(friend) in friends" :key="friend.id">
<friend-item :friend="friend" :active="friend.id === activeFriend.id"
@chat="onSendMessage(friend)" @delete="onDelFriend(friend)"
@ -22,12 +22,12 @@
</div>
</el-scrollbar>
</el-aside>
<el-container class="friend-box">
<div class="friend-header" v-show="userInfo.id">
<el-container class="container">
<div class="header" v-show="userInfo.id">
{{ userInfo.nickName }}
</div>
<div v-show="userInfo.id">
<div class="friend-detail">
<div class="friend-info">
<head-image :size="160" :name="userInfo.nickName" :url="userInfo.headImage" radius="10%"
@click.native="showFullImage()"></head-image>
<div>
@ -42,7 +42,7 @@
<el-descriptions-item label="签名">{{ userInfo.signature }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="frient-btn-group">
<div class="btn-group">
<el-button v-show="isFriend" icon="el-icon-position" type="primary"
@click="onSendMessage(userInfo)">发消息</el-button>
<el-button v-show="!isFriend" icon="el-icon-plus" type="primary"
@ -223,14 +223,14 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.friend-page {
.friend-list-box {
.aside {
display: flex;
flex-direction: column;
background: var(--im-background);
.friend-list-header {
.header {
height: 50px;
display: flex;
align-items: center;
@ -244,10 +244,10 @@ export default {
}
}
.friend-list-items {
.friend-items {
flex: 1;
.index-title {
.letter {
text-align: left;
font-size: var(--im-larger-size-larger);
padding: 5px 15px;
@ -256,11 +256,11 @@ export default {
}
}
.friend-box {
.container {
display: flex;
flex-direction: column;
.friend-header {
.header {
height: 50px;
display: flex;
justify-content: space-between;
@ -271,7 +271,7 @@ export default {
box-sizing: border-box;
}
.friend-detail {
.friend-info {
display: flex;
padding: 50px 80px 20px 80px;
text-align: center;
@ -287,7 +287,7 @@ export default {
}
}
.frient-btn-group {
.btn-group {
text-align: left !important;
padding: 20px;
}

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

@ -1,15 +1,15 @@
<template>
<el-container class="group-page">
<el-aside width="260px" class="group-list-box">
<div class="group-list-header">
<el-aside width="260px" class="aside">
<div class="header">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button>
</div>
<el-scrollbar class="group-list-items">
<el-scrollbar class="group-items">
<div v-for="(groups, i) in groupValues" :key="i">
<div class="index-title">{{ groupKeys[i] }}</div>
<div class="letter">{{ groupKeys[i] }}</div>
<div v-for="group in groups" :key="group.id">
<group-item :group="group" :active="group.id == activeGroup.id"
@click.native="onActiveItem(group)">
@ -19,80 +19,75 @@
</div>
</el-scrollbar>
</el-aside>
<el-container class="group-box">
<div class="group-header" v-show="activeGroup.id">
{{ activeGroup.showGroupName }}({{ groupMembers.length }})
</div>
<div class="group-container">
<div v-show="activeGroup.id">
<div class="group-info">
<el-container class="container">
<div class="header" v-show="activeGroup.id">{{ activeGroup.showGroupName }}({{ showMembers.length }})</div>
<div class="container-box" v-show="activeGroup.id">
<div class="group-info">
<div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" :showLoading="true"
:maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
<head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage"
:name="activeGroup.showGroupName" radius="10%">
</head-image>
<el-button class="send-btn" icon="el-icon-position" type="primary" @click="onSendMessage()">发消息
</el-button>
</div>
<el-form class="form" label-width="130px" :model="activeGroup" :rules="rules" size="small"
ref="groupForm">
<el-form-item label="群聊名称" prop="name">
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="群主">
<el-input :value="ownerName" disabled></el-input>
</el-form-item>
<el-form-item label="群名备注">
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="activeGroup.remarkNickName" maxlength="20"
:placeholder="$store.state.userStore.userInfo.nickName"></el-input>
</el-form-item>
<el-form-item label="群公告">
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3"
maxlength="1024" placeholder="群主未设置"></el-input>
</el-form-item>
<div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
:showLoading="true" :maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
<head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage"
:name="activeGroup.showGroupName" radius="10%">
</head-image>
<el-button class="send-btn" icon="el-icon-position" type="primary"
@click="onSendMessage()">发消息
</el-button>
<el-button type="warning" @click="onInvite()">邀请</el-button>
<el-button type="success" @click="onSaveGroup()">保存</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button>
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button>
</div>
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small"
ref="groupForm">
<el-form-item label="群聊名称" prop="name">
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="群主">
<el-input :value="ownerName" disabled></el-input>
</el-form-item>
<el-form-item label="群名备注">
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="activeGroup.remarkNickName" maxlength="20"
:placeholder="$store.state.userStore.userInfo.nickName"></el-input>
</el-form-item>
<el-form-item label="群公告">
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3"
maxlength="1024" placeholder="群主未设置"></el-input>
</el-form-item>
<div>
<el-button type="warning" @click="onInvite()">邀请</el-button>
<el-button type="success" @click="onSaveGroup()">保存</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button>
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button>
</div>
</el-form>
</div>
<el-divider content-position="center"></el-divider>
<el-scrollbar ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="group-member-list">
<div class="member-tools">
<div class="tool-btn" title="邀请好友进群聊" @click="onInvite()">
<i class="el-icon-plus"></i>
</div>
<div class="tool-text">邀请</div>
<add-group-member ref="addGroupMember" :groupId="activeGroup.id" :members="groupMembers"
@reload="$emit('reload')"></add-group-member>
</div>
<div class="member-tools" v-if="isOwner">
<div class="tool-btn" title="选择成员移出群聊" @click="onRemove()">
<i class="el-icon-minus"></i>
</div>
<div class="tool-text">移除</div>
<group-member-selector ref="removeSelector" title="选择成员进行移除" :group="activeGroup"
@complete="onRemoveComplete"></group-member-selector>
</el-form>
</div>
<el-divider content-position="center"></el-divider>
<el-scrollbar ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="member-items">
<div class="member-tools">
<div class="tool-btn" title="邀请好友进群聊" @click="onInvite()">
<i class="el-icon-plus"></i>
</div>
<div v-for="(member, idx) in showMembers" :key="member.id">
<group-member v-if="idx < showMaxIdx" class="group-member" :member="member"></group-member>
<div class="tool-text">邀请</div>
<add-group-member ref="addGroupMember" :groupId="activeGroup.id" :members="groupMembers"
@reload="$emit('reload')"></add-group-member>
</div>
<div class="member-tools" v-if="isOwner">
<div class="tool-btn" title="选择成员移出群聊" @click="onRemove()">
<i class="el-icon-minus"></i>
</div>
<div class="tool-text">移除</div>
<group-member-selector ref="removeSelector" title="选择成员进行移除" :group="activeGroup"
@complete="onRemoveComplete"></group-member-selector>
</div>
</el-scrollbar>
</div>
<div v-for="(member, idx) in showMembers" :key="member.id">
<group-member v-if="idx < showMaxIdx" class="member-item" :member="member"></group-member>
</div>
</div>
</el-scrollbar>
</div>
</el-container>
</el-container>
@ -348,14 +343,14 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.group-page {
.group-list-box {
.aside {
display: flex;
flex-direction: column;
background: var(--im-background);
.group-list-header {
.header {
height: 50px;
display: flex;
align-items: center;
@ -369,10 +364,10 @@ export default {
}
}
.group-list-items {
.group-items {
flex: 1;
.index-title {
.letter {
text-align: left;
font-size: var(--im-larger-size-larger);
padding: 5px 15px;
@ -381,11 +376,11 @@ export default {
}
}
.group-box {
.container {
display: flex;
flex-direction: column;
.group-header {
.header {
display: flex;
justify-content: space-between;
padding: 0 12px;
@ -398,7 +393,7 @@ export default {
margin: 16px 0;
}
.group-container {
.container-box {
overflow: auto;
padding: 20px;
flex: 1;
@ -407,7 +402,7 @@ export default {
display: flex;
padding: 5px 20px;
.group-form {
.form {
flex: 1;
padding-left: 40px;
max-width: 700px;
@ -450,14 +445,14 @@ export default {
}
}
.group-member-list {
.member-items {
padding: 0 12px;
display: flex;
align-items: center;
flex-wrap: wrap;
text-align: center;
.group-member {
.member-item {
margin-right: 5px;
}
@ -495,8 +490,6 @@ export default {
}
}
}
}
</style>

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

@ -32,13 +32,13 @@
</div>
<div class="botoom">
<div class="botoom-item" @click="isFullscreen = !isFullscreen">
<div class="bottom-item" @click="isFullscreen = !isFullscreen">
<i class="el-icon-full-screen"></i>
</div>
<div class="botoom-item" @click="showSetting">
<div class="bottom-item" @click="showSetting">
<span class="icon iconfont icon-setting" style="font-size: 20px"></span>
</div>
<div class="botoom-item" @click="onExit()" title="退出">
<div class="bottom-item" @click="onExit()" title="退出">
<span class="icon iconfont icon-exit"></span>
</div>
</div>
@ -440,7 +440,6 @@ export default {
border-radius: 4px;
overflow: hidden;
background: var(--im-color-primary-light-9);
//background-image: url('../assets/image/background.jpg');
.app-container {
width: 62vw;
@ -535,7 +534,7 @@ export default {
}
}
.botoom-item {
.bottom-item {
display: flex;
justify-content: center;
align-items: center;

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

@ -1,9 +1,9 @@
<template>
<div class="login-view">
<div class="login-content">
<el-form class="login-form" :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px"
<div class="content">
<el-form class="form" :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px"
@keyup.enter.native="submitForm('loginForm')">
<div class="login-brand">
<div class="title">
<img class="logo" src="../../public/logo.png" />
<div>登录盒子IM</div>
</div>
@ -126,14 +126,14 @@ export default {
box-sizing: border-box;
.login-content {
.content {
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
padding: 10%;
.login-form {
.form {
height: 340px;
width: 400px;
padding: 30px;
@ -144,7 +144,7 @@ export default {
overflow: hidden;
border: 1px solid #ccc;
.login-brand {
.title {
display: flex;
justify-content: center;
align-items: center;

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

@ -2,26 +2,26 @@
<el-container class="register-view">
<div>
<el-form :model="registerForm" status-icon :rules="rules" ref="registerForm" label-width="80px"
class="web-ruleForm">
<div class="register-brand">
class="content">
<div class="title">
<img class="logo" src="../../public/logo.png" />
<div>欢迎成为盒子IM的用户</div>
</div>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="registerForm.userName" autocomplete="off"
placeholder="用户名(登录使用)"></el-input>
<el-input type="userName" v-model="registerForm.userName" autocomplete="off" placeholder="用户名(登录使用)"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickName">
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off"
placeholder="昵称"></el-input>
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off" placeholder="昵称"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password" autocomplete="off"
placeholder="密码"></el-input>
<el-input type="password" v-model="registerForm.password" autocomplete="off" placeholder="密码"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off"
placeholder="确认密码"></el-input>
placeholder="确认密码" maxlength="20"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
@ -133,7 +133,7 @@ export default {
height: 100%;
background: rgb(232, 242, 255);
.web-ruleForm {
.content {
width: 500px;
height: 450px;
padding: 20px;
@ -144,7 +144,7 @@ export default {
overflow: hidden;
border-radius: 3%;
.register-brand {
.title {
display: flex;
justify-content: center;
align-items: center;

Loading…
Cancel
Save