chat-box.vue 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  1. <template>
  2. <view class="page chat-box">
  3. <nav-bar back more @more="onShowMore">{{ title }}</nav-bar>
  4. <view class="chat-main-box" :style="{height: chatMainHeight+'px'}">
  5. <view class="chat-msg" @click="switchChatTabBox('none')">
  6. <scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
  7. :scroll-into-view="'chat-item-' + scrollMsgIdx">
  8. <view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
  9. <chat-message-item :ref="'message'+msgInfo.id" v-if="idx >= showMinIdx"
  10. :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)" :showName="showName(msgInfo)"
  11. @recall="onRecallMessage" @delete="onDeleteMessage" @copy="onCopyMessage"
  12. @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile"
  13. @audioStateChange="onAudioStateChange" :id="'chat-item-' + idx" :msgInfo="msgInfo"
  14. :groupMembers="groupMembers">
  15. </chat-message-item>
  16. </view>
  17. </scroll-view>
  18. </view>
  19. <view v-if="atUserIds.length > 0" class="chat-at-bar" @click="openAtBox()">
  20. <view class="iconfont icon-at">:&nbsp;</view>
  21. <scroll-view v-if="atUserIds.length > 0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">
  22. <view class="chat-at-items">
  23. <view v-for="m in atUserItems" class="chat-at-item" :key="m.userId">
  24. <head-image :name="m.showNickName" :url="m.headImage" size="minier"></head-image>
  25. </view>
  26. </view>
  27. </scroll-view>
  28. </view>
  29. <view class="send-bar">
  30. <view v-if="!showRecord" class="iconfont icon-voice-circle" @click="onRecorderInput()"></view>
  31. <view v-else class="iconfont icon-keyboard" @click="onKeyboardInput()"></view>
  32. <chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record>
  33. <view v-else class="send-text">
  34. <editor id="editor" class="send-text-area" :placeholder="isReceipt ? '[回执消息]' : ''"
  35. :read-only="isReadOnly" @focus="onEditorFocus" @blur="onEditorBlur" @ready="onEditorReady" @input="onTextInput">
  36. </editor>
  37. </view>
  38. <view v-if="chat && chat.type == 'GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
  39. <view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view>
  40. <view v-if="isEmpty" class="iconfont icon-add" @click="onShowToolsChatTab()">
  41. </view>
  42. <button v-if="!isEmpty || atUserIds.length" class="btn-send" type="primary"
  43. @touchend.prevent="sendTextMessage()" size="mini">发送</button>
  44. </view>
  45. </view>
  46. <view class="chat-tab-bar">
  47. <view v-if="chatTabBox == 'tools'" class="chat-tools" :style="{height: keyboardHeight+'px'}">
  48. <view class="chat-tools-item">
  49. <file-upload ref="fileUpload" :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
  50. :onError="onUploadFileFail">
  51. <view class="tool-icon iconfont icon-folder"></view>
  52. </file-upload>
  53. <view class="tool-name">文件</view>
  54. </view>
  55. <view class="chat-tools-item">
  56. <image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
  57. :onSuccess="onUploadImageSuccess" :onError="onUploadImageFail">
  58. <view class="tool-icon iconfont icon-picture"></view>
  59. </image-upload>
  60. <view class="tool-name">相册</view>
  61. </view>
  62. <view class="chat-tools-item">
  63. <image-upload sourceType="camera" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
  64. :onError="onUploadImageFail">
  65. <view class="tool-icon iconfont icon-camera"></view>
  66. </image-upload>
  67. <view class="tool-name">拍摄</view>
  68. </view>
  69. <view class="chat-tools-item" @click="onRecorderInput()">
  70. <view class="tool-icon iconfont icon-microphone"></view>
  71. <view class="tool-name">语音消息</view>
  72. </view>
  73. <view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="switchReceipt()">
  74. <view class="tool-icon iconfont icon-receipt" :class="isReceipt ? 'active' : ''"></view>
  75. <view class="tool-name">回执消息</view>
  76. </view>
  77. <!-- #ifndef MP-WEIXIN -->
  78. <!-- 音视频不支持小程序 -->
  79. <view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onPriviteVideo()">
  80. <view class="tool-icon iconfont icon-video"></view>
  81. <view class="tool-name">视频通话</view>
  82. </view>
  83. <view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onPriviteVoice()">
  84. <view class="tool-icon iconfont icon-call"></view>
  85. <view class="tool-name">语音通话</view>
  86. </view>
  87. <view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="onGroupVideo()">
  88. <view class="tool-icon iconfont icon-call"></view>
  89. <view class="tool-name">语音通话</view>
  90. </view>
  91. <!-- #endif -->
  92. </view>
  93. <scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true"
  94. :style="{height: keyboardHeight+'px'}">
  95. <view class="emotion-item-list">
  96. <image class="emotion-item emoji-large" :title="emoText" :src="$emo.textToPath(emoText)"
  97. v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
  98. lazy-load="true"></image>
  99. </view>
  100. </scroll-view>
  101. </view>
  102. <!-- @用户时选择成员 -->
  103. <chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers"
  104. @complete="onAtComplete"></chat-at-box>
  105. <!-- 群语音通话时选择成员 -->
  106. <!-- #ifndef MP-WEIXIN -->
  107. <group-member-selector ref="selBox" :members="groupMembers" :maxSize="configStore.webrtc.maxChannel"
  108. @complete="onInviteOk"></group-member-selector>
  109. <group-rtc-join ref="rtcJoin" :groupId="group.id"></group-rtc-join>
  110. <!-- #endif -->
  111. </view>
  112. </template>
  113. <script>
  114. import UNI_APP from '@/.env.js';
  115. export default {
  116. data() {
  117. return {
  118. chat: {},
  119. friend: {},
  120. group: {},
  121. groupMembers: [],
  122. isReceipt: false, // 是否回执消息
  123. scrollMsgIdx: 0, // 滚动条定位为到哪条消息
  124. chatTabBox: 'none',
  125. showRecord: false,
  126. chatMainHeight: 0, // 聊天窗口高度
  127. keyboardHeight: 290, // 键盘高度
  128. windowHeight: 1000, // 窗口高度
  129. initHeight: 1000, // h5初始高度
  130. atUserIds: [],
  131. needScrollToBottom: false, // 需要滚动到底部
  132. showMinIdx: 0, // 下标小于showMinIdx的消息不显示,否则可能很卡
  133. reqQueue: [], // 请求队列
  134. isSending: false, // 是否正在发送请求
  135. isShowKeyBoard: false, // 键盘是否正在弹起
  136. editorCtx: null, // 编辑器上下文
  137. isEmpty: true, // 编辑器是否为空
  138. isFocus: false, // 编辑器是否焦点
  139. isReadOnly: false, // 编辑器是否只读
  140. playingAudio: null // 当前正在播放的录音消息
  141. }
  142. },
  143. methods: {
  144. onRecorderInput() {
  145. this.showRecord = true;
  146. this.switchChatTabBox('none');
  147. },
  148. onKeyboardInput() {
  149. this.showRecord = false;
  150. this.switchChatTabBox('none');
  151. },
  152. onSendRecord(data) {
  153. // 检查是否被封禁
  154. if (this.isBanned) {
  155. this.showBannedTip();
  156. return;
  157. }
  158. let msgInfo = {
  159. content: JSON.stringify(data),
  160. type: this.$enums.MESSAGE_TYPE.AUDIO,
  161. receipt: this.isReceipt
  162. }
  163. // 填充对方id
  164. this.fillTargetId(msgInfo, this.chat.targetId);
  165. this.sendMessageRequest(msgInfo).then((m) => {
  166. m.selfSend = true;
  167. this.chatStore.insertMessage(m, this.chat);
  168. // 会话置顶
  169. this.moveChatToTop();
  170. // 滚动到底部
  171. this.scrollToBottom();
  172. this.isReceipt = false;
  173. })
  174. },
  175. onRtCall(msgInfo) {
  176. if (msgInfo.type == this.$enums.MESSAGE_TYPE.ACT_RT_VOICE) {
  177. this.onPriviteVoice();
  178. } else if (msgInfo.type == this.$enums.MESSAGE_TYPE.ACT_RT_VIDEO) {
  179. this.onPriviteVideo();
  180. }
  181. },
  182. onPriviteVideo() {
  183. const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
  184. uni.navigateTo({
  185. url: `/pages/chat/chat-private-video?mode=video&friend=${friendInfo}&isHost=true`
  186. })
  187. },
  188. onPriviteVoice() {
  189. const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
  190. uni.navigateTo({
  191. url: `/pages/chat/chat-private-video?mode=voice&friend=${friendInfo}&isHost=true`
  192. })
  193. },
  194. onGroupVideo() {
  195. // 邀请成员发起通话
  196. let ids = [this.mine.id];
  197. this.$refs.selBox.init(ids, ids);
  198. this.$refs.selBox.open();
  199. },
  200. onInviteOk(ids) {
  201. if (ids.length < 2) {
  202. return;
  203. }
  204. let users = [];
  205. ids.forEach(id => {
  206. let m = this.groupMembers.find(m => m.userId == id);
  207. // 只取部分字段,压缩url长度
  208. users.push({
  209. id: m.userId,
  210. nickName: m.showNickName,
  211. headImage: m.headImage,
  212. isCamera: false,
  213. isMicroPhone: true
  214. })
  215. })
  216. const groupId = this.group.id;
  217. const inviterId = this.mine.id;
  218. const userInfos = encodeURIComponent(JSON.stringify(users));
  219. uni.navigateTo({
  220. url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=true
  221. &inviterId=${inviterId}&userInfos=${userInfos}`
  222. })
  223. },
  224. moveChatToTop() {
  225. let chatIdx = this.chatStore.findChatIdx(this.chat);
  226. this.chatStore.moveTop(chatIdx);
  227. },
  228. switchReceipt() {
  229. this.isReceipt = !this.isReceipt;
  230. },
  231. openAtBox() {
  232. this.$refs.atBox.init(this.atUserIds);
  233. this.$refs.atBox.open();
  234. },
  235. onAtComplete(atUserIds) {
  236. this.atUserIds = atUserIds;
  237. },
  238. onLongPressHead(msgInfo) {
  239. if (!msgInfo.selfSend && this.chat.type == "GROUP" && this.atUserIds.indexOf(msgInfo.sendId) < 0) {
  240. this.atUserIds.push(msgInfo.sendId);
  241. }
  242. },
  243. headImage(msgInfo) {
  244. if (this.chat.type == 'GROUP') {
  245. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  246. return member ? member.headImage : "";
  247. } else {
  248. return msgInfo.selfSend ? this.mine.headImageThumb : this.chat.headImage
  249. }
  250. },
  251. showName(msgInfo) {
  252. if (this.chat.type == 'GROUP') {
  253. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  254. return member ? member.showNickName : "";
  255. } else {
  256. return msgInfo.selfSend ? this.mine.nickName : this.chat.showName
  257. }
  258. },
  259. sendTextMessage() {
  260. this.editorCtx.getContents({
  261. success: (e) => {
  262. // 清空编辑框数据
  263. this.editorCtx.clear();
  264. this.atUserIds = [];
  265. this.isReceipt = false;
  266. // 检查是否被封禁
  267. if (this.isBanned) {
  268. this.showBannedTip();
  269. return;
  270. }
  271. let sendText = this.isReceipt ? "【回执消息】" : "";
  272. e.delta.ops.forEach((op) => {
  273. if (op.insert.image) {
  274. // emo表情
  275. sendText += `#${op.attributes.alt};`
  276. } else(
  277. // 文字
  278. sendText += op.insert
  279. )
  280. })
  281. if (!sendText.trim() && this.atUserIds.length == 0) {
  282. return uni.showToast({
  283. title: "不能发送空白信息",
  284. icon: "none"
  285. });
  286. }
  287. let receiptText = this.isReceipt ? "【回执消息】" : "";
  288. let atText = this.createAtText();
  289. let msgInfo = {
  290. content: receiptText + sendText + atText,
  291. atUserIds: this.atUserIds,
  292. receipt: this.isReceipt,
  293. type: 0
  294. }
  295. // 填充对方id
  296. this.fillTargetId(msgInfo, this.chat.targetId);
  297. this.sendMessageRequest(msgInfo).then((m) => {
  298. m.selfSend = true;
  299. this.chatStore.insertMessage(m, this.chat);
  300. // 会话置顶
  301. this.moveChatToTop();
  302. }).finally(() => {
  303. // 滚动到底部
  304. this.scrollToBottom();
  305. });
  306. }
  307. })
  308. },
  309. createAtText() {
  310. let atText = "";
  311. this.atUserIds.forEach((id) => {
  312. if (id == -1) {
  313. atText += ` @全体成员`;
  314. } else {
  315. let member = this.groupMembers.find((m) => m.userId == id);
  316. if (member) {
  317. atText += ` @${member.showNickName}`;
  318. }
  319. }
  320. })
  321. return atText;
  322. },
  323. fillTargetId(msgInfo, targetId) {
  324. if (this.chat.type == "GROUP") {
  325. msgInfo.groupId = targetId;
  326. } else {
  327. msgInfo.recvId = targetId;
  328. }
  329. },
  330. scrollToBottom() {
  331. let size = this.messageSize;
  332. if (size > 0) {
  333. this.scrollToMsgIdx(size - 1);
  334. }
  335. },
  336. scrollToMsgIdx(idx) {
  337. // 如果scrollMsgIdx值没变化,滚动条不会移动
  338. if (idx == this.scrollMsgIdx && idx > 0) {
  339. this.$nextTick(() => {
  340. // 先滚动到上一条
  341. this.scrollMsgIdx = idx - 1;
  342. // 再滚动目标位置
  343. this.scrollToMsgIdx(idx);
  344. });
  345. return;
  346. }
  347. this.$nextTick(() => {
  348. this.scrollMsgIdx = idx;
  349. });
  350. },
  351. onShowEmoChatTab() {
  352. this.showRecord = false;
  353. this.switchChatTabBox('emo')
  354. },
  355. onShowToolsChatTab() {
  356. this.showRecord = false;
  357. this.switchChatTabBox('tools')
  358. },
  359. switchChatTabBox(chatTabBox) {
  360. this.chatTabBox = chatTabBox;
  361. this.reCalChatMainHeight();
  362. if (chatTabBox != 'tools' && this.$refs.fileUpload) {
  363. this.$refs.fileUpload.hide()
  364. }
  365. },
  366. selectEmoji(emoText) {
  367. let path = this.$emo.textToPath(emoText)
  368. // 先把键盘禁用了,否则会重新弹出键盘
  369. this.isReadOnly = true;
  370. this.isEmpty = false;
  371. this.$nextTick(() => {
  372. this.editorCtx.insertImage({
  373. src: path,
  374. alt: emoText,
  375. extClass: 'emoji-small',
  376. nowrap: true,
  377. complete: () => {
  378. this.isReadOnly = false;
  379. this.editorCtx.blur();
  380. }
  381. });
  382. })
  383. },
  384. onUploadImageBefore(file) {
  385. // 检查是否被封禁
  386. if (this.isBanned) {
  387. this.showBannedTip();
  388. return;
  389. }
  390. let data = {
  391. originUrl: file.path,
  392. thumbUrl: file.path
  393. }
  394. let msgInfo = {
  395. id: 0,
  396. tmpId: this.generateId(),
  397. fileId: file.uid,
  398. sendId: this.mine.id,
  399. content: JSON.stringify(data),
  400. sendTime: new Date().getTime(),
  401. selfSend: true,
  402. type: this.$enums.MESSAGE_TYPE.IMAGE,
  403. readedCount: 0,
  404. loadStatus: "loading",
  405. status: this.$enums.MESSAGE_STATUS.UNSEND
  406. }
  407. // 填充对方id
  408. this.fillTargetId(msgInfo, this.chat.targetId);
  409. // 插入消息
  410. this.chatStore.insertMessage(msgInfo, this.chat);
  411. // 会话置顶
  412. this.moveChatToTop();
  413. // 借助file对象保存
  414. file.msgInfo = msgInfo;
  415. // 滚到最低部
  416. this.scrollToBottom();
  417. return true;
  418. },
  419. onUploadImageSuccess(file, res) {
  420. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  421. msgInfo.content = JSON.stringify(res.data);
  422. msgInfo.receipt = this.isReceipt
  423. this.sendMessageRequest(msgInfo).then((m) => {
  424. msgInfo.loadStatus = 'ok';
  425. msgInfo.id = m.id;
  426. this.isReceipt = false;
  427. this.chatStore.insertMessage(msgInfo, this.chat);
  428. })
  429. },
  430. onUploadImageFail(file, err) {
  431. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  432. msgInfo.loadStatus = 'fail';
  433. this.chatStore.insertMessage(msgInfo, this.chat);
  434. },
  435. onUploadFileBefore(file) {
  436. // 检查是否被封禁
  437. if (this.isBanned) {
  438. this.showBannedTip();
  439. return;
  440. }
  441. let data = {
  442. name: file.name,
  443. size: file.size,
  444. url: file.path
  445. }
  446. let msgInfo = {
  447. id: 0,
  448. tmpId: this.generateId(),
  449. sendId: this.mine.id,
  450. content: JSON.stringify(data),
  451. sendTime: new Date().getTime(),
  452. selfSend: true,
  453. type: this.$enums.MESSAGE_TYPE.FILE,
  454. readedCount: 0,
  455. loadStatus: "loading",
  456. status: this.$enums.MESSAGE_STATUS.UNSEND
  457. }
  458. // 填充对方id
  459. this.fillTargetId(msgInfo, this.chat.targetId);
  460. // 插入消息
  461. this.chatStore.insertMessage(msgInfo, this.chat);
  462. // 会话置顶
  463. this.moveChatToTop();
  464. // 借助file对象保存
  465. file.msgInfo = msgInfo;
  466. // 滚到最低部
  467. this.scrollToBottom();
  468. return true;
  469. },
  470. onUploadFileSuccess(file, res) {
  471. let data = {
  472. name: file.name,
  473. size: file.size,
  474. url: res.data
  475. }
  476. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  477. msgInfo.content = JSON.stringify(data);
  478. msgInfo.receipt = this.isReceipt
  479. this.sendMessageRequest(msgInfo).then((m) => {
  480. msgInfo.loadStatus = 'ok';
  481. msgInfo.id = m.id;
  482. this.isReceipt = false;
  483. this.chatStore.insertMessage(msgInfo, this.chat);
  484. })
  485. },
  486. onUploadFileFail(file, res) {
  487. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  488. msgInfo.loadStatus = 'fail';
  489. this.chatStore.insertMessage(msgInfo, this.chat);
  490. },
  491. onDeleteMessage(msgInfo) {
  492. uni.showModal({
  493. title: '删除消息',
  494. content: '确认删除消息?',
  495. success: (res) => {
  496. if (!res.cancel) {
  497. this.chatStore.deleteMessage(msgInfo, this.chat);
  498. uni.showToast({
  499. title: "删除成功",
  500. icon: "none"
  501. })
  502. }
  503. }
  504. })
  505. },
  506. onRecallMessage(msgInfo) {
  507. uni.showModal({
  508. title: '撤回消息',
  509. content: '确认撤回消息?',
  510. success: (res) => {
  511. if (!res.cancel) {
  512. let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`
  513. this.$http({
  514. url: url,
  515. method: 'DELETE'
  516. }).then(() => {
  517. msgInfo = JSON.parse(JSON.stringify(msgInfo));
  518. msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL;
  519. msgInfo.content = '你撤回了一条消息';
  520. msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
  521. this.chatStore.insertMessage(msgInfo, this.chat);
  522. })
  523. }
  524. }
  525. })
  526. },
  527. onCopyMessage(msgInfo) {
  528. uni.setClipboardData({
  529. data: msgInfo.content,
  530. success: () => {
  531. uni.showToast({ title: '复制成功' });
  532. },
  533. fail: () => {
  534. uni.showToast({ title: '复制失败', icon: 'none' });
  535. }
  536. });
  537. },
  538. onDownloadFile(msgInfo) {
  539. let url = JSON.parse(msgInfo.content).url;
  540. uni.downloadFile({
  541. url: url,
  542. success(res) {
  543. if (res.statusCode === 200) {
  544. var filePath = encodeURI(res.tempFilePath);
  545. uni.openDocument({
  546. filePath: filePath,
  547. showMenu: true
  548. });
  549. }
  550. },
  551. fail(e) {
  552. uni.showToast({
  553. title: "文件下载失败",
  554. icon: "none"
  555. })
  556. }
  557. });
  558. },
  559. onScrollToTop() {
  560. if (this.showMinIdx == 0) {
  561. console.log("消息已滚动到顶部")
  562. return;
  563. }
  564. // #ifndef H5
  565. // 防止滚动条定格在顶部,不能一直往上滚
  566. this.scrollToMsgIdx(this.showMinIdx);
  567. // #endif
  568. // 多展示20条信息
  569. this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
  570. },
  571. onShowMore() {
  572. if (this.chat.type == "GROUP") {
  573. uni.navigateTo({
  574. url: "/pages/group/group-info?id=" + this.group.id
  575. })
  576. } else {
  577. uni.navigateTo({
  578. url: "/pages/common/user-info?id=" + this.friend.id
  579. })
  580. }
  581. },
  582. onTextInput(e) {
  583. this.isEmpty = e.detail.html == '<p><br></p>'
  584. },
  585. onEditorReady() {
  586. const query = uni.createSelectorQuery().in(this);
  587. query.select('#editor').context((res) => {
  588. this.editorCtx = res.context
  589. }).exec()
  590. },
  591. onEditorFocus(e) {
  592. this.isFocus = true;
  593. this.scrollToBottom()
  594. this.switchChatTabBox('none')
  595. },
  596. onEditorBlur(e) {
  597. this.isFocus = false;
  598. },
  599. onAudioStateChange(state, msgInfo) {
  600. const playingAudio = this.$refs['message' + msgInfo.id][0]
  601. if (state == 'PLAYING' && playingAudio != this.playingAudio) {
  602. // 停止之前的录音
  603. this.playingAudio && this.playingAudio.stopPlayAudio();
  604. // 记录当前正在播放的消息
  605. this.playingAudio = playingAudio;
  606. }
  607. },
  608. loadReaded(fid) {
  609. this.$http({
  610. url: `/message/private/maxReadedId?friendId=${fid}`,
  611. method: 'get'
  612. }).then((id) => {
  613. this.chatStore.readedMessage({
  614. friendId: fid,
  615. maxId: id
  616. });
  617. });
  618. },
  619. readedMessage() {
  620. if (this.unreadCount == 0) {
  621. return;
  622. }
  623. let url = ""
  624. if (this.chat.type == "GROUP") {
  625. url = `/message/group/readed?groupId=${this.chat.targetId}`
  626. } else {
  627. url = `/message/private/readed?friendId=${this.chat.targetId}`
  628. }
  629. this.$http({
  630. url: url,
  631. method: 'PUT'
  632. }).then(() => {
  633. this.chatStore.resetUnreadCount(this.chat)
  634. this.scrollToBottom();
  635. })
  636. },
  637. loadGroup(groupId) {
  638. this.$http({
  639. url: `/group/find/${groupId}`,
  640. method: 'GET'
  641. }).then((group) => {
  642. this.group = group;
  643. this.chatStore.updateChatFromGroup(group);
  644. this.groupStore.updateGroup(group);
  645. });
  646. this.$http({
  647. url: `/group/members/${groupId}`,
  648. method: 'GET'
  649. }).then((groupMembers) => {
  650. this.groupMembers = groupMembers;
  651. });
  652. },
  653. loadFriend(friendId) {
  654. // 获取对方最新信息
  655. this.$http({
  656. url: `/user/find/${friendId}`,
  657. method: 'GET'
  658. }).then((friend) => {
  659. this.friend = friend;
  660. this.chatStore.updateChatFromFriend(friend);
  661. this.friendStore.updateFriend(friend);
  662. })
  663. },
  664. rpxTopx(rpx) {
  665. // rpx转换成px
  666. let info = uni.getSystemInfoSync()
  667. let px = info.windowWidth * rpx / 750;
  668. return Math.floor(rpx);
  669. },
  670. sendMessageRequest(msgInfo) {
  671. return new Promise((resolve, reject) => {
  672. // 请求入队列,防止请求"后发先至",导致消息错序
  673. this.reqQueue.push({ msgInfo, resolve, reject });
  674. this.processReqQueue();
  675. })
  676. },
  677. processReqQueue() {
  678. if (this.reqQueue.length && !this.isSending) {
  679. this.isSending = true;
  680. const reqData = this.reqQueue.shift();
  681. this.$http({
  682. url: this.messageAction,
  683. method: 'post',
  684. data: reqData.msgInfo
  685. }).then((res) => {
  686. reqData.resolve(res)
  687. }).catch((e) => {
  688. reqData.reject(e)
  689. }).finally(() => {
  690. this.isSending = false;
  691. // 发送下一条请求
  692. this.processReqQueue();
  693. })
  694. }
  695. },
  696. reCalChatMainHeight() {
  697. setTimeout(() => {
  698. let h = this.windowHeight;
  699. // 减去标题栏高度
  700. h -= 50;
  701. // 减去键盘高度
  702. if (this.isShowKeyBoard || this.chatTabBox != 'none') {
  703. console.log("减去键盘高度:", this.keyboardHeight)
  704. h -= this.keyboardHeight;
  705. this.scrollToBottom();
  706. }
  707. // #ifndef H5
  708. // h5需要减去状态栏高度
  709. h -= uni.getSystemInfoSync().statusBarHeight;
  710. // #endif
  711. this.chatMainHeight = h;
  712. console.log("窗口高度:", this.chatMainHeight)
  713. if (this.isShowKeyBoard || this.chatTabBox != 'none') {
  714. this.scrollToBottom();
  715. }
  716. // ios浏览器键盘把页面顶起后,页面长度不会变化,这里把页面拉到顶部适配一下
  717. // #ifdef H5
  718. if (uni.getSystemInfoSync().platform == 'ios') {
  719. // 不同手机需要的延时时间不一致,采用分段延时的方式处理
  720. const delays = [50, 100, 500];
  721. delays.forEach((delay) => {
  722. setTimeout(() => {
  723. uni.pageScrollTo({
  724. scrollTop: 0,
  725. duration: 10
  726. });
  727. }, delay);
  728. })
  729. }
  730. // #endif
  731. }, 30)
  732. },
  733. listenKeyBoard() {
  734. // #ifdef H5
  735. const userAgent = navigator.userAgent;
  736. const regex = /(macintosh|windows)/i;
  737. if (regex.test(userAgent)) {
  738. // 电脑端不需要弹出键盘
  739. console.log("userAgent:", userAgent)
  740. return;
  741. }
  742. if (uni.getSystemInfoSync().platform == 'ios') {
  743. // ios h5实现键盘监听
  744. window.addEventListener('focusin', this.focusInListener);
  745. window.addEventListener('focusout', this.focusOutListener);
  746. } else {
  747. // 安卓h5实现键盘监听
  748. let initHeight = window.innerHeight;
  749. window.addEventListener('resize', this.resizeListener);
  750. }
  751. // #endif
  752. // #ifndef H5
  753. // app实现键盘监听
  754. uni.onKeyboardHeightChange(this.keyBoardListener);
  755. // #endif
  756. },
  757. unListenKeyboard() {
  758. // #ifdef H5
  759. // 安卓h5实现键盘监听
  760. window.removeEventListener('resize', this.resizeListener);
  761. window.removeEventListener('focusin', this.focusInListener);
  762. window.removeEventListener('focusout', this.focusOutListener);
  763. // #endif
  764. // #ifndef H5
  765. uni.offKeyboardHeightChange(this.keyBoardListener);
  766. // #endif
  767. },
  768. keyBoardListener(res) {
  769. this.isShowKeyBoard = res.height > 0;
  770. if (this.isShowKeyBoard) {
  771. this.keyboardHeight = res.height; // 获取并保存键盘高度
  772. }
  773. this.reCalChatMainHeight();
  774. },
  775. resizeListener() {
  776. console.log("resize")
  777. let keyboardHeight = this.initHeight - window.innerHeight;
  778. this.isShowKeyBoard = keyboardHeight > 150;
  779. if (this.isShowKeyBoard) {
  780. this.keyboardHeight = keyboardHeight;
  781. }
  782. this.reCalChatMainHeight();
  783. },
  784. focusInListener() {
  785. console.log("focusInListener")
  786. this.isShowKeyBoard = true;
  787. this.reCalChatMainHeight();
  788. },
  789. focusOutListener() {
  790. console.log("focusOutListener")
  791. this.isShowKeyBoard = false;
  792. this.reCalChatMainHeight();
  793. },
  794. showBannedTip() {
  795. let msgInfo = {
  796. tmpId: this.generateId(),
  797. sendId: this.mine.id,
  798. sendTime: new Date().getTime(),
  799. type: this.$enums.MESSAGE_TYPE.TIP_TEXT
  800. }
  801. if (this.chat.type == "PRIVATE") {
  802. msgInfo.recvId = this.mine.id
  803. msgInfo.content = "该用户已被管理员封禁,原因:" + this.friend.reason
  804. } else {
  805. msgInfo.groupId = this.group.id;
  806. msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason
  807. }
  808. this.chatStore.insertMessage(msgInfo, this.chat);
  809. },
  810. generateId() {
  811. // 生成临时id
  812. return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));
  813. }
  814. },
  815. computed: {
  816. mine() {
  817. return this.userStore.userInfo;
  818. },
  819. title() {
  820. if (!this.chat) {
  821. return "";
  822. }
  823. let title = this.chat.showName;
  824. if (this.chat.type == "GROUP") {
  825. let size = this.groupMembers.filter(m => !m.quit).length;
  826. title += `(${size})`;
  827. }
  828. return title;
  829. },
  830. messageAction() {
  831. return `/message/${this.chat.type.toLowerCase()}/send`;
  832. },
  833. messageSize() {
  834. if (!this.chat || !this.chat.messages) {
  835. return 0;
  836. }
  837. return this.chat.messages.length;
  838. },
  839. unreadCount() {
  840. if (!this.chat || !this.chat.unreadCount) {
  841. return 0;
  842. }
  843. return this.chat.unreadCount;
  844. },
  845. isBanned() {
  846. return (this.chat.type == "PRIVATE" && this.friend.isBanned) ||
  847. (this.chat.type == "GROUP" && this.group.isBanned)
  848. },
  849. atUserItems() {
  850. let atUsers = [];
  851. this.atUserIds.forEach((id) => {
  852. if (id == -1) {
  853. atUsers.push({
  854. id: -1,
  855. showNickName: "全体成员"
  856. })
  857. return;
  858. }
  859. let member = this.groupMembers.find((m) => m.userId == id);
  860. if (member) {
  861. atUsers.push(member);
  862. }
  863. })
  864. return atUsers;
  865. }
  866. },
  867. watch: {
  868. messageSize: function(newSize, oldSize) {
  869. // 接收到消息时滚动到底部
  870. if (newSize > oldSize) {
  871. let pages = getCurrentPages();
  872. let curPage = pages[pages.length - 1].route;
  873. if (curPage == "pages/chat/chat-box") {
  874. this.scrollToBottom();
  875. } else {
  876. this.needScrollToBottom = true;
  877. }
  878. }
  879. },
  880. unreadCount: {
  881. handler(newCount, oldCount) {
  882. if (newCount > 0) {
  883. // 消息已读
  884. this.readedMessage()
  885. }
  886. }
  887. }
  888. },
  889. onLoad(options) {
  890. // 聊天数据
  891. this.chat = this.chatStore.chats[options.chatIdx];
  892. // 初始状态只显示20条消息
  893. let size = this.messageSize;
  894. this.showMinIdx = size > 20 ? size - 20 : 0;
  895. // 消息已读
  896. this.readedMessage()
  897. // 加载好友或群聊信息
  898. if (this.chat.type == "GROUP") {
  899. this.loadGroup(this.chat.targetId);
  900. } else {
  901. this.loadFriend(this.chat.targetId);
  902. this.loadReaded(this.chat.targetId)
  903. }
  904. // 激活当前会话
  905. this.chatStore.activeChat(options.chatIdx);
  906. // 复位回执消息
  907. this.isReceipt = false;
  908. // 监听键盘高度
  909. this.listenKeyBoard();
  910. // 计算聊天窗口高度
  911. this.$nextTick(() => {
  912. this.windowHeight = uni.getSystemInfoSync().windowHeight;
  913. this.reCalChatMainHeight()
  914. // 兼容ios h5:禁止页面滚动
  915. // #ifdef H5
  916. this.initHeight = window.innerHeight;
  917. document.body.addEventListener('touchmove', function(e) {
  918. e.preventDefault();
  919. }, { passive: false });
  920. // #endif
  921. });
  922. },
  923. onUnload() {
  924. this.unListenKeyboard();
  925. },
  926. onShow() {
  927. if (this.needScrollToBottom) {
  928. // 页面滚到底部
  929. this.scrollToBottom();
  930. this.needScrollToBottom = false;
  931. }
  932. }
  933. }
  934. </script>
  935. <style lang="scss">
  936. .chat-box {
  937. $icon-color: rgba(0, 0, 0, 0.88);
  938. position: relative;
  939. background-color: #fafafa;
  940. .header {
  941. display: flex;
  942. justify-content: center;
  943. align-items: center;
  944. height: 60rpx;
  945. padding: 5px;
  946. background-color: #fafafa;
  947. line-height: 50px;
  948. font-size: $im-font-size-large;
  949. box-shadow: $im-box-shadow-lighter;
  950. z-index: 1;
  951. .btn-side {
  952. position: absolute;
  953. line-height: 60rpx;
  954. cursor: pointer;
  955. &.right {
  956. right: 30rpx;
  957. }
  958. }
  959. }
  960. .chat-main-box {
  961. // #ifdef H5
  962. top: $im-nav-bar-height;
  963. // #endif
  964. // #ifndef H5
  965. top: calc($im-nav-bar-height + var(--status-bar-height));
  966. // #endif
  967. position: fixed;
  968. width: 100%;
  969. display: flex;
  970. flex-direction: column;
  971. z-index: 2;
  972. .chat-msg {
  973. flex: 1;
  974. padding: 0;
  975. overflow: hidden;
  976. position: relative;
  977. background-color: white;
  978. .scroll-box {
  979. height: 100%;
  980. }
  981. }
  982. .chat-at-bar {
  983. display: flex;
  984. align-items: center;
  985. padding: 0 10rpx;
  986. .icon-at {
  987. font-size: $im-font-size-larger;
  988. color: $im-color-primary;
  989. font-weight: bold;
  990. }
  991. .chat-at-scroll-box {
  992. flex: 1;
  993. width: 80%;
  994. .chat-at-items {
  995. display: flex;
  996. align-items: center;
  997. height: 70rpx;
  998. .chat-at-item {
  999. padding: 0 3rpx;
  1000. }
  1001. }
  1002. }
  1003. }
  1004. .send-bar {
  1005. display: flex;
  1006. align-items: center;
  1007. padding: 10rpx;
  1008. border-top: $im-border solid 1px;
  1009. background-color: $im-bg;
  1010. min-height: 80rpx;
  1011. margin-bottom: 14rpx;
  1012. .iconfont {
  1013. font-size: 60rpx;
  1014. margin: 0 10rpx;
  1015. color: $icon-color;
  1016. }
  1017. .chat-record {
  1018. flex: 1;
  1019. }
  1020. .send-text {
  1021. flex: 1;
  1022. overflow: auto;
  1023. padding: 14rpx 20rpx;
  1024. background-color: #fff;
  1025. border-radius: 8rpx;
  1026. font-size: $im-font-size;
  1027. box-sizing: border-box;
  1028. margin: 0 10rpx;
  1029. position: relative;
  1030. .send-text-area {
  1031. width: 100%;
  1032. height: 100%;
  1033. min-height: 40rpx;
  1034. max-height: 200rpx;
  1035. font-size: 30rpx;
  1036. }
  1037. }
  1038. .btn-send {
  1039. margin: 5rpx;
  1040. }
  1041. }
  1042. }
  1043. .chat-tab-bar {
  1044. position: fixed;
  1045. bottom: 0;
  1046. background-color: $im-bg;
  1047. .chat-tools {
  1048. display: flex;
  1049. flex-wrap: wrap;
  1050. align-items: top;
  1051. height: 310px;
  1052. padding: 40rpx;
  1053. box-sizing: border-box;
  1054. .chat-tools-item {
  1055. width: 25%;
  1056. padding: 16rpx;
  1057. box-sizing: border-box;
  1058. display: flex;
  1059. flex-direction: column;
  1060. align-items: center;
  1061. .tool-icon {
  1062. padding: 26rpx;
  1063. font-size: 54rpx;
  1064. border-radius: 20%;
  1065. background-color: white;
  1066. color: $icon-color;
  1067. &:active {
  1068. background-color: $im-bg-active;
  1069. }
  1070. }
  1071. .tool-name {
  1072. height: 60rpx;
  1073. line-height: 60rpx;
  1074. font-size: 28rpx;
  1075. }
  1076. }
  1077. }
  1078. .chat-emotion {
  1079. height: 310px;
  1080. padding: 20rpx;
  1081. box-sizing: border-box;
  1082. .emotion-item-list {
  1083. display: flex;
  1084. flex-wrap: wrap;
  1085. justify-content: space-between;
  1086. align-content: center;
  1087. .emotion-item {
  1088. text-align: center;
  1089. cursor: pointer;
  1090. padding: 5px;
  1091. }
  1092. }
  1093. }
  1094. }
  1095. }
  1096. </style>