Browse Source

群聊功能开发中

master
xie.bx 3 years ago
parent
commit
dc361f21fa
  1. 8
      im-platform/src/main/java/com/lx/implatform/controller/GroupController.java
  2. 2
      im-platform/src/main/java/com/lx/implatform/service/IGroupService.java
  3. 5
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupMessageServiceImpl.java
  4. 47
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupServiceImpl.java
  5. 3
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java
  6. 12
      im-ui/src/components/chat/ChatGroup.vue
  7. 3
      im-ui/src/components/chat/ChatPrivate.vue
  8. 16
      im-ui/src/components/common/HeadImage.vue
  9. 2
      im-ui/src/components/friend/AddFriend.vue
  10. 8
      im-ui/src/components/group/AddGroupMember.vue
  11. 26
      im-ui/src/components/group/GroupMember.vue
  12. 8
      im-ui/src/view/Chat.vue
  13. 32
      im-ui/src/view/Friend.vue
  14. 46
      im-ui/src/view/Group.vue

8
im-platform/src/main/java/com/lx/implatform/controller/GroupController.java

@ -79,5 +79,13 @@ public class GroupController {
return ResultUtils.success(); return ResultUtils.success();
} }
@ApiOperation(value = "踢出群聊",notes="将用户踢出群聊")
@DeleteMapping("/kick/{groupId}")
public Result<GroupVO> kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId,
@NotNull(message = "用户id不能为空") @RequestParam Long userId){
groupService.kickGroup(groupId,userId);
return ResultUtils.success();
}
} }

2
im-platform/src/main/java/com/lx/implatform/service/IGroupService.java

@ -27,6 +27,8 @@ public interface IGroupService extends IService<Group> {
void quitGroup(Long groupId); void quitGroup(Long groupId);
void kickGroup(Long groupId,Long userId);
List<GroupVO> findGroups(); List<GroupVO> findGroups();
void invite(GroupInviteVO vo); void invite(GroupInviteVO vo);

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

@ -65,10 +65,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
} }
userIds.parallelStream().forEach(id->{ userIds.parallelStream().forEach(id->{
if(id == userId){
// 自己不需要推送给自己
return;
}
String key = RedisKey.IM_USER_SERVER_ID + id; String key = RedisKey.IM_USER_SERVER_ID + id;
Integer serverId = (Integer)redisTemplate.opsForValue().get(key); Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
if(serverId != null){ if(serverId != null){

47
im-platform/src/main/java/com/lx/implatform/service/impl/GroupServiceImpl.java

@ -23,14 +23,11 @@ import com.lx.implatform.vo.GroupMemberVO;
import com.lx.implatform.vo.GroupVO; import com.lx.implatform.vo.GroupVO;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Member;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -80,6 +77,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
return vo; return vo;
} }
/** /**
* 修改群聊信息 * 修改群聊信息
* *
@ -109,6 +107,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
return vo; return vo;
} }
/** /**
* 删除群聊 * 删除群聊
* *
@ -121,9 +120,6 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
public void deleteGroup(Long groupId) { public void deleteGroup(Long groupId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = this.getById(groupId); Group group = this.getById(groupId);
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在");
}
if(group.getOwnerId() != session.getId()){ if(group.getOwnerId() != session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊");
} }
@ -143,9 +139,6 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
public void quitGroup(Long groupId) { public void quitGroup(Long groupId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = this.getById(groupId); Group group = this.getById(groupId);
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在");
}
if(group.getOwnerId() == session.getId()){ if(group.getOwnerId() == session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您是群主,不可退出群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您是群主,不可退出群聊");
} }
@ -154,13 +147,31 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
} }
/**
* 将用户踢出群聊
*
* @param groupId 群聊id
* @param userId 用户id
* @return
*/
@Override @Override
public GroupVO findById(Long groupId) { public void kickGroup(Long groupId, Long userId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = super.getById(groupId); Group group = this.getById(groupId);
if(group == null){ if(group.getOwnerId() != session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您不是群主,没有权限踢人");
} }
if(userId == session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"亲,不能自己踢自己哟");
}
// 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId,userId);
}
@Override
public GroupVO findById(Long groupId) {
UserSession session = SessionContext.getSession();
Group group = this.getById(groupId);
GroupMember member = groupMemberService.findByGroupAndUserId(groupId,session.getId()); GroupMember member = groupMemberService.findByGroupAndUserId(groupId,session.getId());
if(member == null){ if(member == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您未加入群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR,"您未加入群聊");
@ -180,7 +191,14 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
@Cacheable(value = "#groupId") @Cacheable(value = "#groupId")
@Override @Override
public Group GetById(Long groupId){ public Group GetById(Long groupId){
return super.getById(groupId); Group group = super.getById(groupId);
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在");
}
if(group.getDeleted()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组已解散");
}
return group;
} }
@ -275,5 +293,4 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
return vos; return vos;
} }
} }

3
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java

@ -31,11 +31,14 @@ public class GroupMessageProcessor extends MessageProcessor<GroupMessageInfo> {
for(Long recvId:recvIds){ for(Long recvId:recvIds){
ChannelHandlerContext channelCtx = WebsocketChannelCtxHloder.getChannelCtx(recvId); ChannelHandlerContext channelCtx = WebsocketChannelCtxHloder.getChannelCtx(recvId);
if(channelCtx != null){ if(channelCtx != null){
// 自己发的消息不用推送
if(recvId != data.getSendId()){
// 推送消息到用户 // 推送消息到用户
SendInfo sendInfo = new SendInfo(); SendInfo sendInfo = new SendInfo();
sendInfo.setCmd(WSCmdEnum.GROUP_MESSAGE.getCode()); sendInfo.setCmd(WSCmdEnum.GROUP_MESSAGE.getCode());
sendInfo.setData(data); sendInfo.setData(data);
channelCtx.channel().writeAndFlush(sendInfo); channelCtx.channel().writeAndFlush(sendInfo);
}
// 设置已读最大id // 设置已读最大id
String key = RedisKey.IM_GROUP_READED_POSITION + data.getGroupId()+":"+recvId; String key = RedisKey.IM_GROUP_READED_POSITION + data.getGroupId()+":"+recvId;
redisTemplate.opsForValue().set(key,data.getId()); redisTemplate.opsForValue().set(key,data.getId());

12
im-ui/src/components/chat/ChatGroup.vue

@ -250,16 +250,10 @@
} }
}, },
watch: {
'chat.targetId': {
handler: function(newGroupId, oldGroupId) {
this.loadGroup(newGroupId);
},
immediate: true
}
},
mounted() { mounted() {
// this.loadGroup(this.chat.targetId); console.log("group mount...")
this.loadGroup(this.chat.targetId);
this.scrollToBottom();
} }
} }
</script> </script>

3
im-ui/src/components/chat/ChatPrivate.vue

@ -215,7 +215,8 @@
} }
}, },
mounted() { mounted() {
console.log(this.chat); console.log("private mount...")
this.scrollToBottom();
} }
} }
</script> </script>

16
im-ui/src/components/common/HeadImage.vue

@ -1,17 +1,17 @@
<template> <template>
<img :src="url" <div class="head-image">
:style="{width: size+'px',height: size+'px',cursor: 'pointer'}" /> <img :src="url" :style="{width: size+'px',height: size+'px',cursor: 'pointer'}" />
<slot></slot>
</div>
</template> </template>
<script> <script>
export default { export default {
name: "headImage", name: "headImage",
data() { data() {
return { return {}
}
},
methods:{
}, },
methods: {},
props: { props: {
size: { size: {
type: Number, type: Number,
@ -25,6 +25,8 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.head-image {
position: relative;
img { img {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -39,5 +41,5 @@
background: url('../../assets/default_head.png') no-repeat 0 0; background: url('../../assets/default_head.png') no-repeat 0 0;
background-size: 100%; background-size: 100%;
} }
}
</style> </style>

2
im-ui/src/components/friend/AddFriend.vue

@ -1,6 +1,6 @@
<template> <template>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="30%" :before-close="handleClose"> <el-dialog title="添加好友" :visible.sync="dialogVisible" width="30%" :before-close="handleClose">
<el-input placeholder="搜索好友" class="input-with-select" v-model="searchText" @keyup.enter.native="handleSearch()"> <el-input placeholder="输入好友昵称,最多展示10条" class="input-with-select" v-model="searchText" @keyup.enter.native="handleSearch()">
<el-button slot="append" icon="el-icon-search" @click="handleSearch()"></el-button> <el-button slot="append" icon="el-icon-search" @click="handleSearch()"></el-button>
</el-input> </el-input>
<el-scrollbar style="height:400px"> <el-scrollbar style="height:400px">

8
im-ui/src/components/group/AddGroupMember.vue

@ -7,9 +7,8 @@
</el-input> </el-input>
<el-scrollbar style="height:500px;"> <el-scrollbar style="height:500px;">
<div v-for="(friend,index) in friends" :key="friend.id"> <div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-show="friend.nickName.startsWith(searchText)" <friend-item v-show="friend.nickName.startsWith(searchText)" :showDelete="false" @click.native="handleSwitchCheck(friend)"
:showDelete="false" @click.native="handleSwitchCheck(friend)" :friend="friend" :friend="friend" :index="index" :active="index === activeIndex">
:index="index" :active="index === activeIndex">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" v-model="friend.isCheck" <el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" v-model="friend.isCheck"
size="medium"></el-checkbox> size="medium"></el-checkbox>
</friend-item> </friend-item>
@ -106,7 +105,8 @@
this.friends = []; this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => { this.$store.state.friendStore.friends.forEach((f) => {
let friend = JSON.parse(JSON.stringify(f)) let friend = JSON.parse(JSON.stringify(f))
let m = this.members.find((m) => m.userId == f.id); let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
console.log(m); console.log(m);
if (m) { if (m) {
// //

26
im-ui/src/components/group/GroupMember.vue

@ -1,6 +1,8 @@
<template> <template>
<div class="group-member"> <div class="group-member">
<head-image :url="member.headImage" :size="60" class=""></head-image> <head-image :url="member.headImage" :size="60" class="">
<div v-if="showDel" @click.stop="handleDelete()" class="btn-kick el-icon-error"></div>
</head-image>
<div class="member-name">{{member.aliasName}}</div> <div class="member-name">{{member.aliasName}}</div>
</div> </div>
</template> </template>
@ -21,7 +23,12 @@
}, },
showDel:{ showDel:{
type: Boolean, type: Boolean,
default: true default: false
}
},
methods:{
handleDelete(){
this.$emit("del",this.member);
} }
} }
} }
@ -43,5 +50,20 @@
text-overflow:ellipsis; text-overflow:ellipsis;
overflow:hidden overflow:hidden
} }
.btn-kick {
display: none;
position: absolute;
right: -8px;
top: -8px;
color: darkred;
font-size: 20px;
cursor: pointer;
}
&:hover .btn-kick{
display: block;
color: #ce1818;
}
} }
</style> </style>

8
im-ui/src/view/Chat.vue

@ -6,10 +6,12 @@
<el-button slot="append" icon="el-icon-search"></el-button> <el-button slot="append" icon="el-icon-search"></el-button>
</el-input> </el-input>
</div> </div>
<el-scrollbar class="l-chat-list" >
<div v-for="(chat,index) in chatStore.chats" :key="chat.type+chat.targetId"> <div v-for="(chat,index) in chatStore.chats" :key="chat.type+chat.targetId">
<chat-item :chat="chat" :index="index" @click.native="handleActiveItem(index)" @del="handleDelItem(chat,index)" <chat-item :chat="chat" :index="index" @click.native="handleActiveItem(index)" @del="handleDelItem(chat,index)"
:active="index === chatStore.activeIndex"></chat-item> :active="index === chatStore.activeIndex"></chat-item>
</div> </div>
</el-scrollbar>
</el-aside> </el-aside>
<el-container class="r-chat-box"> <el-container class="r-chat-box">
<chat-private :chat="activeChat" v-if="activeChat.type=='PRIVATE'"></chat-private> <chat-private :chat="activeChat" v-if="activeChat.type=='PRIVATE'"></chat-private>
@ -137,6 +139,8 @@
<style lang="scss"> <style lang="scss">
.el-container { .el-container {
.l-chat-box { .l-chat-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
background: white; background: white;
width: 3rem; width: 3rem;
@ -146,6 +150,10 @@
background-color: white; background-color: white;
line-height: 50px; line-height: 50px;
} }
.l-friend-ist{
flex: 1;
}
} }
.r-chat-box { .r-chat-box {

32
im-ui/src/view/Friend.vue

@ -1,7 +1,7 @@
<template> <template>
<el-container> <el-container>
<el-aside width="250px" class="l-friend-box"> <el-aside width="250px" class="l-friend-box">
<div class="l-friend-header" height="60px"> <div class="l-friend-header" height="5%">
<div class="l-friend-search"> <div class="l-friend-search">
<el-input width="200px" placeholder="搜索好友" v-model="searchText"> <el-input width="200px" placeholder="搜索好友" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button> <el-button slot="append" icon="el-icon-search"></el-button>
@ -9,17 +9,21 @@
</div> </div>
<el-button plain icon="el-icon-plus" style="border: none; padding:12px; font-size: 20px;color: black;" title="添加好友" <el-button plain icon="el-icon-plus" style="border: none; padding:12px; font-size: 20px;color: black;" title="添加好友"
@click="handleShowAddFriend()"></el-button> @click="handleShowAddFriend()"></el-button>
<add-friend :dialogVisible="showAddFriend" @close="handleCloseAddFriend"> <add-friend :dialogVisible="showAddFriend" @close="handleCloseAddFriend">
</add-friend> </add-friend>
</div> </div>
<el-scrollbar class="l-friend-list" >
<div v-for="(friend,index) in $store.state.friendStore.friends" :key="friend.id"> <div v-for="(friend,index) in $store.state.friendStore.friends" :key="friend.id">
<friend-item v-show="friend.nickName.startsWith(searchText)" :friend="friend" :index="index" :active="index === $store.state.friendStore.activeIndex" <friend-item v-show="friend.nickName.startsWith(searchText)" :friend="friend" :index="index" :active="index === $store.state.friendStore.activeIndex"
@del="handleDelItem(friend,index)" @click.native="handleActiveItem(friend,index)"> @del="handleDelItem(friend,index)" @click.native="handleActiveItem(friend,index)">
</friend-item> </friend-item>
</div> </div>
</el-scrollbar>
</el-aside> </el-aside>
<el-container class="r-friend-box"> <el-container class="r-friend-box">
<div class="r-friend-header" v-show="userInfo.id">
{{userInfo.nickName}}
</div>
<div v-show="userInfo.id"> <div v-show="userInfo.id">
<div class="user-detail"> <div class="user-detail">
<head-image class="detail-head-image" :size="200" :url="userInfo.headImage"></head-image> <head-image class="detail-head-image" :size="200" :url="userInfo.headImage"></head-image>
@ -146,6 +150,8 @@
<style scoped lang="scss"> <style scoped lang="scss">
.el-container { .el-container {
.l-friend-box { .l-friend-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
background: white; background: white;
.l-friend-header { .l-friend-header {
@ -158,18 +164,38 @@
flex: 1; flex: 1;
} }
} }
.l-friend-ist{
flex: 1;
}
} }
.r-friend-box { .r-friend-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px;
.r-friend-header {
width: 100%;
height: 50px;
padding: 5px;
line-height: 50px;
font-size: 20px;
text-align: left;
text-indent: 10px;
background-color: white;
border: #dddddd solid 1px;
}
.user-detail { .user-detail {
width: 100%; width: 100%;
display: flex; display: flex;
padding: 50px 10px 10px 50px; padding: 50px 10px 10px 50px;
text-align: center; text-align: center;
justify-content: space-around;
.info-item { .info-item {
width: 400px; width: 400px;
height: 200px; height: 200px;
margin-left: 20px;
background-color: #ffffff; background-color: #ffffff;
} }
.description { .description {

46
im-ui/src/view/Group.vue

@ -10,19 +10,20 @@
<el-button plain icon="el-icon-plus" style="border: none; padding: 12px; font-size: 20px;color: black;" title="创建群聊" <el-button plain icon="el-icon-plus" style="border: none; padding: 12px; font-size: 20px;color: black;" title="创建群聊"
@click="handleCreateGroup()"></el-button> @click="handleCreateGroup()"></el-button>
</div> </div>
<el-scrollbar class="l-group-list">
<div v-for="(group,index) in groupStore.groups" :key="group.id"> <div v-for="(group,index) in groupStore.groups" :key="group.id">
<group-item v-show="group.remark.startsWith(searchText)" :group="group" :active="index === groupStore.activeIndex" <group-item v-show="group.remark.startsWith(searchText)" :group="group" :active="index === groupStore.activeIndex"
@click.native="handleActiveItem(group,index)"> @click.native="handleActiveItem(group,index)">
</group-item> </group-item>
</div> </div>
</el-scrollbar>
</el-aside> </el-aside>
<el-container class="r-group-box"> <el-container class="r-group-box">
<div class="r-group-header" v-show="groupStore.activeIndex>=0"> <div class="r-group-header" v-show="activeGroup.id">
{{activeGroup.remark}}({{groupMembers.length}}) {{activeGroup.remark}}({{groupMembers.length}})
</div> </div>
<div class="r-group-container"> <div class="r-group-container">
<div v-show="groupStore.activeIndex>=0"> <div v-show="activeGroup.id">
<div class="r-group-info"> <div class="r-group-info">
<div> <div>
<file-upload class="avatar-uploader" action="/api/image/upload" :disabled="!isOwner" :showLoading="true" <file-upload class="avatar-uploader" action="/api/image/upload" :disabled="!isOwner" :showLoading="true"
@ -60,7 +61,8 @@
<el-scrollbar style="height:400px;"> <el-scrollbar style="height:400px;">
<div class="r-group-member-list"> <div class="r-group-member-list">
<div v-for="(member) in groupMembers" :key="member.id"> <div v-for="(member) in groupMembers" :key="member.id">
<group-member class="r-group-member" :member="member" :showDel="true"></group-member> <group-member v-show="!member.quit" class="r-group-member" :member="member" :showDel="isOwner&&member.userId!=activeGroup.ownerId"
@del="handleKick"></group-member>
</div> </div>
<div class="r-group-invite"> <div class="r-group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="handleInviteMember()"> <div class="invite-member-btn" title="邀请好友进群聊" @click="handleInviteMember()">
@ -97,10 +99,7 @@
return { return {
searchText: "", searchText: "",
maxSize: 5 * 1024 * 1024, maxSize: 5 * 1024 * 1024,
activeGroup: { activeGroup: {},
empty: true,
remark: ""
},
groupMembers: [], groupMembers: [],
showAddGroupMember: false, showAddGroupMember: false,
rules: { rules: {
@ -176,6 +175,25 @@
}); });
}) })
},
handleKick(member) {
this.$confirm(`确定将成员'${member.aliasName}'移出群聊吗?`, '确认移出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/api/group/kick/${this.activeGroup.id}`,
method: 'delete',
params: {
userId: member.userId
}
}).then(() => {
this.$message.success(`已将${member.aliasName}移出群聊`);
member.quit = true;
});
})
}, },
handleQuit() { handleQuit() {
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', { this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', {
@ -210,7 +228,7 @@
url: `/api/group/members/${this.activeGroup.id}`, url: `/api/group/members/${this.activeGroup.id}`,
method: "get" method: "get"
}).then((members) => { }).then((members) => {
this.groupMembers = members.filter((m)=>!m.quit); this.groupMembers = members;
}) })
} }
}, },
@ -241,6 +259,8 @@
<style lang="scss"> <style lang="scss">
.im-group-box { .im-group-box {
.l-group-box { .l-group-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
background: white; background: white;
@ -255,6 +275,10 @@
flex: 1; flex: 1;
} }
} }
.l-group-ist{
flex: 1;
}
} }
.r-group-box { .r-group-box {
@ -317,6 +341,10 @@
display: block; display: block;
} }
} }
.send-btn {
margin-top: 10px;
}
} }
.r-group-member-list { .r-group-member-list {

Loading…
Cancel
Save