Browse Source

!11 域名备案通过,改用域名访问

Merge pull request !11 from blue/v_1.1.0
master
blue 3 years ago
committed by Gitee
parent
commit
f9000fb503
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 8
      im-client/src/main/java/com/bx/imclient/sender/IMSender.java
  2. 8
      im-commom/src/main/java/com/bx/imcommon/enums/IMSendCode.java
  3. 9
      im-commom/src/main/java/com/bx/imcommon/model/SendResult.java
  4. 19
      im-platform/src/main/java/com/bx/implatform/config/ICEServer.java
  5. 17
      im-platform/src/main/java/com/bx/implatform/config/ICEServerConfig.java
  6. 126
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java
  7. 10
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  8. 4
      im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java
  9. 29
      im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java
  10. 6
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  11. 5
      im-platform/src/main/resources/application.yml
  12. 16
      im-server/src/main/java/com/bx/imserver/netty/IMServerGroup.java
  13. 36
      im-server/src/main/java/com/bx/imserver/netty/processor/GroupMessageProcessor.java
  14. 4
      im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java
  15. 10
      im-server/src/main/java/com/bx/imserver/netty/processor/PrivateMessageProcessor.java
  16. 19
      im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java
  17. 19
      im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java
  18. 6
      im-server/src/main/java/com/bx/imserver/task/AbstractPullMessageTask.java
  19. 4
      im-server/src/main/java/com/bx/imserver/task/PullUnreadGroupMessageTask.java
  20. 4
      im-server/src/main/java/com/bx/imserver/task/PullUnreadPrivateMessageTask.java
  21. 4
      im-ui/.env.production
  22. 21
      im-ui/src/api/enums.js
  23. 8
      im-ui/src/api/wssocket.js
  24. BIN
      im-ui/src/assets/audio/call.wav
  25. BIN
      im-ui/src/assets/audio/tip.wav
  26. 16
      im-ui/src/assets/iconfont/iconfont.css
  27. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  28. BIN
      im-ui/src/assets/iconfont/iconfont.woff
  29. BIN
      im-ui/src/assets/iconfont/iconfont.woff2
  30. 62
      im-ui/src/components/chat/ChatBox.vue
  31. 8
      im-ui/src/components/chat/ChatHistory.vue
  32. 77
      im-ui/src/components/chat/ChatMessageItem.vue
  33. 383
      im-ui/src/components/chat/ChatPrivateVideo.vue
  34. 131
      im-ui/src/components/chat/ChatVideoAcceptor.vue
  35. 4
      im-ui/src/components/chat/ChatVoice.vue
  36. 5
      im-ui/src/main.js
  37. 30
      im-ui/src/store/uiStore.js
  38. 12
      im-ui/src/store/userStore.js
  39. 72
      im-ui/src/utils/directive/dialogDrag.js
  40. 63
      im-ui/src/view/Home.vue

8
im-client/src/main/java/com/bx/imclient/sender/IMSender.java

@ -4,7 +4,7 @@ import com.bx.imclient.listener.MessageListenerMulticaster;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.RedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.enums.IMSendStatus; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.imcommon.model.GroupMessageInfo;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.imcommon.model.PrivateMessageInfo;
@ -54,8 +54,7 @@ public class IMSender {
SendResult result = new SendResult(); SendResult result = new SendResult();
result.setMessageInfo(messageInfo); result.setMessageInfo(messageInfo);
result.setRecvId(recvId); result.setRecvId(recvId);
result.setStatus(IMSendStatus.FAIL); result.setCode(IMSendCode.NOT_ONLINE);
result.setFailReason("用户不在线");
listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, result); listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, result);
} }
} }
@ -103,8 +102,7 @@ public class IMSender {
SendResult result = new SendResult(); SendResult result = new SendResult();
result.setMessageInfo(messageInfo); result.setMessageInfo(messageInfo);
result.setRecvId(id); result.setRecvId(id);
result.setStatus(IMSendStatus.FAIL); result.setCode(IMSendCode.NOT_ONLINE);
result.setFailReason("用户不在线");
listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE,result); listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE,result);
} }
} }

8
im-commom/src/main/java/com/bx/imcommon/enums/IMSendStatus.java → im-commom/src/main/java/com/bx/imcommon/enums/IMSendCode.java

@ -1,16 +1,18 @@
package com.bx.imcommon.enums; package com.bx.imcommon.enums;
public enum IMSendStatus { public enum IMSendCode {
SUCCESS(0,"发送成功"), SUCCESS(0,"发送成功"),
FAIL(1,"发送失败"); NOT_ONLINE(1,"对方当前不在线"),
NOT_FIND_CHANNEL(2,"未找到对方的channel"),
UNKONW_ERROR(9999,"未知异常");
private int code; private int code;
private String desc; private String desc;
// 构造方法 // 构造方法
IMSendStatus(int code, String desc) { IMSendCode(int code, String desc) {
this.code = code; this.code = code;
this.desc = desc; this.desc = desc;
} }

9
im-commom/src/main/java/com/bx/imcommon/model/SendResult.java

@ -1,6 +1,6 @@
package com.bx.imcommon.model; package com.bx.imcommon.model;
import com.bx.imcommon.enums.IMSendStatus; import com.bx.imcommon.enums.IMSendCode;
import lombok.Data; import lombok.Data;
@Data @Data
@ -14,12 +14,7 @@ public class SendResult<T> {
/* /*
* 发送状态 * 发送状态
*/ */
private IMSendStatus status; private IMSendCode code;
/*
* 失败原因
*/
private String failReason="";
/* /*
* 消息体(透传) * 消息体(透传)

19
im-platform/src/main/java/com/bx/implatform/config/ICEServer.java

@ -0,0 +1,19 @@
package com.bx.implatform.config;
import lombok.Data;
@Data
public class ICEServer {
private String urls;
private String username;
private String credential;
}

17
im-platform/src/main/java/com/bx/implatform/config/ICEServerConfig.java

@ -0,0 +1,17 @@
package com.bx.implatform.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix="webrtc")
public class ICEServerConfig {
private List<ICEServer> iceServers = new ArrayList<>();
}

126
im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java

@ -0,0 +1,126 @@
package com.bx.implatform.controller;
import com.bx.imclient.IMClient;
import com.bx.imcommon.model.PrivateMessageInfo;
import com.bx.implatform.config.ICEServerConfig;
import com.bx.implatform.enums.MessageType;
import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.session.SessionContext;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Api(tags = "webrtc视频单人通话")
@RestController
@RequestMapping("/webrtc/private")
public class WebrtcController {
@Autowired
private IMClient imClient;
@Autowired
private ICEServerConfig iceServerConfig;
@ApiOperation(httpMethod = "POST", value = "呼叫视频通话")
@PostMapping("/call")
public Result call(@RequestParam Long uid, @RequestBody String offer) {
Long userId = SessionContext.getSession().getId();
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_CALL.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(offer);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success();
}
@ApiOperation(httpMethod = "POST", value = "接受视频通话")
@PostMapping("/accept")
public Result accept(@RequestParam Long uid,@RequestBody String answer) {
Long userId = SessionContext.getSession().getId();
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_ACCEPT.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(answer);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success();
}
@ApiOperation(httpMethod = "POST", value = "拒绝视频通话")
@PostMapping("/reject")
public Result reject(@RequestParam Long uid) {
Long userId = SessionContext.getSession().getId();
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_REJECT.code());
message.setRecvId(uid);
message.setSendId(userId);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success();
}
@ApiOperation(httpMethod = "POST", value = "取消呼叫")
@PostMapping("/cancel")
public Result cancel(@RequestParam Long uid) {
Long userId = SessionContext.getSession().getId();
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_CANCEL.code());
message.setRecvId(uid);
message.setSendId(userId);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success();
}
@ApiOperation(httpMethod = "POST", value = "呼叫失败")
@PostMapping("/failed")
public Result failed(@RequestParam Long uid,@RequestParam String reason) {
Long userId = SessionContext.getSession().getId();
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_FAILED.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(reason);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success();
}
@ApiOperation(httpMethod = "POST", value = "挂断")
@PostMapping("/handup")
public Result leave(@RequestParam Long uid) {
Long userId = SessionContext.getSession().getId();
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_HANDUP.code());
message.setRecvId(uid);
message.setSendId(userId);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success();
}
@PostMapping("/candidate")
@ApiOperation(httpMethod = "POST", value = "同步candidate")
public Result candidate(@RequestParam Long uid,@RequestBody String candidate ) {
Long userId = SessionContext.getSession().getId();
PrivateMessageInfo message = new PrivateMessageInfo();
message.setType(MessageType.RTC_CANDIDATE.code());
message.setRecvId(uid);
message.setSendId(userId);
message.setContent(candidate);
imClient.sendPrivateMessage(uid,message);
return ResultUtils.success();
}
@GetMapping("/iceservers")
@ApiOperation(httpMethod = "GET", value = "获取iceservers")
public Result iceservers() {
return ResultUtils.success(iceServerConfig.getIceServers());
}
}

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

@ -7,7 +7,15 @@ public enum MessageType {
FILE(1,"文件"), FILE(1,"文件"),
IMAGE(2,"图片"), IMAGE(2,"图片"),
VIDEO(3,"视频"), VIDEO(3,"视频"),
TIP(10,"系统提示"); TIP(10,"系统提示"),
RTC_CALL(101,"呼叫"),
RTC_ACCEPT(102,"接受"),
RTC_REJECT(103, "拒绝"),
RTC_CANCEL(104,"取消呼叫"),
RTC_FAILED(105,"呼叫失败"),
RTC_HANDUP(106,"挂断"),
RTC_CANDIDATE(107,"同步candidate");
private Integer code; private Integer code;

4
im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java

@ -3,7 +3,7 @@ package com.bx.implatform.listener;
import com.bx.imclient.annotation.IMListener; import com.bx.imclient.annotation.IMListener;
import com.bx.imclient.listener.MessageListener; import com.bx.imclient.listener.MessageListener;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.enums.IMSendStatus; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.imcommon.model.GroupMessageInfo;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.SendResult;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
@ -29,7 +29,7 @@ public class GroupMessageListener implements MessageListener {
} }
// 保存该用户已拉取的最大消息id // 保存该用户已拉取的最大消息id
if(result.getStatus().equals(IMSendStatus.SUCCESS)) { if(result.getCode().equals(IMSendCode.SUCCESS)) {
String key = RedisKey.IM_GROUP_READED_POSITION + messageInfo.getGroupId() + ":" + result.getRecvId(); String key = RedisKey.IM_GROUP_READED_POSITION + messageInfo.getGroupId() + ":" + result.getRecvId();
redisTemplate.opsForValue().set(key, messageInfo.getId()); redisTemplate.opsForValue().set(key, messageInfo.getId());
} }

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

@ -1,10 +1,11 @@
package com.bx.implatform.listener; package com.bx.implatform.listener;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.bx.imclient.IMClient;
import com.bx.imclient.annotation.IMListener; import com.bx.imclient.annotation.IMListener;
import com.bx.imclient.listener.MessageListener; import com.bx.imclient.listener.MessageListener;
import com.bx.imcommon.enums.IMListenerType; import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.enums.IMSendStatus; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.imcommon.model.PrivateMessageInfo;
import com.bx.imcommon.model.SendResult; import com.bx.imcommon.model.SendResult;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
@ -14,6 +15,8 @@ import com.bx.implatform.service.IPrivateMessageService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
@Slf4j @Slf4j
@IMListener(type = IMListenerType.PRIVATE_MESSAGE) @IMListener(type = IMListenerType.PRIVATE_MESSAGE)
@ -22,15 +25,33 @@ public class PrivateMessageListener implements MessageListener {
@Autowired @Autowired
private IPrivateMessageService privateMessageService; private IPrivateMessageService privateMessageService;
@Autowired
private IMClient imClient;
@Override @Override
public void process(SendResult result){ public void process(SendResult result){
PrivateMessageInfo messageInfo = (PrivateMessageInfo) result.getMessageInfo(); PrivateMessageInfo messageInfo = (PrivateMessageInfo) result.getMessageInfo();
if(messageInfo.getType().equals(MessageType.TIP)){ // 提示类数据不记录
// 提示类数据不记录 if(messageInfo.getType().equals(MessageType.TIP.code())){
return; return;
} }
// 视频通话信令不记录
if(messageInfo.getType() >= MessageType.RTC_CALL.code() && messageInfo.getType()< MessageType.RTC_CANDIDATE.code()){
// 通知用户呼叫失败了
if(messageInfo.getType() == MessageType.RTC_CALL.code()
&& !result.getCode().equals(IMSendCode.SUCCESS)){
PrivateMessageInfo sendMessage = new PrivateMessageInfo();
sendMessage.setRecvId(messageInfo.getSendId());
sendMessage.setSendId(messageInfo.getRecvId());
sendMessage.setType(MessageType.RTC_FAILED.code());
sendMessage.setContent(result.getCode().description());
sendMessage.setSendTime(new Date());
imClient.sendPrivateMessage(sendMessage.getRecvId(),sendMessage);
}
}
// 更新消息状态 // 更新消息状态
if(result.getStatus().equals(IMSendStatus.SUCCESS)){ if(result.getCode().equals(IMSendCode.SUCCESS)){
UpdateWrapper<PrivateMessage> updateWrapper = new UpdateWrapper<>(); UpdateWrapper<PrivateMessage> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda().eq(PrivateMessage::getId,messageInfo.getId()) updateWrapper.lambda().eq(PrivateMessage::getId,messageInfo.getId())
.eq(PrivateMessage::getStatus, MessageStatus.UNREAD.code()) .eq(PrivateMessage::getStatus, MessageStatus.UNREAD.code())

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

@ -74,6 +74,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
msg.setSendId(userId); msg.setSendId(userId);
msg.setSendTime(new Date()); msg.setSendTime(new Date());
this.save(msg); this.save(msg);
// 不用发给自己
userIds = userIds.stream().filter(id->userId!=id).collect(Collectors.toList());
// 群发 // 群发
GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class);
imClient.sendGroupMessage(userIds,msgInfo); imClient.sendGroupMessage(userIds,msgInfo);
@ -112,6 +114,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
this.updateById(msg); this.updateById(msg);
// 群发 // 群发
List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId());
// 不用发给自己
userIds = userIds.stream().filter(uid->userId.equals(uid)).collect(Collectors.toList());
GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class);
msgInfo.setType(MessageType.TIP.code()); msgInfo.setType(MessageType.TIP.code());
String content = String.format("'%s'撤回了一条消息",member.getAliasName()); String content = String.format("'%s'撤回了一条消息",member.getAliasName());
@ -140,6 +144,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
QueryWrapper<GroupMessage> wrapper = new QueryWrapper(); QueryWrapper<GroupMessage> wrapper = new QueryWrapper();
wrapper.lambda().eq(GroupMessage::getGroupId,member.getGroupId()) wrapper.lambda().eq(GroupMessage::getGroupId,member.getGroupId())
.gt(GroupMessage::getSendTime,member.getCreatedTime()) .gt(GroupMessage::getSendTime,member.getCreatedTime())
.ne(GroupMessage::getSendId, userId)
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()); .ne(GroupMessage::getStatus, MessageStatus.RECALL.code());
if(maxReadedId!=null){ if(maxReadedId!=null){
wrapper.lambda().gt(GroupMessage::getId,maxReadedId); wrapper.lambda().gt(GroupMessage::getId,maxReadedId);
@ -198,4 +203,5 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return messageInfos; return messageInfos;
} }
} }

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

@ -36,3 +36,8 @@ minio:
bucketName: box-im bucketName: box-im
imagePath: image imagePath: image
filePath: file filePath: file
webrtc:
iceServers:
- urls: stun:stun.l.google.com:19302

16
im-server/src/main/java/com/bx/imserver/netty/IMServerMap.java → im-server/src/main/java/com/bx/imserver/netty/IMServerGroup.java

@ -12,7 +12,7 @@ import java.util.List;
@Slf4j @Slf4j
@Component @Component
public class IMServerMap implements CommandLineRunner { public class IMServerGroup implements CommandLineRunner {
public static volatile long serverId = 0; public static volatile long serverId = 0;
@ -22,6 +22,20 @@ public class IMServerMap implements CommandLineRunner {
@Autowired @Autowired
private List<IMServer> imServers; private List<IMServer> imServers;
/***
* 判断服务器是否就绪
*
* @return
**/
public boolean isReady(){
for(IMServer imServer:imServers){
if(!imServer.isReady()){
return false;
}
}
return true;
}
@Override @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
// 初始化SERVER_ID // 初始化SERVER_ID

36
im-server/src/main/java/com/bx/imserver/netty/processor/GroupMessageProcessor.java

@ -2,7 +2,7 @@ package com.bx.imserver.netty.processor;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.RedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.enums.IMSendStatus; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.imcommon.model.GroupMessageInfo;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
@ -34,28 +34,25 @@ public class GroupMessageProcessor extends MessageProcessor<IMRecvInfo<GroupMes
try { try {
ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(recvId); ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(recvId);
if(channelCtx != null){ if(channelCtx != null){
// 自己发的消息不用推送 // 推送消息到用户
if(recvId != messageInfo.getSendId()){ IMSendInfo sendInfo = new IMSendInfo();
// 推送消息到用户 sendInfo.setCmd(IMCmdType.GROUP_MESSAGE.code());
IMSendInfo sendInfo = new IMSendInfo(); sendInfo.setData(messageInfo);
sendInfo.setCmd(IMCmdType.GROUP_MESSAGE.code()); channelCtx.channel().writeAndFlush(sendInfo);
sendInfo.setData(messageInfo); // 消息发送成功确认
channelCtx.channel().writeAndFlush(sendInfo); String key = RedisKey.IM_RESULT_GROUP_QUEUE;
// 消息发送成功确认 SendResult sendResult = new SendResult();
String key = RedisKey.IM_RESULT_GROUP_QUEUE; sendResult.setRecvId(recvId);
SendResult sendResult = new SendResult(); sendResult.setCode(IMSendCode.SUCCESS);
sendResult.setRecvId(recvId); sendResult.setMessageInfo(messageInfo);
sendResult.setStatus(IMSendStatus.SUCCESS); redisTemplate.opsForList().rightPush(key,sendResult);
sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult);
}
}else { }else {
// 消息发送失败确认 // 消息发送失败确认
String key = RedisKey.IM_RESULT_GROUP_QUEUE; String key = RedisKey.IM_RESULT_GROUP_QUEUE;
SendResult sendResult = new SendResult(); SendResult sendResult = new SendResult();
sendResult.setRecvId(recvId); sendResult.setRecvId(recvId);
sendResult.setStatus(IMSendStatus.FAIL); sendResult.setCode(IMSendCode.NOT_FIND_CHANNEL);
sendResult.setFailReason("未找到WS连接");
sendResult.setMessageInfo(messageInfo); sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult); redisTemplate.opsForList().rightPush(key,sendResult);
log.error("未找到WS连接,发送者:{},群id:{},接收id:{},内容:{}",messageInfo.getSendId(),messageInfo.getGroupId(),recvIds,messageInfo.getContent()); log.error("未找到WS连接,发送者:{},群id:{},接收id:{},内容:{}",messageInfo.getSendId(),messageInfo.getGroupId(),recvIds,messageInfo.getContent());
@ -65,8 +62,7 @@ public class GroupMessageProcessor extends MessageProcessor<IMRecvInfo<GroupMes
String key = RedisKey.IM_RESULT_GROUP_QUEUE; String key = RedisKey.IM_RESULT_GROUP_QUEUE;
SendResult sendResult = new SendResult(); SendResult sendResult = new SendResult();
sendResult.setRecvId(recvId); sendResult.setRecvId(recvId);
sendResult.setStatus(IMSendStatus.FAIL); sendResult.setCode(IMSendCode.UNKONW_ERROR);
sendResult.setFailReason("未知异常");
sendResult.setMessageInfo(messageInfo); sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult); redisTemplate.opsForList().rightPush(key,sendResult);
log.error("发送消息异常,发送者:{},群id:{},接收id:{},内容:{}",messageInfo.getSendId(),messageInfo.getGroupId(),recvIds,messageInfo.getContent()); log.error("发送消息异常,发送者:{},群id:{},接收id:{},内容:{}",messageInfo.getSendId(),messageInfo.getGroupId(),recvIds,messageInfo.getContent());

4
im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java

@ -6,7 +6,7 @@ import com.bx.imcommon.contant.RedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
import com.bx.imcommon.model.LoginInfo; import com.bx.imcommon.model.LoginInfo;
import com.bx.imserver.netty.IMServerMap; import com.bx.imserver.netty.IMServerGroup;
import com.bx.imserver.netty.UserChannelCtxMap; import com.bx.imserver.netty.UserChannelCtxMap;
import com.bx.imserver.netty.ws.WebSocketServer; import com.bx.imserver.netty.ws.WebSocketServer;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
@ -50,7 +50,7 @@ public class LoginProcessor extends MessageProcessor<LoginInfo> {
ctx.channel().attr(attr).set(0L); ctx.channel().attr(attr).set(0L);
// 在redis上记录每个user的channelId,15秒没有心跳,则自动过期 // 在redis上记录每个user的channelId,15秒没有心跳,则自动过期
String key = RedisKey.IM_USER_SERVER_ID+loginInfo.getUserId(); String key = RedisKey.IM_USER_SERVER_ID+loginInfo.getUserId();
redisTemplate.opsForValue().set(key, IMServerMap.serverId, Constant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); redisTemplate.opsForValue().set(key, IMServerGroup.serverId, Constant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS);
// 响应ws // 响应ws
IMSendInfo sendInfo = new IMSendInfo(); IMSendInfo sendInfo = new IMSendInfo();
sendInfo.setCmd(IMCmdType.LOGIN.code()); sendInfo.setCmd(IMCmdType.LOGIN.code());

10
im-server/src/main/java/com/bx/imserver/netty/processor/PrivateMessageProcessor.java

@ -2,7 +2,7 @@ package com.bx.imserver.netty.processor;
import com.bx.imcommon.contant.RedisKey; import com.bx.imcommon.contant.RedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.enums.IMSendStatus; import com.bx.imcommon.enums.IMSendCode;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imcommon.model.IMSendInfo; import com.bx.imcommon.model.IMSendInfo;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.imcommon.model.PrivateMessageInfo;
@ -38,7 +38,7 @@ public class PrivateMessageProcessor extends MessageProcessor<IMRecvInfo<Privat
String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; String key = RedisKey.IM_RESULT_PRIVATE_QUEUE;
SendResult sendResult = new SendResult(); SendResult sendResult = new SendResult();
sendResult.setRecvId(recvId); sendResult.setRecvId(recvId);
sendResult.setStatus(IMSendStatus.SUCCESS); sendResult.setCode(IMSendCode.SUCCESS);
sendResult.setMessageInfo(messageInfo); sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult); redisTemplate.opsForList().rightPush(key,sendResult);
}else{ }else{
@ -46,8 +46,7 @@ public class PrivateMessageProcessor extends MessageProcessor<IMRecvInfo<Privat
String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; String key = RedisKey.IM_RESULT_PRIVATE_QUEUE;
SendResult sendResult = new SendResult(); SendResult sendResult = new SendResult();
sendResult.setRecvId(recvId); sendResult.setRecvId(recvId);
sendResult.setStatus(IMSendStatus.FAIL); sendResult.setCode(IMSendCode.NOT_FIND_CHANNEL);
sendResult.setFailReason("未找到WS连接");
sendResult.setMessageInfo(messageInfo); sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult); redisTemplate.opsForList().rightPush(key,sendResult);
log.error("未找到WS连接,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent()); log.error("未找到WS连接,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent());
@ -57,8 +56,7 @@ public class PrivateMessageProcessor extends MessageProcessor<IMRecvInfo<Privat
String key = RedisKey.IM_RESULT_PRIVATE_QUEUE; String key = RedisKey.IM_RESULT_PRIVATE_QUEUE;
SendResult sendResult = new SendResult(); SendResult sendResult = new SendResult();
sendResult.setRecvId(recvId); sendResult.setRecvId(recvId);
sendResult.setStatus(IMSendStatus.FAIL); sendResult.setCode(IMSendCode.UNKONW_ERROR);
sendResult.setFailReason("未知异常");
sendResult.setMessageInfo(messageInfo); sendResult.setMessageInfo(messageInfo);
redisTemplate.opsForList().rightPush(key,sendResult); redisTemplate.opsForList().rightPush(key,sendResult);
log.error("发送异常,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent(),e); log.error("发送异常,发送者:{},接收者:{},内容:{}",messageInfo.getSendId(),recvId,messageInfo.getContent(),e);

19
im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java

@ -34,9 +34,9 @@ public class TcpSocketServer implements IMServer {
@Value("${tcpsocket.port}") @Value("${tcpsocket.port}")
private int port; private int port;
private ServerBootstrap bootstrap = new ServerBootstrap(); private ServerBootstrap bootstrap;
private EventLoopGroup bossGroup = new NioEventLoopGroup(); private EventLoopGroup bossGroup;
private EventLoopGroup workGroup = new NioEventLoopGroup(); private EventLoopGroup workGroup;
@Override @Override
public boolean isReady() { public boolean isReady() {
@ -45,6 +45,9 @@ public class TcpSocketServer implements IMServer {
@Override @Override
public void start() { public void start() {
bootstrap = new ServerBootstrap();
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
// 设置为主从线程模型 // 设置为主从线程模型
bootstrap.group(bossGroup, workGroup) bootstrap.group(bossGroup, workGroup)
// 设置服务端NIO通信类型 // 设置服务端NIO通信类型
@ -84,10 +87,14 @@ public class TcpSocketServer implements IMServer {
@Override @Override
public void stop(){ public void stop(){
log.info("tcp server 停止"); if(bossGroup != null && !bossGroup.isShuttingDown() && !bossGroup.isShutdown() ) {
bossGroup.shutdownGracefully(); bossGroup.shutdownGracefully();
workGroup.shutdownGracefully(); }
if(workGroup != null && !workGroup.isShuttingDown() && !workGroup.isShutdown() ) {
workGroup.shutdownGracefully();
}
this.ready = false; this.ready = false;
log.info("tcp server 停止");
} }

19
im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java

@ -37,9 +37,9 @@ public class WebSocketServer implements IMServer {
private volatile boolean ready = false; private volatile boolean ready = false;
private ServerBootstrap bootstrap = new ServerBootstrap(); private ServerBootstrap bootstrap;
private EventLoopGroup bossGroup = new NioEventLoopGroup(); private EventLoopGroup bossGroup;
private EventLoopGroup workGroup = new NioEventLoopGroup(); private EventLoopGroup workGroup;
@Override @Override
@ -49,6 +49,9 @@ public class WebSocketServer implements IMServer {
@Override @Override
public void start() { public void start() {
bootstrap = new ServerBootstrap();
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
// 设置为主从线程模型 // 设置为主从线程模型
bootstrap.group(bossGroup, workGroup) bootstrap.group(bossGroup, workGroup)
// 设置服务端NIO通信类型 // 设置服务端NIO通信类型
@ -92,10 +95,14 @@ public class WebSocketServer implements IMServer {
@Override @Override
public void stop() { public void stop() {
log.info("websocket server 停止"); if(bossGroup != null && !bossGroup.isShuttingDown() && !bossGroup.isShutdown() ) {
bossGroup.shutdownGracefully(); bossGroup.shutdownGracefully();
workGroup.shutdownGracefully(); }
if(workGroup != null && !workGroup.isShuttingDown() && !workGroup.isShutdown() ) {
workGroup.shutdownGracefully();
}
this.ready = false; this.ready = false;
log.info("websocket server 停止");
} }

6
im-server/src/main/java/com/bx/imserver/task/AbstractPullMessageTask.java

@ -1,6 +1,6 @@
package com.bx.imserver.task; package com.bx.imserver.task;
import com.bx.imserver.netty.ws.WebSocketServer; import com.bx.imserver.netty.IMServerGroup;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -17,7 +17,7 @@ public abstract class AbstractPullMessageTask{
private ExecutorService executorService; private ExecutorService executorService;
@Autowired @Autowired
private WebSocketServer WSServer; private IMServerGroup serverGroup;
public AbstractPullMessageTask(){ public AbstractPullMessageTask(){
this.threadNum = 1; this.threadNum = 1;
@ -38,7 +38,7 @@ public abstract class AbstractPullMessageTask{
@Override @Override
public void run() { public void run() {
try{ try{
if(WSServer.isReady()){ if(serverGroup.isReady()){
pullMessage(); pullMessage();
} }
Thread.sleep(100); Thread.sleep(100);

4
im-server/src/main/java/com/bx/imserver/task/PullUnreadGroupMessageTask.java

@ -4,7 +4,7 @@ import com.bx.imcommon.contant.RedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.GroupMessageInfo; import com.bx.imcommon.model.GroupMessageInfo;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imserver.netty.IMServerMap; import com.bx.imserver.netty.IMServerGroup;
import com.bx.imserver.netty.processor.MessageProcessor; import com.bx.imserver.netty.processor.MessageProcessor;
import com.bx.imserver.netty.processor.ProcessorFactory; import com.bx.imserver.netty.processor.ProcessorFactory;
import com.bx.imserver.netty.ws.WebSocketServer; import com.bx.imserver.netty.ws.WebSocketServer;
@ -30,7 +30,7 @@ public class PullUnreadGroupMessageTask extends AbstractPullMessageTask {
@Override @Override
public void pullMessage() { public void pullMessage() {
// 从redis拉取未读消息 // 从redis拉取未读消息
String key = RedisKey.IM_UNREAD_GROUP_QUEUE + IMServerMap.serverId; String key = RedisKey.IM_UNREAD_GROUP_QUEUE + IMServerGroup.serverId;
List messageInfos = redisTemplate.opsForList().range(key,0,-1); List messageInfos = redisTemplate.opsForList().range(key,0,-1);
for(Object o: messageInfos){ for(Object o: messageInfos){
redisTemplate.opsForList().leftPop(key); redisTemplate.opsForList().leftPop(key);

4
im-server/src/main/java/com/bx/imserver/task/PullUnreadPrivateMessageTask.java

@ -5,7 +5,7 @@ import com.bx.imcommon.contant.RedisKey;
import com.bx.imcommon.enums.IMCmdType; import com.bx.imcommon.enums.IMCmdType;
import com.bx.imcommon.model.IMRecvInfo; import com.bx.imcommon.model.IMRecvInfo;
import com.bx.imcommon.model.PrivateMessageInfo; import com.bx.imcommon.model.PrivateMessageInfo;
import com.bx.imserver.netty.IMServerMap; import com.bx.imserver.netty.IMServerGroup;
import com.bx.imserver.netty.processor.MessageProcessor; import com.bx.imserver.netty.processor.MessageProcessor;
import com.bx.imserver.netty.processor.ProcessorFactory; import com.bx.imserver.netty.processor.ProcessorFactory;
import com.bx.imserver.netty.ws.WebSocketServer; import com.bx.imserver.netty.ws.WebSocketServer;
@ -30,7 +30,7 @@ public class PullUnreadPrivateMessageTask extends AbstractPullMessageTask {
@Override @Override
public void pullMessage() { public void pullMessage() {
// 从redis拉取未读消息 // 从redis拉取未读消息
String key = RedisKey.IM_UNREAD_PRIVATE_QUEUE + IMServerMap.serverId; String key = RedisKey.IM_UNREAD_PRIVATE_QUEUE + IMServerGroup.serverId;
List messageInfos = redisTemplate.opsForList().range(key,0,-1); List messageInfos = redisTemplate.opsForList().range(key,0,-1);
for(Object o: messageInfos){ for(Object o: messageInfos){
redisTemplate.opsForList().leftPop(key); redisTemplate.opsForList().leftPop(key);

4
im-ui/.env.production

@ -3,6 +3,6 @@ ENV = 'production'
# app名称 # app名称
VUE_APP_NAME = "盒子IM" VUE_APP_NAME = "盒子IM"
# 接口地址 # 接口地址
VUE_APP_BASE_API = 'https://8.134.92.70:443/api' VUE_APP_BASE_API = 'https://www.boxim.online/api'
# ws地址 # ws地址
VUE_APP_WS_URL = 'wss://8.134.92.70:81/im' VUE_APP_WS_URL = 'wss://www.boxim.online:81/im'

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

@ -0,0 +1,21 @@
const MESSAGE_TYPE = {
RTC_CALL: 101,
RTC_ACCEPT: 102,
RTC_REJECT: 103,
RTC_CANCEL: 104,
RTC_FAILED: 105,
RTC_HANDUP: 106,
RTC_CANDIDATE: 107
}
const USER_STATE = {
OFFLINE: 0,
FREE: 1,
BUSY: 2
}
export {
MESSAGE_TYPE,
USER_STATE
}

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

@ -19,20 +19,20 @@ let initWebSocket = () => {
hasLogin = false; hasLogin = false;
websock = new WebSocket(wsurl); websock = new WebSocket(wsurl);
websock.onmessage = function(e) { websock.onmessage = function(e) {
let msg = JSON.parse(e.data) let sendInfo = JSON.parse(e.data)
if (msg.cmd == 0) { if (sendInfo.cmd == 0) {
hasLogin = true; hasLogin = true;
heartCheck.start() heartCheck.start()
console.log('WebSocket登录成功') console.log('WebSocket登录成功')
// 登录成功才算连接完成 // 登录成功才算连接完成
openCallBack && openCallBack(); openCallBack && openCallBack();
} }
else if(msg.cmd==1){ else if(sendInfo.cmd==1){
// 重新开启心跳定时 // 重新开启心跳定时
heartCheck.reset(); heartCheck.reset();
} else { } else {
// 其他消息转发出去 // 其他消息转发出去
messageCallBack && messageCallBack(JSON.parse(e.data)) messageCallBack && messageCallBack(sendInfo.cmd,sendInfo.data)
} }
} }
websock.onclose = function(e) { websock.onclose = function(e) {

BIN
im-ui/src/assets/audio/call.wav

Binary file not shown.

BIN
im-ui/src/assets/audio/tip.wav

Binary file not shown.

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

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3776657 */ font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.woff2?t=1668665799410') format('woff2'), src: url('iconfont.woff2?t=1669336625993') format('woff2'),
url('iconfont.woff?t=1668665799410') format('woff'), url('iconfont.woff?t=1669336625993') format('woff'),
url('iconfont.ttf?t=1668665799410') format('truetype'); url('iconfont.ttf?t=1669336625993') format('truetype');
} }
.iconfont { .iconfont {
@ -41,3 +41,11 @@
content: "\e953"; content: "\e953";
} }
.icon-phone-reject:before {
content: "\e605";
}
.icon-phone-accept:before {
content: "\e8be";
}

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.

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

@ -2,8 +2,7 @@
<el-container class="chat-box"> <el-container class="chat-box">
<el-header height="60px"> <el-header height="60px">
<span>{{title}}</span> <span>{{title}}</span>
<span title="群聊信息" v-show="this.chat.type=='GROUP'" class="btn-side el-icon-more" <span title="群聊信息" v-show="this.chat.type=='GROUP'" class="btn-side el-icon-more" @click="showSide=!showSide"></span>
@click="showSide=!showSide"></span>
</el-header> </el-header>
<el-main style="padding: 0;"> <el-main style="padding: 0;">
<el-container> <el-container>
@ -12,11 +11,9 @@
<div class="im-chat-box"> <div class="im-chat-box">
<ul> <ul>
<li v-for="(msgInfo,idx) in chat.messages" :key="idx"> <li v-for="(msgInfo,idx) in chat.messages" :key="idx">
<message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" <chat-message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
:showName="showName(msgInfo)" :msgInfo="msgInfo" :msgInfo="msgInfo" @delete="deleteMessage" @recall="recallMessage">
@delete="deleteMessage" </chat-message-item>
@recall="recallMessage">
</message-item>
</li> </li>
</ul> </ul>
</div> </div>
@ -26,24 +23,24 @@
<div title="表情" class="icon iconfont icon-biaoqing" ref="emotion" @click="switchEmotionBox()"> <div title="表情" class="icon iconfont icon-biaoqing" ref="emotion" @click="switchEmotionBox()">
</div> </div>
<div title="发送图片"> <div title="发送图片">
<file-upload :action="imageAction" :maxSize="5*1024*1024" <file-upload :action="imageAction" :maxSize="5*1024*1024" :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp','image/gif']"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp','image/gif']"
@before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail"> @before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail">
<i class="el-icon-picture-outline"></i> <i class="el-icon-picture-outline"></i>
</file-upload> </file-upload>
</div> </div>
<div title="发送文件"> <div title="发送文件">
<file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore" <file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore" @success="handleFileSuccess"
@success="handleFileSuccess" @fail="handleFileFail"> @fail="handleFileFail">
<i class="el-icon-wallet"></i> <i class="el-icon-wallet"></i>
</file-upload> </file-upload>
</div> </div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()"> <div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</div> </div>
<div title="视频聊天" v-show="chat.type=='PRIVATE'" class="el-icon-phone-outline" @click="showVideoBox()">
</div>
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div> <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div> </div>
<textarea v-model="sendText" ref="sendBox" class="send-text-area" <textarea v-model="sendText" ref="sendBox" class="send-text-area" @keydown.enter="sendTextMessage()"></textarea>
@keydown.enter="sendTextMessage()"></textarea>
<div class="im-chat-send"> <div class="im-chat-send">
<el-button type="primary" @click="sendTextMessage()">发送</el-button> <el-button type="primary" @click="sendTextMessage()">发送</el-button>
</div> </div>
@ -57,25 +54,22 @@
</el-main> </el-main>
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion> <emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion>
<chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice> <chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice>
<chat-history :visible="showHistory" <chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers" @close="closeHistoryBox"></chat-history>
:chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers"
@close="closeHistoryBox"></chat-history>
</el-container> </el-container>
</template> </template>
<script> <script>
import ChatGroupSide from "./ChatGroupSide.vue"; import ChatGroupSide from "./ChatGroupSide.vue";
import MessageItem from "./MessageItem.vue"; import ChatMessageItem from "./ChatMessageItem.vue";
import FileUpload from "../common/FileUpload.vue"; import FileUpload from "../common/FileUpload.vue";
import Emotion from "../common/Emotion.vue"; import Emotion from "../common/Emotion.vue";
import ChatVoice from "./ChatVoice.vue"; import ChatVoice from "./ChatVoice.vue";
import ChatHistory from "./ChatHistory.vue"; import ChatHistory from "./ChatHistory.vue";
export default { export default {
name: "chatPrivate", name: "chatPrivate",
components: { components: {
MessageItem, ChatMessageItem,
FileUpload, FileUpload,
ChatGroupSide, ChatGroupSide,
Emotion, Emotion,
@ -129,7 +123,7 @@
thumbUrl: url thumbUrl: url
} }
let msgInfo = { let msgInfo = {
id:0, id: 0,
fileId: file.uid, fileId: file.uid,
sendId: this.mine.id, sendId: this.mine.id,
content: JSON.stringify(data), content: JSON.stringify(data),
@ -218,10 +212,17 @@
closeVoiceBox() { closeVoiceBox() {
this.showVoice = false; this.showVoice = false;
}, },
showHistoryBox(){ showVideoBox() {
console.log(this.friend)
this.$store.commit("showChatPrivateVideoBox", {
friend: this.friend,
master: true
});
},
showHistoryBox() {
this.showHistory = true; this.showHistory = true;
}, },
closeHistoryBox(){ closeHistoryBox() {
this.showHistory = false; this.showHistory = false;
}, },
handleSendVoice(data) { handleSendVoice(data) {
@ -292,17 +293,17 @@
return false; return false;
} }
}, },
deleteMessage(msgInfo){ deleteMessage(msgInfo) {
this.$confirm( '确认删除消息?','删除消息', { this.$confirm('确认删除消息?', '删除消息', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$store.commit("deleteMessage",msgInfo); this.$store.commit("deleteMessage", msgInfo);
}); });
}, },
recallMessage(msgInfo){ recallMessage(msgInfo) {
this.$confirm('确认撤回消息?','撤回消息', { this.$confirm('确认撤回消息?', '撤回消息', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
@ -316,10 +317,10 @@
msgInfo = JSON.parse(JSON.stringify(msgInfo)); msgInfo = JSON.parse(JSON.stringify(msgInfo));
msgInfo.type = 10; msgInfo.type = 10;
msgInfo.content = '你撤回了一条消息'; msgInfo.content = '你撤回了一条消息';
this.$store.commit("insertMessage",msgInfo); this.$store.commit("insertMessage", msgInfo);
}) })
}); });
}, },
loadGroup(groupId) { loadGroup(groupId) {
this.$http({ this.$http({
@ -346,6 +347,7 @@
method: 'get' method: 'get'
}).then((friend) => { }).then((friend) => {
this.friend = friend; this.friend = friend;
console.log(this.friend)
this.$store.commit("updateChatFromFriend", friend); this.$store.commit("updateChatFromFriend", friend);
this.$store.commit("updateFriend", friend); this.$store.commit("updateFriend", friend);
}) })

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

@ -5,9 +5,9 @@
<el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar" > <el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar" >
<ul> <ul>
<li v-for="(msgInfo,idx) in messages" :key="idx"> <li v-for="(msgInfo,idx) in messages" :key="idx">
<message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" <chat-message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
:msgInfo="msgInfo" :menu="false"> :msgInfo="msgInfo" :menu="false">
</message-item> </chat-message-item>
</li> </li>
</ul> </ul>
</el-scrollbar> </el-scrollbar>
@ -16,12 +16,12 @@
</template> </template>
<script> <script>
import MessageItem from './MessageItem.vue'; import ChatMessageItem from './ChatMessageItem.vue';
export default { export default {
name: 'chatHistory', name: 'chatHistory',
components: { components: {
MessageItem ChatMessageItem
}, },
props: { props: {
visible: { visible: {

77
im-ui/src/components/chat/MessageItem.vue → im-ui/src/components/chat/ChatMessageItem.vue

@ -1,36 +1,36 @@
<template> <template>
<div class="im-msg-item"> <div class="chat-msg-item">
<div class="im-msg-tip" v-show="msgInfo.type==10">{{msgInfo.content}}</div> <div class="chat-msg-tip" v-show="msgInfo.type==10">{{msgInfo.content}}</div>
<div class="im-msg-normal" v-show="msgInfo.type!=10" :class="{'im-chat-mine':mine}"> <div class="chat-msg-normal" v-show="msgInfo.type!=10" :class="{'chat-msg-mine':mine}">
<div class="head-image"> <div class="head-image">
<head-image :url="headImage" :id="msgInfo.sendId"></head-image> <head-image :url="headImage" :id="msgInfo.sendId"></head-image>
</div> </div>
<div class="im-msg-content"> <div class="chat-msg-content">
<div class="im-msg-top"> <div class="chat-msg-top">
<span>{{showName}}</span> <span>{{showName}}</span>
<chat-time :time="msgInfo.sendTime"></chat-time> <chat-time :time="msgInfo.sendTime"></chat-time>
</div> </div>
<div class="im-msg-bottom" @contextmenu.prevent="showRightMenu($event)"> <div class="chat-msg-bottom" @contextmenu.prevent="showRightMenu($event)">
<span class="im-msg-text" v-if="msgInfo.type==0" v-html="$emo.transform(msgInfo.content)"></span> <span class="chat-msg-text" v-if="msgInfo.type==0" v-html="$emo.transform(msgInfo.content)"></span>
<div class="im-msg-image" v-if="msgInfo.type==1"> <div class="chat-msg-image" v-if="msgInfo.type==1">
<div class="img-load-box" v-loading="loading" element-loading-text="上传中.." element-loading-background="rgba(0, 0, 0, 0.4)"> <div class="img-load-box" v-loading="loading" element-loading-text="上传中.." element-loading-background="rgba(0, 0, 0, 0.4)">
<img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" @click="showFullImageBox()" /> <img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" @click="showFullImageBox()" />
</div> </div>
<span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span> <span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span>
</div> </div>
<div class="im-msg-file" v-if="msgInfo.type==2"> <div class="chat-msg-file" v-if="msgInfo.type==2">
<div class="im-file-box" v-loading="loading"> <div class="chat-file-box" v-loading="loading">
<div class="im-file-info"> <div class="chat-file-info">
<el-link class="im-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link> <el-link class="chat-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link>
<div class="im-file-size">{{fileSize}}</div> <div class="chat-file-size">{{fileSize}}</div>
</div> </div>
<div class="im-file-icon"> <div class="chat-file-icon">
<span type="primary" class="el-icon-document"></span> <span type="primary" class="el-icon-document"></span>
</div> </div>
</div> </div>
<span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span> <span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span>
</div> </div>
<div class="im-msg-voice" v-if="msgInfo.type==3" @click="handlePlayVoice()"> <div class="chat-msg-voice" v-if="msgInfo.type==3" @click="handlePlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> <audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div> </div>
</div> </div>
@ -162,13 +162,13 @@
</script> </script>
<style lang="scss"> <style lang="scss">
.im-msg-item { .chat-msg-item {
.im-msg-tip { .chat-msg-tip {
line-height: 50px; line-height: 50px;
} }
.im-msg-normal { .chat-msg-normal {
position: relative; position: relative;
font-size: 0; font-size: 0;
margin-bottom: 10px; margin-bottom: 10px;
@ -183,11 +183,11 @@
left: 0; left: 0;
} }
.im-msg-content { .chat-msg-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.im-msg-top { .chat-msg-top {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
color: #333; color: #333;
@ -199,10 +199,10 @@
} }
} }
.im-msg-bottom { .chat-msg-bottom {
text-align: left; text-align: left;
.im-msg-text { .chat-msg-text {
position: relative; position: relative;
line-height: 22px; line-height: 22px;
margin-top: 10px; margin-top: 10px;
@ -227,7 +227,7 @@
} }
} }
.im-msg-image { .chat-msg-image {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
flex-direction: row; flex-direction: row;
@ -250,14 +250,14 @@
} }
} }
.im-msg-file { .chat-msg-file {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
.im-file-box { .chat-file-box {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
@ -268,20 +268,20 @@
background-color: #eeeeee; background-color: #eeeeee;
padding: 10px 15px; padding: 10px 15px;
.im-file-info { .chat-file-info {
flex: 1; flex: 1;
height: 100%; height: 100%;
text-align: left; text-align: left;
font-size: 14px; font-size: 14px;
.im-file-name { .chat-file-name {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
margin-bottom: 15px; margin-bottom: 15px;
} }
} }
.im-file-icon { .chat-file-icon {
font-size: 50px; font-size: 50px;
color: #d42e07; color: #d42e07;
} }
@ -296,7 +296,7 @@
} }
.im-msg-voice { .chat-msg-voice {
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
@ -309,7 +309,7 @@
} }
&.im-chat-mine { &.chat-msg-mine {
text-align: right; text-align: right;
padding-left: 0; padding-left: 0;
padding-right: 60px; padding-right: 60px;
@ -319,9 +319,9 @@
right: 0; right: 0;
} }
.im-msg-content { .chat-msg-content {
.im-msg-top { .chat-msg-top {
flex-direction: row-reverse; flex-direction: row-reverse;
span { span {
@ -330,10 +330,10 @@
} }
} }
.im-msg-bottom { .chat-msg-bottom {
text-align: right; text-align: right;
.im-msg-text { .chat-msg-text {
margin-left: 10px; margin-left: 10px;
background-color: #5fb878; background-color: #5fb878;
color: #fff; color: #fff;
@ -348,20 +348,15 @@
} }
} }
.im-msg-image { .chat-msg-image {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.im-msg-file { .chat-msg-file {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
} }
} }
.message-info {
right: 60px !important;
display: inline-block;
}
} }
} }

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

@ -0,0 +1,383 @@
<template>
<el-dialog v-dialogDrag :title="title" top="5vh"
:close-on-click-modal="false"
:close-on-press-escape="false"
:visible.sync="visible" width="50%" height="70%" :before-close="handleClose">
<div class="chat-video">
<div class="chat-video-box">
<div class="chat-video-friend" v-loading="loading" element-loading-text="等待对方接听..." element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.5)">
<head-image class="friend-head-image" :id="this.friend.id" :size="80" :url="this.friend.headImage"></head-image>
<video ref="friendVideo" autoplay=""></video>
</div>
<div class="chat-video-mine">
<video ref="mineVideo" autoplay=""></video>
</div>
</div>
<div class="chat-video-controllbar">
<div v-show="state=='CONNECTING'" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;"
@click="cancel()"></div>
<div v-show="state=='CONNECTED'" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;"
@click="handup()"></div>
</div>
</div>
</el-dialog>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
export default {
name: 'chatVideo',
components: {HeadImage},
props: {
visible: {
type: Boolean
},
friend: {
type: Object
},
master: {
type: Boolean
},
offer: {
type: Object
}
},
data() {
return {
stream: null,
audio: new Audio(),
loading: false,
peerConnection: null,
videoTime: 0,
videoTimer: null,
state: 'NOT_CONNECTED',
candidates: [],
configuration: {
iceServers: []
}
}
},
methods: {
init() {
if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) {
this.$message.error("您的浏览器不支持WebRTC");
if (!this.master) {
this.sendFailed("对方浏览器不支持WebRTC")
}
return;
}
//
this.openCamera((stream) => {
// webrtc
this.setupPeerConnection(stream);
if (this.master) {
//
this.call();
} else {
//
this.accept(this.offer);
}
});
},
openCamera(callback) {
navigator.getUserMedia({
video: true,
audio: true
},
(stream) => {
this.stream = stream;
console.log(this.stream)
this.$refs.mineVideo.srcObject = stream;
this.$refs.mineVideo.muted = true;
callback(stream)
},
(error) => {
this.$message.error("打开摄像头失败:" + error);
callback()
});
},
closeCamera() {
if (this.stream) {
this.stream.getTracks().forEach((track) => {
track.stop();
});
this.$refs.mineVideo.srcObject = null;
this.stream = null;
}
},
setupPeerConnection(stream) {
this.peerConnection = new RTCPeerConnection(this.configuration);
this.peerConnection.ontrack = (e) => {
this.$refs.friendVideo.srcObject = e.streams[0];
};
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
if (this.state == 'CONNECTED') {
// ,
this.sendCandidate(event.candidate);
} else {
// ,
this.candidates.push(event.candidate)
}
}
}
if (stream) {
stream.getTracks().forEach((track) => {
this.peerConnection.addTrack(track, stream);
});
}
this.peerConnection.oniceconnectionstatechange = (event) => {
let state = event.target.iceConnectionState;
console.log("ICE connection status changed : " + state)
if(state == 'connected'){
this.resetTime();
}
};
},
handleMessage(msg) {
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content)));
//
this.loading = false;
//
this.state = 'CONNECTED';
//
this.audio.pause();
// candidate
this.candidates.forEach((candidate) => {
this.sendCandidate(candidate);
})
}
else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
this.$message.error("对方拒绝了您的视频请求");
this.close();
}
else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) {
this.$message.error(msg.content)
this.close();
}
else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content)));
}
else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) {
this.$message.success("对方挂断了视频通话");
this.close();
}
},
call() {
this.peerConnection.createOffer((offer) => {
this.peerConnection.setLocalDescription(offer);
this.$http({
url: `/webrtc/private/call?uid=${this.friend.id}`,
method: 'post',
data: JSON.stringify(offer)
}).then(() => {
this.loading = true;
this.state = 'CONNECTING';
this.audio.play();
});
},
(error) => {
this.$message.error(error);
});
},
accept(offer) {
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
this.peerConnection.createAnswer((answer) => {
this.peerConnection.setLocalDescription(answer);
this.$http({
url: `/webrtc/private/accept?uid=${this.friend.id}`,
method: 'post',
data: JSON.stringify(answer)
})
this.state = 'CONNECTED';
},
(error) => {
this.$message.error(error);
});
},
handup() {
this.$http({
url: `/webrtc/private/handup?uid=${this.friend.id}`,
method: 'post'
})
this.close();
this.$message.success("已挂断视频通话")
},
cancel() {
this.$http({
url: `/webrtc/private/cancel?uid=${this.friend.id}`,
method: 'post'
})
this.close();
this.$message.success("已停止呼叫视频通话")
},
sendFailed(reason) {
this.$http({
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`,
method: 'post'
})
},
sendCandidate(candidate) {
this.$http({
url: `/webrtc/private/candidate?uid=${this.friend.id}`,
method: 'post',
data: JSON.stringify(candidate)
})
},
close() {
this.$emit("close");
this.closeCamera();
this.loading = false;
this.state = 'NOT_CONNECTED';
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.audio.pause();
this.candidates = [];
this.$store.commit("setUserState", this.$enums.USER_STATE.FREE);
this.$refs.friendVideo.srcObject = null;
this.peerConnection.close();
this.peerConnection.onicecandidate = null;
this.peerConnection.onaddstream = null;
},
resetTime(){
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.videoTimer = setInterval(()=>{
this.videoTime++;
},1000)
},
handleClose() {
if (this.state == 'CONNECTED') {
this.handup()
} else if (this.state == 'CONNECTING') {
this.cancel();
} else {
this.close();
}
},
hasUserMedia() {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
return !!navigator.getUserMedia;
},
hasRTCPeerConnection() {
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
return !!window.RTCPeerConnection;
},
initAudio(){
let url = require(`@/assets/audio/call.wav`);
this.audio.src = url;
this.audio.loop = true;
},
initICEServers(){
this.$http({
url: '/webrtc/private/iceservers',
method: 'get'
}).then((servers) => {
this.configuration.iceServers = servers;
})
}
},
watch: {
visible: {
handler(newValue, oldValue) {
if (newValue) {
this.init();
//
this.$store.commit("setUserState", this.$enums.USER_STATE.BUSY);
}
}
}
},
computed: {
title() {
let strTitle = `视频聊天-${this.friend.nickName}`;
if(this.state == 'CONNECTED'){
strTitle += `(${this.currentTime})`;
}else if(this.state == 'CONNECTING'){
strTitle += `(呼叫中)`;
}
return strTitle;
},
currentTime(){
let currentTime = 0;
if(this.state == 'CONNECTED' && this.videoTime){
currentTime = Math.floor(this.videoTime);
}
let min = Math.floor(currentTime/60);
let sec = currentTime%60;
let strTime = min<10?"0":"";
strTime += min;
strTime += ":"
strTime += sec<10?"0":"";
strTime += sec;
return strTime;
}
},
mounted() {
this.initAudio();
this.initICEServers();
}
}
</script>
<style lang="scss" scoped>
.chat-video {
position: relative;
.chat-video-box {
position: relative;
border: #4880b9 solid 1px;
background-color: #eeeeee;
.chat-video-friend {
height: 70vh;
.friend-head-image {
position: absolute;
}
video {
width: 100%;
height: 100%;
object-fit: fill;
}
}
.chat-video-mine {
position: absolute;
z-index: 99999;
width: 25vh;
right: 0;
bottom: 0;
box-shadow: 0px 0px 5px #ccc;
background-color: #cccccc;
video {
width: 100%;
}
}
}
.chat-video-controllbar {
display: flex;
justify-content: space-around;
padding: 10px;
.icon {
font-size: 50px;
cursor: pointer;
}
}
}
</style>

131
im-ui/src/components/chat/ChatVideoAcceptor.vue

@ -0,0 +1,131 @@
<template>
<div class="chat-video-acceptor">
<div>
<head-image :size="120" :url="this.friend.headImage" :id="this.friend.id"></head-image>
</div>
<div>
{{friend.nickName}} 请求和您进行视频通话...
</div>
<div class="acceptor-btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="accpet()"></div>
<div class="icon iconfont icon-phone-reject reject" @click="reject()"></div>
</div>
</div>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
export default {
name: "videoAcceptor",
components:{HeadImage},
props: {
friend:{
type: Object
}
},
data(){
return {
offer:{},
audio: new Audio()
}
},
methods:{
accpet(){
let info ={
friend: this.friend,
master: false,
offer: this.offer
}
this.$store.commit("showChatPrivateVideoBox",info);
this.close();
},
reject(){
this.$http({
url: `/webrtc/private/reject?uid=${this.friend.id}`,
method: 'post'
})
this.close();
},
failed(reason){
this.$http({
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`,
method: 'post'
})
this.close();
},
onCall(msgInfo){
console.log("onCall")
this.offer = JSON.parse(msgInfo.content);
if(this.$store.state.userStore.state == this.$enums.USER_STATE.BUSY){
this.failed("对方正忙,暂时无法接听");
return;
}
//
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(()=>{
this.failed("对方未接听");
},30000)
this.audio.play();
},
onCancel(){
this.$message.success("对方取消了呼叫");
this.close();
},
handleMessage(msgInfo){
if(msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL){
this.onCall(msgInfo);
}else if(msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL){
this.onCancel();
}
},
close(){
this.timer && clearTimeout(this.timer);
this.audio.pause();
this.$emit("close");
},
initAudio(){
let url = require(`@/assets/audio/call.wav`);
this.audio.src = url;
this.audio.loop = true;
}
},
mounted() {
//
this.initAudio();
}
}
</script>
<style scoped lang="scss">
.chat-video-acceptor {
position: absolute;
right: 1px;
bottom: 1px;
width: 250px;
height: 250px;
padding: 20px;
text-align: center;
background-color: #eeeeee;
border: #dddddd solid 1px;
.acceptor-btn-group {
display: flex;
justify-content: space-around;
margin-top: 20px;
.icon {
font-size: 50px;
cursor: pointer;
&.accept {
color: green;
}
&.reject {
color: red;
}
}
}
}
</style>

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

@ -19,7 +19,6 @@
<el-button round type="primary" v-show="state=='COMPLETE'" @click="handleRestartRecord()">重新录音</el-button> <el-button round type="primary" v-show="state=='COMPLETE'" @click="handleRestartRecord()">重新录音</el-button>
<el-button round type="primary" v-show="state=='COMPLETE'" @click="handleSendRecord()">立即发送</el-button> <el-button round type="primary" v-show="state=='COMPLETE'" @click="handleSendRecord()">立即发送</el-button>
</el-row> </el-row>
</el-dialog> </el-dialog>
</template> </template>
@ -61,7 +60,8 @@
this.state = 'RUNNING'; this.state = 'RUNNING';
this.stateTip = "正在录音..."; this.stateTip = "正在录音...";
}).catch(error => { }).catch(error => {
this.$message.error("录音失败"); console.log(error);
this.$message.error(error);
console.log(error); console.log(error);
}); });

5
im-ui/src/main.js

@ -9,7 +9,8 @@ import * as socketApi from './api/wssocket';
import emotion from './api/emotion.js'; import emotion from './api/emotion.js';
import element from './api/element.js'; import element from './api/element.js';
import store from './store'; import store from './store';
import * as enums from './api/enums.js';
import './utils/directive/dialogDrag';
Vue.use(ElementUI); Vue.use(ElementUI);
@ -18,7 +19,7 @@ Vue.prototype.$wsApi = socketApi;
Vue.prototype.$http = httpRequest // http请求方法 Vue.prototype.$http = httpRequest // http请求方法
Vue.prototype.$emo = emotion; // emo表情 Vue.prototype.$emo = emotion; // emo表情
Vue.prototype.$elm = element; // 元素操作 Vue.prototype.$elm = element; // 元素操作
Vue.prototype.$enums = enums; // 枚举
Vue.config.productionTip = false; Vue.config.productionTip = false;
new Vue({ new Vue({

30
im-ui/src/store/uiStore.js

@ -11,6 +11,17 @@ export default {
fullImage: { // 全屏大图 fullImage: { // 全屏大图
show: false, show: false,
url: "" url: ""
},
chatPrivateVideo:{ // 私人视频聊天
show: false,
master: false, // 是否房主
friend:{},
offer:{} // 对方发起带过过来的sdp信息
},
videoAcceptor:{ // 视频呼叫选择
show: false,
friend:{}
} }
}, },
@ -35,6 +46,23 @@ export default {
}, },
closeFullImageBox(state){ closeFullImageBox(state){
state.fullImage.show = false; state.fullImage.show = false;
},
showChatPrivateVideoBox(state,info){
state.chatPrivateVideo.show = true;
state.chatPrivateVideo.friend = info.friend;
state.chatPrivateVideo.master = info.master;
state.chatPrivateVideo.offer = info.offer;
},
closeChatPrivateVideoBox(state){
state.chatPrivateVideo.show = false;
},
showVideoAcceptorBox(state,friend){
state.videoAcceptor.show = true;
state.videoAcceptor.friend = friend;
},
closeVideoAcceptorBox(state){
state.videoAcceptor.show = false;
} }
}, }
} }

12
im-ui/src/store/userStore.js

@ -1,7 +1,12 @@
import {USER_STATE} from "../api/enums.js"
export default { export default {
state: { state: {
userInfo: {} userInfo: {
},
state: USER_STATE.FREE
}, },
mutations: { mutations: {
@ -12,7 +17,10 @@ export default {
this.commit("resetChatStore"); this.commit("resetChatStore");
} }
state.userInfo = userInfo; state.userInfo = userInfo;
} },
setUserState(state, userState) {
state.state = userState;
},
} }
} }

72
im-ui/src/utils/directive/dialogDrag.js

@ -0,0 +1,72 @@
import Vue from 'vue'
 
// v-dialogDrag: 弹窗拖拽
Vue.directive('dialogDrag', {
  bind (el, binding, vnode, oldVnode) {
    const dialogHeaderEl = el.querySelector('.el-dialog__header')
    const dragDom = el.querySelector('.el-dialog')
    dialogHeaderEl.style.cursor = 'move'
 
    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
 
    dialogHeaderEl.onmousedown = (e) => {
      // 鼠标按下,计算当前元素距离可视区的距离
      const disX = e.clientX - dialogHeaderEl.offsetLeft
      const disY = e.clientY - dialogHeaderEl.offsetTop
      const screenWidth = document.body.clientWidth; // body当前宽度
      const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
      const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
      const dragDomheight = dragDom.offsetHeight; // 对话框高度
      const minDragDomLeft = dragDom.offsetLeft;
      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
      const minDragDomTop = dragDom.offsetTop;
      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
 
      // 获取到的值带px 正则匹配替换
      let styL, styT
 
      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
      if (sty.left.includes('%')) {
        styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
        styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
      } else {
        styL = +sty.left.replace(/\px/g, '')
        styT = +sty.top.replace(/\px/g, '')
      }
 
      document.onmousemove = function (e) {
        // 获取body的页面可视宽高
        // var clientHeight = document.documentElement.clientHeight || document.body.clientHeight
        // var clientWidth = document.documentElement.clientWidth || document.body.clientWidth
 
        // 通过事件委托,计算移动的距离
        var l = e.clientX - disX
        var t = e.clientY - disY
 
        // 边界处理
        if (-l > minDragDomLeft) {
          l = -minDragDomLeft;
        } else if (l > maxDragDomLeft) {
          l = maxDragDomLeft;
        }
        if (-t > minDragDomTop) {
          t = -minDragDomTop;
        } else if (t > maxDragDomTop) {
          t = maxDragDomTop;
        }
        // 移动当前元素
        dragDom.style.left = `${l + styL}px`
        dragDom.style.top = `${t + styT}px`
 
        // 将此时的位置传出去
        // binding.value({x:e.pageX,y:e.pageY})
      }
 
      document.onmouseup = function (e) {
        document.onmousemove = null
        document.onmouseup = null
      }
    }
  }
})

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

@ -38,6 +38,17 @@
<setting :visible="showSettingDialog" @close="closeSetting()"></setting> <setting :visible="showSettingDialog" @close="closeSetting()"></setting>
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user" @close="$store.commit('closeUserInfoBox')"></user-info> <user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user" @close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url" @close="$store.commit('closeFullImageBox')"></full-image> <full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url" @close="$store.commit('closeFullImageBox')"></full-image>
<chat-private-video ref="privateVideo" :visible="uiStore.chatPrivateVideo.show"
:friend="uiStore.chatPrivateVideo.friend"
:master="uiStore.chatPrivateVideo.master"
:offer="uiStore.chatPrivateVideo.offer"
@close="$store.commit('closeChatPrivateVideoBox')" >
</chat-private-video>
<chat-video-acceptor ref="videoAcceptor"
v-show="uiStore.videoAcceptor.show"
:friend="uiStore.videoAcceptor.friend"
@close="$store.commit('closeVideoAcceptorBox')" >
</chat-video-acceptor>
</el-container> </el-container>
</template> </template>
@ -46,42 +57,49 @@
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 ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue';
export default { export default {
components: { components: {
HeadImage, HeadImage,
Setting, Setting,
UserInfo, UserInfo,
FullImage FullImage,
ChatPrivateVideo,
ChatVideoAcceptor
}, },
data() { data() {
return { return {
showSettingDialog: false showSettingDialog: false,
} }
}, },
methods: { methods: {
init(userInfo) { init(userInfo) {
this.$store.commit("setUserInfo", userInfo); this.$store.commit("setUserInfo", userInfo);
this.$store.commit("setUserState", this.$enums.USER_STATE.FREE);
this.$store.commit("initStore"); this.$store.commit("initStore");
this.$wsApi.createWebSocket(process.env.VUE_APP_WS_URL, userInfo.id); this.$wsApi.createWebSocket(process.env.VUE_APP_WS_URL, userInfo.id);
this.$wsApi.onopen(() => { this.$wsApi.onopen(() => {
this.pullUnreadMessage(); this.pullUnreadMessage();
}); });
this.$wsApi.onmessage((e) => { this.$wsApi.onmessage((cmd,msgInfo) => {
if (e.cmd == 2) { if (cmd == 2) {
// 线 // 线
this.$message.error("您已在其他地方登陆,将被强制下线"); this.$message.error("您已在其他地方登陆,将被强制下线");
setTimeout(() => { setTimeout(() => {
location.href = "/"; location.href = "/";
}, 1000) }, 1000)
} else if (e.cmd == 3) { } else if (cmd == 3) {
// //
this.handlePrivateMessage(e.data); this.handlePrivateMessage(msgInfo);
} else if (e.cmd == 4) { } else if (cmd == 4) {
// //
this.handleGroupMessage(e.data); this.handleGroupMessage(msgInfo);
} }
}) })
}, },
pullUnreadMessage() { pullUnreadMessage() {
@ -113,6 +131,21 @@
}) })
}, },
insertPrivateMessage(friend, msg) { insertPrivateMessage(friend, msg) {
// webrtc
if(msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL
&& msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE){
//
console.log(msg)
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.privateVideo.handleMessage(msg)
}
return ;
}
let chatInfo = { let chatInfo = {
type: 'PRIVATE', type: 'PRIVATE',
targetId: friend.id, targetId: friend.id,
@ -123,6 +156,8 @@
this.$store.commit("openChat", chatInfo); this.$store.commit("openChat", chatInfo);
// //
this.$store.commit("insertMessage", msg); this.$store.commit("insertMessage", msg);
//
this.playAudioTip();
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
// //
@ -151,6 +186,8 @@
this.$store.commit("openChat", chatInfo); this.$store.commit("openChat", chatInfo);
// //
this.$store.commit("insertMessage", msg); this.$store.commit("insertMessage", msg);
//
this.playAudioTip();
}, },
handleExit() { handleExit() {
this.$http({ this.$http({
@ -161,6 +198,12 @@
location.href = "/"; location.href = "/";
}) })
}, },
playAudioTip(){
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;
audio.play();
},
showSetting() { showSetting() {
this.showSettingDialog = true; this.showSettingDialog = true;
}, },

Loading…
Cancel
Save