Browse Source

支持删除和撤回消息

master
xie.bx 3 years ago
parent
commit
53ebbbe483
  1. 3
      commom/src/main/java/com/bx/common/enums/MessageTypeEnum.java
  2. 6
      im-platform/src/main/java/com/bx/implatform/controller/GroupController.java
  3. 18
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  4. 20
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  5. 2
      im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java
  6. 4
      im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java
  7. 4
      im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java
  8. 94
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  9. 40
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  10. 2
      im-server/src/main/java/com/bx/imserver/websocket/WebsocketChannelCtxHolder.java
  11. 2
      im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolDecoder.java
  12. 2
      im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java
  13. 1
      im-server/src/main/java/com/bx/imserver/websocket/processor/HeartbeatProcessor.java
  14. 2
      im-server/src/main/java/com/bx/imserver/websocket/processor/PrivateMessageProcessor.java
  15. 54
      im-ui/src/components/chat/ChatBox.vue
  16. 448
      im-ui/src/components/chat/MessageItem.vue
  17. 63
      im-ui/src/components/common/RightMenu.vue
  18. 4
      im-ui/src/main.js
  19. 43
      im-ui/src/store/chatStore.js
  20. 2
      im-ui/src/store/friendStore.js
  21. 2
      im-ui/src/store/groupStore.js
  22. 1
      im-ui/src/store/uiStore.js
  23. 2
      im-ui/src/view/Home.vue

3
commom/src/main/java/com/bx/common/enums/MessageTypeEnum.java

@ -6,7 +6,8 @@ public enum MessageTypeEnum {
TEXT(0,"文字"), TEXT(0,"文字"),
FILE(1,"文件"), FILE(1,"文件"),
IMAGE(2,"图片"), IMAGE(2,"图片"),
VIDEO(3,"视频"); VIDEO(3,"视频"),
TIP(10,"系统提示");
private Integer code; private Integer code;

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

@ -40,7 +40,7 @@ public class GroupController {
@ApiOperation(value = "解散群聊",notes="解散群聊") @ApiOperation(value = "解散群聊",notes="解散群聊")
@DeleteMapping("/delete/{groupId}") @DeleteMapping("/delete/{groupId}")
public Result<GroupVO> deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ public Result deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){
groupService.deleteGroup(groupId); groupService.deleteGroup(groupId);
return ResultUtils.success(); return ResultUtils.success();
} }
@ -72,14 +72,14 @@ public class GroupController {
@ApiOperation(value = "退出群聊",notes="退出群聊") @ApiOperation(value = "退出群聊",notes="退出群聊")
@DeleteMapping("/quit/{groupId}") @DeleteMapping("/quit/{groupId}")
public Result<GroupVO> quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){ public Result quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId){
groupService.quitGroup(groupId); groupService.quitGroup(groupId);
return ResultUtils.success(); return ResultUtils.success();
} }
@ApiOperation(value = "踢出群聊",notes="将用户踢出群聊") @ApiOperation(value = "踢出群聊",notes="将用户踢出群聊")
@DeleteMapping("/kick/{groupId}") @DeleteMapping("/kick/{groupId}")
public Result<GroupVO> kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId, public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId,
@NotNull(message = "用户id不能为空") @RequestParam Long userId){ @NotNull(message = "用户id不能为空") @RequestParam Long userId){
groupService.kickGroup(groupId,userId); groupService.kickGroup(groupId,userId);
return ResultUtils.success(); return ResultUtils.success();

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

@ -8,13 +8,10 @@ import com.bx.implatform.vo.GroupMessageVO;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@Api(tags = "群聊消息") @Api(tags = "群聊消息")
@ -28,11 +25,16 @@ public class GroupMessageController {
@PostMapping("/send") @PostMapping("/send")
@ApiOperation(value = "发送群聊消息",notes="发送群聊消息") @ApiOperation(value = "发送群聊消息",notes="发送群聊消息")
public Result register(@Valid @RequestBody GroupMessageVO vo){ public Result<Long> sendMessage(@Valid @RequestBody GroupMessageVO vo){
groupMessageService.sendMessage(vo); return ResultUtils.success(groupMessageService.sendMessage(vo));
return ResultUtils.success();
} }
@DeleteMapping("/recall/{id}")
@ApiOperation(value = "撤回消息",notes="撤回群聊消息")
public Result<Long> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id){
groupMessageService.recallMessage(id);
return ResultUtils.success();
}
@PostMapping("/pullUnreadMessage") @PostMapping("/pullUnreadMessage")
@ApiOperation(value = "拉取未读消息",notes="拉取未读消息") @ApiOperation(value = "拉取未读消息",notes="拉取未读消息")

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

@ -8,12 +8,10 @@ import com.bx.implatform.vo.PrivateMessageVO;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@Api(tags = "私聊消息") @Api(tags = "私聊消息")
@RestController @RestController
@ -24,12 +22,20 @@ public class PrivateMessageController {
private IPrivateMessageService privateMessageService; private IPrivateMessageService privateMessageService;
@PostMapping("/send") @PostMapping("/send")
@ApiOperation(value = "发送消息",notes="发送单人消息") @ApiOperation(value = "发送消息",notes="发送私聊消息")
public Result register(@Valid @RequestBody PrivateMessageVO vo){ public Result<Long> sendMessage(@Valid @RequestBody PrivateMessageVO vo){
privateMessageService.sendMessage(vo); return ResultUtils.success(privateMessageService.sendMessage(vo));
}
@DeleteMapping("/recall/{id}")
@ApiOperation(value = "撤回消息",notes="撤回私聊消息")
public Result<Long> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id){
privateMessageService.recallMessage(id);
return ResultUtils.success(); return ResultUtils.success();
} }
@PostMapping("/pullUnreadMessage") @PostMapping("/pullUnreadMessage")
@ApiOperation(value = "拉取未读消息",notes="拉取未读消息") @ApiOperation(value = "拉取未读消息",notes="拉取未读消息")
public Result pullUnreadMessage(){ public Result pullUnreadMessage(){

2
im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java

@ -51,7 +51,7 @@ public class PrivateMessage extends Model<PrivateMessage> {
private String content; private String content;
/** /**
* 消息类型 * 消息类型 0:文字 1:图片 2:文件 3:语音 10:撤回消息
*/ */
@TableField("type") @TableField("type")
private Integer type; private Integer type;

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

@ -8,7 +8,9 @@ import com.bx.implatform.vo.GroupMessageVO;
public interface IGroupMessageService extends IService<GroupMessage> { public interface IGroupMessageService extends IService<GroupMessage> {
void sendMessage(GroupMessageVO vo); Long sendMessage(GroupMessageVO vo);
void recallMessage(Long id);
void pullUnreadMessage(); void pullUnreadMessage();
} }

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

@ -7,7 +7,9 @@ import com.bx.implatform.vo.PrivateMessageVO;
public interface IPrivateMessageService extends IService<PrivateMessage> { public interface IPrivateMessageService extends IService<PrivateMessage> {
void sendMessage(PrivateMessageVO vo); Long sendMessage(PrivateMessageVO vo);
void recallMessage(Long id);
void pullUnreadMessage(); void pullUnreadMessage();

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

@ -3,6 +3,7 @@ package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.common.contant.RedisKey; import com.bx.common.contant.RedisKey;
import com.bx.common.enums.MessageTypeEnum;
import com.bx.common.enums.ResultCode; import com.bx.common.enums.ResultCode;
import com.bx.common.model.im.GroupMessageInfo; import com.bx.common.model.im.GroupMessageInfo;
import com.bx.common.util.BeanUtils; import com.bx.common.util.BeanUtils;
@ -46,7 +47,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
* @return * @return
*/ */
@Override @Override
public void sendMessage(GroupMessageVO vo) { public Long sendMessage(GroupMessageVO vo) {
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getId();
Group group = groupService.getById(vo.getGroupId()); Group group = groupService.getById(vo.getGroupId());
if(group == null){ if(group == null){
@ -65,31 +66,49 @@ 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);
// 根据群聊每个成员所连的IM-server,进行分组 // 群发
Map<Integer,List<Long>> serverMap = new ConcurrentHashMap<>(); GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class);
userIds.parallelStream().forEach(id->{ this.sendMessage(userIds,msgInfo);
String key = RedisKey.IM_USER_SERVER_ID + id; log.info("发送群聊消息,发送id:{},群聊id:{},内容:{}",userId,vo.getGroupId(),vo.getContent());
Integer serverId = (Integer)redisTemplate.opsForValue().get(key); return msg.getId();
if(serverId != null){ }
if(serverMap.containsKey(serverId)){
serverMap.get(serverId).add(id);
}else {
List<Long> list = Collections.synchronizedList(new LinkedList<Long>());
list.add(id); /**
serverMap.put(serverId,list); * 撤回消息
} *
} * @param id 消息id
}); */
// 逐个server发送 @Override
for (Map.Entry<Integer,List<Long>> entry : serverMap.entrySet()) { public void recallMessage(Long id) {
GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class); Long userId = SessionContext.getSession().getId();
msgInfo.setRecvIds(new LinkedList<>(entry.getValue())); GroupMessage msg = this.getById(id);
String key = RedisKey.IM_UNREAD_GROUP_MESSAGE +entry.getKey(); if(msg == null){
redisTemplate.opsForList().rightPush(key,msgInfo); throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息不存在");
}
if(msg.getSendId() != userId){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"这条消息不是您发送的呢");
}
// 判断是否在群里
GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(),userId);
if(member == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法撤回消息");
} }
log.info("发送群聊消息,发送id:{},群聊id:{}",userId,vo.getGroupId()); // 直接物理删除
this.removeById(id);
// 群发
List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId());
GroupMessageInfo msgInfo = BeanUtils.copyProperties(msg, GroupMessageInfo.class);
msgInfo.setType(MessageTypeEnum.TIP.getCode());
String content = String.format("'%s'撤回了一条消息",member.getAliasName());
msgInfo.setContent(content);
this.sendMessage(userIds,msgInfo);
log.info("删除群聊消息,发送id:{},群聊id:{},内容:{}",userId,msg.getGroupId(),msg.getContent());
} }
/** /**
* 异步拉取群聊消息通过websocket异步推送 * 异步拉取群聊消息通过websocket异步推送
* *
@ -129,6 +148,35 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
}).collect(Collectors.toList()); }).collect(Collectors.toList());
key = RedisKey.IM_UNREAD_GROUP_MESSAGE + serverId; key = RedisKey.IM_UNREAD_GROUP_MESSAGE + serverId;
redisTemplate.opsForList().rightPushAll(key,messageInfos.toArray()); redisTemplate.opsForList().rightPushAll(key,messageInfos.toArray());
log.info("拉取未读群聊消息,用户id:{},群聊id:{},数量:{}",userId,member.getGroupId(),messageInfos.size());
}
}
private void sendMessage(List<Long> userIds, GroupMessageInfo msgInfo){
// 根据群聊每个成员所连的IM-server,进行分组
Map<Integer,List<Long>> serverMap = new ConcurrentHashMap<>();
userIds.parallelStream().forEach(id->{
String key = RedisKey.IM_USER_SERVER_ID + id;
Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
if(serverId != null){
if(serverMap.containsKey(serverId)){
serverMap.get(serverId).add(id);
}else {
// 此处需要加锁,否则list可以会被覆盖
synchronized(serverMap){
List<Long> list = Collections.synchronizedList(new LinkedList<Long>());
list.add(id);
serverMap.put(serverId,list);
}
}
}
});
// 逐个server发送
for (Map.Entry<Integer,List<Long>> entry : serverMap.entrySet()) {
msgInfo.setRecvIds(new LinkedList<>(entry.getValue()));
String key = RedisKey.IM_UNREAD_GROUP_MESSAGE +entry.getKey();
redisTemplate.opsForList().rightPush(key,msgInfo);
} }
} }
} }

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

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.common.contant.RedisKey; import com.bx.common.contant.RedisKey;
import com.bx.common.enums.MessageStatusEnum; import com.bx.common.enums.MessageStatusEnum;
import com.bx.common.enums.MessageTypeEnum;
import com.bx.common.enums.ResultCode; import com.bx.common.enums.ResultCode;
import com.bx.common.model.im.PrivateMessageInfo; import com.bx.common.model.im.PrivateMessageInfo;
import com.bx.common.util.BeanUtils; import com.bx.common.util.BeanUtils;
@ -36,10 +37,10 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
* 发送私聊消息 * 发送私聊消息
* *
* @param vo 私聊消息vo * @param vo 私聊消息vo
* @return * @return 消息id
*/ */
@Override @Override
public void sendMessage(PrivateMessageVO vo) { public Long sendMessage(PrivateMessageVO vo) {
Long userId = SessionContext.getSession().getId(); Long userId = SessionContext.getSession().getId();
Boolean isFriends = friendService.isFriend(userId,vo.getRecvId()); Boolean isFriends = friendService.isFriend(userId,vo.getRecvId());
if(!isFriends){ if(!isFriends){
@ -60,7 +61,39 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class); PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class);
redisTemplate.opsForList().rightPush(sendKey,msgInfo); redisTemplate.opsForList().rightPush(sendKey,msgInfo);
} }
log.info("发送私聊消息,发送id:{},接收id:{}",userId,vo.getRecvId()); log.info("发送私聊消息,发送id:{},接收id:{},内容:{}",userId,vo.getRecvId(),vo.getContent());
return msg.getId();
}
/**
* 撤回消息
*
* @param id 消息id
*/
@Override
public void recallMessage(Long id) {
Long userId = SessionContext.getSession().getId();
PrivateMessage msg = this.getById(id);
if(msg == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息不存在");
}
if(msg.getSendId() != userId){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"这条消息不是您发送的呢");
}
// 直接物理删除
this.removeById(id);
// 获取对方连接的channelId
String key = RedisKey.IM_USER_SERVER_ID+msg.getRecvId();
Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
// 如果对方在线,将数据存储至redis,等待拉取推送
if(serverId != null){
String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId;
PrivateMessageInfo msgInfo = BeanUtils.copyProperties(msg, PrivateMessageInfo.class);
msgInfo.setType(MessageTypeEnum.TIP.getCode());
msgInfo.setContent("对方撤回了一条消息");
redisTemplate.opsForList().rightPush(sendKey,msgInfo);
}
log.info("删除私聊消息,发送id:{},接收id:{},内容:{}",msg.getSendId(),msg.getRecvId(),msg.getContent());
} }
/** /**
@ -90,6 +123,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
}).collect(Collectors.toList()); }).collect(Collectors.toList());
String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId; String sendKey = RedisKey.IM_UNREAD_PRIVATE_MESSAGE + serverId;
redisTemplate.opsForList().rightPushAll(sendKey,infos.toArray()); redisTemplate.opsForList().rightPushAll(sendKey,infos.toArray());
log.info("拉取未读私聊消息,用户id:{},数量:{}",userId,infos.size());
} }
} }
} }

2
im-server/src/main/java/com/bx/imserver/websocket/WebsocketChannelCtxHolder.java

@ -2,7 +2,7 @@ package com.bx.imserver.websocket;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import java.util.*; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;

2
im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolDecoder.java

@ -1,7 +1,7 @@
package com.bx.imserver.websocket.endecode; package com.bx.imserver.websocket.endecode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bx.common.model.im.SendInfo; import com.bx.common.model.im.SendInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

2
im-server/src/main/java/com/bx/imserver/websocket/endecode/MessageProtocolEncoder.java

@ -1,7 +1,7 @@
package com.bx.imserver.websocket.endecode; package com.bx.imserver.websocket.endecode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bx.common.model.im.SendInfo; import com.bx.common.model.im.SendInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder; import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

1
im-server/src/main/java/com/bx/imserver/websocket/processor/HeartbeatProcessor.java

@ -8,7 +8,6 @@ import com.bx.common.model.im.HeartbeatInfo;
import com.bx.common.model.im.SendInfo; import com.bx.common.model.im.SendInfo;
import com.bx.imserver.websocket.WebsocketServer; import com.bx.imserver.websocket.WebsocketServer;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

2
im-server/src/main/java/com/bx/imserver/websocket/processor/PrivateMessageProcessor.java

@ -2,8 +2,8 @@ package com.bx.imserver.websocket.processor;
import com.bx.common.contant.RedisKey; import com.bx.common.contant.RedisKey;
import com.bx.common.enums.WSCmdEnum; import com.bx.common.enums.WSCmdEnum;
import com.bx.common.model.im.SendInfo;
import com.bx.common.model.im.PrivateMessageInfo; import com.bx.common.model.im.PrivateMessageInfo;
import com.bx.common.model.im.SendInfo;
import com.bx.imserver.websocket.WebsocketChannelCtxHolder; import com.bx.imserver.websocket.WebsocketChannelCtxHolder;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

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

@ -13,7 +13,9 @@
<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)" <message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" :msgInfo="msgInfo"> :showName="showName(msgInfo)" :msgInfo="msgInfo"
@delete="deleteMessage"
@recall="recallMessage">
</message-item> </message-item>
</li> </li>
</ul> </ul>
@ -108,10 +110,11 @@
url: this.messageAction, url: this.messageAction,
method: 'post', method: 'post',
data: msgInfo data: msgInfo
}).then((data) => { }).then((id) => {
let info = { let info = {
type: this.chat.type, type: this.chat.type,
targetId: file.raw.targetId, targetId: file.raw.targetId,
msgId : id,
fileId: file.raw.uid, fileId: file.raw.uid,
content: JSON.stringify(res.data), content: JSON.stringify(res.data),
loadStatus: "ok" loadStatus: "ok"
@ -135,6 +138,7 @@
thumbUrl: url thumbUrl: url
} }
let msgInfo = { let msgInfo = {
id:0,
fileId: file.uid, fileId: file.uid,
sendId: this.mine.id, sendId: this.mine.id,
content: JSON.stringify(data), content: JSON.stringify(data),
@ -168,11 +172,12 @@
url: this.messageAction, url: this.messageAction,
method: 'post', method: 'post',
data: msgInfo data: msgInfo
}).then(() => { }).then((id) => {
let info = { let info = {
type: this.chat.type, type: this.chat.type,
targetId: file.raw.targetId, targetId: file.raw.targetId,
fileId: file.raw.uid, fileId: file.raw.uid,
msgId : id,
content: JSON.stringify(data), content: JSON.stringify(data),
loadStatus: "ok" loadStatus: "ok"
} }
@ -196,6 +201,7 @@
url: url url: url
} }
let msgInfo = { let msgInfo = {
id: 0,
fileId: file.uid, fileId: file.uid,
sendId: this.mine.id, sendId: this.mine.id,
content: JSON.stringify(data), content: JSON.stringify(data),
@ -210,7 +216,7 @@
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
// //
this.scrollToBottom(); this.scrollToBottom();
// fileid // file
file.targetId = this.chat.targetId; file.targetId = this.chat.targetId;
}, },
handleCloseSide() { handleCloseSide() {
@ -232,7 +238,6 @@
}, },
showVoiceBox() { showVoiceBox() {
this.showVoice = true; this.showVoice = true;
}, },
closeVoiceBox() { closeVoiceBox() {
this.showVoice = false; this.showVoice = false;
@ -248,8 +253,9 @@
url: this.messageAction, url: this.messageAction,
method: 'post', method: 'post',
data: msgInfo data: msgInfo
}).then(() => { }).then((id) => {
this.$message.success("发送成功"); this.$message.success("发送成功");
msgInfo.id = id;
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;
@ -270,7 +276,6 @@
} }
}, },
sendTextMessage() { sendTextMessage() {
if (!this.sendText.trim()) { if (!this.sendText.trim()) {
this.$message.error("不能发送空白信息"); this.$message.error("不能发送空白信息");
return return
@ -285,9 +290,10 @@
url: this.messageAction, url: this.messageAction,
method: 'post', method: 'post',
data: msgInfo data: msgInfo
}).then((data) => { }).then((id) => {
this.$message.success("发送成功"); this.$message.success("发送成功");
this.sendText = ""; this.sendText = "";
msgInfo.id = id;
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;
@ -304,6 +310,35 @@
return false; return false;
} }
}, },
deleteMessage(msgInfo){
this.$confirm( '确认删除消息?','删除消息', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.commit("deleteMessage",msgInfo);
});
},
recallMessage(msgInfo){
this.$confirm('确认撤回消息?','撤回消息', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`
this.$http({
url: url,
method: 'delete'
}).then(() => {
this.$message.success("消息已撤回");
msgInfo = JSON.parse(JSON.stringify(msgInfo));
msgInfo.type = 10;
msgInfo.content = '你撤回了一条消息';
this.$store.commit("insertMessage",msgInfo);
})
});
},
loadGroup(groupId) { loadGroup(groupId) {
this.$http({ this.$http({
url: `/group/find/${groupId}`, url: `/group/find/${groupId}`,
@ -378,6 +413,7 @@
messageAction() { messageAction() {
return `/message/${this.chat.type.toLowerCase()}/send`; return `/message/${this.chat.type.toLowerCase()}/send`;
} }
}, },
watch: { watch: {
chat: { chat: {
@ -433,7 +469,7 @@
border: #dddddd solid 1px; border: #dddddd solid 1px;
.im-chat-box { .im-chat-box {
ul { >ul {
padding: 20px; padding: 20px;
li { li {

448
im-ui/src/components/chat/MessageItem.vue

@ -1,58 +1,62 @@
<template> <template>
<div class="im-msg-item" :class="{'im-chat-mine':mine}"> <div class="im-msg-item">
<div class="head-image"> <div class="im-msg-tip" v-show="msgInfo.type==10">{{msgInfo.content}}</div>
<head-image :url="headImage" :id="msgInfo.sendId"></head-image> <div class="im-msg-normal" v-show="msgInfo.type!=10" :class="{'im-chat-mine':mine}">
</div> <div class="head-image">
<div class="im-msg-content"> <head-image :url="headImage" :id="msgInfo.sendId"></head-image>
<div class="im-msg-top">
<span>{{showName}}</span>
<chat-time :time="msgInfo.sendTime"></chat-time>
</div> </div>
<div class="im-msg-bottom"> <div class="im-msg-content">
<span class="im-msg-text" v-if="msgInfo.type==0" v-html="$emo.transform(msgInfo.content)"></span> <div class="im-msg-top">
<div class="im-msg-image" v-if="msgInfo.type==1"> <span>{{showName}}</span>
<div class="img-load-box" v-loading="loading" <chat-time :time="msgInfo.sendTime"></chat-time>
element-loading-text="上传中.."
element-loading-background="rgba(0, 0, 0, 0.4)">
<img class="send-image"
:src="JSON.parse(msgInfo.content).thumbUrl"
@click="showFullImageBox()"/>
</div>
<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="im-msg-bottom" @contextmenu.prevent="showRightMenu($event)">
<div class="im-file-box" v-loading="loading"> <span class="im-msg-text" v-if="msgInfo.type==0" v-html="$emo.transform(msgInfo.content)"></span>
<div class="im-file-info"> <div class="im-msg-image" v-if="msgInfo.type==1">
<el-link class="im-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link> <div class="img-load-box" v-loading="loading" element-loading-text="上传中.." element-loading-background="rgba(0, 0, 0, 0.4)">
<div class="im-file-size">{{fileSize}}</div> <img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" @click="showFullImageBox()" />
</div> </div>
<div class="im-file-icon"> <span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span>
<span type="primary" class="el-icon-document"></span> </div>
<div class="im-msg-file" v-if="msgInfo.type==2">
<div class="im-file-box" v-loading="loading">
<div class="im-file-info">
<el-link class="im-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link>
<div class="im-file-size">{{fileSize}}</div>
</div>
<div class="im-file-icon">
<span type="primary" class="el-icon-document"></span>
</div>
</div> </div>
<span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span>
</div>
<div class="im-msg-voice" v-if="msgInfo.type==3" @click="handlePlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div> </div>
<span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span>
</div>
<div class="im-msg-voice" v-if="msgInfo.type==3" @click="handlePlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div> </div>
</div> </div>
</div> </div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show=false"
@select="handleSelectMenu"></right-menu>
</div> </div>
</template> </template>
<script> <script>
import ChatTime from "./ChatTime.vue"; import ChatTime from "./ChatTime.vue";
import HeadImage from "../common/HeadImage.vue"; import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue';
export default { export default {
name: "messageItem", name: "messageItem",
components: { components: {
ChatTime, ChatTime,
HeadImage HeadImage,
RightMenu
}, },
props: { props: {
mine:{ mine: {
type:Boolean, type: Boolean,
required: true required: true
}, },
headImage: { headImage: {
@ -68,42 +72,59 @@
required: true required: true
} }
}, },
data(){ data() {
return { return {
audioPlayState: 'STOP', audioPlayState: 'STOP',
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
}
}
} }
}, },
methods:{ methods: {
handleSendFail(){ handleSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送") this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
}, },
showFullImageBox(){ showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl; let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if(imageUrl){ if (imageUrl) {
this.$store.commit('showFullImageBox',imageUrl); this.$store.commit('showFullImageBox', imageUrl);
} }
}, },
handlePlayVoice(){ handlePlayVoice() {
if(!this.audio){ if (!this.audio) {
this.audio = new Audio(); this.audio = new Audio();
} }
this.audio.src = JSON.parse(this.msgInfo.content).url; this.audio.src = JSON.parse(this.msgInfo.content).url;
this.audio.play(); this.audio.play();
this.handlePlayVoice = 'RUNNING'; this.handlePlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
handleSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
} }
}, },
computed:{ computed: {
loading(){ loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading"; return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
}, },
loadFail(){ loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail"; return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
}, },
data(){ data() {
return JSON.parse(this.msgInfo.content) return JSON.parse(this.msgInfo.content)
}, },
fileSize(){ fileSize() {
let size = this.data.size; let size = this.data.size;
if (size > 1024 * 1024) { if (size > 1024 * 1024) {
return Math.round(size / 1024 / 1024) + "M"; return Math.round(size / 1024 / 1024) + "M";
@ -113,6 +134,22 @@
} }
return size + "B"; 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({
key: 'RECALL',
name: '撤回',
icon: 'el-icon-refresh-left'
});
}
return items;
}
}, },
mounted() { mounted() {
//console.log(this.msgInfo); //console.log(this.msgInfo);
@ -122,198 +159,207 @@
<style lang="scss"> <style lang="scss">
.im-msg-item { .im-msg-item {
position: relative;
font-size: 0;
margin-bottom: 10px;
padding-left: 60px;
min-height: 68px;
.head-image {
position: absolute;
width: 40px;
height: 40px;
top: 0;
left: 0;
}
.im-msg-content { .im-msg-tip {
display: flex; line-height: 50px;
flex-direction: column; }
.im-msg-top { .im-msg-normal {
display: flex; position: relative;
flex-wrap: nowrap; font-size: 0;
color: #333; margin-bottom: 10px;
font-size: 14px; padding-left: 60px;
line-height: 20px; min-height: 68px;
span { .head-image {
margin-right: 12px; position: absolute;
} width: 40px;
height: 40px;
top: 0;
left: 0;
} }
.im-msg-bottom { .im-msg-content {
text-align: left; display: flex;
flex-direction: column;
.im-msg-text { .im-msg-top {
position: relative; display: flex;
line-height: 22px; flex-wrap: nowrap;
margin-top: 10px;
padding: 10px;
background-color: #eeeeee;
border-radius: 3px;
color: #333; color: #333;
display: inline-block;
font-size: 14px; font-size: 14px;
line-height: 20px;
&:after {
content: ""; span {
position: absolute; margin-right: 12px;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #eeeeee transparent transparent;
overflow: hidden;
border-width: 10px;
} }
} }
.im-msg-image{ .im-msg-bottom {
display: flex; text-align: left;
flex-wrap: nowrap;
flex-direction: row; .im-msg-text {
align-items: center; position: relative;
line-height: 22px;
.send-image{ margin-top: 10px;
min-width: 300px; padding: 10px;
min-height: 200px; background-color: #eeeeee;
max-width: 600px; border-radius: 3px;
max-height: 400px; color: #333;
border: #dddddd solid 1px; display: inline-block;
cursor: pointer; font-size: 14px;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #eeeeee transparent transparent;
overflow: hidden;
border-width: 10px;
}
} }
.send-fail{ .im-msg-image {
color: #e60c0c; display: flex;
font-size: 30px; flex-wrap: nowrap;
cursor: pointer; flex-direction: row;
margin: 0 20px; align-items: center;
.send-image {
min-width: 300px;
min-height: 200px;
max-width: 600px;
max-height: 400px;
border: #dddddd solid 1px;
cursor: pointer;
}
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
} }
}
.im-msg-file {
.im-msg-file{
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
cursor: pointer;
.im-file-box{
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
flex-direction: row;
align-items: center; align-items: center;
width: 20%; cursor: pointer;
min-height: 80px;
border: #dddddd solid 1px; .im-file-box {
border-radius: 3px; display: flex;
background-color: #eeeeee; flex-wrap: nowrap;
padding: 10px 15px; align-items: center;
.im-file-info{ width: 20%;
flex:1; min-height: 80px;
height: 100%; border: #dddddd solid 1px;
text-align: left; border-radius: 3px;
font-size: 14px; background-color: #eeeeee;
.im-file-name { padding: 10px 15px;
font-size: 16px;
font-weight: 600; .im-file-info {
margin-bottom: 15px; flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
.im-file-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
}
}
.im-file-icon {
font-size: 50px;
color: #d42e07;
} }
} }
.im-file-icon{ .send-fail {
font-size: 50px; color: #e60c0c;
color: #d42e07; font-size: 30px;
cursor: pointer;
margin: 0 20px;
} }
} }
.send-fail{ .im-msg-voice {
color: #e60c0c; font-size: 14px;
font-size: 30px;
cursor: pointer; cursor: pointer;
margin: 0 20px;
} audio {
height: 45px;
} padding: 5px 0;
}
.im-msg-voice {
font-size: 14px;
cursor: pointer;
audio {
height: 45px;
padding: 5px 0;
} }
} }
} }
}
&.im-chat-mine { &.im-chat-mine {
text-align: right; text-align: right;
padding-left: 0; padding-left: 0;
padding-right: 60px; padding-right: 60px;
.head-image { .head-image {
left: auto; left: auto;
right: 0; right: 0;
} }
.im-msg-content { .im-msg-content {
.im-msg-top { .im-msg-top {
flex-direction: row-reverse; flex-direction: row-reverse;
span { span {
margin-left: 12px; margin-left: 12px;
margin-right: 0; margin-right: 0;
}
} }
}
.im-msg-bottom { .im-msg-bottom {
text-align: right; text-align: right;
.im-msg-text { .im-msg-text {
margin-left: 10px; margin-left: 10px;
background-color: #5fb878; background-color: #5fb878;
color: #fff; color: #fff;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
font-size: 14px; font-size: 14px;
&:after { &:after {
left: auto; left: auto;
right: -10px; right: -10px;
border-top-color: #5fb878; border-top-color: #5fb878;
}
}
.im-msg-image {
flex-direction: row-reverse;
}
.im-msg-file {
flex-direction: row-reverse;
} }
}
.im-msg-image {
flex-direction: row-reverse;
}
.im-msg-file {
flex-direction: row-reverse;
} }
} }
}
.message-info { .message-info {
right: 60px !important; right: 60px !important;
display: inline-block; display: inline-block;
}
} }
}
}
} }
</style> </style>

63
im-ui/src/components/common/RightMenu.vue

@ -0,0 +1,63 @@
<template>
<div class="right-menu-mask" @click="close()" @contextmenu.prevent="close()">
<div class="right-menu" :style="{'left':pos.x+'px','top':pos.y+'px'}">
<el-menu background-color="#f5f5f5" text-color="#333333">
<el-menu-item v-for="(item) in items" :key="item.key" :title="item.name" @click="handleSelectMenu(item)">
<i :class="item.icon"></i>
<span>{{item.name}}</span>
</el-menu-item>
</el-menu>
</div>
</div>
</template>
<script>
export default {
name: "rightMenu",
data() {
return {}
},
props: {
pos: {
type: Object
},
items:{
type: Array
}
},
methods:{
close(){
this.$emit("close");
},
handleSelectMenu(item){
this.$emit("select",item);
}
}
}
</script>
<style lang="scss">
.right-menu-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
}
.right-menu {
position: fixed;
.el-menu-item {
height: 40px;
line-height: 40px;
i {
color: #333333;
}
}
}
</style>

4
im-ui/src/main.js

@ -6,8 +6,8 @@ import 'element-ui/lib/theme-chalk/index.css';
import './assets/iconfont/iconfont.css'; import './assets/iconfont/iconfont.css';
import httpRequest from './api/httpRequest'; import httpRequest from './api/httpRequest';
import * as socketApi from './api/wssocket'; 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';

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

@ -84,7 +84,7 @@ export default {
break; break;
} }
} }
console.log(msgInfo.type) // 插入新的数据
if(msgInfo.type == 1){ if(msgInfo.type == 1){
chat.lastContent = "[图片]"; chat.lastContent = "[图片]";
}else if(msgInfo.type == 2){ }else if(msgInfo.type == 2){
@ -95,12 +95,48 @@ export default {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.messages.push(msgInfo);
// 如果不是当前会话,未读加1 // 如果不是当前会话,未读加1
chat.unreadCount++; chat.unreadCount++;
if(msgInfo.selfSend){ if(msgInfo.selfSend){
chat.unreadCount=0; chat.unreadCount=0;
} }
console.log(msgInfo);
// 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) {
if(msgInfo.id && chat.messages[idx].id == msgInfo.id){
Object.assign(chat.messages[idx], msgInfo);
return;
}
}
// 新的消息
chat.messages.push(msgInfo);
},
deleteMessage(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;
}
}
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if(chat.messages[idx].id && chat.messages[idx].id == msgInfo.id){
chat.messages.splice(idx, 1);
break;
}
// 没有发送成功的,根据发送时间删除
if(!chat.messages[idx].id && chat.messages[idx].sendTime == msgInfo.sendTime){
chat.messages.splice(idx, 1);
break;
}
}
}, },
handleFileUpload(state, info) { handleFileUpload(state, info) {
// 文件上传后数据更新 // 文件上传后数据更新
@ -110,6 +146,9 @@ export default {
if (info.content) { if (info.content) {
msg.content = info.content; msg.content = info.content;
} }
if(info.msgId){
msg.id = info.msgId;
}
}, },
updateChatFromFriend(state, friend) { updateChatFromFriend(state, friend) {
for (let i in state.chats) { for (let i in state.chats) {

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

@ -25,7 +25,7 @@ export default {
state.friends.forEach((f,index)=>{ state.friends.forEach((f,index)=>{
if(f.id==friend.id){ if(f.id==friend.id){
// 拷贝属性 // 拷贝属性
state.friends[index] = Object.assign(state.friends[index], friend); Object.assign(state.friends[index], friend);
} }
}) })
}, },

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

@ -39,7 +39,7 @@ export default {
state.groups.forEach((g,idx)=>{ state.groups.forEach((g,idx)=>{
if(g.id==group.id){ if(g.id==group.id){
// 拷贝属性 // 拷贝属性
state.groups[idx] = Object.assign(state.groups[idx], group); Object.assign(state.groups[idx], group);
} }
}) })
} }

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

@ -12,6 +12,7 @@ export default {
show: false, show: false,
url: "" url: ""
} }
}, },
mutations: { mutations: {
showUserInfoBox(state,user){ showUserInfoBox(state,user){

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

@ -277,4 +277,4 @@
text-align: center; text-align: center;
} }
</style> </style>

Loading…
Cancel
Save