index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <template>
  2. <view class="im-v">
  3. <view @click="hideDrawer">
  4. <mescroll-body ref="mescrollRef" bottom="50%" @init="mescrollInit" :down="downOption" @down="downCallback"
  5. :up="upOption">
  6. <!-- 无更多消息 -->
  7. <view v-if="isEnd && !isMsgList" class="msg-end">没有更多消息了</view>
  8. <view v-if="isMsgList" class="notData-box u-flex-col">
  9. <view class="u-flex-col notData-inner">
  10. <image :src="icon" class="iconImg"></image>
  11. <text class="notData-inner-text">暂无聊天记录</text>
  12. </view>
  13. </view>
  14. <view class="msg-list">
  15. <!-- 消息列表 (必须配置id,以便定位) -->
  16. <view class="msg-list-item" v-for="(msg,index) in msgList" :key="index" :id="'msg'+msg.id"
  17. :class="userId === msg.sendUserId ? 'msg-list-item-r' : 'msg-list-item-l'">
  18. <view class="avatar" v-if="userId === msg.sendUserId">
  19. <u-avatar :src="baseURL+userInfoHeadIcon" size="80"></u-avatar>
  20. </view>
  21. <view class="avatar" v-else>
  22. <u-avatar :src="baseURL+headIcon" size="80"></u-avatar>
  23. </view>
  24. <!-- 文字,表情 -->
  25. <view class="msg-text" v-if="msg.contentType==='text'">
  26. <view v-for="(item,i) in msg.msgContent" :key="i">
  27. <text class="msg-text-txt" v-if="item.type=='text'">{{item.content}}</text>
  28. <image class="msg-text-emoji" :src="item.content" v-if="item.type=='emjio'" />
  29. </view>
  30. </view>
  31. <!-- 图片消息 -->
  32. <view v-if="msg.contentType=='image'" class="msg-img" @click="showPic(msg.msgContent.path)">
  33. <image lazy-load="true" :src="msg.msgContent.path"
  34. :style="{'width': msg.msgContent.width+'px','height': msg.msgContent.height+'px'}">
  35. </image>
  36. </view>
  37. <!-- 语言消息 -->
  38. <view v-if="msg.contentType==='voice'" class="msg-text msg-voice" @click="playVoice(msg)"
  39. :class="playMsgid == msg.id?'play':''">
  40. <view class="length">{{msg.msgContent.length}}</view>
  41. <view class="icon my-voice"></view>
  42. </view>
  43. </view>
  44. </view>
  45. </mescroll-body>
  46. </view>
  47. <!-- 抽屉栏 -->
  48. <view class="input-box" :class="popupLayerClass" @touchmove.stop.prevent="discard">
  49. <view class="input-box-icon icon biaoqing" @click="chooseEmoji"></view>
  50. <!-- #ifndef H5 || APP-HARMONY -->
  51. <view class="input-box-icon icon" :class="isVoice?'jianpan':'yuyin'" @click="switchVoice"></view>
  52. <!-- #endif -->
  53. <view class="voice-mode" :class="[isVoice?'':'hidden',recording?'recording':'']" @touchstart="voiceBegin"
  54. @touchmove.stop.prevent="voiceIng" @touchend="voiceEnd" @touchcancel="voiceCancel">{{voiceTis}}</view>
  55. <view class="text-mode" v-if="!isVoice">
  56. <view class="input-area">
  57. <textarea auto-height :cursor-spacing="8" maxlength="500" v-model="textMsg" @focus="textareaFocus"
  58. :focus="textFocus" />
  59. </view>
  60. </view>
  61. <view class="input-box-icon icon add" @click="openMore"></view>
  62. <view class="send-btn" @click="sendText" v-if="!isVoice">发送</view>
  63. </view>
  64. <view class="popup-layer u-border-top" :class="popupLayerClass" @touchmove.stop.prevent="discard">
  65. <swiper class="emoji-swiper" indicator-dots="true" duration="150" v-show="showEmoji">
  66. <swiper-item v-for="(page,pid) in emojiTree" :key="pid">
  67. <view v-for="(em,eid) in page" :key="eid" @click="addEmoji(em)" class="emoji-item">
  68. <image mode="widthFix" :src="getEmojiUrl(em.url)" class="emoji-item-img"></image>
  69. </view>
  70. </swiper-item>
  71. </swiper>
  72. <view class="more-layer" v-show="showMore">
  73. <view class="list">
  74. <view class="box" @click="chooseImage('album')">
  75. <text class="icon tupian2"></text>
  76. </view>
  77. <!-- #ifndef H5 -->
  78. <view class="box" @click="chooseImage('camera')">
  79. <text class="icon paizhao"></text>
  80. </view>
  81. <!-- #endif -->
  82. </view>
  83. </view>
  84. </view>
  85. <!-- 录音UI效果 -->
  86. <view class="record" :class="recording?'':'hidden'">
  87. <view class="ing" :class="willStop?'hidden':''">
  88. <view class="icon luyin2"></view>
  89. </view>
  90. <view class="cancel" :class="willStop?'':'hidden'">
  91. <view class="icon chehui"></view>
  92. </view>
  93. <view class="tis" :class="willStop?'change':''">{{recordTis}}</view>
  94. </view>
  95. </view>
  96. </template>
  97. <script>
  98. import chat from '@/libs/chat.js'
  99. import {
  100. emojiList,
  101. emojiTree,
  102. imagesMap
  103. } from './emoji.js'
  104. import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
  105. import {
  106. mapGetters
  107. } from 'vuex'
  108. import resources from '@/libs/resources.js'
  109. import {
  110. useChatStore
  111. } from '@/store/modules/chat'
  112. const chatStore = useChatStore()
  113. //播放语音相关参数
  114. const AUDIO = uni.createInnerAudioContext()
  115. export default {
  116. mixins: [MescrollMixin],
  117. name: 'im',
  118. data() {
  119. return {
  120. icon: resources.message.nodata,
  121. formUserId: '',
  122. headIcon: '',
  123. name: '',
  124. downOption: {
  125. auto: true,
  126. },
  127. upOption: {
  128. use: false,
  129. toTop: {
  130. src: ''
  131. }
  132. },
  133. currentPage: 1,
  134. pageSize: 30,
  135. //录音相关参数
  136. // #ifndef H5
  137. //H5不能录音
  138. RECORDER: uni.getRecorderManager(),
  139. // #endif
  140. playMsgid: null,
  141. popupLayerClass: '',
  142. textFocus: false,
  143. showMore: false,
  144. showEmoji: false,
  145. emojiList,
  146. emojiTree,
  147. msgList: [],
  148. isEnd: false,
  149. isMsgList: true,
  150. isVoice: false,
  151. voiceTis: '按住 说话',
  152. recordTis: "手指上滑 取消发送",
  153. recording: false,
  154. willStop: false,
  155. initPoint: {
  156. identifier: 0,
  157. Y: 0
  158. },
  159. recordTimer: null,
  160. recordLength: 0,
  161. textMsg: '',
  162. msgImageList: [],
  163. userId: '',
  164. userInfoHeadIcon: ''
  165. }
  166. },
  167. computed: {
  168. baseURL() {
  169. return this.define.baseURL
  170. }
  171. },
  172. watch: {},
  173. onLoad(option) {
  174. let userInfo = uni.getStorageSync('userInfo')
  175. this.userId = userInfo.userId
  176. this.userInfoHeadIcon = userInfo.headIcon
  177. this.formUserId = option.formUserId;
  178. this.headIcon = option.headIcon;
  179. this.name = option.name;
  180. uni.$on('getMessageList', data => {
  181. this.getMessageList(data)
  182. })
  183. uni.$on('addMsg', data => {
  184. this.addMsg(data)
  185. })
  186. chatStore.setFormUserId(this.formUserId)
  187. const updateReadMessage = {
  188. method: "UpdateReadMessage",
  189. formUserId: this.formUserId,
  190. token: uni.getStorageSync('token')
  191. }
  192. chat.sendMsg(JSON.stringify(updateReadMessage))
  193. uni.setNavigationBarTitle({
  194. title: option.name
  195. });
  196. //语音自然播放结束
  197. // #ifndef MP-ALIPAY
  198. AUDIO.onEnded((res) => {
  199. this.playMsgid = null;
  200. });
  201. // #endif
  202. // #ifndef H5 || MP-ALIPAY
  203. //录音开始事件
  204. this.RECORDER.onStart((e) => {
  205. this.recordBegin(e);
  206. });
  207. //录音结束事件
  208. this.RECORDER.onStop((e) => {
  209. this.recordEnd(e);
  210. });
  211. // #endif
  212. },
  213. onUnload() {
  214. uni.$off('getMessageList')
  215. uni.$off('addMsg')
  216. chatStore.setFormUserId('')
  217. // #ifndef MP-ALIPAY
  218. AUDIO.stop();
  219. // #endif
  220. },
  221. methods: {
  222. getMessageList(data) {
  223. let msgImageList = []
  224. const list = data.list.map(o => {
  225. if (o.contentType === 'image') {
  226. if (o.content) {
  227. let content = {}
  228. if (typeof(o.content) === 'string') {
  229. content = JSON.parse(o.content)
  230. } else {
  231. content = o.content
  232. }
  233. msgImageList.push(this.baseURL + content.path)
  234. }
  235. }
  236. return this.dealMsg(o)
  237. })
  238. this.msgImageList = [...msgImageList, ...this.msgImageList]
  239. let topMsg = this.msgList[0]
  240. this.msgList = [...list, ...this.msgList]
  241. if (this.msgList.length) this.isMsgList = false
  242. if (data.list.length < data.pagination.pageSize) {
  243. this.mescroll.lockDownScroll(true)
  244. this.isEnd = true
  245. }
  246. this.$nextTick(() => {
  247. if (this.currentPage <= 2) {
  248. this.mescroll.scrollTo(99999, 0)
  249. } else if (topMsg) {
  250. let view = uni.createSelectorQuery().select('#msg' + topMsg.id);
  251. view.boundingClientRect(v => {
  252. this.mescroll.scrollTo(v.top - 100, 0)
  253. }).exec();
  254. }
  255. })
  256. },
  257. downCallback() {
  258. const messageList = {
  259. method: "MessageList",
  260. toUserId: this.formUserId,
  261. formUserId: this.userId,
  262. token: uni.getStorageSync('token'),
  263. currentPage: this.currentPage,
  264. pageSize: this.pageSize,
  265. sord: "desc"
  266. }
  267. chat.sendMsg(JSON.stringify(messageList))
  268. this.currentPage++;
  269. this.mescroll.endSuccess();
  270. },
  271. discard() {
  272. return;
  273. },
  274. switchVoice() {
  275. this.hideDrawer();
  276. this.isVoice = !this.isVoice;
  277. },
  278. openMore() {
  279. if (this.showMore) return this.hideDrawer()
  280. this.showMore = true;
  281. this.showEmoji = false;
  282. this.openDrawer();
  283. },
  284. openDrawer() {
  285. this.isVoice = false;
  286. this.popupLayerClass = 'showLayer';
  287. },
  288. hideDrawer() {
  289. this.popupLayerClass = '';
  290. setTimeout(() => {
  291. this.showMore = false;
  292. this.showEmoji = false;
  293. }, 150);
  294. },
  295. textareaFocus() {
  296. this.hideDrawer();
  297. },
  298. chooseEmoji() {
  299. if (this.showEmoji) return this.hideDrawer()
  300. this.showMore = false;
  301. this.showEmoji = true;
  302. this.openDrawer();
  303. },
  304. addEmoji(em) {
  305. this.textMsg += em.alt;
  306. },
  307. getEmojiUrl(url) {
  308. return imagesMap[url.replace('.', '')]
  309. },
  310. chooseImage(type) {
  311. uni.chooseImage({
  312. // #ifdef H5
  313. count: 1,
  314. // #endif
  315. sourceType: [type], //从相册选择
  316. success: (res) => {
  317. this.hideDrawer();
  318. if (res.tempFilePaths.length) res.tempFilePaths.map(o => (this.uploadFile(o)))
  319. }
  320. });
  321. },
  322. /* 上传图片 */
  323. uploadFile(files) {
  324. uni.uploadFile({
  325. url: this.define.comUploadUrl + 'IM',
  326. filePath: files,
  327. name: 'file',
  328. header: {
  329. Authorization: uni.getStorageSync('token') || ''
  330. },
  331. success: (uploadFileRes) => {
  332. const response = uploadFileRes.data ? JSON.parse(uploadFileRes
  333. .data) : {}
  334. if (uploadFileRes.statusCode !== 200) return this.$u.toast(
  335. response.msg)
  336. if (!response.data || !response.data.name) return
  337. const name = response.data.name
  338. this.getImageInfo(files, name)
  339. }
  340. })
  341. },
  342. /* 获取图片信息 */
  343. getImageInfo(files, name) {
  344. uni.getImageInfo({
  345. src: files,
  346. success: (image) => {
  347. let msg = {
  348. name,
  349. width: image.width,
  350. height: image.height,
  351. };
  352. this.sendMessage(msg, 'image');
  353. }
  354. })
  355. },
  356. addMsg(data) {
  357. if (data.method === 'receiveMessage') {
  358. const updateReadMessage = {
  359. method: "UpdateReadMessage",
  360. formUserId: this.formUserId,
  361. token: uni.getStorageSync('token')
  362. }
  363. chat.sendMsg(JSON.stringify(updateReadMessage))
  364. }
  365. data.id = this.$u.guid()
  366. if (data.contentType === "text") {
  367. data.msgContent = this.replaceEmoji(data.content)
  368. }
  369. if (data.contentType === "image") {
  370. this.msgImageList.push(this.baseURL + data.content.path)
  371. data.msgContent = this.setPicSize(data.content)
  372. data.msgContent.path = this.baseURL + data.content.path
  373. }
  374. if (data.contentType === "voice") {
  375. data.msgContent = data.content
  376. }
  377. this.msgList.push(data)
  378. this.$nextTick(() => {
  379. this.mescroll.scrollTo(99999, 0)
  380. })
  381. },
  382. dealMsg(item) {
  383. if (item.contentType === "text") {
  384. item.msgContent = this.replaceEmoji(item.content)
  385. }
  386. if (item.contentType === "image") {
  387. item.msgContent = this.setPicSize(JSON.parse(item.content))
  388. item.msgContent.path = this.baseURL + item.msgContent.path
  389. }
  390. if (item.contentType === "voice") {
  391. item.msgContent = JSON.parse(item.content)
  392. }
  393. return item
  394. },
  395. sendText() {
  396. if (!this.textMsg) return
  397. this.hideDrawer()
  398. this.sendMessage(this.textMsg, 'text')
  399. this.textMsg = ''
  400. },
  401. sendMessage(content, type) {
  402. const messageObj = {
  403. method: "SendMessage",
  404. token: uni.getStorageSync('token'),
  405. toUserId: this.formUserId,
  406. messageType: type,
  407. messageContent: content
  408. }
  409. chat.sendMsg(JSON.stringify(messageObj))
  410. this.isMsgList = false
  411. },
  412. voiceBegin(e) { // 录音开始
  413. this.RECORDER.stop();
  414. if (e.touches.length > 1) {
  415. return;
  416. }
  417. this.initPoint.Y = e.touches[0].clientY;
  418. this.initPoint.identifier = e.touches[0].identifier;
  419. // #ifdef APP-HARMONY
  420. this.RECORDER.start(); //录音开始,
  421. // #endif
  422. // #ifndef APP-HARMONY
  423. this.RECORDER.start({
  424. format: "mp3"
  425. }); //录音开始,
  426. // #endif
  427. },
  428. recordBegin(e) { //录音开始UI效果
  429. this.recording = true;
  430. this.voiceTis = '松开 结束';
  431. this.recordLength = 0;
  432. this.recordTimer = setInterval(() => {
  433. this.recordLength++;
  434. }, 1000)
  435. },
  436. voiceCancel() { // 录音被打断
  437. this.recording = false;
  438. this.voiceTis = '按住 说话';
  439. this.recordTis = '手指上滑 取消发送'
  440. this.willStop = true; //不发送录音
  441. this.RECORDER.stop(); //录音结束
  442. },
  443. voiceIng(e) { // 录音中(判断是否触发上滑取消发送)
  444. if (!this.recording) return
  445. let touche = e.touches[0];
  446. // #ifndef APP-HARMONY
  447. let voice = uni.upx2px(100)
  448. // #endif
  449. // #ifdef APP-HARMONY
  450. let voice = 100 / 2
  451. // #endif
  452. //上滑一个导航栏的高度触发上滑取消发送
  453. if (this.initPoint.Y - touche.clientY >= voice) {
  454. this.willStop = true;
  455. this.recordTis = '松开手指 取消发送'
  456. } else {
  457. this.willStop = false;
  458. this.recordTis = '手指上滑 取消发送'
  459. }
  460. },
  461. voiceEnd(e) { // 结束录音
  462. if (!this.recording) return
  463. this.recording = false;
  464. this.voiceTis = '按住 说话';
  465. this.recordTis = '手指上滑 取消发送'
  466. this.RECORDER.stop(); //录音结束
  467. },
  468. recordEnd(e) { //录音结束(回调文件)
  469. if (!this.willStop) {
  470. let min = parseInt(this.recordLength / 60);
  471. let sec = this.recordLength % 60;
  472. min = min < 10 ? '0' + min : min;
  473. sec = sec < 10 ? '0' + sec : sec;
  474. if (sec < '01') {
  475. this.willStop = true;
  476. this.$u.toast('说话时间太短');
  477. return
  478. }
  479. uni.uploadFile({
  480. url: this.define.comUploadUrl + 'IM',
  481. filePath: e.tempFilePath,
  482. name: 'file',
  483. header: {
  484. Authorization: uni.getStorageSync('token') || ''
  485. },
  486. success: (uploadFileRes) => {
  487. const handleUploadResponse = (uploadFileRes, min, sec) => {
  488. const response = (uploadFileRes.data && JSON.parse(uploadFileRes.data)) ||
  489. {};
  490. if (uploadFileRes.statusCode !== 200) {
  491. this.$u.toast(response.msg || '上传失败,未知错误');
  492. return;
  493. }
  494. const {
  495. data = {}
  496. } = response;
  497. if (!data.name) {
  498. this.$u.toast('上传的文件信息不完整');
  499. return;
  500. }
  501. const msg = {
  502. name: data.name,
  503. length: `${min}:${sec}`
  504. };
  505. this.sendMessage(msg, 'voice');
  506. };
  507. handleUploadResponse(uploadFileRes, min, sec);
  508. }
  509. })
  510. } else {
  511. // console.log('取消发送录音');
  512. }
  513. this.willStop = false;
  514. },
  515. setPicSize(content) { //处理图片尺寸,如果不处理宽高,新进入页面加载图片时候会闪
  516. // 让图片最长边等于设置的最大长度,短边等比例缩小,图片控件真实改变,区别于aspectFit方式。
  517. // #ifndef APP-HARMONY
  518. let maxW = uni.upx2px(350); //350是定义消息图片最大宽度
  519. let maxH = uni.upx2px(350); //350是定义消息图片最大高度
  520. // #endif
  521. // #ifdef APP-HARMONY
  522. let maxW = 350 / 2; //350是定义消息图片最大宽度
  523. let maxH = 350 / 2; //350是定义消息图片最大高度
  524. // #endif
  525. if (content.width > maxW || content.height > maxH) {
  526. let scale = content.width / content.height;
  527. content.width = scale > 1 ? maxW : maxH * scale;
  528. content.height = scale > 1 ? maxW / scale : maxH;
  529. }
  530. return content;
  531. },
  532. replaceEmoji(str) { //替换表情符号为图片
  533. let replacedStr = str.replace(/\[([^(\]|\[)]*)\]/g, item => 'jnpfjnpf' + item + 'jnpfjnpf');
  534. let strArr = replacedStr.split(/jnpfjnpfjnpfjnpf|jnpfjnpf/g)
  535. strArr = strArr.filter(o => o)
  536. let contentList = []
  537. for (let i = 0; i < strArr.length; i++) {
  538. let item = {
  539. content: strArr[i],
  540. type: 'emjio'
  541. }
  542. if (/\[([^(\]|\[)]*)\]/.test(strArr[i])) {
  543. let content = ''
  544. for (let j = 0; j < this.emojiList.length; j++) {
  545. let row = this.emojiList[j];
  546. if (row.alt == strArr[i]) {
  547. content = this.getEmojiUrl(row.url)
  548. break
  549. }
  550. }
  551. item = {
  552. content: content,
  553. type: 'emjio'
  554. }
  555. } else {
  556. item = {
  557. content: strArr[i],
  558. type: 'text'
  559. }
  560. }
  561. contentList.push(item)
  562. }
  563. return contentList
  564. },
  565. showPic(path) { // 预览图片
  566. uni.previewImage({
  567. indicator: "none",
  568. current: path,
  569. urls: this.msgImageList
  570. });
  571. },
  572. playVoice(msg) { // 播放语音
  573. AUDIO.stop();
  574. AUDIO.src = this.baseURL + msg.msgContent.path;
  575. if (this.playMsgid != null && this.playMsgid == msg.id) {
  576. this.$nextTick(() => {
  577. AUDIO.stop();
  578. });
  579. this.playMsgid = null;
  580. } else {
  581. this.$nextTick(() => {
  582. AUDIO.play();
  583. });
  584. this.playMsgid = msg.id;
  585. }
  586. },
  587. }
  588. }
  589. </script>
  590. <style lang="scss">
  591. @import "./index.scss";
  592. </style>