Browse Source

!152 优化: ios h5上拉消息滚动条异常问题

Merge pull request !152 from blue/v_3.0.0
master
blue 9 months ago
committed by Gitee
parent
commit
e24c5e078a
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 2
      db/im-platform.sql
  2. 9
      im-platform/src/main/java/com/bx/implatform/controller/FriendController.java
  3. 8
      im-platform/src/main/java/com/bx/implatform/controller/GroupController.java
  4. 23
      im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java
  5. 23
      im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java
  6. 5
      im-platform/src/main/java/com/bx/implatform/entity/Friend.java
  7. 6
      im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java
  8. 2
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  9. 7
      im-platform/src/main/java/com/bx/implatform/service/FriendService.java
  10. 9
      im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java
  11. 7
      im-platform/src/main/java/com/bx/implatform/service/GroupService.java
  12. 31
      im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java
  13. 8
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java
  14. 27
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  15. 4
      im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java
  16. 3
      im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java
  17. 27
      im-uniapp/App.vue
  18. 2
      im-uniapp/common/enums.js
  19. 6
      im-uniapp/components/chat-item/chat-item.vue
  20. 2
      im-uniapp/components/head-image/head-image.vue
  21. 10
      im-uniapp/components/long-press-menu/long-press-menu.vue
  22. 36
      im-uniapp/im.scss
  23. 55
      im-uniapp/pages/chat/chat-box.vue
  24. 4
      im-uniapp/pages/chat/chat.vue
  25. 51
      im-uniapp/pages/common/user-info.vue
  26. 50
      im-uniapp/pages/group/group-info.vue
  27. 34
      im-uniapp/static/icon/iconfont.css
  28. BIN
      im-uniapp/static/icon/iconfont.ttf
  29. 46
      im-uniapp/store/chatStore.js
  30. 4
      im-uniapp/store/friendStore.js
  31. 4
      im-uniapp/store/groupStore.js
  32. 2
      im-web/src/api/enums.js
  33. 26
      im-web/src/assets/iconfont/iconfont.css
  34. BIN
      im-web/src/assets/iconfont/iconfont.ttf
  35. 41
      im-web/src/components/chat/ChatItem.vue
  36. 8
      im-web/src/components/common/UserInfo.vue
  37. 31
      im-web/src/store/chatStore.js
  38. 4
      im-web/src/store/friendStore.js
  39. 4
      im-web/src/store/groupStore.js
  40. 37
      im-web/src/view/Chat.vue
  41. 11
      im-web/src/view/Friend.vue
  42. 1
      im-web/src/view/Group.vue
  43. 25
      im-web/src/view/Home.vue

2
db/im-platform.sql

@ -22,6 +22,7 @@ create table `im_friend`(
`friend_id` bigint not null comment '好友id', `friend_id` bigint not null comment '好友id',
`friend_nick_name` varchar(255) not null comment '好友昵称', `friend_nick_name` varchar(255) not null comment '好友昵称',
`friend_head_image` varchar(255) default '' comment '好友头像', `friend_head_image` varchar(255) default '' comment '好友头像',
`is_dnd` tinyint comment '免打扰标识(Do Not Disturb) 0:关闭 1:开启',
`deleted` tinyint comment '删除标识 0:正常 1:已删除', `deleted` tinyint comment '删除标识 0:正常 1:已删除',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间',
key `idx_user_id` (`user_id`), key `idx_user_id` (`user_id`),
@ -62,6 +63,7 @@ create table `im_group_member`(
`remark_nick_name` varchar(255) DEFAULT '' comment '显示昵称备注', `remark_nick_name` varchar(255) DEFAULT '' comment '显示昵称备注',
`head_image` varchar(255) DEFAULT '' comment '用户头像', `head_image` varchar(255) DEFAULT '' comment '用户头像',
`remark_group_name` varchar(255) DEFAULT '' comment '显示群名备注', `remark_group_name` varchar(255) DEFAULT '' comment '显示群名备注',
`is_dnd` tinyint comment '免打扰标识(Do Not Disturb) 0:关闭 1:开启',
`quit` tinyint(1) DEFAULT 0 comment '是否已退出', `quit` tinyint(1) DEFAULT 0 comment '是否已退出',
`quit_time` datetime DEFAULT NULL comment '退出时间', `quit_time` datetime DEFAULT NULL comment '退出时间',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间',

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

@ -1,12 +1,14 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit; import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.dto.FriendDndDTO;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.FriendService; import com.bx.implatform.service.FriendService;
import com.bx.implatform.vo.FriendVO; import com.bx.implatform.vo.FriendVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -50,5 +52,12 @@ public class FriendController {
return ResultUtils.success(); return ResultUtils.success();
} }
@PutMapping("/dnd")
@Operation(summary = "开启/关闭免打扰状态", description = "开启/关闭免打扰状态")
public Result setFriendDnd(@Valid @RequestBody FriendDndDTO dto) {
friendService.setDnd(dto);
return ResultUtils.success();
}
} }

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

@ -1,6 +1,7 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit; import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.dto.GroupDndDTO;
import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.dto.GroupInviteDTO;
import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.dto.GroupMemberRemoveDTO;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
@ -101,5 +102,12 @@ public class GroupController {
return ResultUtils.success(); return ResultUtils.success();
} }
@Operation(summary = "开启/关闭免打扰", description = "开启/关闭免打扰")
@PutMapping("/dnd")
public Result setGroupDnd(@Valid @RequestBody GroupDndDTO dto) {
groupService.setDnd(dto);
return ResultUtils.success();
}
} }

23
im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java

@ -0,0 +1,23 @@
package com.bx.implatform.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* @author Blue
* @version 1.0
*/
@Data
@Schema(description = "好友免打扰")
public class FriendDndDTO {
@NotNull(message = "好友id不可为空")
@Schema(description = "好友用户id")
private Long friendId;
@NotNull(message = "消息免打扰状态不可为空")
@Schema(description = "消息免打扰状态")
private Boolean isDnd;
}

23
im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java

@ -0,0 +1,23 @@
package com.bx.implatform.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* @author Blue
* @version 1.0
* @date 2025-02-23
*/
@Data
@Schema(description = "群聊免打扰")
public class GroupDndDTO {
@NotNull(message = "群id不可为空")
@Schema(description = "群组id")
private Long groupId;
@NotNull(message = "免打扰状态不可为空")
@Schema(description = "免打扰状态")
private Boolean isDnd;
}

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

@ -45,6 +45,11 @@ public class Friend{
*/ */
private String friendHeadImage; private String friendHeadImage;
/**
* 是否开启免打扰
*/
private Boolean isDnd;
/** /**
* 是否已删除 * 是否已删除
*/ */

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

@ -58,6 +58,12 @@ public class GroupMember extends Model<GroupMember> {
*/ */
private String remarkGroupName; private String remarkGroupName;
/**
* 是否免打扰
*/
private Boolean isDnd;
/** /**
* 是否已退出 * 是否已退出
*/ */

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

@ -37,8 +37,10 @@ public enum MessageType {
GROUP_UNBAN(52,"群聊解封"), GROUP_UNBAN(52,"群聊解封"),
FRIEND_NEW(80, "新增好友"), FRIEND_NEW(80, "新增好友"),
FRIEND_DEL(81, "删除好友"), FRIEND_DEL(81, "删除好友"),
FRIEND_DND(82, "好友免打扰"),
GROUP_NEW(90, "新增群聊"), GROUP_NEW(90, "新增群聊"),
GROUP_DEL(91, "删除群聊"), GROUP_DEL(91, "删除群聊"),
GROUP_DND(92, "群聊免打扰"),
RTC_CALL_VOICE(100, "语音呼叫"), RTC_CALL_VOICE(100, "语音呼叫"),
RTC_CALL_VIDEO(101, "视频呼叫"), RTC_CALL_VIDEO(101, "视频呼叫"),
RTC_ACCEPT(102, "接受"), RTC_ACCEPT(102, "接受"),

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

@ -1,6 +1,7 @@
package com.bx.implatform.service; package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.implatform.dto.FriendDndDTO;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.Friend;
import com.bx.implatform.vo.FriendVO; import com.bx.implatform.vo.FriendVO;
@ -70,4 +71,10 @@ public interface FriendService extends IService<Friend> {
*/ */
void bindFriend(Long userId, Long friendId); void bindFriend(Long userId, Long friendId);
/**
* 设置好友免打扰状态
* @param dto
*/
void setDnd(FriendDndDTO dto);
} }

9
im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java

@ -1,6 +1,7 @@
package com.bx.implatform.service; package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.implatform.dto.GroupDndDTO;
import com.bx.implatform.entity.GroupMember; import com.bx.implatform.entity.GroupMember;
import java.util.List; import java.util.List;
@ -90,4 +91,12 @@ public interface GroupMemberService extends IService<GroupMember> {
* @param userIds 用户id * @param userIds 用户id
*/ */
Boolean isInGroup(Long groupId,List<Long> userIds); Boolean isInGroup(Long groupId,List<Long> userIds);
/**
* 设置免打扰状态
* @param groupId 群id
* @param userId 用户id
* @param isDnd 是否开启免打扰
*/
void setDnd(Long groupId, Long userId, Boolean isDnd);
} }

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

@ -1,6 +1,7 @@
package com.bx.implatform.service; package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.implatform.dto.GroupDndDTO;
import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.dto.GroupInviteDTO;
import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.dto.GroupMemberRemoveDTO;
import com.bx.implatform.entity.Group; import com.bx.implatform.entity.Group;
@ -92,4 +93,10 @@ public interface GroupService extends IService<Group> {
* @return List<GroupMemberVO> * @return List<GroupMemberVO>
**/ **/
List<GroupMemberVO> findGroupMembers(Long groupId); List<GroupMemberVO> findGroupMembers(Long groupId);
/**
* 开启/关闭免打扰
* @param dto
*/
void setDnd(GroupDndDTO dto);
} }

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

@ -11,6 +11,7 @@ import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.dto.FriendDndDTO;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.Friend;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.entity.User; import com.bx.implatform.entity.User;
@ -138,6 +139,18 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
sendAddFriendMessage(userId, friendId, friend); sendAddFriendMessage(userId, friendId, friend);
} }
@Override
public void setDnd(FriendDndDTO dto) {
UserSession session = SessionContext.getSession();
LambdaUpdateWrapper<Friend> wrapper = Wrappers.lambdaUpdate();
wrapper.eq(Friend::getUserId, session.getUserId());
wrapper.eq(Friend::getFriendId, dto.getFriendId());
wrapper.set(Friend::getIsDnd, dto.getIsDnd());
this.update(wrapper);
// 推送同步消息
sendSyncDndMessage(dto.getFriendId(), dto.getIsDnd());
}
/** /**
* 单向解除好友关系 * 单向解除好友关系
* *
@ -175,6 +188,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
vo.setHeadImage(f.getFriendHeadImage()); vo.setHeadImage(f.getFriendHeadImage());
vo.setNickName(f.getFriendNickName()); vo.setNickName(f.getFriendNickName());
vo.setDeleted(f.getDeleted()); vo.setDeleted(f.getDeleted());
vo.setIsDnd(f.getIsDnd());
return vo; return vo;
} }
@ -254,4 +268,21 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
sendMessage.setData(messageInfo); sendMessage.setData(messageInfo);
imClient.sendPrivateMessage(sendMessage); imClient.sendPrivateMessage(sendMessage);
} }
void sendSyncDndMessage(Long friendId, Boolean isDnd) {
// 同步免打扰状态到其他终端
UserSession session = SessionContext.getSession();
PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setSendId(session.getUserId());
msgInfo.setRecvId(friendId);
msgInfo.setSendTime(new Date());
msgInfo.setType(MessageType.FRIEND_DND.code());
msgInfo.setContent(isDnd.toString());
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setData(msgInfo);
sendMessage.setSendToSelf(true);
imClient.sendPrivateMessage(sendMessage);
}
} }

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

@ -120,4 +120,12 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
return userIds.size() == this.count(wrapper); return userIds.size() == this.count(wrapper);
} }
@Override
public void setDnd(Long groupId, Long userId, Boolean isDnd) {
LambdaUpdateWrapper<GroupMember> wrapper = Wrappers.lambdaUpdate();
wrapper.eq(GroupMember::getGroupId, groupId);
wrapper.eq(GroupMember::getUserId, userId);
wrapper.set(GroupMember::getIsDnd, isDnd);
this.update(wrapper);
}
} }

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

@ -12,6 +12,7 @@ import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.util.CommaTextUtils; import com.bx.imcommon.util.CommaTextUtils;
import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.dto.GroupDndDTO;
import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.dto.GroupInviteDTO;
import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.dto.GroupMemberRemoveDTO;
import com.bx.implatform.entity.*; import com.bx.implatform.entity.*;
@ -314,6 +315,15 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList()); }).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList());
} }
@Override
public void setDnd(GroupDndDTO dto) {
UserSession session = SessionContext.getSession();
groupMemberService.setDnd(dto.getGroupId(), session.getUserId(), dto.getIsDnd());
// 推送同步消息
sendSyncDndMessage(dto.getGroupId(), dto.getIsDnd());
}
private void sendTipMessage(Long groupId, List<Long> recvIds, String content, Boolean sendToAll) { private void sendTipMessage(Long groupId, List<Long> recvIds, String content, Boolean sendToAll) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 消息入库 // 消息入库
@ -351,6 +361,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
vo.setShowNickName(member.getShowNickName()); vo.setShowNickName(member.getShowNickName());
vo.setShowGroupName(StrUtil.blankToDefault(member.getRemarkGroupName(), group.getName())); vo.setShowGroupName(StrUtil.blankToDefault(member.getRemarkGroupName(), group.getName()));
vo.setQuit(member.getQuit()); vo.setQuit(member.getQuit());
vo.setIsDnd(member.getIsDnd());
return vo; return vo;
} }
@ -386,4 +397,20 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
sendMessage.setSendToSelf(false); sendMessage.setSendToSelf(false);
imClient.sendGroupMessage(sendMessage); imClient.sendGroupMessage(sendMessage);
} }
private void sendSyncDndMessage(Long groupId, Boolean isDnd) {
UserSession session = SessionContext.getSession();
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.GROUP_DND.code());
msgInfo.setSendTime(new Date());
msgInfo.setGroupId(groupId);
msgInfo.setSendId(session.getUserId());
msgInfo.setContent(isDnd.toString());
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
imClient.sendGroupMessage(sendMessage);
}
} }

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

@ -20,6 +20,10 @@ public class FriendVO {
@Schema(description = "好友头像") @Schema(description = "好友头像")
private String headImage; private String headImage;
@Schema(description = "是否开启免打扰")
private Boolean isDnd;
@Schema(description = "是否已删除") @Schema(description = "是否已删除")
private Boolean deleted; private Boolean deleted;
} }

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

@ -56,5 +56,8 @@ public class GroupVO {
@Schema(description = "被封禁原因") @Schema(description = "被封禁原因")
private String reason; private String reason;
@Schema(description = "是否开启免打扰")
private Boolean isDnd;
} }

27
im-uniapp/App.vue

@ -146,6 +146,12 @@ export default {
this.friendStore.removeFriend(friendId); this.friendStore.removeFriend(friendId);
return; return;
} }
//
if (msg.type == enums.MESSAGE_TYPE.FRIEND_DND) {
this.friendStore.setDnd(friendId, JSON.parse(msg.content));
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return;
}
// //
let friend = this.loadFriendInfo(friendId); let friend = this.loadFriendInfo(friendId);
this.insertPrivateMessage(friend, msg); this.insertPrivateMessage(friend, msg);
@ -183,15 +189,21 @@ export default {
type: 'PRIVATE', type: 'PRIVATE',
targetId: friend.id, targetId: friend.id,
showName: friend.nickName, showName: friend.nickName,
headImage: friend.headImage headImage: friend.headImage,
isDnd: friend.isDnd
}; };
// //
this.chatStore.openChat(chatInfo); this.chatStore.openChat(chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo);
if (!friend.isDnd && !this.chatStore.isLoading() &&
!msg.selfSend && msgType.isNormal(msg.type) &&
msg.status != enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();
} }
}
}, },
@ -240,6 +252,12 @@ export default {
this.groupStore.removeGroup(msg.groupId); this.groupStore.removeGroup(msg.groupId);
return; return;
} }
//
if (msg.type == enums.MESSAGE_TYPE.GROUP_DND) {
this.groupStore.setDnd(msg.groupId, JSON.parse(msg.content));
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return;
}
// //
let group = this.loadGroupInfo(msg.groupId); let group = this.loadGroupInfo(msg.groupId);
this.insertGroupMessage(group, msg); this.insertGroupMessage(group, msg);
@ -291,15 +309,20 @@ export default {
type: 'GROUP', type: 'GROUP',
targetId: group.id, targetId: group.id,
showName: group.showGroupName, showName: group.showGroupName,
headImage: group.headImageThumb headImage: group.headImageThumb,
isDnd: group.isDnd
}; };
// //
this.chatStore.openChat(chatInfo); this.chatStore.openChat(chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!group.isDnd && !this.chatStore.isLoading() &&
!msg.selfSend && msgType.isNormal(msg.type) &&
msg.status != enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();
} }
}
}, },
loadFriendInfo(id, callback) { loadFriendInfo(id, callback) {

2
im-uniapp/common/enums.js

@ -16,8 +16,10 @@ const MESSAGE_TYPE = {
USER_BANNED: 50, USER_BANNED: 50,
FRIEND_NEW: 80, FRIEND_NEW: 80,
FRIEND_DEL: 81, FRIEND_DEL: 81,
FRIEND_DND: 82,
GROUP_NEW: 90, GROUP_NEW: 90,
GROUP_DEL: 91, GROUP_DEL: 91,
GROUP_DND: 92,
RTC_CALL_VOICE: 100, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101, RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,

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

@ -15,8 +15,10 @@
<view class="chat-content"> <view class="chat-content">
<view class="chat-at-text">{{ atText }}</view> <view class="chat-at-text">{{ atText }}</view>
<view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view> <view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view>
<rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent,'emoji-small')"></rich-text> <rich-text class="chat-content-text"
:nodes="$emo.transform(chat.lastContent,'emoji-small')"></rich-text>
<uni-badge v-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" /> <uni-badge v-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
<view v-if="chat.isDnd" class="icon iconfont icon-dnd"></view>
</view> </view>
</view> </view>
</view> </view>
@ -43,7 +45,7 @@ export default {
methods: { methods: {
showChatBox() { showChatBox() {
// //
if(!getApp().$vm.isInit || this.chatStore.isLoading()){ if (!getApp().$vm.isInit || this.chatStore.isLoading()) {
uni.showToast({ uni.showToast({
title: "正在初始化页面,请稍后...", title: "正在初始化页面,请稍后...",
icon: 'none' icon: 'none'

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

@ -1,5 +1,5 @@
<template> <template>
<view class="head-image" @click="showUserInfo($event)" :title="name"> <view class="head-image none-pointer-events" @click="showUserInfo($event)" :title="name">
<image class="avatar-image" v-if="url" :src="url" :style="avatarImageStyle" lazy-load="true" <image class="avatar-image" v-if="url" :src="url" :style="avatarImageStyle" lazy-load="true"
mode="aspectFill" /> mode="aspectFill" />
<view class="avatar-text" v-if="!url" :style="avatarTextStyle"> <view class="avatar-text" v-if="!url" :style="avatarTextStyle">

10
im-uniapp/components/long-press-menu/long-press-menu.vue

@ -1,5 +1,5 @@
<template> <template>
<view> <view class="long-press-menu none-pointer-events">
<view @longpress.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd"> <view @longpress.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd">
<slot></slot> <slot></slot>
</view> </view>
@ -81,7 +81,8 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.menu-mask { .long-press-menu {
.menu-mask {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
@ -90,9 +91,9 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 999; z-index: 999;
} }
.menu { .menu {
position: fixed; position: fixed;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
@ -117,5 +118,6 @@ export default {
margin-right: 10rpx; margin-right: 10rpx;
} }
} }
}
} }
</style> </style>

36
im-uniapp/im.scss

@ -102,7 +102,6 @@ button[size='mini'] {
} }
} }
.uni-radio-input svg{ .uni-radio-input svg{
border-color: white !important; border-color: white !important;
background-color: $im-color-primary !important; background-color: $im-color-primary !important;
@ -171,6 +170,26 @@ button[size='mini'] {
font-size: 10px !important; font-size: 10px !important;
font-weight: bolder !important; font-weight: bolder !important;
} }
.uni-switch-input-checked {
background-color: $im-color-primary-light-1 !important;
border-color: $im-color-primary-light-1 !important;
}
.uni-modal__title {
font-size: $im-font-size-larger !important;
}
.uni-modal__bd {
font-size: $im-font-size;
}
.uni-modal__ft {
font-size: $im-font-size;
line-height: 90rpx !important;
.uni-modal__btn_primary {
color: $im-color-primary !important;
}
}
.nav-bar { .nav-bar {
height: 100rpx; height: 100rpx;
@ -215,3 +234,18 @@ button[size='mini'] {
height: 36rpx !important; height: 36rpx !important;
vertical-align: bottom !important; vertical-align: bottom !important;
} }
.none-pointer-events {
uni-image img {
// 阻止微信默认长按菜单
pointer-events: none;
-webkit-pointer-events: none;
-ms-pointer-events: none;
-moz-pointer-events: none;
}
}
p {
margin-block-start: 1em;
margin-block-end: 1em;
}

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

@ -4,8 +4,9 @@
<view class="chat-main-box" :style="{height: chatMainHeight+'px'}"> <view class="chat-main-box" :style="{height: chatMainHeight+'px'}">
<view class="chat-message" @click="switchChatTabBox('none')"> <view class="chat-message" @click="switchChatTabBox('none')">
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop" <scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-' + scrollMsgIdx"> @scroll="onScroll" :scroll-into-view="'chat-item-' + scrollMsgIdx" :scroll-top="scrollTop">
<view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx"> <view v-if="chat" class="chat-wrap">
<view v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item :ref="'message'+msgInfo.id" v-if="idx >= showMinIdx" <chat-message-item :ref="'message'+msgInfo.id" v-if="idx >= showMinIdx"
:headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)" :showName="showName(msgInfo)" :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)" :showName="showName(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @copy="onCopyMessage" @recall="onRecallMessage" @delete="onDeleteMessage" @copy="onCopyMessage"
@ -14,6 +15,7 @@
:groupMembers="groupMembers"> :groupMembers="groupMembers">
</chat-message-item> </chat-message-item>
</view> </view>
</view>
</scroll-view> </scroll-view>
<view v-if="!isInBottom" class="scroll-to-bottom" @click="onClickToBottom"> <view v-if="!isInBottom" class="scroll-to-bottom" @click="onClickToBottom">
{{ newMessageSize > 0 ? newMessageSize+'条新消息' :'回到底部'}} {{ newMessageSize > 0 ? newMessageSize+'条新消息' :'回到底部'}}
@ -129,12 +131,11 @@ export default {
scrollMsgIdx: 0, // scrollMsgIdx: 0, //
chatTabBox: 'none', chatTabBox: 'none',
showRecord: false, showRecord: false,
chatMainHeight: 0, // chatMainHeight: 800, //
keyboardHeight: 290, // keyboardHeight: 290, //
windowHeight: 1000, // windowHeight: 1000, //
initHeight: 1000, // h5 initHeight: 1000, // h5
atUserIds: [], atUserIds: [],
needScrollToBottom: false, //
showMinIdx: 0, // showMinIdx showMinIdx: 0, // showMinIdx
reqQueue: [], // reqQueue: [], //
isSending: false, // isSending: false, //
@ -145,7 +146,9 @@ export default {
isReadOnly: false, // isReadOnly: false, //
playingAudio: null, // playingAudio: null, //
isInBottom: true, // isInBottom: true, //
newMessageSize: 0 // newMessageSize: 0, //
scrollTop: 0, // ios h5
scrollViewHeight: 0 //
} }
}, },
methods: { methods: {
@ -575,13 +578,20 @@ export default {
this.newMessageSize = 0; this.newMessageSize = 0;
}, 100) }, 100)
}, },
onScroll(e) {
//
this.scrollViewHeight = e.detail.scrollHeight;
},
onScrollToTop() { onScrollToTop() {
console.log("onScrollToTop")
if (this.showMinIdx > 0) { if (this.showMinIdx > 0) {
// #ifndef H5 // #ifndef H5
// // appscroll-into-view
this.scrollToMsgIdx(this.showMinIdx); this.scrollToMsgIdx(this.showMinIdx);
// #endif // #endif
// #ifdef H5
// h5scroll-top
this.holdingScrollBar(this.scrollViewHeight);
// #endif
// 20 // 20
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0; this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
} }
@ -593,6 +603,20 @@ export default {
this.isInBottom = true; this.isInBottom = true;
this.newMessageSize = 0; this.newMessageSize = 0;
}, },
holdingScrollBar(scrollViewHeight) {
//
const query = uni.createSelectorQuery().in(this);
setTimeout(() => {
query.select('.chat-wrap').boundingClientRect();
query.exec(data => {
this.scrollTop = data[0].height - scrollViewHeight;
if(this.scrollTop < 10){
//
this.holdingScrollBar();
}
});
}, 50)
},
onShowMore() { onShowMore() {
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
uni.navigateTo({ uni.navigateTo({
@ -924,8 +948,10 @@ export default {
}, },
watch: { watch: {
messageSize: function(newSize, oldSize) { messageSize: function(newSize, oldSize) {
// //
if (newSize > oldSize) { if (newSize > oldSize && oldSize > 0) {
let lastMessage = this.chat.messages[newSize - 1];
if (this.$msgType.isNormal(lastMessage.type)) {
if (this.isInBottom) { if (this.isInBottom) {
// , // ,
this.scrollToBottom(); this.scrollToBottom();
@ -934,6 +960,7 @@ export default {
this.newMessageSize++; this.newMessageSize++;
} }
} }
}
}, },
unreadCount: { unreadCount: {
handler(newCount, oldCount) { handler(newCount, oldCount) {
@ -963,6 +990,9 @@ export default {
this.chatStore.activeChat(options.chatIdx); this.chatStore.activeChat(options.chatIdx);
// //
this.isReceipt = false; this.isReceipt = false;
//
this.isInBottom = true;
this.newMessageSize = 0;
// //
this.listenKeyBoard(); this.listenKeyBoard();
// //
@ -982,13 +1012,6 @@ export default {
}, },
onUnload() { onUnload() {
this.unListenKeyboard(); this.unListenKeyboard();
},
onShow() {
if (this.needScrollToBottom) {
//
this.scrollToBottom();
this.needScrollToBottom = false;
}
} }
} }
</script> </script>

4
im-uniapp/pages/chat/chat.vue

@ -6,7 +6,7 @@
<view>消息接收中...</view> <view>消息接收中...</view>
</loading> </loading>
</view> </view>
<view v-if="initializing" class="chat-loading"> <view v-else-if="initializing" class="chat-loading">
<loading :size="50" :mask="false"> <loading :size="50" :mask="false">
<view>正在初始化...</view> <view>正在初始化...</view>
</loading> </loading>
@ -17,7 +17,7 @@
placeholder="搜索"></uni-search-bar> placeholder="搜索"></uni-search-bar>
</view> </view>
</view> </view>
<view class="chat-tip" v-if="!loading && chatStore.chats.length == 0"> <view class="chat-tip" v-if="!initializing && !loading && chatStore.chats.length == 0">
温馨提示您现在还没有任何聊天消息快跟您的好友发起聊天吧~ 温馨提示您现在还没有任何聊天消息快跟您的好友发起聊天吧~
</view> </view>
<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">

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

@ -25,6 +25,12 @@
</view> </view>
</view> </view>
</uni-card> </uni-card>
<bar-group v-if="isFriend">
<switch-bar title="消息免打扰" :checked="friendInfo.isDnd" @change="onDndChange"></switch-bar>
</bar-group>
<bar-group v-if="chatIdx>=0">
<arrow-bar title="清空聊天记录" @tap="onCleanMessage()"></arrow-bar>
</bar-group>
<bar-group> <bar-group>
<btn-bar v-show="isFriend" type="primary" title="发送消息" @tap="onSendMessage()"> <btn-bar v-show="isFriend" type="primary" title="发送消息" @tap="onSendMessage()">
</btn-bar> </btn-bar>
@ -57,6 +63,9 @@ export default {
showName: this.userInfo.nickName, showName: this.userInfo.nickName,
headImage: this.userInfo.headImage, headImage: this.userInfo.headImage,
}; };
if (this.isFriend) {
chat.isDnd = this.friendInfo.isDnd;
}
this.chatStore.openChat(chat); this.chatStore.openChat(chat);
let chatIdx = this.chatStore.findChatIdx(chat); let chatIdx = this.chatStore.findChatIdx(chat);
uni.navigateTo({ uni.navigateTo({
@ -103,6 +112,41 @@ export default {
} }
}) })
}, },
onCleanMessage() {
uni.showModal({
title: '清空聊天记录',
content: `确认删除与'${this.userInfo.nickName}'的聊天记录吗?`,
confirmText: '确认',
success: (res) => {
if (res.cancel)
return;
this.chatStore.cleanMessage(this.chatIdx);
uni.showToast({
title: `您清空了'${this.userInfo.nickName}'的聊天记录`,
icon: 'none'
})
}
})
},
onDndChange(e) {
let isDnd = e.detail.value;
let friendId = this.userInfo.id;
let formData = {
friendId: friendId,
isDnd: isDnd
}
this.$http({
url: '/friend/dnd',
method: 'PUT',
data: formData
}).then(() => {
this.friendStore.setDnd(friendId, isDnd)
let chat = this.chatStore.findChatByFriend(friendId)
if (chat) {
this.chatStore.setDnd(chat, isDnd)
}
})
},
updateFriendInfo() { updateFriendInfo() {
if (this.isFriend) { if (this.isFriend) {
// storestore // storestore
@ -134,6 +178,13 @@ export default {
}, },
friendInfo() { friendInfo() {
return this.friendStore.findFriend(this.userInfo.id); return this.friendStore.findFriend(this.userInfo.id);
},
chatIdx() {
let chat = this.chatStore.findChatByFriend(this.userInfo.id);
if (chat) {
return this.chatStore.findChatIdx(chat);
}
return -1;
} }
}, },
onLoad(options) { onLoad(options) {

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

@ -52,6 +52,12 @@
</view> </view>
<view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view> <view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view>
</view> </view>
<bar-group v-if="!group.quit">
<switch-bar title="消息免打扰" :checked="group.isDnd" @change="onDndChange"></switch-bar>
</bar-group>
<bar-group v-if="!group.quit && chatIdx>=0">
<arrow-bar title="清空聊天记录" @tap="onCleanMessage()"></arrow-bar>
</bar-group>
<bar-group v-if="!group.quit"> <bar-group v-if="!group.quit">
<btn-bar type="primary" title="发送消息" @tap="onSendMessage()"></btn-bar> <btn-bar type="primary" title="发送消息" @tap="onSendMessage()"></btn-bar>
<btn-bar v-if="!isOwner" type="danger" title="退出群聊" @tap="onQuitGroup()"></btn-bar> <btn-bar v-if="!isOwner" type="danger" title="退出群聊" @tap="onQuitGroup()"></btn-bar>
@ -117,6 +123,7 @@ export default {
targetId: this.group.id, targetId: this.group.id,
showName: this.group.showGroupName, showName: this.group.showGroupName,
headImage: this.group.headImage, headImage: this.group.headImage,
isDnd: this.group.isDnd
}; };
this.chatStore.openChat(chat); this.chatStore.openChat(chat);
let chatIdx = this.chatStore.findChatIdx(chat); let chatIdx = this.chatStore.findChatIdx(chat);
@ -178,6 +185,42 @@ export default {
} }
}); });
}, },
onDndChange(e) {
let isDnd = e.detail.value;
let groupId = this.group.id;
let formData = {
groupId: groupId,
isDnd: isDnd
}
this.$http({
url: '/group/dnd',
method: 'PUT',
data: formData
}).then(() => {
this.groupStore.setDnd(groupId, isDnd);
let chat = this.chatStore.findChatByGroup(groupId);
if (chat) {
this.chatStore.setDnd(chat, isDnd)
}
})
},
onCleanMessage() {
uni.showModal({
title: '清空聊天记录',
content: `确认删除群聊'${this.group.name}'的聊天记录吗?`,
confirmText: '确认',
success: (res) => {
if (res.cancel) {
return;
}
this.chatStore.cleanMessage(this.chatIdx);
uni.showToast({
title: `您清空了'${this.group.name}'的聊天记录`,
icon: 'none'
})
}
})
},
loadGroupInfo() { loadGroupInfo() {
this.$http({ this.$http({
url: `/group/find/${this.groupId}`, url: `/group/find/${this.groupId}`,
@ -210,6 +253,13 @@ export default {
}, },
showMaxIdx() { showMaxIdx() {
return this.isOwner ? 8 : 9; return this.isOwner ? 8 : 9;
},
chatIdx() {
let chat = this.chatStore.findChatByGroup(this.groupId);
if (chat) {
return this.chatStore.findChatIdx(chat);
}
return -1;
} }
}, },
onLoad(options) { onLoad(options) {

34
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=1746119818070') format('truetype'); src: url('iconfont.ttf?t=1750317465456') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,34 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-dnd:before {
content: "\e693";
}
.icon-privacy-protocol:before {
content: "\e761";
}
.icon-create-group-2:before {
content: "\e616";
}
.icon-create-group:before {
content: "\e650";
}
.icon-qrcode:before {
content: "\e642";
}
.icon-add-friend:before {
content: "\e64f";
}
.icon-scan:before {
content: "\e8b5";
}
.icon-remove:before { .icon-remove:before {
content: "\e603"; content: "\e603";
} }
@ -51,10 +79,6 @@
content: "\ec44"; content: "\ec44";
} }
.icon-privacy-protocol:before {
content: "\e70a";
}
.icon-un-register:before { .icon-un-register:before {
content: "\e656"; content: "\e656";
} }

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

Binary file not shown.

46
im-uniapp/store/chatStore.js

@ -1,5 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js'; import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js';
import useFriendStore from './friendStore.js';
import useGroupStore from './groupStore.js';
import useUserStore from './userStore'; import useUserStore from './userStore';
let cacheChats = []; let cacheChats = [];
@ -56,6 +58,7 @@ export default defineStore('chatStore', {
type: chatInfo.type, type: chatInfo.type,
showName: chatInfo.showName, showName: chatInfo.showName,
headImage: chatInfo.headImage, headImage: chatInfo.headImage,
isDnd: chatInfo.isDnd,
lastContent: "", lastContent: "",
lastSendTime: new Date().getTime(), lastSendTime: new Date().getTime(),
unreadCount: 0, unreadCount: 0,
@ -105,6 +108,17 @@ export default defineStore('chatStore', {
this.saveToStorage(); this.saveToStorage();
} }
}, },
cleanMessage(idx) {
let chat = this.curChats[idx];
chat.lastContent = '';
chat.hotMinIdx = 0;
chat.unreadCount = 0;
chat.atMe = false;
chat.atAll = false;
chat.stored = false
chat.messages = [];
this.saveToStorage(true);
},
removeChat(idx) { removeChat(idx) {
let chats = this.curChats; let chats = this.curChats;
chats[idx].delete = true; chats[idx].delete = true;
@ -181,7 +195,7 @@ export default defineStore('chatStore', {
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;
// 未读加1 // 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && if (!chat.isDnd && !msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED &&
msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) { msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) {
chat.unreadCount++; chat.unreadCount++;
} }
@ -336,8 +350,34 @@ export default defineStore('chatStore', {
this.refreshChats() this.refreshChats()
} }
}, },
setDnd(chatInfo, isDnd) {
let chat = this.findChat(chatInfo);
if (chat) {
chat.isDnd = isDnd;
chat.unreadCount = 0;
}
},
refreshChats() { refreshChats() {
if (!cacheChats) return; if (!cacheChats) return;
// 更新会话免打扰状态
const friendStore = useFriendStore();
const groupStore = useGroupStore();
cacheChats.forEach(chat => {
if (chat.type == 'PRIVATE') {
let friend = friendStore.findFriend(chat.targetId);
if (friend) {
chat.isDnd = friend.isDnd
}
} else if (chat.type == 'GROUP') {
let group = groupStore.findGroup(chat.targetId);
if (group) {
chat.isDnd = group.isDnd
}
}
if (chat.isDnd) {
chat.unreadCount = 0;
}
})
// 排序 // 排序
cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime); cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
// #ifndef APP-PLUS // #ifndef APP-PLUS
@ -345,8 +385,8 @@ export default defineStore('chatStore', {
* 由于h5和小程序的stroge只有5m,大约只能存储2w条消息 * 由于h5和小程序的stroge只有5m,大约只能存储2w条消息
* 所以这里每个会话只保留1000条消息防止溢出 * 所以这里每个会话只保留1000条消息防止溢出
*/ */
cacheChats.forEach(chat =>{ cacheChats.forEach(chat => {
if(chat.messages.length > 1000){ if (chat.messages.length > 1000) {
let idx = chat.messages.length - 1000; let idx = chat.messages.length - 1000;
chat.messages = chat.messages.slice(idx); chat.messages = chat.messages.slice(idx);
} }

4
im-uniapp/store/friendStore.js

@ -67,6 +67,10 @@ export default defineStore('friendStore', {
this.refreshOnlineStatus(); this.refreshOnlineStatus();
}, 30000) }, 30000)
}, },
setDnd(id, isDnd) {
let friend = this.findFriend(id);
friend.isDnd = isDnd;
},
clear() { clear() {
clearTimeout(this.timer); clearTimeout(this.timer);
this.friends = []; this.friends = [];

4
im-uniapp/store/groupStore.js

@ -25,6 +25,10 @@ export default defineStore('groupStore', {
let g = this.findGroup(group.id); let g = this.findGroup(group.id);
Object.assign(g, group); Object.assign(g, group);
}, },
setDnd(id, isDnd) {
let group = this.findGroup(id);
group.isDnd = isDnd;
},
clear() { clear() {
this.groups = []; this.groups = [];
}, },

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

@ -15,8 +15,10 @@ const MESSAGE_TYPE = {
USER_BANNED: 50, USER_BANNED: 50,
FRIEND_NEW: 80, FRIEND_NEW: 80,
FRIEND_DEL: 81, FRIEND_DEL: 81,
FRIEND_DND: 82,
GROUP_NEW: 90, GROUP_NEW: 90,
GROUP_DEL: 91, GROUP_DEL: 91,
GROUP_DND: 92,
RTC_CALL_VOICE: 100, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101, RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,

26
im-web/src/assets/iconfont/iconfont.css

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3791506 */ font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.ttf?t=1745933248800') format('truetype'); src: url('iconfont.ttf?t=1750245745055') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,30 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-dnd:before {
content: "\e691";
}
.icon-screenshot:before {
content: "\e61c";
}
.icon-close:before {
content: "\e609";
}
.icon-minimize:before {
content: "\e650";
}
.icon-maximize:before {
content: "\e651";
}
.icon-unmaximize:before {
content: "\e611";
}
.icon-man:before { .icon-man:before {
content: "\e615"; content: "\e615";
} }

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

Binary file not shown.

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

@ -9,14 +9,15 @@
<div class="chat-name"> <div class="chat-name">
<div class="chat-name-text"> <div class="chat-name-text">
<div>{{ chat.showName }}</div> <div>{{ chat.showName }}</div>
<el-tag v-if="chat.type == 'GROUP'" size="mini" ></el-tag> <el-tag v-if="chat.type == 'GROUP'" size="mini"></el-tag>
</div> </div>
<div class="chat-time-text">{{ showTime }}</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-at-text">{{ atText }}</div>
<div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div> <div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div>
<div class="chat-content-text" v-html="$emo.transform(chat.lastContent,'emoji-small')"></div> <div class="chat-content-text" v-html="$emo.transform(chat.lastContent, 'emoji-small')"></div>
<div class="icon iconfont icon-dnd" v-if="chat.isDnd"></div>
</div> </div>
</div> </div>
<right-menu ref="rightMenu" @select="onSelectMenu"></right-menu> <right-menu ref="rightMenu" @select="onSelectMenu"></right-menu>
@ -36,15 +37,6 @@ export default {
}, },
data() { data() {
return { return {
menuItems: [{
key: 'TOP',
name: '置顶',
icon: 'el-icon-top'
}, {
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
}]
} }
}, },
props: { props: {
@ -86,6 +78,30 @@ export default {
return "[@全体成员]" return "[@全体成员]"
} }
return ""; return "";
},
menuItems() {
let items = [];
items.push({
key: 'TOP',
name: '置顶'
});
if (this.chat.isDnd) {
items.push({
key: 'DND',
name: '新消息提醒'
})
} else {
items.push({
key: 'DND',
name: '消息免打扰'
})
}
items.push({
key: 'DELETE',
name: '删除聊天',
color: '#F56C6C'
})
return items;
} }
} }
} }
@ -195,6 +211,9 @@ export default {
color: var(--im-text-color-light); color: var(--im-text-color-light);
} }
.icon {
color: var(--im-text-color-light);
}
} }
} }
} }

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

@ -65,8 +65,11 @@ export default {
type: 'PRIVATE', type: 'PRIVATE',
targetId: user.id, targetId: user.id,
showName: user.nickName, showName: user.nickName,
headImage: user.headImage, headImage: user.headImage
}; };
if (this.isFriend) {
chat.isDnd = this.friendInfo.isDnd;
}
this.chatStore.openChat(chat); this.chatStore.openChat(chat);
this.chatStore.setActiveChat(0); this.chatStore.setActiveChat(0);
if (this.$route.path != "/home/chat") { if (this.$route.path != "/home/chat") {
@ -102,6 +105,9 @@ export default {
computed: { computed: {
isFriend() { isFriend() {
return this.friendStore.isFriend(this.user.id); return this.friendStore.isFriend(this.user.id);
},
friendInfo() {
return this.friendStore.findFriend(this.user.id);
} }
} }
} }

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

@ -1,5 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { MESSAGE_TYPE, MESSAGE_STATUS } from "../api/enums.js" import { MESSAGE_TYPE, MESSAGE_STATUS } from "../api/enums.js"
import useFriendStore from './friendStore.js';
import useGroupStore from './groupStore.js';
import useUserStore from './userStore.js'; import useUserStore from './userStore.js';
import localForage from 'localforage'; import localForage from 'localforage';
@ -66,6 +68,7 @@ export default defineStore('chatStore', {
type: chatInfo.type, type: chatInfo.type,
showName: chatInfo.showName, showName: chatInfo.showName,
headImage: chatInfo.headImage, headImage: chatInfo.headImage,
isDnd: chatInfo.isDnd,
lastContent: "", lastContent: "",
lastSendTime: new Date().getTime(), lastSendTime: new Date().getTime(),
unreadCount: 0, unreadCount: 0,
@ -193,7 +196,7 @@ export default defineStore('chatStore', {
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;
// 未读加1 // 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && if (!chat.isDnd && !msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED &&
msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) { msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) {
chat.unreadCount++; chat.unreadCount++;
} }
@ -343,8 +346,34 @@ export default defineStore('chatStore', {
this.refreshChats(); this.refreshChats();
} }
}, },
setDnd(chatInfo, isDnd) {
let chat = this.findChat(chatInfo);
if (chat) {
chat.isDnd = isDnd;
chat.unreadCount = 0;
}
},
refreshChats() { refreshChats() {
if (!cacheChats) return; if (!cacheChats) return;
// 刷新免打扰状态
const friendStore = useFriendStore();
const groupStore = useGroupStore();
cacheChats.forEach(chat => {
if (chat.type == 'PRIVATE') {
let friend = friendStore.findFriend(chat.targetId);
if (friend) {
chat.isDnd = friend.isDnd
}
} else if (chat.type == 'GROUP') {
let group = groupStore.findGroup(chat.targetId);
if (group) {
chat.isDnd = group.isDnd
}
}
if (chat.isDnd) {
chat.unreadCount = 0;
}
})
// 排序 // 排序
cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime); cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
// 记录热数据索引位置 // 记录热数据索引位置

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

@ -42,6 +42,10 @@ export default defineStore('friendStore', {
} }
friend.online = friend.onlineWeb || friend.onlineApp; friend.online = friend.onlineWeb || friend.onlineApp;
}, },
setDnd(id, isDnd) {
let friend = this.findFriend(id);
friend.isDnd = isDnd;
},
clear() { clear() {
this.timer && clearTimeout(this.timer); this.timer && clearTimeout(this.timer);
this.friends = []; this.friends = [];

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

@ -35,6 +35,10 @@ export default defineStore('groupStore', {
group.topMessage = topMessage; group.topMessage = topMessage;
} }
}, },
setDnd(id, isDnd) {
let group = this.findGroup(id);
group.isDnd = isDnd;
},
clear() { clear() {
this.groups = []; this.groups = [];
}, },

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

@ -13,7 +13,7 @@
<div v-for="(chat, index) in chatStore.chats" :key="index"> <div v-for="(chat, index) in chatStore.chats" :key="index">
<chat-item v-show="!chat.delete && chat.showName && chat.showName.includes(searchText)" :chat="chat" <chat-item v-show="!chat.delete && chat.showName && chat.showName.includes(searchText)" :chat="chat"
:index="index" @click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)" :index="index" @click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="chat === chatStore.activeChat"></chat-item> @dnd="onDnd(chat)" :active="chat === chatStore.activeChat"></chat-item>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
@ -51,6 +51,41 @@ export default {
onTop(chatIdx) { onTop(chatIdx) {
this.chatStore.moveTop(chatIdx); this.chatStore.moveTop(chatIdx);
}, },
onDnd(chat) {
if (chat.type == 'PRIVATE') {
this.setFriendDnd(chat, chat.targetId, !chat.isDnd)
} else {
this.setGroupDnd(chat, chat.targetId, !chat.isDnd)
}
},
setFriendDnd(chat, friendId, isDnd) {
let formData = {
friendId: friendId,
isDnd: isDnd
}
this.$http({
url: '/friend/dnd',
method: 'put',
data: formData
}).then(() => {
this.friendStore.setDnd(friendId, isDnd)
this.chatStore.setDnd(chat, isDnd)
})
},
setGroupDnd(chat, groupId, isDnd) {
let formData = {
groupId: groupId,
isDnd: isDnd
}
this.$http({
url: '/group/dnd',
method: 'put',
data: formData
}).then(() => {
this.groupStore.setDnd(groupId, isDnd)
this.chatStore.setDnd(chat, isDnd)
})
}
}, },
computed: { computed: {
loading() { loading() {

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

@ -44,7 +44,7 @@
</div> </div>
<div class="btn-group"> <div class="btn-group">
<el-button v-show="isFriend" icon="el-icon-position" type="primary" <el-button v-show="isFriend" icon="el-icon-position" type="primary"
@click="onSendMessage(userInfo)">发消息</el-button> @click="onSendMessage(activeFriend)">发消息</el-button>
<el-button v-show="!isFriend" icon="el-icon-plus" type="primary" <el-button v-show="!isFriend" icon="el-icon-plus" type="primary"
@click="onAddFriend(userInfo)">加为好友</el-button> @click="onAddFriend(userInfo)">加为好友</el-button>
<el-button v-show="isFriend" icon="el-icon-delete" type="danger" <el-button v-show="isFriend" icon="el-icon-delete" type="danger"
@ -124,12 +124,13 @@ export default {
this.friendStore.addFriend(friend); this.friendStore.addFriend(friend);
}) })
}, },
onSendMessage(user) { onSendMessage(friend) {
let chat = { let chat = {
type: 'PRIVATE', type: 'PRIVATE',
targetId: user.id, targetId: friend.id,
showName: user.nickName, showName: friend.nickName,
headImage: user.headImageThumb, headImage: friend.headImage,
isDnd: friend.isDnd
}; };
this.chatStore.openChat(chat); this.chatStore.openChat(chat);
this.chatStore.setActiveChat(0); this.chatStore.setActiveChat(0);

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

@ -241,6 +241,7 @@ export default {
targetId: this.activeGroup.id, targetId: this.activeGroup.id,
showName: this.activeGroup.showGroupName, showName: this.activeGroup.showGroupName,
headImage: this.activeGroup.headImage, headImage: this.activeGroup.headImage,
isDnd: this.activeGroup.isDnd
}; };
this.chatStore.openChat(chat); this.chatStore.openChat(chat);
this.chatStore.setActiveChat(0); this.chatStore.setActiveChat(0);

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

@ -251,6 +251,12 @@ export default {
this.friendStore.removeFriend(friendId); this.friendStore.removeFriend(friendId);
return; return;
} }
//
if (msg.type == this.$enums.MESSAGE_TYPE.FRIEND_DND) {
this.friendStore.setDnd(friendId, JSON.parse(msg.content));
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return;
}
// webrtc // webrtc
if (this.$msgType.isRtcPrivate(msg.type)) { if (this.$msgType.isRtcPrivate(msg.type)) {
this.$refs.rtcPrivateVideo.onRTCMessage(msg) this.$refs.rtcPrivateVideo.onRTCMessage(msg)
@ -267,14 +273,15 @@ export default {
type: 'PRIVATE', type: 'PRIVATE',
targetId: friend.id, targetId: friend.id,
showName: friend.nickName, showName: friend.nickName,
headImage: friend.headImage headImage: friend.headImage,
isDnd: friend.isDnd
}; };
// //
this.chatStore.openChat(chatInfo); this.chatStore.openChat(chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!msg.selfSend && this.$msgType.isNormal(msg.type) && if (!friend.isDnd && !this.chatStore.isLoading() && !msg.selfSend && this.$msgType.isNormal(msg.type) &&
msg.status != this.$enums.MESSAGE_STATUS.READED) { msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();
} }
@ -324,6 +331,12 @@ export default {
this.groupStore.removeGroup(msg.groupId); this.groupStore.removeGroup(msg.groupId);
return; return;
} }
//
if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_DND) {
this.groupStore.setDnd(msg.groupId, JSON.parse(msg.content));
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return;
}
// //
if (this.$msgType.isRtcGroup(msg.type)) { if (this.$msgType.isRtcGroup(msg.type)) {
this.$nextTick(() => { this.$nextTick(() => {
@ -342,15 +355,17 @@ export default {
type: 'GROUP', type: 'GROUP',
targetId: group.id, targetId: group.id,
showName: group.showGroupName, showName: group.showGroupName,
headImage: group.headImageThumb headImage: group.headImageThumb,
isDnd: group.isDnd
}; };
// //
this.chatStore.openChat(chatInfo); this.chatStore.openChat(chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO && if (!group.isDnd && !this.chatStore.isLoading() &&
msg.status != this.$enums.MESSAGE_STATUS.READED) { !msg.selfSend && this.$msgType.isNormal(msg.type)
&& msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();
} }
}, },

Loading…
Cancel
Save