chat.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. <!-- 会话区 -->
  2. <template>
  3. <view class="chatInterface">
  4. <view class="scroll-view">
  5. <view class="all-history-loaded">
  6. {{allHistoryLoaded ? '已经没有更多的历史消息' : '下拉获取历史消息'}}
  7. </view>
  8. <!--已经收到的消息-->
  9. <view v-for="(message,index) in messages" :key="message.messageId">
  10. <!--时间显示,类似于微信,隔5分钟不发言,才显示时间-->
  11. <view class="time-lag">
  12. {{renderMessageDate(message, index)}}
  13. </view>
  14. <view class="message-item" :class="{'self' : message.senderId == (currentUser && currentUser.uuid)}">
  15. <view style="border-radius: 100%;overflow: hidden;" class="avatar"
  16. v-if="message.senderId != (currentUser && currentUser.uuid)">
  17. <image :src="targetUser.avatar||'../../static/user.png'"></image>
  18. </view>
  19. <view style="border-radius: 100%;overflow: hidden;" class="avatar" v-else>
  20. <image :src="currentUser.avatar||'../../static/user.png'"></image>
  21. </view>
  22. <!-- 文字 -->
  23. <view v-if="message.type == 'text'"
  24. :class="message.payload.text.indexOf('点击查看招聘详情')!==-1?'content content2':'content'">
  25. <view v-if="message.type == 'text'">
  26. <!-- 当发送招聘链接时显示 -->
  27. <view style="" v-if="message.payload.text.indexOf('点击查看招聘详情')!==-1">
  28. <navigator style=""
  29. :url="'/pages/order/detail?id='+message.payload.text.slice(message.payload.text.indexOf('点击查看招聘详情')+8,message.payload.text.length)">
  30. <view>{{message.payload.text.slice(0, message.payload.text.indexOf('点击查看招聘详情'))}}
  31. </view>
  32. <view style="color: #777777;">点击进入详情</view>
  33. </navigator>
  34. </view>
  35. <!-- 否则 -->
  36. <view v-else v-html="renderTextMessage(message)"></view>
  37. </view>
  38. </view>
  39. <!-- 非文字 -->
  40. <view v-else class="content">
  41. <!-- <b class="pending" v-if="message.status == 'sending'"></b>
  42. <b class="send-fail" v-if="message.status == 'fail'"></b> -->
  43. <image class="image-content" v-if="message.type == 'image'" :src="message.payload.url"
  44. :data-url="message.payload.url" @click="showImageFullScreen" mode="widthFix"></image>
  45. <view class="video-snapshot" v-if="message.type == 'video'"
  46. :data-url="message.payload.video.url" @click="playVideo">
  47. <image :src="message.payload.thumbnail.url" mode="aspectFit"></image>
  48. <view class="video-play-icon"></view>
  49. </view>
  50. <GoEasyAudioPlayer v-if="message.type =='audio'" :src="message.payload.url"
  51. :duration="message.payload.duration" />
  52. </view>
  53. </view>
  54. </view>
  55. <view style="height: 100upx;"></view>
  56. </view>
  57. <view :class="[audio.visible ? 'record-icon record-open':'record-icon']" @click="switchAudioKeyboard"></view>
  58. <view class="action-box" v-if="!video.visible">
  59. <view style="display: flex;justify-content: space-between;">
  60. <view v-if="id!=''" class="chat_item">
  61. <!-- <view class="file-icon more-icon" @click="showMore">图片/视频/语音</view> -->
  62. <!-- <view class="file-icon more-icon"
  63. style="margin-left: 20upx;background-image: url('../../static/images_go/call.png');"
  64. @click="safeCall">
  65. 安全拨号</view> -->
  66. <view class="file-icon more-icon"
  67. style="margin-left: 0upx;background-image: url('../../static/images_go/order2.png');"
  68. @click="sendTextMessage('detail')">
  69. 发送当前招聘</view>
  70. </view>
  71. <view v-if="more.show" class="chat_item">
  72. <view style="margin-right: 40upx;" @click="hidMore">收起</view>
  73. </view>
  74. </view>
  75. <view style="padding-bottom: 40upx;display: flex;align-items: center;padding-top: 15upx;"
  76. class="action-top">
  77. <view style="margin-left: 30upx;" class="message-input">
  78. <!-- GoEasyIM最大支持3k的文本消息,如需发送长文本,需调整输入框maxlength值 -->
  79. <input @confirm="sendTextMessage" style="width: 480upx;" type="text" maxlength="700"
  80. placeholder="发送消息" v-model="content" @focus="messageInputFocusin">
  81. </view>
  82. <span class="send-message-btn" @click="sendTextMessage('message')">发送</span>
  83. <image @click="showMore" style="width: 55upx;height: 55upx;margin-left: 10upx;"
  84. src="../../static/images_go/more.png">
  85. </image>
  86. </view>
  87. <!--展示表情列表-->
  88. <view class="action-bottom" v-if="emoji.show" style="justify-content: space-around">
  89. <image class="emoji-item" v-for="(emojiItem, emojiKey, index) in emoji.map" :key="index"
  90. :src="emoji.url + emojiItem" @click="selectEmoji(emojiKey)"></image>
  91. </view>
  92. <!--更多-->
  93. <view class="action-bottom" v-if="more.show">
  94. <view class="more-item" @click="sendImage">
  95. <image src="../../static/images_go/tupian.png"></image>
  96. <text>图片</text>
  97. </view>
  98. <view class="more-item" @click="sendVideo">
  99. <image src="../../static/images_go/shipin.png"></image>
  100. <text>视频</text>
  101. </view>
  102. <view @touchstart="onRecordStart" @touchend="onRecordEnd" class="more-item">
  103. <image src="../../static/images_go/voice.png"></image>
  104. <text>{{audio.recording ? '松开发送' : '按住录音'}}</text>
  105. </view>
  106. </view>
  107. </view>
  108. <view class="record-loading" v-if="audio.recording"></view>
  109. <video v-if="video.visible" :src="video.url" id="videoPlayer"
  110. @fullscreenchange="onVideoFullScreenChange"></video>
  111. </view>
  112. </template>
  113. <script>
  114. import GoEasyAudioPlayer from "../../components/GoEasyAudioPlayer/GoEasyAudioPlayer";
  115. import EmojiDecoder from "../../lib/EmojiDecoder";
  116. import commonData from '../../commonData.js'
  117. import commonFun from '../../commonFun.js'
  118. const recorderManager = uni.getRecorderManager();
  119. export default {
  120. name: "privateChat",
  121. components: {
  122. GoEasyAudioPlayer,
  123. },
  124. data() {
  125. let emojiUrl = 'https://imgcache.qq.com/open/qcloud/tim/assets/emoji/';
  126. let emojiMap = {
  127. '[么么哒]': 'emoji_3@2x.png',
  128. '[乒乓]': 'emoji_4@2x.png',
  129. '[便便]': 'emoji_5@2x.png',
  130. '[信封]': 'emoji_6@2x.png',
  131. '[偷笑]': 'emoji_7@2x.png',
  132. '[傲慢]': 'emoji_8@2x.png'
  133. };
  134. return {
  135. biz_title: '',
  136. id: '',
  137. //聊天文本框
  138. content: '',
  139. friend: null,
  140. currentUser: null,
  141. targetUser: {},
  142. //已经接收到的消息
  143. messages: [],
  144. //已经加载完所有历史消息
  145. allHistoryLoaded: false,
  146. //定义表情列表
  147. emoji: {
  148. url: emojiUrl,
  149. map: emojiMap,
  150. show: false,
  151. decoder: new EmojiDecoder(emojiUrl, emojiMap),
  152. },
  153. more: { //更多按钮
  154. show: false
  155. },
  156. audio: {
  157. //语音录音中
  158. recording: false,
  159. //录音按钮展示
  160. visible: true
  161. },
  162. video: {
  163. visible: false,
  164. url: '',
  165. context: null
  166. },
  167. sys_role: commonData.sys_role, //角色
  168. time1: 0,
  169. time2: 0,
  170. is_rec: false,
  171. }
  172. },
  173. onReady() {
  174. this.video.context = uni.createVideoContext('videoPlayer', this);
  175. // https://uniapp.dcloud.io/api/ui/navigationbar?id=setnavigationbartitle
  176. },
  177. onShow() {
  178. this.loadMoreHistoryMessage();
  179. this.more.show = false;
  180. this.emoji.show = false;
  181. },
  182. onLoad(options) {
  183. // console.log('options')
  184. // console.log(options)
  185. this.id = options.id || ''
  186. this.biz_title = options.title || ''
  187. // 初始化
  188. this.user_info = (uni.getStorageSync('USER_INFO'))
  189. commonFun.init_go_easy(this.user_info, this.sys_role == 0 ? 'user_' : 'mer_')
  190. let imService = getApp().globalData.imService;
  191. // 自己个人信息
  192. this.currentUser = uni.getStorageSync('currentUser');
  193. // 对方个人信息
  194. this.targetUser = uni.getStorageSync('targetUser');
  195. // console.log('打印对方信息')
  196. // console.log(this.targetUser)
  197. uni.setNavigationBarTitle({
  198. title: this.targetUser.name
  199. });
  200. //聊天对象
  201. let friendId = options.to;
  202. //从服务器获取最新的好友信息
  203. this.friend = {
  204. uuid: friendId,
  205. name: this.targetUser.name,
  206. avatar: this.targetUser.avatar,
  207. };
  208. // console.log('onLoad friend - ', this.friend);
  209. this.messages = imService.getPrivateMessages(friendId);
  210. //监听新消息
  211. imService.onNewPrivateMessageReceive = (friendId, message) => {
  212. if (friendId === this.friend.uuid) {
  213. //聊天时,收到消息标记为已读
  214. this.markPrivateMessageAsRead(friendId);
  215. //收到新消息,是滚动到最底部
  216. this.scrollToBottom();
  217. }
  218. };
  219. //每次进入聊天页面,总是滚动到底部
  220. this.scrollToBottom();
  221. // 录音监听器
  222. this.initRecorderListeners();
  223. //收到的消息设置为已读
  224. if (this.messages.length !== 0) {
  225. this.markPrivateMessageAsRead(friendId);
  226. }
  227. // 首次从招聘进入聊天界面提示弹窗
  228. if (this.id) {
  229. let that = this
  230. setTimeout(() => {
  231. let has_chated_ids = uni.getStorageSync('has_chated_ids')
  232. console.log('has_chated_ids')
  233. console.log(has_chated_ids)
  234. if (has_chated_ids == '' || has_chated_ids.indexOf(that.id) == -1) {
  235. uni.showModal({
  236. title: '发送当前招聘?',
  237. success: (res) => {
  238. if (res.confirm) {
  239. that.sendTextMessage('detail')
  240. }
  241. if (has_chated_ids == '') {
  242. has_chated_ids = [that.id]
  243. } else {
  244. has_chated_ids.push(that.id)
  245. }
  246. uni.setStorageSync('has_chated_ids', has_chated_ids)
  247. }
  248. })
  249. }
  250. }, 500)
  251. }
  252. },
  253. onPullDownRefresh(e) {
  254. this.loadMoreHistoryMessage();
  255. },
  256. onUnload() {
  257. //退出聊天页面之前,清空页面传入的监听器
  258. let imService = getApp().globalData.imService;
  259. if (imService) {
  260. imService.onNewPrivateMessageReceive = (friendId, message) => {};
  261. }
  262. },
  263. methods: {
  264. // 安全拨号
  265. safeCall() {
  266. let uuid = this.friend.uuid
  267. let type = 1
  268. if (uuid.indexOf('user_') == 0) {
  269. type = 0
  270. uuid = uuid.slice(5, uuid.length)
  271. } else {
  272. uuid = uuid.slice(4, uuid.length)
  273. }
  274. this.makePhoneCallSafe(type, uuid)
  275. },
  276. //渲染文本消息,如果包含表情,替换为图片
  277. //todo:本不需要该方法,可以在标签里完成,但小程序有兼容性问题,被迫这样实现
  278. renderTextMessage(message) {
  279. return '<span class="text-content">' + this.emoji.decoder.decode(message.payload.text) + '</span>'
  280. },
  281. //像微信那样显示时间,如果有几分钟没发消息了,才显示时间
  282. //todo:本不需要该方法,可以在标签里完成,但小程序有兼容性问题,被迫这样实现
  283. renderMessageDate(message, index) {
  284. if (index === 0) {
  285. return commonFun.formatDate(message.timestamp)
  286. } else {
  287. if (message.timestamp - this.messages[index - 1].timestamp > 5 * 60 * 1000) {
  288. return commonFun.formatDate(message.timestamp)
  289. }
  290. }
  291. return '';
  292. },
  293. initRecorderListeners() {
  294. console.log('录音开始前置')
  295. // 监听录音开始
  296. let that = this
  297. recorderManager.onStart(res => {
  298. if (!that.is_rec) {
  299. console.log('不执行开始')
  300. return
  301. }
  302. console.log(that.time1, that.time2)
  303. console.log('录音开始异步')
  304. console.log(res)
  305. this.audio.recording = true;
  306. });
  307. //录音结束后,发送
  308. recorderManager.onStop((res) => {
  309. let that = this
  310. console.log('录音结束异步')
  311. console.log(that.time2 - that.time1)
  312. if (that.time2 - that.time1 == 0) {
  313. console.log('不执行结束')
  314. return
  315. }
  316. this.audio.recording = false;
  317. let audioMessage = commonFun.goEasy.im.createAudioMessage({
  318. to: {
  319. id: this.friend.uuid,
  320. type: commonFun.GoEasy.IM_SCENE.PRIVATE,
  321. data: {
  322. name: this.friend.name,
  323. avatar: this.friend.avatar
  324. }
  325. },
  326. file: res,
  327. onProgress: function(progress) {
  328. console.log('录音结束后,发送')
  329. console.log(progress)
  330. },
  331. notification: {
  332. title: this.currentUser.name + '发来一段语音',
  333. body: '[语音消息]' // 字段最长 50 字符
  334. }
  335. });
  336. this.sendMessage(audioMessage);
  337. });
  338. // 监听录音报错
  339. recorderManager.onError((res) => {
  340. uni.showModal({
  341. content: '录音报错',
  342. showCancel: false,
  343. success: () => {
  344. console.log("录音报错:", res);
  345. this.audio.recording = false;
  346. }
  347. })
  348. })
  349. },
  350. sendMessage(message) {
  351. let toId = message.to.id;
  352. let imService = getApp().globalData.imService;
  353. let localHistory = imService.getPrivateMessages(toId);
  354. localHistory.push(message);
  355. commonFun.goEasy.im.sendMessage({
  356. message: message,
  357. onSuccess: function(message) {
  358. console.log("内容发送成功.", message);
  359. },
  360. onFailed: function(error) {
  361. console.log("内容发送失败:", error);
  362. }
  363. });
  364. },
  365. sendTextMessage(type = 'detail') { //发送消息
  366. let biz_title = this.biz_title
  367. if ((this.content.trim() !== '' && type != 'detail') || type == 'detail') {
  368. let body = type == 'detail' ? biz_title + ' 点击查看招聘详情' + this.id : this.content;
  369. if (this.content.length >= 50) {
  370. body = this.content.substring(0, 30) + "...";
  371. }
  372. if (type != 'detail') {
  373. if (commonFun.is_contain_sensitive_words(this.content)) {
  374. uni.showModal({
  375. content: '请不要发送包括手机号在内等敏感信息',
  376. showCancel: false
  377. })
  378. return
  379. }
  380. }
  381. // console.log(this.friend.name, this.friend.avatar)
  382. let textMessage = commonFun.goEasy.im.createTextMessage({
  383. text: type == 'detail' ? biz_title + ' 点击查看招聘详情' + this.id : this.content,
  384. to: {
  385. id: this.friend.uuid,
  386. type: commonFun.GoEasy.IM_SCENE.PRIVATE,
  387. data: {
  388. name: this.friend.name,
  389. avatar: this.friend.avatar
  390. }
  391. },
  392. notification: {
  393. title: this.currentUser.name + '发来一段文字',
  394. body: body
  395. }
  396. });
  397. // console.log('textMessage')
  398. // console.log(this.friend.uuid, this.friend.name, this.friend.avatar)
  399. this.sendMessage(textMessage);
  400. }
  401. this.scrollToBottom();
  402. this.content = "";
  403. },
  404. loadMoreHistoryMessage() { //历史消息
  405. let self = this;
  406. let lastMessageTimeStamp = Date.now();
  407. let lastMessage = this.messages[0];
  408. if (lastMessage) {
  409. lastMessageTimeStamp = lastMessage.timestamp;
  410. }
  411. let currentLength = this.messages.length;
  412. commonFun.goEasy.im.history({
  413. userId: self.friend.uuid,
  414. lastTimestamp: lastMessageTimeStamp,
  415. onSuccess: function(result) {
  416. //获取本地记录
  417. let imService = getApp().globalData.imService;
  418. let localHistory = imService.getPrivateMessages(self.friend.uuid);
  419. //添加加载的记录到本地记录尾部
  420. let messages = result.content;
  421. for (let i = messages.length - 1; i >= 0; i--) {
  422. localHistory.unshift(messages[i]);
  423. }
  424. if (localHistory.length === currentLength) {
  425. self.allHistoryLoaded = true;
  426. }
  427. self.messages = localHistory;
  428. uni.stopPullDownRefresh();
  429. },
  430. onFailed: function(error) {
  431. //获取失败
  432. if (error.code === 401) {
  433. console.log("获取历史消息失败,默认不开通,付费应用,可以在我的应用->查看详情,高级功能里自助开通");
  434. } else {
  435. console.log("获取历史消息失败, code:" + error.code + ",错误信息:" + error.content);
  436. }
  437. uni.stopPullDownRefresh();
  438. }
  439. });
  440. },
  441. //语音录制按钮和键盘输入的切换
  442. switchAudioKeyboard() {
  443. // this.audio.visible = !this.audio.visible;
  444. if (uni.authorize) {
  445. uni.authorize({
  446. scope: 'scope.record'
  447. })
  448. }
  449. },
  450. onRecordStart() {
  451. this.is_rec = true
  452. this.time1 = Date.parse(new Date())
  453. console.log('onRecordStart')
  454. try {
  455. recorderManager.start();
  456. } catch (e) {
  457. uni.showModal({
  458. title: '录音错误',
  459. content: '请在app和小程序端体验录音,Uni官方明确H5不支持getRecorderManager, 详情查看Uni官方文档'
  460. });
  461. }
  462. },
  463. onRecordEnd() {
  464. this.is_rec = false
  465. this.time2 = Date.parse(new Date())
  466. console.log(this.audio)
  467. console.log('onRecordEnd')
  468. try {
  469. recorderManager.stop();
  470. } catch (e) {
  471. console.log(e)
  472. }
  473. this.more.show = false;
  474. },
  475. sendVideo() { //发送文件
  476. // return
  477. uni.chooseVideo({
  478. success: (res) => {
  479. if (res.duration > 60) {
  480. uni.showModal({
  481. content: '视频文件不得超过60s',
  482. showCancel: false
  483. })
  484. return
  485. }
  486. let videoMessage = commonFun.goEasy.im.createVideoMessage({
  487. to: {
  488. id: this.friend.uuid,
  489. type: commonFun.GoEasy.IM_SCENE.PRIVATE,
  490. data: {
  491. name: this.friend.name,
  492. avatar: this.friend.avatar
  493. }
  494. },
  495. file: res,
  496. onProgress: function(progress) {
  497. console.log(progress)
  498. },
  499. notification: {
  500. title: this.currentUser.name + '发来一个视频',
  501. body: '[视频消息]' // 字段最长 50 字符
  502. }
  503. });
  504. this.sendMessage(videoMessage);
  505. }
  506. })
  507. },
  508. sendImage() {
  509. // return
  510. uni.chooseImage({
  511. count: 1,
  512. success: (res) => {
  513. let imageMessage = commonFun.goEasy.im.createImageMessage({
  514. to: {
  515. id: this.friend.uuid,
  516. type: commonFun.GoEasy.IM_SCENE.PRIVATE,
  517. data: {
  518. name: this.friend.name,
  519. avatar: this.friend.avatar
  520. }
  521. },
  522. file: res,
  523. onProgress: function(progress) {
  524. console.log(progress)
  525. },
  526. notification: {
  527. title: this.currentUser.name + '发来一张图片',
  528. body: '[图片消息]' // 字段最长 50 字符
  529. }
  530. });
  531. this.sendMessage(imageMessage);
  532. }
  533. });
  534. },
  535. showImageFullScreen(e) {
  536. var imagesUrl = [e.currentTarget.dataset.url];
  537. uni.previewImage({
  538. urls: imagesUrl
  539. });
  540. },
  541. playVideo(e) {
  542. this.video.visible = true;
  543. this.video.url = e.currentTarget.dataset.url;
  544. this.$nextTick(() => {
  545. this.video.context.requestFullScreen({
  546. direction: 0
  547. });
  548. this.video.context.play();
  549. });
  550. },
  551. onVideoFullScreenChange(e) {
  552. //当退出全屏播放时,隐藏播放器
  553. if (this.video.visible && !e.detail.fullScreen) {
  554. this.video.visible = false;
  555. this.video.context.stop();
  556. }
  557. },
  558. messageInputFocusin() {
  559. this.more.show = false;
  560. this.emoji.show = false
  561. },
  562. showEmoji() {
  563. this.emoji.show = !this.emoji.show;
  564. this.more.show = false;
  565. },
  566. showMore() {
  567. this.more.show = !this.more.show;
  568. this.emoji.show = false
  569. },
  570. hidMore() {
  571. this.more.show = false;
  572. },
  573. selectEmoji(emojiKey) {
  574. this.content += emojiKey
  575. },
  576. showCustomMessageForm() {
  577. let to = {
  578. id: this.friend.uuid,
  579. name: this.friend.name,
  580. avatar: this.friend.avatar,
  581. type: commonFun.GoEasy.IM_SCENE.PRIVATE
  582. };
  583. uni.navigateTo({
  584. url: '../customMessage/customMessage?to=' + JSON.stringify(to)
  585. });
  586. },
  587. scrollToBottom() {
  588. this.$nextTick(function() {
  589. uni.pageScrollTo({
  590. scrollTop: 2000000,
  591. duration: 10
  592. })
  593. });
  594. },
  595. markPrivateMessageAsRead(friendId) {
  596. commonFun.goEasy.im.markPrivateMessageAsRead({
  597. userId: friendId,
  598. onSuccess: function() {
  599. console.log('标记为已读成功')
  600. },
  601. onFailed: function(error) {
  602. console.log(error);
  603. }
  604. });
  605. }
  606. }
  607. }
  608. </script>
  609. <style>
  610. @import url("chat.css");
  611. </style>