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. 12
      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. 944
      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}")
@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")

5
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")

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
*/
void recallMessage(Long id);
GroupMessageVO recallMessage(Long id);
/**
* 拉取离线消息只能拉取最近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
*/
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.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);
// 通过群聊对消息进行分组

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;
}
@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);

47
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);

7
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">

12
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 * 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';

9
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);
})
}
}

38
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 ||

10
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();

6
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>

44
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;

2
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>

944
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>
Loading…
Cancel
Save