Browse Source

版本升级:v_3.5.0

master
xsx 11 months ago
parent
commit
101552e058
  1. 18
      im-platform/src/main/java/com/bx/implatform/controller/GroupController.java
  2. 6
      im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java
  3. 28
      im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java
  4. 9
      im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java
  5. 13
      im-platform/src/main/java/com/bx/implatform/service/GroupService.java
  6. 30
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java
  7. 60
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  8. 1
      im-uniapp/App.vue
  9. 7
      im-uniapp/common/recorder-h5.js
  10. 30
      im-uniapp/components/group-member-selector/group-member-selector.vue
  11. 1
      im-uniapp/components/image-upload/image-upload.vue
  12. 1
      im-uniapp/components/virtual-scroller/virtual-scroller.vue
  13. 5
      im-uniapp/pages/chat/chat-box.vue
  14. 1
      im-uniapp/pages/chat/chat-group-video.vue
  15. 70
      im-uniapp/pages/group/group-info.vue
  16. 34
      im-uniapp/pages/group/group-member.vue
  17. 1
      im-uniapp/pages/mine/mine.vue
  18. 1
      im-uniapp/pages/register/register.vue
  19. 42
      im-uniapp/static/icon/iconfont.css
  20. BIN
      im-uniapp/static/icon/iconfont.ttf
  21. 1
      im-uniapp/store/userStore.js
  22. 1
      im-web/src/api/camera.js
  23. 38
      im-web/src/assets/iconfont/iconfont.css
  24. BIN
      im-web/src/assets/iconfont/iconfont.ttf
  25. 127
      im-web/src/components/chat/ChatGroupSide.vue
  26. 4
      im-web/src/components/chat/ChatInput.vue
  27. 34
      im-web/src/components/chat/ChatItem.vue
  28. 18
      im-web/src/components/chat/ChatMessageItem.vue
  29. 14
      im-web/src/components/common/HeadImage.vue
  30. 28
      im-web/src/components/common/RightMenu.vue
  31. 101
      im-web/src/components/common/UserInfo.vue
  32. 36
      im-web/src/components/friend/FriendItem.vue
  33. 60
      im-web/src/components/group/AddGroupMember.vue
  34. 8
      im-web/src/components/group/GroupMember.vue
  35. 47
      im-web/src/components/group/GroupMemberSelector.vue
  36. 1
      im-web/src/components/rtc/RtcPrivateVideo.vue
  37. 1
      im-web/src/view/Friend.vue
  38. 75
      im-web/src/view/Group.vue
  39. 4
      im-web/src/view/Home.vue

18
im-platform/src/main/java/com/bx/implatform/controller/GroupController.java

@ -1,10 +1,11 @@
package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.dto.GroupMemberRemoveDTO;
import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.GroupService;
import com.bx.implatform.vo.GroupInviteVO;
import com.bx.implatform.dto.GroupInviteDTO;
import com.bx.implatform.vo.GroupMemberVO;
import com.bx.implatform.vo.GroupVO;
import io.swagger.v3.oas.annotations.Operation;
@ -62,8 +63,8 @@ public class GroupController {
@RepeatSubmit
@Operation(summary = "邀请进群", description = "邀请好友进群")
@PostMapping("/invite")
public Result invite(@Valid @RequestBody GroupInviteVO vo) {
groupService.invite(vo);
public Result invite(@Valid @RequestBody GroupInviteDTO dto) {
groupService.invite(dto);
return ResultUtils.success();
}
@ -74,6 +75,15 @@ public class GroupController {
return ResultUtils.success(groupService.findGroupMembers(groupId));
}
@RepeatSubmit
@Operation(summary = "将成员移出群聊", description = "将成员移出群聊")
@DeleteMapping("/members/remove")
public Result removeMembers(@Valid @RequestBody GroupMemberRemoveDTO dto) {
groupService.removeGroupMembers(dto);
return ResultUtils.success();
}
@RepeatSubmit
@Operation(summary = "退出群聊", description = "退出群聊")
@DeleteMapping("/quit/{groupId}")
@ -83,7 +93,7 @@ public class GroupController {
}
@RepeatSubmit
@Operation(summary = "踢出群聊", description = "将用户踢出群聊")
@Operation(summary = "踢出群聊(已废弃)", description = "将用户踢出群聊")
@DeleteMapping("/kick/{groupId}")
public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId,
@NotNull(message = "用户id不能为空") @RequestParam Long userId) {

6
im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java → im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java

@ -1,4 +1,4 @@
package com.bx.implatform.vo;
package com.bx.implatform.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
@ -9,8 +9,8 @@ import lombok.Data;
import java.util.List;
@Data
@Schema(description = "邀请好友进群请求VO")
public class GroupInviteVO {
@Schema(description = "邀请好友进群请求DTO")
public class GroupInviteDTO {
@NotNull(message = "群id不可为空")
@Schema(description = "群id")

28
im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java

@ -0,0 +1,28 @@
package com.bx.implatform.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* @author Blue
* @version 1.0
* @date 2025-02-23
*/
@Data
@Schema(description = "移除群聊成员")
public class GroupMemberRemoveDTO {
@NotNull(message = "群id不可为空")
@Schema(description = "群组id")
private Long groupId;
@Size(max = 50, message = "一次最多只能选择50位用户")
@NotEmpty(message = "成员用户id不可为空")
@Schema(description = "成员用户id")
private List<Long> userIds;
}

9
im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java

@ -16,6 +16,7 @@ public interface GroupMemberService extends IService<GroupMember> {
*/
GroupMember findByGroupAndUserId(Long groupId, Long userId);
/**
* 根据用户id查询群聊成员
*
@ -74,6 +75,14 @@ public interface GroupMemberService extends IService<GroupMember> {
*/
void removeByGroupAndUserId(Long groupId, Long userId);
/**
* 根据群聊id和用户id移除成员
*
* @param groupId 群聊id
* @param userIds 用户id
*/
void removeByGroupAndUserIds(Long groupId, List<Long> userIds);
/**
* 用户用户是否在群中
*

13
im-platform/src/main/java/com/bx/implatform/service/GroupService.java

@ -1,8 +1,9 @@
package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.implatform.dto.GroupMemberRemoveDTO;
import com.bx.implatform.entity.Group;
import com.bx.implatform.vo.GroupInviteVO;
import com.bx.implatform.dto.GroupInviteDTO;
import com.bx.implatform.vo.GroupMemberVO;
import com.bx.implatform.vo.GroupVO;
@ -48,6 +49,12 @@ public interface GroupService extends IService<Group> {
*/
void kickGroup(Long groupId, Long userId);
/**
* 将用户移出群聊
* @param dto dto
*/
void removeGroupMembers(GroupMemberRemoveDTO dto);
/**
* 查询当前用户的所有群聊
*
@ -58,9 +65,9 @@ public interface GroupService extends IService<Group> {
/**
* 邀请好友进群
*
* @param vo 群id好友id列表
* @param dto 群id好友id列表
**/
void invite(GroupInviteVO vo);
void invite(GroupInviteDTO dto);
/**
* 根据id查找群聊并进行缓存

30
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java

@ -37,11 +37,13 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
@Override
public GroupMember findByGroupAndUserId(Long groupId, Long userId) {
QueryWrapper<GroupMember> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(GroupMember::getGroupId, groupId).eq(GroupMember::getUserId, userId);
LambdaQueryWrapper<GroupMember> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMember::getGroupId, groupId);
wrapper.eq(GroupMember::getUserId, userId);
return this.getOne(wrapper);
}
@Override
public List<GroupMember> findByUserId(Long userId) {
LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
@ -88,19 +90,35 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
@Override
public void removeByGroupAndUserId(Long groupId, Long userId) {
LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate();
wrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getUserId, userId).set(GroupMember::getQuit, true)
.set(GroupMember::getQuitTime, new Date());
wrapper.eq(GroupMember::getGroupId, groupId);
wrapper.eq(GroupMember::getUserId, userId);
wrapper.set(GroupMember::getQuit, true);
wrapper.set(GroupMember::getQuitTime, new Date());
this.update(wrapper);
}
@CacheEvict(key = "#groupId")
@Override
public void removeByGroupAndUserIds(Long groupId, List<Long> userId) {
LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate();
wrapper.eq(GroupMember::getGroupId, groupId);
wrapper.in(GroupMember::getUserId, userId);
wrapper.set(GroupMember::getQuit, true);
wrapper.set(GroupMember::getQuitTime, new Date());
this.update(wrapper);
}
@Override
public Boolean isInGroup(Long groupId, List<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return true;
}
LambdaQueryWrapper<GroupMember> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getQuit, false)
.in(GroupMember::getUserId, userIds);
wrapper.eq(GroupMember::getGroupId, groupId);
wrapper.eq(GroupMember::getQuit, false);
wrapper.in(GroupMember::getUserId, userIds);
return userIds.size() == this.count(wrapper);
}
}

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

@ -12,6 +12,7 @@ import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.util.CommaTextUtils;
import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.dto.GroupMemberRemoveDTO;
import com.bx.implatform.entity.*;
import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType;
@ -25,7 +26,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.vo.GroupInviteVO;
import com.bx.implatform.dto.GroupInviteDTO;
import com.bx.implatform.vo.GroupMemberVO;
import com.bx.implatform.vo.GroupMessageVO;
import com.bx.implatform.vo.GroupVO;
@ -126,7 +127,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
String content = String.format("'%s'解散了群聊", session.getNickName());
this.sendTipMessage(groupId, userIds, content, true);
// 推送同步消息
this.sendDelGroupMessage(groupId, userIds, false);
this.sendDelGroupMessage(groupId, userIds);
log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName());
}
@ -145,7 +146,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
// 推送退出群聊提示
this.sendTipMessage(groupId, List.of(userId), "您已退出群聊", false);
// 推送同步消息
this.sendDelGroupMessage(groupId, Lists.newArrayList(), true);
this.sendDelGroupMessage(groupId, List.of(userId));
log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
}
@ -167,10 +168,35 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
// 推送踢出群聊提示
this.sendTipMessage(groupId, List.of(userId), "您已被移出群聊", false);
// 推送同步消息
this.sendDelGroupMessage(groupId, List.of(userId), false);
this.sendDelGroupMessage(groupId, List.of(userId));
log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
}
@Override
public void removeGroupMembers(GroupMemberRemoveDTO dto) {
UserSession session = SessionContext.getSession();
Group group = this.getAndCheckById(dto.getGroupId());
if (!group.getOwnerId().equals(session.getUserId())) {
throw new GlobalException("您没有权限");
}
if (dto.getUserIds().contains(group.getOwnerId())) {
throw new GlobalException("不允许移除群主");
}
if (dto.getUserIds().contains(session.getUserId())) {
throw new GlobalException("不允许移除自己");
}
// 删除群聊成员
groupMemberService.removeByGroupAndUserIds(dto.getGroupId(), dto.getUserIds());
// 清理已读缓存
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, dto.getGroupId());
dto.getUserIds().forEach(id -> redisTemplate.opsForHash().delete(key, id.toString()));
// 推送踢出群聊提示
this.sendTipMessage(dto.getGroupId(), dto.getUserIds(), "您已被移出群聊", false);
// 推送同步消息
this.sendDelGroupMessage(dto.getGroupId(), dto.getUserIds());
log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), dto.getUserIds());
}
@Override
public GroupVO findById(Long groupId) {
UserSession session = SessionContext.getSession();
@ -225,22 +251,22 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}
@Override
public void invite(GroupInviteVO vo) {
public void invite(GroupInviteDTO dto) {
UserSession session = SessionContext.getSession();
Group group = this.getAndCheckById(vo.getGroupId());
GroupMember member = groupMemberService.findByGroupAndUserId(vo.getGroupId(), session.getUserId());
Group group = this.getAndCheckById(dto.getGroupId());
GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId());
if (Objects.isNull(group) || member.getQuit()) {
throw new GlobalException("您不在群聊中,邀请失败");
}
// 群聊人数校验
List<GroupMember> members = groupMemberService.findByGroupId(vo.getGroupId());
List<GroupMember> members = groupMemberService.findByGroupId(dto.getGroupId());
long size = members.stream().filter(m -> !m.getQuit()).count();
if (vo.getFriendIds().size() + size > Constant.MAX_LARGE_GROUP_MEMBER) {
if (dto.getFriendIds().size() + size > Constant.MAX_LARGE_GROUP_MEMBER) {
throw new GlobalException("群聊人数不能大于" + Constant.MAX_LARGE_GROUP_MEMBER + "人");
}
// 找出好友信息
List<Friend> friends = friendsService.findByFriendIds(vo.getFriendIds());
if (vo.getFriendIds().size() != friends.size()) {
List<Friend> friends = friendsService.findByFriendIds(dto.getFriendIds());
if (dto.getFriendIds().size() != friends.size()) {
throw new GlobalException("部分用户不是您的好友,邀请失败");
}
// 批量保存成员数据
@ -248,7 +274,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
Optional<GroupMember> optional =
members.stream().filter(m -> m.getUserId().equals(f.getFriendId())).findFirst();
GroupMember groupMember = optional.orElseGet(GroupMember::new);
groupMember.setGroupId(vo.getGroupId());
groupMember.setGroupId(dto.getGroupId());
groupMember.setUserId(f.getFriendId());
groupMember.setUserNickName(f.getFriendNickName());
groupMember.setHeadImage(f.getFriendHeadImage());
@ -265,12 +291,12 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
sendAddGroupMessage(groupVo, List.of(m.getUserId()), false);
}
// 推送进入群聊消息
List<Long> userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId());
List<Long> userIds = groupMemberService.findUserIdsByGroupId(dto.getGroupId());
String memberNames = groupMembers.stream().map(GroupMember::getShowNickName).collect(Collectors.joining(","));
String content = String.format("'%s'邀请'%s'加入了群聊", session.getNickName(), memberNames);
this.sendTipMessage(vo.getGroupId(), userIds, content, true);
this.sendTipMessage(dto.getGroupId(), userIds, content, true);
log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(),
vo.getFriendIds());
dto.getFriendIds());
}
@Override
@ -345,7 +371,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
imClient.sendGroupMessage(sendMessage);
}
private void sendDelGroupMessage(Long groupId, List<Long> recvIds, Boolean sendToSelf) {
private void sendDelGroupMessage(Long groupId, List<Long> recvIds) {
UserSession session = SessionContext.getSession();
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.GROUP_DEL.code());
@ -357,7 +383,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
sendMessage.setRecvIds(recvIds);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(sendToSelf);
sendMessage.setSendToSelf(false);
imClient.sendGroupMessage(sendMessage);
}
}

1
im-uniapp/App.vue

@ -407,7 +407,6 @@ export default {
// #ifdef APP-PLUS
//
setTimeout(() => {
console.log("plus.navigator.closeSplashscreen()")
plus.navigator.closeSplashscreen()
}, delay)
// #endif

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

@ -26,7 +26,6 @@ let checkIsEnable = () => {
let start = () => {
return navigator.mediaDevices.getUserMedia({ audio: true }).then(audioStream => {
console.log("start record")
startTime = new Date().getTime();
chunks = [];
stream = audioStream;
@ -36,7 +35,6 @@ let start = () => {
}
let close = () => {
console.log("stream:", stream)
stream.getTracks().forEach((track) => {
track.stop()
})
@ -47,9 +45,6 @@ let close = () => {
let upload = () => {
return new Promise((resolve, reject) => {
rc.ondataavailable = (e) => {
console.log("ondataavailable:",e.data)
console.log("size:",e.data.size)
console.log("type:",e.data.type)
chunks.push(e.data)
}
rc.onstop = () => {
@ -58,8 +53,6 @@ let upload = () => {
return;
}
duration = (new Date().getTime() - startTime) / 1000;
console.log("时长:", duration)
console.log("上传,chunks:", chunks.length)
const newbolb = new Blob(chunks, { 'type': 'audio/mpeg' });
const name = new Date().getDate() + '.mp3';
const file = new File([newbolb], name)

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

@ -42,12 +42,15 @@
export default {
name: "chat-group-member-choose",
props: {
group: {
type: Object
},
members: {
type: Array
},
maxSize: {
type: Number,
default: -1
default: 50
}
},
data() {
@ -56,10 +59,11 @@ export default {
};
},
methods: {
init(checkedIds, lockedIds) {
init(checkedIds, lockedIds, hideIds) {
this.members.forEach((m) => {
m.checked = checkedIds.indexOf(m.userId) >= 0;
m.locked = lockedIds.indexOf(m.userId) >= 0;
m.hide = hideIds.indexOf(m.userId) >= 0;
});
},
open() {
@ -80,7 +84,7 @@ export default {
},
onClean() {
this.members.forEach((m) => {
if (!m.locked) {
if (!m.locked && m.checked) {
m.checked = false;
}
})
@ -88,20 +92,17 @@ export default {
onOk() {
this.$refs.popup.close();
this.$emit("complete", this.checkedIds)
},
isChecked(m) {
return this.checkedIds.indexOf(m.userId) >= 0;
}
},
computed: {
checkedIds() {
return this.members.filter((m) => m.checked).map(m => m.userId)
return this.checkedMembers.map(m => m.userId)
},
checkedMembers() {
return this.members.filter((m) => m.checked);
return this.members.filter((m) => !m.quit && !m.hide && m.checked);
},
showMembers() {
return this.members.filter(m => !m.quit && m.showNickName.includes(this.searchText))
return this.members.filter(m => !m.quit && !m.hide && m.showNickName.includes(this.searchText))
}
}
}
@ -115,7 +116,8 @@ export default {
flex-direction: column;
background-color: white;
padding: 10rpx;
border-radius: 15rpx;
border-radius: 15rpx 15rpx 0 0;
overflow: hidden;
.top-bar {
display: flex;
@ -158,6 +160,8 @@ export default {
white-space: nowrap;
.member-name {
display: flex;
align-items: center;
flex: 1;
padding-left: 20rpx;
font-size: 30rpx;
@ -165,11 +169,15 @@ export default {
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
.uni-tag {
margin-left: 5rpx;
}
}
}
.scroll-bar {
height: 800rpx;
height: 65vh;
}
}
}

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

@ -54,7 +54,6 @@ export default {
sizeType: ['original'], //original compressed
success: (res) => {
res.tempFiles.forEach((file) => {
console.log("文件:", file)
if (!this.onBefore || this.onBefore(file)) {
//
this.uploadImage(file);

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

@ -28,7 +28,6 @@ export default {
},
methods: {
onScrollToBottom(e) {
console.log("onScrollToBottom")
if (this.showMaxIdx >= this.items.length) {
this.showTip();
} else {

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

@ -733,7 +733,6 @@ export default {
h -= 50;
//
if (this.isShowKeyBoard || this.chatTabBox != 'none') {
console.log("减去键盘高度:", this.keyboardHeight)
h -= this.keyboardHeight;
this.scrollToBottom();
}
@ -742,7 +741,6 @@ export default {
h -= uni.getSystemInfoSync().statusBarHeight;
// #endif
this.chatMainHeight = h;
console.log("窗口高度:", this.chatMainHeight)
if (this.isShowKeyBoard || this.chatTabBox != 'none') {
this.scrollToBottom();
}
@ -806,7 +804,6 @@ export default {
this.reCalChatMainHeight();
},
resizeListener() {
console.log("resize")
let keyboardHeight = this.initHeight - window.innerHeight;
this.isShowKeyBoard = keyboardHeight > 150;
if (this.isShowKeyBoard) {
@ -815,12 +812,10 @@ export default {
this.reCalChatMainHeight();
},
focusInListener() {
console.log("focusInListener")
this.isShowKeyBoard = true;
this.reCalChatMainHeight();
},
focusOutListener() {
console.log("focusOutListener")
this.isShowKeyBoard = false;
this.reCalChatMainHeight();
},

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

@ -76,7 +76,6 @@ export default {
},
},
onBackPress() {
console.log("onBackPress")
this.sendMessageToWebView("NAV_BACK", {})
},
onLoad(options) {

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

@ -4,7 +4,7 @@
<view v-if="!group.quit" class="group-members">
<view class="member-items">
<view v-for="(member, idx) in groupMembers" :key="idx">
<view class="member-item" v-if="idx < 9">
<view class="member-item" v-if="idx < showMaxIdx">
<head-image :id="member.userId" :name="member.showNickName" :url="member.headImage" size="small"
:online="member.online"></head-image>
<view class="member-name">
@ -12,8 +12,17 @@
</view>
</view>
</view>
<view class="invite-btn" @click="onInviteMember()">
<uni-icons type="plusempty" size="20" color="#888888"></uni-icons>
<view class="member-item" @click="onInviteMember()">
<view class="tools-btn">
<uni-icons class="icon" type="plusempty" color="#888888"></uni-icons>
</view>
<view class="member-name">邀请</view>
</view>
<view v-if="isOwner" class="member-item" @click="onRemoveMember()">
<view class="tools-btn">
<text class="icon iconfont icon-remove"></text>
</view>
<view class="member-name">移除</view>
</view>
</view>
<view class="member-more" @click="onShowMoreMmeber()">{{ `查看全部群成员${groupMembers.length}` }}></view>
@ -41,15 +50,15 @@
<view v-if="group.notice" class="form-item">
<uni-notice-bar :text="group.notice" />
</view>
<view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view>
</view>
<bar-group v-if="!group.quit">
<btn-bar type="primary" title="发送消息" @tap="onSendMessage()"></btn-bar>
<btn-bar v-if="!isOwner" type="danger" title="退出群聊" @tap="onQuitGroup()"></btn-bar>
<btn-bar v-if="isOwner" type="danger" title="解散群聊" @tap="onDissolveGroup()"></btn-bar>
</bar-group>
<group-member-selector ref="removeSelector" :members="groupMembers" :group="group"
@complete="onRemoveComplete"></group-member-selector>
</view>
</template>
@ -69,6 +78,29 @@ export default {
url: `/pages/group/group-invite?id=${this.groupId}`
})
},
onRemoveMember() {
//
let hideIds = [this.group.ownerId];
this.$refs.removeSelector.init([], [], hideIds);
this.$refs.removeSelector.open();
},
onRemoveComplete(userIds) {
let data = {
groupId: this.group.id,
userIds: userIds
}
this.$http({
url: "/group/members/remove",
method: 'DELETE',
data: data
}).then(() => {
this.loadGroupMembers();
uni.showToast({
title: `您移除了${userIds.length}位成员`,
icon: 'none'
})
})
},
onShowMoreMmeber() {
uni.navigateTo({
url: `/pages/group/group-member?id=${this.groupId}`
@ -160,7 +192,6 @@ export default {
});
},
loadGroupMembers() {
console.log("loadGroupMembers")
this.$http({
url: `/group/members/${this.groupId}`,
method: "GET"
@ -176,6 +207,9 @@ export default {
},
isOwner() {
return this.group.ownerId == this.userStore.userInfo.id;
},
showMaxIdx() {
return this.isOwner ? 8 : 9;
}
},
onLoad(options) {
@ -218,17 +252,21 @@ export default {
padding-top: 8rpx;
font-size: $im-font-size-smaller;
}
}
.invite-btn {
display: flex;
justify-content: center;
align-items: center;
width: 86rpx;
height: 86rpx;
margin: 10rpx;
border: $im-border solid 2rpx;
border-radius: 10%;
.tools-btn {
display: flex;
justify-content: center;
align-items: center;
border: $im-border solid 1rpx;
border-radius: 10%;
width: 80rpx;
height: 80rpx;
.icon {
font-size: 40rpx !important;
color: $im-text-color-lighter !important;
}
}
}
}

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

@ -13,14 +13,9 @@
<view class="member-item" @click="onShowUserInfo(item.userId)">
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"></head-image>
<view class="member-name">{{ item.showNickName }}
<uni-tag v-if="item.userId == group.ownerId" text="群主" size="small" circle type="error">
</uni-tag>
<uni-tag v-if="item.userId == group.ownerId" text="群主" size="small" circle type="error"></uni-tag>
<uni-tag v-if="item.userId == userStore.userInfo.id" text="我" size="small" circle></uni-tag>
</view>
<view class="member-kick">
<button type="warn" plain v-show="isOwner && !isSelf(item.userId)" size="mini"
@click.stop="onKickOut(item)">移出群聊</button>
</view>
</view>
</template>
</virtual-scroller>
@ -44,27 +39,6 @@ export default {
url: "/pages/common/user-info?id=" + userId
})
},
onKickOut(member) {
uni.showModal({
title: '确认移出?',
content: `确定将成员'${member.showNickName}'移出群聊吗?`,
success: (res) => {
if (res.cancel)
return;
this.$http({
url: `/group/kick/${this.group.id}?userId=${member.userId}`,
method: 'DELETE'
}).then(() => {
uni.showToast({
title: `已将${member.showNickName}移出群聊`,
icon: 'none'
})
member.quit = true;
this.isModify = true;
});
}
})
},
loadGroupInfo(id) {
this.$http({
url: `/group/find/${id}`,
@ -141,12 +115,6 @@ export default {
.uni-tag {
margin-left: 5rpx;
width: 40rpx;
border: 0;
height: 30rpx;
line-height: 30rpx;
font-size: 20rpx;
text-align: center;
}
}
}

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

@ -56,7 +56,6 @@ export default {
title: '确认退出?',
success: (res) => {
if (res.confirm) {
console.log(getApp())
getApp().$vm.exit()
}
}

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

@ -59,7 +59,6 @@ export default {
errorMessage: '请输入确认密码',
}, {
validateFunction: (rule, value, data, callback) => {
console.log("validateFunction")
if (data.password != value) {
callback('两次密码输入不一致')
}

42
im-uniapp/static/icon/iconfont.css

@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 4272106 */
src: url('iconfont.ttf?t=1739084401359') format('truetype');
src: url('iconfont.ttf?t=1746119818070') format('truetype');
}
.iconfont {
@ -11,6 +11,46 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-remove:before {
content: "\e603";
}
.icon-doc:before {
content: "\e61c";
}
.icon-image:before {
content: "\e7f7";
}
.icon-top-message:before {
content: "\e6ff";
}
.icon-setting:before {
content: "\e851";
}
.icon-phone:before {
content: "\e692";
}
.icon-email:before {
content: "\e611";
}
.icon-username:before {
content: "\e60f";
}
.icon-chat-muted:before {
content: "\e634";
}
.icon-chat-unmuted:before {
content: "\ec44";
}
.icon-privacy-protocol:before {
content: "\e70a";
}

BIN
im-uniapp/static/icon/iconfont.ttf

Binary file not shown.

1
im-uniapp/store/userStore.js

@ -20,7 +20,6 @@ export default defineStore('userStore', {
url: '/user/self',
method: 'GET'
}).then((userInfo) => {
console.log(userInfo)
this.setUserInfo(userInfo);
resolve();
}).catch((res) => {

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

@ -21,7 +21,6 @@ ImCamera.prototype.openVideo = function () {
noiseSuppression: true // 开启降噪
}
}
console.log("getUserMedia")
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
console.log("摄像头打开")
this.stream = stream;

38
im-web/src/assets/iconfont/iconfont.css

@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.ttf?t=1718373714629') format('truetype');
src: url('iconfont.ttf?t=1745933248800') format('truetype');
}
.iconfont {
@ -11,6 +11,42 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-man:before {
content: "\e615";
}
.icon-girl:before {
content: "\e602";
}
.icon-no-data:before {
content: "\e61b";
}
.icon-phone:before {
content: "\e692";
}
.icon-email:before {
content: "\e610";
}
.icon-username:before {
content: "\e60f";
}
.icon-chat-unmuted:before {
content: "\ec44";
}
.icon-chat-muted:before {
content: "\e634";
}
.icon-modify:before {
content: "\e60d";
}
.icon-invite-rtc:before {
content: "\e65f";
}

BIN
im-web/src/assets/iconfont/iconfont.ttf

Binary file not shown.

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

@ -5,66 +5,71 @@
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
</div>
<div class="group-side-scrollbar">
<el-scrollbar v-show="!group.quit" ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="group-side-member-list">
<div class="group-side-invite">
<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>
<el-scrollbar v-show="!group.quit" ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="member-list">
<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-side-member" :member="member"
:showDel="false"></group-member>
<div class="tool-text">邀请</div>
<add-group-member ref="addGroupMember" :groupId="group.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="group"
@complete="onRemoveComplete"></group-member-selector>
</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-item label="群聊名称">
<el-input v-model="group.name" disabled 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="group.notice" disabled type="textarea" maxlength="1024"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="group.remarkGroupName" :disabled="!editing" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20"></el-input>
</el-form-item>
<div v-show="!group.quit" class="btn-group">
<el-button v-if="editing" type="success" @click="onSaveGroup()">保存</el-button>
<el-button v-if="!editing" type="primary" @click="editing = !editing">编辑</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
<div v-for="(member, idx) in showMembers" :key="member.id">
<group-member v-if="idx < showMaxIdx" class="group-side-member" :member="member"></group-member>
</div>
</el-form>
</div>
</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-item label="群聊名称">
<el-input v-model="group.name" disabled 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="group.notice" disabled type="textarea" maxlength="1024"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="group.remarkGroupName" :disabled="!editing" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20"></el-input>
</el-form-item>
<div v-show="!group.quit" class="btn-group">
<el-button v-if="editing" type="success" @click="onSaveGroup()">保存</el-button>
<el-button v-if="!editing" type="primary" @click="editing = !editing">编辑</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
</div>
</el-form>
</div>
</template>
<script>
import AddGroupMember from '../group/AddGroupMember.vue';
import GroupMember from '../group/GroupMember.vue';
import GroupMemberSelector from '../group/GroupMemberSelector.vue';
export default {
name: "chatGroupSide",
components: {
AddGroupMember,
GroupMember
GroupMember,
GroupMemberSelector
},
data() {
return {
searchText: "",
editing: false,
showAddGroupMember: false,
showMaxIdx: 50
}
},
@ -80,6 +85,29 @@ export default {
onClose() {
this.$emit('close');
},
onInvite() {
this.$refs.addGroupMember.open()
},
onRemove() {
//
let hideIds = [this.group.ownerId];
this.$refs.removeSelector.open(50, [], [], hideIds);
},
onRemoveComplete(members) {
let userIds = members.map(m => m.userId);
let data = {
groupId: this.group.id,
userIds: userIds
}
this.$http({
url: "/group/members/remove",
method: 'delete',
data: data
}).then(() => {
this.$emit('reload');
this.$message.success(`您移除了${userIds.length}位成员`);
})
},
loadGroupMembers() {
this.$http({
url: `/group/members/${this.group.id}`,
@ -127,12 +155,15 @@ export default {
}
},
computed: {
mine() {
return this.$store.state.userStore.userInfo;
},
ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.group.ownerId);
return member && member.showNickName;
},
isOwner() {
return this.group.ownerId == this.$store.state.userStore.userInfo.id;
return this.group.ownerId == this.mine.id;
},
showMembers() {
return this.groupMembers.filter((m) => !m.quit && m.showNickName.includes(this.searchText))
@ -156,9 +187,6 @@ export default {
padding: 10px;
}
.group-side-scrollbar {
overflow: auto;
}
.el-divider--horizontal {
margin: 0;
@ -168,7 +196,7 @@ export default {
margin-bottom: 0px !important;
}
.group-side-member-list {
.member-list {
padding: 10px;
display: flex;
align-items: center;
@ -180,14 +208,15 @@ export default {
margin-left: 5px;
}
.group-side-invite {
.member-tools {
display: flex;
flex-direction: column;
align-items: center;
width: 50px;
width: 54px;
margin-left: 5px;
.invite-member-btn {
.tool-btn {
width: 38px;
height: 38px;
line-height: 38px;
@ -201,7 +230,7 @@ export default {
}
}
.invite-member-text {
.tool-text {
font-size: 12px;
text-align: center;
width: 100%;

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

@ -117,7 +117,6 @@ export default {
e.preventDefault();
e.stopPropagation();
if (this.atIng) {
console.log('选中at的人')
this.$refs.atBox.select();
return;
}
@ -137,12 +136,10 @@ export default {
}
//
if (e.keyCode === 8) {
console.log("delete")
// dom
setTimeout(() => {
let s = this.$refs.content.innerHTML.trim();
// domdom
console.log(s);
if (s === '' || s === '<br>' || s === '<div>&nbsp;</div>') {
// dom
this.empty();
@ -179,7 +176,6 @@ export default {
blurRange.setEnd(blurRange.endContainer, endOffset);
blurRange.deleteContents()
blurRange.collapse();
console.log("onAtSelect")
this.focus();
//
let element = document.createElement('SPAN')

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

@ -19,8 +19,7 @@
<div class="chat-content-text" v-html="$emo.transform(chat.lastContent,'emoji-small')"></div>
</div>
</div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
<right-menu ref="rightMenu" @select="onSelectMenu"></right-menu>
</div>
</template>
@ -37,22 +36,15 @@ export default {
},
data() {
return {
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
},
items: [{
key: 'TOP',
name: '置顶',
icon: 'el-icon-top'
}, {
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
}]
}
menuItems: [{
key: 'TOP',
name: '置顶',
icon: 'el-icon-top'
}, {
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
}]
}
},
props: {
@ -68,11 +60,7 @@ export default {
},
methods: {
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
this.$refs.rightMenu.open(e, this.menuItems);
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);

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

@ -70,8 +70,7 @@
</div>
</div>
</div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
<right-menu ref="rightMenu" @select="onSelectMenu"></right-menu>
<chat-group-readed ref="chatGroupReadedBox" :msgInfo="msgInfo" :groupMembers="groupMembers"></chat-group-readed>
</div>
</template>
@ -118,14 +117,7 @@ export default {
},
data() {
return {
audioPlayState: 'STOP',
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
}
}
audioPlayState: 'STOP'
}
},
methods: {
@ -147,11 +139,7 @@ export default {
this.onPlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
this.$refs.rightMenu.open(e, this.menuItems);
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);

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

@ -2,8 +2,7 @@
<div class="head-image" @click="showUserInfo($event)" :style="{ cursor: isShowUserInfo ? 'pointer' : null }">
<img class="avatar-image" v-show="url" :src="url" :style="avatarImageStyle" loading="lazy" />
<div class="avatar-text" v-show="!url" :style="avatarTextStyle">
{{ name?.substring(0, 2).toUpperCase() }}
</div>
{{ name?.substring(0, 2).toUpperCase() }}</div>
<div v-show="online" class="online" title="用户当前在线"></div>
<slot></slot>
</div>
@ -15,8 +14,7 @@ export default {
data() {
return {
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
"#c9b455", "#326eb6"
]
"#c9b455", "#326eb6"]
}
},
props: {
@ -61,7 +59,11 @@ export default {
url: `/user/find/${this.id}`,
method: 'get'
}).then((user) => {
this.$store.commit("setUserInfoBoxPos", e);
let pos = {
x: e.x + 30,
y: e.y
}
this.$store.commit("setUserInfoBoxPos", pos);
this.$store.commit("showUserInfoBox", user);
})
}
@ -111,8 +113,6 @@ export default {
display: flex;
align-items: center;
justify-content: center;
//border: 1px solid #ccc;
//box-shadow: var(--im-box-shadow);
}
.online {

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

@ -1,10 +1,9 @@
<template>
<div class="right-menu-mask" @click.stop="close()" @contextmenu.prevent="close()">
<div v-if="show" class="right-menu-mask" @click.stop="close()" @contextmenu.prevent="close()">
<div class="right-menu" :style="{ 'left': pos.x + 'px', 'top': pos.y + 'px' }">
<el-menu text-color="#333333">
<el-menu-item v-for="(item) in items" :key="item.key" :title="item.name"
@click.native.stop="onSelectMenu(item)">
<!-- <span :class="item.icon"></span>-->
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
@ -16,19 +15,23 @@
export default {
name: "rightMenu",
data() {
return {}
},
props: {
pos: {
type: Object
},
items: {
type: Array
return {
show: false,
pos: {
x: 0,
y: 0,
},
items: []
}
},
methods: {
open(pos, items) {
this.pos = pos;
this.items = items;
this.show = true;
},
close() {
this.$emit("close");
this.show = false;
},
onSelectMenu(item) {
this.$emit("select", item);
@ -64,8 +67,7 @@ export default {
height: 36px;
line-height: 36px;
min-width: 100px;
text-align: left;
padding: 0 0 0 20px;
text-align: center;
&:hover {
background-color: var(--im-background-active);

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

@ -1,27 +1,32 @@
<template>
<div class="user-info-mask" @click="$emit('close')">
<div class="user-info" :style="{ left: pos.x + 'px', top: pos.y + 'px' }" @click.stop>
<div class="user-info-box">
<div class="avatar">
<head-image :name="user.nickName" :url="user.headImageThumb" :size="70" :online="user.online"
radius="10%" @click.native="showFullImage()"> </head-image>
<div class="user-info" :style="{ left: pos.x + 'px', top: pos.y + 'px' }" @click.stop>
<div class="user-info-box">
<div class="avatar">
<head-image :name="user.nickName" :url="user.headImageThumb" :size="60" :online="user.online"
@click.native="showFullImage()" radius="10%"> </head-image>
</div>
<div class="info-card">
<div class="header">
<div class="nick-name">{{ user.nickName }}</div>
<div v-if="user.sex == 0" class="icon iconfont icon-man" style="color: darkblue;"></div>
<div v-if="user.sex == 1" class="icon iconfont icon-girl" style="color: darkred;"></div>
</div>
<div>
<el-descriptions :column="1" :title="user.nickName" class="user-info-items">
<el-descriptions-item label="用户名">{{ user.userName }}
</el-descriptions-item>
<el-descriptions-item label="签名">{{ user.signature }}
</el-descriptions-item>
</el-descriptions>
<div class="info-item">
用户名: {{ user.userName }}
</div>
<div class="info-item">
个性签名: {{ user.signature }}
</div>
</div>
<el-divider content-position="center"></el-divider>
<div class="user-btn-group">
<el-button v-show="isFriend" type="primary" @click="onSendMessage()">发消息</el-button>
<el-button v-show="!isFriend" type="primary" @click="onAddFriend()">加为好友</el-button>
</div>
</div>
<el-divider content-position="center"></el-divider>
<div class="user-btn-group">
<el-button v-if="isFriend" type="primary" @click="onSendMessage()">发消息</el-button>
<el-button v-else type="primary" @click="onAddFriend()">加为好友</el-button>
</div>
</div>
</template>
<script>
@ -62,23 +67,7 @@ export default {
this.$emit("close");
},
onAddFriend() {
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: this.user.id
}
}).then(() => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id: this.user.id,
nickName: this.user.nickName,
headImage: this.user.headImageThumb,
online: this.user.online,
deleted: false
}
this.$store.commit("addFriend", friend);
})
this.$refs.applyRef.open(this.user);
},
showFullImage() {
if (this.user.headImage) {
@ -89,6 +78,9 @@ export default {
computed: {
isFriend() {
return this.$store.getters.isFriend(this.user.id);
},
isWaitingApprove() {
return this.$store.getters.isInRecvRequest(this.user.id);
}
}
}
@ -96,7 +88,7 @@ export default {
<style lang="scss">
.user-info-mask {
background-color: rgba($color: #000000, $alpha: 0);
background-color: rgba($color: #f4f4f4, $alpha: 0);
position: fixed;
left: 0;
top: 0;
@ -116,23 +108,33 @@ export default {
display: flex;
align-items: center;
.user-info-items {
margin-left: 10px;
white-space: nowrap;
overflow: hidden;
.info-card {
flex: 1;
padding-left: 10px;
.el-descriptions__header {
margin-bottom: 5px;
}
.header {
display: flex;
align-items: center;
.nick-name {
font-size: var(--im-font-size-large);
font-weight: 600;
}
.el-descriptions__title {
font-size: 18px;
.icon {
margin-left: 3px;
font-size: var(--im-font-size);
}
}
.el-descriptions-item__cell {
padding-bottom: 1px;
.info-item {
font-size: var(--im-font-size);
margin-top: 5px;
word-break: break-all;
}
}
}
.el-divider--horizontal {
@ -141,6 +143,11 @@ export default {
.user-btn-group {
text-align: center;
.wait-text {
font-size: 14px;
color: var(--im-text-color-light);
}
}
}
</style>

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

@ -15,8 +15,7 @@
</i>
</div>
</div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
<right-menu ref="rightMenu" @select="onSelectMenu"></right-menu>
<slot></slot>
</div>
</template>
@ -33,31 +32,22 @@ export default {
},
data() {
return {
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
},
items: [{
key: 'CHAT',
name: '发送消息',
icon: 'el-icon-chat-dot-round'
}, {
key: 'DELETE',
name: '删除好友',
icon: 'el-icon-delete'
}]
}
menuItems: [{
key: 'CHAT',
name: '发送消息',
icon: 'el-icon-chat-dot-round'
}, {
key: 'DELETE',
name: '删除好友',
icon: 'el-icon-delete'
}]
}
},
methods: {
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
if (this.menu) {
this.$refs.rightMenu.open(e, this.menuItems);
}
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);

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

@ -1,5 +1,5 @@
<template>
<el-dialog title="邀请好友" :visible.sync="visible" width="620px" :before-close="onClose">
<el-dialog title="邀请好友" :visible.sync="show" width="620px" :before-close="close">
<div class="agm-container">
<div class="agm-l-box">
<div class="search">
@ -30,7 +30,7 @@
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button>
<el-button @click="close()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
</el-dialog>
@ -46,13 +46,35 @@ export default {
},
data() {
return {
show: false,
searchText: "",
friends: []
}
},
methods: {
onClose() {
this.$emit("close");
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 = {
@ -72,7 +94,7 @@ export default {
}).then(() => {
this.$message.success("邀请成功");
this.$emit("reload");
this.$emit("close");
this.close()
})
}
},
@ -86,9 +108,6 @@ export default {
}
},
props: {
visible: {
type: Boolean
},
groupId: {
type: Number
},
@ -100,32 +119,7 @@ export default {
checkCount() {
return this.friends.filter((f) => f.isCheck && !f.disabled).length;
}
},
watch: {
visible: function (newData, oldData) {
if (newData) {
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);
})
}
}
}
}
</script>

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

@ -2,7 +2,6 @@
<div class="group-member">
<head-image :id="member.userId" :name="member.showNickName" :url="member.headImage" :size="38"
:online="member.online">
<div v-if="showDel" @click.stop="onDelete()" class="btn-kick el-icon-error"></div>
</head-image>
<div class="member-name">{{ member.showNickName }}</div>
</div>
@ -20,16 +19,9 @@ export default {
member: {
type: Object,
required: true
},
showDel: {
type: Boolean,
default: false
}
},
methods: {
onDelete() {
this.$emit("del", this.member);
}
}
}
</script>

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

@ -1,5 +1,5 @@
<template>
<el-dialog title="选择成员" :visible.sync="isShow" width="700px">
<el-dialog :title="title" :visible.sync="isShow" width="700px">
<div class="group-member-selector">
<div class="left-box">
<el-input placeholder="搜索" v-model="searchText">
@ -7,7 +7,8 @@
</el-input>
<virtual-scroller class="scroll-box" :items="showMembers">
<template v-slot="{ item }">
<group-member-item :member="item" @click.native="onClickMember(item)">
<group-member-item :group="group" :groupMembers="showMembers" :member="item" :menu="false"
@click.native="onClickMember(item)">
<el-checkbox :disabled="item.locked" v-model="item.checked" @change="onChange(item)"
@click.native.stop=""></el-checkbox>
</group-member-item>
@ -17,11 +18,13 @@
<div class="arrow el-icon-d-arrow-right"></div>
<div class="right-box">
<div class="select-tip"> 已勾选{{ checkedMembers.length }}位成员</div>
<div class="checked-member-list">
<div v-for="m in members" :key="m.userId">
<group-member class="member-item" v-if="m.checked" :member="m"></group-member>
<el-scrollbar class="scroll-box">
<div class="checked-member-list">
<div v-for="m in members" :key="m.userId">
<group-member class="member-item" v-if="m.checked" :member="m"></group-member>
</div>
</div>
</div>
</el-scrollbar>
</div>
</div>
<span slot="footer" class="dialog-footer">
@ -52,25 +55,30 @@ export default {
}
},
props: {
groupId: {
type: Number
group: {
type: Object
},
title: {
type: String,
default: "选择成员"
}
},
methods: {
open(maxSize, checkedIds, lockedIds) {
open(maxSize, checkedIds, lockedIds, hideIds) {
this.maxSize = maxSize;
this.isShow = true;
this.loadGroupMembers(checkedIds, lockedIds);
this.loadGroupMembers(checkedIds, lockedIds, hideIds);
},
loadGroupMembers(checkedIds, lockedIds) {
loadGroupMembers(checkedIds, lockedIds, hideIds) {
this.$http({
url: `/group/members/${this.groupId}`,
url: `/group/members/${this.group.id}`,
method: 'get'
}).then((members) => {
members.forEach((m) => {
//
m.checked = checkedIds.indexOf(m.userId) >= 0;
m.locked = lockedIds.indexOf(m.userId) >= 0;
m.hide = hideIds.indexOf(m.userId) >= 0;
});
this.members = members;
});
@ -79,13 +87,13 @@ export default {
if (!m.locked) {
m.checked = !m.checked;
}
if (this.checkedMembers.length > this.maxSize) {
if (this.maxSize > 0 && this.checkedMembers.length > this.maxSize) {
this.$message.error(`最多选择${this.maxSize}位成员`)
m.checked = false;
}
},
onChange(m) {
if (this.checkedMembers.length > this.maxSize) {
if (this.maxSize > 0 && this.checkedMembers.length > this.maxSize) {
this.$message.error(`最多选择${this.maxSize}位成员`)
m.checked = false;
}
@ -112,7 +120,6 @@ export default {
return this.members.filter((m) => !m.hide && !m.quit && m.showNickName.includes(this.searchText))
}
}
}
</script>
@ -120,25 +127,21 @@ export default {
.group-member-selector {
display: flex;
.scroll-box {
height: 400px;
}
.left-box {
width: 48%;
overflow: hidden;
border: var(--im-border);
.scroll-box {
height: 400px;
}
.el-input__inner {
border: none;
border-bottom: var(--im-border);
}
}
.arrow {
display: flex;
align-items: center;

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

@ -157,7 +157,6 @@ export default {
})
},
onReject() {
console.log("onReject")
// 退
this.API.reject(this.friend.id);
// 退

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

@ -131,7 +131,6 @@ export default {
showName: user.nickName,
headImage: user.headImageThumb,
};
console.log("chat:", chat)
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
this.$router.push("/home/chat");

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

@ -71,19 +71,24 @@
<el-divider content-position="center"></el-divider>
<el-scrollbar ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="group-member-list">
<div class="group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
<div class="member-tools">
<div class="tool-btn" title="邀请好友进群聊" @click="onInvite()">
<i class="el-icon-plus"></i>
</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 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>
<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>
<group-member v-if="idx < showMaxIdx" class="group-member" :member="member"></group-member>
</div>
</div>
</el-scrollbar>
@ -99,6 +104,7 @@ import GroupItem from '../components/group/GroupItem';
import FileUpload from '../components/common/FileUpload';
import GroupMember from '../components/group/GroupMember.vue';
import AddGroupMember from '../components/group/AddGroupMember.vue';
import GroupMemberSelector from '../components/group/GroupMemberSelector.vue';
import HeadImage from '../components/common/HeadImage.vue';
import { pinyin } from 'pinyin-pro';
@ -109,6 +115,7 @@ export default {
GroupMember,
FileUpload,
AddGroupMember,
GroupMemberSelector,
HeadImage
},
data() {
@ -156,11 +163,28 @@ export default {
//
this.loadGroupMembers();
},
onInviteMember() {
this.showAddGroupMember = true;
onInvite() {
this.$refs.addGroupMember.open();
},
onCloseAddGroupMember() {
this.showAddGroupMember = false;
onRemove() {
//
let hideIds = [this.activeGroup.ownerId];
this.$refs.removeSelector.open(50, [], [], hideIds);
},
onRemoveComplete(members) {
let userIds = members.map(m => m.userId);
let data = {
groupId: this.activeGroup.id,
userIds: userIds
}
this.$http({
url: "/group/members/remove",
method: 'delete',
data: data
}).then(() => {
this.loadGroupMembers();
this.$message.success(`您移除了${userIds.length}位成员`);
})
},
onUploadSuccess(data) {
this.activeGroup.headImage = data.originUrl;
@ -197,25 +221,6 @@ export default {
});
})
},
onKick(member) {
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/kick/${this.activeGroup.id}`,
method: 'delete',
params: {
userId: member.userId
}
}).then(() => {
this.$message.success(`已将${member.showNickName}移出群聊`);
member.quit = true;
});
})
},
onQuit() {
this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', {
confirmButtonText: '确定',
@ -455,13 +460,13 @@ export default {
margin-right: 5px;
}
.group-invite {
.member-tools {
display: flex;
flex-direction: column;
align-items: center;
width: 60px;
.invite-member-btn {
.tool-btn {
width: 38px;
height: 38px;
line-height: 38px;
@ -475,7 +480,7 @@ export default {
}
}
.invite-member-text {
.tool-text {
font-size: var(--im-font-size-smaller);
text-align: center;
width: 100%;

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

@ -1,5 +1,5 @@
<template>
<div class="home-page">
<div class="home-page" @click="$store.commit('closeUserInfoBox')">
<div class="app-container" :class="{ fullscreen: isFullscreen }">
<div class="navi-bar">
<div class="navi-bar-box">
@ -132,7 +132,6 @@ export default {
}
});
this.$wsApi.onClose((e) => {
console.log(e);
if (e.code != 3000) {
// 线
this.reconnectWs();
@ -303,7 +302,6 @@ export default {
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_DEL) {
console.log("this.$enums.MESSAGE_TYPE.GROUP_DE")
this.$store.commit("removeGroup", msg.groupId);
return;
}

Loading…
Cancel
Save