// IM 小程序 SDK
npm install tim-wx-sdk --save
// 发送图片、文件等消息需要的 COS SDK
npm install cos-wx-sdk-v5 --save
//若同步依赖过程中出现问题,请切换 npm 源后再次重试。
npm config set registry http://r.cnpmjs.org/
小程序引入第三方包的时候,需要在工具栏构建一下npm环境,本地设置也要配置一下;
如果构建时候报错,执行上边导入SDK指令前 npm install 一下;
配置完成后根目录会出现一个miniprogram_npm文件;
下面代码就是在需要使用的js脚本中引入
import TIM from 'tim-wx-sdk';
import COS from "cos-wx-sdk-v5";
let options = { SDKAppID: 0 // 控制台的SDKAppID 一个项目一个 };
// 创建 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});
使用IM需要先授权登录 userID、userSig这两个参数需要后台接口返回、根据这个判断当前登录人是谁。
let promise = tim.login({ userID: “userID”, userSig: “userSig” });
promise.then(function (imResponse) {
console.log(imResponse.data, "登录成功"); // 登录成功
if (imResponse.data.repeatLogin === true) {
// 标识账号已登录,本次登录操作为重复登录。v2.5.1 起支持
console.log(imResponse.data.errorInfo);
}
}).catch(function (imError) {
console.warn('login error:', imError); // 登录失败的相关信息
});
本地调试的时候授权登录完之后,代码修改更新后,IM登录失效、真机暂时没有发现、为了防止出现bug,在首页的onshow加了个判断,监听 sdk 是否处于 ready 状态。IM授权登录文档
登陆成功之后就可以调用API文档内的代码块了,实现一些基础功能文本、图片、语音功能基础功能
wxml:
在这里插入代码片<scroll-view bindscroll="refresh" scroll-into-view="{{toView}}" style="height: {{scroll_height}}px;" upper-threshold="100" scroll-y="true" enable-back-to-top="true" class="message-list">
<!-- 每一行 -->
<view class="row" wx:for="{{messages}}" wx:key="{{index}}" id="row_{{index}}">
<!-- 日期 -->
<view class="datetime" wx:if="{{item.msgTime != ''}}">{{item.msgTime}}</view>
<!-- 头像与内容文本 -->
<view class="body" style="flex-flow: {{item.flow == 'in' ? 'row' : 'row-reverse'}}">
<view class="avatar-container">
<image wx:if="{{item.flow=='in'}}" class="avatar" src="{{friendAvatarUrl}}" />
<image wx:else class="avatar" src="{{userData.avatarUrl}}" />
</view>
<!-- 画对话框 -->
<view class="triangle" style="{{item.flow == 'out' ? 'right: 140rpx; background: #7ECB4B' : 'left: 140rpx;'}}"></view>
<view class="content" style="{{item.flow == 'out' ? 'background: #7ECB4B' : ''}}">
<view wx:if="{{item.type === 'TIMTextElem'}}">{{item.payload.text}}</view>
<image class="image-message" wx:elif="{{item.type === 'TIMImageElem'}}" src="{{item.payload.imageInfoArray[1].url}}" bindtap="previewImage" data-src="{{item.payload.imageInfoArray[1].url}}"></image>
<view wx:elif="{{item.type === 'TIMSoundElem'}}" url="{{item.payload.url}}">
<view class="box" bindtap="openAudio" data-eventid="{{'13_'+index}}" data-time="{{item.payload.second}}" data-comkey="{{item.payload.url}}">
<image src="{{'13_'+index==audioIndex?audioGif:audioPng}}" style="height:22px;width:22px" class="_image"></image>
<view style="padding-left: 4px;" class="_div data-v-afeb3abc">
{{item.payload.second<1?1:item.payload.second}}s
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="reply" style="bottom:{{reply_height}}px;">
<view class="Audio">
<image wx:if="{{opration==true}}" bindtap="Audio" src="../image/Audio.png"></image>
<image wx:else bindtap="keyboard" src="../image/keyboard.png"></image>
</view>
<view class="opration-area">
<input bindfocus="bindfocus" wx:if="{{opration==true}}" type="text" bindinput="getContent" value="{{content}}" />
<view wx:else class="voice-button {{touchBtn?'hoverBtn':''}}" bind:touchstart="startAudio" bind:touchend="onTouchEnd" bind:longpress="onLongpress" bind:touchmove="onTouchMove">
{{touchBtn?'松开 结束':'按住说话'}}
</view>
</view>
<view class="{{sendBtn==true?'send':'sendActive'}}" bindtap="sendMsg">发送</view>
<view class="add" bind:tap="moreClick">
<image class="more" src="../image/more.png"></image>
</view>
<!-- <view class="send" bindtap="sendImg">相册</view>
<view class="send" bindtap="startAudio">开始</view>
<view class="send" bindtap="endAudio">结束</view> -->
</view>
<view class="more_box" hidden="{{moreShow}}">
<view class="more_item" bindtap="sendImg">
<view class="img_box">
<image src="../image/picture.png"></image>
</view>
<view style="margin-top:10rpx;">
<text>相册</text>
</view>
</view>
</view>
js:
import TIM from 'tim-wx-sdk';
import COS from "cos-wx-sdk-v5";
let options = {
SDKAppID: 0 // 接入时需要将0替换为您的即时通信 IM 应用的 SDKAppID
};
// 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
let tim = TIM.create(options); // SDK 实例通常用 tim 表示
// 设置 SDK 日志输出级别,详细分级请参见 setLogLevel 接口的说明
tim.setLogLevel(1); // 普通级别,日志量较多,接入时建议使用
// tim.setLogLevel(1); // release 级别,SDK 输出关键信息,生产环境时建议使用
// 注册 COS SDK 插件
tim.registerPlugin({
'cos-wx-sdk': COS
});
const app = getApp()
let recorderManager = wx.getRecorderManager();
// 录音部分参数 小程序文档
const recordOptions = {
duration: 60000, // 录音的时长,单位 ms,最大值 600000(10 分钟)
sampleRate: 44100, // 采样率
numberOfChannels: 1, // 录音通道数
encodeBitRate: 192000, // 编码码率
format: 'aac' // 音频格式,选择此格式创建的音频消息,可以在即时通信 IM 全平台(Android、iOS、微信小程序和Web)互通
};
Page({
data: {
friendId: '',
friendName: '',
friendAvatarUrl: '',
messages: [], // 消息集合
complete: 0, // 是否还有历史消息可以拉取,1 - 表示没有,0 - 表示有
content: '', // 输入框的文本值
lock: false, // 发送消息锁 true - 加锁状态 false - 解锁状态
scroll_height: wx.getSystemInfoSync().windowHeight - 54,
reply_height: 0,
moreShow: true,
userData: [],
audioPng:"../image/audio-play.png",
audioGif:"../image/audio-play.gif",
audioState:true,
/**
* 历史消息消息集合(结构如下):
* nextReqMessageID 用于续拉,分页续拉时需传入该字段。
* isCompleted 表示是否已经拉完所有消息。
*/
nextReqMessageID: "",
isCompleted: "",
isFirstGetList: true,
audioContext: null,
opration: true,
touchBtn: false,
recording: false,
stopflag: false,
cancelRecord: false,
refreshTime: '',
ScrollLoading: 0,
audioIndex:null,
sendBtn:true
},
onLoad: function (options) {
//
this.setData({
friendId: options.friendId,
friendName: options.friendName,
friendAvatarUrl: options.friendAvatarUrl,
conversationID: options.conversationID
})
wx.setNavigationBarTitle({
title: options.friendName
})
var that = this
var userData = JSON.parse(wx.getStorageSync('userData'))
that.data.messages = [] // 清空历史消息
let audioContext = wx.createInnerAudioContext()
this.setData({
userData,
audioContext
})
// 将某会话下所有未读消息已读上报
let promise = tim.setMessageRead({ conversationID: options.conversationID });
promise.then(function (imResponse) {
// 已读上报成功
}).catch(function (imError) {
// 已读上报失败
});
},
onShow: function () {
let that = this;
// 获取当前聊天的历史列表
that.getMessageList();
that.scrollToBottom();
// 获取收到的单聊信息
let onMessageReceived = function (event) {
// event.data - 存储 Message 对象的数组 - [Message]
let msgList = that.data.messages
handlerHistoryMsgs(event.data, that)
that.scrollToBottom();
};
tim.on(TIM.EVENT.MESSAGE_RECEIVED, onMessageReceived)
// 监听录音结束
recorderManager.onStop(function (res) {
if (that.data.recording) {
if (that.data.cancelRecord) {
wx.hideToast()
that.setData({
cancelRecord: false
})
} else {
// 创建消息实例,接口返回的实例可以上屏
const message = tim.createAudioMessage({
to: that.data.friendId,
conversationType: TIM.TYPES.CONV_C2C,
payload: {
file: res
},
onProgress: function (event) { }
});
// 发送消息
let promise = tim.sendMessage(message);
promise.then(function (imResponse) {
// 发送成功
that.addMessage(imResponse.data.message, that)
}).catch(function (imError) {
// 发送失败
});
that.setData({
recording: false
})
}
} else {
wx.showToast({
title: '说话时间太短',
duration: 1000,
image: '../image/err.png'
})
}
});
},
onUnload: function () {
},
/**
* 获取消息列表
*/
getMessageList() {
let that = this;
let cb = tim.getMessageList({
conversationID: conversationID,//会话列表传递过来的参数
count: 15
})
cb.then(function (imResponse) {
const messageList = imResponse.data.messageList; // 消息列表。
const nextReqMessageID = imResponse.data.nextReqMessageID; // 用于续拉,分页续拉时需传入该字段。
const isCompleted = imResponse.data.isCompleted; // 表示是否已经拉完所有消息。
that.setData({
nextReqMessageID: nextReqMessageID,
isCompleted: isCompleted
})
handlerHistoryMsgs(messageList, that);
that.scrollToBottom();
});
},
/**
* 获取文本的消息
*/
getContent: function (e) {
if(e.detail.value ==""){
this.setData({sendBtn:true})
}else{
this.setData({sendBtn:false})
}
console.log(e)
var that = this;
that.setData({
content: e.detail.value
})
},
/**
* 发送消息
*/
sendMsg: function (e) {
if(this.data.content ==""){
wx.showToast({
title: '请输入内容',
duration: 1000,
icon:'none'
})
return
}
var that = this
// 发送文本消息,Web 端与小程序端相同
// 1. 创建消息实例,接口返回的实例可以上屏
let message = tim.createTextMessage({
to: this.data.friendId,
conversationType: TIM.TYPES.CONV_C2C,
payload: {
text: this.data.content
}
});
// 2. 发送消息
let promise = tim.sendMessage(message);
promise.then(function (imResponse) {
// 发送成功
that.addMessage(imResponse.data.message, that)
that.setData({sendBtn:true})
}).catch(function (imError) {
// 发送失败
});
},
/**
* 刷新文本消息
*/
addMessage: function (msg, that) {
var messages = that.data.messages;
messages.push(msg);
that.setData({
messages: messages,
content: '' // 清空输入框文本
})
that.scrollToBottom();
},
/**
* 发送图片消息
*/
sendImg() {
let that = this;
wx.chooseImage({
sourceType: ['album'], // 从相册选择
count: 1, // 只选一张,目前 SDK 不支持一次发送多张图片
success: function (res) {
// 2. 创建消息实例,接口返回的实例可以上屏
let message = tim.createImageMessage({
to: that.data.friendId,
conversationType: TIM.TYPES.CONV_C2C,
payload: {
file: res
},
onProgress: function (event) {
}
});
// 3. 发送图片
let promise = tim.sendMessage(message);
promise.then(function (imResponse) {
// 发送成功
that.addMessage(imResponse.data.message, that)
}).catch(function (imError) {
// 发送失败
});
}
})
},
scrollToBottom: function () {
this.setData({
toView: 'row_' + (this.data.messages.length - 1)
});
},
previewImage(e) {
let src = '';
wx.previewImage({
current: e.currentTarget.dataset.src, // 当前显示图片的http链接
urls: [e.currentTarget.dataset.src]
})
},
// 录制语音
startAudio: function () {
wx.showToast({
title: '上滑取消发送',
duration: 10000,
image: '../image/cancel.png'
})
this.setData({
touchBtn: true
})
if (this.data.stopFlag) {
return;
}
recorderManager.start(recordOptions);
recorderManager.onError(function (errMsg) {
});
},
// # 利用长按判断录音是否太短
onLongpress() {
this.setData({
recording: true
})
},
// 发送录音
onTouchEnd: function () {
wx.hideToast()
let that = this;
that.setData({
touchBtn: false
})
if (that.data.stopFlag) {
return;
}
if (that.data.recording) {
recorderManager.stop();
} else {
that.setData({
stopFlag: true
})
setTimeout(() => {
recorderManager.stop();
that.setData({
stopFlag: false
})
}, 400);
}
},
// 播放语音
openAudio(audio) {
console.log(audio)
let index = audio.currentTarget.dataset.eventid
this.setData({
audioIndex:index
// audioState:false
})
this.data.audioContext.src = audio.currentTarget.dataset.comkey
this.data.audioContext.autoplay = true;
this.data.audioContext.play()
this.data.audioContext.onPlay((res) => {
})
this.data.audioContext.onEnded(() => {
wx.hideToast()
this.setData({
audioIndex:null
})
console.log("语音结束了")
})
this.data.audioContext.onError((res) => {
})
},
// 上滑取消
onTouchMove(e) {
if (e.touches[0].clientY < 520) {
// # 取消发送
this.setData({
cancelRecord: true
});
wx.showToast({
title: '松开,取消发送',
duration: 10000,
image: '../image/cancel.png'
})
} else {
// # 不取消
wx.hideToast()
wx.showToast({
title: '上滑取消发送',
duration: 10000,
image: '../image/cancel.png'
})
this.setData({
cancelRecord: false
})
}
},
// 下拉加载聊天记录
refresh: function (e) {
let that = this
if (that.data.ScrollLoading == 1) { //防止多次触发
return false
}
if (e.detail.scrollTop < 1) {
that.setData({ ScrollLoading: 1 })
wx.showLoading({
title: '加载中',
})
setTimeout(() => {
let promise = tim.getMessageList({ conversationID: that.data.conversationID, nextReqMessageID: that.data.nextReqMessageID, count: 15 });
promise.then(function (imResponse) {
const newMessageList = imResponse.data.messageList; // 消息列表。
const nextReqMessageID = imResponse.data.nextReqMessageID; // 用于续拉,分页续拉时需传入该字段。
const isCompleted = imResponse.data.isCompleted; // 表示是否已经拉完所有消息。
that.setData({
nextReqMessageID: nextReqMessageID,
isCompleted: isCompleted,
messages: newMessageList.concat(that.data.messages)
})
wx.hideLoading()
that.setData({ ScrollLoading: 0 })
// handlerHistoryMsgs(messageList, that);
});
}, 800);
}
// setTimeout(function(){
// var date = new Date();
// },300);
},
// 切换
Audio() {
this.setData({
opration: false
})
},
keyboard() {
this.setData({
opration: true
})
},
moreClick() {
if (this.data.moreShow) {
this.setData({
moreShow: false,
reply_height: 92,
scroll_height: this.data.scroll_height - 92
})
}
},
bindfocus() {
this.setData({
moreShow: true,
reply_height: 0,
scroll_height: wx.getSystemInfoSync().windowHeight - 54
})
}
})
/**
* 处理历史消息
*/
function handlerHistoryMsgs(result, that) {
var historyMsgs = that.data.messages;
result.forEach(item => {
historyMsgs.push(item)
})
// historyMsgs.push(result[0])
that.setData({
messages: historyMsgs,
})
// 将某会话下所有未读消息已读上报
let promise = tim.setMessageRead({ conversationID: that.data.conversationID });
promise.then(function (imResponse) {
// 已读上报成功
}).catch(function (imError) {
// 已读上报失败
});
}
wxss:
/** 聊天窗口样式
* 54px为回复框高度,js同
*/
/*聊天记录*/
page{
background: rgb(245, 245, 245);
}
.message-list {
/*margin-bottom: 54px;*/
background: rgb(235, 235, 235);
}
/*单元行*/
.row {
display: flex;
flex-direction: column;
margin: 0 30rpx;
}
/*日期*/
.datetime {
font-size: 10px;
padding: 10px 0;
color: #999;
text-align: center;
}
.send {
font-size: 15px;
/* padding-right: 10px; */
color: #999;
text-align: center;
height: 70%;
border: 1px solid #e4dfdf;
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
border-radius: 10rpx;
}
.sendActive {
font-size: 15px;
/* padding-right: 10px; */
color: #fff;
text-align: center;
height: 70%;
border: 1px solid #05c15f;
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
border-radius: 10rpx;
background-color: #05c15f;
}
.Audio {
font-size: 15px;
color: #999;
text-align: center;
padding-left: 10rpx;
}
.Audio image {
width: 50rpx;
height: 50rpx;
}
.add {
height: 70%;
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
}
.more {
width: 50rpx;
height: 50rpx;
}
/*主体*/
.body {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
margin-top: 10px;
}
/*头像容器*/
.body.avatar-container {
width: 20%;
}
/*头像*/
.body .avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin: 0 20rpx;
}
/*文本消息*/
.body .content {
font-size: 16px;
background: #fff;
border-radius: 5px;
padding: 10px;
line-height: 22px;
margin-bottom: 10px;
word-wrap: break-word;
max-width: 300rpx;
}
/* 三角箭头 */
.body .triangle {
background: white;
width: 20rpx;
height: 20rpx;
margin-top: 26rpx;
transform: rotate(45deg);
position: absolute;
}
/*图片消息*/
.picture {
width: 160px;
}
/*回复框*/
.reply {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
position: fixed;
/* bottom: 0; */
width: 100%;
height: 54px;
border-top: 1px solid rgb(215, 215, 215);
background: rgb(245, 245, 245);
}
.reply .voice-image {
width: 25px;
height: 25px;
margin-left: 3%;
}
/*文本输入或语音录入*/
.reply .opration-area {
flex: 1;
padding: 8px;
}
/*回复文本框*/
.reply input {
background: rgb(252, 252, 252);
height: 36px;
border: 1px solid rgb(221, 221, 221);
border-radius: 6px;
padding-left: 3px;
}
/*选取图片*/
.reply .choose-image {
width: 25px;
height: 25px;
margin-right: 3%;
}
/*按住说话button*/
.voice-button {
height: 36px;
color: #818181;
font-size: 14px;
line-height: 36px;
text-align: center;
border: 1px solid #e4dfdf;
border-radius: 10rpx;
}
/*悬浮提示框*/
.hud-container {
position: fixed;
width: 150px;
height: 150px;
left: 50%;
top: 50%;
margin-left: -75px;
margin-top: -75px;
}
/*背景层*/
.hud-background {
position: absolute;
width: 100%;
height: 100%;
background: #999;
opacity: 0.8;
z-index: 11;
border-radius: 10px;
}
/*悬浮框主体*/
.hud-body {
position: relative;
width: 100%;
height: 100%;
z-index: 19;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
/*图标*/
.hud-body image {
margin-top: 20px;
width: 80px;
height: 80px;
}
/*文字*/
.hud-body .tip {
color: #fff;
text-align: center;
width: 90%;
line-height: 34px;
margin: 0 auto;
margin-bottom: 10px;
width: 90%;
}
.hud-body .warning {
background: #c33;
border-radius: 5px;
}
.image-message {
max-width: 100%;
border-radius: 4rpx;
}
.box {
display: flex;
height: 40rpx;
line-height: 40rpx;
}
.hoverBtn {
background-color: rgb(226, 220, 220);
color: #fff;
border-radius: 10rpx;
}
.more_box {
height: 138rpx;
width: 100%;
padding: 15rpx;
display: flex;
background: rgb(245, 245, 245);
position: fixed;
bottom: 0;
}
.more_item {
text-align: center;
height: 150rpx;
font-size: 24rpx;
margin-left: 26rpx
}
.img_box {
width: 80rpx;
height: 84rpx;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10rpx;
}
.img_box image {
width: 40rpx;
height: 40rpx;
}
会话列表的代码我就不贴了 按照文档来没有什么坑,希望对大家有所帮助。