Browse Source

支持群聊回执消息

master
Blue 2 years ago
parent
commit
2eed546b74
  1. 5
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  2. 3
      im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java
  3. 10
      im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java
  4. 4
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  5. 7
      im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java
  6. 3
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java
  7. 104
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  8. 13
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  9. 7
      im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java
  10. 5
      im-platform/src/main/resources/db/db.sql
  11. 1
      im-ui/src/api/enums.js
  12. 1
      im-ui/src/api/wssocket.js
  13. 12
      im-ui/src/assets/iconfont/iconfont.css
  14. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  15. BIN
      im-ui/src/assets/iconfont/iconfont.woff
  16. BIN
      im-ui/src/assets/iconfont/iconfont.woff2
  17. 55
      im-ui/src/components/chat/ChatAtBox.vue
  18. 75
      im-ui/src/components/chat/ChatBox.vue
  19. 67
      im-ui/src/components/chat/ChatGroupMember.vue
  20. 186
      im-ui/src/components/chat/ChatGroupReaded.vue
  21. 49
      im-ui/src/components/chat/ChatMessageItem.vue
  22. 1
      im-ui/src/components/chat/ChatPrivateVideo.vue
  23. 3
      im-ui/src/components/chat/ChatVoice.vue
  24. 1
      im-ui/src/components/group/AddGroupMember.vue
  25. 97
      im-ui/src/store/chatStore.js
  26. 1
      im-ui/src/store/friendStore.js
  27. 1
      im-ui/src/store/groupStore.js
  28. 1
      im-ui/src/store/index.js
  29. 28
      im-ui/src/view/Home.vue
  30. 12
      im-uniapp/App.vue
  31. 1
      im-uniapp/common/enums.js
  32. 131
      im-uniapp/components/chat-group-readed/chat-group-readed.vue
  33. 24
      im-uniapp/components/chat-message-item/chat-message-item.vue
  34. 36
      im-uniapp/pages/chat/chat-box.vue
  35. 10
      im-uniapp/static/icon/iconfont.css
  36. BIN
      im-uniapp/static/icon/iconfont.ttf
  37. 104
      im-uniapp/store/chatStore.js

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

@ -50,6 +50,11 @@ public class GroupMessageController {
return ResultUtils.success();
}
@GetMapping("/findReadedUsers")
@ApiOperation(value = "获取已读用户id", notes = "获取消息已读用户列表")
public Result<List<Long>> findReadedUsers(@RequestParam Long groupId,@RequestParam Long messageId) {
return ResultUtils.success(groupMessageService.findReadedUsers(groupId,messageId));
}
@GetMapping("/history")
@ApiOperation(value = "查询聊天记录", notes = "查询聊天记录")

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

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

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

@ -62,13 +62,19 @@ public class GroupMessage extends Model<GroupMessage> {
private String content;
/**
* 消息类型 0:文字 1:图片 2:文件
* 消息类型 MessageType
*/
@TableField("type")
private Integer type;
/**
* 状态
* 是否回执消息
*/
@TableField("receipt")
private Boolean receipt;
/**
* 状态 MessageStatus
*/
@TableField("status")
private Integer status;

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

@ -34,6 +34,10 @@ public enum MessageType {
*/
READED(11, "已读"),
/**
* 消息已读回执(更新已读数量)
*/
RECEIPT(12, "消息已读回执"),
/**
* 呼叫
*/

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

@ -39,6 +39,13 @@ public interface IGroupMessageService extends IService<GroupMessage> {
*/
void readedMessage(Long groupId);
/**
* 查询群里消息已读用户id列表
* @param groupId 群里id
* @param messageId 消息id
* @return 已读用户id集合
*/
List<Long> findReadedUsers(Long groupId,Long messageId);
/**
* 拉取历史聊天记录
*

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

@ -61,7 +61,8 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
public List<Long> findUserIdsByGroupId(Long groupId) {
LambdaQueryWrapper<GroupMember> memberWrapper = Wrappers.lambdaQuery();
memberWrapper.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getQuit, false);
.eq(GroupMember::getQuit, false)
.select(GroupMember::getUserId);
List<GroupMember> members = this.list(memberWrapper);
return members.stream().map(GroupMember::getUserId).collect(Collectors.toList());
}

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

@ -152,11 +152,11 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return new ArrayList<>();
}
Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId);
Set<Long> ids = groupMemberMap.keySet();
Set<Long> groupIds = groupMemberMap.keySet();
// 只能拉取最近1个月的
Date minDate = DateUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId).gt(GroupMessage::getSendTime, minDate).in(GroupMessage::getGroupId, ids)
wrapper.gt(GroupMessage::getId, minId).gt(GroupMessage::getSendTime, minDate).in(GroupMessage::getGroupId, groupIds)
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByAsc(GroupMessage::getId).last("limit 100");
List<GroupMessage> messages = this.list(wrapper);
@ -176,25 +176,24 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
}
return vo;
}).collect(Collectors.toList());
// 消息状态,数据库没有存群聊的消息状态,需要从redis取
List<String> keys = ids.stream().map(id -> String.join(":", RedisKey.IM_GROUP_READED_POSITION, id.toString(), session.getUserId().toString()))
.collect(Collectors.toList());
List<Object> sendPos = redisTemplate.opsForValue().multiGet(keys);
int idx = 0;
for (Long id : ids) {
Object o = sendPos.get(idx);
Integer sendMaxId = Objects.isNull(o) ? -1 : (Integer) o;
vos.stream().filter(vo -> vo.getGroupId().equals(id)).forEach(vo -> {
if (vo.getId() <= sendMaxId) {
// 已读
vo.setStatus(MessageStatus.READED.code());
} else {
// 未推送
vo.setStatus(MessageStatus.UNSEND.code());
}
// 通过群聊对消息进行分组
Map<Long, List<GroupMessageVO>> messageGroupMap = vos.stream().collect(Collectors.groupingBy(GroupMessageVO::getGroupId));
messageGroupMap.forEach((groupId, messageVos) -> {
// 填充消息状态
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString());
long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString());
messageVos.forEach(messageVo -> messageVo.setStatus(readedMaxId >= messageVo.getId() ? MessageStatus.READED.code() : MessageStatus.UNSEND.code()));
// 针对回执消息填充已读人数
List<GroupMessageVO> receiptMessageVos = messageVos.stream().filter(GroupMessageVO::getReceipt).collect(Collectors.toList());
if (!receiptMessageVos.isEmpty()) {
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
receiptMessageVos.forEach(receiptMessageVo -> {
int count = getReadedUserIds(maxIdMap, receiptMessageVo.getId(),receiptMessageVo.getSendId()).size();
receiptMessageVo.setReadedCount(count);
});
idx++;
}
});
return vos;
}
@ -203,12 +202,15 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
UserSession session = SessionContext.getSession();
// 取出最后的消息id
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId).orderByDesc(GroupMessage::getId).last("limit 1").select(GroupMessage::getId);
wrapper.eq(GroupMessage::getGroupId, groupId)
.orderByDesc(GroupMessage::getId)
.last("limit 1")
.select(GroupMessage::getId);
GroupMessage message = this.getOne(wrapper);
if (Objects.isNull(message)) {
return;
}
// 推送消息给自己的其他终端
// 推送消息给自己的其他终端,同步清空会话列表中的未读数量
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.READED.code());
msgInfo.setSendTime(new Date());
@ -220,10 +222,52 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
sendMessage.setData(msgInfo);
sendMessage.setSendResult(true);
imClient.sendGroupMessage(sendMessage);
// 已读消息key
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
// 原来的已读消息位置
Object maxReadedId = redisTemplate.opsForHash().get(key, session.getUserId().toString());
// 记录已读消息位置
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId, session.getUserId());
redisTemplate.opsForValue().set(key, message.getId());
redisTemplate.opsForHash().put(key, session.getUserId().toString(), message.getId());
// 推送消息回执,刷新已读人数显示
wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId);
wrapper.gt(!Objects.isNull(maxReadedId), GroupMessage::getId, maxReadedId);
wrapper.le(!Objects.isNull(maxReadedId), GroupMessage::getId, message.getId());
wrapper.eq(GroupMessage::getReceipt, true);
List<GroupMessage> receiptMessages = this.list(wrapper);
if (CollectionUtil.isNotEmpty(receiptMessages)) {
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId);
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
for (GroupMessage receiptMessage : receiptMessages) {
Integer readedCount = getReadedUserIds(maxIdMap, receiptMessage.getId(),receiptMessage.getSendId()).size();
msgInfo = new GroupMessageVO();
msgInfo.setId(receiptMessage.getId());
msgInfo.setGroupId(groupId);
msgInfo.setReadedCount(readedCount);
msgInfo.setType(MessageType.RECEIPT.code());;
sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
imClient.sendGroupMessage(sendMessage);
}
}
}
@Override
public List<Long> findReadedUsers(Long groupId, Long messageId) {
GroupMessage message = this.getById(messageId);
if (Objects.isNull(message)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息不存在");
}
// 已读位置key
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
// 一次获取所有用户的已读位置
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
// 返回已读用户的id集合
return getReadedUserIds(maxIdMap, message.getId(),message.getSendId());
}
@Override
@ -249,4 +293,18 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return messageInfos;
}
private List<Long> getReadedUserIds(Map<Object, Object> maxIdMap, Long messageId, Long sendId) {
List<Long> userIds = new LinkedList<>();
maxIdMap.forEach((k, v) -> {
Long userId = Long.valueOf(k.toString());
Long maxId = Long.valueOf(v.toString());
// 发送者不计入已读人数
if (!sendId.equals(userId) && maxId >= messageId) {
userIds.add(userId);
}
});
return userIds;
}
}

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

@ -1,5 +1,6 @@
package com.bx.implatform.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -29,6 +30,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -44,7 +46,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
private final IGroupMemberService groupMemberService;
private final IFriendService friendsService;
private final IMClient imClient;
private final RedisTemplate<String, Object> redisTemplate;
@Override
public GroupVO createGroup(GroupVO vo) {
UserSession session = SessionContext.getSession();
@ -107,6 +109,9 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
this.updateById(group);
// 删除成员数据
groupMemberService.removeByGroupId(groupId);
// 清理已读缓存
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.delete(key);
log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName());
}
@ -119,6 +124,9 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}
// 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId, userId);
// 清理已读缓存
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key,userId.toString());
log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
}
@ -134,6 +142,9 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}
// 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId, userId);
// 清理已读缓存
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key,userId.toString());
log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
}

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

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

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

@ -70,8 +70,9 @@ create table `im_group_message`(
`send_nick_name` varchar(255) DEFAULT '' comment '发送用户昵称',
`content` text comment '发送内容',
`at_user_ids` varchar(1024) comment '被@的用户id列表,逗号分隔',
`type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 10:系统提示' ,
`status` tinyint(1) DEFAULT 0 comment '状态 0:正常 2:撤回',
`receipt` tinyint DEFAULT 0 comment '是否回执消息',
`type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 4:视频 10:系统提示' ,
`status` tinyint(1) DEFAULT 0 comment '状态 0:未发出 1:已送达 2:撤回 3:已读',
`send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间',
key `idx_group_id` (group_id)
)ENGINE=InnoDB CHARSET=utf8mb3 comment '群消息';

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

@ -7,6 +7,7 @@ const MESSAGE_TYPE = {
VIDEO:4,
RECALL:10,
READED:11,
RECEIPT:12,
TIP_TIME:20,
RTC_CALL: 101,
RTC_ACCEPT: 102,

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

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

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

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

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

@ -1,26 +1,18 @@
<template>
<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 v-for="(member) in showMembers" :key="member.id">
<chat-group-member :member="member" :height="40" @click.native="onSelectMember(member)"></chat-group-member>
</div>
</el-scrollbar>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
import ChatGroupMember from "./ChatGroupMember.vue";
export default {
name: "chatAtBox",
components: {
HeadImage
ChatGroupMember
},
props: {
searchText: {
@ -58,7 +50,7 @@
})
}
this.members.forEach((m) => {
if (m.userId != userId && m.aliasName.startsWith(this.searchText)) {
if (m.userId != userId && !m.quit && m.aliasName.startsWith(this.searchText)) {
this.showMembers.push(m);
}
})
@ -134,42 +126,5 @@
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>

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

@ -14,8 +14,8 @@
<ul>
<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)" :showName="showName(msgInfo)"
:msgInfo="msgInfo" @delete="deleteMessage" @recall="recallMessage">
:headImage="headImage(msgInfo)" :showName="showName(msgInfo)" :msgInfo="msgInfo"
:groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage">
</chat-message-item>
</li>
</ul>
@ -39,6 +39,9 @@
<i class="el-icon-wallet"></i>
</file-upload>
</div>
<div title="回执消息" v-show="chat.type == 'GROUP'" class="icon iconfont icon-receipt"
:class="isReceipt ? 'chat-tool-active' : ''" @click="onSwitchReceipt">
</div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</div>
<div title="视频聊天" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@ -49,10 +52,9 @@
<div class="send-content-area">
<div contenteditable="true" v-show="!sendImageUrl" ref="editBox" class="send-text-area"
:disabled="lockMessage" @paste.prevent="onEditorPaste"
@compositionstart="onEditorCompositionStart"
@compositionend="onEditorCompositionEnd" @input="onEditorInput"
placeholder="温馨提示:可以粘贴截图到这里了哦~" @blur="onEditBoxBlur()" @keydown.down="onKeyDown"
@keydown.up="onKeyUp" @keydown.enter.prevent="onKeyEnter">
@compositionstart="onEditorCompositionStart" @compositionend="onEditorCompositionEnd"
@input="onEditorInput" :placeholder="isReceipt ? '【回执消息】' : ''" @blur="onEditBoxBlur()"
@keydown.down="onKeyDown" @keydown.up="onKeyUp" @keydown.enter.prevent="onKeyEnter">x
</div>
<div v-show="sendImageUrl" class="send-image-area">
@ -78,8 +80,8 @@
<chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers" :search-text="atSearchText"
@select="onAtSelect"></chat-at-box>
<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>
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers"
@close="closeHistoryBox"></chat-history>
</el-container>
</div>
</template>
@ -116,6 +118,7 @@
groupMembers: [],
sendImageUrl: "",
sendImageFile: "",
isReceipt: true,
showVoice: false, //
showSide: false, //
showHistory: false, //
@ -217,13 +220,16 @@
this.atSearchText = "";
this.$refs.editBox.focus()
},
onSwitchReceipt() {
this.isReceipt = !this.isReceipt;
},
createSendText() {
let sendText = ""
let sendText = this.isReceipt ? "【回执消息】" : "";
this.$refs.editBox.childNodes.forEach((node) => {
if (node.nodeName == "#text") {
sendText += this.html2Escape(node.textContent);
} else if (node.nodeName == "SPAN") {
sendText += node.innerText;
sendText += node.innerHTML;
} else if (node.nodeName == "IMG") {
sendText += node.dataset.code;
}
@ -250,14 +256,14 @@
return ids;
},
onEditorPaste(e) {
let txt = event.clipboardData.getData('Text')
let txt = e.clipboardData.getData('Text')
if (typeof (txt) == 'string') {
let range = window.getSelection().getRangeAt(0)
let textNode = document.createTextNode(txt);
range.insertNode(textNode)
range.collapse();
}
let items = (event.clipboardData || window.clipboardData).items
let items = (e.clipboardData || window.clipboardData).items
if (items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
@ -275,6 +281,7 @@
onImageSuccess(data, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt;
this.$http({
url: this.messageAction,
method: 'post',
@ -282,6 +289,7 @@
}).then((id) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = id;
this.isReceipt = false;
this.$store.commit("insertMessage", msgInfo);
})
},
@ -304,6 +312,7 @@
sendTime: new Date().getTime(),
selfSend: true,
type: 1,
readedCount: 0,
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
}
@ -324,6 +333,7 @@
}
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt
this.$http({
url: this.messageAction,
method: 'post',
@ -331,6 +341,7 @@
}).then((id) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = id;
this.isReceipt = false;
this.$store.commit("insertMessage", msgInfo);
})
},
@ -354,6 +365,7 @@
selfSend: true,
type: 2,
loadStatus: "loading",
readedCount: 0,
status: this.$enums.MESSAGE_STATUS.UNSEND
}
// id
@ -388,7 +400,6 @@
x: left + width / 2,
y: top
})
},
onEmotion(emoText) {
//
@ -428,7 +439,8 @@
onSendVoice(data) {
let msgInfo = {
content: JSON.stringify(data),
type: 3
type: 3,
receipt: this.isReceipt
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -442,6 +454,7 @@
msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true;
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
msgInfo.readedCount = 0;
this.$store.commit("insertMessage", msgInfo);
//
this.$refs.editBox.focus();
@ -449,6 +462,7 @@
this.scrollToBottom();
//
this.showVoice = false;
this.isReceipt = false;
})
},
fillTargetId(msgInfo, targetId) {
@ -501,6 +515,7 @@
// @
if (this.chat.type == "GROUP") {
msgInfo.atUserIds = this.createAtUserIds();
msgInfo.receipt = this.isReceipt;
}
this.lockMessage = true;
this.$http({
@ -512,6 +527,7 @@
msgInfo.sendTime = new Date().getTime();
msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true;
msgInfo.readedCount = 0;
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo);
}).finally(() => {
@ -519,6 +535,7 @@
this.lockMessage = false;
this.scrollToBottom();
this.resetEditor();
this.isReceipt = false;
});
},
@ -679,6 +696,8 @@
this.showMinIdx = size > 30 ? size - 30 : 0;
//
this.resetEditor();
//
this.isReceipt = false;
}
},
immediate: true
@ -757,19 +776,28 @@
text-align: left;
box-sizing: border-box;
border: #dddddd solid 1px;
padding: 2px;
>div {
margin-left: 10px;
font-size: 22px;
cursor: pointer;
color: #333333;
line-height: 40px;
line-height: 34px;
width: 34px;
height: 34px;
text-align: center;
border-radius: 3px;
&:hover {
color: black;
}
&.chat-tool-active {
background: #ddd;
}
}
}
.send-content-area {
position: relative;
@ -779,6 +807,7 @@
background-color: white !important;
.send-text-area {
box-sizing: border-box;
padding: 5px;
@ -792,11 +821,22 @@
text-align: left;
line-height: 30 px;
&:before {
content: attr(placeholder);
color: gray;
}
.at {
color: blue;
font-weight: 600;
}
.receipt {
color: darkblue;
font-size: 15px;
font-weight: 600;
}
.emo {
width: 30px;
height: 30px;
@ -850,5 +890,4 @@
border: #dddddd solid 1px;
animation: rtl-drawer-in .3s 1ms;
}
}
</style>
}</style>

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

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

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

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

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

@ -18,6 +18,7 @@
<span>{{ $date.toTimeText(msgInfo.sendTime) }}</span>
</div>
<div class="chat-msg-bottom" @contextmenu.prevent="showRightMenu($event)">
<div ref="chatMsgBox">
<span class="chat-msg-text" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT"
v-html="$emo.transform(msgInfo.content)"></span>
<div class="chat-msg-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
@ -43,32 +44,38 @@
<span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span>
</div>
<div class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO"
@click="onPlayVoice()">
</div>
<div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div>
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span>
<div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<span v-if="msgInfo.readedCount>=0">{{msgInfo.readedCount}}人已读</span>
<span v-else class="icon iconfont icon-ok" title="全体已读"></span>
</div>
</div>
</div>
</div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@close="rightMenu.show=false" @select="onSelectMenu"></right-menu>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show = false"
@select="onSelectMenu"></right-menu>
<chat-group-readed ref="chatGroupReadedBox" :groupMembers="groupMembers"></chat-group-readed>
</div>
</template>
<script>
import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue';
export default {
name: "messageItem",
components: {
HeadImage,
RightMenu
RightMenu,
ChatGroupReaded
},
props: {
mode: {
@ -91,6 +98,9 @@
type: Object,
required: true
},
groupMembers: {
type: Array
},
menu: {
type: Boolean,
default: true
@ -136,6 +146,10 @@
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
},
onShowReadedBox() {
let rect = this.$refs.chatMsgBox.getBoundingClientRect();
this.$refs.chatGroupReadedBox.open(this.msgInfo, rect);
}
},
computed: {
@ -178,7 +192,7 @@
}
</script>
<style scoped lang="scss">
<style lang="scss">
.chat-msg-item {
.chat-msg-tip {
@ -225,7 +239,7 @@
.chat-msg-bottom {
display: inline-block;
padding-right: 80px;
padding-right: 300px;
.chat-msg-text {
display: block;
@ -282,7 +296,7 @@
flex-direction: row;
align-items: center;
cursor: pointer;
padding-bottom: 5px;
.chat-file-box {
display: flex;
flex-wrap: nowrap;
@ -338,16 +352,27 @@
}
.chat-unread {
font-size: 10px;
font-size: 12px;
color: #f23c0f;
font-weight: 600;
}
.chat-readed {
font-size: 10px;
font-size: 12px;
color: #888;
font-weight: 600;
}
.chat-receipt{
font-size: 13px;
color: blue;
cursor: pointer;
.icon-ok {
font-size: 20px;
color: green;
}
}
}
}
@ -375,7 +400,7 @@
}
.chat-msg-bottom {
padding-left: 80px;
padding-left: 180px;
padding-right: 0;
.chat-msg-text {

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

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

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

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

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

@ -110,7 +110,6 @@
let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
console.log(m);
if (m) {
//
friend.disabled = true;

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

@ -123,17 +123,21 @@ export default {
}
},
insertMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
this.commit("moveTop", idx)
break;
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
let chat = this.getters.findChat(msgInfo);
let message = this.getters.findMessage(chat, msgInfo);
if(message){
Object.assign(message, msgInfo);
this.commit("saveToStorage");
return;
}
// 插入新的数据
if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
@ -162,28 +166,6 @@ export default {
chat.atAll = true;
}
}
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) {
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
}
// 间隔大于10分钟插入时间显示
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
chat.messages.push({
@ -196,19 +178,18 @@ export default {
chat.messages.push(msgInfo);
this.commit("saveToStorage");
},
deleteMessage(state, msgInfo) {
updateMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
break;
}
let chat = this.getters.findChat(msgInfo);
let message = this.getters.findMessage(chat, msgInfo);
if(message){
// 属性拷贝
Object.assign(message, msgInfo);
this.commit("saveToStorage");
}
},
deleteMessage(state, msgInfo) {
let chat = this.getters.findChat(msgInfo);
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
@ -290,5 +271,37 @@ export default {
resolve();
})
}
},
getters: {
findChat: (state) => (msgInfo) => {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
break;
}
}
return chat;
},
findMessage: (state) => (chat, msgInfo) => {
if (!chat) {
return null;
}
for (let idx in chat.messages) {
// 通过id判断
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
return chat.messages[idx];
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
return chat.messages[idx];
}
}
}
}
}

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

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

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

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

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

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

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

@ -3,8 +3,7 @@
<el-aside width="80px" class="navi-bar">
<div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName"
:url="$store.state.userStore.userInfo.headImageThumb" :size="60"
@click.native="showSettingDialog=true">
:url="$store.state.userStore.userInfo.headImageThumb" :size="60" @click.native="showSettingDialog = true">
</head-image>
</div>
@ -47,8 +46,8 @@
:friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master"
:offer="uiStore.chatPrivateVideo.offer" @close="$store.commit('closeChatPrivateVideoBox')">
</chat-private-video>
<chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show"
:friend="uiStore.videoAcceptor.friend" @close="$store.commit('closeVideoAcceptorBox')">
<chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show" :friend="uiStore.videoAcceptor.friend"
@close="$store.commit('closeVideoAcceptorBox')">
</chat-video-acceptor>
</el-container>
</template>
@ -221,20 +220,30 @@
}
},
handleGroupMessage(msg) {
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
let groupId = msg.groupId;
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
//
let chatInfo = {
type: 'GROUP',
targetId: groupId
targetId: msg.groupId
}
this.$store.commit("resetUnreadCount", chatInfo)
return;
}
this.loadGroupInfo(groupId).then((group) => {
//
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
//
let msgInfo = {
id: msg.id,
groupId: msg.groupId,
readedCount: msg.readedCount
};
this.$store.commit("updateMessage", msgInfo)
return;
}
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
this.loadGroupInfo(msg.groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
@ -341,7 +350,6 @@
</script>
<style scoped lang="scss">
.navi-bar {
background: #333333;
padding: 10px;

12
im-uniapp/App.vue

@ -166,6 +166,17 @@
store.commit("resetUnreadCount", chatInfo)
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
//
let msgInfo = {
id: msg.id,
groupId: msg.groupId,
readedCount: msg.readedCount
};
this.$store.commit("updateMessage", msgInfo)
return;
}
this.loadGroupInfo(groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
@ -234,7 +245,6 @@
// this.audioTip.play();
},
initAudit() {
console.log("initAudit")
if (store.state.userStore.userInfo.type == 1) {
//
uni.setTabBarItem({

1
im-uniapp/common/enums.js

@ -7,6 +7,7 @@ const MESSAGE_TYPE = {
VIDEO:4,
RECALL:10,
READED:11,
RECEIPT:12,
TIP_TIME:20,
RTC_CALL: 101,
RTC_ACCEPT: 102,

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

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

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

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

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

@ -12,7 +12,7 @@
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx"
:msgInfo="msgInfo">
:msgInfo="msgInfo" :groupMembers="groupMembers">
</chat-message-item>
</view>
</scroll-view>
@ -30,6 +30,7 @@
<view class="send-bar">
<view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt?'[回执消息]':''"
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange"
@input="onTextInput" confirm-type="send" confirm-hold :hold-keyboard="true"></textarea>
</view>
@ -68,6 +69,10 @@
<view class="tool-icon iconfont icon-microphone"></view>
<view class="tool-name">语音输入</view>
</view>
<view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="switchReceipt()">
<view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view>
<view class="tool-name">回执消息</view>
</view>
<view class="chat-tools-item" @click="showTip()">
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">呼叫</view>
@ -96,6 +101,7 @@
group: {},
groupMembers: [],
sendText: "",
isReceipt: false, //
showVoice: false, //
scrollMsgIdx: 0, //
chatTabBox: 'none',
@ -108,10 +114,13 @@
methods: {
showTip() {
uni.showToast({
title: "加班开发中...",
title: "暂未支持...",
icon: "none"
})
},
switchReceipt(){
this.isReceipt = !this.isReceipt;
},
openAtBox() {
this.$refs.atBox.init(this.atUserIds);
this.$refs.atBox.open();
@ -146,12 +155,13 @@
title: "不能发送空白信息",
icon: "none"
});
}
let atText = this.createAtText()
let receiptText = this.isReceipt? "【回执消息】":"";
let atText = this.createAtText();
let msgInfo = {
content: this.sendText + atText,
content: receiptText + this.sendText + atText,
atUserIds: this.atUserIds,
receipt : this.isReceipt,
type: 0
}
// id
@ -166,6 +176,7 @@
msgInfo.sendTime = new Date().getTime();
msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true;
msgInfo.readedCount = 0,
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo);
this.sendText = "";
@ -174,6 +185,7 @@
this.scrollToBottom();
// @
this.atUserIds = [];
this.isReceipt = false;
});
},
createAtText() {
@ -215,7 +227,6 @@
return;
}
this.$nextTick(() => {
console.log("scrollToMsgIdx", this.scrollMsgIdx)
this.scrollMsgIdx = idx;
});
@ -256,6 +267,7 @@
sendTime: new Date().getTime(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.IMAGE,
readedCount: 0,
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
}
@ -272,6 +284,7 @@
onUploadImageSuccess(file, res) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(res.data);
msgInfo.receipt = this.isReceipt
this.$http({
url: this.messageAction,
method: 'POST',
@ -279,6 +292,7 @@
}).then((id) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = id;
this.isReceipt = false;
this.$store.commit("insertMessage", msgInfo);
})
},
@ -300,6 +314,7 @@
sendTime: new Date().getTime(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.FILE,
readedCount: 0,
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
}
@ -321,6 +336,7 @@
}
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt
this.$http({
url: this.messageAction,
method: 'POST',
@ -328,6 +344,7 @@
}).then((id) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = id;
this.isReceipt = false;
this.$store.commit("insertMessage", msgInfo);
})
},
@ -399,7 +416,6 @@
//
this.scrollToMsgIdx(this.showMinIdx);
// #endif
// 10
this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
},
@ -565,6 +581,8 @@
this.loadFriend(this.chat.targetId);
this.loadReaded(this.chat.targetId)
}
//
this.isReceipt = false;
},
onUnload() {
this.$store.commit("activeChat", -1);
@ -709,6 +727,10 @@
font-size: 80rpx;
background-color: white;
border-radius: 20%;
&.active{
background-color: #ddd;
}
}
.tool-name {

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

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

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

Binary file not shown.

104
im-uniapp/store/chatStore.js

@ -5,7 +5,6 @@ import {
import userStore from './userStore';
export default {
state: {
activeIndex: -1,
chats: [],
@ -127,18 +126,23 @@ export default {
insertMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
this.commit("moveTop", idx)
break;
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
let chat = this.getters.findChat(msgInfo);
let message = this.getters.findMessage(chat, msgInfo);
if(message){
Object.assign(message, msgInfo);
this.commit("saveToStorage");
return;
}
// 会话列表内容
if(!state.loadingPrivateMsg && !state.loadingPrivateMsg){
if(!state.loadingPrivateMsg && !state.loadingGroupMsg){
if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
chat.lastContent = "[图片]";
} else if (msgInfo.type == MESSAGE_TYPE.FILE) {
@ -166,28 +170,8 @@ export default {
chat.atAll = true;
}
}
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) {
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
}
// 间隔大于10分钟插入时间显示
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
chat.messages.push({
@ -199,21 +183,20 @@ export default {
// 新的消息
chat.messages.push(msgInfo);
this.commit("saveToStorage");
},
deleteMessage(state, msgInfo) {
updateMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
break;
}
let chat = this.getters.findChat(msgInfo);
let message = this.getters.findMessage(chat, msgInfo);
if(message){
// 属性拷贝
Object.assign(message, msgInfo);
this.commit("saveToStorage");
}
},
deleteMessage(state, msgInfo) {
// 获取对方id或群id
let chat = this.getters.findChat(msgInfo);
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
@ -324,6 +307,37 @@ export default {
});
})
}
},
getters: {
findChat: (state) => (msgInfo) => {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let chat = null;
for (let idx in state.chats) {
if (state.chats[idx].type == type &&
state.chats[idx].targetId === targetId) {
chat = state.chats[idx];
break;
}
}
return chat;
},
findMessage: (state) => (chat, msgInfo) => {
if (!chat) {
return null;
}
for (let idx in chat.messages) {
// 通过id判断
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
return chat.messages[idx];
}
// 正在发送中的消息可能没有id,通过发送时间判断
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
return chat.messages[idx];
}
}
}
}
}
Loading…
Cancel
Save