Browse Source

已读未读显示(完成)

master
xie.bx 2 years ago
parent
commit
c000c2e7ab
  1. 10
      im-client/src/main/java/com/bx/imclient/sender/IMSender.java
  2. 4
      im-commom/src/main/java/com/bx/imcommon/contant/IMRedisKey.java
  3. 1
      im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java
  4. 2
      im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java
  5. 19
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  6. 2
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  7. 2
      im-server/src/main/java/com/bx/imserver/task/PullUnreadGroupMessageTask.java
  8. 2
      im-server/src/main/java/com/bx/imserver/task/PullUnreadPrivateMessageTask.java
  9. 41
      im-ui/src/components/chat/ChatBox.vue
  10. 2
      im-ui/src/components/chat/ChatMessageItem.vue
  11. 38
      im-ui/src/store/chatStore.js
  12. 17
      im-ui/src/view/Home.vue
  13. 148
      im-uniapp/App.vue
  14. 21
      im-uniapp/common/emotion.js
  15. 11
      im-uniapp/common/enums.js
  16. 3
      im-uniapp/common/request.js
  17. 39
      im-uniapp/components/chat-message-item/chat-message-item.vue
  18. 35
      im-uniapp/components/loading/loading.vue
  19. 63
      im-uniapp/pages/chat/chat-box.vue
  20. 26
      im-uniapp/pages/chat/chat.vue
  21. 9
      im-uniapp/pages/login/login.vue
  22. 87
      im-uniapp/store/chatStore.js

10
im-client/src/main/java/com/bx/imclient/sender/IMSender.java

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -8,11 +8,11 @@
<el-main style="padding: 0;"> <el-main style="padding: 0;">
<el-container> <el-container>
<el-container class="content-box"> <el-container class="content-box">
<el-main class="im-chat-main" id="chatScrollBox"> <el-main class="im-chat-main" id="chatScrollBox" @scroll="handleScroll">
<div class="im-chat-box"> <div class="im-chat-box">
<ul> <ul>
<li v-for="(msgInfo,idx) in chat.messages" :key="idx"> <li v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" <chat-message-item v-show="idx>=showMinIdx" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" :msgInfo="msgInfo" @delete="deleteMessage" :showName="showName(msgInfo)" :msgInfo="msgInfo" @delete="deleteMessage"
@recall="recallMessage"> @recall="recallMessage">
</chat-message-item> </chat-message-item>
@ -115,7 +115,8 @@
y: 0 y: 0
}, },
showHistory: false, // showHistory: false, //
lockMessage: false // lockMessage: false, //
showMinIdx: 0 // showMinIdx
} }
}, },
methods: { methods: {
@ -236,6 +237,22 @@
handleCloseSide() { handleCloseSide() {
this.showSide = false; this.showSide = false;
}, },
handleScrollToTop() {
// 10
this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
},
handleScroll(e) {
let scrollElement = e.target
let scrollTop = scrollElement.scrollTop
if (scrollTop <30 ) { // 在顶部,不滚动的情况
console.log("next")
// 20
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
}
},
switchEmotionBox() { switchEmotionBox() {
this.showEmotion = !this.showEmotion; this.showEmotion = !this.showEmotion;
let width = this.$refs.emotion.offsetWidth; let width = this.$refs.emotion.offsetWidth;
@ -329,7 +346,6 @@
}, },
sendTextMessage() { sendTextMessage() {
if (!this.sendText.trim()) { if (!this.sendText.trim()) {
this.$message.error("不能发送空白信息");
return return
} }
let msgInfo = { let msgInfo = {
@ -457,7 +473,7 @@
}, },
scrollToBottom() { scrollToBottom() {
this.$nextTick(() => { this.$nextTick(() => {
const div = document.getElementById("chatScrollBox"); let div = document.getElementById("chatScrollBox");
div.scrollTop = div.scrollHeight; div.scrollTop = div.scrollHeight;
}); });
} }
@ -490,8 +506,9 @@
watch: { watch: {
chat: { chat: {
handler(newChat, oldChat) { handler(newChat, oldChat) {
if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type || newChat.targetId != oldChat if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
.targetId)) { newChat.targetId != oldChat.targetId)) {
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
this.loadGroup(this.chat.targetId); this.loadGroup(this.chat.targetId);
} else { } else {
@ -499,6 +516,9 @@
} }
this.scrollToBottom(); this.scrollToBottom();
this.sendText = ""; this.sendText = "";
// 30
let size = this.chat.messages.length;
this.showMinIdx = size > 30 ? size - 30 : 0;
// //
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.sendBox.focus(); this.$refs.sendBox.focus();
@ -515,6 +535,10 @@
} }
} }
} }
},
mounted() {
let div = document.getElementById("chatScrollBox");
div.addEventListener('scroll', this.handleScroll)
} }
} }
</script> </script>
@ -606,8 +630,7 @@
color: black; color: black;
background-color: #f8f8f8 !important; background-color: #f8f8f8 !important;
outline-color: rgba(83, 160, 231, 0.61); outline-color: rgba(83, 160, 231, 0.61);
text-align: left;
border: 0;
} }
.send-image-area { .send-image-area {

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

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

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

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

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

@ -74,13 +74,13 @@
data() { data() {
return { return {
showSettingDialog: false, showSettingDialog: false,
lastPlayAudioTime: new Date()-1000
} }
}, },
methods: { methods: {
init() { init() {
this.$store.dispatch("load").then(() => { this.$store.dispatch("load").then(() => {
// // 线
this.loadPrivateMessage(this.$store.state.chatStore.privateMsgMaxId); this.loadPrivateMessage(this.$store.state.chatStore.privateMsgMaxId);
this.loadGroupMessage(this.$store.state.chatStore.groupMsgMaxId); this.loadGroupMessage(this.$store.state.chatStore.groupMsgMaxId);
// ws // ws
@ -202,8 +202,9 @@
// //
this.$store.commit("insertMessage", msg); this.$store.commit("insertMessage", msg);
// //
!msg.selfSend && this.playAudioTip(); if(!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED){
this.playAudioTip();
}
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
// //
@ -236,7 +237,9 @@
// //
this.$store.commit("insertMessage", msg); this.$store.commit("insertMessage", msg);
// //
!msg.selfSend && this.playAudioTip(); if(!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED){
this.playAudioTip();
}
}, },
handleExit() { handleExit() {
this.$wsApi.close(); this.$wsApi.close();
@ -244,10 +247,14 @@
location.href = "/"; location.href = "/";
}, },
playAudioTip() { playAudioTip() {
if(new Date() - this.lastPlayAudioTime > 1000){
this.lastPlayAudioTime = new Date();
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;

148
im-uniapp/App.vue

@ -18,6 +18,9 @@
this.initAudit(); this.initAudit();
// websocket // websocket
this.initWebSocket(); this.initWebSocket();
// 线
this.loadPrivateMessage(store.state.chatStore.privateMsgMaxId);
this.loadGroupMessage(store.state.chatStore.groupMsgMaxId);
}).catch((e) => { }).catch((e) => {
console.log(e); console.log(e);
this.exit(); this.exit();
@ -28,10 +31,7 @@
let userId = store.state.userStore.userInfo.id; let userId = store.state.userStore.userInfo.id;
wsApi.init(process.env.WS_URL, loginInfo.accessToken); wsApi.init(process.env.WS_URL, loginInfo.accessToken);
wsApi.connect(); wsApi.connect();
wsApi.onOpen(()=>{ wsApi.onOpen()
//
this.pullUnreadMessage();
})
wsApi.onMessage((cmd, msgInfo) => { wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) { if (cmd == 2) {
// 线 // 线
@ -41,14 +41,10 @@
}) })
this.exit(); this.exit();
} else if (cmd == 3) { } else if (cmd == 3) {
// //
msgInfo.selfSend = userId == msgInfo.sendId;
//
this.handlePrivateMessage(msgInfo); this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) { } else if (cmd == 4) {
// //
msgInfo.selfSend = userId == msgInfo.sendId;
//
this.handleGroupMessage(msgInfo); this.handleGroupMessage(msgInfo);
} }
}); });
@ -66,33 +62,64 @@
} }
}) })
}, },
pullUnreadMessage() { loadPrivateMessage(minId) {
// store.commit("loadingPrivateMsg", true)
http({ http({
url: "/message/private/pullUnreadMessage", url: "/message/private/loadMessage?minId=" + minId,
method: 'POST' method: 'get'
}); }).then((msgInfos) => {
// msgInfos.forEach((msgInfo) => {
this.handlePrivateMessage(msgInfo);
})
if (msgInfos.length == 100) {
//
this.loadPrivateMessage(msgInfos[99].id);
} else {
store.commit("loadingPrivateMsg", false)
}
})
},
loadGroupMessage(minId) {
store.commit("loadingGroupMsg", true)
http({ http({
url: "/message/group/pullUnreadMessage", url: "/message/group/loadMessage?minId=" + minId,
method: 'POST' method: 'get'
}); }).then((msgInfos) => {
msgInfos.forEach((msgInfo) => {
this.handleGroupMessage(msgInfo);
})
if (msgInfos.length == 100) {
//
this.loadGroupMessage(msgInfos[99].id);
} else {
store.commit("loadingGroupMsg", false)
}
})
}, },
handlePrivateMessage(msg) { handlePrivateMessage(msg) {
//
msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId; let friendId = msg.selfSend ? msg.recvId : msg.sendId;
let friend = store.state.friendStore.friends.find((f) => f.id == friendId); //
if (!friend) { if (msg.type == enums.MESSAGE_TYPE.READED) {
http({ if (msg.selfSend) {
url: `/friend/find/${msg.sendId}`, //
method: 'GET' let chatInfo = {
}).then((friend) => { type: 'PRIVATE',
this.insertPrivateMessage(friend, msg); targetId: friendId
store.commit("addFriend", friend); }
}) store.commit("resetUnreadCount", chatInfo)
} else { } else {
// //
this.insertPrivateMessage(friend, msg); store.commit("readedMessage", friendId)
}
return;
} }
this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg);
})
}, },
insertPrivateMessage(friend, msg) { insertPrivateMessage(friend, msg) {
@ -115,19 +142,23 @@
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
let group = store.state.groupStore.groups.find((g) => g.id == msg.groupId); //
if (!group) { msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
http({ let groupId = msg.groupId;
url: `/group/find/${msg.groupId}`, //
method: 'get' if (msg.type == enums.MESSAGE_TYPE.READED) {
}).then((group) => { //
let chatInfo = {
type: 'GROUP',
targetId: groupId
}
store.commit("resetUnreadCount", chatInfo)
return;
}
this.loadGroupInfo(groupId).then((group) => {
//
this.insertGroupMessage(group, msg); this.insertGroupMessage(group, msg);
store.commit("addGroup", group);
}) })
} else {
//
this.insertGroupMessage(group, msg);
}
}, },
insertGroupMessage(group, msg) { insertGroupMessage(group, msg) {
@ -144,11 +175,43 @@
// //
!msg.selfSend && this.playAudioTip(); !msg.selfSend && this.playAudioTip();
}, },
loadFriendInfo(id) {
return new Promise((resolve, reject) => {
let friend = store.state.friendStore.friends.find((f) => f.id == id);
if (friend) {
resolve(friend);
} else {
http({
url: `/friend/find/${id}`,
method: 'get'
}).then((friend) => {
store.commit("addFriend", friend);
resolve(friend)
})
}
});
},
loadGroupInfo(id) {
return new Promise((resolve, reject) => {
let group = store.state.groupStore.groups.find((g) => g.id == id);
if (group) {
resolve(group);
} else {
http({
url: `/group/find/${id}`,
method: 'get'
}).then((group) => {
resolve(group)
store.commit("addGroup", group);
})
}
});
},
exit() { exit() {
console.log("exit"); console.log("exit");
wsApi.close(); wsApi.close();
uni.removeStorageSync("loginInfo"); uni.removeStorageSync("loginInfo");
uni.navigateTo({ uni.reLaunch({
url: "/pages/login/login" url: "/pages/login/login"
}) })
store.dispatch("unload"); store.dispatch("unload");
@ -214,5 +277,4 @@
// #endif // #endif
background-color: #f8f8f8; background-color: #f8f8f8;
} }
</style> </style>

21
im-uniapp/common/emotion.js

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

11
im-uniapp/common/enums.js

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

3
im-uniapp/common/request.js

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

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

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

35
im-uniapp/components/loading/loading.vue

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

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

@ -6,9 +6,10 @@
<uni-icons class="btn-side right" type="more-filled" size="30" @click="onShowMore()"></uni-icons> <uni-icons class="btn-side right" type="more-filled" size="30" @click="onShowMore()"></uni-icons>
</view> </view>
<view class="chat-msg" @click="switchChatTabBox('none',true)"> <view class="chat-msg" @click="switchChatTabBox('none',true)">
<scroll-view class="scroll-box" scroll-y="true" :scroll-into-view="'chat-item-'+scrollMsgIdx"> <scroll-view class="scroll-box" scroll-y="true" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx"> <view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" <chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile" @recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile"
:id="'chat-item-'+idx" :msgInfo="msgInfo"> :id="'chat-item-'+idx" :msgInfo="msgInfo">
</chat-message-item> </chat-message-item>
@ -32,8 +33,8 @@
<view class="chat-tab-bar" v-show="chatTabBox!='none' ||showKeyBoard " :style="{height:`${keyboardHeight}px`}"> <view class="chat-tab-bar" v-show="chatTabBox!='none' ||showKeyBoard " :style="{height:`${keyboardHeight}px`}">
<view v-if="chatTabBox == 'tools'" class="chat-tools"> <view v-if="chatTabBox == 'tools'" class="chat-tools">
<view class="chat-tools-item"> <view class="chat-tools-item">
<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess" <image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
:onError="onUploadImageFail"> :onSuccess="onUploadImageSuccess" :onError="onUploadImageFail">
<view class="tool-icon iconfont icon-picture"></view> <view class="tool-icon iconfont icon-picture"></view>
</image-upload> </image-upload>
<view class="tool-name">相册</view> <view class="tool-name">相册</view>
@ -66,8 +67,9 @@
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true"> <scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
<view class="emotion-item-list"> <view class="emotion-item-list">
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)" v-for="(emoText, i) in $emo.emoTextList" <image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
:key="i" @click="selectEmoji(emoText)" mode="aspectFit" lazy-load="true"></image> v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
lazy-load="true"></image>
</view> </view>
</scroll-view> </scroll-view>
<view v-if="showKeyBoard"></view> <view v-if="showKeyBoard"></view>
@ -89,7 +91,8 @@
scrollMsgIdx: 0, // scrollMsgIdx: 0, //
chatTabBox: 'none', chatTabBox: 'none',
showKeyBoard: false, showKeyBoard: false,
keyboardHeight: 322 keyboardHeight: 322,
showMinIdx: 0 // showMinIdx
} }
}, },
methods: { methods: {
@ -139,6 +142,7 @@
msgInfo.sendTime = new Date().getTime(); msgInfo.sendTime = new Date().getTime();
msgInfo.sendId = this.$store.state.userStore.userInfo.id; msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true; msgInfo.selfSend = true;
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
this.sendText = ""; this.sendText = "";
}).finally(() => { }).finally(() => {
@ -171,6 +175,7 @@
return; return;
} }
this.$nextTick(() => { this.$nextTick(() => {
console.log("scrollToMsgIdx",this.scrollMsgIdx)
this.scrollMsgIdx = idx; this.scrollMsgIdx = idx;
}); });
@ -211,7 +216,8 @@
sendTime: new Date().getTime(), sendTime: new Date().getTime(),
selfSend: true, selfSend: true,
type: this.$enums.MESSAGE_TYPE.IMAGE, type: this.$enums.MESSAGE_TYPE.IMAGE,
loadStatus: "loading" loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
} }
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
@ -254,7 +260,8 @@
sendTime: new Date().getTime(), sendTime: new Date().getTime(),
selfSend: true, selfSend: true,
type: this.$enums.MESSAGE_TYPE.FILE, type: this.$enums.MESSAGE_TYPE.FILE,
loadStatus: "loading" loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
} }
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
@ -318,6 +325,7 @@
msgInfo = JSON.parse(JSON.stringify(msgInfo)); msgInfo = JSON.parse(JSON.stringify(msgInfo));
msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL; msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL;
msgInfo.content = '你撤回了一条消息'; msgInfo.content = '你撤回了一条消息';
msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}) })
} }
@ -346,6 +354,12 @@
} }
}); });
}, },
onScrollToTop() {
//
this.scrollToMsgIdx(this.showMinIdx);
// 10
this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
},
onShowMore() { onShowMore() {
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
uni.navigateTo({ uni.navigateTo({
@ -357,6 +371,20 @@
}) })
} }
}, },
readedMessage() {
if (this.chat.type == "GROUP") {
var url = `/message/group/readed?groupId=${this.chat.targetId}`
} else {
url = `/message/private/readed?friendId=${this.chat.targetId}`
}
this.$http({
url: url,
method: 'PUT'
}).then(() => {
this.$store.commit("resetUnreadCount", this.chat)
this.scrollToBottom();
})
},
loadGroup(groupId) { loadGroup(groupId) {
this.$http({ this.$http({
url: `/group/find/${groupId}`, url: `/group/find/${groupId}`,
@ -416,6 +444,9 @@
return 0; return 0;
} }
return this.chat.messages.length; return this.chat.messages.length;
},
unreadCount() {
return this.chat.unreadCount;
} }
}, },
watch: { watch: {
@ -424,15 +455,28 @@
if (newSize > oldSize) { if (newSize > oldSize) {
this.scrollToBottom(); this.scrollToBottom();
} }
},
unreadCount: {
handler(newCount, oldCount) {
if (newCount > 0) {
//
this.readedMessage()
}
}
} }
}, },
onLoad(options) { onLoad(options) {
// //
this.chat = this.$store.state.chatStore.chats[options.chatIdx]; this.chat = this.$store.state.chatStore.chats[options.chatIdx];
// 30
let size = this.chat.messages.length;
this.showMinIdx = size > 30 ? size - 30 : 0;
// //
this.$store.commit("activeChat", options.chatIdx); this.$store.commit("activeChat", options.chatIdx);
// //
this.scrollToBottom(); this.scrollToBottom();
//
this.readedMessage()
// //
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
this.loadGroup(this.chat.targetId); this.loadGroup(this.chat.targetId);
@ -475,6 +519,7 @@
&.left { &.left {
left: 30rpx; left: 30rpx;
} }
&.right { &.right {
right: 30rpx; right: 30rpx;
} }

26
im-uniapp/pages/chat/chat.vue

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

9
im-uniapp/pages/login/login.vue

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

87
im-uniapp/store/chatStore.js

@ -1,23 +1,33 @@
import {MESSAGE_TYPE} from '@/common/enums.js'; import {
MESSAGE_TYPE,
MESSAGE_STATUS
} from '@/common/enums.js';
import userStore from './userStore'; import userStore from './userStore';
export default { export default {
state: { state: {
chats: [] activeIndex: -1,
chats: [],
privateMsgMaxId: 0,
groupMsgMaxId: 0,
loadingPrivateMsg: false,
loadingGroupMsg: false,
}, },
mutations: { mutations: {
initChats(state,chats){ initChats(state, chatsData) {
state.chats = chatsData.chats ||[];
state.privateMsgMaxId = chatsData.privateMsgMaxId||0;
state.groupMsgMaxId = chatsData.groupMsgMaxId||0;
// 防止图片一直处在加载中状态 // 防止图片一直处在加载中状态
chats.forEach((chat)=>{ state.chats.forEach((chat) => {
chat.messages.forEach((msg) => { chat.messages.forEach((msg) => {
if (msg.loadStatus == "loading") { if (msg.loadStatus == "loading") {
msg.loadStatus = "fail" msg.loadStatus = "fail"
} }
}) })
}) })
state.chats = chats;
}, },
openChat(state, chatInfo) { openChat(state, chatInfo) {
let chat = null; let chat = null;
@ -53,6 +63,29 @@ export default {
state.chats[idx].unreadCount = 0; state.chats[idx].unreadCount = 0;
} }
}, },
resetUnreadCount(state, chatInfo) {
for (let idx in state.chats) {
if (state.chats[idx].type == chatInfo.type &&
state.chats[idx].targetId == chatInfo.targetId) {
state.chats[idx].unreadCount = 0;
}
}
this.commit("saveToStorage");
},
readedMessage(state, friendId) {
for (let idx in state.chats) {
if (state.chats[idx].type == 'PRIVATE' &&
state.chats[idx].targetId == friendId) {
state.chats[idx].messages.forEach((m) => {
console.log("readedMessage")
if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) {
m.status = MESSAGE_STATUS.READED
}
})
}
}
this.commit("saveToStorage");
},
removeChat(state, idx) { removeChat(state, idx) {
state.chats.splice(idx, 1); state.chats.splice(idx, 1);
this.commit("saveToStorage"); this.commit("saveToStorage");
@ -104,13 +137,16 @@ export default {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
// 如果不是当前会话,未读加1 // 未读加1
if(chatIdx != state.activeIndex){ if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++; chat.unreadCount++;
} }
// 自己回复了消息,说明消息已读 // 记录消息的最大id
if(msgInfo.selfSend){ if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
chat.unreadCount=0; state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
} }
// 如果是已存在消息,则覆盖旧的消息数据 // 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) { for (let idx in chat.messages) {
@ -120,8 +156,8 @@ export default {
return; return;
} }
// 正在发送中的消息可能没有id,通过发送时间判断 // 正在发送中的消息可能没有id,通过发送时间判断
if(msgInfo.selfSend && chat.messages[idx].selfSend if (msgInfo.selfSend && chat.messages[idx].selfSend &&
&& chat.messages[idx].sendTime == msgInfo.sendTime){ chat.messages[idx].sendTime == msgInfo.sendTime) {
Object.assign(chat.messages[idx], msgInfo); Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage"); this.commit("saveToStorage");
return; return;
@ -160,8 +196,8 @@ export default {
break; break;
} }
// 正在发送中的消息可能没有id,根据发送时间删除 // 正在发送中的消息可能没有id,根据发送时间删除
if(msgInfo.selfSend && chat.messages[idx].selfSend if (msgInfo.selfSend && chat.messages[idx].selfSend &&
&&chat.messages[idx].sendTime == msgInfo.sendTime){ chat.messages[idx].sendTime == msgInfo.sendTime) {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
break; break;
} }
@ -190,15 +226,32 @@ export default {
} }
this.commit("saveToStorage"); this.commit("saveToStorage");
}, },
loadingPrivateMsg(state, loadding) {
state.loadingPrivateMsg = loadding;
},
loadingGroupMsg(state, loadding) {
state.loadingGroupMsg = loadding;
},
saveToStorage(state) { saveToStorage(state) {
let userId = userStore.state.userInfo.id; let userId = userStore.state.userInfo.id;
let key = "chats-" + userId;
let chatsData = {
privateMsgMaxId: state.privateMsgMaxId,
groupMsgMaxId: state.groupMsgMaxId,
chats: state.chats
}
uni.setStorage({ uni.setStorage({
key:"chats-"+userId, key: key,
data: state.chats data: chatsData
}) })
}, },
clear(state) { clear(state) {
state.chats = []; state.chats = [];
state.activeIndex = -1;
state.privateMsgMaxId = 0;
state.groupMsgMaxId = 0;
state.loadingPrivateMsg = false;
state.loadingGroupMsg = false;
} }
}, },
actions: { actions: {
@ -212,8 +265,6 @@ export default {
resolve() resolve()
}, },
fail(e) { fail(e) {
// 不存在聊天记录,清空聊天列表
context.commit("initChats",[]);
resolve() resolve()
} }
}); });

Loading…
Cancel
Save