chatting.vue 13 KB

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