This commit is contained in:
2021-04-07 15:18:17 +08:00
parent dfdf73fdfc
commit ab7a7cf62f
88 changed files with 2974 additions and 4 deletions

3
app.js
View File

@@ -1,5 +1,6 @@
// app.js // app.js
import GoEasyIM from './utils/goeasy-im-1.5.1.js'; import GoEasyIM from './static/lib/goeasy-im-1.5.1.js';
App({ App({
onLaunch: function () { onLaunch: function () {
wx.im = GoEasyIM.getInstance({ wx.im = GoEasyIM.getInstance({

View File

@@ -2,7 +2,14 @@
"pages":[ "pages":[
"pages/index/index", "pages/index/index",
"pages/ltjm/ltjm", "pages/ltjm/ltjm",
"pages/liaotian/liaotian" "pages/liaotian/liaotian",
"pages/conversations/conversations",
"pages/login/login",
"pages/contacts/contacts",
"pages/mine/mine",
"pages/chat/groupChat/groupChat",
"pages/chat/privateChat/privateChat",
"pages/chat/groupMember/groupMember"
], ],
"tabBar":{ "tabBar":{
"color": "#f00", "color": "#f00",
@@ -12,8 +19,12 @@
"pagePath": "pages/index/index", "pagePath": "pages/index/index",
"text": "首页" "text": "首页"
},{ },{
"pagePath": "pages/liaotian/liaotian", "pagePath" : "pages/conversations/conversations",
"text": "聊天" "text":"信息"
},
{
"pagePath" : "pages/contacts/contacts",
"text" : "通讯录"
}] }]
}, },
"window":{ "window":{

View File

@@ -0,0 +1,63 @@
Component({
options: {
addGlobalClass: true,
},
properties: {
src: {
type: String,
value: ""
},
duration: {
type: Number,
value: 0
}
},
data: {
width: "",
play: false,
finalDuration: "",
audioContext: null
},
methods: {
playAudio() {
// 播放时才创建audioContext,播放完毕销毁
var self = this;
this.setData({
audioContext: wx.createInnerAudioContext()
});
this.data.audioContext.src = this.data.src;
this.switchAudioState();
setTimeout(() => {
self.switchAudioState();
self.data.audioContext.destroy();
}, self.data.finalDuration*1000);
this.data.audioContext.play();
this.data.audioContext.onPlay(()=>{
console.log("正在播放......");
});
this.data.audioContext.onError((res) => {
console.log("audio error:",res)
});
},
switchAudioState(){
this.setData({
play: !this.data.play
});
},
},
attached: function() {
// 在组件实例进入页面节点树时执行
this.setData({
width: Math.ceil(this.data.duration)*7+80,
finalDuration: Math.ceil(this.data.duration),
});
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
// 语音还在播放时退出该界面时audioContext还没有被销毁因此调用该方法清空audioContext
if(this.data.audioContext != null){
this.data.audioContext.destroy();
}
},
})

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,7 @@
<view class="goeasy-audio-player" bindtap="playAudio">
<view class="audio-facade" style="width:{{width}}rpx">
<image wx:if="{{!play}}" class="audio-facade-bg" src="/static/images/audioImage/voice.png"></image>
<image wx:else class="audio-facade-bg audio-play-icon" src="/static/images/audioImage/play.gif"></image>
<view class="record-second">{{finalDuration}}</view>
</view>
</view>

View File

@@ -0,0 +1,34 @@
.goeasy-audio-player {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.audio-facade {
display: flex;
align-items: center;
min-width: 80rpx;
max-width: 300rpx;
height: 60rpx;
padding: 6rpx 10rpx;
border-radius: 14rpx;
line-height: 30rpx;
background: #D02129;
font-size: 24rpx;
color: #ffffff;
}
.audio-facade .audio-play-icon {
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);
}
.audio-facade-bg {
width: 40rpx;
height: 35rpx;
}
.record-second {
padding-left: 14rpx;
}

View File

@@ -0,0 +1,49 @@
/* customMessage.js */
Component({
data: {
to: null,//接收方
type: "", //私聊还是群聊
show: false,//是否展示自定义消息组件
goods : '',
price : '',
number : ''
},
methods:{
setNumber(e){
this.setData({
number: e.detail.value
});
},
setGoods(e){
this.setData({goods: e.detail.value});
},
setPrice(e){
this.setData({
price: e.detail.value
});
},
createCustomMessage () {
let customMessage = wx.im.createCustomMessage({
type : 'order',
payload : {
number : this.data.number,
goods : this.data.goods,
price : this.data.price
},
to: {
id : this.data.to.uuid,
type : this.data.type,
data : {name : this.data.to.name, avatar: this.data.to.avatar}
}
});
this.triggerEvent("sendCustomMessage",customMessage);
this.close();
},
close () {
this.setData({
show: false
});
}
}
})

View File

@@ -0,0 +1,5 @@
{
"component": true,
"usingComponents":{},
"navigationBarTitleText": "自定义消息"
}

View File

@@ -0,0 +1,29 @@
<view>
<view wx:if="{{show}}" class="goeasy-custom-message">
<view class="custom-message-box">
<view class="goeasy-custom-message-title">发送订单</view>
<view class="content">
<view>
<view class="order-item">编号:</view>
<view class="order-item">商品:</view>
<view class="order-item">金额:</view>
</view>
<view>
<view class="order-input">
<input class="input" type="text" bindinput="setNumber" maxlength="20"/>
</view>
<view class="order-input">
<input class="input" type="text" bindinput="setGoods" maxlength="20"/>
</view>
<view class="order-input">
<input class="input" type="text" bindinput="setPrice" maxlength="10" />
</view>
<view class="action-btn">
<view class="cancel-btn" bindtap="close">取消</view>
<view class="send-btn" bindtap="createCustomMessage">发送</view>
</view>
</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,78 @@
/* customMessage.wxss */
.goeasy-custom-message {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
background: #fff;
}
.custom-message-box {
padding: 0 40rpx;
}
.goeasy-custom-message-title {
text-align: center;
font-weight: 600;
font-size: 40rpx;
line-height: 200rpx;
color: #000000;
}
.content {
display: flex;
justify-content: center;
}
.order-item {
display: flex;
align-items: center;
height: 80rpx;
margin-top: 40rpx;
}
.order-input {
height: 80rpx;
margin-top: 40rpx;
}
.input {
width: 500rpx;
height: 80rpx;
padding: 10rpx;
border-radius: 10rpx;
box-sizing: border-box;
font-size: 28rpx;
background: #EFEFEF;
}
.action-btn {
display: flex;
justify-content: space-around;
margin-top: 80rpx;
}
.send-btn {
width: 240rpx;
height: 80rpx;
background: #618DFF;
line-height: 80rpx;
text-align: center;
border-radius: 10rpx;
color: #FFFFFF;
font-size: 32rpx;
}
.cancel-btn {
width: 240rpx;
height: 80rpx;
background: #FFFFFF;
line-height: 80rpx;
text-align: center;
border-radius: 10rpx;
color: #666666;
font-size: 32rpx;
border: 1px solid rgba(0, 0, 0, 0.1)
}

View File

@@ -0,0 +1,67 @@
const recorderManager = wx.getRecorderManager();
Component({
options: {
addGlobalClass: true, // 加载组件css文件,需在app.wxss中引入组件css文件
},
data: {
recording: false,
stopSignaled: false,
clickLongPress: false,
},
methods: {
startRecord: function() {
console.log('start');
this.setData({
clickLongPress: true
});
recorderManager.start();
},
stopRecord: function() {
console.log('end');
if (!this.data.recording && this.data.clickLongPress) {
console.log('in1', this.data.clickLongPress);
this.setData({
stopSignaled: true,
clickLongPress: false
});
} else {
this.setData({
recording: false,
});
recorderManager.stop();
}
}
},
attached() {
var self = this;
recorderManager.onStart(function() {
self.setData({
recording: true,
clickLongPress: false
});
if (self.data.stopSignaled) {
self.setData({
stopSignaled: false
});
recorderManager.stop();
}
});
recorderManager.onStop(function(res) {
self.setData({
recording: false
});
if(res.duration < 100) {
return;
}
self.triggerEvent('onStop', res);
});
recorderManager.onError(function() {
self.setData({
recording: false
});
});
}
})

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,6 @@
<view class="goeasy-recorder">
<view bindtouchstart="startRecord" bindtouchend="stopRecord" class="record-msg-box">
{{recording ? '松开发送' : '按下录音'}}
</view>
<image wx:if="{{recording}}" class="record-icon" src="../../static/images/recordImage/loading.gif"></image>
</view>

View File

@@ -0,0 +1,32 @@
.goeasy-recorder {
height: 80rpx;
background-color: #ffffff;
flex: 1;
display: flex;
}
.record-msg-box {
flex: 1;
height: 80rpx;
padding-left: 20rpx;
padding: 0;
border-radius: 12rpx;
box-sizing: border-box;
line-height: 80rpx;
font-size: 28rpx;
text-align: center;
color: #FFFFFF;
background: #cccccc;
}
.record-icon {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 158rpx;
margin: auto;
width: 316rpx;
height: 308rpx;
}

View File

@@ -0,0 +1,48 @@
Component({
options: {
multipleSlots: true, // 在组件定义时的选项中启用多slot支持
},
data: {
videoContext: null,
show : false,
src : '',
duration : 0
},
methods: {
play({url='', duration=0}) {
this.setData({
show : true,
src : url,
duration : duration,
videoContext: wx.createVideoContext('videoPlayer', this)
})
},
onPlay () {
console.log('onplay');
this.data.videoContext.requestFullScreen({
direction : 0
})
},
onFullScreenChange(e) {
// 视频的全屏与退出全屏都会执行
//当退出全屏播放时,隐藏播放器
if(this.data.show && !e.detail.fullScreen){
this.setData({
show : false
})
this.data.videoContext.stop();
}
}
},
attached: function() {
// 在组件实例进入页面节点树时执行
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
if(this.data.videoContext != null){
this.data.videoContext.stop();
}
}
})

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,3 @@
<view class="goeasy-video-player" style="width0;height:0;overflow:hidden">
<video id="videoPlayer" wx:if="{{show}}" src="{{src}}" autoplay="true" custom-cache="{{false}}" bindfullscreenchange="onFullScreenChange" bindplay="onPlay" controls duration="{{duration}}"></video>
</view>

View File

@@ -0,0 +1,11 @@
.goeasy-video-player {
width: 100%;
height: 100%;
}
.mask {
position: absolute;
top: 0;
z-index: 1;
opacity: 0.6;
background: #333;
}

View File

@@ -0,0 +1,332 @@
/* groupChat.js */
// 定义表情
import EmojiDecoder from "../../../static/lib/EmojiDecoder";
let emojiUrl = 'https://imgcache.qq.com/open/qcloud/tim/assets/emoji/';
let 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'
};
const app = getApp();
Page({
data: {
content: '',
group: null,
messages: [],
//默认为false展示输入框, 为true时显示录音按钮
recordVisible: false,
currentUser: null,
groupMemberNum: 0,
groupMembers: {},
allHistoryLoaded: false,
// 表情
emoji : {
url : emojiUrl,
map : emojiMap,
show : false,
decoder : new EmojiDecoder(emojiUrl,emojiMap)
},
more : {//更多按钮
show : false
},
imService: null,
// 群名称
groupTitle: ""
},
onPullDownRefresh () {
this.loadMoreHistoryMessage();
},
onLoad(options) {
// 初始化群数据
let groupId = options.to;
let imService = app.globalData.imService;
let currentUser = imService.currentUser;
let group = imService.findGroupById(groupId);
let groupMembers = imService.getGroupMembers(groupId);
let groupTitle = group.name + "" + Object.keys(groupMembers).length + "";
this.setData({
group: group,
imService: imService,
groupTitle: groupTitle,
currentUser: currentUser,
groupMembers: groupMembers,
});
// 获取群消息
let messages = this.data.imService.getGroupMessages(groupId);
// 渲染表情与消息间隔5分钟显示时间
this.renderMessages(messages);
this.scrollToBottom();
// 收到的消息设置为已读
if(this.data.messages.length !==0){
this.markGroupMessageAsRead(groupId);
}
// 监听群消息
this.data.imService.onNewGroupMessageReceive = (groupId, message)=> {
if (groupId === this.data.group.uuid) {
// 渲染messages
this.renderMessages(this.data.messages);
this.scrollToBottom();
// 如果收到当前群消息则清除当前群的未读消息
this.markGroupMessageAsRead(groupId);
}
};
},
onUnload() {
// 退出聊天页面之前,清空页面传入的监听器
if(this.data.imService){
this.data.imService.onNewGroupMessageReceive = function () {};
}
},
onRecordStop(res) {
// 发送语音
let audioMessage = wx.im.createAudioMessage({
to: {
id : this.data.group.uuid,
type : wx.GoEasyIM.SCENE.GROUP,
data : {name:this.data.group.name, avatar:this.data.group.avatar}
},
file: res.detail,
onProgress :function (progress) {
console.log(progress)
}
});
this.sendMessage(audioMessage);
},
sendTextMessage() {
// 发送文本与表情
if (this.data.content.trim() !== '') {
let textMessage = wx.im.createTextMessage({
text: this.data.content,
to : {
id : this.data.group.uuid,
type : wx.GoEasyIM.SCENE.GROUP,
data : {name:this.data.group.name, avatar:this.data.group.avatar}
}
});
this.sendMessage(textMessage);
}
this.setData({
content: ""
});
},
sendImage(){
// 发送图片
let self = this;
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success (res) {
let imageMessage = wx.im.createImageMessage({
to : {
id : self.data.group.uuid,
type : wx.GoEasyIM.SCENE.GROUP,
data : {name:self.data.group.name, avatar:self.data.group.avatar}
},
file: res,
onProgress :function (progress) {
console.log(progress)
}
});
self.sendMessage(imageMessage);
}
});
},
sendVideo(){
// 发送视频
let self = this;
wx.chooseVideo({
sourceType: ['album','camera'],
maxDuration: 60,
camera: 'back',
success(res) {
let videoMessage = wx.im.createVideoMessage({
to : {
id : self.data.group.uuid,
type : wx.GoEasyIM.SCENE.GROUP,
data : {name:self.data.group.name, avatar:self.data.group.avatar}
},
file: res,
onProgress :function (progress) {
console.log(progress)
}
});
self.sendMessage(videoMessage);
}
})
},
sendMessage(message){
let self = this;
this.data.messages.push(message);
this.renderMessages(this.data.messages);
self.scrollToBottom();
let promise = wx.im.sendMessage(message);
promise.then((res) => {
console.log('发送消息成功');
self.renderMessages(self.data.messages);
})
.catch(e => {
console.log('发送失败',e)
});
},
showCustomMessageForm(){
// 展示自定义消息页面
let self = this;
let customMessage = this.selectComponent("#customMessage");
customMessage.setData({
show: true,
to: self.data.group,
type: wx.GoEasyIM.SCENE.GROUP
});
},
sendCustomMessage(event){
let customMessage = event.detail;
this.sendMessage(customMessage);
// 发送自定义消息关闭更多菜单栏
this.setData({
["more.show"]: false,
["emoji.show"]: false,
});
},
loadMoreHistoryMessage() { //历史消息
//历史消息
let lastMessageTimeStamp = Date.now();
let lastMessage = this.data.messages[0];
if (lastMessage) {
lastMessageTimeStamp = lastMessage.timestamp;
}
let currentLength = this.data.messages.length;
let promise = this.data.imService.loadGroupHistoryMessage(this.data.group.uuid, lastMessageTimeStamp);
promise.then(messages => {
if (messages.length === currentLength) {
this.setData({
allHistoryLoaded: true
})
}
this.renderMessages(this.data.messages);
wx.stopPullDownRefresh();
}).catch(e => {
console.log(e);
wx.stopPullDownRefresh();
})
},
renderMessages(messages){
messages.forEach((message,index)=>{
if(index === 0){
// 当页面只有一条消息时,显示发送时间
message.showTime = app.formatDate(message.timestamp);
}else {
// 当前消息与上条消息的发送时间进行比对超过5分钟则显示当前消息的发送时间
if (message.timestamp - messages[index - 1].timestamp > 5 * 60 * 1000) {
message.showTime = app.formatDate(message.timestamp);
}
}
if(message.type === 'text'){
// 渲染表情与文本消息
let text = this.data.emoji.decoder.decode(message.payload.text);
message.node= text;
}
});
this.setData({
messages: messages
});
},
showMembers() { //显示群成员
wx.navigateTo({
url: '../groupMember/groupMember?group=' + JSON.stringify(this.data.group)
});
},
markGroupMessageAsRead (groupId) {
wx.im.markGroupMessageAsRead(groupId)
.then(() => {
console.log('标记为已读成功')
})
.catch(e => {
console.log('标记为已读失败', e)
})
},
setContent(e) {
// 监听输入的消息
let content = e.detail.value;
this.setData({
content: content
});
},
switchAudioKeyboard() {
// 语音录制按钮和键盘输入的切换
this.setData({
recordVisible: !this.data.recordVisible
});
if(this.data.more.show || this.data.emoji.show){
this.setData({
["more.show"]: false,
["emoji.show"]: false
});
}
if(this.data.recordVisible){
// 录音授权
wx.authorize({
scope: 'scope.record',
success() {}
});
}
},
playVideo (e) {
//播放视频
this.selectComponent("#videoPlayer").play({
url : e.currentTarget.dataset.url,
duration : e.currentTarget.dataset.duration
})
},
previewImage(event) {
// 预览图片
let imagesUrl = [event.currentTarget.dataset.src];
wx.previewImage({
urls: imagesUrl // 需要预览的图片http链接列表
})
},
selectEmoji(e){
// 选择表情
let emojiKey = e.currentTarget.dataset.emojikey;
emojiKey = this.data.content + emojiKey;
this.setData({
content: emojiKey
});
},
messageInputFocusin(){
this.setData({
["more.show"]: false,
["emoji.show"]: false
});
},
showEmoji(){
this.setData({
["emoji.show"]: true,
["more.show"]: false,
recordVisible: false
});
// 关闭手机键盘
wx.hideKeyboard();
},
showMore(){
this.setData({
["more.show"]: true,
["emoji.show"]: false
});
// 关闭手机键盘
wx.hideKeyboard();
},
scrollToBottom() { // 滑动到最底部
wx.pageScrollTo({
scrollTop : 200000,
duration :10
})
}
})

View File

@@ -0,0 +1,11 @@
{
"navigationBarTitleText": "",
"enablePullDownRefresh" : true,
"backgroundTextStyle" : "dark",
"usingComponents": {
"GoEasyRecorder": "/components/GoEasyRecorder/goEasyRecorder",
"GoEasyAudioPlayer": "/components/GoEasyAudioPlayer/goEasyAudioPlayer",
"GoEasyVideoPlayer": "/components/GoEasyVideoPlayer/goEasyVideoPlayer",
"GoEasyCustomMessage": "/components/GoEasyCustomMessage/customMessage"
}
}

View File

@@ -0,0 +1,91 @@
<page-meta>
<navigation-bar background-color="#D02129" title="{{groupTitle}}"/>
</page-meta>
<view class="groupChat">
<view>
<image class="group-member-icon" src="/static/images/group-icon.png" bindtap="showMembers"/>
<view scroll-y class="scroll-view">
<view class="header">
<text>{{allHistoryLoaded ? '已经没有更多的历史消息' : '下拉获取历史消息'}}</text>
</view>
<!--已经收到的消息-->
<view wx:for="{{messages || []}}" wx:for-index="index" wx:key="index" wx:for-item="message">
<!--时间显示类似于微信隔5分钟不发言才显示时间-->
<view class="time-lag">
{{message.showTime}}
</view>
<view class="{{message.senderId == currentUser.uuid ? 'message-item self' : 'message-item'}}">
<view class="avatar other-icon" wx:if="{{message.senderId !== currentUser.uuid}}">
<image class="avatar" src="{{message.senderData.avatar}}" />
</view>
<view class="avatar self-icon" wx:else>
<image class="avatar" src="{{currentUser.avatar}}" />
</view>
<view class="content">
<image src="/static/images/pending.gif" class="pending" wx:if="{{ message.status === 'new'}}"></image>
<image src="/static/images/failed.png" class="send-fail" wx:if="{{message.status === 'fail'}}"></image>
<rich-text class="text-content" nodes="{{message.node}}" wx:if="{{message.type ==='text'}}"></rich-text>
<image class="image-content" wx:if="{{message.type === 'image'}}" src="{{message.payload.url}}" bindtap="previewImage"
data-src="{{message.payload.url}}" mode="aspectFit"/>
<GoEasyAudioPlayer id="goEasyAudio" wx:if="{{message.type =='audio'}}" src="{{message.payload.url}}" duration="{{message.payload.duration}}" />
<view class="video-snapshot" bindtap="playVideo" data-url="{{message.payload.video.url}}" data-duration="{{message.payload.video.duration}}" wx:if="{{message.type == 'video'}}">
<image class="thumbnail-image" src="{{message.payload.thumbnail.url}}" mode="aspectFit"></image>
<image class="play-icon" src="/static/images/videoImage/play.png" mode="aspectFit"></image>
</view>
<view class="custom-message" wx:if="{{message.type === 'order'}}">
<view class="title">
<image src="../../../static/images/dingdan.png"></image>
<text>自定义消息</text>
</view>
<view class="custom-message-item">编号: {{message.payload.number}}</view>
<view class="custom-message-item">商品: {{message.payload.goods}}</view>
<view class="custom-message-item">金额: {{message.payload.price}}</view>
</view>
</view>
</view>
</view>
</view>
<!--发送消息,视频,语音,自定义消息操作-->
<view class="action-box">
<view class="action-top">
<view bindtap="switchAudioKeyboard" class="action-icon">
<image wx:if="{{!recordVisible}}" class="microphone-icon" src="/static/images/record-appearance-icon.png"></image>
<image wx:else class="keyboard-icon" src="/static/images/jianpan.png"></image>
</view>
<!-- 录音 -->
<GoEasyRecorder style="flex: 1;" wx:if="{{recordVisible}}" bind:onStop="onRecordStop"></GoEasyRecorder>
<!-- GoEasyIM最大支持3k的文本消息如需发送长文本需调整输入框maxlength值 -->
<input wx:else type="text" maxlength="700" placeholder="发送消息" confirm-hold hold-keyboard="{{true}}" adjust-position="{{true}}" class="msg-input-box"
data-content="content" bindinput="setContent" bindfocus="messageInputFocusin" value="{{content}}" />
<view class="action-icon">
<image src="/static/images/emoji.png" class="emoji-icon" bindtap="showEmoji"></image>
</view>
<view class="action-icon">
<image src="/static/images/more.png" class="more-icon" bindtap="showMore"></image>
</view>
<view class="send-btn-box" bindtap="sendTextMessage">发送</view>
</view>
<!--展示表情列表-->
<view class="action-bottom" wx:if="{{emoji.show}}" style="justify-content: space-around">
<image class="image" wx:for="{{emoji.map}}" wx:for-item="emojiItem" wx:for-index="key" wx:key="key" src="{{emoji.url + emojiItem}}" bindtap="selectEmoji" data-emojiKey="{{key}}"></image>
</view>
<!--更多-->
<view class="action-bottom" wx:if="{{more.show}}">
<view class="more-item" bindtap="sendImage">
<image class="image" src="../../../static/images/tupian.png"></image>
<text class="text">图片</text>
</view>
<view class="more-item" bindtap="sendVideo">
<image class="image" src="../../../static/images/shipin.png"></image>
<text class="text">视频</text>
</view>
<view class="more-item" bindtap="showCustomMessageForm">
<image class="image" src="../../../static/images/zidingyi.png"></image>
<text class="text">自定义消息</text>
</view>
</view>
</view>
</view>
<GoEasyVideoPlayer id="videoPlayer"></GoEasyVideoPlayer>
<GoEasyCustomMessage id="customMessage" bind:sendCustomMessage="sendCustomMessage"></GoEasyCustomMessage>
</view>

View File

@@ -0,0 +1,276 @@
/* groupChat.wxss */
page {
height: 100%;
}
.groupChat {
height: 100%;
}
.group-member-icon {
width: 60rpx;
height: 60rpx;
position: fixed;
top: 60rpx;
right: 20rpx;
background-color: #C4C4C4;
z-index: 2;
border-radius: 60rpx;
}
.scroll-view {
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
padding-bottom: 130rpx;
}
.header {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
height: 90rpx;
font-size: 24rpx;
color: gray !important;
text-decoration: none !important;
}
.time-lag{
font-size: 20rpx;
text-align: center;
}
.message-item {
max-height: 400rpx;
padding: 20rpx 0;
overflow: hidden;
display: flex;
}
.self{
overflow: hidden;
display: flex;
justify-content: flex-start;
flex-direction: row-reverse;
}
.avatar{
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
flex-grow: 0;
}
.other-icon {
margin: 0 20rpx;
}
.self-icon {
margin: 0 20rpx;
}
.content{
font-size: 34rpx;
line-height: 44rpx;
max-height: 400rpx;
display: flex;
align-items: center;
justify-content: right;
text-align: right;
}
.pending {
width: 30rpx;
height: 30rpx;
padding: 10rpx;
}
.send-fail{
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
flex-grow: 0;
flex-shrink: 0;
}
.text-content{
padding: 16rpx;
border-radius: 12rpx;
color: #ffffff;
background:#D02129;
word-break: break-all;
text-align: left;
vertical-align: center;
display: block;
}
.image-content{
padding: 16rpx;
border-radius: 12rpx;
width: 300rpx;
height: 300rpx;
}
.video-snapshot {
position: relative;
max-height: 240rpx;
max-width: 300rpx;
overflow: hidden;
}
.thumbnail-image{
max-height: 240rpx;
max-width: 300rpx;
}
.play-icon {
position: absolute;
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
top: 50%;
left: 50%;
margin-left: -40rpx;
margin-top: -40rpx;
z-index: 1000;
opacity: 1;
}
.custom-message{
width: 400rpx;
height: 260rpx;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: flex-start;
box-sizing: border-box;
padding: 10rpx 30rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 20rpx;
}
.title{
width: 100%;
display: flex;
align-items: center;
font-size: 30rpx;
}
.title text {
padding: 10rpx;
}
.custom-message-item{
text-align: left;
font-size: 28rpx;
overflow: hidden;
width: 100%;
text-overflow:ellipsis;
white-space: nowrap;
}
.action-box {
width: 100%;
position: fixed;
bottom: 0;
left: 0;
z-index: 1000;
background-color: #FAFAFA;
}
.action-top {
display: flex;
width: 100%;
align-items: center;
height: 130rpx;
padding: 20rpx 10rpx 20rpx 10rpx;
box-sizing: border-box;
background-color: #FAFAFA;
}
.action-icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.microphone-icon {
width: 45rpx;
height: 50rpx;
padding: 0 10rpx;
}
.keyboard-icon {
width: 50rpx;
height: 50rpx;
padding: 0 10rpx;
}
.msg-input-box {
flex: 1;
height: 80rpx;
padding-left: 20rpx;
border-radius: 12rpx;
box-sizing: border-box;
line-height: 80rpx;
font-size: 28rpx;
background-color: #efefef;
}
.emoji-icon {
width: 50rpx;
height: 50rpx;
padding-left: 15rpx;
}
.more-icon {
width: 58rpx;
height: 58rpx;
padding-left: 15rpx;
}
.send-btn-box {
width: 80rpx;
box-sizing: border-box;
text-align: center;
line-height: 80rpx;
font-size: 28rpx;
color: #95949A;
}
.action-bottom {
display: flex;
padding: 20rpx 10rpx 20rpx 10rpx;
height: 320rpx;
box-sizing: border-box;
background: #fff;
}
.image {
width: 100rpx;
height: 100rpx;
}
.more-item{
display: flex;
flex-direction: column;
width: 150rpx;
height: 150rpx;
margin-right: 20rpx;
align-items: center;
}
.text {
font-size: 20rpx;
text-align: center;
line-height: 50rpx;
color: #666666;
}
.title image {
width: 40rpx;
height: 40rpx;
}

View File

@@ -0,0 +1,20 @@
/* groupMember.js */
const app = getApp()
Page({
data: {
currentUser : null,
groupMembersMap : {},
groupMemberNum: 0
},
onLoad(options){
let group = JSON.parse(options.group);
let groupMemberMap = app.globalData.imService.getGroupMembers(group.uuid);
let groupMemberNum = Object.keys(groupMemberMap).length;
this.setData({
groupMemberNum: groupMemberNum,
groupMembersMap: groupMemberMap,
});
},
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": ""
}

View File

@@ -0,0 +1,15 @@
<!--groupMember.wxml-->
<page-meta>
<navigation-bar
front-color="#FFFFFF"
background-color="#D02129"
title="群成员({{groupMemberNum}})"
/>
</page-meta>
<view class="groupMember">
<view class="member-layer">
<view class="member">
<image src="{{member.avatar}}" class="group-member-avatar avatar" wx:for="{{groupMembersMap}}" wx:key="key" wx:for-item="member"/>
</view>
</view>
</view>

View File

@@ -0,0 +1,51 @@
/* groupMember.wxss */
page {
width: 100%;
height: 100%;
font-family: Source Han Sans CN;
}
.member-layer {
display: flex;
flex-direction: column;
position: fixed;
left: 0;
right: 0;
z-index: 1000;
height: 100%;
background: #FFFFFF;
}
.exit-icon {
padding-left: 20rpx;
line-height: 120rpx;
font-size: 36rpx;
color: #FFFFFF;
}
.header-group-name {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #FFFFFF;
}
.member {
padding: 10rpx;
margin-top: 10rpx;
}
.avatar {
width: 96rpx;
height: 96rpx;
min-width: 96rpx;
min-height: 96rpx;
}
.group-member-avatar {
margin-right: 10rpx;
margin-bottom: 10rpx;
}

View File

@@ -0,0 +1,323 @@
/* privateChat.js */
import EmojiDecoder from "../../../static/lib/EmojiDecoder";
let emojiUrl = 'https://imgcache.qq.com/open/qcloud/tim/assets/emoji/';
let 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'
};
const app = getApp();
Page({
data: {
content: '',
friend: null,
currentUser: null,
messages: [],
//默认为false展示输入框, 为true时显示录音按钮
recordVisible: false,
//所有历史消息加载完成标识
allHistoryLoaded: false,
//定义表情列表
emoji : {
url : emojiUrl,
map : emojiMap,
show: false,
decoder: new EmojiDecoder(emojiUrl, emojiMap),
},
more : {//更多按钮
show : false
},
imService : null,
},
onPullDownRefresh () {
this.loadMoreHistoryMessage();
},
onLoad: function(options) {
// 获取初始数据并加载
let friendId = options.to;
let imService = app.globalData.imService;
let currentUser = imService.currentUser;
let friend = imService.findFriendById(friendId);
this.setData({
friend: friend,
imService: imService,
currentUser: currentUser,
});
// 获取消息
let messages = this.data.imService.getPrivateMessages(friendId);
// 渲染表情与消息间隔5分钟显示时间
this.renderMessages(messages);
this.scrollToBottom();
// 收到的消息设置为已读
if(this.data.messages.length !==0){
this.markPrivateMessageAsRead(friendId);
}
//传入监听器,收到一条私聊消息总是滚到到页面底部
this.data.imService.onNewPrivateMessageReceive = (friendId, message)=> {
if (friendId === this.data.friend.uuid) {
this.renderMessages(this.data.messages);
this.scrollToBottom();
// 如果是好友发送则清除未读消息
this.markPrivateMessageAsRead(friendId);
}
};
},
onUnload () {
//退出聊天页面之前,清空页面传入的监听器
if(this.data.imService) {
this.data.imService.onNewPrivateMessageReceive = (friendId, message)=> {};
}
},
onRecordStop(res) {
// 发送语音
let audioMessage = wx.im.createAudioMessage({
to: {
id : this.data.friend.uuid,
type : wx.GoEasyIM.SCENE.PRIVATE,
data : {name:this.data.friend.name, avatar:this.data.friend.avatar}
},
file: res.detail,
onProgress :function (progress) {
console.log(progress)
}
});
this.sendMessage(audioMessage);
},
sendTextMessage() {
// 发送文本与表情
if (this.data.content.trim() !== '') {
let textMessage = wx.im.createTextMessage({
text: this.data.content,
to : {
id : this.data.friend.uuid,
type : wx.GoEasyIM.SCENE.PRIVATE,
data : {name:this.data.friend.name, avatar:this.data.friend.avatar}
}
});
this.sendMessage(textMessage);
}
this.setData({
content: ""
});
},
sendImage(){
// 发送图片
let self = this;
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success (res) {
let imageMessage = wx.im.createImageMessage({
to : {
id : self.data.friend.uuid,
type : wx.GoEasyIM.SCENE.PRIVATE,
data : {name:self.data.friend.name, avatar:self.data.friend.avatar}
},
file: res,
onProgress :function (progress) {
console.log(progress)
}
});
self.sendMessage(imageMessage);
}
});
},
sendVideo(){
// 发送视频
let self = this;
wx.chooseVideo({
sourceType: ['album','camera'],
maxDuration: 60,
camera: 'back',
success(res) {
let videoMessage = wx.im.createVideoMessage({
to : {
id : self.data.friend.uuid,
type : wx.GoEasyIM.SCENE.PRIVATE,
data : {name:self.data.friend.name, avatar:self.data.friend.avatar}
},
file: res,
onProgress :function (progress) {
console.log(progress)
}
});
self.sendMessage(videoMessage);
}
})
},
sendMessage(message){
let self = this;
this.data.messages.push(message);
this.renderMessages(this.data.messages);
this.scrollToBottom();
let promise = wx.im.sendMessage(message);
promise.then((res) => {
console.log('发送消息成功');
self.renderMessages(self.data.messages);
})
.catch(e => {
console.log('发送失败',e)
});
},
showCustomMessageForm(){
let self = this;
let customMessage = this.selectComponent("#customMessage");
customMessage.setData({
show: true,
to: self.data.friend,
type: wx.GoEasyIM.SCENE.PRIVATE
});
},
sendCustomMessage(event){
let customerMessage = event.detail;
this.sendMessage(customerMessage);
// 发送自定义消息关闭更多菜单栏
this.setData({
["more.show"]: false,
["emoji.show"]: false,
});
},
loadMoreHistoryMessage() {
//历史消息
let friendId = this.data.friend.uuid;
let lastMessageTimeStamp = Date.now();
let lastMessage = this.data.messages[0];
if (lastMessage) {
lastMessageTimeStamp = lastMessage.timestamp;
}
let currentLength = this.data.messages.length;
let promise = app.globalData.imService.loadPrivateHistoryMessage(friendId, lastMessageTimeStamp);
promise.then(messages => {
if (messages.length === currentLength) {
this.setData({
allHistoryLoaded: true
})
}
this.renderMessages(this.data.messages);
wx.stopPullDownRefresh();
}).catch(e => {
console.log(e)
wx.stopPullDownRefresh();
})
},
renderMessages(messages){
console.log(this.data.emoji.decoder)
messages.forEach((message,index)=>{
if(index === 0){
// 当页面只有一条消息时,显示发送时间
message.showTime = app.formatDate(message.timestamp);
}else {
// 当前消息与上条消息的发送时间进行比对超过5分钟则显示当前消息的发送时间
if (message.timestamp - messages[index - 1].timestamp > 5 * 60 * 1000) {
message.showTime = app.formatDate(message.timestamp);
}
}
if(message.type === 'text'){
// 渲染表情与文本消息
let text = this.data.emoji.decoder.decode(message.payload.text);
message.node= text;
}
});
this.setData({
messages: messages
});
},
markPrivateMessageAsRead (friendId) {
wx.im.markPrivateMessageAsRead(friendId)
.then(() => {
console.log('标记为已读成功')
})
.catch(e => {
console.log('标记为已读失败', e)
})
},
setContent(e) {
// 监听输入的消息
let content = e.detail.value;
this.setData({
content: content
});
},
switchAudioKeyboard() {
// 语音录制按钮和键盘输入的切换
this.setData({
recordVisible: !this.data.recordVisible
});
if(this.data.more.show || this.data.emoji.show){
this.setData({
["more.show"]: false,
["emoji.show"]: false
});
}
if(this.data.recordVisible){
// 录音授权
wx.authorize({
scope: 'scope.record',
success() {}
});
}
},
playVideo (e) {
//播放视频
this.selectComponent("#videoPlayer").play({
url : e.currentTarget.dataset.url,
duration : e.currentTarget.dataset.duration
})
},
previewImage(event) {
// 预览图片
let imagesUrl = [event.currentTarget.dataset.src];
wx.previewImage({
urls: imagesUrl // 需要预览的图片http链接列表
})
},
selectEmoji(e){
// 选择表情
let emojiKey = e.currentTarget.dataset.emojikey;
emojiKey = this.data.content + emojiKey;
this.setData({
content: emojiKey
});
},
messageInputFocusin(){
this.setData({
["more.show"]: false,
["emoji.show"]: false
});
},
showEmoji(){
this.setData({
["emoji.show"]: true,
["more.show"]: false,
recordVisible: false
});
// 关闭手机键盘
wx.hideKeyboard();
},
showMore(){
this.setData({
["more.show"]: true,
["emoji.show"]: false
});
// 关闭手机键盘
wx.hideKeyboard();
},
scrollToBottom() { // 滑动到最底部
wx.pageScrollTo({
scrollTop : 200000,
duration :10
})
}
})

View File

@@ -0,0 +1,11 @@
{
"navigationBarTitleText": "私聊",
"enablePullDownRefresh" : true,
"backgroundTextStyle" : "dark",
"usingComponents": {
"GoEasyRecorder": "/components/GoEasyRecorder/goEasyRecorder",
"GoEasyAudioPlayer": "/components/GoEasyAudioPlayer/goEasyAudioPlayer",
"GoEasyVideoPlayer": "/components/GoEasyVideoPlayer/goEasyVideoPlayer",
"GoEasyCustomMessage": "/components/GoEasyCustomMessage/customMessage"
}
}

View File

@@ -0,0 +1,92 @@
<!--privateChat.wxml-->
<page-meta>
<navigation-bar title="{{friend.name}}" front-color="#FFFFFF" background-color="#D02129" />
</page-meta>
<view class="chat">
<view class="chat-box">
<view class="scroll-view">
<view class="header">
<text>{{allHistoryLoaded ? '已经没有更多的历史消息' : '下拉获取历史消息'}}</text>
</view>
<!--已经收到的消息-->
<view wx:for="{{messages || []}}" wx:for-index="index" wx:key="index" wx:for-item="message">
<!--时间显示类似于微信隔5分钟不发言才显示时间-->
<view class="time-lag">
{{message.showTime}}
</view>
<!--消息内容-->
<view class="{{message.senderId == currentUser.uuid ? 'message-item self' : 'message-item'}}">
<view class="avatar other-icon" wx:if="{{message.senderId !== currentUser.uuid}}">
<image class="avatar" src="{{friend.avatar}}" />
</view>
<view class="avatar self-icon" wx:else>
<image class="avatar" src="{{currentUser.avatar}}" />
</view>
<view class="content">
<image src="/static/images/pending.gif" class="pending" wx:if="{{message.status === 'new'}}"></image>
<image src="/static/images/failed.png" class="send-fail" wx:if="{{message.status == 'fail'}}"></image>
<rich-text class="text-content" nodes="{{message.node}}" wx:if="{{message.type ==='text'}}"></rich-text>
<image class="image-content" wx:if="{{message.type === 'image'}}" src="{{message.payload.url}}" bindtap="previewImage"
data-src="{{message.payload.url}}" mode="aspectFit"/>
<GoEasyAudioPlayer id="goEasyAudio" wx:if="{{message.type =='audio'}}" src="{{message.payload.url}}" duration="{{message.payload.duration}}" />
<view class="video-snapshot" bindtap="playVideo" data-url="{{message.payload.video.url}}" data-duration="{{message.payload.video.duration}}" wx:if="{{message.type == 'video'}}">
<image class="thumbnail-image" src="{{message.payload.thumbnail.url}}" mode="aspectFit"></image>
<image class="play-icon" src="/static/images/videoImage/play.png" mode="aspectFit"></image>
</view>
<view class="custom-message" wx:if="{{message.type === 'order'}}">
<view class="title">
<image src="../../../static/images/dingdan.png"></image>
<text>自定义消息</text>
</view>
<view class="custom-message-item">编号: {{message.payload.number}}</view>
<view class="custom-message-item">商品: {{message.payload.goods}}</view>
<view class="custom-message-item">金额: {{message.payload.price}}</view>
</view>
</view>
</view>
</view>
</view>
<!--发送消息,视频,语音,自定义消息操作-->
<view class="action-box">
<view class="action-top">
<view bindtap="switchAudioKeyboard" class="action-icon">
<image wx:if="{{!recordVisible}}" class="microphone-icon" src="/static/images/record-appearance-icon.png"></image>
<image wx:else class="keyboard-icon" src="/static/images/jianpan.png"></image>
</view>
<!-- 录音 -->
<GoEasyRecorder style="flex: 1;" wx:if="{{recordVisible}}" bind:onStop="onRecordStop"></GoEasyRecorder>
<!-- GoEasyIM最大支持3k的文本消息如需发送长文本需调整输入框maxlength值 -->
<input wx:else type="text" maxlength="700" placeholder="发送消息" confirm-hold hold-keyboard="{{true}}" adjust-position="{{true}}" class="msg-input-box"
data-content="content" bindinput="setContent" bindfocus="messageInputFocusin" value="{{content}}" />
<view class="action-icon">
<image src="/static/images/emoji.png" class="emoji-icon" bindtap="showEmoji"></image>
</view>
<view class="action-icon">
<image src="/static/images/more.png" class="more-icon" bindtap="showMore"></image>
</view>
<view class="send-btn-box" bindtap="sendTextMessage">发送</view>
</view>
<!--展示表情列表-->
<view class="action-bottom" wx:if="{{emoji.show}}" style="justify-content: space-around">
<image class="image" wx:for="{{emoji.map}}" wx:for-item="emojiItem" wx:for-index="key" wx:key="key" src="{{emoji.url + emojiItem}}" bindtap="selectEmoji" data-emojiKey="{{key}}"></image>
</view>
<!--更多-->
<view class="action-bottom" wx:if="{{more.show}}">
<view class="more-item" bindtap="sendImage">
<image class="image" src="../../../static/images/tupian.png"></image>
<text class="text">图片</text>
</view>
<view class="more-item" bindtap="sendVideo">
<image class="image" src="../../../static/images/shipin.png"></image>
<text class="text">视频</text>
</view>
<view class="more-item" bindtap="showCustomMessageForm">
<image class="image" src="../../../static/images/zidingyi.png"></image>
<text class="text">自定义消息</text>
</view>
</view>
</view>
</view>
<GoEasyVideoPlayer id="videoPlayer"></GoEasyVideoPlayer>
<GoEasyCustomMessage id="customMessage" bind:sendCustomMessage="sendCustomMessage"></GoEasyCustomMessage>
</view>

View File

@@ -0,0 +1,264 @@
/* privateChat.wxss */
page {
height: 100%;
}
.chat {
height: 100%;
}
.scroll-view {
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
padding-bottom: 130rpx;
}
.header {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
height: 90rpx;
font-size: 24rpx;
color: gray !important;
text-decoration: none !important;
}
.time-lag{
font-size: 20rpx;
text-align: center;
}
.message-item {
max-height: 400rpx;
padding: 20rpx 0;
overflow: hidden;
display: flex;
}
.self{
overflow: hidden;
display: flex;
justify-content: flex-start;
flex-direction: row-reverse;
}
.avatar{
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
flex-grow: 0;
}
.other-icon {
margin: 0 20rpx;
}
.self-icon {
margin: 0 20rpx;
}
.content{
font-size: 34rpx;
line-height: 44rpx;
max-height: 400rpx;
display: flex;
align-items: center;
justify-content: right;
text-align: right;
}
.pending {
width: 30rpx;
height: 30rpx;
padding: 10rpx;
}
.send-fail{
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
flex-grow: 0;
flex-shrink: 0;
}
.text-content{
padding: 16rpx;
border-radius: 12rpx;
color: #ffffff;
background:#D02129;
word-break: break-all;
text-align: left;
vertical-align: center;
display: block;
}
.image-content{
padding: 16rpx;
border-radius: 12rpx;
width: 300rpx;
height: 300rpx;
}
.video-snapshot {
position: relative;
max-height: 240rpx;
max-width: 300rpx;
overflow: hidden;
}
.thumbnail-image{
max-height: 240rpx;
max-width: 300rpx;
}
.play-icon {
position: absolute;
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
top: 50%;
left: 50%;
margin-left: -40rpx;
margin-top: -40rpx;
z-index: 1000;
opacity: 1;
}
.custom-message{
width: 400rpx;
height: 260rpx;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: flex-start;
box-sizing: border-box;
padding: 10rpx 30rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 20rpx;
}
.title{
width: 100%;
display: flex;
align-items: center;
font-size: 30rpx;
}
.title text {
padding: 10rpx;
}
.custom-message-item{
text-align: left;
font-size: 28rpx;
overflow: hidden;
width: 100%;
text-overflow:ellipsis;
white-space: nowrap;
}
.action-box {
width: 100%;
position: fixed;
bottom: 0;
left: 0;
z-index: 1000;
background-color: #FAFAFA;
}
.action-top {
display: flex;
align-items: center;
width: 100%;
height: 130rpx;
padding: 20rpx 10rpx 20rpx 10rpx;
box-sizing: border-box;
background-color: #FAFAFA;
}
.action-icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.microphone-icon {
width: 45rpx;
height: 50rpx;
padding: 0 10rpx;
}
.keyboard-icon {
width: 50rpx;
height: 50rpx;
padding: 0 10rpx;
}
.msg-input-box {
flex: 1;
height: 80rpx;
padding-left: 20rpx;
border-radius: 12rpx;
box-sizing: border-box;
line-height: 80rpx;
font-size: 28rpx;
background-color: #efefef;
}
.emoji-icon {
width: 50rpx;
height: 50rpx;
padding-left: 15rpx;
}
.more-icon {
width: 58rpx;
height: 58rpx;
padding-left: 15rpx;
}
.send-btn-box {
width: 80rpx;
box-sizing: border-box;
text-align: center;
line-height: 80rpx;
font-size: 28rpx;
color: #95949A;
}
.action-bottom {
display: flex;
padding: 20rpx 10rpx 20rpx 10rpx;
height: 320rpx;
box-sizing: border-box;
background: #fff;
}
.image {
height: 100rpx;
width: 100rpx;
}
.more-item{
display: flex;
flex-direction: column;
width: 150rpx;
height: 150rpx;
margin-right: 20rpx;
align-items: center;
}
.text {
font-size: 20rpx;
text-align: center;
line-height: 50rpx;
color: #666666;
}
.title image {
width: 40rpx;
height: 40rpx;
}

View File

@@ -0,0 +1,34 @@
/* contacts.js */
import restapi from "../../static/lib/restapi";
const app = getApp()
Page({
data: {
groups:[],
friends:[],
},
onShow () {
let currentUser = app.globalData.imService.currentUser;
let groups = restapi.findGroups(currentUser);
let friends = restapi.findFriends(currentUser);
this.setData({
groups: groups,
friends: friends,
});
},
onUnload(){
app.globalData.imService.disconnect();
},
enterChat (e) {//进入私聊
let type = e.currentTarget.dataset.type;
let conversation = e.currentTarget.dataset.conversation;
let path = type === wx.GoEasyIM.SCENE.PRIVATE?
'../chat/privateChat/privateChat?to='+conversation.uuid
:'../chat/groupChat/groupChat?to='+ conversation.uuid;
wx.navigateTo({
url : path
});
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "联系人"
}

View File

@@ -0,0 +1,26 @@
<!--contacts.wxml-->
<view class="contacts">
<view class="contacts-container">
<view class="user-list">
<view class="user-list-item" wx:for="{{groups || []}}" wx:key="key" wx:for-item="group" bindtap="enterChat" data-type="group" data-conversation="{{group}}">
<view class="user-item-avatar">
<image src="{{group.avatar}}"></image>
</view>
<view class="user-item-info">
<text class="user-item-info__name">{{group.name}}</text>
</view>
</view>
</view>
<view class="contacts-title" v-if="friends && friends.length !=0">联系人</view>
<view class="user-list">
<view class="user-list-item" wx:for="{{friends || []}}" wx:for-item="friend" bindtap="enterChat" data-type="private" data-conversation="{{friend}}" wx:key="key">
<div class="user-item-avatar">
<image src="{{friend.avatar}}"></image>
</div>
<div class="user-item-info">
<span class="user-item-info__name">{{friend.name}}</span>
</div>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,87 @@
/* contacts.wxss */
page {
width: 100%;
height: 100%;
font-family: Source Han Sans CN;
}
.contacts{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.contacts .contacts-container{
width: 100%;
overflow: auto;
}
.contacts .user-list-item{
height: 132rpx;
padding-left: 32rpx;
display: flex;
align-items: center;
}
.contacts .contacts-title{
height: 80rpx;
line-height: 100rpx;
font-size: 30rpx;
color: #666666;
background: #F3F4F7;
text-indent: 44rpx;
}
.contacts .user-list{
flex-grow: 1;
background: #ffffff;
display: flex;
flex-direction: column;
}
.contacts .user-item-avatar{
width: 96rpx;
height: 96rpx;
margin-right: 32rpx;
overflow: hidden;
position: relative;
}
.contacts .user-item-avatar image{
width: 100%;
height: 100%;
display: block;
}
.contacts .user-item-info{
height: 130rpx;
padding-right: 32rpx;
line-height: 88rpx;
flex-grow: 1;
border-bottom: 1px solid #EFEFEF;
display: flex;
justify-content: space-between;
align-items: center;
}
.contacts .user-item-info__name{
font-size: 30rpx;
font-family: Source Han Sans CN;
font-style: normal;
font-weight: bold;
color: #262628;
}
.contacts .user-item-info__tips{
height: 44rpx;
width: 64rpx;
border-radius: 24rpx;
font-size: 26rpx;
line-height: 44rpx;
background: #D02129;
color: #ffffff;
font-family: Cabin;
text-align: center;
}
.contacts .online-dot{
position: absolute;
width: 32rpx!important;
height: 32rpx!important;
right: 0;
bottom: 0;
}

View File

@@ -0,0 +1,142 @@
const app = getApp()
import IMService from '../../static/lib/imservice.js';
Page({
data : {
conversations : [],
action : {
conversation : null,
show : false,
toastMessage : '',
showToast : false
}
},
onShow () {
let currentUser = wx.getStorageSync("currentUser");
if(!currentUser){
wx.redirectTo({
url : '../login/login'
});
return;
}
if(wx.im.getStatus() === 'disconnected') {
app.globalData.imService= new IMService(wx.im);
app.globalData.imService.connectIM(currentUser);
}
wx.showLoading({
title: '加载中'
});
//监听会话列表变化
let self = this;
wx.im.on(wx.GoEasyIM.EVENT.CONVERSATIONS_UPDATED, (conversations) => {
// 设置tabBar未读消息总数以及conversation
self.setConversations(conversations);
});
//加载会话列表
wx.im.latestConversations()
.then(res => {
let content = res.content;
self.setConversations(content);
wx.hideLoading();
})
.catch(e => {
console.log(e);
wx.hideLoading();
});
},
onHide(){
// 销毁conversation监听器
wx.im.on(wx.GoEasyIM.EVENT.CONVERSATIONS_UPDATED, (conversations) => {});
},
setConversations (conversations) {
conversations.conversations && conversations.conversations.map((item) => {
// 格式化时间格式
item.lastMessage.date = app.formatDate(item.lastMessage.timestamp)
});
this.setData({
conversations : conversations.conversations
});
this.setUnreadAmount(conversations.unreadTotal);
},
navigateToChat (e) {
let conversation = e.currentTarget.dataset.conversation;
let path = conversation.type === wx.GoEasyIM.SCENE.PRIVATE?
'../chat/privateChat/privateChat?to='+conversation.userId
:'../chat/groupChat/groupChat?to='+ conversation.groupId;
wx.navigateTo({
url : path
});
},
setUnreadAmount(unreadTotal) {
if(unreadTotal >0){
wx.setTabBarBadge({
index: 0,
text: unreadTotal.toString()
});
}else{
wx.hideTabBarRedDot({
index :0
});
}
},
showAction(e){
let conversation = e.currentTarget.dataset.conversation;
this.setData({
["action.conversation"]: conversation,
["action.show"]: true
});
},
topConversation(){
let conversation = this.data.action.conversation;
let title = conversation.top ? '取消置顶失败' : '置顶失败';
let promise;
wx.showLoading({
title: ""
});
if(conversation.type === wx.GoEasyIM.SCENE.PRIVATE){
promise = wx.im.topPrivateConversation(conversation.userId, !conversation.top)
}else{
promise = wx.im.topGroupConversation(conversation.groupId, !conversation.top)
}
this.afterDoAction(promise, title)
},
removeConversation(){
wx.showLoading({title: "删除中"});
let conversation = this.data.action.conversation;
let promise;
if(conversation.type === wx.GoEasyIM.SCENE.PRIVATE){
promise = wx.im.removePrivateConversation(conversation.userId);
}else{
promise = wx.im.removeGroupConversation(conversation.groupId);
}
this.afterDoAction(promise, '删除失败')
},
afterDoAction (promise, failedDescription) {
promise.then(() => {
wx.hideLoading()
}).catch(() => {
let self = this;
wx.hideLoading();
this.setData({
["action.showToast"]: true,
["action.toastMessage"]: failedDescription,
});
setTimeout(() => {
self.setData({
["action.showToast"]: false
});
},2000);
});
this.setData({
["action.show"]: false
})
},
// 关闭弹窗
closeMask(){
this.setData({
["action.show"]: false
})
},
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "会话列表"
}

View File

@@ -0,0 +1,43 @@
<view class="conversations-container">
<scroll-view class="conversations" scroll-y="true" enable-flex="true">
<view wx:if="{{conversations.length !=0}}">
<view class="scroll-item" wx:for="{{conversations}}" wx:for-item="conversation" wx:key="key">
<view class="item-head">
<image src="{{conversation.data.avatar}}" class="head-icon"></image>
<view wx:if="{{conversation.unread}}" class="item-head_unread">{{conversation.unread}}</view>
</view>
<view class="scroll-item_info">
<view class="item-info-top">
<text class="item-info-top_name">{{conversation.data.name}}</text>
<view class="item-info-top_time">{{conversation.lastMessage.date}}</view>
</view>
<view class="item-info-bottom">
<view class="item-info-bottom-item" bindtap="navigateToChat" data-conversation="{{conversation}}" >
<view class="item-info-top_content" wx:if="{{conversation.lastMessage.type == 'text'}}">{{conversation.lastMessage.payload.text}}</view>
<view class="item-info-top_content" wx:elif="{{conversation.lastMessage.type == 'video'}}">[视频消息]</view>
<view class="item-info-top_content" wx:elif="{{conversation.lastMessage.type == 'audio'}}">[语音消息]</view>
<view class="item-info-top_content" wx:elif="{{conversation.lastMessage.type == 'image'}}">[图片消息]</view>
<view class="item-info-top_content" wx:elif="{{conversation.lastMessage.type == 'file'}}">[文件消息]</view>
<view class="item-info-top_content" wx:elif="{{conversation.lastMessage.type == 'order'}}">[自定义消息:订单]</view>
<view class="item-info-top_content" wx:else>[[未识别内容]]</view>
<image class="item-info-bottom_action" catchtap="showAction" data-conversation="{{conversation}}" src="../../static/images/action.png"></image>
</view>
</view>
</view>
</view>
</view>
<view class="no-conversation" wx:else>
当前没有会话为空
</view>
<view class="action-container" wx:if="{{action.show}}">
<view class="layer" bindtap="closeMask"></view>
<view class="action-box">
<view class="action-item" bindtap="topConversation">{{action.conversation.top ? '取消置顶' : '置顶聊天'}}</view>
<view class="action-item" bindtap="removeConversation">删除聊天</view>
</view>
</view>
<view class="action-toast" wx:if="{{action.showToast}}">
{{action.toastMessage}}
</view>
</scroll-view>
</view>

View File

@@ -0,0 +1,164 @@
page{ height: 100%; }
.conversations-container{
width: 100%;
overflow: hidden;
height: 100%;
}
.conversations{
width: 750rpx;
height: 100%;
overflow-x: hidden;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.conversations .scroll-item{
height: 152rpx;
padding-left: 32rpx;
display: flex;
align-items: center;
}
.conversations .scroll-item .head-icon{
width:100rpx;
height: 100rpx;
margin-right: 28rpx;
}
.conversations .scroll-item_info{
height: 151rpx;
width: 590rpx;
padding-right: 32rpx;
box-sizing: border-box;
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: 26rpx;
color: rgba(179, 179, 179, 0.8);
font-family: Source Han Sans CN;
}
.conversations .item-info-bottom{
height: 40rpx;
line-height: 40rpx;
overflow: hidden;
}
.conversations .item-info-bottom-item{
display: flex;
align-items: center;
justify-content: space-between;
}
.item-info-bottom .item-info-top_content{
font-size: 30rpx;
color: #b3b3b3;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
.item-info-bottom .item-info-bottom_unread{
padding: 6rpx;
background-color: #EE593C;
color: #FFFFFF;
font-size: 24rpx;
line-height: 28rpx;
border-radius: 24rpx;
min-width: 24rpx;
min-height: 24rpx;
text-align: center;
}
.no-conversation{
width: 100%;
text-align: center;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #9D9D9D;
}
.item-info-bottom .item-info-bottom_action{
width:30rpx;
height: 30rpx;
font-size: 20rpx;
background-size: 28rpx 30rpx;
}
.item-head{
position: relative;
}
.item-head .item-head_unread{
padding: 6rpx;
background-color: #EE593C;
color: #FFFFFF;
font-size: 24rpx;
line-height: 28rpx;
border-radius: 24rpx;
min-width: 24rpx;
min-height: 24rpx;
text-align: center;
position: absolute;
top:0;
right: 15rpx;
}
.action-container{
width: 100%;
height: 100%;
position: absolute;
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;
}
.action-toast{
position: absolute;
width: 400rpx;
height: 100rpx;
font-size: 30rpx;
line-height: 100rpx;
background: #9D9D9D;
border-radius: 20rpx;
top:50%;
left: 50%;
margin: -50rpx -200rpx;
text-align: center;
color: #262628;
}

27
pages/login/login.js Normal file
View File

@@ -0,0 +1,27 @@
/* login.js */
import restapi from "../../static/lib/restapi";
Page({
data: {
username:"",
password:"",
showError:false,
},
login: function(e) {
let username = e.detail.value.username;
let password = e.detail.value.password;
if (username.trim() !== "" && password.trim() !== "") {
let user = restapi.findUser(username,password);
if (user) {
wx.setStorageSync('currentUser',user);
// 页面跳转
wx.switchTab({
url:'../conversations/conversations'
});
return;
}
}
this.setData({
showError:true
});
}
})

3
pages/login/login.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": ""
}

13
pages/login/login.wxml Normal file
View File

@@ -0,0 +1,13 @@
<!--login.wxml-->
<view class="container">
<view class="login">
<form bindsubmit="login">
<view class="title">GoEasy IM</view>
<input class="input-box" type="text" placeholder="请输入账号" name="username" confirm-hold hold-keyboard="{{true}}" adjust-position="{{true}}"/>
<input class="input-box" type="password" placeholder="请输入密码" name= "password" />
<view wx:if="{{showError}}" class="error">请输入正确的用户名和密码</view>
<button class="login-btn" form-type="submit">登录</button>
<view class="login-tips">登录所需用户名和密码见 restapi.js</view>
</form>
</view>
</view>

67
pages/login/login.wxss Normal file
View File

@@ -0,0 +1,67 @@
/* login.wxss */
page {
width: 100%;
height: 100%;
font-family: Source Han Sans CN;
}
.login {
display: flex;
align-items: center;
flex-direction: column;
padding: 72rpx;
}
form{
width: 100%;
height: 100%;
overflow: hidden;
}
.title {
width: 100%;
font-size: 84rpx;
font-style: normal;
font-weight: bold;
color: #D02129;
text-align: center;
margin-bottom: 40rpx;
}
.login-tips {
text-align: center;
margin-top: 40rpx;
color: #999999;
}
.input-box {
width: 100%;
height: 100rpx;
padding: 20rpx;
border: 2rpx solid #D02129;
box-sizing: border-box;
margin-bottom: 40rpx;
}
.error {
display: flex;
align-items: center;
width: 100%;
height: 120rpx;
padding-left: 20rpx;
box-sizing: border-box;
margin-bottom: 40rpx;
color: #D02129;
background: rgba(208, 33, 41, 0.1);
}
.login-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100rpx;
color: #FFFFFF;
background: #D02129;
}

29
pages/mine/mine.js Normal file
View File

@@ -0,0 +1,29 @@
/* login.js */
const app = getApp()
Page({
data : {
currentUser : null
},
onShow () {
let service = app.globalData.imService;
this.setData({
currentUser : service.currentUser
});
if(!this.data.currentUser) {
wx.redirectTo({
url : '../login/login'
})
}
},
logout () {
wx.im.disconnect().then(() => {
console.log("断连成功");
wx.removeStorageSync("currentUser");
app.globalData.imService= null;
wx.redirectTo({
url: '../login/login'
})
})
}
})

3
pages/mine/mine.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "我的"
}

11
pages/mine/mine.wxml Normal file
View File

@@ -0,0 +1,11 @@
<!--login.wxml-->
<div class="mine">
<div class="top">
<image src="{{currentUser.avatar}}"></image>
<view class="name">{{currentUser.name}}</view>
</div>
<div class="bottom">
<text>欢迎体验GoEasyIM</text>
<view class="logout" bindtap="logout">注销</view>
</div>
</div>

36
pages/mine/mine.wxss Normal file
View File

@@ -0,0 +1,36 @@
.mine{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.top{
height: 400rpx;
background: #F3F4F7;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.top image{
width:156rpx;
height: 156rpx;
border-radius: 156rpx;
}
.top .name{
line-height: 80rpx;
}
.bottom{
text-align: center;
line-height: 200rpx;
}
.bottom .logout{
width:266rpx;
height: 76rpx;
border-radius: 10rpx;
background: #D02129;
color: #ffffff;
font-size: 32rpx;
margin:0 auto;
line-height: 76rpx;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

BIN
static/images/Avatar-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
static/images/Avatar-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
static/images/Avatar-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
static/images/Avatar-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
static/images/Vector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

BIN
static/images/action.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

BIN
static/images/chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

BIN
static/images/contacts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

BIN
static/images/dingdan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

BIN
static/images/emoji.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

BIN
static/images/failed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

BIN
static/images/file-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

BIN
static/images/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

BIN
static/images/goeasy.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/images/green-dot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

BIN
static/images/group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/images/im.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

BIN
static/images/jianpan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

BIN
static/images/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

BIN
static/images/mine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

BIN
static/images/more.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

BIN
static/images/pending.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/images/shipin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

BIN
static/images/tupian.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

BIN
static/images/uniapp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

BIN
static/images/wx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/images/zidingyi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

View File

@@ -0,0 +1,33 @@
/*
* @Author: jack.lu
* @Date: 2020/9/11
* @Last Modified by: jack.lu
* @Last Modified time: 2020/9/11 4:35 下午
*/
class EmojiDecoder {
emojiMap = null;
url = "";
patterns = [];
metaChars = /[[\]{}()*+?.\\|^$\-,&#\s]/g;
decode = this.decode;
constructor(url,emojiMap) {
this.url = url || '';
this.emojiMap = emojiMap || {};
for (let i in this.emojiMap) {
if (this.emojiMap.hasOwnProperty(i)){
this.patterns.push('('+i.replace(this.metaChars, "\\$&")+')');
}
}
console.log(this)
}
decode (text) {
return text.replace(new RegExp(this.patterns.join('|'),'g'), (match) => {
return typeof this.emojiMap[match] != 'undefined' ? '<img height="20rpx" width="20rpx" src="'+this.url+this.emojiMap[match]+'" />' : match;
});
}
}
export default EmojiDecoder

File diff suppressed because one or more lines are too long

205
static/lib/imservice.js Normal file
View File

@@ -0,0 +1,205 @@
/*
* @Author: jack.lu
* @Date: 2020-4-21 10:10:20
* @Last Modified by: jack.lu
* @Last Modified time: 2020-4-21 15:01:41
*/
import GoEasyIM from './goeasy-im-1.5.1';
import restApi from './restapi';
function Friend(uuid, name, avatar) {
this.uuid = uuid;
this.name = name;
this.avatar = avatar;
}
function Group(uuid, name, avatar) {
this.uuid = uuid;
this.name = name;
this.avatar = avatar;
}
function CurrentUser(uuid, name, avatar) {
this.uuid = uuid;
this.name = name;
this.avatar = avatar;
}
function IMService(im) {
this.im = im;
//当前“我”
this.currentUser = null;
//私聊消息记录map格式每个好友对应一个数组
this.privateMessages = {};
//群聊消息记录map格式每个群对应一个数组
this.groupMessages = {};
/*
* 监听器们
*
* 可以在页面里,根据需求,重写以下监听器,
* 便于当各种事件触发时,页面能够执行对应的响应
*
*/
//收到一条私聊消息
this.onNewPrivateMessageReceive = function (friendId, message) {};
//收到一条群聊消息
this.onNewGroupMessageReceive = function (groupId, message) {};
}
//获取群成员
IMService.prototype.getGroupMembers = function (groupId) {
let members = restApi.findGroupMembers(groupId);
let membersMap = {};
members.map(item => {
membersMap[item.uuid] = item
});
return membersMap;
};
IMService.prototype.findGroupById = function (groupId) {
let group = restApi.findGroupById(groupId);
return new Group(group.uuid, group.name, group.avatar);
};
IMService.prototype.getGroupMessages = function (groupId) {
if (!this.groupMessages[groupId]) {
this.groupMessages[groupId] = [];
}
return this.groupMessages[groupId]
};
IMService.prototype.findFriendById = function (userId) {
let user = restApi.findUserById(userId);
return new Friend(user.uuid, user.name, user.avatar);
};
IMService.prototype.getPrivateMessages = function (friendId) {
if (!this.privateMessages[friendId]) {
this.privateMessages[friendId] = [];
}
return this.privateMessages[friendId]
};
//连接GoEasy
IMService.prototype.connectIM = function (currentUser) {
this.currentUser = currentUser;
//初始化IM相关的监听器
this.initialIMListeners();
this.im.connect({
id: this.currentUser.uuid,
data: {
avatar: this.currentUser.avatar,
name: this.currentUser.name
}
}).then(() => {
console.log('connect成功')
}).catch(error => {
console.log('connect失败,请确保网络正常appkey和host正确code:' + error.code + " content:" + error.content);
});
this.subscribeGroupMessage();
};
IMService.prototype.subscribeGroupMessage = function () {
let groups = restApi.findGroups(this.currentUser);
let groupIds = groups.map(item => item.uuid);
this.im.subscribeGroup(groupIds)
.then(() => {
console.log('订阅群消息成功')
})
.catch(error => {
console.log('订阅群消息失败')
console.log(error)
})
}
//IM监听
IMService.prototype.initialIMListeners = function () {
this.im.on(GoEasyIM.EVENT.CONNECTED, () => {
console.log('连接成功.')
});
this.im.on(GoEasyIM.EVENT.DISCONNECTED, () => {
console.log('连接断开.')
});
this.im.on(GoEasyIM.EVENT.CONNECTING, (times) => {
console.log('连接中', times);
});
//监听私聊消息
this.im.on(GoEasyIM.EVENT.PRIVATE_MESSAGE_RECEIVED, (message) => {
//更新私聊消息记录
let friendId;
if (this.currentUser.uuid == message.senderId) {
friendId = message.receiverId;
} else {
friendId = message.senderId;
}
let friendMessages = this.getPrivateMessages(friendId);
friendMessages.push(message);
//如果页面传入了相应的listener执行listener
this.onNewPrivateMessageReceive(friendId, message);
});
//监听群聊消息
this.im.on(GoEasyIM.EVENT.GROUP_MESSAGE_RECEIVED, (message) => {
let groupId = message.groupId;
//更新群聊消息记录
let groupMessages = this.getGroupMessages(groupId);
groupMessages.push(message);
//如果页面传入了相应的listener执行listener
this.onNewGroupMessageReceive(groupId, message);
})
};
//加载单聊历史消息
IMService.prototype.loadPrivateHistoryMessage = function (friendId, timeStamp) {
return new Promise((resolve, reject) => {
this.im.history({
friendId: friendId,
lastTimestamp: timeStamp
}).then(result => {
let history = result.content;
let friendMessages = this.getPrivateMessages(friendId);
for (let i = history.length - 1; i >=0; i--) {
friendMessages.unshift(history[i])
}
resolve(friendMessages)
}).catch(error => {
if (error.code == 401) {
console.log("您尚未开通历史消息请登录GoEasy查看应用详情里自助启用.");
}
reject(error)
});
})
};
//群聊历史消息
IMService.prototype.loadGroupHistoryMessage = function (groupId, timeStamp) {
return new Promise((resolve, reject) => {
this.im.history({
groupId: groupId,
lastTimestamp: timeStamp
}).then(result => {
let history = result.content;
let chatMessage = this.getGroupMessages(groupId);
for (let i = history.length - 1; i >= 0; i--) {
chatMessage.unshift(history[i]);
}
resolve(chatMessage)
}).catch(error => {
if (error.code == 401) {
console.log("您尚未开通历史消息请登录GoEasy查看应用详情里自助启用.");
}
reject(error)
});
})
};
export default IMService;

94
static/lib/restapi.js Normal file
View File

@@ -0,0 +1,94 @@
//用户数据示例
let users = [
{
"uuid": "08c0a6ec-a42b-47b2-bb1e-15e0f5f9a19a",
"name": "Mattie",
"password": "123",
"avatar": '/static/images/Avatar-1.png'
},
{
"uuid": "3bb179af-bcc5-4fe0-9dac-c05688484649",
"name": "Wallace",
"password": "123",
"avatar": '/static/images/Avatar-2.png'
},
{
"uuid": "fdee46b0-4b01-4590-bdba-6586d7617f95",
"name": "Tracy",
"password": "123",
"avatar": '/static/images/Avatar-3.png'
},
{
"uuid": "33c3693b-dbb0-4bc9-99c6-fa77b9eb763f",
"name": "Juanita",
"password": "123",
"avatar": '/static/images/Avatar-4.png'
}
];
//群数据示例
let groups = [
{
"uuid": "group-a42b-47b2-bb1e-15e0f5f9a19a",
"name": "小程序交流群",
"avatar" : '/static/images/wx.png',
"userList": ['08c0a6ec-a42b-47b2-bb1e-15e0f5f9a19a', '3bb179af-bcc5-4fe0-9dac-c05688484649', 'fdee46b0-4b01-4590-bdba-6586d7617f95', '33c3693b-dbb0-4bc9-99c6-fa77b9eb763f']
},
{
"uuid": "group-4b01-4590-bdba-6586d7617f95",
"name": "UniApp交流群",
"avatar" : '/static/images/uniapp.png',
"userList": ['08c0a6ec-a42b-47b2-bb1e-15e0f5f9a19a', 'fdee46b0-4b01-4590-bdba-6586d7617f95', '33c3693b-dbb0-4bc9-99c6-fa77b9eb763f']
},
{
"uuid": "group-dbb0-4bc9-99c6-fa77b9eb763f",
"name": "GoEasy交流群",
"avatar" : '/static/images/goeasy.jpeg',
"userList": ['08c0a6ec-a42b-47b2-bb1e-15e0f5f9a19a', '3bb179af-bcc5-4fe0-9dac-c05688484649']
}
];
function RestApi() {
}
RestApi.prototype.findFriends = function (user) {
var friendList = users.filter(v => v.uuid != user.uuid);
return friendList;
};
RestApi.prototype.findGroups = function (user) {
var groupList = groups.filter(v => v.userList.find(id => id == user.uuid));
return groupList;
};
RestApi.prototype.findUser = function (username, password) {
var user = users.find(user => (user.name == username && user.password == password))
return user;
};
RestApi.prototype.findGroupById = function (groupId) {
var group = groups.find(group => (group.uuid == groupId));
return group;
};
RestApi.prototype.findUserById = function (userId) {
var user = users.find(user => (user.uuid == userId))
return user;
};
RestApi.prototype.findGroupMembers = function (groupId) {
let members = [];
let group = groups.find(v => v.uuid == groupId);
users.map(user => {
if (group.userList.find(v => v == user.uuid)) {
members.push(user)
}
});
return members;
};
export default new RestApi();