Browse Source

!51 安卓和ios版本上线

Merge pull request !51 from blue/v_2.0.0
master
blue 2 years ago
committed by Gitee
parent
commit
f636455b85
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 2
      .gitignore
  2. 20
      README.md
  3. 89
      im-commom/src/main/java/com/bx/imcommon/util/CommaTextUtils.java
  4. 12
      im-platform/src/main/java/com/bx/implatform/IMPlatformApp.java
  5. 11
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  6. 7
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  7. 3
      im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java
  8. 5
      im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java
  9. 22
      im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java
  10. 19
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  11. 10
      im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java
  12. 14
      im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java
  13. 6
      im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java
  14. 23
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java
  15. 250
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  16. 97
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  17. 87
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  18. 10
      im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java
  19. 8
      im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java
  20. 9
      im-platform/src/main/resources/application.yml
  21. 10
      im-platform/src/main/resources/db/db.sql
  22. 4
      im-server/pom.xml
  23. 4
      im-server/src/main/resources/application.yml
  24. 2
      im-ui/package.json
  25. 3
      im-ui/src/api/enums.js
  26. 1
      im-ui/src/api/wssocket.js
  27. 12
      im-ui/src/assets/iconfont/iconfont.css
  28. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  29. BIN
      im-ui/src/assets/iconfont/iconfont.woff
  30. BIN
      im-ui/src/assets/iconfont/iconfont.woff2
  31. 55
      im-ui/src/components/chat/ChatAtBox.vue
  32. 1471
      im-ui/src/components/chat/ChatBox.vue
  33. 67
      im-ui/src/components/chat/ChatGroupMember.vue
  34. 187
      im-ui/src/components/chat/ChatGroupReaded.vue
  35. 8
      im-ui/src/components/chat/ChatGroupSide.vue
  36. 660
      im-ui/src/components/chat/ChatMessageItem.vue
  37. 1
      im-ui/src/components/chat/ChatPrivateVideo.vue
  38. 3
      im-ui/src/components/chat/ChatVoice.vue
  39. 1
      im-ui/src/components/group/AddGroupMember.vue
  40. 136
      im-ui/src/store/chatStore.js
  41. 1
      im-ui/src/store/friendStore.js
  42. 1
      im-ui/src/store/groupStore.js
  43. 1
      im-ui/src/store/index.js
  44. 3
      im-ui/src/view/Group.vue
  45. 624
      im-ui/src/view/Home.vue
  46. 17
      im-ui/src/view/Login.vue
  47. 16
      im-uniapp/.env.js
  48. 128
      im-uniapp/App.vue
  49. 3
      im-uniapp/common/enums.js
  50. 6
      im-uniapp/common/request.js
  51. 14
      im-uniapp/common/wssocket.js
  52. 131
      im-uniapp/components/chat-group-readed/chat-group-readed.vue
  53. 27
      im-uniapp/components/chat-message-item/chat-message-item.vue
  54. 7
      im-uniapp/components/file-upload/file-upload.vue
  55. 4
      im-uniapp/components/image-upload/image-upload.vue
  56. 1
      im-uniapp/main.js
  57. 51
      im-uniapp/manifest.json
  58. 34
      im-uniapp/package.json
  59. 1
      im-uniapp/pages.json
  60. 57
      im-uniapp/pages/chat/chat-box.vue
  61. 8
      im-uniapp/pages/group/group-info.vue
  62. 2
      im-uniapp/pages/group/group.vue
  63. 2
      im-uniapp/pages/login/login.vue
  64. 10
      im-uniapp/static/icon/iconfont.css
  65. BIN
      im-uniapp/static/icon/iconfont.ttf
  66. BIN
      im-uniapp/static/logo/logo.png
  67. 133
      im-uniapp/store/chatStore.js

2
.gitignore

@ -8,3 +8,5 @@
/im-server/src/main/resources/logback-prod.xml /im-server/src/main/resources/logback-prod.xml
/im-commom/im-commom.iml /im-commom/im-commom.iml
/im-uniapp/node_modules/ /im-uniapp/node_modules/
/im-ui/jsconfig.json
/package-lock.json

20
README.md

@ -16,33 +16,33 @@
#### 近期更新 #### 近期更新
发布2.0版本,本次更新加入了uniapp版本: 发布2.0版本,本次更新加入了uniapp移动端:
- 支持移动端和web端同时在线,多端消息同步 - 支持移动端和web端同时在线,多端消息同步
- 目前仅兼容h5和微信小程序,后续会继续兼容更多终端类型 - 目前已兼容h5、微信小程序,安卓和IOS
- 聊天窗口加入已读未读显示 - 聊天窗口加入已读未读显示
- 群聊加入@功能 - 群聊加入@功能
- 界面风格升级,表情包更新、生成文字头像等 - 界面风格升级,表情包更新、生成文字头像等
#### 在线体验 #### 在线体验
web地址:https://www.boxim.online
微信小程序: 账号:张三/123456 李四/123456,也可以在网页端自行注册账号
![输入图片说明](%E6%88%AA%E5%9B%BE/wx%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%BA%8C%E7%BB%B4%E7%A0%81.jpg) 网页端:https://www.boxim.online
移动安卓端:https://www.boxim.online/download/boxim.apk
H5地址: https://www.boxim.online/h5/ ,或扫码: 移动H5端: https://www.boxim.online/h5/ ,或扫码:
![输入图片说明](%E6%88%AA%E5%9B%BE/h5%E4%BA%8C%E7%BB%B4%E7%A0%81.png) ![输入图片说明](%E6%88%AA%E5%9B%BE/h5%E4%BA%8C%E7%BB%B4%E7%A0%81.png)
微信小程序:
![输入图片说明](%E6%88%AA%E5%9B%BE/wx%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%BA%8C%E7%BB%B4%E7%A0%81.jpg)
账号:
张三/123456
李四/123456
也可以自行注册账号
#### 相关项目 #### 相关项目

89
im-commom/src/main/java/com/bx/imcommon/util/CommaTextUtils.java

@ -0,0 +1,89 @@
package com.bx.imcommon.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 逗号分格文本处理工具类
*
* @author: blue
* @date: 2023-11-09 09:52:49
* @version: 1.0
*/
public class CommaTextUtils {
/**
* 文本转列表
*
* @param strText 文件
* @return 列表
*/
public static List<String> asList(String strText) {
if (StrUtil.isEmpty(strText)) {
return new LinkedList<>();
}
return new LinkedList<>(Arrays.asList(strText.split(",")));
}
/**
* 列表转字符串并且自动清空去重排序
*
* @param texts 列表
* @return 文本
*/
public static <T> String asText(Collection<T> texts) {
if (CollUtil.isEmpty(texts)) {
return StrUtil.EMPTY;
}
return texts.stream().map(text -> StrUtil.toString(text)).filter(StrUtil::isNotEmpty).distinct().sorted().collect(Collectors.joining(","));
}
/**
* 追加一个单词
*
* @param strText 文本
* @param word 单词
* @return 文本
*/
public static <T> String appendWord(String strText, T word) {
List<String> texts = asList(strText);
texts.add(StrUtil.toString(word));
return asText(texts);
}
/**
* 删除一个单词
*
* @param strText 文本
* @param word 单词
* @return 文本
*/
public static <T> String removeWord(String strText, T word) {
List<String> texts = asList(strText);
texts.remove(StrUtil.toString(word));
return asText(texts);
}
/**
* 合并
*
* @param strText1 文本1
* @param strText2 文本2
* @return 文本
*/
public static String merge(String strText1, String strText2) {
List<String> texts = asList(strText1);
texts.addAll(asList(strText2));
return asText(texts);
}
}

12
im-platform/src/main/java/com/bx/implatform/IMPlatformApp.java

@ -1,11 +1,22 @@
package com.bx.implatform; package com.bx.implatform;
import cn.hutool.core.util.StrUtil;
import com.bx.implatform.contant.RedisKey;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@Slf4j @Slf4j
@EnableAspectJAutoProxy(exposeProxy = true) @EnableAspectJAutoProxy(exposeProxy = true)
@ -16,4 +27,5 @@ public class IMPlatformApp {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(IMPlatformApp.class, args); SpringApplication.run(IMPlatformApp.class, args);
} }
} }

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

@ -42,6 +42,12 @@ public class GroupMessageController {
return ResultUtils.success(groupMessageService.loadMessage(minId)); return ResultUtils.success(groupMessageService.loadMessage(minId));
} }
@GetMapping("/pullOfflineMessage")
@ApiOperation(value = "拉取离线消息", notes = "拉取离线消息,消息将通过webscoket异步推送")
public Result pullOfflineMessage(@RequestParam Long minId) {
groupMessageService.pullOfflineMessage(minId);
return ResultUtils.success();
}
@PutMapping("/readed") @PutMapping("/readed")
@ApiOperation(value = "消息已读", notes = "将群聊中的消息状态置为已读") @ApiOperation(value = "消息已读", notes = "将群聊中的消息状态置为已读")
@ -50,6 +56,11 @@ public class GroupMessageController {
return ResultUtils.success(); return ResultUtils.success();
} }
@GetMapping("/findReadedUsers")
@ApiOperation(value = "获取已读用户id", notes = "获取消息已读用户列表")
public Result<List<Long>> findReadedUsers(@RequestParam Long groupId,@RequestParam Long messageId) {
return ResultUtils.success(groupMessageService.findReadedUsers(groupId,messageId));
}
@GetMapping("/history") @GetMapping("/history")
@ApiOperation(value = "查询聊天记录", notes = "查询聊天记录") @ApiOperation(value = "查询聊天记录", notes = "查询聊天记录")

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

@ -43,6 +43,13 @@ public class PrivateMessageController {
return ResultUtils.success(privateMessageService.loadMessage(minId)); return ResultUtils.success(privateMessageService.loadMessage(minId));
} }
@GetMapping("/pullOfflineMessage")
@ApiOperation(value = "拉取离线消息", notes = "拉取离线消息,消息将通过webscoket异步推送")
public Result pullOfflineMessage(@RequestParam Long minId) {
privateMessageService.pullOfflineMessage(minId);
return ResultUtils.success();
}
@PutMapping("/readed") @PutMapping("/readed")
@ApiOperation(value = "消息已读", notes = "将会话中接收的消息状态置为已读") @ApiOperation(value = "消息已读", notes = "将会话中接收的消息状态置为已读")
public Result readedMessage(@RequestParam Long friendId) { public Result readedMessage(@RequestParam Long friendId) {

3
im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java

@ -27,6 +27,9 @@ public class GroupMessageDTO {
@ApiModelProperty(value = "消息类型") @ApiModelProperty(value = "消息类型")
private Integer type; private Integer type;
@ApiModelProperty(value = "是否回执消息")
private Boolean receipt = false;
@Size(max = 20, message = "一次最多只能@20个小伙伴哦") @Size(max = 20, message = "一次最多只能@20个小伙伴哦")
@ApiModelProperty(value = "被@用户列表") @ApiModelProperty(value = "被@用户列表")
private List<Long> atUserIds; private List<Long> atUserIds;

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

@ -69,6 +69,11 @@ public class GroupMember extends Model<GroupMember> {
@TableField("quit") @TableField("quit")
private Boolean quit; private Boolean quit;
/**
* 退群时间
*/
@TableField("quit_time")
private Date quitTime;
/** /**
* 创建时间 * 创建时间

22
im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java

@ -50,6 +50,12 @@ public class GroupMessage extends Model<GroupMessage> {
@TableField("send_nick_name") @TableField("send_nick_name")
private String sendNickName; private String sendNickName;
/**
* 接受用户id,为空表示全体发送
*/
@TableField("recv_ids")
private String recvIds;
/** /**
* @用户列表 * @用户列表
*/ */
@ -62,13 +68,25 @@ public class GroupMessage extends Model<GroupMessage> {
private String content; private String content;
/** /**
* 消息类型 0:文字 1:图片 2:文件 * 消息类型 MessageType
*/ */
@TableField("type") @TableField("type")
private Integer type; private Integer type;
/** /**
* 状态 * 是否回执消息
*/
@TableField("receipt")
private Boolean receipt;
/**
* 回执消息是否完成
*/
@TableField("receipt_ok")
private Boolean receiptOk;
/**
* 状态 MessageStatus
*/ */
@TableField("status") @TableField("status")
private Integer status; private Integer status;

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

@ -1,6 +1,8 @@
package com.bx.implatform.enums; package com.bx.implatform.enums;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor @AllArgsConstructor
public enum MessageType { public enum MessageType {
@ -34,6 +36,23 @@ public enum MessageType {
*/ */
READED(11, "已读"), READED(11, "已读"),
/**
* 消息已读回执
*/
RECEIPT(12, "消息已读回执"),
/**
* 时间提示
*/
TIP_TIME(20,"时间提示"),
/**
* 文字提示
*/
TIP_TEXT(21,"文字提示"),
/**
* 消息加载标记
*/
LOADDING(30,"加载中"),
/** /**
* 呼叫 * 呼叫
*/ */

10
im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java

@ -24,6 +24,14 @@ public interface IGroupMemberService extends IService<GroupMember> {
*/ */
List<GroupMember> findByUserId(Long userId); List<GroupMember> findByUserId(Long userId);
/**
* 根据用户id查询一个月内退的群
*
* @param userId 用户id
* @return 成员列表
*/
List<GroupMember> findQuitInMonth(Long userId);
/** /**
* 根据群聊id查询群聊成员包括已退出 * 根据群聊id查询群聊成员包括已退出
* *
@ -32,6 +40,8 @@ public interface IGroupMemberService extends IService<GroupMember> {
*/ */
List<GroupMember> findByGroupId(Long groupId); List<GroupMember> findByGroupId(Long groupId);
/** /**
* 根据群聊id查询没有退出的群聊成员id * 根据群聊id查询没有退出的群聊成员id
* *

14
im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java

@ -32,6 +32,13 @@ public interface IGroupMessageService extends IService<GroupMessage> {
*/ */
List<GroupMessageVO> loadMessage(Long minId); List<GroupMessageVO> loadMessage(Long minId);
/**
* 拉取离线消息只能拉取最近1个月的消息最多拉取1000条
*
* @param minId 消息起始id
*/
void pullOfflineMessage(Long minId);
/** /**
* 消息已读,同步其他终端清空未读数量 * 消息已读,同步其他终端清空未读数量
* *
@ -39,6 +46,13 @@ public interface IGroupMessageService extends IService<GroupMessage> {
*/ */
void readedMessage(Long groupId); void readedMessage(Long groupId);
/**
* 查询群里消息已读用户id列表
* @param groupId 群里id
* @param messageId 消息id
* @return 已读用户id集合
*/
List<Long> findReadedUsers(Long groupId,Long messageId);
/** /**
* 拉取历史聊天记录 * 拉取历史聊天记录
* *

6
im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java

@ -44,6 +44,12 @@ public interface IPrivateMessageService extends IService<PrivateMessage> {
*/ */
List<PrivateMessageVO> loadMessage(Long minId); List<PrivateMessageVO> loadMessage(Long minId);
/**
* 拉取离线消息只能拉取最近1个月的消息最多拉取1000条
*
* @param minId 消息起始id
*/
void pullOfflineMessage(Long minId);
/** /**
* 消息已读,将整个会话的消息都置为已读状态 * 消息已读,将整个会话的消息都置为已读状态

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

@ -9,11 +9,13 @@ import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.entity.GroupMember; import com.bx.implatform.entity.GroupMember;
import com.bx.implatform.mapper.GroupMemberMapper; import com.bx.implatform.mapper.GroupMemberMapper;
import com.bx.implatform.service.IGroupMemberService; import com.bx.implatform.service.IGroupMemberService;
import com.bx.implatform.util.DateTimeUtils;
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -34,7 +36,7 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
@Override @Override
public GroupMember findByGroupAndUserId(Long groupId, Long userId) { public GroupMember findByGroupAndUserId(Long groupId, Long userId) {
QueryWrapper<GroupMember> wrapper = new QueryWrapper<>(); QueryWrapper<GroupMember> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(GroupMember::getGroupId, groupId) wrapper.lambda().eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, userId); .eq(GroupMember::getUserId, userId);
@ -49,6 +51,16 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
return this.list(memberWrapper); return this.list(memberWrapper);
} }
@Override
public List<GroupMember> findQuitInMonth(Long userId) {
Date monthTime = DateTimeUtils.addMonths(new Date(),-1);
LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
memberWrapper.eq(GroupMember::getUserId, userId)
.eq(GroupMember::getQuit, true)
.ge(GroupMember::getQuitTime,monthTime);
return this.list(memberWrapper);
}
@Override @Override
public List<GroupMember> findByGroupId(Long groupId) { public List<GroupMember> findByGroupId(Long groupId) {
LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
@ -61,7 +73,8 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
public List<Long> findUserIdsByGroupId(Long groupId) { public List<Long> findUserIdsByGroupId(Long groupId) {
LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
memberWrapper.eq(GroupMember::getGroupId, groupId) memberWrapper.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getQuit, false); .eq(GroupMember::getQuit, false)
.select(GroupMember::getUserId);
List<GroupMember> members = this.list(memberWrapper); List<GroupMember> members = this.list(memberWrapper);
return members.stream().map(GroupMember::getUserId).collect(Collectors.toList()); return members.stream().map(GroupMember::getUserId).collect(Collectors.toList());
} }
@ -71,7 +84,8 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
public void removeByGroupId(Long groupId) { public void removeByGroupId(Long groupId) {
LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate(); LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate();
wrapper.eq(GroupMember::getGroupId, groupId) wrapper.eq(GroupMember::getGroupId, groupId)
.set(GroupMember::getQuit, true); .set(GroupMember::getQuit, true)
.set(GroupMember::getQuitTime,new Date());
this.update(wrapper); this.update(wrapper);
} }
@ -81,7 +95,8 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate(); LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate();
wrapper.eq(GroupMember::getGroupId, groupId) wrapper.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, userId) .eq(GroupMember::getUserId, userId)
.set(GroupMember::getQuit, true); .set(GroupMember::getQuit, true)
.set(GroupMember::getQuitTime,new Date());
this.update(wrapper); this.update(wrapper);
} }
} }

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

@ -10,8 +10,10 @@ 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.imclient.IMClient;
import com.bx.imcommon.contant.IMConstant; import com.bx.imcommon.contant.IMConstant;
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.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;
@ -39,6 +41,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -63,7 +66,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
} }
// 是否在群聊里面 // 是否在群聊里面
GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId()); GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId());
if (Objects.isNull(member) || Boolean.TRUE.equals(member.getQuit())) { if (Objects.isNull(member) || member.getQuit()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法发送消息"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法发送消息");
} }
// 群聊成员列表 // 群聊成员列表
@ -152,50 +155,145 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return new ArrayList<>(); return new ArrayList<>();
} }
Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId); Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId);
Set<Long> ids = groupMemberMap.keySet(); Set<Long> groupIds = groupMemberMap.keySet();
// 只能拉取最近1个月的 // 只能拉取最近1个月的
Date minDate = DateUtils.addMonths(new Date(), -1); Date minDate = DateUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId).gt(GroupMessage::getSendTime, minDate).in(GroupMessage::getGroupId, ids) wrapper.gt(GroupMessage::getId, minId).gt(GroupMessage::getSendTime, minDate).in(GroupMessage::getGroupId, groupIds)
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByAsc(GroupMessage::getId).last("limit 100"); .ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByAsc(GroupMessage::getId).last("limit 100");
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
// 转成vo // 转成vo
List<GroupMessageVO> vos = messages.stream() List<GroupMessageVO> vos = messages.stream()
.filter(m -> { .filter(m -> {
//排除加群之前的消息 //排除加群之前的消息
GroupMember member = groupMemberMap.get(m.getGroupId());
return Objects.nonNull(member) && DateUtil.compare(member.getCreatedTime(), m.getSendTime()) <= 0;
})
.map(m -> {
GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class);
// 被@用户列表
if (StringUtils.isNotBlank(m.getAtUserIds()) && Objects.nonNull(vo)) {
List<String> atIds = Splitter.on(",").trimResults().splitToList(m.getAtUserIds());
vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList()));
}
return vo;
}).collect(Collectors.toList());
// 通过群聊对消息进行分组
Map<Long, List<GroupMessageVO>> messageGroupMap = vos.stream().collect(Collectors.groupingBy(GroupMessageVO::getGroupId));
messageGroupMap.forEach((groupId, messageVos) -> {
// 填充消息状态
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString());
long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString());
messageVos.forEach(messageVo -> messageVo.setStatus(readedMaxId >= messageVo.getId() ? MessageStatus.READED.code() : MessageStatus.UNSEND.code()));
// 针对回执消息填充已读人数
List<GroupMessageVO> receiptMessageVos = messageVos.stream().filter(GroupMessageVO::getReceipt).collect(Collectors.toList());
if (!receiptMessageVos.isEmpty()) {
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
receiptMessageVos.forEach(receiptMessageVo -> {
int count = getReadedUserIds(maxIdMap, receiptMessageVo.getId(),receiptMessageVo.getSendId()).size();
receiptMessageVo.setReadedCount(count);
});
}
});
return vos;
}
@Override
public void pullOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession();
if(!imClient.isOnline(session.getUserId())){
throw new GlobalException(ResultCode.PROGRAM_ERROR, "网络连接失败,无法拉取离线消息");
}
// 查询用户加入的群组
List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId);
Set<Long> groupIds = groupMemberMap.keySet();
if(CollectionUtil.isEmpty(groupIds)){
return;
}
// 开启加载中标志
this.sendLoadingMessage(true);
// 只能拉取最近1个月的,最多拉取1000条
Date minDate = DateUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId)
.gt(GroupMessage::getSendTime, minDate)
.in(GroupMessage::getGroupId, groupIds)
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
.orderByDesc(GroupMessage::getId).last("limit 1000");
List<GroupMessage> messages = this.list(wrapper);
// 通过群聊对消息进行分组
Map<Long, List<GroupMessage>> messageGroupMap = messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId));
// 退群前的消息
List<GroupMember> quitMembers = groupMemberService.findQuitInMonth(session.getUserId());
for(GroupMember quitMember: quitMembers){
wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId)
.between(GroupMessage::getSendTime, minDate,quitMember.getQuitTime())
.eq(GroupMessage::getGroupId, quitMember.getGroupId())
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
.orderByDesc(GroupMessage::getId)
.last("limit 100");
List<GroupMessage> groupMessages = this.list(wrapper);
messageGroupMap.put(quitMember.getGroupId(),groupMessages);
groupMemberMap.put(quitMember.getGroupId(),quitMember);
}
// 推送消息
AtomicInteger sendCount = new AtomicInteger();
messageGroupMap.forEach((groupId, groupMessages) -> {
// id从小到大排序
CollectionUtil.reverse(groupMessages);
// 填充消息状态
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString());
long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString());
Map<Object, Object> maxIdMap = null;
for(GroupMessage m:groupMessages){
// 排除加群之前的消息
GroupMember member = groupMemberMap.get(m.getGroupId()); GroupMember member = groupMemberMap.get(m.getGroupId());
return Objects.nonNull(member) && DateUtil.compare(member.getCreatedTime(), m.getSendTime()) <= 0; if(DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0){
}) continue;
.map(m -> { }
// 排除不需要接收的消息
List<String> recvIds = CommaTextUtils.asList(m.getRecvIds());
if(!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())){
continue;
}
// 组装vo
GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class); GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class);
// 被@用户列表 // 被@用户列表
if (StringUtils.isNotBlank(m.getAtUserIds()) && Objects.nonNull(vo)) { if (StringUtils.isNotBlank(m.getAtUserIds()) && Objects.nonNull(vo)) {
List<String> atIds = Splitter.on(",").trimResults().splitToList(m.getAtUserIds()); List<String> atIds = Splitter.on(",").trimResults().splitToList(m.getAtUserIds());
vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList())); vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList()));
} }
return vo; // 填充状态
}).collect(Collectors.toList()); vo.setStatus(readedMaxId >= m.getId() ? MessageStatus.READED.code() : MessageStatus.UNSEND.code());
// 消息状态,数据库没有存群聊的消息状态,需要从redis取 // 针对回执消息填充已读人数
List<String> keys = ids.stream().map(id -> String.join(":", RedisKey.IM_GROUP_READED_POSITION, id.toString(), session.getUserId().toString())) if(m.getReceipt()){
.collect(Collectors.toList()); if(Objects.isNull(maxIdMap)) {
List<Object> sendPos = redisTemplate.opsForValue().multiGet(keys); maxIdMap = redisTemplate.opsForHash().entries(key);
int idx = 0; }
for (Long id : ids) { int count = getReadedUserIds(maxIdMap, m.getId(),m.getSendId()).size();
Object o = sendPos.get(idx); vo.setReadedCount(count);
Integer sendMaxId = Objects.isNull(o) ? -1 : (Integer) o;
vos.stream().filter(vo -> vo.getGroupId().equals(id)).forEach(vo -> {
if (vo.getId() <= sendMaxId) {
// 已读
vo.setStatus(MessageStatus.READED.code());
} else {
// 未推送
vo.setStatus(MessageStatus.UNSEND.code());
} }
}); // 推送
idx++; IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
} sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code()));
return vos; sendMessage.setRecvIds(Arrays.asList(session.getUserId()));
sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal()));
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(false);
sendMessage.setData(vo);
imClient.sendGroupMessage(sendMessage);
sendCount.getAndIncrement();
}
});
// 关闭加载中标志
this.sendLoadingMessage(false);
log.info("拉取离线群聊消息,用户id:{},数量:{}",session.getUserId(),sendCount.get());
} }
@Override @Override
@ -203,12 +301,15 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 取出最后的消息id // 取出最后的消息id
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId).orderByDesc(GroupMessage::getId).last("limit 1").select(GroupMessage::getId); wrapper.eq(GroupMessage::getGroupId, groupId)
.orderByDesc(GroupMessage::getId)
.last("limit 1")
.select(GroupMessage::getId);
GroupMessage message = this.getOne(wrapper); GroupMessage message = this.getOne(wrapper);
if (Objects.isNull(message)) { if (Objects.isNull(message)) {
return; return;
} }
// 推送消息给自己的其他终端 // 推送消息给自己的其他终端,同步清空会话列表中的未读数量
GroupMessageVO msgInfo = new GroupMessageVO(); GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.READED.code()); msgInfo.setType(MessageType.READED.code());
msgInfo.setSendTime(new Date()); msgInfo.setSendTime(new Date());
@ -220,10 +321,65 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
sendMessage.setData(msgInfo); sendMessage.setData(msgInfo);
sendMessage.setSendResult(true); sendMessage.setSendResult(true);
imClient.sendGroupMessage(sendMessage); imClient.sendGroupMessage(sendMessage);
// 已读消息key
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
// 原来的已读消息位置
Object maxReadedId = redisTemplate.opsForHash().get(key, session.getUserId().toString());
// 记录已读消息位置 // 记录已读消息位置
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId, session.getUserId()); redisTemplate.opsForHash().put(key, session.getUserId().toString(), message.getId());
redisTemplate.opsForValue().set(key, message.getId()); // 推送消息回执,刷新已读人数显示
wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId);
wrapper.gt(!Objects.isNull(maxReadedId), GroupMessage::getId, maxReadedId);
wrapper.le(!Objects.isNull(maxReadedId), GroupMessage::getId, message.getId());
wrapper.ne(GroupMessage::getStatus, MessageStatus.RECALL.code());
wrapper.eq(GroupMessage::getReceipt, true);
List<GroupMessage> receiptMessages = this.list(wrapper);
if (CollectionUtil.isNotEmpty(receiptMessages)) {
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId);
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
for (GroupMessage receiptMessage : receiptMessages) {
Integer readedCount = getReadedUserIds(maxIdMap, receiptMessage.getId(),receiptMessage.getSendId()).size();
// 如果所有人都已读,记录回执消息完成标记
if(readedCount >= userIds.size() - 1){
receiptMessage.setReceiptOk(true);
this.updateById(receiptMessage);
}
msgInfo = new GroupMessageVO();
msgInfo.setId(receiptMessage.getId());
msgInfo.setGroupId(groupId);
msgInfo.setReadedCount(readedCount);
msgInfo.setReceiptOk(receiptMessage.getReceiptOk());
msgInfo.setType(MessageType.RECEIPT.code());
sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
imClient.sendGroupMessage(sendMessage);
}
}
}
@Override
public List<Long> findReadedUsers(Long groupId, Long messageId) {
UserSession session = SessionContext.getSession();
GroupMessage message = this.getById(messageId);
if (Objects.isNull(message)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息不存在");
}
// 是否在群聊里面
GroupMember member = groupMemberService.findByGroupAndUserId(groupId, session.getUserId());
if (Objects.isNull(member) || member.getQuit()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面");
}
// 已读位置key
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
// 一次获取所有用户的已读位置
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
// 返回已读用户的id集合
return getReadedUserIds(maxIdMap, message.getId(),message.getSendId());
} }
@Override @Override
@ -249,4 +405,32 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return messageInfos; return messageInfos;
} }
private List<Long> getReadedUserIds(Map<Object, Object> maxIdMap, Long messageId, Long sendId) {
List<Long> userIds = new LinkedList<>();
maxIdMap.forEach((k, v) -> {
Long userId = Long.valueOf(k.toString());
long maxId = Long.parseLong(v.toString());
// 发送者不计入已读人数
if (!sendId.equals(userId) && maxId >= messageId) {
userIds.add(userId);
}
});
return userIds;
}
private void sendLoadingMessage(Boolean isLoadding){
UserSession session = SessionContext.getSession();
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.LOADDING.code());
msgInfo.setContent(isLoadding.toString());
IMGroupMessage sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(Arrays.asList(session.getUserId()));
sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal()));
sendMessage.setData(msgInfo);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
imClient.sendGroupMessage(sendMessage);
}
} }

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

@ -1,27 +1,30 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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;
import com.bx.imclient.IMClient; import com.bx.imclient.IMClient;
import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.util.CommaTextUtils;
import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.*;
import com.bx.implatform.entity.Group; import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.entity.GroupMember; import com.bx.implatform.enums.MessageType;
import com.bx.implatform.entity.User;
import com.bx.implatform.enums.ResultCode; import com.bx.implatform.enums.ResultCode;
import com.bx.implatform.exception.GlobalException; import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.mapper.GroupMapper; import com.bx.implatform.mapper.GroupMapper;
import com.bx.implatform.service.IFriendService; import com.bx.implatform.mapper.GroupMessageMapper;
import com.bx.implatform.service.IGroupMemberService; import com.bx.implatform.service.*;
import com.bx.implatform.service.IGroupService;
import com.bx.implatform.service.IUserService;
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.util.BeanUtils;
import com.bx.implatform.vo.GroupInviteVO; import com.bx.implatform.vo.GroupInviteVO;
import com.bx.implatform.vo.GroupMemberVO; import com.bx.implatform.vo.GroupMemberVO;
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;
@ -29,6 +32,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -42,9 +46,10 @@ import java.util.stream.Collectors;
public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements IGroupService { public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements IGroupService {
private final IUserService userService; private final IUserService userService;
private final IGroupMemberService groupMemberService; private final IGroupMemberService groupMemberService;
private final GroupMessageMapper groupMessageMapper;
private final IFriendService friendsService; private final IFriendService friendsService;
private final IMClient imClient; private final IMClient imClient;
private final RedisTemplate<String, Object> redisTemplate;
@Override @Override
public GroupVO createGroup(GroupVO vo) { public GroupVO createGroup(GroupVO vo) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
@ -83,7 +88,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
} }
// 更新成员信息 // 更新成员信息
GroupMember member = groupMemberService.findByGroupAndUserId(vo.getId(), session.getUserId()); GroupMember member = groupMemberService.findByGroupAndUserId(vo.getId(), session.getUserId());
if (member == null) { if (Objects.isNull(member) || member.getQuit()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不是群聊的成员"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不是群聊的成员");
} }
member.setAliasName(StringUtils.isEmpty(vo.getAliasName()) ? session.getNickName() : vo.getAliasName()); member.setAliasName(StringUtils.isEmpty(vo.getAliasName()) ? session.getNickName() : vo.getAliasName());
@ -102,11 +107,18 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
if (!group.getOwnerId().equals(session.getUserId())) { if (!group.getOwnerId().equals(session.getUserId())) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "只有群主才有权限解除群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "只有群主才有权限解除群聊");
} }
// 群聊用户id
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId);
// 逻辑删除群数据 // 逻辑删除群数据
group.setDeleted(true); group.setDeleted(true);
this.updateById(group); this.updateById(group);
// 删除成员数据 // 删除成员数据
groupMemberService.removeByGroupId(groupId); groupMemberService.removeByGroupId(groupId);
// 清理已读缓存
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.delete(key);
// 推送解散群聊提示
this.sendTipMessage(groupId,userIds,String.format("'%s'解散了群聊",session.getNickName()));
log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName());
} }
@ -119,6 +131,11 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
} }
// 删除群聊成员 // 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId, userId); groupMemberService.removeByGroupAndUserId(groupId, userId);
// 清理已读缓存
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key,userId.toString());
// 推送退出群聊提示
this.sendTipMessage(groupId,Arrays.asList(userId),"您已退出群聊");
log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
} }
@ -130,24 +147,33 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不是群主,没有权限踢人"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不是群主,没有权限踢人");
} }
if (userId.equals(session.getUserId())) { if (userId.equals(session.getUserId())) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "亲,不能自己踢自己哟"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "亲,不能移除自己哟");
} }
// 删除群聊成员 // 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId, userId); groupMemberService.removeByGroupAndUserId(groupId, userId);
// 清理已读缓存
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key,userId.toString());
// 推送踢出群聊提示
this.sendTipMessage(groupId,Arrays.asList(userId),"您已被移出群聊");
log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
} }
@Override @Override
public GroupVO findById(Long groupId) { public GroupVO findById(Long groupId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = this.getById(groupId); Group group = super.getById(groupId);
if (Objects.isNull(group)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "群组不存在");
}
GroupMember member = groupMemberService.findByGroupAndUserId(groupId, session.getUserId()); GroupMember member = groupMemberService.findByGroupAndUserId(groupId, session.getUserId());
if (member == null) { if (Objects.isNull(member)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您未加入群聊"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "您未加入群聊");
} }
GroupVO vo = BeanUtils.copyProperties(group, GroupVO.class); GroupVO vo = BeanUtils.copyProperties(group, GroupVO.class);
vo.setAliasName(member.getAliasName()); vo.setAliasName(member.getAliasName());
vo.setRemark(member.getRemark()); vo.setRemark(member.getRemark());
vo.setQuit(member.getQuit());
return vo; return vo;
} }
@ -155,7 +181,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
@Override @Override
public Group getById(Long groupId) { public Group getById(Long groupId) {
Group group = super.getById(groupId); Group group = super.getById(groupId);
if (group == null) { if (Objects.isNull(group)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "群组不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "群组不存在");
} }
if (group.getDeleted()) { if (group.getDeleted()) {
@ -169,6 +195,8 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 查询当前用户的群id列表 // 查询当前用户的群id列表
List<GroupMember> groupMembers = groupMemberService.findByUserId(session.getUserId()); List<GroupMember> groupMembers = groupMemberService.findByUserId(session.getUserId());
// 一个月内退的群可能存在退群前的离线消息,一并返回作为前端缓存
groupMembers.addAll(groupMemberService.findQuitInMonth(session.getUserId()));
if (groupMembers.isEmpty()) { if (groupMembers.isEmpty()) {
return new LinkedList<>(); return new LinkedList<>();
} }
@ -183,6 +211,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
GroupMember member = groupMembers.stream().filter(m -> g.getId().equals(m.getGroupId())).findFirst().get(); GroupMember member = groupMembers.stream().filter(m -> g.getId().equals(m.getGroupId())).findFirst().get();
vo.setAliasName(member.getAliasName()); vo.setAliasName(member.getAliasName());
vo.setRemark(member.getRemark()); vo.setRemark(member.getRemark());
vo.setQuit(member.getQuit());
return vo; return vo;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
@ -191,9 +220,13 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
public void invite(GroupInviteVO vo) { public void invite(GroupInviteVO vo) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = this.getById(vo.getGroupId()); Group group = this.getById(vo.getGroupId());
if (group == null) { if (Objects.isNull(group)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊不存在");
} }
GroupMember member = groupMemberService.findByGroupAndUserId(vo.getGroupId(), session.getUserId());
if (Objects.isNull(group) || member.getQuit()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您不在群聊中,邀请失败");
}
// 群聊人数校验 // 群聊人数校验
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();
@ -223,6 +256,11 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
if (!groupMembers.isEmpty()) { if (!groupMembers.isEmpty()) {
groupMemberService.saveOrUpdateBatch(group.getId(), groupMembers); groupMemberService.saveOrUpdateBatch(group.getId(), groupMembers);
} }
// 推送进入群聊消息
List<Long> userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId());
String memberNames = groupMembers.stream().map(GroupMember::getAliasName).collect(Collectors.joining(","));
String content = String.format("'%s'邀请'%s'加入了群聊",session.getNickName(), memberNames);
this.sendTipMessage(vo.getGroupId(),userIds,content);
log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(), vo.getFriendIds()); log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(), vo.getFriendIds());
} }
@ -238,4 +276,33 @@ 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){
UserSession session = SessionContext.getSession();
// 消息入库
GroupMessage message = new GroupMessage();
message.setContent(content);
message.setType(MessageType.TIP_TEXT.code());
message.setStatus(MessageStatus.UNSEND.code());
message.setSendTime(new Date());
message.setSendNickName(session.getNickName());
message.setGroupId(groupId);
message.setSendId(session.getUserId());
message.setRecvIds(CommaTextUtils.asText(recvIds));
groupMessageMapper.insert(message);
// 推送
GroupMessageVO msgInfo = BeanUtils.copyProperties(message,GroupMessageVO.class);
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
if(CollUtil.isEmpty(recvIds)){
// 为空表示向全体发送
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId);
sendMessage.setRecvIds(userIds);
}else{
sendMessage.setRecvIds(recvIds);
}
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(false);
imClient.sendGroupMessage(sendMessage);
}
} }

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

@ -1,5 +1,6 @@
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;
@ -7,6 +8,8 @@ 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.imclient.IMClient;
import com.bx.imcommon.contant.IMConstant; import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMGroupMessage;
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;
@ -23,6 +26,7 @@ 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.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.PrivateMessageVO; import com.bx.implatform.vo.PrivateMessageVO;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -172,23 +176,82 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
return messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class)).collect(Collectors.toList()); return messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class)).collect(Collectors.toList());
} }
@Override
public void pullOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession();
if(!imClient.isOnline(session.getUserId())){
throw new GlobalException(ResultCode.PROGRAM_ERROR, "网络连接失败,无法拉取离线消息");
}
// 查询用户好友列表
List<Friend> friends = friendService.findFriendByUserId(session.getUserId());
if (friends.isEmpty()) {
return;
}
// 开启加载中标志
this.sendLoadingMessage(true);
List<Long> friendIds = friends.stream().map(Friend::getFriendId).collect(Collectors.toList());
// 获取当前用户的消息
LambdaQueryWrapper<PrivateMessage> queryWrapper = Wrappers.lambdaQuery();
// 只能拉取最近1个月的1000条消息
Date minDate = DateUtils.addMonths(new Date(), -1);
queryWrapper.gt(PrivateMessage::getId, minId)
.ge(PrivateMessage::getSendTime, minDate)
.ne(PrivateMessage::getStatus, MessageStatus.RECALL.code())
.and(wrap -> wrap.and(
wp -> wp.eq(PrivateMessage::getSendId, session.getUserId())
.in(PrivateMessage::getRecvId, friendIds))
.or(wp -> wp.eq(PrivateMessage::getRecvId, session.getUserId())
.in(PrivateMessage::getSendId, friendIds)))
.orderByDesc(PrivateMessage::getId)
.last("limit 1000");
List<PrivateMessage> messages = this.list(queryWrapper);
// 消息顺序从小到大
CollectionUtil.reverse(messages);
// 推送消息
for(PrivateMessage m:messages ){
PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code()));
sendMessage.setRecvId(session.getUserId());
sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal()));
sendMessage.setSendToSelf(false);
sendMessage.setData(vo);
sendMessage.setSendResult(true);
imClient.sendPrivateMessage(sendMessage);
}
// 关闭加载中标志
this.sendLoadingMessage(false);
log.info("拉取私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size());
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public void readedMessage(Long friendId) { public void readedMessage(Long friendId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 推送消息 // 推送消息给自己,清空会话列表上的已读数量
PrivateMessageVO msgInfo = new PrivateMessageVO(); PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setType(MessageType.READED.code()); msgInfo.setType(MessageType.READED.code());
msgInfo.setSendTime(new Date());
msgInfo.setSendId(session.getUserId()); msgInfo.setSendId(session.getUserId());
msgInfo.setRecvId(friendId); msgInfo.setRecvId(friendId);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>(); IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setData(msgInfo);
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(session.getUserId());
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
imClient.sendPrivateMessage(sendMessage);
// 推送回执消息给对方,更新已读状态
msgInfo = new PrivateMessageVO();
msgInfo.setType(MessageType.RECEIPT.code());
msgInfo.setSendId(session.getUserId());
msgInfo.setRecvId(friendId);
sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(friendId); sendMessage.setRecvId(friendId);
sendMessage.setSendToSelf(true); sendMessage.setSendToSelf(false);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false); sendMessage.setSendResult(false);
sendMessage.setData(msgInfo);
imClient.sendPrivateMessage(sendMessage); imClient.sendPrivateMessage(sendMessage);
// 修改消息状态为已读 // 修改消息状态为已读
LambdaUpdateWrapper<PrivateMessage> updateWrapper = Wrappers.lambdaUpdate(); LambdaUpdateWrapper<PrivateMessage> updateWrapper = Wrappers.lambdaUpdate();
@ -217,4 +280,20 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
} }
return message.getId(); return message.getId();
} }
private void sendLoadingMessage(Boolean isLoadding){
UserSession session = SessionContext.getSession();
PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setType(MessageType.LOADDING.code());
msgInfo.setContent(isLoadding.toString());
IMPrivateMessage sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(session.getUserId());
sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal()));
sendMessage.setData(msgInfo);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
imClient.sendPrivateMessage(sendMessage);
}
} }

10
im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java

@ -3,6 +3,7 @@ package com.bx.implatform.vo;
import com.bx.imcommon.serializer.DateToLongSerializer; import com.bx.imcommon.serializer.DateToLongSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import io.swagger.models.auth.In;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
@ -29,6 +30,15 @@ public class GroupMessageVO {
@ApiModelProperty(value = "消息内容类型 具体枚举值由应用层定义") @ApiModelProperty(value = "消息内容类型 具体枚举值由应用层定义")
private Integer type; private Integer type;
@ApiModelProperty(value = "是否回执消息")
private Boolean receipt;
@ApiModelProperty(value = "回执消息是否完成")
private Boolean receiptOk;
@ApiModelProperty(value = "已读消息数量")
private Integer readedCount = 0;
@ApiModelProperty(value = "@用户列表") @ApiModelProperty(value = "@用户列表")
private List<Long> atUserIds; private List<Long> atUserIds;

8
im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java

@ -1,5 +1,6 @@
package com.bx.implatform.vo; package com.bx.implatform.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
@ -40,4 +41,11 @@ public class GroupVO {
@ApiModelProperty(value = "群聊显示备注") @ApiModelProperty(value = "群聊显示备注")
private String remark; private String remark;
@ApiModelProperty(value = "是否已删除")
private Boolean deleted;
@ApiModelProperty(value = "是否已退出")
private Boolean quit;
} }

9
im-platform/src/main/resources/application.yml

@ -1,7 +1,5 @@
#这是配置服务的端口
server: server:
port: 8888 port: 8888
#配置项目的数据源
spring: spring:
application: application:
name: im-platform name: im-platform
@ -17,7 +15,6 @@ spring:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
database: 1
servlet: servlet:
multipart: multipart:
@ -26,12 +23,10 @@ spring:
mybatis-plus: mybatis-plus:
configuration: configuration:
# 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射 # 是否开启自动驼峰命名规则
map-underscore-to-camel-case: false map-underscore-to-camel-case: false
#log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# mapper
mapper-locations: mapper-locations:
# *.xml的具体路径
- classpath*:mapper/*.xml - classpath*:mapper/*.xml
minio: minio:
endpoint: http://127.0.0.1:9001 #内网地址 endpoint: http://127.0.0.1:9001 #内网地址

10
im-platform/src/main/resources/db/db.sql

@ -55,9 +55,10 @@ create table `im_group_member`(
`group_id` bigint not null comment '群id', `group_id` bigint not null comment '群id',
`user_id` bigint not null comment '用户id', `user_id` bigint not null comment '用户id',
`alias_name` varchar(255) DEFAULT '' comment '组内显示名称', `alias_name` varchar(255) DEFAULT '' comment '组内显示名称',
`head_image` varchar(255) default '' comment '用户头像', `head_image` varchar(255) DEFAULT '' comment '用户头像',
`remark` varchar(255) DEFAULT '' comment '备注', `remark` varchar(255) DEFAULT '' comment '备注',
`quit` tinyint(1) DEFAULT 0 comment '是否已退出', `quit` tinyint(1) DEFAULT 0 comment '是否已退出',
`quit_time` datetime DEFAULT NULL comment '退出时间',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间',
key `idx_group_id`(`group_id`), key `idx_group_id`(`group_id`),
key `idx_user_id`(`user_id`) key `idx_user_id`(`user_id`)
@ -68,10 +69,13 @@ create table `im_group_message`(
`group_id` bigint not null comment '群id', `group_id` bigint not null comment '群id',
`send_id` bigint not null comment '发送用户id', `send_id` bigint not null comment '发送用户id',
`send_nick_name` varchar(255) DEFAULT '' comment '发送用户昵称', `send_nick_name` varchar(255) DEFAULT '' comment '发送用户昵称',
`recv_ids` varchar(1024) DEFAULT '' comment '接收用户id,逗号分隔,为空表示发给所有成员',
`content` text comment '发送内容', `content` text comment '发送内容',
`at_user_ids` varchar(1024) comment '被@的用户id列表,逗号分隔', `at_user_ids` varchar(1024) comment '被@的用户id列表,逗号分隔',
`type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 10:系统提示' , `receipt` tinyint DEFAULT 0 comment '是否回执消息',
`status` tinyint(1) DEFAULT 0 comment '状态 0:正常 2:撤回', `receipt_ok` tinyint DEFAULT 0 comment '回执消息是否完成',
`type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 4:视频 10:系统提示' ,
`status` tinyint(1) DEFAULT 0 comment '状态 0:未发出 1:已送达 2:撤回 3:已读',
`send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间', `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间',
key `idx_group_id` (group_id) key `idx_group_id` (group_id)
)ENGINE=InnoDB CHARSET=utf8mb3 comment '群消息'; )ENGINE=InnoDB CHARSET=utf8mb3 comment '群消息';

4
im-server/pom.xml

@ -22,10 +22,6 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId> <artifactId>spring-boot</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.netty</groupId> <groupId>io.netty</groupId>
<artifactId>netty-all</artifactId> <artifactId>netty-all</artifactId>

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

@ -1,11 +1,7 @@
server:
port: 8877
spring: spring:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
database: 1
websocket: websocket:
enable: true enable: true

2
im-ui/package.json

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "set NODE_OPTIONS=--openssl-legacy-provider & vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {

3
im-ui/src/api/enums.js

@ -7,7 +7,10 @@ const MESSAGE_TYPE = {
VIDEO:4, VIDEO:4,
RECALL:10, RECALL:10,
READED:11, READED:11,
RECEIPT:12,
TIP_TIME:20, TIP_TIME:20,
TIP_TEXT:21,
LOADDING:30,
RTC_CALL: 101, RTC_CALL: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,
RTC_REJECT: 103, RTC_REJECT: 103,

1
im-ui/src/api/wssocket.js

@ -105,7 +105,6 @@ let heartCheck = {
// 实际调用的方法 // 实际调用的方法
let sendMessage = (agentData) => { let sendMessage = (agentData) => {
// console.log(globalCallback)
if (websock.readyState === websock.OPEN) { if (websock.readyState === websock.OPEN) {
// 若是ws开启状态 // 若是ws开启状态
websock.send(JSON.stringify(agentData)) websock.send(JSON.stringify(agentData))

12
im-ui/src/assets/iconfont/iconfont.css

@ -1,8 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3791506 */ font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.woff2?t=1669336625993') format('woff2'), src: url('iconfont.ttf?t=1706022894868') format('truetype');
url('iconfont.woff?t=1669336625993') format('woff'),
url('iconfont.ttf?t=1669336625993') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +11,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-ok:before {
content: "\e6ac";
}
.icon-receipt:before {
content: "\e61a";
}
.icon-biaoqing:before { .icon-biaoqing:before {
content: "\e60c"; content: "\e60c";
} }

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

Binary file not shown.

BIN
im-ui/src/assets/iconfont/iconfont.woff

Binary file not shown.

BIN
im-ui/src/assets/iconfont/iconfont.woff2

Binary file not shown.

55
im-ui/src/components/chat/ChatAtBox.vue

@ -1,26 +1,18 @@
<template> <template>
<el-scrollbar v-show="show" ref="scrollBox" class="group-member-choose" <el-scrollbar v-show="show" ref="scrollBox" class="group-member-choose"
:style="{'left':pos.x+'px','top':pos.y-300+'px'}"> :style="{'left':pos.x+'px','top':pos.y-300+'px'}">
<div v-for="(member,idx) in showMembers" :key="member.id"> <div v-for="(member) in showMembers" :key="member.id">
<div class="member-item" :class="idx==activeIdx?'active':''" @click="onSelectMember(member)"> <chat-group-member :member="member" :height="40" @click.native="onSelectMember(member)"></chat-group-member>
<div class="member-avatar">
<head-image :size="25" :name="member.aliasName" :url="member.headImage"> </head-image>
</div>
<div class="member-name">
<div>{{member.aliasName}}</div>
</div>
</div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</template> </template>
<script> <script>
import HeadImage from '../common/HeadImage.vue'; import ChatGroupMember from "./ChatGroupMember.vue";
export default { export default {
name: "chatAtBox", name: "chatAtBox",
components: { components: {
HeadImage ChatGroupMember
}, },
props: { props: {
searchText: { searchText: {
@ -58,7 +50,7 @@
}) })
} }
this.members.forEach((m) => { this.members.forEach((m) => {
if (m.userId != userId && m.aliasName.startsWith(this.searchText)) { if (m.userId != userId && !m.quit && m.aliasName.startsWith(this.searchText)) {
this.showMembers.push(m); this.showMembers.push(m);
} }
}) })
@ -134,42 +126,5 @@
border-radius: 5px; border-radius: 5px;
background-color: #f5f5f5; background-color: #f5f5f5;
box-shadow: 0px 0px 10px #ccc; box-shadow: 0px 0px 10px #ccc;
.member-item {
display: flex;
height: 35px;
margin-bottom: 1px;
position: relative;
padding: 0 5px;
align-items: center;
background-color: #fafafa;
white-space: nowrap;
box-sizing: border-box;
&:hover {
background-color: #eeeeee;
}
&.active {
background-color: #eeeeee;
}
.member-avatar {
width: 25px;
height: 25px;
}
.member-name {
padding-left: 10px;
height: 100%;
text-align: left;
line-height: 40px;
white-space: nowrap;
overflow: hidden;
font-size: 14px;
font-weight: 600;
}
}
} }
</style> </style>

1471
im-ui/src/components/chat/ChatBox.vue

File diff suppressed because it is too large

67
im-ui/src/components/chat/ChatGroupMember.vue

@ -0,0 +1,67 @@
<template>
<div class="chat-group-member" :style="{'height':height+'px'}">
<div class="member-avatar">
<head-image :size="headImageSize" :name="member.aliasName" :url="member.headImage"> </head-image>
</div>
<div class="member-name" :style="{'line-height':height+'px'}">
<div>{{ member.aliasName }}</div>
</div>
</div>
</template>
<script>
import HeadImage from "../common/HeadImage.vue";
export default {
name: "groupMember",
components: { HeadImage },
data() {
return {};
},
props: {
member: {
type: Object,
required: true
},
height:{
type: Number,
default: 50
}
},
computed:{
headImageSize(){
return Math.ceil(this.height * 0.75)
}
}
}
</script>
<style lang="scss">
.chat-group-member {
display: flex;
margin-bottom: 1px;
position: relative;
padding: 0 5px;
align-items: center;
background-color: #fafafa;
white-space: nowrap;
box-sizing: border-box;
&:hover {
background-color: #eeeeee;
}
&.active {
background-color: #eeeeee;
}
.member-name {
padding-left: 10px;
height: 100%;
text-align: left;
white-space: nowrap;
overflow: hidden;
font-size: 14px;
font-weight: 600;
}
}
</style>

187
im-ui/src/components/chat/ChatGroupReaded.vue

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

8
im-ui/src/components/chat/ChatGroupSide.vue

@ -1,12 +1,12 @@
<template> <template>
<div class="chat-group-side"> <div class="chat-group-side">
<div class="group-side-search"> <div v-show="!group.quit" class="group-side-search">
<el-input placeholder="搜索群成员" v-model="searchText"> <el-input placeholder="搜索群成员" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button> <el-button slot="append" icon="el-icon-search"></el-button>
</el-input> </el-input>
</div> </div>
<el-scrollbar class="group-side-scrollbar"> <el-scrollbar class="group-side-scrollbar">
<div class="group-side-member-list"> <div v-show="!group.quit" class="group-side-member-list">
<div class="group-side-invite"> <div class="group-side-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember=true"> <div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember=true">
<i class="el-icon-plus"></i> <i class="el-icon-plus"></i>
@ -20,7 +20,7 @@
:showDel="false"></group-member> :showDel="false"></group-member>
</div> </div>
</div> </div>
<el-divider 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"> <el-form labelPosition="top" class="group-side-form" :model="group">
<el-form-item label="群聊名称"> <el-form-item label="群聊名称">
<el-input v-model="group.name" disabled maxlength="20"></el-input> <el-input v-model="group.name" disabled maxlength="20"></el-input>
@ -38,7 +38,7 @@
<el-input v-model="group.aliasName" :disabled="!editing" placeholder="xx" maxlength="20"></el-input> <el-input v-model="group.aliasName" :disabled="!editing" placeholder="xx" maxlength="20"></el-input>
</el-form-item> </el-form-item>
<div class="btn-group"> <div v-show="!group.quit" class="btn-group">
<el-button v-show="editing" type="success" @click="onSaveGroup()">提交</el-button> <el-button v-show="editing" type="success" @click="onSaveGroup()">提交</el-button>
<el-button v-show="!editing" type="primary" @click="editing=!editing">编辑</el-button> <el-button v-show="!editing" type="primary" @click="editing=!editing">编辑</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button> <el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>

660
im-ui/src/components/chat/ChatMessageItem.vue

@ -1,408 +1,434 @@
<template> <template>
<div class="chat-msg-item"> <div class="chat-msg-item">
<div class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</div> <div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">{{ msgInfo.content }}</div>
<div class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME"> <div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{$date.toTimeText(msgInfo.sendTime)}} {{ $date.toTimeText(msgInfo.sendTime) }}
</div> </div>
<div class="chat-msg-normal" v-show="msgInfo.type>=0 && msgInfo.type<10" :class="{'chat-msg-mine':mine}"> <div class="chat-msg-normal" v-show="msgInfo.type >= 0 && msgInfo.type < 10" :class="{ 'chat-msg-mine': mine }">
<div class="head-image"> <div class="head-image">
<head-image :name="showName" :size="40" :url="headImage" :id="msgInfo.sendId"></head-image> <head-image :name="showName" :size="40" :url="headImage" :id="msgInfo.sendId"></head-image>
</div> </div>
<div class="chat-msg-content"> <div class="chat-msg-content">
<div v-show="mode==1 && msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top"> <div v-show="mode == 1 && msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<span>{{showName}}</span> <span>{{ showName }}</span>
</div> </div>
<div v-show="mode==2" class="chat-msg-top"> <div v-show="mode == 2" class="chat-msg-top">
<span>{{showName}}</span> <span>{{ showName }}</span>
<span>{{$date.toTimeText(msgInfo.sendTime)}}</span> <span>{{ $date.toTimeText(msgInfo.sendTime) }}</span>
</div> </div>
<div class="chat-msg-bottom" @contextmenu.prevent="showRightMenu($event)"> <div class="chat-msg-bottom" @contextmenu.prevent="showRightMenu($event)">
<span class="chat-msg-text" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT" <div ref="chatMsgBox">
v-html="$emo.transform(msgInfo.content)"></span> <span class="chat-msg-text" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT"
<div class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE"> v-html="$emo.transform(msgInfo.content)"></span>
<div class="img-load-box" v-loading="loading" element-loading-text="上传中.." <div class="chat-msg-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
element-loading-background="rgba(0, 0, 0, 0.4)"> <div class="img-load-box" v-loading="loading" element-loading-text="上传中.."
<img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" element-loading-background="rgba(0, 0, 0, 0.4)">
@click="showFullImageBox()" /> <img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl"
</div> @click="showFullImageBox()" />
<span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span>
</div>
<div class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE">
<div class="chat-file-box" v-loading="loading">
<div class="chat-file-info">
<el-link class="chat-file-name" :underline="true" target="_blank" type="primary"
:href="data.url">{{data.name}}</el-link>
<div class="chat-file-size">{{fileSize}}</div>
</div> </div>
<div class="chat-file-icon"> <span title="发送失败" v-show="loadFail" @click="onSendFail"
<span type="primary" class="el-icon-document"></span> class="send-fail el-icon-warning"></span>
</div>
<div class="chat-msg-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<div class="chat-file-box" v-loading="loading">
<div class="chat-file-info">
<el-link class="chat-file-name" :underline="true" target="_blank" type="primary"
:href="data.url">{{ data.name }}</el-link>
<div class="chat-file-size">{{ fileSize }}</div>
</div>
<div class="chat-file-icon">
<span type="primary" class="el-icon-document"></span>
</div>
</div> </div>
<span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span>
</div> </div>
<span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span>
</div> </div>
<div class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" <div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
@click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> <audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div> </div>
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId <span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</span> && msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId <span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</span> && msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span>
<div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span>
<span v-else>{{msgInfo.readedCount}}人已读</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show = false"
@close="rightMenu.show=false" @select="onSelectMenu"></right-menu> @select="onSelectMenu"></right-menu>
<chat-group-readed ref="chatGroupReadedBox" :msgInfo="msgInfo" :groupMembers="groupMembers"></chat-group-readed>
</div> </div>
</template> </template>
<script> <script>
import HeadImage from "../common/HeadImage.vue"; import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue';
export default { export default {
name: "messageItem", name: "messageItem",
components: { components: {
HeadImage, HeadImage,
RightMenu RightMenu,
ChatGroupReaded
},
props: {
mode: {
type: Number,
default: 1
}, },
props: { mine: {
mode: { type: Boolean,
type: Number, required: true
default: 1
},
mine: {
type: Boolean,
required: true
},
headImage: {
type: String,
required: true
},
showName: {
type: String,
required: true
},
msgInfo: {
type: Object,
required: true
},
menu: {
type: Boolean,
default: true
}
}, },
data() { headImage: {
return { type: String,
audioPlayState: 'STOP', required: true
rightMenu: { },
show: false, showName: {
pos: { type: String,
x: 0, required: true
y: 0 },
} msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array
},
menu: {
type: Boolean,
default: true
}
},
data() {
return {
audioPlayState: 'STOP',
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
} }
} }
}
},
methods: {
onSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
}, },
methods: { showFullImageBox() {
onSendFail() { let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送") if (imageUrl) {
}, this.$store.commit('showFullImageBox', imageUrl);
showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if (imageUrl) {
this.$store.commit('showFullImageBox', imageUrl);
}
},
onPlayVoice() {
if (!this.audio) {
this.audio = new Audio();
}
this.audio.src = JSON.parse(this.msgInfo.content).url;
this.audio.play();
this.onPlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
} }
}, },
computed: { onPlayVoice() {
loading() { if (!this.audio) {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading"; this.audio = new Audio();
}, }
loadFail() { this.audio.src = JSON.parse(this.msgInfo.content).url;
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail"; this.audio.play();
}, this.onPlayVoice = 'RUNNING';
data() { },
return JSON.parse(this.msgInfo.content) showRightMenu(e) {
}, this.rightMenu.pos = {
fileSize() { x: e.x,
let size = this.data.size; y: e.y
if (size > 1024 * 1024) { };
return Math.round(size / 1024 / 1024) + "M"; this.rightMenu.show = "true";
} },
if (size > 1024) { onSelectMenu(item) {
return Math.round(size / 1024) + "KB"; this.$emit(item.key.toLowerCase(), this.msgInfo);
} },
return size + "B"; onShowReadedBox() {
}, let rect = this.$refs.chatMsgBox.getBoundingClientRect();
menuItems() { this.$refs.chatGroupReadedBox.open(rect);
let items = []; }
},
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
},
data() {
return JSON.parse(this.msgInfo.content)
},
fileSize() {
let size = this.data.size;
if (size > 1024 * 1024) {
return Math.round(size / 1024 / 1024) + "M";
}
if (size > 1024) {
return Math.round(size / 1024) + "KB";
}
return size + "B";
},
menuItems() {
let items = [];
items.push({
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
});
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
items.push({ items.push({
key: 'DELETE', key: 'RECALL',
name: '删除', name: '撤回',
icon: 'el-icon-delete' icon: 'el-icon-refresh-left'
}); });
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
items.push({
key: 'RECALL',
name: '撤回',
icon: 'el-icon-refresh-left'
});
}
return items;
} }
return items;
} }
} }
}
</script> </script>
<style scoped lang="scss"> <style lang="scss">
.chat-msg-item { .chat-msg-item {
.chat-msg-tip { .chat-msg-tip {
line-height: 50px; line-height: 50px;
font-size: 14px; font-size: 14px;
}
.chat-msg-normal {
position: relative;
font-size: 0;
padding-left: 60px;
min-height: 50px;
margin-top: 10px;
.head-image {
position: absolute;
width: 40px;
height: 40px;
top: 0;
left: 0;
} }
.chat-msg-normal { .chat-msg-content {
position: relative; text-align: left;
font-size: 0;
padding-left: 60px;
min-height: 50px;
margin-top: 10px;
.head-image { .send-fail {
position: absolute; color: #e60c0c;
width: 40px; font-size: 30px;
height: 40px; cursor: pointer;
top: 0; margin: 0 20px;
left: 0;
} }
.chat-msg-content { .chat-msg-top {
text-align: left; display: flex;
flex-wrap: nowrap;
color: #333;
font-size: 14px;
line-height: 20px;
.send-fail { span {
color: #e60c0c; margin-right: 12px;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
} }
}
.chat-msg-top { .chat-msg-bottom {
display: flex; display: inline-block;
flex-wrap: nowrap; padding-right: 300px;
color: #333;
font-size: 14px; .chat-msg-text {
line-height: 20px; display: block;
position: relative;
span { line-height: 30px;
margin-right: 12px; margin-top: 3px;
padding: 7px;
background-color: white;
border-radius: 10px;
color: black;
display: block;
font-size: 16px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
box-shadow: 1px 1px 1px #c0c0f0;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: white transparent transparent;
overflow: hidden;
border-width: 10px;
} }
} }
.chat-msg-bottom { .chat-msg-image {
display: inline-block; display: flex;
padding-right: 80px; flex-wrap: nowrap;
flex-direction: row;
.chat-msg-text { align-items: center;
display: block;
position: relative; .send-image {
line-height: 30px; min-width: 200px;
margin-top: 3px; min-height: 150px;
padding: 7px; max-width: 400px;
background-color: white; max-height: 300px;
border-radius: 10px; border: #dddddd solid 1px;
color: black; border: 5px solid #ccc;
display: block; border-radius: 6px;
font-size: 16px; cursor: pointer;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
box-shadow: 1px 1px 1px #c0c0f0;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: white transparent transparent;
overflow: hidden;
border-width: 10px;
}
} }
.chat-msg-image { }
.chat-msg-file {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
cursor: pointer;
padding-bottom: 5px;
.chat-file-box {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
flex-direction: row;
align-items: center; align-items: center;
min-height: 80px;
.send-image { box-shadow: 5px 5px 2px #c0c0c0;
min-width: 200px; border: #dddddd solid 1px;
min-height: 150px; border-radius: 6px;
max-width: 400px; background-color: #eeeeee;
max-height: 300px; padding: 10px 15px;
border: #dddddd solid 1px;
border: 5px solid #ccc; .chat-file-info {
border-radius: 6px; flex: 1;
cursor: pointer; height: 100%;
text-align: left;
font-size: 14px;
.chat-file-name {
display: inline-block;
min-width: 150px;
max-width: 300px;
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
white-space: pre-wrap;
word-break: break-all;
}
} }
.chat-file-icon {
font-size: 50px;
color: #d42e07;
}
} }
.chat-msg-file { .send-fail {
display: flex; color: #e60c0c;
flex-wrap: nowrap; font-size: 30px;
flex-direction: row;
align-items: center;
cursor: pointer; cursor: pointer;
margin: 0 20px;
}
.chat-file-box { }
display: flex;
flex-wrap: nowrap;
align-items: center;
min-height: 80px;
box-shadow: 5px 5px 2px #c0c0c0;
border: #dddddd solid 1px;
border-radius: 6px;
background-color: #eeeeee;
padding: 10px 15px;
.chat-file-info {
flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
.chat-file-name {
display: inline-block;
min-width: 150px;
max-width: 300px;
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
white-space: pre-wrap;
word-break: break-all;
}
}
.chat-file-icon {
font-size: 50px;
color: #d42e07;
}
}
.send-fail { .chat-msg-voice {
color: #e60c0c; font-size: 14px;
font-size: 30px; cursor: pointer;
cursor: pointer;
margin: 0 20px;
}
audio {
height: 45px;
padding: 5px 0;
} }
}
.chat-msg-voice { .chat-unread {
font-size: 14px; font-size: 12px;
cursor: pointer; color: #f23c0f;
font-weight: 600;
}
audio { .chat-readed {
height: 45px; font-size: 12px;
padding: 5px 0; color: #888;
} font-weight: 600;
} }
.chat-unread { .chat-receipt{
font-size: 10px; font-size: 13px;
color: #f23c0f; color: blue;
font-weight: 600; cursor: pointer;
}
.chat-readed { .icon-ok {
font-size: 10px; font-size: 20px;
color: #888; color: #329432;
font-weight: 600;
} }
} }
} }
}
&.chat-msg-mine {
text-align: right;
padding-left: 0;
padding-right: 60px;
&.chat-msg-mine { .head-image {
left: auto;
right: 0;
}
.chat-msg-content {
text-align: right; text-align: right;
padding-left: 0;
padding-right: 60px;
.head-image { .chat-msg-top {
left: auto; flex-direction: row-reverse;
right: 0;
span {
margin-left: 12px;
margin-right: 0;
}
} }
.chat-msg-content { .chat-msg-bottom {
text-align: right; padding-left: 180px;
padding-right: 0;
.chat-msg-top { .chat-msg-text {
flex-direction: row-reverse; margin-left: 10px;
background-color: rgb(88, 127, 240);
color: #fff;
vertical-align: top;
box-shadow: 1px 1px 1px #ccc;
span { &:after {
margin-left: 12px; left: auto;
margin-right: 0; right: -10px;
border-top-color: rgb(88, 127, 240);
} }
} }
.chat-msg-bottom { .chat-msg-image {
padding-left: 80px; flex-direction: row-reverse;
padding-right: 0; }
.chat-msg-text {
margin-left: 10px;
background-color: rgb(88, 127, 240);
color: #fff;
vertical-align: top;
box-shadow: 1px 1px 1px #ccc;
&:after {
left: auto;
right: -10px;
border-top-color: rgb(88, 127, 240);
}
}
.chat-msg-image {
flex-direction: row-reverse;
}
.chat-msg-file { .chat-msg-file {
flex-direction: row-reverse; flex-direction: row-reverse;
}
} }
} }
} }
} }
} }
}
</style> </style>

1
im-ui/src/components/chat/ChatPrivateVideo.vue

@ -93,7 +93,6 @@
}, },
(stream) => { (stream) => {
this.stream = stream; this.stream = stream;
console.log(this.stream)
this.$refs.mineVideo.srcObject = stream; this.$refs.mineVideo.srcObject = stream;
this.$refs.mineVideo.muted = true; this.$refs.mineVideo.muted = true;
callback(stream) callback(stream)

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

@ -60,9 +60,7 @@
this.state = 'RUNNING'; this.state = 'RUNNING';
this.stateTip = "正在录音..."; this.stateTip = "正在录音...";
}).catch(error => { }).catch(error => {
console.log(error);
this.$message.error(error); this.$message.error(error);
console.log(error);
}); });
@ -90,7 +88,6 @@
this.mode = 'PLAY'; this.mode = 'PLAY';
}, },
onStopAudio() { onStopAudio() {
console.log(this.$refs.audio);
this.$refs.audio.pause(); this.$refs.audio.pause();
this.mode = 'RECORD'; this.mode = 'RECORD';
}, },

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

@ -110,7 +110,6 @@
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);
console.log(m);
if (m) { if (m) {
// //
friend.disabled = true; friend.disabled = true;

136
im-ui/src/store/chatStore.js

@ -77,10 +77,10 @@ export default {
state.chats[idx].messages.forEach((m) => { state.chats[idx].messages.forEach((m) => {
if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) { if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) {
// pos.maxId为空表示整个会话已读 // pos.maxId为空表示整个会话已读
if(!pos.maxId || m.id <= pos.maxId){ if (!pos.maxId || m.id <= pos.maxId) {
m.status = MESSAGE_STATUS.READED m.status = MESSAGE_STATUS.READED
} }
} }
}) })
} }
@ -96,8 +96,8 @@ export default {
}, },
moveTop(state, idx) { moveTop(state, idx) {
// 加载中不移动,很耗性能 // 加载中不移动,很耗性能
if(state.loadingPrivateMsg || state.loadingGroupMsg){ if (state.loadingPrivateMsg || state.loadingGroupMsg) {
return ; return;
} }
if (idx > 0) { if (idx > 0) {
let chat = state.chats[idx]; let chat = state.chats[idx];
@ -106,14 +106,6 @@ export default {
this.commit("saveToStorage"); this.commit("saveToStorage");
} }
}, },
removeGroupChat(state, groupId) {
for (let idx in state.chats) {
if (state.chats[idx].type == 'GROUP' &&
state.chats[idx].targetId == groupId) {
this.commit("removeChat", idx);
}
}
},
removePrivateChat(state, friendId) { removePrivateChat(state, friendId) {
for (let idx in state.chats) { for (let idx in state.chats) {
if (state.chats[idx].type == 'PRIVATE' && if (state.chats[idx].type == 'PRIVATE' &&
@ -123,17 +115,21 @@ export default {
} }
}, },
insertMessage(state, msgInfo) { insertMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE'; let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId; // 记录消息的最大id
let chat = null; if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
for (let idx in state.chats) { state.privateMsgMaxId = msgInfo.id;
if (state.chats[idx].type == type && }
state.chats[idx].targetId === targetId) { if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
chat = state.chats[idx]; state.groupMsgMaxId = msgInfo.id;
this.commit("moveTop", idx) }
break; // 如果是已存在消息,则覆盖旧的消息数据
} let chat = this.getters.findChat(msgInfo);
let message = this.getters.findMessage(chat, msgInfo);
if (message) {
Object.assign(message, msgInfo);
this.commit("saveToStorage");
return;
} }
// 插入新的数据 // 插入新的数据
if (msgInfo.type == MESSAGE_TYPE.IMAGE) { if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
@ -142,13 +138,13 @@ export default {
chat.lastContent = "[文件]"; chat.lastContent = "[文件]";
} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) { } else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
} else { } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
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) {
chat.unreadCount++; chat.unreadCount++;
} }
// 是否有人@我 // 是否有人@我
@ -162,28 +158,6 @@ export default {
chat.atAll = true; chat.atAll = true;
} }
} }
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) {
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
}
// 间隔大于10分钟插入时间显示 // 间隔大于10分钟插入时间显示
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) { if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
chat.messages.push({ chat.messages.push({
@ -196,19 +170,18 @@ export default {
chat.messages.push(msgInfo); chat.messages.push(msgInfo);
this.commit("saveToStorage"); this.commit("saveToStorage");
}, },
deleteMessage(state, msgInfo) { updateMessage(state, msgInfo) {
// 获取对方id或群id // 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE'; let chat = this.getters.findChat(msgInfo);
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId; let message = this.getters.findMessage(chat, msgInfo);
let chat = null; if (message) {
for (let idx in state.chats) { // 属性拷贝
if (state.chats[idx].type == type && Object.assign(message, msgInfo);
state.chats[idx].targetId === targetId) { this.commit("saveToStorage");
chat = state.chats[idx];
break;
}
} }
},
deleteMessage(state, msgInfo) {
let chat = this.getters.findChat(msgInfo);
for (let idx in chat.messages) { for (let idx in chat.messages) {
// 已经发送成功的,根据id删除 // 已经发送成功的,根据id删除
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) { if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
@ -249,18 +222,18 @@ export default {
loadingPrivateMsg(state, loadding) { loadingPrivateMsg(state, loadding) {
state.loadingPrivateMsg = loadding; state.loadingPrivateMsg = loadding;
if(!state.loadingPrivateMsg && !state.loadingGroupMsg){ if (!state.loadingPrivateMsg && !state.loadingGroupMsg) {
this.commit("sort") this.commit("sort")
} }
}, },
loadingGroupMsg(state, loadding) { loadingGroupMsg(state, loadding) {
state.loadingGroupMsg = loadding; state.loadingGroupMsg = loadding;
if(!state.loadingPrivateMsg && !state.loadingGroupMsg){ if (!state.loadingPrivateMsg && !state.loadingGroupMsg) {
this.commit("sort") this.commit("sort")
} }
}, },
sort(state){ sort(state) {
state.chats.sort((c1,c2)=>c2.lastSendTime-c1.lastSendTime); state.chats.sort((c1, c2) => c2.lastSendTime - c1.lastSendTime);
}, },
saveToStorage(state) { saveToStorage(state) {
let userId = userStore.state.userInfo.id; let userId = userStore.state.userInfo.id;
@ -290,5 +263,46 @@ export default {
resolve(); resolve();
}) })
} }
},
getters: {
findChatIdx: (state) => (chat) => {
for (let idx in state.chats) {
if (state.chats[idx].type == chat.type &&
state.chats[idx].targetId === chat.targetId) {
chat = state.chats[idx];
return idx
}
}
},
findChat: (state) => (msgInfo) => {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
break;
}
}
return chat;
},
findMessage: (state) => (chat, msgInfo) => {
if (!chat) {
return null;
}
for (let idx in chat.messages) {
// 通过id判断
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
return chat.messages[idx];
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
return chat.messages[idx];
}
}
}
} }
} }

1
im-ui/src/store/friendStore.js

@ -96,7 +96,6 @@ export default {
}).then((friends) => { }).then((friends) => {
context.commit("setFriends", friends); context.commit("setFriends", friends);
context.commit("refreshOnlineStatus"); context.commit("refreshOnlineStatus");
console.log("loadFriend")
resolve() resolve()
}).catch((res) => { }).catch((res) => {
reject(); reject();

1
im-ui/src/store/groupStore.js

@ -47,7 +47,6 @@ export default {
method: 'GET' method: 'GET'
}).then((groups) => { }).then((groups) => {
context.commit("setGroups", groups); context.commit("setGroups", groups);
console.log("loadGroup")
resolve(); resolve();
}).catch((res) => { }).catch((res) => {
reject(res); reject(res);

1
im-ui/src/store/index.js

@ -15,7 +15,6 @@ export default new Vuex.Store({
}, },
actions: { actions: {
load(context) { load(context) {
console.log("load")
return this.dispatch("loadUser").then(() => { return this.dispatch("loadUser").then(() => {
const promises = []; const promises = [];
promises.push(this.dispatch("loadFriend")); promises.push(this.dispatch("loadFriend"));

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

@ -12,7 +12,7 @@
</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="(group,index) in groupStore.groups" :key="index">
<group-item v-show="group.remark.startsWith(searchText)" :group="group" <group-item v-show="!group.quit&&group.remark.startsWith(searchText)" :group="group"
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)"> :active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)">
</group-item> </group-item>
</div> </div>
@ -189,7 +189,6 @@
}).then(() => { }).then(() => {
this.$message.success(`群聊'${this.activeGroup.name}'已解散`); this.$message.success(`群聊'${this.activeGroup.name}'已解散`);
this.$store.commit("removeGroup", this.activeGroup.id); this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("removeGroupChat", this.activeGroup.id);
this.reset(); this.reset();
}); });
}) })

624
im-ui/src/view/Home.vue

@ -4,15 +4,14 @@
<div class="user-head-image"> <div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName" <head-image :name="$store.state.userStore.userInfo.nickName"
:url="$store.state.userStore.userInfo.headImageThumb" :size="60" :url="$store.state.userStore.userInfo.headImageThumb" :size="60"
@click.native="showSettingDialog=true"> @click.native="showSettingDialog = true">
</head-image> </head-image>
</div> </div>
<el-menu background-color="#333333" text-color="#ddd" style="margin-top: 30px;"> <el-menu background-color="#333333" text-color="#ddd" style="margin-top: 30px;">
<el-menu-item title="聊天"> <el-menu-item title="聊天">
<router-link v-bind:to="'/home/chat'"> <router-link v-bind:to="'/home/chat'">
<span class="el-icon-chat-dot-round"></span> <span class="el-icon-chat-dot-round"></span>
<div v-show="unreadCount>0" class="unread-text">{{unreadCount}}</div> <div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
</router-link> </router-link>
</el-menu-item> </el-menu-item>
<el-menu-item title="好友"> <el-menu-item title="好友">
@ -25,12 +24,10 @@
<span class="icon iconfont icon-group_fill"></span> <span class="icon iconfont icon-group_fill"></span>
</router-link> </router-link>
</el-menu-item> </el-menu-item>
<el-menu-item title="设置" @click="showSetting()"> <el-menu-item title="设置" @click="showSetting()">
<span class="el-icon-setting"></span> <span class="el-icon-setting"></span>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div class="exit-box" @click="onExit()" title="退出"> <div class="exit-box" @click="onExit()" title="退出">
<span class="el-icon-circle-close"></span> <span class="el-icon-circle-close"></span>
</div> </div>
@ -47,367 +44,350 @@
:friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master" :friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master"
:offer="uiStore.chatPrivateVideo.offer" @close="$store.commit('closeChatPrivateVideoBox')"> :offer="uiStore.chatPrivateVideo.offer" @close="$store.commit('closeChatPrivateVideoBox')">
</chat-private-video> </chat-private-video>
<chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show" <chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show" :friend="uiStore.videoAcceptor.friend"
:friend="uiStore.videoAcceptor.friend" @close="$store.commit('closeVideoAcceptorBox')"> @close="$store.commit('closeVideoAcceptorBox')">
</chat-video-acceptor> </chat-video-acceptor>
</el-container> </el-container>
</template> </template>
<script> <script>
import HeadImage from '../components/common/HeadImage.vue'; import HeadImage from '../components/common/HeadImage.vue';
import Setting from '../components/setting/Setting.vue'; import Setting from '../components/setting/Setting.vue';
import UserInfo from '../components/common/UserInfo.vue'; import UserInfo from '../components/common/UserInfo.vue';
import FullImage from '../components/common/FullImage.vue'; import FullImage from '../components/common/FullImage.vue';
import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue'; import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue';
import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue'; import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue';
export default {
components: {
HeadImage,
Setting,
UserInfo,
FullImage,
ChatPrivateVideo,
ChatVideoAcceptor
},
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date() - 1000
}
},
methods: {
init() {
this.$store.dispatch("load").then(() => {
// ws
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.onConnect(() => {
// 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
});
this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// ws
this.$wsApi.close(3000)
// 线
this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", {
confirmButtonText: '确定',
callback: action => {
location.href = "/";
}
});
export default { } else if (cmd == 3) {
components: { //
HeadImage, this.handlePrivateMessage(msgInfo);
Setting, } else if (cmd == 4) {
UserInfo, //
FullImage, this.handleGroupMessage(msgInfo);
ChatPrivateVideo,
ChatVideoAcceptor
},
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date() - 1000
}
},
methods: {
init() {
this.$store.dispatch("load").then(() => {
// ws
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.onConnect(()=>{
// 线
this.loadPrivateMessage(this.$store.state.chatStore.privateMsgMaxId);
this.loadGroupMessage(this.$store.state.chatStore.groupMsgMaxId);
});
this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// ws
this.$wsApi.close(3000)
// 线
this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", {
confirmButtonText: '确定',
callback: action => {
location.href = "/";
}
});
} else if (cmd == 3) {
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
this.handleGroupMessage(msgInfo);
}
});
this.$wsApi.onClose((e) => {
console.log(e);
if (e.code != 3000) {
// 线
this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
}
});
}).catch((e) => {
console.log("初始化失败", e);
})
},
loadPrivateMessage(minId) {
this.$store.commit("loadingPrivateMsg", true)
this.$http({
url: "/message/private/loadMessage?minId=" + minId,
method: 'get'
}).then((msgInfos) => {
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == this.$store.state.userStore.userInfo.id;
let friendId = msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let friend = this.$store.state.friendStore.friends.find((f) => f.id == friendId);
if (friend) {
this.insertPrivateMessage(friend, msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadPrivateMessage(msgInfos[99].id);
} else {
this.$store.commit("loadingPrivateMsg", false)
} }
}) });
}, this.$wsApi.onClose((e) => {
loadGroupMessage(minId) { console.log(e);
this.$store.commit("loadingGroupMsg", true) if (e.code != 3000) {
this.$http({ // 线
url: "/message/group/loadMessage?minId=" + minId, this.$message.error("连接断开,正在尝试重新连接...");
method: 'get' this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
}).then((msgInfos) => {
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == this.$store.state.userStore.userInfo.id;
let groupId = msgInfo.groupId;
let group = this.$store.state.groupStore.groups.find((g) => g.id == groupId);
if (group) {
this.insertGroupMessage(group, msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadGroupMessage(msgInfos[99].id);
} else {
this.$store.commit("loadingGroupMsg", false)
}
})
},
handlePrivateMessage(msg) {
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
if (msg.selfSend) {
//
let chatInfo = {
type: 'PRIVATE',
targetId: friendId
}
this.$store.commit("resetUnreadCount", chatInfo)
} else {
//
this.$store.commit("readedMessage", {friendId:friendId})
} }
return; });
} }).catch((e) => {
console.log("初始化失败", e);
this.loadFriendInfo(friendId).then((friend) => { })
this.insertPrivateMessage(friend, msg); },
pullPrivateOfflineMessage(minId) {
this.$http({
url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'get'
});
},
pullGroupOfflineMessage(minId) {
this.$http({
url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'get'
});
},
handlePrivateMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADDING) {
this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content))
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
this.$store.commit("resetUnreadCount", {
type: 'PRIVATE',
targetId: msg.recvId
}) })
}, return;
insertPrivateMessage(friend, msg) { }
// webrtc // ,
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL && if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { this.$store.commit("readedMessage", { friendId: msg.sendId })
// return;
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL || }
msg.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) { //
this.$store.commit("showVideoAcceptorBox", friend); msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
this.$refs.videoAcceptor.handleMessage(msg) // id
} else { let friendId = msg.selfSend ? msg.recvId : msg.sendId;
this.$refs.videoAcceptor.close() this.loadFriendInfo(friendId).then((friend) => {
this.$refs.privateVideo.handleMessage(msg) this.insertPrivateMessage(friend, msg);
} })
return; },
insertPrivateMessage(friend, msg) {
// webrtc
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL &&
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) {
this.$store.commit("showVideoAcceptorBox", friend);
this.$refs.videoAcceptor.handleMessage(msg)
} else {
this.$refs.videoAcceptor.close()
this.$refs.privateVideo.handleMessage(msg)
} }
return;
}
let chatInfo = { let chatInfo = {
type: 'PRIVATE', type: 'PRIVATE',
targetId: friend.id, targetId: friend.id,
showName: friend.nickName, showName: friend.nickName,
headImage: friend.headImage headImage: friend.headImage
}; };
// //
this.$store.commit("openChat", chatInfo); this.$store.commit("openChat", chatInfo);
// //
this.$store.commit("insertMessage", msg); this.$store.commit("insertMessage", msg);
// //
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) { if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();
} }
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
// //
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; if (msg.type == this.$enums.MESSAGE_TYPE.LOADDING) {
let groupId = msg.groupId; this.$store.commit("loadingGroupMsg", JSON.parse(msg.content))
// return;
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { }
// //
let chatInfo = { if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
type: 'GROUP', //
targetId: groupId
}
this.$store.commit("resetUnreadCount", chatInfo)
return;
}
this.loadGroupInfo(groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
},
insertGroupMessage(group, msg) {
let chatInfo = { let chatInfo = {
type: 'GROUP', type: 'GROUP',
targetId: group.id, targetId: msg.groupId
showName: group.remark,
headImage: group.headImageThumb
};
//
this.$store.commit("openChat", chatInfo);
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
onExit() {
this.$wsApi.close(3000);
sessionStorage.removeItem("accessToken");
location.href = "/";
},
playAudioTip() {
if (new Date() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date();
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;
audio.play();
} }
this.$store.commit("resetUnreadCount", chatInfo)
}, return;
showSetting() { }
this.showSettingDialog = true; //
}, if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
closeSetting() { //
this.showSettingDialog = false; let msgInfo = {
}, id: msg.id,
loadFriendInfo(id) { groupId: msg.groupId,
return new Promise((resolve, reject) => { readedCount: msg.readedCount,
let friend = this.$store.state.friendStore.friends.find((f) => f.id == id); receiptOk: msg.receiptOk
if (friend) { };
resolve(friend); this.$store.commit("updateMessage", msgInfo)
} else { return;
this.$http({
url: `/friend/find/${id}`,
method: 'get'
}).then((friend) => {
this.$store.commit("addFriend", friend);
resolve(friend)
})
}
});
},
loadGroupInfo(id) {
return new Promise((resolve, reject) => {
let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
if (group) {
resolve(group);
} else {
this.$http({
url: `/group/find/${id}`,
method: 'get'
}).then((group) => {
resolve(group)
this.$store.commit("addGroup", group);
})
}
});
} }
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
this.loadGroupInfo(msg.groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
}, },
computed: { insertGroupMessage(group, msg) {
uiStore() { let chatInfo = {
return this.$store.state.uiStore; type: 'GROUP',
}, targetId: group.id,
unreadCount() { showName: group.remark,
let unreadCount = 0; headImage: group.headImageThumb
let chats = this.$store.state.chatStore.chats; };
chats.forEach((chat) => { //
unreadCount += chat.unreadCount this.$store.commit("openChat", chatInfo);
}); //
return unreadCount; this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
} }
}, },
watch: { onExit() {
unreadCount: { this.$wsApi.close(3000);
handler(newCount, oldCount) { sessionStorage.removeItem("accessToken");
let tip = newCount > 0 ? `${newCount}条未读` : ""; location.href = "/";
this.$elm.setTitleTip(tip); },
}, playAudioTip() {
immediate: true if (new Date() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date();
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;
audio.play();
} }
},
showSetting() {
this.showSettingDialog = true;
},
closeSetting() {
this.showSettingDialog = false;
},
loadFriendInfo(id) {
return new Promise((resolve, reject) => {
let friend = this.$store.state.friendStore.friends.find((f) => f.id == id);
if (friend) {
resolve(friend);
} else {
this.$http({
url: `/friend/find/${id}`,
method: 'get'
}).then((friend) => {
this.$store.commit("addFriend", friend);
resolve(friend)
})
}
});
}, },
mounted() { loadGroupInfo(id) {
this.init(); return new Promise((resolve, reject) => {
let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
if (group) {
resolve(group);
} else {
this.$http({
url: `/group/find/${id}`,
method: 'get'
}).then((group) => {
resolve(group)
this.$store.commit("addGroup", group);
})
}
});
}
},
computed: {
uiStore() {
return this.$store.state.uiStore;
}, },
unmounted() { unreadCount() {
this.$wsApi.close(); let unreadCount = 0;
let chats = this.$store.state.chatStore.chats;
chats.forEach((chat) => {
unreadCount += chat.unreadCount
});
return unreadCount;
} }
},
watch: {
unreadCount: {
handler(newCount, oldCount) {
let tip = newCount > 0 ? `${newCount}条未读` : "";
this.$elm.setTitleTip(tip);
},
immediate: true
}
},
mounted() {
this.init();
},
unmounted() {
this.$wsApi.close();
} }
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.navi-bar {
background: #333333;
padding: 10px;
padding-top: 50px;
.navi-bar { .el-menu {
background: #333333; border: none;
padding: 10px; flex: 1;
padding-top: 50px;
.el-menu {
border: none;
flex: 1;
.el-menu-item { .el-menu-item {
margin: 25px 0; margin: 25px 0;
.router-link-exact-active span { .router-link-exact-active span {
color: white !important; color: white !important;
} }
span { span {
font-size: 24px !important; font-size: 24px !important;
color: #aaaaaa; color: #aaaaaa;
&:hover { &:hover {
color: white !important; color: white !important;
}
} }
}
.unread-text { .unread-text {
position: absolute; position: absolute;
line-height: 20px; line-height: 20px;
background-color: #f56c6c; background-color: #f56c6c;
left: 36px; left: 36px;
top: 7px; top: 7px;
color: white; color: white;
border-radius: 30px; border-radius: 30px;
padding: 0 5px; padding: 0 5px;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
border: 1px solid #f1e5e5; border: 1px solid #f1e5e5;
}
} }
} }
}
.exit-box { .exit-box {
position: absolute; position: absolute;
width: 60px; width: 60px;
bottom: 40px; bottom: 40px;
color: #aaaaaa; color: #aaaaaa;
font-size: 24px; font-size: 24px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: white !important; color: white !important;
}
} }
} }
}
.content-box { .content-box {
padding: 0; padding: 0;
background-color: #E9EEF3; background-color: #E9EEF3;
color: #333; color: #333;
text-align: center; text-align: center;
} }
</style> </style>

17
im-ui/src/view/Login.vue

@ -19,7 +19,22 @@
</li> </li>
</ul> </ul>
</div> </div>
<br/> <div>
<h3>最近更新(2024-01-28)</h3>
<ul>
<li>支持群聊已读显示(回执消息)</li>
<li>群聊会话窗口增加邀请退群移除解散提示</li>
</ul>
</div>
<div>
<h3>最近更新(2024-02-24)</h3>
<ul>
<li>uniapp端兼容ios和andriod,
<a href="https://www.boxim.online/download/boxim.apk" target="_blank">点击下载安卓客户端</a>
</li>
<li>uniapp端的启动和打包方式有所变化具体请参考语雀文档</li>
</ul>
</div>
<div> <div>
<h3>项目依旧完全开源可内网部署如果项目对您有帮助,请帮忙点个star:</h3> <h3>项目依旧完全开源可内网部署如果项目对您有帮助,请帮忙点个star:</h3>
</div> </div>

16
im-uniapp/.env.js

@ -0,0 +1,16 @@
//设置环境(打包前修改此变量)
const ENV = "DEV";
const UNI_APP = {}
if(ENV=="DEV"){
UNI_APP.BASE_URL = "http://127.0.0.1:8888";
UNI_APP.WS_URL = "ws://127.0.0.1:8878/im";
// H5 走本地代理解决跨域问题
// #ifdef H5
UNI_APP.BASE_URL = "/api";
// #endif
}
if(ENV=="PROD"){
UNI_APP.BASE_URL = "https://www.boxim.online/api";
UNI_APP.WS_URL = "wss://www.boxim.online:81/im";
}
export default UNI_APP

128
im-uniapp/App.vue

@ -3,7 +3,8 @@
import http from './common/request'; import http from './common/request';
import * as enums from './common/enums'; import * as enums from './common/enums';
import * as wsApi from './common/wssocket'; import * as wsApi from './common/wssocket';
import UNI_APP from '@/.env.js'
export default { export default {
data() { data() {
return { return {
@ -26,11 +27,11 @@
initWebSocket() { initWebSocket() {
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
wsApi.init(); wsApi.init();
wsApi.connect(process.env.WS_URL, loginInfo.accessToken); wsApi.connect(UNI_APP.WS_URL, loginInfo.accessToken);
wsApi.onConnect(() => { wsApi.onConnect(() => {
// 线 // 线
this.loadPrivateMessage(store.state.chatStore.privateMsgMaxId); this.pullPrivateOfflineMessage(store.state.chatStore.privateMsgMaxId);
this.loadGroupMessage(store.state.chatStore.groupMsgMaxId); this.pullGroupOfflineMessage(store.state.chatStore.groupMsgMaxId);
}); });
wsApi.onMessage((cmd, msgInfo) => { wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) { if (cmd == 2) {
@ -49,85 +50,53 @@
} }
}); });
wsApi.onClose((res) => { wsApi.onClose((res) => {
// 3000 // 1000
if (res.code != 3000) { if (res.code != 1000) {
// //
uni.showToast({ uni.showToast({
title: '连接已断开,尝试重新连接...', title: '连接已断开,尝试重新连接...',
icon: 'none', icon: 'none',
}) })
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
wsApi.reconnect(process.env.WS_URL, loginInfo.accessToken); wsApi.reconnect(UNI_APP.WS_URL, loginInfo.accessToken);
} }
}) })
}, },
loadPrivateMessage(minId) { pullPrivateOfflineMessage(minId) {
store.commit("loadingPrivateMsg", true)
http({ http({
url: "/message/private/loadMessage?minId=" + minId, url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'GET' method: 'get'
}).then((msgInfos) => { });
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id;
let friendId = msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let friend = store.state.friendStore.friends.find((f) => f.id == friendId);
if (friend) {
this.insertPrivateMessage(friend, msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadPrivateMessage(msgInfos[99].id);
} else {
store.commit("loadingPrivateMsg", false)
}
})
}, },
loadGroupMessage(minId) { pullGroupOfflineMessage(minId) {
store.commit("loadingGroupMsg", true)
http({ http({
url: "/message/group/loadMessage?minId=" + minId, url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'GET' method: 'get'
}).then((msgInfos) => { });
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id;
let groupId = msgInfo.groupId;
let group = store.state.groupStore.groups.find((g) => g.id == groupId);
if (group) {
this.insertGroupMessage(group, msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadGroupMessage(msgInfos[99].id);
} else {
store.commit("loadingGroupMsg", false)
}
})
}, },
handlePrivateMessage(msg) { handlePrivateMessage(msg) {
//
if (msg.type == enums.MESSAGE_TYPE.LOADDING) {
store.commit("loadingPrivateMsg", JSON.parse(msg.content))
return;
}
//
if (msg.type == enums.MESSAGE_TYPE.READED) {
store.commit("resetUnreadCount", {
type: 'PRIVATE',
targetId: msg.recvId
})
return;
}
// ,
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
store.commit("readedMessage", { friendId: msg.sendId })
return;
}
// //
msg.selfSend = msg.sendId == store.state.userStore.userInfo.id; msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
// id // id
let friendId = msg.selfSend ? msg.recvId : msg.sendId; let friendId = msg.selfSend ? msg.recvId : msg.sendId;
//
if (msg.type == enums.MESSAGE_TYPE.READED) {
if (msg.selfSend) {
//
let chatInfo = {
type: 'PRIVATE',
targetId: friendId
}
store.commit("resetUnreadCount", chatInfo)
} else {
//
store.commit("readedMessage", {
friendId: friendId
})
}
return;
}
this.loadFriendInfo(friendId).then((friend) => { this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg); this.insertPrivateMessage(friend, msg);
}) })
@ -153,20 +122,36 @@
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
// //
msg.selfSend = msg.sendId == store.state.userStore.userInfo.id; if (msg.type == enums.MESSAGE_TYPE.LOADDING) {
let groupId = msg.groupId; store.commit("loadingGroupMsg",JSON.parse(msg.content))
return;
}
// //
if (msg.type == enums.MESSAGE_TYPE.READED) { if (msg.type == enums.MESSAGE_TYPE.READED) {
// //
let chatInfo = { let chatInfo = {
type: 'GROUP', type: 'GROUP',
targetId: groupId targetId: msg.groupId
} }
store.commit("resetUnreadCount", chatInfo) store.commit("resetUnreadCount", chatInfo)
return; return;
} }
this.loadGroupInfo(groupId).then((group) => { //
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
//
let msgInfo = {
id: msg.id,
groupId: msg.groupId,
readedCount: msg.readedCount,
receiptOk: msg.receiptOk
};
store.commit("updateMessage", msgInfo)
return;
}
//
msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
this.loadGroupInfo(msg.groupId).then((group) => {
// //
this.insertGroupMessage(group, msg); this.insertGroupMessage(group, msg);
}) })
@ -220,7 +205,7 @@
}, },
exit() { exit() {
console.log("exit"); console.log("exit");
wsApi.close(); wsApi.close(1000);
uni.removeStorageSync("loginInfo"); uni.removeStorageSync("loginInfo");
uni.reLaunch({ uni.reLaunch({
url: "/pages/login/login" url: "/pages/login/login"
@ -234,7 +219,6 @@
// this.audioTip.play(); // this.audioTip.play();
}, },
initAudit() { initAudit() {
console.log("initAudit")
if (store.state.userStore.userInfo.type == 1) { if (store.state.userStore.userInfo.type == 1) {
// //
uni.setTabBarItem({ uni.setTabBarItem({

3
im-uniapp/common/enums.js

@ -7,7 +7,10 @@ const MESSAGE_TYPE = {
VIDEO:4, VIDEO:4,
RECALL:10, RECALL:10,
READED:11, READED:11,
RECEIPT:12,
TIP_TIME:20, TIP_TIME:20,
TIP_TEXT:21,
LOADDING:30,
RTC_CALL: 101, RTC_CALL: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,
RTC_REJECT: 103, RTC_REJECT: 103,

6
im-uniapp/common/request.js

@ -1,3 +1,5 @@
import UNI_APP from '@/.env.js'
// 请求队列 // 请求队列
let requestList = []; let requestList = [];
// 是否正在刷新中 // 是否正在刷新中
@ -11,7 +13,7 @@ const request = (options) => {
} }
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
uni.request({ uni.request({
url: process.env.BASE_URL + options.url, url: UNI_APP.BASE_URL + options.url,
method: options.method || 'GET', method: options.method || 'GET',
header: header, header: header,
data: options.data || {}, data: options.data || {},
@ -71,7 +73,7 @@ const reqRefreshToken = (loginInfo) => {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
uni.request({ uni.request({
method: 'PUT', method: 'PUT',
url: process.env.BASE_URL + '/refreshToken', url: UNI_APP.BASE_URL + '/refreshToken',
header: { header: {
refreshToken: loginInfo.refreshToken refreshToken: loginInfo.refreshToken
}, },

14
im-uniapp/common/wssocket.js

@ -46,17 +46,15 @@ let init = () => {
uni.onSocketClose((res) => { uni.onSocketClose((res) => {
console.log('WebSocket连接关闭') console.log('WebSocket连接关闭')
isConnect = false; //断开后修改标识 isConnect = false;
closeCallBack && closeCallBack(res); closeCallBack && closeCallBack(res);
}) })
uni.onSocketError((e) => { uni.onSocketError((e) => {
console.log(e) console.log(e)
isConnect = false; //连接断开修改标识 isConnect = false;
uni.showModal({ // APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连
content: '连接失败,可能是websocket服务不可用,请稍后再试', closeCallBack && closeCallBack({code: 1006});
showCancel: false,
})
}) })
}; };
@ -95,12 +93,12 @@ let reconnect = (wsurl, accessToken) => {
}; };
//设置关闭连接 //设置关闭连接
let close = () => { let close = (code) => {
if (!isConnect) { if (!isConnect) {
return; return;
} }
uni.closeSocket({ uni.closeSocket({
code: 3000, code: code,
complete: (res) => { complete: (res) => {
console.log("关闭websocket连接"); console.log("关闭websocket连接");
isConnect = false; isConnect = false;

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

@ -0,0 +1,131 @@
<template>
<uni-popup ref="popup" type="bottom">
<view class="chat-group-readed">
<view class="uni-padding-wrap uni-common-mt">
<uni-segmented-control :current="current" :values="items" style-type="button" active-color="#587ff0" @clickItem="onClickItem"/>
</view>
<view class="content">
<view v-if="current === 0">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="m in readedMembers" :key="m.userId">
<view class="member-item">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
:size="90"></head-image>
<view class="member-name">{{ m.aliasName}}</view>
</view>
</view>
</scroll-view>
</view>
<view v-if="current === 1">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="m in unreadMembers" :key="m.userId">
<view class="member-item">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
:size="90"></head-image>
<view class="member-name">{{ m.aliasName}}</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: "chat-group-readed",
data() {
return {
items: ['已读', '未读'],
current: 0,
readedMembers: [],
unreadMembers: []
};
},
props: {
msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array
}
},
methods: {
open() {
this.$refs.popup.open();
this.loadReadedUser();
},
loadReadedUser() {
this.readedMembers = [];
this.unreadMembers = [];
this.$http({
url: `/message/group/findReadedUsers?groupId=${this.msgInfo.groupId}&messageId=${this.msgInfo.id}`,
method: 'Get'
}).then(userIds => {
this.groupMembers.forEach(member => {
// 退
if (member.userId == this.msgInfo.sendId || member.quit) {
return;
}
//
if (userIds.find(userId => member.userId == userId)) {
this.readedMembers.push(member);
} else {
this.unreadMembers.push(member);
}
})
this.items[0] = `已读(${this.readedMembers.length})`;
this.items[1] = `未读(${this.unreadMembers.length})`;
//
this.$store.commit("updateMessage", {
id: this.msgInfo.id,
groupId: this.msgInfo.groupId,
readedCount: this.readedMembers.length
})
})
},
onClickItem(e){
this.current = e.currentIndex;
}
}
}
</script>
<style lang="scss" scoped>
.chat-group-readed {
position: relative;
border: #dddddd solid 1rpx;
display: flex;
flex-direction: column;
background-color: white;
padding: 10rpx;
border-radius: 15rpx;
.scroll-bar {
height: 800rpx;
}
.member-item {
height: 120rpx;
display: flex;
position: relative;
padding: 0 30rpx;
align-items: center;
background-color: white;
white-space: nowrap;
.member-name {
flex: 1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
}
</style>

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

@ -1,6 +1,6 @@
<template> <template>
<view class="chat-msg-item"> <view class="chat-msg-item">
<view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view> <view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.RECALL||msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">{{msgInfo.content}}</view>
<view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME"> <view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME">
{{$date.toTimeText(msgInfo.sendTime)}} {{$date.toTimeText(msgInfo.sendTime)}}
</view> </view>
@ -44,17 +44,20 @@
&& msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text> && msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId <text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text> && msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text>
<view class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
<text v-else>{{msgInfo.readedCount}}人已读</text>
</view>
<!-- <!--
<view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()"> <view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> <audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</view> </view>
--> -->
</view> </view>
</view> </view>
</view> </view>
<chat-group-readed ref="chatGroupReaded" :groupMembers="groupMembers" :msgInfo="msgInfo"></chat-group-readed>
<pop-menu v-if="menu.show" :menu-style="menu.style" :items="menuItems" @close="menu.show=false" <pop-menu v-if="menu.show" :menu-style="menu.style" :items="menuItems" @close="menu.show=false"
@select="onSelectMenu"></pop-menu> @select="onSelectMenu"></pop-menu>
</view> </view>
@ -75,6 +78,9 @@
msgInfo: { msgInfo: {
type: Object, type: Object,
required: true required: true
},
groupMembers: {
type: Array
} }
}, },
data() { data() {
@ -135,6 +141,9 @@
uni.previewImage({ uni.previewImage({
urls: [imageUrl] urls: [imageUrl]
}) })
},
onShowReadedBox() {
this.$refs.chatGroupReaded.open();
} }
}, },
computed: { computed: {
@ -345,6 +354,16 @@
color: #ccc; color: #ccc;
font-weight: 600; font-weight: 600;
} }
.chat-receipt {
font-size: 13px;
color: darkblue;
font-weight: 600;
.icon-ok {
font-size: 20px;
color: #329432;
}
}
} }
} }

7
im-uniapp/components/file-upload/file-upload.vue

@ -5,6 +5,8 @@
</template> </template>
<script> <script>
import UNI_APP from '@/.env.js';
export default { export default {
name: "file-upload", name: "file-upload",
data() { data() {
@ -34,8 +36,9 @@
}, },
methods: { methods: {
selectAndUpload() { selectAndUpload() {
console.log(uni.chooseFile)
console.log(uni.chooseMessageFile)
let chooseFile = uni.chooseFile || uni.chooseMessageFile; let chooseFile = uni.chooseFile || uni.chooseMessageFile;
console.log(chooseFile)
chooseFile({ chooseFile({
success: (res) => { success: (res) => {
res.tempFiles.forEach((file) => { res.tempFiles.forEach((file) => {
@ -56,7 +59,7 @@
}, },
uploadFile(file) { uploadFile(file) {
uni.uploadFile({ uni.uploadFile({
url: process.env.BASE_URL + '/file/upload', url: UNI_APP.BASE_URL + '/file/upload',
header: { header: {
accessToken: uni.getStorageSync("loginInfo").accessToken accessToken: uni.getStorageSync("loginInfo").accessToken
}, },

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

@ -5,6 +5,8 @@
</template> </template>
<script> <script>
import UNI_APP from '@/.env.js'
export default { export default {
name: "image-upload", name: "image-upload",
data() { data() {
@ -65,7 +67,7 @@
}, },
uploadImage(file) { uploadImage(file) {
uni.uploadFile({ uni.uploadFile({
url: process.env.BASE_URL + '/image/upload', url: UNI_APP.BASE_URL + '/image/upload',
header: { header: {
accessToken: uni.getStorageSync("loginInfo").accessToken accessToken: uni.getStorageSync("loginInfo").accessToken
}, },

1
im-uniapp/main.js

@ -5,7 +5,6 @@ import * as enums from './common/enums.js';
import * as date from './common/date'; import * as date from './common/date';
import * as socketApi from './common/wssocket'; import * as socketApi from './common/wssocket';
import store from './store'; import store from './store';
import { createSSRApp } from 'vue' import { createSSRApp } from 'vue'
export function createApp() { export function createApp() {

51
im-uniapp/manifest.json

@ -1,5 +1,5 @@
{ {
"name" : "im-uniapp", "name" : "盒子IM",
"appid" : "__UNI__69DD57A", "appid" : "__UNI__69DD57A",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
@ -17,7 +17,9 @@
"delay" : 0 "delay" : 0
}, },
/* */ /* */
"modules" : {}, "modules" : {
"Camera" : {}
},
/* */ /* */
"distribute" : { "distribute" : {
/* android */ /* android */
@ -38,12 +40,51 @@
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>", "<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
] ],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 21
}, },
/* ios */ /* ios */
"ios" : {}, "ios" : {
"dSYMs" : false
},
/* SDK */ /* SDK */
"sdkConfigs" : {} "sdkConfigs" : {
"ad" : {},
"speech" : {}
},
"icons" : {
"android" : {
"xhdpi" : "unpackage/res/icons/96x96.png",
"hdpi" : "unpackage/res/icons/72x72.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
} }
}, },
/* */ /* */

34
im-uniapp/package.json

@ -1,40 +1,6 @@
{ {
"uni-app": { "uni-app": {
"scripts": { "scripts": {
"dev-h5": {
"title": "开发环境-H5",
"browser":"chrome",
"env": {
"UNI_PLATFORM": "h5",
"BASE_URL": "/api",
"WS_URL": "ws://127.0.0.1:8878/im"
}
},
"dev-wx-mini": {
"title": "开发环境-微信小程序",
"env": {
"UNI_PLATFORM": "mp-weixin",
"BASE_URL": "http://127.0.0.1:8888",
"WS_URL": "ws://127.0.0.1:8878/im"
}
},
"prod-h5": {
"title": "正式环境-H5",
"browser":"chrome",
"env": {
"UNI_PLATFORM": "h5",
"BASE_URL": "https://www.boxim.online/api",
"WS_URL": "wss://www.boxim.online:81/im"
}
},
"prod-wx-mini": {
"title": "正式环境-微信小程序",
"env": {
"UNI_PLATFORM": "mp-weixin",
"BASE_URL": "https://www.boxim.online/api",
"WS_URL": "wss://www.boxim.online:81/im"
}
}
} }
} }
} }

1
im-uniapp/pages.json

@ -1,4 +1,5 @@
{ {
"lazyCodeLoading":"requiredComponents",
"pages": [{ "pages": [{
"path": "pages/login/login" "path": "pages/login/login"
}, { }, {

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

@ -12,7 +12,7 @@
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" <chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage" :showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx"
:msgInfo="msgInfo"> :msgInfo="msgInfo" :groupMembers="groupMembers">
</chat-message-item> </chat-message-item>
</view> </view>
</scroll-view> </scroll-view>
@ -30,6 +30,7 @@
<view class="send-bar"> <view class="send-bar">
<view class="send-text"> <view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false" <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt?'[回执消息]':''"
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange" :adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange"
@input="onTextInput" confirm-type="send" confirm-hold :hold-keyboard="true"></textarea> @input="onTextInput" confirm-type="send" confirm-hold :hold-keyboard="true"></textarea>
</view> </view>
@ -57,6 +58,8 @@
</image-upload> </image-upload>
<view class="tool-name">拍摄</view> <view class="tool-name">拍摄</view>
</view> </view>
<!-- #ifndef APP-PLUS -->
<!-- APP 暂时不支持选择文件 -->
<view class="chat-tools-item"> <view class="chat-tools-item">
<file-upload :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess" <file-upload :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
:onError="onUploadFileFail"> :onError="onUploadFileFail">
@ -64,10 +67,15 @@
</file-upload> </file-upload>
<view class="tool-name">文件</view> <view class="tool-name">文件</view>
</view> </view>
<!-- #endif -->
<view class="chat-tools-item" @click="showTip()"> <view class="chat-tools-item" @click="showTip()">
<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 class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view>
<view class="tool-name">回执消息</view>
</view>
<view class="chat-tools-item" @click="showTip()"> <view class="chat-tools-item" @click="showTip()">
<view class="tool-icon iconfont icon-call"></view> <view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">呼叫</view> <view class="tool-name">呼叫</view>
@ -96,6 +104,7 @@
group: {}, group: {},
groupMembers: [], groupMembers: [],
sendText: "", sendText: "",
isReceipt: false, //
showVoice: false, // showVoice: false, //
scrollMsgIdx: 0, // scrollMsgIdx: 0, //
chatTabBox: 'none', chatTabBox: 'none',
@ -107,11 +116,19 @@
}, },
methods: { methods: {
showTip() { showTip() {
uni.showToast({ uni.showToast({
title: "加班开发中...", title: "暂未支持...",
icon: "none" icon: "none"
}) })
}, },
moveChatToTop(){
let chatIdx = this.$store.getters.findChatIdx(this.chat);
this.$store.commit("moveTop",chatIdx);
},
switchReceipt(){
this.isReceipt = !this.isReceipt;
},
openAtBox() { openAtBox() {
this.$refs.atBox.init(this.atUserIds); this.$refs.atBox.init(this.atUserIds);
this.$refs.atBox.open(); this.$refs.atBox.open();
@ -146,12 +163,13 @@
title: "不能发送空白信息", title: "不能发送空白信息",
icon: "none" icon: "none"
}); });
} }
let atText = this.createAtText() let receiptText = this.isReceipt? "【回执消息】":"";
let atText = this.createAtText();
let msgInfo = { let msgInfo = {
content: this.sendText + atText, content: receiptText + this.sendText + atText,
atUserIds: this.atUserIds, atUserIds: this.atUserIds,
receipt : this.isReceipt,
type: 0 type: 0
} }
// id // id
@ -166,14 +184,18 @@
msgInfo.sendTime = new Date().getTime(); msgInfo.sendTime = new Date().getTime();
msgInfo.sendId = this.$store.state.userStore.userInfo.id; msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true; msgInfo.selfSend = true;
msgInfo.readedCount = 0,
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND; msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
//
this.moveChatToTop();
this.sendText = ""; this.sendText = "";
}).finally(() => { }).finally(() => {
// //
this.scrollToBottom(); this.scrollToBottom();
// @ // @
this.atUserIds = []; this.atUserIds = [];
this.isReceipt = false;
}); });
}, },
createAtText() { createAtText() {
@ -215,7 +237,6 @@
return; return;
} }
this.$nextTick(() => { this.$nextTick(() => {
console.log("scrollToMsgIdx", this.scrollMsgIdx)
this.scrollMsgIdx = idx; this.scrollMsgIdx = idx;
}); });
@ -256,6 +277,7 @@
sendTime: new Date().getTime(), sendTime: new Date().getTime(),
selfSend: true, selfSend: true,
type: this.$enums.MESSAGE_TYPE.IMAGE, type: this.$enums.MESSAGE_TYPE.IMAGE,
readedCount: 0,
loadStatus: "loading", loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND status: this.$enums.MESSAGE_STATUS.UNSEND
} }
@ -263,6 +285,8 @@
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
// //
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
//
this.moveChatToTop();
// file // file
file.msgInfo = msgInfo; file.msgInfo = msgInfo;
// //
@ -272,6 +296,7 @@
onUploadImageSuccess(file, res) { onUploadImageSuccess(file, res) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(res.data); msgInfo.content = JSON.stringify(res.data);
msgInfo.receipt = this.isReceipt
this.$http({ this.$http({
url: this.messageAction, url: this.messageAction,
method: 'POST', method: 'POST',
@ -279,6 +304,7 @@
}).then((id) => { }).then((id) => {
msgInfo.loadStatus = 'ok'; msgInfo.loadStatus = 'ok';
msgInfo.id = id; msgInfo.id = id;
this.isReceipt = false;
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}) })
}, },
@ -300,6 +326,7 @@
sendTime: new Date().getTime(), sendTime: new Date().getTime(),
selfSend: true, selfSend: true,
type: this.$enums.MESSAGE_TYPE.FILE, type: this.$enums.MESSAGE_TYPE.FILE,
readedCount: 0,
loadStatus: "loading", loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND status: this.$enums.MESSAGE_STATUS.UNSEND
} }
@ -307,6 +334,8 @@
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
// //
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
//
this.moveChatToTop();
// file // file
file.msgInfo = msgInfo; file.msgInfo = msgInfo;
// //
@ -321,6 +350,7 @@
} }
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data); msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt
this.$http({ this.$http({
url: this.messageAction, url: this.messageAction,
method: 'POST', method: 'POST',
@ -328,6 +358,7 @@
}).then((id) => { }).then((id) => {
msgInfo.loadStatus = 'ok'; msgInfo.loadStatus = 'ok';
msgInfo.id = id; msgInfo.id = id;
this.isReceipt = false;
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}) })
}, },
@ -399,7 +430,6 @@
// //
this.scrollToMsgIdx(this.showMinIdx); this.scrollToMsgIdx(this.showMinIdx);
// #endif // #endif
// 10 // 10
this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0; this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
}, },
@ -565,6 +595,8 @@
this.loadFriend(this.chat.targetId); this.loadFriend(this.chat.targetId);
this.loadReaded(this.chat.targetId) this.loadReaded(this.chat.targetId)
} }
//
this.isReceipt = false;
}, },
onUnload() { onUnload() {
this.$store.commit("activeChat", -1); this.$store.commit("activeChat", -1);
@ -695,20 +727,23 @@
.chat-tools { .chat-tools {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between;
.chat-tools-item { .chat-tools-item {
width: 140rpx; width: 140rpx;
padding: 15rpx; padding: 16rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.tool-icon { .tool-icon {
padding: 15rpx; padding: 18rpx;
font-size: 80rpx; font-size: 80rpx;
background-color: white; background-color: white;
border-radius: 20%; border-radius: 20%;
&.active{
background-color: #ddd;
}
} }
.tool-name { .tool-name {

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

@ -1,6 +1,6 @@
<template> <template>
<view v-if="$store.state.userStore.userInfo.type == 1" class="page group-info"> <view v-if="$store.state.userStore.userInfo.type == 1" class="page group-info">
<view class="group-members"> <view v-if="!group.quit" class="group-members">
<view class="member-items"> <view class="member-items">
<view v-for="(member,idx) in groupMembers" :key="idx"> <view v-for="(member,idx) in groupMembers" :key="idx">
<view class="member-item" v-if="idx<9"> <view class="member-item" v-if="idx<9">
@ -42,9 +42,9 @@
<uni-section title="群公告:" titleFontSize="14px"> <uni-section title="群公告:" titleFontSize="14px">
<uni-notice-bar :text="group.notice" /> <uni-notice-bar :text="group.notice" />
</uni-section> </uni-section>
<view class="group-edit" @click="onEditGroup()">修改群聊资料 > </view> <view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view>
</view> </view>
<view class="btn-group"> <view v-if="!group.quit" class="btn-group">
<button class="btn" type="primary" @click="onSendMessage()">发消息</button> <button class="btn" type="primary" @click="onSendMessage()">发消息</button>
<button class="btn" v-show="!isOwner" type="warn" @click="onQuitGroup()">退出群聊</button> <button class="btn" v-show="!isOwner" type="warn" @click="onQuitGroup()">退出群聊</button>
<button class="btn" v-show="isOwner" type="warn" @click="onDissolveGroup()">解散群聊</button> <button class="btn" v-show="isOwner" type="warn" @click="onDissolveGroup()">解散群聊</button>
@ -111,7 +111,6 @@
url:"/pages/group/group" url:"/pages/group/group"
}); });
this.$store.commit("removeGroup", this.groupId); this.$store.commit("removeGroup", this.groupId);
this.$store.commit("removeGroupChat", this.groupId);
},100) },100)
} }
}) })
@ -141,7 +140,6 @@
url:"/pages/group/group" url:"/pages/group/group"
}); });
this.$store.commit("removeGroup", this.groupId); this.$store.commit("removeGroup", this.groupId);
this.$store.commit("removeGroupChat", this.groupId);
},100) },100)
} }
}) })

2
im-uniapp/pages/group/group.vue

@ -14,7 +14,7 @@
<view class="group-items" v-else> <view class="group-items" v-else>
<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="group in $store.state.groupStore.groups" :key="group.id"> <view v-for="group in $store.state.groupStore.groups" :key="group.id">
<group-item :group="group"></group-item> <group-item v-if="!group.quit" :group="group"></group-item>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>

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

@ -47,6 +47,8 @@
method: 'POST' method: 'POST'
}).then(data => { }).then(data => {
console.log("登录成功,自动跳转到聊天页面...") console.log("登录成功,自动跳转到聊天页面...")
uni.setStorageSync("userName", this.loginForm.userName);
uni.setStorageSync("password", this.loginForm.password);
uni.setStorageSync("loginInfo", data); uni.setStorageSync("loginInfo", data);
// App.vue // App.vue
getApp().init() getApp().init()

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

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4272106 */ font-family: "iconfont"; /* Project id 4272106 */
src: url('iconfont.ttf?t=1699795609670') format('truetype'); src: url('iconfont.ttf?t=1706027587101') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-receipt:before {
content: "\e61a";
}
.icon-ok:before {
content: "\e65a";
}
.icon-at:before { .icon-at:before {
content: "\e7de"; content: "\e7de";
} }

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

Binary file not shown.

BIN
im-uniapp/static/logo/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

133
im-uniapp/store/chatStore.js

@ -1,11 +1,7 @@
import { import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js';
MESSAGE_TYPE,
MESSAGE_STATUS
} from '@/common/enums.js';
import userStore from './userStore'; import userStore from './userStore';
export default { export default {
state: { state: {
activeIndex: -1, activeIndex: -1,
chats: [], chats: [],
@ -96,14 +92,6 @@ export default {
state.chats.splice(idx, 1); state.chats.splice(idx, 1);
this.commit("saveToStorage"); this.commit("saveToStorage");
}, },
removeGroupChat(state, groupId) {
for (let idx in state.chats) {
if (state.chats[idx].type == 'GROUP' &&
state.chats[idx].targetId == groupId) {
this.commit("removeChat", idx);
}
}
},
removePrivateChat(state, userId) { removePrivateChat(state, userId) {
for (let idx in state.chats) { for (let idx in state.chats) {
if (state.chats[idx].type == 'PRIVATE' && if (state.chats[idx].type == 'PRIVATE' &&
@ -127,32 +115,38 @@ export default {
insertMessage(state, msgInfo) { insertMessage(state, msgInfo) {
// 获取对方id或群id // 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE'; let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId; // 记录消息的最大id
let chat = null; if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
for (let idx in state.chats) { state.privateMsgMaxId = msgInfo.id;
if (state.chats[idx].type == type && }
state.chats[idx].targetId === targetId) { if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
chat = state.chats[idx]; state.groupMsgMaxId = msgInfo.id;
this.commit("moveTop", idx) }
break; // 如果是已存在消息,则覆盖旧的消息数据
} let chat = this.getters.findChat(msgInfo);
let message = this.getters.findMessage(chat, msgInfo);
if(message){
Object.assign(message, msgInfo);
this.commit("saveToStorage");
return;
} }
// 会话列表内容 // 会话列表内容
if(!state.loadingPrivateMsg && !state.loadingPrivateMsg){ if(!state.loadingPrivateMsg && !state.loadingGroupMsg){
if (msgInfo.type == MESSAGE_TYPE.IMAGE) { if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
chat.lastContent = "[图片]"; chat.lastContent = "[图片]";
} else if (msgInfo.type == MESSAGE_TYPE.FILE) { } else if (msgInfo.type == MESSAGE_TYPE.FILE) {
chat.lastContent = "[文件]"; chat.lastContent = "[文件]";
} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) { } else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
} else { } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
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) {
chat.unreadCount++; chat.unreadCount++;
} }
// 是否有人@我 // 是否有人@我
@ -166,28 +160,6 @@ export default {
chat.atAll = true; chat.atAll = true;
} }
} }
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) {
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
}
// 间隔大于10分钟插入时间显示 // 间隔大于10分钟插入时间显示
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) { if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
chat.messages.push({ chat.messages.push({
@ -199,21 +171,20 @@ export default {
// 新的消息 // 新的消息
chat.messages.push(msgInfo); chat.messages.push(msgInfo);
this.commit("saveToStorage"); this.commit("saveToStorage");
}, },
deleteMessage(state, msgInfo) { updateMessage(state, msgInfo) {
// 获取对方id或群id // 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE'; let chat = this.getters.findChat(msgInfo);
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId; let message = this.getters.findMessage(chat, msgInfo);
let chat = null; if(message){
for (let idx in state.chats) { // 属性拷贝
if (state.chats[idx].type == type && Object.assign(message, msgInfo);
state.chats[idx].targetId === targetId) { this.commit("saveToStorage");
chat = state.chats[idx];
break;
}
} }
},
deleteMessage(state, msgInfo) {
// 获取对方id或群id
let chat = this.getters.findChat(msgInfo);
for (let idx in chat.messages) { for (let idx in chat.messages) {
// 已经发送成功的,根据id删除 // 已经发送成功的,根据id删除
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) { if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
@ -273,7 +244,7 @@ export default {
chat.lastContent = "[文件]"; chat.lastContent = "[文件]";
} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) { } else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
} else { } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
@ -324,6 +295,46 @@ export default {
}); });
}) })
} }
},
getters: {
findChatIdx: (state) => (chat) => {
for (let idx in state.chats) {
if (state.chats[idx].type == chat.type &&
state.chats[idx].targetId === chat.targetId) {
chat = state.chats[idx];
return idx;
}
}
},
findChat: (state) => (msgInfo) => {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
break;
}
}
return chat;
},
findMessage: (state) => (chat, msgInfo) => {
if (!chat) {
return null;
}
for (let idx in chat.messages) {
// 通过id判断
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
return chat.messages[idx];
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
return chat.messages[idx];
}
}
}
} }
} }
Loading…
Cancel
Save