Просмотр исходного кода

消息撤回优化,支持离线撤回

xsx 11 месяцев назад
Родитель
Сommit
641babd2be

+ 2 - 3
im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java

@@ -30,9 +30,8 @@ public class GroupMessageController {
 
     @DeleteMapping("/recall/{id}")
     @Operation(summary = "撤回消息", description = "撤回群聊消息")
-    public Result<Long> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) {
-        groupMessageService.recallMessage(id);
-        return ResultUtils.success();
+    public Result<GroupMessageVO> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) {
+        return ResultUtils.success(groupMessageService.recallMessage(id));
     }
 
     @GetMapping("/pullOfflineMessage")

+ 2 - 3
im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java

@@ -30,9 +30,8 @@ public class PrivateMessageController {
 
     @DeleteMapping("/recall/{id}")
     @Operation(summary = "撤回消息", description = "撤回私聊消息")
-    public Result<Long> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) {
-        privateMessageService.recallMessage(id);
-        return ResultUtils.success();
+    public Result<PrivateMessageVO> recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) {
+        return ResultUtils.success( privateMessageService.recallMessage(id));
     }
 
     @GetMapping("/pullOfflineMessage")

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

@@ -22,7 +22,7 @@ public interface GroupMessageService extends IService<GroupMessage> {
      *
      * @param id 消息id
      */
-    void recallMessage(Long id);
+    GroupMessageVO recallMessage(Long id);
 
     /**
      * 拉取离线消息,只能拉取最近1个月的消息,最多拉取1000条

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

@@ -23,7 +23,7 @@ public interface PrivateMessageService extends IService<PrivateMessage> {
      *
      * @param id 消息id
      */
-    void recallMessage(Long id);
+    PrivateMessageVO recallMessage(Long id);
 
     /**
      * 拉取历史聊天记录

+ 15 - 19
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java

@@ -38,6 +38,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.time.DateUtils;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -92,8 +93,9 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
         return msgInfo;
     }
 
+    @Transactional
     @Override
-    public void recallMessage(Long id) {
+    public GroupMessageVO recallMessage(Long id) {
         UserSession session = SessionContext.getSession();
         GroupMessage msg = this.getById(id);
         if (Objects.isNull(msg)) {
@@ -113,31 +115,26 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
         // 修改数据库
         msg.setStatus(MessageStatus.RECALL.code());
         this.updateById(msg);
+        // 生成一条撤回消息
+        GroupMessage recallMsg = new GroupMessage();
+        recallMsg.setStatus(MessageStatus.UNSEND.code());
+        recallMsg.setType(MessageType.RECALL.code());
+        recallMsg.setGroupId(msg.getGroupId());
+        recallMsg.setSendId(session.getUserId());
+        recallMsg.setSendNickName(member.getShowNickName());
+        recallMsg.setContent(id.toString());
+        recallMsg.setSendTime(new Date());
+        this.save(recallMsg);
         // 群发
         List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId());
-        // 不用发给自己
-        userIds = userIds.stream().filter(uid -> !session.getUserId().equals(uid)).collect(Collectors.toList());
-        GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
-        msgInfo.setType(MessageType.RECALL.code());
-        String content = String.format("'%s'撤回了一条消息", member.getShowNickName());
-        msgInfo.setContent(content);
-        msgInfo.setSendTime(new Date());
-
+        GroupMessageVO msgInfo = BeanUtils.copyProperties(recallMsg, GroupMessageVO.class);
         IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
         sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
         sendMessage.setRecvIds(userIds);
         sendMessage.setData(msgInfo);
-        sendMessage.setSendResult(false);
-        sendMessage.setSendToSelf(false);
-        imClient.sendGroupMessage(sendMessage);
-
-        // 推给自己其他终端
-        msgInfo.setContent("你撤回了一条消息");
-        sendMessage.setSendToSelf(true);
-        sendMessage.setRecvIds(Collections.emptyList());
-        sendMessage.setRecvTerminals(Collections.emptyList());
         imClient.sendGroupMessage(sendMessage);
         log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), msg.getGroupId(), msg.getContent());
+        return msgInfo;
     }
 
 
@@ -165,7 +162,6 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
         wrapper.gt(GroupMessage::getId, minId)
             .gt(GroupMessage::getSendTime, minDate)
             .in(GroupMessage::getGroupId, groupIds)
-            .ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
             .orderByAsc(GroupMessage::getId);
         List<GroupMessage> messages = this.list(wrapper);
         // 通过群聊对消息进行分组

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

@@ -74,8 +74,9 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
         return msgInfo;
     }
 
+    @Transactional
     @Override
-    public void recallMessage(Long id) {
+    public PrivateMessageVO recallMessage(Long id) {
         UserSession session = SessionContext.getSession();
         PrivateMessage msg = this.getById(id);
         if (Objects.isNull(msg)) {
@@ -90,26 +91,24 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
         // 修改消息状态
         msg.setStatus(MessageStatus.RECALL.code());
         this.updateById(msg);
+        // 生成一条撤回消息
+        PrivateMessage recallMsg = new PrivateMessage();
+        recallMsg.setSendId(session.getUserId());
+        recallMsg.setStatus(MessageStatus.UNSEND.code());
+        recallMsg.setSendTime(new Date());
+        recallMsg.setRecvId(msg.getRecvId());
+        recallMsg.setType(MessageType.RECALL.code());
+        recallMsg.setContent(id.toString());
+        this.save(recallMsg);
         // 推送消息
-        PrivateMessageVO msgInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class);
-        msgInfo.setType(MessageType.RECALL.code());
-        msgInfo.setSendTime(new Date());
-        msgInfo.setContent("对方撤回了一条消息");
-
+        PrivateMessageVO msgInfo = BeanUtils.copyProperties(recallMsg, PrivateMessageVO.class);
         IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
         sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
         sendMessage.setRecvId(msgInfo.getRecvId());
-        sendMessage.setSendToSelf(false);
         sendMessage.setData(msgInfo);
-        sendMessage.setSendResult(false);
-        imClient.sendPrivateMessage(sendMessage);
-
-        // 推给自己其他终端
-        msgInfo.setContent("你撤回了一条消息");
-        sendMessage.setSendToSelf(true);
-        sendMessage.setRecvTerminals(Collections.emptyList());
         imClient.sendPrivateMessage(sendMessage);
         log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}", msg.getSendId(), msg.getRecvId(), msg.getContent());
+        return msgInfo;
     }
 
     @Override
@@ -154,8 +153,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
         // 只能拉取最近3个月的消息,移动端只拉取一个月消息
         int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3;
         Date minDate = DateUtils.addMonths(new Date(), -months);
-        queryWrapper.gt(PrivateMessage::getId, minId).ge(PrivateMessage::getSendTime, minDate)
-            .ne(PrivateMessage::getStatus, MessageStatus.RECALL.code()).and(wrap -> wrap.and(
+        queryWrapper.gt(PrivateMessage::getId, minId).ge(PrivateMessage::getSendTime, minDate).and(wrap -> wrap.and(
                     wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).in(PrivateMessage::getRecvId, friendIds))
                 .or(wp -> wp.eq(PrivateMessage::getRecvId, session.getUserId()).in(PrivateMessage::getSendId, friendIds)))
             .orderByAsc(PrivateMessage::getId);

+ 28 - 19
im-uniapp/App.vue

@@ -107,6 +107,15 @@ export default {
 			})
 		},
 		handlePrivateMessage(msg) {
+			// 标记这条消息是不是自己发的
+			msg.selfSend = msg.sendId == this.userStore.userInfo.id;
+			// 好友id
+			let friendId = msg.selfSend ? msg.recvId : msg.sendId;
+			// 会话信息
+			let chatInfo = {
+				type: 'PRIVATE',
+				targetId: friendId
+			}
 			// 消息加载标志
 			if (msg.type == enums.MESSAGE_TYPE.LOADING) {
 				this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content))
@@ -114,10 +123,7 @@ export default {
 			}
 			// 消息已读处理,清空已读数量
 			if (msg.type == enums.MESSAGE_TYPE.READED) {
-				this.chatStore.resetUnreadCount({
-					type: 'PRIVATE',
-					targetId: msg.recvId
-				})
+				this.chatStore.resetUnreadCount(chatInfo);
 				return;
 			}
 			// 消息回执处理,改消息状态为已读
@@ -127,10 +133,12 @@ export default {
 				})
 				return;
 			}
-			// 标记这条消息是不是自己发的
-			msg.selfSend = msg.sendId == this.userStore.userInfo.id;
-			// 好友id
-			let friendId = msg.selfSend ? msg.recvId : msg.sendId;
+			// 消息撤回
+			if (msg.type == enums.MESSAGE_TYPE.RECALL) {
+				this.chatStore.recallMessage(msg, chatInfo);
+				return;
+			}
+			// 消息插入
 			this.loadFriendInfo(friendId, (friend) => {
 				this.insertPrivateMessage(friend, msg);
 			})
@@ -177,6 +185,12 @@ export default {
 
 		},
 		handleGroupMessage(msg) {
+			// 标记这条消息是不是自己发的
+			msg.selfSend = msg.sendId == this.userStore.userInfo.id;
+			let chatInfo = {
+				type: 'GROUP',
+				targetId: msg.groupId
+			}
 			// 消息加载标志
 			if (msg.type == enums.MESSAGE_TYPE.LOADING) {
 				this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content))
@@ -185,19 +199,11 @@ export default {
 			// 消息已读处理
 			if (msg.type == enums.MESSAGE_TYPE.READED) {
 				// 我已读对方的消息,清空已读数量
-				let chatInfo = {
-					type: 'GROUP',
-					targetId: msg.groupId
-				}
 				this.chatStore.resetUnreadCount(chatInfo)
 				return;
 			}
 			// 消息回执处理
 			if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
-				let chatInfo = {
-					type: 'GROUP',
-					targetId: msg.groupId
-				}
 				// 更新消息已读人数
 				let msgInfo = {
 					id: msg.id,
@@ -205,11 +211,14 @@ export default {
 					readedCount: msg.readedCount,
 					receiptOk: msg.receiptOk
 				};
-				this.chatStore.updateMessage(msgInfo,chatInfo)
+				this.chatStore.updateMessage(msgInfo, chatInfo)
+				return;
+			}
+			// 消息撤回
+			if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) {
+				this.chatStore.recallMessage(msg, chatInfo)
 				return;
 			}
-			// 标记这条消息是不是自己发的
-			msg.selfSend = msg.sendId == this.userStore.userInfo.id;
 			this.loadGroupInfo(msg.groupId, (group) => {
 				// 插入群聊消息
 				this.insertGroupMessage(group, msg);

+ 3 - 4
im-uniapp/components/chat-message-item/chat-message-item.vue

@@ -1,13 +1,12 @@
 <template>
 	<view class="chat-msg-item">
-		<view class="chat-msg-tip"
-			v-if="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
+		<view class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
 			{{ msgInfo.content }}
 		</view>
-		<view class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
+		<view class="chat-msg-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
 			{{ $date.toTimeText(msgInfo.sendTime) }}
 		</view>
-		<view class="chat-msg-normal" v-if="isNormal" :class="{ 'chat-msg-mine': msgInfo.selfSend }">
+		<view class="chat-msg-normal" v-else-if="isNormal" :class="{ 'chat-msg-mine': msgInfo.selfSend }">
 			<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
 				:name="showName" size="small"></head-image>
 			<view class="chat-msg-content">

+ 7 - 9
im-uniapp/main.js

@@ -19,17 +19,15 @@ import arrowBar from '@/components/bar/arrow-bar'
 import btnBar from '@/components/bar/btn-bar'
 import switchBar from '@/components/bar/switch-bar'
 // #ifdef H5
-// import VConsole from 'vconsole'
-// new VConsole();
-// #endif
-// #ifdef H5  
-import ImageResize from "quill-image-resize-mp";  
-import Quill from "quill";  
+import * as recorder from './common/recorder-h5';
+import ImageResize from "quill-image-resize-mp";
+import Quill from "quill"; 
+// 以下组件用于兼容部分手机聊天边框无法输入的问题
 window.Quill = Quill;  
 window.ImageResize = { default: ImageResize };  
-// #endif  
-// #ifdef H5
-import * as recorder from './common/recorder-h5';
+// 调试器
+// import VConsole from 'vconsole'
+// new VConsole();
 // #endif
 // #ifndef H5
 import * as recorder from './common/recorder-app';

+ 3 - 6
im-uniapp/pages/chat/chat-box.vue

@@ -519,12 +519,9 @@ export default {
 						this.$http({
 							url: url,
 							method: 'DELETE'
-						}).then(() => {
-							msgInfo = JSON.parse(JSON.stringify(msgInfo));
-							msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL;
-							msgInfo.content = '你撤回了一条消息';
-							msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
-							this.chatStore.insertMessage(msgInfo, this.chat);
+						}).then((m) => {
+							m.selfSend = true;
+							this.chatStore.recallMessage(m, this.chat);
 						})
 					}
 				}

+ 31 - 7
im-uniapp/store/chatStore.js

@@ -156,10 +156,6 @@ export default defineStore('chatStore', {
 			let message = this.findMessage(chat, msgInfo);
 			if (message) {
 				Object.assign(message, msgInfo);
-				// 撤回消息需要显示
-				if (msgInfo.type == MESSAGE_TYPE.RECALL) {
-					chat.lastContent = msgInfo.content;
-				}
 				chat.stored = false;
 				this.saveToStorage();
 				return;
@@ -248,9 +244,8 @@ export default defineStore('chatStore', {
 					chat.messages.splice(idx, 1);
 					break;
 				}
-				// 正在发送中的消息可能没有id,根据发送时间删除
-				if (msgInfo.selfSend && chat.messages[idx].selfSend &&
-					chat.messages[idx].sendTime == msgInfo.sendTime) {
+				// 正在发送中的消息可能没有id,只有临时id
+				if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
 					chat.messages.splice(idx, 1);
 					break;
 				}
@@ -258,6 +253,35 @@ export default defineStore('chatStore', {
 			chat.stored = false;
 			this.saveToStorage();
 		},
+		recallMessage(msgInfo, chatInfo) {
+			let chat = this.findChat(chatInfo);
+			if (!chat) return;
+			// 要撤回的消息id
+			let id = msgInfo.content;
+			let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
+			for (let idx in chat.messages) {
+				let m = chat.messages[idx];
+				if (m.id && m.id == id) {
+					// 改造成一条提示消息
+					m.status = MESSAGE_STATUS.RECALL;
+					m.content = name + "撤回了一条消息";
+					m.type = MESSAGE_TYPE.TIP_TEXT
+					// 会话列表
+					chat.lastContent = m.content;
+					chat.lastSendTime = msgInfo.sendTime;
+					chat.sendNickName = '';
+					chat.unreadCount++;
+				}
+				// 被引用的消息也要撤回
+				if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
+					m.quoteMessage.content = "引用内容已撤回";
+					m.quoteMessage.status = MESSAGE_STATUS.RECALL;
+					m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT
+				}
+			}
+			chat.stored = false;
+			this.saveToStorage();
+		},
 		updateChatFromFriend(friend) {
 			let chat = this.findChatByFriend(friend.id)
 			if (chat && (chat.headImage != friend.headImageThumb ||

+ 3 - 7
im-web/src/components/chat/ChatBox.vue

@@ -490,13 +490,10 @@ export default {
 				this.$http({
 					url: url,
 					method: 'delete'
-				}).then(() => {
+				}).then((m) => {
 					this.$message.success("消息已撤回");
-					msgInfo = JSON.parse(JSON.stringify(msgInfo));
-					msgInfo.type = 10;
-					msgInfo.content = '你撤回了一条消息';
-					msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
-					this.$store.commit("insertMessage", [msgInfo, this.chat]);
+					m.selfSend = true;
+					this.$store.commit("recallMessage", [m, this.chat]);
 				})
 			});
 		},
@@ -572,7 +569,6 @@ export default {
 			}
 		},
 		resetEditor() {
-
 			this.$nextTick(() => {
 				this.$refs.chatInputEditor.clear();
 				this.$refs.chatInputEditor.focus();

+ 3 - 3
im-web/src/components/chat/ChatMessageItem.vue

@@ -1,13 +1,13 @@
 <template>
 	<div class="chat-msg-item">
 		<div class="chat-msg-tip"
-			v-if="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
+			v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
 			{{ msgInfo.content }}
 		</div>
-		<div class="chat-msg-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
+		<div class="chat-msg-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
 			{{ $date.toTimeText(msgInfo.sendTime) }}
 		</div>
-		<div class="chat-msg-normal" v-if="isNormal" :class="{ 'chat-msg-mine': mine }">
+		<div class="chat-msg-normal" v-else-if="isNormal" :class="{ 'chat-msg-mine': mine }">
 			<div class="head-image">
 				<head-image :name="showName" :size="38" :url="headImage" :id="msgInfo.sendId"></head-image>
 			</div>

+ 35 - 9
im-web/src/store/chatStore.js

@@ -151,10 +151,6 @@ export default {
 			let message = this.getters.findMessage(chat, msgInfo);
 			if (message) {
 				Object.assign(message, msgInfo);
-				// 撤回消息需要显示
-				if (msgInfo.type == MESSAGE_TYPE.RECALL) {
-					chat.lastContent = msgInfo.content;
-				}
 				chat.stored = false;
 				this.commit("saveToStorage");
 				return;
@@ -236,9 +232,8 @@ export default {
 					chat.messages.splice(idx, 1);
 					break;
 				}
-				// 正在发送中的消息可能没有id,根据发送时间删除
-				if (msgInfo.selfSend && chat.messages[idx].selfSend &&
-					chat.messages[idx].sendTime == msgInfo.sendTime) {
+				// 正在发送中的消息可能没有id,只有临时id
+				if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
 					chat.messages.splice(idx, 1);
 					break;
 				}
@@ -246,11 +241,42 @@ export default {
 			chat.stored = false;
 			this.commit("saveToStorage");
 		},
+		recallMessage(state, [msgInfo, chatInfo]) {
+			let chat = this.getters.findChat(chatInfo);
+			if (!chat) return;
+			// 要撤回的消息id
+			let id = msgInfo.content;
+			let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
+			for (let idx in chat.messages) {
+				let m = chat.messages[idx];
+				if (m.id && m.id == id) {
+					// 改造成一条提示消息
+					m.status = MESSAGE_STATUS.RECALL;
+					m.content = name + "撤回了一条消息";
+					m.type = MESSAGE_TYPE.TIP_TEXT
+					// 会话列表
+					chat.lastContent = m.content;
+					chat.lastSendTime = msgInfo.sendTime;
+					chat.sendNickName = '';
+					if(!msgInfo.selfSend){
+						chat.unreadCount++;
+					}
+				}
+				// 被引用的消息也要撤回
+				if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
+					m.quoteMessage.content = "引用内容已撤回";
+					m.quoteMessage.status = MESSAGE_STATUS.RECALL;
+					m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT
+				}
+			}
+			chat.stored = false;
+			this.commit("saveToStorage");
+		},
 		updateChatFromFriend(state, friend) {
 			let chat = this.getters.findChatByFriend(friend.id);
 			// 更新会话中的群名和头像
 			if (chat && (chat.headImage != friend.headImageThumb ||
-				chat.showName != friend.nickName)) {
+					chat.showName != friend.nickName)) {
 				chat.headImage = friend.headImageThumb;
 				chat.showName = friend.nickName;
 				chat.stored = false;
@@ -260,7 +286,7 @@ export default {
 		updateChatFromGroup(state, group) {
 			let chat = this.getters.findChatByGroup(group.id);
 			if (chat && (chat.headImage != group.headImageThumb ||
-				chat.showName != group.showGroupName)) {
+					chat.showName != group.showGroupName)) {
 				// 更新会话中的群名称和头像
 				chat.headImage = group.headImageThumb;
 				chat.showName = group.showGroupName;

+ 1 - 1
im-web/src/view/Chat.vue

@@ -11,7 +11,7 @@
       </div>
       <el-scrollbar class="chat-list-items" v-else>
         <div v-for="(chat, index) in chatStore.chats" :key="index">
-          <chat-item v-show="!chat.delete && chat.showName.includes(searchText)" :chat="chat" :index="index"
+          <chat-item v-show="!chat.delete && chat.showName && chat.showName.includes(searchText)" :chat="chat" :index="index"
             @click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
             :active="chat === chatStore.activeChat"></chat-item>
         </div>

+ 480 - 466
im-web/src/view/Home.vue

@@ -1,60 +1,61 @@
 <template>
-  <div class="home-page">
-    <div class="app-container" :class="{ fullscreen: isFullscreen }">
-      <div class="navi-bar">
-        <div class="navi-bar-box">
-          <div class="top">
-            <div class="user-head-image">
-              <head-image :name="$store.state.userStore.userInfo.nickName" :size="38"
-                :url="$store.state.userStore.userInfo.headImageThumb" @click.native="showSettingDialog = true">
-              </head-image>
-            </div>
+	<div class="home-page">
+		<div class="app-container" :class="{ fullscreen: isFullscreen }">
+			<div class="navi-bar">
+				<div class="navi-bar-box">
+					<div class="top">
+						<div class="user-head-image">
+							<head-image :name="$store.state.userStore.userInfo.nickName" :size="38"
+								:url="$store.state.userStore.userInfo.headImageThumb"
+								@click.native="showSettingDialog = true">
+							</head-image>
+						</div>
 
-            <div class="menu">
-              <router-link class="link" v-bind:to="'/home/chat'">
-                <div class="menu-item">
-                  <span class="icon iconfont icon-chat"></span>
-                  <div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
-                </div>
-              </router-link>
-              <router-link class="link" v-bind:to="'/home/friend'">
-                <div class="menu-item">
-                  <span class="icon iconfont icon-friend"></span>
-                </div>
-              </router-link>
-              <router-link class="link" v-bind:to="'/home/group'">
-                <div class="menu-item">
-                  <span class="icon iconfont icon-group" style="font-size: 28px"></span>
-                </div>
-              </router-link>
-            </div>
-          </div>
+						<div class="menu">
+							<router-link class="link" v-bind:to="'/home/chat'">
+								<div class="menu-item">
+									<span class="icon iconfont icon-chat"></span>
+									<div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
+								</div>
+							</router-link>
+							<router-link class="link" v-bind:to="'/home/friend'">
+								<div class="menu-item">
+									<span class="icon iconfont icon-friend"></span>
+								</div>
+							</router-link>
+							<router-link class="link" v-bind:to="'/home/group'">
+								<div class="menu-item">
+									<span class="icon iconfont icon-group" style="font-size: 28px"></span>
+								</div>
+							</router-link>
+						</div>
+					</div>
 
-          <div class="botoom">
-            <div class="botoom-item" @click="isFullscreen = !isFullscreen">
-              <i class="el-icon-full-screen"></i>
-            </div>
-            <div class="botoom-item" @click="showSetting">
-              <span class="icon iconfont icon-setting" style="font-size: 20px"></span>
-            </div>
-            <div class="botoom-item" @click="onExit()" title="退出">
-              <span class="icon iconfont icon-exit"></span>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="content-box">
-        <router-view></router-view>
-      </div>
-      <setting :visible="showSettingDialog" @close="closeSetting()"></setting>
-      <user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user"
-        @close="$store.commit('closeUserInfoBox')"></user-info>
-      <full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
-        @close="$store.commit('closeFullImageBox')"></full-image>
-      <rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
-      <rtc-group-video ref="rtcGroupVideo"></rtc-group-video>
-    </div>
-  </div>
+					<div class="botoom">
+						<div class="botoom-item" @click="isFullscreen = !isFullscreen">
+							<i class="el-icon-full-screen"></i>
+						</div>
+						<div class="botoom-item" @click="showSetting">
+							<span class="icon iconfont icon-setting" style="font-size: 20px"></span>
+						</div>
+						<div class="botoom-item" @click="onExit()" title="退出">
+							<span class="icon iconfont icon-exit"></span>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="content-box">
+				<router-view></router-view>
+			</div>
+			<setting :visible="showSettingDialog" @close="closeSetting()"></setting>
+			<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user"
+				@close="$store.commit('closeUserInfoBox')"></user-info>
+			<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
+				@close="$store.commit('closeFullImageBox')"></full-image>
+			<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
+			<rtc-group-video ref="rtcGroupVideo"></rtc-group-video>
+		</div>
+	</div>
 </template>
 
 <script>
@@ -67,443 +68,456 @@ import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue';
 import RtcGroupVideo from '../components/rtc/RtcGroupVideo.vue';
 
 export default {
-  components: {
-    HeadImage,
-    Setting,
-    UserInfo,
-    FullImage,
-    RtcPrivateVideo,
-    RtcPrivateAcceptor,
-    RtcGroupVideo
-  },
-  data() {
-    return {
-      showSettingDialog: false,
-      lastPlayAudioTime: new Date().getTime() - 1000,
-      isFullscreen: true
-    }
-  },
-  methods: {
-    init() {
-      this.$eventBus.$on('openPrivateVideo', (rctInfo) => {
-        // 进入单人视频通话
-        this.$refs.rtcPrivateVideo.open(rctInfo);
-      });
-      this.$eventBus.$on('openGroupVideo', (rctInfo) => {
-        // 进入多人视频通话
-        this.$refs.rtcGroupVideo.open(rctInfo);
-      });
+	components: {
+		HeadImage,
+		Setting,
+		UserInfo,
+		FullImage,
+		RtcPrivateVideo,
+		RtcPrivateAcceptor,
+		RtcGroupVideo
+	},
+	data() {
+		return {
+			showSettingDialog: false,
+			lastPlayAudioTime: new Date().getTime() - 1000,
+			isFullscreen: true
+		}
+	},
+	methods: {
+		init() {
+			this.$eventBus.$on('openPrivateVideo', (rctInfo) => {
+				// 进入单人视频通话
+				this.$refs.rtcPrivateVideo.open(rctInfo);
+			});
+			this.$eventBus.$on('openGroupVideo', (rctInfo) => {
+				// 进入多人视频通话
+				this.$refs.rtcGroupVideo.open(rctInfo);
+			});
 
-      this.$store.dispatch("load").then(() => {
-        // ws初始化
-        this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
-        this.$wsApi.onConnect(() => {
-          // 加载离线消息
-          this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
-          this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
-        });
-        this.$wsApi.onMessage((cmd, msgInfo) => {
-          if (cmd == 2) {
-            // 关闭ws
-            this.$wsApi.close(3000)
-            // 异地登录,强制下线
-            this.$alert("您已在其他地方登录,将被强制下线", "强制下线通知", {
-              confirmButtonText: '确定',
-              callback: action => {
-                location.href = "/";
-              }
-            });
+			this.$store.dispatch("load").then(() => {
+				// ws初始化
+				this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
+				this.$wsApi.onConnect(() => {
+					// 加载离线消息
+					this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
+					this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
+				});
+				this.$wsApi.onMessage((cmd, msgInfo) => {
+					if (cmd == 2) {
+						// 关闭ws
+						this.$wsApi.close(3000)
+						// 异地登录,强制下线
+						this.$alert("您已在其他地方登录,将被强制下线", "强制下线通知", {
+							confirmButtonText: '确定',
+							callback: action => {
+								location.href = "/";
+							}
+						});
 
-          } else if (cmd == 3) {
-            // 插入私聊消息
-            this.handlePrivateMessage(msgInfo);
-          } else if (cmd == 4) {
-            // 插入群聊消息
-            this.handleGroupMessage(msgInfo);
-          } else if (cmd == 5) {
-            // 处理系统消息
-            this.handleSystemMessage(msgInfo);
-          }
-        });
-        this.$wsApi.onClose((e) => {
-          console.log(e);
-          if (e.code != 3000) {
-            // 断线重连
-            this.$message.error("连接断开,正在尝试重新连接...");
-            this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
-              "accessToken"));
-          }
-        });
-      }).catch((e) => {
-        console.log("初始化失败", e);
-      })
-    },
-    pullPrivateOfflineMessage(minId) {
-      this.$store.commit("loadingPrivateMsg", true)
-      this.$http({
-        url: "/message/private/pullOfflineMessage?minId=" + minId,
-        method: 'GET'
-      }).catch(() => {
-        this.$store.commit("loadingPrivateMsg", false)
-      })
-    },
-    pullGroupOfflineMessage(minId) {
-      this.$store.commit("loadingGroupMsg", true)
-      this.$http({
-        url: "/message/group/pullOfflineMessage?minId=" + minId,
-        method: 'GET'
-      }).catch(() => {
-        this.$store.commit("loadingGroupMsg", false)
-      })
-    },
-    handlePrivateMessage(msg) {
-      // 消息加载标志
-      if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
-        this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content))
-        return;
-      }
-      // 消息已读处理,清空已读数量
-      if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
-        this.$store.commit("resetUnreadCount", {
-          type: 'PRIVATE',
-          targetId: msg.recvId
-        })
-        return;
-      }
-      // 消息回执处理,改消息状态为已读
-      if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
-        this.$store.commit("readedMessage", {
-          friendId: msg.sendId
-        })
-        return;
-      }
-      // 标记这条消息是不是自己发的
-      msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
-      // 单人webrtc 信令
-      if (this.$msgType.isRtcPrivate(msg.type)) {
-        this.$refs.rtcPrivateVideo.onRTCMessage(msg)
-        return;
-      }
-      // 好友id
-      let friendId = msg.selfSend ? msg.recvId : msg.sendId;
-      this.loadFriendInfo(friendId).then((friend) => {
-        this.insertPrivateMessage(friend, msg);
-      })
-    },
-    insertPrivateMessage(friend, msg) {
+					} else if (cmd == 3) {
+						// 插入私聊消息
+						this.handlePrivateMessage(msgInfo);
+					} else if (cmd == 4) {
+						// 插入群聊消息
+						this.handleGroupMessage(msgInfo);
+					} else if (cmd == 5) {
+						// 处理系统消息
+						this.handleSystemMessage(msgInfo);
+					}
+				});
+				this.$wsApi.onClose((e) => {
+					console.log(e);
+					if (e.code != 3000) {
+						// 断线重连
+						this.$message.error("连接断开,正在尝试重新连接...");
+						this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
+							"accessToken"));
+					}
+				});
+			}).catch((e) => {
+				console.log("初始化失败", e);
+			})
+		},
+		pullPrivateOfflineMessage(minId) {
+			this.$store.commit("loadingPrivateMsg", true)
+			this.$http({
+				url: "/message/private/pullOfflineMessage?minId=" + minId,
+				method: 'GET'
+			}).catch(() => {
+				this.$store.commit("loadingPrivateMsg", false)
+			})
+		},
+		pullGroupOfflineMessage(minId) {
+			this.$store.commit("loadingGroupMsg", true)
+			this.$http({
+				url: "/message/group/pullOfflineMessage?minId=" + minId,
+				method: 'GET'
+			}).catch(() => {
+				this.$store.commit("loadingGroupMsg", false)
+			})
+		},
+		handlePrivateMessage(msg) {
+			// 标记这条消息是不是自己发的
+			msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
+			// 好友id
+			let friendId = msg.selfSend ? msg.recvId : msg.sendId;
+			// 会话信息
+			let chatInfo = {
+				type: 'PRIVATE',
+				targetId: friendId
+			}
+			// 消息加载标志
+			if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
+				this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content))
+				return;
+			}
+			// 消息已读处理,清空已读数量
+			if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
+				this.$store.commit("resetUnreadCount", chatInfo)
+				return;
+			}
+			// 消息回执处理,改消息状态为已读
+			if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
+				this.$store.commit("readedMessage", {
+					friendId: msg.sendId
+				})
+				return;
+			}
+			// 消息撤回
+			if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) {
+				this.$store.commit("recallMessage", [msg, chatInfo])
+				return;
+			}
+			// 单人webrtc 信令
+			if (this.$msgType.isRtcPrivate(msg.type)) {
+				this.$refs.rtcPrivateVideo.onRTCMessage(msg)
+				return;
+			}
+			// 好友id
+			this.loadFriendInfo(friendId).then((friend) => {
+				this.insertPrivateMessage(friend, msg);
+			})
+		},
+		insertPrivateMessage(friend, msg) {
 
-      let chatInfo = {
-        type: 'PRIVATE',
-        targetId: friend.id,
-        showName: friend.nickName,
-        headImage: friend.headImage
-      };
-      // 打开会话
-      this.$store.commit("openChat", chatInfo);
-      // 插入消息
-      this.$store.commit("insertMessage", [msg, chatInfo]);
-      // 播放提示音
-      if (!msg.selfSend && this.$msgType.isNormal(msg.type) &&
-        msg.status != this.$enums.MESSAGE_STATUS.READED) {
-        this.playAudioTip();
-      }
-    },
-    handleGroupMessage(msg) {
-      // 消息加载标志
-      if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
-        this.$store.commit("loadingGroupMsg", JSON.parse(msg.content))
-        return;
-      }
-      // 消息已读处理
-      if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
-        // 我已读对方的消息,清空已读数量
-        let chatInfo = {
-          type: 'GROUP',
-          targetId: msg.groupId
-        }
-        this.$store.commit("resetUnreadCount", chatInfo)
-        return;
-      }
-      // 消息回执处理
-      if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
-        // 更新消息已读人数
-        let msgInfo = {
-          id: msg.id,
-          groupId: msg.groupId,
-          readedCount: msg.readedCount,
-          receiptOk: msg.receiptOk
-        };
-        this.$store.commit("updateMessage", msgInfo)
-        return;
-      }
-      // 标记这条消息是不是自己发的
-      msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
-      // 群视频信令
-      if (this.$msgType.isRtcGroup(msg.type)) {
-        this.$nextTick(() => {
-          this.$refs.rtcGroupVideo.onRTCMessage(msg);
-        })
-        return;
-      }
-      this.loadGroupInfo(msg.groupId).then((group) => {
-        // 插入群聊消息
-        this.insertGroupMessage(group, msg);
-      })
-    },
-    insertGroupMessage(group, msg) {
-      let chatInfo = {
-        type: 'GROUP',
-        targetId: group.id,
-        showName: group.showGroupName,
-        headImage: group.headImageThumb
-      };
-      // 打开会话
-      this.$store.commit("openChat", chatInfo);
-      // 插入消息
-      this.$store.commit("insertMessage", [msg, chatInfo]);
-      // 播放提示音
-      if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
-        msg.status != this.$enums.MESSAGE_STATUS.READED) {
-        this.playAudioTip();
-      }
-    },
-    handleSystemMessage(msg) {
-      // 用户被封禁
+			let chatInfo = {
+				type: 'PRIVATE',
+				targetId: friend.id,
+				showName: friend.nickName,
+				headImage: friend.headImage
+			};
+			// 打开会话
+			this.$store.commit("openChat", chatInfo);
+			// 插入消息
+			this.$store.commit("insertMessage", [msg, chatInfo]);
+			// 播放提示音
+			if (!msg.selfSend && this.$msgType.isNormal(msg.type) &&
+				msg.status != this.$enums.MESSAGE_STATUS.READED) {
+				this.playAudioTip();
+			}
+		},
+		handleGroupMessage(msg) {
+			// 标记这条消息是不是自己发的
+			msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
+			let chatInfo = {
+				type: 'GROUP',
+				targetId: msg.groupId
+			}
+			// 消息加载标志
+			if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
+				this.$store.commit("loadingGroupMsg", JSON.parse(msg.content))
+				return;
+			}
+			// 消息已读处理
+			if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
+				// 我已读对方的消息,清空已读数量
+				this.$store.commit("resetUnreadCount", chatInfo)
+				return;
+			}
+			// 消息回执处理
+			if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
+				// 更新消息已读人数
+				let msgInfo = {
+					id: msg.id,
+					groupId: msg.groupId,
+					readedCount: msg.readedCount,
+					receiptOk: msg.receiptOk
+				};
+				this.$store.commit("updateMessage", [msgInfo, chatInfo])
+				return;
+			}
+			// 消息撤回
+			if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) {
+				this.$store.commit("recallMessage", [msg, chatInfo])
+				return;
+			}
+			// 群视频信令
+			if (this.$msgType.isRtcGroup(msg.type)) {
+				this.$nextTick(() => {
+					this.$refs.rtcGroupVideo.onRTCMessage(msg);
+				})
+				return;
+			}
+			this.loadGroupInfo(msg.groupId).then((group) => {
+				// 插入群聊消息
+				this.insertGroupMessage(group, msg);
+			})
+		},
+		insertGroupMessage(group, msg) {
+			let chatInfo = {
+				type: 'GROUP',
+				targetId: group.id,
+				showName: group.showGroupName,
+				headImage: group.headImageThumb
+			};
+			// 打开会话
+			this.$store.commit("openChat", chatInfo);
+			// 插入消息
+			this.$store.commit("insertMessage", [msg, chatInfo]);
+			// 播放提示音
+			if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
+				msg.status != this.$enums.MESSAGE_STATUS.READED) {
+				this.playAudioTip();
+			}
+		},
+		handleSystemMessage(msg) {
+			// 用户被封禁
 
-      if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) {
-        this.$wsApi.close(3000);
-        this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", {
-          confirmButtonText: '确定',
-          callback: action => {
-            this.onExit();
-          }
-        });
-        return;
-      }
-    },
-    onExit() {
-      this.$wsApi.close(3000);
-      sessionStorage.removeItem("accessToken");
-      location.href = "/";
-    },
-    playAudioTip() {
-      // 离线消息不播放铃声
-      if (this.$store.getters.isLoading()) {
-        return;
-      }
-      // 防止过于密集播放
-      if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
-        this.lastPlayAudioTime = new Date().getTime();
-        let audio = new Audio();
-        let url = require(`@/assets/audio/tip.wav`);
-        audio.src = url;
-        audio.play();
-      }
+			if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) {
+				this.$wsApi.close(3000);
+				this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", {
+					confirmButtonText: '确定',
+					callback: action => {
+						this.onExit();
+					}
+				});
+				return;
+			}
+		},
+		onExit() {
+			this.$wsApi.close(3000);
+			sessionStorage.removeItem("accessToken");
+			location.href = "/";
+		},
+		playAudioTip() {
+			// 离线消息不播放铃声
+			if (this.$store.getters.isLoading()) {
+				return;
+			}
+			// 防止过于密集播放
+			if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
+				this.lastPlayAudioTime = new Date().getTime();
+				let audio = new Audio();
+				let url = require(`@/assets/audio/tip.wav`);
+				audio.src = url;
+				audio.play();
+			}
 
-    },
-    showSetting() {
-      this.showSettingDialog = true;
-    },
-    closeSetting() {
-      this.showSettingDialog = false;
-    },
-    loadFriendInfo(id) {
-      return new Promise((resolve, reject) => {
-        let friend = this.$store.state.friendStore.friends.find((f) => f.id == id);
-        if (friend) {
-          resolve(friend);
-        } else {
-          this.$http({
-            url: `/friend/find/${id}`,
-            method: 'get'
-          }).then((friend) => {
-            this.$store.commit("addFriend", friend);
-            resolve(friend)
-          })
-        }
-      });
-    },
-    loadGroupInfo(id) {
-      return new Promise((resolve, reject) => {
-        let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
-        if (group) {
-          resolve(group);
-        } else {
-          this.$http({
-            url: `/group/find/${id}`,
-            method: 'get'
-          }).then((group) => {
-            resolve(group)
-            this.$store.commit("addGroup", group);
-          })
-        }
-      });
-    }
-  },
-  computed: {
-    uiStore() {
-      return this.$store.state.uiStore;
-    },
-    unreadCount() {
-      let unreadCount = 0;
-      let chats = this.$store.state.chatStore.chats;
-      chats.forEach((chat) => {
-        if (!chat.delete) {
-          unreadCount += chat.unreadCount
-        }
-      });
-      return unreadCount;
-    }
-  },
-  watch: {
-    unreadCount: {
-      handler(newCount, oldCount) {
-        let tip = newCount > 0 ? `${newCount}条未读` : "";
-        this.$elm.setTitleTip(tip);
-      },
-      immediate: true
-    }
-  },
-  mounted() {
-    this.init();
-  },
-  unmounted() {
-    this.$wsApi.close();
-  }
+		},
+		showSetting() {
+			this.showSettingDialog = true;
+		},
+		closeSetting() {
+			this.showSettingDialog = false;
+		},
+		loadFriendInfo(id) {
+			return new Promise((resolve, reject) => {
+				let friend = this.$store.state.friendStore.friends.find((f) => f.id == id);
+				if (friend) {
+					resolve(friend);
+				} else {
+					this.$http({
+						url: `/friend/find/${id}`,
+						method: 'get'
+					}).then((friend) => {
+						this.$store.commit("addFriend", friend);
+						resolve(friend)
+					})
+				}
+			});
+		},
+		loadGroupInfo(id) {
+			return new Promise((resolve, reject) => {
+				let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
+				if (group) {
+					resolve(group);
+				} else {
+					this.$http({
+						url: `/group/find/${id}`,
+						method: 'get'
+					}).then((group) => {
+						resolve(group)
+						this.$store.commit("addGroup", group);
+					})
+				}
+			});
+		}
+	},
+	computed: {
+		uiStore() {
+			return this.$store.state.uiStore;
+		},
+		unreadCount() {
+			let unreadCount = 0;
+			let chats = this.$store.state.chatStore.chats;
+			chats.forEach((chat) => {
+				if (!chat.delete) {
+					unreadCount += chat.unreadCount
+				}
+			});
+			return unreadCount;
+		}
+	},
+	watch: {
+		unreadCount: {
+			handler(newCount, oldCount) {
+				let tip = newCount > 0 ? `${newCount}条未读` : "";
+				this.$elm.setTitleTip(tip);
+			},
+			immediate: true
+		}
+	},
+	mounted() {
+		this.init();
+	},
+	unmounted() {
+		this.$wsApi.close();
+	}
 }
 </script>
 
 <style scoped lang="scss">
 .home-page {
-  height: 100vh;
-  width: 100vw;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 4px;
-  overflow: hidden;
-  background: var(--im-color-primary-light-9);
-  //background-image: url('../assets/image/background.jpg');
+	height: 100vh;
+	width: 100vw;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	border-radius: 4px;
+	overflow: hidden;
+	background: var(--im-color-primary-light-9);
+	//background-image: url('../assets/image/background.jpg');
 
-  .app-container {
-    width: 62vw;
-    height: 80vh;
-    display: flex;
-    min-height: 600px;
-    min-width: 970px;
-    position: absolute;
-    border-radius: 4px;
-    overflow: hidden;
-    box-shadow: var(--im-box-shadow-dark);
-    transition: 0.2s;
+	.app-container {
+		width: 62vw;
+		height: 80vh;
+		display: flex;
+		min-height: 600px;
+		min-width: 970px;
+		position: absolute;
+		border-radius: 4px;
+		overflow: hidden;
+		box-shadow: var(--im-box-shadow-dark);
+		transition: 0.2s;
 
-    &.fullscreen {
-      transition: 0.2s;
-      width: 100vw;
-      height: 100vh;
-    }
-  }
+		&.fullscreen {
+			transition: 0.2s;
+			width: 100vw;
+			height: 100vh;
+		}
+	}
 
-  .navi-bar {
-    --icon-font-size: 22px;
-    --width: 60px;
-    width: var(--width);
-    background: var(--im-color-primary-light-1);
-    padding-top: 20px;
+	.navi-bar {
+		--icon-font-size: 22px;
+		--width: 60px;
+		width: var(--width);
+		background: var(--im-color-primary-light-1);
+		padding-top: 20px;
 
-    .navi-bar-box {
-      height: 100%;
-      display: flex;
-      flex-direction: column;
-      justify-content: space-between;
+		.navi-bar-box {
+			height: 100%;
+			display: flex;
+			flex-direction: column;
+			justify-content: space-between;
 
-      .botoom {
-        margin-bottom: 30px;
-      }
-    }
+			.botoom {
+				margin-bottom: 30px;
+			}
+		}
 
-    .user-head-image {
-      display: flex;
-      justify-content: center;
-    }
+		.user-head-image {
+			display: flex;
+			justify-content: center;
+		}
 
-    .menu {
-      height: 200px;
-      //margin-top: 10px;
-      display: flex;
-      flex-direction: column;
-      justify-content: center;
-      align-content: center;
+		.menu {
+			height: 200px;
+			//margin-top: 10px;
+			display: flex;
+			flex-direction: column;
+			justify-content: center;
+			align-content: center;
 
-      .link {
-        text-decoration: none;
-      }
+			.link {
+				text-decoration: none;
+			}
 
-      .router-link-active .menu-item {
-        color: #fff;
-        background: var(--im-color-primary-light-2);
-      }
+			.router-link-active .menu-item {
+				color: #fff;
+				background: var(--im-color-primary-light-2);
+			}
 
-      .link:not(.router-link-active) .menu-item:hover {
-        color: var(--im-color-primary-light-7);
-      }
+			.link:not(.router-link-active) .menu-item:hover {
+				color: var(--im-color-primary-light-7);
+			}
 
-      .menu-item {
-        position: relative;
-        color: var(--im-color-primary-light-4);
-        width: var(--width);
-        height: 46px;
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        margin-bottom: 12px;
+			.menu-item {
+				position: relative;
+				color: var(--im-color-primary-light-4);
+				width: var(--width);
+				height: 46px;
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				margin-bottom: 12px;
 
-        .icon {
-          font-size: var(--icon-font-size)
-        }
+				.icon {
+					font-size: var(--icon-font-size)
+				}
 
-        .unread-text {
-          position: absolute;
-          background-color: var(--im-color-danger);
-          left: 28px;
-          top: 8px;
-          color: white;
-          border-radius: 30px;
-          padding: 0 5px;
-          font-size: var(--im-font-size-smaller);
-          text-align: center;
-          white-space: nowrap;
-          border: 1px solid #f1e5e5;
-        }
-      }
-    }
+				.unread-text {
+					position: absolute;
+					background-color: var(--im-color-danger);
+					left: 28px;
+					top: 8px;
+					color: white;
+					border-radius: 30px;
+					padding: 0 5px;
+					font-size: var(--im-font-size-smaller);
+					text-align: center;
+					white-space: nowrap;
+					border: 1px solid #f1e5e5;
+				}
+			}
+		}
 
-    .botoom-item {
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      height: 50px;
-      width: 100%;
-      cursor: pointer;
-      color: var(--im-color-primary-light-4);
-      font-size: var(--icon-font-size);
+		.botoom-item {
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			height: 50px;
+			width: 100%;
+			cursor: pointer;
+			color: var(--im-color-primary-light-4);
+			font-size: var(--icon-font-size);
 
-      .icon {
-        font-size: var(--icon-font-size)
-      }
+			.icon {
+				font-size: var(--icon-font-size)
+			}
 
-      &:hover {
-        font-weight: 600;
-        color: var(--im-color-primary-light-7);
-      }
-    }
-  }
+			&:hover {
+				font-weight: 600;
+				color: var(--im-color-primary-light-7);
+			}
+		}
+	}
 
-  .content-box {
-    flex: 1;
-    padding: 0;
-    background-color: #fff;
-    text-align: center;
-  }
+	.content-box {
+		flex: 1;
+		padding: 0;
+		background-color: #fff;
+		text-align: center;
+	}
 }
-</style>
+</style>