Browse Source

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

master
xsx 1 year ago
parent
commit
641babd2be
  1. 5
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  2. 5
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  3. 2
      im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java
  4. 2
      im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java
  5. 34
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  6. 30
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  7. 47
      im-uniapp/App.vue
  8. 7
      im-uniapp/components/chat-message-item/chat-message-item.vue
  9. 16
      im-uniapp/main.js
  10. 9
      im-uniapp/pages/chat/chat-box.vue
  11. 38
      im-uniapp/store/chatStore.js
  12. 10
      im-web/src/components/chat/ChatBox.vue
  13. 6
      im-web/src/components/chat/ChatMessageItem.vue
  14. 44
      im-web/src/store/chatStore.js
  15. 2
      im-web/src/view/Chat.vue
  16. 946
      im-web/src/view/Home.vue

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

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

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

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

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

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

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

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

34
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.apache.commons.lang3.time.DateUtils;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -92,8 +93,9 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return msgInfo; return msgInfo;
} }
@Transactional
@Override @Override
public void recallMessage(Long id) { public GroupMessageVO recallMessage(Long id) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
GroupMessage msg = this.getById(id); GroupMessage msg = this.getById(id);
if (Objects.isNull(msg)) { if (Objects.isNull(msg)) {
@ -113,31 +115,26 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 修改数据库 // 修改数据库
msg.setStatus(MessageStatus.RECALL.code()); msg.setStatus(MessageStatus.RECALL.code());
this.updateById(msg); 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()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId());
// 不用发给自己 GroupMessageVO msgInfo = BeanUtils.copyProperties(recallMsg, GroupMessageVO.class);
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());
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(userIds); sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo); 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); imClient.sendGroupMessage(sendMessage);
log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), msg.getGroupId(), msg.getContent()); 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) wrapper.gt(GroupMessage::getId, minId)
.gt(GroupMessage::getSendTime, minDate) .gt(GroupMessage::getSendTime, minDate)
.in(GroupMessage::getGroupId, groupIds) .in(GroupMessage::getGroupId, groupIds)
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
.orderByAsc(GroupMessage::getId); .orderByAsc(GroupMessage::getId);
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
// 通过群聊对消息进行分组 // 通过群聊对消息进行分组

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

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

47
im-uniapp/App.vue

@ -107,6 +107,15 @@ export default {
}) })
}, },
handlePrivateMessage(msg) { 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) { if (msg.type == enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content)) this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content))
@ -114,10 +123,7 @@ export default {
} }
// //
if (msg.type == enums.MESSAGE_TYPE.READED) { if (msg.type == enums.MESSAGE_TYPE.READED) {
this.chatStore.resetUnreadCount({ this.chatStore.resetUnreadCount(chatInfo);
type: 'PRIVATE',
targetId: msg.recvId
})
return; return;
} }
// , // ,
@ -127,10 +133,12 @@ export default {
}) })
return; return;
} }
// //
msg.selfSend = msg.sendId == this.userStore.userInfo.id; if (msg.type == enums.MESSAGE_TYPE.RECALL) {
// id this.chatStore.recallMessage(msg, chatInfo);
let friendId = msg.selfSend ? msg.recvId : msg.sendId; return;
}
//
this.loadFriendInfo(friendId, (friend) => { this.loadFriendInfo(friendId, (friend) => {
this.insertPrivateMessage(friend, msg); this.insertPrivateMessage(friend, msg);
}) })
@ -177,6 +185,12 @@ export default {
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
//
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
}
// //
if (msg.type == enums.MESSAGE_TYPE.LOADING) { if (msg.type == enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content)) this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content))
@ -185,19 +199,11 @@ export default {
// //
if (msg.type == enums.MESSAGE_TYPE.READED) { if (msg.type == enums.MESSAGE_TYPE.READED) {
// //
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
}
this.chatStore.resetUnreadCount(chatInfo) this.chatStore.resetUnreadCount(chatInfo)
return; return;
} }
// //
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) { if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
}
// //
let msgInfo = { let msgInfo = {
id: msg.id, id: msg.id,
@ -205,11 +211,14 @@ export default {
readedCount: msg.readedCount, readedCount: msg.readedCount,
receiptOk: msg.receiptOk 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; return;
} }
//
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
this.loadGroupInfo(msg.groupId, (group) => { this.loadGroupInfo(msg.groupId, (group) => {
// //
this.insertGroupMessage(group, msg); this.insertGroupMessage(group, msg);

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

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

16
im-uniapp/main.js

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

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

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

38
im-uniapp/store/chatStore.js

@ -156,10 +156,6 @@ export default defineStore('chatStore', {
let message = this.findMessage(chat, msgInfo); let message = this.findMessage(chat, msgInfo);
if (message) { if (message) {
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
return; return;
@ -248,9 +244,8 @@ export default defineStore('chatStore', {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
// 正在发送中的消息可能没有id,根据发送时间删除 // 正在发送中的消息可能没有id,只有临时id
if (msgInfo.selfSend && chat.messages[idx].selfSend && if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages[idx].sendTime == msgInfo.sendTime) {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
@ -258,6 +253,35 @@ export default defineStore('chatStore', {
chat.stored = false; chat.stored = false;
this.saveToStorage(); 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) { updateChatFromFriend(friend) {
let chat = this.findChatByFriend(friend.id) let chat = this.findChatByFriend(friend.id)
if (chat && (chat.headImage != friend.headImageThumb || if (chat && (chat.headImage != friend.headImageThumb ||

10
im-web/src/components/chat/ChatBox.vue

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

6
im-web/src/components/chat/ChatMessageItem.vue

@ -1,13 +1,13 @@
<template> <template>
<div class="chat-msg-item"> <div class="chat-msg-item">
<div class="chat-msg-tip" <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 }} {{ msgInfo.content }}
</div> </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) }} {{ $date.toTimeText(msgInfo.sendTime) }}
</div> </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"> <div class="head-image">
<head-image :name="showName" :size="38" :url="headImage" :id="msgInfo.sendId"></head-image> <head-image :name="showName" :size="38" :url="headImage" :id="msgInfo.sendId"></head-image>
</div> </div>

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

@ -151,10 +151,6 @@ export default {
let message = this.getters.findMessage(chat, msgInfo); let message = this.getters.findMessage(chat, msgInfo);
if (message) { if (message) {
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.commit("saveToStorage");
return; return;
@ -236,9 +232,8 @@ export default {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
// 正在发送中的消息可能没有id,根据发送时间删除 // 正在发送中的消息可能没有id,只有临时id
if (msgInfo.selfSend && chat.messages[idx].selfSend && if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages[idx].sendTime == msgInfo.sendTime) {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
@ -246,11 +241,42 @@ export default {
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); 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) { updateChatFromFriend(state, friend) {
let chat = this.getters.findChatByFriend(friend.id); let chat = this.getters.findChatByFriend(friend.id);
// 更新会话中的群名和头像 // 更新会话中的群名和头像
if (chat && (chat.headImage != friend.headImageThumb || if (chat && (chat.headImage != friend.headImageThumb ||
chat.showName != friend.nickName)) { chat.showName != friend.nickName)) {
chat.headImage = friend.headImageThumb; chat.headImage = friend.headImageThumb;
chat.showName = friend.nickName; chat.showName = friend.nickName;
chat.stored = false; chat.stored = false;
@ -260,7 +286,7 @@ export default {
updateChatFromGroup(state, group) { updateChatFromGroup(state, group) {
let chat = this.getters.findChatByGroup(group.id); let chat = this.getters.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb || if (chat && (chat.headImage != group.headImageThumb ||
chat.showName != group.showGroupName)) { chat.showName != group.showGroupName)) {
// 更新会话中的群名称和头像 // 更新会话中的群名称和头像
chat.headImage = group.headImageThumb; chat.headImage = group.headImageThumb;
chat.showName = group.showGroupName; chat.showName = group.showGroupName;

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

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

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

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