IM即时通讯官网
https://cloud.tencent.com/document/product/269
SDK安装
// IM 小程序 SDK
npm install tim-wx-sdk --save
// 发送图片、文件等消息需要的 COS SDK
npm install cos-wx-sdk-v5 --save
小程序使用IM
import TIM from '../miniprogram_npm/tim-wx-sdk/tim-wx.js';
import COS from "../miniprogram_npm/cos-wx-sdk-v5/index.js";
var SDKAppID = ''; // 您的即时通信 IM 应用的 SDKAppID
var admin = 'administrator'; // 您的即时通信 IM 应用的管理员,默认administrator
let options = {
SDKAppID: SDKAppID
};
// 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
let tim = TIM.create(options); // SDK 实例通常用 tim 表示
// 注册 COS SDK 插件
tim.registerPlugin({
'cos-wx-sdk': COS
});
tim.setLogLevel(1); //日志打印,1或0(0时日志信息较全面)
tim.on(TIM.EVENT.KICKED_OUT, loginIM); //实时监听IM的登录状态。
//IM登录
async function loginIM() {
let user = await getUser();
let usersig = await getUsreSig(user.userId);
await checkAccount(user.userId, user.nickname, user.avatarUrl);
tim.login({
userID: user.userId,
userSig: usersig
}).then(function (imResponse) {
console.log('登录成功'); // 登录成功
}).catch(function (imError) {
console.warn('login error:', imError); // 登录失败的相关信息
});
}
//获取小程序用户信息
function getUser() {
return new Promise((resolve) => {
var user = {};
user.userId = wx.getStorageSync('userId');
user.nickname = wx.getStorageSync('nickName')
user.avatarUrl = wx.getStorageSync('avatarUrl')
console.log('ava', user.avatarUrl)
resolve(user)
})
}
// 检查IM用户是否存在,不存在就去注册
async function checkAccount(teacherID, nickname, avatarUrl) {
let usersig = await getUsreSig(admin)
return new wx.request({
url: 'https://console.tim.qq.com/v4/im_open_login_svc/account_check?sdkappid=' + SDKAppID + '&identifier=' + admin + '&usersig=' + usersig + '&random=2&contenttype=json',
method: 'post',
data: {
"CheckItem": [{
"UserID": teacherID
}]
},
success: function (res) {
console.log('res',res)
if (res.data.ResultItem[0].AccountStatus == 'NotImported') {
importAccount(teacherID, usersig, nickname, avatarUrl)
}
}
})
}
//注册IM用户
function importAccount(teacherID, usersig, nickname, avatarUrl) {
console.log('head', avatarUrl)
wx.request({
url: 'https://console.tim.qq.com/v4/im_open_login_svc/account_import?sdkappid=' + SDKAppID + '&identifier=' + admin + '&usersig=' + usersig + '&random=2&contenttype=json',
method: 'post',
data: {
"Identifier": teacherID,
"Nick": nickname,
"FaceUrl": avatarUrl
},
success: function (res) {
console.log(res)
}
})
}
//通知后端请求获取用户登录即时通信 IM 的密码
function getUsreSig(userId) {
return new Promise((resolve) => {
wx.request({
url: config.host + '/user/getSig?userId=' + userId,
method: 'post',
success: function (res) {
resolve(res.data.result)
}
})
})
}
获取会话列表
var getConversionList = function () {
tim.getConversationList().then(function (imResponse) {
const conversationList = imResponse.data.conversationList; // 会话列表,用该列表覆盖原有的会话列表
console.log(conversationList)
}).catch(function (imError) {
console.warn('getConversationList error:', imError); // 获取会话列表失败的相关信息
});
}
实时监听会话列表
tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, getConversionList);
会话框
<view class="md_header" wx:if="{{pictureUrl.length==0}}">
<image src="{{imageList.bg2}}" style="width: 100%;height: 100%;position: absolute;z-index: -999"/>
<!-- <view class='top' style="height:39px;">
<view class="goback">
<image src="{{image}}goback.png" bindtap="gobackmessage"></image>
<view>{{nickname}}</view>
</view>
</view> -->
<view class="main" style="margin-top:{{marginTop}}">
<image src="{{image}}gobackt.png" style="width: 6%;height: 8%;position: absolute;z-index: 99;margin:2vh 1.5vw" bindtap="gobackmessage"></image>
<view id="nickNamestyle" style="z-index:-98">{{nickname}}</view>
<image src="{{image}}mdetailbg.png" style="width: 100%;height: {{mdetailbgheight}};position: absolute;z-index: -99"/>
<scroll-view id="dialog_list" scroll-y="{{true}}" scroll-with-animation='{{true}}' scroll-top='{{scroll_top}}' scroll-into-view="{{toView}}" scroll-with-animation="true" enable-back-to-top="true" style="height:{{comment_bottom}};padding-top:16vh;" id="scroll-wrap">
<!--单独一个对话-->
<view class="loading_view" wx:if="{{isLoading}}">
<text class="subtext">正在载入更多...</text>
</view>
<view class="subtext" style="text-align:center;height: 50rpx;" wx:if="{{noMore}}">没有更多了~</view>
<view id="inner-wrap" bindtouchstart="start_fn" bindtouchend="end_fn" class="singleDialog" wx:for="{{testMessageDetail}}" wx:key="item" wx:for-index="index">
<!--显示时间-->
<view class="showTime" wx:if="{{item.time}}">{{item.time}}</view>
<view wx:if="{{item.flow=='in'}}" class="left-box" id='msg-{{index}}'>
<image src="{{image}}inviteteacher.gif" style="width: 20vh;height: 20vh;margin-left: -4vh;margin-top: -5vh;margin-right: 1vw;"></image>
<image class="ava" src="{{testImg}}" />
<view class="details">
<view class="content" wx:if="{{item.payload.text}}">{{item.payload.text}}</view>
<view style="height:{{windowWidth*0.2*item.payload.imageInfoArray[0].height/item.payload.imageInfoArray[0].width}}px;width:20vw;margin:3px 10px" wx:if="{{item.payload.imageInfoArray}}">
<image bindlongpress="saveImage" bindtap="previewImage" data-width="{{item.payload.imageInfoArray[0].width}}" data-height="{{item.payload.imageInfoArray[0].height}}" data-index="{{index}}" src="{{item.payload.imageInfoArray[0].imageUrl}}"></image>
</view>
<view wx:if='{{item.payload.url}}' class="contentafter" style="margin-left:16px;margin-top:5vh"></view>
<view wx:if='{{item.payload.url}}' class="video" style="width: 37vw;">
<audio id="myVideo-{{index}}" src="{{item.payload.remoteAudioUrl?item.payload.remoteAudioUrl:item.payload.url}}" enable-danmu danmu-btn controls="{{false}}" autoplay='{{false}}' bindtimeupdate="videoUpdate" data-second="{{item.payload.second}}" objectFit="fill"></audio>
<view class='process-container'>
<image src="{{image}}luyinthree.png" class='slider-container' bindtap='videoOpreation' data-index="{{index}}"></image>
<view class='slider-container' id="slider-container">
<slider bindchange="sliderChange" bindchanging="sliderChanging" step="1" value="{{item.sliderValue}}" backgroundColor="#A8A8A8" activeColor="#rgb(250, 232, 114)" block-color="#FFEE83" blockSize="1"/>
</view>
<view class="second">{{item.payload.second}}<text style="font-size:18px">"</text></view>
</view>
</view>
</view>
</view>
<view wx:if="{{item.flow=='out'}}" class="right-box" id='msg-{{index}}'>
<view class="details">
<view class="content" wx:if="{{item.payload.text}}"><view class="contentafter"></view>{{item.payload.text}}</view>
<view style="height:{{windowWidth*0.2*item.payload.imageInfoArray[0].height/item.payload.imageInfoArray[0].width}}px;width:20vw;margin:3px 10px" wx:if="{{item.payload.imageInfoArray}}">
<image bindlongpress="saveImage" bindtap="previewImage" data-index="{{index}}" data-width="{{item.payload.imageInfoArray[0].width}}" data-height="{{item.payload.imageInfoArray[0].height}}" data-url="{{item.payload.imageInfoArray[2].imageUrl}}" src="{{item.payload.imageInfoArray[2].imageUrl}}" wx:if="{{item.payload.imageInfoArray}}"></image>
</view>
<view wx:if='{{item.payload.url}}' class="video" style="width: 37vw">
<audio id="myVideo-{{index}}" src="{{item.payload.remoteAudioUrl?item.payload.remoteAudioUrl:item.payload.url}}" enable-danmu danmu-btn controls="{{false}}" autoplay='{{false}}' bindtimeupdate="videoUpdate" data-second="{{item.payload.second}}" objectFit="fill"></audio>
<view class='process-container'>
<image src="{{image}}luyinthree.png" class='slider-container' bindtap='videoOpreation' data-index="{{index}}"></image>
<!-- <image wx:if="{{!item.playStates}}" src="{{image}}start.png" class='slider-container' bindtap='videoOpreation' data-index="{{index}}"></image>
<image wx:if="{{item.playStates}}" src="{{image}}en.png" class='slider-container' bindtap='videoOpreation' data-index="{{index}}"></image> -->
<view class='slider-container' id="slider-container">
<slider bindchange="sliderChange" bindchanging="sliderChanging" step="1" value="{{item.sliderValue}}" backgroundColor="#A8A8A8" activeColor="#rgb(250, 232, 114)" block-color="#FFEE83" blockSize="1"/>
</view>
<view class="second">{{item.payload.second}}<text style="font-size:18px">"</text><view class="contentafter" style="margin-right:-6.9vw;margin-top:-7vh;position:relative;z-index:-99"></view></view>
</view>
</view>
</view>
<image src='{{image}}avabg.png' style="height: 15vh;width: 15vh;">
<view class="userinfo-avatar">
<open-data class="ava" style="margin-left:0vw" type="userAvatarUrl"></open-data>
</view>
</image>
<!-- <image src="{{image}}inviteteacher.gif" style="width: 20vh;height: 20vh;margin-top: -5vh;margin-right: -1.5vw;"></image> -->
</view>
</view>
</scroll-view>
<!--发送信息-->
<view class="mycomment comment_bottom" style="bottom:{{inputBottom}}">
<image bindtap="startluyin" wx:if="{{!luyin}}" src="{{image}}luyin0.png" style="width: 5vh;height: 8vh;border-radius:50%;margin-left:1vw;margin-top:3vh"></image>
<image bindtap="startluyin" wx:if="{{luyin}}" src="{{image}}luyin1.png" style="width: 8vh;height: 7vh;border-radius:50%;margin-left:1vw;margin-top:3vh"></image>
<view bindtouchstart='start' bindtouchend="end" class="comment_input" wx:if="{{luyin}}" style="text-align:center;color:#9d9f90">
<image src="{{image}}speaking.png" style="width: 21vh;height: 5vh;margin-left:0vw;margin-top:3vh"></image>
</view>
<input class="comment_input" wx:if="{{!luyin}}" bindinput='replyInputChange' bindconfirm='sendClick' adjust-position='{{false}}' bindfocus="focus" bindblur="blur" value='{{inputVal}}' confirm-type='send' placeholder="请输入文本 ···" confirm-hold='false'></input>
<image class="commit_btn" src="{{image}}sendcontent.png" data-value='{{inputVal}}' bindtap='sendClick' />
<image bindtap="sendOther" src="{{image}}sendother.png" style="width: 7vh;height: 7vh;margin-right:1vw;margin-top:3vh"></image>
<view class="mask" catchtouchmove="preventTouchMove" bindtap="cancelReplying" wx:if="{{isReplying}}" />
</view>
<view wx:if="{{comment_bottom=='53vh'}}" class="sendOther">
<image bindtap="sendPicture" src="{{image}}chat_pic.png" style="height:57px;width:57px;margin:1vh 40px 0px 40px"></image>
<image bindtap="getpiano" src="{{image}}chat_ceping.png" style="height:57px;width:57px;margin-top:1vh"></image>
</view>
</view>
</view>
<view class="modal-mask"></view>
<view class='lookPiture' style="z-index:10;position:absolute;height:100vh" wx:if="{{pictureUrl.length!=0}}">
<view class='top' style="height:39px;">
<view class="goback">
<image src="{{image}}goback.png" bindtap="gobackdetail"></image>
</view>
</view>
<scroll-view scroll-y='true' scroll-x='true' style="text-align: center;width:100vw;height:99vh;margin-top:-11.5vh" bindtouchmove="touchmoveCallback" bindtouchstart="touchstartCallback">
<image data-url="{{pictureUrl}}" src="{{pictureUrl}}" style="{{pictureUrlStyle}}" bindload="imgload"></image>
</scroll-view>
</view>
.md_header{
background-size: 100% 100%;
height: 100vh;
}
.md_header .goback image{
width: 8vh;
height: 8vh;
z-index: 2;
position: absolute;
margin-left: 2vw;
margin-top: 2vh;
background: rgba(58, 53, 53, 0.705);
border-radius: 50%;
padding: 1vh;
}
.md_header .goback view{
font-weight: 600;
font-size: 23px;
font-family: "楷体";
color: rgb(255, 255, 255);
margin-left: 38px;
position: absolute;
z-index: 2;
padding: 2px;
}
.modal-mask {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
background: #000;
opacity: 0.2;
z-index: 1;
}
.md_header .main{
height: 100vh;
width: 65vw;
position: relative;
z-index: 2;
margin-left: 18vw;
}
#nickNamestyle{
position: absolute;
margin: 1.5vh 6vw;
color: rgb(196, 124, 12, 1);
font-size: 19px;
font-family: '楷体';
font-weight: 700;
}
#dialog_list {
display: flex;
flex-direction: column;
overflow: auto;
height: 75.5vh;
}
/*隐藏滚动条*/
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.singleDialog {
width: 100%;
flex-direction: column;
}
.ava {
height: 10vh;
width: 10vh;
border-radius: 50%;
margin-left: -16.5vh;
}
.details {
display: flex;
flex-direction: column;
margin-left: 0rpx;
max-width: 80%;
margin: 8vh 0vh;
margin-top: -2vh;
}
.username {
color: gray;
font-size: 28rpx;
margin: auto 20rpx;
}
.showTime {
font-size: 12px;
text-align: center;
color: #9d9f90;
padding: 0vh 0vh 3vh 0vh;
margin-top: -7vh;
}
/*加载部分*/
.loading_view {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.loading {
margin-right: 10rpx;
width: 45rpx;
height: 45rpx;
animation: animation 1s linear infinite;
}
@keyframes animation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(1turn);
}
}
.subtext {
color: gray;
font-size: 12px;
}
/* 发送信息 */
.mycomment {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.comment_input {
width: 70%;
font-size: 15px;
padding: 5px 10px 5px 15px;
margin-top: 3vh;
border-radius: 6px;
}
.commit_btn {
height: 7vh;
width: 6vh;
margin-top: 3vh;
margin-right: 2vw;
}
.comment_bottom {
position: relative;
width: 100%;
z-index: 9;
height: 12vh;
}
.system-msg {
text-align: center;
margin: 20rpx;
}
.system-title {
font-size: 32rpx;
font-weight: bold;
}
.system-content {
color: gray;
font-size: 28rpx;
}
.left-box {
display: flex;
flex-direction: row;
padding: 0px 10px;
width: 100%;
}
.right-box {
display: flex;
flex-direction: row;
padding: 0px 10px;
justify-content: flex-end;
margin-bottom: 1vh;
}
.right-box .content {
margin-top: 14px;
margin-right: 10px;
font-size: 15px;
border-radius: 5px;
padding: 5px 15px;
background:#fff;
word-break: break-all;
text-align: left;
position: relative;
font-family: '楷体';
min-height: 10vh;
line-height: 10vh;
}
.right-box .contentafter {
content: '';
width: 13rpx;
height: 13rpx;
transform: rotate(45deg);
background-color: #fff;
border: 1px #fff;
border-style: solid none none solid ;
border-radius: 2px;
float: right;
margin-right: -20px;
margin-top: 7px;
}
.left-box .contentafter {
content: '';
width: 13rpx;
height: 13rpx;
transform: rotate(45deg);
background-color: rgba(253, 225, 81, 1);
border: 1px rgba(253, 225, 81, 1);
border-style: solid none none solid ;
border-radius: 2px;
float: right;
margin-right: -20px;
margin-top: 7px;
}
.left-box .content {
font-size: 15px;
border-radius: 5px;
padding: 5px 15px;
background:linear-gradient(90deg,rgba(253, 225, 81, 1),rgba(253, 225, 81, 0.6));
margin: 3px 10px;
word-break: break-all;
text-align: left;
font-family: '楷体';
position: relative;
margin-left: 19px;
min-height: 10vh;
line-height: 10vh;
}
.left-box .content:after {
content: '';
width: 13rpx;
height: 13rpx;
position: absolute;
top: 16rpx;
left: -5px;
transform: rotate(45deg);
background-color: rgba(253, 225, 81, 1);
border: 1px rgba(253, 225, 81, 1);
border-style: solid none none solid ;
border-radius: 2px
}
.details video{
width: 0px !important;
height: 0px !important;
}
.details image{
width: 20vw;
height: 100%;
}
.process-container image{
width: 6vh;
height: 6vh;
margin-left: 2vw;
margin-top: 4vh;
}
.details #slider-container{
width: 27vw;
margin-top: -11vh;
margin-left: 5vw;
}
.right-box .second{
margin-top: -11vh;
margin-left: 31vw;
color: rgb(196, 124, 12, 1);
font-size: 13px;
}
.left-box .second{
margin-top: -11vh;
margin-left: 31vw;
color: rgb(196, 124, 12, 1);
font-size: 13px;
}
.right-box .details .video{
margin: 3vh 3vh;
background: #fff;
height: 16vh;
border-radius: 5px;
width: 25vw;
line-height: 9vh;
}
.left-box .details .video{
margin: -8vh 6vh 3vh;
background: rgba(253, 225, 81, 1);
height: 16vh;
border-radius: 5px;
width: 25vw;
line-height: 9vh;
}
.details .process-container{
margin-top: -7vh;
align-items: center;
justify-content: center;
width: 25vw;
}
.sendOther image{
display: inline-block;
}
.sendOther{
background:rgb(226, 208, 178);
height: 20vh;
margin-top: -1vh
}
.lookPiture .goback image{
width: 8vh;
height: 8vh;
z-index: 2;
position: absolute;
margin-left: 2vw;
margin-top: 2vh;
background: rgba(58, 53, 53, 0.705);
border-radius: 50%;
padding: 1vh;
}
.userinfo-avatar {
overflow: hidden;
width: 10.5vh;
height: 10.5vh;
border-radius: 50%;
margin-top: -13vh;
margin-left: 2vh;
}
获取消息列表
getAll: function(){
let that = this;
var time = '';
if (that.data.conversationID){
let getMessageList = tim.getMessageList({ conversationID: that.data.conversationID, count: 15 }).then(async function (imResponse) { //获取15条消息
const messageList = imResponse.data.messageList // 会话列表,用该列表覆盖原有的会话列表
const nextReqMessageID = imResponse.data.nextReqMessageID; //获取下一页消息的标志位
const isCompleted = imResponse.data.isCompleted; //是否获取完了所有消息
for (var i = 0; messageList.length > i; i++) {
if (messageList[i].payload.url && messageList[i].payload.second>1000) {
console.log('messageList[i].payload.second', messageList[i].payload.second)
messageList[i].payload.second = (messageList[i].payload.second / 1000).toFixed(0);
console.log('messageList[i].payload.second', messageList[i].payload.second)
}
messageList[i].playStates = false;
messageList[i].sliderValue = 0;
messageList[i].time = await that.getTime(messageList[i]); //消息的时间处理
}
testMessageDetail = messageList
that.setData({
testMessageDetail: messageList,
toView: 'msg-' + (messageList.length - 1), //定位到最后一条消息
nextReqMessageID: nextReqMessageID,
isCompleted: isCompleted
})
console.log('metail',testMessageDetail)
}).catch(function (imError) {
console.warn('getMessageList error:', imError); // 获取会话列表失败的相关信息
});
}
// tim.on(TIM.EVENT.SDK_READY, getMessageList)
},
//修改时间格式
getTime:function(message){
var that = this;
return new Promise((resolve)=>{
var messageTime = message.time+''
if (messageTime.indexOf(":") < 0 && messageTime != '') {
var date = new Date(message.time * 1000);
var Y = date.getFullYear() + '-';
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' ';
var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':';
var m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + '';
var s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds());
var myDate = new Date;
var year = myDate.getFullYear() + '-'; //获取当前年
var mon = (myDate.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'; //获取当前月
var day = (myDate.getDate() < 10 ? '0' + myDate.getDate() : myDate.getDate()) + ' '; //获取当前日
if ((year + mon + day) == (Y + M + D)) {
message.time = h + m;
if (that.data.time == h + m) {
message.time = '';
}
} else if ((year == Y) && (mon == M) && (day - D == 1)) {
message.time = '昨天' + h + m;
if (that.data.time == h + m) {
message.time = '';
}
} else {
message.time = Y + M + D + h + m;
if (that.data.time == h + m) {
message.time = '';
}
}
}
this.setData({
time: h+m
})
resolve(message.time)
})
},
消息列表下一页(下拉加载更多)
小程序scroll-view上拉加载更多,下拉刷新数据
(https://blog.csdn.net/qq_39389646/article/details/105817747)
if (!that.data.isCompleted) {
that.setData({
isLoading: true
})
console.log(that.data.conversationID)
let getMessageList = tim.getMessageList({ conversationID: that.data.conversationID, nextReqMessageID: that.data.nextReqMessageID, count: 15 }).then(async function (imResponse) {
const messageList = imResponse.data.messageList// 会话列表,用该列表覆盖原有的会话列表
const nextReqMessageID = imResponse.data.nextReqMessageID;
const isCompleted = imResponse.data.isCompleted;
for (var i = (messageList.length - 1); 0 <= i; i--) {
if (messageList[i].payload.url && messageList[i].payload.second > 1000) {
console.log('messageList[i].payload.second', messageList[i].payload.second)
messageList[i].payload.second = (messageList[i].payload.second / 1000).toFixed(0);
console.log('messageList[i].payload.second', messageList[i].payload.second)
}
messageList[i].playStates = false;
messageList[i].sliderValue = 0;
messageList[i].time = await that.getTime(messageList[i]);
testMessageDetail.unshift(messageList[i])
}
IM.nextReqMessageID = that.data.nextReqMessageID
that.setData({
testMessageDetail: testMessageDetail,
nextReqMessageID: nextReqMessageID,
isCompleted: isCompleted,
isLoading: false,
toView: 'msg-'+that.data.toView
})
}).catch(function (imError) {
console.warn('getMessageList error:', imError); // 获取会话列表失败的相关信息
});