index.vue 17 KB

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