chatting.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  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" :adjust-position="false" cursor-spacing="20" v-model="inputText"
  28. @confirm="sendTextMessage" maxlength="140" type="text" placeholder-class="input-placeholder" 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.bottomVal = 0
  184. },
  185. back() {
  186. uni.navigateBack({
  187. delta: 1
  188. })
  189. },
  190. prevent() {
  191. return false;
  192. },
  193. stopVideoHander() {
  194. this.videoPlay = false;
  195. },
  196. /**
  197. * 计算scroll高度
  198. */
  199. computedScollviewHeight() {
  200. let query = uni.createSelectorQuery().in(this);
  201. let heightLeaf = 0;
  202. query.selectAll('#topnav,#talk-box').boundingClientRect(data => {
  203. data.forEach(item => {
  204. heightLeaf += item.height;
  205. })
  206. }).exec(() => {
  207. let sysInfo = uni.getSystemInfoSync();
  208. this.scrollHeight = sysInfo.windowHeight - heightLeaf;
  209. this.$refs.messageList.scrollToButtom();
  210. });
  211. },
  212. /**
  213. * 推荐下拉刷新、加载更多
  214. */
  215. scrollRefresh() {
  216. if (this.scrollRefreshing) {
  217. return;
  218. }
  219. this.scrollRefreshing = true;
  220. setTimeout(() => {
  221. this.scrollTriggered = false;
  222. this.scrollRefreshing = false;
  223. }, 1000);
  224. this.isLoadPreData = true;
  225. },
  226. scrollPulling(e) { },
  227. scrollRestore() { this.scrollTriggered = true; },
  228. scrollAbort() { },
  229. scrollToBottom() { },
  230. showActionPanel(index) {
  231. if (this.showActionIndex === index) {
  232. this.showActionIndex = -1;
  233. setTimeout(() => {
  234. this.computedScollviewHeight();
  235. }, 50)
  236. return;
  237. }
  238. switch (index) {
  239. case 0:
  240. this.showActionIndex = index;
  241. setTimeout(() => {
  242. this.computedScollviewHeight();
  243. }, 50)
  244. break;
  245. case 1:
  246. this.showActionIndex = -1;
  247. uni.chooseImage({
  248. count: 1,
  249. sizeType: ['original', 'compressed'],
  250. sourceType: ['album', 'camera'],
  251. success: res => {
  252. console.log(res)
  253. const message = uni.$TUIKit.createImageMessage({
  254. to: String(this.userInfo.id),
  255. conversationType: this.conversation.type,
  256. payload: {
  257. file: res
  258. },
  259. onProgress: event => { }
  260. });
  261. this.$sendTIMMessage(message);
  262. }
  263. })
  264. break;
  265. case 2:
  266. uni.chooseVideo({
  267. sourceType: ['album', 'camera'],
  268. maxDuration: 60,
  269. camera: 'back',
  270. success: res => {
  271. const message = uni.$TUIKit.createVideoMessage({
  272. to: String(this.userInfo.id),
  273. conversationType: this.conversation.type,
  274. payload: {
  275. file: res
  276. },
  277. onProgress: event => { }
  278. });
  279. this.$sendTIMMessage(message);
  280. }
  281. })
  282. break;
  283. case 3:
  284. this.showActionIndex = index;
  285. setTimeout(() => {
  286. this.computedScollviewHeight();
  287. }, 50)
  288. break;
  289. }
  290. },
  291. appendMessage(e) {
  292. this.inputText += e.detail.message;
  293. },
  294. startRecordManage() {
  295. // 加载声音录制管理器
  296. this.recorderManager = uni.getRecorderManager();
  297. console.log(this.recorderManager)
  298. this.recorderManager.onStop(res => {
  299. console.log(res)
  300. clearInterval(this.recordTimer);
  301. // 兼容 uniapp 打包app,duration 和 fileSize 需要用户自己补充
  302. // 文件大小 = (音频码率) x 时间长度(单位:秒) / 8
  303. let msg = {
  304. duration: res.duration ? res.duration : this.recordTime * 1000,
  305. tempFilePath: res.tempFilePath,
  306. fileSize: res.fileSize ? res.fileSize : ((48 * this.recordTime) / 8) * 1024
  307. };
  308. uni.hideLoading();
  309. // 兼容 uniapp 语音消息没有duration
  310. if (this.canSend) {
  311. if (msg.duration < 1000) {
  312. uni.showToast({
  313. title: '录音时间太短',
  314. icon: 'none'
  315. });
  316. } else {
  317. // res.tempFilePath 存储录音文件的临时路径
  318. const message = uni.$TUIKit.createAudioMessage({
  319. to: String(this.userInfo.id),
  320. conversationType: this.conversation.type,
  321. payload: {
  322. file: msg
  323. }
  324. });
  325. this.$sendTIMMessage(message);
  326. }
  327. }
  328. this.startPoint = 0;
  329. this.isRecording = false;
  330. this.canSend = true;
  331. this.voiceText = '按住说话,松手发送';
  332. });
  333. this.recorderManager.onError(err => {
  334. console.log(err)
  335. })
  336. },
  337. handleLongPress(e) {
  338. uni.vibrateShort();
  339. this.recorderManager.start({
  340. duration: 60000,
  341. // 录音的时长,单位 ms,最大值 600000(10 分钟)
  342. sampleRate: 44100,
  343. // 采样率
  344. numberOfChannels: 1,
  345. // 录音通道数
  346. encodeBitRate: 192000,
  347. // 编码码率
  348. format: 'aac' // 音频格式,选择此格式创建的音频消息,可以在即时通信 IM 全平台(Android、iOS、微信小程序和Web)互通
  349. });
  350. this.startPoint = e.touches[0];
  351. this.voiceText = '正在录音,上划可取消';
  352. this.isRecording = true;
  353. this.recordTime = 0;
  354. this.recordTimer = setInterval(() => {
  355. this.recordTime++;
  356. if (this.recorTime >= this.recordTimeTotal) {
  357. this.recorderManager.stop();
  358. clearInterval(this.recordTimer);
  359. this.recordTimer = null;
  360. }
  361. }, 1000);
  362. },
  363. // 录音时的手势上划移动距离对应文案变化
  364. handleTouchMove(e) {
  365. if (this.isRecording) {
  366. if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 100) {
  367. this.voiceText = '松开手指,取消发送';
  368. this.canSend = false;
  369. } else if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 20) {
  370. this.voiceText = '上划可取消';
  371. this.canSend = true;
  372. } else {
  373. this.voiceText = '抬起停止';
  374. this.canSend = true;
  375. }
  376. }
  377. },
  378. // 手指离开页面滑动
  379. handleTouchEnd() {
  380. this.isRecording = false;
  381. this.voiceText = '按住说话,松手发送';
  382. uni.hideLoading();
  383. this.recorderManager.stop();
  384. },
  385. sendTextMessage() {
  386. const to = String(this.userInfo.id);
  387. const text = this.inputText;
  388. const message = uni.$TUIKit.createTextMessage({
  389. to,
  390. conversationType: this.conversation.type,
  391. payload: {
  392. text
  393. }
  394. });
  395. this.inputText = '';
  396. this.$sendTIMMessage(message);
  397. },
  398. $sendTIMMessage(message) {
  399. uni.$TUIKit.sendMessage(message).then((res) => {
  400. this.$EventBus.$emit('sendMessage', message)
  401. this.$refs.messageList.scrollToButtom();
  402. }).catch((error) => {
  403. uni.showToast({
  404. title: '发送消息失败',
  405. icon: "none"
  406. })
  407. })
  408. },
  409. }
  410. }
  411. </script>
  412. <style lang="scss" scoped>
  413. .container {
  414. width: 100vw;
  415. height: 100vh;
  416. background-color: $bgcolor1;
  417. position: relative;
  418. overflow: hidden;
  419. .topnav {
  420. padding: 0 10rpx;
  421. position: fixed;
  422. top: 0;
  423. left: 0;
  424. width: 100vw;
  425. z-index: 100;
  426. background-color: $bgcolor1;
  427. .nav-item {
  428. width: 40rpx;
  429. height: 40rpx;
  430. margin-left: 16rpx;
  431. .nav-img {
  432. width: 40rpx;
  433. height: 40rpx;
  434. }
  435. }
  436. .nav-center {
  437. flex: 1;
  438. flex-direction: column;
  439. .nav-text {
  440. color: $fontcolor5;
  441. height: 40rpx;
  442. text-align: center;
  443. }
  444. .nav-tip {
  445. color: $fontcolor2;
  446. }
  447. }
  448. }
  449. .talk-box {
  450. position: fixed;
  451. bottom: 0;
  452. left: 0;
  453. width: 100vw;
  454. padding-top: 16rpx;
  455. background-color: $bgcolor1;
  456. .input-box {
  457. margin: 0rpx 32rpx;
  458. height: 80rpx;
  459. border-radius: 80rpx;
  460. background-color: $bgcolor4;
  461. padding: 0 24rpx;
  462. .input {
  463. width: 100%;
  464. height: 100%;
  465. color: #ffffff;
  466. font-size: 22rpx;
  467. }
  468. .input-btn {
  469. background-color: $primary;
  470. color: #ffffff;
  471. border-radius: 16rpx;
  472. height: 56rpx;
  473. line-height: 56rpx;
  474. width: 120rpx;
  475. text-align: center;
  476. }
  477. }
  478. .action-box {
  479. padding: 0rpx 0rpx 20rpx 0rpx;
  480. margin: 0 90rpx;
  481. margin-top: 36rpx;
  482. .act-img {
  483. width: 56rpx;
  484. height: 56rpx;
  485. }
  486. }
  487. .action-panel {
  488. width: 100vw;
  489. .voice-panel {
  490. height: 400rpx;
  491. flex-direction: column;
  492. .voice-text {
  493. color: #7D7DA4;
  494. text-align: center;
  495. }
  496. .voice-img {
  497. width: 200rpx;
  498. height: 200rpx;
  499. margin-top: 40rpx;
  500. }
  501. }
  502. }
  503. .emoji-box {
  504. padding: 0 11rpx;
  505. box-sizing: border-box;
  506. height: 400rpx;
  507. flex-wrap: wrap;
  508. transition: height .3s;
  509. .emoji-item {
  510. width: 64rpx;
  511. height: 64rpx;
  512. padding: 20rpx;
  513. .emoji-img {
  514. width: 64rpx;
  515. height: 64rpx;
  516. }
  517. }
  518. }
  519. }
  520. .container-box {
  521. position: fixed;
  522. display: flex;
  523. justify-content: center;
  524. align-items: center;
  525. left: 0;
  526. right: 0;
  527. bottom: 0;
  528. top: 0;
  529. background-color: rgba(0, 0, 0, 0.5);
  530. .video-message {
  531. width: 90vw;
  532. height: auto;
  533. }
  534. }
  535. }</style>