Browse Source

!30 加入群聊@功能

Merge pull request !30 from blue/v_2.0.0
master
blue 2 years ago
committed by Gitee
parent
commit
0fda066333
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 9
      README.md
  2. 9
      im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java
  3. 11
      im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java
  4. 1
      im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java
  5. 60
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  6. 1
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  7. 7
      im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java
  8. 8
      im-platform/src/main/resources/application.yml
  9. 2
      im-platform/src/main/resources/db/db.sql
  10. 16
      im-ui/src/api/emotion.js
  11. 34
      im-ui/src/api/wssocket.js
  12. 175
      im-ui/src/components/chat/ChatAtBox.vue
  13. 321
      im-ui/src/components/chat/ChatBox.vue
  14. 10
      im-ui/src/components/chat/ChatGroupSide.vue
  15. 8
      im-ui/src/components/chat/ChatHistory.vue
  16. 76
      im-ui/src/components/chat/ChatItem.vue
  17. 24
      im-ui/src/components/chat/ChatMessageItem.vue
  18. 40
      im-ui/src/components/chat/ChatVoice.vue
  19. 36
      im-ui/src/components/common/Emotion.vue
  20. 6
      im-ui/src/components/common/FileUpload.vue
  21. 6
      im-ui/src/components/common/FullImage.vue
  22. 4
      im-ui/src/components/common/RightMenu.vue
  23. 8
      im-ui/src/components/common/UserInfo.vue
  24. 14
      im-ui/src/components/friend/AddFriend.vue
  25. 14
      im-ui/src/components/friend/FriendItem.vue
  26. 22
      im-ui/src/components/group/AddGroupMember.vue
  27. 12
      im-ui/src/components/group/GroupItem.vue
  28. 4
      im-ui/src/components/group/GroupMember.vue
  29. 14
      im-ui/src/components/setting/Setting.vue
  30. 14
      im-ui/src/store/chatStore.js
  31. 4
      im-ui/src/store/friendStore.js
  32. 4
      im-ui/src/store/groupStore.js
  33. 35
      im-ui/src/view/Chat.vue
  34. 89
      im-ui/src/view/Friend.vue
  35. 112
      im-ui/src/view/Group.vue
  36. 34
      im-ui/src/view/Home.vue
  37. 7
      im-ui/src/view/Login.vue
  38. 26
      im-uniapp/App.vue
  39. 2
      im-uniapp/common/emotion.js
  40. 39
      im-uniapp/common/wssocket.js
  41. 176
      im-uniapp/components/chat-at-box/chat-at-box.vue
  42. 59
      im-uniapp/components/chat-item/chat-item.vue
  43. 12
      im-uniapp/components/chat-message-item/chat-message-item.vue
  44. 8
      im-uniapp/components/friend-item/friend-item.vue
  45. 8
      im-uniapp/components/group-item/group-item.vue
  46. 6
      im-uniapp/components/head-image/head-image.vue
  47. 8
      im-uniapp/package.json
  48. 117
      im-uniapp/pages/chat/chat-box.vue
  49. 2
      im-uniapp/pages/group/group-invite.vue
  50. 3
      im-uniapp/pages/login/login.vue
  51. 6
      im-uniapp/static/icon/iconfont.css
  52. BIN
      im-uniapp/static/icon/iconfont.ttf
  53. 47
      im-uniapp/store/chatStore.js
  54. 1
      im-uniapp/store/friendStore.js

9
README.md

@ -13,14 +13,13 @@
#### 近期更新 #### 近期更新
发布2.0版本,本次更新主要是加入了uniapp版本: 发布2.0版本,本次更新加入了uniapp版本:
- 支持移动端和web端同时在线,多端消息同步 - 支持移动端和web端同时在线,多端消息同步
- 目前仅兼容h5和微信小程序,后续会继续兼容更多终端类型 - 目前仅兼容h5和微信小程序,后续会继续兼容更多终端类型
- 页面风格优化:表情包更新、自动生成文字头像等 - 聊天窗口加入已读未读显示
- 群聊加入@功能
感兴趣的小伙伴,可在下方扫码体验 - 界面风格升级,表情包更新、生成文字头像等
#### 在线体验 #### 在线体验

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

@ -7,6 +7,8 @@ import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
@Data @Data
@ApiModel("群聊消息DTO") @ApiModel("群聊消息DTO")
@ -16,8 +18,7 @@ public class GroupMessageDTO {
@ApiModelProperty(value = "群聊id") @ApiModelProperty(value = "群聊id")
private Long groupId; private Long groupId;
@Length(max=1024,message = "发送内容长度不得大于1024")
@Length(max=1024,message = "内容长度不得大于1024")
@NotEmpty(message="发送内容不可为空") @NotEmpty(message="发送内容不可为空")
@ApiModelProperty(value = "发送内容") @ApiModelProperty(value = "发送内容")
private String content; private String content;
@ -25,4 +26,8 @@ public class GroupMessageDTO {
@NotNull(message="消息类型不可为空") @NotNull(message="消息类型不可为空")
@ApiModelProperty(value = "消息类型") @ApiModelProperty(value = "消息类型")
private Integer type; private Integer type;
@Size(max = 20,message = "一次最多只能@20个小伙伴哦")
@ApiModelProperty(value = "被@用户列表")
private List<Long> atUserIds;
} }

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

@ -44,6 +44,17 @@ public class GroupMessage extends Model<GroupMessage> {
@TableField("send_id") @TableField("send_id")
private Long sendId; private Long sendId;
/**
* 发送用户昵称
*/
@TableField("send_nick_name")
private String sendNickName;
/**
* @用户列表
*/
@TableField("at_user_ids")
private String atUserIds;
/** /**
* 发送内容 * 发送内容
*/ */

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

@ -18,7 +18,6 @@ public interface IGroupMemberService extends IService<GroupMember> {
List<Long> findUserIdsByGroupId(Long groupId); List<Long> findUserIdsByGroupId(Long groupId);
boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members); boolean saveOrUpdateBatch(Long groupId,List<GroupMember> members);
void removeByGroupId(Long groupId); void removeByGroupId(Long groupId);

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

@ -1,5 +1,6 @@
package com.bx.implatform.service.impl; package com.bx.implatform.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@ -7,7 +8,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient; import com.bx.imclient.IMClient;
import com.bx.imcommon.contant.IMConstant; import com.bx.imcommon.contant.IMConstant;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.util.DateTimeUtils; import com.bx.implatform.util.DateTimeUtils;
import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.GroupMessageVO;
import com.bx.imcommon.model.IMGroupMessage; import com.bx.imcommon.model.IMGroupMessage;
@ -28,16 +28,13 @@ import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession; import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils; import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.dto.GroupMessageDTO; import com.bx.implatform.dto.GroupMessageDTO;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collections; import java.util.*;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -62,26 +59,33 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
public Long sendMessage(GroupMessageDTO dto) { public Long sendMessage(GroupMessageDTO dto) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Group group = groupService.getById(dto.getGroupId()); Group group = groupService.getById(dto.getGroupId());
if (group == null) { if (Objects.isNull(group)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊不存在"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊不存在");
} }
if (group.getDeleted()) { if (group.getDeleted()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊已解散"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊已解散");
} }
// 判断是否在群里 // 是否在群
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId()); GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId());
if (!userIds.contains(session.getUserId())) { if (Objects.isNull(member)||member.getQuit()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法发送消息"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法发送消息");
} }
// 群聊成员列表
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
// 不用发给自己
userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList());
// 保存消息 // 保存消息
GroupMessage msg = BeanUtils.copyProperties(dto, GroupMessage.class); GroupMessage msg = BeanUtils.copyProperties(dto, GroupMessage.class);
msg.setSendId(session.getUserId()); msg.setSendId(session.getUserId());
msg.setSendTime(new Date()); msg.setSendTime(new Date());
msg.setSendNickName(member.getAliasName());
if(CollectionUtil.isNotEmpty(dto.getAtUserIds())){
msg.setAtUserIds(StrUtil.join(",",dto.getAtUserIds()));
}
this.save(msg); this.save(msg);
// 不用发给自己
userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList());
// 群发 // 群发
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class); GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
msgInfo.setAtUserIds(dto.getAtUserIds());
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(userIds); sendMessage.setRecvIds(userIds);
@ -112,7 +116,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
} }
// 判断是否在群里 // 判断是否在群里
GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(), session.getUserId()); GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(), session.getUserId());
if (member == null) { if (Objects.isNull(member)||member.getQuit()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法撤回消息"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法撤回消息");
} }
// 修改数据库 // 修改数据库
@ -198,6 +202,9 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
List<GroupMember> members = groupMemberService.findByUserId(session.getUserId()); List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
List<Long> ids = members.stream().map(GroupMember::getGroupId).collect(Collectors.toList()); List<Long> ids = members.stream().map(GroupMember::getGroupId).collect(Collectors.toList());
if(CollectionUtil.isEmpty(ids)){
return Collections.EMPTY_LIST;
}
// 只能拉取最近1个月的 // 只能拉取最近1个月的
Date minDate = DateTimeUtils.addMonths(new Date(), -1); Date minDate = DateTimeUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
@ -210,7 +217,13 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
// 转成vo // 转成vo
List<GroupMessageVO> vos = messages.stream().map(m -> BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList()); List<GroupMessageVO> vos = messages.stream().map(m -> {
GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class);
// 被@用户列表
List<String> atIds = Arrays.asList(StrUtil.split(m.getAtUserIds(),","));
vo.setAtUserIds(atIds.stream().map(id->Long.parseLong(id)).collect(Collectors.toList()));
return vo;
}).collect(Collectors.toList());
// 消息状态,数据库没有存群聊的消息状态,需要从redis取 // 消息状态,数据库没有存群聊的消息状态,需要从redis取
List<String> keys = ids.stream() List<String> keys = ids.stream()
.map(id -> String.join(":", RedisKey.IM_GROUP_READED_POSITION, id.toString(), session.getUserId().toString())) .map(id -> String.join(":", RedisKey.IM_GROUP_READED_POSITION, id.toString(), session.getUserId().toString()))
@ -242,6 +255,16 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
@Override @Override
public void readedMessage(Long groupId) { public void readedMessage(Long groupId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 取出最后的消息id
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId)
.orderByDesc(GroupMessage::getId)
.last("limit 1")
.select(GroupMessage::getId);
GroupMessage message = this.getOne(wrapper);
if(Objects.isNull(message)){
return;
}
// 推送消息给自己的其他终端 // 推送消息给自己的其他终端
GroupMessageVO msgInfo = new GroupMessageVO(); GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.READED.code()); msgInfo.setType(MessageType.READED.code());
@ -254,14 +277,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
sendMessage.setData(msgInfo); sendMessage.setData(msgInfo);
sendMessage.setSendResult(false); sendMessage.setSendResult(false);
imClient.sendGroupMessage(sendMessage); imClient.sendGroupMessage(sendMessage);
// 记录已读消息位置
// 记录已读位置
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId)
.orderByDesc(GroupMessage::getId)
.last("limit 1")
.select(GroupMessage::getId);
GroupMessage message = this.getOne(wrapper);
String key = StrUtil.join(":",RedisKey.IM_GROUP_READED_POSITION,groupId,session.getUserId()); String key = StrUtil.join(":",RedisKey.IM_GROUP_READED_POSITION,groupId,session.getUserId());
redisTemplate.opsForValue().set(key, message.getId()); redisTemplate.opsForValue().set(key, message.getId());

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

@ -76,7 +76,6 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
groupMember.setUserId(user.getId()); groupMember.setUserId(user.getId());
groupMember.setAliasName(StringUtils.isEmpty(vo.getAliasName())?session.getNickName():vo.getAliasName()); groupMember.setAliasName(StringUtils.isEmpty(vo.getAliasName())?session.getNickName():vo.getAliasName());
groupMember.setRemark(StringUtils.isEmpty(vo.getRemark())?group.getName():vo.getRemark()); groupMember.setRemark(StringUtils.isEmpty(vo.getRemark())?group.getName():vo.getRemark());
groupMember.setHeadImage(user.getHeadImageThumb());
groupMemberService.save(groupMember); groupMemberService.save(groupMember);
vo.setId(group.getId()); vo.setId(group.getId());

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

@ -6,6 +6,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
import java.util.List;
@Data @Data
public class GroupMessageVO { public class GroupMessageVO {
@ -19,12 +20,18 @@ public class GroupMessageVO {
@ApiModelProperty(value = " 发送者id") @ApiModelProperty(value = " 发送者id")
private Long sendId; private Long sendId;
@ApiModelProperty(value = " 发送者昵称")
private String sendNickName;
@ApiModelProperty(value = "消息内容") @ApiModelProperty(value = "消息内容")
private String content; private String content;
@ApiModelProperty(value = "消息内容类型 具体枚举值由应用层定义") @ApiModelProperty(value = "消息内容类型 具体枚举值由应用层定义")
private Integer type; private Integer type;
@ApiModelProperty(value = "@用户列表")
private List<Long> atUserIds;
@ApiModelProperty(value = " 状态") @ApiModelProperty(value = " 状态")
private Integer status; private Integer status;

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

@ -29,11 +29,11 @@ mybatis-plus:
# *.xml的具体路径 # *.xml的具体路径
- classpath*:mapper/*.xml - classpath*:mapper/*.xml
minio: minio:
endpoint: http://127.0.0.1:9001 #内网地址 endpoint: http://42.194.187.243:9001 #内网地址
public: http://127.0.0.1:9001 #外网访问地址 public: http://42.194.187.243:9001 #外网访问地址
accessKey: admin accessKey: admin
secretKey: 12345678 secretKey: admin123456
bucketName: box-im bucketName: box-im2
imagePath: image imagePath: image
filePath: file filePath: file

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

@ -67,7 +67,9 @@ create table `im_group_message`(
`id` bigint not null auto_increment primary key comment 'id', `id` bigint not null auto_increment primary key comment 'id',
`group_id` bigint not null comment '群id', `group_id` bigint not null comment '群id',
`send_id` bigint not null comment '发送用户id', `send_id` bigint not null comment '发送用户id',
`send_nick_name` varchar(255) DEFAULT '' comment '发送用户昵称',
`content` text comment '发送内容', `content` text comment '发送内容',
`at_user_ids` varchar(1024) comment '被@的用户id列表,逗号分隔',
`type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 10:系统提示' , `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 10:系统提示' ,
`status` tinyint(1) DEFAULT 0 comment '状态 0:正常 2:撤回', `status` tinyint(1) DEFAULT 0 comment '状态 0:正常 2:撤回',
`send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间', `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间',

16
im-ui/src/api/emotion.js

@ -16,15 +16,25 @@ let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if(idx==-1){ if(idx==-1){
return ""; return emoText;
} }
let url = require(`@/assets/emoji/${idx}.gif`); let url = require(`@/assets/emoji/${idx}.gif`);
return `<img src="${url}" style="width:40px;height:40px;vertical-align:bottom;"/>` return `<img src="${url}" style="width:35px;height:35px;vertical-align:bottom;"/>`
} }
let textToUrl = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
if(idx==-1){
return "";
}
let url = require(`@/assets/emoji/${idx}.gif`);
return url;
}
export default { export default {
emoTextList, emoTextList,
transform, transform,
textToImg textToImg,
textToUrl
} }

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

@ -1,19 +1,11 @@
var websock = null; var websock = null;
let rec; //断线重连后,延迟5秒重新创建WebSocket连接 rec用来存储延迟请求的代码 let rec; //断线重连后,延迟5秒重新创建WebSocket连接 rec用来存储延迟请求的代码
let isConnect = false; //连接标识 避免重复连接 let isConnect = false; //连接标识 避免重复连接
let wsurl = "";
let accessToken = "";
let messageCallBack = null; let messageCallBack = null;
let openCallBack = null;
let closeCallBack = null let closeCallBack = null
let init = (url,token) => { let connect = (wsurl,accessToken) => {
wsurl = url;
accessToken = token;
};
let connect = () => {
try { try {
if (isConnect) { if (isConnect) {
return; return;
@ -25,12 +17,9 @@ let connect = () => {
if (sendInfo.cmd == 0) { if (sendInfo.cmd == 0) {
heartCheck.start() heartCheck.start()
console.log('WebSocket登录成功') console.log('WebSocket登录成功')
// 登录成功才算连接完成
openCallBack && openCallBack();
} else if (sendInfo.cmd == 1) { } else if (sendInfo.cmd == 1) {
// 重新开启心跳定时 // 重新开启心跳定时
heartCheck.reset(); heartCheck.reset();
console.log("")
} else { } else {
// 其他消息转发出去 // 其他消息转发出去
console.log("收到消息:",sendInfo); console.log("收到消息:",sendInfo);
@ -59,16 +48,16 @@ let connect = () => {
websock.onerror = function() { websock.onerror = function() {
console.log('WebSocket连接发生错误') console.log('WebSocket连接发生错误')
isConnect = false; //连接断开修改标识 isConnect = false; //连接断开修改标识
reConnect(); reconnect(wsurl,accessToken);
} }
} catch (e) { } catch (e) {
console.log("尝试创建连接失败"); console.log("尝试创建连接失败");
reConnect(); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接 reconnect(wsurl,accessToken); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接
} }
}; };
//定义重连函数 //定义重连函数
let reConnect = () => { let reconnect = (wsurl,accessToken) => {
console.log("尝试重新连接"); console.log("尝试重新连接");
if (isConnect){ if (isConnect){
//如果已经连上就不在重连了 //如果已经连上就不在重连了
@ -76,12 +65,12 @@ let reConnect = () => {
} }
rec && clearTimeout(rec); rec && clearTimeout(rec);
rec = setTimeout(function() { // 延迟5秒重连 避免过多次过频繁请求重连 rec = setTimeout(function() { // 延迟5秒重连 避免过多次过频繁请求重连
connect(); connect(wsurl,accessToken);
}, 5000); }, 15000);
}; };
//设置关闭连接 //设置关闭连接
let close = () => { let close = (code) => {
websock && websock.close(); websock && websock.close(code);
}; };
@ -136,20 +125,15 @@ let onMessage = (callback) => {
} }
let onOpen = (callback) => {
openCallBack = callback;
}
let onClose = (callback) => { let onClose = (callback) => {
closeCallBack = callback; closeCallBack = callback;
} }
// 将方法暴露出去 // 将方法暴露出去
export { export {
init,
connect, connect,
reconnect,
close, close,
sendMessage, sendMessage,
onOpen,
onMessage, onMessage,
onClose onClose
} }

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

@ -0,0 +1,175 @@
<template>
<el-scrollbar v-show="show" ref="scrollBox" class="group-member-choose"
:style="{'left':pos.x+'px','top':pos.y-300+'px'}">
<div v-for="(member,idx) in showMembers" :key="member.id">
<div class="member-item" :class="idx==activeIdx?'active':''" @click="onSelectMember(member)">
<div class="member-avatar">
<head-image :size="25" :name="member.aliasName" :url="member.headImage"> </head-image>
</div>
<div class="member-name">
<div>{{member.aliasName}}</div>
</div>
</div>
</div>
</el-scrollbar>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
export default {
name: "chatAtBox",
components: {
HeadImage
},
props: {
searchText: {
type: String,
default: ""
},
ownerId: {
type: Number,
},
members: {
type: Array
}
},
data() {
return {
show: false,
pos: {
x: 0,
y: 0
},
activeIdx: 0,
showMembers: []
};
},
methods: {
init() {
this.$refs.scrollBox.wrap.scrollTop = 0;
this.showMembers = [];
let userId = this.$store.state.userStore.userInfo.id;
let name = "全体成员";
if (this.ownerId == userId && name.startsWith(this.searchText)) {
this.showMembers.push({
userId: -1,
aliasName: name
})
}
this.members.forEach((m) => {
if (m.userId != userId && m.aliasName.startsWith(this.searchText)) {
this.showMembers.push(m);
}
})
this.activeIdx = this.showMembers.length > 0 ? 0: -1;
},
open(pos) {
this.show = true;
this.pos = pos;
this.init();
},
close() {
this.show = false;
},
moveUp() {
if (this.activeIdx > 0) {
this.activeIdx--;
this.scrollToActive()
}
},
moveDown() {
if (this.activeIdx < this.showMembers.length - 1) {
this.activeIdx++;
this.scrollToActive()
}
},
select() {
if (this.activeIdx >= 0) {
this.onSelectMember(this.showMembers[this.activeIdx])
}
this.close();
},
scrollToActive() {
if (this.activeIdx * 35 - this.$refs.scrollBox.wrap.clientHeight > this.$refs.scrollBox.wrap.scrollTop) {
this.$refs.scrollBox.wrap.scrollTop += 140;
if (this.$refs.scrollBox.wrap.scrollTop > this.$refs.scrollBox.wrap.scrollHeight) {
this.$refs.scrollBox.wrap.scrollTop = this.$refs.scrollBox.wrap.scrollHeight
}
}
if (this.activeIdx * 35 < this.$refs.scrollBox.wrap.scrollTop) {
this.$refs.scrollBox.wrap.scrollTop -= 140;
if (this.$refs.scrollBox.wrap.scrollTop < 0) {
this.$refs.scrollBox.wrap.scrollTop = 0;
}
}
},
onSelectMember(member) {
this.$emit("select", member);
this.show = false;
}
},
computed: {
isOwner() {
return this.$store.state.userStore.userInfo.id == this.ownerId;
}
},
watch: {
searchText: {
handler(newText, oldText) {
this.init();
}
}
}
}
</script>
<style scoped lang="scss">
.group-member-choose {
position: fixed;
width: 200px;
height: 300px;
border: 1px solid #b4b4b4;
border-radius: 5px;
background-color: #f5f5f5;
box-shadow: 0px 0px 10px #ccc;
.member-item {
display: flex;
height: 35px;
margin-bottom: 1px;
position: relative;
padding: 0 5px;
align-items: center;
background-color: #fafafa;
white-space: nowrap;
box-sizing: border-box;
&:hover {
background-color: #eeeeee;
}
&.active {
background-color: #eeeeee;
}
.member-avatar {
width: 25px;
height: 25px;
}
.member-name {
padding-left: 10px;
height: 100%;
text-align: left;
line-height: 40px;
white-space: nowrap;
overflow: hidden;
font-size: 14px;
font-weight: 600;
}
}
}
</style>

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

@ -1,5 +1,6 @@
<template> <template>
<el-container class="chat-box"> <div class="chat-box" @click="closeRefBox()" @mousemove="readedMessage()">
<el-container>
<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"
@ -8,13 +9,13 @@
<el-main style="padding: 0;"> <el-main style="padding: 0;">
<el-container> <el-container>
<el-container class="content-box"> <el-container class="content-box">
<el-main class="im-chat-main" id="chatScrollBox" @scroll="handleScroll"> <el-main class="im-chat-main" id="chatScrollBox" @scroll="onScroll">
<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">
<chat-message-item v-show="idx>=showMinIdx" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" <chat-message-item v-show="idx>=showMinIdx" :mine="msgInfo.sendId == mine.id"
:showName="showName(msgInfo)" :msgInfo="msgInfo" @delete="deleteMessage" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
@recall="recallMessage"> :msgInfo="msgInfo" @delete="deleteMessage" @recall="recallMessage">
</chat-message-item> </chat-message-item>
</li> </li>
</ul> </ul>
@ -23,18 +24,18 @@
<el-footer height="240px" class="im-chat-footer"> <el-footer height="240px" class="im-chat-footer">
<div class="chat-tool-bar"> <div class="chat-tool-bar">
<div title="表情" class="icon iconfont icon-biaoqing" ref="emotion" <div title="表情" class="icon iconfont icon-biaoqing" ref="emotion"
@click="switchEmotionBox()"> @click.stop="showEmotionBox()">
</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="onImageBefore" @success="onImageSuccess" @fail="onImageFail">
<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="onFileBefore"
@success="handleFileSuccess" @fail="handleFileFail"> @success="onFileSuccess" @fail="onFileFail">
<i class="el-icon-wallet"></i> <i class="el-icon-wallet"></i>
</file-upload> </file-upload>
</div> </div>
@ -46,9 +47,13 @@
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div> <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div> </div>
<div class="send-content-area"> <div class="send-content-area">
<textarea v-show="!sendImageUrl" v-model="sendText" ref="sendBox" class="send-text-area" <div contenteditable="true" v-show="!sendImageUrl" ref="editBox" class="send-text-area"
:disabled="lockMessage" @keydown.enter="sendTextMessage()" @paste="handlePaste" :disabled="lockMessage" @paste.prevent="onEditorPaste"
placeholder="温馨提示:可以粘贴截图到这里了哦~"></textarea> @compositionstart="onEditorCompositionStart"
@compositionend="onEditorCompositionEnd" @input="onEditorInput"
placeholder="温馨提示:可以粘贴截图到这里了哦~" @blur="onEditBoxBlur()" @keydown.down="onKeyDown"
@keydown.up="onKeyUp" @keydown.enter.prevent="onKeyEnter">
</div>
<div v-show="sendImageUrl" class="send-image-area"> <div v-show="sendImageUrl" class="send-image-area">
<div class="send-image-box"> <div class="send-image-box">
@ -58,7 +63,7 @@
</div> </div>
</div> </div>
<div class="send-btn-area"> <div class="send-btn-area">
<el-button type="primary" size="small" @click="handleSendMessage()">发送</el-button> <el-button type="primary" size="small" @click="sendMessage()">发送</el-button>
</div> </div>
</div> </div>
</el-footer> </el-footer>
@ -69,11 +74,14 @@
</el-aside> </el-aside>
</el-container> </el-container>
</el-main> </el-main>
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion> <emotion ref="emoBox" @emotion="onEmotion"></Emotion>
<chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice> <chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers" :search-text="atSearchText"
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers" @select="onAtSelect"></chat-at-box>
@close="closeHistoryBox"></chat-history> <chat-voice :visible="showVoice" @close="closeVoiceBox" @send="onSendVoice"></chat-voice>
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group"
:groupMembers="groupMembers" @close="closeHistoryBox"></chat-history>
</el-container> </el-container>
</div>
</template> </template>
<script> <script>
@ -83,6 +91,7 @@
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";
import ChatAtBox from "./ChatAtBox.vue"
export default { export default {
name: "chatPrivate", name: "chatPrivate",
@ -92,7 +101,8 @@
ChatGroupSide, ChatGroupSide,
Emotion, Emotion,
ChatVoice, ChatVoice,
ChatHistory ChatHistory,
ChatAtBox
}, },
props: { props: {
chat: { chat: {
@ -104,28 +114,141 @@
friend: {}, friend: {},
group: {}, group: {},
groupMembers: [], groupMembers: [],
sendText: "",
sendImageUrl: "", sendImageUrl: "",
sendImageFile: "", sendImageFile: "",
showVoice: false, // showVoice: false, //
showSide: false, // showSide: false, //
showEmotion: false, // emoji
emoBoxPos: { // emoji
x: 0,
y: 0
},
showHistory: false, // showHistory: false, //
lockMessage: false, // lockMessage: false, //
showMinIdx: 0 // showMinIdx showMinIdx: 0, // showMinIdx
atSearchText: "",
focusNode: null, //
focusOffset: null, //
zhLock: false //
} }
}, },
methods: { methods: {
handlePaste(e) { closeRefBox() {
this.$refs.emoBox.close();
this.$refs.atBox.close();
},
onKeyDown() {
if (this.$refs.atBox.show) {
this.$refs.atBox.moveDown()
}
},
onKeyUp() {
if (this.$refs.atBox.show) {
this.$refs.atBox.moveUp()
}
},
onKeyEnter() {
if (this.$refs.atBox.show) {
// ,
this.focusOffset += this.atSearchText.length;
this.$refs.atBox.select();
} else {
this.sendMessage();
}
},
onEditBoxBlur() {
let selection = window.getSelection()
// (emoji)
this.focusNode = selection.focusNode;
this.focusOffset = selection.focusOffset;
},
onEditorInput(e) {
// @
if (this.chat.type == "GROUP" && !this.zhLock) {
if (e.data == '@') {
//
this.showAtBox(e);
} else {
let selection = window.getSelection()
let range = selection.getRangeAt(0)
this.focusNode = selection.focusNode;
// @
let stIdx = this.focusNode.textContent.lastIndexOf('@');
this.atSearchText = this.focusNode.textContent.substring(stIdx + 1);
}
}
},
onEditorCompositionStart() {
this.zhLock = true;
},
onEditorCompositionEnd(e) {
this.zhLock = false;
this.onEditorInput(e);
},
showAtBox(e) {
this.atSearchText = "";
let selection = window.getSelection()
let range = selection.getRangeAt(0)
//
this.focusNode = selection.focusNode;
this.focusOffset = selection.focusOffset;
//
let pos = range.getBoundingClientRect();
this.$refs.atBox.open({
x: pos.x,
y: pos.y
})
},
onAtSelect(member) {
let range = window.getSelection().getRangeAt(0)
// @xx
range.setStart(this.focusNode, this.focusOffset - 1 - this.atSearchText.length)
range.setEnd(this.focusNode, this.focusOffset)
range.deleteContents()
//
let element = document.createElement('SPAN')
element.className = "at"
element.dataset.id = member.userId;
element.contentEditable = 'false'
element.innerText = `@${member.aliasName}`
range.insertNode(element)
//
range.collapse()
//
let textNode = document.createTextNode('\u00A0');
range.insertNode(textNode)
range.collapse()
this.atSearchText = "";
this.$refs.editBox.focus()
},
createSendText() {
let sendText = ""
this.$refs.editBox.childNodes.forEach((node) => {
if (node.nodeName == "#text") {
sendText += node.textContent;
} else if (node.nodeName == "SPAN") {
sendText += node.innerText;
} else if (node.nodeName == "IMG") {
sendText += node.dataset.code;
}
})
return sendText;
},
createAtUserIds() {
let ids = [];
this.$refs.editBox.childNodes.forEach((node) => {
if (node.nodeName == "SPAN") {
ids.push(node.dataset.id);
}
})
return ids;
},
onEditorPaste(e) {
let txt = event.clipboardData.getData('Text') let txt = event.clipboardData.getData('Text')
if (typeof(txt) == 'string') { if (typeof(txt) == 'string') {
this.sendText += txt let range = window.getSelection().getRangeAt(0)
let textNode = document.createTextNode(txt);
range.insertNode(textNode)
range.collapse();
} }
const items = (event.clipboardData || window.clipboardData).items let items = (event.clipboardData || window.clipboardData).items
if (items.length) { if (items.length) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) { if (items[i].type.indexOf('image') !== -1) {
@ -140,7 +263,7 @@
this.sendImageUrl = ""; this.sendImageUrl = "";
this.sendImageFile = null; this.sendImageFile = null;
}, },
handleImageSuccess(data, file) { onImageSuccess(data, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo));
msgInfo.content = JSON.stringify(data); msgInfo.content = JSON.stringify(data);
this.$http({ this.$http({
@ -153,12 +276,12 @@
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}) })
}, },
handleImageFail(e, file) { onImageFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo));
msgInfo.loadStatus = 'fail'; msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}, },
handleImageBefore(file) { onImageBefore(file) {
let url = URL.createObjectURL(file); let url = URL.createObjectURL(file);
let data = { let data = {
originUrl: url, originUrl: url,
@ -184,7 +307,7 @@
// file // file
file.msgInfo = msgInfo; file.msgInfo = msgInfo;
}, },
handleFileSuccess(url, file) { onFileSuccess(url, file) {
let data = { let data = {
name: file.name, name: file.name,
size: file.size, size: file.size,
@ -202,13 +325,12 @@
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}) })
}, },
handleFileFail(e, file) { onFileFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo));
msgInfo.loadStatus = 'fail'; msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}, },
handleFileBefore(file) { onFileBefore(file) {
let url = URL.createObjectURL(file); let url = URL.createObjectURL(file);
let data = { let data = {
name: file.name, name: file.name,
@ -234,14 +356,14 @@
// file // file
file.msgInfo = msgInfo; file.msgInfo = msgInfo;
}, },
handleCloseSide() { onCloseSide() {
this.showSide = false; this.showSide = false;
}, },
handleScrollToTop() { onScrollToTop() {
// 10 // 10
this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0; this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
}, },
handleScroll(e) { onScroll(e) {
let scrollElement = e.target let scrollElement = e.target
let scrollTop = scrollElement.scrollTop let scrollTop = scrollElement.scrollTop
if (scrollTop < 30) { // , if (scrollTop < 30) { // ,
@ -249,19 +371,32 @@
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0; this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
} }
}, },
switchEmotionBox() { showEmotionBox() {
this.showEmotion = !this.showEmotion;
let width = this.$refs.emotion.offsetWidth; let width = this.$refs.emotion.offsetWidth;
let left = this.$elm.fixLeft(this.$refs.emotion); let left = this.$elm.fixLeft(this.$refs.emotion);
let top = this.$elm.fixTop(this.$refs.emotion); let top = this.$elm.fixTop(this.$refs.emotion);
this.emoBoxPos.y = top; this.$refs.emoBox.open({
this.emoBoxPos.x = left + width / 2; x: left + width / 2,
y: top
})
}, },
handleEmotion(emoText) { onEmotion(emoText) {
this.sendText += emoText;
this.showEmotion = false;
// //
this.$refs.sendBox.focus(); this.$refs.editBox.focus();
let range = window.getSelection().getRangeAt(0);
//
range.setStart(this.focusNode, this.focusOffset)
let element = document.createElement('IMG')
element.className = "emo"
element.dataset.code = emoText;
element.contentEditable = 'true'
element.setAttribute("src", this.$emo.textToUrl(emoText));
//
range.insertNode(element)
//
range.collapse()
}, },
showVoiceBox() { showVoiceBox() {
this.showVoice = true; this.showVoice = true;
@ -281,7 +416,7 @@
closeHistoryBox() { closeHistoryBox() {
this.showHistory = false; this.showHistory = false;
}, },
handleSendVoice(data) { onSendVoice(data) {
let msgInfo = { let msgInfo = {
content: JSON.stringify(data), content: JSON.stringify(data),
type: 3 type: 3
@ -300,7 +435,7 @@
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND; msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
// //
this.$refs.sendBox.focus(); this.$refs.editBox.focus();
// //
this.scrollToBottom(); this.scrollToBottom();
// //
@ -314,16 +449,18 @@
msgInfo.recvId = targetId; msgInfo.recvId = targetId;
} }
}, },
handleSendMessage() { sendMessage() {
if (this.sendImageFile) { if (this.sendImageFile) {
this.sendImageMessage(); this.sendImageMessage();
} else { } else {
this.sendTextMessage(); this.sendTextMessage();
} }
//
this.readedMessage()
}, },
sendImageMessage() { sendImageMessage() {
let file = this.sendImageFile; let file = this.sendImageFile;
this.handleImageBefore(this.sendImageFile); this.onImageBefore(this.sendImageFile);
let formData = new FormData() let formData = new FormData()
formData.append('file', file.raw || file) formData.append('file', file.raw || file)
this.$http.post("/image/upload", formData, { this.$http.post("/image/upload", formData, {
@ -331,32 +468,37 @@
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} }
}).then((data) => { }).then((data) => {
this.handleImageSuccess(data, file); this.onImageSuccess(data, file);
}).catch((res) => { }).catch((res) => {
this.handleImageSuccess(res, file); this.onImageSuccess(res, file);
}) })
this.sendImageFile = null; this.sendImageFile = null;
this.sendImageUrl = ""; this.sendImageUrl = "";
this.$nextTick(() => this.$refs.sendBox.focus()); this.$nextTick(() => this.$refs.editBox.focus());
this.scrollToBottom(); this.scrollToBottom();
}, },
sendTextMessage() { sendTextMessage() {
if (!this.sendText.trim()) { let sendText = this.createSendText();
if (!sendText.trim()) {
return return
} }
this.$refs.editBox.cle
let msgInfo = { let msgInfo = {
content: this.sendText, content: sendText,
type: 0 type: 0
} }
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
// @
if (this.chat.type == "GROUP") {
msgInfo.atUserIds = this.createAtUserIds();
}
this.lockMessage = true; this.lockMessage = true;
this.$http({ this.$http({
url: this.messageAction, url: this.messageAction,
method: 'post', method: 'post',
data: msgInfo data: msgInfo
}).then((id) => { }).then((id) => {
this.sendText = "";
msgInfo.id = id; 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;
@ -366,17 +508,10 @@
}).finally(() => { }).finally(() => {
// //
this.lockMessage = false; this.lockMessage = false;
//
this.$nextTick(() => this.$refs.sendBox.focus());
//
this.scrollToBottom(); this.scrollToBottom();
this.resetEditor();
}); });
const e = window.event || arguments[0];
if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === 13) {
e.returnValue = false;
e.preventDefault();
return false;
}
}, },
deleteMessage(msgInfo) { deleteMessage(msgInfo) {
this.$confirm('确认删除消息?', '删除消息', { this.$confirm('确认删除消息?', '删除消息', {
@ -408,6 +543,9 @@
}); });
}, },
readedMessage() { readedMessage() {
if(this.chat.unreadCount==0){
return;
}
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
var url = `/message/group/readed?groupId=${this.chat.targetId}` var url = `/message/group/readed?groupId=${this.chat.targetId}`
} else { } else {
@ -418,7 +556,6 @@
method: 'put' method: 'put'
}).then(() => { }).then(() => {
this.$store.commit("resetUnreadCount", this.chat) this.$store.commit("resetUnreadCount", this.chat)
this.scrollToBottom();
}) })
}, },
loadGroup(groupId) { loadGroup(groupId) {
@ -467,6 +604,14 @@
return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
} }
}, },
resetEditor() {
this.sendImageUrl = "";
this.sendImageFile = null;
this.$nextTick(() => {
this.$refs.editBox.innerHTML = "";
this.$refs.editBox.focus();
});
},
scrollToBottom() { scrollToBottom() {
this.$nextTick(() => { this.$nextTick(() => {
let div = document.getElementById("chatScrollBox"); let div = document.getElementById("chatScrollBox");
@ -511,16 +656,14 @@
} }
// //
this.scrollToBottom(); this.scrollToBottom();
this.sendText = ""; this.showSide = false;
// //
this.readedMessage() this.readedMessage()
// 30 // 30
let size = this.chat.messages.length; let size = this.chat.messages.length;
this.showMinIdx = size > 30 ? size - 30 : 0; this.showMinIdx = size > 30 ? size - 30 : 0;
// //
this.$nextTick(() => { this.resetEditor();
this.$refs.sendBox.focus();
})
} }
}, },
immediate: true immediate: true
@ -528,24 +671,25 @@
unreadCount: { unreadCount: {
handler(newCount, oldCount) { handler(newCount, oldCount) {
if (newCount > 0) { if (newCount > 0) {
// //
this.readedMessage() this.scrollToBottom();
} }
} }
} }
}, },
mounted() { mounted() {
let div = document.getElementById("chatScrollBox"); let div = document.getElementById("chatScrollBox");
div.addEventListener('scroll', this.handleScroll) div.addEventListener('scroll', this.onScroll)
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.chat-box { .chat-box {
background: white; position: relative;
width: 100%;
background: #f8f8f8;
border: #dddddd solid 1px; border: #dddddd solid 1px;
.el-header { .el-header {
padding: 5px; padding: 5px;
background-color: white; background-color: white;
@ -573,7 +717,7 @@
.im-chat-box { .im-chat-box {
>ul { >ul {
padding: 20px; padding: 0 20px;
li { li {
list-style-type: none; list-style-type: none;
@ -612,11 +756,12 @@
} }
.send-content-area { .send-content-area {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
background-color: #f8f8f8 !important; background-color: white !important;
outline-color: rgba(83, 160, 231, 0.61);
.send-text-area { .send-text-area {
box-sizing: border-box; box-sizing: border-box;
@ -626,20 +771,32 @@
resize: none; resize: none;
font-size: 16px; font-size: 16px;
color: black; color: black;
background-color: #f8f8f8 !important;
outline-color: rgba(83, 160, 231, 0.61); outline-color: rgba(83, 160, 231, 0.61);
text-align: left;
line-height: 30 px;
.at {
color: blue;
font-weight: 600;
}
.emo {
width: 30px;
height: 30px;
vertical-align: bottom;
}
} }
.send-image-area { .send-image-area {
text-align: left; text-align: left;
border: #53a0e7 solid 1px;
.send-image-box { .send-image-box {
position: relative; position: relative;
display: inline-block; display: inline-block;
.send-image { .send-image {
max-height: 190px; max-height: 180px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 2%; border-radius: 2%;
margin: 2px; margin: 2px;

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

@ -39,9 +39,9 @@
</el-form-item> </el-form-item>
<div class="btn-group"> <div class="btn-group">
<el-button v-show="editing" type="success" @click="handleSaveGroup()">提交</el-button> <el-button v-show="editing" type="success" @click="onSaveGroup()">提交</el-button>
<el-button v-show="!editing" type="primary" @click="editing=!editing">编辑</el-button> <el-button v-show="!editing" type="primary" @click="editing=!editing">编辑</el-button>
<el-button type="danger" v-show="!isOwner" @click="handleQuit()">退出群聊</el-button> <el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
</div> </div>
</el-form> </el-form>
</el-scrollbar> </el-scrollbar>
@ -75,7 +75,7 @@
} }
}, },
methods: { methods: {
handleClose() { onClose() {
this.$emit('close'); this.$emit('close');
}, },
loadGroupMembers() { loadGroupMembers() {
@ -86,7 +86,7 @@
this.groupMembers = members; this.groupMembers = members;
}) })
}, },
handleSaveGroup() { onSaveGroup() {
let vo = this.group; let vo = this.group;
this.$http({ this.$http({
url: "/group/modify", url: "/group/modify",
@ -98,7 +98,7 @@
this.$message.success("修改成功"); this.$message.success("修改成功");
}) })
}, },
handleQuit() { onQuit() {
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', { this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',

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

@ -1,5 +1,5 @@
<template> <template>
<el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="handleClose"> <el-drawer title="聊天历史记录" size="700px" :visible.sync="visible" direction="rtl" :before-close="onClose">
<div class="chat-history" v-loading="loading" <div class="chat-history" v-loading="loading"
element-loading-text="拼命加载中"> element-loading-text="拼命加载中">
<el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar" > <el-scrollbar class="chat-history-scrollbar" ref="scrollbar" id="historyScrollbar" >
@ -51,13 +51,13 @@
} }
}, },
methods: { methods: {
handleClose() { onClose() {
this.page = 1; this.page = 1;
this.messages = []; this.messages = [];
this.loadAll = false; this.loadAll = false;
this.$emit('close'); this.$emit('close');
}, },
handleScroll() { onScroll() {
let high = this.$refs.scrollbar.$refs.wrap.scrollTop; // let high = this.$refs.scrollbar.$refs.wrap.scrollTop; //
let timeDiff = new Date().getTime() - this.lastScrollTime.getTime(); let timeDiff = new Date().getTime() - this.lastScrollTime.getTime();
if ( high < 30 && timeDiff>500) { if ( high < 30 && timeDiff>500) {
@ -139,7 +139,7 @@
if (newValue) { if (newValue) {
this.loadMessages(); this.loadMessages();
this.$nextTick(() => { this.$nextTick(() => {
document.getElementById('historyScrollbar').addEventListener("mousewheel", this.handleScroll,true); document.getElementById('historyScrollbar').addEventListener("mousewheel", this.onScroll,true);
}); });
} }
} }

76
im-ui/src/components/chat/ChatItem.vue

@ -1,20 +1,23 @@
<template> <template>
<div class="chat-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)"> <div class="chat-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="chat-left"> <div class="chat-left">
<head-image :url="chat.headImage" :name="chat.showName" :size="50" :id="chat.type=='PRIVATE'?chat.targetId:0"></head-image> <head-image :url="chat.headImage" :name="chat.showName" :size="45"
:id="chat.type=='PRIVATE'?chat.targetId:0"></head-image>
<div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div> <div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div>
</div> </div>
<div class="chat-right"> <div class="chat-right">
<div class="chat-name"> <div class="chat-name">
{{ chat.showName}} <div class="chat-name-text">{{chat.showName}}</div>
<div class="chat-time-text">{{showTime}}</div>
</div> </div>
<div class="chat-content"> <div class="chat-content">
<div class="chat-at-text">{{atText}}</div>
<div class="chat-send-name" v-show="chat.sendNickName">{{chat.sendNickName+':&nbsp;'}}</div>
<div class="chat-content-text" v-html="$emo.transform(chat.lastContent)"></div> <div class="chat-content-text" v-html="$emo.transform(chat.lastContent)"></div>
<div class="chat-time">{{showTime}}</div>
</div> </div>
</div> </div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" <right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" @close="rightMenu.show=false"
@close="rightMenu.show=false" @select="handleSelectMenu"></right-menu> @select="onSelectMenu"></right-menu>
</div> </div>
</template> </template>
@ -68,13 +71,21 @@
}; };
this.rightMenu.show = "true"; this.rightMenu.show = "true";
}, },
handleSelectMenu(item) { onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo); this.$emit(item.key.toLowerCase(), this.msgInfo);
} }
}, },
computed: { computed: {
showTime() { showTime() {
return this.$date.toTimeText(this.chat.lastSendTime, true) return this.$date.toTimeText(this.chat.lastSendTime, true)
},
atText() {
if (this.chat.atMe) {
return "[有人@我]"
} else if (this.chat.atAll) {
return "[@全体成员]"
}
return "";
} }
} }
} }
@ -82,13 +93,13 @@
<style lang="scss"> <style lang="scss">
.chat-item { .chat-item {
height: 65px; height: 50px;
display: flex; display: flex;
margin-bottom: 1px; margin-bottom: 1px;
position: relative; position: relative;
padding: 5px;
padding-left: 10px; padding-left: 10px;
align-items: center; align-items: center;
padding-right: 5px;
background-color: #fafafa; background-color: #fafafa;
white-space: nowrap; white-space: nowrap;
color: black; color: black;
@ -105,8 +116,8 @@
.chat-left { .chat-left {
position: relative; position: relative;
display: flex; display: flex;
width: 50px; width: 45px;
height: 50px; height: 45x;
.unread-text { .unread-text {
position: absolute; position: absolute;
@ -131,36 +142,57 @@
padding-left: 10px; padding-left: 10px;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
.chat-name { .chat-name {
display: flex;
line-height: 25px;
height: 25px;
.chat-name-text {
flex: 1;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
line-height: 30px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.chat-time-text{
font-size: 13px;
text-align: right;
color: #888888;
white-space: nowrap;
overflow: hidden;
padding-left: 10px;
}
}
.chat-content { .chat-content {
display: flex; display: flex;
line-height: 30px; line-height: 22px;
.chat-at-text {
color: #c70b0b;
font-size: 12px;
}
.chat-send-name{
font-size: 13px;
}
.chat-content-text { .chat-content-text {
flex: 1; flex: 1;
font-size: 14px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 13px;
img { img {
width: 30px !important; width: 20px !important;
height: 30px !important; height: 20px !important;
vertical-align: bottom;
} }
} }
.chat-time {
font-size: 13px;
text-align: right;
color: #888888;
white-space: nowrap;
overflow: hidden;
}
} }
} }
} }

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

@ -26,7 +26,7 @@
<img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" <img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl"
@click="showFullImageBox()" /> @click="showFullImageBox()" />
</div> </div>
<span title="发送失败" v-show="loadFail" @click="handleSendFail" <span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span> class="send-fail el-icon-warning"></span>
</div> </div>
<div class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE"> <div class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE">
@ -40,11 +40,11 @@
<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" <span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span> class="send-fail el-icon-warning"></span>
</div> </div>
<div class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" <div class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO"
@click="handlePlayVoice()"> @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> <audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div> </div>
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId <span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
@ -56,7 +56,7 @@
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@close="rightMenu.show=false" @select="handleSelectMenu"></right-menu> @close="rightMenu.show=false" @select="onSelectMenu"></right-menu>
</div> </div>
</template> </template>
@ -110,7 +110,7 @@
}, },
methods: { methods: {
handleSendFail() { onSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送") this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
}, },
showFullImageBox() { showFullImageBox() {
@ -119,13 +119,13 @@
this.$store.commit('showFullImageBox', imageUrl); this.$store.commit('showFullImageBox', imageUrl);
} }
}, },
handlePlayVoice() { onPlayVoice() {
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.onPlayVoice = 'RUNNING';
}, },
showRightMenu(e) { showRightMenu(e) {
this.rightMenu.pos = { this.rightMenu.pos = {
@ -134,7 +134,7 @@
}; };
this.rightMenu.show = "true"; this.rightMenu.show = "true";
}, },
handleSelectMenu(item) { onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo); this.$emit(item.key.toLowerCase(), this.msgInfo);
} }
}, },
@ -233,7 +233,7 @@
line-height: 30px; line-height: 30px;
margin-top: 3px; margin-top: 3px;
padding: 7px; padding: 7px;
background-color: rgb(235, 235, 245); background-color: white;
border-radius: 10px; border-radius: 10px;
color: black; color: black;
display: block; display: block;
@ -241,7 +241,7 @@
text-align: left; text-align: left;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
box-shadow: 2px 2px 2px #c0c0f0; box-shadow: 1px 1px 1px #c0c0f0;
&:after { &:after {
content: ""; content: "";
@ -251,7 +251,7 @@
width: 0; width: 0;
height: 0; height: 0;
border-style: solid dashed dashed; border-style: solid dashed dashed;
border-color: rgb(235, 235, 245) transparent transparent; border-color: white transparent transparent;
overflow: hidden; overflow: hidden;
border-width: 10px; border-width: 10px;
} }
@ -383,7 +383,7 @@
background-color: rgb(88, 127, 240); background-color: rgb(88, 127, 240);
color: #fff; color: #fff;
vertical-align: top; vertical-align: top;
box-shadow: 2px 2px 1px #ccc; box-shadow: 1px 1px 1px #ccc;
&:after { &:after {
left: auto; left: auto;

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

@ -1,23 +1,23 @@
<template> <template>
<el-dialog class="chat-voice" title="语音录制" :visible.sync="visible" width="600px" :before-close="handleClose"> <el-dialog class="chat-voice" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose">
<div v-show="mode=='RECORD'"> <div v-show="mode=='RECORD'">
<div class="chat-voice-tip">{{stateTip}}</div> <div class="chat-voice-tip">{{stateTip}}</div>
<div>时长: {{state=='STOP'?0:parseInt(rc.duration)}}s</div> <div>时长: {{state=='STOP'?0:parseInt(rc.duration)}}s</div>
</div> </div>
<audio v-show="mode=='PLAY'" :src="url" controls ref="audio" @ended="handleStopAudio()"></audio> <audio v-show="mode=='PLAY'" :src="url" controls ref="audio" @ended="onStopAudio()"></audio>
<el-divider content-position="center"></el-divider> <el-divider content-position="center"></el-divider>
<el-row class="chat-voice-btn-group"> <el-row class="chat-voice-btn-group">
<el-button round type="primary" v-show="state=='STOP'" @click="handleStartRecord()">开始录音</el-button> <el-button round type="primary" v-show="state=='STOP'" @click="onStartRecord()">开始录音</el-button>
<el-button round type="warning" v-show="state=='RUNNING'" @click="handlePauseRecord()">暂停录音</el-button> <el-button round type="warning" v-show="state=='RUNNING'" @click="onPauseRecord()">暂停录音</el-button>
<el-button round type="primary" v-show="state=='PAUSE'" @click="handleResumeRecord()">继续录音</el-button> <el-button round type="primary" v-show="state=='PAUSE'" @click="onResumeRecord()">继续录音</el-button>
<el-button round type="danger" v-show="state=='RUNNING'||state=='PAUSE'" @click="handleCompleteRecord()"> <el-button round type="danger" v-show="state=='RUNNING'||state=='PAUSE'" @click="onCompleteRecord()">
结束录音</el-button> 结束录音</el-button>
<el-button round type="success" v-show="state=='COMPLETE' && mode!='PLAY'" @click="handlePlayAudio()">播放录音 <el-button round type="success" v-show="state=='COMPLETE' && mode!='PLAY'" @click="onPlayAudio()">播放录音
</el-button> </el-button>
<el-button round type="warning" v-show="state=='COMPLETE' && mode=='PLAY'" @click="handleStopAudio()">停止播放 <el-button round type="warning" v-show="state=='COMPLETE' && mode=='PLAY'" @click="onStopAudio()">停止播放
</el-button> </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="onRestartRecord()">重新录音</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="onSendRecord()">立即发送</el-button>
</el-row> </el-row>
</el-dialog> </el-dialog>
@ -45,7 +45,7 @@
} }
}, },
methods: { methods: {
handleClose() { onClose() {
// //
this.rc.destroy(); this.rc.destroy();
this.rc = new Recorder(); this.rc = new Recorder();
@ -55,7 +55,7 @@
this.stateTip = '未开始'; this.stateTip = '未开始';
this.$emit("close"); this.$emit("close");
}, },
handleStartRecord() { onStartRecord() {
this.rc.start().then((stream) => { this.rc.start().then((stream) => {
this.state = 'RUNNING'; this.state = 'RUNNING';
this.stateTip = "正在录音..."; this.stateTip = "正在录音...";
@ -67,34 +67,34 @@
}, },
handlePauseRecord() { onPauseRecord() {
this.rc.pause(); this.rc.pause();
this.state = 'PAUSE'; this.state = 'PAUSE';
this.stateTip = "已暂停录音"; this.stateTip = "已暂停录音";
}, },
handleResumeRecord() { onResumeRecord() {
this.rc.resume(); this.rc.resume();
this.state = 'RUNNING'; this.state = 'RUNNING';
this.stateTip = "正在录音..."; this.stateTip = "正在录音...";
}, },
handleCompleteRecord() { onCompleteRecord() {
this.rc.pause(); this.rc.pause();
this.state = 'COMPLETE'; this.state = 'COMPLETE';
this.stateTip = "已结束录音"; this.stateTip = "已结束录音";
}, },
handlePlayAudio() { onPlayAudio() {
let wav = this.rc.getWAVBlob(); let wav = this.rc.getWAVBlob();
let url = URL.createObjectURL(wav); let url = URL.createObjectURL(wav);
this.$refs.audio.src = url; this.$refs.audio.src = url;
this.$refs.audio.play(); this.$refs.audio.play();
this.mode = 'PLAY'; this.mode = 'PLAY';
}, },
handleStopAudio() { onStopAudio() {
console.log(this.$refs.audio); console.log(this.$refs.audio);
this.$refs.audio.pause(); this.$refs.audio.pause();
this.mode = 'RECORD'; this.mode = 'RECORD';
}, },
handleRestartRecord() { onRestartRecord() {
this.rc.destroy(); this.rc.destroy();
this.rc = new Recorder() this.rc = new Recorder()
this.rc.start(); this.rc.start();
@ -102,7 +102,7 @@
this.mode = 'RECORD'; this.mode = 'RECORD';
this.stateTip = "正在录音..."; this.stateTip = "正在录音...";
}, },
handleSendRecord() { onSendRecord() {
let wav = this.rc.getWAVBlob(); let wav = this.rc.getWAVBlob();
let name = new Date().getDate() + '.wav'; let name = new Date().getDate() + '.wav';
var formData = new window.FormData() var formData = new window.FormData()
@ -120,7 +120,7 @@
url: url url: url
} }
this.$emit("send", data); this.$emit("send", data);
this.handleClose(); this.onClose();
}) })
} }
} }

36
im-ui/src/components/common/Emotion.vue

@ -1,9 +1,10 @@
<template> <template>
<div class="emotion-mask" @click="$emit('emotion','')"> <div v-show="show" @click="close()">
<div class="emotion-box" :style="{'left':x+'px','top':y+'px'}"> <div class="emotion-box" :style="{'left':x+'px','top':y+'px'}">
<el-scrollbar style="height:250px"> <el-scrollbar style="height:250px">
<div class="emotion-item-list"> <div class="emotion-item-list">
<div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="clickHandler(emoText)" v-html="$emo.textToImg(emoText)"> <div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i"
@click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText)">
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
@ -14,18 +15,26 @@
<script> <script>
export default { export default {
name: "emotion", name: "emotion",
props: { data() {
return {
show: false,
pos: { pos: {
type: Object x: 0,
y: 0
}
} }
},
data() {
return {}
}, },
methods: { methods: {
clickHandler(emoText) { onClickEmo(emoText) {
let emotion = `#${emoText};` let emotion = `#${emoText};`
this.$emit('emotion', emotion) this.$emit('emotion', emotion)
},
open(pos) {
this.pos = pos;
this.show = true;
},
close() {
this.show = false;
} }
}, },
computed: { computed: {
@ -39,15 +48,7 @@
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.emotion-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
.emotion-box { .emotion-box {
position: fixed; position: fixed;
@ -58,6 +59,7 @@
border-radius: 5px; border-radius: 5px;
background-color: #f5f5f5; background-color: #f5f5f5;
box-shadow: 0px 0px 10px #ccc; box-shadow: 0px 0px 10px #ccc;
.emotion-item-list { .emotion-item-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

6
im-ui/src/components/common/FileUpload.vue

@ -1,6 +1,6 @@
<template> <template>
<el-upload :action="action" :headers="uploadHeaders" :accept="fileTypes==null?'':fileTypes.join(',')" <el-upload :action="action" :headers="uploadHeaders" :accept="fileTypes==null?'':fileTypes.join(',')"
:show-file-list="false" :on-success="handleSuccess" :on-error="handleError" :disabled="disabled" :before-upload="beforeUpload"> :show-file-list="false" :on-success="onSuccess" :on-error="onError" :disabled="disabled" :before-upload="beforeUpload">
<slot></slot> <slot></slot>
</el-upload> </el-upload>
</template> </template>
@ -37,7 +37,7 @@
} }
}, },
methods: { methods: {
handleSuccess(res, file) { onSuccess(res, file) {
this.loading && this.loading.close(); this.loading && this.loading.close();
if (res.code == 200) { if (res.code == 200) {
this.$emit("success", res.data, file); this.$emit("success", res.data, file);
@ -46,7 +46,7 @@
this.$emit("fail", res, file); this.$emit("fail", res, file);
} }
}, },
handleError(err, file) { onError(err, file) {
this.$emit("fail", err, file); this.$emit("fail", err, file);
}, },
beforeUpload(file) { beforeUpload(file) {

6
im-ui/src/components/common/FullImage.vue

@ -1,10 +1,10 @@
<template> <template>
<div class="full-image" v-show="visible" :before-close="handleClose" :modal="true"> <div class="full-image" v-show="visible" :before-close="onClose" :modal="true">
<div class="mask"></div> <div class="mask"></div>
<div class="image-box"> <div class="image-box">
<img :src="url"/> <img :src="url"/>
</div> </div>
<div class="close" @click="handleClose">x</div> <div class="close" @click="onClose">x</div>
</div> </div>
</template> </template>
@ -17,7 +17,7 @@
} }
}, },
methods: { methods: {
handleClose() { onClose() {
this.$emit("close"); this.$emit("close");
} }
}, },

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

@ -3,7 +3,7 @@
<div class="right-menu" :style="{'left':pos.x+'px','top':pos.y+'px'}"> <div class="right-menu" :style="{'left':pos.x+'px','top':pos.y+'px'}">
<el-menu text-color="#333333"> <el-menu text-color="#333333">
<el-menu-item v-for="(item) in items" :key="item.key" :title="item.name" <el-menu-item v-for="(item) in items" :key="item.key" :title="item.name"
@click="handleSelectMenu(item)"> @click="onSelectMenu(item)">
<span :class="item.icon"></span> <span :class="item.icon"></span>
<span>{{item.name}}</span> <span>{{item.name}}</span>
@ -31,7 +31,7 @@
close() { close() {
this.$emit("close"); this.$emit("close");
}, },
handleSelectMenu(item) { onSelectMenu(item) {
this.$emit("select", item); this.$emit("select", item);
} }
} }

8
im-ui/src/components/common/UserInfo.vue

@ -18,8 +18,8 @@
</div> </div>
<el-divider content-position="center"></el-divider> <el-divider content-position="center"></el-divider>
<div class="user-btn-group"> <div class="user-btn-group">
<el-button v-show="isFriend" type="primary" @click="handleSendMessage()">发消息</el-button> <el-button v-show="isFriend" type="primary" @click="onSendMessage()">发消息</el-button>
<el-button v-show="!isFriend" type="primary" @click="handleAddFriend()">加为好友</el-button> <el-button v-show="!isFriend" type="primary" @click="onAddFriend()">加为好友</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -47,7 +47,7 @@
} }
}, },
methods: { methods: {
handleSendMessage() { onSendMessage() {
let user = this.user; let user = this.user;
let chat = { let chat = {
type: 'PRIVATE', type: 'PRIVATE',
@ -62,7 +62,7 @@
} }
this.$emit("close"); this.$emit("close");
}, },
handleAddFriend() { onAddFriend() {
this.$http({ this.$http({
url: "/friend/add", url: "/friend/add",
method: "post", method: "post",

14
im-ui/src/components/friend/AddFriend.vue

@ -1,7 +1,7 @@
<template> <template>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="30%" :before-close="handleClose"> <el-dialog title="添加好友" :visible.sync="dialogVisible" width="30%" :before-close="onClose">
<el-input placeholder="输入用户名或昵称,最多展示20条" class="input-with-select" v-model="searchText" @keyup.enter.native="handleSearch()"> <el-input placeholder="输入用户名或昵称,最多展示20条" class="input-with-select" v-model="searchText" @keyup.enter.native="onSearch()">
<el-button slot="append" icon="el-icon-search" @click="handleSearch()"></el-button> <el-button slot="append" icon="el-icon-search" @click="onSearch()"></el-button>
</el-input> </el-input>
<el-scrollbar style="height:400px"> <el-scrollbar style="height:400px">
<div v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id"> <div v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id">
@ -21,7 +21,7 @@
<div>昵称:{{user.nickName}}</div> <div>昵称:{{user.nickName}}</div>
</div> </div>
</div> </div>
<el-button type="success" size="small" v-show="!isFriend(user.id)" plain @click="handleAddFriend(user)">添加</el-button> <el-button type="success" size="small" v-show="!isFriend(user.id)" plain @click="onAddFriend(user)">添加</el-button>
<el-button type="info" size="small" v-show="isFriend(user.id)" plain disabled>已添加</el-button> <el-button type="info" size="small" v-show="isFriend(user.id)" plain disabled>已添加</el-button>
</div> </div>
</div> </div>
@ -48,10 +48,10 @@
} }
}, },
methods: { methods: {
handleClose() { onClose() {
this.$emit("close"); this.$emit("close");
}, },
handleSearch() { onSearch() {
this.$http({ this.$http({
url: "/user/findByName", url: "/user/findByName",
method: "get", method: "get",
@ -62,7 +62,7 @@
this.users = data; this.users = data;
}) })
}, },
handleAddFriend(user){ onAddFriend(user){
this.$http({ this.$http({
url: "/friend/add", url: "/friend/add",
method: "post", method: "post",

14
im-ui/src/components/friend/FriendItem.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="friend-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)"> <div class="friend-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="friend-avatar"> <div class="friend-avatar">
<head-image :name="friend.nickName" :url="friend.headImage" :online="friend.online"> <head-image :size="45" :name="friend.nickName" :url="friend.headImage" :online="friend.online">
</head-image> </head-image>
</div> </div>
<div class="friend-info"> <div class="friend-info">
@ -14,7 +14,7 @@
</div> </div>
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@close="rightMenu.show=false" @select="handleSelectMenu"></right-menu> @close="rightMenu.show=false" @select="onSelectMenu"></right-menu>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -57,7 +57,7 @@
}; };
this.rightMenu.show = "true"; this.rightMenu.show = "true";
}, },
handleSelectMenu(item) { onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo); this.$emit(item.key.toLowerCase(), this.msgInfo);
} }
}, },
@ -83,13 +83,13 @@
<style scope lang="scss"> <style scope lang="scss">
.friend-item { .friend-item {
height: 65px; height: 50px;
display: flex; display: flex;
margin-bottom: 1px; margin-bottom: 1px;
position: relative; position: relative;
padding: 5px;
padding-left: 10px; padding-left: 10px;
align-items: center; align-items: center;
padding-right: 5px;
background-color: #fafafa; background-color: #fafafa;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
@ -106,8 +106,8 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 50px; width: 45px;
height: 50px; height: 45px;
} }
.friend-info { .friend-info {

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

@ -1,13 +1,13 @@
<template> <template>
<el-dialog title="邀请好友" :visible.sync="visible" width="50%" :before-close="handleClose"> <el-dialog title="邀请好友" :visible.sync="visible" width="50%" :before-close="onClose">
<div class="agm-container"> <div class="agm-container">
<div class="agm-l-box"> <div class="agm-l-box">
<el-input width="200px" placeholder="搜索好友" class="input-with-select" v-model="searchText" @keyup.enter.native="handleSearch()"> <el-input width="200px" placeholder="搜索好友" class="input-with-select" v-model="searchText" @keyup.enter.native="onSearch()">
<el-button slot="append" icon="el-icon-search" @click="handleSearch()"></el-button> <el-button slot="append" icon="el-icon-search" @click="onSearch()"></el-button>
</el-input> </el-input>
<el-scrollbar style="height:400px;"> <el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id"> <div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-show="friend.nickName.startsWith(searchText)" :showDelete="false" @click.native="handleSwitchCheck(friend)" <friend-item v-show="friend.nickName.startsWith(searchText)" :showDelete="false" @click.native="onSwitchCheck(friend)"
:menu="false" :friend="friend" :index="index" :active="index === activeIndex"> :menu="false" :friend="friend" :index="index" :active="index === activeIndex">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" v-model="friend.isCheck" <el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" v-model="friend.isCheck"
size="medium"></el-checkbox> size="medium"></el-checkbox>
@ -20,7 +20,7 @@
<el-scrollbar style="height:400px;"> <el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id"> <div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" <friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend"
:index="index" :active="false" @del="handleRemoveFriend(friend,index)" :index="index" :active="false" @del="onRemoveFriend(friend,index)"
:menu="false"> :menu="false">
</friend-item> </friend-item>
</div> </div>
@ -28,8 +28,8 @@
</div> </div>
</div> </div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="handleClose()"> </el-button> <el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="handleOk()"> </el-button> <el-button type="primary" @click="onOk()"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</template> </template>
@ -50,10 +50,10 @@
} }
}, },
methods: { methods: {
handleClose() { onClose() {
this.$emit("close"); this.$emit("close");
}, },
handleOk() { onOk() {
let inviteVO = { let inviteVO = {
groupId: this.groupId, groupId: this.groupId,
@ -76,10 +76,10 @@
}) })
} }
}, },
handleRemoveFriend(friend, index) { onRemoveFriend(friend, index) {
friend.isCheck = false; friend.isCheck = false;
}, },
handleSwitchCheck(friend) { onSwitchCheck(friend) {
if (!friend.disabled) { if (!friend.disabled) {
friend.isCheck = !friend.isCheck friend.isCheck = !friend.isCheck
} }

12
im-ui/src/components/group/GroupItem.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="group-item" :class="active ? 'active' : ''"> <div class="group-item" :class="active ? 'active' : ''">
<div class="group-avatar"> <div class="group-avatar">
<head-image :name="group.remark" :url="group.headImage"> </head-image> <head-image :size="45" :name="group.remark" :url="group.headImage"> </head-image>
</div> </div>
<div class="group-name"> <div class="group-name">
<div>{{group.remark}}</div> <div>{{group.remark}}</div>
@ -34,13 +34,13 @@
<style lang="scss" > <style lang="scss" >
.group-item { .group-item {
height: 65px; height: 50px;
display: flex; display: flex;
margin-bottom: 1px; margin-bottom: 1px;
position: relative; position: relative;
padding: 5px;
padding-left: 10px; padding-left: 10px;
align-items: center; align-items: center;
padding-right: 5px;
background-color: #fafafa; background-color: #fafafa;
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
@ -52,15 +52,15 @@
} }
.group-avatar { .group-avatar {
width: 50px; width: 45px;
height: 50px; height: 45px;
} }
.group-name { .group-name {
padding-left: 10px; padding-left: 10px;
height: 100%; height: 100%;
text-align: left; text-align: left;
line-height: 65px; line-height: 50px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
font-size: 14px; font-size: 14px;

4
im-ui/src/components/group/GroupMember.vue

@ -3,7 +3,7 @@
<head-image :id="member.userId" :name="member.aliasName" <head-image :id="member.userId" :name="member.aliasName"
:url="member.headImage" :size="50" :url="member.headImage" :size="50"
:online="member.online" > :online="member.online" >
<div v-if="showDel" @click.stop="handleDelete()" class="btn-kick el-icon-error"></div> <div v-if="showDel" @click.stop="onDelete()" class="btn-kick el-icon-error"></div>
</head-image> </head-image>
<div class="member-name">{{member.aliasName}}</div> <div class="member-name">{{member.aliasName}}</div>
@ -29,7 +29,7 @@
} }
}, },
methods:{ methods:{
handleDelete(){ onDelete(){
this.$emit("del",this.member); this.$emit("del",this.member);
} }
} }

14
im-ui/src/components/setting/Setting.vue

@ -1,12 +1,12 @@
<template> <template>
<el-dialog class="setting" title="设置" :visible.sync="visible" width="500px" :before-close="handleClose"> <el-dialog class="setting" title="设置" :visible.sync="visible" width="500px" :before-close="onClose">
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm"> <el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm">
<el-form-item label="头像"> <el-form-item label="头像">
<file-upload class="avatar-uploader" <file-upload class="avatar-uploader"
:action="imageAction" :action="imageAction"
:showLoading="true" :showLoading="true"
:maxSize="maxSize" :maxSize="maxSize"
@success="handleUploadSuccess" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> :fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar"> <img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar">
@ -31,8 +31,8 @@
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="handleClose()"> </el-button> <el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="handleSubmit()"> </el-button> <el-button type="primary" @click="onSubmit()"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</template> </template>
@ -65,10 +65,10 @@
}, },
methods: { methods: {
handleClose() { onClose() {
this.$emit("close"); this.$emit("close");
}, },
handleSubmit() { onSubmit() {
this.$refs['settingForm'].validate((valid) => { this.$refs['settingForm'].validate((valid) => {
if (!valid) { if (!valid) {
return false; return false;
@ -84,7 +84,7 @@
}) })
}); });
}, },
handleUploadSuccess(res, file) { onUploadSuccess(res, file) {
this.userInfo.headImage = res.data.originUrl; this.userInfo.headImage = res.data.originUrl;
this.userInfo.headImageThumb = res.data.thumbUrl; this.userInfo.headImageThumb = res.data.thumbUrl;
} }

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

@ -73,6 +73,8 @@ export default {
if (state.chats[idx].type == chatInfo.type && if (state.chats[idx].type == chatInfo.type &&
state.chats[idx].targetId == chatInfo.targetId) { state.chats[idx].targetId == chatInfo.targetId) {
state.chats[idx].unreadCount = 0; state.chats[idx].unreadCount = 0;
state.chats[idx].atMe = false;
state.chats[idx].atAll = false;
} }
} }
this.commit("saveToStorage"); this.commit("saveToStorage");
@ -121,7 +123,6 @@ export default {
} }
}, },
insertMessage(state, msgInfo) { insertMessage(state, msgInfo) {
// 获取对方id或群id // 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE'; let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId; let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
@ -144,10 +145,21 @@ export default {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName;
// 未读加1 // 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) { if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++; chat.unreadCount++;
} }
// 是否有人@我
if(!msgInfo.selfSend && chat.type=="GROUP" && msgInfo.atUserIds){
let userId = userStore.state.userInfo.id;
if(msgInfo.atUserIds.indexOf(userId)>=0){
chat.atMe = true;
}
if(msgInfo.atUserIds.indexOf(-1)>=0){
chat.atAll = true;
}
}
// 记录消息的最大id // 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) { if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id; state.privateMsgMaxId = msgInfo.id;

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

@ -27,9 +27,7 @@ export default {
}, },
removeFriend(state, index) { removeFriend(state, index) {
state.friends.splice(index, 1); state.friends.splice(index, 1);
if(state.activeIndex >= state.friends.length){ state.activeIndex = -1;
state.activeIndex = state.friends.length-1;
}
}, },
addFriend(state, friend) { addFriend(state, friend) {
state.friends.push(friend); state.friends.push(friend);

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

@ -20,9 +20,7 @@ export default {
state.groups.forEach((g, index) => { state.groups.forEach((g, index) => {
if (g.id == groupId) { if (g.id == groupId) {
state.groups.splice(index, 1); state.groups.splice(index, 1);
if (state.activeIndex >= state.groups.length) { state.activeIndex = -1;
state.activeIndex = state.groups.length - 1;
}
} }
}) })

35
im-ui/src/view/Chat.vue

@ -1,23 +1,24 @@
<template> <template>
<el-container> <el-container class="chat-page">
<el-aside width="250px" class="l-chat-box"> <el-aside width="260px" class="chat-list-box">
<div class="l-chat-header"> <div class="chat-list-header">
<el-input width="200px" placeholder="搜索" v-model="searchText"> <el-input width="200px" placeholder="搜索" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button> <el-button slot="append" icon="el-icon-search"></el-button>
</el-input> </el-input>
</div> </div>
<div class="l-chat-loadding" v-if="loading" v-loading="true" element-loading-text="消息接收中..." <div class="chat-list-loadding" v-if="loading" v-loading="true" element-loading-text="消息接收中..."
element-loading-spinner="el-icon-loading" element-loading-background="#eee"> element-loading-spinner="el-icon-loading" element-loading-background="#eee">
<div class="chat-loading-box"></div>
</div> </div>
<el-scrollbar class="l-chat-list"> <el-scrollbar class="chat-list-items">
<div v-for="(chat,index) in chatStore.chats" :key="index"> <div v-for="(chat,index) in chatStore.chats" :key="index">
<chat-item v-show="chat.showName.startsWith(searchText)" :chat="chat" :index="index" <chat-item v-show="chat.showName.startsWith(searchText)" :chat="chat" :index="index"
@click.native="handleActiveItem(index)" @delete="handleDelItem(index)" @top="handleTop(index)" @click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="index === chatStore.activeIndex"></chat-item> :active="index === chatStore.activeIndex"></chat-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
<el-container class="r-chat-box"> <el-container class="chat-box">
<chat-box v-show="activeChat.targetId>0" :chat="activeChat"></chat-box> <chat-box v-show="activeChat.targetId>0" :chat="activeChat"></chat-box>
</el-container> </el-container>
</el-container> </el-container>
@ -42,13 +43,13 @@
} }
}, },
methods: { methods: {
handleActiveItem(index) { onActiveItem(index) {
this.$store.commit("activeChat", index); this.$store.commit("activeChat", index);
}, },
handleDelItem(index) { onDelItem(index) {
this.$store.commit("removeChat", index); this.$store.commit("removeChat", index);
}, },
handleTop(chatIdx) { onTop(chatIdx) {
this.$store.commit("moveTop", chatIdx); this.$store.commit("moveTop", chatIdx);
}, },
}, },
@ -79,26 +80,30 @@
</script> </script>
<style lang="scss"> <style lang="scss">
.el-container { .chat-page {
.l-chat-box { .chat-list-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
background: white; background: white;
width: 3rem; width: 3rem;
.l-chat-header { .chat-list-header {
padding: 5px; padding: 5px;
background-color: white; background-color: white;
line-height: 50px; line-height: 50px;
} }
.l-chat-loadding{ .chat-list-loadding{
height: 50px; height: 50px;
background-color: #eee; background-color: #eee;
.chat-loading-box{
height: 100%;
}
} }
.l-friend-ist { .chat-list-items {
flex: 1; flex: 1;
} }
} }

89
im-ui/src/view/Friend.vue

@ -1,36 +1,37 @@
<template> <template>
<el-container> <el-container class="friend-page">
<el-aside width="250px" class="l-friend-box"> <el-aside width="260px" class="friend-list-box">
<div class="l-friend-header"> <div class="friend-list-header">
<div class="l-friend-search"> <div class="friend-list-search">
<el-input width="200px" placeholder="搜索好友" v-model="searchText"> <el-input width="200px" placeholder="搜索好友" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button> <el-button slot="append" icon="el-icon-search"></el-button>
</el-input> </el-input>
</div> </div>
<el-button plain icon="el-icon-plus" style="border: none; padding:12px; font-size: 20px;color: black;" <el-button plain icon="el-icon-plus" style="border: none; padding:12px; font-size: 20px;color: black;"
title="添加好友" @click="handleShowAddFriend()"></el-button> title="添加好友" @click="onShowAddFriend()"></el-button>
<add-friend :dialogVisible="showAddFriend" @close="handleCloseAddFriend"> <add-friend :dialogVisible="showAddFriend" @close="onCloseAddFriend">
</add-friend> </add-friend>
</div> </div>
<el-scrollbar class="l-friend-list"> <el-scrollbar class="friend-list-items">
<div v-for="(friend,index) in $store.state.friendStore.friends" :key="index"> <div v-for="(friend,index) in $store.state.friendStore.friends" :key="index">
<friend-item v-show="friend.nickName.startsWith(searchText)" :index="index" <friend-item v-show="friend.nickName.startsWith(searchText)" :index="index"
:active="index === $store.state.friendStore.activeIndex" @chat="handleSendMessage(friend)" :active="index === $store.state.friendStore.activeIndex" @chat="onSendMessage(friend)"
@delete="handleDelItem(friend,index)" @click.native="handleActiveItem(friend,index)"> @delete="onDelItem(friend,index)" @click.native="onActiveItem(friend,index)">
</friend-item> </friend-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
<el-container class="r-friend-box"> <el-container class="friend-box">
<div class="r-friend-header" v-show="userInfo.id"> <div class="friend-header" v-show="userInfo.id">
{{userInfo.nickName}} {{userInfo.nickName}}
</div> </div>
<div v-show="userInfo.id"> <div v-show="userInfo.id">
<div class="user-detail"> <div class="friend-detail">
<head-image class="detail-head-image" :size="200" <head-image :size="200"
:name="userInfo.nickName" :name="userInfo.nickName"
:url="userInfo.headImage" :url="userInfo.headImage"
@click.native="showFullImage()"></head-image> @click.native="showFullImage()"></head-image>
<div>
<div class="info-item"> <div class="info-item">
<el-descriptions title="好友信息" class="description" :column="1"> <el-descriptions title="好友信息" class="description" :column="1">
<el-descriptions-item label="用户名">{{ userInfo.userName }} <el-descriptions-item label="用户名">{{ userInfo.userName }}
@ -40,11 +41,17 @@
<el-descriptions-item label="性别">{{ userInfo.sex==0?"男":"女" }}</el-descriptions-item> <el-descriptions-item label="性别">{{ userInfo.sex==0?"男":"女" }}</el-descriptions-item>
<el-descriptions-item label="签名">{{ userInfo.signature }}</el-descriptions-item> <el-descriptions-item label="签名">{{ userInfo.signature }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div>
<div class="frient-btn-group">
<el-button v-show="isFriend" icon="el-icon-chat-dot-round" type="primary" @click="onSendMessage(userInfo)">发送消息</el-button>
<el-button v-show="!isFriend" icon="el-icon-plus" type="primary" @click="onAddFriend(userInfo)">加为好友</el-button>
<el-button v-show="isFriend" icon="el-icon-delete" type="danger" @click="onDelItem(userInfo,friendStore.activeIndex)">删除好友</el-button>
</div> </div>
</div> </div>
<div class="btn-group">
<el-button class="send-btn" @click="handleSendMessage(userInfo)">发送消息</el-button>
</div> </div>
<el-divider content-position="center"></el-divider>
</div> </div>
</el-container> </el-container>
</el-container> </el-container>
@ -71,17 +78,17 @@
} }
}, },
methods: { methods: {
handleShowAddFriend() { onShowAddFriend() {
this.showAddFriend = true; this.showAddFriend = true;
}, },
handleCloseAddFriend() { onCloseAddFriend() {
this.showAddFriend = false; this.showAddFriend = false;
}, },
handleActiveItem(friend, index) { onActiveItem(friend, index) {
this.$store.commit("activeFriend", index); this.$store.commit("activeFriend", index);
this.loadUserInfo(friend, index); this.loadUserInfo(friend, index);
}, },
handleDelItem(friend, index) { onDelItem(friend, index) {
this.$confirm(`确认要解除与 '${friend.nickName}'的好友关系吗?`, '确认解除?', { this.$confirm(`确认要解除与 '${friend.nickName}'的好友关系吗?`, '确认解除?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@ -97,7 +104,25 @@
}) })
}) })
}, },
handleSendMessage(user) { onAddFriend(user){
this.$http({
url: "/friend/add",
method: "post",
params: {
friendId: user.id
}
}).then((data) => {
this.$message.success("添加成功,对方已成为您的好友");
let friend = {
id:user.id,
nickName: user.nickName,
headImage: user.headImage,
online: user.online
}
this.$store.commit("addFriend",friend);
})
},
onSendMessage(user) {
let chat = { let chat = {
type: 'PRIVATE', type: 'PRIVATE',
targetId: user.id, targetId: user.id,
@ -112,7 +137,6 @@
if (this.userInfo.headImage) { if (this.userInfo.headImage) {
this.$store.commit('showFullImageBox', this.userInfo.headImage); this.$store.commit('showFullImageBox', this.userInfo.headImage);
} }
}, },
updateFriendInfo(friend, user, index) { updateFriendInfo(friend, user, index) {
// storestore // storestore
@ -145,6 +169,9 @@
computed: { computed: {
friendStore() { friendStore() {
return this.$store.state.friendStore; return this.$store.state.friendStore;
},
isFriend(){
return this.friendStore.friends.find((f)=>f.id==this.userInfo.id);
} }
}, },
mounted() { mounted() {
@ -158,36 +185,36 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.el-container { .friend-page {
.l-friend-box { .friend-list-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
background: white; background: white;
.l-friend-header { .friend-list-header {
height: 50px; height: 50px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 5px; padding: 5px;
background-color: white; background-color: white;
.l-friend-search { .friend-list-search {
flex: 1; flex: 1;
} }
} }
.l-friend-ist { .friend-list-items {
flex: 1; flex: 1;
} }
} }
.r-friend-box { .friend-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
.r-friend-header { .friend-header {
width: 100%; width: 100%;
height: 50px; height: 50px;
padding: 5px; padding: 5px;
@ -200,7 +227,7 @@
border: #dddddd solid 1px; border: #dddddd solid 1px;
} }
.user-detail { .friend-detail {
display: flex; display: flex;
padding: 50px 80px 20px 80px; padding: 50px 80px 20px 80px;
text-align: center; text-align: center;
@ -215,9 +242,9 @@
} }
} }
.btn-group { .frient-btn-group {
text-align: left !important; text-align: left !important;
padding-left: 120px; padding: 20px;
} }
} }
} }

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

@ -1,33 +1,33 @@
<template> <template>
<el-container class="im-group-box"> <el-container class="group-page">
<el-aside width="250px" class="l-group-box"> <el-aside width="260px" class="group-list-box">
<div class="l-group-header"> <div class="group-list-header">
<div class="l-group-search"> <div class="group-list-search">
<el-input width="200px" placeholder="搜索群聊" v-model="searchText"> <el-input width="200px" placeholder="搜索群聊" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button> <el-button slot="append" icon="el-icon-search"></el-button>
</el-input> </el-input>
</div> </div>
<el-button plain icon="el-icon-plus" style="border: none; padding: 12px; font-size: 20px;color: black;" <el-button plain icon="el-icon-plus" style="border: none; padding: 12px; font-size: 20px;color: black;"
title="创建群聊" @click="handleCreateGroup()"></el-button> title="创建群聊" @click="onCreateGroup()"></el-button>
</div> </div>
<el-scrollbar class="l-group-list"> <el-scrollbar class="group-list-items">
<div v-for="(group,index) in groupStore.groups" :key="index"> <div v-for="(group,index) in groupStore.groups" :key="index">
<group-item v-show="group.remark.startsWith(searchText)" :group="group" <group-item v-show="group.remark.startsWith(searchText)" :group="group"
:active="index === groupStore.activeIndex" @click.native="handleActiveItem(group,index)"> :active="index === groupStore.activeIndex" @click.native="onActiveItem(group,index)">
</group-item> </group-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
<el-container class="r-group-box"> <el-container class="group-box">
<div class="r-group-header" v-show="activeGroup.id"> <div class="group-header" v-show="activeGroup.id">
{{activeGroup.remark}}({{groupMembers.length}}) {{activeGroup.remark}}({{groupMembers.length}})
</div> </div>
<el-scrollbar class="r-group-container"> <el-scrollbar class="group-container">
<div v-show="activeGroup.id"> <div v-show="activeGroup.id">
<div class="r-group-info"> <div class="group-info">
<div> <div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" <file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
:showLoading="true" :maxSize="maxSize" @success="handleUploadSuccess" :showLoading="true" :maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> :fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar"> <img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
@ -36,9 +36,9 @@
:url="activeGroup.headImage" :url="activeGroup.headImage"
:name="activeGroup.remark"> :name="activeGroup.remark">
</head-image> </head-image>
<el-button class="send-btn" @click="handleSendMessage()">发送消息</el-button> <el-button class="send-btn" icon="el-icon-chat-dot-round" type="primary" @click="onSendMessage()">发送消息</el-button>
</div> </div>
<el-form class="r-group-form" label-width="130px" :model="activeGroup" :rules="rules" <el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules"
ref="groupForm"> ref="groupForm">
<el-form-item label="群聊名称" prop="name"> <el-form-item label="群聊名称" prop="name">
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input> <el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
@ -57,29 +57,29 @@
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" <el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea"
maxlength="1024" placeholder="群主未设置"></el-input> maxlength="1024" placeholder="群主未设置"></el-input>
</el-form-item> </el-form-item>
<div class="btn-group"> <div>
<el-button type="success" @click="handleSaveGroup()">提交</el-button> <el-button type="success" @click="onSaveGroup()">提交</el-button>
<el-button type="danger" v-show="!isOwner" @click="handleQuit()">退出群聊</el-button> <el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
<el-button type="danger" v-show="isOwner" @click="handleDissolve()">解散群聊</el-button> <el-button type="danger" v-show="isOwner" @click="onDissolve()">解散群聊</el-button>
</div> </div>
</el-form> </el-form>
</div> </div>
<el-divider content-position="center"></el-divider> <el-divider content-position="center"></el-divider>
<el-scrollbar style="height:400px;"> <el-scrollbar style="height:200px;">
<div class="r-group-member-list"> <div class="group-member-list">
<div v-for="(member) in groupMembers" :key="member.id"> <div v-for="(member) in groupMembers" :key="member.id">
<group-member v-show="!member.quit" class="r-group-member" :member="member" <group-member v-show="!member.quit" class="group-member" :member="member"
:showDel="isOwner&&member.userId!=activeGroup.ownerId" :showDel="isOwner&&member.userId!=activeGroup.ownerId"
@del="handleKick"></group-member> @del="onKick"></group-member>
</div> </div>
<div class="r-group-invite"> <div class="group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="handleInviteMember()"> <div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
<i class="el-icon-plus"></i> <i class="el-icon-plus"></i>
</div> </div>
<div class="invite-member-text">邀请</div> <div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" <add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id"
:members="groupMembers" @reload="loadGroupMembers" :members="groupMembers" @reload="loadGroupMembers"
@close="handleCloseAddGroupMember"></add-group-member> @close="onCloseAddGroupMember"></add-group-member>
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
@ -122,7 +122,7 @@
}; };
}, },
methods: { methods: {
handleCreateGroup() { onCreateGroup() {
this.$prompt('请输入群聊名称', '创建群聊', { this.$prompt('请输入群聊名称', '创建群聊', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@ -134,8 +134,6 @@
name: o.value, name: o.value,
remark: o.value, remark: o.value,
aliasName: userInfo.name, aliasName: userInfo.name,
headImage: userInfo.headImage,
headImageThumb: userInfo.headImageThumb,
ownerId: userInfo.id ownerId: userInfo.id
} }
this.$http({ this.$http({
@ -147,24 +145,24 @@
}) })
}) })
}, },
handleActiveItem(group, index) { onActiveItem(group, index) {
this.$store.commit("activeGroup", index); this.$store.commit("activeGroup", index);
// store // store
this.activeGroup = JSON.parse(JSON.stringify(group)); this.activeGroup = JSON.parse(JSON.stringify(group));
// //
this.loadGroupMembers(); this.loadGroupMembers();
}, },
handleInviteMember() { onInviteMember() {
this.showAddGroupMember = true; this.showAddGroupMember = true;
}, },
handleCloseAddGroupMember() { onCloseAddGroupMember() {
this.showAddGroupMember = false; this.showAddGroupMember = false;
}, },
handleUploadSuccess(res) { onUploadSuccess(res) {
this.activeGroup.headImage = res.data.originUrl; this.activeGroup.headImage = res.data.originUrl;
this.activeGroup.headImageThumb = res.data.thumbUrl; this.activeGroup.headImageThumb = res.data.thumbUrl;
}, },
handleSaveGroup() { onSaveGroup() {
this.$refs['groupForm'].validate((valid) => { this.$refs['groupForm'].validate((valid) => {
if (valid) { if (valid) {
let vo = this.activeGroup; let vo = this.activeGroup;
@ -179,7 +177,7 @@
} }
}); });
}, },
handleDissolve() { onDissolve() {
this.$confirm('确认要解散群聊吗?', '确认解散?', { this.$confirm('确认要解散群聊吗?', '确认解散?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@ -191,14 +189,13 @@
}).then(() => { }).then(() => {
this.$message.success(`群聊'${this.activeGroup.name}'已解散`); this.$message.success(`群聊'${this.activeGroup.name}'已解散`);
this.$store.commit("removeGroup", this.activeGroup.id); this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("activeGroup", -1);
this.$store.commit("removeGroupChat", this.activeGroup.id); this.$store.commit("removeGroupChat", this.activeGroup.id);
this.activeGroup = {}; this.reset();
}); });
}) })
}, },
handleKick(member) { onKick(member) {
this.$confirm(`确定将成员'${member.aliasName}'移出群聊吗?`, '确认移出?', { this.$confirm(`确定将成员'${member.aliasName}'移出群聊吗?`, '确认移出?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@ -217,7 +214,7 @@
}) })
}, },
handleQuit() { onQuit() {
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', { this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@ -228,13 +225,13 @@
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$store.commit("removeGroup", this.activeGroup.id); this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("activeGroup", -1);
this.$store.commit("removeGroupChat", this.activeGroup.id); this.$store.commit("removeGroupChat", this.activeGroup.id);
this.reset();
}); });
}) })
}, },
handleSendMessage() { onSendMessage() {
let chat = { let chat = {
type: 'GROUP', type: 'GROUP',
targetId: this.activeGroup.id, targetId: this.activeGroup.id,
@ -252,6 +249,10 @@
}).then((members) => { }).then((members) => {
this.groupMembers = members; this.groupMembers = members;
}) })
},
reset(){
this.activeGroup={};
this.groupMembers=[];
} }
}, },
computed: { computed: {
@ -282,36 +283,36 @@
</script> </script>
<style lang="scss"> <style lang="scss">
.im-group-box { .group-page {
.l-group-box { .group-list-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
background: white; background: white;
.l-group-header { .group-list-header {
height: 50px; height: 50px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 5px; padding: 5px;
background-color: white; background-color: white;
.l-group-search { .group-list-search {
flex: 1; flex: 1;
} }
} }
.l-group-ist { .group-list-items {
flex: 1; flex: 1;
} }
} }
.r-group-box { .group-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: #dddddd solid 1px; border: #dddddd solid 1px;
.r-group-header { .group-header {
width: 100%; width: 100%;
height: 50px; height: 50px;
padding: 5px; padding: 5px;
@ -324,17 +325,16 @@
border: #dddddd solid 1px; border: #dddddd solid 1px;
} }
.r-group-container { .group-container {
padding: 50px; padding: 20px;
.group-info {
.r-group-info {
display: flex; display: flex;
padding: 5px 20px; padding: 5px 20px;
.r-group-form { .group-form {
flex: 1; flex: 1;
padding-left: 40px; padding-left: 40px;
max-width: 800px; max-width: 700px;
} }
.avatar-uploader { .avatar-uploader {
@ -373,7 +373,7 @@
} }
} }
.r-group-member-list { .group-member-list {
padding: 5px 20px; padding: 5px 20px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -381,11 +381,11 @@
font-size: 16px; font-size: 16px;
text-align: center; text-align: center;
.r-group-member { .group-member {
margin-right: 15px; margin-right: 15px;
} }
.r-group-invite { .group-invite {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

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

@ -1,5 +1,5 @@
<template> <template>
<el-container> <el-container class="home-page">
<el-aside width="80px" class="navi-bar"> <el-aside width="80px" class="navi-bar">
<div class="user-head-image"> <div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName" <head-image :name="$store.state.userStore.userInfo.nickName"
@ -31,7 +31,7 @@
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div class="exit-box" @click="handleExit()" title="退出"> <div class="exit-box" @click="onExit()" title="退出">
<span class="el-icon-circle-close"></span> <span class="el-icon-circle-close"></span>
</div> </div>
</el-aside> </el-aside>
@ -84,16 +84,19 @@
this.loadPrivateMessage(this.$store.state.chatStore.privateMsgMaxId); this.loadPrivateMessage(this.$store.state.chatStore.privateMsgMaxId);
this.loadGroupMessage(this.$store.state.chatStore.groupMsgMaxId); this.loadGroupMessage(this.$store.state.chatStore.groupMsgMaxId);
// ws // ws
this.$wsApi.init(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.connect();
this.$wsApi.onOpen();
this.$wsApi.onMessage((cmd, msgInfo) => { this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) { if (cmd == 2) {
// ws
this.$wsApi.close(3000)
// 线 // 线
this.$message.error("您已在其他地方登陆,将被强制下线"); this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", {
setTimeout(() => { confirmButtonText: '确定',
callback: action => {
location.href = "/"; location.href = "/";
}, 1000) }
});
} else if (cmd == 3) { } else if (cmd == 3) {
// //
this.handlePrivateMessage(msgInfo); this.handlePrivateMessage(msgInfo);
@ -104,12 +107,10 @@
}) })
this.$wsApi.onClose((e) => { this.$wsApi.onClose((e) => {
console.log(e); console.log(e);
if (e.code == 1006) { if (e.code != 3000) {
// // 线
this.$message.error("连接已断开,请重新登录"); this.$message.error("连接断开,正在尝试重新连接...");
location.href = "/"; this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
} else {
this.$wsApi.connect();
} }
}); });
}).catch((e) => { }).catch((e) => {
@ -251,8 +252,8 @@
this.playAudioTip(); this.playAudioTip();
} }
}, },
handleExit() { onExit() {
this.$wsApi.close(); this.$wsApi.close(3000);
sessionStorage.removeItem("accessToken"); sessionStorage.removeItem("accessToken");
location.href = "/"; location.href = "/";
}, },
@ -337,6 +338,7 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.navi-bar { .navi-bar {
background: #333333; background: #333333;
padding: 10px; padding: 10px;

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

@ -17,6 +17,13 @@
<li>修改拉取离线消息机制:用户登录后,自动从服务器同步最近1个月的消息</li> <li>修改拉取离线消息机制:用户登录后,自动从服务器同步最近1个月的消息</li>
</ul> </ul>
</div> </div>
<div>
<h3>最近更新(2023-11-12)</h3>
<ul>
<li>群聊加入@功能</li>
<li>聊天输入框支持显示emoji表情</li>
</ul>
</div>
<div> <div>
<h3>项目依旧完全开源可内网部署如果项目对您有帮助,请帮忙点个star:</h3> <h3>项目依旧完全开源可内网部署如果项目对您有帮助,请帮忙点个star:</h3>
</div> </div>

26
im-uniapp/App.vue

@ -28,10 +28,8 @@
}, },
initWebSocket() { initWebSocket() {
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
let userId = store.state.userStore.userInfo.id; wsApi.init();
wsApi.init(process.env.WS_URL, loginInfo.accessToken); wsApi.connect(process.env.WS_URL, loginInfo.accessToken);
wsApi.connect();
wsApi.onOpen()
wsApi.onMessage((cmd, msgInfo) => { wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) { if (cmd == 2) {
// 线 // 线
@ -49,16 +47,15 @@
} }
}); });
wsApi.onClose((res) => { wsApi.onClose((res) => {
// 10063000APP // 3000
if (res.code == 1006) { if (res.code != 3000) {
//
uni.showToast({ uni.showToast({
title: '连接已断开,请重新登录', title: '连接已断开,尝试重新连接...',
icon: 'none', icon: 'none',
}) })
this.exit(); let loginInfo = uni.getStorageSync("loginInfo")
} else if (res.code != 3000) { wsApi.reconnect(process.env.WS_URL, loginInfo.accessToken);
//
wsApi.connect();
} }
}) })
}, },
@ -66,7 +63,7 @@
store.commit("loadingPrivateMsg", true) store.commit("loadingPrivateMsg", true)
http({ http({
url: "/message/private/loadMessage?minId=" + minId, url: "/message/private/loadMessage?minId=" + minId,
method: 'get' method: 'GET'
}).then((msgInfos) => { }).then((msgInfos) => {
msgInfos.forEach((msgInfo) => { msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id; msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id;
@ -76,6 +73,7 @@
this.insertPrivateMessage(friend,msgInfo); this.insertPrivateMessage(friend,msgInfo);
} }
}) })
store.commit("refreshChats");
if (msgInfos.length == 100) { if (msgInfos.length == 100) {
// //
this.loadPrivateMessage(msgInfos[99].id); this.loadPrivateMessage(msgInfos[99].id);
@ -88,7 +86,7 @@
store.commit("loadingGroupMsg", true) store.commit("loadingGroupMsg", true)
http({ http({
url: "/message/group/loadMessage?minId=" + minId, url: "/message/group/loadMessage?minId=" + minId,
method: 'get' method: 'GET'
}).then((msgInfos) => { }).then((msgInfos) => {
msgInfos.forEach((msgInfo) => { msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id; msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id;
@ -98,6 +96,7 @@
this.insertGroupMessage(group,msgInfo); this.insertGroupMessage(group,msgInfo);
} }
}) })
store.commit("refreshChats");
if (msgInfos.length == 100) { if (msgInfos.length == 100) {
// //
this.loadGroupMessage(msgInfos[99].id); this.loadGroupMessage(msgInfos[99].id);
@ -250,7 +249,6 @@
} }
}, },
onLaunch() { onLaunch() {
// //
if (uni.getStorageSync("loginInfo")) { if (uni.getStorageSync("loginInfo")) {
// //

2
im-uniapp/common/emotion.js

@ -18,7 +18,7 @@ let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if (idx == -1) { if (idx == -1) {
return ""; return emoText;
} }
let path = textToPath(emoText); let path = textToPath(emoText);
// #ifdef MP // #ifdef MP

39
im-uniapp/common/wssocket.js

@ -1,20 +1,17 @@
let wsurl = ""; let wsurl = "";
let accessToken = ""; let accessToken = "";
let openCallBack = null;
let messageCallBack = null; let messageCallBack = null;
let closeCallBack = null; let closeCallBack = null;
let isConnect = false; //连接标识 避免重复连接 let isConnect = false; //连接标识 避免重复连接
let hasInit = false; let rec = null;
let isInit = false;
let init = (url, token) => { let init = () => {
wsurl = url; // 防止重复初始化
accessToken = token; if (isInit) {
// 防止重新注册事件
if(hasInit){
return; return;
} }
hasInit = true; isInit = true;
uni.onSocketOpen((res) => { uni.onSocketOpen((res) => {
console.log("WebSocket连接已打开"); console.log("WebSocket连接已打开");
isConnect = true; isConnect = true;
@ -35,8 +32,6 @@ let init = (url, token) => {
if (sendInfo.cmd == 0) { if (sendInfo.cmd == 0) {
heartCheck.start() heartCheck.start()
console.log('WebSocket登录成功') console.log('WebSocket登录成功')
// 登录成功才算连接完成
openCallBack && openCallBack();
} else if (sendInfo.cmd == 1) { } else if (sendInfo.cmd == 1) {
// 重新开启心跳定时 // 重新开启心跳定时
heartCheck.reset(); heartCheck.reset();
@ -48,7 +43,6 @@ let init = (url, token) => {
}) })
uni.onSocketClose((res) => { uni.onSocketClose((res) => {
console.log(res)
console.log('WebSocket连接关闭') console.log('WebSocket连接关闭')
isConnect = false; //断开后修改标识 isConnect = false; //断开后修改标识
closeCallBack && closeCallBack(res); closeCallBack && closeCallBack(res);
@ -64,7 +58,9 @@ let init = (url, token) => {
}) })
}; };
let connect = ()=>{ let connect = (url, token) => {
wsurl = url;
accessToken = token;
if (isConnect) { if (isConnect) {
return; return;
} }
@ -83,6 +79,18 @@ let connect = ()=>{
}); });
} }
//定义重连函数
let reconnect = (wsurl, accessToken) => {
console.log("尝试重新连接");
if (isConnect) {
//如果已经连上就不在重连了
return;
}
rec && clearTimeout(rec);
rec = setTimeout(function() { // 延迟15秒重连 避免过多次过频繁请求重连
connect(wsurl, accessToken);
}, 15000);
};
//设置关闭连接 //设置关闭连接
let close = () => { let close = () => {
@ -142,9 +150,6 @@ function onMessage(callback) {
messageCallBack = callback; messageCallBack = callback;
} }
function onOpen(callback) {
openCallBack = callback;
}
function onClose(callback) { function onClose(callback) {
closeCallBack = callback; closeCallBack = callback;
@ -155,9 +160,9 @@ function onClose(callback) {
export { export {
init, init,
connect, connect,
reconnect,
close, close,
sendMessage, sendMessage,
onMessage, onMessage,
onOpen,
onClose onClose
} }

176
im-uniapp/components/chat-at-box/chat-at-box.vue

@ -0,0 +1,176 @@
<template>
<uni-popup ref="popup" type="bottom" @change="onChange">
<view class="chat-at-box">
<view class="chat-at-top">
<view class="chat-at-tip"> 选择要提醒的人</view>
<button class="chat-at-btn" type="warn" size="mini" @click="onClean()">清空 </button>
<button class="chat-at-btn" type="primary" size="mini" @click="onOk()">确定({{atUserIds.length}})
</button>
</view>
<scroll-view v-show="atUserIds.length>0" scroll-x="true" scroll-left="120">
<view class="at-user-items">
<view v-for="m in showMembers" v-show="m.checked" class="at-user-item">
<head-image :name="m.aliasName" :url="m.headImage" :size="60"></head-image>
</view>
</view>
</scroll-view>
<view class="search-bar">
<uni-search-bar v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar>
</view>
<view class="member-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="m in showMembers" v-show="m.aliasName.startsWith(searchText)"
:key="m.userId">
<view class="member-item" @click="onSwitchChecked(m)">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
:size="90"></head-image>
<view class="member-name">{{ m.aliasName}}</view>
<view class="member-checked">
<radio :checked="m.checked" @click.stop="onSwitchChecked(m)" />
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: "chat-at-box",
props: {
ownerId: {
type: Number,
},
members: {
type: Array
}
},
data() {
return {
searchText: "",
showMembers:[]
};
},
methods: {
init(atUserIds) {
this.showMembers = [];
let userId = this.$store.state.userStore.userInfo.id;
if(this.ownerId == userId){
this.showMembers.push({
userId:-1,
aliasName: "全体成员"
})
}
this.members.forEach((m) => {
if(m.userId != userId){
m.checked = atUserIds.indexOf(m.userId) >= 0;
this.showMembers.push(m);
}
});
},
open() {
this.$refs.popup.open();
},
onSwitchChecked(member) {
member.checked = !member.checked;
},
onClean() {
this.showMembers.forEach((m) => {
m.checked = false;
})
},
onOk() {
this.$refs.popup.close();
},
onChange(e) {
if (!e.show) {
this.$emit("complete", this.atUserIds)
}
}
},
computed: {
atUserIds() {
let ids = [];
this.showMembers.forEach((m) => {
if (m.checked) {
ids.push(m.userId);
}
})
return ids;
}
}
}
</script>
<style lang="scss" scoped>
.chat-at-box {
position: relative;
border: #dddddd solid 1rpx;
display: flex;
flex-direction: column;
background-color: white;
padding: 10rpx;
border-radius: 15rpx;
.chat-at-top {
display: flex;
align-items: center;
height: 70rpx;
padding: 10rpx;
.chat-at-tip {
flex: 1;
}
.chat-at-btn {
margin-left: 10rpx;
}
}
.at-user-items {
display: flex;
align-items: center;
height: 90rpx;
.at-user-item {
padding: 3rpx;
}
}
.member-items {
position: relative;
flex: 1;
overflow: hidden;
.member-item {
height: 120rpx;
display: flex;
position: relative;
padding: 0 30rpx;
align-items: center;
background-color: white;
white-space: nowrap;
.member-name {
flex: 1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
.scroll-bar {
height: 800rpx;
}
}
}
</style>

59
im-uniapp/components/chat-item/chat-item.vue

@ -1,16 +1,18 @@
<template> <template>
<view class="chat-item" @click="showChatBox()"> <view class="chat-item" @click="showChatBox()">
<view class="left"> <view class="left">
<head-image :url="chat.headImage" :name="chat.showName" :size="100"></head-image> <head-image :url="chat.headImage" :name="chat.showName" :size="90"></head-image>
<view v-if="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</view> <view v-if="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</view>
</view> </view>
<view class="chat-right"> <view class="chat-right">
<view class="chat-name"> <view class="chat-name">
{{ chat.showName}} <view class="chat-name-text">{{chat.showName}}</view>
<view class="chat-time">{{$date.toTimeText(chat.lastSendTime)}}</view>
</view> </view>
<view class="chat-content"> <view class="chat-content">
<view class="chat-content-text" v-html="$emo.transform(chat.lastContent)"></view> <view class="chat-at-text">{{atText}}</view>
<view class="chat-time">{{$date.toTimeText(chat.lastSendTime)}}</view> <view class="chat-send-name" v-show="chat.sendNickName">{{chat.sendNickName+':&nbsp;'}}</view>
<rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent)"></rich-text>
</view> </view>
</view> </view>
</view> </view>
@ -36,19 +38,29 @@
url: "/pages/chat/chat-box?chatIdx=" + this.index url: "/pages/chat/chat-box?chatIdx=" + this.index
}) })
} }
},
computed: {
atText() {
if (this.chat.atMe) {
return "[有人@我]"
} else if (this.chat.atAll) {
return "[@全体成员]"
}
return "";
}
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.chat-item { .chat-item {
height: 120rpx; height: 100rpx;
display: flex; display: flex;
margin-bottom: 2rpx; margin-bottom: 2rpx;
position: relative; position: relative;
padding-left: 30rpx; padding: 10rpx;
padding-left: 20rpx;
align-items: center; align-items: center;
padding-right: 10rpx;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
@ -87,15 +99,38 @@
overflow: hidden; overflow: hidden;
.chat-name { .chat-name {
display: flex;
line-height: 50rpx;
height: 50rpx;
.chat-name-text {
flex: 1;
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
line-height: 60rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.chat-time {
font-size: 26rpx;
text-align: right;
color: #888888;
white-space: nowrap;
overflow: hidden;
}
}
.chat-content { .chat-content {
display: flex; display: flex;
line-height: 44rpx;
.chat-at-text {
color: #c70b0b;
font-size: 24rpx;
}
.chat-send-name {
font-size: 26rpx;
}
.chat-content-text { .chat-content-text {
flex: 1; flex: 1;
@ -105,14 +140,6 @@
line-height: 50rpx; line-height: 50rpx;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.chat-time {
font-size: 26rpx;
text-align: right;
color: #888888;
white-space: nowrap;
overflow: hidden;
}
} }
} }
} }

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

@ -7,7 +7,7 @@
<view class="chat-msg-normal" v-if="msgInfo.type>=0 && msgInfo.type<10" <view class="chat-msg-normal" v-if="msgInfo.type>=0 && msgInfo.type<10"
:class="{'chat-msg-mine':msgInfo.selfSend}"> :class="{'chat-msg-mine':msgInfo.selfSend}">
<head-image class="avatar" :id="msgInfo.sendId" :url="headImage" :name="showName" :size="80"></head-image> <head-image class="avatar" @longpress="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage" :name="showName" :size="80"></head-image>
<view class="chat-msg-content" @longpress="onShowMenu($event)"> <view class="chat-msg-content" @longpress="onShowMenu($event)">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top"> <view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<text>{{showName}}</text> <text>{{showName}}</text>
@ -231,7 +231,7 @@
line-height: 60rpx; line-height: 60rpx;
margin-top: 10rpx; margin-top: 10rpx;
padding: 10rpx 20rpx; padding: 10rpx 20rpx;
background-color: #ebebf5; background-color: white;
border-radius: 10rpx; border-radius: 10rpx;
color: #333; color: #333;
font-size: 30rpx; font-size: 30rpx;
@ -239,17 +239,17 @@
display: block; display: block;
word-break: break-all; word-break: break-all;
white-space: pre-line; white-space: pre-line;
box-shadow: 2px 2px 2px #c0c0f0; box-shadow: 1px 1px 1px #c0c0f0;
&:after { &:after {
content: ""; content: "";
position: absolute; position: absolute;
left: -20rpx; left: -20rpx;
top: 26rpx; top: 26rpx;
width: 0; width: 6rpx;
height: 0; height: 6rpx;
border-style: solid dashed dashed; border-style: solid dashed dashed;
border-color: #ebebf5 transparent transparent; border-color: white transparent transparent;
overflow: hidden; overflow: hidden;
border-width: 18rpx; border-width: 18rpx;
} }

8
im-uniapp/components/friend-item/friend-item.vue

@ -1,7 +1,7 @@
<template> <template>
<view class="friend-item" @click="showFriendInfo()"> <view class="friend-item" @click="showFriendInfo()">
<head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage" <head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage"
:size="100"></head-image> :size="90"></head-image>
<view class="friend-info"> <view class="friend-info">
<view class="friend-name">{{ friend.nickName}}</view> <view class="friend-name">{{ friend.nickName}}</view>
<view class="friend-online"> <view class="friend-online">
@ -37,13 +37,13 @@
<style scope lang="scss"> <style scope lang="scss">
.friend-item { .friend-item {
height: 120rpx; height: 100rpx;
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 1rpx;
position: relative; position: relative;
padding-left: 30rpx; padding: 10rpx;
padding-left: 20rpx;
align-items: center; align-items: center;
padding-right: 10rpx;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;

8
im-uniapp/components/group-item/group-item.vue

@ -1,7 +1,7 @@
<template> <template>
<view class="group-item" @click="showGroupInfo()"> <view class="group-item" @click="showGroupInfo()">
<head-image :name="group.remark" <head-image :name="group.remark"
:url="group.headImage" :size="100"></head-image> :url="group.headImage" :size="90"></head-image>
<view class="group-name"> <view class="group-name">
<view>{{ group.remark}}</view> <view>{{ group.remark}}</view>
</view> </view>
@ -31,13 +31,13 @@
<style scope lang="scss"> <style scope lang="scss">
.group-item { .group-item {
height: 120rpx; height: 100rpx;
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 1rpx;
position: relative; position: relative;
padding-left: 30rpx; padding: 10rpx;
padding-left: 20rpx;
align-items: center; align-items: center;
padding-right: 10rpx;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {

6
im-uniapp/components/head-image/head-image.vue

@ -1,5 +1,5 @@
<template> <template>
<view class="head-image" @click="showUserInfo($event)"> <view class="head-image" @click="showUserInfo($event)" :title="name">
<image class="avatar-image" v-if="url" :src="url" <image class="avatar-image" v-if="url" :src="url"
:style="avatarImageStyle" lazy-load="true" mode="aspectFill"/> :style="avatarImageStyle" lazy-load="true" mode="aspectFill"/>
<view class="avatar-text" v-if="!url" :style="avatarTextStyle"> <view class="avatar-text" v-if="!url" :style="avatarTextStyle">
@ -26,7 +26,7 @@
}, },
size: { size: {
type: Number, type: Number,
default: 50 default: 20
}, },
url: { url: {
type: String type: String
@ -77,6 +77,8 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
border-radius: 10%; border-radius: 10%;
border: 1px solid #ccc;
vertical-align: bottom;
} }
.avatar-text { .avatar-text {

8
im-uniapp/package.json

@ -6,16 +6,16 @@
"browser":"chrome", "browser":"chrome",
"env": { "env": {
"UNI_PLATFORM": "h5", "UNI_PLATFORM": "h5",
"BASE_URL": "http://192.168.1.5:8888", "BASE_URL": "http://192.168.43.6:8888",
"WS_URL": "ws://192.168.1.5:8878/im" "WS_URL": "ws://192.168.43.6:8878/im"
} }
}, },
"dev-wx-mini": { "dev-wx-mini": {
"title": "开发环境-微信小程序", "title": "开发环境-微信小程序",
"env": { "env": {
"UNI_PLATFORM": "mp-weixin", "UNI_PLATFORM": "mp-weixin",
"BASE_URL": "http://192.168.1.5:8888", "BASE_URL": "http://192.168.43.6:8888",
"WS_URL": "ws://192.168.1.5:8878/im" "WS_URL": "ws://192.168.43.6:8878/im"
} }
}, },
"prod-h5": { "prod-h5": {

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

@ -9,24 +9,35 @@
<scroll-view class="scroll-box" scroll-y="true" @scrolltoupper="onScrollToTop" <scroll-view class="scroll-box" scroll-y="true" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-'+scrollMsgIdx"> :scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx"> <view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" <chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile" :showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
:id="'chat-item-'+idx" :msgInfo="msgInfo"> @longPressHead="onLongPressHead(msgInfo)"
@download="onDownloadFile" :id="'chat-item-'+idx" :msgInfo="msgInfo">
</chat-message-item> </chat-message-item>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<view v-if="atUserIds.length>0" class="chat-at-bar" @click="openAtBox()">
<view class="iconfont icon-at">:&nbsp;</view>
<scroll-view v-if="atUserIds.length>0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">
<view class="chat-at-items">
<view v-for="m in atUserItems" class="chat-at-item">
<head-image :name="m.aliasName" :url="m.headImage" :size="50"></head-image>
</view>
</view>
</scroll-view>
</view>
<view class="send-bar"> <view class="send-bar">
<view class="iconfont icon-voice-circle" @click="showTip()"></view>
<view class="send-text"> <view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false" <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange" :adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange"
confirm-type="send" confirm-hold :hold-keyboard="true"></textarea> confirm-type="send" confirm-hold :hold-keyboard="true"></textarea>
</view> </view>
<view v-if="chat.type=='GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view> <view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view>
<view v-show="sendText==''" class="iconfont icon-add" @click="switchChatTabBox('tools',true)"> <view v-if="sendText==''" class="iconfont icon-add" @click="switchChatTabBox('tools',true)">
</view> </view>
<button v-show="sendText!=''" class="btn-send" type="primary" @touchend.prevent="sendTextMessage()" <button v-if="sendText!=''||atUserIds.length>0" class="btn-send" type="primary" @touchend.prevent="sendTextMessage()"
size="mini">发送</button> size="mini">发送</button>
</view> </view>
@ -53,7 +64,6 @@
</file-upload> </file-upload>
<view class="tool-name">文件</view> <view class="tool-name">文件</view>
</view> </view>
<view class="chat-tools-item" @click="showTip()"> <view class="chat-tools-item" @click="showTip()">
<view class="tool-icon iconfont icon-microphone"></view> <view class="tool-icon iconfont icon-microphone"></view>
<view class="tool-name">语音输入</view> <view class="tool-name">语音输入</view>
@ -62,9 +72,7 @@
<view class="tool-icon iconfont icon-call"></view> <view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">呼叫</view> <view class="tool-name">呼叫</view>
</view> </view>
</view> </view>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true"> <scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
<view class="emotion-item-list"> <view class="emotion-item-list">
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)" <image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
@ -74,7 +82,8 @@
</scroll-view> </scroll-view>
<view v-if="showKeyBoard"></view> <view v-if="showKeyBoard"></view>
</view> </view>
<chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers"
@complete="onAtComplete"></chat-at-box>
</view> </view>
</template> </template>
@ -92,6 +101,7 @@
chatTabBox: 'none', chatTabBox: 'none',
showKeyBoard: false, showKeyBoard: false,
keyboardHeight: 322, keyboardHeight: 322,
atUserIds: [],
showMinIdx: 0 // showMinIdx showMinIdx: 0 // showMinIdx
} }
}, },
@ -102,6 +112,18 @@
icon: "none" icon: "none"
}) })
}, },
openAtBox() {
this.$refs.atBox.init(this.atUserIds);
this.$refs.atBox.open();
},
onAtComplete(atUserIds) {
this.atUserIds = atUserIds;
},
onLongPressHead(msgInfo){
if(!msgInfo.selfSend && this.chat.type=="GROUP" && this.atUserIds.indexOf(msgInfo.sendId)<0){
this.atUserIds.push(msgInfo.sendId);
}
},
headImage(msgInfo) { headImage(msgInfo) {
if (this.chat.type == 'GROUP') { if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
@ -117,17 +139,19 @@
} else { } else {
return msgInfo.selfSend ? this.mine.nickName : this.chat.showName return msgInfo.selfSend ? this.mine.nickName : this.chat.showName
} }
}, },
sendTextMessage() { sendTextMessage() {
if (!this.sendText.trim()) { if (!this.sendText.trim() && this.atUserIds.length==0) {
return uni.showToast({ return uni.showToast({
title: "不能发送空白信息", title: "不能发送空白信息",
icon: "none" icon: "none"
}); });
} }
let atText = this.createAtText()
let msgInfo = { let msgInfo = {
content: this.sendText, content: this.sendText + atText,
atUserIds: this.atUserIds,
type: 0 type: 0
} }
// id // id
@ -148,8 +172,24 @@
}).finally(() => { }).finally(() => {
// //
this.scrollToBottom(); this.scrollToBottom();
// @
this.atUserIds = [];
}); });
}, },
createAtText() {
let atText = "";
this.atUserIds.forEach((id) => {
if(id==-1){
atText += ` @全体成员`;
}else{
let member = this.groupMembers.find((m)=>m.userId==id);
if (member) {
atText += ` @${member.aliasName}`;
}
}
})
return atText;
},
fillTargetId(msgInfo, targetId) { fillTargetId(msgInfo, targetId) {
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
msgInfo.groupId = targetId; msgInfo.groupId = targetId;
@ -450,6 +490,20 @@
}, },
unreadCount() { unreadCount() {
return this.chat.unreadCount; return this.chat.unreadCount;
},
atUserItems(){
let atUsers = [];
this.atUserIds.forEach((id)=>{
if(id==-1){
atUsers.push({id:-1,aliasName:"全体成员"})
return;
}
let member = this.groupMembers.find((m)=>m.userId==id);
if(member){
atUsers.push(member);
}
})
return atUsers;
} }
}, },
watch: { watch: {
@ -527,8 +581,6 @@
right: 30rpx; right: 30rpx;
} }
} }
} }
@ -538,13 +590,41 @@
border: #dddddd solid 1px; border: #dddddd solid 1px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: white; background-color: #f8f8f8;
.scroll-box { .scroll-box {
height: 100%; height: 100%;
} }
} }
.chat-at-bar {
display: flex;
align-items: center;
padding: 0 10rpx;
border: #dddddd solid 1px;
.icon-at {
font-size: 35rpx;
color: darkblue;
font-weight: 600;
}
.chat-at-scroll-box {
flex: 1;
width: 80%;
.chat-at-items {
display: flex;
align-items: center;
height: 70rpx;
.chat-at-item {
padding: 0 3rpx;
}
}
}
}
.send-bar { .send-bar {
display: flex; display: flex;
align-items: center; align-items: center;
@ -554,7 +634,7 @@
background-color: white; background-color: white;
.iconfont { .iconfont {
font-size: 70rpx; font-size: 60rpx;
margin: 3rpx; margin: 3rpx;
} }
@ -573,7 +653,6 @@
.send-text-area { .send-text-area {
width: 100%; width: 100%;
} }
} }
.btn-send { .btn-send {
@ -588,7 +667,6 @@
background-color: whitesmoke; background-color: whitesmoke;
.chat-tools { .chat-tools {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
@ -611,7 +689,6 @@
height: 60rpx; height: 60rpx;
line-height: 60rpx; line-height: 60rpx;
font-size: 25rpx; font-size: 25rpx;
} }
} }
} }

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

@ -18,8 +18,6 @@
</view> </view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>

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

@ -48,8 +48,6 @@
}).then(data => { }).then(data => {
console.log("登录成功,自动跳转到聊天页面...") console.log("登录成功,自动跳转到聊天页面...")
uni.setStorageSync("loginInfo", data); uni.setStorageSync("loginInfo", data);
uni.setStorageSync("userName",this.loginForm.userName)
uni.setStorageSync("password",this.loginForm.password)
// App.vue // App.vue
getApp().init() getApp().init()
// //
@ -68,6 +66,7 @@
uni.switchTab({ uni.switchTab({
url: "/pages/chat/chat" url: "/pages/chat/chat"
}) })
} }
} }
} }

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

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

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

Binary file not shown.

47
im-uniapp/store/chatStore.js

@ -35,9 +35,11 @@ export default {
if (state.chats[i].type == chatInfo.type && if (state.chats[i].type == chatInfo.type &&
state.chats[i].targetId === chatInfo.targetId) { state.chats[i].targetId === chatInfo.targetId) {
chat = state.chats[i]; chat = state.chats[i];
// 放置头部 // 放置头部(这个操作非常耗资源,正在加载消息时不执行)
if(!state.loadingPrivateMsg && !state.loadingPrivateMsg){
state.chats.splice(i, 1); state.chats.splice(i, 1);
state.chats.unshift(chat); state.chats.unshift(chat);
}
break; break;
} }
} }
@ -68,6 +70,8 @@ export default {
if (state.chats[idx].type == chatInfo.type && if (state.chats[idx].type == chatInfo.type &&
state.chats[idx].targetId == chatInfo.targetId) { state.chats[idx].targetId == chatInfo.targetId) {
state.chats[idx].unreadCount = 0; state.chats[idx].unreadCount = 0;
state.chats[idx].atMe = false;
state.chats[idx].atAll = false;
} }
} }
this.commit("saveToStorage"); this.commit("saveToStorage");
@ -77,7 +81,6 @@ export default {
if (state.chats[idx].type == 'PRIVATE' && if (state.chats[idx].type == 'PRIVATE' &&
state.chats[idx].targetId == friendId) { state.chats[idx].targetId == friendId) {
state.chats[idx].messages.forEach((m) => { state.chats[idx].messages.forEach((m) => {
console.log("readedMessage")
if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) { if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) {
m.status = MESSAGE_STATUS.READED m.status = MESSAGE_STATUS.READED
} }
@ -126,7 +129,8 @@ export default {
break; break;
} }
} }
// 插入新的数据 // 会话列表内容
if(!state.loadingPrivateMsg && !state.loadingPrivateMsg){
if (msgInfo.type == MESSAGE_TYPE.IMAGE) { if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
chat.lastContent = "[图片]"; chat.lastContent = "[图片]";
} else if (msgInfo.type == MESSAGE_TYPE.FILE) { } else if (msgInfo.type == MESSAGE_TYPE.FILE) {
@ -137,10 +141,24 @@ export default {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName;
}
// 未读加1 // 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) { if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++; chat.unreadCount++;
} }
// 是否有人@我
if(!msgInfo.selfSend && chat.type=="GROUP" && msgInfo.atUserIds){
let userId = userStore.state.userInfo.id;
if(msgInfo.atUserIds.indexOf(userId)>=0){
chat.atMe = true;
}
if(msgInfo.atUserIds.indexOf(-1)>=0){
chat.atAll = true;
}
}
// 记录消息的最大id // 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) { if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id; state.privateMsgMaxId = msgInfo.id;
@ -232,6 +250,29 @@ export default {
loadingGroupMsg(state, loadding) { loadingGroupMsg(state, loadding) {
state.loadingGroupMsg = loadding; state.loadingGroupMsg = loadding;
}, },
refreshChats(state){
state.chats.forEach((chat)=>{
if(chat.messages.length>0){
let msgInfo = chat.messages[chat.messages.length-1];
if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
chat.lastContent = "[图片]";
} else if (msgInfo.type == MESSAGE_TYPE.FILE) {
chat.lastContent = "[文件]";
} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
chat.lastContent = "[语音]";
} else {
chat.lastContent = msgInfo.content;
}
chat.lastSendTime = msgInfo.sendTime;
}else{
chat.lastContent = "";
chat.lastSendTime = new Date()
}
})
state.chats.sort((chat1, chat2) => {
return chat2.lastSendTime-chat1.lastSendTime;
});
},
saveToStorage(state) { saveToStorage(state) {
let userId = userStore.state.userInfo.id; let userId = userStore.state.userInfo.id;
let key = "chats-" + userId; let key = "chats-" + userId;

1
im-uniapp/store/friendStore.js

@ -36,7 +36,6 @@ export default {
state.friends.forEach((f) => { state.friends.forEach((f) => {
let userTerminal = onlineTerminals.find((o) => f.id == o.userId); let userTerminal = onlineTerminals.find((o) => f.id == o.userId);
if (userTerminal) { if (userTerminal) {
console.log(userTerminal)
f.online = true; f.online = true;
f.onlineTerminals = userTerminal.terminals; f.onlineTerminals = userTerminal.terminals;
f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB) >= 0 f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB) >= 0

Loading…
Cancel
Save