chat-box.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. <template>
  2. <view class="page chat-box">
  3. <nav-bar back more @more="onShowMore">{{ title }}</nav-bar>
  4. <view class="chat-msg" @click="switchChatTabBox('none', true)">
  5. <scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
  6. :scroll-into-view="'chat-item-' + scrollMsgIdx">
  7. <view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
  8. <chat-message-item v-if="idx >= showMinIdx" :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)"
  9. :showName="showName(msgInfo)" @recall="onRecallMessage" @copy="onCopyMessage"
  10. @delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile"
  11. :id="'chat-item-' + idx" :msgInfo="msgInfo" :groupMembers="groupMembers">
  12. </chat-message-item>
  13. </view>
  14. </scroll-view>
  15. </view>
  16. <view v-if="atUserIds.length > 0" class="chat-at-bar" @click="openAtBox()">
  17. <view class="iconfont icon-at">:&nbsp;</view>
  18. <scroll-view v-if="atUserIds.length > 0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">
  19. <view class="chat-at-items">
  20. <view v-for="m in atUserItems" class="chat-at-item">
  21. <head-image :name="m.showNickName" :url="m.headImage" size="minier"></head-image>
  22. </view>
  23. </view>
  24. </scroll-view>
  25. </view>
  26. <view class="send-bar">
  27. <view v-if="!showRecord" class="iconfont icon-voice-circle" @click="onRecorderInput()"></view>
  28. <view v-else class="iconfont icon-keyboard" @click="onKeyboardInput()"></view>
  29. <chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record>
  30. <view v-else class="send-text">
  31. <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
  32. :placeholder="isReceipt ? '[回执消息]' : ''" :adjust-position="false" @confirm="sendTextMessage()"
  33. @keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold
  34. :hold-keyboard="true"></textarea>
  35. </view>
  36. <view v-if="chat && chat.type == 'GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
  37. <view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view>
  38. <view v-if="sendText == ''" class="iconfont icon-add" @click="onShowToolsChatTab()">
  39. </view>
  40. <button v-if="sendText != '' || atUserIds.length > 0" class="btn-send" type="primary"
  41. @touchend.prevent="sendTextMessage()" size="mini">发送</button>
  42. </view>
  43. <view class="chat-tab-bar" v-show="chatTabBox != 'none' || (showKeyBoard && !isH5)"
  44. :style="{ height: `${keyboardHeight}px` }">
  45. <view v-if="chatTabBox == 'tools'" class="chat-tools">
  46. <view class="chat-tools-item">
  47. <image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
  48. :onSuccess="onUploadImageSuccess" :onError="onUploadImageFail">
  49. <view class="tool-icon iconfont icon-picture"></view>
  50. </image-upload>
  51. <view class="tool-name">相册</view>
  52. </view>
  53. <view class="chat-tools-item">
  54. <image-upload sourceType="camera" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
  55. :onError="onUploadImageFail">
  56. <view class="tool-icon iconfont icon-camera"></view>
  57. </image-upload>
  58. <view class="tool-name">拍摄</view>
  59. </view>
  60. <view class="chat-tools-item">
  61. <file-upload :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
  62. :onError="onUploadFileFail">
  63. <view class="tool-icon iconfont icon-folder"></view>
  64. </file-upload>
  65. <view class="tool-name">文件</view>
  66. </view>
  67. <view class="chat-tools-item" @click="onRecorderInput()">
  68. <view class="tool-icon iconfont icon-microphone"></view>
  69. <view class="tool-name">语音消息</view>
  70. </view>
  71. <view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="switchReceipt()">
  72. <view class="tool-icon iconfont icon-receipt" :class="isReceipt ? 'active' : ''"></view>
  73. <view class="tool-name">回执消息</view>
  74. </view>
  75. <!-- #ifndef MP-WEIXIN -->
  76. <!-- 音视频不支持小程序 -->
  77. <view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onPriviteVideo()">
  78. <view class="tool-icon iconfont icon-video"></view>
  79. <view class="tool-name">视频通话</view>
  80. </view>
  81. <view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onPriviteVoice()">
  82. <view class="tool-icon iconfont icon-call"></view>
  83. <view class="tool-name">语音通话</view>
  84. </view>
  85. <view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="onGroupVideo()">
  86. <view class="tool-icon iconfont icon-call"></view>
  87. <view class="tool-name">语音通话</view>
  88. </view>
  89. <!-- #endif -->
  90. </view>
  91. <scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true">
  92. <view class="emotion-item-list">
  93. <image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
  94. v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
  95. lazy-load="true"></image>
  96. </view>
  97. </scroll-view>
  98. <view v-if="showKeyBoard"></view>
  99. </view>
  100. <!-- @用户时选择成员 -->
  101. <chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers"
  102. @complete="onAtComplete"></chat-at-box>
  103. <!-- 群语音通话时选择成员 -->
  104. <!-- #ifndef MP-WEIXIN -->
  105. <group-member-selector ref="selBox" :members="groupMembers" :maxSize="configStore.webrtc.maxChannel"
  106. @complete="onInviteOk"></group-member-selector>
  107. <group-rtc-join ref="rtcJoin" :groupId="group.id"></group-rtc-join>
  108. <!-- #endif -->
  109. </view>
  110. </template>
  111. <script>
  112. import UNI_APP from '@/.env.js';
  113. export default {
  114. data() {
  115. return {
  116. chat: {},
  117. friend: {},
  118. group: {},
  119. groupMembers: [],
  120. sendText: "",
  121. isReceipt: false, // 是否回执消息
  122. scrollMsgIdx: 0, // 滚动条定位为到哪条消息
  123. chatTabBox: 'none',
  124. showKeyBoard: false,
  125. showRecord: false,
  126. keyboardHeight: 322,
  127. atUserIds: [],
  128. needScrollToBottom: false, // 需要滚动到底部
  129. showMinIdx: 0, // 下标小于showMinIdx的消息不显示,否则可能很卡
  130. reqQueue: [], // 请求队列
  131. isSending: false, // 是否正在发送请求
  132. isH5: false // h5键盘会强制推起页面,有些地方要区别对待
  133. }
  134. },
  135. methods: {
  136. onRecorderInput() {
  137. this.showRecord = true;
  138. this.switchChatTabBox('none', true);
  139. },
  140. onKeyboardInput() {
  141. this.showRecord = false;
  142. this.switchChatTabBox('none', false);
  143. },
  144. onSendRecord(data) {
  145. let msgInfo = {
  146. content: JSON.stringify(data),
  147. type: this.$enums.MESSAGE_TYPE.AUDIO,
  148. receipt: this.isReceipt
  149. }
  150. // 填充对方id
  151. this.fillTargetId(msgInfo, this.chat.targetId);
  152. this.sendMessageRequest(msgInfo).then((m) => {
  153. m.selfSend = true;
  154. this.chatStore.insertMessage(m);
  155. // 会话置顶
  156. this.moveChatToTop();
  157. // 滚动到底部
  158. this.scrollToBottom();
  159. this.isReceipt = false;
  160. })
  161. },
  162. onRtCall(msgInfo) {
  163. if (msgInfo.type == this.$enums.MESSAGE_TYPE.ACT_RT_VOICE) {
  164. this.onPriviteVoice();
  165. } else if (msgInfo.type == this.$enums.MESSAGE_TYPE.ACT_RT_VIDEO) {
  166. this.onPriviteVideo();
  167. }
  168. },
  169. onPriviteVideo() {
  170. const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
  171. uni.navigateTo({
  172. url: `/pages/chat/chat-private-video?mode=video&friend=${friendInfo}&isHost=true`
  173. })
  174. },
  175. onPriviteVoice() {
  176. const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
  177. uni.navigateTo({
  178. url: `/pages/chat/chat-private-video?mode=voice&friend=${friendInfo}&isHost=true`
  179. })
  180. },
  181. onGroupVideo() {
  182. // 邀请成员发起通话
  183. let ids = [this.mine.id];
  184. this.$refs.selBox.init(ids, ids);
  185. this.$refs.selBox.open();
  186. },
  187. onInviteOk(ids) {
  188. if (ids.length < 2) {
  189. return;
  190. }
  191. let users = [];
  192. ids.forEach(id => {
  193. let m = this.groupMembers.find(m => m.userId == id);
  194. // 只取部分字段,压缩url长度
  195. users.push({
  196. id: m.userId,
  197. nickName: m.showNickName,
  198. headImage: m.headImage,
  199. isCamera: false,
  200. isMicroPhone: true
  201. })
  202. })
  203. const groupId = this.group.id;
  204. const inviterId = this.mine.id;
  205. const userInfos = encodeURIComponent(JSON.stringify(users));
  206. uni.navigateTo({
  207. url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=true
  208. &inviterId=${inviterId}&userInfos=${userInfos}`
  209. })
  210. },
  211. moveChatToTop() {
  212. let chatIdx = this.chatStore.findChatIdx(this.chat);
  213. this.chatStore.moveTop(chatIdx);
  214. },
  215. switchReceipt() {
  216. this.isReceipt = !this.isReceipt;
  217. },
  218. openAtBox() {
  219. this.$refs.atBox.init(this.atUserIds);
  220. this.$refs.atBox.open();
  221. },
  222. onAtComplete(atUserIds) {
  223. this.atUserIds = atUserIds;
  224. },
  225. onLongPressHead(msgInfo) {
  226. if (!msgInfo.selfSend && this.chat.type == "GROUP" && this.atUserIds.indexOf(msgInfo.sendId) < 0) {
  227. this.atUserIds.push(msgInfo.sendId);
  228. }
  229. },
  230. headImage(msgInfo) {
  231. if (this.chat.type == 'GROUP') {
  232. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  233. return member ? member.headImage : "";
  234. } else {
  235. return msgInfo.selfSend ? this.mine.headImageThumb : this.chat.headImage
  236. }
  237. },
  238. showName(msgInfo) {
  239. if (this.chat.type == 'GROUP') {
  240. let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
  241. return member ? member.showNickName : "";
  242. } else {
  243. return msgInfo.selfSend ? this.mine.nickName : this.chat.showName
  244. }
  245. },
  246. sendTextMessage() {
  247. if (!this.sendText.trim() && this.atUserIds.length == 0) {
  248. return uni.showToast({
  249. title: "不能发送空白信息",
  250. icon: "none"
  251. });
  252. }
  253. let receiptText = this.isReceipt ? "【回执消息】" : "";
  254. let atText = this.createAtText();
  255. let msgInfo = {
  256. content: receiptText + this.sendText + atText,
  257. atUserIds: this.atUserIds,
  258. receipt: this.isReceipt,
  259. type: 0
  260. }
  261. this.sendText = "";
  262. // 填充对方id
  263. this.fillTargetId(msgInfo, this.chat.targetId);
  264. this.sendMessageRequest(msgInfo).then((m) => {
  265. m.selfSend = true;
  266. this.chatStore.insertMessage(m);
  267. // 会话置顶
  268. this.moveChatToTop();
  269. }).finally(() => {
  270. // 滚动到底部
  271. this.scrollToBottom();
  272. // 清空@用户列表
  273. this.atUserIds = [];
  274. this.isReceipt = false;
  275. });
  276. },
  277. createAtText() {
  278. let atText = "";
  279. this.atUserIds.forEach((id) => {
  280. if (id == -1) {
  281. atText += ` @全体成员`;
  282. } else {
  283. let member = this.groupMembers.find((m) => m.userId == id);
  284. if (member) {
  285. atText += ` @${member.showNickName}`;
  286. }
  287. }
  288. })
  289. return atText;
  290. },
  291. fillTargetId(msgInfo, targetId) {
  292. if (this.chat.type == "GROUP") {
  293. msgInfo.groupId = targetId;
  294. } else {
  295. msgInfo.recvId = targetId;
  296. }
  297. },
  298. scrollToBottom() {
  299. let size = this.messageSize;
  300. if (size > 0) {
  301. this.scrollToMsgIdx(size - 1);
  302. }
  303. },
  304. scrollToMsgIdx(idx) {
  305. // 如果scrollMsgIdx值没变化,滚动条不会移动
  306. if (idx == this.scrollMsgIdx && idx > 0) {
  307. this.$nextTick(() => {
  308. // 先滚动到上一条
  309. this.scrollMsgIdx = idx - 1;
  310. // 再滚动目标位置
  311. this.scrollToMsgIdx(idx);
  312. });
  313. return;
  314. }
  315. this.$nextTick(() => {
  316. this.scrollMsgIdx = idx;
  317. });
  318. },
  319. onShowEmoChatTab() {
  320. this.showRecord = false;
  321. this.switchChatTabBox('emo', true)
  322. },
  323. onShowToolsChatTab() {
  324. this.showRecord = false;
  325. this.switchChatTabBox('tools', true)
  326. },
  327. switchChatTabBox(chatTabBox, hideKeyBoard) {
  328. this.chatTabBox = chatTabBox;
  329. if (hideKeyBoard) {
  330. uni.hideKeyboard();
  331. this.showKeyBoard = false;
  332. }
  333. },
  334. selectEmoji(emoText) {
  335. this.sendText += `#${emoText};`;
  336. },
  337. onKeyboardheightchange(e) {
  338. if (e.detail.height > 0) {
  339. this.showKeyBoard = true;
  340. this.switchChatTabBox('none', false)
  341. this.keyboardHeight = this.rpxTopx(e.detail.height);
  342. this.scrollToBottom();
  343. } else {
  344. this.showKeyBoard = false;
  345. }
  346. },
  347. onUploadImageBefore(file) {
  348. let data = {
  349. originUrl: file.path,
  350. thumbUrl: file.path
  351. }
  352. let msgInfo = {
  353. id: 0,
  354. tmpId: this.generateId(),
  355. fileId: file.uid,
  356. sendId: this.mine.id,
  357. content: JSON.stringify(data),
  358. sendTime: new Date().getTime(),
  359. selfSend: true,
  360. type: this.$enums.MESSAGE_TYPE.IMAGE,
  361. readedCount: 0,
  362. loadStatus: "loading",
  363. status: this.$enums.MESSAGE_STATUS.UNSEND
  364. }
  365. // 填充对方id
  366. this.fillTargetId(msgInfo, this.chat.targetId);
  367. // 插入消息
  368. this.chatStore.insertMessage(msgInfo);
  369. // 会话置顶
  370. this.moveChatToTop();
  371. // 借助file对象保存
  372. file.msgInfo = msgInfo;
  373. // 滚到最低部
  374. this.scrollToBottom();
  375. return true;
  376. },
  377. onUploadImageSuccess(file, res) {
  378. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  379. msgInfo.content = JSON.stringify(res.data);
  380. msgInfo.receipt = this.isReceipt
  381. this.sendMessageRequest(msgInfo).then((m) => {
  382. msgInfo.loadStatus = 'ok';
  383. msgInfo.id = m.id;
  384. this.isReceipt = false;
  385. this.chatStore.insertMessage(msgInfo);
  386. })
  387. },
  388. onUploadImageFail(file, err) {
  389. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  390. msgInfo.loadStatus = 'fail';
  391. this.chatStore.insertMessage(msgInfo);
  392. },
  393. onUploadFileBefore(file) {
  394. let data = {
  395. name: file.name,
  396. size: file.size,
  397. url: file.path
  398. }
  399. let msgInfo = {
  400. id: 0,
  401. tmpId: this.generateId(),
  402. sendId: this.mine.id,
  403. content: JSON.stringify(data),
  404. sendTime: new Date().getTime(),
  405. selfSend: true,
  406. type: this.$enums.MESSAGE_TYPE.FILE,
  407. readedCount: 0,
  408. loadStatus: "loading",
  409. status: this.$enums.MESSAGE_STATUS.UNSEND
  410. }
  411. // 填充对方id
  412. this.fillTargetId(msgInfo, this.chat.targetId);
  413. // 插入消息
  414. this.chatStore.insertMessage(msgInfo);
  415. // 会话置顶
  416. this.moveChatToTop();
  417. // 借助file对象保存
  418. file.msgInfo = msgInfo;
  419. // 滚到最低部
  420. this.scrollToBottom();
  421. return true;
  422. },
  423. onUploadFileSuccess(file, res) {
  424. let data = {
  425. name: file.name,
  426. size: file.size,
  427. url: res.data
  428. }
  429. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  430. msgInfo.content = JSON.stringify(data);
  431. msgInfo.receipt = this.isReceipt
  432. this.sendMessageRequest(msgInfo).then((m) => {
  433. msgInfo.loadStatus = 'ok';
  434. msgInfo.id = m.id;
  435. this.isReceipt = false;
  436. this.chatStore.insertMessage(msgInfo);
  437. })
  438. },
  439. onUploadFileFail(file, res) {
  440. let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
  441. msgInfo.loadStatus = 'fail';
  442. this.chatStore.insertMessage(msgInfo);
  443. },
  444. onDeleteMessage(msgInfo) {
  445. uni.showModal({
  446. title: '删除消息',
  447. content: '确认删除消息?',
  448. success: (res) => {
  449. if (!res.cancel) {
  450. this.chatStore.deleteMessage(msgInfo);
  451. uni.showToast({
  452. title: "删除成功",
  453. icon: "none"
  454. })
  455. }
  456. }
  457. })
  458. },
  459. onRecallMessage(msgInfo) {
  460. uni.showModal({
  461. title: '撤回消息',
  462. content: '确认撤回消息?',
  463. success: (res) => {
  464. if (!res.cancel) {
  465. let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`
  466. this.$http({
  467. url: url,
  468. method: 'DELETE'
  469. }).then(() => {
  470. msgInfo = JSON.parse(JSON.stringify(msgInfo));
  471. msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL;
  472. msgInfo.content = '你撤回了一条消息';
  473. msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
  474. this.chatStore.insertMessage(msgInfo);
  475. })
  476. }
  477. }
  478. })
  479. },
  480. onCopyMessage(msgInfo) {
  481. uni.setClipboardData({
  482. data: msgInfo.content,
  483. success: () => {
  484. uni.showToast({ title: '已复制', icon: 'none' });
  485. },
  486. fail: () => {
  487. uni.showToast({ title: '复制失败', icon: 'none' });
  488. }
  489. });
  490. },
  491. onDownloadFile(msgInfo) {
  492. let url = JSON.parse(msgInfo.content).url;
  493. uni.downloadFile({
  494. url: url,
  495. success(res) {
  496. if (res.statusCode === 200) {
  497. var filePath = encodeURI(res.tempFilePath);
  498. uni.openDocument({
  499. filePath: filePath,
  500. showMenu: true
  501. });
  502. }
  503. },
  504. fail(e) {
  505. uni.showToast({
  506. title: "文件下载失败",
  507. icon: "none"
  508. })
  509. }
  510. });
  511. },
  512. onScrollToTop() {
  513. if (this.showMinIdx == 0) {
  514. console.log("消息已滚动到顶部")
  515. return;
  516. }
  517. // #ifndef H5
  518. // 防止滚动条定格在顶部,不能一直往上滚
  519. this.scrollToMsgIdx(this.showMinIdx);
  520. // #endif
  521. // 多展示0条信息
  522. this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
  523. },
  524. onShowMore() {
  525. if (this.chat.type == "GROUP") {
  526. uni.navigateTo({
  527. url: "/pages/group/group-info?id=" + this.group.id
  528. })
  529. } else {
  530. uni.navigateTo({
  531. url: "/pages/common/user-info?id=" + this.friend.id
  532. })
  533. }
  534. },
  535. onTextInput(e) {
  536. let idx = e.detail.cursor - 1;
  537. if (this.chat.type == 'GROUP' && e.detail.value[idx] == '@') {
  538. this.openAtBox();
  539. let sendText = e.detail.value.replace("@", '');
  540. this.$nextTick(() => {
  541. this.sendText = sendText;
  542. })
  543. }
  544. },
  545. loadReaded(fid) {
  546. this.$http({
  547. url: `/message/private/maxReadedId?friendId=${fid}`,
  548. method: 'get'
  549. }).then((id) => {
  550. this.chatStore.readedMessage({
  551. friendId: fid,
  552. maxId: id
  553. });
  554. });
  555. },
  556. readedMessage() {
  557. if (this.unreadCount == 0) {
  558. return;
  559. }
  560. let url = ""
  561. if (this.chat.type == "GROUP") {
  562. url = `/message/group/readed?groupId=${this.chat.targetId}`
  563. } else {
  564. url = `/message/private/readed?friendId=${this.chat.targetId}`
  565. }
  566. this.$http({
  567. url: url,
  568. method: 'PUT'
  569. }).then(() => {
  570. this.chatStore.resetUnreadCount(this.chat)
  571. this.scrollToBottom();
  572. })
  573. },
  574. loadGroup(groupId) {
  575. this.$http({
  576. url: `/group/find/${groupId}`,
  577. method: 'GET'
  578. }).then((group) => {
  579. this.group = group;
  580. this.chatStore.updateChatFromGroup(group);
  581. this.groupStore.updateGroup(group);
  582. });
  583. this.$http({
  584. url: `/group/members/${groupId}`,
  585. method: 'GET'
  586. }).then((groupMembers) => {
  587. this.groupMembers = groupMembers;
  588. });
  589. },
  590. loadFriend(friendId) {
  591. // 获取对方最新信息
  592. this.$http({
  593. url: `/user/find/${friendId}`,
  594. method: 'GET'
  595. }).then((friend) => {
  596. this.friend = friend;
  597. this.chatStore.updateChatFromFriend(friend);
  598. this.friendStore.updateFriend(friend);
  599. })
  600. },
  601. rpxTopx(rpx) {
  602. // px转换成rpx
  603. let info = uni.getSystemInfoSync()
  604. let px = info.windowWidth * rpx / 750;
  605. return Math.floor(rpx);
  606. },
  607. sendMessageRequest(msgInfo) {
  608. return new Promise((resolve, reject) => {
  609. // 请求入队列,防止请求"后发先至",导致消息错序
  610. this.reqQueue.push({ msgInfo, resolve, reject });
  611. this.processReqQueue();
  612. })
  613. },
  614. processReqQueue() {
  615. if (this.reqQueue.length && !this.isSending) {
  616. this.isSending = true;
  617. const reqData = this.reqQueue.shift();
  618. this.$http({
  619. url: this.messageAction,
  620. method: 'post',
  621. data: reqData.msgInfo
  622. }).then((res) => {
  623. reqData.resolve(res)
  624. }).catch((e) => {
  625. reqData.reject(e)
  626. }).finally(() => {
  627. this.isSending = false;
  628. // 发送下一条请求
  629. this.processReqQueue();
  630. })
  631. }
  632. },
  633. listenKeyBoardForH5() {
  634. // 由于H5无法触发TextArea的@keyboardheightchange事件,所以通过
  635. // 监听屏幕高度变化来实现键盘监听
  636. let initHeight = window.innerHeight;
  637. window.addEventListener('resize', () => {
  638. let keyboardHeight = initHeight - window.innerHeight;
  639. if (keyboardHeight > 0) {
  640. this.keyboardHeight = keyboardHeight - 20;
  641. this.showKeyBoard = true;
  642. this.switchChatTabBox('none', false)
  643. this.scrollToBottom();
  644. } else {
  645. this.showKeyBoard = false;
  646. }
  647. });
  648. },
  649. generateId() {
  650. // 生成临时id
  651. return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));
  652. }
  653. },
  654. computed: {
  655. mine() {
  656. return this.userStore.userInfo;
  657. },
  658. title() {
  659. if (!this.chat) {
  660. return "";
  661. }
  662. let title = this.chat.showName;
  663. if (this.chat.type == "GROUP") {
  664. let size = this.groupMembers.filter(m => !m.quit).length;
  665. title += `(${size})`;
  666. }
  667. return title;
  668. },
  669. messageAction() {
  670. return `/message/${this.chat.type.toLowerCase()}/send`;
  671. },
  672. messageSize() {
  673. if (!this.chat || !this.chat.messages) {
  674. return 0;
  675. }
  676. return this.chat.messages.length;
  677. },
  678. unreadCount() {
  679. if (!this.chat || !this.chat.unreadCount) {
  680. return 0;
  681. }
  682. return this.chat.unreadCount;
  683. },
  684. atUserItems() {
  685. let atUsers = [];
  686. this.atUserIds.forEach((id) => {
  687. if (id == -1) {
  688. atUsers.push({
  689. id: -1,
  690. showNickName: "全体成员"
  691. })
  692. return;
  693. }
  694. let member = this.groupMembers.find((m) => m.userId == id);
  695. if (member) {
  696. atUsers.push(member);
  697. }
  698. })
  699. return atUsers;
  700. }
  701. },
  702. watch: {
  703. messageSize: function (newSize, oldSize) {
  704. // 接收到消息时滚动到底部
  705. if (newSize > oldSize) {
  706. let pages = getCurrentPages();
  707. let curPage = pages[pages.length - 1].route;
  708. if (curPage == "pages/chat/chat-box") {
  709. this.scrollToBottom();
  710. } else {
  711. this.needScrollToBottom = true;
  712. }
  713. }
  714. },
  715. unreadCount: {
  716. handler(newCount, oldCount) {
  717. if (newCount > 0) {
  718. // 消息已读
  719. this.readedMessage()
  720. }
  721. }
  722. }
  723. },
  724. onLoad(options) {
  725. // #ifdef H5
  726. this.isH5 = true;
  727. this.listenKeyBoardForH5();
  728. // #endif
  729. // 聊天数据
  730. this.chat = this.chatStore.chats[options.chatIdx];
  731. // 初始状态只显示20条消息
  732. let size = this.messageSize;
  733. this.showMinIdx = size > 20 ? size - 20 : 0;
  734. // 消息已读
  735. this.readedMessage()
  736. // 加载好友或群聊信息
  737. if (this.chat.type == "GROUP") {
  738. this.loadGroup(this.chat.targetId);
  739. } else {
  740. this.loadFriend(this.chat.targetId);
  741. this.loadReaded(this.chat.targetId)
  742. }
  743. // 激活当前会话
  744. this.chatStore.activeChat(options.chatIdx);
  745. // 复位回执消息
  746. this.isReceipt = false;
  747. },
  748. onShow() {
  749. if (this.needScrollToBottom) {
  750. // 页面滚到底部
  751. this.scrollToBottom();
  752. this.needScrollToBottom = false;
  753. }
  754. }
  755. }
  756. </script>
  757. <style lang="scss" scoped>
  758. .chat-box {
  759. position: relative;
  760. display: flex;
  761. flex-direction: column;
  762. .header {
  763. display: flex;
  764. justify-content: center;
  765. align-items: center;
  766. height: 60rpx;
  767. padding: 5px;
  768. background-color: #f9f9f9;
  769. line-height: 50px;
  770. font-size: $im-font-size-large;
  771. box-shadow: $im-box-shadow-lighter;
  772. z-index: 1;
  773. .btn-side {
  774. position: absolute;
  775. line-height: 60rpx;
  776. cursor: pointer;
  777. &.right {
  778. right: 30rpx;
  779. }
  780. }
  781. }
  782. .chat-msg {
  783. flex: 1;
  784. padding: 0;
  785. overflow: hidden;
  786. position: relative;
  787. background-color: white;
  788. .scroll-box {
  789. height: 100%;
  790. }
  791. }
  792. .chat-at-bar {
  793. display: flex;
  794. align-items: center;
  795. padding: 0 10rpx;
  796. .icon-at {
  797. font-size: $im-font-size-larger;
  798. color: $im-color-primary;
  799. font-weight: bold;
  800. }
  801. .chat-at-scroll-box {
  802. flex: 1;
  803. width: 80%;
  804. .chat-at-items {
  805. display: flex;
  806. align-items: center;
  807. height: 70rpx;
  808. .chat-at-item {
  809. padding: 0 3rpx;
  810. }
  811. }
  812. }
  813. }
  814. $icon-color: rgba(0, 0, 0, 0.88);
  815. .send-bar {
  816. display: flex;
  817. align-items: center;
  818. padding: 10rpx;
  819. //margin-bottom: 10rpx;
  820. border-top: $im-border solid 1px;
  821. background-color: $im-bg;
  822. height: 80rpx;
  823. //box-shadow: $im-box-shadow-lighter;
  824. z-index: 1;
  825. .iconfont {
  826. font-size: 60rpx;
  827. margin: 0 10rpx;
  828. color: $icon-color;
  829. }
  830. .chat-record {
  831. flex: 1;
  832. }
  833. .send-text {
  834. flex: 1;
  835. overflow: auto;
  836. padding: 14rpx 20rpx;
  837. background-color: #fff;
  838. border-radius: 8rpx;
  839. font-size: $im-font-size;
  840. box-sizing: border-box;
  841. margin: 0 10rpx;
  842. .send-text-area {
  843. width: 100%;
  844. }
  845. }
  846. .btn-send {
  847. margin: 5rpx;
  848. }
  849. }
  850. .chat-tab-bar {
  851. height: 500rpx;
  852. padding: 20rpx;
  853. background-color: $im-bg;
  854. .chat-tools {
  855. display: flex;
  856. flex-wrap: wrap;
  857. padding-top: 20rpx;
  858. .chat-tools-item {
  859. width: 25%;
  860. padding: 16rpx;
  861. box-sizing: border-box;
  862. display: flex;
  863. flex-direction: column;
  864. align-items: center;
  865. .tool-icon {
  866. padding: 26rpx;
  867. font-size: 54rpx;
  868. border-radius: 20%;
  869. background-color: white;
  870. color: $icon-color;
  871. &:active {
  872. background-color: $im-bg-active;
  873. }
  874. }
  875. .tool-name {
  876. height: 60rpx;
  877. line-height: 60rpx;
  878. font-size: 28rpx;
  879. }
  880. }
  881. }
  882. .chat-emotion {
  883. height: 100%;
  884. .emotion-item-list {
  885. display: flex;
  886. flex-wrap: wrap;
  887. justify-content: space-between;
  888. .emotion-item {
  889. width: 34px;
  890. height: 34px;
  891. text-align: center;
  892. cursor: pointer;
  893. padding: 6px;
  894. }
  895. }
  896. }
  897. }
  898. }
  899. </style>