官方文档 初始化与建立连接 | GoEasy文档
1.引入sdk,可在demo里直接复制。引入静态资源(图片及css文件)
--- lib文件夹在demo有,可以直接复制,图片资源和style目录里的文件demo也有,路径保持和图中一致。
2.后台创建goeasy应用后,在main.js中初始化goeasy
import GoEasy from '@/uni_modules/GOEASY-IM/js_sdk/goeasy-2.13.2.esm.min.js'
import GRTC from './lib/goeasy-rtc-0.2.1.esm.min.js'GoEasy.init({
host: "hangzhou.goeasy.io", //应用所在的区域地址: 【hangzhou.goeasy.io |singapore.goeasy.io】
appkey: "xxxxxx", // common key
modules: ['im'],
// true表示支持通知栏提醒,false则表示不需要通知栏提醒
allowNotification: true //仅有效于app,小程序和H5将会被自动忽略
});
GRTC.init(GoEasy);//定义全局变量
uni.$GoEasy = GoEasy;
uni.$GRTC = GRTC;
3.创建im文件夹,新建index.vue(会话列表) , chat.vue(一对一单聊),文件内容在最下方。
4.goeasy免费日活十五个用户,为了节省日活只有在点击聊天按钮时连接goeasy,下面是私聊方法
const GoEasy = uni.$GoEasy;
// 聊天
async goIM(item) {
if (!this.$u.func.checkLogin()) return
if (this.userInfo.id == item.user_id) return this.$u.toast("您不能与自己聊天")
uni.showLoading();
GoEasy.connect({
id: this.userInfo.id + '', //im必填,最大长度60字符
data: {
"avatar": this.userInfo.avatar,
"name": this.userInfo.nickname
},
onSuccess: () => {
console.log("链接成功");
this.goImChat(item)
},
onFailed: (error) => {
console.log('Failed to connect GoEasy, code:' + error.code + ',error:' + error
.content);
if (error.code == 408) {
console.log("已连接请务重复链接");
this.goImChat(item)
return
}
this.$u.toast(error.content)
},
onProgress: (attempts) => {
console.log('GoEasy is connecting', attempts);
}
});},
goImChat(item) {
uni.hideLoading()//私聊对方数据
let toData = {
avatar: item.avatar,
id: item.user_id + '',
phone: item.phone,
name: item.nickname,
}//当前用户数据
let userInfo = {
avatar: this.userInfo.avatar,
email: '暂无',
id: this.userInfo.id + '',
phone: this.userInfo.mobile,
name: this.userInfo.nickname
}
uni.$currentUser = userInfo
uni.setStorageSync('toData', toData)
uni.navigateTo({
url: `/pages/im/chat?to=${item.user_id + ''}`
})
},
5.如需发送图片和视频,需要配置云存储,下面是官方配置文档链接
阿里云 配置阿里云对象存储OSS - IM即时通讯 | GoEasy文档
腾讯云 https://docs.goeasy.io/2.x/im/message/media/tencentcos
index.vue文件
<template>
<view class="container"><scroll-view class="conversations" scroll-y="true">
<view v-if="conversations.length > 0">
<view class="scroll-item" v-for="(conversation, key) in conversations" :key="key">
<view class="item-head" style="position: relative;">
<image :src="conversation.data.avatar" class="head-icon"
style="width: 80rpx;height: 80rpx;border-radius: 50%;" mode="aspectFill"></image>
<view class="item-head_unread" v-if="conversation.unread">
{{ conversation.unread >=99 ? 99 : conversation.unread}}
</view>
</view>
<view class="scroll-item_info" @click="chat(conversation)">
<view class="item-info-top" style="font-size: 28rpx;">
<text class="item-info-top_name"
style="font-weight:bolder;">{{ conversation.data.name }}</text>
<view class="item-info-top_time" style="font-size: 24rpx;">
{{ formatDate(conversation.lastMessage.timestamp) }}
</view>
</view>
<view class="item-info-bottom" style="margin-top: 10rpx;">
<view class="item-info-bottom-item">
<view class="item-info-top_content" v-if="!conversation.lastMessage.recalled">
<text class="unread-text" style="font-size: 24rpx;">
{{ conversation.lastMessage.read === false && conversation.lastMessage.senderId === currentUser.id ? '[未读]' : '' }}
</text><text v-if="conversation.lastMessage.senderId === currentUser.id"
style="font-size: 24rpx;">我: </text>
<text style="font-size: 24rpx;"
v-else>{{ conversation.type === 'group' ? conversation.lastMessage.senderData.name : conversation.data.name }}:
</text>
<text style="font-size: 24rpx;"
v-if="conversation.lastMessage.type === 'text'">{{ conversation.lastMessage.payload.text }}</text>
<text style="font-size: 24rpx;"
v-else-if="conversation.lastMessage.type === 'video'">[视频消息]</text>
<text style="font-size: 24rpx;"
v-else-if="conversation.lastMessage.type === 'audio'">[语音消息]</text>
<text style="font-size: 24rpx;"
v-else-if="conversation.lastMessage.type === 'image'">[图片消息]</text>
<text style="font-size: 24rpx;"
v-else-if="conversation.lastMessage.type === 'file'">[文件消息]</text>
<text style="font-size: 24rpx;"
v-else-if="conversation.lastMessage.type === 'order'">[自定义消息:订单]</text>
<text style="font-size: 24rpx;" v-else>[[未识别内容]]</text>
</view>
<view class="item-info-top_content" v-else>
<text style="font-size: 24rpx;">
{{conversation.lastMessage.recaller.id === currentUser.id ? '你' : conversation.lastMessage.recaller.data.name}}撤回了一条消息
</text>
</view>
<view class="item-info-bottom_action" @click.stop="showAction(conversation)"></view>
</view>
</view>
</view>
</view>
</view>
<view class="no-conversation" v-else>当前没有会话</view>
<view class="action-container" v-if="actionPopup.visible">
<view class="layer" @click="actionPopup.visible = false"></view>
<view class="action-box">
<view class="action-item" @click="topConversation">
{{ actionPopup.conversation.top ? '取消置顶' : '置顶聊天' }}
</view>
<view class="action-item" @click="deleteConversation">删除聊天</view>
</view>
</view>
</scroll-view>
</view></template>
<script>
import {
formatDate
} from '@/lib/utils';
import restApi from '@/lib/restapi';const GoEasy = uni.$GoEasy;
const GRTC = uni.$GRTC;
export default {
name: 'conversation',
data() {
return {
conversations: [],actionPopup: {
conversation: null,
visible: false
},
currentUser: null,}
},
onShow() {
// this.currentUser = this.userInfo
let userInfo = {
avatar: this.userInfo.avatar,
id: this.userInfo.id + '',
phone: this.userInfo.mobile,
name: this.userInfo.nickname,}
// if (this.type == 1) userInfo.name = this.userInfo.nickname
uni.$currentUser = userInfo
this.currentUser = uni.$currentUser
if (GoEasy.getConnectionStatus() === 'disconnected') {
this.connectGoEasy(); //连接goeasy
this.subscribeGroup(); //建立连接后,就应该订阅群聊消息,避免漏掉
}
this.loadConversations(); //加载会话列表
this.initGoEasyListeners();
},
onLoad(op) {
this.type = op.type
},
beforeDestroy() {
GoEasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.renderConversations);
},
methods: {
// 前往系统消息页面
goInfo() {
uni.navigateTo({
url: this.type == 1 ? `/pages/order/system-info` : `/pages/teacher-pages/order/system-info`
})
},
formatDate,
connectGoEasy() {
uni.showLoading();
GoEasy.connect({
id: this.currentUser.id, //im必填,最大长度60字符
data: {
"avatar": this.currentUser.avatar,
"name": this.currentUser.name,
},
onSuccess: () => {
console.log("链接成功");
console.log('GoEasy connect successfully.--')
},
onFailed: (error) => {
console.log('Failed to connect GoEasy, code:' + error.code + ',error:' + error
.content);
},
onProgress: (attempts) => {
console.log('GoEasy is connecting', attempts);
}
});
},
initGoEasyListeners() {
GoEasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.renderConversations); //监听会话列表变化
GoEasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.setUnreadAmount); // 移除之前的设置角标回调,防止重复回调
GoEasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.setUnreadAmount); // 设置角标
// #ifdef APP-PLUS
GRTC.off(GRTC.EVENT.RING, this.onRing); //移除之前的监听来电事件,防止重复回调
GRTC.on(GRTC.EVENT.RING, this.onRing); //监听来电事件
// #endif
},
onRing() {
const currentCall = GRTC.currentCall();
if (currentCall.groupId) {
uni.navigateTo({
url: `./rtc/group/ring`,
})
} else {
uni.navigateTo({
url: `./rtc/private/ring`,
})
}
},// 加载最新的会话列表
loadConversations() {
GoEasy.im.latestConversations({
onSuccess: (result) => {
uni.hideLoading();
let content = result.content;
this.renderConversations(content);
this.setUnreadAmount(content);
},
onFailed: (error) => {
uni.hideLoading();
console.log('获取最新会话列表失败, error:', error);
}
});
},
renderConversations(content) {
this.conversations = content.conversations;
},
setUnreadAmount(content) {
const unreadTotal = content.unreadTotal;
if (unreadTotal > 0) {
uni.setTabBarBadge({
index: 0,
text: unreadTotal.toString()
});
} else {
uni.removeTabBarBadge({
index: 0
});
}
// #ifdef APP-PLUS
GoEasy.setBadge({
badge: unreadTotal,
onSuccess: function() {
console.log("setBadge successfully.")
},
onFailed: function(error) {
console.log("Failed to setBadge,error:" + error);
}
});
// #endif
},
subscribeGroup() {
let groups = restApi.findGroups(this.currentUser);
let groupIds = groups.map(item => item.id);
GoEasy.im.subscribeGroup({
groupIds: groupIds,
onSuccess: function() {
console.log('订阅群消息成功');
},
onFailed: function(error) {
console.log('订阅群消息失败:', error);
}
});
},
topConversation() { //会话置顶
this.actionPopup.visible = false;
let conversation = this.actionPopup.conversation;
let description = conversation.top ? '取消置顶' : '置顶';
GoEasy.im.topConversation({
conversation: conversation,
top: !conversation.top,
onSuccess: function() {
uni.showToast({
title: description + '成功',
icon: 'none'
});
},
onFailed: function(error) {
console.log(description, '失败:', error);
}
});
},
deleteConversation() {
uni.showModal({
content: '确认删除这条会话吗?',
success: (res) => {
if (res.confirm) {
let conversation = this.actionPopup.conversation;
this.actionPopup.visible = false;
GoEasy.im.removeConversation({
conversation: conversation,
onSuccess: function() {
console.log('删除会话成功');
},
onFailed: function(error) {
console.log(error);
},
});
} else {
this.actionPopup.visible = false;
}
},
})
},
chat(conversation) {
let path = conversation.type === GoEasy.IM_SCENE.PRIVATE ?
`/pages/im/chat?to=${conversation.userId}` :
'/pages/im/chat?to=' + conversation.groupId;
console.log(path);
let toData = {
avatar: conversation.data.avatar,
id: conversation.userId + '',
name: conversation.data.name,
}
uni.setStorageSync('toData', toData)
uni.navigateTo({
url: path
});
},
showAction(conversation) {
this.actionPopup.conversation = conversation;
this.actionPopup.visible = true;
}
}
}
</script><style>
page {
height: 100%;
}.container {
height: 100%;
background-color: white;
}.conversations {
width: 750rpx;
overflow-x: hidden;
display: flex;
flex-direction: column;
height: 100%;
background-color: white;
}.conversations .scroll-item {
height: 152rpx;
display: flex;
align-items: center;
padding-left: 32rpx;
}.conversations .scroll-item .head-icon {
width: 100rpx;
height: 100rpx;
margin-right: 28rpx;
}.conversations .scroll-item_info {
height: 151rpx;
width: 590rpx;
padding-right: 32rpx;
border-bottom: 1px solid #EFEFEF;
}.conversations .scroll-item_info .item-info-top {
padding-top: 20rpx;
height: 60rpx;
line-height: 60rpx;
display: flex;
align-items: center;
justify-content: space-between;}
.conversations .item-info-top_name {
font-size: 34rpx;
color: #262628;
}.conversations .item-info-top_time {
font-size: 34rpx;
color: rgba(179, 179, 179, 0.8);
}.conversations .item-info-bottom {
height: 40rpx;
line-height: 40rpx;
overflow: hidden;
}.conversations .item-info-bottom-item {
display: flex;
justify-content: space-between;
}.item-info-bottom .item-info-top_content {
font-size: 34rpx;
color: #b3b3b3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;}
.item-info-bottom .item-info-bottom_action {
width: 50rpx;
height: 50rpx;
font-size: 34rpx;
background: url("@/static/goeasyImg/action.png") no-repeat center;
background-size: 28rpx 30rpx;
}.no-conversation {
width: 100%;
text-align: center;
height: 80rpx;
line-height: 80rpx;
font-size: 34rpx;
color: #9D9D9D;
}.item-head {
position: relative;
}.item-head .item-head_unread {
/* padding: 6rpx; */
background-color: #EE593C;
color: #FFFFFF;
font-size: 20rpx;
line-height: 28rpx;
border-radius: 50%;
/* min-width: 24rpx;
min-height: 24rpx; */
width: 30rpx;
height: 30rpx;
line-height: 30rpx;
text-align: center;
position: absolute;
top: 0;
right: 15rpx;
}.action-container {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}.action-container .layer {
position: absolute;
top: 0;
left: 0;
background: rgba(51, 51, 51, 0.5);
width: 100%;
height: 100%;
z-index: 99;
}.action-box {
width: 400rpx;
height: 240rpx;
background: #ffffff;
position: relative;
z-index: 100;
border-radius: 20rpx;
overflow: hidden;
}.action-item {
text-align: center;
line-height: 120rpx;
font-size: 34rpx;
color: #262628;
border-bottom: 1px solid #EFEFEF;
}.unread-text {
color: #d02129;
}
</style>
chat.vue文件
<template>
<view class="chatInterface" @contextmenu.prevent="">
<view class="scroll-view">
<image v-if="history.loading" class="history-loaded" src="/static/goeasyImg/loading.svg" />
<view v-else :class="history.allLoaded ? 'history-loaded':'load'" @click="loadHistoryMessage(false)">
<view>{{ history.allLoaded ? '已经没有更多的历史消息' : '点击获取历史消息' }}</view>
</view><checkbox-group @change="selectMessages">
<!--消息记录-->
<view v-for="(message,index) in history.messages" :key="message.messageId">
<!--时间显示,类似于微信,隔5分钟不发言,才显示时间-->
<view class="time-lag">
{{ renderMessageDate(message, index) }}
</view>
<view class="message-recalled" v-if="message.recalled">
<view v-if="message.recaller.id === currentUser.id" class="message-recalled-self">
<view>你撤回了一条消息</view>
<span v-if="message.type === 'text' && Date.now()-message.timestamp< 60 * 1000 "
@click="editRecalledMessage(message.payload.text)">重新编辑</span>
</view>
<view v-else>{{ message.recaller.data.name }}撤回了一条消息</view>
</view>
<view class="message-item" v-else>
<view class="message-item-checkbox">
<checkbox v-show="messageSelector.visible && message.status !== 'sending'"
:value="message.messageId" :checked="messageSelector.messages.includes(message)" />
</view>
<view class="message-item-content" :class="{'self' : message.senderId === currentUser.id}">
<view class="avatar">
<image :src="message.senderId === currentUser.id? currentUser.avatar : friend.avatar"
style="width: 80rpx;height: 80rpx;border-radius: 50%;" mode="aspectFill">
</image>
</view><view class="content" @click.right="showActionPopup(message)"
@longpress="showActionPopup(message)">
<view class="message-payload">
<b class="pending" v-if="message.status === 'sending'"></b>
<b class="send-fail" v-if="message.status === 'fail'"></b>
<view v-if="message.type === 'text'" class="text-content"
v-html="renderTextMessage(message)"></view>
<image v-if="message.type === 'image'" :data-url="message.payload.url"
:src="message.payload.thumbnail" class="image-content" mode="heightFix"
@click="showImageFullScreen"></image>
<view class="video-snapshot" v-if="message.type === 'video'"
:data-url="message.payload.video.url" @click="playVideo">
<image :src="message.payload.thumbnail.url"
:style="{height: getImageHeight(message.payload.thumbnail.width,message.payload.thumbnail.height)+'rpx' }"
mode="heightFix"></image>
<view class="video-play-icon"></view>
</view>
<view class="file-content" v-if="message.type === 'file'">
<view class="file-info">
<span class="file-name">{{ message.payload.name }}</span>
<span
class="file-size">{{ (message.payload.size / 1024).toFixed(2) }}KB</span>
</view>
<image class="file-img" src="/static/goeasyImg/file-icon.png"></image>
</view>
<view v-if="message.type ==='audio'" class="audio-content"
@click="playAudio(message)">
<view class="audio-facade"
:style="{width:Math.ceil(message.payload.duration)*7 + 50 + 'px'}">
<view class="audio-facade-bg"
:class="{'play-icon':audioPlayer.playingMessage && audioPlayer.playingMessage.messageId === message.messageId}">
</view>
<view>{{Math.ceil(message.payload.duration) || 1}}<span>"</span></view>
</view>
</view>
<view v-if="message.type === 'order'" class="order-content">
<view class="order-id">订单号:{{ message.payload.id }}</view>
<view class="order-body">
<image :src="message.payload.url" class="order-img"></image>
<view>
<view class="order-name">{{ message.payload.name }}</view>
<view class="order-info">
<view class="order-price">{{ message.payload.price }}</view>
<view class="order-count">共{{ message.payload.count }}件</view>
</view>
</view>
</view>
</view>
</view>
<view v-if="message.senderId === currentUser.id"
:class="message.read ?'message-read':'message-unread'">
<view v-if="message.status === 'success'" style="font-size: 22rpx;">
{{ message.read ? '已读' : '未读' }}
</view>
</view>
</view>
</view>
</view>
</view>
</checkbox-group>
</view>
<view class="action-box" v-if="!videoPlayer.visible && !messageSelector.visible">
<view class="action-top">
<!-- <view @click="switchAudioKeyboard">
<image class="more" v-if="audio.visible" src="/static/goeasyImg/jianpan.png"></image>
<image class="more" v-else src="/static/goeasyImg/audio.png"></image>
</view>
<view v-if="audio.visible" class="record-input" @touchend.stop="onRecordEnd"
@touchstart.stop="onRecordStart">
{{ recorderManager.recording ? '松开发送' : '按住录音' }}
</view> --><input v-model="text" @confirm="sendTextMessage" class="consult-input" maxlength="700"
placeholder="发送消息" type="text" />
<view @click="switchEmojiKeyboard">
<image class="more" v-if="emoji.visible" src="/static/goeasyImg/jianpan.png"></image>
<image class="more" v-else src="/static/goeasyImg/emoji.png"></image>
</view>
<view>
<image @click="showOtherTypesMessagePanel()" class="more" src="/static/goeasyImg/more.png" />
</view>
<view v-if="text" class="send-btn-box">
<text class="btn" @click="sendTextMessage()">发送</text>
</view>
</view>
<!--展示表情列表-->
<view class="action-bottom action-bottom-emoji" v-if="emoji.visible">
<image class="emoji-item" v-for="(emojiItem, emojiKey, index) in emoji.map" :key="index"
:src="emoji.url + emojiItem" @click="chooseEmoji(emojiKey)"></image>
</view>
<!--其他类型消息面板-->
<view v-if="otherTypesMessagePanelVisible" class="action-bottom">
<view class="more-icon">
<image @click="sendImageMessage()" class="operation-icon" src="/static/goeasyImg/picture.png">
</image>
<view class="operation-title">图片</view>
</view>
<view class="more-icon">
<image @click="sendVideoMessage()" class="operation-icon" src="/static/goeasyImg/video.png"></image>
<view class="operation-title">视频</view>
</view>
<!-- <view class="more-icon">
<image @click="showOrderMessageList()" class="operation-icon" src="/static/goeasyImg/order.png">
</image>
<view class="operation-title">订单</view>
</view>
<view class="more-icon">
<image @click="privateCall()" class="operation-icon" src="/static/goeasyImg/rtc.png"></image>
<view class="operation-title">视频通话</view>
</view> -->
</view>
</view>
<view class="action-popup" @touchmove.stop.prevent v-if="actionPopup.visible">
<view class="layer"></view>
<view class="action-list">
<view class="action-item" @click="deleteSingleMessage">删除</view>
<view class="action-item" v-if="actionPopup.recallable" @click="recallMessage">撤回</view>
<view class="action-item" @click="showCheckBox">多选</view>
<view class="action-item" @click="hideActionPopup">取消</view>
</view>
</view>
<view class="messageSelector-box" v-if="messageSelector.visible">
<image class="messageSelector-btn" @click="deleteMultipleMessages" src="/static/goeasyImg/delete.png">
</image>
</view>
<view class="record-loading" v-if="recorderManager.recording"></view>
<video v-if="videoPlayer.visible" :src="videoPlayer.url" id="videoPlayer"
@fullscreenchange="onVideoFullScreenChange"></video>
<view v-if="orderList.visible" class="order-list">
<view class="orders-content">
<view class="title">
<view>请选择一个订单</view>
<view class="close" @click="hideOrderMessageList">×</view>
</view>
<view class="orders">
<view v-for="(order, index) in orderList.orders" :key="index" class="order-item"
@click="sendOrderMessage(order)">
<view class="order-id">订单号:{{ order.id }}</view>
<view class="order-body">
<image :src="order.url" class="order-img"></image>
<view class="order-name">{{ order.name }}</view>
<view class="order-right">
<view class="order-price">{{ order.price }}</view>
<view class="order-count">共{{ order.count }}件</view>
</view>
</view>
</view>
</view>
</view>
</view></view>
</template><script>
import EmojiDecoder from '@/lib/EmojiDecoder';
import restApi from '@/lib/restapi';
import {
formatDate
} from '@/lib/utils';
import RecorderManager from '@/lib/RecorderManager';const IMAGE_MAX_WIDTH = 200;
const IMAGE_MAX_HEIGHT = 150;
const recorderManager = new RecorderManager();
const GoEasy = uni.$GoEasy;
const GRTC = uni.$GRTC;
export default {
name: 'privateChat',
data() {
const emojiUrl = 'https://imgcache.qq.com/open/qcloud/tim/assets/emoji/';
const emojiMap = {
'[么么哒]': 'emoji_3@2x.png',
'[乒乓]': 'emoji_4@2x.png',
'[便便]': 'emoji_5@2x.png',
'[信封]': 'emoji_6@2x.png',
'[偷笑]': 'emoji_7@2x.png',
'[傲慢]': 'emoji_8@2x.png'
};
return {
//聊天文本框
text: '',
friend: null,
to: {}, // 作为createMessage的参数
currentUser: null,//定义表情列表
emoji: {
url: emojiUrl,
map: emojiMap,
visible: false,
decoder: new EmojiDecoder(emojiUrl, emojiMap),
},
//是否展示‘其他消息类型面板’
otherTypesMessagePanelVisible: false,
orderList: {
orders: [],
visible: false
},
history: {
messages: [],
allLoaded: false,
loading: false
},
recorderManager: recorderManager,
audio: {
//录音按钮展示
visible: false
},
audioPlayer: {
innerAudioContext: null,
playingMessage: null,
},
videoPlayer: {
visible: false,
url: '',
context: null
},
// 展示消息删除弹出框
actionPopup: {
visible: false,
message: null,
recallable: false,
},
// 消息选择
messageSelector: {
visible: false,
messages: []
},
order_type: 1, //1是用户自己购买 2是老师给用户购买,
teacher_id: '',
for_user_id: '', //老师给用户买时传对方用户的id
need_id: '', //需求id,老师给学生下单传
}
},
onLoad(options) {
this.order_type = options.order_type
this.teacher_id = options.teacher_id
this.for_user_id = options.for_user_id
this.need_id = options.need_id//聊天对象
let id = options.to;
// this.friend = restApi.findUserById(id);
this.friend = uni.getStorageSync('toData')
this.currentUser = uni.$currentUser;
this.to = {
id: this.friend.id,
type: GoEasy.IM_SCENE.PRIVATE,
data: {
name: this.friend.name,
avatar: this.friend.avatar,
teacher_id: this.friend.teacher_id || 0
}
};this.initGoEasyListeners();
// 语音播放器
// this.initialAudioPlayer();
// 录音监听器
// this.initRecorderListeners();
},
onShow() {
this.otherTypesMessagePanelVisible = false;
this.emoji.visible = false;
},
onReady() {
this.loadHistoryMessage(true);
this.videoPlayer.context = uni.createVideoContext('videoPlayer', this);
// https://uniapp.dcloud.io/api/ui/navigationbar?id=setnavigationbartitle
uni.setNavigationBarTitle({
title: this.friend.teacher_id ? this.friend.name + '老师' : this.friend.name
});
},
onPullDownRefresh(e) {
this.loadHistoryMessage(false);
},
onUnload() {
//退出聊天页面之前,清空监听器
GoEasy.im.off(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, this.onMessageReceived);
GoEasy.im.off(GoEasy.IM_EVENT.MESSAGE_DELETED, this.onMessageDeleted);
},
methods: {
goPay() {
if (!this.$u.func.checkLogin()) return
let url = `/pages/order/pay-course?teacher_id=${this.teacher_id}&order_type=${this.order_type}`
if (this.order_type == 2) url =
`/pages/order/pay-course?teacher_id=${this.teacher_id}&order_type=${this.order_type}&for_user_id=${this.for_user_id}&need_id=${this.need_id}`
uni.navigateTo({
url
})
},
//渲染文本消息,如果包含表情,替换为图片
//todo:本不需要该方法,可以在标签里完成,但小程序有兼容性问题,被迫这样实现
renderTextMessage(message) {
return '<span>' + this.emoji.decoder.decode(message.payload.text) + '</span>'
},
//像微信那样显示时间,如果有几分钟没发消息了,才显示时间
//todo:本不需要该方法,可以在标签里完成,但小程序有兼容性问题,被迫这样实现
renderMessageDate(message, index) {
if (index === 0) {
return formatDate(message.timestamp)
} else {
if (message.timestamp - this.history.messages[index - 1].timestamp > 5 * 60 * 1000) {
return formatDate(message.timestamp)
}
}
return '';
},
initGoEasyListeners() {
// 监听私聊消息
GoEasy.im.on(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, this.onMessageReceived);
//监听消息删除
GoEasy.im.on(GoEasy.IM_EVENT.MESSAGE_DELETED, this.onMessageDeleted);
},
onMessageReceived(message) {
let senderId = message.senderId;
let receiverId = message.receiverId;
let friendId = this.currentUser.id === senderId ? receiverId : senderId;
if (friendId === this.friend.id) {
this.history.messages.push(message);
//聊天时,收到消息标记为已读
this.markPrivateMessageAsRead();
//收到新消息,是滚动到最底部
this.scrollToBottom();
}
},
onMessageDeleted(deletedMessages) {
deletedMessages.forEach(message => {
let senderId = message.senderId;
let receiverId = message.receiverId;
let friendId = this.currentUser.id === senderId ? receiverId : senderId;
if (friendId === this.friend.id) {
let index = this.history.messages.indexOf(message);
if (index > -1) {
this.history.messages.splice(index, 1);
}
}
});
},
initialAudioPlayer() {
this.audioPlayer.innerAudioContext = uni.createInnerAudioContext();
this.audioPlayer.innerAudioContext.onEnded(() => {
this.audioPlayer.playingMessage = null;
});
this.audioPlayer.innerAudioContext.onStop(() => {
this.audioPlayer.playingMessage = null;
});
},
initRecorderListeners() {
recorderManager.onRecordComplete((file, duration) => {
if (duration < 1000) {
uni.showToast({
icon: 'none',
title: '录音时间太短',
duration: 500
});
return;
}
GoEasy.im.createAudioMessage({
to: this.to,
file: file,
notification: {
title: this.currentUser.name + '发来一段语音',
body: '[语音消息]', // 字段最长 50 字符
sound: 'message',
badge: '+1'
},
onProgress: function(progress) {
console.log(progress)
},
onSuccess: (message) => {
this.sendMessage(message);
},
onFailed: (e) => {
console.log('error :', e);
}
});
});
},
/**
* 核心就是设置高度,产生明确占位
*
* 小 (宽度和高度都小于预设尺寸)
* 设高=原始高度
* 宽 (宽度>高度)
* 高度= 根据宽度等比缩放
* 窄 (宽度<高度)或方(宽度=高度)
* 设高=MAX height
*
* @param width,height
* @returns number
*/
getImageHeight(width, height) {
if (width < IMAGE_MAX_WIDTH && height < IMAGE_MAX_HEIGHT) {
return height * 2;
} else if (width > height) {
return (IMAGE_MAX_WIDTH / width * height) * 2;
} else if (width === height || width < height) {
return IMAGE_MAX_HEIGHT * 2;
}
},
sendMessage(message) {
this.history.messages.push(message);
this.scrollToBottom();
GoEasy.im.sendMessage({
message: message,
onSuccess: function(message) {
console.log('发送成功.', message);
},
onFailed: function(error) {
if (error.code === 507) {
console.log(
'发送语音/图片/视频/文件失败,没有配置OSS存储,详情参考:https://docs.goeasy.io/2.x/im/message/media/alioss'
);
} else {
console.log('发送失败:', error);
}
}
});
},
sendTextMessage() {
if (this.text.trim() !== '') {
let body = this.text;
if (this.text.length >= 50) {
body = this.text.substring(0, 30) + '...';
}
GoEasy.im.createTextMessage({
text: this.text,
to: this.to,
notification: {
title: this.currentUser.name + '发来一段文字',
body: body,
sound: 'message',
badge: '+1'
},
onSuccess: (message) => {
this.sendMessage(message);
},
onFailed: (e) => {
console.log('error :', e);
}
});
}
this.text = '';
},
sendVideoMessage() {
uni.chooseVideo({
success: (res) => {
GoEasy.im.createVideoMessage({
to: this.to,
file: res,
notification: {
title: this.currentUser.name + '发来一个视频',
body: '[视频消息]', // 字段最长 50 字符
sound: 'message',
badge: '+1'
},
onProgress: function(progress) {
console.log(progress)
},
onSuccess: (message) => {
this.otherTypesMessagePanelVisible = false;
this.sendMessage(message);
},
onFailed: (e) => {
console.log('error :', e);
}
});
}
})
},
sendImageMessage() {
uni.chooseImage({
count: 9,
success: (res) => {
res.tempFiles.forEach(file => {
GoEasy.im.createImageMessage({
to: this.to,
file: file,
notification: {
title: this.currentUser.name + '发来一张图片',
body: '[图片消息]', // 字段最长 50 字符
sound: 'message',
badge: '+1'
},
onProgress: function(progress) {
console.log(progress)
},
onSuccess: (message) => {
this.otherTypesMessagePanelVisible = false;
this.sendMessage(message);
},
onFailed: (e) => {
console.log('error :', e);
}
});
})
}
});
},
sendOrderMessage(order) {
//GoEasyIM自定义消息,实现订单发送
GoEasy.im.createCustomMessage({
type: 'order',
payload: order,
to: this.to,
notification: {
title: this.currentUser.name + '发来一个订单',
body: '[订单消息]',
sound: 'message',
badge: '+1'
},
onSuccess: (message) => {
this.otherTypesMessagePanelVisible = false;
this.sendMessage(message);
},
onFailed: (e) => {
console.log('error :', e);
}
});
this.orderList.visible = false;
},
showActionPopup(message) {
const MAX_RECALLABLE_TIME = 3 * 60 * 1000; //3分钟以内的消息才可以撤回
this.messageSelector.messages = [message];
if ((Date.now() - message.timestamp) < MAX_RECALLABLE_TIME && message.senderId === this.currentUser.id &&
message.status === 'success') {
this.actionPopup.recallable = true;
} else {
this.actionPopup.recallable = false;
}
this.actionPopup.visible = true;
},
hideActionPopup() {
this.actionPopup.visible = false;
this.actionPopup.message = null;
},
deleteSingleMessage() {
uni.showModal({
content: '确认删除?',
success: (res) => {
this.actionPopup.visible = false;
if (res.confirm) {
this.deleteMessage();
}
},
})
},
deleteMultipleMessages() {
if (this.messageSelector.messages.length > 0) {
uni.showModal({
content: '确认删除?',
success: (res) => {
this.messageSelector.visible = false;
if (res.confirm) {
this.deleteMessage();
}
},
})
}
},
deleteMessage() {
GoEasy.im.deleteMessage({
messages: this.messageSelector.messages,
onSuccess: (result) => {
this.messageSelector.messages.forEach(message => {
let index = this.history.messages.indexOf(message);
if (index > -1) {
this.history.messages.splice(index, 1);
}
});
this.messageSelector.messages = [];
},
onFailed: (error) => {
console.log('error:', error);
}
});
},
recallMessage() {
this.actionPopup.visible = false;
GoEasy.im.recallMessage({
messages: this.messageSelector.messages,
onSuccess: () => {
console.log('撤回成功');
},
onFailed: (error) => {
console.log('撤回失败,error:', error);
}
});
},
editRecalledMessage(text) {
if (this.audio.visible) {
this.audio.visible = false;
}
this.text = text;
},
showCheckBox() {
this.messageSelector.messages = [];
this.messageSelector.visible = true;
this.actionPopup.visible = false;
},
selectMessages(e) {
const selectedMessageIds = e.detail.value;
let selectedMessages = [];
this.history.messages.forEach(message => {
if (selectedMessageIds.includes(message.messageId)) {
selectedMessages.push(message);
}
})
this.messageSelector.messages = selectedMessages;
},
loadHistoryMessage(scrollToBottom) { //历史消息
this.history.loading = true;
let lastMessageTimeStamp = null;
let lastMessage = this.history.messages[0];
if (lastMessage) {
lastMessageTimeStamp = lastMessage.timestamp;
}
GoEasy.im.history({
id: this.friend.id,
type: GoEasy.IM_SCENE.PRIVATE,
lastTimestamp: lastMessageTimeStamp,
limit: 10,
onSuccess: (result) => {
uni.stopPullDownRefresh();
this.history.loading = false;
let messages = result.content;
if (messages.length === 0) {
this.history.allLoaded = true;
} else {
if (lastMessageTimeStamp) {
this.history.messages = messages.concat(this.history.messages);
} else {
this.history.messages = messages;
}
if (messages.length < 10) {
this.history.allLoaded = true;
}
if (scrollToBottom) {
this.scrollToBottom();
//收到的消息设置为已读
this.markPrivateMessageAsRead();
}
}
},
onFailed: (error) => {
//获取失败
console.log('获取历史消息失败:', error);
uni.stopPullDownRefresh();
this.history.loading = false;
}
});
},
//语音录制按钮和键盘输入的切换
switchAudioKeyboard() {
if (!this.audio.visible) {
recorderManager.authorize().then(() => {
console.log('录音权限获取成功');
this.audio.visible = true;
}).catch((err) => {
console.log('err:', err)
uni.showModal({
title: '获取录音权限失败',
content: '请先打开麦克风权限'
});
});
} else {
this.audio.visible = false;
}
},
onRecordStart() {
recorderManager.start();
},
onRecordEnd() {
recorderManager.stop();
},
showImageFullScreen(e) {
let imagesUrl = [e.currentTarget.dataset.url];
uni.previewImage({
urls: imagesUrl
});
},
playVideo(e) {
this.videoPlayer.visible = true;
this.videoPlayer.url = e.currentTarget.dataset.url;
this.$nextTick(() => {
this.videoPlayer.context.requestFullScreen({
direction: 0
});
this.videoPlayer.context.play();
});
},
playAudio(audioMessage) {
let playingMessage = this.audioPlayer.playingMessage;if (playingMessage) {
this.audioPlayer.innerAudioContext.stop();
// 如果点击的消息正在播放,就认为是停止播放操作
if (playingMessage === audioMessage) {
return;
}
}
this.audioPlayer.playingMessage = audioMessage;
this.audioPlayer.innerAudioContext.src = audioMessage.payload.url;
this.audioPlayer.innerAudioContext.play();
},
onVideoFullScreenChange(e) {
//当退出全屏播放时,隐藏播放器
if (this.videoPlayer.visible && !e.detail.fullScreen) {
this.videoPlayer.visible = false;
this.videoPlayer.context.stop();
}
},
messageInputFocusin() {
this.otherTypesMessagePanelVisible = false;
this.emoji.visible = false;
},
switchEmojiKeyboard() {
this.emoji.visible = !this.emoji.visible;
this.otherTypesMessagePanelVisible = false;
},
showOtherTypesMessagePanel() {
this.otherTypesMessagePanelVisible = !this.otherTypesMessagePanelVisible;
this.emoji.visible = false;
},
chooseEmoji(emojiKey) {
this.text += emojiKey;
},
showOrderMessageList() {
this.orderList.orders = restApi.getOrderList();
this.orderList.visible = true;
},
hideOrderMessageList() {
this.orderList.visible = false;
},
privateCall() {
uni.showActionSheet({
itemList: ['视频通话', '音频通话'],
success: (res) => {
const mediaType = res.tapIndex === 0 ? 1 : 0;
const notificationBody = res.tapIndex === 0 ? '邀请你视频通话' : '邀请你语音通话';
GRTC.call({
calleeId: this.friend.id,
mediaType: mediaType,
notification: {
title: this.currentUser.name,
body: notificationBody,
sound: 'ring',
badge: '+1'
},
}).then(() => {
uni.navigateTo({
url: `./rtc/private/dial`,
})
}).catch((error) => {
console.log("呼叫失败:", error);
uni.showToast({
icon: "error",
title: "呼叫失败:" + error,
duration: 2000
})
})
},
fail: (res) => {
console.log(res.errMsg);
}
});
},
scrollToBottom() {
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 2000000,
duration: 0
});
});
},
markPrivateMessageAsRead() {
GoEasy.im.markMessageAsRead({
id: this.to.id,
type: this.to.type,
onSuccess: function() {
console.log('标记私聊已读成功');
},
onFailed: function(error) {
console.log("标记私聊已读失败", error);
}
});
}
}
}
</script><style>
@import url('@/static/style/chatInterface.css');
</style>