ChatBox.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <template>
  2. <el-container class="chat-box">
  3. <el-header height="60px">
  4. <span>{{title}}</span>
  5. <span title="群聊信息" v-show="this.chat.type=='GROUP'" class="btn-side el-icon-more"
  6. @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="handleScroll">
  12. <div class="im-chat-box">
  13. <ul>
  14. <li v-for="(msgInfo,idx) in chat.messages" :key="idx">
  15. <chat-message-item v-show="idx>=showMinIdx" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
  16. :showName="showName(msgInfo)" :msgInfo="msgInfo" @delete="deleteMessage"
  17. @recall="recallMessage">
  18. </chat-message-item>
  19. </li>
  20. </ul>
  21. </div>
  22. </el-main>
  23. <el-footer height="240px" class="im-chat-footer">
  24. <div class="chat-tool-bar">
  25. <div title="表情" class="icon iconfont icon-biaoqing" ref="emotion"
  26. @click="switchEmotionBox()">
  27. </div>
  28. <div title="发送图片">
  29. <file-upload :action="imageAction" :maxSize="5*1024*1024"
  30. :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp','image/gif']"
  31. @before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail">
  32. <i class="el-icon-picture-outline"></i>
  33. </file-upload>
  34. </div>
  35. <div title="发送文件">
  36. <file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore"
  37. @success="handleFileSuccess" @fail="handleFileFail">
  38. <i class="el-icon-wallet"></i>
  39. </file-upload>
  40. </div>
  41. <div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
  42. </div>
  43. <div title="视频聊天" v-show="chat.type=='PRIVATE'" class="el-icon-phone-outline"
  44. @click="showVideoBox()">
  45. </div>
  46. <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
  47. </div>
  48. <div class="send-content-area">
  49. <textarea v-show="!sendImageUrl" v-model="sendText" ref="sendBox" class="send-text-area"
  50. :disabled="lockMessage" @keydown.enter="sendTextMessage()" @paste="handlePaste"
  51. placeholder="温馨提示:可以粘贴截图到这里了哦~"></textarea>
  52. <div v-show="sendImageUrl" class="send-image-area">
  53. <div class="send-image-box">
  54. <img class="send-image" :src="sendImageUrl" />
  55. <span class="send-image-close el-icon-close" title="删除"
  56. @click="removeSendImage()"></span>
  57. </div>
  58. </div>
  59. <div class="send-btn-area">
  60. <el-button type="primary" size="small" @click="handleSendMessage()">发送</el-button>
  61. </div>
  62. </div>
  63. </el-footer>
  64. </el-container>
  65. <el-aside class="chat-group-side-box" width="300px" v-show="showSide">
  66. <chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)">
  67. </chat-group-side>
  68. </el-aside>
  69. </el-container>
  70. </el-main>
  71. <emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion>
  72. <chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice>
  73. <chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers"
  74. @close="closeHistoryBox"></chat-history>
  75. </el-container>
  76. </template>
  77. <script>
  78. import ChatGroupSide from "./ChatGroupSide.vue";
  79. import ChatMessageItem from "./ChatMessageItem.vue";
  80. import FileUpload from "../common/FileUpload.vue";
  81. import Emotion from "../common/Emotion.vue";
  82. import ChatVoice from "./ChatVoice.vue";
  83. import ChatHistory from "./ChatHistory.vue";
  84. export default {
  85. name: "chatPrivate",
  86. components: {
  87. ChatMessageItem,
  88. FileUpload,
  89. ChatGroupSide,
  90. Emotion,
  91. ChatVoice,
  92. ChatHistory
  93. },
  94. props: {
  95. chat: {
  96. type: Object
  97. }
  98. },
  99. data() {
  100. return {
  101. friend: {},
  102. group: {},
  103. groupMembers: [],
  104. sendText: "",
  105. sendImageUrl: "",
  106. sendImageFile: "",
  107. showVoice: false, // 是否显示语音录制弹窗
  108. showSide: false, // 是否显示群聊信息栏
  109. showEmotion: false, // 是否显示emoji表情
  110. emoBoxPos: { // emoji表情弹出位置
  111. x: 0,
  112. y: 0
  113. },
  114. showHistory: false, // 是否显示历史聊天记录
  115. lockMessage: false, // 是否锁定发送,
  116. showMinIdx: 0 // 下标低于showMinIdx的消息不显示,否则页面会很卡
  117. }
  118. },
  119. methods: {
  120. handlePaste(e) {
  121. let txt = event.clipboardData.getData('Text')
  122. if (typeof(txt) == 'string') {
  123. this.sendText += txt
  124. }
  125. const items = (event.clipboardData || window.clipboardData).items
  126. if (items.length) {
  127. for (let i = 0; i < items.length; i++) {
  128. if (items[i].type.indexOf('image') !== -1) {
  129. let file = items[i].getAsFile();
  130. this.sendImageFile = file;
  131. this.sendImageUrl = URL.createObjectURL(file);
  132. }
  133. }
  134. }
  135. },
  136. removeSendImage() {
  137. this.sendImageUrl = "";
  138. this.sendImageFile = null;
  139. },
  140. handleImageSuccess(data, file) {
  141. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo));
  142. msgInfo.content = JSON.stringify(data);
  143. this.$http({
  144. url: this.messageAction,
  145. method: 'post',
  146. data: msgInfo
  147. }).then((id) => {
  148. msgInfo.loadStatus = 'ok';
  149. msgInfo.id = id;
  150. this.$store.commit("insertMessage", msgInfo);
  151. })
  152. },
  153. handleImageFail(e, file) {
  154. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo));
  155. msgInfo.loadStatus = 'fail';
  156. this.$store.commit("insertMessage", msgInfo);
  157. },
  158. handleImageBefore(file) {
  159. let url = URL.createObjectURL(file);
  160. let data = {
  161. originUrl: url,
  162. thumbUrl: url
  163. }
  164. let msgInfo = {
  165. id: 0,
  166. fileId: file.uid,
  167. sendId: this.mine.id,
  168. content: JSON.stringify(data),
  169. sendTime: new Date().getTime(),
  170. selfSend: true,
  171. type: 1,
  172. loadStatus: "loading",
  173. status: this.$enums.MESSAGE_STATUS.UNSEND
  174. }
  175. // 填充对方id
  176. this.fillTargetId(msgInfo, this.chat.targetId);
  177. // 插入消息
  178. this.$store.commit("insertMessage", msgInfo);
  179. // 滚动到底部
  180. this.scrollToBottom();
  181. // 借助file对象保存
  182. file.msgInfo = msgInfo;
  183. },
  184. handleFileSuccess(url, file) {
  185. let data = {
  186. name: file.name,
  187. size: file.size,
  188. url: url
  189. }
  190. let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo));
  191. msgInfo.content = JSON.stringify(data);
  192. this.$http({
  193. url: this.messageAction,
  194. method: 'post',
  195. data: msgInfo
  196. }).then((id) => {
  197. msgInfo.loadStatus = 'ok';
  198. msgInfo.id = id;
  199. this.$store.commit("insertMessage", msgInfo);
  200. })
  201. },
  202. handleFileFail(e, file) {
  203. let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo));
  204. msgInfo.loadStatus = 'fail';
  205. this.$store.commit("insertMessage", msgInfo);
  206. },
  207. handleFileBefore(file) {
  208. let url = URL.createObjectURL(file);
  209. let data = {
  210. name: file.name,
  211. size: file.size,
  212. url: url
  213. }
  214. let msgInfo = {
  215. id: 0,
  216. sendId: this.mine.id,
  217. content: JSON.stringify(data),
  218. sendTime: new Date().getTime(),
  219. selfSend: true,
  220. type: 2,
  221. loadStatus: "loading",
  222. status: this.$enums.MESSAGE_STATUS.UNSEND
  223. }
  224. // 填充对方id
  225. this.fillTargetId(msgInfo, this.chat.targetId);
  226. // 插入消息
  227. this.$store.commit("insertMessage", msgInfo);
  228. // 滚动到底部
  229. this.scrollToBottom();
  230. // 借助file对象透传
  231. file.msgInfo = msgInfo;
  232. },
  233. handleCloseSide() {
  234. this.showSide = false;
  235. },
  236. handleScrollToTop() {
  237. // 多展示10条信息
  238. this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
  239. },
  240. handleScroll(e) {
  241. let scrollElement = e.target
  242. let scrollTop = scrollElement.scrollTop
  243. if (scrollTop <30 ) { // 在顶部,不滚动的情况
  244. console.log("next")
  245. // 多展示20条信息
  246. this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
  247. }
  248. },
  249. switchEmotionBox() {
  250. this.showEmotion = !this.showEmotion;
  251. let width = this.$refs.emotion.offsetWidth;
  252. let left = this.$elm.fixLeft(this.$refs.emotion);
  253. let top = this.$elm.fixTop(this.$refs.emotion);
  254. this.emoBoxPos.y = top;
  255. this.emoBoxPos.x = left + width / 2;
  256. },
  257. handleEmotion(emoText) {
  258. this.sendText += emoText;
  259. this.showEmotion = false;
  260. // 保持输入框焦点
  261. this.$refs.sendBox.focus();
  262. },
  263. showVoiceBox() {
  264. this.showVoice = true;
  265. },
  266. closeVoiceBox() {
  267. this.showVoice = false;
  268. },
  269. showVideoBox() {
  270. this.$store.commit("showChatPrivateVideoBox", {
  271. friend: this.friend,
  272. master: true
  273. });
  274. },
  275. showHistoryBox() {
  276. this.showHistory = true;
  277. },
  278. closeHistoryBox() {
  279. this.showHistory = false;
  280. },
  281. handleSendVoice(data) {
  282. let msgInfo = {
  283. content: JSON.stringify(data),
  284. type: 3
  285. }
  286. // 填充对方id
  287. this.fillTargetId(msgInfo, this.chat.targetId);
  288. this.$http({
  289. url: this.messageAction,
  290. method: 'post',
  291. data: msgInfo
  292. }).then((id) => {
  293. msgInfo.id = id;
  294. msgInfo.sendTime = new Date().getTime();
  295. msgInfo.sendId = this.$store.state.userStore.userInfo.id;
  296. msgInfo.selfSend = true;
  297. msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
  298. this.$store.commit("insertMessage", msgInfo);
  299. // 保持输入框焦点
  300. this.$refs.sendBox.focus();
  301. // 滚动到底部
  302. this.scrollToBottom();
  303. // 关闭录音窗口
  304. this.showVoice = false;
  305. })
  306. },
  307. fillTargetId(msgInfo, targetId) {
  308. if (this.chat.type == "GROUP") {
  309. msgInfo.groupId = targetId;
  310. } else {
  311. msgInfo.recvId = targetId;
  312. }
  313. },
  314. handleSendMessage() {
  315. if (this.sendImageFile) {
  316. this.sendImageMessage();
  317. } else {
  318. this.sendTextMessage();
  319. }
  320. },
  321. sendImageMessage() {
  322. let file = this.sendImageFile;
  323. this.handleImageBefore(this.sendImageFile);
  324. let formData = new FormData()
  325. formData.append('file', file.raw || file)
  326. this.$http.post("/image/upload", formData, {
  327. headers: {
  328. 'Content-Type': 'multipart/form-data'
  329. }
  330. }).then((data) => {
  331. this.handleImageSuccess(data, file);
  332. }).catch((res) => {
  333. this.handleImageSuccess(res, file);
  334. })
  335. this.sendImageFile = null;
  336. this.sendImageUrl = "";
  337. this.$nextTick(() => this.$refs.sendBox.focus());
  338. this.scrollToBottom();
  339. },
  340. sendTextMessage() {
  341. if (!this.sendText.trim()) {
  342. return
  343. }
  344. let msgInfo = {
  345. content: this.sendText,
  346. type: 0
  347. }
  348. // 填充对方id
  349. this.fillTargetId(msgInfo, this.chat.targetId);
  350. this.lockMessage = true;
  351. this.$http({
  352. url: this.messageAction,
  353. method: 'post',
  354. data: msgInfo
  355. }).then((id) => {
  356. this.sendText = "";
  357. msgInfo.id = id;
  358. msgInfo.sendTime = new Date().getTime();
  359. msgInfo.sendId = this.$store.state.userStore.userInfo.id;
  360. msgInfo.selfSend = true;
  361. msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
  362. this.$store.commit("insertMessage", msgInfo);
  363. }).finally(() => {
  364. // 解除锁定
  365. this.lockMessage = false;
  366. // 保持输入框焦点
  367. this.$nextTick(() => this.$refs.sendBox.focus());
  368. // 滚动到底部
  369. this.scrollToBottom();
  370. });
  371. const e = window.event || arguments[0];
  372. if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === 13) {
  373. e.returnValue = false;
  374. e.preventDefault();
  375. return false;
  376. }
  377. },
  378. deleteMessage(msgInfo) {
  379. this.$confirm('确认删除消息?', '删除消息', {
  380. confirmButtonText: '确定',
  381. cancelButtonText: '取消',
  382. type: 'warning'
  383. }).then(() => {
  384. this.$store.commit("deleteMessage", msgInfo);
  385. });
  386. },
  387. recallMessage(msgInfo) {
  388. this.$confirm('确认撤回消息?', '撤回消息', {
  389. confirmButtonText: '确定',
  390. cancelButtonText: '取消',
  391. type: 'warning'
  392. }).then(() => {
  393. let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`
  394. this.$http({
  395. url: url,
  396. method: 'delete'
  397. }).then(() => {
  398. this.$message.success("消息已撤回");
  399. msgInfo = JSON.parse(JSON.stringify(msgInfo));
  400. msgInfo.type = 10;
  401. msgInfo.content = '你撤回了一条消息';
  402. msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
  403. this.$store.commit("insertMessage", msgInfo);
  404. })
  405. });
  406. },
  407. readedMessage() {
  408. if (this.chat.type == "GROUP") {
  409. var url = `/message/group/readed?groupId=${this.chat.targetId}`
  410. } else {
  411. url = `/message/private/readed?friendId=${this.chat.targetId}`
  412. }
  413. this.$http({
  414. url: url,
  415. method: 'put'
  416. }).then(() => {
  417. this.$store.commit("resetUnreadCount", this.chat)
  418. this.scrollToBottom();
  419. })
  420. },
  421. loadGroup(groupId) {
  422. this.$http({
  423. url: `/group/find/${groupId}`,
  424. method: 'get'
  425. }).then((group) => {
  426. this.group = group;
  427. this.$store.commit("updateChatFromGroup", group);
  428. this.$store.commit("updateGroup", group);
  429. });
  430. this.$http({
  431. url: `/group/members/${groupId}`,
  432. method: 'get'
  433. }).then((groupMembers) => {
  434. this.groupMembers = groupMembers;
  435. });
  436. },
  437. loadFriend(friendId) {
  438. // 获取对方最新信息
  439. this.$http({
  440. url: `/user/find/${friendId}`,
  441. method: 'get'
  442. }).then((friend) => {
  443. this.friend = friend;
  444. this.$store.commit("updateChatFromFriend", friend);
  445. this.$store.commit("updateFriend", friend);
  446. })
  447. },
  448. showName(msgInfo) {
  449. if (this.chat.type == 'GROUP') {
  450. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  451. return member ? member.aliasName : "";
  452. } else {
  453. return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName
  454. }
  455. },
  456. headImage(msgInfo) {
  457. if (this.chat.type == 'GROUP') {
  458. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  459. return member ? member.headImage : "";
  460. } else {
  461. return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage
  462. }
  463. },
  464. scrollToBottom() {
  465. this.$nextTick(() => {
  466. let div = document.getElementById("chatScrollBox");
  467. div.scrollTop = div.scrollHeight;
  468. });
  469. }
  470. },
  471. computed: {
  472. mine() {
  473. return this.$store.state.userStore.userInfo;
  474. },
  475. title() {
  476. let title = this.chat.showName;
  477. if (this.chat.type == "GROUP") {
  478. let size = this.groupMembers.filter(m => !m.quit).length;
  479. title += `(${size})`;
  480. }
  481. return title;
  482. },
  483. imageAction() {
  484. return `${process.env.VUE_APP_BASE_API}/image/upload`;
  485. },
  486. fileAction() {
  487. return `${process.env.VUE_APP_BASE_API}/file/upload`;
  488. },
  489. messageAction() {
  490. return `/message/${this.chat.type.toLowerCase()}/send`;
  491. },
  492. unreadCount() {
  493. return this.chat.unreadCount;
  494. }
  495. },
  496. watch: {
  497. chat: {
  498. handler(newChat, oldChat) {
  499. if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
  500. newChat.targetId != oldChat.targetId)) {
  501. if (this.chat.type == "GROUP") {
  502. this.loadGroup(this.chat.targetId);
  503. } else {
  504. this.loadFriend(this.chat.targetId);
  505. }
  506. this.scrollToBottom();
  507. this.sendText = "";
  508. // 初始状态只显示30条消息
  509. let size = this.chat.messages.length;
  510. this.showMinIdx = size > 30 ? size - 30 : 0;
  511. // 保持输入框焦点
  512. this.$nextTick(() => {
  513. this.$refs.sendBox.focus();
  514. })
  515. }
  516. },
  517. immediate: true
  518. },
  519. unreadCount: {
  520. handler(newCount, oldCount) {
  521. if (newCount > 0) {
  522. // 消息已读
  523. this.readedMessage()
  524. }
  525. }
  526. }
  527. },
  528. mounted() {
  529. let div = document.getElementById("chatScrollBox");
  530. div.addEventListener('scroll', this.handleScroll)
  531. }
  532. }
  533. </script>
  534. <style lang="scss">
  535. .chat-box {
  536. background: white;
  537. border: #dddddd solid 1px;
  538. .el-header {
  539. padding: 5px;
  540. background-color: white;
  541. line-height: 50px;
  542. font-size: 20px;
  543. font-weight: 600;
  544. border: #dddddd solid 1px;
  545. .btn-side {
  546. position: absolute;
  547. right: 20px;
  548. line-height: 60px;
  549. font-size: 22px;
  550. cursor: pointer;
  551. &:hover {
  552. font-size: 30px;
  553. }
  554. }
  555. }
  556. .im-chat-main {
  557. padding: 0;
  558. border: #dddddd solid 1px;
  559. .im-chat-box {
  560. >ul {
  561. padding: 20px;
  562. li {
  563. list-style-type: none;
  564. }
  565. }
  566. }
  567. }
  568. .im-chat-footer {
  569. display: flex;
  570. flex-direction: column;
  571. padding: 0;
  572. border: #dddddd solid 1px;
  573. .chat-tool-bar {
  574. display: flex;
  575. position: relative;
  576. width: 100%;
  577. height: 40px;
  578. text-align: left;
  579. box-sizing: border-box;
  580. border: #dddddd solid 1px;
  581. >div {
  582. margin-left: 10px;
  583. font-size: 22px;
  584. cursor: pointer;
  585. color: #333333;
  586. line-height: 40px;
  587. &:hover {
  588. color: black;
  589. }
  590. }
  591. }
  592. .send-content-area {
  593. display: flex;
  594. flex-direction: column;
  595. height: 100%;
  596. background-color: #f8f8f8 !important;
  597. outline-color: rgba(83, 160, 231, 0.61);
  598. .send-text-area {
  599. box-sizing: border-box;
  600. padding: 5px;
  601. width: 100%;
  602. flex: 1;
  603. resize: none;
  604. font-size: 16px;
  605. color: black;
  606. background-color: #f8f8f8 !important;
  607. outline-color: rgba(83, 160, 231, 0.61);
  608. }
  609. .send-image-area {
  610. text-align: left;
  611. .send-image-box {
  612. position: relative;
  613. display: inline-block;
  614. .send-image {
  615. max-height: 190px;
  616. border: 1px solid #ccc;
  617. border-radius: 2%;
  618. margin: 2px;
  619. }
  620. .send-image-close {
  621. position: absolute;
  622. padding: 3px;
  623. right: 7px;
  624. top: 7px;
  625. color: white;
  626. cursor: pointer;
  627. font-size: 15px;
  628. font-weight: 600;
  629. background-color: #aaa;
  630. border-radius: 50%;
  631. border: 1px solid #ccc;
  632. }
  633. }
  634. }
  635. .send-btn-area {
  636. padding: 10px;
  637. position: absolute;
  638. bottom: 0;
  639. right: 0;
  640. }
  641. }
  642. }
  643. .chat-group-side-box {
  644. border: #dddddd solid 1px;
  645. animation: rtl-drawer-in .3s 1ms;
  646. }
  647. }
  648. </style>