chat-box.vue 36 KB

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