ChatBox.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. <template>
  2. <div class="chat-box" @click="closeRefBox()" @mousemove="readedMessage()">
  3. <el-container>
  4. <el-header height="60px">
  5. <span>{{title}}</span>
  6. <span title="群聊信息" v-show="this.chat.type=='GROUP'" class="btn-side el-icon-more"
  7. @click="showSide=!showSide"></span>
  8. </el-header>
  9. <el-main style="padding: 0;">
  10. <el-container>
  11. <el-container class="content-box">
  12. <el-main class="im-chat-main" id="chatScrollBox" @scroll="onScroll">
  13. <div class="im-chat-box">
  14. <ul>
  15. <li v-for="(msgInfo,idx) in chat.messages" :key="idx">
  16. <chat-message-item v-show="idx>=showMinIdx" :mine="msgInfo.sendId == mine.id"
  17. :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
  18. :msgInfo="msgInfo" @delete="deleteMessage" @recall="recallMessage">
  19. </chat-message-item>
  20. </li>
  21. </ul>
  22. </div>
  23. </el-main>
  24. <el-footer height="240px" class="im-chat-footer">
  25. <div class="chat-tool-bar">
  26. <div title="表情" class="icon iconfont icon-biaoqing" 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 :action="'/file/upload'" :maxSize="10*1024*1024" @before="onFileBefore"
  38. @success="onFileSuccess" @fail="onFileFail">
  39. <i class="el-icon-wallet"></i>
  40. </file-upload>
  41. </div>
  42. <div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
  43. </div>
  44. <div title="视频聊天" v-show="chat.type=='PRIVATE'" class="el-icon-phone-outline"
  45. @click="showVideoBox()">
  46. </div>
  47. <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
  48. </div>
  49. <div class="send-content-area">
  50. <div contenteditable="true" v-show="!sendImageUrl" id="inputText" ref="editBox" class="send-text-area"
  51. :disabled="lockMessage" @paste.prevent="onEditorPaste"
  52. @compositionstart="onEditorCompositionStart"
  53. @compositionend="onEditorCompositionEnd" @input="onEditorInput"
  54. placeholder="温馨提示:可以粘贴截图到这里了哦~" @blur="onEditBoxBlur()" @keydown.down="onKeyDown"
  55. @keydown.up="onKeyUp" @keydown.enter.prevent="onKeyEnter">
  56. </div>
  57. <div v-show="sendImageUrl" class="send-image-area">
  58. <div class="send-image-box">
  59. <img class="send-image" :src="sendImageUrl" />
  60. <span class="send-image-close el-icon-close" title="删除"
  61. @click="removeSendImage()"></span>
  62. </div>
  63. </div>
  64. <div class="send-btn-area">
  65. <el-button type="primary" size="small" @click="sendMessage()">发送</el-button>
  66. </div>
  67. </div>
  68. </el-footer>
  69. </el-container>
  70. <el-aside class="chat-group-side-box" width="300px" v-show="showSide">
  71. <chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)">
  72. </chat-group-side>
  73. </el-aside>
  74. </el-container>
  75. </el-main>
  76. <emotion ref="emoBox" @emotion="onEmotion"></Emotion>
  77. <chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers" :search-text="atSearchText"
  78. @select="onAtSelect"></chat-at-box>
  79. <chat-voice :visible="showVoice" @close="closeVoiceBox" @send="onSendVoice"></chat-voice>
  80. <chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group"
  81. :groupMembers="groupMembers" @close="closeHistoryBox"></chat-history>
  82. </el-container>
  83. </div>
  84. </template>
  85. <script>
  86. import ChatGroupSide from "./ChatGroupSide.vue";
  87. import ChatMessageItem from "./ChatMessageItem.vue";
  88. import FileUpload from "../common/FileUpload.vue";
  89. import Emotion from "../common/Emotion.vue";
  90. import ChatVoice from "./ChatVoice.vue";
  91. import ChatHistory from "./ChatHistory.vue";
  92. import ChatAtBox from "./ChatAtBox.vue"
  93. export default {
  94. name: "chatPrivate",
  95. components: {
  96. ChatMessageItem,
  97. FileUpload,
  98. ChatGroupSide,
  99. Emotion,
  100. ChatVoice,
  101. ChatHistory,
  102. ChatAtBox
  103. },
  104. props: {
  105. chat: {
  106. type: Object
  107. }
  108. },
  109. data() {
  110. return {
  111. friend: {},
  112. group: {},
  113. groupMembers: [],
  114. sendImageUrl: "",
  115. sendImageFile: "",
  116. showVoice: false, // 是否显示语音录制弹窗
  117. showSide: false, // 是否显示群聊信息栏
  118. showHistory: false, // 是否显示历史聊天记录
  119. lockMessage: false, // 是否锁定发送,
  120. showMinIdx: 0, // 下标低于showMinIdx的消息不显示,否则页面会很卡
  121. atSearchText: "",
  122. focusNode: null, // 缓存光标所在节点
  123. focusOffset: null, // 缓存光标所在节点位置
  124. zhLock: false // 解决中文输入法触发英文的情况
  125. }
  126. },
  127. methods: {
  128. closeRefBox() {
  129. this.$refs.emoBox.close();
  130. this.$refs.atBox.close();
  131. },
  132. onKeyDown() {
  133. if (this.$refs.atBox.show) {
  134. this.$refs.atBox.moveDown()
  135. }
  136. },
  137. onKeyUp() {
  138. if (this.$refs.atBox.show) {
  139. this.$refs.atBox.moveUp()
  140. }
  141. },
  142. onKeyEnter() {
  143. if (this.$refs.atBox.show) {
  144. // 键盘操作不会自动修正焦点,需要手动修正,原因不详
  145. this.focusOffset += this.atSearchText.length;
  146. this.$refs.atBox.select();
  147. } else {
  148. this.sendMessage();
  149. }
  150. },
  151. onEditBoxBlur() {
  152. let selection = window.getSelection()
  153. // 记录光标位置(点击emoji时)
  154. this.focusNode = selection.focusNode;
  155. this.focusOffset = selection.focusOffset;
  156. },
  157. onEditorInput(e) {
  158. // 如果触发 @
  159. if (this.chat.type == "GROUP" && !this.zhLock) {
  160. if (e.data == '@') {
  161. // 打开选择弹窗
  162. this.showAtBox(e);
  163. } else {
  164. let selection = window.getSelection()
  165. let range = selection.getRangeAt(0)
  166. this.focusNode = selection.focusNode;
  167. // 截取@后面的名称作为过滤条件
  168. let stIdx = this.focusNode.textContent.lastIndexOf('@');
  169. this.atSearchText = this.focusNode.textContent.substring(stIdx + 1);
  170. }
  171. }
  172. },
  173. onEditorCompositionStart() {
  174. this.zhLock = true;
  175. },
  176. onEditorCompositionEnd(e) {
  177. this.zhLock = false;
  178. this.onEditorInput(e);
  179. },
  180. showAtBox(e) {
  181. this.atSearchText = "";
  182. let selection = window.getSelection()
  183. let range = selection.getRangeAt(0)
  184. // 记录光标所在位置
  185. this.focusNode = selection.focusNode;
  186. this.focusOffset = selection.focusOffset;
  187. // 光标所在坐标
  188. let pos = range.getBoundingClientRect();
  189. this.$refs.atBox.open({
  190. x: pos.x,
  191. y: pos.y
  192. })
  193. },
  194. onAtSelect(member) {
  195. let range = window.getSelection().getRangeAt(0)
  196. // 选中输入的 @xx 符
  197. range.setStart(this.focusNode, this.focusOffset - 1 - this.atSearchText.length)
  198. range.setEnd(this.focusNode, this.focusOffset)
  199. range.deleteContents()
  200. // 创建元素节点
  201. let element = document.createElement('SPAN')
  202. element.className = "at"
  203. element.dataset.id = member.userId;
  204. element.contentEditable = 'false'
  205. element.innerText = `@${member.aliasName}`
  206. range.insertNode(element)
  207. // 光标移动到末尾
  208. range.collapse()
  209. // 插入空格
  210. let textNode = document.createTextNode('\u00A0');
  211. range.insertNode(textNode)
  212. range.collapse()
  213. this.atSearchText = "";
  214. this.$refs.editBox.focus()
  215. },
  216. createSendText() {
  217. let sendText = ""
  218. this.$refs.editBox.childNodes.forEach((node) => {
  219. if (node.nodeName == "#text") {
  220. sendText = document.getElementById("inputText").innerHTML;
  221. } else if (node.nodeName == "SPAN") {
  222. sendText += node.innerText;
  223. } else if (node.nodeName == "IMG") {
  224. sendText += node.dataset.code;
  225. }
  226. })
  227. return sendText;
  228. },
  229. createAtUserIds() {
  230. let ids = [];
  231. this.$refs.editBox.childNodes.forEach((node) => {
  232. if (node.nodeName == "SPAN") {
  233. ids.push(node.dataset.id);
  234. }
  235. })
  236. return ids;
  237. },
  238. onEditorPaste(e) {
  239. let txt = event.clipboardData.getData('Text')
  240. if (typeof(txt) == 'string') {
  241. let range = window.getSelection().getRangeAt(0)
  242. let textNode = document.createTextNode(txt);
  243. range.insertNode(textNode)
  244. range.collapse();
  245. }
  246. let items = (event.clipboardData || window.clipboardData).items
  247. if (items.length) {
  248. for (let i = 0; i < items.length; i++) {
  249. if (items[i].type.indexOf('image') !== -1) {
  250. let file = items[i].getAsFile();
  251. this.sendImageFile = file;
  252. this.sendImageUrl = URL.createObjectURL(file);
  253. }
  254. }
  255. }
  256. },
  257. removeSendImage() {
  258. this.sendImageUrl = "";
  259. this.sendImageFile = null;
  260. },
  261. onImageSuccess(data, file) {
  262. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  263. msgInfo.content = JSON.stringify(data);
  264. this.$http({
  265. url: this.messageAction,
  266. method: 'post',
  267. data: msgInfo
  268. }).then((id) => {
  269. msgInfo.loadStatus = 'ok';
  270. msgInfo.id = id;
  271. this.$store.commit("insertMessage", msgInfo);
  272. })
  273. },
  274. onImageFail(e, file) {
  275. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  276. msgInfo.loadStatus = 'fail';
  277. this.$store.commit("insertMessage", msgInfo);
  278. },
  279. onImageBefore(file) {
  280. let url = URL.createObjectURL(file);
  281. let data = {
  282. originUrl: url,
  283. thumbUrl: url
  284. }
  285. let msgInfo = {
  286. id: 0,
  287. fileId: file.uid,
  288. sendId: this.mine.id,
  289. content: JSON.stringify(data),
  290. sendTime: new Date().getTime(),
  291. selfSend: true,
  292. type: 1,
  293. loadStatus: "loading",
  294. status: this.$enums.MESSAGE_STATUS.UNSEND
  295. }
  296. // 填充对方id
  297. this.fillTargetId(msgInfo, this.chat.targetId);
  298. // 插入消息
  299. this.$store.commit("insertMessage", msgInfo);
  300. // 滚动到底部
  301. this.scrollToBottom();
  302. // 借助file对象保存
  303. file.msgInfo = msgInfo;
  304. },
  305. onFileSuccess(url, file) {
  306. let data = {
  307. name: file.name,
  308. size: file.size,
  309. url: url
  310. }
  311. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  312. msgInfo.content = JSON.stringify(data);
  313. this.$http({
  314. url: this.messageAction,
  315. method: 'post',
  316. data: msgInfo
  317. }).then((id) => {
  318. msgInfo.loadStatus = 'ok';
  319. msgInfo.id = id;
  320. this.$store.commit("insertMessage", msgInfo);
  321. })
  322. },
  323. onFileFail(e, file) {
  324. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  325. msgInfo.loadStatus = 'fail';
  326. this.$store.commit("insertMessage", msgInfo);
  327. },
  328. onFileBefore(file) {
  329. let url = URL.createObjectURL(file);
  330. let data = {
  331. name: file.name,
  332. size: file.size,
  333. url: url
  334. }
  335. let msgInfo = {
  336. id: 0,
  337. sendId: this.mine.id,
  338. content: JSON.stringify(data),
  339. sendTime: new Date().getTime(),
  340. selfSend: true,
  341. type: 2,
  342. loadStatus: "loading",
  343. status: this.$enums.MESSAGE_STATUS.UNSEND
  344. }
  345. // 填充对方id
  346. this.fillTargetId(msgInfo, this.chat.targetId);
  347. // 插入消息
  348. this.$store.commit("insertMessage", msgInfo);
  349. // 滚动到底部
  350. this.scrollToBottom();
  351. // 借助file对象透传
  352. file.msgInfo = msgInfo;
  353. },
  354. onCloseSide() {
  355. this.showSide = false;
  356. },
  357. onScrollToTop() {
  358. // 多展示10条信息
  359. this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
  360. },
  361. onScroll(e) {
  362. let scrollElement = e.target
  363. let scrollTop = scrollElement.scrollTop
  364. if (scrollTop < 30) { // 在顶部,不滚动的情况
  365. // 多展示20条信息
  366. this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
  367. }
  368. },
  369. showEmotionBox() {
  370. let width = this.$refs.emotion.offsetWidth;
  371. let left = this.$elm.fixLeft(this.$refs.emotion);
  372. let top = this.$elm.fixTop(this.$refs.emotion);
  373. this.$refs.emoBox.open({
  374. x: left + width / 2,
  375. y: top
  376. })
  377. },
  378. onEmotion(emoText) {
  379. // 保持输入框焦点
  380. this.$refs.editBox.focus();
  381. let range = window.getSelection().getRangeAt(0);
  382. // 插入光标所在位置
  383. range.setStart(this.focusNode, this.focusOffset)
  384. let element = document.createElement('IMG')
  385. element.className = "emo"
  386. element.dataset.code = emoText;
  387. element.contentEditable = 'true'
  388. element.setAttribute("src", this.$emo.textToUrl(emoText));
  389. // 选中元素节点
  390. range.insertNode(element)
  391. // 光标移动到末尾
  392. range.collapse()
  393. },
  394. showVoiceBox() {
  395. this.showVoice = true;
  396. },
  397. closeVoiceBox() {
  398. this.showVoice = false;
  399. },
  400. showVideoBox() {
  401. this.$store.commit("showChatPrivateVideoBox", {
  402. friend: this.friend,
  403. master: true
  404. });
  405. },
  406. showHistoryBox() {
  407. this.showHistory = true;
  408. },
  409. closeHistoryBox() {
  410. this.showHistory = false;
  411. },
  412. onSendVoice(data) {
  413. let msgInfo = {
  414. content: JSON.stringify(data),
  415. type: 3
  416. }
  417. // 填充对方id
  418. this.fillTargetId(msgInfo, this.chat.targetId);
  419. this.$http({
  420. url: this.messageAction,
  421. method: 'post',
  422. data: msgInfo
  423. }).then((id) => {
  424. msgInfo.id = id;
  425. msgInfo.sendTime = new Date().getTime();
  426. msgInfo.sendId = this.$store.state.userStore.userInfo.id;
  427. msgInfo.selfSend = true;
  428. msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
  429. this.$store.commit("insertMessage", msgInfo);
  430. // 保持输入框焦点
  431. this.$refs.editBox.focus();
  432. // 滚动到底部
  433. this.scrollToBottom();
  434. // 关闭录音窗口
  435. this.showVoice = false;
  436. })
  437. },
  438. fillTargetId(msgInfo, targetId) {
  439. if (this.chat.type == "GROUP") {
  440. msgInfo.groupId = targetId;
  441. } else {
  442. msgInfo.recvId = targetId;
  443. }
  444. },
  445. sendMessage() {
  446. if (this.sendImageFile) {
  447. this.sendImageMessage();
  448. } else {
  449. this.sendTextMessage();
  450. }
  451. // 消息已读
  452. this.readedMessage()
  453. },
  454. sendImageMessage() {
  455. let file = this.sendImageFile;
  456. this.onImageBefore(this.sendImageFile);
  457. let formData = new FormData()
  458. formData.append('file', file)
  459. this.$http.post("/image/upload", formData, {
  460. headers: {
  461. 'Content-Type': 'multipart/form-data'
  462. }
  463. }).then((data) => {
  464. this.onImageSuccess(data, file);
  465. }).catch((res) => {
  466. this.onImageSuccess(res, file);
  467. })
  468. this.sendImageFile = null;
  469. this.sendImageUrl = "";
  470. this.$nextTick(() => this.$refs.editBox.focus());
  471. this.scrollToBottom();
  472. },
  473. sendTextMessage() {
  474. let sendText = this.createSendText();
  475. if (!sendText.trim()) {
  476. return
  477. }
  478. this.$refs.editBox.cle
  479. let msgInfo = {
  480. content: sendText,
  481. type: 0
  482. }
  483. // 填充对方id
  484. this.fillTargetId(msgInfo, this.chat.targetId);
  485. // 被@人员列表
  486. if (this.chat.type == "GROUP") {
  487. msgInfo.atUserIds = this.createAtUserIds();
  488. }
  489. this.lockMessage = true;
  490. this.$http({
  491. url: this.messageAction,
  492. method: 'post',
  493. data: msgInfo
  494. }).then((id) => {
  495. msgInfo.id = id;
  496. msgInfo.sendTime = new Date().getTime();
  497. msgInfo.sendId = this.$store.state.userStore.userInfo.id;
  498. msgInfo.selfSend = true;
  499. msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
  500. this.$store.commit("insertMessage", msgInfo);
  501. }).finally(() => {
  502. // 解除锁定
  503. this.lockMessage = false;
  504. this.scrollToBottom();
  505. this.resetEditor();
  506. });
  507. },
  508. deleteMessage(msgInfo) {
  509. this.$confirm('确认删除消息?', '删除消息', {
  510. confirmButtonText: '确定',
  511. cancelButtonText: '取消',
  512. type: 'warning'
  513. }).then(() => {
  514. this.$store.commit("deleteMessage", msgInfo);
  515. });
  516. },
  517. recallMessage(msgInfo) {
  518. this.$confirm('确认撤回消息?', '撤回消息', {
  519. confirmButtonText: '确定',
  520. cancelButtonText: '取消',
  521. type: 'warning'
  522. }).then(() => {
  523. let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`
  524. this.$http({
  525. url: url,
  526. method: 'delete'
  527. }).then(() => {
  528. this.$message.success("消息已撤回");
  529. msgInfo = JSON.parse(JSON.stringify(msgInfo));
  530. msgInfo.type = 10;
  531. msgInfo.content = '你撤回了一条消息';
  532. msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
  533. this.$store.commit("insertMessage", msgInfo);
  534. })
  535. });
  536. },
  537. readedMessage() {
  538. if (this.chat.unreadCount == 0) {
  539. return;
  540. }
  541. this.$store.commit("resetUnreadCount", this.chat)
  542. if (this.chat.type == "GROUP") {
  543. var url = `/message/group/readed?groupId=${this.chat.targetId}`
  544. } else {
  545. url = `/message/private/readed?friendId=${this.chat.targetId}`
  546. }
  547. this.$http({
  548. url: url,
  549. method: 'put'
  550. }).then(() => {})
  551. },
  552. loadReaded(fId) {
  553. this.$http({
  554. url: `/message/private/maxReadedId?friendId=${fId}`,
  555. method: 'get'
  556. }).then((id) => {
  557. this.$store.commit("readedMessage", {
  558. friendId: fId,
  559. maxId: id
  560. });
  561. });
  562. },
  563. loadGroup(groupId) {
  564. this.$http({
  565. url: `/group/find/${groupId}`,
  566. method: 'get'
  567. }).then((group) => {
  568. this.group = group;
  569. this.$store.commit("updateChatFromGroup", group);
  570. this.$store.commit("updateGroup", group);
  571. });
  572. this.$http({
  573. url: `/group/members/${groupId}`,
  574. method: 'get'
  575. }).then((groupMembers) => {
  576. this.groupMembers = groupMembers;
  577. });
  578. },
  579. loadFriend(friendId) {
  580. // 获取对方最新信息
  581. this.$http({
  582. url: `/user/find/${friendId}`,
  583. method: 'get'
  584. }).then((friend) => {
  585. this.friend = friend;
  586. this.$store.commit("updateChatFromFriend", friend);
  587. this.$store.commit("updateFriend", friend);
  588. })
  589. },
  590. showName(msgInfo) {
  591. if (this.chat.type == 'GROUP') {
  592. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  593. return member ? member.aliasName : "";
  594. } else {
  595. return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName
  596. }
  597. },
  598. headImage(msgInfo) {
  599. if (this.chat.type == 'GROUP') {
  600. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  601. return member ? member.headImage : "";
  602. } else {
  603. return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
  604. }
  605. },
  606. resetEditor() {
  607. this.sendImageUrl = "";
  608. this.sendImageFile = null;
  609. this.$nextTick(() => {
  610. this.$refs.editBox.innerHTML = "";
  611. this.$refs.editBox.focus();
  612. });
  613. },
  614. scrollToBottom() {
  615. this.$nextTick(() => {
  616. let div = document.getElementById("chatScrollBox");
  617. div.scrollTop = div.scrollHeight;
  618. });
  619. }
  620. },
  621. computed: {
  622. mine() {
  623. return this.$store.state.userStore.userInfo;
  624. },
  625. title() {
  626. let title = this.chat.showName;
  627. if (this.chat.type == "GROUP") {
  628. let size = this.groupMembers.filter(m => !m.quit).length;
  629. title += `(${size})`;
  630. }
  631. return title;
  632. },
  633. messageAction() {
  634. return `/message/${this.chat.type.toLowerCase()}/send`;
  635. },
  636. unreadCount() {
  637. return this.chat.unreadCount;
  638. }
  639. },
  640. watch: {
  641. chat: {
  642. handler(newChat, oldChat) {
  643. if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
  644. newChat.targetId != oldChat.targetId)) {
  645. if (this.chat.type == "GROUP") {
  646. this.loadGroup(this.chat.targetId);
  647. } else {
  648. this.loadFriend(this.chat.targetId);
  649. // 加载已读状态
  650. this.loadReaded(this.chat.targetId)
  651. }
  652. // 滚到底部
  653. this.scrollToBottom();
  654. this.showSide = false;
  655. // 消息已读
  656. this.readedMessage()
  657. // 初始状态只显示30条消息
  658. let size = this.chat.messages.length;
  659. this.showMinIdx = size > 30 ? size - 30 : 0;
  660. // 重置输入框
  661. this.resetEditor();
  662. }
  663. },
  664. immediate: true
  665. },
  666. unreadCount: {
  667. handler(newCount, oldCount) {
  668. if (newCount > 0) {
  669. // 拉至底部
  670. this.scrollToBottom();
  671. }
  672. }
  673. }
  674. },
  675. mounted() {
  676. let div = document.getElementById("chatScrollBox");
  677. div.addEventListener('scroll', this.onScroll)
  678. }
  679. }
  680. </script>
  681. <style lang="scss">
  682. .chat-box {
  683. position: relative;
  684. width: 100%;
  685. background: #f8f8f8;
  686. border: #dddddd solid 1px;
  687. .el-header {
  688. padding: 5px;
  689. background-color: white;
  690. line-height: 50px;
  691. font-size: 20px;
  692. font-weight: 600;
  693. border: #dddddd solid 1px;
  694. .btn-side {
  695. position: absolute;
  696. right: 20px;
  697. line-height: 60px;
  698. font-size: 22px;
  699. cursor: pointer;
  700. &:hover {
  701. font-size: 30px;
  702. }
  703. }
  704. }
  705. .im-chat-main {
  706. padding: 0;
  707. border: #dddddd solid 1px;
  708. .im-chat-box {
  709. >ul {
  710. padding: 0 20px;
  711. li {
  712. list-style-type: none;
  713. }
  714. }
  715. }
  716. }
  717. .im-chat-footer {
  718. display: flex;
  719. flex-direction: column;
  720. padding: 0;
  721. border: #dddddd solid 1px;
  722. .chat-tool-bar {
  723. display: flex;
  724. position: relative;
  725. width: 100%;
  726. height: 40px;
  727. text-align: left;
  728. box-sizing: border-box;
  729. border: #dddddd solid 1px;
  730. >div {
  731. margin-left: 10px;
  732. font-size: 22px;
  733. cursor: pointer;
  734. color: #333333;
  735. line-height: 40px;
  736. &:hover {
  737. color: black;
  738. }
  739. }
  740. }
  741. .send-content-area {
  742. position: relative;
  743. display: flex;
  744. flex-direction: column;
  745. height: 100%;
  746. background-color: white !important;
  747. .send-text-area {
  748. box-sizing: border-box;
  749. padding: 5px;
  750. width: 100%;
  751. flex: 1;
  752. resize: none;
  753. font-size: 16px;
  754. color: black;
  755. outline-color: rgba(83, 160, 231, 0.61);
  756. text-align: left;
  757. line-height: 30 px;
  758. .at {
  759. color: blue;
  760. font-weight: 600;
  761. }
  762. .emo {
  763. width: 30px;
  764. height: 30px;
  765. vertical-align: bottom;
  766. }
  767. }
  768. .send-image-area {
  769. text-align: left;
  770. border: #53a0e7 solid 1px;
  771. .send-image-box {
  772. position: relative;
  773. display: inline-block;
  774. .send-image {
  775. max-height: 180px;
  776. border: 1px solid #ccc;
  777. border-radius: 2%;
  778. margin: 2px;
  779. }
  780. .send-image-close {
  781. position: absolute;
  782. padding: 3px;
  783. right: 7px;
  784. top: 7px;
  785. color: white;
  786. cursor: pointer;
  787. font-size: 15px;
  788. font-weight: 600;
  789. background-color: #aaa;
  790. border-radius: 50%;
  791. border: 1px solid #ccc;
  792. }
  793. }
  794. }
  795. .send-btn-area {
  796. padding: 10px;
  797. position: absolute;
  798. bottom: 0;
  799. right: 0;
  800. }
  801. }
  802. }
  803. .chat-group-side-box {
  804. border: #dddddd solid 1px;
  805. animation: rtl-drawer-in .3s 1ms;
  806. }
  807. }
  808. </style>