chat.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. <template>
  2. <view class="chat">
  3. <view class="chat-content" @touchstart="hideDrawer">
  4. <scroll-view id="chat-content-srcoll" scroll-y="true" :scroll-with-animation="scrollAnimation"
  5. :scroll-top="scrollTop" :scroll-into-view="scrollToView" @scrolltoupper="loadHistory"
  6. upper-threshold="50">
  7. <!-- 加载历史数据waitingUI -->
  8. <view class="chat-content-srcoll-loading" v-if="isHistoryLoading">
  9. <view class="spinner">
  10. <view class="rect1"></view>
  11. <view class="rect2"></view>
  12. <view class="rect3"></view>
  13. <view class="rect4"></view>
  14. <view class="rect5"></view>
  15. </view>
  16. </view>
  17. <!-- 聊天 -->
  18. <view class="chat-content-srcoll-list">
  19. <view class="chat-content-srcoll-list-item" v-for="(item, index) in msgList" :key="index"
  20. :id="item.ID">
  21. <!-- 用户信息 -->
  22. <block>
  23. <!-- 自己发出的信息 -->
  24. <view class="my-info" v-if="item.flow === 'out'">
  25. <!-- 左-消息 -->
  26. <view class="my-info-left">
  27. <!-- 文字消息 -->
  28. <view class="">
  29. <view class="my-username">
  30. <view class="time">{{ timeFliter(item.time) }}</view>
  31. <view class="time">{{ item.nick }}</view>
  32. </view>
  33. <view v-if="item.type === TIM.TYPES.MSG_TEXT" class="bubble">
  34. <rich-text :nodes="nodesFliter(item.payload.text)"></rich-text>
  35. </view>
  36. <view class="bubble" v-else-if="item.type === TIM.TYPES.MSG_IMAGE">
  37. <u-image :src="item.payload.imageInfoArray[0].imageUrl"
  38. :width="item.payload.imageInfoArray[0].width > 400 ? 400 : item.payload.imageInfoArray[0].width"
  39. :height="item.payload.imageInfoArray[0].height > 400 ? 400 : item.payload.imageInfoArray[0].height"
  40. border-radius="10"
  41. @click="previewImage(item.payload.imageInfoArray[0].imageUrl)"></u-image>
  42. </view>
  43. <view v-else-if="item.type === TIM.TYPES.MSG_CUSTOM" class="bubble">
  44. <view>
  45. {{ item.flow === 'out' ? '发起' : '接收' }}{{ JSON.parse(JSON.parse(item.payload.data).data).call_type == 1 ? '语音通话' : '视频通话' }}
  46. </view>
  47. </view>
  48. </view>
  49. </view>
  50. <!-- 右-头像 -->
  51. <view class="my-info-right">
  52. <u-avatar :src="item.avatar" size="80" mode="square" bg-color="#fff"></u-avatar>
  53. </view>
  54. </view>
  55. <!-- 别人发出的消息 -->
  56. <view class="other-info" v-else>
  57. <!-- 左-头像 -->
  58. <view class="other-info-left">
  59. <u-avatar :src="item.avatar" size="80" mode="square" bg-color="#fff"></u-avatar>
  60. </view>
  61. <!-- 右-用户名称-时间-消息 -->
  62. <view class="other-info-right">
  63. <view class="username">
  64. <view class="name">{{ item.nick }}</view>
  65. <view class="time">{{ timeFliter(item.time) }}</view>
  66. </view>
  67. <!-- 文字消息 -->
  68. <view v-if="item.type === TIM.TYPES.MSG_TEXT" class="bubble">
  69. <rich-text :nodes="nodesFliter(item.payload.text)"></rich-text>
  70. </view>
  71. <view class="bubble" v-else-if="item.type === TIM.TYPES.MSG_IMAGE">
  72. <u-image :src="item.payload.imageInfoArray[0].imageUrl"
  73. :width="item.payload.imageInfoArray[0].width > 400 ? 400 : item.payload.imageInfoArray[0].width"
  74. :height="item.payload.imageInfoArray[0].height > 400 ? 400 : item.payload.imageInfoArray[0].height"
  75. border-radius="10"
  76. @click="previewImage(item.payload.imageInfoArray[0].imageUrl)"></u-image>
  77. </view>
  78. <view v-else-if="item.type === TIM.TYPES.MSG_CUSTOM" class="bubble">
  79. <view>
  80. {{ item.flow === 'out' ? '发起' : '接收' }}{{ JSON.parse(JSON.parse(item.payload.data).data).call_type == 1 ? '语音通话' : '视频通话' }}
  81. </view>
  82. </view>
  83. </view>
  84. </view>
  85. </block>
  86. </view>
  87. </view>
  88. </scroll-view>
  89. </view>
  90. <!-- 底部输入栏 -->
  91. <view class="input-box" :class="popupLayerClass" @touchmove.stop.prevent="discard">
  92. <view class="more" @tap="showMore">
  93. <view class="icon add"></view>
  94. </view>
  95. <view class="textbox">
  96. <view class="text-mode">
  97. <view class="box">
  98. <textarea auto-height="true" v-model="textMsg" @focus="textareaFocus" />
  99. </view>
  100. <view class="em" @tap="chooseEmoji">
  101. <view class="icon biaoqing"></view>
  102. </view>
  103. </view>
  104. </view>
  105. <view class="send" @tap="sendText">
  106. <view class="btn">发送</view>
  107. </view>
  108. </view>
  109. <!-- 抽屉栏 -->
  110. <view class="popup-layer" :class="popupLayerClass" @touchmove.stop.prevent="discard">
  111. <!-- 表情 -->
  112. <swiper class="emoji-swiper" :class="{ hidden: hideEmoji }" indicator-dots="true" duration="150">
  113. <swiper-item v-for="(page,pid) in emojiList" :key="pid">
  114. <view v-for="(em, eid) in page" :key="eid" @tap="addEmoji(em)">
  115. <image mode="widthFix" :src="'/static/img/emoji/'+ em.url"></image>
  116. </view>
  117. </swiper-item>
  118. </swiper>
  119. <!-- 更多功能 相册-拍照- -->
  120. <view class="more-layer" :class="{ hidden: hideMore }">
  121. <view class="list">
  122. <view class="box" @tap="chooseImage">
  123. <view class="icon tupian2"></view>
  124. </view>
  125. <view class="box" @tap="camera">
  126. <view class="icon paizhao"></view>
  127. </view>
  128. <!-- <view class="box" @tap="call(1)">
  129. <view class="iconfont icon-yuyin shipin"></view>
  130. </view>
  131. <view class="box" @tap="call(2)">
  132. <view class="iconfont icon-shipin shipin"></view>
  133. </view> -->
  134. </view>
  135. </view>
  136. </view>
  137. <!-- 语音聊天 -->
  138. <!-- <trtc-calling ref="trtcCalling"></trtc-calling> -->
  139. </view>
  140. </template>
  141. <script>
  142. // import TrtcCalling from 'components/trtc-calling/index.vue'
  143. import {
  144. mapState
  145. } from 'vuex';
  146. export default {
  147. components: {
  148. // TrtcCalling
  149. },
  150. data() {
  151. return {
  152. // 消息列表
  153. isHistoryLoading: false,
  154. scrollAnimation: false,
  155. scrollTop: 0,
  156. scrollToView: '',
  157. //TIM变量
  158. conversationActive: null,
  159. toUserId: '',
  160. toUserInfo: null,
  161. userInfo: null,
  162. msgList: [],
  163. nextReqMessageID: '',
  164. count: 15,
  165. isCompleted: '',
  166. TIM: null,
  167. // 抽屉参数
  168. popupLayerClass: '',
  169. isVoice: false,
  170. emojiList: this.$commen.emojiList,
  171. recording: false,
  172. textMsg: '',
  173. //表情定义
  174. hideEmoji: true,
  175. // more参数
  176. hideMore: true
  177. }
  178. },
  179. onLoad(page) {
  180. if (page.title) {
  181. uni.setNavigationBarTitle({
  182. title: page.title
  183. })
  184. }
  185. this.conversationActive = this.$store.state.conversationActive;
  186. this.toUserId = this.$store.state.toUserId;
  187. this.TIM = this.$TIM
  188. this.getMsgList();
  189. },
  190. onShow() {
  191. this.scrollTop = 9999999;
  192. this.isHistoryLoading = false;
  193. this.readMessage();
  194. // setTimeout(() => {
  195. // this.$refs.trtcCalling.initListener();
  196. // }, 300)
  197. },
  198. computed: {
  199. ...mapState({
  200. currentMessageList: state => state.currentMessageList
  201. })
  202. },
  203. watch: {
  204. currentMessageList(newVal, oldVal) {
  205. this.msgList = newVal
  206. this.screenMsg(newVal, oldVal)
  207. }
  208. },
  209. onUnload() {
  210. this.readMessage();
  211. },
  212. methods: {
  213. /**
  214. * 消息已读
  215. */
  216. readMessage() {
  217. // 退出页面 将所有的会话内的消息设置为已读
  218. console.log(this.conversationActive.conversationID)
  219. this.tim.setMessageRead({
  220. conversationID: this.conversationActive.conversationID
  221. }).then(function(imResponse) {
  222. console.log('全部已读')
  223. // 已读上报成功
  224. }).catch(function(imError) {
  225. // 已读上报失败
  226. console.warn('setMessageRead error:', imError);
  227. });
  228. },
  229. /**
  230. * 加载初始页面消息
  231. */
  232. getMsgList() {
  233. // 历史消息列表
  234. let conversationID = this.conversationActive.conversationID
  235. this.tim.getMessageList({
  236. conversationID: conversationID,
  237. count: this.count
  238. }).then((res) => {
  239. console.log(res.data)
  240. this.$store.commit('pushCurrentMessageList', res.data.messageList)
  241. this.nextReqMessageID = res.data.nextReqMessageID // 用于续拉,分页续拉时需传入该字段。
  242. this.isCompleted = res.data.isCompleted
  243. this.scrollToView = res.data.messageList[res.data.messageList.length - 1]?.ID
  244. }).catch(err => {
  245. console.log(err)
  246. // this.$u.route({
  247. // url: 'pages/login/login'
  248. // })
  249. })
  250. // 滚动到底部
  251. this.$nextTick(function() {
  252. //进入页面滚动到底部
  253. this.scrollTop = 9999;
  254. this.$nextTick(function() {
  255. this.scrollAnimation = true;
  256. });
  257. });
  258. },
  259. /**
  260. * 接受消息(定位消息)
  261. * @param { Object } newVal
  262. * @param { Object } oldVal
  263. */
  264. screenMsg(newVal, oldVal) {
  265. if (newVal[0] && oldVal[0]) {
  266. if (newVal[0].ID != oldVal[0].ID && newVal.length >= this.count) {
  267. let _index = newVal.length - oldVal.length - 1
  268. this.$nextTick(() => {
  269. this.scrollToView = newVal[_index].ID
  270. });
  271. // 拉取历史记录不用改变定位消息
  272. } else {
  273. //新的消息来了 自动向下滑动到最新消息
  274. this.$nextTick(() => {
  275. this.scrollToView = newVal[newVal.length - 1].ID
  276. });
  277. }
  278. } else {
  279. // 第一次拉取历史记录 定位到最后一条消息
  280. this.$nextTick(() => {
  281. this.scrollToView = newVal[newVal.length - 1].ID;
  282. this.scrollTop = 9999999;
  283. });
  284. }
  285. },
  286. /**
  287. * 时间过滤
  288. * @param {Object} timer
  289. */
  290. timeFliter(timer) {
  291. let timeData = new Date(timer * 1000)
  292. let str = this.$format.dateTimeFliter(timeData)
  293. return str
  294. },
  295. /**
  296. * 聊天的节点加上外层的
  297. * @param {String} str
  298. */
  299. nodesFliter(str) {
  300. let nodeStr = '<div style="align-items: center;word-wrap:break-word;">' + str + '</div>'
  301. return nodeStr
  302. },
  303. /**
  304. * 隐藏抽屉
  305. */
  306. hideDrawer() {
  307. this.popupLayerClass = '';
  308. setTimeout(() => {
  309. this.hideMore = true;
  310. this.hideEmoji = true;
  311. }, 150);
  312. },
  313. /**
  314. * 选择表情
  315. */
  316. chooseEmoji() {
  317. this.hideMore = true;
  318. if (this.hideEmoji) {
  319. this.hideEmoji = false;
  320. this.openDrawer();
  321. } else {
  322. this.hideDrawer();
  323. }
  324. },
  325. /**
  326. * 触发滑动到顶部(加载历史信息记录)
  327. * @param {Object} e
  328. */
  329. loadHistory(e) {
  330. this.isHistoryLoading = true;
  331. // 更多消息列表
  332. let conversationID = this.conversationActive.conversationID
  333. this.tim.getMessageList({
  334. conversationID: conversationID,
  335. nextReqMessageID: this.nextReqMessageID,
  336. count: this.count
  337. }).then((res) => {
  338. this.$store.commit('unshiftCurrentMessageList', res.data.messageList)
  339. this.nextReqMessageID = res.data.nextReqMessageID // 用于续拉,分页续拉时需传入该字段。
  340. this.isCompleted = res.data.isCompleted
  341. this.isHistoryLoading = false;
  342. });
  343. },
  344. /**
  345. * 获取焦点,如果不是选表情ing,则关闭抽屉
  346. */
  347. textareaFocus() {
  348. if (this.popupLayerClass == 'showLayer' && this.hideMore == false) {
  349. this.hideDrawer();
  350. }
  351. },
  352. /**
  353. * 发送消息
  354. */
  355. sendText() {
  356. this.hideDrawer(); //隐藏抽屉
  357. if (!this.textMsg) {
  358. return;
  359. }
  360. let content = this.replaceEmoji(this.textMsg);
  361. let msg = {
  362. text: content
  363. }
  364. this.sendMsg(msg, 'text');
  365. this.textMsg = ''; //清空输入框
  366. },
  367. /**
  368. * 发送消息
  369. * @param {Object} content
  370. * @param {Object} type
  371. */
  372. sendMsg(content, type) {
  373. let message;
  374. if (type === 'text') {
  375. message = this.tim.createTextMessage({
  376. to: this.toUserId,
  377. conversationType: 'C2C',
  378. payload: {
  379. text: content.text
  380. }
  381. });
  382. } else if (type === 'img') {
  383. message = this.tim.createImageMessage({
  384. to: this.toUserId,
  385. conversationType: 'C2C',
  386. payload: {
  387. file: content
  388. }
  389. })
  390. }
  391. this.$store.commit('pushCurrentMessageList', message)
  392. this.tim.sendMessage(message).then(res => {
  393. this.$nextTick(() => {
  394. // 滚动到底
  395. this.scrollToView = res.data.message.ID
  396. });
  397. })
  398. },
  399. //替换表情符号为图片
  400. replaceEmoji(str) {
  401. let replacedStr = str.replace(/\[([^(\]|\[)]*)\]/g, (item, index) => {
  402. for (let i = 0; i < this.emojiList.length; i++) {
  403. let row = this.emojiList[i];
  404. for (let j = 0; j < row.length; j++) {
  405. let EM = row[j];
  406. if (EM.alt == item) {
  407. //在线表情路径,图文混排必须使用网络路径,请上传一份表情到你的服务器后再替换此路径
  408. //比如你上传服务器后,你的100.gif路径为https://www.xxx.com/emoji/100.gif 则替换onlinePath填写为https://www.xxx.com/emoji/
  409. let onlinePath = 'http://veterhchat.hw.hongweisoft.com/static/img/emoji/'
  410. let imgstr = '<img src="' + onlinePath + EM.url + '">';
  411. return imgstr;
  412. }
  413. }
  414. }
  415. });
  416. return replacedStr;
  417. },
  418. /**
  419. * 更多功能(点击+弹出)
  420. */
  421. showMore() {
  422. this.hideEmoji = true;
  423. if (this.hideMore) {
  424. this.hideMore = false;
  425. this.openDrawer();
  426. } else {
  427. this.hideDrawer();
  428. }
  429. },
  430. discard() {
  431. return;
  432. },
  433. // 打开抽屉
  434. openDrawer() {
  435. this.popupLayerClass = 'showLayer';
  436. },
  437. /**
  438. * 隐藏抽屉
  439. */
  440. hideDrawer() {
  441. this.popupLayerClass = '';
  442. setTimeout(() => {
  443. this.hideMore = true;
  444. this.hideEmoji = true;
  445. }, 150);
  446. },
  447. /**
  448. * 添加表情
  449. * @param {Object} em
  450. */
  451. addEmoji(em) {
  452. this.textMsg += em.alt;
  453. },
  454. /**
  455. * 选择图片发送
  456. */
  457. chooseImage() {
  458. this.getImage('album');
  459. },
  460. /**
  461. * 拍照
  462. */
  463. camera() {
  464. this.getImage('camera');
  465. },
  466. /**
  467. * 通话
  468. */
  469. call(type) {
  470. this.trtcCalling.call({
  471. userID: this.toUserId,
  472. type: type,
  473. timeout: 300
  474. }).then((res) => {
  475. console.log('成功:', res)
  476. this.$refs.trtcCalling.openDialog()
  477. }).catch(err => {
  478. console.log('失败', err)
  479. uni.showToast({
  480. title: '调用通话失败',
  481. icon: 'none'
  482. })
  483. })
  484. },
  485. /**
  486. * 选照片 or 拍照
  487. * @param {Object} type
  488. */
  489. getImage(type) {
  490. this.hideDrawer();
  491. uni.chooseImage({
  492. sourceType: [type],
  493. sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
  494. success: (res) => {
  495. this.sendMsg(res.tempFiles[0], 'img')
  496. },
  497. fail: (err) => {
  498. uni.showToast({
  499. icon: 'none',
  500. title: '图片上传失败!'
  501. })
  502. }
  503. });
  504. },
  505. /**
  506. * 预览图片
  507. */
  508. previewImage(img) {
  509. uni.previewImage({
  510. urls: [img]
  511. });
  512. }
  513. }
  514. }
  515. </script>
  516. <style lang="scss" scoped>
  517. @font-face {
  518. font-family: "iconfont";
  519. /* Project id 2965205 */
  520. src: url('//at.alicdn.com/t/font_2965205_e6crsnlz8jf.woff2?t=1637829206479') format('woff2'),
  521. url('//at.alicdn.com/t/font_2965205_e6crsnlz8jf.woff?t=1637829206479') format('woff'),
  522. url('//at.alicdn.com/t/font_2965205_e6crsnlz8jf.ttf?t=1637829206479') format('truetype');
  523. }
  524. .iconfont {
  525. font-family: "iconfont" !important;
  526. font-size: 16px;
  527. font-style: normal;
  528. -webkit-font-smoothing: antialiased;
  529. -moz-osx-font-smoothing: grayscale;
  530. }
  531. .icon-yuyin:before {
  532. content: "\e6e1";
  533. }
  534. .icon-shipin:before {
  535. content: "\e64f";
  536. }
  537. @import './chat.scss';
  538. </style>