ChatBox.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. <template>
  2. <div class="chat-box" @click="closeRefBox()" @mousemove="readedMessage()">
  3. <el-container>
  4. <el-header height="50px">
  5. <span>{{ title }}</span>
  6. <span title="群聊信息" v-show="isGroup" class="btn-side el-icon-more" @click="showSide = !showSide"></span>
  7. </el-header>
  8. <el-main style="padding: 0;">
  9. <el-container>
  10. <el-container class="content-box">
  11. <el-main class="im-chat-main" id="chatScrollBox" @scroll="onScroll">
  12. <div class="im-chat-box">
  13. <div v-for="(msgInfo, idx) in showMessages" :key="showMinIdx + idx">
  14. <chat-message-item @call="onCall(msgInfo.type)" :mine="msgInfo.sendId == mine.id"
  15. :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" :msgInfo="msgInfo"
  16. :groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage">
  17. </chat-message-item>
  18. </div>
  19. </div>
  20. </el-main>
  21. <div v-if="!isInBottom" class="scroll-to-bottom" @click="scrollToBottom">
  22. {{ newMessageSize > 0 ? newMessageSize + '条新消息' : '回到底部' }}
  23. </div>
  24. <el-footer height="220px" class="im-chat-footer">
  25. <div class="chat-tool-bar">
  26. <div title="表情" class="icon iconfont icon-emoji" ref="emotion"
  27. @click.stop="showEmotionBox()">
  28. </div>
  29. <div title="发送图片">
  30. <file-upload :action="'/image/upload'" :maxSize="5 * 1024 * 1024"
  31. :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp', 'image/gif']"
  32. @before="onImageBefore" @success="onImageSuccess" @fail="onImageFail">
  33. <i class="el-icon-picture-outline"></i>
  34. </file-upload>
  35. </div>
  36. <div title="发送文件">
  37. <file-upload ref="fileUpload" :action="'/file/upload'" :maxSize="10 * 1024 * 1024"
  38. @before="onFileBefore" @success="onFileSuccess" @fail="onFileFail">
  39. <i class="el-icon-wallet"></i>
  40. </file-upload>
  41. </div>
  42. <div title="回执消息" v-show="isGroup && memberSize <= 500"
  43. class="icon iconfont icon-receipt" :class="isReceipt ? 'chat-tool-active' : ''"
  44. @click="onSwitchReceipt">
  45. </div>
  46. <div title="发送语音" class="el-icon-microphone" @click="showRecordBox()">
  47. </div>
  48. <div title="语音通话" v-show="isPrivate" class="el-icon-phone-outline"
  49. @click="showPrivateVideo('voice')">
  50. </div>
  51. <div title="语音通话" v-show="isGroup" class="el-icon-phone-outline"
  52. @click="onGroupVideo()">
  53. </div>
  54. <div title="视频通话" v-show="isPrivate" class="el-icon-video-camera"
  55. @click="showPrivateVideo('video')">
  56. </div>
  57. <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
  58. </div>
  59. <div class="send-content-area">
  60. <ChatInput :ownerId="group.ownerId" ref="chatInputEditor" :group-members="groupMembers"
  61. @submit="sendMessage" />
  62. <div class="send-btn-area">
  63. <el-button type="primary" icon="el-icon-s-promotion"
  64. @click="notifySend()">发送</el-button>
  65. </div>
  66. </div>
  67. </el-footer>
  68. </el-container>
  69. <el-aside class="side-box" width="320px" v-if="showSide">
  70. <chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)">
  71. </chat-group-side>
  72. </el-aside>
  73. </el-container>
  74. </el-main>
  75. <emotion ref="emoBox" @emotion="onEmotion"></Emotion>
  76. <chat-record :visible="showRecord" @close="closeRecordBox" @send="onSendRecord"></chat-record>
  77. <group-member-selector ref="rtcSel" :group="group" @complete="onInviteOk"></group-member-selector>
  78. <rtc-group-join ref="rtcJoin" :groupId="group.id"></rtc-group-join>
  79. <chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group"
  80. :groupMembers="groupMembers" @close="closeHistoryBox"></chat-history>
  81. </el-container>
  82. </div>
  83. </template>
  84. <script>
  85. import ChatGroupSide from "./ChatGroupSide.vue";
  86. import ChatMessageItem from "./ChatMessageItem.vue";
  87. import FileUpload from "../common/FileUpload.vue";
  88. import Emotion from "../common/Emotion.vue";
  89. import ChatRecord from "./ChatRecord.vue";
  90. import ChatHistory from "./ChatHistory.vue";
  91. import ChatAtBox from "./ChatAtBox.vue"
  92. import GroupMemberSelector from "../group/GroupMemberSelector.vue"
  93. import RtcGroupJoin from "../rtc/RtcGroupJoin.vue"
  94. import ChatInput from "./ChatInput";
  95. export default {
  96. name: "chatPrivate",
  97. components: {
  98. ChatInput,
  99. ChatMessageItem,
  100. FileUpload,
  101. ChatGroupSide,
  102. Emotion,
  103. ChatRecord,
  104. ChatHistory,
  105. ChatAtBox,
  106. GroupMemberSelector,
  107. RtcGroupJoin
  108. },
  109. props: {
  110. chat: {
  111. type: Object
  112. }
  113. },
  114. data() {
  115. return {
  116. userInfo: {},
  117. group: {},
  118. groupMembers: [],
  119. sendImageUrl: "",
  120. sendImageFile: "",
  121. placeholder: "",
  122. isReceipt: true,
  123. showRecord: false, // 是否显示语音录制弹窗
  124. showSide: false, // 是否显示群聊信息栏
  125. showHistory: false, // 是否显示历史聊天记录
  126. lockMessage: false, // 是否锁定发送,
  127. showMinIdx: 0, // 下标低于showMinIdx的消息不显示,否则页面会很卡置
  128. reqQueue: [], // 等待发送的请求队列
  129. isSending: false, // 是否正在发消息
  130. isInBottom: false, // 滚动条是否在底部
  131. newMessageSize: 0 // 滚动条不在底部时新的消息数量
  132. }
  133. },
  134. methods: {
  135. moveChatToTop() {
  136. let chatIdx = this.chatStore.findChatIdx(this.chat);
  137. this.chatStore.moveTop(chatIdx);
  138. },
  139. closeRefBox() {
  140. this.$refs.emoBox.close();
  141. // this.$refs.atBox.close();
  142. },
  143. onCall(type) {
  144. if (type == this.$enums.MESSAGE_TYPE.ACT_RT_VOICE) {
  145. this.showPrivateVideo('voice');
  146. } else if (type == this.$enums.MESSAGE_TYPE.ACT_RT_VIDEO) {
  147. this.showPrivateVideo('video');
  148. }
  149. },
  150. onSwitchReceipt() {
  151. this.isReceipt = !this.isReceipt;
  152. this.refreshPlaceHolder();
  153. },
  154. onImageSuccess(data, file) {
  155. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  156. msgInfo.content = JSON.stringify(data);
  157. msgInfo.receipt = this.isReceipt;
  158. this.sendMessageRequest(msgInfo).then((m) => {
  159. msgInfo.loadStatus = 'ok';
  160. msgInfo.id = m.id;
  161. this.isReceipt = false;
  162. this.chatStore.insertMessage(msgInfo, file.chat);
  163. })
  164. },
  165. onImageFail(e, file) {
  166. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  167. msgInfo.loadStatus = 'fail';
  168. this.chatStore.insertMessage(msgInfo, file.chat);
  169. },
  170. onImageBefore(file) {
  171. // 被封禁提示
  172. if (this.isBanned) {
  173. this.showBannedTip();
  174. return;
  175. }
  176. let url = URL.createObjectURL(file);
  177. let data = {
  178. originUrl: url,
  179. thumbUrl: url
  180. }
  181. let msgInfo = {
  182. id: 0,
  183. tmpId: this.generateId(),
  184. fileId: file.uid,
  185. sendId: this.mine.id,
  186. content: JSON.stringify(data),
  187. sendTime: new Date().getTime(),
  188. selfSend: true,
  189. type: 1,
  190. readedCount: 0,
  191. loadStatus: "loading",
  192. status: this.$enums.MESSAGE_STATUS.UNSEND
  193. }
  194. // 填充对方id
  195. this.fillTargetId(msgInfo, this.chat.targetId);
  196. // 插入消息
  197. this.chatStore.insertMessage(msgInfo, this.chat);
  198. // 会话置顶
  199. this.moveChatToTop();
  200. // 滚动到底部
  201. this.scrollToBottom();
  202. // 借助file对象保存
  203. file.msgInfo = msgInfo;
  204. file.chat = this.chat;
  205. },
  206. onFileSuccess(url, file) {
  207. let data = {
  208. name: file.name,
  209. size: file.size,
  210. url: url
  211. }
  212. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  213. msgInfo.content = JSON.stringify(data);
  214. msgInfo.receipt = this.isReceipt
  215. this.sendMessageRequest(msgInfo).then((m) => {
  216. msgInfo.loadStatus = 'ok';
  217. msgInfo.id = m.id;
  218. this.isReceipt = false;
  219. this.refreshPlaceHolder();
  220. this.chatStore.insertMessage(msgInfo, file.chat);
  221. })
  222. },
  223. onFileFail(e, file) {
  224. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  225. msgInfo.loadStatus = 'fail';
  226. this.chatStore.insertMessage(msgInfo, file.chat);
  227. },
  228. onFileBefore(file) {
  229. // 被封禁提示
  230. if (this.isBanned) {
  231. this.showBannedTip();
  232. return;
  233. }
  234. let url = URL.createObjectURL(file);
  235. let data = {
  236. name: file.name,
  237. size: file.size,
  238. url: url
  239. }
  240. let msgInfo = {
  241. id: 0,
  242. tmpId: this.generateId(),
  243. sendId: this.mine.id,
  244. content: JSON.stringify(data),
  245. sendTime: new Date().getTime(),
  246. selfSend: true,
  247. type: 2,
  248. loadStatus: "loading",
  249. readedCount: 0,
  250. status: this.$enums.MESSAGE_STATUS.UNSEND
  251. }
  252. // 填充对方id
  253. this.fillTargetId(msgInfo, this.chat.targetId);
  254. // 插入消息
  255. this.chatStore.insertMessage(msgInfo, this.chat);
  256. // 会话置顶
  257. this.moveChatToTop();
  258. // 滚动到底部
  259. this.scrollToBottom();
  260. // 借助file对象透传
  261. file.msgInfo = msgInfo;
  262. file.chat = this.chat;
  263. },
  264. onCloseSide() {
  265. this.showSide = false;
  266. },
  267. onScrollToTop() {
  268. // 多展示10条信息
  269. this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
  270. },
  271. onScroll(e) {
  272. let scrollElement = e.target
  273. let scrollTop = scrollElement.scrollTop
  274. if (scrollTop < 30) { // 在顶部,不滚动的情况
  275. // 多展示20条信息
  276. this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
  277. this.isInBottom = false;
  278. }
  279. // 滚到底部
  280. if (scrollTop + scrollElement.clientHeight >= scrollElement.scrollHeight - 30) {
  281. this.isInBottom = true;
  282. this.newMessageSize = 0;
  283. }
  284. },
  285. showEmotionBox() {
  286. let width = this.$refs.emotion.offsetWidth;
  287. let left = this.$elm.fixLeft(this.$refs.emotion);
  288. let top = this.$elm.fixTop(this.$refs.emotion);
  289. this.$refs.emoBox.open({
  290. x: left + width / 2,
  291. y: top
  292. })
  293. },
  294. onEmotion(emoText) {
  295. this.$refs.chatInputEditor.insertEmoji(emoText);
  296. },
  297. showRecordBox() {
  298. this.showRecord = true;
  299. },
  300. closeRecordBox() {
  301. this.showRecord = false;
  302. },
  303. showPrivateVideo(mode) {
  304. // 检查是否被封禁
  305. if (this.isBanned) {
  306. this.showBannedTip();
  307. return;
  308. }
  309. let rtcInfo = {
  310. mode: mode,
  311. isHost: true,
  312. friend: this.friend,
  313. }
  314. // 通过home.vue打开单人视频窗口
  315. this.$eventBus.$emit("openPrivateVideo", rtcInfo);
  316. },
  317. onGroupVideo() {
  318. // 检查是否被封禁
  319. if (this.isBanned) {
  320. this.showBannedTip();
  321. return;
  322. }
  323. // 邀请成员发起通话
  324. let ids = [this.mine.id];
  325. let maxChannel = this.configStore.webrtc.maxChannel;
  326. this.$refs.rtcSel.open(maxChannel, ids, ids, []);
  327. },
  328. onInviteOk(members) {
  329. if (members.length < 2) {
  330. return;
  331. }
  332. let userInfos = [];
  333. members.forEach(m => {
  334. userInfos.push({
  335. id: m.userId,
  336. nickName: m.showNickName,
  337. headImage: m.headImage,
  338. isCamera: false,
  339. isMicroPhone: true
  340. })
  341. })
  342. let rtcInfo = {
  343. isHost: true,
  344. groupId: this.group.id,
  345. inviterId: this.mine.id,
  346. userInfos: userInfos
  347. }
  348. // 通过home.vue打开多人视频窗口
  349. this.$eventBus.$emit("openGroupVideo", rtcInfo);
  350. },
  351. showHistoryBox() {
  352. this.showHistory = true;
  353. },
  354. closeHistoryBox() {
  355. this.showHistory = false;
  356. },
  357. onSendRecord(data) {
  358. // 检查是否被封禁
  359. if (this.isBanned) {
  360. this.showBannedTip();
  361. return;
  362. }
  363. let msgInfo = {
  364. content: JSON.stringify(data),
  365. type: 3,
  366. receipt: this.isReceipt
  367. }
  368. // 填充对方id
  369. this.fillTargetId(msgInfo, this.chat.targetId);
  370. this.sendMessageRequest(msgInfo).then(m => {
  371. m.selfSend = true;
  372. this.chatStore.insertMessage(m, this.chat);
  373. // 会话置顶
  374. this.moveChatToTop();
  375. // 保持输入框焦点
  376. this.$refs.chatInputEditor.focus();
  377. // 滚动到底部
  378. this.scrollToBottom();
  379. // 关闭录音窗口
  380. this.showRecord = false;
  381. this.isReceipt = false;
  382. this.refreshPlaceHolder();
  383. })
  384. },
  385. fillTargetId(msgInfo, targetId) {
  386. if (this.chat.type == "GROUP") {
  387. msgInfo.groupId = targetId;
  388. } else {
  389. msgInfo.recvId = targetId;
  390. }
  391. },
  392. notifySend() {
  393. this.$refs.chatInputEditor.submit();
  394. },
  395. async sendMessage(fullList) {
  396. this.resetEditor();
  397. this.readedMessage();
  398. // 检查是否被封禁
  399. if (this.isBanned) {
  400. this.showBannedTip();
  401. return;
  402. }
  403. let sendText = this.isReceipt ? "【回执消息】" : "";
  404. fullList.forEach(async msg => {
  405. switch (msg.type) {
  406. case "text":
  407. await this.sendTextMessage(sendText + msg.content, msg.atUserIds);
  408. break;
  409. case "image":
  410. await this.sendImageMessage(msg.content.file);
  411. break;
  412. case "file":
  413. await this.sendFileMessage(msg.content.file);
  414. break;
  415. }
  416. })
  417. },
  418. sendImageMessage(file) {
  419. return new Promise((resolve, reject) => {
  420. this.onImageBefore(file);
  421. let formData = new FormData()
  422. formData.append('file', file)
  423. this.$http.post("/image/upload", formData, {
  424. headers: {
  425. 'Content-Type': 'multipart/form-data'
  426. }
  427. }).then((data) => {
  428. this.onImageSuccess(data, file);
  429. resolve();
  430. }).catch((res) => {
  431. this.onImageFail(res, file);
  432. reject();
  433. })
  434. this.$nextTick(() => this.$refs.chatInputEditor.focus());
  435. this.scrollToBottom();
  436. });
  437. },
  438. sendTextMessage(sendText, atUserIds) {
  439. return new Promise((resolve, reject) => {
  440. if (!sendText.trim()) {
  441. reject();
  442. }
  443. let msgInfo = {
  444. content: sendText,
  445. type: 0
  446. }
  447. // 填充对方id
  448. this.fillTargetId(msgInfo, this.chat.targetId);
  449. // 被@人员列表
  450. if (this.chat.type == "GROUP") {
  451. msgInfo.atUserIds = atUserIds;
  452. msgInfo.receipt = this.isReceipt;
  453. }
  454. this.lockMessage = true;
  455. this.sendMessageRequest(msgInfo).then((m) => {
  456. m.selfSend = true;
  457. this.chatStore.insertMessage(m, this.chat);
  458. // 会话置顶
  459. this.moveChatToTop();
  460. }).finally(() => {
  461. // 解除锁定
  462. this.scrollToBottom();
  463. this.isReceipt = false;
  464. resolve();
  465. });
  466. });
  467. },
  468. sendFileMessage(file) {
  469. return new Promise((resolve, reject) => {
  470. let check = this.$refs.fileUpload.beforeUpload(file);
  471. if (check) {
  472. this.$refs.fileUpload.onFileUpload({ file });
  473. }
  474. })
  475. },
  476. deleteMessage(msgInfo) {
  477. this.$confirm('确认删除消息?', '删除消息', {
  478. confirmButtonText: '确定',
  479. cancelButtonText: '取消',
  480. type: 'warning'
  481. }).then(() => {
  482. this.chatStore.deleteMessage(msgInfo, this.chat);
  483. });
  484. },
  485. recallMessage(msgInfo) {
  486. this.$confirm('确认撤回消息?', '撤回消息', {
  487. confirmButtonText: '确定',
  488. cancelButtonText: '取消',
  489. type: 'warning'
  490. }).then(() => {
  491. let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`
  492. this.$http({
  493. url: url,
  494. method: 'delete'
  495. }).then((m) => {
  496. this.$message.success("消息已撤回");
  497. m.selfSend = true;
  498. this.chatStore.recallMessage(m, this.chat);
  499. })
  500. });
  501. },
  502. readedMessage() {
  503. if (this.chat.unreadCount > 0) {
  504. if (this.isGroup) {
  505. var url = `/message/group/readed?groupId=${this.chat.targetId}`
  506. } else {
  507. url = `/message/private/readed?friendId=${this.chat.targetId}`
  508. }
  509. this.$http({
  510. url: url,
  511. method: 'put'
  512. }).then(() => { })
  513. this.chatStore.resetUnreadCount(this.chat)
  514. }
  515. },
  516. loadReaded(fId) {
  517. this.$http({
  518. url: `/message/private/maxReadedId?friendId=${fId}`,
  519. method: 'get'
  520. }).then((id) => {
  521. this.chatStore.readedMessage({
  522. friendId: fId,
  523. maxId: id
  524. });
  525. });
  526. },
  527. loadGroup(groupId) {
  528. this.$http({
  529. url: `/group/find/${groupId}`,
  530. method: 'get'
  531. }).then((group) => {
  532. this.group = group;
  533. this.chatStore.updateChatFromGroup(group);
  534. this.groupStore.updateGroup(group);
  535. });
  536. this.$http({
  537. url: `/group/members/${groupId}`,
  538. method: 'get'
  539. }).then((groupMembers) => {
  540. this.groupMembers = groupMembers;
  541. });
  542. },
  543. updateFriendInfo() {
  544. if (this.isFriend) {
  545. // store的数据不能直接修改,深拷贝一份store的数据
  546. let friend = JSON.parse(JSON.stringify(this.friend));
  547. friend.headImage = this.userInfo.headImageThumb;
  548. friend.nickName = this.userInfo.nickName;
  549. friend.showNickName = friend.remarkNickName ? friend.remarkNickName : friend.nickName;
  550. this.chatStore.updateChatFromFriend(friend);
  551. this.friendStore.updateFriend(friend);
  552. } else {
  553. this.chatStore.updateChatFromUser(this.userInfo);
  554. }
  555. },
  556. loadFriend(friendId) {
  557. // 获取好友信息
  558. this.$http({
  559. url: `/user/find/${friendId}`,
  560. method: 'GET'
  561. }).then((userInfo) => {
  562. this.userInfo = userInfo;
  563. this.updateFriendInfo();
  564. })
  565. },
  566. showName(msgInfo) {
  567. if (!msgInfo) {
  568. return ""
  569. }
  570. if (this.isGroup) {
  571. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  572. return member ? member.showNickName : msgInfo.sendNickName || "";
  573. } else {
  574. return msgInfo.selfSend ? this.mine.nickName : this.chat.showName
  575. }
  576. },
  577. headImage(msgInfo) {
  578. if (this.isGroup) {
  579. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  580. return member ? member.headImage : "";
  581. } else {
  582. return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
  583. }
  584. },
  585. resetEditor() {
  586. this.$nextTick(() => {
  587. this.$refs.chatInputEditor.clear();
  588. this.$refs.chatInputEditor.focus();
  589. });
  590. },
  591. scrollToBottom() {
  592. this.$nextTick(() => {
  593. let div = document.getElementById("chatScrollBox");
  594. div.scrollTop = div.scrollHeight;
  595. });
  596. },
  597. refreshPlaceHolder() {
  598. if (this.isReceipt) {
  599. this.placeholder = "【回执消息】"
  600. } else if (this.$refs.editBox && this.$refs.editBox.innerHTML) {
  601. this.placeholder = ""
  602. } else {
  603. this.placeholder = "聊点什么吧~";
  604. }
  605. },
  606. sendMessageRequest(msgInfo) {
  607. return new Promise((resolve, reject) => {
  608. // 请求入队列,防止请求"后发先至",导致消息错序
  609. this.reqQueue.push({ msgInfo, resolve, reject });
  610. this.processReqQueue();
  611. })
  612. },
  613. processReqQueue() {
  614. if (this.reqQueue.length && !this.isSending) {
  615. this.isSending = true;
  616. const reqData = this.reqQueue.shift();
  617. this.$http({
  618. url: this.messageAction,
  619. method: 'post',
  620. data: reqData.msgInfo
  621. }).then((res) => {
  622. reqData.resolve(res)
  623. }).catch((e) => {
  624. reqData.reject(e)
  625. }).finally(() => {
  626. this.isSending = false;
  627. // 发送下一条请求
  628. this.processReqQueue();
  629. })
  630. }
  631. },
  632. showBannedTip() {
  633. let msgInfo = {
  634. tmpId: this.generateId(),
  635. sendId: this.mine.id,
  636. sendTime: new Date().getTime(),
  637. type: this.$enums.MESSAGE_TYPE.TIP_TEXT
  638. }
  639. if (this.chat.type == "PRIVATE") {
  640. msgInfo.recvId = this.mine.id
  641. msgInfo.content = "该用户已被管理员封禁,原因:" + this.userInfo.reason
  642. } else {
  643. msgInfo.groupId = this.group.id;
  644. msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason
  645. }
  646. this.chatStore.insertMessage(msgInfo, this.chat);
  647. },
  648. generateId() {
  649. // 生成临时id
  650. return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));
  651. }
  652. },
  653. computed: {
  654. mine() {
  655. return this.userStore.userInfo;
  656. },
  657. isFriend() {
  658. return this.friendStore.isFriend(this.userInfo.id);
  659. },
  660. friend() {
  661. return this.friendStore.findFriend(this.userInfo.id)
  662. },
  663. title() {
  664. let title = this.chat.showName;
  665. if (this.chat.type == "GROUP") {
  666. let size = this.groupMembers.filter(m => !m.quit).length;
  667. title += `(${size})`;
  668. }
  669. return title;
  670. },
  671. messageAction() {
  672. return `/message/${this.chat.type.toLowerCase()}/send`;
  673. },
  674. unreadCount() {
  675. return this.chat.unreadCount;
  676. },
  677. showMessages() {
  678. return this.chat.messages.slice(this.showMinIdx)
  679. },
  680. messageSize() {
  681. if (!this.chat || !this.chat.messages) {
  682. return 0;
  683. }
  684. return this.chat.messages.length;
  685. },
  686. isBanned() {
  687. return (this.chat.type == "PRIVATE" && this.userInfo.isBanned) ||
  688. (this.chat.type == "GROUP" && this.group.isBanned)
  689. },
  690. memberSize() {
  691. return this.groupMembers.filter(m => !m.quit).length;
  692. },
  693. isGroup() {
  694. return this.chat.type == 'GROUP';
  695. },
  696. isPrivate() {
  697. return this.chat.type == 'PRIVATE';
  698. }
  699. },
  700. watch: {
  701. chat: {
  702. handler(newChat, oldChat) {
  703. if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
  704. newChat.targetId != oldChat.targetId)) {
  705. this.userInfo = {}
  706. this.group = {};
  707. this.groupMembers = [];
  708. if (this.chat.type == "GROUP") {
  709. this.loadGroup(this.chat.targetId);
  710. } else {
  711. this.loadFriend(this.chat.targetId);
  712. // 加载已读状态
  713. this.loadReaded(this.chat.targetId)
  714. }
  715. // 滚到底部
  716. this.scrollToBottom();
  717. this.showSide = false;
  718. // 消息已读
  719. this.readedMessage()
  720. // 初始状态只显示30条消息
  721. let size = this.chat.messages.length;
  722. this.showMinIdx = size > 30 ? size - 30 : 0;
  723. // 重置输入框
  724. this.resetEditor();
  725. // 复位回执消息
  726. this.isReceipt = false;
  727. // 更新placeholder
  728. this.refreshPlaceHolder();
  729. }
  730. },
  731. immediate: true
  732. },
  733. messageSize: {
  734. handler(newSize, oldSize) {
  735. if (newSize > oldSize) {
  736. if (this.isInBottom) {
  737. // 拉至底部
  738. this.scrollToBottom();
  739. } else {
  740. // 增加新消息提醒
  741. this.newMessageSize++;
  742. }
  743. }
  744. }
  745. }
  746. },
  747. mounted() {
  748. let div = document.getElementById("chatScrollBox");
  749. div.addEventListener('scroll', this.onScroll)
  750. }
  751. }
  752. </script>
  753. <style lang="scss" scoped>
  754. .chat-box {
  755. position: relative;
  756. width: 100%;
  757. background: #fff;
  758. .el-header {
  759. display: flex;
  760. justify-content: space-between;
  761. padding: 0 12px;
  762. line-height: 50px;
  763. font-size: var(--im-font-size-larger);
  764. border-bottom: var(--im-border);
  765. .btn-side {
  766. position: absolute;
  767. right: 20px;
  768. line-height: 50px;
  769. font-size: 20px;
  770. cursor: pointer;
  771. color: var(--im-text-color-light);
  772. }
  773. }
  774. .content-box {
  775. position: relative;
  776. .im-chat-main {
  777. padding: 0;
  778. background-color: #fff;
  779. .im-chat-box {
  780. >ul {
  781. padding: 0 20px;
  782. li {
  783. list-style-type: none;
  784. }
  785. }
  786. }
  787. }
  788. .scroll-to-bottom {
  789. text-align: right;
  790. position: absolute;
  791. right: 20px;
  792. bottom: 230px;
  793. color: var(--im-color-primary);
  794. font-size: var(--im-font-size);
  795. font-weight: 600;
  796. background: #eee;
  797. padding: 5px 15px;
  798. border-radius: 15px;
  799. cursor: pointer;
  800. z-index: 99;
  801. box-shadow: var(--im-box-shadow-light);
  802. }
  803. .im-chat-footer {
  804. display: flex;
  805. flex-direction: column;
  806. padding: 0;
  807. .chat-tool-bar {
  808. display: flex;
  809. position: relative;
  810. width: 100%;
  811. height: 36px;
  812. text-align: left;
  813. box-sizing: border-box;
  814. border-top: var(--im-border);
  815. padding: 4px 2px 2px 8px;
  816. >div {
  817. font-size: 22px;
  818. cursor: pointer;
  819. line-height: 30px;
  820. width: 30px;
  821. height: 30px;
  822. text-align: center;
  823. border-radius: 2px;
  824. margin-right: 8px;
  825. color: #999;
  826. transition: 0.3s;
  827. &.chat-tool-active {
  828. font-weight: 600;
  829. color: var(--im-color-primary);
  830. background-color: #ddd;
  831. }
  832. }
  833. >div:hover {
  834. color: #333;
  835. }
  836. }
  837. .send-content-area {
  838. position: relative;
  839. display: flex;
  840. flex-direction: column;
  841. height: 100%;
  842. background-color: white !important;
  843. .send-btn-area {
  844. padding: 10px;
  845. position: absolute;
  846. bottom: 4px;
  847. right: 6px;
  848. }
  849. }
  850. }
  851. }
  852. .side-box {
  853. border-left: var(--im-border);
  854. }
  855. }
  856. </style>