chatting.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <template>
  2. <view class="container">
  3. <view id="topnav" class="topnav flex-start"
  4. :style="{ 'height': `${topbarOffsetHeight - statusBarHeight}px`, 'padding-top': `${statusBarHeight}px` }">
  5. <view class="nav-item flex-center" @click="back">
  6. <image :src="`${assetsUrl}back.png`" mode="widthFix" class="nav-img"></image>
  7. </view>
  8. <view class="nav-center flex-center">
  9. <view class="nav-text font32 fw600">
  10. {{ userInfo.nick }}
  11. </view>
  12. <view class="nav-tip font20 fw400">
  13. <text></text>{{ onlineState }} · {{ userInfo.distance }}
  14. </view>
  15. </view>
  16. <view class="nav-item"></view>
  17. </view>
  18. <view class="message-list" :style="[{ 'height': `${scrollHeight}px`, 'margin-top': `${topbarOffsetHeight}px` }]">
  19. <!-- <view class="message-list" :style="[messageStyle]"> -->
  20. <TUI-message-list id="message-list" ref="messageList" :conversation="conversation" :scrollHeight="scrollHeight"
  21. :topbarOffsetHeight="topbarOffsetHeight" />
  22. </view>
  23. <view class="talk-box" :style="[{ 'bottom': `${bottomVal}px` }]" id="talk-box" @touchmove="prevent()">
  24. <view class="input-box flex-between">
  25. <!-- <van-field v-model="inputText" label="文本" placeholder="请输入消息…" /> -->
  26. <!-- <input type="text" class="input" confirm-type="send" placeholder="请输入消息…" :adjust-position="true" v-model="inputText" placeholder-style="color:#7D7DA4 ;font-size:24rpx" @confirm="sendTextMessage"> -->
  27. <input class="TUI-message-input-area input" :adjust-position="false" cursor-spacing="20" v-model="inputText"
  28. @confirm="sendTextMessage" maxlength="140" type="text" placeholder-style="color:#7D7DA4 ;font-size:24rpx"
  29. placeholder="请输入消息…" @focus="inputBindFocus" @blur="inputBindBlur" />
  30. <view class="input-btn font22 fw400" @click="sendTextMessage">
  31. 发送
  32. </view>
  33. </view>
  34. <view class="action-box flex-between">
  35. <image :src="`${assetsUrl}talk-voice.png`" mode="aspectFit" class="act-img" @click="showActionPanel(0)"></image>
  36. <image :src="`${assetsUrl}talk-pic.png`" mode="aspectFit" class="act-img" @click="showActionPanel(1)"></image>
  37. <image :src="`${assetsUrl}talk-video.png`" mode="aspectFit" class="act-img" @click="showActionPanel(2)"></image>
  38. <image :src="`${assetsUrl}talk-emo.png`" mode="aspectFit" class="act-img" @click="showActionPanel(3)"></image>
  39. </view>
  40. <view class="action-panel" :style="{ 'padding-bottom': `${showActionIndex === -1 ? '68rpx' : '20rpx'}` }">
  41. <view class="voice-panel flex-center" v-if="showActionIndex === 0">
  42. <view class="voice-text font22 fw400">
  43. {{ voiceText }}
  44. </view>
  45. <image :src="`${assetsUrl}talk-voice-${isRecording ? 'delete' : 'open'}.png`" mode="aspectFill" class="voice-img"
  46. @longpress="handleLongPress" @touchmove="handleTouchMove" @touchend="handleTouchEnd"></image>
  47. </view>
  48. <view class="emoji-box" v-if="showActionIndex === 3">
  49. <TUI-Emoji @enterEmoji="appendMessage"></TUI-Emoji>
  50. </view>
  51. </view>
  52. </view>
  53. <view v-if="videoPlay" class="container-box" @tap.stop="stopVideoHander">
  54. <video v-if="videoPlay" class="video-message" :src="videoMessage.payload.videoUrl || videoMessage.videoUrl"
  55. :poster="videoMessage.payload.thumbUrl || videoMessage.videoCover" object-fit="cover" error="videoError"
  56. autoplay="true" direction="0" show-fullscreen-btn="false"
  57. :style="{ 'width': `${videoMessage.payload.thumbWidth || videoMessage.width}px`, 'height': `${videoMessage.payload.thumbHeight || videoMessage.height}px` }" />
  58. </view>
  59. </view>
  60. </template>
  61. <script>
  62. import TUIEmoji from '../../components/tui-chat/message-elements/emoji/index';
  63. import TUIMessageList from '../../components/tui-chat/message-list/index';
  64. export default {
  65. components: {
  66. TUIMessageList,
  67. TUIEmoji
  68. },
  69. data() {
  70. return {
  71. assetsUrl: this.$util.assetsUrl,
  72. scrollHeight: 0,
  73. topNavHeight: 0,
  74. userInfo: null,
  75. messageList: [],
  76. message: null,//消息实例
  77. onlineState: '',
  78. scrollRefreshing: false,
  79. scrollTriggered: true,
  80. conversation: '',
  81. conversationID: '',
  82. inputText: '',
  83. showSendBtn: false,
  84. showActionIndex: -1,
  85. recorderManager: null,
  86. voiceText: '按住说话,松手发送',
  87. recordTimeTotal: 60,//最大长度,30秒
  88. isRecording: false,
  89. canSend: false,
  90. startPoint: 0,
  91. videoPlay: false,
  92. videoMessage: {},
  93. bottomVal: 0 // 底部bottom
  94. }
  95. },
  96. computed: {
  97. statusBarHeight() {
  98. return this.$store.state.statusBarHeight;
  99. },
  100. topbarOffsetHeight() {
  101. return this.$store.state.topbarOffsetHeight;
  102. },
  103. messageStyle() {
  104. let obj = {
  105. 'height': `${this.scrollHeight}px`,
  106. 'margin-top': `${this.topbarOffsetHeight}px`
  107. }
  108. return obj
  109. // [{,'margin-top':`${topbarOffsetHeight}px`}]
  110. }
  111. },
  112. created() {
  113. uni.$on('videoPlayerHandler', value => {
  114. this.videoPlay = value.isPlay;
  115. this.videoMessage = value.message;
  116. });
  117. uni.authorize({
  118. scope: 'scope.record',
  119. success: () => {
  120. this.startRecordManage();
  121. },
  122. fail: err => {
  123. uni.showModal({
  124. content: '检测到您没打开录音功能权限,是否去设置打开?',
  125. confirmText: "确认",
  126. cancelText: '取消',
  127. success: res => {
  128. if (res.confirm) {
  129. uni.openSetting({
  130. success: ress => {
  131. if (ress.authSetting) {
  132. this.startRecordManage();
  133. }
  134. }
  135. })
  136. }
  137. }
  138. })
  139. }
  140. })
  141. },
  142. onLoad(options) {
  143. this.conversationID = options.conversationid;
  144. uni.$TUIKit.setMessageRead({
  145. conversationID: this.conversationID
  146. })
  147. uni.$TUIKit.getConversationProfile(this.conversationID).then(res => {
  148. const { conversation } = res.data;
  149. this.conversation = conversation;
  150. console.log(this.conversation)
  151. });
  152. let pages = getCurrentPages();
  153. let prePage = pages[pages.length - 2];
  154. this.userInfo = prePage.$vm.userInfo;
  155. },
  156. mounted() {
  157. if (this.userInfo.lastActiveTime === 0 || this.userInfo.online) {
  158. this.onlineState = '在线';
  159. }
  160. else if (this.userInfo.lastActiveTime < 30 && !this.userInfo.online) {
  161. this.onlineState = '刚刚';
  162. }
  163. else {
  164. this.onlineState = '离线';
  165. }
  166. setTimeout(() => {
  167. this.computedScollviewHeight();
  168. }, 500)
  169. },
  170. methods: {
  171. inputBindFocus(e) {
  172. // 获取手机键盘的高度,赋值给input 所在盒子的 bottom 值
  173. // 注意!!! 这里的 px 至关重要!!! 我搜到的很多解决方案都没有说这里要添加 px
  174. // this.$emit('changeBottomVal', e.detail.height + 'px')
  175. this.scrollHeight = e.detail.height
  176. this.bottomVal = e.detail.height
  177. console.log(e.detail.height, 'e.detail.height', e, 'this.scrollHeight', this.scrollHeight)
  178. },
  179. inputBindBlur() {
  180. // input 失去焦点,键盘隐藏,设置 input 所在盒子的 bottom 值为0
  181. // this.$emit('changeBottomVal', 0)
  182. // this.scrollHeight = 0
  183. this.computedScollviewHeight()
  184. this.bottomVal = 0
  185. },
  186. back() {
  187. uni.navigateBack({
  188. delta: 1
  189. })
  190. },
  191. prevent() {
  192. return false;
  193. },
  194. stopVideoHander() {
  195. this.videoPlay = false;
  196. },
  197. /**
  198. * 计算scroll高度
  199. */
  200. computedScollviewHeight() {
  201. let query = uni.createSelectorQuery().in(this);
  202. let heightLeaf = 0;
  203. query.selectAll('#topnav,#talk-box').boundingClientRect(data => {
  204. data.forEach(item => {
  205. heightLeaf += item.height;
  206. })
  207. }).exec(() => {
  208. let sysInfo = uni.getSystemInfoSync();
  209. this.scrollHeight = sysInfo.windowHeight - heightLeaf;
  210. this.$refs.messageList.scrollToButtom();
  211. });
  212. },
  213. /**
  214. * 推荐下拉刷新、加载更多
  215. */
  216. scrollRefresh() {
  217. if (this.scrollRefreshing) {
  218. return;
  219. }
  220. this.scrollRefreshing = true;
  221. setTimeout(() => {
  222. this.scrollTriggered = false;
  223. this.scrollRefreshing = false;
  224. }, 1000);
  225. this.isLoadPreData = true;
  226. },
  227. scrollPulling(e) { },
  228. scrollRestore() { this.scrollTriggered = true; },
  229. scrollAbort() { },
  230. scrollToBottom() { },
  231. showActionPanel(index) {
  232. if (this.showActionIndex === index) {
  233. this.showActionIndex = -1;
  234. setTimeout(() => {
  235. this.computedScollviewHeight();
  236. }, 50)
  237. return;
  238. }
  239. switch (index) {
  240. case 0:
  241. this.showActionIndex = index;
  242. setTimeout(() => {
  243. this.computedScollviewHeight();
  244. }, 50)
  245. break;
  246. case 1:
  247. this.showActionIndex = -1;
  248. uni.chooseImage({
  249. count: 1,
  250. sizeType: ['original', 'compressed'],
  251. sourceType: ['album', 'camera'],
  252. success: res => {
  253. console.log(res)
  254. const message = uni.$TUIKit.createImageMessage({
  255. to: String(this.userInfo.id),
  256. conversationType: this.conversation.type,
  257. payload: {
  258. file: res
  259. },
  260. onProgress: event => { }
  261. });
  262. this.$sendTIMMessage(message);
  263. }
  264. })
  265. break;
  266. case 2:
  267. uni.chooseVideo({
  268. sourceType: ['album', 'camera'],
  269. maxDuration: 60,
  270. camera: 'back',
  271. success: res => {
  272. const message = uni.$TUIKit.createVideoMessage({
  273. to: String(this.userInfo.id),
  274. conversationType: this.conversation.type,
  275. payload: {
  276. file: res
  277. },
  278. onProgress: event => { }
  279. });
  280. this.$sendTIMMessage(message);
  281. }
  282. })
  283. break;
  284. case 3:
  285. this.showActionIndex = index;
  286. setTimeout(() => {
  287. this.computedScollviewHeight();
  288. }, 50)
  289. break;
  290. }
  291. },
  292. appendMessage(e) {
  293. this.inputText += e.detail.message;
  294. },
  295. startRecordManage() {
  296. // 加载声音录制管理器
  297. this.recorderManager = uni.getRecorderManager();
  298. console.log(this.recorderManager)
  299. this.recorderManager.onStop(res => {
  300. console.log(res)
  301. clearInterval(this.recordTimer);
  302. // 兼容 uniapp 打包app,duration 和 fileSize 需要用户自己补充
  303. // 文件大小 = (音频码率) x 时间长度(单位:秒) / 8
  304. let msg = {
  305. duration: res.duration ? res.duration : this.recordTime * 1000,
  306. tempFilePath: res.tempFilePath,
  307. fileSize: res.fileSize ? res.fileSize : ((48 * this.recordTime) / 8) * 1024
  308. };
  309. uni.hideLoading();
  310. // 兼容 uniapp 语音消息没有duration
  311. if (this.canSend) {
  312. if (msg.duration < 1000) {
  313. uni.showToast({
  314. title: '录音时间太短',
  315. icon: 'none'
  316. });
  317. } else {
  318. // res.tempFilePath 存储录音文件的临时路径
  319. const message = uni.$TUIKit.createAudioMessage({
  320. to: String(this.userInfo.id),
  321. conversationType: this.conversation.type,
  322. payload: {
  323. file: msg
  324. }
  325. });
  326. this.$sendTIMMessage(message);
  327. }
  328. }
  329. this.startPoint = 0;
  330. this.isRecording = false;
  331. this.canSend = true;
  332. this.voiceText = '按住说话,松手发送';
  333. });
  334. this.recorderManager.onError(err => {
  335. console.log(err)
  336. })
  337. },
  338. handleLongPress(e) {
  339. uni.vibrateShort();
  340. this.recorderManager.start({
  341. duration: 60000,
  342. // 录音的时长,单位 ms,最大值 600000(10 分钟)
  343. sampleRate: 44100,
  344. // 采样率
  345. numberOfChannels: 1,
  346. // 录音通道数
  347. encodeBitRate: 192000,
  348. // 编码码率
  349. format: 'aac' // 音频格式,选择此格式创建的音频消息,可以在即时通信 IM 全平台(Android、iOS、微信小程序和Web)互通
  350. });
  351. this.startPoint = e.touches[0];
  352. this.voiceText = '正在录音,上划可取消';
  353. this.isRecording = true;
  354. this.recordTime = 0;
  355. this.recordTimer = setInterval(() => {
  356. this.recordTime++;
  357. if (this.recorTime >= this.recordTimeTotal) {
  358. this.recorderManager.stop();
  359. clearInterval(this.recordTimer);
  360. this.recordTimer = null;
  361. }
  362. }, 1000);
  363. },
  364. // 录音时的手势上划移动距离对应文案变化
  365. handleTouchMove(e) {
  366. if (this.isRecording) {
  367. if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 100) {
  368. this.voiceText = '松开手指,取消发送';
  369. this.canSend = false;
  370. } else if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 20) {
  371. this.voiceText = '上划可取消';
  372. this.canSend = true;
  373. } else {
  374. this.voiceText = '抬起停止';
  375. this.canSend = true;
  376. }
  377. }
  378. },
  379. // 手指离开页面滑动
  380. handleTouchEnd() {
  381. this.isRecording = false;
  382. this.voiceText = '按住说话,松手发送';
  383. uni.hideLoading();
  384. this.recorderManager.stop();
  385. },
  386. sendTextMessage() {
  387. const to = String(this.userInfo.id);
  388. const text = this.inputText;
  389. const message = uni.$TUIKit.createTextMessage({
  390. to,
  391. conversationType: this.conversation.type,
  392. payload: {
  393. text
  394. }
  395. });
  396. this.inputText = '';
  397. this.$sendTIMMessage(message);
  398. },
  399. $sendTIMMessage(message) {
  400. uni.$TUIKit.sendMessage(message).then((res) => {
  401. this.$EventBus.$emit('sendMessage', message)
  402. this.$refs.messageList.scrollToButtom();
  403. }).catch((error) => {
  404. uni.showToast({
  405. title: '发送消息失败',
  406. icon: "none"
  407. })
  408. })
  409. },
  410. }
  411. }
  412. </script>
  413. <style lang="scss" scoped>
  414. .container {
  415. width: 100vw;
  416. height: 100vh;
  417. background-color: $bgcolor1;
  418. position: relative;
  419. overflow: hidden;
  420. .topnav {
  421. padding: 0 10rpx;
  422. position: fixed;
  423. top: 0;
  424. left: 0;
  425. width: 100vw;
  426. z-index: 100;
  427. background-color: $bgcolor1;
  428. .nav-item {
  429. width: 40rpx;
  430. height: 40rpx;
  431. margin-left: 16rpx;
  432. .nav-img {
  433. width: 40rpx;
  434. height: 40rpx;
  435. }
  436. }
  437. .nav-center {
  438. flex: 1;
  439. flex-direction: column;
  440. .nav-text {
  441. color: $fontcolor5;
  442. height: 40rpx;
  443. text-align: center;
  444. }
  445. .nav-tip {
  446. color: $fontcolor2;
  447. }
  448. }
  449. }
  450. .talk-box {
  451. position: fixed;
  452. bottom: 0;
  453. left: 0;
  454. width: 100vw;
  455. padding-top: 16rpx;
  456. background-color: $bgcolor1;
  457. .input-box {
  458. margin: 0rpx 32rpx;
  459. height: 80rpx;
  460. border-radius: 80rpx;
  461. background-color: $bgcolor4;
  462. padding: 0 24rpx;
  463. .input {
  464. width: 100%;
  465. height: 100%;
  466. color: #ffffff;
  467. font-size: 22rpx;
  468. }
  469. .input-btn {
  470. background-color: $primary;
  471. color: #ffffff;
  472. border-radius: 16rpx;
  473. height: 56rpx;
  474. line-height: 56rpx;
  475. width: 120rpx;
  476. text-align: center;
  477. }
  478. }
  479. .action-box {
  480. padding: 0rpx 0rpx 20rpx 0rpx;
  481. margin: 0 90rpx;
  482. margin-top: 36rpx;
  483. .act-img {
  484. width: 56rpx;
  485. height: 56rpx;
  486. }
  487. }
  488. .action-panel {
  489. width: 100vw;
  490. .voice-panel {
  491. height: 400rpx;
  492. flex-direction: column;
  493. .voice-text {
  494. color: #7D7DA4;
  495. text-align: center;
  496. }
  497. .voice-img {
  498. width: 200rpx;
  499. height: 200rpx;
  500. margin-top: 40rpx;
  501. }
  502. }
  503. }
  504. .emoji-box {
  505. padding: 0 11rpx;
  506. box-sizing: border-box;
  507. height: 400rpx;
  508. flex-wrap: wrap;
  509. transition: height .3s;
  510. .emoji-item {
  511. width: 64rpx;
  512. height: 64rpx;
  513. padding: 20rpx;
  514. .emoji-img {
  515. width: 64rpx;
  516. height: 64rpx;
  517. }
  518. }
  519. }
  520. }
  521. .container-box {
  522. position: fixed;
  523. display: flex;
  524. justify-content: center;
  525. align-items: center;
  526. left: 0;
  527. right: 0;
  528. bottom: 0;
  529. top: 0;
  530. background-color: rgba(0, 0, 0, 0.5);
  531. .video-message {
  532. width: 90vw;
  533. height: auto;
  534. }
  535. }
  536. }</style>