Browse Source

!137 html字符转义

Merge pull request !137 from blue/v_3.0.0
master
blue 12 months ago
committed by Gitee
parent
commit
74cfd46b9b
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 6
      im-common/src/main/java/com/bx/imcommon/enums/IMTerminalType.java
  2. 1
      im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java
  3. 10
      im-platform/src/main/java/com/bx/implatform/contant/Constant.java
  4. 21
      im-platform/src/main/java/com/bx/implatform/controller/FriendController.java
  5. 6
      im-platform/src/main/java/com/bx/implatform/controller/GroupController.java
  6. 5
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  7. 1
      im-platform/src/main/java/com/bx/implatform/controller/LoginController.java
  8. 5
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  9. 1
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java
  10. 5
      im-platform/src/main/java/com/bx/implatform/entity/Friend.java
  11. 6
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  12. 3
      im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java
  13. 32
      im-platform/src/main/java/com/bx/implatform/service/FriendService.java
  14. 2
      im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java
  15. 2
      im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java
  16. 7
      im-platform/src/main/java/com/bx/implatform/service/SensitiveWordService.java
  17. 217
      im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java
  18. 41
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  19. 80
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  20. 58
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  21. 1
      im-platform/src/main/java/com/bx/implatform/task/consumer/GroupBannedConsumerTask.java
  22. 1
      im-platform/src/main/java/com/bx/implatform/task/consumer/GroupUnbanConsumerTask.java
  23. 3
      im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java
  24. 2
      im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java
  25. 1
      im-platform/src/main/resources/application-dev.yml
  26. 4
      im-server/src/main/resources/application-dev.yml
  27. 12
      im-uniapp/.env.js
  28. 214
      im-uniapp/App.vue
  29. 18
      im-uniapp/common/emotion.js
  30. 4
      im-uniapp/common/enums.js
  31. 100
      im-uniapp/common/wssocket.js
  32. 33
      im-uniapp/components/chat-at-box/chat-at-box.vue
  33. 24
      im-uniapp/components/chat-group-readed/chat-group-readed.vue
  34. 7
      im-uniapp/components/chat-message-item/chat-message-item.vue
  35. 34
      im-uniapp/components/group-member-selector/group-member-selector.vue
  36. 56
      im-uniapp/components/virtual-scroller/virtual-scroller.vue
  37. 16
      im-uniapp/main.js
  38. 4
      im-uniapp/manifest.json
  39. 65
      im-uniapp/pages/chat/chat-box.vue
  40. 30
      im-uniapp/pages/chat/chat.vue
  41. 20
      im-uniapp/pages/common/external-link.vue
  42. 38
      im-uniapp/pages/common/user-info.vue
  43. 17
      im-uniapp/pages/friend/friend-add.vue
  44. 9
      im-uniapp/pages/friend/friend.vue
  45. 67
      im-uniapp/pages/group/group-info.vue
  46. 8
      im-uniapp/pages/group/group-invite.vue
  47. 35
      im-uniapp/pages/group/group-member.vue
  48. 26
      im-uniapp/pages/login/login.vue
  49. 38
      im-uniapp/pages/register/register.vue
  50. 65
      im-uniapp/store/chatStore.js
  51. 33
      im-uniapp/store/friendStore.js
  52. 24
      im-uniapp/store/groupStore.js
  53. 1
      im-web/package.json
  54. 8
      im-web/src/api/emotion.js
  55. 4
      im-web/src/api/enums.js
  56. 18
      im-web/src/assets/style/im.scss
  57. 6
      im-web/src/components/chat/ChatAtBox.vue
  58. 126
      im-web/src/components/chat/ChatBox.vue
  59. 318
      im-web/src/components/chat/ChatGroupReaded.vue
  60. 51
      im-web/src/components/chat/ChatGroupSide.vue
  61. 11
      im-web/src/components/chat/ChatInput.vue
  62. 7
      im-web/src/components/chat/ChatItem.vue
  63. 8
      im-web/src/components/chat/ChatMessageItem.vue
  64. 2
      im-web/src/components/common/Emotion.vue
  65. 13
      im-web/src/components/common/UserInfo.vue
  66. 74
      im-web/src/components/common/VirtualScroller.vue
  67. 21
      im-web/src/components/friend/AddFriend.vue
  68. 9
      im-web/src/components/friend/FriendItem.vue
  69. 16
      im-web/src/components/group/AddGroupMember.vue
  70. 27
      im-web/src/components/group/GroupMemberSelector.vue
  71. 63
      im-web/src/store/chatStore.js
  72. 34
      im-web/src/store/friendStore.js
  73. 30
      im-web/src/store/groupStore.js
  74. 2
      im-web/src/view/Chat.vue
  75. 139
      im-web/src/view/Friend.vue
  76. 824
      im-web/src/view/Group.vue
  77. 995
      im-web/src/view/Home.vue

6
im-common/src/main/java/com/bx/imcommon/enums/IMTerminalType.java

@ -20,7 +20,11 @@ public enum IMTerminalType {
/** /**
* pc * pc
*/ */
PC(2, "pc"); PC(2, "pc"),
/**
* 未知
*/
UNKNOW(-1, "未知");
private final Integer code; private final Integer code;

1
im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java

@ -2,7 +2,6 @@ package com.bx.implatform.config;
import com.bx.implatform.config.props.MinioProperties; import com.bx.implatform.config.props.MinioProperties;
import io.minio.MinioClient; import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

10
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_LARGE_GROUP_MEMBER = 10000L;
/** /**
* 群聊最大人数 * 普通群人数上限
*/ */
public static final Long MAX_GROUP_MEMBER = 500L; public static final Long MAX_NORMAL_GROUP_MEMBER = 500L;
} }

21
im-platform/src/main/java/com/bx/implatform/controller/FriendController.java

@ -1,11 +1,9 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit; import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.entity.Friend;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.FriendService; import com.bx.implatform.service.FriendService;
import com.bx.implatform.session.SessionContext;
import com.bx.implatform.vo.FriendVO; import com.bx.implatform.vo.FriendVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -15,7 +13,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@Tag(name = "好友") @Tag(name = "好友")
@RestController @RestController
@ -28,15 +25,7 @@ public class FriendController {
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "好友列表", description = "获取好友列表") @Operation(summary = "好友列表", description = "获取好友列表")
public Result<List<FriendVO>> findFriends() { public Result<List<FriendVO>> findFriends() {
List<Friend> friends = friendService.findFriendByUserId(SessionContext.getSession().getUserId()); return ResultUtils.success(friendService.findFriends());
List<FriendVO> vos = friends.stream().map(f -> {
FriendVO vo = new FriendVO();
vo.setId(f.getFriendId());
vo.setHeadImage(f.getFriendHeadImage());
vo.setNickName(f.getFriendNickName());
return vo;
}).collect(Collectors.toList());
return ResultUtils.success(vos);
} }
@ -62,13 +51,5 @@ public class FriendController {
return ResultUtils.success(); return ResultUtils.success();
} }
@PutMapping("/update")
@Operation(summary = "更新好友信息", description = "更新好友头像或昵称")
public Result modifyFriend(@Valid @RequestBody FriendVO vo) {
friendService.update(vo);
return ResultUtils.success();
}
} }

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

@ -31,12 +31,14 @@ public class GroupController {
return ResultUtils.success(groupService.createGroup(vo)); return ResultUtils.success(groupService.createGroup(vo));
} }
@RepeatSubmit
@Operation(summary = "修改群聊信息", description = "修改群聊信息") @Operation(summary = "修改群聊信息", description = "修改群聊信息")
@PutMapping("/modify") @PutMapping("/modify")
public Result<GroupVO> modifyGroup(@Valid @RequestBody GroupVO vo) { public Result<GroupVO> modifyGroup(@Valid @RequestBody GroupVO vo) {
return ResultUtils.success(groupService.modifyGroup(vo)); return ResultUtils.success(groupService.modifyGroup(vo));
} }
@RepeatSubmit
@Operation(summary = "解散群聊", description = "解散群聊") @Operation(summary = "解散群聊", description = "解散群聊")
@DeleteMapping("/delete/{groupId}") @DeleteMapping("/delete/{groupId}")
public Result deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) { public Result deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) {
@ -44,6 +46,7 @@ public class GroupController {
return ResultUtils.success(); return ResultUtils.success();
} }
@Operation(summary = "查询群聊", description = "查询单个群聊信息") @Operation(summary = "查询群聊", description = "查询单个群聊信息")
@GetMapping("/find/{groupId}") @GetMapping("/find/{groupId}")
public Result<GroupVO> findGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) { public Result<GroupVO> findGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) {
@ -56,6 +59,7 @@ public class GroupController {
return ResultUtils.success(groupService.findGroups()); return ResultUtils.success(groupService.findGroups());
} }
@RepeatSubmit
@Operation(summary = "邀请进群", description = "邀请好友进群") @Operation(summary = "邀请进群", description = "邀请好友进群")
@PostMapping("/invite") @PostMapping("/invite")
public Result invite(@Valid @RequestBody GroupInviteVO vo) { public Result invite(@Valid @RequestBody GroupInviteVO vo) {
@ -70,6 +74,7 @@ public class GroupController {
return ResultUtils.success(groupService.findGroupMembers(groupId)); return ResultUtils.success(groupService.findGroupMembers(groupId));
} }
@RepeatSubmit
@Operation(summary = "退出群聊", description = "退出群聊") @Operation(summary = "退出群聊", description = "退出群聊")
@DeleteMapping("/quit/{groupId}") @DeleteMapping("/quit/{groupId}")
public Result quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) { public Result quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) {
@ -77,6 +82,7 @@ public class GroupController {
return ResultUtils.success(); return ResultUtils.success();
} }
@RepeatSubmit
@Operation(summary = "踢出群聊", description = "将用户踢出群聊") @Operation(summary = "踢出群聊", description = "将用户踢出群聊")
@DeleteMapping("/kick/{groupId}") @DeleteMapping("/kick/{groupId}")
public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId, public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId,

5
im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java

@ -30,9 +30,8 @@ public class GroupMessageController {
@DeleteMapping("/recall/{id}") @DeleteMapping("/recall/{id}")
@Operation(summary = "撤回消息", description = "撤回群聊消息") @Operation(summary = "撤回消息", description = "撤回群聊消息")
public Result<Long> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) { public Result<GroupMessageVO> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) {
groupMessageService.recallMessage(id); return ResultUtils.success(groupMessageService.recallMessage(id));
return ResultUtils.success();
} }
@GetMapping("/pullOfflineMessage") @GetMapping("/pullOfflineMessage")

1
im-platform/src/main/java/com/bx/implatform/controller/LoginController.java

@ -1,6 +1,5 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.dto.LoginDTO; import com.bx.implatform.dto.LoginDTO;
import com.bx.implatform.dto.ModifyPwdDTO; import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.dto.RegisterDTO; import com.bx.implatform.dto.RegisterDTO;

5
im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java

@ -30,9 +30,8 @@ public class PrivateMessageController {
@DeleteMapping("/recall/{id}") @DeleteMapping("/recall/{id}")
@Operation(summary = "撤回消息", description = "撤回私聊消息") @Operation(summary = "撤回消息", description = "撤回私聊消息")
public Result<Long> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) { public Result<PrivateMessageVO> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) {
privateMessageService.recallMessage(id); return ResultUtils.success( privateMessageService.recallMessage(id));
return ResultUtils.success();
} }
@GetMapping("/pullOfflineMessage") @GetMapping("/pullOfflineMessage")

1
im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java

@ -1,6 +1,5 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.OnlineCheck;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.WebrtcPrivateService; import com.bx.implatform.service.WebrtcPrivateService;

5
im-platform/src/main/java/com/bx/implatform/entity/Friend.java

@ -45,6 +45,11 @@ public class Friend{
*/ */
private String friendHeadImage; private String friendHeadImage;
/**
* 是否已删除
*/
private Boolean deleted;
/** /**
* 创建时间 * 创建时间
*/ */

6
im-platform/src/main/java/com/bx/implatform/enums/MessageType.java

@ -10,6 +10,8 @@ import lombok.AllArgsConstructor;
* 30-39: UI交互类消息: 显示加载状态等 * 30-39: UI交互类消息: 显示加载状态等
* 40-49: 操作交互类消息: 语音通话视频通话消息等 * 40-49: 操作交互类消息: 语音通话视频通话消息等
* 50-60: 后台操作类消息: 用户封禁群组封禁等 * 50-60: 后台操作类消息: 用户封禁群组封禁等
* 80-89: 好友变化消息
* 90-99: 群聊变化消息
* 100-199: 单人语音通话rtc信令 * 100-199: 单人语音通话rtc信令
* 200-299: 多人语音通话rtc信令 * 200-299: 多人语音通话rtc信令
* *
@ -33,6 +35,10 @@ public enum MessageType {
USER_BANNED(50,"用户封禁"), USER_BANNED(50,"用户封禁"),
GROUP_BANNED(51,"群聊封禁"), GROUP_BANNED(51,"群聊封禁"),
GROUP_UNBAN(52,"群聊解封"), GROUP_UNBAN(52,"群聊解封"),
FRIEND_NEW(80, "新增好友"),
FRIEND_DEL(81, "删除好友"),
GROUP_NEW(90, "新增群聊"),
GROUP_DEL(91, "删除群聊"),
RTC_CALL_VOICE(100, "语音呼叫"), RTC_CALL_VOICE(100, "语音呼叫"),
RTC_CALL_VIDEO(101, "视频呼叫"), RTC_CALL_VIDEO(101, "视频呼叫"),
RTC_ACCEPT(102, "接受"), RTC_ACCEPT(102, "接受"),

3
im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java

@ -17,6 +17,7 @@ import org.springframework.context.annotation.Lazy;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
@Slf4j @Slf4j
@ -31,7 +32,7 @@ public class PrivateMessageListener implements MessageListener<PrivateMessageVO>
for(IMSendResult<PrivateMessageVO> result : results){ for(IMSendResult<PrivateMessageVO> result : results){
PrivateMessageVO messageInfo = result.getData(); PrivateMessageVO messageInfo = result.getData();
// 更新消息状态,这里只处理成功消息,失败的消息继续保持未读状态 // 更新消息状态,这里只处理成功消息,失败的消息继续保持未读状态
if (result.getCode().equals(IMSendCode.SUCCESS.code())) { if (result.getCode().equals(IMSendCode.SUCCESS.code()) && !Objects.isNull(messageInfo.getId())) {
messageIds.add(messageInfo.getId()); messageIds.add(messageInfo.getId());
log.info("消息送达,消息id:{},发送者:{},接收者:{},终端:{}", messageInfo.getId(), result.getSender().getId(), result.getReceiver().getId(), result.getReceiver().getTerminal()); log.info("消息送达,消息id:{},发送者:{},接收者:{},终端:{}", messageInfo.getId(), result.getSender().getId(), result.getReceiver().getId(), result.getReceiver().getTerminal());
} }

32
im-platform/src/main/java/com/bx/implatform/service/FriendService.java

@ -17,14 +17,27 @@ public interface FriendService extends IService<Friend> {
*/ */
Boolean isFriend(Long userId1, Long userId2); Boolean isFriend(Long userId1, Long userId2);
/**
* 查询用户的所有好友,包括已删除的
*
* @return 好友列表
*/
List<Friend> findAllFriends();
/** /**
* 查询用户的所有好友 * 查询用户的所有好友
* *
* @param userId 用户id * @param friendIds 好友id
* @return 好友列表
*/
List<Friend> findByFriendIds(List<Long> friendIds);
/**
* 查询当前用户的所有好友
*
* @return 好友列表 * @return 好友列表
*/ */
List<Friend> findFriendByUserId(Long userId); List<FriendVO> findFriends();
/** /**
* 添加好友互相建立好友关系 * 添加好友互相建立好友关系
@ -41,17 +54,20 @@ public interface FriendService extends IService<Friend> {
void delFriend(Long friendId); void delFriend(Long friendId);
/** /**
* 更新好友信息主要是头像和昵称 * 查询指定的某个好友信息
* *
* @param vo 好友vo * @param friendId 好友的用户id
* @return 好友信息
*/ */
void update(FriendVO vo); FriendVO findFriend(Long friendId);
/** /**
* 查询指定的某个好友信息 * 绑定好友关系
* *
* @param userId 好友的id
* @param friendId 好友的用户id * @param friendId 好友的用户id
* @return 好友信息 * @return 好友信息
*/ */
FriendVO findFriend(Long friendId); void bindFriend(Long userId, Long friendId);
}
}

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

@ -22,7 +22,7 @@ public interface GroupMessageService extends IService<GroupMessage> {
* *
* @param id 消息id * @param id 消息id
*/ */
void recallMessage(Long id); GroupMessageVO recallMessage(Long id);
/** /**
* 拉取离线消息只能拉取最近1个月的消息最多拉取1000条 * 拉取离线消息只能拉取最近1个月的消息最多拉取1000条

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

@ -23,7 +23,7 @@ public interface PrivateMessageService extends IService<PrivateMessage> {
* *
* @param id 消息id * @param id 消息id
*/ */
void recallMessage(Long id); PrivateMessageVO recallMessage(Long id);
/** /**
* 拉取历史聊天记录 * 拉取历史聊天记录

7
im-platform/src/main/java/com/bx/implatform/service/SensitiveWordService.java

@ -1,14 +1,7 @@
package com.bx.implatform.service; package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.implatform.dto.LoginDTO;
import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.dto.RegisterDTO;
import com.bx.implatform.entity.SensitiveWord; import com.bx.implatform.entity.SensitiveWord;
import com.bx.implatform.entity.User;
import com.bx.implatform.vo.LoginVO;
import com.bx.implatform.vo.OnlineTerminalVO;
import com.bx.implatform.vo.UserVO;
import java.util.List; import java.util.List;

217
im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java

@ -1,19 +1,31 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient;
import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.Friend;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.entity.User; import com.bx.implatform.entity.User;
import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType;
import com.bx.implatform.exception.GlobalException; import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.mapper.FriendMapper; import com.bx.implatform.mapper.FriendMapper;
import com.bx.implatform.mapper.PrivateMessageMapper;
import com.bx.implatform.mapper.UserMapper; import com.bx.implatform.mapper.UserMapper;
import com.bx.implatform.service.FriendService; import com.bx.implatform.service.FriendService;
import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession; import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.vo.FriendVO; import com.bx.implatform.vo.FriendVO;
import com.bx.implatform.vo.PrivateMessageVO;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext; import org.springframework.aop.framework.AopContext;
@ -23,8 +35,10 @@ 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.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
@ -32,15 +46,33 @@ import java.util.Objects;
@CacheConfig(cacheNames = RedisKey.IM_CACHE_FRIEND) @CacheConfig(cacheNames = RedisKey.IM_CACHE_FRIEND)
public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> implements FriendService { public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> implements FriendService {
private final PrivateMessageMapper privateMessageMapper;
private final UserMapper userMapper; private final UserMapper userMapper;
private final IMClient imClient;
@Override @Override
public List<Friend> findFriendByUserId(Long userId) { public List<Friend> findAllFriends() {
LambdaQueryWrapper<Friend> queryWrapper = Wrappers.lambdaQuery(); Long userId = SessionContext.getSession().getUserId();
queryWrapper.eq(Friend::getUserId, userId); LambdaQueryWrapper<Friend> wrapper = Wrappers.lambdaQuery();
return this.list(queryWrapper); wrapper.eq(Friend::getUserId, userId);
return this.list(wrapper);
} }
@Override
public List<Friend> findByFriendIds(List<Long> friendIds) {
Long userId = SessionContext.getSession().getUserId();
LambdaQueryWrapper<Friend> wrapper = Wrappers.lambdaQuery();
wrapper.eq(Friend::getUserId, userId);
wrapper.in(Friend::getFriendId, friendIds);
wrapper.eq(Friend::getDeleted,false);
return this.list(wrapper);
}
@Override
public List<FriendVO> findFriends() {
List<Friend> friends = this.findAllFriends();
return friends.stream().map(this::conver).collect(Collectors.toList());
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
@ -50,53 +82,37 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
throw new GlobalException("不允许添加自己为好友"); throw new GlobalException("不允许添加自己为好友");
} }
// 互相绑定好友关系 // 互相绑定好友关系
FriendServiceImpl proxy = (FriendServiceImpl) AopContext.currentProxy(); FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
proxy.bindFriend(userId, friendId); proxy.bindFriend(userId, friendId);
proxy.bindFriend(friendId, userId); proxy.bindFriend(friendId, userId);
// 推送添加好友提示
sendAddTipMessage(friendId);
log.info("添加好友,用户id:{},好友id:{}", userId, friendId); log.info("添加好友,用户id:{},好友id:{}", userId, friendId);
} }
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public void delFriend(Long friendId) { public void delFriend(Long friendId) {
long userId = SessionContext.getSession().getUserId(); Long userId = SessionContext.getSession().getUserId();
// 互相解除好友关系,走代理清理缓存 // 互相解除好友关系,走代理清理缓存
FriendServiceImpl proxy = (FriendServiceImpl) AopContext.currentProxy(); FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
proxy.unbindFriend(userId, friendId); proxy.unbindFriend(userId, friendId);
proxy.unbindFriend(friendId, userId); proxy.unbindFriend(friendId, userId);
// 推送解除好友提示
sendDelTipMessage(friendId);
log.info("删除好友,用户id:{},好友id:{}", userId, friendId); log.info("删除好友,用户id:{},好友id:{}", userId, friendId);
} }
@Cacheable(key = "#userId1+':'+#userId2") @Cacheable(key = "#userId1+':'+#userId2")
@Override @Override
public Boolean isFriend(Long userId1, Long userId2) { public Boolean isFriend(Long userId1, Long userId2) {
QueryWrapper<Friend> queryWrapper = new QueryWrapper<>(); LambdaQueryWrapper<Friend> wrapper = Wrappers.lambdaQuery();
queryWrapper.lambda() wrapper.eq(Friend::getUserId, userId1);
.eq(Friend::getUserId, userId1) wrapper.eq(Friend::getFriendId, userId2);
.eq(Friend::getFriendId, userId2); wrapper.eq(Friend::getDeleted,false);
return this.count(queryWrapper) > 0; return this.exists(wrapper);
} }
@Override
public void update(FriendVO vo) {
long userId = SessionContext.getSession().getUserId();
QueryWrapper<Friend> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda()
.eq(Friend::getUserId, userId)
.eq(Friend::getFriendId, vo.getId());
Friend f = this.getOne(queryWrapper);
if (Objects.isNull(f)) {
throw new GlobalException("对方不是您的好友");
}
f.setFriendHeadImage(vo.getHeadImage());
f.setFriendNickName(vo.getNickName());
this.updateById(f);
}
/** /**
* 单向绑定好友关系 * 单向绑定好友关系
* *
@ -105,22 +121,23 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
*/ */
@CacheEvict(key = "#userId+':'+#friendId") @CacheEvict(key = "#userId+':'+#friendId")
public void bindFriend(Long userId, Long friendId) { public void bindFriend(Long userId, Long friendId) {
QueryWrapper<Friend> queryWrapper = new QueryWrapper<>(); QueryWrapper<Friend> wrapper = new QueryWrapper<>();
queryWrapper.lambda() wrapper.lambda().eq(Friend::getUserId, userId).eq(Friend::getFriendId, friendId);
.eq(Friend::getUserId, userId) Friend friend = this.getOne(wrapper);
.eq(Friend::getFriendId, friendId); if (Objects.isNull(friend)) {
if (this.count(queryWrapper) == 0) { friend = new Friend();
Friend friend = new Friend();
friend.setUserId(userId);
friend.setFriendId(friendId);
User friendInfo = userMapper.selectById(friendId);
friend.setFriendHeadImage(friendInfo.getHeadImage());
friend.setFriendNickName(friendInfo.getNickName());
this.save(friend);
} }
friend.setUserId(userId);
friend.setFriendId(friendId);
User friendInfo = userMapper.selectById(friendId);
friend.setFriendHeadImage(friendInfo.getHeadImage());
friend.setFriendNickName(friendInfo.getNickName());
friend.setDeleted(false);
this.saveOrUpdate(friend);
// 推送好友变化信息s
sendAddFriendMessage(userId, friendId, friend);
} }
/** /**
* 单向解除好友关系 * 单向解除好友关系
* *
@ -129,30 +146,112 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
*/ */
@CacheEvict(key = "#userId+':'+#friendId") @CacheEvict(key = "#userId+':'+#friendId")
public void unbindFriend(Long userId, Long friendId) { public void unbindFriend(Long userId, Long friendId) {
QueryWrapper<Friend> queryWrapper = new QueryWrapper<>(); // 逻辑删除
queryWrapper.lambda() LambdaUpdateWrapper<Friend> wrapper = Wrappers.lambdaUpdate();
.eq(Friend::getUserId, userId) wrapper.eq(Friend::getUserId, userId);
.eq(Friend::getFriendId, friendId); wrapper.eq(Friend::getFriendId, friendId);
List<Friend> friends = this.list(queryWrapper); wrapper.set(Friend::getDeleted,true);
friends.forEach(friend -> this.removeById(friend.getId())); this.update(wrapper);
// 推送好友变化信息
sendDelFriendMessage(userId, friendId);
} }
@Override @Override
public FriendVO findFriend(Long friendId) { public FriendVO findFriend(Long friendId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
QueryWrapper<Friend> wrapper = new QueryWrapper<>(); LambdaQueryWrapper<Friend> wrapper = Wrappers.lambdaQuery();
wrapper.lambda() wrapper.eq(Friend::getUserId, session.getUserId());
.eq(Friend::getUserId, session.getUserId()) wrapper.eq(Friend::getFriendId, friendId);
.eq(Friend::getFriendId, friendId);
Friend friend = this.getOne(wrapper); Friend friend = this.getOne(wrapper);
if (Objects.isNull(friend)) { if (Objects.isNull(friend)) {
throw new GlobalException("对方不是您的好友"); throw new GlobalException("对方不是您的好友");
} }
return conver(friend);
}
private FriendVO conver(Friend f) {
FriendVO vo = new FriendVO(); FriendVO vo = new FriendVO();
vo.setId(friend.getFriendId()); vo.setId(f.getFriendId());
vo.setHeadImage(friend.getFriendHeadImage()); vo.setHeadImage(f.getFriendHeadImage());
vo.setNickName(friend.getFriendNickName()); vo.setNickName(f.getFriendNickName());
vo.setDeleted(f.getDeleted());
return vo; return vo;
} }
void sendAddFriendMessage(Long userId, Long friendId, Friend friend) {
// 推送好友状态信息
PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setSendId(friendId);
msgInfo.setRecvId(userId);
msgInfo.setSendTime(new Date());
msgInfo.setType(MessageType.FRIEND_NEW.code());
FriendVO vo = conver(friend);
msgInfo.setContent(JSON.toJSONString(vo));
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(friendId, IMTerminalType.UNKNOW.code()));
sendMessage.setRecvId(userId);
sendMessage.setData(msgInfo);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
imClient.sendPrivateMessage(sendMessage);
}
void sendDelFriendMessage(Long userId, Long friendId) {
// 推送好友状态信息
PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setSendId(friendId);
msgInfo.setRecvId(userId);
msgInfo.setSendTime(new Date());
msgInfo.setType(MessageType.FRIEND_DEL.code());
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(friendId, IMTerminalType.UNKNOW.code()));
sendMessage.setRecvId(userId);
sendMessage.setData(msgInfo);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
imClient.sendPrivateMessage(sendMessage);
}
void sendAddTipMessage(Long friendId) {
UserSession session = SessionContext.getSession();
PrivateMessage msg = new PrivateMessage();
msg.setSendId(session.getUserId());
msg.setRecvId(friendId);
msg.setContent("你们已成为好友,现在可以开始聊天了");
msg.setSendTime(new Date());
msg.setStatus(MessageStatus.UNSEND.code());
msg.setType(MessageType.TIP_TEXT.code());
privateMessageMapper.insert(msg);
// 推给对方
PrivateMessageVO messageInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(friendId);
sendMessage.setSendToSelf(false);
sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage);
// 推给自己
sendMessage.setRecvId(session.getUserId());
imClient.sendPrivateMessage(sendMessage);
}
void sendDelTipMessage(Long friendId){
UserSession session = SessionContext.getSession();
// 推送好友状态信息
PrivateMessage msg = new PrivateMessage();
msg.setSendId(session.getUserId());
msg.setRecvId(friendId);
msg.setSendTime(new Date());
msg.setType(MessageType.TIP_TEXT.code());
msg.setStatus(MessageStatus.UNSEND.code());
msg.setContent("你们的好友关系已被解除");
privateMessageMapper.insert(msg);
// 推送
PrivateMessageVO messageInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(friendId, IMTerminalType.UNKNOW.code()));
sendMessage.setRecvId(friendId);
sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage);
}
} }

41
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;
@ -31,13 +32,12 @@ import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils; import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.util.SensitiveFilterUtil; import com.bx.implatform.util.SensitiveFilterUtil;
import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.GroupMessageVO;
import com.google.common.base.Splitter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -65,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.MAX_LARGE_GROUP_MEMBER) {
// 大群的回执消息过于消耗资源,不允许发送
throw new GlobalException(String.format("当前群聊大于%s人,不支持发送回执消息", Constant.MAX_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());
// 保存消息 // 保存消息
@ -92,8 +96,9 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return msgInfo; return msgInfo;
} }
@Transactional
@Override @Override
public void recallMessage(Long id) { public GroupMessageVO recallMessage(Long id) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
GroupMessage msg = this.getById(id); GroupMessage msg = this.getById(id);
if (Objects.isNull(msg)) { if (Objects.isNull(msg)) {
@ -113,31 +118,26 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 修改数据库 // 修改数据库
msg.setStatus(MessageStatus.RECALL.code()); msg.setStatus(MessageStatus.RECALL.code());
this.updateById(msg); this.updateById(msg);
// 生成一条撤回消息
GroupMessage recallMsg = new GroupMessage();
recallMsg.setStatus(MessageStatus.UNSEND.code());
recallMsg.setType(MessageType.RECALL.code());
recallMsg.setGroupId(msg.getGroupId());
recallMsg.setSendId(session.getUserId());
recallMsg.setSendNickName(member.getShowNickName());
recallMsg.setContent(id.toString());
recallMsg.setSendTime(new Date());
this.save(recallMsg);
// 群发 // 群发
List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId());
// 不用发给自己 GroupMessageVO msgInfo = BeanUtils.copyProperties(recallMsg, GroupMessageVO.class);
userIds = userIds.stream().filter(uid -> !session.getUserId().equals(uid)).collect(Collectors.toList());
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
msgInfo.setType(MessageType.RECALL.code());
String content = String.format("'%s'撤回了一条消息", member.getShowNickName());
msgInfo.setContent(content);
msgInfo.setSendTime(new Date());
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(userIds); sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo); sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(false);
imClient.sendGroupMessage(sendMessage);
// 推给自己其他终端
msgInfo.setContent("你撤回了一条消息");
sendMessage.setSendToSelf(true);
sendMessage.setRecvIds(Collections.emptyList());
sendMessage.setRecvTerminals(Collections.emptyList());
imClient.sendGroupMessage(sendMessage); imClient.sendGroupMessage(sendMessage);
log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), msg.getGroupId(), msg.getContent()); log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), msg.getGroupId(), msg.getContent());
return msgInfo;
} }
@ -165,7 +165,6 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
wrapper.gt(GroupMessage::getId, minId) wrapper.gt(GroupMessage::getId, minId)
.gt(GroupMessage::getSendTime, minDate) .gt(GroupMessage::getSendTime, minDate)
.in(GroupMessage::getGroupId, groupIds) .in(GroupMessage::getGroupId, groupIds)
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
.orderByAsc(GroupMessage::getId); .orderByAsc(GroupMessage::getId);
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
// 通过群聊对消息进行分组 // 通过群聊对消息进行分组

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

@ -2,6 +2,7 @@ package com.bx.implatform.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -30,6 +31,7 @@ import com.bx.implatform.vo.GroupMessageVO;
import com.bx.implatform.vo.GroupVO; import com.bx.implatform.vo.GroupVO;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
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;
@ -69,9 +71,12 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
member.setRemarkNickName(vo.getRemarkNickName()); member.setRemarkNickName(vo.getRemarkNickName());
member.setRemarkGroupName(vo.getRemarkGroupName()); member.setRemarkGroupName(vo.getRemarkGroupName());
groupMemberService.save(member); groupMemberService.save(member);
GroupVO groupVo = findById(group.getId());
// 推送同步消息给自己的其他终端
sendAddGroupMessage(groupVo, Lists.newArrayList(), true);
// 返回 // 返回
log.info("创建群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); log.info("创建群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName());
return findById(group.getId()); return groupVo;
} }
@CacheEvict(key = "#vo.getId()") @CacheEvict(key = "#vo.getId()")
@ -95,7 +100,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
this.updateById(group); this.updateById(group);
} }
log.info("修改群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); log.info("修改群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName());
return convert(group,member); return convert(group, member);
} }
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@ -118,7 +123,10 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.delete(key); redisTemplate.delete(key);
// 推送解散群聊提示 // 推送解散群聊提示
this.sendTipMessage(groupId, userIds, String.format("'%s'解散了群聊", session.getNickName())); String content = String.format("'%s'解散了群聊", session.getNickName());
this.sendTipMessage(groupId, userIds, content, true);
// 推送同步消息
this.sendDelGroupMessage(groupId, userIds, false);
log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName());
} }
@ -135,7 +143,9 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key, userId.toString()); redisTemplate.opsForHash().delete(key, userId.toString());
// 推送退出群聊提示 // 推送退出群聊提示
this.sendTipMessage(groupId, List.of(userId), "您已退出群聊"); this.sendTipMessage(groupId, List.of(userId), "您已退出群聊", false);
// 推送同步消息
this.sendDelGroupMessage(groupId, Lists.newArrayList(), true);
log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
} }
@ -155,7 +165,9 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key, userId.toString()); redisTemplate.opsForHash().delete(key, userId.toString());
// 推送踢出群聊提示 // 推送踢出群聊提示
this.sendTipMessage(groupId, List.of(userId), "您已被移出群聊"); this.sendTipMessage(groupId, List.of(userId), "您已被移出群聊", false);
// 推送同步消息
this.sendDelGroupMessage(groupId, List.of(userId), false);
log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
} }
@ -170,7 +182,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
if (Objects.isNull(member)) { if (Objects.isNull(member)) {
throw new GlobalException("您未加入群聊"); throw new GlobalException("您未加入群聊");
} }
return convert(group,member); return convert(group, member);
} }
@Cacheable(key = "#groupId") @Cacheable(key = "#groupId")
@ -223,18 +235,16 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
// 群聊人数校验 // 群聊人数校验
List<GroupMember> members = groupMemberService.findByGroupId(vo.getGroupId()); List<GroupMember> members = groupMemberService.findByGroupId(vo.getGroupId());
long size = members.stream().filter(m -> !m.getQuit()).count(); long size = members.stream().filter(m -> !m.getQuit()).count();
if (vo.getFriendIds().size() + size > Constant.MAX_GROUP_MEMBER) { if (vo.getFriendIds().size() + size > Constant.MAX_LARGE_GROUP_MEMBER) {
throw new GlobalException("群聊人数不能大于" + Constant.MAX_GROUP_MEMBER + "人"); throw new GlobalException("群聊人数不能大于" + Constant.MAX_LARGE_GROUP_MEMBER + "人");
} }
// 找出好友信息 // 找出好友信息
List<Friend> friends = friendsService.findFriendByUserId(session.getUserId()); List<Friend> friends = friendsService.findByFriendIds(vo.getFriendIds());
List<Friend> friendsList = vo.getFriendIds().stream() if (vo.getFriendIds().size() != friends.size()) {
.map(id -> friends.stream().filter(f -> f.getFriendId().equals(id)).findFirst().get()).toList();
if (friendsList.size() != vo.getFriendIds().size()) {
throw new GlobalException("部分用户不是您的好友,邀请失败"); throw new GlobalException("部分用户不是您的好友,邀请失败");
} }
// 批量保存成员数据 // 批量保存成员数据
List<GroupMember> groupMembers = friendsList.stream().map(f -> { List<GroupMember> groupMembers = friends.stream().map(f -> {
Optional<GroupMember> optional = Optional<GroupMember> optional =
members.stream().filter(m -> m.getUserId().equals(f.getFriendId())).findFirst(); members.stream().filter(m -> m.getUserId().equals(f.getFriendId())).findFirst();
GroupMember groupMember = optional.orElseGet(GroupMember::new); GroupMember groupMember = optional.orElseGet(GroupMember::new);
@ -249,11 +259,16 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
if (!groupMembers.isEmpty()) { if (!groupMembers.isEmpty()) {
groupMemberService.saveOrUpdateBatch(group.getId(), groupMembers); groupMemberService.saveOrUpdateBatch(group.getId(), groupMembers);
} }
// 推送同步消息给被邀请人
for (GroupMember m : groupMembers) {
GroupVO groupVo = convert(group, m);
sendAddGroupMessage(groupVo, List.of(m.getUserId()), false);
}
// 推送进入群聊消息 // 推送进入群聊消息
List<Long> userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId());
String memberNames = groupMembers.stream().map(GroupMember::getShowNickName).collect(Collectors.joining(",")); String memberNames = groupMembers.stream().map(GroupMember::getShowNickName).collect(Collectors.joining(","));
String content = String.format("'%s'邀请'%s'加入了群聊", session.getNickName(), memberNames); String content = String.format("'%s'邀请'%s'加入了群聊", session.getNickName(), memberNames);
this.sendTipMessage(vo.getGroupId(), userIds, content); this.sendTipMessage(vo.getGroupId(), userIds, content, true);
log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(), log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(),
vo.getFriendIds()); vo.getFriendIds());
} }
@ -273,7 +288,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList()); }).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList());
} }
private void sendTipMessage(Long groupId, List<Long> recvIds, String content) { private void sendTipMessage(Long groupId, List<Long> recvIds, String content, Boolean sendToAll) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 消息入库 // 消息入库
GroupMessage message = new GroupMessage(); GroupMessage message = new GroupMessage();
@ -284,7 +299,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
message.setSendNickName(session.getNickName()); message.setSendNickName(session.getNickName());
message.setGroupId(groupId); message.setGroupId(groupId);
message.setSendId(session.getUserId()); message.setSendId(session.getUserId());
message.setRecvIds(CommaTextUtils.asText(recvIds)); message.setRecvIds(sendToAll ? "" : CommaTextUtils.asText(recvIds));
groupMessageMapper.insert(message); groupMessageMapper.insert(message);
// 推送 // 推送
GroupMessageVO msgInfo = BeanUtils.copyProperties(message, GroupMessageVO.class); GroupMessageVO msgInfo = BeanUtils.copyProperties(message, GroupMessageVO.class);
@ -312,4 +327,37 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
vo.setQuit(member.getQuit()); vo.setQuit(member.getQuit());
return vo; return vo;
} }
private void sendAddGroupMessage(GroupVO group, List<Long> recvIds, Boolean sendToSelf) {
UserSession session = SessionContext.getSession();
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setContent(JSON.toJSONString(group));
msgInfo.setType(MessageType.GROUP_NEW.code());
msgInfo.setSendTime(new Date());
msgInfo.setGroupId(group.getId());
msgInfo.setSendId(session.getUserId());
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(recvIds);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(sendToSelf);
imClient.sendGroupMessage(sendMessage);
}
private void sendDelGroupMessage(Long groupId, List<Long> recvIds, Boolean sendToSelf) {
UserSession session = SessionContext.getSession();
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.GROUP_DEL.code());
msgInfo.setSendTime(new Date());
msgInfo.setGroupId(groupId);
msgInfo.setSendId(session.getUserId());
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(recvIds);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(sendToSelf);
imClient.sendGroupMessage(sendMessage);
}
} }

58
im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java

@ -1,6 +1,5 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@ -12,7 +11,6 @@ import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.dto.PrivateMessageDTO; import com.bx.implatform.dto.PrivateMessageDTO;
import com.bx.implatform.entity.Friend;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.enums.MessageStatus; import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType; import com.bx.implatform.enums.MessageType;
@ -31,7 +29,9 @@ import org.apache.commons.lang3.time.DateUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.*; import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -74,8 +74,9 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
return msgInfo; return msgInfo;
} }
@Transactional
@Override @Override
public void recallMessage(Long id) { public PrivateMessageVO recallMessage(Long id) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
PrivateMessage msg = this.getById(id); PrivateMessage msg = this.getById(id);
if (Objects.isNull(msg)) { if (Objects.isNull(msg)) {
@ -90,26 +91,24 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
// 修改消息状态 // 修改消息状态
msg.setStatus(MessageStatus.RECALL.code()); msg.setStatus(MessageStatus.RECALL.code());
this.updateById(msg); this.updateById(msg);
// 生成一条撤回消息
PrivateMessage recallMsg = new PrivateMessage();
recallMsg.setSendId(session.getUserId());
recallMsg.setStatus(MessageStatus.UNSEND.code());
recallMsg.setSendTime(new Date());
recallMsg.setRecvId(msg.getRecvId());
recallMsg.setType(MessageType.RECALL.code());
recallMsg.setContent(id.toString());
this.save(recallMsg);
// 推送消息 // 推送消息
PrivateMessageVO msgInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class); PrivateMessageVO msgInfo = BeanUtils.copyProperties(recallMsg, PrivateMessageVO.class);
msgInfo.setType(MessageType.RECALL.code());
msgInfo.setSendTime(new Date());
msgInfo.setContent("对方撤回了一条消息");
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>(); IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(msgInfo.getRecvId()); sendMessage.setRecvId(msgInfo.getRecvId());
sendMessage.setSendToSelf(false);
sendMessage.setData(msgInfo); sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
imClient.sendPrivateMessage(sendMessage);
// 推给自己其他终端
msgInfo.setContent("你撤回了一条消息");
sendMessage.setSendToSelf(true);
sendMessage.setRecvTerminals(Collections.emptyList());
imClient.sendPrivateMessage(sendMessage); imClient.sendPrivateMessage(sendMessage);
log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}", msg.getSendId(), msg.getRecvId(), msg.getContent()); log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}", msg.getSendId(), msg.getRecvId(), msg.getContent());
return msgInfo;
} }
@Override @Override
@ -136,30 +135,19 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
@Override @Override
public void pullOfflineMessage(Long minId) { public void pullOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
if (!imClient.isOnline(session.getUserId())) {
throw new GlobalException("网络连接失败,无法拉取离线消息");
}
// 查询用户好友列表
List<Friend> friends = friendService.findFriendByUserId(session.getUserId());
if (friends.isEmpty()) {
// 关闭加载中标志
this.sendLoadingMessage(false);
return;
}
// 开启加载中标志 // 开启加载中标志
this.sendLoadingMessage(true); this.sendLoadingMessage(true);
List<Long> friendIds = friends.stream().map(Friend::getFriendId).collect(Collectors.toList());
// 获取当前用户的消息 // 获取当前用户的消息
LambdaQueryWrapper<PrivateMessage> queryWrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<PrivateMessage> wrapper = Wrappers.lambdaQuery();
// 只能拉取最近3个月的消息,移动端只拉取一个月消息 // 只能拉取最近3个月的消息,移动端只拉取一个月消息
int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3; int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3;
Date minDate = DateUtils.addMonths(new Date(), -months); Date minDate = DateUtils.addMonths(new Date(), -months);
queryWrapper.gt(PrivateMessage::getId, minId).ge(PrivateMessage::getSendTime, minDate) wrapper.gt(PrivateMessage::getId, minId);
.ne(PrivateMessage::getStatus, MessageStatus.RECALL.code()).and(wrap -> wrap.and( wrapper.ge(PrivateMessage::getSendTime, minDate);
wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).in(PrivateMessage::getRecvId, friendIds)) wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or()
.or(wp -> wp.eq(PrivateMessage::getRecvId, session.getUserId()).in(PrivateMessage::getSendId, friendIds))) .eq(PrivateMessage::getRecvId, session.getUserId()));
.orderByAsc(PrivateMessage::getId); wrapper.orderByAsc(PrivateMessage::getId);
List<PrivateMessage> messages = this.list(queryWrapper); List<PrivateMessage> messages = this.list(wrapper);
// 推送消息 // 推送消息
for (PrivateMessage m : messages) { for (PrivateMessage m : messages) {
PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class); PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class);

1
im-platform/src/main/java/com/bx/implatform/task/consumer/GroupBannedConsumerTask.java

@ -61,7 +61,6 @@ public class GroupBannedConsumerTask extends RedisMQConsumer<GroupBanDTO> {
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(Constant.SYS_USER_ID, IMTerminalType.PC.code())); sendMessage.setSender(new IMUserInfo(Constant.SYS_USER_ID, IMTerminalType.PC.code()));
sendMessage.setRecvIds(userIds); sendMessage.setRecvIds(userIds);
sendMessage.setSendResult(true);
sendMessage.setSendToSelf(false); sendMessage.setSendToSelf(false);
sendMessage.setData(msgInfo); sendMessage.setData(msgInfo);
imClient.sendGroupMessage(sendMessage); imClient.sendGroupMessage(sendMessage);

1
im-platform/src/main/java/com/bx/implatform/task/consumer/GroupUnbanConsumerTask.java

@ -60,7 +60,6 @@ public class GroupUnbanConsumerTask extends RedisMQConsumer<GroupUnbanDTO> {
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(Constant.SYS_USER_ID, IMTerminalType.PC.code())); sendMessage.setSender(new IMUserInfo(Constant.SYS_USER_ID, IMTerminalType.PC.code()));
sendMessage.setRecvIds(userIds); sendMessage.setRecvIds(userIds);
sendMessage.setSendResult(true);
sendMessage.setSendToSelf(false); sendMessage.setSendToSelf(false);
sendMessage.setData(msgInfo); sendMessage.setData(msgInfo);
imClient.sendGroupMessage(sendMessage); imClient.sendGroupMessage(sendMessage);

3
im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java

@ -19,4 +19,7 @@ public class FriendVO {
@Schema(description = "好友头像") @Schema(description = "好友头像")
private String headImage; private String headImage;
@Schema(description = "是否已删除")
private Boolean deleted;
} }

2
im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java

@ -3,6 +3,7 @@ package com.bx.implatform.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
@ -15,6 +16,7 @@ public class GroupInviteVO {
@Schema(description = "群id") @Schema(description = "群id")
private Long groupId; private Long groupId;
@Size(max = 50, message = "一次最多只能邀请50位用户")
@NotEmpty(message = "群id不可为空") @NotEmpty(message = "群id不可为空")
@Schema(description = "好友id列表不可为空") @Schema(description = "好友id列表不可为空")
private List<Long> friendIds; private List<Long> friendIds;

1
im-platform/src/main/resources/application-dev.yml

@ -8,7 +8,6 @@ spring:
redis: redis:
host: localhost host: localhost
port: 6379 port: 6379
database: 1
minio: minio:
endpoint: http://127.0.0.1:9000 #内网地址 endpoint: http://127.0.0.1:9000 #内网地址

4
im-server/src/main/resources/application-dev.yml

@ -2,6 +2,4 @@ spring:
data: data:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
database: 1

12
im-uniapp/.env.js

@ -1,12 +1,22 @@
//设置环境(打包前修改此变量) //设置环境(打包前修改此变量)
const ENV = "DEV"; const ENV = "DEV";
const UNI_APP = {} const UNI_APP = {}
// 每个会话最大消息缓存数量,-1表示不限制
UNI_APP.MAX_MESSAGE_SIZE = 3000;
// 表情包路径
UNI_APP.EMO_URL = "/static/emoji/";
// #ifdef MP-WEIXIN
// 微信小程序的本地表情包经常莫名失效,建议将表情放到服务器中
// UNI_APP.EMO_URL = "https://www.boxim.online/emoji/";
// #endif
if(ENV=="DEV"){ if(ENV=="DEV"){
UNI_APP.BASE_URL = "http://127.0.0.1:8888"; UNI_APP.BASE_URL = "http://127.0.0.1:8888";
UNI_APP.WS_URL = "ws://127.0.0.1:8878/im"; UNI_APP.WS_URL = "ws://127.0.0.1:8878/im";
// H5 走本地代理解决跨域问题 // H5 走本地代理解决跨域问题
// #ifdef H5 // #ifdef H5
UNI_APP.BASE_URL = "/api"; UNI_APP.BASE_URL = "/api";
// #endif // #endif
} }
if(ENV=="PROD"){ if(ENV=="PROD"){

214
im-uniapp/App.vue

@ -17,6 +17,7 @@ export default {
}, },
methods: { methods: {
init() { init() {
this.reconnecting = false;
this.isExit = false; this.isExit = false;
// //
this.loadStore().then(() => { this.loadStore().then(() => {
@ -30,20 +31,17 @@ export default {
}, },
initWebSocket() { initWebSocket() {
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
wsApi.init();
wsApi.connect(UNI_APP.WS_URL, loginInfo.accessToken); wsApi.connect(UNI_APP.WS_URL, loginInfo.accessToken);
wsApi.onConnect(() => { wsApi.onConnect(() => {
//
if (this.reconnecting) { if (this.reconnecting) {
this.reconnecting = false; //
uni.showToast({ this.onReconnectWs();
title: "已重新连接", } else {
icon: 'none' // 线
}) this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
} }
// 线
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
}); });
wsApi.onMessage((cmd, msgInfo) => { wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) { if (cmd == 2) {
@ -107,6 +105,15 @@ export default {
}) })
}, },
handlePrivateMessage(msg) { handlePrivateMessage(msg) {
//
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
//
let chatInfo = {
type: 'PRIVATE',
targetId: friendId
}
// //
if (msg.type == enums.MESSAGE_TYPE.LOADING) { if (msg.type == enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content)) this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content))
@ -114,10 +121,7 @@ export default {
} }
// //
if (msg.type == enums.MESSAGE_TYPE.READED) { if (msg.type == enums.MESSAGE_TYPE.READED) {
this.chatStore.resetUnreadCount({ this.chatStore.resetUnreadCount(chatInfo);
type: 'PRIVATE',
targetId: msg.recvId
})
return; return;
} }
// , // ,
@ -127,13 +131,24 @@ export default {
}) })
return; return;
} }
// //
msg.selfSend = msg.sendId == this.userStore.userInfo.id; if (msg.type == enums.MESSAGE_TYPE.RECALL) {
// id this.chatStore.recallMessage(msg, chatInfo);
let friendId = msg.selfSend ? msg.recvId : msg.sendId; return;
this.loadFriendInfo(friendId, (friend) => { }
this.insertPrivateMessage(friend, msg); //
}) if (msg.type == enums.MESSAGE_TYPE.FRIEND_NEW) {
this.friendStore.addFriend(JSON.parse(msg.content));
return;
}
//
if (msg.type == enums.MESSAGE_TYPE.FRIEND_DEL) {
this.friendStore.removeFriend(friendId);
return;
}
//
let friend = this.loadFriendInfo(friendId);
this.insertPrivateMessage(friend, msg);
}, },
insertPrivateMessage(friend, msg) { insertPrivateMessage(friend, msg) {
// //
@ -162,21 +177,31 @@ export default {
}, delayTime) }, delayTime)
return; return;
} }
let chatInfo = {
type: 'PRIVATE',
targetId: friend.id,
showName: friend.nickName,
headImage: friend.headImage
};
//
this.chatStore.openChat(chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo); if (msgType.isNormal(msg.type) || msgType.isTip(msg.type) || msgType.isAction(msg.type)) {
// let chatInfo = {
this.playAudioTip(); type: 'PRIVATE',
targetId: friend.id,
showName: friend.nickName,
headImage: friend.headImage
};
//
this.chatStore.openChat(chatInfo);
//
this.chatStore.insertMessage(msg, chatInfo);
//
this.playAudioTip();
}
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
//
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
}
// //
if (msg.type == enums.MESSAGE_TYPE.LOADING) { if (msg.type == enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content)) this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content))
@ -185,19 +210,11 @@ export default {
// //
if (msg.type == enums.MESSAGE_TYPE.READED) { if (msg.type == enums.MESSAGE_TYPE.READED) {
// //
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
}
this.chatStore.resetUnreadCount(chatInfo) this.chatStore.resetUnreadCount(chatInfo)
return; return;
} }
// //
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) { if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
}
// //
let msgInfo = { let msgInfo = {
id: msg.id, id: msg.id,
@ -205,15 +222,28 @@ export default {
readedCount: msg.readedCount, readedCount: msg.readedCount,
receiptOk: msg.receiptOk receiptOk: msg.receiptOk
}; };
this.chatStore.updateMessage(msgInfo,chatInfo) this.chatStore.updateMessage(msgInfo, chatInfo)
return; return;
} }
// //
msg.selfSend = msg.sendId == this.userStore.userInfo.id; if (msg.type == enums.MESSAGE_TYPE.RECALL) {
this.loadGroupInfo(msg.groupId, (group) => { this.chatStore.recallMessage(msg, chatInfo)
// return;
this.insertGroupMessage(group, msg); }
}) //
if (msg.type == enums.MESSAGE_TYPE.GROUP_NEW) {
this.groupStore.addGroup(JSON.parse(msg.content));
return;
}
//
if (msg.type == enums.MESSAGE_TYPE.GROUP_DEL) {
this.groupStore.removeGroup(msg.groupId);
return;
}
//
let group = this.loadGroupInfo(msg.groupId);
this.insertGroupMessage(group, msg);
}, },
handleSystemMessage(msg) { handleSystemMessage(msg) {
if (msg.type == enums.MESSAGE_TYPE.USER_BANNED) { if (msg.type == enums.MESSAGE_TYPE.USER_BANNED) {
@ -255,47 +285,45 @@ export default {
}, delayTime) }, delayTime)
return; return;
} }
let chatInfo = {
type: 'GROUP',
targetId: group.id,
showName: group.showGroupName,
headImage: group.headImageThumb
};
//
this.chatStore.openChat(chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo); if (msgType.isNormal(msg.type) || msgType.isTip(msg.type) || msgType.isAction(msg.type)) {
// let chatInfo = {
this.playAudioTip(); type: 'GROUP',
targetId: group.id,
showName: group.showGroupName,
headImage: group.headImageThumb
};
//
this.chatStore.openChat(chatInfo);
//
this.chatStore.insertMessage(msg, chatInfo);
//
this.playAudioTip();
}
}, },
loadFriendInfo(id, callback) { loadFriendInfo(id, callback) {
let friend = this.friendStore.findFriend(id); let friend = this.friendStore.findFriend(id);
if (friend) { if (!friend) {
callback(friend); console.log("未知用户:", id)
} else { friend = {
http({ id: id,
url: `/friend/find/${id}`, showNickName: "未知用户",
method: 'GET' headImage: ""
}).then((friend) => { }
this.friendStore.addFriend(friend);
callback(friend)
})
} }
return friend;
}, },
loadGroupInfo(id, callback) { loadGroupInfo(id) {
let group = this.groupStore.findGroup(id); let group = this.groupStore.findGroup(id);
if (group) { if (!group) {
callback(group); group = {
} else { id: id,
http({ showGroupName: "未知群聊",
url: `/group/find/${id}`, headImageThumb: ""
method: 'GET' }
}).then((group) => {
this.groupStore.addGroup(group);
callback(group)
})
} }
return group;
}, },
exit() { exit() {
console.log("exit"); console.log("exit");
@ -341,12 +369,11 @@ export default {
// //
this.reconnecting = true; this.reconnecting = true;
// token // token
this.reloadUserInfo().then((userInfo) => { this.userStore.loadUser().then((userInfo) => {
uni.showToast({ uni.showToast({
title: '连接已断开,尝试重新连接...', title: '连接已断开,尝试重新连接...',
icon: 'none', icon: 'none'
}) })
this.userStore.setUserInfo(userInfo);
// //
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
wsApi.reconnect(UNI_APP.WS_URL, loginInfo.accessToken); wsApi.reconnect(UNI_APP.WS_URL, loginInfo.accessToken);
@ -357,10 +384,23 @@ export default {
}, 5000) }, 5000)
}) })
}, },
reloadUserInfo() { onReconnectWs() {
return http({ this.reconnecting = false;
url: '/user/self', //
method: 'GET' const promises = [];
promises.push(this.friendStore.loadFriend());
promises.push(this.groupStore.loadGroup());
Promise.all(promises).then(() => {
uni.showToast({
title: "已重新连接",
icon: 'none'
})
// 线
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
}).catch((e) => {
console.log(e);
this.exit();
}) })
}, },
closeSplashscreen(delay) { closeSplashscreen(delay) {

18
im-uniapp/common/emotion.js

@ -1,3 +1,6 @@
import UNI_APP from '@/.env.js'
const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心', '笑哭', '拍手', '惊喜', '打气', const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心', '笑哭', '拍手', '惊喜', '打气',
'大哭', '流泪', '饥饿', '难受', '健身', '示爱', '色色', '眨眼', '暴怒', '惊恐', '大哭', '流泪', '饥饿', '难受', '健身', '示爱', '色色', '眨眼', '暴怒', '惊恐',
'思考', '头晕', '大吐', '酷笑', '翻滚', '享受', '鼻涕', '快乐', '雀跃', '微笑', '思考', '头晕', '大吐', '酷笑', '翻滚', '享受', '鼻涕', '快乐', '雀跃', '微笑',
@ -13,30 +16,23 @@ let containEmoji = (content) => {
} }
let transform = (content, extClass) => { let transform = (content, extClass) => {
return content.replace(regex, (emoText) => { return content.replace(regex, (emoText)=>{
// 将匹配结果替换表情图片 // 将匹配结果替换表情图片
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if (idx == -1) { if (idx == -1) {
return emoText; return emoText;
} }
let path = textToPath(emoText, true); let path = textToPath(emoText);
let img = `<img src="${path}" class="${extClass}"/>`; let img = `<img src="${path}" class="${extClass}"/>`;
return img; return img;
}); });
} }
let textToPath = (emoText, isRichText) => { let textToPath = (emoText) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
let path = `/static/emoji/${idx}.gif`; return UNI_APP.EMO_URL + idx + ".gif";
// #ifdef MP-WEIXIN
// 小程序的表情要去掉最前面"/"(但有的时候又不能去掉,十分奇怪)
if (isRichText) {
path = path.slice(1);
}
// #endif
return path;
} }
export default { export default {

4
im-uniapp/common/enums.js

@ -14,6 +14,10 @@ const MESSAGE_TYPE = {
ACT_RT_VOICE: 40, ACT_RT_VOICE: 40,
ACT_RT_VIDEO: 41, ACT_RT_VIDEO: 41,
USER_BANNED: 50, USER_BANNED: 50,
FRIEND_NEW: 80,
FRIEND_DEL: 81,
GROUP_NEW: 90,
GROUP_DEL: 91,
RTC_CALL_VOICE: 100, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101, RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,

100
im-uniapp/common/wssocket.js

@ -1,20 +1,33 @@
let wsurl = "";
let accessToken = ""; let accessToken = "";
let messageCallBack = null; let messageCallBack = null;
let closeCallBack = null; let closeCallBack = null;
let connectCallBack = null; let connectCallBack = null;
let isConnect = false; //连接标识 避免重复连接 let isConnect = false; //连接标识 避免重复连接
let rec = null; let rec = null;
let isInit = false;
let lastConnectTime = new Date(); // 最后一次连接时间 let lastConnectTime = new Date(); // 最后一次连接时间
let socketTask = null;
let init = () => { let connect = (wsurl, token) => {
// 防止重复初始化 accessToken = token;
if (isInit) { if (isConnect) {
return; return;
} }
isInit = true; lastConnectTime = new Date();
uni.onSocketOpen((res) => { socketTask = uni.connectSocket({
url: wsurl,
success: (res) => {
console.log("websocket连接成功");
},
fail: (e) => {
console.log(e);
console.log("websocket连接失败,10s后重连");
setTimeout(() => {
connect();
}, 10000)
}
});
socketTask.onOpen((res) => {
console.log("WebSocket连接已打开"); console.log("WebSocket连接已打开");
isConnect = true; isConnect = true;
// 发送登录命令 // 发送登录命令
@ -24,12 +37,12 @@ let init = () => {
accessToken: accessToken accessToken: accessToken
} }
}; };
uni.sendSocketMessage({ socketTask.send({
data: JSON.stringify(loginInfo) data: JSON.stringify(loginInfo)
}); });
}) })
uni.onSocketMessage((res) => { socketTask.onMessage((res) => {
let sendInfo = JSON.parse(res.data) let sendInfo = JSON.parse(res.data)
if (sendInfo.cmd == 0) { if (sendInfo.cmd == 0) {
heartCheck.start() heartCheck.start()
@ -45,54 +58,31 @@ let init = () => {
} }
}) })
uni.onSocketClose((res) => { socketTask.onClose((res) => {
console.log('WebSocket连接关闭') console.log('WebSocket连接关闭')
isConnect = false; isConnect = false;
closeCallBack && closeCallBack(res); closeCallBack && closeCallBack(res);
}) })
uni.onSocketError((e) => { socketTask.onError((e) => {
console.log(e) console.log(e)
isConnect = false; isConnect = false;
// APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连 // APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连
closeCallBack && closeCallBack({ code: 1006 }); closeCallBack && closeCallBack({ code: 1006 });
}) })
};
let connect = (url, token) => {
wsurl = url;
accessToken = token;
if (isConnect) {
return;
}
lastConnectTime = new Date();
uni.connectSocket({
url: wsurl,
success: (res) => {
console.log("websocket连接成功");
},
fail: (e) => {
console.log(e);
console.log("websocket连接失败,10s后重连");
setTimeout(() => {
connect();
}, 10000)
}
});
} }
//定义重连函数 //定义重连函数
let reconnect = (wsurl, accessToken) => { let reconnect = (wsurl, accessToken) => {
console.log("尝试重新连接"); console.log("尝试重新连接");
if (isConnect) { if (isConnect) {
//如果已经连上就不在重连了
return; return;
} }
// 延迟10秒重连 避免过多次过频繁请求重连 // 延迟10秒重连 避免过多次过频繁请求重连
let timeDiff = new Date().getTime() - lastConnectTime.getTime() let timeDiff = new Date().getTime() - lastConnectTime.getTime()
let delay = timeDiff < 10000 ? 10000 - timeDiff : 0; let delay = timeDiff < 10000 ? 10000 - timeDiff : 0;
rec && clearTimeout(rec); rec && clearTimeout(rec);
rec = setTimeout(function () { rec = setTimeout(function() {
connect(wsurl, accessToken); connect(wsurl, accessToken);
}, delay); }, delay);
}; };
@ -102,7 +92,7 @@ let close = (code) => {
if (!isConnect) { if (!isConnect) {
return; return;
} }
uni.closeSocket({ socketTask.close({
code: code, code: code,
complete: (res) => { complete: (res) => {
console.log("关闭websocket连接"); console.log("关闭websocket连接");
@ -115,39 +105,28 @@ let close = (code) => {
}; };
//心跳设置 // 心跳设置
var heartCheck = { let heartCheck = {
timeout: 10000, //每段时间发送一次心跳包 这里设置为30s timeout: 20000, // 每段时间发送一次心跳包 这里设置为20s
timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象) timeoutObj: null, // 延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
start: function () { start: function() {
if (isConnect) { if (isConnect) {
console.log('发送WebSocket心跳') console.log('发送WebSocket心跳')
let heartBeat = { let heartBeat = {
cmd: 1, cmd: 1,
data: {} data: {}
}; };
uni.sendSocketMessage({ sendMessage(JSON.stringify(heartBeat))
data: JSON.stringify(heartBeat),
fail(res) {
console.log(res);
}
})
} }
}, },
reset: function () { reset: function() {
clearTimeout(this.timeoutObj); clearTimeout(this.timeoutObj);
this.timeoutObj = setTimeout(function () { this.timeoutObj = setTimeout(() => heartCheck.start(), this.timeout);
heartCheck.start();
}, this.timeout);
} }
};
} let sendMessage = (message) => {
socketTask.send({ data: message })
// 实际调用的方法
function sendMessage(agentData) {
uni.sendSocketMessage({
data: agentData
})
} }
let onConnect = (callback) => { let onConnect = (callback) => {
@ -155,19 +134,18 @@ let onConnect = (callback) => {
} }
function onMessage(callback) { let onMessage = (callback) => {
messageCallBack = callback; messageCallBack = callback;
} }
function onClose(callback) { let onClose = (callback) => {
closeCallBack = callback; closeCallBack = callback;
} }
// 将方法暴露出去 // 将方法暴露出去
export { export {
init,
connect, connect,
reconnect, reconnect,
close, close,

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>

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

@ -1,13 +1,12 @@
<template> <template>
<view class="chat-msg-item"> <view class="chat-msg-item">
<view class="chat-msg-tip" <view class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
v-if="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{ msgInfo.content }} {{ msgInfo.content }}
</view> </view>
<view class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME"> <view class="chat-msg-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{ $date.toTimeText(msgInfo.sendTime) }} {{ $date.toTimeText(msgInfo.sendTime) }}
</view> </view>
<view class="chat-msg-normal" v-if="isNormal" :class="{ 'chat-msg-mine': msgInfo.selfSend }"> <view class="chat-msg-normal" v-else-if="isNormal" :class="{ 'chat-msg-mine': msgInfo.selfSend }">
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage" <head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
:name="showName" size="small"></head-image> :name="showName" size="small"></head-image>
<view class="chat-msg-content"> <view class="chat-msg-content">

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>

16
im-uniapp/main.js

@ -19,17 +19,15 @@ import arrowBar from '@/components/bar/arrow-bar'
import btnBar from '@/components/bar/btn-bar' import btnBar from '@/components/bar/btn-bar'
import switchBar from '@/components/bar/switch-bar' import switchBar from '@/components/bar/switch-bar'
// #ifdef H5 // #ifdef H5
// import VConsole from 'vconsole' import * as recorder from './common/recorder-h5';
// new VConsole(); import ImageResize from "quill-image-resize-mp";
// #endif import Quill from "quill";
// #ifdef H5 // 以下组件用于兼容部分手机聊天边框无法输入的问题
import ImageResize from "quill-image-resize-mp";
import Quill from "quill";
window.Quill = Quill; window.Quill = Quill;
window.ImageResize = { default: ImageResize }; window.ImageResize = { default: ImageResize };
// #endif // 调试器
// #ifdef H5 // import VConsole from 'vconsole'
import * as recorder from './common/recorder-h5'; // new VConsole();
// #endif // #endif
// #ifndef H5 // #ifndef H5
import * as recorder from './common/recorder-app'; import * as recorder from './common/recorder-app';

4
im-uniapp/manifest.json

@ -2,8 +2,8 @@
"name" : "盒子IM", "name" : "盒子IM",
"appid" : "__UNI__69DD57A", "appid" : "__UNI__69DD57A",
"description" : "", "description" : "",
"versionName" : "3.1.0", "versionName" : "3.4.0",
"versionCode" : 3100, "versionCode" : 3400,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {

65
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>
@ -94,7 +94,7 @@
<scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true" <scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true"
:style="{height: keyboardHeight+'px'}"> :style="{height: keyboardHeight+'px'}">
<view class="emotion-item-list"> <view class="emotion-item-list">
<image class="emotion-item emoji-large" :title="emoText" :src="$emo.textToPath(emoText,false)" <image class="emotion-item emoji-large" :title="emoText" :src="$emo.textToPath(emoText)"
v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit" v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
lazy-load="true"></image> lazy-load="true"></image>
</view> </view>
@ -119,7 +119,7 @@ export default {
data() { data() {
return { return {
chat: {}, chat: {},
friend: {}, userInfo: {},
group: {}, group: {},
groupMembers: [], groupMembers: [],
isReceipt: false, // isReceipt: false, //
@ -289,7 +289,7 @@ export default {
let receiptText = this.isReceipt ? "【回执消息】" : ""; let receiptText = this.isReceipt ? "【回执消息】" : "";
let atText = this.createAtText(); let atText = this.createAtText();
let msgInfo = { let msgInfo = {
content: receiptText + sendText + atText, content: receiptText + this.html2Escape(sendText) + atText,
atUserIds: this.atUserIds, atUserIds: this.atUserIds,
receipt: this.isReceipt, receipt: this.isReceipt,
type: 0 type: 0
@ -370,7 +370,7 @@ export default {
} }
}, },
selectEmoji(emoText) { selectEmoji(emoText) {
let path = this.$emo.textToPath(emoText, true) let path = this.$emo.textToPath(emoText)
// //
this.isReadOnly = true; this.isReadOnly = true;
this.isEmpty = false; this.isEmpty = false;
@ -519,12 +519,9 @@ export default {
this.$http({ this.$http({
url: url, url: url,
method: 'DELETE' method: 'DELETE'
}).then(() => { }).then((m) => {
msgInfo = JSON.parse(JSON.stringify(msgInfo)); m.selfSend = true;
msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL; this.chatStore.recallMessage(m, this.chat);
msgInfo.content = '你撤回了一条消息';
msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
this.chatStore.insertMessage(msgInfo, this.chat);
}) })
} }
} }
@ -581,7 +578,7 @@ export default {
}) })
} else { } else {
uni.navigateTo({ uni.navigateTo({
url: "/pages/common/user-info?id=" + this.friend.id url: "/pages/common/user-info?id=" + this.userInfo.id
}) })
} }
}, },
@ -660,15 +657,29 @@ export default {
this.groupMembers = groupMembers; this.groupMembers = groupMembers;
}); });
}, },
updateFriendInfo() {
if (this.isFriend) {
// storestore
let friend = JSON.parse(JSON.stringify(this.friend));
friend.headImage = this.userInfo.headImageThumb;
friend.nickName = this.userInfo.nickName;
friend.showNickName = friend.remarkNickName ? friend.remarkNickName : friend.nickName;
//
this.friendStore.updateFriend(friend);
//
this.chatStore.updateChatFromFriend(friend);
} else {
this.chatStore.updateChatFromUser(this.userInfo);
}
},
loadFriend(friendId) { loadFriend(friendId) {
// //
this.$http({ this.$http({
url: `/user/find/${friendId}`, url: `/user/find/${friendId}`,
method: 'GET' method: 'GET'
}).then((friend) => { }).then((userInfo) => {
this.friend = friend; this.userInfo = userInfo;
this.chatStore.updateChatFromFriend(friend); this.updateFriendInfo();
this.friendStore.updateFriend(friend);
}) })
}, },
rpxTopx(rpx) { rpxTopx(rpx) {
@ -677,6 +688,16 @@ export default {
let px = info.windowWidth * rpx / 750; let px = info.windowWidth * rpx / 750;
return Math.floor(rpx); return Math.floor(rpx);
}, },
html2Escape(strHtml) {
return strHtml.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
} [c];
});
},
sendMessageRequest(msgInfo) { sendMessageRequest(msgInfo) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// "" // ""
@ -810,7 +831,7 @@ export default {
} }
if (this.chat.type == "PRIVATE") { if (this.chat.type == "PRIVATE") {
msgInfo.recvId = this.mine.id msgInfo.recvId = this.mine.id
msgInfo.content = "该用户已被管理员封禁,原因:" + this.friend.reason msgInfo.content = "该用户已被管理员封禁,原因:" + this.userInfo.reason
} else { } else {
msgInfo.groupId = this.group.id; msgInfo.groupId = this.group.id;
msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason
@ -826,6 +847,9 @@ export default {
mine() { mine() {
return this.userStore.userInfo; return this.userStore.userInfo;
}, },
friend() {
return this.friendStore.findFriend(this.userInfo.id);
},
title() { title() {
if (!this.chat) { if (!this.chat) {
return ""; return "";
@ -853,7 +877,7 @@ export default {
return this.chat.unreadCount; return this.chat.unreadCount;
}, },
isBanned() { isBanned() {
return (this.chat.type == "PRIVATE" && this.friend.isBanned) || return (this.chat.type == "PRIVATE" && this.userInfo.isBanned) ||
(this.chat.type == "GROUP" && this.group.isBanned) (this.chat.type == "GROUP" && this.group.isBanned)
}, },
atUserItems() { atUserItems() {
@ -872,6 +896,9 @@ export default {
} }
}) })
return atUsers; return atUsers;
},
memberSize() {
return this.groupMembers.filter(m => !m.quit).length;
} }
}, },
watch: { watch: {

30
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'
} }
] ]
} }
} }
@ -114,8 +113,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 +132,7 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.tab-page { .tab-page {
position: relative; position: relative;
display: flex; display: flex;
@ -149,7 +152,6 @@ export default {
width: 100%; width: 100%;
height: 120rpx; height: 120rpx;
background: white; background: white;
color: $im-text-color-lighter; color: $im-text-color-lighter;
.loading-box { .loading-box {

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>

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

@ -9,17 +9,17 @@
<view class="info-item"> <view class="info-item">
<view class="info-primary"> <view class="info-primary">
<text class="info-username"> <text class="info-username">
{{ userInfo.userName }} {{ userInfo.nickName }}
</text> </text>
<text v-show="userInfo.sex == 0" class="iconfont icon-man" color="darkblue"></text> <text v-show="userInfo.sex == 0" class="iconfont icon-man" color="darkblue"></text>
<text v-show="userInfo.sex == 1" class="iconfont icon-girl" color="darkred"></text> <text v-show="userInfo.sex == 1" class="iconfont icon-girl" color="darkred"></text>
</view> </view>
<view class="info-text"> <view class="info-text">
<text class="label-text"> <text class="label-text">
昵称: 用户名:
</text> </text>
<text class="content-text"> <text class="content-text">
{{ userInfo.nickName }} {{ userInfo.userName }}
</text> </text>
</view> </view>
<view class="info-text"> <view class="info-text">
@ -82,7 +82,8 @@ export default {
id: this.userInfo.id, id: this.userInfo.id,
nickName: this.userInfo.nickName, nickName: this.userInfo.nickName,
headImage: this.userInfo.headImageThumb, headImage: this.userInfo.headImageThumb,
online: this.userInfo.online online: this.userInfo.online,
deleted: false
} }
this.friendStore.addFriend(friend); this.friendStore.addFriend(friend);
uni.showToast({ uni.showToast({
@ -113,20 +114,17 @@ export default {
}) })
}, },
updateFriendInfo() { updateFriendInfo() {
// storestore if (this.isFriend) {
let friend = JSON.parse(JSON.stringify(this.friendInfo)); // storestore
friend.headImage = this.userInfo.headImageThumb; let friend = JSON.parse(JSON.stringify(this.friendInfo));
friend.nickName = this.userInfo.nickName; friend.headImage = this.userInfo.headImageThumb;
this.$http({ friend.nickName = this.userInfo.nickName;
url: "/friend/update",
method: "PUT",
data: friend
}).then(() => {
// //
this.friendStore.updateFriend(friend); this.friendStore.updateFriend(friend);
// //
this.chatStore.updateChatFromFriend(this.userInfo); this.chatStore.updateChatFromFriend(this.userInfo);
}) }
}, },
loadUserInfo(id) { loadUserInfo(id) {
this.$http({ this.$http({
@ -135,21 +133,17 @@ export default {
}).then((user) => { }).then((user) => {
this.userInfo = user; this.userInfo = user;
// //
if (this.isFriend && (this.userInfo.headImageThumb != this.friendInfo.headImage || this.updateFriendInfo()
this.userInfo.nickName != this.friendInfo.nickName)) {
this.updateFriendInfo()
}
}) })
} }
}, },
computed: { computed: {
isFriend() { isFriend() {
return !!this.friendInfo; return this.friendStore.isFriend(this.userInfo.id);
}, },
friendInfo() { friendInfo() {
let friends = this.friendStore.friends; return this.friendStore.findFriend(this.userInfo.id);
let friend = friends.find((f) => f.id == this.userInfo.id);
return friend;
} }
}, },
onLoad(options) { onLoad(options) {

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

@ -14,11 +14,11 @@
<head-image :id="user.id" :name="user.nickName" :online="user.online" <head-image :id="user.id" :name="user.nickName" :online="user.online"
:url="user.headImage"></head-image> :url="user.headImage"></head-image>
<view class="user-info"> <view class="user-info">
<view class="user-name"> <view class="nick-name">
<view>{{ user.userName }}</view> <view>{{ user.nickName }}</view>
<uni-tag v-if="user.status == 1" circle type="error" text="已注销" size="small"></uni-tag> <uni-tag v-if="user.status == 1" circle type="error" text="已注销" size="small"></uni-tag>
</view> </view>
<view class="nick-name">{{ `昵称:${user.nickName}`}}</view> <view class="user-name">用户名:{{ `${user.userName}`}}</view>
</view> </view>
<view class="user-btns"> <view class="user-btns">
<button type="primary" v-show="!isFriend(user.id)" size="mini" <button type="primary" v-show="!isFriend(user.id)" size="mini"
@ -61,7 +61,8 @@ export default {
id: user.id, id: user.id,
nickName: user.nickName, nickName: user.nickName,
headImage: user.headImage, headImage: user.headImage,
online: user.online online: user.online,
delete: false
} }
this.friendStore.addFriend(friend); this.friendStore.addFriend(friend);
uni.showToast({ uni.showToast({
@ -76,9 +77,7 @@ export default {
}) })
}, },
isFriend(userId) { isFriend(userId) {
let friends = this.friendStore.friends; return this.friendStore.isFriend(userId);
let friend = friends.find((f) => f.id == userId);
return !!friend;
} }
} }
} }
@ -114,7 +113,7 @@ export default {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
.user-name { .nick-name {
display: flex; display: flex;
flex: 1; flex: 1;
font-size: $im-font-size-large; font-size: $im-font-size-large;
@ -129,7 +128,7 @@ export default {
} }
} }
.nick-name { .user-name {
display: flex; display: flex;
font-size: $im-font-size-smaller; font-size: $im-font-size-smaller;
color: $im-text-color-lighter; color: $im-text-color-lighter;

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

@ -8,7 +8,7 @@
placeholder="点击搜索好友"></uni-search-bar> placeholder="点击搜索好友"></uni-search-bar>
</view> </view>
</view> </view>
<view class="friend-tip" v-if="friends.length == 0"> <view class="friend-tip" v-if="friendIdx.length == 0">
温馨提示您现在还没有任何好友快点击右上方'+'按钮添加好友吧~ 温馨提示您现在还没有任何好友快点击右上方'+'按钮添加好友吧~
</view> </view>
<view class="friend-items" v-else> <view class="friend-items" v-else>
@ -56,14 +56,11 @@ export default {
} }
}, },
computed: { computed: {
friends() {
return this.friendStore.friends;
},
friendGroupMap() { friendGroupMap() {
// //
let groupMap = new Map(); let groupMap = new Map();
this.friends.forEach((f) => { this.friendStore.friends.forEach((f) => {
if (this.searchText && !f.nickName.includes(this.searchText)) { if (f.deleted || (this.searchText && !f.nickName.includes(this.searchText))) {
return; return;
} }
let letter = this.firstLetter(f.nickName).toUpperCase(); let letter = this.firstLetter(f.nickName).toUpperCase();

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

@ -35,16 +35,16 @@
<view class="label">我在本群的昵称</view> <view class="label">我在本群的昵称</view>
<view class="value">{{group.showNickName}}</view> <view class="value">{{group.showNickName}}</view>
</view> </view>
<view v-if="group.notice" class="form-item" > <view v-if="group.notice" class="form-item">
<view class="label">群公告</view> <view class="label">群公告</view>
</view> </view>
<view v-if="group.notice" class="form-item" > <view v-if="group.notice" class="form-item">
<uni-notice-bar :text="group.notice" /> <uni-notice-bar :text="group.notice" />
</view> </view>
<view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view> <view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view>
</view> </view>
<bar-group v-if="!group.quit"> <bar-group v-if="!group.quit">
<btn-bar type="primary" title="发送消息" @tap="onSendMessage()"></btn-bar> <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="onQuitGroup()"></btn-bar>
@ -103,21 +103,18 @@ export default {
url: `/group/quit/${this.groupId}`, url: `/group/quit/${this.groupId}`,
method: 'DELETE' method: 'DELETE'
}).then(() => { }).then(() => {
uni.showModal({ uni.showToast({
title: `退出成功`, title: `您退出了群聊'${this.group.name}'`,
content: `您已退出群聊'${this.group.name}'`, icon: "none"
showCancel: false,
success: () => {
setTimeout(() => {
uni.switchTab({
url: "/pages/group/group"
});
this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this
.groupId);
}, 100)
}
}) })
setTimeout(() => {
uni.switchTab({
url: "/pages/group/group"
});
this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this
.groupId);
}, 1500)
}); });
} }
}); });
@ -133,25 +130,21 @@ export default {
url: `/group/delete/${this.groupId}`, url: `/group/delete/${this.groupId}`,
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
uni.showModal({ uni.showToast({
title: `解散成功`, title: `您解散了群聊'${this.group.name}'`,
content: `群聊'${this.group.name}'已解散`, icon: "none"
showCancel: false,
success: () => {
setTimeout(() => {
uni.switchTab({
url: "/pages/group/group"
});
this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this
.groupId);
}, 100)
}
}) })
setTimeout(() => {
uni.switchTab({
url: "/pages/group/group"
});
this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this
.groupId);
}, 1500)
}); });
} }
}); });
}, },
loadGroupInfo() { loadGroupInfo() {
this.$http({ this.$http({
@ -256,14 +249,14 @@ export default {
background: white; background: white;
align-items: center; align-items: center;
margin-top: 2rpx; margin-top: 2rpx;
.label { .label {
width: 220rpx; width: 220rpx;
line-height: 100rpx; line-height: 100rpx;
font-size: $im-font-size; font-size: $im-font-size;
white-space: nowrap; white-space: nowrap;
} }
.value { .value {
flex: 1; flex: 1;
text-align: right; text-align: right;
@ -274,9 +267,9 @@ export default {
overflow: hidden; overflow: hidden;
} }
} }
.group-edit { .group-edit {
padding: 10rpx 40rpx 30rpx 40rpx ; padding: 10rpx 40rpx 30rpx 40rpx;
text-align: center; text-align: center;
background: white; background: white;
font-size: $im-font-size-small; font-size: $im-font-size-small;

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

@ -8,9 +8,9 @@
</view> </view>
<view class="friend-items"> <view class="friend-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="friend in friendItems" v-show="!searchText || friend.nickName.includes(searchText)" <view v-for="friend in friendItems" :key="friend.id">
:key="friend.id"> <view v-show="!searchText || friend.nickName.includes(searchText)" class="friend-item"
<view class="friend-item" @click="onSwitchChecked(friend)" @click="onSwitchChecked(friend)"
:class="{ checked: friend.checked, disabled: friend.disabled }"> :class="{ checked: friend.checked, disabled: friend.disabled }">
<head-image :name="friend.nickName" :online="friend.online" <head-image :name="friend.nickName" :online="friend.online"
:url="friend.headImage"></head-image> :url="friend.headImage"></head-image>
@ -79,7 +79,7 @@ export default {
initFriendItems() { initFriendItems() {
this.friendItems = []; this.friendItems = [];
let friends = this.friendStore.friends; let friends = this.friendStore.friends;
friends.forEach((f => { friends.filter(f => !f.deleted).forEach((f => {
let item = { let item = {
id: f.id, id: f.id,
headImage: f.headImage, headImage: f.headImage,

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) {

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

@ -1,15 +1,17 @@
<template> <template>
<view class="login"> <view class="login">
<view class="title">欢迎登录</view> <view class="title">欢迎登录</view>
<uni-forms :modelValue="loginForm" :rules="rules" validate-trigger="bind"> <view class="form">
<uni-forms-item name="userName"> <uni-forms :modelValue="loginForm" :rules="rules" validate-trigger="bind">
<uni-easyinput type="text" v-model="loginForm.userName" prefix-icon="person" placeholder="用户名" /> <uni-forms-item name="userName">
</uni-forms-item> <uni-easyinput type="text" v-model="loginForm.userName" prefix-icon="person" placeholder="用户名" />
<uni-forms-item name="password"> </uni-forms-item>
<uni-easyinput type="password" v-model="loginForm.password" prefix-icon="locked" placeholder="密码" /> <uni-forms-item name="password">
</uni-forms-item> <uni-easyinput type="password" v-model="loginForm.password" prefix-icon="locked" placeholder="密码" />
<button class="btn-submit" @click="submit" type="primary">登录</button> </uni-forms-item>
</uni-forms> <button class="btn-submit" @click="submit" type="primary">登录</button>
</uni-forms>
</view>
<navigator class="nav-register" url="/pages/register/register"> <navigator class="nav-register" url="/pages/register/register">
没有账号,前往注册 没有账号,前往注册
</navigator> </navigator>
@ -69,10 +71,10 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.login { .login {
.title { .title {
padding-top: 150rpx; padding-top: 250rpx;
padding-bottom: 50rpx; padding-bottom: 50rpx;
color: $im-color-primary; color: $im-color-primary;
text-align: center; text-align: center;
@ -80,7 +82,7 @@ export default {
font-weight: bold; font-weight: bold;
} }
.uni-forms { .form {
padding: 50rpx; padding: 50rpx;
.btn-submit { .btn-submit {

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

@ -1,21 +1,23 @@
<template> <template>
<view class="register"> <view class="register">
<view class="title">欢迎注册</view> <view class="title">欢迎注册</view>
<uni-forms ref="form" :modelValue="dataForm" :rules="rules" validate-trigger="bind" label-width="80px"> <view class="form">
<uni-forms-item name="userName" label="用户名"> <uni-forms ref="form" :modelValue="dataForm" :rules="rules" validate-trigger="bind" label-width="80px">
<uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" /> <uni-forms-item name="userName" label="用户名">
</uni-forms-item> <uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" />
<uni-forms-item name="nickName" label="昵称"> </uni-forms-item>
<uni-easyinput type="text" v-model="dataForm.nickName" placeholder="昵称" /> <uni-forms-item name="nickName" label="昵称">
</uni-forms-item> <uni-easyinput type="text" v-model="dataForm.nickName" placeholder="昵称" />
<uni-forms-item name="password" label="密码"> </uni-forms-item>
<uni-easyinput type="password" v-model="dataForm.password" placeholder="密码" /> <uni-forms-item name="password" label="密码">
</uni-forms-item> <uni-easyinput type="password" v-model="dataForm.password" placeholder="密码" />
<uni-forms-item name="corfirmPassword" label="确认密码"> </uni-forms-item>
<uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" /> <uni-forms-item name="corfirmPassword" label="确认密码">
</uni-forms-item> <uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" />
<button class="btn-submit" @click="submit" type="primary">注册并登录</button> </uni-forms-item>
</uni-forms> <button class="btn-submit" @click="submit" type="primary">注册并登录</button>
</uni-forms>
</view>
<navigator class="nav-login" url="/pages/login/login"> <navigator class="nav-login" url="/pages/login/login">
返回登录页面 返回登录页面
</navigator> </navigator>
@ -111,10 +113,10 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.register { .register {
.title { .title {
padding-top: 150rpx; padding-top: 250rpx;
padding-bottom: 50rpx; padding-bottom: 50rpx;
color: $im-color-primary; color: $im-color-primary;
text-align: center; text-align: center;
@ -122,7 +124,7 @@ export default {
font-weight: 600; font-weight: 600;
} }
.uni-forms { .form {
padding: 50rpx; padding: 50rpx;
.btn-submit { .btn-submit {

65
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', {
@ -18,8 +19,12 @@ export default defineStore('chatStore', {
cacheChats = []; cacheChats = [];
this.chats = []; this.chats = [];
for (let chat of chatsData.chats) { for (let chat of chatsData.chats) {
// 暂存至缓冲区
chat.stored = false; chat.stored = false;
// 清理多余的消息,避免消息过多导致卡顿
if (UNI_APP.MAX_MESSAGE_SIZE > 0 && chat.messages.length > UNI_APP.MAX_MESSAGE_SIZE) {
chat.messages = chat.messages.slice(0, UNI_APP.MAX_MESSAGE_SIZE);
}
// 暂存至缓冲区
cacheChats.push(JSON.parse(JSON.stringify(chat))); cacheChats.push(JSON.parse(JSON.stringify(chat)));
// 加载期间显示只前15个会话做做样子,一切都为了加快初始化时间 // 加载期间显示只前15个会话做做样子,一切都为了加快初始化时间
if (this.chats.length < 15) { if (this.chats.length < 15) {
@ -90,6 +95,7 @@ export default defineStore('chatStore', {
}, },
readedMessage(pos) { readedMessage(pos) {
let chat = this.findChatByFriend(pos.friendId); let chat = this.findChatByFriend(pos.friendId);
if (!chat) return;
chat.messages.forEach((m) => { chat.messages.forEach((m) => {
if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) { if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) {
// pos.maxId为空表示整个会话已读 // pos.maxId为空表示整个会话已读
@ -156,10 +162,6 @@ export default defineStore('chatStore', {
let message = this.findMessage(chat, msgInfo); let message = this.findMessage(chat, msgInfo);
if (message) { if (message) {
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
return; return;
@ -184,7 +186,7 @@ export default defineStore('chatStore', {
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;
// 未读加1 // 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED &&
msgInfo.type != MESSAGE_TYPE.TIP_TEXT) { msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) {
chat.unreadCount++; chat.unreadCount++;
} }
// 是否有人@我 // 是否有人@我
@ -248,9 +250,8 @@ export default defineStore('chatStore', {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
// 正在发送中的消息可能没有id,根据发送时间删除 // 正在发送中的消息可能没有id,只有临时id
if (msgInfo.selfSend && chat.messages[idx].selfSend && if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages[idx].sendTime == msgInfo.sendTime) {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
@ -258,17 +259,59 @@ export default defineStore('chatStore', {
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
}, },
recallMessage(msgInfo, chatInfo) {
let chat = this.findChat(chatInfo);
if (!chat) return;
// 要撤回的消息id
let id = msgInfo.content;
let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
for (let idx in chat.messages) {
let m = chat.messages[idx];
if (m.id && m.id == id) {
// 改造成一条提示消息
m.status = MESSAGE_STATUS.RECALL;
m.content = name + "撤回了一条消息";
m.type = MESSAGE_TYPE.TIP_TEXT
// 会话列表
chat.lastContent = m.content;
chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = '';
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++;
}
}
// 被引用的消息也要撤回
if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
m.quoteMessage.content = "引用内容已撤回";
m.quoteMessage.status = MESSAGE_STATUS.RECALL;
m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT
}
}
chat.stored = false;
this.saveToStorage();
},
updateChatFromFriend(friend) { updateChatFromFriend(friend) {
let chat = this.findChatByFriend(friend.id) let chat = this.findChatByFriend(friend.id)
if (chat && (chat.headImage != friend.headImageThumb || if (chat && (chat.headImage != friend.headImage ||
chat.showName != friend.nickName)) { chat.showName != friend.nickName)) {
// 更新会话中的群名和头像 // 更新会话中的群名和头像
chat.headImage = friend.headImageThumb; chat.headImage = friend.headImage;
chat.showName = friend.nickName; chat.showName = friend.nickName;
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
} }
}, },
updateChatFromUser(user) {
let chat = this.findChatByFriend(user.id);
// 更新会话中的昵称和头像
if (chat && (chat.headImage != user.headImageThumb ||
chat.showName != user.nickName)) {
chat.headImage = user.headImageThumb;
chat.showName = user.nickName;
chat.stored = false;
this.saveToStorage();
}
},
updateChatFromGroup(group) { updateChatFromGroup(group) {
let chat = this.findChatByGroup(group.id); let chat = this.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb || if (chat && (chat.headImage != group.headImageThumb ||

33
im-uniapp/store/friendStore.js

@ -27,14 +27,14 @@ export default defineStore('friendStore', {
f.onlineApp = copy.onlineApp; f.onlineApp = copy.onlineApp;
}, },
removeFriend(id) { removeFriend(id) {
this.friends.forEach((f, idx) => { this.friends.filter(f => f.id == id).forEach(f => f.deleted = true);
if (f.id == id) {
this.friends.splice(idx, 1)
}
})
}, },
addFriend(friend) { addFriend(friend) {
this.friends.push(friend); if (this.friends.some((f) => f.id == friend.id)) {
this.updateFriend(friend)
} else {
this.friends.unshift(friend);
}
}, },
setOnlineStatus(onlineTerminals) { setOnlineStatus(onlineTerminals) {
this.friends.forEach((f) => { this.friends.forEach((f) => {
@ -51,16 +51,16 @@ export default defineStore('friendStore', {
}); });
}, },
refreshOnlineStatus() { refreshOnlineStatus() {
if (this.friends.length > 0) { let userIds = this.friends.filter((f) => !f.deleted).map((f) => f.id);
let userIds = []; if (userIds.length == 0) {
this.friends.forEach(f => userIds.push(f.id)); return;
http({
url: '/user/terminal/online?userIds=' + userIds.join(','),
method: 'GET'
}).then((onlineTerminals) => {
this.setOnlineStatus(onlineTerminals);
})
} }
http({
url: '/user/terminal/online?userIds=' + userIds.join(','),
method: 'GET'
}).then((onlineTerminals) => {
this.setOnlineStatus(onlineTerminals);
})
// 30s后重新拉取 // 30s后重新拉取
clearTimeout(this.timer); clearTimeout(this.timer);
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
@ -88,6 +88,9 @@ export default defineStore('friendStore', {
} }
}, },
getters: { getters: {
isFriend: (state) => (userId) => {
return state.friends.filter((f) => !f.deleted).some((f) => f.id == userId);
},
findFriend: (state) => (id) => { findFriend: (state) => (id) => {
return state.friends.find((f) => f.id == id); return state.friends.find((f) => f.id == id);
} }

24
im-uniapp/store/groupStore.js

@ -4,29 +4,22 @@ import http from '@/common/request';
export default defineStore('groupStore', { export default defineStore('groupStore', {
state: () => { state: () => {
return { return {
groups: [], groups: []
activeIndex: -1
} }
}, },
actions: { actions: {
setGroups(groups) { setGroups(groups) {
this.groups = groups; this.groups = groups;
}, },
activeGroup(index) {
this.activeIndex = index;
},
addGroup(group) { addGroup(group) {
this.groups.unshift(group); if (this.groups.some((g) => g.id == group.id)) {
this.updateGroup(group);
} else {
this.groups.unshift(group);
}
}, },
removeGroup(groupId) { removeGroup(id) {
this.groups.forEach((g, index) => { this.groups.filter(g => g.id == id).forEach(g => g.quit = true);
if (g.id == groupId) {
this.groups.splice(index, 1);
if (this.activeIndex >= this.groups.length) {
this.activeIndex = this.groups.length - 1;
}
}
})
}, },
updateGroup(group) { updateGroup(group) {
let g = this.findGroup(group.id); let g = this.findGroup(group.id);
@ -34,7 +27,6 @@ export default defineStore('groupStore', {
}, },
clear() { clear() {
this.groups = []; this.groups = [];
this.activeGroup = -1;
}, },
loadGroup() { loadGroup() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

1
im-web/package.json

@ -15,6 +15,7 @@
"localforage": "1.10.0", "localforage": "1.10.0",
"sass": "1.32.12", "sass": "1.32.12",
"sass-loader": "10.1.1", "sass-loader": "10.1.1",
"pinyin-pro": "^3.26.0",
"vue": "2.7.16", "vue": "2.7.16",
"vue-axios": "3.5.2", "vue-axios": "3.5.2",
"vue-router": "3.6.5", "vue-router": "3.6.5",

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

@ -7,19 +7,19 @@ const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心',
]; ];
let transform = (content) => { let transform = (content, extClass) => {
return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg); return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, (text) => textToImg(text, extClass));
} }
// 将匹配结果替换表情图片 // 将匹配结果替换表情图片
let textToImg = (emoText) => { let textToImg = (emoText, extClass) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if (idx == -1) { if (idx == -1) {
return emoText; return emoText;
} }
let url = require(`@/assets/emoji/${idx}.gif`); let url = require(`@/assets/emoji/${idx}.gif`);
return `<img src="${url}" style="width:32px;height:32px;vertical-align:bottom;"/>` return `<img src="${url}" class="${extClass}" />`
} }
let textToUrl = (emoText) => { let textToUrl = (emoText) => {

4
im-web/src/api/enums.js

@ -13,6 +13,10 @@ const MESSAGE_TYPE = {
ACT_RT_VOICE: 40, ACT_RT_VOICE: 40,
ACT_RT_VIDEO: 41, ACT_RT_VIDEO: 41,
USER_BANNED: 50, USER_BANNED: 50,
FRIEND_NEW: 80,
FRIEND_DEL: 81,
GROUP_NEW: 90,
GROUP_DEL: 91,
RTC_CALL_VOICE: 100, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101, RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,

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

@ -89,3 +89,21 @@ section {
} }
} }
.emoji-large {
width: 32px;
height: 32px;
vertical-align: bottom;
}
.emoji-normal {
width: 26px;
height: 26px;
vertical-align: bottom;
}
.emoji-small {
width: 20px;
height: 20px;
vertical-align: bottom;
}

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>

126
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>
@ -117,7 +118,7 @@ export default {
}, },
data() { data() {
return { return {
friend: {}, userInfo: {},
group: {}, group: {},
groupMembers: [], groupMembers: [],
sendImageUrl: "", sendImageUrl: "",
@ -490,13 +491,10 @@ export default {
this.$http({ this.$http({
url: url, url: url,
method: 'delete' method: 'delete'
}).then(() => { }).then((m) => {
this.$message.success("消息已撤回"); this.$message.success("消息已撤回");
msgInfo = JSON.parse(JSON.stringify(msgInfo)); m.selfSend = true;
msgInfo.type = 10; this.$store.commit("recallMessage", [m, this.chat]);
msgInfo.content = '你撤回了一条消息';
msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
this.$store.commit("insertMessage", [msgInfo, this.chat]);
}) })
}); });
}, },
@ -513,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({
@ -543,15 +541,27 @@ export default {
this.groupMembers = groupMembers; this.groupMembers = groupMembers;
}); });
}, },
updateFriendInfo() {
if (this.isFriend) {
// storestore
let friend = JSON.parse(JSON.stringify(this.friend));
friend.headImage = this.userInfo.headImageThumb;
friend.nickName = this.userInfo.nickName;
friend.showNickName = friend.remarkNickName ? friend.remarkNickName : friend.nickName;
this.$store.commit("updateChatFromFriend", friend);
this.$store.commit("updateFriend", friend);
} else {
this.$store.commit("updateChatFromUser", this.userInfo);
}
},
loadFriend(friendId) { loadFriend(friendId) {
// //
this.$http({ this.$http({
url: `/user/find/${friendId}`, url: `/user/find/${friendId}`,
method: 'get' method: 'GET'
}).then((friend) => { }).then((userInfo) => {
this.friend = friend; this.userInfo = userInfo;
this.$store.commit("updateChatFromFriend", friend); this.updateFriendInfo();
this.$store.commit("updateFriend", friend);
}) })
}, },
showName(msgInfo) { showName(msgInfo) {
@ -572,7 +582,6 @@ export default {
} }
}, },
resetEditor() { resetEditor() {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.chatInputEditor.clear(); this.$refs.chatInputEditor.clear();
this.$refs.chatInputEditor.focus(); this.$refs.chatInputEditor.focus();
@ -628,7 +637,7 @@ export default {
} }
if (this.chat.type == "PRIVATE") { if (this.chat.type == "PRIVATE") {
msgInfo.recvId = this.mine.id msgInfo.recvId = this.mine.id
msgInfo.content = "该用户已被管理员封禁,原因:" + this.friend.reason msgInfo.content = "该用户已被管理员封禁,原因:" + this.userInfo.reason
} else { } else {
msgInfo.groupId = this.group.id; msgInfo.groupId = this.group.id;
msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason
@ -644,6 +653,12 @@ export default {
mine() { mine() {
return this.$store.state.userStore.userInfo; return this.$store.state.userStore.userInfo;
}, },
isFriend() {
return this.$store.getters.isFriend(this.userInfo.id);
},
friend() {
return this.$store.getters.findFriend(this.userInfo.id)
},
title() { title() {
let title = this.chat.showName; let title = this.chat.showName;
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
@ -665,15 +680,18 @@ export default {
return this.chat.messages.length; return this.chat.messages.length;
}, },
isBanned() { isBanned() {
return (this.chat.type == "PRIVATE" && this.friend.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 {
@ -801,72 +819,6 @@ export default {
height: 100%; height: 100%;
background-color: white !important; background-color: white !important;
.send-text-area {
box-sizing: border-box;
padding: 5px;
width: 100%;
flex: 1;
resize: none;
font-size: 16px;
outline: none;
text-align: left;
line-height: 30px;
&:before {
content: attr(placeholder);
color: gray;
}
.at {
color: blue;
font-weight: 600;
}
.receipt {
color: darkblue;
font-size: 15px;
font-weight: 600;
}
.emo {
width: 30px;
height: 30px;
vertical-align: bottom;
}
}
.send-image-area {
text-align: left;
border: #53a0e7 solid 1px;
.send-image-box {
position: relative;
display: inline-block;
.send-image {
max-height: 180px;
border: 1px solid #ccc;
border-radius: 2%;
margin: 2px;
}
.send-image-close {
position: absolute;
padding: 3px;
right: 7px;
top: 7px;
color: white;
cursor: pointer;
font-size: 15px;
font-weight: 600;
background-color: #aaa;
border-radius: 50%;
border: 1px solid #ccc;
}
}
}
.send-btn-area { .send-btn-area {
padding: 10px; padding: 10px;
position: absolute; position: absolute;

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>

51
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: {
@ -109,12 +112,19 @@ export default {
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$store.commit("removeGroup", this.group.id); this.$store.commit("removeGroup", this.group.id);
this.$store.commit("activeGroup", -1);
this.$store.commit("removeGroupChat", this.group.id); this.$store.commit("removeGroupChat", this.group.id);
}); });
}) })
}, },
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() {
@ -123,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>

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

@ -249,7 +249,7 @@ export default {
}, },
insertEmoji(emojiText) { insertEmoji(emojiText) {
let emojiElement = document.createElement('img'); let emojiElement = document.createElement('img');
emojiElement.className = 'chat-emoji no-text'; emojiElement.className = 'emoji-normal no-text';
emojiElement.dataset.emojiCode = emojiText; emojiElement.dataset.emojiCode = emojiText;
emojiElement.src = this.$emo.textToUrl(emojiText); emojiElement.src = this.$emo.textToUrl(emojiText);
@ -482,7 +482,7 @@ export default {
bottom: 0; bottom: 0;
outline: none; outline: none;
padding: 5px; padding: 5px;
line-height: 1.5; line-height: 26px;
font-size: var(--im-font-size); font-size: var(--im-font-size);
text-align: left; text-align: left;
overflow-y: auto; overflow-y: auto;
@ -504,13 +504,6 @@ export default {
cursor: pointer; cursor: pointer;
} }
.chat-emoji {
width: 30px;
height: 30px;
vertical-align: top;
cursor: pointer;
}
.chat-file-container { .chat-file-container {
max-width: 65%; max-width: 65%;
padding: 10px; padding: 10px;

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

@ -16,7 +16,7 @@
<div class="chat-content"> <div class="chat-content">
<div class="chat-at-text">{{ atText }}</div> <div class="chat-at-text">{{ atText }}</div>
<div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div> <div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div>
<div class="chat-content-text" v-html="$emo.transform(chat.lastContent)"></div> <div class="chat-content-text" v-html="$emo.transform(chat.lastContent,'emoji-small')"></div>
</div> </div>
</div> </div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" <right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@ -216,11 +216,6 @@ export default {
font-size: var(--im-font-size-small); font-size: var(--im-font-size-small);
color: var(--im-text-color-light); color: var(--im-text-color-light);
img {
width: 20px !important;
height: 20px !important;
vertical-align: bottom;
}
} }
} }

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

@ -1,13 +1,13 @@
<template> <template>
<div class="chat-msg-item"> <div class="chat-msg-item">
<div class="chat-msg-tip" <div class="chat-msg-tip"
v-if="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT"> v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{ msgInfo.content }} {{ msgInfo.content }}
</div> </div>
<div class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME"> <div class="chat-msg-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{ $date.toTimeText(msgInfo.sendTime) }} {{ $date.toTimeText(msgInfo.sendTime) }}
</div> </div>
<div class="chat-msg-normal" v-if="isNormal" :class="{ 'chat-msg-mine': mine }"> <div class="chat-msg-normal" v-else-if="isNormal" :class="{ 'chat-msg-mine': mine }">
<div class="head-image"> <div class="head-image">
<head-image :name="showName" :size="38" :url="headImage" :id="msgInfo.sendId"></head-image> <head-image :name="showName" :size="38" :url="headImage" :id="msgInfo.sendId"></head-image>
</div> </div>
@ -207,7 +207,7 @@ export default {
htmlText() { htmlText() {
let color = this.msgInfo.selfSend ? 'white' : ''; let color = this.msgInfo.selfSend ? 'white' : '';
let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color) let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color)
return this.$emo.transform(text) return this.$emo.transform(text,'emoji-normal')
} }
} }
} }

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

@ -4,7 +4,7 @@
<el-scrollbar style="height: 220px"> <el-scrollbar style="height: 220px">
<div class="emotion-item-list"> <div class="emotion-item-list">
<div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i" <div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i"
@click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText)"> @click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText,'emoji-large')">
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>

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

@ -7,8 +7,8 @@
radius="10%" @click.native="showFullImage()"> </head-image> radius="10%" @click.native="showFullImage()"> </head-image>
</div> </div>
<div> <div>
<el-descriptions :column="1" :title="user.userName" class="user-info-items"> <el-descriptions :column="1" :title="user.nickName" class="user-info-items">
<el-descriptions-item label="昵称">{{ user.nickName }} <el-descriptions-item label="用户名">{{ user.userName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="签名">{{ user.signature }} <el-descriptions-item label="签名">{{ user.signature }}
</el-descriptions-item> </el-descriptions-item>
@ -68,13 +68,14 @@ export default {
params: { params: {
friendId: this.user.id friendId: this.user.id
} }
}).then((data) => { }).then(() => {
this.$message.success("添加成功,对方已成为您的好友"); this.$message.success("添加成功,对方已成为您的好友");
let friend = { let friend = {
id: this.user.id, id: this.user.id,
nickName: this.user.nickName, nickName: this.user.nickName,
headImage: this.user.headImageThumb, headImage: this.user.headImageThumb,
online: this.user.online online: this.user.online,
deleted: false
} }
this.$store.commit("addFriend", friend); this.$store.commit("addFriend", friend);
}) })
@ -87,9 +88,7 @@ export default {
}, },
computed: { computed: {
isFriend() { isFriend() {
let friends = this.$store.state.friendStore.friends; return this.$store.getters.isFriend(this.user.id);
let friend = friends.find((f) => f.id == this.user.id);
return friend != undefined;
} }
} }
} }

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>

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

@ -12,13 +12,13 @@
<head-image :name="user.nickName" :url="user.headImage" :online="user.online"></head-image> <head-image :name="user.nickName" :url="user.headImage" :online="user.online"></head-image>
</div> </div>
<div class="add-friend-text"> <div class="add-friend-text">
<div class="text-user-name"> <div class="nick-name">
<div>{{ user.userName }}</div> <div>{{ user.nickName }}</div>
<div :class="user.online ? 'online-status online' : 'online-status'">{{ <div :class="user.online ? 'online-status online' : 'online-status'">{{
user.online ? "[在线]" :"[离线]"}}</div> user.online ? "[在线]" :"[离线]"}}</div>
</div> </div>
<div class="text-nick-name"> <div class="user-name">
<div>昵称:{{ user.nickName }}</div> <div>用户名:{{ user.userName }}</div>
</div> </div>
</div> </div>
<el-button type="success" size="mini" v-show="!isFriend(user.id)" <el-button type="success" size="mini" v-show="!isFriend(user.id)"
@ -74,21 +74,20 @@ export default {
params: { params: {
friendId: user.id friendId: user.id
} }
}).then((data) => { }).then(() => {
this.$message.success("添加成功,对方已成为您的好友"); this.$message.success("添加成功,对方已成为您的好友");
let friend = { let friend = {
id: user.id, id: user.id,
nickName: user.nickName, nickName: user.nickName,
headImage: user.headImage, headImage: user.headImage,
online: user.online online: user.online,
deleted: false
} }
this.$store.commit("addFriend", friend); this.$store.commit("addFriend", friend);
}) })
}, },
isFriend(userId) { isFriend(userId) {
let friends = this.$store.state.friendStore.friends; return this.$store.getters.isFriend(userId);
let friend = friends.find((f) => f.id == userId);
return friend != undefined;
} }
} }
} }
@ -112,7 +111,7 @@ export default {
flex-shrink: 0; flex-shrink: 0;
overflow: hidden; overflow: hidden;
.text-user-name { .nick-name {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-weight: 600; font-weight: 600;
@ -129,7 +128,7 @@ export default {
} }
} }
.text-nick-name { .user-name {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-size: 12px; font-size: 12px;

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

@ -63,17 +63,12 @@ export default {
this.$emit(item.key.toLowerCase(), this.msgInfo); this.$emit(item.key.toLowerCase(), this.msgInfo);
} }
}, },
computed: {
friend() {
return this.$store.state.friendStore.friends[this.index];
}
},
props: { props: {
active: { active: {
type: Boolean type: Boolean
}, },
index: { friend: {
type: Number type: Object
}, },
menu: { menu: {
type: Boolean, type: Boolean,

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

@ -8,9 +8,9 @@
</el-input> </el-input>
</div> </div>
<el-scrollbar style="height:400px;"> <el-scrollbar style="height:400px;">
<div v-for="(friend, index) in friends" :key="friend.id"> <div v-for="friend in friends" :key="friend.id">
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false" <friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index" :active="false"> @click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :active="false">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" <el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox"
v-model="friend.isCheck" size="medium"></el-checkbox> v-model="friend.isCheck" size="medium"></el-checkbox>
</friend-item> </friend-item>
@ -21,9 +21,9 @@
<div class="agm-r-box"> <div class="agm-r-box">
<div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div> <div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div>
<el-scrollbar style="height:400px;"> <el-scrollbar style="height:400px;">
<div v-for="(friend, index) in friends" :key="friend.id"> <div v-for="friend in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index" :active="false" <friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :active="false"
@del="onRemoveFriend(friend, index)" :menu="false"> @del="onRemoveFriend(friend)" :menu="false">
</friend-item> </friend-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
@ -55,7 +55,6 @@ export default {
this.$emit("close"); this.$emit("close");
}, },
onOk() { onOk() {
let inviteVO = { let inviteVO = {
groupId: this.groupId, groupId: this.groupId,
friendIds: [] friendIds: []
@ -77,7 +76,7 @@ export default {
}) })
} }
}, },
onRemoveFriend(friend, index) { onRemoveFriend(friend) {
friend.isCheck = false; friend.isCheck = false;
}, },
onSwitchCheck(friend) { onSwitchCheck(friend) {
@ -107,6 +106,9 @@ export default {
if (newData) { if (newData) {
this.friends = []; this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => { this.$store.state.friendStore.friends.forEach((f) => {
if (f.deleted) {
return;
}
let friend = JSON.parse(JSON.stringify(f)) let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit) let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id); .find((m) => m.userId == f.id);

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);

63
im-web/src/store/chatStore.js

@ -82,6 +82,7 @@ export default {
}, },
readedMessage(state, pos) { readedMessage(state, pos) {
let chat = this.getters.findChatByFriend(pos.friendId); let chat = this.getters.findChatByFriend(pos.friendId);
if (!chat) return;
chat.messages.forEach((m) => { chat.messages.forEach((m) => {
if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) { if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) {
// pos.maxId为空表示整个会话已读 // pos.maxId为空表示整个会话已读
@ -151,10 +152,6 @@ export default {
let message = this.getters.findMessage(chat, msgInfo); let message = this.getters.findMessage(chat, msgInfo);
if (message) { if (message) {
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.commit("saveToStorage");
return; return;
@ -179,7 +176,7 @@ export default {
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;
// 未读加1 // 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED &&
msgInfo.type != MESSAGE_TYPE.TIP_TEXT) { msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) {
chat.unreadCount++; chat.unreadCount++;
} }
// 是否有人@我 // 是否有人@我
@ -236,9 +233,8 @@ export default {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
// 正在发送中的消息可能没有id,根据发送时间删除 // 正在发送中的消息可能没有id,只有临时id
if (msgInfo.selfSend && chat.messages[idx].selfSend && if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages[idx].sendTime == msgInfo.sendTime) {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
@ -246,21 +242,63 @@ export default {
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.commit("saveToStorage");
}, },
recallMessage(state, [msgInfo, chatInfo]) {
let chat = this.getters.findChat(chatInfo);
if (!chat) return;
// 要撤回的消息id
let id = msgInfo.content;
let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
for (let idx in chat.messages) {
let m = chat.messages[idx];
if (m.id && m.id == id) {
// 改造成一条提示消息
m.status = MESSAGE_STATUS.RECALL;
m.content = name + "撤回了一条消息";
m.type = MESSAGE_TYPE.TIP_TEXT
// 会话列表
chat.lastContent = m.content;
chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = '';
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++;
}
}
// 被引用的消息也要撤回
if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
m.quoteMessage.content = "引用内容已撤回";
m.quoteMessage.status = MESSAGE_STATUS.RECALL;
m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT
}
}
chat.stored = false;
this.commit("saveToStorage");
},
updateChatFromFriend(state, friend) { updateChatFromFriend(state, friend) {
let chat = this.getters.findChatByFriend(friend.id); let chat = this.getters.findChatByFriend(friend.id);
// 更新会话中的群名和头像 // 更新会话中的群名和头像
if (chat && (chat.headImage != friend.headImageThumb || if (chat && (chat.headImage != friend.headImage ||
chat.showName != friend.nickName)) { chat.showName != friend.nickName)) {
chat.headImage = friend.headImageThumb; chat.headImage = friend.headImage;
chat.showName = friend.nickName; chat.showName = friend.nickName;
chat.stored = false; chat.stored = false;
this.commit("saveToStorage") this.commit("saveToStorage")
} }
}, },
updateChatFromUser(user) {
let chat = this.getters.findChatByFriend(user.id);
// 更新会话中的昵称和头像
if (chat && (chat.headImage != user.headImageThumb ||
chat.showName != user.nickName)) {
chat.headImage = user.headImageThumb;
chat.showName = user.nickName;
chat.stored = false;
this.saveToStorage();
}
},
updateChatFromGroup(state, group) { updateChatFromGroup(state, group) {
let chat = this.getters.findChatByGroup(group.id); let chat = this.getters.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb || if (chat && (chat.headImage != group.headImageThumb ||
chat.showName != group.showGroupName)) { chat.showName != group.showGroupName)) {
// 更新会话中的群名称和头像 // 更新会话中的群名称和头像
chat.headImage = group.headImageThumb; chat.headImage = group.headImageThumb;
chat.showName = group.showGroupName; chat.showName = group.showGroupName;
@ -307,7 +345,6 @@ export default {
// 只存储有改动的会话 // 只存储有改动的会话
let chatKey = `${key}-${chat.type}-${chat.targetId}` let chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) { if (!chat.stored) {
console.log(chatKey)
if (chat.delete) { if (chat.delete) {
localForage.removeItem(chatKey); localForage.removeItem(chatKey);
} else { } else {

34
im-web/src/store/friendStore.js

@ -5,7 +5,6 @@ export default {
state: { state: {
friends: [], friends: [],
activeFriend: null,
timer: null timer: null
}, },
mutations: { mutations: {
@ -27,24 +26,21 @@ export default {
} }
}) })
}, },
activeFriend(state, idx) { removeFriend(state, id) {
state.activeFriend = state.friends[idx]; state.friends.filter(f => f.id == id).forEach(f => f.deleted = true);
},
removeFriend(state, idx) {
if (state.friends[idx] == state.activeFriend) {
state.activeFriend = null;
}
state.friends.splice(idx, 1);
}, },
addFriend(state, friend) { addFriend(state, friend) {
state.friends.push(friend); if (state.friends.some((f) => f.id == friend.id)) {
this.commit("updateFriend", friend)
} else {
state.friends.unshift(friend);
}
}, },
refreshOnlineStatus(state) { refreshOnlineStatus(state) {
let userIds = []; let userIds = state.friends.filter((f) => !f.deleted).map((f) => f.id);
if (state.friends.length == 0) { if (userIds.length == 0) {
return; return;
} }
state.friends.forEach((f) => { userIds.push(f.id) });
http({ http({
url: '/user/terminal/online', url: '/user/terminal/online',
method: 'get', method: 'get',
@ -52,7 +48,6 @@ export default {
}).then((onlineTerminals) => { }).then((onlineTerminals) => {
this.commit("setOnlineStatus", onlineTerminals); this.commit("setOnlineStatus", onlineTerminals);
}) })
// 30s后重新拉取 // 30s后重新拉取
state.timer && clearTimeout(state.timer); state.timer && clearTimeout(state.timer);
state.timer = setTimeout(() => { state.timer = setTimeout(() => {
@ -87,7 +82,6 @@ export default {
state.timer && clearTimeout(state.timer); state.timer && clearTimeout(state.timer);
state.friends = []; state.friends = [];
state.timer = null; state.timer = null;
state.activeFriend = [];
} }
}, },
actions: { actions: {
@ -100,10 +94,18 @@ export default {
context.commit("setFriends", friends); context.commit("setFriends", friends);
context.commit("refreshOnlineStatus"); context.commit("refreshOnlineStatus");
resolve() resolve()
}).catch((res) => { }).catch(() => {
reject(); reject();
}) })
}); });
} }
},
getters: {
isFriend: (state) => (userId) => {
return state.friends.filter((f) => !f.deleted).some((f) => f.id == userId);
},
findFriend: (state) => (userId) => {
return state.friends.find((f) => f.id == userId);
}
} }
} }

30
im-web/src/store/groupStore.js

@ -1,31 +1,23 @@
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
export default { export default {
state: { state: {
groups: [], groups: []
activeGroup: null,
}, },
mutations: { mutations: {
setGroups(state, groups) { setGroups(state, groups) {
state.groups = groups; state.groups = groups;
}, },
activeGroup(state, idx) {
state.activeGroup = state.groups[idx];
},
addGroup(state, group) { addGroup(state, group) {
state.groups.unshift(group); if (state.groups.some((g) => g.id == group.id)) {
}, this.commit("updateGroup", group)
removeGroup(state, groupId) { } else {
state.groups.forEach((g, idx) => { state.groups.unshift(group);
if (g.id == groupId) {
state.groups.splice(idx, 1);
}
})
if (state.activeGroup && state.activeGroup.id == groupId) {
state.activeGroup = null;
} }
}, },
removeGroup(state, id) {
state.groups.filter(g => g.id == id).forEach(g => g.quit = true);
},
updateGroup(state, group) { updateGroup(state, group) {
state.groups.forEach((g, idx) => { state.groups.forEach((g, idx) => {
if (g.id == group.id) { if (g.id == group.id) {
@ -36,7 +28,6 @@ export default {
}, },
clear(state) { clear(state) {
state.groups = []; state.groups = [];
state.activeGroup = null;
} }
}, },
actions: { actions: {
@ -53,5 +44,10 @@ export default {
}) })
}); });
} }
},
getters: {
findGroup: (state) => (id) => {
return state.groups.find((g) => g.id == id);
}
} }
} }

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

@ -11,7 +11,7 @@
</div> </div>
<el-scrollbar class="chat-list-items" v-else> <el-scrollbar class="chat-list-items" v-else>
<div v-for="(chat, index) in chatStore.chats" :key="index"> <div v-for="(chat, index) in chatStore.chats" :key="index">
<chat-item v-show="!chat.delete && chat.showName.includes(searchText)" :chat="chat" :index="index" <chat-item v-show="!chat.delete && chat.showName && chat.showName.includes(searchText)" :chat="chat" :index="index"
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)" @click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="chat === chatStore.activeChat"></chat-item> :active="chat === chatStore.activeChat"></chat-item>
</div> </div>

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

@ -10,11 +10,15 @@
<add-friend :dialogVisible="showAddFriend" @close="onCloseAddFriend"></add-friend> <add-friend :dialogVisible="showAddFriend" @close="onCloseAddFriend"></add-friend>
</div> </div>
<el-scrollbar class="friend-list-items"> <el-scrollbar class="friend-list-items">
<div v-for="(friend, index) in $store.state.friendStore.friends" :key="index"> <div v-for="(friends, i) in friendValues" :key="i">
<friend-item v-show="friend.nickName.includes(searchText)" :index="index" <div class="index-title">{{ friendKeys[i] }}</div>
:active="friend === $store.state.friendStore.activeFriend" @chat="onSendMessage(friend)" <div v-for="(friend) in friends" :key="friend.id">
@delete="onDelItem(friend, index)" @click.native="onActiveItem(friend, index)"> <friend-item :friend="friend" :active="friend.id === activeFriend.id"
</friend-item> @chat="onSendMessage(friend)" @delete="onDelFriend(friend)"
@click.native="onActiveItem(friend)">
</friend-item>
</div>
<div v-if="i < friendValues.length - 1" class="divider"></div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
@ -33,7 +37,8 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="昵称">{{ userInfo.nickName }} <el-descriptions-item label="昵称">{{ userInfo.nickName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="性别">{{ userInfo.sex == 0 ? "男" : "女" }}</el-descriptions-item> <el-descriptions-item label="性别">{{ userInfo.sex == 0 ? "男" : "女"
}}</el-descriptions-item>
<el-descriptions-item label="签名">{{ userInfo.signature }}</el-descriptions-item> <el-descriptions-item label="签名">{{ userInfo.signature }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
@ -43,12 +48,10 @@
<el-button v-show="!isFriend" icon="el-icon-plus" type="primary" <el-button v-show="!isFriend" icon="el-icon-plus" type="primary"
@click="onAddFriend(userInfo)">加为好友</el-button> @click="onAddFriend(userInfo)">加为好友</el-button>
<el-button v-show="isFriend" icon="el-icon-delete" type="danger" <el-button v-show="isFriend" icon="el-icon-delete" type="danger"
@click="onDelItem(userInfo, activeIdx)">删除好友</el-button> @click="onDelFriend(userInfo)">删除好友</el-button>
</div> </div>
</div> </div>
</div> </div>
<!-- <el-divider content-position="center"></el-divider>-->
</div> </div>
</el-container> </el-container>
</el-container> </el-container>
@ -58,6 +61,7 @@
import FriendItem from "../components/friend/FriendItem.vue"; import FriendItem from "../components/friend/FriendItem.vue";
import AddFriend from "../components/friend/AddFriend.vue"; import AddFriend from "../components/friend/AddFriend.vue";
import HeadImage from "../components/common/HeadImage.vue"; import HeadImage from "../components/common/HeadImage.vue";
import { pinyin } from 'pinyin-pro';
export default { export default {
name: "friend", name: "friend",
@ -71,8 +75,8 @@ export default {
return { return {
searchText: "", searchText: "",
showAddFriend: false, showAddFriend: false,
activeIdx: -1, userInfo: {},
userInfo: {} activeFriend: {}
} }
}, },
methods: { methods: {
@ -82,12 +86,11 @@ export default {
onCloseAddFriend() { onCloseAddFriend() {
this.showAddFriend = false; this.showAddFriend = false;
}, },
onActiveItem(friend, idx) { onActiveItem(friend) {
this.$store.commit("activeFriend", idx); this.activeFriend = friend;
this.activeIdx = idx this.loadUserInfo(friend.id);
this.loadUserInfo(friend, idx);
}, },
onDelItem(friend, idx) { onDelFriend(friend) {
this.$confirm(`确认删除'${friend.nickName}',并清空聊天记录吗?`, '确认解除?', { this.$confirm(`确认删除'${friend.nickName}',并清空聊天记录吗?`, '确认解除?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@ -96,9 +99,9 @@ export default {
this.$http({ this.$http({
url: `/friend/delete/${friend.id}`, url: `/friend/delete/${friend.id}`,
method: 'delete' method: 'delete'
}).then((data) => { }).then(() => {
this.$message.success("删除好友成功"); this.$message.success("删除好友成功");
this.$store.commit("removeFriend", idx); this.$store.commit("removeFriend", friend.id);
this.$store.commit("removePrivateChat", friend.id); this.$store.commit("removePrivateChat", friend.id);
}) })
}) })
@ -110,7 +113,7 @@ export default {
params: { params: {
friendId: user.id friendId: user.id
} }
}).then((data) => { }).then(() => {
this.$message.success("添加成功,对方已成为您的好友"); this.$message.success("添加成功,对方已成为您的好友");
let friend = { let friend = {
id: user.id, id: user.id,
@ -128,6 +131,7 @@ export default {
showName: user.nickName, showName: user.nickName,
headImage: user.headImageThumb, headImage: user.headImageThumb,
}; };
console.log("chat:", chat)
this.$store.commit("openChat", chat); this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0); this.$store.commit("activeChat", 0);
this.$router.push("/home/chat"); this.$router.push("/home/chat");
@ -137,32 +141,37 @@ export default {
this.$store.commit('showFullImageBox', this.userInfo.headImage); this.$store.commit('showFullImageBox', this.userInfo.headImage);
} }
}, },
updateFriendInfo(friend, user, index) { updateFriendInfo() {
// storestore if (this.isFriend) {
friend = JSON.parse(JSON.stringify(friend)); // storestore
friend.headImage = user.headImageThumb; let friend = JSON.parse(JSON.stringify(this.activeFriend));
friend.nickName = user.nickName; friend.headImage = this.userInfo.headImageThumb;
this.$http({ friend.nickName = this.userInfo.nickName;
url: "/friend/update", this.$store.commit("updateChatFromFriend", friend);
method: "put",
data: friend
}).then(() => {
this.$store.commit("updateFriend", friend); this.$store.commit("updateFriend", friend);
this.$store.commit("updateChatFromFriend", user); }
})
}, },
loadUserInfo(friend, index) { loadUserInfo(id) {
//
this.$http({ this.$http({
url: `/user/find/${friend.id}`, url: `/user/find/${id}`,
method: 'get' method: 'GET'
}).then((user) => { }).then((userInfo) => {
this.userInfo = user; this.userInfo = userInfo;
// this.updateFriendInfo();
if (user.headImageThumb != friend.headImage ||
user.nickName != friend.nickName) {
this.updateFriendInfo(friend, user, index)
}
}) })
},
firstLetter(strText) {
// 使pinyin-pro
let pinyinOptions = {
toneType: 'none', //
type: 'normal' //
};
let pyText = pinyin(strText, pinyinOptions);
return pyText[0];
},
isEnglish(character) {
return /^[A-Za-z]+$/.test(character);
} }
}, },
computed: { computed: {
@ -170,7 +179,46 @@ export default {
return this.$store.state.friendStore; return this.$store.state.friendStore;
}, },
isFriend() { isFriend() {
return this.friendStore.friends.find((f) => f.id == this.userInfo.id); return this.$store.getters.isFriend(this.userInfo.id);
},
friendMap() {
//
let map = new Map();
this.friendStore.friends.forEach((f) => {
if (f.deleted || (this.searchText && !f.nickName.includes(this.searchText))) {
return;
}
let letter = this.firstLetter(f.nickName).toUpperCase();
// #
if (!this.isEnglish(letter)) {
letter = "#"
}
if (f.online) {
letter = '在线'
}
if (map.has(letter)) {
map.get(letter).push(f);
} else {
map.set(letter, [f]);
}
})
//
let arrayObj = Array.from(map);
arrayObj.sort((a, b) => {
// #
if (a[0] == '#' || b[0] == '#') {
return b[0].localeCompare(a[0])
}
return a[0].localeCompare(b[0])
})
map = new Map(arrayObj.map(i => [i[0], i[1]]));
return map;
},
friendKeys() {
return Array.from(this.friendMap.keys());
},
friendValues() {
return Array.from(this.friendMap.values());
} }
} }
} }
@ -199,6 +247,13 @@ export default {
.friend-list-items { .friend-list-items {
flex: 1; flex: 1;
.index-title {
text-align: left;
font-size: var(--im-larger-size-larger);
padding: 5px 15px;
color: var(--im-text-color-light);
}
} }
} }

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

@ -1,87 +1,96 @@
<template> <template>
<el-container class="group-page"> <el-container class="group-page">
<el-aside width="260px" class="group-list-box"> <el-aside width="260px" class="group-list-box">
<div class="group-list-header"> <div class="group-list-header">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText"> <el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i> <i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input> </el-input>
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button> <el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button>
</div> </div>
<el-scrollbar class="group-list-items"> <el-scrollbar class="group-list-items">
<div v-for="(group, index) in groupStore.groups" :key="index"> <div v-for="(groups, i) in groupValues" :key="i">
<group-item v-show="!group.quit && group.showGroupName.includes(searchText)" :group="group" <div class="index-title">{{ groupKeys[i] }}</div>
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group, index)"> <div v-for="group in groups" :key="group.id">
</group-item> <group-item :group="group" :active="group.id == activeGroup.id"
</div> @click.native="onActiveItem(group)">
</el-scrollbar> </group-item>
</el-aside> </div>
<el-container class="group-box"> <div v-if="i < groupValues.length - 1" class="divider"></div>
<div class="group-header" v-show="activeGroup.id"> </div>
{{ activeGroup.showGroupName }}({{ groupMembers.length }}) </el-scrollbar>
</div> </el-aside>
<div class="group-container"> <el-container class="group-box">
<div v-show="activeGroup.id"> <div class="group-header" v-show="activeGroup.id">
<div class="group-info"> {{ activeGroup.showGroupName }}({{ groupMembers.length }})
<div> </div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" :showLoading="true" <div class="group-container">
:maxSize="maxSize" @success="onUploadSuccess" <div v-show="activeGroup.id">
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']"> <div class="group-info">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar"> <div>
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
</file-upload> :showLoading="true" :maxSize="maxSize" @success="onUploadSuccess"
<head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage" :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
:name="activeGroup.showGroupName" radius="10%"> <img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
</head-image> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
<el-button class="send-btn" icon="el-icon-position" type="primary" @click="onSendMessage()">发消息 </file-upload>
</el-button> <head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage"
</div> :name="activeGroup.showGroupName" radius="10%">
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small" </head-image>
ref="groupForm"> <el-button class="send-btn" icon="el-icon-position" type="primary"
<el-form-item label="群聊名称" prop="name"> @click="onSendMessage()">发消息
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input> </el-button>
</el-form-item> </div>
<el-form-item label="群主"> <el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small"
<el-input :value="ownerName" disabled></el-input> ref="groupForm">
</el-form-item> <el-form-item label="群聊名称" prop="name">
<el-form-item label="群名备注"> <el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name" </el-form-item>
maxlength="20"></el-input> <el-form-item label="群主">
</el-form-item> <el-input :value="ownerName" disabled></el-input>
<el-form-item label="我在本群的昵称"> </el-form-item>
<el-input v-model="activeGroup.remarkNickName" maxlength="20" <el-form-item label="群名备注">
:placeholder="$store.state.userStore.userInfo.nickName"></el-input> <el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
</el-form-item> maxlength="20"></el-input>
<el-form-item label="群公告"> </el-form-item>
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3" maxlength="1024" <el-form-item label="我在本群的昵称">
placeholder="群主未设置"></el-input> <el-input v-model="activeGroup.remarkNickName" maxlength="20"
</el-form-item> :placeholder="$store.state.userStore.userInfo.nickName"></el-input>
<div> </el-form-item>
<el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button> <el-form-item label="群公告">
<el-button type="success" @click="onSaveGroup()">保存</el-button> <el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3"
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button> maxlength="1024" placeholder="群主未设置"></el-input>
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button> </el-form-item>
</div> <div>
</el-form> <el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button>
</div> <el-button type="success" @click="onSaveGroup()">保存</el-button>
<el-divider content-position="center"></el-divider> <el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button>
<div class="group-member-list"> <el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button>
<div v-for="(member) in groupMembers" :key="member.id"> </div>
<group-member v-show="!member.quit" class="group-member" :member="member" </el-form>
:showDel="isOwner && member.userId != activeGroup.ownerId" @del="onKick"></group-member> </div>
</div> <el-divider content-position="center"></el-divider>
<div class="group-invite"> <el-scrollbar ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()"> <div class="group-member-list">
<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" </div>
@reload="loadGroupMembers" @close="onCloseAddGroupMember"></add-group-member> <div class="invite-member-text">邀请</div>
</div> <add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id"
</div> :members="groupMembers" @reload="loadGroupMembers"
</div> @close="onCloseAddGroupMember"></add-group-member>
</div> </div>
</el-container> <div v-for="(member, idx) in showMembers" :key="member.id">
</el-container> <group-member v-if="idx < showMaxIdx" class="group-member" :member="member"
:showDel="isOwner && member.userId != activeGroup.ownerId"
@del="onKick"></group-member>
</div>
</div>
</el-scrollbar>
</div>
</div>
</el-container>
</el-container>
</template> </template>
@ -91,324 +100,397 @@ import FileUpload from '../components/common/FileUpload';
import GroupMember from '../components/group/GroupMember.vue'; import GroupMember from '../components/group/GroupMember.vue';
import AddGroupMember from '../components/group/AddGroupMember.vue'; import AddGroupMember from '../components/group/AddGroupMember.vue';
import HeadImage from '../components/common/HeadImage.vue'; import HeadImage from '../components/common/HeadImage.vue';
import { pinyin } from 'pinyin-pro';
export default { export default {
name: "group", name: "group",
components: { components: {
GroupItem, GroupItem,
GroupMember, GroupMember,
FileUpload, FileUpload,
AddGroupMember, AddGroupMember,
HeadImage HeadImage
}, },
data() { data() {
return { return {
searchText: "", searchText: "",
maxSize: 5 * 1024 * 1024, maxSize: 5 * 1024 * 1024,
activeGroup: {}, activeGroup: {},
groupMembers: [], groupMembers: [],
showAddGroupMember: false, showAddGroupMember: false,
rules: { showMaxIdx: 150,
name: [{ rules: {
required: true, name: [{
message: '请输入群聊名称', required: true,
trigger: 'blur' message: '请输入群聊名称',
}] trigger: 'blur'
} }]
}; }
}, };
methods: { },
onCreateGroup() { methods: {
this.$prompt('请输入群聊名称', '创建群聊', { onCreateGroup() {
confirmButtonText: '确定', this.$prompt('请输入群聊名称', '创建群聊', {
cancelButtonText: '取消', confirmButtonText: '确定',
inputPattern: /\S/, cancelButtonText: '取消',
inputErrorMessage: '请输入群聊名称' inputPattern: /\S/,
}).then((o) => { inputErrorMessage: '请输入群聊名称'
let userInfo = this.$store.state.userStore.userInfo; }).then((o) => {
let data = { let userInfo = this.$store.state.userStore.userInfo;
name: o.value let data = {
} name: o.value
this.$http({ }
url: `/group/create?groupName=${o.value}`, this.$http({
method: 'post', url: `/group/create?groupName=${o.value}`,
data: data method: 'post',
}).then((group) => { data: data
this.$store.commit("addGroup", group); }).then((group) => {
}) this.$store.commit("addGroup", group);
}) })
}, })
onActiveItem(group, index) { },
this.$store.commit("activeGroup", index); onActiveItem(group) {
// store this.showMaxIdx = 150;
this.activeGroup = JSON.parse(JSON.stringify(group)); // store
// this.activeGroup = JSON.parse(JSON.stringify(group));
this.loadGroupMembers(); //
}, this.loadGroupMembers();
onInviteMember() { },
this.showAddGroupMember = true; onInviteMember() {
}, this.showAddGroupMember = true;
onCloseAddGroupMember() { },
this.showAddGroupMember = false; onCloseAddGroupMember() {
}, this.showAddGroupMember = false;
onUploadSuccess(data) { },
this.activeGroup.headImage = data.originUrl; onUploadSuccess(data) {
this.activeGroup.headImageThumb = data.thumbUrl; this.activeGroup.headImage = data.originUrl;
}, this.activeGroup.headImageThumb = data.thumbUrl;
onSaveGroup() { },
this.$refs['groupForm'].validate((valid) => { onSaveGroup() {
if (valid) { this.$refs['groupForm'].validate((valid) => {
let vo = this.activeGroup; if (valid) {
this.$http({ let vo = this.activeGroup;
url: "/group/modify", this.$http({
method: "put", url: "/group/modify",
data: vo method: "put",
}).then((group) => { data: vo
this.$store.commit("updateGroup", group); }).then((group) => {
this.$message.success("修改成功"); this.$store.commit("updateGroup", group);
}) this.$message.success("修改成功");
} })
}); }
}, });
onDissolve() { },
this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', { onDissolve() {
confirmButtonText: '确定', this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', {
cancelButtonText: '取消', confirmButtonText: '确定',
type: 'warning' cancelButtonText: '取消',
}).then(() => { type: 'warning'
this.$http({ }).then(() => {
url: `/group/delete/${this.activeGroup.id}`, this.$http({
method: 'delete' url: `/group/delete/${this.activeGroup.id}`,
}).then(() => { method: 'delete'
this.$message.success(`群聊'${this.activeGroup.name}'已解散`); }).then(() => {
this.$store.commit("removeGroup", this.activeGroup.id); this.$message.success(`群聊'${this.activeGroup.name}'已解散`);
this.$store.commit("removeGroupChat", this.activeGroup.id); this.$store.commit("removeGroup", this.activeGroup.id);
this.reset(); this.reset();
}); });
}) })
},
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;
});
})
}, },
onKick(member) { onQuit() {
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', { this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$http({ this.$http({
url: `/group/kick/${this.activeGroup.id}`, url: `/group/quit/${this.activeGroup.id}`,
method: 'delete', method: 'delete'
params: { }).then(() => {
userId: member.userId this.$message.success(`您已退出'${this.activeGroup.name}'`);
} this.$store.commit("removeGroup", this.activeGroup.id);
}).then(() => { this.$store.commit("removeGroupChat", this.activeGroup.id);
this.$message.success(`已将${member.showNickName}移出群聊`); this.reset();
member.quit = true; });
}); })
}) },
onSendMessage() {
}, let chat = {
onQuit() { type: 'GROUP',
this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', { targetId: this.activeGroup.id,
confirmButtonText: '确定', showName: this.activeGroup.showGroupName,
cancelButtonText: '取消', headImage: this.activeGroup.headImage,
type: 'warning' };
}).then(() => { this.$store.commit("openChat", chat);
this.$http({ this.$store.commit("activeChat", 0);
url: `/group/quit/${this.activeGroup.id}`, this.$router.push("/home/chat");
method: 'delete' },
}).then(() => { onScroll(e) {
this.$message.success(`您已退出'${this.activeGroup.name}'`); const scrollbar = e.target;
this.$store.commit("removeGroup", this.activeGroup.id); //
this.$store.commit("removeGroupChat", this.activeGroup.id); if (scrollbar.scrollTop + scrollbar.clientHeight >= scrollbar.scrollHeight - 30) {
this.reset(); if (this.showMaxIdx < this.showMembers.length) {
}); this.showMaxIdx += 50;
}) }
}
}, },
onSendMessage() { loadGroupMembers() {
let chat = { this.$http({
type: 'GROUP', url: `/group/members/${this.activeGroup.id}`,
targetId: this.activeGroup.id, method: "get"
showName: this.activeGroup.showGroupName, }).then((members) => {
headImage: this.activeGroup.headImage, this.groupMembers = members;
}; })
this.$store.commit("openChat", chat); },
this.$store.commit("activeChat", 0); reset() {
this.$router.push("/home/chat"); this.activeGroup = {};
}, this.groupMembers = [];
loadGroupMembers() { },
this.$http({ firstLetter(strText) {
url: `/group/members/${this.activeGroup.id}`, // 使pinyin-pro
method: "get" let pinyinOptions = {
}).then((members) => { toneType: 'none', //
this.groupMembers = members; type: 'normal' //
}) };
}, let pyText = pinyin(strText, pinyinOptions);
reset() { return pyText[0];
this.activeGroup = {}; },
this.groupMembers = []; isEnglish(character) {
} return /^[A-Za-z]+$/.test(character);
}, }
computed: { },
groupStore() { computed: {
return this.$store.state.groupStore; groupStore() {
}, return this.$store.state.groupStore;
ownerName() { },
let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId); ownerName() {
return member && member.showNickName; let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId);
}, return member && member.showNickName;
isOwner() { },
return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id; isOwner() {
}, return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id;
imageAction() { },
return `/image/upload`; imageAction() {
} return `/image/upload`;
} },
groupMap() {
//
let map = new Map();
this.groupStore.groups.forEach((g) => {
if (g.quit || (this.searchText && !g.showGroupName.includes(this.searchText))) {
return;
}
let letter = this.firstLetter(g.showGroupName).toUpperCase();
// #
if (!this.isEnglish(letter)) {
letter = "#"
}
if (map.has(letter)) {
map.get(letter).push(g);
} else {
map.set(letter, [g]);
}
})
//
let arrayObj = Array.from(map);
arrayObj.sort((a, b) => {
// #
if (a[0] == '#' || b[0] == '#') {
return b[0].localeCompare(a[0])
}
return a[0].localeCompare(b[0])
})
map = new Map(arrayObj.map(i => [i[0], i[1]]));
return map;
},
groupKeys() {
return Array.from(this.groupMap.keys());
},
groupValues() {
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>
<style lang="scss"> <style lang="scss">
.group-page { .group-page {
.group-list-box { .group-list-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--im-background); background: var(--im-background);
.group-list-header {
height: 50px;
display: flex;
align-items: center;
padding: 0 8px;
.group-list-header { .add-btn {
height: 50px; padding: 5px !important;
display: flex; margin: 5px;
align-items: center; font-size: 16px;
padding: 0 8px; border-radius: 50%;
}
}
.add-btn { .group-list-items {
padding: 5px !important; flex: 1;
margin: 5px;
font-size: 16px;
border-radius: 50%;
}
}
.group-list-items { .index-title {
flex: 1; text-align: left;
} font-size: var(--im-larger-size-larger);
} padding: 5px 15px;
color: var(--im-text-color-light);
}
}
}
.group-box { .group-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.group-header { .group-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 0 12px; padding: 0 12px;
line-height: 50px; line-height: 50px;
font-size: var(--im-font-size-larger); font-size: var(--im-font-size-larger);
border-bottom: var(--im-border); border-bottom: var(--im-border);
} }
.el-divider--horizontal { .el-divider--horizontal {
margin: 16px 0; margin: 16px 0;
} }
.group-container { .group-container {
overflow: auto; overflow: auto;
padding: 20px; padding: 20px;
flex: 1; flex: 1;
.group-info { .group-info {
display: flex; display: flex;
padding: 5px 20px; padding: 5px 20px;
.group-form { .group-form {
flex: 1; flex: 1;
padding-left: 40px; padding-left: 40px;
max-width: 700px; max-width: 700px;
} }
.avatar-uploader { .avatar-uploader {
--width: 160px; --width: 160px;
text-align: left; text-align: left;
.el-upload { .el-upload {
border: 1px dashed #d9d9d9 !important; border: 1px dashed #d9d9d9 !important;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.el-upload:hover { .el-upload:hover {
border-color: #409EFF; border-color: #409EFF;
} }
.avatar-uploader-icon { .avatar-uploader-icon {
font-size: 28px; font-size: 28px;
color: #8c939d; color: #8c939d;
width: var(--width); width: var(--width);
height: var(--width); height: var(--width);
line-height: var(--width); line-height: var(--width);
text-align: center; text-align: center;
} }
.avatar { .avatar {
width: var(--width); width: var(--width);
height: var(--width); height: var(--width);
display: block; display: block;
} }
} }
.send-btn { .send-btn {
margin-top: 12px; margin-top: 12px;
} }
} }
.group-member-list { .group-member-list {
padding: 0 12px; padding: 0 12px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
text-align: center; text-align: center;
.group-member { .group-member {
margin-right: 5px; margin-right: 5px;
} }
.group-invite { .group-invite {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 60px; width: 60px;
.invite-member-btn { .invite-member-btn {
width: 38px; width: 38px;
height: 38px; height: 38px;
line-height: 38px; line-height: 38px;
border: var(--im-border); border: var(--im-border);
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
box-sizing: border-box; box-sizing: border-box;
&:hover { &:hover {
border: #aaaaaa solid 1px; border: #aaaaaa solid 1px;
} }
} }
.invite-member-text { .invite-member-text {
font-size: var(--im-font-size-smaller); font-size: var(--im-font-size-smaller);
text-align: center; text-align: center;
width: 100%; width: 100%;
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden overflow: hidden
} }
} }
} }
} }
} }
} }
</style> </style>

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

File diff suppressed because it is too large
Loading…
Cancel
Save