Browse Source

已读未读显示(完成)

xie.bx 2 years ago
parent
commit
c000c2e7ab

+ 4 - 6
im-client/src/main/java/com/bx/imclient/sender/IMSender.java

@@ -14,8 +14,6 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 
 @Service
 public class IMSender {
@@ -34,7 +32,7 @@ public class IMSender {
             Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
             // 如果对方在线,将数据存储至redis,等待拉取推送
             if (serverId != null) {
-                String sendKey = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE, serverId.toString());
+                String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE, serverId.toString());
                 IMRecvInfo recvInfo = new IMRecvInfo();
                 recvInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code());
                 recvInfo.setSendResult(message.getSendResult());
@@ -63,7 +61,7 @@ public class IMSender {
                 Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
                 // 如果终端在线,将数据存储至redis,等待拉取推送
                 if (serverId != null) {
-                    String sendKey = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE, serverId.toString());
+                    String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE, serverId.toString());
                     IMRecvInfo recvInfo = new IMRecvInfo();
                     // 自己的消息不需要回推消息结果
                     recvInfo.setSendResult(false);
@@ -112,7 +110,7 @@ public class IMSender {
             recvInfo.setSendResult(message.getSendResult());
             recvInfo.setData(message.getData());
             // 推送至队列
-            String key = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE, entry.getKey().toString());
+            String key = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE, entry.getKey().toString());
             redisTemplate.opsForList().rightPush(key, recvInfo);
         }
         // 对离线用户回复消息状态
@@ -144,7 +142,7 @@ public class IMSender {
                     // 自己的消息不需要回推消息结果
                     recvInfo.setSendResult(false);
                     recvInfo.setData(message.getData());
-                    String sendKey = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE, serverId.toString());
+                    String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE, serverId.toString());
                     redisTemplate.opsForList().rightPush(sendKey, recvInfo);
                 }
             }

+ 2 - 2
im-commom/src/main/java/com/bx/imcommon/contant/IMRedisKey.java

@@ -7,9 +7,9 @@ public class IMRedisKey {
     // 用户ID所连接的IM-server的ID
     public final static String  IM_USER_SERVER_ID = "im:user:server_id";
     // 未读私聊消息队列
-    public final static String IM_UNREAD_PRIVATE_QUEUE = "im:unread:private";
+    public final static String IM_MESSAGE_PRIVATE_QUEUE = "im:message:private";
     // 未读群聊消息队列
-    public final static String IM_UNREAD_GROUP_QUEUE = "im:unread:group";
+    public final static String IM_MESSAGE_GROUP_QUEUE = "im:message:group";
     // 私聊消息发送结果队列
     public final static String IM_RESULT_PRIVATE_QUEUE = "im:result:private";
     // 群聊消息发送结果队列

+ 1 - 0
im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java

@@ -22,6 +22,7 @@ public class GroupMessageListener implements MessageListener<GroupMessageVO> {
     @Override
     public void process(IMSendResult<GroupMessageVO> result){
         GroupMessageVO messageInfo = result.getData();
+        // todo 删除
         // 保存该用户已拉取的最大消息id
         if(result.getCode().equals(IMSendCode.SUCCESS.code())) {
             String key = String.join(":",RedisKey.IM_GROUP_READED_POSITION,messageInfo.getGroupId().toString(),result.getReceiver().getId().toString());

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

@@ -79,7 +79,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
     @Override
     public void delFriend(Long friendId) {
         long userId = SessionContext.getSession().getUserId();
-        // 互相解除好友关系
+        // 互相解除好友关系,走代理清理缓存
         FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
         proxy.unbindFriend(userId,friendId);
         proxy.unbindFriend(friendId,userId);

+ 16 - 3
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.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.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -27,6 +28,7 @@ import com.bx.implatform.session.UserSession;
 import com.bx.implatform.util.BeanUtils;
 import com.bx.implatform.dto.GroupMessageDTO;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
@@ -50,7 +52,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
     private IMClient imClient;
 
     /**
-     * 发送群聊消息(与mysql所有交换都要进行缓存)
+     * 发送群聊消息(高并发接口,查询mysql接口都要进行缓存)
      *
      * @param dto 群聊消息
      * @return 群聊id
@@ -218,8 +220,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
             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.SENDED.code());
+                    // 已
+                    vo.setStatus(MessageStatus.READED.code());
                 } else {
                     // 未推送
                     vo.setStatus(MessageStatus.UNSEND.code());
@@ -250,6 +252,17 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
         sendMessage.setData(msgInfo);
         sendMessage.setSendResult(false);
         imClient.sendGroupMessage(sendMessage);
+
+        // 记录已读位置
+        LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
+        wrapper.eq(GroupMessage::getGroupId, groupId)
+                .orderByDesc(GroupMessage::getId)
+                .last("limit 1")
+                .select(GroupMessage::getId);
+        GroupMessage message = this.getOne(wrapper);
+        String key = StrUtil.join(":",RedisKey.IM_GROUP_READED_POSITION,groupId,session.getUserId());
+        redisTemplate.opsForValue().set(key, message.getId());
+
     }
 
     /**

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

@@ -45,7 +45,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
     private IMClient imClient;
 
     /**
-     * 发送私聊消息
+     * 发送私聊消息(高并发接口,查询mysql接口都要进行缓存)
      *
      * @param dto 私聊消息
      * @return 消息id

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

@@ -23,7 +23,7 @@ public class PullUnreadGroupMessageTask extends  AbstractPullMessageTask {
     @Override
     public void pullMessage() {
         // 从redis拉取未读消息
-        String key = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE,IMServerGroup.serverId+"");
+        String key = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE,IMServerGroup.serverId+"");
         JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
         if(jsonObject != null){
             IMRecvInfo recvInfo = jsonObject.toJavaObject(IMRecvInfo.class);

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

@@ -25,7 +25,7 @@ public class PullUnreadPrivateMessageTask extends  AbstractPullMessageTask {
     @Override
     public void pullMessage() {
         // 从redis拉取未读消息
-        String key = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE ,IMServerGroup.serverId+"");
+        String key = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE,IMServerGroup.serverId+"");
         JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
         if(jsonObject!=null){
             IMRecvInfo recvInfo = jsonObject.toJavaObject(IMRecvInfo.class);

+ 36 - 13
im-ui/src/components/chat/ChatBox.vue

@@ -8,11 +8,11 @@
 		<el-main style="padding: 0;">
 			<el-container>
 				<el-container class="content-box">
-					<el-main class="im-chat-main" id="chatScrollBox">
+					<el-main class="im-chat-main" id="chatScrollBox" @scroll="handleScroll">
 						<div class="im-chat-box">
 							<ul>
 								<li v-for="(msgInfo,idx) in chat.messages" :key="idx">
-									<chat-message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
+									<chat-message-item v-show="idx>=showMinIdx" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
 										:showName="showName(msgInfo)" :msgInfo="msgInfo" @delete="deleteMessage"
 										@recall="recallMessage">
 									</chat-message-item>
@@ -115,7 +115,8 @@
 					y: 0
 				},
 				showHistory: false, // 是否显示历史聊天记录
-				lockMessage: false // 是否锁定发送
+				lockMessage: false, // 是否锁定发送,
+				showMinIdx: 0 // 下标低于showMinIdx的消息不显示,否则页面会很卡
 			}
 		},
 		methods: {
@@ -236,6 +237,22 @@
 			handleCloseSide() {
 				this.showSide = false;
 			},
+			handleScrollToTop() {
+				// 多展示10条信息
+				this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
+			},
+			handleScroll(e) {
+			
+				let scrollElement = e.target
+				let scrollTop = scrollElement.scrollTop
+				if (scrollTop <30 ) { // 在顶部,不滚动的情况
+					console.log("next")
+					// 多展示20条信息
+					this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
+	
+				}
+				
+			},
 			switchEmotionBox() {
 				this.showEmotion = !this.showEmotion;
 				let width = this.$refs.emotion.offsetWidth;
@@ -329,7 +346,6 @@
 			},
 			sendTextMessage() {
 				if (!this.sendText.trim()) {
-					this.$message.error("不能发送空白信息");
 					return
 				}
 				let msgInfo = {
@@ -396,16 +412,16 @@
 				});
 			},
 			readedMessage() {
-				if(this.chat.type == "GROUP"){
+				if (this.chat.type == "GROUP") {
 					var url = `/message/group/readed?groupId=${this.chat.targetId}`
-				}else{
+				} else {
 					url = `/message/private/readed?friendId=${this.chat.targetId}`
 				}
 				this.$http({
 					url: url,
 					method: 'put'
 				}).then(() => {
-					this.$store.commit("resetUnreadCount",this.chat)
+					this.$store.commit("resetUnreadCount", this.chat)
 					this.scrollToBottom();
 				})
 			},
@@ -457,7 +473,7 @@
 			},
 			scrollToBottom() {
 				this.$nextTick(() => {
-					const div = document.getElementById("chatScrollBox");
+					let div = document.getElementById("chatScrollBox");
 					div.scrollTop = div.scrollHeight;
 				});
 			}
@@ -490,8 +506,9 @@
 		watch: {
 			chat: {
 				handler(newChat, oldChat) {
-					if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type || newChat.targetId != oldChat
-							.targetId)) {
+					if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
+							newChat.targetId != oldChat.targetId)) {
+
 						if (this.chat.type == "GROUP") {
 							this.loadGroup(this.chat.targetId);
 						} else {
@@ -499,6 +516,9 @@
 						}
 						this.scrollToBottom();
 						this.sendText = "";
+						// 初始状态只显示30条消息
+						let size = this.chat.messages.length;
+						this.showMinIdx = size > 30 ? size - 30 : 0;
 						// 保持输入框焦点
 						this.$nextTick(() => {
 							this.$refs.sendBox.focus();
@@ -509,12 +529,16 @@
 			},
 			unreadCount: {
 				handler(newCount, oldCount) {
-					if(newCount > 0){
+					if (newCount > 0) {
 						// 消息已读
 						this.readedMessage()
 					}
 				}
 			}
+		},
+		mounted() {
+			let div = document.getElementById("chatScrollBox");
+			div.addEventListener('scroll', this.handleScroll)
 		}
 	}
 </script>
@@ -606,8 +630,7 @@
 					color: black;
 					background-color: #f8f8f8 !important;
 					outline-color: rgba(83, 160, 231, 0.61);
-					text-align: left;
-					border: 0;
+
 				}
 
 				.send-image-area {

+ 2 - 2
im-ui/src/components/chat/ChatMessageItem.vue

@@ -342,10 +342,10 @@
 						color: #f23c0f;
 						font-weight: 600;
 					}
-
+					
 					.chat-readed {
 						font-size: 10px;
-						color: #aaa;
+						color: #888;
 						font-weight: 600;
 					}
 				}

+ 26 - 26
im-ui/src/store/chatStore.js

@@ -15,22 +15,16 @@ export default {
 	},
 
 	mutations: {
-		initChats(state, chats) {
-			state.chats = chats||[];
+		initChats(state, chatsData) {
+			state.chats = chatsData.chats || [];
+			state.privateMsgMaxId = chatsData.privateMsgMaxId || 0;
+			state.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
+			// 防止图片一直处在加载中状态
 			state.chats.forEach((chat) => {
 				chat.messages.forEach((msg) => {
-					// 防止图片一直处在加载中状态
 					if (msg.loadStatus == "loading") {
 						msg.loadStatus = "fail"
 					}
-					// 记录最大私聊消息id
-					if(chat.type == "PRIVATE" && msg.id && msg.id>state.privateMsgMaxId){
-						state.privateMsgMaxId = msg.id;
-					}
-					// 记录最大群聊消息id
-					if(chat.type == "GROUP" && msg.id && msg.id>state.groupMsgMaxId){
-						state.groupMsgMaxId = msg.id;
-					}
 				})
 			})
 		},
@@ -76,17 +70,17 @@ export default {
 		},
 		resetUnreadCount(state, chatInfo) {
 			for (let idx in state.chats) {
-				if (state.chats[idx].type == chatInfo.type
-				 && state.chats[idx].targetId == chatInfo.targetId) {
-					state.chats[idx].unreadCount=0;
+				if (state.chats[idx].type == chatInfo.type &&
+					state.chats[idx].targetId == chatInfo.targetId) {
+					state.chats[idx].unreadCount = 0;
 				}
 			}
 			this.commit("saveToStorage");
 		},
 		readedMessage(state, friendId) {
 			for (let idx in state.chats) {
-				if (state.chats[idx].type == 'PRIVATE'
-				 && state.chats[idx].targetId == friendId) {
+				if (state.chats[idx].type == 'PRIVATE' &&
+					state.chats[idx].targetId == friendId) {
 					state.chats[idx].messages.forEach((m) => {
 						if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) {
 							m.status = MESSAGE_STATUS.READED
@@ -154,12 +148,11 @@ export default {
 			if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
 				chat.unreadCount++;
 			}
-
 			// 记录消息的最大id
-			if (msgInfo.id && type=="PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
+			if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
 				state.privateMsgMaxId = msgInfo.id;
-			} 
-			if (msgInfo.id && type=="GROUP" && msgInfo.id > state.groupMsgMaxId) {
+			}
+			if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
 				state.groupMsgMaxId = msgInfo.id;
 			}
 			// 如果是已存在消息,则覆盖旧的消息数据
@@ -239,17 +232,22 @@ export default {
 			}
 			this.commit("saveToStorage");
 		},
-		
-		loadingPrivateMsg(state,loadding){
+
+		loadingPrivateMsg(state, loadding) {
 			state.loadingPrivateMsg = loadding;
 		},
-		loadingGroupMsg(state,loadding){
+		loadingGroupMsg(state, loadding) {
 			state.loadingGroupMsg = loadding;
 		},
 		saveToStorage(state) {
 			let userId = userStore.state.userInfo.id;
 			let key = "chats-" + userId;
-			localStorage.setItem(key, JSON.stringify(state.chats));
+			let chatsData = {
+				privateMsgMaxId: state.privateMsgMaxId,
+				groupMsgMaxId: state.groupMsgMaxId,
+				chats: state.chats
+			}
+			localStorage.setItem(key, JSON.stringify(chatsData));
 		},
 		clear(state) {
 			state.activeIndex = -1;
@@ -262,8 +260,10 @@ export default {
 				let userId = userStore.state.userInfo.id;
 				let key = "chats-" + userId;
 				let item = localStorage.getItem(key)
-				let chats = JSON.parse(localStorage.getItem(key));
-				context.commit("initChats", chats);
+				if (item) {
+					let chatsData = JSON.parse(item);
+					context.commit("initChats", chatsData);
+				}
 				resolve();
 			})
 		}

+ 16 - 9
im-ui/src/view/Home.vue

@@ -74,13 +74,13 @@
 		data() {
 			return {
 				showSettingDialog: false,
-
+				lastPlayAudioTime: new Date()-1000
 			}
 		},
 		methods: {
 			init() {
 				this.$store.dispatch("load").then(() => {
-					// 加载未拉取的消息
+					// 加载离线消息
 					this.loadPrivateMessage(this.$store.state.chatStore.privateMsgMaxId);
 					this.loadGroupMessage(this.$store.state.chatStore.groupMsgMaxId);
 					// ws初始化
@@ -202,8 +202,9 @@
 				// 插入消息
 				this.$store.commit("insertMessage", msg);
 				// 播放提示音
-				!msg.selfSend && this.playAudioTip();
-
+				if(!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED){
+					this.playAudioTip();
+				}
 			},
 			handleGroupMessage(msg) {
 				// 标记这条消息是不是自己发的
@@ -236,7 +237,9 @@
 				// 插入消息
 				this.$store.commit("insertMessage", msg);
 				// 播放提示音
-				!msg.selfSend && this.playAudioTip();
+				if(!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED){
+					this.playAudioTip();
+				}
 			},
 			handleExit() {
 				this.$wsApi.close();
@@ -244,10 +247,14 @@
 				location.href = "/";
 			},
 			playAudioTip() {
-				let audio = new Audio();
-				let url = require(`@/assets/audio/tip.wav`);
-				audio.src = url;
-				audio.play();
+				if(new Date() - this.lastPlayAudioTime > 1000){
+					this.lastPlayAudioTime = new Date();
+					let audio = new Audio();
+					let url = require(`@/assets/audio/tip.wav`);
+					audio.src = url;
+					audio.play();
+				} 
+				
 			},
 			showSetting() {
 				this.showSettingDialog = true;

+ 114 - 52
im-uniapp/App.vue

@@ -18,6 +18,9 @@
 					this.initAudit();
 					// 初始化websocket
 					this.initWebSocket();
+					// 加载离线消息
+					this.loadPrivateMessage(store.state.chatStore.privateMsgMaxId);
+					this.loadGroupMessage(store.state.chatStore.groupMsgMaxId);
 				}).catch((e) => {
 					console.log(e);
 					this.exit();
@@ -28,10 +31,7 @@
 				let userId = store.state.userStore.userInfo.id;
 				wsApi.init(process.env.WS_URL, loginInfo.accessToken);
 				wsApi.connect();
-				wsApi.onOpen(()=>{
-					// 拉取未读消息
-					this.pullUnreadMessage();
-				})
+				wsApi.onOpen()
 				wsApi.onMessage((cmd, msgInfo) => {
 					if (cmd == 2) {
 						// 异地登录,强制下线
@@ -41,58 +41,85 @@
 						})
 						this.exit();
 					} else if (cmd == 3) {
-						// 标记这条消息是不是自己发的
-						msgInfo.selfSend = userId == msgInfo.sendId;
-						// 插入私聊消息
+						// 私聊消息
 						this.handlePrivateMessage(msgInfo);
 					} else if (cmd == 4) {
-						// 标记这条消息是不是自己发的
-						msgInfo.selfSend = userId == msgInfo.sendId;
-						// 插入群聊消息
+						// 群聊消息
 						this.handleGroupMessage(msgInfo);
 					}
 				});
-				wsApi.onClose((res)=>{
+				wsApi.onClose((res) => {
 					// 1006是服务器主动断开,3000是APP主动关闭
-					if(res.code == 1006){
+					if (res.code == 1006) {
 						uni.showToast({
 							title: '连接已断开,请重新登录',
 							icon: 'none',
 						})
 						this.exit();
-					}else if(res.code != 3000){
+					} else if (res.code != 3000) {
 						// 重新连接
 						wsApi.connect();
 					}
 				})
 			},
-			pullUnreadMessage() {
-				// 拉取未读私聊消息
+			loadPrivateMessage(minId) {
+				store.commit("loadingPrivateMsg", true)
 				http({
-					url: "/message/private/pullUnreadMessage",
-					method: 'POST'
-				});
-				// 拉取未读群聊消息
+					url: "/message/private/loadMessage?minId=" + minId,
+					method: 'get'
+				}).then((msgInfos) => {
+					msgInfos.forEach((msgInfo) => {
+						this.handlePrivateMessage(msgInfo);
+					})
+					if (msgInfos.length == 100) {
+						// 继续拉取
+						this.loadPrivateMessage(msgInfos[99].id);
+					} else {
+						store.commit("loadingPrivateMsg", false)
+					}
+				})
+			},
+			loadGroupMessage(minId) {
+				store.commit("loadingGroupMsg", true)
 				http({
-					url: "/message/group/pullUnreadMessage",
-					method: 'POST'
-				});
+					url: "/message/group/loadMessage?minId=" + minId,
+					method: 'get'
+				}).then((msgInfos) => {
+					msgInfos.forEach((msgInfo) => {
+						this.handleGroupMessage(msgInfo);
+					})
+					if (msgInfos.length == 100) {
+						// 继续拉取
+						this.loadGroupMessage(msgInfos[99].id);
+					} else {
+						store.commit("loadingGroupMsg", false)
+					}
+				})
 			},
 			handlePrivateMessage(msg) {
+				// 标记这条消息是不是自己发的
+				msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
+				// 好友id
 				let friendId = msg.selfSend ? msg.recvId : msg.sendId;
-				let friend = store.state.friendStore.friends.find((f) => f.id == friendId);
-				if (!friend) {
-					http({
-						url: `/friend/find/${msg.sendId}`,
-						method: 'GET'
-					}).then((friend) => {
-						this.insertPrivateMessage(friend, msg);
-						store.commit("addFriend", friend);
-					})
-				} else {
-					// 好友列表不存在好友信息,则发请求获取好友信息
-					this.insertPrivateMessage(friend, msg);
+				// 消息已读处理
+				if (msg.type == enums.MESSAGE_TYPE.READED) {
+					if (msg.selfSend) {
+						// 我已读对方的消息,清空已读数量
+						let chatInfo = {
+							type: 'PRIVATE',
+							targetId: friendId
+						}
+						store.commit("resetUnreadCount", chatInfo)
+					} else {
+						// 对方已读我的消息,修改消息状态为已读
+						store.commit("readedMessage", friendId)
+						
+					}
+					return;
 				}
+				this.loadFriendInfo(friendId).then((friend) => {
+					this.insertPrivateMessage(friend, msg);
+				})
 
 			},
 			insertPrivateMessage(friend, msg) {
@@ -115,19 +142,23 @@
 
 			},
 			handleGroupMessage(msg) {
-				let group = store.state.groupStore.groups.find((g) => g.id == msg.groupId);
-				if (!group) {
-					http({
-						url: `/group/find/${msg.groupId}`,
-						method: 'get'
-					}).then((group) => {
-						this.insertGroupMessage(group, msg);
-						store.commit("addGroup", group);
-					})
-				} else {
-					// 群聊缓存存在,直接插入群聊消息
-					this.insertGroupMessage(group, msg);
+				// 标记这条消息是不是自己发的
+				msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
+				let groupId = msg.groupId;
+				// 消息已读处理
+				if (msg.type == enums.MESSAGE_TYPE.READED) {
+					// 我已读对方的消息,清空已读数量
+					let chatInfo = {
+						type: 'GROUP',
+						targetId: groupId
+					}
+					store.commit("resetUnreadCount", chatInfo)
+					return;
 				}
+				this.loadGroupInfo(groupId).then((group) => {
+					// 插入群聊消息
+					this.insertGroupMessage(group, msg);
+				})
 
 			},
 			insertGroupMessage(group, msg) {
@@ -144,11 +175,43 @@
 				// 播放提示音
 				!msg.selfSend && this.playAudioTip();
 			},
+			loadFriendInfo(id) {
+				return new Promise((resolve, reject) => {
+					let friend = store.state.friendStore.friends.find((f) => f.id == id);
+					if (friend) {
+						resolve(friend);
+					} else {
+						http({
+							url: `/friend/find/${id}`,
+							method: 'get'
+						}).then((friend) => {
+							store.commit("addFriend", friend);
+							resolve(friend)
+						})
+					}
+				});
+			},
+			loadGroupInfo(id) {
+				return new Promise((resolve, reject) => {
+					let group = store.state.groupStore.groups.find((g) => g.id == id);
+					if (group) {
+						resolve(group);
+					} else {
+						http({
+							url: `/group/find/${id}`,
+							method: 'get'
+						}).then((group) => {
+							resolve(group)
+							store.commit("addGroup", group);
+						})
+					}
+				});
+			},
 			exit() {
 				console.log("exit");
 				wsApi.close();
 				uni.removeStorageSync("loginInfo");
-				uni.navigateTo({
+				uni.reLaunch({
 					url: "/pages/login/login"
 				})
 				store.dispatch("unload");
@@ -161,18 +224,18 @@
 			},
 			initAudit() {
 				console.log("initAudit")
-				if(store.state.userStore.userInfo.type == 1){
+				if (store.state.userStore.userInfo.type == 1) {
 					// 显示群组功能
 					uni.setTabBarItem({
 						index: 2,
 						text: "群聊"
-					})	
-				}else{
+					})
+				} else {
 					// 隐藏群组功能
 					uni.setTabBarItem({
 						index: 2,
 						text: "搜索"
-					})	
+					})
 				}
 			}
 		},
@@ -214,5 +277,4 @@
 		// #endif
 		background-color: #f8f8f8;
 	}
-	
 </style>

+ 4 - 19
im-uniapp/common/emotion.js

@@ -7,41 +7,26 @@ const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心',
 ];
 
 
-let emoImageUrlList = [];
-
-// 备注:经过测试,小程序的<rich-text>无法显示相对路径的图片,所以在这里对图片提前全部转成绝对路径
-// 提前初始化图片的url
-for (let i = 0; i < emoTextList.length; i++) {
-	let path = `/static/emoji2/${i}.gif`;
-	uni.getImageInfo({
-		src: path,
-		success(res) {
-			emoImageUrlList[i] = res.path
-		},
-		fail(res) {
-			emoImageUrlList = path;
-		}
-	});
-}
-
 
 let transform = (content) => {
 	return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg);
 }
 
 
-
 // 将匹配结果替换表情图片
 let textToImg = (emoText) => {
 	let word = emoText.replace(/\#|\;/gi, '');
 	let idx = emoTextList.indexOf(word);
+	if (idx == -1) {
+		return "";
+	}
 	let path = textToPath(emoText);
 	// #ifdef MP
 	// 微信小程序不能有前面的'/'
 	path = path.slice(1);
 	// #endif
 	let img = `<img src="${path}" style="with:35px;height:35px;
-	margin: 0 -2px;vertical-align:bottom;"/>`;
+		margin: 0 -2px;vertical-align:bottom;"/>`;
 	return img;
 }
 

+ 10 - 1
im-uniapp/common/enums.js

@@ -6,6 +6,7 @@ const MESSAGE_TYPE = {
 	AUDIO:3,
 	VIDEO:4,
 	RECALL:10,
+	READED:11,
 	TIP_TIME:20,
 	RTC_CALL: 101,
 	RTC_ACCEPT: 102,
@@ -27,8 +28,16 @@ const TERMINAL_TYPE = {
 	APP: 1
 }
 
+const MESSAGE_STATUS = {
+	UNSEND: 0,
+	SENDED: 1,
+	RECALL:2,
+	READED:3
+}
+
 export {
 	MESSAGE_TYPE,
 	USER_STATE,
-	TERMINAL_TYPE
+	TERMINAL_TYPE,
+	MESSAGE_STATUS
 }

+ 0 - 3
im-uniapp/common/request.js

@@ -43,9 +43,6 @@ const request = (options) => {
 					requestList.forEach(cb => cb());
 					requestList = [];
 					isRefreshToken = false;
-					// 保存token
-					console.log(res.data.data.accessToken)
-					
 					// 重新发送刚才的请求
 					return resolve(request(options))
 

+ 34 - 21
im-uniapp/components/chat-message-item/chat-message-item.vue

@@ -2,12 +2,12 @@
 	<view class="chat-msg-item">
 		<view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view>
 		<view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME">
-		{{$date.toTimeText(msgInfo.sendTime)}}</view>
-		
+			{{$date.toTimeText(msgInfo.sendTime)}}
+		</view>
+
 		<view class="chat-msg-normal" v-if="msgInfo.type>=0 && msgInfo.type<10"
 			:class="{'chat-msg-mine':msgInfo.selfSend}">
-			<head-image class="avatar" :id="msgInfo.sendId" :url="headImage"
-			:name="showName" :size="80"></head-image>
+			<head-image class="avatar" :id="msgInfo.sendId" :url="headImage" :name="showName" :size="80"></head-image>
 			<view class="chat-msg-content" @longpress="onShowMenu($event)">
 				<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
 					<text>{{showName}}</text>
@@ -18,8 +18,8 @@
 						:nodes="$emo.transform(msgInfo.content)"></rich-text>
 					<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE">
 						<view class="img-load-box">
-							<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true"
-								@click.stop="onShowFullImage()">
+							<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl"
+								lazy-load="true" @click.stop="onShowFullImage()">
 							</image>
 							<loading v-if="loading"></loading>
 						</view>
@@ -40,6 +40,11 @@
 						<text title="发送失败" v-if="loadFail" @click="onSendFail"
 							class="send-fail iconfont icon-warning-circle-fill"></text>
 					</view>
+					<text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
+							&& 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-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
 						<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
@@ -219,12 +224,13 @@
 
 				.chat-msg-bottom {
 					display: inline-block;
-					padding-right: 80rpx ;
-					.chat-msg-text {		
+					padding-right: 80rpx;
+
+					.chat-msg-text {
 						position: relative;
 						line-height: 60rpx;
 						margin-top: 10rpx;
-						padding: 10rpx;
+						padding: 10rpx 20rpx;
 						background-color: #ebebf5;
 						border-radius: 10rpx;
 						color: #333;
@@ -234,6 +240,7 @@
 						word-break: break-all;
 						white-space: pre-line;
 						box-shadow: 2px 2px 2px #c0c0f0;
+
 						&:after {
 							content: "";
 							position: absolute;
@@ -244,7 +251,7 @@
 							border-style: solid dashed dashed;
 							border-color: #ebebf5 transparent transparent;
 							overflow: hidden;
-							border-width: 20rpx;
+							border-width: 18rpx;
 						}
 					}
 
@@ -295,13 +302,14 @@
 							background-color: #eeeeee;
 							padding: 10px 15px;
 							box-shadow: 2px 2px 2px #c0c0c0;
+
 							.chat-file-info {
 								flex: 1;
 								height: 100%;
 								text-align: left;
 								font-size: 14px;
 								width: 300rpx;
-	
+
 								.chat-file-name {
 									font-size: 16px;
 									font-weight: 600;
@@ -325,14 +333,17 @@
 
 					}
 
-					.chat-msg-voice {
-						font-size: 14px;
-						cursor: pointer;
-
-						audio {
-							height: 45px;
-							padding: 5px 0;
-						}
+					
+					.chat-unread {
+						font-size: 10px;
+						color: #f23c0f;
+						font-weight: 600;
+					}
+					
+					.chat-readed {
+						font-size: 10px;
+						color: #ccc;
+						font-weight: 600;
 					}
 				}
 			}
@@ -350,15 +361,17 @@
 
 				.chat-msg-content {
 					text-align: right;
-					
+
 					.chat-msg-bottom {
-						padding-left: 80rpx ;
+						padding-left: 80rpx;
 						padding-right: 0;
+
 						.chat-msg-text {
 							margin-left: 10px;
 							background-color: #587ff0;
 							color: #fff;
 							box-shadow: 1px 1px 1px #ccc;
+
 							&:after {
 								left: auto;
 								right: -10px;

+ 53 - 38
im-uniapp/components/loading/loading.vue

@@ -1,49 +1,64 @@
 <template>
-  <view class="loading-box">
-    <view class="rotate iconfont icon-loading" ></view>
-  </view>
+	<view class="loading-box" :style="loadingStyle">
+		<view class="rotate iconfont icon-loading" :style="icontStyle"></view>
+		<slot></slot>
+	</view>
 </template>
 
 <script>
-  export default {
-    data() {
-      return {};
-    },
-    methods: {
-    },
-    computed: {
-    }
-  };
+	import {
+		computed
+	} from "vue"
+	export default {
+		data() {
+			return {}
+		},
+		props: {
+			size: {
+				type: Number,
+				default: 100
+			},
+			mask: {
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			icontStyle() {
+				return `font-size:${this.size}rpx`;
+			},
+			loadingStyle() {
+				return this.mask ? "background: rgba(0, 0, 0, 0.3);" : "";
+			}
+		}
+	}
 </script>
 
-<style>
-  .loading-box {
-    width: 100%;
-    height: 100%;
-    background-color: rgba(0, 0, 0, 0.4);
-    position: absolute;
-    left: 0;
-    top: 0;
-    z-index: 10000;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-  }
+<style lang="scss" scoped>
+	.loading-box {
+		width: 100%;
+		height: 100%;
+		position: absolute;
+		left: 0;
+		top: 0;
+		z-index: 10000;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
 
+	.rotate {
+		animation: rotate 2s ease-in-out infinite;
 
+	}
 
-  .rotate {
-    animation: rotate 2s ease-in-out infinite;
-	font-size: 100rpx;
-  }
+	@keyframes rotate {
+		from {
+			transform: rotate(0deg)
+		}
 
-  @keyframes rotate {
-    from {
-      transform: rotate(0deg)
-    }
-
-    to {
-      transform: rotate(360deg)
-    }
-  }
+		to {
+			transform: rotate(360deg)
+		}
+	}
 </style>

+ 64 - 19
im-uniapp/pages/chat/chat-box.vue

@@ -6,9 +6,10 @@
 			<uni-icons class="btn-side right" type="more-filled" size="30" @click="onShowMore()"></uni-icons>
 		</view>
 		<view class="chat-msg" @click="switchChatTabBox('none',true)">
-			<scroll-view class="scroll-box" scroll-y="true" :scroll-into-view="'chat-item-'+scrollMsgIdx">
+			<scroll-view class="scroll-box" scroll-y="true" @scrolltoupper="onScrollToTop"
+			:scroll-into-view="'chat-item-'+scrollMsgIdx">
 				<view v-for="(msgInfo,idx) in chat.messages" :key="idx">
-					<chat-message-item :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
+					<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
 						@recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile"
 						:id="'chat-item-'+idx" :msgInfo="msgInfo">
 					</chat-message-item>
@@ -32,8 +33,8 @@
 		<view class="chat-tab-bar" v-show="chatTabBox!='none' ||showKeyBoard " :style="{height:`${keyboardHeight}px`}">
 			<view v-if="chatTabBox == 'tools'" class="chat-tools">
 				<view class="chat-tools-item">
-					<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
-						:onError="onUploadImageFail">
+					<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
+						:onSuccess="onUploadImageSuccess" :onError="onUploadImageFail">
 						<view class="tool-icon iconfont icon-picture"></view>
 					</image-upload>
 					<view class="tool-name">相册</view>
@@ -66,8 +67,9 @@
 
 			<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
 				<view class="emotion-item-list">
-					<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)" v-for="(emoText, i) in $emo.emoTextList"
-						:key="i" @click="selectEmoji(emoText)" mode="aspectFit" lazy-load="true"></image>
+					<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
+						v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
+						lazy-load="true"></image>
 				</view>
 			</scroll-view>
 			<view v-if="showKeyBoard"></view>
@@ -89,11 +91,12 @@
 				scrollMsgIdx: 0, // 滚动条定位为到哪条消息
 				chatTabBox: 'none',
 				showKeyBoard: false,
-				keyboardHeight: 322
+				keyboardHeight: 322,
+				showMinIdx: 0 // 下标小于showMinIdx的消息不显示,否则可能很卡
 			}
 		},
 		methods: {
-			showTip(){
+			showTip() {
 				uni.showToast({
 					title: "加班开发中...",
 					icon: "none"
@@ -139,6 +142,7 @@
 					msgInfo.sendTime = new Date().getTime();
 					msgInfo.sendId = this.$store.state.userStore.userInfo.id;
 					msgInfo.selfSend = true;
+					msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
 					this.$store.commit("insertMessage", msgInfo);
 					this.sendText = "";
 				}).finally(() => {
@@ -171,6 +175,7 @@
 					return;
 				}
 				this.$nextTick(() => {
+					console.log("scrollToMsgIdx",this.scrollMsgIdx)
 					this.scrollMsgIdx = idx;
 				});
 
@@ -184,7 +189,7 @@
 			selectEmoji(emoText) {
 				this.sendText += `#${emoText};`;
 			},
-			onNavBack(){
+			onNavBack() {
 				uni.switchTab({
 					url: "/pages/chat/chat"
 				})
@@ -211,7 +216,8 @@
 					sendTime: new Date().getTime(),
 					selfSend: true,
 					type: this.$enums.MESSAGE_TYPE.IMAGE,
-					loadStatus: "loading"
+					loadStatus: "loading",
+					status: this.$enums.MESSAGE_STATUS.UNSEND
 				}
 				// 填充对方id
 				this.fillTargetId(msgInfo, this.chat.targetId);
@@ -254,7 +260,8 @@
 					sendTime: new Date().getTime(),
 					selfSend: true,
 					type: this.$enums.MESSAGE_TYPE.FILE,
-					loadStatus: "loading"
+					loadStatus: "loading",
+					status: this.$enums.MESSAGE_STATUS.UNSEND
 				}
 				// 填充对方id
 				this.fillTargetId(msgInfo, this.chat.targetId);
@@ -318,6 +325,7 @@
 								msgInfo = JSON.parse(JSON.stringify(msgInfo));
 								msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL;
 								msgInfo.content = '你撤回了一条消息';
+								msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
 								this.$store.commit("insertMessage", msgInfo);
 							})
 						}
@@ -337,7 +345,7 @@
 							});
 						}
 					},
-					fail(e){
+					fail(e) {
 						console.log(e);
 						uni.showToast({
 							title: "文件下载失败",
@@ -346,17 +354,37 @@
 					}
 				});
 			},
-			onShowMore(){
+			onScrollToTop() {
+				// 防止滚动条定格在顶部,不能一直往上滚
+				this.scrollToMsgIdx(this.showMinIdx);
+				// 多展示10条信息
+				this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
+			},
+			onShowMore() {
 				if (this.chat.type == "GROUP") {
 					uni.navigateTo({
-						url: "/pages/group/group-info?id="+this.group.id
+						url: "/pages/group/group-info?id=" + this.group.id
 					})
-				}else{
+				} else {
 					uni.navigateTo({
-						url: "/pages/common/user-info?id="+this.friend.id
+						url: "/pages/common/user-info?id=" + this.friend.id
 					})
 				}
 			},
+			readedMessage() {
+				if (this.chat.type == "GROUP") {
+					var url = `/message/group/readed?groupId=${this.chat.targetId}`
+				} else {
+					url = `/message/private/readed?friendId=${this.chat.targetId}`
+				}
+				this.$http({
+					url: url,
+					method: 'PUT'
+				}).then(() => {
+					this.$store.commit("resetUnreadCount", this.chat)
+					this.scrollToBottom();
+				})
+			},
 			loadGroup(groupId) {
 				this.$http({
 					url: `/group/find/${groupId}`,
@@ -416,6 +444,9 @@
 					return 0;
 				}
 				return this.chat.messages.length;
+			},
+			unreadCount() {
+				return this.chat.unreadCount;
 			}
 		},
 		watch: {
@@ -424,15 +455,28 @@
 				if (newSize > oldSize) {
 					this.scrollToBottom();
 				}
+			},
+			unreadCount: {
+				handler(newCount, oldCount) {
+					if (newCount > 0) {
+						// 消息已读
+						this.readedMessage()
+					}
+				}
 			}
 		},
 		onLoad(options) {
 			// 聊天数据
 			this.chat = this.$store.state.chatStore.chats[options.chatIdx];
+			// 初始状态只显示30条消息
+			let size = this.chat.messages.length;
+			this.showMinIdx = size > 30 ? size - 30 : 0;
 			// 激活当前会话
 			this.$store.commit("activeChat", options.chatIdx);
 			// 页面滚到底部
 			this.scrollToBottom();
+			// 消息已读
+			this.readedMessage()
 			// 加载好友或群聊信息
 			if (this.chat.type == "GROUP") {
 				this.loadGroup(this.chat.targetId);
@@ -471,16 +515,17 @@
 				line-height: 60rpx;
 				font-size: 28rpx;
 				cursor: pointer;
-				
+
 				&.left {
 					left: 30rpx;
 				}
+
 				&.right {
 					right: 30rpx;
 				}
 			}
-			
-			
+
+
 		}
 
 

+ 23 - 3
im-uniapp/pages/chat/chat.vue

@@ -1,10 +1,16 @@
 <template>
 	<view class="tab-page">
-		<view class="chat-tip" v-if="$store.state.chatStore.chats.length==0">
+
+		<view v-if="loading" class="chat-loading" >
+			<loading  :size="50" :mask="false">
+				<view>消息接收中...</view>
+			</loading>
+		</view>
+		<view class="chat-tip" v-if="!loading && chatStore.chats.length==0">
 			温馨提示:您现在还没有任何聊天消息,快跟您的好友发起聊天吧~
 		</view>
 		<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">
-			<view v-for="(chat,index) in $store.state.chatStore.chats" :key="index">
+			<view v-for="(chat,index) in chatStore.chats" :key="index">
 				<chat-item :chat="chat" :index="index" @longpress.native="onShowMenu($event,index)"></chat-item>
 			</view>
 		</scroll-view>
@@ -97,12 +103,18 @@
 			}
 		},
 		computed: {
+			chatStore() {
+				return this.$store.state.chatStore;
+			},
 			unreadCount() {
 				let count = 0;
-				this.$store.state.chatStore.chats.forEach(chat => {
+				this.chatStore.chats.forEach(chat => {
 					count += chat.unreadCount;
 				})
 				return count;
+			},
+			loading() {
+				return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg
 			}
 		},
 		watch: {
@@ -126,4 +138,12 @@
 		color: darkblue;
 		font-size: 30rpx;
 	}
+
+	.chat-loading {
+		display: block;
+		height: 100rpx;
+		background: white;
+		position: relative;
+		color: blue;
+	}
 </style>

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

@@ -20,8 +20,8 @@
 			return {
 				loginForm: {
 					terminal: 1, // APP终端
-					userName: 'blue',
-					password: '123456'
+					userName: '',
+					password: ''
 				},
 				rules: {
 					userName: {
@@ -48,6 +48,8 @@
 				}).then(data => {
 					console.log("登录成功,自动跳转到聊天页面...")
 					uni.setStorageSync("loginInfo", data);
+					uni.setStorageSync("userName",this.loginForm.userName)
+					uni.setStorageSync("password",this.loginForm.password)
 					// 调用App.vue的初始化方法
 					getApp().init()
 					// 跳转到聊天页面   
@@ -58,13 +60,14 @@
 			}
 		},
 		onLoad() {
+			this.loginForm.userName = uni.getStorageSync("userName");
+			this.loginForm.password = uni.getStorageSync("password");
 			let loginInfo = uni.getStorageSync("loginInfo");
 			if (loginInfo) {
 				// 跳转到聊天页面
 				uni.switchTab({
 					url: "/pages/chat/chat"
 				})
-				
 			}
 		}
 	}

+ 96 - 45
im-uniapp/store/chatStore.js

@@ -1,23 +1,33 @@
-import {MESSAGE_TYPE} from '@/common/enums.js';
+import {
+	MESSAGE_TYPE,
+	MESSAGE_STATUS
+} from '@/common/enums.js';
 import userStore from './userStore';
 
 export default {
-	
+
 	state: {
-		chats: []
+		activeIndex: -1,
+		chats: [],
+		privateMsgMaxId: 0,
+		groupMsgMaxId: 0,
+		loadingPrivateMsg: false,
+		loadingGroupMsg: false,
 	},
 
 	mutations: {
-		initChats(state,chats){
+		initChats(state, chatsData) {
+			state.chats = chatsData.chats ||[];
+			state.privateMsgMaxId = chatsData.privateMsgMaxId||0;
+			state.groupMsgMaxId = chatsData.groupMsgMaxId||0;
 			// 防止图片一直处在加载中状态
-			chats.forEach((chat)=>{
-				chat.messages.forEach((msg)=>{
-					if(msg.loadStatus == "loading"){
+			state.chats.forEach((chat) => {
+				chat.messages.forEach((msg) => {
+					if (msg.loadStatus == "loading") {
 						msg.loadStatus = "fail"
 					}
 				})
 			})
-			state.chats = chats;
 		},
 		openChat(state, chatInfo) {
 			let chat = null;
@@ -49,10 +59,33 @@ export default {
 		},
 		activeChat(state, idx) {
 			state.activeIndex = idx;
-			if(idx>=0){
+			if (idx >= 0) {
 				state.chats[idx].unreadCount = 0;
 			}
 		},
+		resetUnreadCount(state, chatInfo) {
+			for (let idx in state.chats) {
+				if (state.chats[idx].type == chatInfo.type &&
+					state.chats[idx].targetId == chatInfo.targetId) {
+					state.chats[idx].unreadCount = 0;
+				}
+			}
+			this.commit("saveToStorage");
+		},
+		readedMessage(state, friendId) {
+			for (let idx in state.chats) {
+				if (state.chats[idx].type == 'PRIVATE' &&
+					state.chats[idx].targetId == friendId) {
+					state.chats[idx].messages.forEach((m) => {
+						console.log("readedMessage")
+						if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) {
+							m.status = MESSAGE_STATUS.READED
+						}
+					})
+				}
+			}
+			this.commit("saveToStorage");
+		},
 		removeChat(state, idx) {
 			state.chats.splice(idx, 1);
 			this.commit("saveToStorage");
@@ -73,7 +106,7 @@ export default {
 				}
 			}
 		},
-		moveTop(state,idx){
+		moveTop(state, idx) {
 			let chat = state.chats[idx];
 			// 放置头部
 			state.chats.splice(idx, 1);
@@ -94,41 +127,44 @@ export default {
 				}
 			}
 			// 插入新的数据
-			if(msgInfo.type == MESSAGE_TYPE.IMAGE){
-				chat.lastContent =  "[图片]";
-			}else if(msgInfo.type == MESSAGE_TYPE.FILE){
+			if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
+				chat.lastContent = "[图片]";
+			} else if (msgInfo.type == MESSAGE_TYPE.FILE) {
 				chat.lastContent = "[文件]";
-			}else if(msgInfo.type == MESSAGE_TYPE.AUDIO){
+			} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
 				chat.lastContent = "[语音]";
-			}else{
-				chat.lastContent =  msgInfo.content;
+			} else {
+				chat.lastContent = msgInfo.content;
 			}
 			chat.lastSendTime = msgInfo.sendTime;
-			// 如果不是当前会话,未读加1
-			if(chatIdx !=  state.activeIndex){
+			// 未读加1
+			if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
 				chat.unreadCount++;
 			}
-			// 自己回复了消息,说明消息已读
-			if(msgInfo.selfSend){
-				chat.unreadCount=0;
+			// 记录消息的最大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){
+				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){
+				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)){
+			if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
 				chat.messages.push({
 					sendTime: msgInfo.sendTime,
 					type: MESSAGE_TYPE.TIP_TIME,
@@ -138,9 +174,9 @@ export default {
 			// 新的消息
 			chat.messages.push(msgInfo);
 			this.commit("saveToStorage");
-			
+
 		},
-		deleteMessage(state, msgInfo){
+		deleteMessage(state, msgInfo) {
 			// 获取对方id或群id
 			let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
 			let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
@@ -152,16 +188,16 @@ export default {
 					break;
 				}
 			}
-			
+
 			for (let idx in chat.messages) {
 				// 已经发送成功的,根据id删除
-				if(chat.messages[idx].id && chat.messages[idx].id == msgInfo.id){
+				if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
 					chat.messages.splice(idx, 1);
 					break;
 				}
 				// 正在发送中的消息可能没有id,根据发送时间删除
-				if(msgInfo.selfSend && chat.messages[idx].selfSend 
-				&&chat.messages[idx].sendTime == msgInfo.sendTime){
+				if (msgInfo.selfSend && chat.messages[idx].selfSend &&
+					chat.messages[idx].sendTime == msgInfo.sendTime) {
 					chat.messages.splice(idx, 1);
 					break;
 				}
@@ -171,7 +207,7 @@ export default {
 		updateChatFromFriend(state, friend) {
 			for (let i in state.chats) {
 				let chat = state.chats[i];
-				if (chat.type=='PRIVATE' && chat.targetId == friend.id) {
+				if (chat.type == 'PRIVATE' && chat.targetId == friend.id) {
 					chat.headImage = friend.headImageThumb;
 					chat.showName = friend.nickName;
 					break;
@@ -182,7 +218,7 @@ export default {
 		updateChatFromGroup(state, group) {
 			for (let i in state.chats) {
 				let chat = state.chats[i];
-				if (chat.type=='GROUP' && chat.targetId == group.id) {
+				if (chat.type == 'GROUP' && chat.targetId == group.id) {
 					chat.headImage = group.headImageThumb;
 					chat.showName = group.remark;
 					break;
@@ -190,35 +226,50 @@ export default {
 			}
 			this.commit("saveToStorage");
 		},
-		saveToStorage(state){
+		loadingPrivateMsg(state, loadding) {
+			state.loadingPrivateMsg = loadding;
+		},
+		loadingGroupMsg(state, loadding) {
+			state.loadingGroupMsg = loadding;
+		},
+		saveToStorage(state) {
 			let userId = userStore.state.userInfo.id;
+			let key = "chats-" + userId;
+			let chatsData = {
+				privateMsgMaxId: state.privateMsgMaxId,
+				groupMsgMaxId: state.groupMsgMaxId,
+				chats: state.chats
+			}
 			uni.setStorage({
-				key:"chats-"+userId,
-				data: state.chats 
+				key: key,
+				data: chatsData
 			})
 		},
-		clear(state){
+		clear(state) {
 			state.chats = [];
+			state.activeIndex = -1;
+			state.privateMsgMaxId = 0;
+			state.groupMsgMaxId = 0;
+			state.loadingPrivateMsg = false;
+			state.loadingGroupMsg = false;
 		}
-	}, 
-	actions:{
+	},
+	actions: {
 		loadChat(context) {
 			return new Promise((resolve, reject) => {
 				let userId = userStore.state.userInfo.id;
 				uni.getStorage({
-					key:"chats-"+userId,
+					key: "chats-" + userId,
 					success(res) {
-						context.commit("initChats",res.data);
+						context.commit("initChats", res.data);
 						resolve()
 					},
 					fail(e) {
-						// 不存在聊天记录,清空聊天列表
-						context.commit("initChats",[]);
 						resolve()
 					}
 				});
 			})
 		}
-		
+
 	}
-}
+}