Browse Source

群聊优化,群人员限制放宽至1万人

master
xsx 1 year ago
parent
commit
c421a5dbdd
  1. 8
      im-platform/src/main/java/com/bx/implatform/contant/Constant.java
  2. 5
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  3. 33
      im-uniapp/components/chat-at-box/chat-at-box.vue
  4. 24
      im-uniapp/components/chat-group-readed/chat-group-readed.vue
  5. 34
      im-uniapp/components/group-member-selector/group-member-selector.vue
  6. 56
      im-uniapp/components/virtual-scroller/virtual-scroller.vue
  7. 5
      im-uniapp/pages/chat/chat-box.vue
  8. 35
      im-uniapp/pages/chat/chat.vue
  9. 20
      im-uniapp/pages/common/external-link.vue
  10. 35
      im-uniapp/pages/group/group-member.vue
  11. 1
      im-uniapp/store/chatStore.js
  12. 6
      im-web/src/components/chat/ChatAtBox.vue
  13. 18
      im-web/src/components/chat/ChatBox.vue
  14. 318
      im-web/src/components/chat/ChatGroupReaded.vue
  15. 50
      im-web/src/components/chat/ChatGroupSide.vue
  16. 74
      im-web/src/components/common/VirtualScroller.vue
  17. 27
      im-web/src/components/group/GroupMemberSelector.vue
  18. 48
      im-web/src/view/Group.vue

8
im-platform/src/main/java/com/bx/implatform/contant/Constant.java

@ -14,9 +14,15 @@ public final class Constant {
* 最大上传文件大小 * 最大上传文件大小
*/ */
public static final Long MAX_FILE_SIZE = 20 * 1024 * 1024L; public static final Long MAX_FILE_SIZE = 20 * 1024 * 1024L;
/** /**
* 群聊最大人数 * 群聊最大人数
*/ */
public static final Long MAX_GROUP_MEMBER = 500L; public static final Long MAX_GROUP_MEMBER = 10000L;
/**
* 回执消息限制最大人数
*/
public static final Long LARGE_GROUP_MEMBER = 500L;
} }

5
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java

@ -14,6 +14,7 @@ import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMGroupMessage; import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.util.CommaTextUtils; import com.bx.imcommon.util.CommaTextUtils;
import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.dto.GroupMessageDTO; import com.bx.implatform.dto.GroupMessageDTO;
import com.bx.implatform.entity.Group; import com.bx.implatform.entity.Group;
@ -64,6 +65,10 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
} }
// 群聊成员列表 // 群聊成员列表
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
if (dto.getReceipt() && userIds.size() > Constant.LARGE_GROUP_MEMBER) {
// 大群的回执消息过于消耗资源,不允许发送
throw new GlobalException(String.format("当前群聊大于%s人,不支持发送回执消息", Constant.LARGE_GROUP_MEMBER));
}
// 不用发给自己 // 不用发给自己
userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList()); userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList());
// 保存消息 // 保存消息

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

@ -9,7 +9,7 @@
</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" :key="m.userId"> <view v-for="m in checkedMembers" class="at-user-item" :key="m.userId">
<head-image :name="m.showNickName" :url="m.headImage" size="mini"></head-image> <head-image :name="m.showNickName" :url="m.headImage" size="mini"></head-image>
</view> </view>
</view> </view>
@ -18,18 +18,15 @@
<uni-search-bar v-model="searchText" cancelButton="none" radius="100" placeholder="搜索"></uni-search-bar> <uni-search-bar v-model="searchText" cancelButton="none" radius="100" placeholder="搜索"></uni-search-bar>
</view> </view>
<view class="member-items"> <view class="member-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <virtual-scroller class="scroll-bar" :items="memberItems">
<view v-for="m in showMembers" v-show="m.showNickName.includes(searchText)" :key="m.userId"> <template v-slot="{ item }">
<view class="member-item" :class="{ checked: m.checked }" @click="onSwitchChecked(m)"> <view class="member-item" :class="{ checked: item.checked }" @click="onSwitchChecked(item)">
<head-image :name="m.showNickName" :online="m.online" :url="m.headImage" <head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
size="small"></head-image> size="small"></head-image>
<view class="member-name">{{ m.showNickName }}</view> <view class="member-name">{{ item.showNickName }}</view>
<!-- <view class="member-checked">-->
<!-- <radio :checked="m.checked" @click.stop="onSwitchChecked(m)" />-->
<!-- </view>-->
</view> </view>
</view> </template>
</scroll-view> </virtual-scroller>
</view> </view>
</view> </view>
</uni-popup> </uni-popup>
@ -91,13 +88,13 @@ export default {
}, },
computed: { computed: {
atUserIds() { atUserIds() {
let ids = []; return this.showMembers.filter(m => m.checked).map(m => m.userId);
this.showMembers.forEach((m) => { },
if (m.checked) { checkedMembers() {
ids.push(m.userId); return this.showMembers.filter(m => m.checked);
} },
}) memberItems() {
return ids; return this.showMembers.filter(m => m.showNickName.includes(this.searchText));
} }
} }
} }

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

@ -7,26 +7,26 @@
</view> </view>
<view class="content"> <view class="content">
<view v-if="current === 0"> <view v-if="current === 0">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <virtual-scroller class="scroll-bar" :items="readedMembers">
<view v-for="m in readedMembers" :key="m.userId"> <template v-slot="{ item }">
<view class="member-item"> <view class="member-item">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage" <head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
:size="90"></head-image> :size="90"></head-image>
<view class="member-name">{{ m.aliasName }}</view> <view class="member-name">{{ item.showNickName }}</view>
</view> </view>
</view> </template>
</scroll-view> </virtual-scroller>
</view> </view>
<view v-if="current === 1"> <view v-if="current === 1">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <virtual-scroller class="scroll-bar" :items="unreadMembers">
<view v-for="m in unreadMembers" :key="m.userId"> <template v-slot="{ item }">
<view class="member-item"> <view class="member-item">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage" <head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
:size="90"></head-image> :size="90"></head-image>
<view class="member-name">{{ m.aliasName }}</view> <view class="member-name">{{ item.showNickName }}</view>
</view> </view>
</view> </template>
</scroll-view> </virtual-scroller>
</view> </view>
</view> </view>
</view> </view>

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

@ -9,7 +9,7 @@
</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" :key="m.userId"> <view v-for="m in checkedMembers" 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>
@ -18,18 +18,20 @@
<uni-search-bar v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar> <uni-search-bar v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar>
</view> </view>
<view class="member-items"> <view class="member-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <virtual-scroller class="scroll-bar" :items="showMembers">
<view v-for="m in members" v-show="!m.quit && m.showNickName.includes(searchText)" :key="m.userId"> <template v-slot="{ item }">
<view class="member-item" @click="onSwitchChecked(m)"> <view class="member-item" @click="onSwitchChecked(item)">
<head-image :name="m.showNickName" :online="m.online" :url="m.headImage" <head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
:size="90"></head-image> :size="90"></head-image>
<view class="member-name">{{ m.showNickName }}</view> <view class="member-name">{{ item.showNickName }}
</view>
<view class="member-checked"> <view class="member-checked">
<radio :checked="m.checked" :disabled="m.locked" @click.stop="onSwitchChecked(m)" /> <radio :checked="item.checked" :disabled="item.locked"
@click.stop="onSwitchChecked(item)" />
</view> </view>
</view> </view>
</view> </template>
</scroll-view> </virtual-scroller>
</view> </view>
</view> </view>
</uni-popup> </uni-popup>
@ -93,13 +95,13 @@ export default {
}, },
computed: { computed: {
checkedIds() { checkedIds() {
let ids = []; return this.members.filter((m) => m.checked).map(m => m.userId)
this.members.forEach((m) => { },
if (m.checked) { checkedMembers() {
ids.push(m.userId); return this.members.filter((m) => m.checked);
} },
}) showMembers() {
return ids; return this.members.filter(m => !m.quit && m.showNickName.includes(this.searchText))
} }
} }
} }

56
im-uniapp/components/virtual-scroller/virtual-scroller.vue

@ -0,0 +1,56 @@
<template>
<scroll-view scroll-y="true" upper-threshold="200" @scrolltolower="onScrollToBottom" scroll-with-animation="true">
<view v-for="(item, idx) in showItems" :key="idx">
<slot :item="item">
</slot>
</view>
</scroll-view>
</template>
<script>
export default {
name: "virtual-scroller",
data() {
return {
page: 1,
isInitEvent: false,
lockTip: false
}
},
props: {
items: {
type: Array
},
size: {
type: Number,
default: 30
}
},
methods: {
onScrollToBottom(e) {
console.log("onScrollToBottom")
if (this.showMaxIdx >= this.items.length) {
this.showTip();
} else {
this.page++;
}
},
showTip() {
uni.showToast({
title: "已滚动至底部",
icon: 'none'
});
}
},
computed: {
showMaxIdx() {
return Math.min(this.page * this.size, this.items.length);
},
showItems() {
return this.items.slice(0, this.showMaxIdx);
}
}
}
</script>
<style scoped></style>

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

@ -71,7 +71,7 @@
<view class="tool-icon iconfont icon-microphone"></view> <view class="tool-icon iconfont icon-microphone"></view>
<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' && memberSize<=500" 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>
@ -886,6 +886,9 @@ export default {
} }
}) })
return atUsers; return atUsers;
},
memberSize() {
return this.groupMembers.filter(m => !m.quit).length;
} }
}, },
watch: { watch: {

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

@ -31,7 +31,6 @@
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
@ -43,16 +42,16 @@ export default {
chatIdx: -1, chatIdx: -1,
isTouchMove: false, isTouchMove: false,
items: [{ items: [{
key: 'DELETE', key: 'DELETE',
name: '删除该聊天', name: '删除该聊天',
icon: 'trash', icon: 'trash',
color: '#e64e4e' color: '#e64e4e'
}, },
{ {
key: 'TOP', key: 'TOP',
name: '置顶该聊天', name: '置顶该聊天',
icon: 'arrow-up' icon: 'arrow-up'
} }
] ]
} }
} }
@ -77,12 +76,6 @@ export default {
moveToTop(chatIdx) { moveToTop(chatIdx) {
this.chatStore.moveTop(chatIdx); this.chatStore.moveTop(chatIdx);
}, },
isShowChat(chat) {
if (chat.delete) {
return false;
}
return !this.searchText || chat.showName.includes(this.searchText)
},
onSearch() { onSearch() {
this.showSearch = !this.showSearch; this.showSearch = !this.showSearch;
this.searchText = ""; this.searchText = "";
@ -114,8 +107,12 @@ export default {
loading() { loading() {
return this.chatStore.isLoading(); return this.chatStore.isLoading();
}, },
initializing(){ initializing() {
return !getApp().$vm.isInit; return !getApp().$vm.isInit;
},
showChats() {
this.chatStore.chats.filter((chat) => !chat.delete && chat.showName && chat.showName.includes(this
.searchText))
} }
}, },
watch: { watch: {
@ -129,7 +126,7 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.tab-page { .tab-page {
position: relative; position: relative;
display: flex; display: flex;

20
im-uniapp/pages/common/external-link.vue

@ -0,0 +1,20 @@
<template>
<view>
<web-view :src="linkUrl"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
linkUrl: ''
};
},
onLoad(options) {
this.linkUrl = decodeURIComponent(options.url);
}
}
</script>
<style></style>

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

@ -8,24 +8,22 @@
</view> </view>
</view> </view>
<view class="member-items"> <view class="member-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <virtual-scroller class="scroll-bar" :items="showMembers">
<view v-for="(member, idx) in groupMembers" <template v-slot="{ item }">
v-show="!searchText || member.showNickName.includes(searchText)" :key="idx"> <view class="member-item" @click="onShowUserInfo(item.userId)">
<view class="member-item" @click="onShowUserInfo(member.userId)"> <head-image :name="item.showNickName" :online="item.online" :url="item.headImage"></head-image>
<head-image :name="member.showNickName" :online="member.online" <view class="member-name">{{ item.showNickName }}
:url="member.headImage"></head-image> <uni-tag v-if="item.userId == group.ownerId" text="群主" size="small" circle type="error">
<view class="member-name">{{ member.showNickName }}
<uni-tag v-if="member.userId == group.ownerId" text="群主" size="small" circle type="error">
</uni-tag> </uni-tag>
<uni-tag v-if="member.userId == userStore.userInfo.id" text="我" size="small" circle></uni-tag> <uni-tag v-if="item.userId == userStore.userInfo.id" 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(item.userId)" size="mini"
@click.stop="onKickOut(member, idx)">移出群聊</button> @click.stop="onKickOut(item)">移出群聊</button>
</view> </view>
</view> </view>
</view> </template>
</scroll-view> </virtual-scroller>
</view> </view>
</view> </view>
</template> </template>
@ -37,7 +35,7 @@ export default {
isModify: false, isModify: false,
searchText: "", searchText: "",
group: {}, group: {},
groupMembers: [] members: []
} }
}, },
methods: { methods: {
@ -46,7 +44,7 @@ export default {
url: "/pages/common/user-info?id=" + userId url: "/pages/common/user-info?id=" + userId
}) })
}, },
onKickOut(member, idx) { onKickOut(member) {
uni.showModal({ uni.showModal({
title: '确认移出?', title: '确认移出?',
content: `确定将成员'${member.showNickName}'移出群聊吗?`, content: `确定将成员'${member.showNickName}'移出群聊吗?`,
@ -61,7 +59,7 @@ export default {
title: `已将${member.showNickName}移出群聊`, title: `已将${member.showNickName}移出群聊`,
icon: 'none' icon: 'none'
}) })
this.groupMembers.splice(idx, 1); member.quit = true;
this.isModify = true; this.isModify = true;
}); });
} }
@ -80,7 +78,7 @@ export default {
url: `/group/members/${id}`, url: `/group/members/${id}`,
method: "GET" method: "GET"
}).then((members) => { }).then((members) => {
this.groupMembers = members.filter(m => !m.quit); this.members = members;
}) })
}, },
isSelf(userId) { isSelf(userId) {
@ -90,6 +88,9 @@ export default {
computed: { computed: {
isOwner() { isOwner() {
return this.userStore.userInfo.id == this.group.ownerId; return this.userStore.userInfo.id == this.group.ownerId;
},
showMembers() {
return this.members.filter(m => !m.quit && m.showNickName.includes(this.searchText))
} }
}, },
onLoad(options) { onLoad(options) {

1
im-uniapp/store/chatStore.js

@ -1,6 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js'; import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js';
import useUserStore from './userStore'; import useUserStore from './userStore';
import UNI_APP from '../.env';
let cacheChats = []; let cacheChats = [];
export default defineStore('chatStore', { export default defineStore('chatStore', {

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

@ -51,6 +51,10 @@ export default {
}) })
} }
this.members.forEach((m) => { this.members.forEach((m) => {
// 100
if (this.showMembers.length > 100) {
return;
}
if (m.userId != userId && !m.quit && m.showNickName.startsWith(this.searchText)) { if (m.userId != userId && !m.quit && m.showNickName.startsWith(this.searchText)) {
this.showMembers.push(m); this.showMembers.push(m);
} }
@ -128,4 +132,4 @@ export default {
background-color: #fff; background-color: #fff;
box-shadow: var(--im-box-shadow); box-shadow: var(--im-box-shadow);
} }
</style> </style>

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

@ -41,8 +41,9 @@
<i class="el-icon-wallet"></i> <i class="el-icon-wallet"></i>
</file-upload> </file-upload>
</div> </div>
<div title="回执消息" v-show="chat.type == 'GROUP'" class="icon iconfont icon-receipt" <div title="回执消息" v-show="chat.type == 'GROUP' && memberSize <= 500"
:class="isReceipt ? 'chat-tool-active' : ''" @click="onSwitchReceipt"> class="icon iconfont icon-receipt" :class="isReceipt ? 'chat-tool-active' : ''"
@click="onSwitchReceipt">
</div> </div>
<div title="发送语音" class="el-icon-microphone" @click="showRecordBox()"> <div title="发送语音" class="el-icon-microphone" @click="showRecordBox()">
</div> </div>
@ -67,7 +68,7 @@
</div> </div>
</el-footer> </el-footer>
</el-container> </el-container>
<el-aside class="chat-group-side-box" width="260px" v-if="showSide"> <el-aside class="chat-group-side-box" width="320px" 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>
@ -510,7 +511,7 @@ export default {
this.$http({ this.$http({
url: url, url: url,
method: 'put' method: 'put'
}).then(() => {}) }).then(() => { })
}, },
loadReaded(fId) { loadReaded(fId) {
this.$http({ this.$http({
@ -549,7 +550,7 @@ export default {
friend.showNickName = friend.remarkNickName ? friend.remarkNickName : friend.nickName; friend.showNickName = friend.remarkNickName ? friend.remarkNickName : friend.nickName;
this.$store.commit("updateChatFromFriend", friend); this.$store.commit("updateChatFromFriend", friend);
this.$store.commit("updateFriend", friend); this.$store.commit("updateFriend", friend);
}else { } else {
this.$store.commit("updateChatFromUser", this.userInfo); this.$store.commit("updateChatFromUser", this.userInfo);
} }
}, },
@ -656,7 +657,7 @@ export default {
return this.$store.getters.isFriend(this.userInfo.id); return this.$store.getters.isFriend(this.userInfo.id);
}, },
friend() { friend() {
return this.$store.getters.findFriend(this.userInfo.id) return this.$store.getters.findFriend(this.userInfo.id)
}, },
title() { title() {
let title = this.chat.showName; let title = this.chat.showName;
@ -681,13 +682,16 @@ export default {
isBanned() { isBanned() {
return (this.chat.type == "PRIVATE" && this.userInfo.isBanned) || return (this.chat.type == "PRIVATE" && this.userInfo.isBanned) ||
(this.chat.type == "GROUP" && this.group.isBanned) (this.chat.type == "GROUP" && this.group.isBanned)
},
memberSize() {
return this.groupMembers.filter(m => !m.quit).length;
} }
}, },
watch: { watch: {
chat: { chat: {
handler(newChat, oldChat) { handler(newChat, oldChat) {
if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type || if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
newChat.targetId != oldChat.targetId)) { newChat.targetId != oldChat.targetId)) {
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
this.loadGroup(this.chat.targetId); this.loadGroup(this.chat.targetId);
} else { } else {

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

@ -1,183 +1,189 @@
<template> <template>
<div v-show="show"> <div v-show="show">
<div class="chat-group-readed-mask" @click.self="close()"> <div class="chat-group-readed-mask" @click.self="close()">
<div class="chat-group-readed" :style="{ 'left': pos.x + 'px', 'top': pos.y + 'px' }" @click.prevent=""> <div class="chat-group-readed" :style="{ 'left': pos.x + 'px', 'top': pos.y + 'px' }" @click.prevent="">
<el-tabs type="border-card" :stretch="true"> <el-tabs type="border-card" :stretch="true">
<el-tab-pane :label="`已读(${readedMembers.length})`"> <el-tab-pane :label="`已读(${readedMembers.length})`">
<el-scrollbar class="scroll-box"> <virtual-scroller class="scroll-box" :items="readedMembers">
<div v-for="(member) in readedMembers" :key="member.id"> <template v-slot="{ item }">
<chat-group-member :member="member"></chat-group-member> <chat-group-member :member="item"></chat-group-member>
</div> </template>
</el-scrollbar> </virtual-scroller>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="`未读(${unreadMembers.length})`"> <el-tab-pane :label="`未读(${unreadMembers.length})`">
<el-scrollbar class="scroll-box"> <virtual-scroller class="scroll-box" :items="unreadMembers">
<div v-for="(member) in unreadMembers" :key="member.id"> <template v-slot="{ item }">
<chat-group-member :member="member"></chat-group-member> <chat-group-member :member="item"></chat-group-member>
</div> </template>
</el-scrollbar> </virtual-scroller>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div v-show="msgInfo.selfSend" class="arrow-right" :style="{ 'top': pos.arrowY + 'px' }"> <div v-show="msgInfo.selfSend" class="arrow-right" :style="{ 'top': pos.arrowY + 'px' }">
<div class="arrow-right-inner"> <div class="arrow-right-inner">
</div> </div>
</div> </div>
<div v-show="!msgInfo.selfSend" class="arrow-left" :style="{ 'top': pos.arrowY + 'px' }"> <div v-show="!msgInfo.selfSend" class="arrow-left" :style="{ 'top': pos.arrowY + 'px' }">
<div class="arrow-left-inner"> <div class="arrow-left-inner">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import VirtualScroller from '../common/VirtualScroller.vue';
import ChatGroupMember from "./ChatGroupMember.vue"; import ChatGroupMember from "./ChatGroupMember.vue";
export default { export default {
name: "chatGroupReaded", name: "chatGroupReaded",
components: { components: {
ChatGroupMember ChatGroupMember, VirtualScroller
}, },
data() { data() {
return { return {
show: false, show: false,
pos: { pos: {
x: 0, x: 0,
y: 0, y: 0,
arrowY: 0 arrowY: 0
}, },
readedMembers: [], readedMembers: [],
unreadMembers: [] unreadMembers: []
} }
}, },
props: { props: {
groupMembers: { groupMembers: {
type: Array type: Array
}, },
msgInfo: { msgInfo: {
type: Object type: Object
} }
}, },
methods: { methods: {
close() { close() {
this.show = false; this.show = false;
}, },
open(rect) { open(rect) {
this.show = true; this.show = true;
this.pos.arrowY = 200; this.pos.arrowY = 200;
// //
if (this.msgInfo.selfSend) { if (this.msgInfo.selfSend) {
// //
this.pos.x = rect.left - 310; this.pos.x = rect.left - 310;
} else { } else {
// //
this.pos.x = rect.right + 20; this.pos.x = rect.right + 20;
} }
this.pos.y = rect.top + rect.height / 2 - 215; this.pos.y = rect.top + rect.height / 2 - 215;
// //
if (this.pos.y < 0) { if (this.pos.y < 0) {
this.pos.arrowY += this.pos.y this.pos.arrowY += this.pos.y
this.pos.y = 0; this.pos.y = 0;
} }
this.loadReadedUser() this.loadReadedUser()
}, },
loadReadedUser() { loadReadedUser() {
this.readedMembers = []; this.readedMembers = [];
this.unreadMembers = []; this.unreadMembers = [];
this.$http({ this.$http({
url: "/message/group/findReadedUsers", url: "/message/group/findReadedUsers",
method: 'get', method: 'get',
params: { groupId: this.msgInfo.groupId, messageId: this.msgInfo.id } params: { groupId: this.msgInfo.groupId, messageId: this.msgInfo.id }
}).then(userIds => { }).then(userIds => {
this.groupMembers.forEach(member => { this.groupMembers.forEach(member => {
// 退 // 退
if (member.userId == this.msgInfo.sendId || member.quit) { if (member.userId == this.msgInfo.sendId || member.quit) {
return; return;
} }
// //
if (userIds.find(userId => member.userId == userId)) { if (userIds.find(userId => member.userId == userId)) {
this.readedMembers.push(member); this.readedMembers.push(member);
} else { } else {
this.unreadMembers.push(member); this.unreadMembers.push(member);
} }
}) })
// //
this.$store.commit("updateMessage", { let msgInfo = {
id: this.msgInfo.id, id: this.msgInfo.id,
groupId: this.msgInfo.groupId, groupId: this.msgInfo.groupId,
readedCount: this.readedMembers.length readedCount: this.readedMembers.length
}) }
}) let chatInfo = {
} type: 'GROUP',
} targetId: this.msgInfo.groupId
}
this.$store.commit("updateMessage", [msgInfo, chatInfo])
})
}
}
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.chat-group-readed-mask { .chat-group-readed-mask {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 9999; z-index: 9999;
} }
.chat-group-readed { .chat-group-readed {
position: fixed; position: fixed;
width: 300px; width: 300px;
.scroll-box { .scroll-box {
height: 400px; height: 400px;
} }
.arrow-left { .arrow-left {
position: absolute; position: absolute;
left: -15px; left: -15px;
width: 0; width: 0;
height: 0; height: 0;
border-top: 15px solid transparent; border-top: 15px solid transparent;
border-bottom: 15px solid transparent; border-bottom: 15px solid transparent;
border-right: 15px solid #ccc; border-right: 15px solid #ccc;
.arrow-left-inner { .arrow-left-inner {
position: absolute; position: absolute;
top: -12px; top: -12px;
left: 3px; left: 3px;
width: 0; width: 0;
height: 0; height: 0;
overflow: hidden; overflow: hidden;
border-top: 12px solid transparent; border-top: 12px solid transparent;
border-bottom: 12px solid transparent; border-bottom: 12px solid transparent;
border-right: 12px solid white; border-right: 12px solid white;
} }
} }
.arrow-right { .arrow-right {
position: absolute; position: absolute;
right: -15px; right: -15px;
width: 0; width: 0;
height: 0; height: 0;
border-top: 15px solid transparent; border-top: 15px solid transparent;
border-bottom: 15px solid transparent; border-bottom: 15px solid transparent;
border-left: 15px solid #ccc; border-left: 15px solid #ccc;
.arrow-right-inner { .arrow-right-inner {
position: absolute; position: absolute;
top: -12px; top: -12px;
right: 3px; right: 3px;
width: 0; width: 0;
height: 0; height: 0;
overflow: hidden; overflow: hidden;
border-top: 12px solid transparent; border-top: 12px solid transparent;
border-bottom: 12px solid transparent; border-bottom: 12px solid transparent;
border-left: 12px solid white; border-left: 12px solid white;
} }
} }
} }
</style> </style>

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

@ -6,20 +6,22 @@
</el-input> </el-input>
</div> </div>
<div class="group-side-scrollbar"> <div class="group-side-scrollbar">
<div v-show="!group.quit" class="group-side-member-list"> <el-scrollbar v-show="!group.quit" ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="group-side-invite"> <div class="group-side-member-list">
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember = true"> <div class="group-side-invite">
<i class="el-icon-plus"></i> <div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember = true">
<i class="el-icon-plus"></i>
</div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers"
@reload="$emit('reload')" @close="showAddGroupMember = false"></add-group-member>
</div>
<div v-for="(member, idx) in showMembers" :key="member.id">
<group-member v-if="idx < showMaxIdx" class="group-side-member" :member="member"
:showDel="false"></group-member>
</div> </div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers"
@reload="$emit('reload')" @close="showAddGroupMember = false"></add-group-member>
</div>
<div v-for="(member) in groupMembers" :key="member.id">
<group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)"
:member="member" :showDel="false"></group-member>
</div> </div>
</div> </el-scrollbar>
<el-divider v-if="!group.quit" content-position="center"></el-divider> <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="group-side-form" :model="group" size="small">
<el-form-item label="群聊名称"> <el-form-item label="群聊名称">
@ -62,7 +64,8 @@ export default {
return { return {
searchText: "", searchText: "",
editing: false, editing: false,
showAddGroupMember: false showAddGroupMember: false,
showMaxIdx: 50
} }
}, },
props: { props: {
@ -113,7 +116,15 @@ export default {
}); });
}) })
}, },
onScroll(e) {
const scrollbar = e.target;
//
if (scrollbar.scrollTop + scrollbar.clientHeight >= scrollbar.scrollHeight - 30) {
if (this.showMaxIdx < this.showMembers.length) {
this.showMaxIdx += 30;
}
}
}
}, },
computed: { computed: {
ownerName() { ownerName() {
@ -122,8 +133,17 @@ export default {
}, },
isOwner() { isOwner() {
return this.group.ownerId == this.$store.state.userStore.userInfo.id; return this.group.ownerId == this.$store.state.userStore.userInfo.id;
},
showMembers() {
return this.groupMembers.filter((m) => !m.quit && m.showNickName.includes(this.searchText))
},
scrollHeight() {
return Math.min(400, 80 + this.showMembers.length / 5 * 80);
} }
},
mounted() {
let scrollWrap = this.$refs.scrollbar.$el.querySelector('.el-scrollbar__wrap');
scrollWrap.addEventListener('scroll', this.onScroll);
} }
} }
</script> </script>

74
im-web/src/components/common/VirtualScroller.vue

@ -0,0 +1,74 @@
<template>
<el-scrollbar ref="scrollbar">
<div v-for="(item, idx) in items" :key="idx">
<slot :item="item" v-if=" idx < showMaxIdx">
</slot>
</div>
</el-scrollbar>
</template>
<script>
export default {
name: "virtualScroller",
data() {
return {
page: 1,
isInitEvent: false,
lockTip: false
}
},
props: {
items: {
type: Array
},
size: {
type: Number,
default: 30
}
},
methods: {
init() {
this.page = 1;
this.initEvent();
},
initEvent() {
if (!this.isInitEvent) {
let scrollWrap = this.$refs.scrollbar.$el.querySelector('.el-scrollbar__wrap');
scrollWrap.addEventListener('scroll', this.onScroll);
this.isInitEvent = true;
}
},
onScroll(e) {
const scrollbar = e.target;
//
if (scrollbar.scrollTop + scrollbar.clientHeight >= scrollbar.scrollHeight - 30) {
if(this.showMaxIdx >= this.items.length ){
this.showTip();
}else{
this.page++;
}
}
},
showTip(){
// 3
if(!this.lockTip){
this.$message.success("已到滚动到底部")
this.lockTip = true;
setTimeout(()=>{
this.lockTip = false;
},3000)
}
}
},
computed: {
showMaxIdx() {
return Math.min(this.page * this.size, this.items.length);
}
},
mounted(){
this.initEvent();
}
}
</script>
<style scoped></style>

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

@ -5,15 +5,14 @@
<el-input placeholder="搜索" v-model="searchText"> <el-input placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="suffix"> </i> <i class="el-icon-search el-input__icon" slot="suffix"> </i>
</el-input> </el-input>
<el-scrollbar style="height:400px;"> <virtual-scroller class="scroll-box" :items="showMembers">
<div v-for="m in members" :key="m.userId"> <template v-slot="{ item }">
<group-member-item v-show="!m.quit && m.showNickName.includes(searchText)" :member="m" <group-member-item :member="item" @click.native="onClickMember(item)">
@click.native="onClickMember(m)"> <el-checkbox :disabled="item.locked" v-model="item.checked" @change="onChange(item)"
<el-checkbox :disabled="m.locked" v-model="m.checked" @change="onChange(m)"
@click.native.stop=""></el-checkbox> @click.native.stop=""></el-checkbox>
</group-member-item> </group-member-item>
</div> </template>
</el-scrollbar> </virtual-scroller>
</div> </div>
<div class="arrow el-icon-d-arrow-right"></div> <div class="arrow el-icon-d-arrow-right"></div>
<div class="right-box"> <div class="right-box">
@ -33,6 +32,7 @@
</template> </template>
<script> <script>
import VirtualScroller from '../common/VirtualScroller.vue';
import GroupMemberItem from './GroupMemberItem.vue'; import GroupMemberItem from './GroupMemberItem.vue';
import GroupMember from './GroupMember.vue'; import GroupMember from './GroupMember.vue';
@ -40,7 +40,8 @@ export default {
name: "addGroupMember", name: "addGroupMember",
components: { components: {
GroupMemberItem, GroupMemberItem,
GroupMember GroupMember,
VirtualScroller
}, },
data() { data() {
return { return {
@ -106,6 +107,9 @@ export default {
} }
}) })
return ids; return ids;
},
showMembers() {
return this.members.filter((m) => !m.hide && !m.quit && m.showNickName.includes(this.searchText))
} }
} }
@ -116,11 +120,18 @@ export default {
.group-member-selector { .group-member-selector {
display: flex; display: flex;
.left-box { .left-box {
width: 48%; width: 48%;
overflow: hidden; overflow: hidden;
border: var(--im-border); border: var(--im-border);
.scroll-box {
height: 400px;
}
.el-input__inner { .el-input__inner {
border: none; border: none;
border-bottom: var(--im-border); border-bottom: var(--im-border);

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

@ -67,20 +67,22 @@
</el-form> </el-form>
</div> </div>
<el-divider content-position="center"></el-divider> <el-divider content-position="center"></el-divider>
<div class="group-member-list"> <el-scrollbar ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div v-for="(member) in groupMembers" :key="member.id"> <div class="group-member-list">
<group-member v-show="!member.quit" class="group-member" :member="member" <div class="group-invite">
:showDel="isOwner && member.userId != activeGroup.ownerId" @del="onKick"></group-member> <div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
</div> <i class="el-icon-plus"></i>
<div class="group-invite"> </div>
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()"> <div class="invite-member-text">邀请</div>
<i class="el-icon-plus"></i> <add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" :members="groupMembers"
@reload="loadGroupMembers" @close="onCloseAddGroupMember"></add-group-member>
</div>
<div v-for="(member, idx) in showMembers" :key="member.id">
<group-member v-if="idx < showMaxIdx" class="group-member" :member="member"
:showDel="isOwner && member.userId != activeGroup.ownerId" @del="onKick"></group-member>
</div> </div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" :members="groupMembers"
@reload="loadGroupMembers" @close="onCloseAddGroupMember"></add-group-member>
</div> </div>
</div> </el-scrollbar>
</div> </div>
</div> </div>
</el-container> </el-container>
@ -112,6 +114,7 @@ export default {
activeGroup: {}, activeGroup: {},
groupMembers: [], groupMembers: [],
showAddGroupMember: false, showAddGroupMember: false,
showMaxIdx: 150,
rules: { rules: {
name: [{ name: [{
required: true, required: true,
@ -143,6 +146,7 @@ export default {
}) })
}, },
onActiveItem(group) { onActiveItem(group) {
this.showMaxIdx = 150;
// store // store
this.activeGroup = JSON.parse(JSON.stringify(group)); this.activeGroup = JSON.parse(JSON.stringify(group));
// //
@ -188,7 +192,6 @@ export default {
this.reset(); this.reset();
}); });
}) })
}, },
onKick(member) { onKick(member) {
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', { this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', {
@ -237,6 +240,15 @@ export default {
this.$store.commit("activeChat", 0); this.$store.commit("activeChat", 0);
this.$router.push("/home/chat"); this.$router.push("/home/chat");
}, },
onScroll(e) {
const scrollbar = e.target;
//
if (scrollbar.scrollTop + scrollbar.clientHeight >= scrollbar.scrollHeight - 30) {
if (this.showMaxIdx < this.showMembers.length) {
this.showMaxIdx += 50;
}
}
},
loadGroupMembers() { loadGroupMembers() {
this.$http({ this.$http({
url: `/group/members/${this.activeGroup.id}`, url: `/group/members/${this.activeGroup.id}`,
@ -311,7 +323,17 @@ export default {
}, },
groupValues() { groupValues() {
return Array.from(this.groupMap.values()); return Array.from(this.groupMap.values());
},
showMembers() {
return this.groupMembers.filter((m) => !m.quit)
},
scrollHeight() {
return Math.min(300, 80 + this.showMembers.length / 10 * 80);
} }
},
mounted() {
let scrollWrap = this.$refs.scrollbar.$el.querySelector('.el-scrollbar__wrap');
scrollWrap.addEventListener('scroll', this.onScroll);
} }
} }
</script> </script>

Loading…
Cancel
Save