Browse Source

优化: 消息支持显示发送状态

master
xsx 9 months ago
parent
commit
19948509c4
  1. 10
      im-uniapp/common/enums.js
  2. 84
      im-uniapp/components/chat-message-item/chat-message-item.vue
  3. 7
      im-uniapp/components/head-image/head-image.vue
  4. 13
      im-uniapp/components/loading/loading.vue
  5. 62
      im-uniapp/pages/chat/chat-box.vue
  6. 10
      im-uniapp/store/chatStore.js
  7. 10
      im-web/src/api/enums.js
  8. 69
      im-web/src/components/chat/ChatBox.vue
  9. 99
      im-web/src/components/chat/ChatMessageItem.vue
  10. 34
      im-web/src/store/chatStore.js
  11. 16
      im-web/src/view/Login.vue

10
im-uniapp/common/enums.js

@ -54,10 +54,12 @@ const TERMINAL_TYPE = {
}
const MESSAGE_STATUS = {
UNSEND: 0,
SENDED: 1,
RECALL: 2,
READED: 3
FAILED: -2, // 发送失败
SENDING: -1, // 发送中(消息没到服务器)
PENDING: 0, // 未送达(消息已到服务器,但对方没收到)
DELIVERED: 1, // 已送达(对方已收到,但是未读消息)
RECALL: 2, // 已撤回
READED: 3, // 消息已读
}
export {

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

@ -14,6 +14,7 @@
<text>{{ showName }}</text>
</view>
<view class="bottom">
<view class="message-content-wrapper">
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<!-- up-parse支持点击a标签,但是不支持显示emo表情也不支持换行 -->
@ -23,34 +24,31 @@
<rich-text v-else class="message-text" :nodes="nodesText"></rich-text>
</long-press-menu>
</view>
<view class="message-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<view class="message-image" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="image-box">
<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl"
lazy-load="true" @click.stop="onShowFullImage()">
<image class="send-image" mode="heightFix"
:src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true"
@click.stop="onShowFullImage()">
</image>
<loading v-if="loading"></loading>
<loading v-if="sending"></loading>
</view>
</long-press-menu>
<text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view>
<view class="message-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<view class="message-file" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="file-box">
<view class="file-info">
<uni-link class="file-name" :text="data.name" showUnderLine="true" color="#007BFF"
:href="data.url"></uni-link>
<uni-link class="file-name" :text="data.name" showUnderLine="true"
color="#007BFF" :href="data.url"></uni-link>
<view class="file-size">{{ fileSize }}</view>
</view>
<view class="file-icon iconfont icon-file"></view>
<loading v-if="loading"></loading>
<loading v-if="sending"></loading>
</view>
</long-press-menu>
<text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view>
<long-press-menu v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" :items="menuItems"
<long-press-menu v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" :items="menuItems"
@select="onSelectMenu">
<view class="message-audio message-text" @click="onPlayAudio()">
<text class="iconfont icon-voice-play"></text>
@ -59,6 +57,12 @@
<text v-if="audioPlayState == 'PLAYING'" class="iconfont icon-pause"></text>
</view>
</long-press-menu>
<view v-if="sending&&isTextMessage" class="sending">
<loading size="40" icon-color="#656adf" :mask="false"></loading>
</view>
<view v-else-if="sendFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></view>
</view>
<long-press-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">
<view class="chat-realtime message-text" @click="$emit('call')">
<text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE"
@ -68,11 +72,9 @@
<text>{{ msgInfo.content }}</text>
</view>
</long-press-menu>
<view class="message-status" v-if="!isAction">
<text class="chat-readed" v-if="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-if="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</text>
<view class="message-status" v-if="!isAction && msgInfo.selfSend && !msgInfo.groupId">
<text class="chat-readed" v-if="msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-else>未读</text>
</view>
<view class="chat-receipt" v-if="msgInfo.receipt" @click="onShowReadedBox">
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
@ -118,7 +120,7 @@ export default {
methods: {
onSendFail() {
uni.showToast({
title: "该文件已发送失败,目前不支持自动重新发送,建议手动重新发送",
title: "该消息已发送失败,目前不支持自动重新发送,建议手动重新发送",
icon: "none"
})
},
@ -177,11 +179,11 @@ export default {
}
},
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
sending() {
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.SENDING;
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
sendFail() {
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.FAILED;
},
data() {
return JSON.parse(this.msgInfo.content)
@ -227,6 +229,9 @@ export default {
}
return items;
},
isTextMessage() {
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.TEXT
},
isAction() {
return this.$msgType.isAction(this.msgInfo.type);
},
@ -285,6 +290,26 @@ export default {
padding-right: 80rpx;
margin-top: 5rpx;
.message-content-wrapper {
position: relative;
display: flex;
align-items: center;
.sending {
position: relative;
margin: 0 6rpx;
.icon-loading {
color: $im-color-primary;
}
}
.send-fail {
color: #e60c0c;
font-size: 50rpx;
margin: 0 5rpx;
}
.message-text {
position: relative;
line-height: 1.6;
@ -397,6 +422,7 @@ export default {
padding-right: 8px;
}
}
}
.chat-realtime {
display: flex;
@ -454,6 +480,10 @@ export default {
padding-left: 80rpx;
padding-right: 0;
.message-content-wrapper {
flex-direction: row-reverse;
}
.message-text {
margin-left: 10px;
background-color: $im-color-primary-light-2;
@ -466,14 +496,6 @@ export default {
}
}
.message-image {
flex-direction: row-reverse;
}
.message-file {
flex-direction: row-reverse;
}
.message-audio {
flex-direction: row-reverse;

7
im-uniapp/components/head-image/head-image.vue

@ -16,7 +16,8 @@ export default {
data() {
return {
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
"#c9b455", "#326eb6"]
"#c9b455", "#326eb6"
]
}
},
props: {
@ -34,6 +35,10 @@ export default {
type: String,
default: null
},
radius: {
type: String,
default: "50%"
},
online: {
type: Boolean,
default: false

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

@ -6,7 +6,6 @@
</template>
<script>
export default {
data() {
return {}
@ -16,6 +15,10 @@ export default {
type: Number,
default: 100
},
iconColor: {
type: String,
default: ''
},
mask: {
type: Boolean,
default: true
@ -23,7 +26,13 @@ export default {
},
computed: {
icontStyle() {
return `font-size:${this.size}rpx`;
let style = `font-size:${this.size}rpx;`;
if(this.iconColor){
style += `color: ${this.iconColor};`
}else if(this.mask){
style += 'color: #eee;'
}
return style;
},
loadingStyle() {
return this.mask ? "background: rgba(0, 0, 0, 0.3);" : "";

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

@ -173,9 +173,17 @@ export default {
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
//
const chat = this.chat;
//
let tmpMessage = this.buildTmpMessage(msgInfo);
this.chatStore.insertMessage(tmpMessage, chat);
this.moveChatToTop();
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.chatStore.insertMessage(m, this.chat);
//
tmpMessage.id = m.id;
tmpMessage.status = m.status;
this.chatStore.insertMessage(tmpMessage, chat);
//
this.moveChatToTop();
//
@ -302,7 +310,7 @@ export default {
content: receiptText + sendText + atText,
atUserIds: this.atUserIds,
receipt: this.isReceipt,
type: 0
type: this.$enums.MESSAGE_TYPE.TEXT
}
// @
this.atUserIds = [];
@ -311,11 +319,23 @@ export default {
this.fillTargetId(msgInfo, this.chat.targetId);
//
const chat = this.chat;
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.chatStore.insertMessage(m, chat);
//
//
let tmpMessage = this.buildTmpMessage(msgInfo);
this.chatStore.insertMessage(tmpMessage, chat);
this.moveChatToTop();
this.sendMessageRequest(msgInfo).then((m) => {
//
tmpMessage = JSON.parse(JSON.stringify(tmpMessage));
tmpMessage.id = m.id;
tmpMessage.status = m.status;
tmpMessage.content = m.content;
this.chatStore.insertMessage(tmpMessage, chat);
}).catch(() => {
//
tmpMessage = JSON.parse(JSON.stringify(tmpMessage));
tmpMessage.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.insertMessage(tmpMessage, chat);
}).finally(() => {
//
this.scrollToBottom();
@ -410,7 +430,6 @@ export default {
thumbUrl: file.path
}
let msgInfo = {
id: 0,
tmpId: this.generateId(),
fileId: file.uid,
sendId: this.mine.id,
@ -419,8 +438,7 @@ export default {
selfSend: true,
type: this.$enums.MESSAGE_TYPE.IMAGE,
readedCount: 0,
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
status: this.$enums.MESSAGE_STATUS.SENDING
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -440,15 +458,15 @@ export default {
msgInfo.content = JSON.stringify(res.data);
msgInfo.receipt = this.isReceipt
this.sendMessageRequest(msgInfo).then((m) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
msgInfo.status = m.status;
this.isReceipt = false;
this.chatStore.insertMessage(msgInfo, file.chat);
})
},
onUploadImageFail(file, err) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail';
msgInfo.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.insertMessage(msgInfo, file.chat);
},
onUploadFileBefore(file) {
@ -463,7 +481,6 @@ export default {
url: file.path
}
let msgInfo = {
id: 0,
tmpId: this.generateId(),
sendId: this.mine.id,
content: JSON.stringify(data),
@ -471,8 +488,7 @@ export default {
selfSend: true,
type: this.$enums.MESSAGE_TYPE.FILE,
readedCount: 0,
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
status: this.$enums.MESSAGE_STATUS.SENDING
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -497,15 +513,15 @@ export default {
msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt
this.sendMessageRequest(msgInfo).then((m) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
msgInfo.status = m.status;
this.isReceipt = false;
this.chatStore.insertMessage(msgInfo, file.chat);
})
},
onUploadFileFail(file, res) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail';
msgInfo.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.insertMessage(msgInfo, file.chat);
},
onDeleteMessage(msgInfo) {
@ -875,6 +891,18 @@ export default {
}
this.chatStore.insertMessage(msgInfo, this.chat);
},
buildTmpMessage(msgInfo) {
let message = JSON.parse(JSON.stringify(msgInfo));
message.tmpId = this.generateId();
message.sendId = this.mine.id;
message.sendTime = new Date().getTime();
message.status = this.$enums.MESSAGE_STATUS.SENDING;
message.selfSend = true;
if (this.isGroup) {
message.readedCount = 0;
}
return message;
},
generateId() {
// id
return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));

10
im-uniapp/store/chatStore.js

@ -30,11 +30,11 @@ export default defineStore('chatStore', {
}
this.privateMsgMaxId = chatsData.privateMsgMaxId || 0;
this.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
// 防止图片一直处在加载中状态
cacheChats.forEach((chat) => {
chat.messages.forEach((msg) => {
if (msg.loadStatus == "loading") {
msg.loadStatus = "fail"
// 防止消息一直处在发送中状态
cacheChats.forEach(chat => {
chat.messages.forEach(msg => {
if (msg.status == MESSAGE_STATUS.SENDING) {
msg.status = MESSAGE_STATUS.FAILED
}
})
})

10
im-web/src/api/enums.js

@ -55,10 +55,12 @@ const TERMINAL_TYPE = {
}
const MESSAGE_STATUS = {
UNSEND: 0,
SENDED: 1,
RECALL: 2,
READED: 3
FAILED: -2, // 发送失败
SENDING: -1, // 发送中(消息没到服务器)
PENDING: 0, // 未送达(消息已到服务器,但对方没收到)
DELIVERED: 1, // 已送达(对方已收到,但是未读消息)
RECALL: 2, // 已撤回
READED: 3, // 消息已读
}

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

@ -159,15 +159,15 @@ export default {
msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt;
this.sendMessageRequest(msgInfo).then((m) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
msgInfo.status = m.status;
this.isReceipt = false;
this.chatStore.insertMessage(msgInfo, file.chat);
})
},
onImageFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail';
msgInfo.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.insertMessage(msgInfo, file.chat);
},
onImageBefore(file) {
@ -182,17 +182,15 @@ export default {
thumbUrl: url
}
let msgInfo = {
id: 0,
tmpId: this.generateId(),
fileId: file.uid,
sendId: this.mine.id,
content: JSON.stringify(data),
sendTime: new Date().getTime(),
selfSend: true,
type: 1,
type: this.$enums.MESSAGE_TYPE.IMAGE,
readedCount: 0,
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
status: this.$enums.MESSAGE_STATUS.SENDING
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -216,8 +214,8 @@ export default {
msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt
this.sendMessageRequest(msgInfo).then((m) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
msgInfo.status = m.status;
this.isReceipt = false;
this.refreshPlaceHolder();
this.chatStore.insertMessage(msgInfo, file.chat);
@ -225,7 +223,7 @@ export default {
},
onFileFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail';
msgInfo.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.insertMessage(msgInfo, file.chat);
},
onFileBefore(file) {
@ -241,16 +239,14 @@ export default {
url: url
}
let msgInfo = {
id: 0,
tmpId: this.generateId(),
sendId: this.mine.id,
content: JSON.stringify(data),
sendTime: new Date().getTime(),
selfSend: true,
type: 2,
loadStatus: "loading",
type: this.$enums.MESSAGE_TYPE.FILE,
readedCount: 0,
status: this.$enums.MESSAGE_STATUS.UNSEND
status: this.$enums.MESSAGE_STATUS.SENDING
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -366,14 +362,22 @@ export default {
}
let msgInfo = {
content: JSON.stringify(data),
type: 3,
type: this.$enums.MESSAGE_TYPE.AUDIO,
receipt: this.isReceipt
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
//
const chat = this.chat;
//
let tmpMessage = this.buildTmpMessage(msgInfo);
this.chatStore.insertMessage(tmpMessage, chat);
this.moveChatToTop();
this.sendMessageRequest(msgInfo).then(m => {
m.selfSend = true;
this.chatStore.insertMessage(m, this.chat);
//
tmpMessage.id = m.id;
tmpMessage.status = m.status;
this.chatStore.insertMessage(tmpMessage, chat);
//
this.moveChatToTop();
//
@ -384,6 +388,9 @@ export default {
this.showRecord = false;
this.isReceipt = false;
this.refreshPlaceHolder();
}).catch(() => {
tmpMessage.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.insertMessage(tmpMessage, this.chat);
})
},
fillTargetId(msgInfo, targetId) {
@ -446,7 +453,7 @@ export default {
}
let msgInfo = {
content: sendText,
type: 0
type: this.$enums.MESSAGE_TYPE.TEXT
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -456,11 +463,23 @@ export default {
msgInfo.receipt = this.isReceipt;
}
this.lockMessage = true;
//
const chat = this.chat;
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.chatStore.insertMessage(m, chat);
//
let tmpMessage = this.buildTmpMessage(msgInfo);
this.chatStore.insertMessage(tmpMessage, chat);
this.moveChatToTop();
//
this.sendMessageRequest(msgInfo).then((m) => {
//
tmpMessage.id = m.id;
tmpMessage.status = m.status;
tmpMessage.content = m.content;
this.chatStore.insertMessage(tmpMessage, chat);
}).catch(() => {
//
tmpMessage.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.insertMessage(tmpMessage, chat);
}).finally(() => {
this.scrollToBottom();
this.isReceipt = false;
@ -649,6 +668,18 @@ export default {
}
this.chatStore.insertMessage(msgInfo, this.chat);
},
buildTmpMessage(msgInfo) {
let message = JSON.parse(JSON.stringify(msgInfo));
message.tmpId = this.generateId();
message.sendId = this.mine.id;
message.sendTime = new Date().getTime();
message.status = this.$enums.MESSAGE_STATUS.SENDING;
message.selfSend = true;
if (this.isGroup) {
message.readedCount = 0;
}
return message;
},
generateId() {
// id
return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));

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

@ -19,20 +19,17 @@
<span>{{ $date.toTimeText(msgInfo.sendTime) }}</span>
</div>
<div class="message-bottom" @contextmenu.prevent="showRightMenu($event)">
<div ref="chatMsgBox">
<span class="message-text" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT"
v-html="htmlText"></span>
<div class="message-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<div class="img-load-box" v-loading="loading" element-loading-text="上传中.."
<div ref="chatMsgBox" class="message-content-wrapper">
<span class="message-text" v-if="isTextMessage" v-html="htmlText"></span>
<div class="message-image" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<div class="img-load-box" v-loading="sending" element-loading-text="发送中.."
element-loading-background="rgba(0, 0, 0, 0.4)">
<img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl"
@click="showFullImageBox()" loading="lazy" />
</div>
<span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span>
</div>
<div class="message-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<div class="chat-file-box" v-loading="loading">
<div class="message-file" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
<div class="chat-file-box" v-loading="sending">
<div class="chat-file-info">
<el-link class="chat-file-name" :underline="true" target="_blank" type="primary"
:href="data.url" :download="data.name">{{ data.name }}</el-link>
@ -42,13 +39,15 @@
<span type="primary" class="el-icon-document"></span>
</div>
</div>
<span title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail el-icon-warning"></span>
</div>
</div>
<div class="message-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<div class="message-voice" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO"
@click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div>
<div title="发送中" v-if="sending && isTextMessage" class="sending" v-loading="'true'"></div>
<div title="发送失败" v-else-if="sendFail" @click="onSendFail" class="send-fail el-icon-warning">
</div>
</div>
<div class="chat-action message-text" v-if="isAction">
<span v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE" title="重新呼叫"
@click="$emit('call')" class="iconfont icon-chat-voice"></span>
@ -56,11 +55,9 @@
@click="$emit('call')" class="iconfont icon-chat-video"></span>
<span>{{ msgInfo.content }}</span>
</div>
<div class="message-status" v-if="!isAction">
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span>
<div class="message-status" v-if="!isAction && msgInfo.selfSend && !isGroupMessage">
<span class="chat-readed" v-if="msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-else>未读</span>
</div>
<div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span>
@ -121,7 +118,7 @@ export default {
},
methods: {
onSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
this.$message.error("该消息已发送失败,目前不支持自动重新发送,建议手动重新发送")
},
showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
@ -149,11 +146,11 @@ export default {
}
},
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
sending() {
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.SENDING;
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
sendFail() {
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.FAILED;
},
data() {
return JSON.parse(this.msgInfo.content)
@ -184,6 +181,9 @@ export default {
}
return items;
},
isTextMessage() {
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.TEXT
},
isAction() {
return this.$msgType.isAction(this.msgInfo.type);
},
@ -196,13 +196,18 @@ export default {
let text = this.$str.html2Escape(this.msgInfo.content)
text = this.$url.replaceURLWithHTMLLinks(text, color)
return this.$emo.transform(text, 'emoji-normal')
},
isGroupMessage() {
return !!this.msgInfo.groupId;
}
}
}
</script>
<style lang="scss" scoped>
<style lang="scss">
.chat-message-item {
padding: 2px 10px;
border-radius: 10px;
.message-tip {
line-height: 50px;
@ -228,13 +233,6 @@ export default {
.content {
text-align: left;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
.message-top {
display: flex;
flex-wrap: nowrap;
@ -252,11 +250,35 @@ export default {
padding-right: 300px;
padding-left: 5px;
.message-content-wrapper {
position: relative;
display: flex;
align-items: end;
.sending {
width: 25px;
height: 25px;
.circular {
width: 25px;
height: 25px;
}
}
.send-fail {
color: #e45050;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
}
.message-text {
flex: 1;
display: inline-block;
position: relative;
line-height: 26px;
//margin-top: 3px;
padding: 6px 10px;
background-color: var(--im-background);
border-radius: 10px;
@ -373,6 +395,7 @@ export default {
}
.message-status {
margin-top: 3px;
display: block;
.chat-readed {
@ -432,6 +455,10 @@ export default {
padding-left: 180px;
padding-right: 5px;
.message-content-wrapper {
flex-direction: row-reverse;
}
.message-text {
margin-left: 10px;
background-color: var(--im-color-primary-light-2);
@ -444,14 +471,6 @@ export default {
}
}
.message-image {
flex-direction: row-reverse;
}
.message-file {
flex-direction: row-reverse;
}
.chat-action {
flex-direction: row-reverse;

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

@ -40,11 +40,11 @@ export default defineStore('chatStore', {
this.privateMsgMaxId = chatsData.privateMsgMaxId || 0;
this.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
cacheChats = chatsData.chats || [];
// 防止图片一直处在加载中状态
cacheChats.forEach((chat) => {
chat.messages.forEach((msg) => {
if (msg.loadStatus == "loading") {
msg.loadStatus = "fail"
// 防止消息一直处在发送中状态
cacheChats.forEach(chat => {
chat.messages.forEach(msg => {
if (msg.status == MESSAGE_STATUS.SENDING) {
msg.status = MESSAGE_STATUS.FAILED
}
})
})
@ -396,43 +396,53 @@ export default defineStore('chatStore', {
if (this.isLoading()) {
return;
}
let userStore = useUserStore();
const userStore = useUserStore();
let userId = userStore.userInfo.id;
let key = "chats-" + userId;
let chatKeys = [];
// 按会话为单位存储,
this.chats.forEach((chat) => {
const promises = [];
// 按会话为单位存储
for (let idx in this.chats) {
let chat = this.chats[idx];
// 只存储有改动的会话
let chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) {
if (chat.delete) {
localForage.removeItem(chatKey);
let hotKey = chatKey + '-hot';
promises.push(localForage.removeItem(chatKey))
promises.push(localForage.removeItem(hotKey))
} else {
// 存储冷数据
if (withColdMessage) {
let coldChat = Object.assign({}, chat);
coldChat.messages = chat.messages.slice(0, chat.hotMinIdx);
localForage.setItem(chatKey, coldChat)
promises.push(localForage.setItem(chatKey, coldChat))
}
// 存储热消息
let hotKey = chatKey + '-hot';
let hotChat = Object.assign({}, chat);
hotChat.messages = chat.messages.slice(chat.hotMinIdx)
localForage.setItem(hotKey, hotChat)
promises.push(localForage.setItem(hotKey, hotChat))
}
chat.stored = true;
}
if (!chat.delete) {
chatKeys.push(chatKey);
}
})
}
// 会话核心信息
let chatsData = {
privateMsgMaxId: this.privateMsgMaxId,
groupMsgMaxId: this.groupMsgMaxId,
systemMsgMaxSeqNo: this.systemMsgMaxSeqNo,
chatKeys: chatKeys
}
Promise.all(promises).then(() => {
localForage.setItem(key, chatsData)
}).catch(() => {
console.log("本地消息缓存存储失败")
})
// 清理已删除的会话
this.chats = this.chats.filter(chat => !chat.delete)
},

16
im-web/src/view/Login.vue

@ -1,22 +1,22 @@
<template>
<div class="login-view">
<div class="content">
<el-form class="form" :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px"
<el-form class="form" :model="loginForm" status-icon :rules="rules" ref="loginForm"
@keyup.enter.native="submitForm('loginForm')">
<div class="title">
<img class="logo" src="../../public/logo.png" />
<div>登录盒子IM</div>
</div>
<el-form-item label="终端" prop="userName" v-show="false">
<el-form-item prop="terminal" v-show="false">
<el-input type="terminal" v-model="loginForm.terminal" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="loginForm.userName" autocomplete="off"
placeholder="用户名"></el-input>
<el-form-item prop="userName">
<el-input type="userName" v-model="loginForm.userName" autocomplete="off" placeholder="用户名"
prefix-icon="el-icon-user"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password" autocomplete="off"
placeholder="密码"></el-input>
<el-form-item prop="password">
<el-input type="password" v-model="loginForm.password" autocomplete="off" placeholder="密码"
prefix-icon="el-icon-lock"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')">登录</el-button>

Loading…
Cancel
Save