Browse Source

Merge branch 'v_3.0.0' of https://gitee.com/bluexsx/box-im

# Conflicts:
#	im-web/src/components/chat/ChatBox.vue
#	im-web/src/components/chat/ChatInput.vue
#	im-web/src/components/chat/ChatMessageItem.vue
master
xsx 2 years ago
parent
commit
a8d1b1b29e
  1. 7
      im-commom/src/main/java/com/bx/imcommon/mq/RedisMQPullTask.java
  2. 58
      im-uniapp/pages/chat/chat-box.vue
  3. 6
      im-web/src/components/chat/ChatAtBox.vue
  4. 421
      im-web/src/components/chat/ChatBox.vue
  5. 571
      im-web/src/components/chat/ChatInput.vue
  6. 15
      im-web/src/components/chat/ChatMessageItem.vue
  7. 14
      im-web/src/view/Home.vue

7
im-commom/src/main/java/com/bx/imcommon/mq/RedisMQPullTask.java

@ -53,6 +53,8 @@ public class RedisMQPullTask implements CommandLineRunner {
List<Object> datas = new LinkedList<>(); List<Object> datas = new LinkedList<>();
try { try {
if(redisMQTemplate.isClose()){ if(redisMQTemplate.isClose()){
// 如果redis未初始化或已断开,3s后再重新尝试消费
EXECUTOR.schedule(this, 3, TimeUnit.SECONDS);
return; return;
} }
if (consumer.isReady()) { if (consumer.isReady()) {
@ -79,7 +81,7 @@ public class RedisMQPullTask implements CommandLineRunner {
if (!EXECUTOR.isShutdown()) { if (!EXECUTOR.isShutdown()) {
if (datas.size() < batchSize) { if (datas.size() < batchSize) {
// 数据已经消费完,等待下一个周期继续拉取 // 数据已经消费完,等待下一个周期继续拉取
EXECUTOR.schedule(this, period, TimeUnit.MICROSECONDS); EXECUTOR.schedule(this, period, TimeUnit.MILLISECONDS);
} else { } else {
// 数据没有消费完,直接开启下一个消费周期 // 数据没有消费完,直接开启下一个消费周期
EXECUTOR.execute(this); EXECUTOR.execute(this);
@ -102,6 +104,9 @@ public class RedisMQPullTask implements CommandLineRunner {
objects.add(obj); objects.add(obj);
obj = redisMQTemplate.opsForList().leftPop(key); obj = redisMQTemplate.opsForList().leftPop(key);
} }
if (!Objects.isNull(obj)){
objects.add(obj);
}
} }
return objects; return objects;
} }

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

@ -132,9 +132,10 @@
showRecord: false, showRecord: false,
keyboardHeight: 322, keyboardHeight: 322,
atUserIds: [], atUserIds: [],
recordText: "",
needScrollToBottom: false, // needScrollToBottom: false, //
showMinIdx: 0 // showMinIdx showMinIdx: 0, // showMinIdx
reqQueue: [], //
isSending: false //
} }
}, },
methods: { methods: {
@ -154,11 +155,7 @@
} }
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
this.$http({ this.sendMessageRequest(msgInfo).then((m) => {
url: this.messageAction,
method: 'POST',
data: msgInfo
}).then((m) => {
m.selfSend = true; m.selfSend = true;
this.$store.commit("insertMessage", m); this.$store.commit("insertMessage", m);
// //
@ -278,19 +275,14 @@
receipt: this.isReceipt, receipt: this.isReceipt,
type: 0 type: 0
} }
this.sendText = "";
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
this.sendText = ""; this.sendMessageRequest(msgInfo).then((m) => {
this.$http({
url: this.messageAction,
method: 'POST',
data: msgInfo
}).then((m) => {
m.selfSend = true; m.selfSend = true;
this.$store.commit("insertMessage", m); this.$store.commit("insertMessage", m);
// //
this.moveChatToTop(); this.moveChatToTop();
this.sendText = "";
}).finally(() => { }).finally(() => {
// //
this.scrollToBottom(); this.scrollToBottom();
@ -408,11 +400,7 @@
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(res.data); msgInfo.content = JSON.stringify(res.data);
msgInfo.receipt = this.isReceipt msgInfo.receipt = this.isReceipt
this.$http({ this.sendMessageRequest(msgInfo).then((m) => {
url: this.messageAction,
method: 'POST',
data: msgInfo
}).then((m) => {
msgInfo.loadStatus = 'ok'; msgInfo.loadStatus = 'ok';
msgInfo.id = m.id; msgInfo.id = m.id;
this.isReceipt = false; this.isReceipt = false;
@ -463,11 +451,7 @@
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data); msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt msgInfo.receipt = this.isReceipt
this.$http({ this.sendMessageRequest(msgInfo).then((m) => {
url: this.messageAction,
method: 'POST',
data: msgInfo
}).then((m) => {
msgInfo.loadStatus = 'ok'; msgInfo.loadStatus = 'ok';
msgInfo.id = m.id; msgInfo.id = m.id;
this.isReceipt = false; this.isReceipt = false;
@ -632,6 +616,32 @@
let px = info.windowWidth * rpx / 750; let px = info.windowWidth * rpx / 750;
return Math.floor(rpx); return Math.floor(rpx);
}, },
sendMessageRequest(msgInfo){
return new Promise((resolve,reject)=>{
// ""
this.reqQueue.push({msgInfo,resolve,reject});
this.processReqQueue();
})
},
processReqQueue(){
if (this.reqQueue.length && !this.isSending) {
this.isSending = true;
const reqData = this.reqQueue.shift();
this.$http({
url: this.messageAction,
method: 'post',
data: reqData.msgInfo
}).then((res)=>{
reqData.resolve(res)
}).catch((e)=>{
reqData.reject(e)
}).finally(()=>{
this.isSending = false;
//
this.processReqQueue();
})
}
},
generateId(){ generateId(){
// id // id
return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000)); return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));

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

@ -1,5 +1,5 @@
<template> <template>
<el-scrollbar v-show="show" ref="scrollBox" class="group-member-choose" <el-scrollbar v-show="show&&showMembers.length" ref="scrollBox" class="group-member-choose"
:style="{'left':pos.x+'px','top':pos.y-300+'px'}"> :style="{'left':pos.x+'px','top':pos.y-300+'px'}">
<div v-for="(member,idx) in showMembers" :key="member.id"> <div v-for="(member,idx) in showMembers" :key="member.id">
<chat-group-member :member="member" :height="40" :active='activeIdx==idx' <chat-group-member :member="member" :height="40" :active='activeIdx==idx'
@ -56,10 +56,6 @@
} }
}) })
this.activeIdx = this.showMembers.length > 0 ? 0: -1; this.activeIdx = this.showMembers.length > 0 ? 0: -1;
console.log(this.showMembers.length)
if(this.showMembers.length == 0){
this.close();
}
}, },
open(pos) { open(pos) {
this.show = true; this.show = true;

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

@ -36,7 +36,7 @@
</file-upload> </file-upload>
</div> </div>
<div title="发送文件"> <div title="发送文件">
<file-upload :action="'/file/upload'" :maxSize="10 * 1024 * 1024" <file-upload ref="fileUpload" :action="'/file/upload'" :maxSize="10 * 1024 * 1024"
@before="onFileBefore" @success="onFileSuccess" @fail="onFileFail"> @before="onFileBefore" @success="onFileSuccess" @fail="onFileFail">
<i class="el-icon-wallet"></i> <i class="el-icon-wallet"></i>
</file-upload> </file-upload>
@ -58,23 +58,10 @@
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div> <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div> </div>
<div class="send-content-area"> <div class="send-content-area">
<div contenteditable="true" v-show="!sendImageUrl" ref="editBox" class="send-text-area" <ChatInput :ownerId="group.ownerId" ref="chatInputEditor" :group-members="groupMembers"
:disabled="lockMessage" @paste.prevent="onEditorPaste" @submit="sendMessage" />
@compositionstart="onEditorCompositionStart"
@compositionend="onEditorCompositionEnd" @input="onEditorInput"
:placeholder="placeholder" @blur="onEditBoxBlur()" @keyup.down="onKeyDown"
@keyup.up.prevent="onKeyUp" @keydown.enter.prevent="onKeyEnter">x
</div>
<div v-show="sendImageUrl" class="send-image-area">
<div class="send-image-box">
<img class="send-image" :src="sendImageUrl" />
<span class="send-image-close el-icon-close" title="删除"
@click="removeSendImage()"></span>
</div>
</div>
<div class="send-btn-area"> <div class="send-btn-area">
<el-button type="primary" size="small" @click="sendMessage()">发送</el-button> <el-button type="primary" size="small" @click="notifySend()">发送</el-button>
</div> </div>
</div> </div>
</el-footer> </el-footer>
@ -86,8 +73,6 @@
</el-container> </el-container>
</el-main> </el-main>
<emotion ref="emoBox" @emotion="onEmotion"></Emotion> <emotion ref="emoBox" @emotion="onEmotion"></Emotion>
<chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers" :search-text="atSearchText"
@select="onAtSelect"></chat-at-box>
<chat-record :visible="showRecord" @close="closeRecordBox" @send="onSendRecord"></chat-record> <chat-record :visible="showRecord" @close="closeRecordBox" @send="onSendRecord"></chat-record>
<group-member-selector ref="rtcSel" :groupId="group.id" @complete="onInviteOk"></group-member-selector> <group-member-selector ref="rtcSel" :groupId="group.id" @complete="onInviteOk"></group-member-selector>
<rtc-group-join ref="rtcJoin" :groupId="group.id"></rtc-group-join> <rtc-group-join ref="rtcJoin" :groupId="group.id"></rtc-group-join>
@ -107,11 +92,13 @@
import ChatAtBox from "./ChatAtBox.vue" import ChatAtBox from "./ChatAtBox.vue"
import GroupMemberSelector from "../group/GroupMemberSelector.vue" import GroupMemberSelector from "../group/GroupMemberSelector.vue"
import RtcGroupJoin from "../rtc/RtcGroupJoin.vue" import RtcGroupJoin from "../rtc/RtcGroupJoin.vue"
import ChatInput from "./ChatInput";
export default { export default {
name: "chatPrivate", name: "chatPrivate",
components: { components: {
ChatInput,
ChatMessageItem, ChatMessageItem,
FileUpload, FileUpload,
ChatGroupSide, ChatGroupSide,
@ -140,11 +127,9 @@
showSide: false, // showSide: false, //
showHistory: false, // showHistory: false, //
lockMessage: false, // lockMessage: false, //
showMinIdx: 0, // showMinIdx showMinIdx: 0, // showMinIdx
atSearchText: "", reqQueue: [],
focusNode: null, // isSending : false
focusOffset: null, //
zhLock: false //
} }
}, },
methods: { methods: {
@ -154,7 +139,7 @@
}, },
closeRefBox() { closeRefBox() {
this.$refs.emoBox.close(); this.$refs.emoBox.close();
this.$refs.atBox.close(); // this.$refs.atBox.close();
}, },
onCall(type) { onCall(type) {
if (type == this.$enums.MESSAGE_TYPE.ACT_RT_VOICE) { if (type == this.$enums.MESSAGE_TYPE.ACT_RT_VOICE) {
@ -163,169 +148,18 @@
this.showPrivateVideo('video'); this.showPrivateVideo('video');
} }
}, },
onKeyDown() {
console.log("onKeyDown")
if (this.$refs.atBox.show) {
this.$refs.atBox.moveDown()
}
},
onKeyUp() {
if (this.$refs.atBox.show) {
this.$refs.atBox.moveUp()
}
},
onKeyEnter() {
if (this.$refs.atBox.show) {
// ,
this.focusOffset += this.atSearchText.length;
this.$refs.atBox.select();
} else {
this.sendMessage();
}
},
onEditBoxBlur() {
let selection = window.getSelection()
// (emoji)
this.focusNode = selection.focusNode;
this.focusOffset = selection.focusOffset;
},
onEditorInput(e, e2, e3) {
// @
if (this.chat.type == "GROUP" && !this.zhLock) {
console.log("onEditorInput")
if (e.inputType === "deleteContentBackward" && !this.atSearchText) {
this.$refs.atBox.close();
}
if (e.data == '@') {
//
this.showAtBox(e);
} else if(this.$refs.atBox.show){
let selection = window.getSelection()
let range = selection.getRangeAt(0)
this.focusNode = selection.focusNode;
// @
let stIdx = this.focusNode.textContent.lastIndexOf('@');
this.atSearchText = this.focusNode.textContent.substring(stIdx + 1);
}
}
this.refreshPlaceHolder();
},
onEditorCompositionStart() {
this.zhLock = true;
},
onEditorCompositionEnd(e) {
this.zhLock = false;
this.onEditorInput(e);
},
showAtBox(e) {
this.atSearchText = "";
let selection = window.getSelection()
let range = selection.getRangeAt(0)
//
this.focusNode = selection.focusNode;
this.focusOffset = selection.focusOffset;
//
let pos = range.getBoundingClientRect();
this.$refs.atBox.open({
x: pos.x,
y: pos.y
})
},
onAtSelect(member) {
let range = window.getSelection().getRangeAt(0)
// @xx
range.setStart(this.focusNode, this.focusOffset - 1 - this.atSearchText.length)
range.setEnd(this.focusNode, this.focusOffset)
range.deleteContents()
//
let element = document.createElement('SPAN')
element.className = "at"
element.dataset.id = member.userId;
element.contentEditable = 'false'
element.innerText = `@${member.showNickName}`
range.insertNode(element)
//
range.collapse()
//
let textNode = document.createTextNode('\u00A0');
range.insertNode(textNode)
range.collapse()
this.atSearchText = "";
this.$refs.editBox.focus()
},
onSwitchReceipt() { onSwitchReceipt() {
this.isReceipt = !this.isReceipt; this.isReceipt = !this.isReceipt;
this.refreshPlaceHolder(); this.refreshPlaceHolder();
}, },
createSendText() {
let sendText = this.isReceipt ? "【回执消息】" : "";
this.$refs.editBox.childNodes.forEach((node) => {
if (node.nodeName == "#text") {
sendText += this.html2Escape(node.textContent);
} else if (node.nodeName == "SPAN") {
sendText += node.innerHTML;
} else if (node.nodeName == "IMG") {
sendText += node.dataset.code;
}
})
return sendText;
},
html2Escape(strHtml) {
return strHtml.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
} [c];
});
},
createAtUserIds() {
let ids = [];
this.$refs.editBox.childNodes.forEach((node) => {
if (node.nodeName == "SPAN") {
ids.push(node.dataset.id);
}
})
return ids;
},
onEditorPaste(e) {
let txt = e.clipboardData.getData('Text')
if (typeof(txt) == 'string') {
let range = window.getSelection().getRangeAt(0)
let textNode = document.createTextNode(txt);
range.insertNode(textNode)
range.collapse();
}
let items = (e.clipboardData || window.clipboardData).items
if (items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
this.sendImageFile = file;
this.sendImageUrl = URL.createObjectURL(file);
}
}
}
},
removeSendImage() {
this.sendImageUrl = "";
this.sendImageFile = null;
},
onImageSuccess(data, file) { onImageSuccess(data, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data); msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt; msgInfo.receipt = this.isReceipt;
this.$http({ this.sendMessageRequest(msgInfo).then((id) => {
url: this.messageAction,
method: 'post',
data: msgInfo
}).then((m) => {
msgInfo.loadStatus = 'ok'; msgInfo.loadStatus = 'ok';
msgInfo.id = m.id; msgInfo.id = id;
this.isReceipt = false; this.isReceipt = false;
this.refreshPlaceHolder();
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}) })
}, },
@ -373,11 +207,7 @@
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data); msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt msgInfo.receipt = this.isReceipt
this.$http({ this.sendMessageRequest(msgInfo).then((m) => {
url: this.messageAction,
method: 'post',
data: msgInfo
}).then((m) => {
msgInfo.loadStatus = 'ok'; msgInfo.loadStatus = 'ok';
msgInfo.id = m.id; msgInfo.id = m.id;
this.isReceipt = false; this.isReceipt = false;
@ -445,21 +275,7 @@
}) })
}, },
onEmotion(emoText) { onEmotion(emoText) {
// this.$refs.chatInputEditor.insertEmoji(emoText);
this.$refs.editBox.focus();
let range = window.getSelection().getRangeAt(0);
//
range.setStart(this.focusNode, this.focusOffset)
let element = document.createElement('IMG')
element.className = "emo"
element.dataset.code = emoText;
element.contentEditable = 'true'
element.setAttribute("src", this.$emo.textToUrl(emoText));
//
range.insertNode(element)
//
range.collapse()
}, },
showRecordBox() { showRecordBox() {
this.showRecord = true; this.showRecord = true;
@ -489,7 +305,6 @@
let ids = [this.mine.id]; let ids = [this.mine.id];
let maxChannel = this.$store.state.configStore.webrtc.maxChannel; let maxChannel = this.$store.state.configStore.webrtc.maxChannel;
this.$refs.rtcSel.open(maxChannel, ids, ids); this.$refs.rtcSel.open(maxChannel, ids, ids);
} }
}) })
@ -531,24 +346,19 @@
} }
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
this.$http({ this.sendMessageRequest(msgInfo).then((m) => {
url: this.messageAction,
method: 'post',
data: msgInfo
}).then((m) => {
m.selfSend = true; m.selfSend = true;
this.$store.commit("insertMessage", m); this.$store.commit("insertMessage", m);
// //
this.moveChatToTop(); this.moveChatToTop();
// //
this.$refs.editBox.focus(); this.$refs.chatInputEditor.focus();
// //
this.scrollToBottom(); this.scrollToBottom();
// //
this.showRecord = false; this.showRecord = false;
this.isReceipt = false; this.isReceipt = false;
this.refreshPlaceHolder(); this.refreshPlaceHolder();
}) })
}, },
fillTargetId(msgInfo, targetId) { fillTargetId(msgInfo, targetId) {
@ -558,70 +368,87 @@
msgInfo.recvId = targetId; msgInfo.recvId = targetId;
} }
}, },
sendMessage() { notifySend() {
if (this.sendImageFile) { this.$refs.chatInputEditor.submit();
this.sendImageMessage();
} else {
this.sendTextMessage();
}
//
this.readedMessage()
},
sendImageMessage() {
let file = this.sendImageFile;
this.onImageBefore(this.sendImageFile);
let formData = new FormData()
formData.append('file', file)
this.$http.post("/image/upload", formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((data) => {
this.onImageSuccess(data, file);
}).catch((res) => {
this.onImageSuccess(res, file);
})
this.sendImageFile = null;
this.sendImageUrl = "";
this.$nextTick(() => this.$refs.editBox.focus());
this.scrollToBottom();
}, },
sendTextMessage() { async sendMessage(fullList) {
let sendText = this.createSendText(); this.resetEditor();
if (!sendText.trim()) { this.readedMessage();
return let sendText = this.isReceipt ? "【回执消息】" : "";
} let promiseList = [];
this.$refs.editBox.cle for (let i = 0; i < fullList.length; i++) {
let msgInfo = { let msg = fullList[i];
content: sendText, switch (msg.type) {
type: 0 case "text":
} await this.sendTextMessage(sendText + msg.content,msg.atUserIds);
// id break;
this.fillTargetId(msgInfo, this.chat.targetId); case "image":
// @ await this.sendImageMessage(msg.content.file);
if (this.chat.type == "GROUP") { break;
msgInfo.atUserIds = this.createAtUserIds(); case "file":
msgInfo.receipt = this.isReceipt; await this.sendFileMessage(msg.content.file);
break;
}
} }
this.lockMessage = true; },
this.$http({ sendImageMessage(file) {
url: this.messageAction, return new Promise((resolve,reject)=>{
method: 'post', this.onImageBefore(file);
data: msgInfo let formData = new FormData()
}).then((m) => { formData.append('file', file)
m.selfSend = true; this.$http.post("/image/upload", formData, {
this.$store.commit("insertMessage", m); headers: {
// 'Content-Type': 'multipart/form-data'
this.moveChatToTop(); }
}).finally(() => { }).then((data) => {
// this.onImageSuccess(data, file);
this.lockMessage = false; resolve();
}).catch((res) => {
this.onImageFail(res, file);
reject();
})
this.$nextTick(() => this.$refs.chatInputEditor.focus());
this.scrollToBottom(); this.scrollToBottom();
this.resetEditor(); });
this.isReceipt = false; },
this.refreshPlaceHolder(); sendTextMessage(sendText,atUserIds) {
return new Promise((resolve,reject)=>{
if (!sendText.trim()) {
reject();
}
let msgInfo = {
content: sendText,
type: 0
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
// @
if (this.chat.type == "GROUP") {
msgInfo.atUserIds = atUserIds;
msgInfo.receipt = this.isReceipt;
}
this.lockMessage = true;
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.$store.commit("insertMessage", m);
//
this.moveChatToTop();
}).finally(() => {
//
this.scrollToBottom();
this.isReceipt = false;
resolve();
});
}); });
},
sendFileMessage(file) {
return new Promise((resolve,reject)=>{
let check = this.$refs.fileUpload.beforeUpload(file);
if (check) {
this.$refs.fileUpload.onFileUpload({ file });
}
})
}, },
deleteMessage(msgInfo) { deleteMessage(msgInfo) {
this.$confirm('确认删除消息?', '删除消息', { this.$confirm('确认删除消息?', '删除消息', {
@ -686,7 +513,6 @@
this.group = group; this.group = group;
this.$store.commit("updateChatFromGroup", group); this.$store.commit("updateChatFromGroup", group);
this.$store.commit("updateGroup", group); this.$store.commit("updateGroup", group);
}); });
this.$http({ this.$http({
@ -725,11 +551,10 @@
} }
}, },
resetEditor() { resetEditor() {
this.sendImageUrl = "";
this.sendImageFile = null;
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.editBox.innerHTML = ""; this.$refs.chatInputEditor.clear();
this.$refs.editBox.focus(); this.$refs.chatInputEditor.focus();
}); });
}, },
scrollToBottom() { scrollToBottom() {
@ -747,6 +572,32 @@
this.placeholder = "聊点什么吧~"; this.placeholder = "聊点什么吧~";
} }
}, },
sendMessageRequest(msgInfo){
return new Promise((resolve,reject)=>{
// ""
this.reqQueue.push({msgInfo,resolve,reject});
this.processReqQueue();
})
},
processReqQueue(){
if (this.reqQueue.length && !this.isSending) {
this.isSending = true;
const reqData = this.reqQueue.shift();
this.$http({
url: this.messageAction,
method: 'post',
data: reqData.msgInfo
}).then((res)=>{
reqData.resolve(res)
}).catch((e)=>{
reqData.reject(e)
}).finally(()=>{
this.isSending = false;
//
this.processReqQueue();
})
}
},
generateId() { generateId() {
// id // id
return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000)); return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));
@ -776,7 +627,6 @@
} }
return this.chat.messages.length; return this.chat.messages.length;
} }
}, },
watch: { watch: {
chat: { chat: {
@ -829,6 +679,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
background: #f8f8f8; background: #f8f8f8;
border: #dddddd solid 1px;
.el-header { .el-header {
padding: 3px; padding: 3px;
@ -844,13 +695,12 @@
line-height: 50px; line-height: 50px;
font-size: 25px; font-size: 25px;
cursor: pointer; cursor: pointer;
} }
} }
.im-chat-main { .im-chat-main {
padding: 0; padding: 0;
background-color: white; background-color: #f8f8f8;
.im-chat-box { .im-chat-box {
>ul { >ul {
@ -877,26 +727,33 @@
box-sizing: border-box; box-sizing: border-box;
border-top: #ccc solid 1px; border-top: #ccc solid 1px;
padding: 2px; padding: 2px;
background-color: #E8F2FF; background-color: #e8f2ff;
>div { >div {
font-size: 22px; font-size: 22px;
cursor: pointer; cursor: pointer;
color: black; color: black;
line-height: 34px; line-height: 30px;
width: 34px; width: 30px;
height: 34px; height: 30px;
text-align: center; text-align: center;
border-radius: 3px; border-radius: 3px;
margin: 3px;
&:hover { &:hover {
color: black; color: black;
} }
&.chat-tool-active { &.chat-tool-active {
background: #ddd; font-weight: 600;
color: #195ee2;
background-color: #ddd;
} }
} }
>div:hover {
color: #949494;
}
} }
.send-content-area { .send-content-area {
@ -914,10 +771,10 @@
resize: none; resize: none;
font-size: 16px; font-size: 16px;
color: black; color: black;
outline-color: rgba(83, 160, 231, 0.61); outline: none;
text-align: left; text-align: left;
line-height: 30 px; line-height: 30px;
&:before { &:before {
content: attr(placeholder); content: attr(placeholder);
@ -971,7 +828,6 @@
border: 1px solid #ccc; border: 1px solid #ccc;
} }
} }
} }
.send-btn-area { .send-btn-area {
@ -981,7 +837,6 @@
right: 0; right: 0;
} }
} }
} }
.chat-group-side-box { .chat-group-side-box {

571
im-web/src/components/chat/ChatInput.vue

@ -0,0 +1,571 @@
<template>
<div class="chat-input-area">
<div :class="['edit-chat-container',isEmpty?'':'not-empty']" contenteditable="true" @paste.prevent="onPaste"
@keydown="onKeydown" @compositionstart="compositionFlag=true" @compositionend="onCompositionEnd"
@input="onEditorInput" @mousedown="onMousedown" ref="content" @blur="onBlur">
</div>
<chat-at-box @select="onAtSelect" :search-text="atSearchText" ref="atBox" :ownerId="ownerId"
:members="groupMembers"></chat-at-box>
</div>
</template>
<script>
import ChatAtBox from "./ChatAtBox";
export default {
name: "ChatInput",
components: { ChatAtBox },
props: {
ownerId: {
type: Number,
},
groupMembers: {
type: Array,
},
},
data() {
return {
imageList: [],
fileList: [],
currentId: 0,
atSearchText: null,
compositionFlag: false,
atIng: false,
isEmpty: true,
changeStored: true,
blurRange: null
}
},
methods: {
onPaste(e) {
this.isEmpty = false;
let txt = e.clipboardData.getData('Text')
let range = window.getSelection().getRangeAt(0)
if (range.startContainer !== range.endContainer || range.startOffset !== range.endOffset) {
range.deleteContents();
}
//
if (txt && typeof(txt) == 'string') {
let textNode = document.createTextNode(txt);
range.insertNode(textNode)
range.collapse();
return;
}
let items = (e.clipboardData || window.clipboardData).items
if (items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
let imagePush = {
fileId: this.generateId(),
file: file,
url: URL.createObjectURL(file)
};
this.imageList[imagePush.fileId] = (imagePush);
let line = this.newLine();
let imageElement = document.createElement('img');
imageElement.className = 'chat-image no-text';
imageElement.src = imagePush.url;
imageElement.dataset.imgId = imagePush.fileId;
line.appendChild(imageElement);
let after = document.createTextNode('\u00A0');
line.appendChild(after);
this.selectElement(after, 1);
} else {
let asFile = items[i].getAsFile();
if (!asFile) {
continue;
}
let filePush = { fileId: this.generateId(), file: asFile };
this.fileList[filePush.fileId] = (filePush)
let line = this.newLine();
let fileElement = this.createFile(filePush);
line.appendChild(fileElement);
let after = document.createTextNode('\u00A0');
line.appendChild(after);
this.selectElement(after, 1);
}
}
}
range.collapse();
},
selectElement(element, endOffset) {
let selection = window.getSelection();
// vuedom
this.$nextTick(() => {
let t1 = document.createRange();
t1.setStart(element, 0);
t1.setEnd(element, endOffset || 0);
if (element.firstChild) {
t1.selectNodeContents(element.firstChild);
}
t1.collapse();
selection.removeAllRanges();
selection.addRange(t1);
//
if (element.focus) {
element.focus();
}
})
},
onCompositionEnd(e) {
this.compositionFlag = false;
this.onEditorInput(e);
},
onKeydown(e) {
if (e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
if (this.atIng) {
console.log('选中at的人')
this.$refs.atBox.select();
return;
}
if (e.ctrlKey) {
let line = this.newLine();
let after = document.createTextNode('\u00A0');
line.appendChild(after);
this.selectElement(line.childNodes[0], 0);
} else {
//
if (this.compositionFlag) {
return;
}
this.submit();
}
return;
}
//
if (e.keyCode === 8) {
console.log("delete")
// dom
setTimeout(() => {
let s = this.$refs.content.innerHTML.trim();
// domdom
console.log(s);
if (s === '' || s === '<br>' || s === '<div>&nbsp;</div>' ) {
// dom
this.empty();
this.isEmpty = true;
this.selectElement(this.$refs.content);
} else {
this.isEmpty = false;
}
})
}
// at
if (this.atIng) {
if (e.keyCode === 38) {
e.preventDefault();
e.stopPropagation();
this.$refs.atBox.moveUp();
}
if (e.keyCode === 40) {
e.preventDefault();
e.stopPropagation();
this.$refs.atBox.moveDown();
}
}
},
onAtSelect(member) {
this.atIng = false;
// @xx
let blurRange = this.blurRange;
let endContainer = blurRange.endContainer
let startOffset = endContainer.data.indexOf("@"+this.atSearchText);
let endOffset = startOffset + this.atSearchText.length + 1;
blurRange.setStart(blurRange.endContainer, startOffset);
blurRange.setEnd(blurRange.endContainer, endOffset);
blurRange.deleteContents()
blurRange.collapse();
console.log("onAtSelect")
this.focus();
//
let element = document.createElement('SPAN')
element.className = "chat-at-user";
element.dataset.id = member.userId;
element.contentEditable = 'false'
element.innerText = `@${member.showNickName}`
blurRange.insertNode(element)
//
blurRange.collapse()
//
let textNode = document.createTextNode('\u00A0');
blurRange.insertNode(textNode);
blurRange.collapse()
this.atSearchText = "";
this.selectElement(textNode, 1);
},
onEditorInput(e) {
this.isEmpty = false;
this.changeStored = false;
if (this.$props.groupMembers && !this.compositionFlag) {
let selection = window.getSelection()
let range = selection.getRangeAt(0);
// @
let endContainer = range.endContainer;
let endOffset = range.endOffset;
let textContent = endContainer.textContent;
let startIndex = -1;
for (let i = endOffset; i >= 0; i--) {
if (textContent[i] === '@') {
startIndex = i;
break;
}
}
// at
if (startIndex === -1) {
this.$refs.atBox.close();
return;
}
let endIndex = endOffset;
for (let i = endOffset; i < textContent.length; i++) {
if (textContent[i] === ' ') {
endIndex = i;
break;
}
}
this.atSearchText = textContent.substring(startIndex + 1, endIndex).trim();
//
if (this.atSearchText == '') {
this.showAtBox(e)
}
}
},
onBlur(e) {
this.updateRange();
},
onMousedown() {
if (this.atIng) {
this.$refs.atBox.close();
this.atIng = false;
}
},
insertEmoji(emojiText) {
let emojiElement = document.createElement('img');
emojiElement.className = 'chat-emoji no-text';
emojiElement.dataset.emojiCode = emojiText;
emojiElement.src = this.$emo.textToUrl(emojiText);
let blurRange = this.blurRange;
if (!blurRange) {
this.focus();
this.updateRange();
blurRange = this.blurRange;
}
if (blurRange.startContainer !== blurRange.endContainer || blurRange.startOffset !== blurRange.endOffset) {
blurRange.deleteContents();
}
blurRange.insertNode(emojiElement);
blurRange.collapse()
let textNode = document.createTextNode('\u00A0');
blurRange.insertNode(textNode)
blurRange.collapse()
this.selectElement(textNode);
this.updateRange();
this.isEmpty = false;
},
generateId() {
return this.currentId++;
},
createFile(filePush) {
let file = filePush.file;
let fileId = filePush.fileId;
let container = document.createElement('div');
container.className = 'chat-file-container no-text';
container.contentEditable = 'false';
container.dataset.fileId = fileId;
let left = document.createElement('div');
left.className = 'file-position-left';
container.appendChild(left);
let icon = document.createElement('div');
icon.className = 'el-icon-document';
left.appendChild(icon);
let right = document.createElement('div');
right.className = 'file-position-right';
container.appendChild(right);
let fileName = document.createElement('div');
fileName.className = 'file-name';
fileName.innerText = file.name;
let fileSize = document.createElement('div');
fileSize.className = 'file-size';
fileSize.innerText = this.sizeConvert(file.size);
right.appendChild(fileName);
right.appendChild(fileSize);
return container;
},
sizeConvert(len) {
if (len < 1024) {
return len + 'B';
} else if (len < 1024 * 1024) {
return (len / 1024).toFixed(2) + 'KB';
} else if (len < 1024 * 1024 * 1024) {
return (len / 1024 / 1024).toFixed(2) + 'MB';
} else {
return (len / 1024 / 1024 / 1024).toFixed(2) + 'GB';
}
},
updateRange() {
let selection = window.getSelection();
this.blurRange = selection.getRangeAt(0);
},
newLine() {
let selection = window.getSelection();
let range = selection.getRangeAt(0);
let divElement = document.createElement('div');
let endContainer = range.endContainer;
let parentElement = endContainer.parentElement;
if (parentElement.parentElement === this.$refs.content) {
divElement.innerHTML = endContainer.textContent.substring(range.endOffset).trim();
endContainer.textContent = endContainer.textContent.substring(0, range.endOffset);
// div
parentElement.insertAdjacentElement('afterend', divElement);
} else {
divElement.innerHTML = '';
this.$refs.content.append(divElement);
}
return divElement;
},
clear() {
this.empty();
this.imageList = [];
this.fileList = [];
},
empty() {
this.$refs.content.innerHTML = "";
let line = this.newLine();
let after = document.createTextNode('\u00A0');
line.appendChild(after);
this.$nextTick(()=>this.selectElement(after));
},
showAtBox(e) {
this.atIng = true;
// showtext
// this.atSearchText = "";
let selection = window.getSelection()
let range = selection.getRangeAt(0)
//
let pos = range.getBoundingClientRect();
this.$refs.atBox.open({
x: pos.x,
y: pos.y
})
//
this.updateRange();
},
html2Escape(strHtml) {
return strHtml.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
} [c];
});
},
submit() {
let nodes = this.$refs.content.childNodes;
let fullList = [];
let tempText = '';
let atUserIds = [];
let each = (nodes) => {
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
if (!node) {
continue;
}
if (node.nodeType === 3) {
tempText += this.html2Escape(node.textContent);
continue;
}
let nodeName = node.nodeName.toLowerCase();
if (nodeName === 'script') {
continue;
}
let text = tempText.trim();
if (nodeName === 'img') {
let imgId = node.dataset.imgId;
if (imgId) {
if (text) {
fullList.push({
type: 'text',
content: text,
atUserIds: atUserIds
})
tempText = '';
atUserIds = []
}
fullList.push({
type: 'image',
content: this.imageList[imgId]
})
} else {
let emojiCode = node.dataset.emojiCode;
tempText += emojiCode;
}
} else if (nodeName === 'div') {
let fileId = node.dataset.fileId
//
if (fileId) {
if (text) {
fullList.push({
type: 'text',
content: text,
atUserIds: atUserIds
})
tempText = '';
atUserIds = []
}
fullList.push({
type: 'file',
content: this.fileList[fileId]
})
} else {
tempText += '\n';
each(node.childNodes);
}
} else if (nodeName === 'span') {
if(node.dataset.id){
tempText += node.innerHTML;
atUserIds.push(node.dataset.id)
}else {
tempText += node.outerHtml;
}
}
}
}
each(nodes)
let text = tempText.trim();
if (text !== '') {
fullList.push({
type: 'text',
content: text,
atUserIds: atUserIds
})
}
this.$emit('submit', fullList);
},
focus() {
this.$refs.content.focus();
}
}
}
</script>
<style lang="scss">
.chat-input-area {
width: 100%;
height: 100%;
position: relative;
.edit-chat-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #c3c3c3;
outline: none;
padding: 5px;
line-height: 30px;
font-size: 16px;
text-align: left;
overflow-y: scroll;
// bug
>div:before {
content: "\00a0";
font-size: 14px;
position: absolute;
top: 0;
left: 0;
}
.chat-image {
display: block;
max-width: 200px;
max-height: 100px;
border: 1px solid #e6e6e6;
cursor: pointer;
}
.chat-emoji {
width: 30px;
height: 30px;
vertical-align: top;
cursor: pointer;
}
.chat-file-container {
max-width: 65%;
padding: 10px;
border: 2px solid #587ff0;
display: flex;
background: #eeeC;
border-radius: 10px;
.file-position-left {
display: flex;
width: 80px;
justify-content: center;
align-items: center;
.el-icon-document {
font-size: 40px;
text-align: center;
color: #d42e07;
}
}
.file-position-right {
flex: 1;
.file-name {
font-size: 16px;
font-weight: 600;
color: #66b1ff;
}
.file-size {
font-size: 14px;
font-weight: 600;
color: black;
}
}
}
.chat-at-user {
color: #00f;
font-weight: 600;
border-radius: 3px;
}
}
.edit-chat-container>div:nth-of-type(1):after {
content: '请输入消息(按Ctrl+Enter键换行)';
color: gray;
}
.edit-chat-container.not-empty>div:nth-of-type(1):after {
content: none;
}
}
</style>

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

@ -67,10 +67,8 @@
<span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span> <span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span>
<span v-else>{{msgInfo.readedCount}}人已读</span> <span v-else>{{msgInfo.readedCount}}人已读</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@close="rightMenu.show = false" @select="onSelectMenu"></right-menu> @close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
@ -129,7 +127,6 @@
} }
} }
} }
}, },
methods: { methods: {
onSendFail() { onSendFail() {
@ -200,7 +197,7 @@
} }
return items; return items;
}, },
isAction(){ isAction() {
return this.$msgType.isAction(this.msgInfo.type); return this.$msgType.isAction(this.msgInfo.type);
}, },
isNormal() { isNormal() {
@ -280,6 +277,7 @@
position: absolute; position: absolute;
left: -10px; left: -10px;
top: 13px; top: 13px;
z-index: -1;
width: 0; width: 0;
height: 0; height: 0;
border-style: solid dashed dashed; border-style: solid dashed dashed;
@ -407,6 +405,13 @@
color: #329432; color: #329432;
} }
} }
.chat-at-user {
padding: 2px 5px;
border-radius: 3px;
font-weight: 600;
cursor: pointer;
}
} }
} }
@ -442,7 +447,7 @@
background-color: rgb(88, 127, 240); background-color: rgb(88, 127, 240);
color: #fff; color: #fff;
vertical-align: top; vertical-align: top;
&:after { &:after {
left: auto; left: auto;
right: -10px; right: -10px;

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

@ -126,16 +126,22 @@
}) })
}, },
pullPrivateOfflineMessage(minId) { pullPrivateOfflineMessage(minId) {
this.$store.commit("loadingPrivateMsg", true)
this.$http({ this.$http({
url: "/message/private/pullOfflineMessage?minId=" + minId, url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'get' method: 'GET'
}); }).catch(() => {
this.$store.commit("loadingPrivateMsg", false)
})
}, },
pullGroupOfflineMessage(minId) { pullGroupOfflineMessage(minId) {
this.$store.commit("loadingGroupMsg", true)
this.$http({ this.$http({
url: "/message/group/pullOfflineMessage?minId=" + minId, url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'get' method: 'GET'
}); }).catch(() => {
this.$store.commit("loadingGroupMsg", false)
})
}, },
handlePrivateMessage(msg) { handlePrivateMessage(msg) {
// //

Loading…
Cancel
Save