微信小程序原生接入腾讯云im(单聊,列表,聊天界面,自定义消息,自动回复)

微信小程序原生接入腾讯云im(单聊,列表,聊天界面,自定义消息,自动回复)

发送图片语音消息传送→

文章目录

1.项目需求
2.参考文档
3.效果图
4.初始化 集成SDK
5.登录
6.会话列表
7.聊天页面
8.遇到的问题

项目需求

公司需要在已上线的小程序中新增聊天功能(房源租赁小程序,跟置业顾问沟通)(ÒωÓױ)!,最后决定使用腾讯云im,无奈 只能硬着头皮上 o(╥﹏╥)o,接下来记录一下整个开发步骤,后期还会不断优化聊天功能 。╮(╯▽╰)╭

参考文档

https://cloud.tencent.com/document/product/269/37413 (腾讯im 集成文档)
https://imsdk-1252463788.file.myqcloud.com/IM_DOC/Web/SDK.html?=_ga=1.205222681.809978884.1544594125#createTextMessage(sdk
客户端api文档)

微信小程序原生接入腾讯云im(单聊,列表,聊天界面,自定义消息,自动回复)_第1张图片

由于腾讯云的demo使用了mpvue框架 我这儿是用原生写的 o(╥﹏╥)o,只能参考文档自己采坑。

效果图

微信小程序原生接入腾讯云im(单聊,列表,聊天界面,自定义消息,自动回复)_第2张图片聊天页面
微信小程序原生接入腾讯云im(单聊,列表,聊天界面,自定义消息,自动回复)_第3张图片会话列表

初始化

参考腾讯云im集成文档 小程序项目集成方法如下(若同步依赖过程中出现问题,请切换 npm 源后再次重试(使用cnpm))

1,集成SDK

// IM 小程序 SDK
npm install tim-wx-sdk --save
// 发送图片、文件等消息需要的 COS SDK
npm install cos-wx-sdk-v5 --save

2,在项目脚本里引入模块,并初始化
这里的初始化代码写在了app.js文件里(里面包含各种监听事件,这里暂时还未对各个监听函数进行封装)(目前阶段主要用到了 收到消息,发送消息 )
// 这里引入了一个监听器 (因为小程序没有类似vuex的状态管理器 当global里面的数据变化时不能及时同步到聊天页面 因此 这个监听器可以emit一个方法 到需要更新会话数据的页面 在那里进行赋值)后面会在遇到的问题里说这个方法
引入

import TIM from 'tim-wx-sdk'
import COS from "cos-wx-sdk-v5"
App({
  onLaunch: function () {
  	this.iminit()
  },
  iminit() {
    let options = {
      SDKAppID: ****** // 接入时需要将0替换为您的即时通信 IM 应用的 SDKAppID
    }
    var that = this
    // 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
    let tim = TIM.create(options);// SDK 实例通常用 tim 表示
    // 设置 SDK 日志输出级别,详细分级请参见 setLogLevel 接口的说明
    // tim.setLogLevel(0); // 普通级别,日志量较多,接入时建议使用
    tim.setLogLevel(1); // release 级别,SDK 输出关键信息,生产环境时建议使用
    // 注册 COS SDK 插件
    tim.registerPlugin({'cos-wx-sdk': COS})
    // 监听事件,例如:
    tim.on(TIM.EVENT.SDK_READY, function(event) {
      console.log('SDK_READY')
      that.globalData.isImLogin = true
      wx.setStorageSync('isImLogin', true)
      // 收到离线消息和会话列表同步完毕通知,接入侧可以调用 sendMessage 等需要鉴权的接口
      // event.name - TIM.EVENT.SDK_READY
    });
  
    tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event) {
      console.log('收到消息')
      // 若同时收到多个会话 需要根据conversationID来判断是哪个人的会话
      var msgarr = []
      var newMsgForm = event.data[0].conversationID // 定义会话键值
      console.log(msgarr[newMsgForm])
      if(msgarr[newMsgForm]) {
        msgarr[newMsgForm].push(event.data[0])
      } else {
        msgarr[newMsgForm] = [event.data[0]]
      }
      console.log(msgarr[newMsgForm])
      that.globalData.myMessages = msgarr
      // 这里引入了一个监听器 (因为小程序没有类似vuex的状态管理器 当global里面的数据变化时不能及时同步到聊天页面 因此 这个监听器可以emit一个方法 到需要更新会话数据的页面 在那里进行赋值)
      wx.event.emit('testFunc',that.globalData.myMessages,newMsgForm) // 详情页的函数
      wx.event.emit('conversation') // 会话列表的监听函数
      // 未读消息数
      var number = wx.getStorageSync('number_msg') || 0
      // 根据isRead判断是否未读 否则加1
      if(!event.data[0].isRead) {
        number = number++
      }
      console.log(number)
      wx.setStorageSync('number_msg', number)
      // 如果有未读数 需要设置tabbar的红点标志 反之去掉红点标志
      if(number>0) {
        wx.setTabBarBadge({
          index: 2,
          text: number.toString()
        })
      } else {
        wx.hideTabBarRedDot({
          index: 2
        })
      }
      // 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
      // event.name - TIM.EVENT.MESSAGE_RECEIVED
      // event.data - 存储 Message 对象的数组 - [Message]
    })
  
    tim.on(TIM.EVENT.MESSAGE_REVOKED, function(event) {
      // 收到消息被撤回的通知
      // event.name - TIM.EVENT.MESSAGE_REVOKED
      // event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isRevoked 属性值为 true
    });
  
    tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, function(event) {
      // 更新当前所有会话列表
      // 注意 这个函数在首次点击进入会话列表的时候也会执行 因此点击消息 可以显示当前的未读消息数(unreadCount表示未读数)
      console.log('发送了消息')
      console.log('更新当前所有会话列表')
      var conversationList = event.data
      var number =  0
      conversationList.forEach(e => {
        number = number + e.unreadCount
      })
      wx.setStorageSync('number_msg', number)
      if(number>0) {
        wx.setTabBarBadge({
          index: 2,
          text: number.toString()
        })
      } else {
        wx.hideTabBarRedDot({
          index: 2
        })
      }
      // 收到会话列表更新通知,可通过遍历 event.data 获取会话列表数据并渲染到页面
      // event.name - TIM.EVENT.CONVERSATION_LIST_UPDATED
      // event.data - 存储 Conversation 对象的数组 - [Conversation]
    });
  
    tim.on(TIM.EVENT.GROUP_LIST_UPDATED, function(event) {
      // 收到群组列表更新通知,可通过遍历 event.data 获取群组列表数据并渲染到页面
      // event.name - TIM.EVENT.GROUP_LIST_UPDATED
      // event.data - 存储 Group 对象的数组 - [Group]
    });
  
    tim.on(TIM.EVENT.GROUP_SYSTEM_NOTICE_RECEIVED, function(event) {
      // 收到新的群系统通知
      // event.name - TIM.EVENT.GROUP_SYSTEM_NOTICE_RECEIVED
      // event.data.type - 群系统通知的类型,详情请参见 GroupSystemNoticePayload 的 operationType 枚举值说明
      // event.data.message - Message 对象,可将 event.data.message.content 渲染到到页面
    });
  
    tim.on(TIM.EVENT.PROFILE_UPDATED, function(event) {
      // 收到自己或好友的资料变更通知
      // event.name - TIM.EVENT.PROFILE_UPDATED
      // event.data - 存储 Profile 对象的数组 - [Profile]
    });
  
    tim.on(TIM.EVENT.BLACKLIST_UPDATED, function(event) {
      // 收到黑名单列表更新通知
      // event.name - TIM.EVENT.BLACKLIST_UPDATED
      // event.data - 存储 userID 的数组 - [userID]
    });
  
    tim.on(TIM.EVENT.ERROR, function(event) {
      // 收到 SDK 发生错误通知,可以获取错误码和错误信息
      // event.name - TIM.EVENT.ERROR
      // event.data.code - 错误码
      // event.data.message - 错误信息
    });
  
    tim.on(TIM.EVENT.SDK_NOT_READY, function(event) {
      // wx.setStorageSync('isImLogin', false)
      console.log('SDK_NOT_READY')
      that.globalData.isImLogin = false
      wx.setStorageSync('isImLogin', false)
      // 收到 SDK 进入 not ready 状态通知,此时 SDK 无法正常工作
      // event.name - TIM.EVENT.SDK_NOT_READY
    });
  
    tim.on(TIM.EVENT.KICKED_OUT, function(event) {
      console.log('KICKED_OUT')
      wx.setStorageSync('isImLogin', false)
      that.globalData.isImLogin = false
      // 收到被踢下线通知
      // event.name - TIM.EVENT.KICKED_OUT
      // event.data.type - 被踢下线的原因,例如:
      //    - TIM.TYPES.KICKED_OUT_MULT_ACCOUNT 多实例登录被踢
      //    - TIM.TYPES.KICKED_OUT_MULT_DEVICE 多终端登录被踢
      //    - TIM.TYPES.KICKED_OUT_USERSIG_EXPIRED 签名过期被踢
    })
    that.globalData.tim = tim
  },
  globalData: {
    tim: '',
    isImLogin: false,
    msgList: [],
    myMessages: new Map(),
    tabBottom: 0, // 全面屏底部黑条高度
    accountTid: '', //当前用户的tid
    isDetail: true  
  }
 })

登录

点击底部消息到消息列表 (onShow里面加判断)

// 因为所有的api调用都需要SDK处于read状态才可以 此处如果登录我存在了global里面 因为不知道如何判断SDK是否处于read状态 只能每次进入都登录一次(不刷新的话不需要重新登录) 呃(⊙o⊙)…
// wx.getStorageSync('isImLogin') 之前尝试存在本地缓存 发现一刷新 SDK就不处于read状态了 
 onShow: function () {
	  if (app.globalData.isImLogin) {
	    // 已经登录了SDK处于read状态
	    this.setData({
	      hasUserInfo: true
	    })
	    // 由于登录是写在会话列表的 因此如果已经登录 (SDK处于ready状态)就直接获取会话列表(会话列表函数在下面会话列表里整体贴)
	    this.initRecentContactList()
	  } else {
	    if (wx.getStorageSync('tokenAdmin')) {
	      util.sLoading()
	      this.setData({
	        hasUserInfo: true
	      })
	      // 获取登录密码userSign和tid(这里通过后端接口获取)
	      this.getPassword()
	    } else {
	      // 没有登录 就会出现一个授权页 让用户登录(小程序的登录)针对没有登录过的用户,登录过的用户做了静默登录 会自动登录
	      this.setData({
	        hasUserInfo: false
	      })
	    }
	  }
 }
  // 获取登录所用的userSign 和 tid(密码)
  getPassword() {
    http.getUserSign({
      header: {
        'Authorization': 'bearer ' + wx.getStorageSync('tokenAdmin').access_token
      },
      data: {
        openId: wx.getStorageSync('tokenAdmin').openId,
        nickName: app.globalData.userInfo ? app.globalData.userInfo.nickName : '',
        faceUrl: app.globalData.userInfo ? app.globalData.userInfo.avatarUrl : ''
      },
      success: res => {
        this.setData({
          userSign: res.data.sign,
          userId: res.data.tid
        })
        app.globalData.accountTid = res.data.tid
        this.loginIm()
      },
      fail: err => {
        util.sLoadingHide()
        wx.showToast({
          title: 'get password error' + err,
          icon: 'none',
          duration: 3000
        })
        console.log(err)
      }
    })
  },
  //腾讯云im的登录
  loginIm() {
    var that = this
    var tim = app.globalData.tim
    let promise = tim.login({userID: that.data.userId, userSig: that.data.userSign});
    promise.then(function(imResponse) {
      console.log(imResponse)
      console.log('登录成功')
      wx.setStorageSync('isImLogin', true)
      app.globalData.isImLogin = true
      setTimeout(() => {
        // 拉取会话列表
        that.initRecentContactList()
      }, 1000);
    }).catch(function(imError) {
      util.sLoadingHide()
      wx.showToast({
        title: 'login error' + imError,
        icon: 'none',
        duration: 3000
      })
      console.warn('login error:', imError); // 登录失败的相关信息
    })
  },

会话列表

1,会话列表wxml

<!--pages/message/index.wxml-->
<wxs src="../../utils/filter.wxs" module="filter"/>
<view style='padding-top: calc({{height}}px + 18rpx)'>
  // 自定义头部
  <nav-bar title="消息" showIcon="0"></nav-bar>
  <block wx:for="{{msg}}" wx:key="index">
    <view class="item" bindtap="contactsClick" data-conversationid="{{item.conversationID}}" data-name="{{item.userProfile.nick}}" data-avatar="{{item.userProfile.avatar}}">
      <image src="{{item.userProfile.avatar ? item.userProfile.avatar : '/images/avatar.png'}}" class="avatar"></image>
      <view class="right">
        <view class="name"><text>{{item.userProfile.nick}}</text><text class="tag" wx:if="{{filter.consultant(item.userProfile.userID)}}">置业顾问</text></view>
        <view class="text" wx:if="{{item.lastMessage.type != 'TIMCustomElem'}}">{{item.lastMessage.payload.text}}</view>
        <view class="text" wx:if="{{item.lastMessage.type == 'TIMCustomElem'}}">[房源]{{item.lastMessage.payload.data.title || item.lastMessage.payload.description}}<text style="padding-left: 10rpx">{{item.lastMessage.payload.data.price || item.lastMessage.payload.extension}}元/m²·月</text></view>
      </view>
      <view class="time">{{filter.getDateDiff(item.lastMessage.lastTime, now)}}</view>
      <view class="unreadCount" wx:if="{{item.unreadCount > 0}}">{{item.unreadCount * 1 > 99 ? '99+' : item.unreadCount}}</view>
    </view>
  </block>
  <!-- 使用消息需要授权登录 根据需要 自己封装-->
  <login wx:if="{{!hasUserInfo}}" bind:closePage="closePage">
    <view class="middle_box">
      <view class="line" style="height: calc({{height}}px + 16rpx)"></view>
      <image class="yzz_logo" src="/images/yzz_logo.png"></image>
      <view class="des">为了给您提供更好的服务,壹直租申请获取您的昵称、头像信息</view>
      <view class="login_btn">授权登录</view>
    </view>
  </login>
</view>
<view wx:if="{{ empty_show }}" class="empty">
  <image src="/images/msg_empty.png" class="msg_empty"></image>
  <view class="empty_text">暂无聊天记录</view>
</view>

2,会话列表页面样式wxss

/* pages/message/index.wxss */
page{
  background-color: #fff;
}
.item{
  border-bottom: 1px solid #EDEDED;
  height: 149rpx;
  display: flex;
  position: relative;
  align-items: center;
}
.item:nth-of-type(1) {
  border-top: 1px solid #EDEDED;
}
.avatar{
  width: 89rpx;
  height: 89rpx;
  border-radius: 50%;
  margin-left: 40rpx;
  margin-right: 22rpx;
  box-sizing: content-box;
}
.right .name {
  font-size:32rpx;
  color:rgba(35,35,35,1);
  margin-bottom: 8rpx;
  display: flex;
  align-items: center;
}
.right .name .tag{
  width:114rpx;
  height:30rpx;
  background:rgba(246,247,248,1);
  border-radius:15rpx;
  color: #9EA2AC;
  font-size: 22rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 4rpx;
  margin-left: 10rpx;
}
.right .text {
  color: #A2A3A4;
  font-size: 24rpx;
  width: 466rpx;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.item .time{
  position: absolute;
  right: 34rpx;
  top: 44rpx;
  color: #9EA2AC;
  font-size: 20rpx;
}
.unreadCount {
  position: absolute;
  right: 34rpx;
  top: 79rpx;
  color: #fff;
  background-color: #F00C22;
  font-size: 20rpx;
  border-radius: 50rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0 10rpx;
  height: 32rpx;
  min-width: 32rpx;
}
.middle_box{
  position: fixed;
  left: 0;
  top: 0;
  z-index: 999;
  height: 100%;
  width: 100%;
  display:flex;
  flex-direction:column;
  align-items:center;
  /* justify-content: center; */
  background-color: #fff;
}
.middle_box .line{
  width:750rpx;
  box-shadow:0px 1px 0px 0px rgba(239,239,239,1);
}
.yzz_logo{
  width: 104rpx;
  height: 165rpx;
  margin-top: 290rpx;
  margin-bottom: 50rpx;
}
.middle_box .des{
  width:373rpx;
  height:55rpx;
  font-size:24rpx;
  color:rgba(160,160,160,1);
  margin-bottom: 140rpx;
  line-height: 38rpx;
  text-align: left;
}
.login_btn{
  width:548rpx;
  height:88rpx;
  background:rgba(255,147,40,1);
  border-radius:6rpx;
  font-size: 28rpx;
  color: #FFFFFF;
  display: flex;
  justify-content: center;
  align-items: center;
}
.empty{
  display: flex;
  flex-direction: column;
  align-items: center;
  border-top: 1px solid #efefef;
}
.msg_empty{
  width: 350rpx;
  height: 246rpx;
  margin-top: 198rpx;
}
.empty_text{
  font-size: 24rpx;
  color: #AAAAAA;
  margin-top: 52rpx;
}

3,会话列表页面js

data: {
    userId: '',
    hasUserInfo: false,
    userSign: '',
    nickName: '',
    msg: [],
    empty_show: false,
    now: '',
    height: app.globalData.height 
  },
    // 点击消息列表跳转到聊天详情页(需要把列表页的头像传过去,因为详情获取的数据里面没有聊天头像)
  contactsClick(e) {
    var conversationID= e.currentTarget.dataset.conversationid // 置业顾问的conversationID(当前会话的人)
    var avatar= e.currentTarget.dataset.avatar
    var name= e.currentTarget.dataset.name
    wx.navigateTo({
      url: '/subpackages/message-detail/index?conversationID=' + conversationID + '&avatar=' + avatar  + '&name=' + name,
    })
  },
  // 获取会话列表 (必须要在SDK处于ready状态调用(否则会报错))
  initRecentContactList() {
    var that = this
    // 拉取会话列表
    var tim = app.globalData.tim
    let promise = tim.getConversationList();
    if(!promise) {
      util.sLoadingHide()
      wx.showToast({
        title: 'SDK not ready',
        icon: 'none',
        duration: 3000
      })
      return
    }
    promise.then(function(imResponse) {
      util.sLoadingHide()
      console.log('会话列表')
      console.log(imResponse)
      // 如果最后一条消息是自定义消息的话,处理一下data
      const conversationList = imResponse.data.conversationList; // 会话列表,用该列表覆盖原有的会话列表
      conversationList.forEach(e => {
        if(e.lastMessage.type == 'TIMCustomElem') {
          var data = e.lastMessage.payload.data
          var new_data = ''
          if(typeof(data) == 'string' && data) {
            new_data = JSON.parse(data)
          }
          e.lastMessage.payload.data = new_data
        }
      })
      that.setData({
        msg: conversationList,
        empty_show: conversationList && conversationList.length>0 ? false : true
      })
      var number = 0
      conversationList.forEach(e => {
        number = number + e.unreadCount
      })
      if(number>0) {
        wx.setTabBarBadge({
          index: 2,
          text: number.toString()
        })
      } else {
        wx.hideTabBarRedDot({
          index: 2
        })
      }
    }).catch(function(imError) {
      util.sLoadingHide()
      wx.showToast({
        title: 'getConversationList error:' + imError,
        icon: 'none',
        duration: 3000
      })
      console.warn('getConversationList error:', imError); // 获取会话列表失败的相关信息
    })
  },

补充: 从房源进入的跳转(需要多传一个type和housid)(详情页的置业顾问 在这儿封装成了的组件,使用如下(主要看msgindex))

详情页写法:

<block wx:for="{{baseDto.consultantsDtos}}" wx:key="index" wx:if="{{index>
  <adviser item="{{item}}" houseid="{{baseDto.id}}" msgindex="{{msg_index}}" type="{{rentType}}"></adviser>
</block>
onLoad: function (options) {
    msg_index: options.index || 0
},

组件内部跳转

meassge(e) {
      console.log(e.currentTarget.dataset)
      var houseid = e.currentTarget.dataset.houseid
      var type = e.currentTarget.dataset.type // 0 building 1 shop
      var avatar = e.currentTarget.dataset.avatar
      var conversationID = 'C2C' + e.currentTarget.dataset.tid
      var name = e.currentTarget.dataset.name
      // C2Cc2020042017735
      if(this.properties.msgindex) {
        // 点击直接返回聊天界面(从聊天界面进入的)(处理多次从发送的房源点进去再聊天 小程序页面打开数超过10个点不动问题)
        wx.navigateBack({
          delta: 1
        })
      } else {
        wx.navigateTo({
          url: '/subpackages/message-detail/index?type=' + type + '&houseid=' + houseid + '&conversationID=' + conversationID + '&avatar=' + avatar  + '&name=' + name,
        })
      }
    },

聊天页面

详情页:可以一对一文字聊天 下拉加载更多历史记录 进入聊天详情的入口有两个 1、从房源详情进入 2、直接从会话列表进入 区别: 房源详情进入需要 发送一条当前房源的数据信息(自定义信息)需要有一条自动回复 (类似置业顾问的欢迎语),从列表进入则不需要。

1、页面wxml

<view class='chat' id="chat" style="min-height:{{height}}px; padding-bottom:116rpx; background-color:#EFF0F3">
  <!-- <view class="more"><text class="more_text">{{more_text}}</text></view> 下拉加载更多 -->
  <view class="more"><text class="more_text">聊天的时候,置业顾问无法知道您的手机号!</text></view>
  <block wx:for="{{myMessages}}" wx:key="index" >
    <!-- 自定义消息 -->
    <view class="chat_box" wx:if="{{item.type == 'TIMCustomElem'}}">
      <view class="chat-item" wx:if="{{item.flow == 'in'}}" data-type="{{item.payload.data.type}}" data-id="{{item.payload.data.id}}" bindtap="house_detail">
        <image class='avatar' style="margin-right: 19rpx;" mode= "scaleToFill" src="{{friendAvatarUrl ? friendAvatarUrl : '/images/avatar.png'}}"></image>
        <view class="custom_box">
          <image src="{{item.payload.data.house_pic}}" class="pic"></image>
          <view class="des_box">
            <view class="title">{{item.payload.data.title}}</view>
            <view class="des">
              <view>{{item.payload.data.area}}</view>
              <view style="padding:0 8rpx">|</view>
              <view class="park_name">{{item.payload.data.city}}·{{item.payload.data.park}}</view>
            </view>
            <view class="price">{{item.payload.data.price}}元/m²·月</view>
          </view>
        </view>
      </view>
      <view wx:else class="chat-item flex-wrap" data-type="{{item.payload.data.type}}" data-id="{{item.payload.data.id}}" bindtap="house_detail">
        <view class='avatar' style="margin-left: 19rpx" wx:if="{{item.flow == 'out'}}">
          <open-data type="userAvatarUrl"></open-data>
        </view>
        <view class="custom_box">
          <image src="{{item.payload.data.house_pic}}" class="pic"></image>
          <view class="des_box">
            <view class="title">{{item.payload.data.title}}</view>
            <view class="des">
              <view>{{item.payload.data.area}}</view>
              <view style="padding:0 8rpx">|</view>
              <view class="park_name">{{item.payload.data.city}}·{{item.payload.data.park}}</view>
            </view>
            <view class="price">{{item.payload.data.price}}元/m²·月</view>
          </view>
        </view>
      </view>
    </view>
    <view class="chat_box" wx:if="{{item.type != 'TIMCustomElem'}}">
      <view class="chat-item {{item.flow == 'in' ? '' : 'flex-wrap'}}">
        <image wx:if="{{item.flow == 'in'}}" class='avatar' style="margin-right: 19rpx;" mode= "scaleToFill" src="{{friendAvatarUrl ? friendAvatarUrl : '/images/avatar.png'}}"></image>
        <view class='avatar' style="margin-left: 19rpx" wx:else>
          <open-data  type="userAvatarUrl"></open-data>
        </view>
        <view class='content'>{{item.payload.text}}</view>
      </view>
    </view>
  </block>
</view>
<view class="chat-footer" style="padding-bottom: calc({{tabBottom}}px + 25rpx)">
<view class='input' bindtap="bindFocus">
  <textarea class="inputArea" focus="{{focus}}" fixed="true" cursor-spacing="25" disable-default-padding="true" bindinput="bindKeyInput" bindfocus="bindfocus" bindblur="bindblur" value="{{inputValue}}" placeholder=""/>
  <text class="placeHolder" wx:if="{{inputShow}}">对ta发送消息</text>
</view>
  <view class='send' bindtap='bindConfirm'>发送</view>
</view>

2,聊天详情页css

/* subpackages/message-detail/index.wxss */
.custom_box{
  width: 510rpx;
  border-radius: 4rpx;
  box-shadow:0px 4px 12px 0px rgba(4,0,0,0.03);
  background-color: #fff;
  display: flex;
  padding: 25rpx 20rpx;
}
.chat{
  overflow: scroll;
}
.pic{
  width: 162rpx;
  height: 141rpx;
  border-radius: 2rpx;
  margin-right: 18rpx;
  flex-shrink: 0;
}
.des_box{

}
.title{
  font-size: 28rpx;
  color: #232323;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient: vertical;
}
.des_box .des{
  display: flex;
  color: #999999;
  font-size: 20rpx;
  width: 280rpx;
  height: 30rpx;
  margin-top: 2rpx;
}
.park_name{
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.des_box .price{
  font-size: 28rpx;
  color: #FF8711;
  margin-top: 6rpx;
}
.chat_box{
  padding: 0 33rpx;
}
.avatar{
  width:78rpx;
  height:78rpx;
  border-radius:50%;
  overflow: hidden;
}
.chat-item{
  display: flex;
  margin-bottom: 46rpx;
}
.chat-item.flex-wrap{
  flex-direction: row-reverse;
}
.chat-item .content{
  max-width: 512rpx;
  padding: 24rpx;
  border-radius: 4rpx;
  background-color: #fff;
  color: #232323;
  font-size: 28rpx;
  word-wrap: break-word;
}
.chat-footer{
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  background: #ffffff;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx 25rpx;
}
.chat-footer.full_sucreen{
  padding-bottom: 100rpx;
}
.input{
  width:527rpx;
  height:76rpx;
  line-height: 76rpx;
  background:rgba(255,255,255,1);
  border: none;
  border:1px solid rgba(212, 215, 222, 1);
  border-radius:6rpx;
  font-size: 26rpx;
  padding:0 20rpx;
  display: flex;
  flex-direction: row;
  align-items: center;
  position: relative;
}
.inputArea{
  position: absolute;
  width: 487rpx;
  height: 30rpx;
  line-height: 30rpx;
  left: 20rpx;
  top:50%;
  margin-top: -15rpx;
  z-index: 1;
}
.placeHolder{
  position: absolute;
  font-size: 26rpx;
color: #cccccc;
  height: 50rpx;
  line-height: 50rpx;
  left: 20rpx;
  top:50%;
  margin-top: -25rpx;
  z-index: 0;
}
.send{
  color: #fff;
  background-color: #FF9328;
  width: 124rpx;
  height: 76rpx;
  border-radius: 12rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 26rpx;
}
.footer-h{
  position: fixed;
  top: 100px;
}
.more{
  display: flex;
  justify-content: center;
  align-items: center;
}
.more_text{
  padding: 6rpx 14rpx;
  background:rgba(216,216,216,1);
  border-radius:4rpx;
  color: #FFFFFF;
  font-size: 20rpx;
  margin: 30rpx auto;
}

3、聊天详情页js

import TIM from 'tim-wx-sdk'
import http from '../../utils/api.js'
const app = getApp()
Page({

  /**
   * 页面的初始数据
   */
  data: {
    noData: '/images/defaultPark.png',
    houseDefault: '/images/delete.png',
    inputValue:'',//发送的文字消息内容
    myMessages: [],//消息
    selToID:0,
    scrollTop: 0,
    houseId:'',//房源id
    type:'',//房源类型
    height:'',
    complete:0,//默认为有历史记录可以拉取
    is_lock:true,//发送消息锁,
    nav_title: '',
    tim: '',
    userSign: '',
    userId: '', // 自己的id
    conversationID: '', // 置业顾问的id
    msgList: app.globalData.msgList,
    friendAvatarUrl: '',
    tabBottom: app.globalData.tabBottom,
    top_height: app.globalData.height,
    isCompleted: false,
    nextReqMessageID: '',
    more_text: '下拉查看更多历史信息',
    isSuperSend: false,
    isDetail: false,
    inputHeight: 0,
    inputShow:true,
    focus:false,
    adjust: true
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    var that = this
    wx.showLoading({
      title: '加载中...',
      icon: 'none'
    })
    that.setData({
      conversationID: options.conversationID,
      friendAvatarUrl: options.avatar,
      height: wx.getSystemInfoSync().windowHeight,
      houseId: options.houseid * 1 || '',
      type: options.type* 1, // 0 building 1 shop
      nav_title: options.name,// 设置头部title(自定义的)
      isDetail: true
    })
    wx.setNavigationBarTitle({
      title: options.name
    })
    // 滚动到底部
    that.pageScrollToBottom()
    wx.event.on('testFunc',(e,newMsgForm)=>{
      console.log('testFunc')
      if((newMsgForm === options.conversationID) && app.globalData.isDetail) {
        var newmsg = app.globalData.myMessages[that.data.conversationID]
        if (newmsg) {
          newmsg.forEach(e => {
            if(e.type == 'TIMCustomElem') {
              if(typeof(e.payload.data) == 'string' && e.payload.data) {
                var new_data = JSON.parse(e.payload.data)
                e.payload.data = new_data
              }
            }
            if(!e.isRead) {
              that.setData({
                myMessages: that.data.myMessages.concat(newmsg)
              })
            }
          })
        }
        console.log(that.data.myMessages)
        that.setMessageRead()
        that.pageScrollToBottom()
      }
    })
    // watch.setWatcher(that); // 设置监听器,建议在onLoad下调用
    if(app.globalData.isImLogin) {
      console.log('登录了')
      // 获取消息列表
      that.getMsgList()
    } else {
      console.log('未登录')
      that.getPassword()
    }
  },
  watch:{
    myMessages:function(newVal,oldVal){
      console.log(newVal,oldVal)
    }
  },
  inputFocus(e) {
    console.log(e)
    var inputHeight = 0
    if (e.detail.height) {
      inputHeight = e.detail.height
    }
    this.setData({
      inputHeight: inputHeight
    })
    this.pageScrollToBottom()
  },
  inputBlur(e) {
    this.setData({
      inputHeight: 0,
    })
  },
  getPassword() {
    var that = this
    http.getUserSign({
      header: {
        'Authorization': 'bearer ' + wx.getStorageSync('tokenAdmin').access_token
      },
      data: {
        openId: wx.getStorageSync('tokenAdmin').openId,
        nickName: app.globalData.userInfo ? app.globalData.userInfo.nickName : '',
        faceUrl: app.globalData.userInfo ? app.globalData.userInfo.avatarUrl : ''
      },
      success: res => {
        that.setData({
          userSign: res.data.sign,
          userId: res.data.tid
        })
        app.globalData.accountTid = res.data.tid
        var tim = app.globalData.tim
        let promise = tim.login({userID: res.data.tid, userSig: res.data.sign})
        promise.then(res => {
          console.log('登录成功')
          wx.setStorageSync('isImLogin', true)
          app.globalData.isImLogin = true
          setTimeout(() => {
            that.getMsgList()
          }, 1000);
        })
      },
      fail: err => {
        console.log(err)
      }
    })
  },
  getMsgList() {
    console.log('获取会话列表')
    var that = this
    var tim = app.globalData.tim
    if (that.data.houseId) {
      // 从房源详情进入聊天界面(请求房源详情 发送一条自定义信息)// 0 building 1 shop
      if (that.data.type * 1 === 0) {
        that.createXzlmsg()
      } else if(that.data.type * 1 === 1){
        that.createShopmsg()
      }
    }
    // 拉取会话列表
    var params = {
      conversationID: that.data.conversationID, 
      count: 15,
      nextReqMessageID: that.data.nextReqMessageID
    }
    let promise = tim.getMessageList(params);
    promise.then(function(imResponse) {
      console.log('会话列表')
      const messageList = imResponse.data.messageList; // 消息列表。
      // 处理自定义的消息
      messageList.forEach(e => {
        if(e.type == 'TIMCustomElem') {
          if(typeof(e.payload.data) == 'string' && e.payload.data) {
            var new_data = JSON.parse(e.payload.data)
            e.payload.data = new_data
          }
        }
      })
      const nextReqMessageID = imResponse.data.nextReqMessageID; // 用于续拉,分页续拉时需传入该字段。
      const isCompleted = imResponse.data.isCompleted; // 表示是否已经拉完所有消息。
      // 将某会话下所有未读消息已读上报
      that.setMessageRead()
      that.setData({
        myMessages: messageList,
        isCompleted: isCompleted,
        nextReqMessageID: nextReqMessageID,
        more_text: isCompleted ? '没有更多了': '下拉查看更多历史信息'
      })
      wx.hideLoading()
      that.pageScrollToBottom()
    }).catch(function(imError) {
      console.warn('getConversationList error:', imError); // 获取会话列表失败的相关信息
    });
  },
  // 默认欢迎语
  getSingleMsg() {
    var that = this
    var text = '您好,我是天安置业顾问' + that.data.nav_title + ',很高兴为您服务,请问有什么可以帮到您?'
    http.sendSingleMsg({
      header: {
        'Content-Type': 'application/json',
        'Authorization': 'bearer ' + wx.getStorageSync('tokenAdmin').access_token
      },
      data: {
        fromAccount: that.data.conversationID.slice(3),
        toAccount: app.globalData.accountTid,
        text: text,
        isSuperSend: that.data.isSuperSend,
      },
      success: res => {
        console.log('发送欢迎语')
        that.pageScrollToBottom()
      },
      fail: err=> {
        console.log(err)
      }
    })
  },
  // 下来加载更多聊天历史记录
  getMoreMsgList() {
    wx.hideLoading()
    // console.log('获取会话列表')
    var tim = app.globalData.tim
    var that = this
    // 拉取会话列表
    var params = {
      conversationID: that.data.conversationID, 
      count: 15,
      nextReqMessageID: that.data.nextReqMessageID
    }
    let promise = tim.getMessageList(params);
    promise.then(function(imResponse) {
      // console.log('下拉获取会话列表')
      // 处理自定义的消息
      imResponse.data.messageList.forEach(e => {
        if(e.type == 'TIMCustomElem') {
          if(e.payload.data) {
            var new_data = JSON.parse(e.payload.data)
            e.payload.data = new_data
          }
        }
      })
      const messageList = imResponse.data.messageList.concat(that.data.myMessages); // 消息列表。
      const nextReqMessageID = imResponse.data.nextReqMessageID; // 用于续拉,分页续拉时需传入该字段。
      const isCompleted = imResponse.data.isCompleted; // 表示是否已经拉完所有消息。
      that.setData({
        myMessages: messageList,
        isCompleted: isCompleted,
        nextReqMessageID: nextReqMessageID,
        more_text: isCompleted ? '没有更多了': '下拉查看更多历史信息'
      })
    }).catch(function(imError) {
      console.warn('getConversationList error:', imError); // 获取会话列表失败的相关信息
    });
  },
  // 设置已读上报
  setMessageRead() {
    var tim = app.globalData.tim
    var that = this
    let promise = tim.setMessageRead({conversationID: that.data.conversationID})
    promise.then(function(imResponse) {
      // 已读上报成功
      var noready = 0
      that.data.myMessages.forEach(e => {
        if(!e.isRead) {
          noready++
        }
      })
      var number = wx.getStorageSync('number_msg')
      var newNumber = number - noready
      wx.setStorageSync('number_msg', newNumber)
    }).catch(function(imError) {
      // 已读上报失败
      console.warn('setMessageRead error:', imError);
    })
  },
  //创建自定义房源消息体
  createXzlmsg(){
    // console.log('创建自定义房源消息体')
    var that = this;
    var id = that.data.houseId
    http.xzlDetail(id, {
      data: {
        timestamp: Date.parse(new Date())
      },
      success: res => {
        if(res.code == 200) {
          var house_pic = res.data.coverUrl ? res.data.coverUrl : '/images/detail_default.jpg' // 房源图片
          var area = res.data.areaConstruction // 面积
          var price = res.data.unitPrice // 单价
          var park = res.data.parkName // 园区名称
          var city = res.data.parkArea // 城市
          var title = res.data.title // 标题
          var type = 0 // 类型 // 0:写字楼,1:商铺,2:广告位
          const params =  {
            house_pic: house_pic,
            area: area,
            price: price,
            park: park,
            city: city,
            title: title,
            type: type,
            id: id
          }
          const option = {
            to: that.data.conversationID.slice(3), // 消息的接收方
            conversationType: TIM.TYPES.CONV_C2C, // 会话类型取值TIM.TYPES.CONV_C2C或TIM.TYPES.CONV_GROUP
            payload: {
              data: JSON.stringify(params),// 自定义消息的数据字段
              description: params.title, // 自定义消息的说明字段
              extension: params.price // 自定义消息的扩展字段
            } // 消息内容的容器
          }
          const tim = app.globalData.tim
          // 2. 创建消息实例,接口返回的实例可以上屏
          let message = tim.createCustomMessage(option)
          // 2. 发送消息
          let promise = tim.sendMessage(message)
          promise.then(function(res){
            // 发送成功
            // console.log('自定义消息发送成功')
            var new_data = JSON.parse(res.data.message.payload.data) 
            res.data.message.payload.data = new_data
            var messageList = that.data.myMessages
            messageList.push(res.data.message)
            that.setData({
              myMessages: messageList
            })
            // 发送自定义欢迎语
            that.getSingleMsg()
          })
        }
      },
      fail: err => {
        console.log(err)
      }
    })
  },
  //创建自定义房源消息体(商铺)
  createShopmsg(){
    var that = this;
    var id = that.data.houseId
    http.shopDetail(id, {
      data: {
        timestamp: Date.parse(new Date())
      },
      success: res => {
        if(res.code == 200) {
          var house_pic = res.data.coverUrl ? res.data.coverUrl : '/images/detail_default.jpg' // 房源图片
          var area = res.data.areaConstruction // 面积
          var price = res.data.unitPrice || '0' // 单价
          var park = res.data.parkName // 园区名称
          var city = res.data.parkArea // 城市
          var title = res.data.title // 标题
          var type = 1 // 类型
          const params =  {
            house_pic: house_pic,
            area: area,
            price: price,
            park: park,
            city: city,
            title: title,
            type: type,
            id: id
          }
          const option = {
            to: that.data.conversationID.slice(3), // 消息的接收方
            conversationType: TIM.TYPES.CONV_C2C, // 会话类型取值TIM.TYPES.CONV_C2C或TIM.TYPES.CONV_GROUP
            payload: {
              data: JSON.stringify(params),// 自定义消息的数据字段
              description: params.title, // 自定义消息的说明字段
              extension: params.price // 自定义消息的扩展字段
            } // 消息内容的容器
          }
          const tim = app.globalData.tim
          // 2. 创建消息实例,接口返回的实例可以上屏
          let message = tim.createCustomMessage(option)
          // 2. 发送消息
          let promise = tim.sendMessage(message)
          promise.then(function(res){
            // 发送成功
            var new_data = JSON.parse(res.data.message.payload.data) 
            res.data.message.payload.data = new_data
            var messageList = that.data.myMessages
            messageList.push(res.data.message)
            that.setData({
              myMessages: messageList
            })
            // 发送自定义欢迎语
            that.getSingleMsg()
          })
        }
      },
      fail: err => {
        console.log(err)
      }
    })
  },
  //获取普通文本消息
  bindKeyInput(e){
    var that = this;
     that.setData({
      inputValue:e.detail.value,
    })
  },
  bindfocus(){
    var that = this;
     that.setData({
      inputShow:false,
      focus:true,
      adjust: true
    })
  },
  bindblur(){
    var that = this;
    if(that.data.inputValue){
      that.setData({
        inputShow:false,
        focus:false
      })
    }else{
      that.setData({
        inputShow:true,
        focus:false
      })
    }
    // 键盘消失
    wx.hideKeyboard()
    // this.setData({
    //   adjust: false
    // })
  },
  // 发送普通文本消息
  bindConfirm(e) {
    var that = this;
    if(that.data.is_lock){
      that.setData({
        is_lock:false
      })
      if (that.data.inputValue.length == 0) {
        wx.showToast({
          title: '消息不能为空!',
          icon:'none'
        })
        that.setData({
          is_lock: true
        })
        return;
      }
      var content = {
        text: that.data.inputValue
      };
      var tim = app.globalData.tim
      var options = {
        to: that.data.conversationID.slice(3), // 消息的接收方
        conversationType: TIM.TYPES.CONV_C2C, // 会话类型取值TIM.TYPES.CONV_C2C或TIM.TYPES.CONV_GROUP
        payload: content // 消息内容的容器
      }
      // // 发送文本消息,Web 端与小程序端相同
      // 1. 创建消息实例,接口返回的实例可以上屏
      let message = tim.createTextMessage(options)
      // 2. 发送消息
      let promise = tim.sendMessage(message)
      promise.then(function(imResponse) {
        // 发送成功
        var messageList = that.data.myMessages
        messageList.push(imResponse.data.message)
        that.setData({
          is_lock:true,
          myMessages: messageList
        })
        that.pageScrollToBottom()
        that.clearInput()
      }).catch(function(imError) {
        // 发送失败
        console.warn('sendMessage error:', imError);
      })
    }
  },
  // 清除输入框
  clearInput(e){
    this.setData({
      inputValue:''
    })
  },
  // 跳转
  house_detail(e) {
    var type = e.currentTarget.dataset.type
    var id = e.currentTarget.dataset.id
    // // 0:写字楼,1:商铺
    if (type*1 === 0) {
      wx.navigateTo({
        url: `/pageHouse/xzl-detail/index?id=${id}&&index=1`
      })
    } else if(type*1 === 1) {
      wx.navigateTo({
        url: `/pageHouse/shop-detail/index?id=${id}&&index=1`
      })
    }
  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    app.globalData.isDetail = true
  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {
    // 键盘消失
    wx.hideKeyboard()
    // this.setData({
    //   adjust: false
    // })
  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {
  	// 关闭聊天界面的时候需要把当前聊天界面的监听器关闭 否则会一直监听着 在其他页面出现调用多次的问题
    wx.event.off("testFunc")
    // 键盘消失
    wx.hideKeyboard()
    // this.setData({
    //   adjust: false
    // })
  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {
    var that = this
    if(!that.data.isCompleted) {
      wx.showLoading({
        title: '加载历史记录中...',
        icon: 'none'
      })
      that.getMoreMsgList()
    } else {
      wx.showToast({
        title: '没有更多历史记录了',
        icon:'none'
      })
    }
    setTimeout(() => {
      wx.stopPullDownRefresh(true)
    }, 300);
  },
  pageScrollToBottom() {
    wx.createSelectorQuery().select('#chat').boundingClientRect(function (rect) {
      // 使页面滚动到底部
      wx.pageScrollTo({
        selector: '#chat',
        scrollTop: rect ? rect.height : 0,
        duration: 0
      })
    }).exec()
  }
})

遇到的问题

开发过程遇到的其中一个问题就是数据同步 应该是有很多方法的 这儿采用的是引入一个监听器 使用方法 1,在app.js里面
import Event from './utils/event.js'
//挂载到wx对象上
wx.event=new Event();

2,创建event.js文件放在util里面
/utils/event.js

class Event {

      /**
    
      * on 方法把订阅者所想要订阅的事件及相应的回调函数记录在 Event 对象的 _cbs 属性中
    
      */
    
      on(event, fn) {
    
        if (typeof fn != "function") {
    
          console.error('fn must be a function')
    
          return
    
        }
    
        this._cbs = this._cbs || {};
    
        (this._cbs[event] = this._cbs[event] || []).push(fn)
    
      }
    
      /**
    
      * emit 方法接受一个事件名称参数,在 Event 对象的 _cbs 属性中取出对应的数组,并逐个执行里面的回调函数
    
      */
    
      emit(event) {
    
        this._cbs = this._cbs || {}
    
        var callbacks = this._cbs[event], args
    
        if (callbacks) {
    
          callbacks = callbacks.slice(0)
    
          args = [].slice.call(arguments, 1)
    
          for (var i = 0, len = callbacks.length; i < len; i++) {
    
            callbacks[i].apply(null, args)
    
          }
    
        }
    
      }
    
      /**
    
      * off 方法接受事件名称和当初注册的回调函数作参数,在 Event 对象的 _cbs 属性中删除对应的回调函数。
    
      */
    
      off(event, fn) {
    
        this._cbs = this._cbs || {}
    
        // all
    
        if (!arguments.length) {
    
          this._cbs = {}
    
          return
    
        }
    
        var callbacks = this._cbs[event]
    
        if (!callbacks) return
    
        // remove all handlers
    
        if (arguments.length === 1) {
    
          delete this._cbs[event]
    
          return
    
        }
    
        // remove specific handler
    
        var cb
    
        for (var i = 0, len = callbacks.length; i < len; i++) {
    
          cb = callbacks[i]
    
          if (cb === fn || cb.fn === fn) {
    
            callbacks.splice(i, 1)
    
            break
    
          }
    
        }
    
        return
    
      }
    
    }
    
    export default Event

然后就是登录问题 我一直觉得我的登录是有问题的 哈哈 如果有人指点就 更好了 嘿嘿 欢迎留言评论 后续会继续采坑优化 也会加上更多聊天功能 。不说啦 不说啦 继续我的采坑道路┭┮﹏┭┮。

你可能感兴趣的:(小程序)