add file
@@ -0,0 +1,50 @@
|
||||
# TUICallkit 组件接入说明
|
||||
TUICallkit 是小程序音视频通话 UI 组件,通过编写几行代码,就可以为您的 小程序 应用添加音视频通话功能。
|
||||
|
||||
## 环境准备
|
||||
|
||||
- 微信 App iOS 最低版本要求:7.0.9
|
||||
- 微信 App Android 最低版本要求:7.0.8
|
||||
- 小程序基础库最低版本要求:2.10.0
|
||||
- 由于小程序测试号不具备 live-pusher 和 live-player 的使用权限,请使用企业小程序账号申请相关权限进行开发
|
||||
- 由于微信开发者工具不支持原生组件(即 live-pusher 和 live-player 标签),需要在真机上进行运行体验
|
||||
|
||||
## 特性
|
||||
- ⚡️ 功能全面 —— 支持单人/多人/音频/视频通话、支持视频转音频通话、支持自由选择通话设备
|
||||
- 🎨 灵活样式 —— 组件开源,可复用逻辑,自定义 UI 样式
|
||||
- 🛠 优秀生态 —— 与 [TUIKit](https://cloud.tencent.com/document/product/269/79737) 协同使用,可以在 [TIM](https://cloud.tencent.com/document/product/269) 会话中直接发起音视频通话
|
||||
- 🌍 跨平台 —— 支持 Android、iOS、Web、小程序、Flutter、UniApp 等 [多个开发平台](https://cloud.tencent.com/document/product/647/78742)
|
||||
- ☁️ 低延迟 —— 腾讯云全球链路资源储备,保证国际链路端到端平均时延 < 300ms
|
||||
- 🤙🏻 低卡顿 —— 抗丢包率超过 80%、抗网络抖动超过 1000ms,弱网环境仍顺畅稳定
|
||||
- 🌈 高品质 —— 支持 720P、1080P 高清画质,70% 丢包率仍可正常视频
|
||||
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
TUICallkit
|
||||
├─ component // UI 组件
|
||||
├─ calling // 呼叫中 UI 组件(1v1通话)
|
||||
└─ connected // 通话中 UI 组件(1v1通话)
|
||||
├─ groupCalling // 呼叫中 UI 组件(群组通话)
|
||||
└─ groupConnected // 通话中 UI 组件(群组通话)
|
||||
├─ pages
|
||||
├─ globalCall // 全局监听页面
|
||||
├─ serve
|
||||
├─ callManager // 全局监听类
|
||||
├─ static // UI icon 图片
|
||||
```
|
||||
|
||||
|
||||
## 使用指引
|
||||
为方便您的使用,本组件配套多篇使用指引:
|
||||
- 如果您想要了解 TUICallKit,请阅读 [组件介绍 TUICallKit](https://cloud.tencent.com/document/product/647/78742)。
|
||||
- 如果您想把我们的功能直接嵌入到您的项目中,请阅读 [快速接入 TUICallKit](https://cloud.tencent.com/document/product/647/78733)。
|
||||
- 如果您想要了解详细 API,请阅读 [API 概览](https://cloud.tencent.com/document/product/647/78759)。
|
||||
|
||||
## 附录
|
||||
|
||||
- 如果你遇到了困难,可以先参阅 [常见问题](https://cloud.tencent.com/document/product/647/78733)。
|
||||
- 如果发现了示例代码的 bug,欢迎提交 issue。
|
||||
- 欢迎加入 QQ 群:**646165204**,进行技术交流和反馈~
|
||||
-
|
||||
@@ -0,0 +1,695 @@
|
||||
import TUICallEngine, { EVENT, MEDIA_TYPE, AUDIO_PLAYBACK_DEVICE, STATUS } from 'tuicall-engine-wx';
|
||||
|
||||
// 组件旨在跨终端维护一个通话状态管理机,以事件发布机制驱动上层进行管理,并通过API调用进行状态变更。
|
||||
// 组件设计思路将UI和状态管理分离。您可以通过修改`component`文件夹下的文件,适配您的业务场景,
|
||||
// 在UI展示上,您可以通过属性的方式,将上层的用户头像,名称等数据传入组件内部,`static`下的icon和默认头像图片,
|
||||
// 只是为了展示基础的效果,您需要根据业务场景进行修改。
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
config: {
|
||||
type: Object,
|
||||
value: {
|
||||
sdkAppID: 0,
|
||||
userID: '',
|
||||
userSig: '',
|
||||
type: 1,
|
||||
tim: null,
|
||||
},
|
||||
},
|
||||
backgroundMute: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
observers: {
|
||||
|
||||
},
|
||||
|
||||
data: {
|
||||
callStatus: STATUS.IDLE, // idle、calling、connection
|
||||
isSponsor: false, // 呼叫者身份 true为呼叫者 false为被叫者
|
||||
pusher: {}, // TRTC 本地流
|
||||
playerList: [], // TRTC 远端流
|
||||
playerProcess: {}, // 经过处理的的远端流(多人通话)
|
||||
isGroup: false, // 是否为多人通话
|
||||
remoteUsers: [], // 单人通话远程用户资料(不包含自身)
|
||||
allUsers: [], // 多人通话用户资料(包含自身)
|
||||
sponsor: '', // 主叫方
|
||||
screen: 'pusher', // 视屏通话中,显示大屏幕的流(只限1v1聊天
|
||||
soundMode: AUDIO_PLAYBACK_DEVICE.SPEAKER, // 声音模式 听筒/扬声器
|
||||
showToatTime: 0, // 弹窗时长
|
||||
ownUserId: '',
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
resetUI() {
|
||||
// 收起键盘
|
||||
wx.hideKeyboard();
|
||||
},
|
||||
// 新的邀请回调事件
|
||||
handleNewInvitationReceived(event) {
|
||||
this.resetUI();
|
||||
this.data.config.type = event.data.inviteData.callType;
|
||||
// 判断是否为多人通话
|
||||
if (event.data.isFromGroup) {
|
||||
this.setData({
|
||||
isGroup: true,
|
||||
sponsor: event.data.sponsor,
|
||||
});
|
||||
// 将主叫方和被叫列表合并在一起 组成全部通话人数
|
||||
const newList = [...event.data.inviteeList, event.data.sponsor];
|
||||
// 获取用户信息
|
||||
this.getUserProfile(newList);
|
||||
} else {
|
||||
this.getUserProfile([event.data.sponsor]);
|
||||
}
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
callStatus: STATUS.CALLING,
|
||||
isSponsor: false,
|
||||
});
|
||||
},
|
||||
|
||||
// 用户接听
|
||||
handleUserAccept(event) {
|
||||
// 主叫方则唤起通话页面
|
||||
if (this.data.isSponsor) {
|
||||
this.setData({
|
||||
callStatus: STATUS.CONNECTED,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 远端用户进入通话
|
||||
handleUserEnter(res) {
|
||||
const newList = this.data.allUsers;
|
||||
// 多人通话
|
||||
if (this.data.isGroup) {
|
||||
// 改变远端用户信息中的isEnter属性
|
||||
for (let i = 0;i < newList.length;i++) {
|
||||
if (newList[i].userID === res.data.userID) {
|
||||
newList[i].isEnter = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
playerList: res.playerList,
|
||||
allUsers: newList,
|
||||
});
|
||||
},
|
||||
// 远端用户离开通话
|
||||
handleUserLeave(res) {
|
||||
// 多人通话
|
||||
if (this.data.isGroup) {
|
||||
wx.showToast({
|
||||
title: `${res.data.userID}离开通话`,
|
||||
});
|
||||
this.deleteUsers(this.data.allUsers, res.data.userID);
|
||||
}
|
||||
this.setData({
|
||||
playerList: res.data.playerList,
|
||||
});
|
||||
},
|
||||
// 用户数据更新
|
||||
handleUserUpdate(res) {
|
||||
this.handleNetStatus(res.data);
|
||||
const newplayer = {};
|
||||
const newres = res.data.playerList;
|
||||
// 多人通话
|
||||
if (this.data.isGroup) {
|
||||
// 处理远端流
|
||||
for (let i = 0;i < newres.length;i++) {
|
||||
const { userID } = newres[i];
|
||||
newplayer[userID] = newres[i];
|
||||
}
|
||||
};
|
||||
this.setData({
|
||||
pusher: res.data.pusher,
|
||||
playerList: res.data.playerList,
|
||||
playerProcess: newplayer,
|
||||
});
|
||||
},
|
||||
// 判断网络状态
|
||||
handleNetStatus(data) {
|
||||
if (data.pusher.netQualityLevel > 4) {
|
||||
wx.showToast({
|
||||
icon: 'none',
|
||||
title: '您当前网络不佳',
|
||||
});
|
||||
}
|
||||
data.playerList.map((item) => {
|
||||
if (item.netStatus.netQualityLevel > 4) {
|
||||
const name = data.playerList.length > 1 ? item.nick : '对方';
|
||||
wx.showToast({
|
||||
icon: 'none',
|
||||
title: `${name}当前网络不佳`,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
// 用户拒绝
|
||||
handleInviteeReject(event) {
|
||||
if (this.data.isGroup) {
|
||||
this.deleteUsers(this.data.allUsers, event.data.invitee);
|
||||
}
|
||||
wx.showToast({
|
||||
title: `${event.data.invitee}已拒绝`,
|
||||
});
|
||||
},
|
||||
// 用户不在线
|
||||
handleNoResponse(event) {
|
||||
if (this.data.isGroup) {
|
||||
this.deleteUsers(this.data.allUsers, event.data.timeoutUserList);
|
||||
}
|
||||
wx.showToast({
|
||||
icon: 'none',
|
||||
title: `${event.data.timeoutUserList}无应答`,
|
||||
});
|
||||
},
|
||||
// 用户忙线
|
||||
handleLineBusy(event) {
|
||||
if (this.data.isGroup) {
|
||||
this.deleteUsers(this.data.allUsers, event.data.invitee);
|
||||
}
|
||||
this.showToast(event.data.invitee);
|
||||
},
|
||||
|
||||
showToast(event) {
|
||||
this.setData({
|
||||
showToatTime: this.data.showToatTime + 500,
|
||||
});
|
||||
setTimeout(() => {
|
||||
wx.showToast({
|
||||
title: `${event}忙线中`,
|
||||
});
|
||||
}, this.data.showToatTime);
|
||||
},
|
||||
|
||||
// 用户取消
|
||||
handleCallingCancel(event) {
|
||||
if (event.data.invitee !== this.data.config.userID) {
|
||||
wx.showToast({
|
||||
title: `${event.data.invitee}取消通话`,
|
||||
});
|
||||
}
|
||||
this.reset();
|
||||
},
|
||||
// 通话超时未应答
|
||||
handleCallingTimeout(event) {
|
||||
if (this.data.isGroup) {
|
||||
// 若是自身未应答 则不弹窗
|
||||
if (this.data.config.userID === event.data.timeoutUserList[0]) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
const newList = this.deleteUsers(this.data.allUsers, event.data.timeoutUserList);
|
||||
this.setData({
|
||||
allUsers: newList,
|
||||
});
|
||||
}
|
||||
if (this.data.playerList.length === 0) {
|
||||
this.reset();
|
||||
}
|
||||
wx.showToast({
|
||||
title: `${event.data.timeoutUserList[0]}超时无应答`,
|
||||
});
|
||||
},
|
||||
handleCallingUser(userIDList) {
|
||||
const remoteUsers = [...this.data.remoteUsers];
|
||||
const userProfile = remoteUsers.filter(item => userIDList.some(userItem => `${userItem}` === item.userID));
|
||||
this.setData({
|
||||
remoteUsers: remoteUsers.filter(item => userIDList.some(userItem => userItem !== item.userID)),
|
||||
});
|
||||
let nick = '';
|
||||
for (let i = 0; i < userProfile.length; i++) {
|
||||
nick += `${userProfile[i].nick}、`;
|
||||
}
|
||||
return nick.slice(0, -1);
|
||||
},
|
||||
// 通话结束
|
||||
handleCallingEnd(event) {
|
||||
wx.showToast({
|
||||
title: '通话结束',
|
||||
duration: 800,
|
||||
});
|
||||
this.reset();
|
||||
},
|
||||
|
||||
// SDK Ready 回调
|
||||
handleSDKReady() {
|
||||
// 呼叫需在sdk ready之后
|
||||
},
|
||||
// 被踢下线
|
||||
handleKickedOut() {
|
||||
wx.showToast({
|
||||
title: '您已被踢下线',
|
||||
});
|
||||
},
|
||||
// 切换通话模式
|
||||
handleCallMode(event) {
|
||||
this.data.config.type = event.data.type;
|
||||
this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
});
|
||||
},
|
||||
|
||||
// 删除用户列表操作
|
||||
deleteUsers(usersList, userID) {
|
||||
// 若userID不是数组,则将其转换为数组
|
||||
if (!Array.isArray(userID)) {
|
||||
userID = [userID];
|
||||
}
|
||||
const list = usersList.filter(item => !userID.includes(item.userID));
|
||||
this.setData({
|
||||
allUsers: list,
|
||||
});
|
||||
},
|
||||
|
||||
// 增加用户列表操作
|
||||
addUsers(usersList, userID) {
|
||||
// 若userID不是数组,则将其转换为数组
|
||||
if (!Array.isArray(userID)) {
|
||||
userID = [userID];
|
||||
}
|
||||
const newList = [...usersList, ...userID];
|
||||
return newList;
|
||||
},
|
||||
|
||||
|
||||
// 增加 tsignaling 事件监听
|
||||
_addTSignalingEvent() {
|
||||
// 被邀请通话
|
||||
wx.$TUICallEngine.on(EVENT.INVITED, this.handleNewInvitationReceived, this);
|
||||
// 用户接听
|
||||
wx.$TUICallEngine.on(EVENT.USER_ACCEPT, this.handleUserAccept, this);
|
||||
// 用户进入通话
|
||||
wx.$TUICallEngine.on(EVENT.USER_ENTER, this.handleUserEnter, this);
|
||||
// 用户离开通话
|
||||
wx.$TUICallEngine.on(EVENT.USER_LEAVE, this.handleUserLeave, this);
|
||||
// 用户更新数据
|
||||
wx.$TUICallEngine.on(EVENT.USER_UPDATE, this.handleUserUpdate, this);
|
||||
// 用户拒绝通话
|
||||
wx.$TUICallEngine.on(EVENT.REJECT, this.handleInviteeReject, this);
|
||||
// 用户无响应
|
||||
wx.$TUICallEngine.on(EVENT.NO_RESP, this.handleNoResponse, this);
|
||||
// 用户忙线
|
||||
wx.$TUICallEngine.on(EVENT.LINE_BUSY, this.handleLineBusy, this);
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.on(EVENT.CALLING_CANCEL, this.handleCallingCancel, this);
|
||||
// 通话超时未应答
|
||||
wx.$TUICallEngine.on(EVENT.CALLING_TIMEOUT, this.handleCallingTimeout, this);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.on(EVENT.CALL_END, this.handleCallingEnd, this);
|
||||
// SDK Ready 回调
|
||||
wx.$TUICallEngine.on(EVENT.SDK_READY, this.handleSDKReady, this);
|
||||
// 被踢下线
|
||||
wx.$TUICallEngine.on(EVENT.KICKED_OUT, this.handleKickedOut, this);
|
||||
// 切换通话模式
|
||||
wx.$TUICallEngine.on(EVENT.CALL_MODE, this.handleCallMode, this);
|
||||
// 自己发送消息
|
||||
wx.$TUICallEngine.on(EVENT.MESSAGE_SENT_BY_ME, this.messageSentByMe, this);
|
||||
},
|
||||
// 取消 tsignaling 事件监听
|
||||
_removeTSignalingEvent() {
|
||||
// 被邀请通话
|
||||
wx.$TUICallEngine.off(EVENT.INVITED, this.handleNewInvitationReceived);
|
||||
// 用户接听
|
||||
wx.$TUICallEngine.off(EVENT.USER_ACCEPT, this.handleUserAccept);
|
||||
// 用户进入通话
|
||||
wx.$TUICallEngine.off(EVENT.USER_ENTER, this.handleUserEnter);
|
||||
// 用户离开通话
|
||||
wx.$TUICallEngine.off(EVENT.USER_LEAVE, this.handleUserLeave);
|
||||
// 用户更新数据
|
||||
wx.$TUICallEngine.off(EVENT.USER_UPDATE, this.handleUserUpdate);
|
||||
// 用户拒绝通话
|
||||
wx.$TUICallEngine.off(EVENT.REJECT, this.handleInviteeReject);
|
||||
// 用户无响应
|
||||
wx.$TUICallEngine.off(EVENT.NO_RESP, this.handleNoResponse);
|
||||
// 用户忙线
|
||||
wx.$TUICallEngine.off(EVENT.LINE_BUSY, this.handleLineBusy);
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_CANCEL, this.handleCallingCancel);
|
||||
// 通话超时未应答
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_TIMEOUT, this.handleCallingTimeout);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.off(EVENT.CALL_END, this.handleCallingEnd);
|
||||
// SDK Ready 回调
|
||||
wx.$TUICallEngine.off(EVENT.SDK_READY, this.handleSDKReady);
|
||||
// 被踢下线
|
||||
wx.$TUICallEngine.off(EVENT.KICKED_OUT, this.handleKickedOut);
|
||||
// 切换通话模式
|
||||
wx.$TUICallEngine.off(EVENT.CALL_MODE, this.handleCallMode);
|
||||
// 自己发送消息
|
||||
wx.$TUICallEngine.off(EVENT.MESSAGE_SENT_BY_ME, this.messageSentByMe);
|
||||
},
|
||||
/**
|
||||
* C2C邀请通话,被邀请方会收到的回调
|
||||
* 如果当前处于通话中,可以调用该函数以邀请第三方进入通话
|
||||
*
|
||||
* @param userID 被邀请方
|
||||
* @param type 0-为之, 1-语音通话,2-视频通话
|
||||
*/
|
||||
async call(params) {
|
||||
this.resetUI();
|
||||
if (this.data.callStatus !== STATUS.IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
await wx.$TUICallEngine.call({ userID: params.userID, type: params.type }).then((res) => {
|
||||
this.data.config.type = params.type;
|
||||
this.getUserProfile([params.userID]);
|
||||
this.setData({
|
||||
pusher: res.pusher,
|
||||
config: this.data.config,
|
||||
callStatus: STATUS.CALLING,
|
||||
isSponsor: true,
|
||||
});
|
||||
this.setSoundMode(this.data.config.type === MEDIA_TYPE.AUDIO ? AUDIO_PLAYBACK_DEVICE.EAR : AUDIO_PLAYBACK_DEVICE.SPEAKER);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* IM群组邀请通话,被邀请方会收到的回调
|
||||
* 如果当前处于通话中,可以继续调用该函数继续邀请他人进入通话,同时正在通话的用户会收到的回调
|
||||
*
|
||||
* @param userIDList 邀请列表
|
||||
* @param type 1-语音通话,2-视频通话
|
||||
* @param groupID IM群组ID
|
||||
*/
|
||||
async groupCall(params) {
|
||||
// 判断是否存在groupID
|
||||
if (!params.groupID) {
|
||||
wx.showToast({
|
||||
title: '群ID为空',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.resetUI();
|
||||
if (this.data.callStatus !== STATUS.IDLE) {
|
||||
return;
|
||||
}
|
||||
wx.$TUICallEngine.groupCall({ userIDList: params.userIDList, type: params.type, groupID: params.groupID }).then((res) => {
|
||||
this.data.config.type = params.type;
|
||||
this.setData({
|
||||
pusher: res.pusher,
|
||||
config: this.data.config,
|
||||
callStatus: STATUS.CALLING,
|
||||
isSponsor: true,
|
||||
isGroup: true,
|
||||
sponsor: this.data.config.userID,
|
||||
});
|
||||
// 将自身的userID插入到邀请列表中,组成完整的用户信息
|
||||
const list = JSON.parse(JSON.stringify(params.userIDList));
|
||||
list.unshift(this.data.config.userID);
|
||||
// 获取用户信息
|
||||
this.getUserProfile(list);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 当您作为被邀请方收到 {@link TRTCCallingDelegate#onInvited } 的回调时,可以调用该函数接听来电
|
||||
*/
|
||||
async accept() {
|
||||
wx.$TUICallEngine.accept().then((res) => {
|
||||
this.setData({
|
||||
pusher: res.pusher,
|
||||
callStatus: STATUS.CONNECTED,
|
||||
});
|
||||
// 多人通话需要对自身位置进行修正,将其放到首位
|
||||
if (this.data.isGroup) {
|
||||
const newList = this.data.allUsers;
|
||||
for (let i = 0;i < newList.length;i++) {
|
||||
if (newList[i].userID === this.data.config.userID) {
|
||||
newList[i].isEnter = true;
|
||||
[newList[i], newList[0]] = [newList[0], newList[i]];
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
allUsers: newList,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: 'error',
|
||||
content: error.message,
|
||||
showCancel: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 当您作为被邀请方收到的回调时,可以调用该函数拒绝来电
|
||||
*/
|
||||
async reject() {
|
||||
wx.$TUICallEngine.reject().then((res) => {
|
||||
this.reset();
|
||||
});
|
||||
},
|
||||
|
||||
messageSentByMe(event) {
|
||||
const message = event.data.data;
|
||||
this.triggerEvent('sendMessage', {
|
||||
message,
|
||||
});
|
||||
},
|
||||
|
||||
// xml层,是否开启扬声器
|
||||
setSoundMode(type) {
|
||||
this.setData({
|
||||
soundMode: wx.$TUICallEngine.selectAudioPlaybackDevice(type),
|
||||
});
|
||||
},
|
||||
|
||||
// xml层,挂断
|
||||
async _hangUp() {
|
||||
await wx.$TUICallEngine.hangup();
|
||||
this.reset();
|
||||
},
|
||||
|
||||
// 切换大小屏 (仅支持1v1聊天)
|
||||
toggleViewSize(event) {
|
||||
this.setData({
|
||||
// FIXME _toggleViewSize 不应该为TUICallEngine的方法 后续修改
|
||||
screen: wx.$TUICallEngine._toggleViewSize(event),
|
||||
});
|
||||
},
|
||||
// 数据重置
|
||||
reset() {
|
||||
this.setData({
|
||||
callStatus: STATUS.IDLE,
|
||||
isSponsor: false,
|
||||
isGroup: false,
|
||||
soundMode: AUDIO_PLAYBACK_DEVICE.SPEAKER,
|
||||
pusher: {}, // TRTC 本地流
|
||||
playerList: [], // TRTC 远端流
|
||||
showToatTime: 0,
|
||||
});
|
||||
},
|
||||
|
||||
// 呼叫中的事件处理
|
||||
handleCallingEvent(data) {
|
||||
const { name } = data.detail;
|
||||
switch (name) {
|
||||
case 'accept':
|
||||
this.setSoundMode(this.data.config.type === MEDIA_TYPE.AUDIO ? AUDIO_PLAYBACK_DEVICE.EAR : AUDIO_PLAYBACK_DEVICE.SPEAKER);
|
||||
this.accept();
|
||||
break;
|
||||
case 'hangup':
|
||||
this._hangUp();
|
||||
break;
|
||||
case 'reject':
|
||||
this.reject();
|
||||
break;
|
||||
case 'toggleSwitchCamera':
|
||||
wx.$TUICallEngine.switchCamera();
|
||||
break;
|
||||
case 'switchAudioCall':
|
||||
wx.$TUICallEngine.switchCallMediaType(MEDIA_TYPE.AUDIO).then((res) => {
|
||||
this.data.config.type = MEDIA_TYPE.AUDIO;
|
||||
this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
// 通话中的事件处理
|
||||
handleConnectedEvent(data) {
|
||||
const { name, event } = data.detail;
|
||||
switch (name) {
|
||||
case 'toggleViewSize':
|
||||
this.toggleViewSize(event);
|
||||
break;
|
||||
case 'pusherNetStatus':
|
||||
wx.$TUICallEngine._pusherNetStatus(event);
|
||||
break;
|
||||
case 'playNetStatus':
|
||||
wx.$TUICallEngine._playNetStatus(event);
|
||||
break;
|
||||
case 'pusherStateChangeHandler':
|
||||
wx.$TUICallEngine._pusherStateChangeHandler(event);
|
||||
break;
|
||||
case 'pusherAudioVolumeNotify':
|
||||
wx.$TUICallEngine._pusherAudioVolumeNotify(event);
|
||||
break;
|
||||
case 'playerStateChange':
|
||||
wx.$TUICallEngine._playerStateChange(event);
|
||||
break;
|
||||
case 'playerAudioVolumeNotify':
|
||||
wx.$TUICallEngine._playerAudioVolumeNotify(event);
|
||||
break;
|
||||
case 'pusherAudioHandler':
|
||||
wx.$TUICallEngine._pusherAudioHandler(event);
|
||||
break;
|
||||
case 'hangup':
|
||||
this._hangUp();
|
||||
break;
|
||||
case 'toggleSoundMode':
|
||||
this.setSoundMode(this.data.soundMode === AUDIO_PLAYBACK_DEVICE.EAR ? AUDIO_PLAYBACK_DEVICE.SPEAKER : AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
break;
|
||||
case 'pusherVideoHandler':
|
||||
wx.$TUICallEngine._pusherVideoHandler(event);
|
||||
break;
|
||||
case 'toggleSwitchCamera':
|
||||
wx.$TUICallEngine.switchCamera(event);
|
||||
break;
|
||||
case 'switchAudioCall':
|
||||
wx.$TUICallEngine.switchCallMediaType(MEDIA_TYPE.AUDIO).then((res) => {
|
||||
this.data.config.type = MEDIA_TYPE.AUDIO;
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
});
|
||||
this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// 设置用户的头像、昵称
|
||||
setSelfInfo(nickName, avatar) {
|
||||
return wx.$TUICallEngine.setSelfInfo(nickName, avatar);
|
||||
},
|
||||
|
||||
// 获取用户资料
|
||||
async getUserProfile(userList) {
|
||||
const imResponse = await this.getTim().getUserProfile({ userIDList: userList });
|
||||
// 修正用户资料
|
||||
this.modifyUser(imResponse.data);
|
||||
},
|
||||
|
||||
// 修正用户资料
|
||||
modifyUser(userIDList) {
|
||||
const { sponsor } = this.data;
|
||||
if (this.data.isGroup) {
|
||||
// 多人通话需要将呼叫者放到第一位 isEnter的作用是区分用户是否进入房间
|
||||
for (let i = 0;i < userIDList.length;i++) {
|
||||
// 主叫方的标志位设置成true
|
||||
if (userIDList[i].userID === sponsor) {
|
||||
userIDList[i].isEnter = true;
|
||||
// 对主叫方位置进行修正 将其放到首位
|
||||
[userIDList[i], userIDList[0]] = [userIDList[0], userIDList[i]];
|
||||
} else {
|
||||
// 其他用户默认未进入房间 设置为false
|
||||
userIDList[i].isEnter = false;
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
allUsers: userIDList,
|
||||
});
|
||||
}
|
||||
this.setData({
|
||||
remoteUsers: userIDList,
|
||||
});
|
||||
},
|
||||
|
||||
// 获取 tim 实例
|
||||
getTim() {
|
||||
return wx.$TUICallEngine.getTim();
|
||||
},
|
||||
// 初始化TRTCCalling
|
||||
async init(params) {
|
||||
// 兼容从config和init中传值
|
||||
const { sdkAppID, tim, userID, userSig, SDKAppID } = { ...this.data.config, ...params };
|
||||
this.setData({
|
||||
ownUserId: userID,
|
||||
config: {
|
||||
...this.data.config,
|
||||
sdkAppID: sdkAppID || SDKAppID,
|
||||
userID,
|
||||
userSig,
|
||||
},
|
||||
});
|
||||
if (!wx.$TUICallEngine) {
|
||||
wx.$TUICallEngine = TUICallEngine.createInstance({
|
||||
tim,
|
||||
sdkAppID: sdkAppID || SDKAppID,
|
||||
});
|
||||
try {
|
||||
await wx.$TUICallEngine.init({
|
||||
userID,
|
||||
userSig,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
this._addTSignalingEvent();
|
||||
},
|
||||
// 销毁 TUICallEngine
|
||||
destroyed() {
|
||||
if (wx.$TUICallEngine) {
|
||||
this._removeTSignalingEvent();
|
||||
}
|
||||
if (!wx.$globalCallSign) {
|
||||
wx.$TUICallEngine.destroyInstance();
|
||||
wx.$TUICallEngine = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
|
||||
},
|
||||
attached() {
|
||||
|
||||
},
|
||||
ready() {
|
||||
this.reset();
|
||||
if (wx.$globalCallSign) {
|
||||
wx.$CallManagerInstance.removeEngineInvite();
|
||||
}
|
||||
},
|
||||
detached() {
|
||||
this.destroyed();
|
||||
this.reset();
|
||||
if (wx.$globalCallSign) {
|
||||
wx.$CallManagerInstance.addEngineInvite();
|
||||
}
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
pageLifetimes: {
|
||||
show() {
|
||||
|
||||
},
|
||||
hide() {
|
||||
},
|
||||
resize() {
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"TUI-Calling": "./component/calling/calling",
|
||||
"TUI-Connected": "./component/connected/connected",
|
||||
"TUI-groupCalling": "./component/groupCalling/groupCalling",
|
||||
"TUI-groupConnected": "./component/groupConnected/groupConnected"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<view class="TUICalling {{callStatus === 'idle' ? 'hidden': 'show'}}">
|
||||
<view class="TRTCCaling-container {{isGroup && config.type === 2 ?'groupConnected':''}}">
|
||||
<TUI-Calling
|
||||
wx:if="{{callStatus === 'calling' && !isGroup}}"
|
||||
isSponsor="{{isSponsor}}"
|
||||
pusher="{{pusher}}"
|
||||
isGroup="{{isGroup}}"
|
||||
callType="{{config.type}}"
|
||||
remoteUsers="{{remoteUsers}}"
|
||||
bind:callingEvent="handleCallingEvent"
|
||||
></TUI-Calling>
|
||||
|
||||
<TUI-groupCalling
|
||||
wx:if="{{callStatus === 'calling' && isGroup}}"
|
||||
isSponsor="{{isSponsor}}"
|
||||
pusher="{{pusher}}"
|
||||
isGroup="{{isGroup}}"
|
||||
callType="{{config.type}}"
|
||||
allUsers="{{allUsers}}"
|
||||
ownUserId="{{ownUserId}}"
|
||||
bind:callingEvent="handleCallingEvent"
|
||||
></TUI-groupCalling>
|
||||
|
||||
|
||||
<TUI-Connected
|
||||
wx:if="{{callStatus === 'connected' && !isGroup}}"
|
||||
playerList="{{playerList}}"
|
||||
isGroup="{{isGroup}}"
|
||||
userList="{{userList}}"
|
||||
pusher="{{pusher}}"
|
||||
callType="{{config.type}}"
|
||||
soundMode="{{soundMode}}"
|
||||
screen="{{screen}}"
|
||||
bind:connectedEvent="handleConnectedEvent"
|
||||
></TUI-Connected>
|
||||
|
||||
<TUI-groupConnected
|
||||
wx:if="{{callStatus === 'connected' && isGroup}}"
|
||||
allUsers="{{allUsers}}"
|
||||
playerList="{{playerList}}"
|
||||
userList="{{userList}}"
|
||||
isGroup="{{isGroup}}"
|
||||
pusher="{{pusher}}"
|
||||
callType="{{config.type}}"
|
||||
soundMode="{{soundMode}}"
|
||||
screen="{{screen}}"
|
||||
ownUserId="{{ownUserId}}"
|
||||
playerProcess="{{ playerProcess}}"
|
||||
bind:connectedEvent="handleConnectedEvent"
|
||||
></TUI-groupConnected>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,25 @@
|
||||
.TRTCCaling-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
/* background-image: url(https://mc.qcloudimg.com/static/img/7da57e0050d308e2e1b1e31afbc42929/bg.png); */
|
||||
margin: 0;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.TUICalling {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 10;
|
||||
background: #ffffff;
|
||||
}
|
||||
.show {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.groupConnected{
|
||||
background-color: #2c292923;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// components/tui-calling/TUICalling/component/calling.js
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
isSponsor: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
remoteUsers: {
|
||||
type: Array,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
isClick: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
|
||||
},
|
||||
attached() {
|
||||
},
|
||||
ready() {
|
||||
},
|
||||
detached() {
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
async handleCheckAuthor(e) {
|
||||
const type =this.data.callType;
|
||||
wx.getSetting({
|
||||
success: async (res) => {
|
||||
const isRecord = res.authSetting['scope.record'];
|
||||
const isCamera = res.authSetting['scope.camera'];
|
||||
if (!isRecord && type === 1) {
|
||||
const title = '麦克风权限授权';
|
||||
const content = '使用语音通话,需要在设置中对麦克风进行授权允许';
|
||||
try {
|
||||
await wx.authorize({ scope: 'scope.record' });
|
||||
this.accept(e);
|
||||
} catch (e) {
|
||||
this.handleShowModal(title, content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((!isRecord || !isCamera) && type === 2) {
|
||||
const title = '麦克风、摄像头权限授权';
|
||||
const content = '使用视频通话,需要在设置中对麦克风、摄像头进行授权允许';
|
||||
try {
|
||||
await wx.authorize({ scope: 'scope.record' });
|
||||
await wx.authorize({ scope: 'scope.camera' });
|
||||
this.accept(e);
|
||||
} catch (e) {
|
||||
this.handleShowModal(title, content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.accept(e);
|
||||
},
|
||||
});
|
||||
},
|
||||
handleShowModal(title, content) {
|
||||
wx.showModal({
|
||||
title,
|
||||
content,
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
accept(event) {
|
||||
this.setData({
|
||||
isClick: false,
|
||||
});
|
||||
const data = {
|
||||
name: 'accept',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
reject(event) {
|
||||
const data = {
|
||||
name: 'reject',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
handleErrorImage(e) {
|
||||
const { id } = e.target;
|
||||
const remoteUsers = this.data.remoteUsers.map((item) => {
|
||||
if (item.userID === id) {
|
||||
item.avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setData({
|
||||
remoteUsers,
|
||||
});
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<view class="invite-call" wx:if="{{callType === 2}}">
|
||||
<live-pusher class="local-video" wx:if="{{remoteUsers.length === 1}}" device-position="{{pusher.frontCamera}}" />
|
||||
<view class="invite-calling">
|
||||
<view class="invite-calling-header" wx:if="{{remoteUsers.length === 1}}">
|
||||
<!-- <view class="invite-calling-header-left">
|
||||
<image src="../../static/swtich-camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera" />
|
||||
</view> -->
|
||||
<view class="invite-calling-header-right">
|
||||
<view class="invite-calling-header-message">
|
||||
<label class="tips">{{remoteUsers[0].nick || remoteUsers[0].userID}}</label>
|
||||
<text class="tips-subtitle" wx:if="{{!isSponsor}}">邀请你视频通话</text>
|
||||
<text class="tips-subtitle" wx:else>等待对方接受</text>
|
||||
</view>
|
||||
<image class="avatar" src="{{remoteUsers[0].avatar || '../../static/default_avatar.png'}}"
|
||||
id="{{remoteUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="invite-calling-header invite-calling-list" wx:else>
|
||||
<view class="invite-calling-item" wx:for="{{remoteUsers}}" wx:key="userID">
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" id="{{item.userID}}"
|
||||
binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-message">
|
||||
<label class="tips">{{item.nick || item.userID}}</label>
|
||||
<text class="tips-subtitle" wx:if="{{!isSponsor}}">邀请你视频通话</text>
|
||||
<text class="tips-subtitle" wx:else>等待对方接受</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item call-switch" catch:tap="switchAudioCall">
|
||||
<view class="call-operate">
|
||||
<image src="../../static/trans.png" />
|
||||
</view>
|
||||
<text>切到语音通话</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="btn-container">
|
||||
<view class="call-operate" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera" />
|
||||
</view>
|
||||
</view>
|
||||
<text>挂断</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="btn-operate" wx:if="{{!isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<text>挂断</text>
|
||||
</view>
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
|
||||
<image src="../../static/dialing.png" />
|
||||
</view>
|
||||
<text>接听</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="incoming-call audio-call" wx:if="{{callType === 1}}">
|
||||
<view class="invite-calling-single">
|
||||
<image class="avatar" src="{{remoteUsers[0].avatar || '../../static/default_avatar.png'}}"
|
||||
id="{{remoteUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
<view class="tips">{{remoteUsers[0].nick || remoteUsers[0].userID}}</view>
|
||||
<view wx:if="{{isSponsor && callType === 1}}" class="tips-subtitle">{{'等待对方接受'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view wx:if="{{!isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="call-operate" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
|
||||
<image src="../../static/dialing.png"/>
|
||||
</view>
|
||||
<view style="margin-top:10px">接听</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px">挂断</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,242 @@
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-operate {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* flex-direction: column;
|
||||
text-align: center; */
|
||||
|
||||
}
|
||||
.btn-operate-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn-operate-item text {
|
||||
font-size: 14px;
|
||||
color: #f0e9e9;
|
||||
padding: 5px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.call-switch text{
|
||||
padding: 5px;
|
||||
color: #f0e9e9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.call-operate {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
border-radius: 8vh;
|
||||
margin: 0 15vw;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
margin: 0 auto;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.40); */
|
||||
font-weight: 600;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tips-subtitle {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
text-align: right;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.30); */
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.invite-call {
|
||||
/* background: #ffffff; */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.invite-call .local-video {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.invite-call .invite-calling {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.invite-calling-header {
|
||||
margin-top:107px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 16px;
|
||||
|
||||
}
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.invite-calling-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.invite-calling-header-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.invite-calling-header-right image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.invite-calling .footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
}
|
||||
.invite-calling .btn-operate{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.trtc-calling {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.audio-call {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
.audio-call > .btn-operate{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.audio-call > image {
|
||||
width: 40vw;
|
||||
height: 40vw;
|
||||
display: block;
|
||||
margin: 20vw 30vw;
|
||||
margin-top: 40vw;
|
||||
}
|
||||
|
||||
.invite-calling-single > image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
margin: 140px auto 15px;
|
||||
/* margin: 20vw 30vw; */
|
||||
}
|
||||
|
||||
.invite-calling-single .tips {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.invite-calling-single .tips-subtitle {
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #97989C;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invite-calling-list {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.invite-calling-item {
|
||||
position: relative;
|
||||
margin: 0 12px;
|
||||
}
|
||||
.invite-calling-item image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.invite-calling-item-message {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #dddddd;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
playerList: {
|
||||
type: Array,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
soundMode: {
|
||||
type: String,
|
||||
},
|
||||
screen: {
|
||||
type: String,
|
||||
},
|
||||
userList: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
toggleViewSize(event) {
|
||||
const data = {
|
||||
name: 'toggleViewSize',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherNetStatus(event) {
|
||||
const data = {
|
||||
name: 'pusherNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playNetStatus(event) {
|
||||
const data = {
|
||||
name: 'playNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherStateChangeHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherStateChangeHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerStateChange(event) {
|
||||
const data = {
|
||||
name: 'playerStateChange',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'playerAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSoundMode(event) {
|
||||
const data = {
|
||||
name: 'toggleSoundMode',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherVideoHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherVideoHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
handleConnectErrorImage(e) {
|
||||
const { flag, key, index } = e.target.dataset;
|
||||
if (flag === 'pusher') {
|
||||
this.data.pusher.avatar = '../../static/default_avatar.png';
|
||||
this.setData({
|
||||
pusher: this.data.pusher,
|
||||
});
|
||||
} else {
|
||||
this.data[key][index].avatar = '../../static/default_avatar.png';
|
||||
if (this.data.playerList[index]) {
|
||||
this.data.playerList[index].avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
this.setData({
|
||||
playerList: this.data.playerList,
|
||||
[key]: this.data[key],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<view class="TUICalling-connected-layout {{callType === 1 ? 'audio' : 'video'}}">
|
||||
<view
|
||||
class="{{callType === 1 ? 'pusher-audio' : playerList.length > 1 ? 'stream-box' : (screen === 'pusher' ? 'pusher-video' : 'player')}}"
|
||||
data-screen="pusher" catch:tap="toggleViewSize">
|
||||
<live-pusher class="{{callType === 1 ? 'pusher-audio' : 'live'}}" url="{{pusher.url}}" mode="{{pusher.mode}}"
|
||||
autopush="{{true}}" enable-camera="{{pusher.enableCamera}}" enable-mic="{{true}}"
|
||||
muted="{{!pusher.enableMic}}" enable-agc="{{true}}" enable-ans="{{true}}"
|
||||
enable-ear-monitor="{{pusher.enableEarMonitor}}" auto-focus="{{pusher.enableAutoFocus}}"
|
||||
zoom="{{pusher.enableZoom}}" min-bitrate="{{pusher.minBitrate}}" max-bitrate="{{pusher.maxBitrate}}"
|
||||
video-width="{{pusher.videoWidth}}" video-height="{{pusher.videoHeight}}" beauty="{{pusher.beautyLevel}}"
|
||||
whiteness="{{pusher.whitenessLevel}}" orientation="{{pusher.videoOrientation}}"
|
||||
aspect="{{pusher.videoAspect}}" device-position="{{pusher.frontCamera}}"
|
||||
remote-mirror="{{pusher.enableRemoteMirror}}" local-mirror="{{pusher.localMirror}}"
|
||||
background-mute="{{pusher.enableBackgroundMute}}" audio-quality="{{pusher.audioQuality}}"
|
||||
audio-volume-type="{{pusher.audioVolumeType}}" audio-reverb-type="{{pusher.audioReverbType}}"
|
||||
waiting-image="{{pusher.waitingImage}}" beauty-style="{{pusher.beautyStyle}}" filter="{{pusher.filter}}"
|
||||
bindstatechange="pusherStateChangeHandler" bindnetstatus="pusherNetStatus" binderror="pusherErrorHandler"
|
||||
bindaudiovolumenotify="pusherAudioVolumeNotify" />
|
||||
</view>
|
||||
<view wx:if="{{callType === 1}}" class="TRTCCalling-call-audio-box {{playerList.length > 1 && 'mutil-img'}}">
|
||||
<view class="TRTCCalling-call-audio-img" wx:if="{{playerList.length > 1}}">
|
||||
<image src="{{pusher.avatar || '../../static/default_avatar.png'}}" class="img-place-holder avatar"
|
||||
data-value="{{pusher.userID}}" data-flag="pusher" binderror="handleConnectErrorImage" />
|
||||
<text class="audio-name">{{pusher.nick || pusher.userID}}(自己)</text>
|
||||
</view>
|
||||
<view class="TRTCCalling-call-audio-img" wx:for="{{userList || playerList}}" wx:key="userID">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}" class="img-place-holder avatar"
|
||||
data-value="{{item}}" data-flag="player" data-key="userList" data-index="{{index}}" binderror="handleConnectErrorImage" />
|
||||
<text class="audio-name">{{item.nick || item.userID}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:for="{{playerList}}" wx:key="streamID"
|
||||
class="view-container player-container {{callType === 1 ? 'player-audio' : ''}}">
|
||||
<view
|
||||
class="{{callType === 1 ? 'player-audio' : playerList.length > 1 ? 'stream-box' : (screen === 'player' ? 'pusher-video' : 'player')}}"
|
||||
data-screen="player" catch:tap="toggleViewSize">
|
||||
<live-player class="live" wx:if="{{ item.hasAudio || item.hasVideo }}" id="{{item.id}}" data-userid="{{item.userID}}" data-streamid="{{item.streamID}}"
|
||||
data-streamtype="{{item.streamType}}" src="{{item.src}}" mode="RTC" autoplay="{{item.autoplay}}"
|
||||
mute-audio="{{item.muteAudio}}" mute-video="{{item.muteVideo}}" orientation="{{item.orientation}}"
|
||||
object-fit="{{item.objectFit}}" background-mute="{{item.enableBackgroundMute}}"
|
||||
min-cache="{{item.minCache}}" max-cache="{{item.maxCache}}" sound-mode="{{soundMode}}"
|
||||
enable-recv-message="{{item.enableRecvMessage}}" auto-pause-if-navigate="{{item.autoPauseIfNavigate}}"
|
||||
auto-pause-if-open-native="{{item.autoPauseIfOpenNative}}" bindstatechange="playerStateChange"
|
||||
bindfullscreenchange="playerFullscreenChange" bindnetstatus="playNetStatus"
|
||||
bindaudiovolumenotify="playerAudioVolumeNotify" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="handle-btns">
|
||||
<view class="other-view {{callType === 1 ? 'black' : 'white'}}">
|
||||
<text>{{pusher.chatTime}}</text>
|
||||
</view>
|
||||
<view class="btn-operate-item call-switch" catch:tap="switchAudioCall">
|
||||
<view class="call-operate">
|
||||
<image src="../../static/trans.png" />
|
||||
</view>
|
||||
<text>切到语音通话</text>
|
||||
</view>
|
||||
<view class="btn-list">
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="pusherAudioHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableMic? '../../static/audio-true.png': '../../static/audio-false.png'}} ">
|
||||
</image>
|
||||
</view>
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">麦克风</view>
|
||||
</view>
|
||||
<view class="button-container" wx:if="{{callType === 1}}">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="toggleSoundMode">
|
||||
<image class="btn-image"
|
||||
src="{{soundMode === 'ear' ? '../../static/speaker-false.png': '../../static/speaker-true.png'}} ">
|
||||
</image>
|
||||
</view>
|
||||
<text class="{{callType === 2 ? 'white' : ''}}">扬声器</text>
|
||||
</view>
|
||||
|
||||
<view class="button-container" wx:if="{{callType === 2}}">
|
||||
<view class="btn-normal" bindtap="pusherVideoHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableCamera ? '../../static/camera-true.png': '../../static/camera-false.png'}} ">
|
||||
</image>
|
||||
</view>
|
||||
<text class="white">摄像头</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-list" wx:if="{{callType===2}}">
|
||||
<!-- <view class="btn-list-item">
|
||||
<view wx:if="{{playerList.length === 1}}" class="btn-normal" bindtap="switchAudioCall">
|
||||
<image class="btn-image btn-image-small" src="{{ '../../static/trans.png'}} "></image>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="btn-list-item other-view">
|
||||
<view class="btn-container">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
<view wx:if="{{pusher.enableCamera}}" class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="white">挂断</text>
|
||||
</view>
|
||||
<!-- <view class="btn-list-item btn-footer">
|
||||
<view wx:if="{{pusher.enableCamera}}" class="{{playerList.length > 1 ? 'multi-camera' : 'camera'}}">
|
||||
<image class="camera-image" src="../../static/swtich-camera.png"
|
||||
data-device="{{pusher.frontCamera}}" catch:tap="toggleSwitchCamera" />
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,253 @@
|
||||
.player {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 107px;
|
||||
width: 100px;
|
||||
height: 178px;
|
||||
padding: 16px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.pusher-video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background-color: #f75c45; */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stream-box {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 50vw;
|
||||
height: 260px;
|
||||
/* background-color: #f75c45; */
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.handle-btns {
|
||||
position: absolute;
|
||||
bottom: 44px;
|
||||
width: 100vw;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handle-btns .btn-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-normal {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* background: white; */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.btn-hangup {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
/*background: #f75c45;*/
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-hangup > .btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-footer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-footer .multi-camera {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-footer .camera {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 107px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.btn-footer .camera .camera-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.TUICalling-connected-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.audio {
|
||||
padding-top: 15vh;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.pusher-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.player-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.live {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.other-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: #f0e9e9;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000000;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mutil-img {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img > image {
|
||||
width: 25vw;
|
||||
height: 25vw;
|
||||
margin: 0 4vw;
|
||||
border-radius: 4vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img text {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-list-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.btn-image-small {
|
||||
transform: scale(.7);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #dddddd;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: -88px;
|
||||
}
|
||||
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.call-switch text {
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-operate-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-operate-item text {
|
||||
padding: 8px 0;
|
||||
font-size: 18px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// components/tui-calling/TUICalling/component/calling.js
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
isSponsor: {
|
||||
type: Boolean,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
allUsers: {
|
||||
type: Array,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
},
|
||||
ownUserId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
userID: null,
|
||||
isClick: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
},
|
||||
attached() {
|
||||
},
|
||||
ready() {
|
||||
|
||||
},
|
||||
detached() {
|
||||
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
async handleCheckAuthor(e) {
|
||||
const type =this.data.callType;
|
||||
wx.getSetting({
|
||||
success: async (res) => {
|
||||
const isRecord = res.authSetting['scope.record'];
|
||||
const isCamera = res.authSetting['scope.camera'];
|
||||
if (!isRecord && type === 1) {
|
||||
const title = '麦克风权限授权';
|
||||
const content = '使用语音通话,需要在设置中对麦克风进行授权允许';
|
||||
try {
|
||||
await wx.authorize({ scope: 'scope.record' });
|
||||
this.accept(e);
|
||||
} catch (e) {
|
||||
this.handleShowModal(title, content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((!isRecord || !isCamera) && type === 2) {
|
||||
const title = '麦克风、摄像头权限授权';
|
||||
const content = '使用视频通话,需要在设置中对麦克风、摄像头进行授权允许';
|
||||
try {
|
||||
await wx.authorize({ scope: 'scope.record' });
|
||||
await wx.authorize({ scope: 'scope.camera' });
|
||||
this.accept(e);
|
||||
} catch (e) {
|
||||
this.handleShowModal(title, content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.accept(e);
|
||||
},
|
||||
});
|
||||
},
|
||||
handleShowModal(title, content) {
|
||||
wx.showModal({
|
||||
title,
|
||||
content,
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
accept(event) {
|
||||
this.setData({
|
||||
isClick: false,
|
||||
});
|
||||
const data = {
|
||||
name: 'accept',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
reject(event) {
|
||||
const data = {
|
||||
name: 'reject',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
handleErrorImage(e) {
|
||||
const { id } = e.target;
|
||||
const allUsers = this.data.allUsers.map((item) => {
|
||||
if (item.userID === id) {
|
||||
item.avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setData({
|
||||
allUsers,
|
||||
});
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<view class="trtc-calling-index">
|
||||
<!-- 视频通话 -->
|
||||
<view class="invite-call" wx:if="{{callType === 2}}">
|
||||
<live-pusher class="local-video" wx:if="{{isGroup===false}}" device-position="{{pusher.frontCamera}}" />
|
||||
<view class="invite-calling">
|
||||
<!-- 主叫方 -->
|
||||
<swiper class="swiper" wx:if="{{isSponsor}}" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white" indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item" wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{item.userID !== ownUserId}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
<!-- 被叫方 -->
|
||||
<view wx:else>
|
||||
<view class="invite-calling-single">
|
||||
<image class="avatar" src="{{allUsers[0].avatar || '../../static/default_avatar.png'}}" id="{{allUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
<view class="tips">{{allUsers[0].nick || allUsers[0].userID }}</view>
|
||||
<view class="invite-txt">邀请你参加多人通话</view>
|
||||
</view>
|
||||
<view class="invite-other-txt">参与通话的还有:</view>
|
||||
<view class="invite-other-list">
|
||||
<view class="invite-other-item" wx:if="{{index>0}}" wx:for="{{allUsers}}" wx:key="item">
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-other-item-name">{{ item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<!-- <view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item call-switch" catch:tap="switchAudioCall">
|
||||
<view class="call-operate">
|
||||
<image src="../../static/trans.png" />
|
||||
</view>
|
||||
<text>切到语音通话</text>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="btn-container">
|
||||
<view class="call-operate" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<!-- <view class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}" catch:tap="toggleSwitchCamera" />
|
||||
</view> -->
|
||||
</view>
|
||||
<text style="color: #666666">挂断</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-operate" wx:if="{{!isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<text style="color: #666666">挂断</text>
|
||||
</view>
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
|
||||
<image src="../../static/dialing.png" />
|
||||
</view>
|
||||
<text style="color: #666666">接听</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 语音通话 -->
|
||||
<view class="incoming-call audio-call" wx:if="{{callType === 1}}">
|
||||
<!-- 主叫方 -->
|
||||
<swiper class="swiper" wx:if="{{isSponsor}}" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white" indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item" wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{item.userID !== ownUserId}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
<!-- 被叫方 -->
|
||||
<view wx:else>
|
||||
<view class="invite-calling-single">
|
||||
<image class="avatar" src="{{allUsers[0].avatar || '../../static/default_avatar.png'}}" id="{{allUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
<view class="tips">{{allUsers[0].nick || allUsers[0].userID }}</view>
|
||||
<view class="invite-txt">邀请你参加多人通话</view>
|
||||
</view>
|
||||
<view class="invite-other-txt">参与通话的还有:</view>
|
||||
<view class="invite-other-list">
|
||||
<view class="invite-other-item" wx:if="{{index>0}}" wx:for="{{allUsers}}" wx:key="item">
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-other-item-name">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 菜单 -->
|
||||
<view class="footer">
|
||||
<view wx:if="{{!isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px;color: #666666">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="call-operate" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
|
||||
<image src="../../static/dialing.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px;color: #666666">接听</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px;color: #666666">挂断</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,366 @@
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-operate {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* flex-direction: column;
|
||||
text-align: center; */
|
||||
|
||||
}
|
||||
|
||||
.btn-operate-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-operate-item text {
|
||||
font-size: 14px;
|
||||
color: #f0e9e9;
|
||||
padding: 5px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.call-switch text {
|
||||
padding: 5px;
|
||||
color: #f0e9e9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.call-operate {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
border-radius: 8vh;
|
||||
margin: 0 15vw;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
margin: 0 auto;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.40); */
|
||||
font-weight: 600;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tips-subtitle {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
text-align: right;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.30); */
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.invite-call {
|
||||
/* background: #ffffff; */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100px;
|
||||
height: 187px;
|
||||
}
|
||||
|
||||
.invite-call .local-video {
|
||||
width: 100px;
|
||||
height: 187px;
|
||||
}
|
||||
|
||||
.invite-call .invite-calling {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.invite-calling-header {
|
||||
margin-top: 107px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.invite-calling-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.invite-calling-header-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.invite-calling-header-right image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.invite-calling .footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invite-calling .btn-operate {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.trtc-calling {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.audio-call {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.audio-call>.btn-operate {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.audio-call>image {
|
||||
width: 40vw;
|
||||
height: 40vw;
|
||||
display: block;
|
||||
margin: 20vw 30vw;
|
||||
margin-top: 40vw;
|
||||
}
|
||||
|
||||
.invite-calling-single>image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
margin: 120px auto 15px;
|
||||
/* margin: 20vw 30vw; */
|
||||
}
|
||||
|
||||
.invite-calling-single .tips {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.invite-calling-single .tips-subtitle {
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #97989C;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
margin-top: 107px;
|
||||
min-height: 374px;
|
||||
}
|
||||
|
||||
.invite-calling-list {
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
.invite-calling-item {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-item image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.invite-calling-item-message {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
float: left;
|
||||
background: rgba(0, 0, 0, 0.60);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg {
|
||||
position: absolute;
|
||||
left: calc(50% - 20px);
|
||||
top: calc(50% - 20px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
-webkit-transform: rotate(360deg);
|
||||
animation: rotation 2s linear infinite;
|
||||
-moz-animation: rotation 2s linear infinite;
|
||||
-webkit-animation: rotation 2s linear infinite;
|
||||
-o-animation: rotation 2s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.invite-calling-item-id {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
bottom: 2%;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
|
||||
/* 被叫者 */
|
||||
.invite-txt {
|
||||
width: 126px;
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
margin: 16px auto 60px auto;
|
||||
}
|
||||
|
||||
.invite-other-txt {
|
||||
width: 112px;
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
margin: 0 auto 24px auto;
|
||||
}
|
||||
|
||||
.invite-other-list {
|
||||
position: absolute;
|
||||
left: 14vw;
|
||||
margin-top: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 272px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.invite-other-item {
|
||||
flex: 0.25;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
text-align: center;
|
||||
max-width: 64px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.invite-other-item image {
|
||||
border-radius: 10%;
|
||||
max-width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.invite-other-item-name {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
letter-spacing: 0;
|
||||
line-height: 18px;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
playerList: {
|
||||
type: Array,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
soundMode: {
|
||||
type: String,
|
||||
},
|
||||
screen: {
|
||||
type: String,
|
||||
},
|
||||
userList: {
|
||||
type: Object,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
},
|
||||
allUsers: {
|
||||
type: Array,
|
||||
},
|
||||
playerProcess: {
|
||||
type: Object,
|
||||
},
|
||||
ownUserId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
userID: null,
|
||||
},
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
},
|
||||
attached() {
|
||||
},
|
||||
ready() {
|
||||
},
|
||||
detached() {
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
toggleViewSize(event) {
|
||||
const data = {
|
||||
name: 'toggleViewSize',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherNetStatus(event) {
|
||||
const data = {
|
||||
name: 'pusherNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playNetStatus(event) {
|
||||
const data = {
|
||||
name: 'playNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherStateChangeHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherStateChangeHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerStateChange(event) {
|
||||
const data = {
|
||||
name: 'playerStateChange',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'playerAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSoundMode(event) {
|
||||
const data = {
|
||||
name: 'toggleSoundMode',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherVideoHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherVideoHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
handleConnectErrorImage(e) {
|
||||
const { flag, key, index } = e.target.dataset;
|
||||
if (flag === 'pusher') {
|
||||
this.data.pusher.avatar = '../../static/default_avatar.png';
|
||||
this.setData({
|
||||
pusher: this.data.pusher,
|
||||
});
|
||||
} else {
|
||||
this.data[key][index].avatar = '../../static/default_avatar.png';
|
||||
if (this.data.playerList[index]) {
|
||||
this.data.playerList[index].avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
this.setData({
|
||||
playerList: this.data.playerList,
|
||||
[key]: this.data[key],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {},
|
||||
"window": {
|
||||
"backgroundColor": "#2c292923;"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<view class="{{callType === 2?'TUICalling-connected-video':'TUICalling-connected-layout'}}">
|
||||
<!-- 语音通话 -->
|
||||
<view wx:if="{{callType === 1}}">
|
||||
<swiper class="swiper" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white"
|
||||
indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item"
|
||||
wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{!item.isEnter}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}"
|
||||
binderror="handleErrorImage"/>
|
||||
<!-- 音量图标 -->
|
||||
<view class="player-control">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}"></image>
|
||||
<view class="name">{{item.userID===ownUserId?'我':item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
</view>
|
||||
<view class="{{callType === 1 ? 'pusher-audio' : ''}}">
|
||||
<swiper class="swiper" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white"
|
||||
indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item"
|
||||
wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{!item.isEnter}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<view wx:else>
|
||||
<!-- 本地流 -->
|
||||
<view wx:if="{{item.userID===ownUserId}}"
|
||||
class="{{callType === 1 ? 'pusher-audio' : 'play-item'}}" data-screen="pusher"
|
||||
catch:tap="toggleViewSize">
|
||||
<live-pusher class="{{callType === 1 ? 'pusher-audio' : 'pusher-ownvideo'}}"
|
||||
url="{{pusher.url}}" mode="{{pusher.mode}}" autopush="{{true}}"
|
||||
enable-camera="{{pusher.enableCamera}}" enable-mic="{{true}}"
|
||||
muted="{{!pusher.enableMic}}" enable-agc="{{true}}" enable-ans="{{true}}"
|
||||
enable-ear-monitor="{{pusher.enableEarMonitor}}"
|
||||
auto-focus="{{pusher.enableAutoFocus}}" zoom="{{pusher.enableZoom}}"
|
||||
min-bitrate="{{pusher.minBitrate}}" max-bitrate="{{pusher.maxBitrate}}"
|
||||
video-width="{{pusher.videoWidth}}" video-height="{{pusher.videoHeight}}"
|
||||
beauty="{{pusher.beautyLevel}}" whiteness="{{pusher.whitenessLevel}}"
|
||||
orientation="{{pusher.videoOrientation}}" aspect="{{pusher.videoAspect}}"
|
||||
device-position="{{pusher.frontCamera}}"
|
||||
remote-mirror="{{pusher.enableRemoteMirror}}"
|
||||
local-mirror="{{pusher.localMirror}}"
|
||||
background-mute="{{pusher.enableBackgroundMute}}"
|
||||
audio-quality="{{pusher.audioQuality}}"
|
||||
audio-volume-type="{{pusher.audioVolumeType}}"
|
||||
audio-reverb-type="{{pusher.audioReverbType}}"
|
||||
waiting-image="{{pusher.waitingImage}}"
|
||||
beauty-style="{{pusher.beautyStyle}}" filter="{{pusher.filter}}"
|
||||
bindstatechange="pusherStateChangeHandler" bindnetstatus="pusherNetStatus"
|
||||
binderror="pusherErrorHandler"
|
||||
bindaudiovolumenotify="pusherAudioVolumeNotify"/>
|
||||
<view class="player-control">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}"></image>
|
||||
<view class="name">我</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 远端流 -->
|
||||
<view catch:tap="toggleViewSize" class="{{callType === 1 ? 'pusher-audio' : 'play-item'}}"
|
||||
wx:else>
|
||||
<live-player
|
||||
wx:if="{{playerProcess[item.userID]}}"
|
||||
wx:if="{{ playerProcess[item.userID].hasAudio || playerProcess[item.userID].hasVideo }}"
|
||||
class="{{callType === 1 ? 'pusher-audio' : 'pusher-ownvideo'}}"
|
||||
id="{{playerProcess[item.userID].id}}"
|
||||
data-userid="{{playerProcess[item.userID].userID}}"
|
||||
data-streamid="{{playerProcess[item.userID].streamID}}"
|
||||
data-streamtype="{{playerProcess[item.userID].streamType}}"
|
||||
src="{{playerProcess[item.userID].src}}" mode="RTC"
|
||||
autoplay="{{playerProcess[item.userID].autoplay}}"
|
||||
mute-audio="{{playerProcess[item.userID].muteAudio}}"
|
||||
mute-video="{{playerProcess[item.userID].muteVideo}}"
|
||||
orientation="{{playerProcess[item.userID].orientation}}"
|
||||
object-fit="{{playerProcess[item.userID].objectFit}}"
|
||||
background-mute="{{playerProcess[item.userID].enableBackgroundMute}}"
|
||||
min-cache="{{playerProcess[item.userID].minCache}}"
|
||||
max-cache="{{playerProcess[item.userID].maxCache}}"
|
||||
sound-mode="{{soundMode}}"
|
||||
enable-recv-message="{{playerProcess[item.userID].enableRecvMessage}}"
|
||||
auto-pause-if-navigate="{{playerProcess[item.userID].autoPauseIfNavigate}}"
|
||||
auto-pause-if-open-native="{{playerProcess[item.userID].autoPauseIfOpenNative}}"
|
||||
bindstatechange="playerStateChange"
|
||||
bindfullscreenchange="playerFullscreenChange" bindnetstatus="playNetStatus"
|
||||
bindaudiovolumenotify="playerAudioVolumeNotify"/>
|
||||
<!-- 音量图标 -->
|
||||
<view class="player-control">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}"></image>
|
||||
<view class="name">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
</view>
|
||||
<!-- 菜单 -->
|
||||
<view class="handle-btns">
|
||||
<view class="other-view {{callType === 1 ? 'black' : 'white'}}">
|
||||
<text>{{pusher.chatTime}}</text>
|
||||
</view>
|
||||
<view class="btn-list">
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="pusherAudioHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableMic? '../../static/audio-true.png': '../../static/audio-false.png'}} "></image>
|
||||
</view>
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">麦克风</view>
|
||||
</view>
|
||||
<view class="button-container" wx:if="{{callType === 1}}">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="toggleSoundMode">
|
||||
<image class="btn-image"
|
||||
src="{{soundMode === 'ear' ? '../../static/speaker-false.png': '../../static/speaker-true.png'}} "></image>
|
||||
</view>
|
||||
<text class="{{callType === 2 ? 'white' : ''}}">扬声器</text>
|
||||
</view>
|
||||
<view class="button-container" wx:if="{{callType === 2}}">
|
||||
<view class="btn-normal" bindtap="pusherVideoHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableCamera ? '../../static/camera-true.png': '../../static/camera-false.png'}} "></image>
|
||||
</view>
|
||||
<text class="white">摄像头</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-list" wx:if="{{callType===2}}">
|
||||
<view class="btn-list-item other-view">
|
||||
<view class="btn-container">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
<view wx:if="{{pusher.enableCamera}}" class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera"/>
|
||||
</view>
|
||||
</view>
|
||||
<text class="white">挂断</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,414 @@
|
||||
/* 全屏设置 */
|
||||
.TUICalling-connected-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.TUICalling-connected-video {
|
||||
width: 100%;
|
||||
height: 180%;
|
||||
}
|
||||
|
||||
/* 本地音频 */
|
||||
.stream-box {
|
||||
float: left;
|
||||
width: 187px;
|
||||
height: 187px;
|
||||
position: absolute;
|
||||
top: 13vh;
|
||||
}
|
||||
|
||||
/* 远端音频列表 */
|
||||
.swiper {
|
||||
margin-top: 107px;
|
||||
min-height: 374px;
|
||||
}
|
||||
|
||||
.invite-calling-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
.invite-calling-item {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-item image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 本地视频 */
|
||||
.play-item {
|
||||
width: 100%;
|
||||
height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pusher-ownvideo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* 远端视频列表 */
|
||||
.swiper-list {
|
||||
min-height: 189px;
|
||||
}
|
||||
|
||||
.player-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.player-item {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
min-height: 187px;
|
||||
}
|
||||
|
||||
/* 音量图标 */
|
||||
.player-control {
|
||||
background-color: rgba(0, 0, 0, .4);
|
||||
border-radius: 0 6px 6px 0;
|
||||
color: #fff;
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
max-width: 50%;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.player-control image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.player-control .name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
flex: 1;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 菜单 */
|
||||
.handle-btns {
|
||||
position: absolute;
|
||||
bottom: 44px;
|
||||
width: 100vw;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handle-btns .btn-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-normal {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* background: white; */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.btn-hangup {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
/*background: #f75c45;*/
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-hangup>.btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-footer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-footer .multi-camera {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-footer .camera {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 107px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.btn-footer .camera .camera-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.audio {
|
||||
padding-top: 15vh;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.pusher-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.player-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.other-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.white {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000000;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-box {
|
||||
margin-top: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
/*
|
||||
.mutil-img {
|
||||
justify-content: flex-start !important;
|
||||
} */
|
||||
|
||||
/* .TRTCCalling-call-audio-img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img > image {
|
||||
width: 25vw;
|
||||
height: 25vw;
|
||||
margin: 0 4vw;
|
||||
border-radius: 4vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img text {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
} */
|
||||
|
||||
.TRTCCalling-calling-list {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
min-height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.TRTCCalling-calling-list image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.TRTCCalling-calling-item-id {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
bottom: 2%;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-list-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.btn-image-small {
|
||||
transform: scale(.7);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #dddddd;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: -88px;
|
||||
}
|
||||
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.call-switch text {
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-operate-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-operate-item text {
|
||||
padding: 8px 0;
|
||||
font-size: 18px;
|
||||
color: #666666;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.invite-calling-item-message {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
float: left;
|
||||
background: rgba(0, 0, 0, 0.60);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg {
|
||||
position: absolute;
|
||||
left: calc(50% - 20px);
|
||||
top: calc(50% - 20px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
-webkit-transform: rotate(360deg);
|
||||
animation: rotation 2s linear infinite;
|
||||
-moz-animation: rotation 2s linear infinite;
|
||||
-webkit-animation: rotation 2s linear infinite;
|
||||
-o-animation: rotation 2s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.invite-calling-item-id {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
bottom: 2%;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
Page({
|
||||
TUICallKit: null,
|
||||
data: {
|
||||
config: {
|
||||
},
|
||||
},
|
||||
async onLoad(option) {
|
||||
const config = JSON.parse(option.configData);
|
||||
this.setData(
|
||||
{
|
||||
config: { ...this.data.config, ...config },
|
||||
},
|
||||
async () => {
|
||||
this.TUICallKit = this.selectComponent('#TUICallKit-component');
|
||||
try {
|
||||
await this.TUICallKit.init(config);
|
||||
const event = JSON.parse(option.data);
|
||||
wx.$TUICallEngine.TRTCCallingDelegate.onInvited(event.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
this.TUICallKit.destroyed();
|
||||
},
|
||||
|
||||
onShow() {},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"TUICallKit": "../../TUICallKit"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<view class="">
|
||||
<TUICallKit
|
||||
id="TUICallKit-component"
|
||||
></TUICallKit>
|
||||
</view>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<tuicallkit ref="TUICallKit"></tuicallkit>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
onLoad(option) {
|
||||
const config = JSON.parse(option.configData);
|
||||
this.$nextTick(async () => {
|
||||
await this.$refs.TUICallKit.init(config);
|
||||
const event = JSON.parse(option.data);
|
||||
wx.$TUICallEngine.TRTCCallingDelegate.onInvited(event.data);
|
||||
})
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
onUnload() {
|
||||
this.$refs.TUICallKit.destroyed();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
|
||||
import TIM from 'tim-wx-sdk';
|
||||
import TUICallEngine, { EVENT } from 'tuicall-engine-wx';
|
||||
|
||||
/**
|
||||
* @param sdkAppID 用户的sdkAppID 必传
|
||||
* @param userID 用户的userID 必传
|
||||
* @param userSig 用户的userSig 必传
|
||||
* @param globalCallPagePath 跳转的路径 必传
|
||||
* @param tim tim实例 非必传
|
||||
*/
|
||||
export class CallManager {
|
||||
sdkAppID = 0
|
||||
userID = ''
|
||||
userSig = ''
|
||||
tim = null
|
||||
globalCallPagePath = []
|
||||
constructor() {
|
||||
}
|
||||
async init(params) {
|
||||
const { sdkAppID, SDKAppID, userID, tim, globalCallPagePath, userSig } = params;
|
||||
this.sdkAppID = sdkAppID || SDKAppID;
|
||||
this.userID = userID;
|
||||
this.userSig = userSig;
|
||||
this.globalCallPagePath = globalCallPagePath;
|
||||
this.tim = tim;
|
||||
// 挂载全局变量
|
||||
wx.$globalCallSign = true;
|
||||
// 设置标志位 用于移除监听
|
||||
wx.$CallManagerInstance = this;
|
||||
|
||||
if (!this.tim) {
|
||||
this.tim = TIM.create({
|
||||
SDKAppID: this.sdkAppID,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建 TUICallEngine 实例
|
||||
wx.$TUICallEngine = TUICallEngine.createInstance({
|
||||
sdkAppID: this.sdkAppID,
|
||||
tim: this.tim,
|
||||
});
|
||||
// 调用 init 方法
|
||||
await wx.$TUICallEngine.init({
|
||||
userID: this.userID,
|
||||
userSig: this.userSig,
|
||||
});
|
||||
// 监听 TUICallEngine 内部的 TSignaling 事件
|
||||
this.addEngineInvite();
|
||||
};
|
||||
|
||||
addEngineInvite() {
|
||||
wx.$TUICallEngine.on(EVENT.INVITED, this.handleNewInvitationReceived, this);
|
||||
};
|
||||
|
||||
addEngineCallEnd() {
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.on(EVENT.CALLING_CANCEL, this.handleCallEnd, this);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.on(EVENT.CALL_END, this.handleCallEnd, this);
|
||||
}
|
||||
|
||||
removeEngineInvite() {
|
||||
wx.$TUICallEngine.off(EVENT.INVITED, this.handleNewInvitationReceived, this);
|
||||
this.removeEngineCallEnd();
|
||||
}
|
||||
|
||||
removeEngineCallEnd() {
|
||||
// 若当前已在globalCall页面 则无需处理
|
||||
if (this.getRoute().route === this.globalCallPagePath) {
|
||||
return;
|
||||
}
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_CANCEL, this.handleCallEnd, this);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.off(EVENT.CALL_END, this.handleCallEnd, this);
|
||||
}
|
||||
|
||||
|
||||
handleNewInvitationReceived(event) {
|
||||
// 若当前已在globalCall页面 则无需处理
|
||||
if (this.getRoute().route === this.globalCallPagePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 监听 TUICallEngine 自身的通话结束事件
|
||||
this.addEngineCallEnd();
|
||||
const configData = {
|
||||
sdkAppID: this.sdkAppID,
|
||||
userID: this.userID,
|
||||
userSig: this.userSig,
|
||||
};
|
||||
wx.navigateTo({
|
||||
url: `/${this.globalCallPagePath}?data=${JSON.stringify(event)}&configData=${JSON.stringify(configData)}`,
|
||||
});
|
||||
};
|
||||
|
||||
handleCallEnd() {
|
||||
wx.$TUICallEngine._resetTUICallEngine();
|
||||
wx.navigateBack({
|
||||
success: () => {
|
||||
},
|
||||
fail: () => {
|
||||
},
|
||||
complete: () => {
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_CANCEL, this.handleCallEnd, this);
|
||||
wx.$TUICallEngine.off(EVENT.CALL_END, this.handleCallEnd, this);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前的页面地址
|
||||
getRoute() {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
// 卸载 callManger
|
||||
destroyed() {
|
||||
this.removeEngineInvite();
|
||||
this.reset();
|
||||
wx.$globalCallSign = false;
|
||||
wx.$TUICallEngine = null;
|
||||
}
|
||||
reset() {
|
||||
this.sdkAppID = 0;
|
||||
this.userID = '';
|
||||
this.userSig = '';
|
||||
this.tim = null;
|
||||
this.globalCallPagePath = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1017 B |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 799 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 714 B |
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"_from": "@tencentcloud/call-uikit-wechat@^1.1.3",
|
||||
"_id": "@tencentcloud/call-uikit-wechat@1.3.2",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-SbqtN89vDzddepNtIymJhzvJy5zqgrEA0yMDCJBupC4ZeiJi2BSwqXa6XAjq0jck7GSjO4g/LriUukZoM2DrOQ==",
|
||||
"_location": "/@tencentcloud/call-uikit-wechat",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "@tencentcloud/call-uikit-wechat@^1.1.3",
|
||||
"name": "@tencentcloud/call-uikit-wechat",
|
||||
"escapedName": "@tencentcloud%2fcall-uikit-wechat",
|
||||
"scope": "@tencentcloud",
|
||||
"rawSpec": "^1.1.3",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^1.1.3"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/@tencentcloud/chat-uikit-wechat"
|
||||
],
|
||||
"_resolved": "https://registry.npmmirror.com/@tencentcloud/call-uikit-wechat/-/call-uikit-wechat-1.3.2.tgz",
|
||||
"_shasum": "d2c2174bd36345d9f169505b9d0527d4e2dce136",
|
||||
"_spec": "@tencentcloud/call-uikit-wechat@^1.1.3",
|
||||
"_where": "E:\\WeChatProjects\\test_miniprogram\\node_modules\\@tencentcloud\\chat-uikit-wechat",
|
||||
"author": {
|
||||
"name": "jonyttang",
|
||||
"email": "jonyttang@tencent.com"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tencentyun/TUICallKit/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"tuicall-engine-wx": "^1.4.1"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "An Open-source Voice & Video Calling UI Component Based on Tencent Cloud Service.",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"lib": "lib"
|
||||
},
|
||||
"homepage": "https://cloud.tencent.com/document/product/647/78733",
|
||||
"keywords": [
|
||||
"uikit",
|
||||
"call",
|
||||
"Minipragram",
|
||||
"tencent",
|
||||
"chat",
|
||||
"video",
|
||||
"audio",
|
||||
"voice",
|
||||
"语音",
|
||||
"视频",
|
||||
"电话",
|
||||
"通话"
|
||||
],
|
||||
"license": "ISC",
|
||||
"main": "index.js",
|
||||
"name": "@tencentcloud/call-uikit-wechat",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tencentyun/TUICallKit.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"version": "1.3.2"
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
## 关于腾讯云即时通信 IM
|
||||
|
||||
腾讯云即时通信(Instant Messaging,IM)基于 QQ 底层 IM 能力开发,仅需植入 SDK 即可轻松集成聊天、会话、群组、资料管理能力,帮助您实现文字、图片、短语音、短视频等富媒体消息收发,全面满足通信需要。
|
||||
|
||||
## 关于 chat-uikit-wechat
|
||||
|
||||
chat-uikit-wechat 是基于腾讯云 IM SDK 的一款 小程序 UI 组件库,它提供了一些通用的 UI 组件,包含会话、聊天、群组、音视频通话等功能。基于 UI 组件您可以像搭积木一样快速搭建起自己的业务逻辑。
|
||||
chat-uikit-wechat 中的组件在实现 UI 功能的同时,会调用 IM SDK 相应的接口实现 IM 相关逻辑和数据的处理,因而开发者在使用 chat-uikit-wechat 时只需关注自身业务或个性化扩展即可。
|
||||
chat-uikit-wechat 效果如下图所示:
|
||||
<img width="1015" src="https://user-images.githubusercontent.com/40623255/202661227-d4227dcc-bada-42a6-a57b-0d0c0abc098b.png"/>
|
||||
|
||||
本文介绍如何快速集成腾讯云 Web IM SDK 的 VUE UI 组件库。对于其他平台,请参考文档:
|
||||
|
||||
[**chat-uikit-vue**](https://github.com/TencentCloud/chat-uikit-vue)
|
||||
|
||||
[**chat-uikit-react**](https://github.com/TencentCloud/chat-uikit-react)
|
||||
|
||||
[**chat-uikit-uniapp**](https://github.com/TencentCloud/chat-uikit-uniapp)
|
||||
|
||||
[**chat-uikit-ios**](https://github.com/TencentCloud/chat-uikit-ios)
|
||||
|
||||
[**chat-uikit-android**](https://github.com/TencentCloud/chat-uikit-android)
|
||||
|
||||
[**chat-uikit-flutter**](https://github.com/TencentCloud/chat-uikit-flutter)
|
||||
|
||||
|
||||
|
||||
## 发送您的第一条消息
|
||||
|
||||
### 开发环境要求
|
||||
|
||||
- 微信开发者工具
|
||||
- JavaScript
|
||||
|
||||
|
||||
### TUIKit 源码集成 - github方式集成
|
||||
|
||||
#### 步骤1:创建项目
|
||||
|
||||
在微信开发者工具上创建一个小程序项目,选择不使用模版。
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/40623255/202665077-b4f01580-69f2-493c-9fab-4f4ef8e5021a.png"/>
|
||||
|
||||
#### 步骤2:下载 TUIKit 组件
|
||||
|
||||
在微信开发者工具内新建终端。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/6735b8ead18ffa7c80f2e16cebbdc9d1.png"/>
|
||||
|
||||
通过 `git clone` 方式下载 TUIKit 组件及其相关依赖, 为了方便您的后续使用,建议您通过以下命令将整个 `chat-uikit-wechat` 复制到您项目的根目录下,并重命名为 TUIKit:
|
||||
|
||||
|
||||
```shell
|
||||
# 项目根目录命令行执行
|
||||
git clone https://github.com/TencentCloud/chat-uikit-wechat.git
|
||||
|
||||
|
||||
# 移动并重命名到项目的根目录下
|
||||
# macOS
|
||||
mv chat-uikit-wechat ./TUIKit
|
||||
# windows
|
||||
move chat-uikit-wechat .\TUIKit
|
||||
|
||||
```
|
||||
|
||||
成功后目录结构如图所示:
|
||||
<img width="300" src="https://qcloudimg.tencent-cloud.cn/raw/b2cf42ffef896731a170e138f4dd053f.png"/>
|
||||
|
||||
#### 步骤3:引入 TUIKit 组件
|
||||
|
||||
##### 方式一: 主包引入 (适用于业务逻辑简单的小程序)
|
||||
在 page 页面引用 TUIKit 组件,为此您需要分别修改 index.wxml 、index.js 和 index.json。
|
||||
|
||||
wxml 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/9816f2a2141357fbaced7e77929392f8.png"/>
|
||||
|
||||
```javascript
|
||||
<view>
|
||||
<TUIKit config="{{config}}" id="TUIKit"></TUIKit>
|
||||
</view>
|
||||
```
|
||||
|
||||
js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/b9c02ec038b4b397f175591c7b5ef876.png"/>
|
||||
|
||||
```javascript
|
||||
import TIM from '../../TUIKit/lib/tim-wx-sdk';
|
||||
import { genTestUserSig } from '../../TUIKit/debug/GenerateTestUserSig';
|
||||
import TIMUploadPlugin from '../../TUIKit/lib/tim-upload-plugin';
|
||||
import TIMProfanityFilterPlugin from '../../TUIKit/lib/tim-profanity-filter-plugin';
|
||||
|
||||
|
||||
Page({
|
||||
data: {
|
||||
config: {
|
||||
userID: '', //User ID
|
||||
SDKAPPID: 0, // Your SDKAppID
|
||||
SECRETKEY: '', // Your secretKey
|
||||
EXPIRETIME: 604800,
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const userSig = genTestUserSig(this.data.config).userSig
|
||||
wx.$TUIKit = TIM.create({
|
||||
SDKAppID: this.data.config.SDKAPPID
|
||||
})
|
||||
wx.$chat_SDKAppID = this.data.config.SDKAPPID;
|
||||
wx.$chat_userID = this.data.config.userID;
|
||||
wx.$chat_userSig = userSig;
|
||||
wx.$TUIKitTIM = TIM;
|
||||
wx.$TUIKit.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
||||
wx.$TUIKit.registerPlugin({ 'tim-profanity-filter-plugin': TIMProfanityFilterPlugin });
|
||||
wx.$TUIKit.login({
|
||||
userID: this.data.config.userID,
|
||||
userSig
|
||||
});
|
||||
wx.setStorage({
|
||||
key: 'currentUserID',
|
||||
data: [],
|
||||
});
|
||||
wx.$TUIKit.on(wx.$TUIKitTIM.EVENT.SDK_READY, this.onSDKReady,this);
|
||||
},
|
||||
onUnload() {
|
||||
wx.$TUIKit.off(wx.$TUIKitTIM.EVENT.SDK_READY, this.onSDKReady,this);
|
||||
},
|
||||
onSDKReady() {
|
||||
const TUIKit = this.selectComponent('#TUIKit');
|
||||
TUIKit.init();
|
||||
}
|
||||
});
|
||||
```
|
||||
json 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/866e12c4bf19e08c71c233158cc19106.png"/>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"usingComponents": {
|
||||
"TUIKit": "../../TUIKit/index"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
```
|
||||
|
||||
##### 方式二:分包引入 (适用于业务逻辑复杂,按需载入的小程序)
|
||||
小程序分包有如下好处:
|
||||
- 规避所有逻辑代码放主包,导致主包文件体积超限问题
|
||||
- 支持按需载入,降低小程序载入耗时和页面渲染耗时
|
||||
- 支持更加复杂的功能
|
||||
分包流程:
|
||||
1.在自己项目里创建分包,本文以 TUI-CustomerService 为例。和 pages 同级创建 TUI-CustomerService 文件夹,并在文件夹内部创建 pages 文件夹并且在其下创建 index 页面。
|
||||
创建后的目录结构:
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/bc1352da5ea30bb3a8134bedbc421a9b.png"/>
|
||||
|
||||
2.在 app.json 文件注册分包。
|
||||
|
||||
```javascript
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index"
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "TUI-CustomerService",
|
||||
"name": "TUI-CustomerService",
|
||||
"pages": [
|
||||
"pages/index"
|
||||
],
|
||||
"independent": false
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "Weixin",
|
||||
"navigationBarTextStyle": "black"
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
```
|
||||
3.将 TUIKit 文件夹复制到分包目录下。
|
||||
成功后的目录结构:
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/5abd5dc90d2e5d53b3ed1a264e0398f8.png"/>
|
||||
|
||||
4.将 TUIKit 文件夹下的 debug 和 lib 文件夹复制到主包。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/00a9557954be659dd32f00d45195daac.png"/>
|
||||
|
||||
5. 在分包内引用 TUIKit组件,为此需要分别修改分包内部 index.wxml 、index.js 、index.json 文件,以及 app.js 文件。
|
||||
|
||||
wxml 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/072f4f8f78d512f28ac8e82ed9925055.png"/>
|
||||
|
||||
```javascript
|
||||
<view>
|
||||
<TUIKit config="{{config}}" id="TUIKit"></TUIKit>
|
||||
</view>
|
||||
```
|
||||
|
||||
js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/d36a0543c7fe94def0fc36042eddc28c.png"/>
|
||||
|
||||
```javascript
|
||||
Page({
|
||||
|
||||
// 其他代码
|
||||
|
||||
onLoad() {
|
||||
const TUIKit = this.selectComponent('#TUIKit');
|
||||
TUIKit.init();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
json 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/4499096c37b9b29abffa21b71fb90e9e.png"/>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"usingComponents": {
|
||||
"TUIKit": "../TUIKit/index"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
```
|
||||
app.js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/170aa919af6db0e7b32ace5da9d417f1.png"/>
|
||||
|
||||
```javascript
|
||||
import TIM from './lib/tim-wx-sdk';
|
||||
import TIMUploadPlugin from './lib/tim-upload-plugin';
|
||||
import TIMProfanityFilterPlugin from './lib/tim-profanity-filter-plugin';
|
||||
import { genTestUserSig } from './debug/GenerateTestUserSig';
|
||||
App({
|
||||
onLaunch: function () {
|
||||
wx.$TUIKit = TIM.create({
|
||||
SDKAppID: this.globalData.config.SDKAPPID,
|
||||
});
|
||||
const userSig = genTestUserSig(this.globalData.config).userSig
|
||||
wx.$chat_SDKAppID = this.globalData.config.SDKAPPID;
|
||||
wx.$TUIKitTIM = TIM;
|
||||
wx.$chat_userID = this.globalData.config.userID;
|
||||
wx.$chat_userSig = userSig;
|
||||
wx.$TUIKit.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
||||
wx.$TUIKit.registerPlugin({ 'tim-profanity-filter-plugin': TIMProfanityFilterPlugin });
|
||||
wx.$TUIKit.login({
|
||||
userID: this.globalData.config.userID,
|
||||
userSig
|
||||
});
|
||||
// 监听系统级事件
|
||||
wx.$TUIKit.on(wx.$TUIKitTIM.EVENT.SDK_READY, this.onSDKReady);
|
||||
},
|
||||
globalData: {
|
||||
config: {
|
||||
userID: '', //User ID
|
||||
SECRETKEY: '', // Your secretKey
|
||||
SDKAPPID: 0, // Your SDKAppID
|
||||
EXPIRETIME: 604800,
|
||||
},
|
||||
},
|
||||
onSDKReady() {
|
||||
},
|
||||
});
|
||||
```
|
||||
6. 按需载入分包,您需要修改主包 pages 下的 index.wxml 、index.js。
|
||||
|
||||
wxml 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/86f2698910c2e255727f419625441ed9.png"/>
|
||||
|
||||
```javascript
|
||||
<view class="container" bindtap="handleJump">
|
||||
载入腾讯云 IM 分包
|
||||
</view>
|
||||
```
|
||||
|
||||
js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/5c41dff77345aa364bde46039f84ffd7.png"/>
|
||||
|
||||
```javascript
|
||||
Page({
|
||||
handleJump() {
|
||||
wx.navigateTo({
|
||||
url: '../../TUI-CustomerService/pages/index',
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤4: 获取 SDKAppID 、密钥与 userID
|
||||
|
||||
设置步骤3示例代码中的相关参数 SDKAPPID、SECRETKEY 以及 userID ,其中 SDKAppID 和密钥等信息,可通过 [即时通信 IM 控制台](https://console.cloud.tencent.com/im) 获取,单击目标应用卡片,进入应用的基础配置页面。例如:
|
||||
<img style="width:600px; max-width: inherit;" src="https://qcloudimg.tencent-cloud.cn/raw/44a331ce39f05f7080cf33ca9bc8e5dd.png"/>
|
||||
userID 信息,可通过 [即时通信 IM 控制台](https://console.cloud.tencent.com/im) 进行创建和获取,单击目标应用卡片,进入应用的账号管理页面,即可创建账号并获取 userID。例如:
|
||||
<img style="width:870px; max-width: inherit;" src="https://qcloudimg.tencent-cloud.cn/raw/94c801b7258612f8a4018728d862252f.png"/>
|
||||
|
||||
### 步骤5:编译小程序
|
||||
|
||||
- 请在本地设置里面勾选上“不校验合法域名、web-view (业务域名)、 TLS 版本以及 HTTPS 证书”。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/e32530c238362d5bb597c1171f6646ff.png"/>
|
||||
|
||||
- 点击【清缓存】->【全部清除】,避免开发者工具的缓存造成渲染异常。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/2c68432c6e3399df21517e521c356299.png"/>
|
||||
|
||||
- 点击【编译】。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/b98aebdadf932e036e9900aea5651c1e.png"/>
|
||||
|
||||
### 步骤6:发送您的第一条消息
|
||||
<img style="width:1000" src="https://user-images.githubusercontent.com/40623255/202665415-ea50357f-4c86-4f18-bacb-e731a64d9a31.png" />
|
||||
<img style="width:1000" src="https://qcloudimg.tencent-cloud.cn/raw/02eb06fbc13cdf27664fe55eb2e10b49.png" />
|
||||
|
||||
### 常见问题
|
||||
#### 1. 什么是 UserSig?
|
||||
|
||||
UserSig 是用户登录即时通信 IM 的密码,其本质是对 UserID 等信息加密后得到的密文。
|
||||
|
||||
#### 2. 如何生成 UserSig?
|
||||
|
||||
UserSig 签发方式是将 UserSig 的计算代码集成到您的服务端,并提供面向项目的接口,在需要 UserSig 时由您的项目向业务服务器发起请求获取动态 UserSig。更多详情请参见 [服务端生成 UserSig](https://cloud.tencent.com/document/product/269/32688#GeneratingdynamicUserSig)。
|
||||
|
||||
> !
|
||||
>
|
||||
> 本文示例代码采用的获取 UserSig 的方案是在客户端代码中配置 SECRETKEY,该方法中 SECRETKEY 很容易被反编译逆向破解,一旦您的密钥泄露,攻击者就可以盗用您的腾讯云流量,因此**该方法仅适合本地跑通功能调试**。 正确的 UserSig 签发方式请参见上文。
|
||||
|
||||
### 3. 小程序如果需要上线或者部署正式环境怎么办?
|
||||
请在**微信公众平台**>**开发**>**开发管理**>**开发设置**>**服务器域名**中进行域名配置:
|
||||
|
||||
从v2.11.2起 SDK 支持了 WebSocket,WebSocket 版本须添加以下域名到 **socket 合法域名**:
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`wss://wss.im.qcloud.com`| Web IM 业务域名 | 必须|
|
||||
|`wss://wss.tim.qq.com`| Web IM 业务域名 | 必须|
|
||||
|
||||
将以下域名添加到 **request 合法域名**:
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`https://web.sdk.qcloud.com`| Web IM 业务域名 | 必须|
|
||||
|`https://webim.tim.qq.com` | Web IM 业务域名 | 必须|
|
||||
|`https://api.im.qcloud.com` | Web IM 业务域名 | 必须|
|
||||
|
||||
|
||||
将以下域名添加到 **uploadFile 合法域名**:
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`https://cos.ap-shanghai.myqcloud.com` | 文件上传域名 | 必须|
|
||||
|`https://cos.ap-shanghai.tencentcos.cn` | 文件上传域名 | 必须|
|
||||
|`https://cos.ap-guangzhou.myqcloud.com` | 文件上传域名 | 必须|
|
||||
|
||||
|
||||
将以下域名添加到 **downloadFile 合法域名**:
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`https://cos.ap-shanghai.myqcloud.com` | 文件下载域名 | 必须|
|
||||
|`https://cos.ap-shanghai.tencentcos.cn` | 文件下载域名 | 必须|
|
||||
|`https://cos.ap-guangzhou.myqcloud.com` | 文件下载域名 | 必须|
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
## 1.0.12 (2023-1-4)
|
||||
### 新增
|
||||
- 支持本地消息审核[需在控制台开启](https://console.cloud.tencent.com/im/local-audit-setting)
|
||||
### 修复
|
||||
- 修复发送图片重复问题
|
||||
- 修复群提示消息展示问题
|
||||
|
||||
## 1.0.10 (2022-12-10)
|
||||
### 新增
|
||||
- 支持群通话[音视频通话](https://cloud.tencent.com/document/product/269/68378)
|
||||
### 修复
|
||||
- 修复已知问题,提升稳定性
|
||||
|
||||
## 1.0.8 (2022-11-20)
|
||||
### 新增
|
||||
- 支持 github 仓库形式接入源码
|
||||
### 修复
|
||||
- 修复已知问题,提升稳定性
|
||||
|
||||
|
||||
## 1.0.5 (2022-10-10)
|
||||
|
||||
### 新增
|
||||
- 支持集成 1V1 音视频通话[音视频通话](https://cloud.tencent.com/document/product/269/68378)
|
||||
- 提供分包接入解决方案
|
||||
### 修复
|
||||
- 修复已知问题,提升稳定性
|
||||
|
||||
## 1.0.0 (2022-09-15)
|
||||
|
||||
- 支持组件形式集成
|
||||
|
||||
### 新增
|
||||
- [TUIKit界面库 - 小程序](https://cloud.tencent.com/document/product/269/79721)
|
||||
- [集成基础功能 - 小程序](https://cloud.tencent.com/document/product/269/62768)
|
||||
- [设置界面风格 - 小程序](https://cloud.tencent.com/document/product/269/79083)
|
||||
- [添加自定义消息 - 小程序](https://cloud.tencent.com/document/product/269/62789)
|
||||
@@ -0,0 +1,358 @@
|
||||
## 关于腾讯云即时通信 IM
|
||||
|
||||
腾讯云即时通信(Instant Messaging,IM)基于 QQ 底层 IM 能力开发,仅需植入 SDK 即可轻松集成聊天、会话、群组、资料管理能力,帮助您实现文字、图片、短语音、短视频等富媒体消息收发,全面满足通信需要。
|
||||
|
||||
## 关于 chat-uikit-wechat
|
||||
|
||||
chat-uikit-wechat 是基于腾讯云 IM SDK 的一款 小程序 UI 组件库,它提供了一些通用的 UI 组件,包含会话、聊天、群组、音视频通话等功能。基于 UI 组件您可以像搭积木一样快速搭建起自己的业务逻辑。
|
||||
chat-uikit-wechat 中的组件在实现 UI 功能的同时,会调用 IM SDK 相应的接口实现 IM 相关逻辑和数据的处理,因而开发者在使用 chat-uikit-wechat 时只需关注自身业务或个性化扩展即可。
|
||||
chat-uikit-wechat 效果如下图所示:
|
||||
<img width="1015" src="https://user-images.githubusercontent.com/40623255/202661227-d4227dcc-bada-42a6-a57b-0d0c0abc098b.png" />
|
||||
|
||||
|
||||
## 发送您的第一条消息
|
||||
|
||||
### 开发环境要求
|
||||
|
||||
- 微信开发者工具
|
||||
- JavaScript
|
||||
- node(12.13.0 <= node版本 <= 17.0.0, 推荐使用 Node.js 官方 LTS 版本 16.17.0)
|
||||
- npm(版本请与 node 版本匹配)
|
||||
|
||||
### TUIKit 源码集成
|
||||
|
||||
#### 步骤1:创建项目
|
||||
|
||||
在微信开发者工具上创建一个小程序项目,选择不使用模版。
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/40623255/202665077-b4f01580-69f2-493c-9fab-4f4ef8e5021a.png"/>
|
||||
|
||||
#### 步骤2:下载 TUIKit 组件
|
||||
|
||||
微信开发者工具创建的小程序不会默认创建 package.json 文件,因此您需要先创建 package.json 文件。新建终端,如下:
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/6735b8ead18ffa7c80f2e16cebbdc9d1.png"/>
|
||||
|
||||
输入:
|
||||
|
||||
```javascript
|
||||
npm init
|
||||
```
|
||||
然后通过 npm 方式下载 TUIKit 组件, 为了方便您后续的拓展,建议您将 TUIKit 组件复制到自己的小程序目录下:
|
||||
|
||||
macOS端
|
||||
```javascript
|
||||
npm i @tencentcloud/chat-uikit-wechat
|
||||
```
|
||||
```javascript
|
||||
mkdir -p ./TUIKit && cp -r node_modules/@tencentcloud/chat-uikit-wechat/ ./TUIKit
|
||||
```
|
||||
Windows端
|
||||
```javascript
|
||||
npm i @tencentcloud/chat-uikit-wechat
|
||||
```
|
||||
```javascript
|
||||
xcopy node_modules\@tencentcloud\chat-uikit-wechat .\TUIKit /i /e
|
||||
```
|
||||
成功后目录结构如图所示:
|
||||
<img width="300" src="https://qcloudimg.tencent-cloud.cn/raw/8f0b5274acd80602f2a431313034a2b9.png"/>
|
||||
|
||||
#### 步骤3:引入 TUIKit 组件
|
||||
|
||||
##### 方式一: 主包引入 (适用于业务逻辑简单的小程序)
|
||||
在 page 页面引用 TUIKit 组件,为此您需要分别修改 index.wxml 、index.js 和 index.json。
|
||||
|
||||
wxml 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/9816f2a2141357fbaced7e77929392f8.png"/>
|
||||
|
||||
```javascript
|
||||
<view>
|
||||
<TUIKit config="{{config}}" id="TUIKit"></TUIKit>
|
||||
</view>
|
||||
```
|
||||
|
||||
js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/b9c02ec038b4b397f175591c7b5ef876.png"/>
|
||||
|
||||
```javascript
|
||||
import TIM from '../../TUIKit/lib/tim-wx-sdk';
|
||||
import { genTestUserSig } from '../../TUIKit/debug/GenerateTestUserSig';
|
||||
import TIMUploadPlugin from '../../TUIKit/lib/tim-upload-plugin';
|
||||
import TIMProfanityFilterPlugin from '../../TUIKit/lib/tim-profanity-filter-plugin';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
config: {
|
||||
userID: '', //User ID
|
||||
SDKAPPID: 0, // Your SDKAppID
|
||||
SECRETKEY: '', // Your secretKey
|
||||
EXPIRETIME: 604800,
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const userSig = genTestUserSig(this.data.config).userSig
|
||||
wx.$TUIKit = TIM.create({
|
||||
SDKAppID: this.data.config.SDKAPPID
|
||||
})
|
||||
wx.$chat_SDKAppID = this.data.config.SDKAPPID;
|
||||
wx.$chat_userID = this.data.config.userID;
|
||||
wx.$chat_userSig = userSig;
|
||||
wx.$TUIKitTIM = TIM;
|
||||
wx.$TUIKit.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
||||
wx.$TUIKit.registerPlugin({ 'tim-profanity-filter-plugin': TIMProfanityFilterPlugin });
|
||||
wx.$TUIKit.login({
|
||||
userID: this.data.config.userID,
|
||||
userSig
|
||||
});
|
||||
wx.setStorage({
|
||||
key: 'currentUserID',
|
||||
data: [],
|
||||
});
|
||||
wx.$TUIKit.on(wx.$TUIKitTIM.EVENT.SDK_READY, this.onSDKReady,this);
|
||||
},
|
||||
onUnload() {
|
||||
wx.$TUIKit.off(wx.$TUIKitTIM.EVENT.SDK_READY, this.onSDKReady,this);
|
||||
},
|
||||
onSDKReady() {
|
||||
const TUIKit = this.selectComponent('#TUIKit');
|
||||
TUIKit.init();
|
||||
}
|
||||
});
|
||||
```
|
||||
json 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/866e12c4bf19e08c71c233158cc19106.png"/>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"usingComponents": {
|
||||
"TUIKit": "../../TUIKit/index"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
```
|
||||
|
||||
##### 方式二:分包引入 (适用于业务逻辑复杂,按需载入的小程序)
|
||||
小程序分包有如下好处:
|
||||
- 规避所有逻辑代码放主包,导致主包文件体积超限问题
|
||||
- 支持按需载入,降低小程序载入耗时和页面渲染耗时
|
||||
- 支持更加复杂的功能
|
||||
分包流程:
|
||||
1.在自己项目里创建分包,本文以 TUI—CustomerService 为例。和 pages 同级创建 TUI—CustomerService 文件夹,并在文件夹内部创建 pages 文件夹并且其下创建 index 页面。
|
||||
创建后的目录结构:
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/bc1352da5ea30bb3a8134bedbc421a9b.png"/>
|
||||
|
||||
2.在 app.json 文件注册分包。
|
||||
|
||||
```javascript
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index"
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "TUI-CustomerService",
|
||||
"name": "TUI-CustomerService",
|
||||
"pages": [
|
||||
"pages/index"
|
||||
],
|
||||
"independent": false
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "Weixin",
|
||||
"navigationBarTextStyle": "black"
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
```
|
||||
3.将 TUIKit 文件夹复制到分包目录下。
|
||||
成功后的目录结构:
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/5abd5dc90d2e5d53b3ed1a264e0398f8.png"/>
|
||||
|
||||
4.将 TUIKit 文件夹下的 debug 和 lib 文件夹复制到主包。
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/00a9557954be659dd32f00d45195daac.png"/>
|
||||
|
||||
5. 在分包内引用 TUIKit组件,为此需要分别修改分包内部 index.wxml 、index.js 、index.json 文件,以及 app.js 文件。
|
||||
|
||||
wxml 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/072f4f8f78d512f28ac8e82ed9925055.png"/>
|
||||
|
||||
```javascript
|
||||
<view>
|
||||
<TUIKit config="{{config}}" id="TUIKit"></TUIKit>
|
||||
</view>
|
||||
```
|
||||
|
||||
js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/d36a0543c7fe94def0fc36042eddc28c.png"/>
|
||||
|
||||
```javascript
|
||||
Page({
|
||||
|
||||
// 其他代码
|
||||
|
||||
onLoad() {
|
||||
const TUIKit = this.selectComponent('#TUIKit');
|
||||
TUIKit.init();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
json 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/4499096c37b9b29abffa21b71fb90e9e.png"/>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"usingComponents": {
|
||||
"TUIKit": "../TUIKit/index"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
```
|
||||
app.js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/170aa919af6db0e7b32ace5da9d417f1.png"/>
|
||||
|
||||
```javascript
|
||||
import TIM from './lib/tim-wx-sdk';
|
||||
import TIMUploadPlugin from './lib/tim-upload-plugin';
|
||||
import TIMProfanityFilterPlugin from './lib/tim-profanity-filter-plugin';
|
||||
import { genTestUserSig } from './debug/GenerateTestUserSig';
|
||||
App({
|
||||
onLaunch: function () {
|
||||
wx.$TUIKit = TIM.create({
|
||||
SDKAppID: this.globalData.config.SDKAPPID,
|
||||
});
|
||||
const userSig = genTestUserSig(this.globalData.config).userSig
|
||||
wx.$chat_SDKAppID = this.globalData.config.SDKAPPID;
|
||||
wx.$TUIKitTIM = TIM;
|
||||
wx.$chat_userID = this.globalData.config.userID;
|
||||
wx.$chat_userSig = userSig;
|
||||
wx.$TUIKit.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
||||
wx.$TUIKit.registerPlugin({ 'tim-profanity-filter-plugin': TIMProfanityFilterPlugin });
|
||||
wx.$TUIKit.login({
|
||||
userID: this.globalData.config.userID,
|
||||
userSig
|
||||
});
|
||||
// 监听系统级事件
|
||||
wx.$TUIKit.on(wx.$TUIKitTIM.EVENT.SDK_READY, this.onSDKReady);
|
||||
},
|
||||
globalData: {
|
||||
config: {
|
||||
userID: '', //User ID
|
||||
SECRETKEY: '', // Your secretKey
|
||||
SDKAPPID: 0, // Your SDKAppID
|
||||
EXPIRETIME: 604800,
|
||||
},
|
||||
},
|
||||
onSDKReady() {
|
||||
},
|
||||
});
|
||||
```
|
||||
6. 按需载入分包,您需要修改主包 pages 下的 index.wxml 、index.js。
|
||||
|
||||
wxml 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/86f2698910c2e255727f419625441ed9.png"/>
|
||||
|
||||
```javascript
|
||||
<view class="container" bindtap="handleJump">
|
||||
载入腾讯云 IM 分包
|
||||
</view>
|
||||
```
|
||||
|
||||
js 文件
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/5c41dff77345aa364bde46039f84ffd7.png"/>
|
||||
|
||||
```javascript
|
||||
Page({
|
||||
handleJump() {
|
||||
wx.navigateTo({
|
||||
url: '../../TUI-CustomerService/pages/index',
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤4: 获取 SDKAppID 、密钥与 userID
|
||||
|
||||
设置步骤3示例代码中的相关参数 SDKAPPID、SECRETKEY 以及 userID ,其中 SDKAppID 和密钥等信息,可通过 [即时通信 IM 控制台](https://console.cloud.tencent.com/im) 获取,单击目标应用卡片,进入应用的基础配置页面。例如:
|
||||
<img style="width:600px; max-width: inherit;" src="https://qcloudimg.tencent-cloud.cn/raw/44a331ce39f05f7080cf33ca9bc8e5dd.png"/>
|
||||
userID 信息,可通过 [即时通信 IM 控制台](https://console.cloud.tencent.com/im) 进行创建和获取,单击目标应用卡片,进入应用的账号管理页面,即可创建账号并获取 userID。例如:
|
||||
<img style="width:870px; max-width: inherit;" src="https://qcloudimg.tencent-cloud.cn/raw/94c801b7258612f8a4018728d862252f.png"/>
|
||||
|
||||
### 步骤5:编译小程序
|
||||
|
||||
- 请在本地设置里面勾选上“不校验合法域名、web-view (业务域名)、 TLS 版本以及 HTTPS 证书”。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/e32530c238362d5bb597c1171f6646ff.png"/>
|
||||
|
||||
- 点击【清缓存】->【全部清除】,避免开发者工具的缓存造成渲染异常。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/2c68432c6e3399df21517e521c356299.png"/>
|
||||
|
||||
- 点击【编译】。
|
||||
|
||||
<img src="https://qcloudimg.tencent-cloud.cn/raw/b98aebdadf932e036e9900aea5651c1e.png"/>
|
||||
|
||||
### 步骤6:发送您的第一条消息
|
||||
<img style="width:1000" src="https://qcloudimg.tencent-cloud.cn/raw/4673f24d0fc8319c4788d505d9fde774.png" />
|
||||
<img style="width:1000" src="https://qcloudimg.tencent-cloud.cn/raw/02eb06fbc13cdf27664fe55eb2e10b49.png" />
|
||||
|
||||
### 常见问题
|
||||
#### 1. 什么是 UserSig?
|
||||
|
||||
UserSig 是用户登录即时通信 IM 的密码,其本质是对 UserID 等信息加密后得到的密文。
|
||||
|
||||
#### 2. 如何生成 UserSig?
|
||||
|
||||
UserSig 签发方式是将 UserSig 的计算代码集成到您的服务端,并提供面向项目的接口,在需要 UserSig 时由您的项目向业务服务器发起请求获取动态 UserSig。更多详情请参见 [服务端生成 UserSig](https://cloud.tencent.com/document/product/269/32688#GeneratingdynamicUserSig)。
|
||||
|
||||
> !
|
||||
>
|
||||
> 本文示例代码采用的获取 UserSig 的方案是在客户端代码中配置 SECRETKEY,该方法中 SECRETKEY 很容易被反编译逆向破解,一旦您的密钥泄露,攻击者就可以盗用您的腾讯云流量,因此**该方法仅适合本地跑通功能调试**。 正确的 UserSig 签发方式请参见上文。
|
||||
|
||||
### 3. 小程序如果需要上线或者部署正式环境怎么办?
|
||||
请在**微信公众平台**>**开发**>**开发管理**>**开发设置**>**服务器域名**中进行域名配置:
|
||||
|
||||
从v2.11.2起 SDK 支持了 WebSocket,WebSocket 版本须添加以下域名到 **socket 合法域名**:
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`wss://wss.im.qcloud.com`| Web IM 业务域名 | 必须|
|
||||
|`wss://wss.tim.qq.com`| Web IM 业务域名 | 必须|
|
||||
|
||||
将以下域名添加到 **request 合法域名**:
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`https://web.sdk.qcloud.com`| Web IM 业务域名 | 必须|
|
||||
|`https://webim.tim.qq.com` | Web IM 业务域名 | 必须|
|
||||
|`https://api.im.qcloud.com` | Web IM 业务域名 | 必须|
|
||||
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`https://cos.ap-shanghai.myqcloud.com` | 文件上传域名 | 必须|
|
||||
|`https://cos.ap-shanghai.tencentcos.cn` | 文件上传域名 | 必须|
|
||||
|`https://cos.ap-guangzhou.myqcloud.com` | 文件上传域名 | 必须|
|
||||
|
||||
|
||||
将以下域名添加到 **downloadFile 合法域名**:
|
||||
|
||||
| 域名 | 说明 | 是否必须 |
|
||||
|-------|---------|----|
|
||||
|`https://cos.ap-shanghai.myqcloud.com` | 文件下载域名 | 必须|
|
||||
|`https://cos.ap-shanghai.tencentcos.cn` | 文件下载域名 | 必须|
|
||||
|`https://cos.ap-guangzhou.myqcloud.com` | 文件下载域名 | 必须|
|
||||
@@ -0,0 +1,3 @@
|
||||
Component({
|
||||
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { parseAudio } from '../../../../../utils/message-parse';
|
||||
|
||||
// 创建audio控件
|
||||
const myaudio = wx.createInnerAudioContext();
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
renderDom: parseAudio(newVal),
|
||||
message: newVal,
|
||||
});
|
||||
},
|
||||
},
|
||||
messageList: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.filtterAudioMessage(newVal);
|
||||
this.setData({
|
||||
audioMessageList: newVal,
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
detached() {
|
||||
myaudio.stop();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
message: '',
|
||||
renderDom: [],
|
||||
Audio: [],
|
||||
audioMessageList: [],
|
||||
audioSave: [],
|
||||
audKey: '', // 当前选中的音频key
|
||||
indexAudio: Number,
|
||||
isPlay: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
// 过滤语音消息,从消息列表里面筛选出语音消息
|
||||
filtterAudioMessage(messageList) {
|
||||
const list = [];
|
||||
for (let index = 0; index < messageList.length; index++) {
|
||||
if (messageList[index].type === 'TIMSoundElem') {
|
||||
list.push(messageList[index]);
|
||||
Object.assign(messageList[index], {
|
||||
isPlaying: false,
|
||||
}),
|
||||
this.data.audioSave = list;
|
||||
this.setData({
|
||||
audioSave: this.data.audioSave,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 音频播放
|
||||
audioPlay(e) {
|
||||
const { id } = e.currentTarget.dataset;
|
||||
const { audioSave } = this.data;
|
||||
|
||||
// 设置状态
|
||||
audioSave.forEach((message, index) => {
|
||||
message.isPlaying = false;
|
||||
if (audioSave[index].ID == id) {
|
||||
message.isPlaying = true;
|
||||
const indexAudio = audioSave.findIndex(value => value.ID == audioSave[index].ID);
|
||||
this.setData({
|
||||
indexAudio,
|
||||
isPlay: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.setData({
|
||||
audioSave,
|
||||
audKey: this.data.indexAudio,
|
||||
isPlay: true,
|
||||
});
|
||||
myaudio.autoplay = true;
|
||||
const { audKey } = this.data;
|
||||
const playSrc = audioSave[audKey].payload.url;
|
||||
myaudio.src = playSrc;
|
||||
myaudio.play();
|
||||
// 开始监听
|
||||
myaudio.onPlay(() => {
|
||||
console.log('开始播放');
|
||||
});
|
||||
|
||||
// 结束监听
|
||||
myaudio.onEnded(() => {
|
||||
console.log('自动播放完毕');
|
||||
audioSave[this.data.indexAudio].isPlaying = false;
|
||||
this.setData({
|
||||
audioSave,
|
||||
isPlay: false,
|
||||
});
|
||||
});
|
||||
|
||||
// 错误回调
|
||||
myaudio.onError((err) => {
|
||||
console.log(err);
|
||||
audioSave[this.data.indexAudio].isPlaying = false;
|
||||
this.setData({
|
||||
audioSave,
|
||||
});
|
||||
return;
|
||||
});
|
||||
},
|
||||
|
||||
// 音频停止
|
||||
audioStop(e) {
|
||||
const { key } = e.currentTarget.dataset;
|
||||
const { audioSave } = this.data;
|
||||
// 设置状态
|
||||
audioSave.forEach((message, index) => {
|
||||
message.isPlaying = false;
|
||||
});
|
||||
this.setData({
|
||||
audioSave,
|
||||
isPlay: false,
|
||||
});
|
||||
myaudio.stop();
|
||||
|
||||
// 停止监听
|
||||
myaudio.onStop(() => {
|
||||
console.log('停止播放');
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<block>
|
||||
<view class="audio-message {{isMine?'my-audio':''}}">
|
||||
<!-- 默认状态 未播放 -->
|
||||
<view class='audio' wx:if="{{!isPlay}}" bindtap='audioPlay' data-id="{{message.ID}}" >
|
||||
<image class="image {{isMine?'my-image':''}}" src="../../../../../static/images/sendingaudio.png"/> {{renderDom[0].second}}s
|
||||
</view>
|
||||
<!-- 当前正在播放状态 -->
|
||||
<view class='audio' wx:else data-value="{{message}}" bindtap='audioStop' data-id="{{message.ID}}" >
|
||||
<image class="image {{isMine?'my-image':''}}" src="../../../../../static/images/sendingaudio.png"/> {{renderDom[0].second}}s
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
.audio-message {
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 2px 10px 10px 10px;
|
||||
border: 1px solid #D9D9D9;
|
||||
}
|
||||
.my-audio {
|
||||
border-radius: 10px 2px 10px 10px;
|
||||
background: rgba(0,110,255,0.10);
|
||||
border: 1px solid rgba(0,110,255,0.30);
|
||||
}
|
||||
.audio {
|
||||
/*border-radius: 2px 10px 10px 10px;*/
|
||||
height: 60rpx;
|
||||
font-family: PingFangSC-Medium;
|
||||
font-size: 28rpx;
|
||||
color: #000000;
|
||||
line-height: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.image{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
.my-image{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform:rotate(180deg)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import formateTime from '../../../../../utils/formate-time';
|
||||
import constant from '../../../../../utils/constant';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
message: newVal,
|
||||
renderDom: this.parseCustom(newVal),
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
// 解析音视频通话消息
|
||||
extractCallingInfoFromMessage(message) {
|
||||
const callingmessage = JSON.parse(message.payload.data);
|
||||
if (callingmessage.businessID !== 1) {
|
||||
return '';
|
||||
}
|
||||
const objectData = JSON.parse(callingmessage.data);
|
||||
switch (callingmessage.actionType) {
|
||||
case 1: {
|
||||
if (objectData.call_end >= 0) {
|
||||
return `通话时长:${formateTime(objectData.call_end)}`;
|
||||
}
|
||||
if (objectData.data && objectData.data.cmd === 'switchToAudio') {
|
||||
return '切换语音通话';
|
||||
}
|
||||
if (objectData.data && objectData.data.cmd === 'switchToVideo') {
|
||||
return '切换视频通话';
|
||||
}
|
||||
return '发起通话';
|
||||
}
|
||||
case 2:
|
||||
return '取消通话';
|
||||
case 3:
|
||||
if (objectData.data && objectData.data.cmd === 'switchToAudio') {
|
||||
return '切换语音通话';
|
||||
}
|
||||
if (objectData.data && objectData.data.cmd === 'switchToVideo') {
|
||||
return '切换视频通话';
|
||||
}
|
||||
return '已接听';
|
||||
case 4:
|
||||
return '拒绝通话';
|
||||
case 5:
|
||||
if (objectData.data && objectData.data.cmd === 'switchToAudio') {
|
||||
return '切换语音通话';
|
||||
}
|
||||
if (objectData.data && objectData.data.cmd === 'switchToVideo') {
|
||||
return '切换视频通话';
|
||||
}
|
||||
return '无应答';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
parseCustom(message) {
|
||||
const { BUSINESS_ID_TEXT } = constant;
|
||||
// 群消息解析
|
||||
if (message.payload.data === BUSINESS_ID_TEXT.CREATE_GROUP) {
|
||||
const renderDom = [{
|
||||
type: 'group_create',
|
||||
text: message.payload.extension,
|
||||
}];
|
||||
return renderDom;
|
||||
}
|
||||
try {
|
||||
const customMessage = JSON.parse(message.payload.data);
|
||||
// 约定自定义消息的 data 字段作为区分,不解析的不进行展示
|
||||
if (customMessage.businessID === BUSINESS_ID_TEXT.ORDER) {
|
||||
const renderDom = [{
|
||||
type: 'order',
|
||||
name: 'custom',
|
||||
title: customMessage.title || '',
|
||||
imageUrl: customMessage.imageUrl || '',
|
||||
price: customMessage.price || 0,
|
||||
description: customMessage.description,
|
||||
}];
|
||||
return renderDom;
|
||||
}
|
||||
// 服务评价
|
||||
if (customMessage.businessID === BUSINESS_ID_TEXT.EVALUATION) {
|
||||
const renderDom = [{
|
||||
type: 'evaluation',
|
||||
title: message.payload.description,
|
||||
score: customMessage.score,
|
||||
description: customMessage.comment,
|
||||
}];
|
||||
return renderDom;
|
||||
}
|
||||
// native 自定义消息解析
|
||||
if (customMessage.businessID === BUSINESS_ID_TEXT.LINK) {
|
||||
const renderDom = [{
|
||||
type: 'text_link',
|
||||
text: customMessage.text,
|
||||
}];
|
||||
return renderDom;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
// 客服咨询
|
||||
try {
|
||||
const extension = JSON.parse(message.payload.extension);
|
||||
if (message.payload.data === BUSINESS_ID_TEXT.CONSULTION) {
|
||||
const renderDom = [{
|
||||
type: 'consultion',
|
||||
title: extension.title || '',
|
||||
item: extension.item || 0,
|
||||
description: extension.description,
|
||||
}];
|
||||
return renderDom;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
// 音视频通话消息解析
|
||||
try {
|
||||
const callingmessage = JSON.parse(message.payload.data);
|
||||
if (callingmessage.businessID === 1) {
|
||||
if (message.conversationType === wx.$TUIKitTIM.TYPES.CONV_GROUP) {
|
||||
if (message.payload.data.actionType === 5) {
|
||||
message.nick = message.payload.data.inviteeList ? message.payload.data.inviteeList.join(',') : message.from;
|
||||
}
|
||||
const _text = this.extractCallingInfoFromMessage(message);
|
||||
const groupText = `${_text}`;
|
||||
const renderDom = [{
|
||||
type: 'groupCalling',
|
||||
text: groupText,
|
||||
userIDList: [],
|
||||
}];
|
||||
return renderDom;
|
||||
}
|
||||
if (message.conversationType === wx.$TUIKitTIM.TYPES.CONV_C2C) {
|
||||
const c2cText = this.extractCallingInfoFromMessage(message);
|
||||
const renderDom = [{
|
||||
type: 'c2cCalling',
|
||||
text: c2cText,
|
||||
}];
|
||||
return renderDom;
|
||||
}
|
||||
}
|
||||
return [{
|
||||
type: 'notSupport',
|
||||
text: '[自定义消息]',
|
||||
}];
|
||||
} catch (error) {
|
||||
}
|
||||
},
|
||||
openLink(e) {
|
||||
if (e.currentTarget.dataset.value.key === '立即前往') {
|
||||
wx.navigateTo({
|
||||
url: '/pages/TUI-User-Center/webview/webview?url=https://cloud.tencent.com/act/pro/imnew?from=16975&wechatMobile',
|
||||
});
|
||||
} else if (e.currentTarget.dataset.value.key === '立即体验') {
|
||||
wx.navigateTo({
|
||||
url: '/pages/TUI-User-Center/webview/webview?url=https://cloud.tencent.com/document/product/269/68091',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<view>
|
||||
<view wx:if="{{renderDom[0].type ==='order'}}" class="custom-message {{isMine?'my-custom':''}}">
|
||||
<image class="custom-image" src="{{renderDom[0].imageUrl}}" />
|
||||
<view class="custom-content">
|
||||
<view class="custom-content-title">{{renderDom[0].title}}</view>
|
||||
<view class="custom-content-description">{{renderDom[0].description}}</view>
|
||||
<view class="custom-content-price">{{renderDom[0].price}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{renderDom[0].type ==='consultion'}}" class="custom-message {{isMine?'my-custom':''}}">
|
||||
<view class="custom-content">
|
||||
<view>
|
||||
<text class="custom-content-title">{{renderDom[0].title}}</text>
|
||||
<text class="custom-content-hyperlinks" bindtap="openLink" data-value="{{renderDom[0].hyperlinks_text}}">{{renderDom[0].hyperlinks_text.key}}</text>
|
||||
<view class="custom-content-description" wx:for="{{renderDom[0].item}}" wx:key="index" id="{{item.key}}">{{item.key}}
|
||||
</view>
|
||||
</view>
|
||||
<text class="custom-content-description">{{renderDom[0].description}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{renderDom[0].type ==='evaluation'}}" class="custom-message {{isMine?'my-custom':''}}">
|
||||
<view class="custom-content">
|
||||
<view class="custom-content-title">{{renderDom[0].title}}</view>
|
||||
<view class="custom-content-score">
|
||||
<image class="score-star" wx:for="{{renderDom[0].score}}" wx:key="*this" src="../../../../../static/images/star.png" />
|
||||
</view>
|
||||
<view class="custom-content-description">{{renderDom[0].description}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{renderDom[0].type === 'text_link'}}" class="message-body-span text-message">
|
||||
<view class="message-body-span-text">{{renderDom[0].text}}</view>
|
||||
<text class="message-body-span-link">查看详情>></text>
|
||||
</view>
|
||||
<view wx:if="{{renderDom[0].type ==='group_create'}}" class="custom-message {{isMine?'my-custom':''}}" >
|
||||
<view class="custom-content-text">{{renderDom[0].text}}</view>
|
||||
</view>
|
||||
<view wx:if="{{renderDom[0].type ==='c2cCalling' || renderDom[0].type ==='groupCalling'}}" class="custom-message {{isMine?'my-custom':''}}" >
|
||||
<view class="custom-content-text">{{renderDom[0].text}}</view>
|
||||
</view>
|
||||
<view wx:if="{{renderDom[0].type ==='notSupport'}}" class="message-body-span text-message" >
|
||||
<view class="message-body-span-text">{{renderDom[0].text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
.custom-message {
|
||||
background: #FBFBFB;
|
||||
border-radius: 4rpx 20rpx 20rpx 20rpx;
|
||||
display: flex;
|
||||
padding: 10rpx 24rpx;
|
||||
background-color: #fff;
|
||||
border: 1px solid #D9D9D9;
|
||||
}
|
||||
.my-custom {
|
||||
border-radius: 10px 2px 10px 10px;
|
||||
border: 1px solid rgba(0,110,255,0.30);
|
||||
}
|
||||
.custom-content-title {
|
||||
font-family: PingFangSC-Medium;
|
||||
max-width: 268rpx;
|
||||
height: 34rpx;
|
||||
font-size: 28rpx;
|
||||
color: #000000;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 12rpx;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-content-description {
|
||||
font-family: PingFangSC-Regular;
|
||||
width: 278rpx;
|
||||
line-height: 34rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
letter-spacing: 0;
|
||||
line-height: 40rpx;
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.custom-content-price {
|
||||
font-family: PingFangSC-Medium;
|
||||
line-height: 50rpx;
|
||||
color: #FF7201;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.custom-image {
|
||||
width: 135rpx;
|
||||
height: 135rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-right: 10rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.custom-content-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 12rpx;
|
||||
}
|
||||
.custom-content-score .score-star {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.text-message {
|
||||
display: inline-flex;
|
||||
max-width: 60vw;
|
||||
line-height: 52rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #F8F8F8;
|
||||
border: 1px solid #D9D9D9;
|
||||
border-radius: 2px 10px 10px 10px;
|
||||
}
|
||||
.my-text {
|
||||
border-radius: 10px 2px 10px 10px;
|
||||
border: 1px solid rgba(0,110,255,0.30);
|
||||
background: rgba(0,110,255,0.10);
|
||||
}
|
||||
.message-body-span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/*justify-content: flex-start;*/
|
||||
flex-wrap: wrap;
|
||||
outline: none;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
position: relative;
|
||||
max-width: 60vw;
|
||||
}
|
||||
.message-body-span-text {
|
||||
/* width: 434rpx; */
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #000000;
|
||||
letter-spacing: 0;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
.message-body-span-link {
|
||||
color: blue;
|
||||
}
|
||||
.message-body-span-link {
|
||||
color: blue;
|
||||
}
|
||||
.message-body-span-link {
|
||||
color: blue;
|
||||
}
|
||||
.custom-content-text{
|
||||
font-family: PingFangSC-Regular;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
font-size: 28rpx;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.custom-content-hyperlinks{
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
line-height: 40rpx;
|
||||
font-size: 28rpx;
|
||||
color: #006EFF;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { emojiName, emojiUrl, emojiMap } from '../../../../../utils/emojiMap';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
emojiList: [],
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
for (let i = 0; i < emojiName.length; i++) {
|
||||
this.data.emojiList.push({
|
||||
emojiName: emojiName[i],
|
||||
url: emojiUrl + emojiMap[emojiName[i]],
|
||||
});
|
||||
}
|
||||
this.setData({
|
||||
emojiList: this.data.emojiList,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
handleEnterEmoji(event) {
|
||||
this.triggerEvent('enterEmoji', {
|
||||
message: event.currentTarget.dataset.name,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<scroll-view scroll-y="true" enable-flex="true" class="TUI-Emoji">
|
||||
<view class="TUI-emoji-image" wx:for="{{emojiList}}" wx:key="index" >
|
||||
<image data-name="{{item.emojiName}}" src="{{item.url}}" bindtap="handleEnterEmoji" />
|
||||
</view>>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,19 @@
|
||||
.TUI-Emoji {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: 4vw;
|
||||
}
|
||||
|
||||
.TUI-emoji-image {
|
||||
width: 9vw;
|
||||
height: 9vw;
|
||||
margin: 2vw;
|
||||
}
|
||||
|
||||
.TUI-emoji-image > image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
renderDom: this.parseFace(newVal),
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
renderDom: [],
|
||||
percent: 0,
|
||||
faceUrl: 'https://web.sdk.qcloud.com/im/assets/face-elem/',
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
// 解析face 消息
|
||||
parseFace(message) {
|
||||
// 兼容android的大表情格式
|
||||
if (message.payload.data.indexOf('@2x') < 0) {
|
||||
message.payload.data = `${message.payload.data}@2x`;
|
||||
}
|
||||
const renderDom = {
|
||||
src: `${this.data.faceUrl + message.payload.data}.png`,
|
||||
};
|
||||
return renderDom;
|
||||
},
|
||||
|
||||
previewImage() {
|
||||
wx.previewImage({
|
||||
current: this.data.renderDom[0].src, // 当前显示图片的http链接
|
||||
urls: [this.data.renderDom[0].src],
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<view class="TUI-faceMessage" bindtap="previewImage">
|
||||
<image class="face-message" src="{{renderDom.src}}" />
|
||||
</view>
|
||||
@@ -0,0 +1,13 @@
|
||||
.TUI-faceMessage {
|
||||
width: 150px;
|
||||
height: 110px;
|
||||
max-width: 60vw;
|
||||
}
|
||||
.face-message {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
}
|
||||
.my-image {
|
||||
border-radius: 10px 2px 10px 10px;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
filePayload: newVal.payload,
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
Show: false,
|
||||
filePayload: {},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
download() {
|
||||
this.setData({
|
||||
Show: true,
|
||||
});
|
||||
},
|
||||
downloadConfirm() {
|
||||
wx.downloadFile({
|
||||
url: this.data.filePayload.fileUrl,
|
||||
success(res) {
|
||||
const filePath = res.tempFilePath;
|
||||
wx.openDocument({
|
||||
filePath,
|
||||
success() {
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.setData({
|
||||
Show: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<view class="TUI-fileMessage">
|
||||
<view class="fileMessage">
|
||||
<view class="fileMessage-box">
|
||||
<image class="file-icon" src="../../../../static/images/file.png" />
|
||||
<label bindtap="download" class="file-title">{{filePayload.fileName}}</label>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pop" wx:if="{{Show}}">
|
||||
<view class="text-box">
|
||||
<text class="download-confirm" catchtap="downloadConfirm">下载</text>
|
||||
</view>
|
||||
<view class="text-box">
|
||||
<text class="abandon" bindtap="cancel">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,57 @@
|
||||
.TUI-fileMessage {
|
||||
display: flex;
|
||||
padding: 10rpx 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 2px 10px 10px 10px;
|
||||
border: 1px solid #D9D9D9;
|
||||
}
|
||||
.fileMessage{
|
||||
display: flex;
|
||||
}
|
||||
.fileMessage-box{
|
||||
display: flex;
|
||||
background: white;
|
||||
align-items: center;
|
||||
height: 150rpx;
|
||||
}
|
||||
.file-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
.pop{
|
||||
position: fixed;
|
||||
width: 50%;
|
||||
bottom: 400rpx;
|
||||
margin-left: 90rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 99999;
|
||||
}
|
||||
.text-box{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 112rpx;
|
||||
}
|
||||
.download-confirm{
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 16px;
|
||||
color: #E85454;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
line-height: 22px;
|
||||
}
|
||||
.abandon{
|
||||
opacity: 0.8;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 16px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
line-height: 22px;
|
||||
}
|
||||
.file-title {
|
||||
max-width: 53vw;
|
||||
display: inline;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { parseImage } from '../../../../../utils/message-parse';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
renderDom: parseImage(newVal),
|
||||
percent: newVal.percent,
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
renderDom: [],
|
||||
percent: 0,
|
||||
showSave: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
previewImage() {
|
||||
wx.previewImage({
|
||||
current: this.data.renderDom[0].src, // 当前显示图片的http链接
|
||||
urls: [this.data.renderDom[0].src], // 图片链接必须是数组
|
||||
success: () => {
|
||||
this.setData({
|
||||
showSave: true,
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({
|
||||
showSave: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<view class="TUI-ImageMessage" bindtap="previewImage">
|
||||
<image class="image-message {{isMine?'my-image':''}}" mode="aspectFill" src="{{renderDom[0].src}}" />
|
||||
<image wx:if="{{showSave}}" class="image-message {{isMine?'my-image':''}}" mode="aspectFill" src="{{renderDom[0].src}}" show-menu-by-longpress="{{true}}"/>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.TUI-ImageMessage {
|
||||
width: 150px;
|
||||
}
|
||||
.image-message {
|
||||
width: 100%;
|
||||
max-height: 300rpx;
|
||||
height: 300rpx;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
}
|
||||
.my-image {
|
||||
height: 300rpx;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
}
|
||||
.big-image {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fix;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
message: newVal,
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<!--TUI-CustomerService/components/tui-chat/message-elements/relay-message/index.wxml-->
|
||||
<text class="message-body-span text-message">[聊天记录]</text>
|
||||
@@ -0,0 +1,21 @@
|
||||
/* TUI-CustomerService/components/tui-chat/message-elements/relay-message/index.wxss */
|
||||
.message-body-span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/*justify-content: flex-start;*/
|
||||
flex-wrap: wrap;
|
||||
outline: none;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
position: relative;
|
||||
max-width: 60vw;
|
||||
}
|
||||
.text-message {
|
||||
display: inline-flex;
|
||||
max-width: 60vw;
|
||||
line-height: 52rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #F8F8F8;
|
||||
border: 1px solid #D9D9D9;
|
||||
border-radius: 2px 10px 10px 10px;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
message: newVal,
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
message: ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
resendMessage(e) {
|
||||
this.triggerEvent('resendMessage', {
|
||||
message: e.currentTarget.dataset.value.payload.text
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<view class="revoke" data-value="{{message}}">
|
||||
<label class="name" wx:if="{{message.flow === 'in'}}">{{message.nick || message.from}}</label>
|
||||
<label class="name" wx:else>你</label>
|
||||
<span class="name">撤回了一条消息</span>
|
||||
<span class="edit" wx:if="{{message.flow === 'out' && message.type === 'TIMTextElem'}}" bindtap="resendMessage" data-value="{{message}}">重新编辑</span>
|
||||
</view>
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
.revoke{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center
|
||||
}
|
||||
.name {
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
padding: 6px 0px;
|
||||
}
|
||||
.edit{
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #006eff;
|
||||
letter-spacing: 0;
|
||||
padding-left: 8px
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { parseGroupSystemNotice } from '../../../../../utils/message-parse';
|
||||
import { caculateTimeago } from '../../../../../utils/common';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
message: newVal,
|
||||
messageTime: caculateTimeago(newVal.time * 1000),
|
||||
renderDom: parseGroupSystemNotice(newVal),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
message: {},
|
||||
options: '处理',
|
||||
messageTime: '',
|
||||
renderDom: '',
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
// 在组件实例进入页面节点树时执行
|
||||
},
|
||||
detached() {
|
||||
// 在组件实例被从页面节点树移除时执行
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
handleClick() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['同意', '拒绝'],
|
||||
success: (res) => {
|
||||
this.triggerEvent('changeSystemMessageList', {
|
||||
message: this.data.message,
|
||||
});
|
||||
const option = {
|
||||
handleAction: 'Agree',
|
||||
handleMessage: '欢迎进群',
|
||||
message: this.data.message,
|
||||
};
|
||||
if (res.tapIndex === 1) {
|
||||
this.triggerEvent('changeSystemMessageList', {
|
||||
message: this.data.message,
|
||||
});
|
||||
option.handleAction = 'Reject';
|
||||
option.handleMessage = '拒绝申请';
|
||||
}
|
||||
wx.$TUIKit.handleGroupApplication(option)
|
||||
.then(() => {
|
||||
wx.showToast({ title: option.handleAction === 'Agree' ? '已同意申请' : '已拒绝申请' });
|
||||
})
|
||||
.catch((error) => {
|
||||
wx.showToast({
|
||||
title: error.message || '处理失败',
|
||||
icon: 'none',
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<view class="container">
|
||||
<view wx:if="{{message.payload.operationType === 1}}" class="card handle">
|
||||
<view>
|
||||
<view class="time" >{{messageTime}}</view>
|
||||
{{renderDom}}
|
||||
</view>
|
||||
<view class="choose">
|
||||
<view class="button" bindtap="handleClick"> {{options}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card" wx:else>
|
||||
<view class="time">{{messageTime}}</view>
|
||||
{{renderDom}}
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,24 @@
|
||||
.handle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card{
|
||||
font-size: 14px;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #abdcff;
|
||||
background-color: #f0faff;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.time {
|
||||
|
||||
}
|
||||
.button{
|
||||
color: blue;
|
||||
border-radius: 8px;
|
||||
line-height: 30px;
|
||||
font-size: 16px;
|
||||
width: 70px;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { parseText } from '../../../../../utils/message-parse';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
renderDom: parseText(newVal),
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
// 在组件实例进入页面节点树时执行
|
||||
},
|
||||
detached() {
|
||||
// 在组件实例被从页面节点树移除时执行
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<view class="text-message {{isMine?'my-text':''}}" >
|
||||
<view class="message-body-span" wx:for="{{renderDom}}" wx:key="index">
|
||||
<span class="message-body-span-text" wx:if="{{item.name === 'span'}}">{{item.text}}</span>
|
||||
<image wx:if="{{item.name === 'img'}}" class="emoji-icon" src="{{item.src}}" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,47 @@
|
||||
.text-message {
|
||||
max-width: 60vw;
|
||||
line-height: 52rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #F8F8F8;
|
||||
border: 1px solid #D9D9D9;
|
||||
border-radius: 2px 10px 10px 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.my-text {
|
||||
border-radius: 10px 2px 10px 10px;
|
||||
border: 1px solid rgba(0,110,255,0.30);
|
||||
background: rgba(0,110,255,0.10);
|
||||
}
|
||||
.message-body-span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/*justify-content: flex-start;*/
|
||||
flex-wrap: wrap;
|
||||
outline: none;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
position: relative;
|
||||
max-width: 60vw;
|
||||
}
|
||||
.message-body-span-text {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
letter-spacing: 0;
|
||||
line-height: 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.message-body-span-image {
|
||||
display: inline-block;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
.emoji-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { parseGroupTip } from '../../../../../utils/message-parse';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
renderDom: parseGroupTip(newVal),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
// 在组件实例进入页面节点树时执行
|
||||
},
|
||||
detached() {
|
||||
// 在组件实例被从页面节点树移除时执行
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<view class="tip-message">
|
||||
<view class="text-message">{{renderDom[0].text}}</view>
|
||||
</view>
|
||||
@@ -0,0 +1,9 @@
|
||||
.tip-message {
|
||||
width: 100%;
|
||||
}
|
||||
.text-message {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { parseVideo } from '../../../../../utils/message-parse';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
message: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
message: newVal,
|
||||
renderDom: parseVideo(newVal),
|
||||
});
|
||||
},
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
message: {},
|
||||
showSaveFlag: Number,
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
showVideoFullScreenChange(event) {
|
||||
if (event.detail.fullScreen) {
|
||||
this.setData({
|
||||
showSaveFlag: 1,
|
||||
});
|
||||
} else {
|
||||
this.setData({
|
||||
showSaveFlag: 2,
|
||||
});
|
||||
}
|
||||
},
|
||||
// 1代表当前状态处于全屏,2代表当前状态不处于全屏。
|
||||
handleLongPress(e) {
|
||||
if (this.data.showSaveFlag === 1) {
|
||||
wx.showModal({
|
||||
content: '确认保存该视频?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.downloadFile({
|
||||
url: this.data.message.payload.videoUrl,
|
||||
success(res) {
|
||||
// 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
|
||||
if (res.statusCode === 200) {
|
||||
wx.saveVideoToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success() {
|
||||
wx.showToast({
|
||||
title: '保存成功!',
|
||||
duration: 800,
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
fail(error) {
|
||||
wx.showToast({
|
||||
title: '保存失败!',
|
||||
duration: 800,
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<view class="video-message" >
|
||||
<video class="video-box {{isMine?'my-video':''}}" src="{{renderDom[0].src}}"
|
||||
poster="{{message.payload.thumbUrl}}" error="videoError" bindfullscreenchange="showVideoFullScreenChange"
|
||||
bind:longpress="handleLongPress"></video>
|
||||
</view>
|
||||
@@ -0,0 +1,12 @@
|
||||
.video-message {
|
||||
width: 150px;
|
||||
height: 110px;
|
||||
max-width: 60vw;
|
||||
}
|
||||
.video-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.my-video {
|
||||
border-radius: 10px 2px 10px 10px;
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
import logger from '../../../../utils/logger';
|
||||
import constant from '../../../../utils/constant';
|
||||
// eslint-disable-next-line no-undef
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
conversation: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer(newVal) {
|
||||
this.setData({
|
||||
conversation: newVal,
|
||||
});
|
||||
},
|
||||
},
|
||||
hasCallKit: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer(hasCallKit) {
|
||||
this.setData({
|
||||
hasCallKit,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
conversation: {},
|
||||
message: '',
|
||||
extensionArea: false,
|
||||
sendMessageBtn: false,
|
||||
displayFlag: '',
|
||||
isAudio: false,
|
||||
bottomVal: 0,
|
||||
startPoint: 0,
|
||||
popupToggle: false,
|
||||
isRecording: false,
|
||||
canSend: true,
|
||||
text: '按住说话',
|
||||
title: ' ',
|
||||
notShow: false,
|
||||
isShow: true,
|
||||
commonFunction: [
|
||||
{ name: '常用语', key: '0' },
|
||||
{ name: '发送订单', key: '1' },
|
||||
{ name: '服务评价', key: '2' },
|
||||
],
|
||||
displayServiceEvaluation: false,
|
||||
showErrorImageFlag: 0,
|
||||
messageList: [],
|
||||
isFirstSendTyping: true,
|
||||
time: 0,
|
||||
focus: false,
|
||||
isEmoji: false,
|
||||
fileList: [],
|
||||
hasCallKit: false,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
// 加载声音录制管理器
|
||||
this.recorderManager = wx.getRecorderManager();
|
||||
this.recorderManager.onStop((res) => {
|
||||
wx.hideLoading();
|
||||
if (this.data.canSend) {
|
||||
if (res.duration < 1000) {
|
||||
wx.showToast({
|
||||
title: '录音时间太短',
|
||||
icon: 'none',
|
||||
});
|
||||
} else {
|
||||
// res.tempFilePath 存储录音文件的临时路径
|
||||
const message = wx.$TUIKit.createAudioMessage({
|
||||
to: this.getToAccount(),
|
||||
conversationType: this.data.conversation.type,
|
||||
payload: {
|
||||
file: res,
|
||||
},
|
||||
});
|
||||
this.$sendTIMMessage(message);
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
startPoint: 0,
|
||||
popupToggle: false,
|
||||
isRecording: false,
|
||||
canSend: true,
|
||||
title: ' ',
|
||||
text: '按住说话',
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
// 获取消息列表来判断是否发送正在输入状态
|
||||
getMessageList(conversation) {
|
||||
wx.$TUIKit.getMessageList({
|
||||
conversationID: conversation.conversationID,
|
||||
nextReqMessageID: this.data.nextReqMessageID,
|
||||
count: 15,
|
||||
}).then((res) => {
|
||||
const { messageList } = res.data;
|
||||
this.setData({
|
||||
messageList,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 打开录音开关
|
||||
switchAudio() {
|
||||
this.setData({
|
||||
isAudio: !this.data.isAudio,
|
||||
isEmoji: false,
|
||||
text: '按住说话',
|
||||
focus: false,
|
||||
});
|
||||
},
|
||||
|
||||
// 长按录音
|
||||
handleLongPress(e) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'messageType',
|
||||
ext1: 'messageType-audio',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
this.recorderManager.start({
|
||||
duration: 60000, // 录音的时长,单位 ms,最大值 600000(10 分钟)
|
||||
sampleRate: 44100, // 采样率
|
||||
numberOfChannels: 1, // 录音通道数
|
||||
encodeBitRate: 192000, // 编码码率
|
||||
format: 'aac', // 音频格式,选择此格式创建的音频消息,可以在即时通信 IM 全平台(Android、iOS、微信小程序和Web)互通
|
||||
});
|
||||
this.setData({
|
||||
startPoint: e.touches[0],
|
||||
title: '正在录音',
|
||||
// isRecording : true,
|
||||
// canSend: true,
|
||||
notShow: true,
|
||||
isShow: false,
|
||||
isRecording: true,
|
||||
popupToggle: true,
|
||||
});
|
||||
},
|
||||
|
||||
// 录音时的手势上划移动距离对应文案变化
|
||||
handleTouchMove(e) {
|
||||
if (this.data.isRecording) {
|
||||
if (this.data.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 100) {
|
||||
this.setData({
|
||||
text: '抬起停止',
|
||||
title: '松开手指,取消发送',
|
||||
canSend: false,
|
||||
});
|
||||
} else if (this.data.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 20) {
|
||||
this.setData({
|
||||
text: '抬起停止',
|
||||
title: '上划可取消',
|
||||
canSend: true,
|
||||
});
|
||||
} else {
|
||||
this.setData({
|
||||
text: '抬起停止',
|
||||
title: '正在录音',
|
||||
canSend: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 手指离开页面滑动
|
||||
handleTouchEnd() {
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
popupToggle: false,
|
||||
|
||||
});
|
||||
wx.hideLoading();
|
||||
this.recorderManager.stop();
|
||||
},
|
||||
// 选中表情消息
|
||||
handleEmoji() {
|
||||
let targetFlag = 'emoji';
|
||||
if (this.data.displayFlag === 'emoji') {
|
||||
targetFlag = '';
|
||||
}
|
||||
this.setData({
|
||||
isAudio: false,
|
||||
isEmoji: true,
|
||||
displayFlag: targetFlag,
|
||||
focus: false,
|
||||
});
|
||||
},
|
||||
|
||||
// 选自定义消息
|
||||
handleExtensions() {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'chooseExtensions',
|
||||
ext1: 'chooseExtensions',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
let targetFlag = 'extension';
|
||||
if (this.data.displayFlag === 'extension') {
|
||||
targetFlag = '';
|
||||
}
|
||||
this.setData({
|
||||
displayFlag: targetFlag,
|
||||
});
|
||||
},
|
||||
|
||||
error(e) {
|
||||
console.log(e.detail);
|
||||
},
|
||||
|
||||
handleSendPicture() {
|
||||
this.sendMediaMessage('camera', 'image');
|
||||
},
|
||||
|
||||
handleSendImage() {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'messageType',
|
||||
ext1: 'messageType-image',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
this.sendMediaMessage('album', 'image');
|
||||
},
|
||||
|
||||
sendMediaMessage(type, mediaType) {
|
||||
const { fileList } = this.data;
|
||||
wx.chooseMedia({
|
||||
count: 9,
|
||||
sourceType: [type],
|
||||
mediaType: [mediaType],
|
||||
success: (res) => {
|
||||
const mediaInfoList = res.tempFiles;
|
||||
mediaInfoList.forEach((mediaInfo) => {
|
||||
fileList.push({ type: res.type, tempFiles: [{ tempFilePath: mediaInfo.tempFilePath }] });
|
||||
});
|
||||
fileList.forEach((file) => {
|
||||
if (file.type === 'image') {
|
||||
this.handleSendImageMessage(file);
|
||||
}
|
||||
if (file.type === 'video') {
|
||||
this.handleSendVideoMessage(file);
|
||||
}
|
||||
});
|
||||
this.data.fileList = [];
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 发送图片消息
|
||||
handleSendImageMessage(file) {
|
||||
const message = wx.$TUIKit.createImageMessage({
|
||||
to: this.getToAccount(),
|
||||
conversationType: this.data.conversation.type,
|
||||
payload: {
|
||||
file,
|
||||
},
|
||||
onProgress: (percent) => {
|
||||
message.percent = percent;
|
||||
},
|
||||
});
|
||||
this.$sendTIMMessage(message);
|
||||
},
|
||||
|
||||
// 发送视频消息
|
||||
handleSendVideoMessage(file) {
|
||||
const message = wx.$TUIKit.createVideoMessage({
|
||||
to: this.getToAccount(),
|
||||
conversationType: this.data.conversation.type,
|
||||
payload: {
|
||||
file,
|
||||
},
|
||||
onProgress: (percent) => {
|
||||
message.percent = percent;
|
||||
},
|
||||
});
|
||||
this.$sendTIMMessage(message);
|
||||
},
|
||||
|
||||
handleShootVideo() {
|
||||
this.sendMediaMessage('camera', 'video');
|
||||
},
|
||||
|
||||
handleSendVideo() {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'messageType',
|
||||
ext1: 'messageType-video',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
this.sendMediaMessage('album', 'video');
|
||||
},
|
||||
|
||||
handleCommonFunctions(e) {
|
||||
switch (e.target.dataset.function.key) {
|
||||
case '0':
|
||||
this.setData({
|
||||
displayCommonWords: true,
|
||||
});
|
||||
break;
|
||||
case '1':
|
||||
this.setData({
|
||||
displayOrderList: true,
|
||||
});
|
||||
break;
|
||||
case '2':
|
||||
this.setData({
|
||||
displayServiceEvaluation: true,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
handleSendOrder() {
|
||||
this.setData({
|
||||
displayOrderList: true,
|
||||
});
|
||||
},
|
||||
|
||||
appendMessage(e) {
|
||||
this.setData({
|
||||
message: this.data.message + e.detail.message,
|
||||
sendMessageBtn: true,
|
||||
});
|
||||
},
|
||||
|
||||
getToAccount() {
|
||||
if (!this.data.conversation || !this.data.conversation.conversationID) {
|
||||
return '';
|
||||
}
|
||||
switch (this.data.conversation.type) {
|
||||
case wx.$TUIKitTIM.TYPES.CONV_C2C:
|
||||
return this.data.conversation.conversationID.replace(wx.$TUIKitTIM.TYPES.CONV_C2C, '');
|
||||
case wx.$TUIKitTIM.TYPES.CONV_GROUP:
|
||||
return this.data.conversation.conversationID.replace(wx.$TUIKitTIM.TYPES.CONV_GROUP, '');
|
||||
default:
|
||||
return this.data.conversation.conversationID;
|
||||
}
|
||||
},
|
||||
async handleCheckAuthorize(e) {
|
||||
const type = e.currentTarget.dataset.value;
|
||||
wx.getSetting({
|
||||
success: async (res) => {
|
||||
const isRecord = res.authSetting['scope.record'];
|
||||
const isCamera = res.authSetting['scope.camera'];
|
||||
if (!isRecord && type === 1) {
|
||||
const title = '麦克风权限授权';
|
||||
const content = '使用语音通话,需要在设置中对麦克风进行授权允许';
|
||||
try {
|
||||
await wx.authorize({ scope: 'scope.record' });
|
||||
this.handleCalling(e);
|
||||
} catch (e) {
|
||||
this.handleShowModal(title, content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((!isRecord || !isCamera) && type === 2) {
|
||||
const title = '麦克风、摄像头权限授权';
|
||||
const content = '使用视频通话,需要在设置中对麦克风、摄像头进行授权允许';
|
||||
try {
|
||||
await wx.authorize({ scope: 'scope.record' });
|
||||
await wx.authorize({ scope: 'scope.camera' });
|
||||
this.handleCalling(e);
|
||||
} catch (e) {
|
||||
this.handleShowModal(title, content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.handleCalling(e);
|
||||
},
|
||||
});
|
||||
},
|
||||
handleShowModal(title, content) {
|
||||
wx.showModal({
|
||||
title,
|
||||
content,
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
handleCalling(e) {
|
||||
if (!this.data.hasCallKit) {
|
||||
wx.showToast({
|
||||
title: '请先集成 TUICallKit 组件',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const type = e.currentTarget.dataset.value;
|
||||
const conversationType = this.data.conversation.type;
|
||||
if (conversationType === wx.$TUIKitTIM.TYPES.CONV_GROUP) {
|
||||
if (type === 1) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'audioCall',
|
||||
ext1: 'audioCall-group',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
} else if (type === 2) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'videoCall',
|
||||
ext1: 'videoCall-group',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
}
|
||||
this.triggerEvent('handleCall', {
|
||||
type,
|
||||
conversationType,
|
||||
});
|
||||
}
|
||||
if (conversationType === wx.$TUIKitTIM.TYPES.CONV_C2C) {
|
||||
const { userID } = this.data.conversation.userProfile;
|
||||
if (type === 1) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'audioCall',
|
||||
ext1: 'audioCall-1v1',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
} else if (type === 2) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'videoCall',
|
||||
ext1: 'videoCall-1v1',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
}
|
||||
this.triggerEvent('handleCall', {
|
||||
conversationType,
|
||||
type,
|
||||
userID,
|
||||
});
|
||||
}
|
||||
this.setData({
|
||||
displayFlag: '',
|
||||
});
|
||||
},
|
||||
|
||||
sendTextMessage(msg, flag) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'messageType',
|
||||
ext1: 'messageType-text',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
const to = this.getToAccount();
|
||||
const text = flag ? msg : this.data.message;
|
||||
const { FEAT_NATIVE_CODE } = constant;
|
||||
const message = wx.$TUIKit.createTextMessage({
|
||||
to,
|
||||
conversationType: this.data.conversation.type,
|
||||
payload: {
|
||||
text,
|
||||
},
|
||||
cloudCustomData: JSON.stringify({ messageFeature:
|
||||
{
|
||||
needTyping: FEAT_NATIVE_CODE.FEAT_TYPING,
|
||||
version: FEAT_NATIVE_CODE.NATIVE_VERSION,
|
||||
},
|
||||
}),
|
||||
});
|
||||
this.setData({
|
||||
message: '',
|
||||
sendMessageBtn: false,
|
||||
});
|
||||
this.$sendTIMMessage(message);
|
||||
},
|
||||
|
||||
// 监听输入框value值变化
|
||||
onInputValueChange(event) {
|
||||
if (event.detail.message || event.detail.value) {
|
||||
this.setData({
|
||||
message: event.detail.message || event.detail.value,
|
||||
sendMessageBtn: true,
|
||||
});
|
||||
} else {
|
||||
this.setData({
|
||||
sendMessageBtn: false,
|
||||
});
|
||||
}
|
||||
event.detail.value && this.sendTypingStatusMessage();
|
||||
},
|
||||
|
||||
// 发送正在输入状态消息
|
||||
sendTypingStatusMessage() {
|
||||
const { BUSINESS_ID_TEXT, FEAT_NATIVE_CODE } = constant;
|
||||
// 创建正在输入状态消息, "typingStatus":1,正在输入中1, 输入结束0, "version": 1 兼容老版本,userAction:0, // 14表示正在输入,actionParam:"EIMAMSG_InputStatus_Ing" //"EIMAMSG_InputStatus_Ing" 表示正在输入, "EIMAMSG_InputStatus_End" 表示输入结束
|
||||
const typingMessage = wx.$TUIKit.createCustomMessage({
|
||||
to: this.getToAccount(),
|
||||
conversationType: this.data.conversation.type,
|
||||
payload: {
|
||||
data: JSON.stringify({
|
||||
businessID: BUSINESS_ID_TEXT.USER_TYPING,
|
||||
typingStatus: FEAT_NATIVE_CODE.ISTYPING_STATUS,
|
||||
version: FEAT_NATIVE_CODE.NATIVE_VERSION,
|
||||
userAction: FEAT_NATIVE_CODE.ISTYPING_ACTION,
|
||||
actionParam: constant.TYPE_INPUT_STATUS_ING,
|
||||
}),
|
||||
description: '',
|
||||
extension: '',
|
||||
},
|
||||
cloudCustomData: JSON.stringify({
|
||||
messageFeature: {
|
||||
needTyping: FEAT_NATIVE_CODE.FEAT_TYPING,
|
||||
version: FEAT_NATIVE_CODE.NATIVE_VERSION,
|
||||
},
|
||||
}),
|
||||
});
|
||||
// 在消息列表中过滤出对方的消息,并且获取最新消息的时间。
|
||||
const inList = this.data.messageList.filter(item => item.flow === 'in');
|
||||
if (inList.length === 0) return;
|
||||
const sortList = inList.sort((firstItem, secondItem) => secondItem.time - firstItem.time);
|
||||
const newMessageTime = sortList[0].time * 1000;
|
||||
// 发送正在输入状态消息的触发条件。
|
||||
const isSendTypingMessage = this.data.messageList.every((item) => {
|
||||
try {
|
||||
const sendTypingMessage = JSON.parse(item.cloudCustomData);
|
||||
return sendTypingMessage.messageFeature.needTyping;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// 获取当前编辑时间,与收到对方最新的一条消息时间相比,时间小于30s则发送正在输入状态消息/
|
||||
const now = new Date().getTime();
|
||||
const timeDifference = (now - newMessageTime);
|
||||
|
||||
if (isSendTypingMessage && timeDifference > (1000 * 30)) return;
|
||||
if (this.data.isFirstSendTyping) {
|
||||
this.$sendTypingMessage(typingMessage);
|
||||
this.setData({
|
||||
isFirstSendTyping: false,
|
||||
});
|
||||
} else {
|
||||
this.data.time = setTimeout(() => {
|
||||
this.$sendTypingMessage(typingMessage);
|
||||
}, (1000 * 4));
|
||||
}
|
||||
},
|
||||
// 监听是否获取焦点,有焦点则向父级传值,动态改变input组件的高度。
|
||||
inputBindFocus(event) {
|
||||
this.setData({
|
||||
focus: true,
|
||||
});
|
||||
this.getMessageList(this.data.conversation);
|
||||
this.triggerEvent('pullKeysBoards', {
|
||||
event,
|
||||
});
|
||||
// 有焦点则关闭除键盘之外的操作界面,例如表情组件。
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
// 监听是否失去焦点
|
||||
inputBindBlur(event) {
|
||||
const { BUSINESS_ID_TEXT, FEAT_NATIVE_CODE } = constant;
|
||||
const typingMessage = wx.$TUIKit.createCustomMessage({
|
||||
to: this.getToAccount(),
|
||||
conversationType: this.data.conversation.type,
|
||||
payload: {
|
||||
data: JSON.stringify({
|
||||
businessID: BUSINESS_ID_TEXT.USER_TYPING,
|
||||
typingStatus: FEAT_NATIVE_CODE.NOTTYPING_STATUS,
|
||||
version: FEAT_NATIVE_CODE.NATIVE_VERSION,
|
||||
userAction: FEAT_NATIVE_CODE.NOTTYPING_ACTION,
|
||||
actionParam: constant.TYPE_INPUT_STATUS_END,
|
||||
}),
|
||||
cloudCustomData: JSON.stringify({ messageFeature:
|
||||
{
|
||||
needTyping: FEAT_NATIVE_CODE.FEAT_TYPING,
|
||||
version: FEAT_NATIVE_CODE.NATIVE_VERSION,
|
||||
},
|
||||
}),
|
||||
description: '',
|
||||
extension: '',
|
||||
},
|
||||
});
|
||||
this.$sendTypingMessage(typingMessage);
|
||||
this.setData({
|
||||
isFirstSendTyping: true,
|
||||
});
|
||||
clearTimeout(this.data.time);
|
||||
this.triggerEvent('downKeysBoards', {
|
||||
event,
|
||||
});
|
||||
},
|
||||
|
||||
$handleSendTextMessage(event) {
|
||||
this.sendTextMessage(event.detail.message, true);
|
||||
this.setData({
|
||||
displayCommonWords: false,
|
||||
});
|
||||
},
|
||||
|
||||
$handleSendCustomMessage(e) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'messageType',
|
||||
ext1: 'messageType-custom',
|
||||
ext2: wx.$chat_reportType,
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
const message = wx.$TUIKit.createCustomMessage({
|
||||
to: this.getToAccount(),
|
||||
conversationType: this.data.conversation.type,
|
||||
payload: e.detail.payload,
|
||||
});
|
||||
this.$sendTIMMessage(message);
|
||||
this.setData({
|
||||
displayOrderList: false,
|
||||
displayCommonWords: false,
|
||||
});
|
||||
},
|
||||
|
||||
$handleCloseCards(e) {
|
||||
switch (e.detail.key) {
|
||||
case '0':
|
||||
this.setData({
|
||||
displayCommonWords: false,
|
||||
});
|
||||
break;
|
||||
case '1':
|
||||
this.setData({
|
||||
displayOrderList: false,
|
||||
});
|
||||
break;
|
||||
case '2':
|
||||
this.setData({
|
||||
displayServiceEvaluation: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
// 发送正在输入消息
|
||||
$sendTypingMessage(message) {
|
||||
wx.$TUIKit.sendMessage(message, {
|
||||
onlineUserOnly: true,
|
||||
});
|
||||
},
|
||||
|
||||
$sendTIMMessage(message) {
|
||||
this.triggerEvent('sendMessage', {
|
||||
message,
|
||||
});
|
||||
wx.$TUIKit.sendMessage(message, {
|
||||
offlinePushInfo: {
|
||||
disablePush: true,
|
||||
},
|
||||
}).then(() => {
|
||||
const firstSendMessage = wx.getStorageSync('isFirstSendMessage');
|
||||
if (firstSendMessage) {
|
||||
wx.aegis.reportEvent({
|
||||
name: 'sendMessage',
|
||||
ext1: 'sendMessage-success',
|
||||
ext2: 'imTuikitExternal',
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.log(`| TUI-chat | message-input | sendMessageError: ${error.code} `);
|
||||
wx.aegis.reportEvent({
|
||||
name: 'sendMessage',
|
||||
ext1: `sendMessage-failed#error: ${error}`,
|
||||
ext2: 'imTuikitExternal',
|
||||
ext3: wx.$chat_SDKAppID,
|
||||
});
|
||||
this.triggerEvent('showMessageErrorImage', {
|
||||
showErrorImageFlag: error.code,
|
||||
message,
|
||||
});
|
||||
});
|
||||
this.setData({
|
||||
displayFlag: '',
|
||||
});
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.setData({
|
||||
displayFlag: '',
|
||||
});
|
||||
},
|
||||
|
||||
handleServiceEvaluation() {
|
||||
this.setData({
|
||||
displayServiceEvaluation: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"Emoji": "../MessageElements/Emoji/index",
|
||||
"CommonWords": "../MessagePrivate/CommonWords/index",
|
||||
"OrderList": "../MessagePrivate/OrderList/index",
|
||||
"ServiceEvaluation": "../MessagePrivate/ServiceEvaluation/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<view class="TUI-message-input-container">
|
||||
<view class="TUI-commom-function">
|
||||
<view class="TUI-commom-function-item" wx:for="{{commonFunction}}" wx:key="index" data-function="{{item}}" bindtap="handleCommonFunctions">{{item.name}}</view>
|
||||
</view>
|
||||
<view class="TUI-message-input">
|
||||
<image class="TUI-icon" bindtap="switchAudio" src="{{isAudio ? '../../../../static/assets/keyboard.svg' : '../../../../static/assets/audio.svg'}}" />
|
||||
<view wx:if="{{!isAudio || isEmoji}}" class="TUI-message-input-main {{ focus && 'TUI-message-input-main-focus'}}" >
|
||||
<textarea class="TUI-message-input-area" adjust-position="{{false}}" cursor-spacing="20"
|
||||
value="{{message}}" bindinput="onInputValueChange" maxlength="-1" type="text" auto-height="{{true}}"
|
||||
placeholder="" placeholder-class="input-placeholder" confirm-type="send" show-confirm-bar="{{false}}"
|
||||
bindfocus="inputBindFocus"
|
||||
bindblur="inputBindBlur"
|
||||
bindconfirm="sendTextMessage"/>
|
||||
</view>
|
||||
<view wx:if="{{isAudio}}" class="TUI-message-input-main"
|
||||
bind:longpress="handleLongPress"
|
||||
bind:touchmove="handleTouchMove"
|
||||
bind:touchend="handleTouchEnd"
|
||||
style="display: flex; justify-content: center; font-size: 32rpx; font-family: PingFangSC-Regular; height: 30px">
|
||||
<text >{{text}}</text>
|
||||
</view>
|
||||
<view class="TUI-message-input-functions" hover-class="none">
|
||||
<view class="TUI-sendMessage-btn">
|
||||
<image class="TUI-icon" bindtap="handleEmoji" src="../../../../static/assets/face-emoji.svg" />
|
||||
</view>
|
||||
<view wx:if="{{!sendMessageBtn}}" bindtap="handleExtensions" class="TUI-sendMessage-btn">
|
||||
<image class="TUI-icon" src="../../../../static/assets/more.svg" />
|
||||
</view>
|
||||
<view wx:else class="TUI-sendMessage-btn" bindtap="sendTextMessage">
|
||||
发送
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{displayFlag === 'emoji'}}" class="TUI-Emoji-area">
|
||||
<Emoji bind:enterEmoji="appendMessage" />
|
||||
</view>
|
||||
<view wx:if="{{displayFlag === 'extension'}}" class="TUI-Extensions">
|
||||
<view class="TUI-Extension-slot" bindtap="handleSendPicture">
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/take-photo.svg" />
|
||||
<view class="TUI-Extension-slot-name">拍摄照片</view>
|
||||
</view>
|
||||
<view class="TUI-Extension-slot" bindtap="handleSendImage">
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/send-img.svg" />
|
||||
<view class="TUI-Extension-slot-name">发送图片</view>
|
||||
</view>
|
||||
<view class="TUI-Extension-slot" bindtap="handleShootVideo">
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/take-video.svg" />
|
||||
<view class="TUI-Extension-slot-name">拍摄视频</view>
|
||||
</view>
|
||||
<view class="TUI-Extension-slot" bindtap="handleSendVideo">
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/send-video.svg" />
|
||||
<view class="TUI-Extension-slot-name">发送视频</view>
|
||||
</view>
|
||||
<view class="TUI-Extension-slot" data-value="{{1}}" bindtap="handleCheckAuthorize" >
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/audio-calling.svg" />
|
||||
<view class="TUI-Extension-slot-name">语音通话</view>
|
||||
</view>
|
||||
<view class="TUI-Extension-slot" data-value="{{2}}" bindtap="handleCheckAuthorize" >
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/video-calling.svg" />
|
||||
<view class="TUI-Extension-slot-name">视频通话</view>
|
||||
</view>
|
||||
<view class="TUI-Extension-slot" bindtap="handleServiceEvaluation">
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/service-assess.svg" />
|
||||
<view class="TUI-Extension-slot-name">服务评价</view>
|
||||
</view>
|
||||
<view class="TUI-Extension-slot" bindtap="handleSendOrder">
|
||||
<image class="TUI-Extension-icon" src="../../../../static/assets/send-order.svg" />
|
||||
<view class="TUI-Extension-slot-name">发送订单</view>
|
||||
</view>
|
||||
</view>
|
||||
<CommonWords class="tui-cards" display="{{displayCommonWords}}" bind:sendMessage="$handleSendTextMessage" bind:close="$handleCloseCards" />
|
||||
<OrderList class="tui-cards" display="{{displayOrderList}}" bind:sendCustomMessage="$handleSendCustomMessage" bind:close="$handleCloseCards"/>
|
||||
<ServiceEvaluation class="tui-cards" display="{{displayServiceEvaluation}}" bind:sendCustomMessage="$handleSendCustomMessage" bind:close="$handleCloseCards"/>
|
||||
</view>
|
||||
<view class="record-modal" wx:if="{{popupToggle}}" bind:longpress="handleLongPress"
|
||||
bind:touchmove="handleTouchMove"
|
||||
bind:touchend="handleTouchEnd">
|
||||
<view class="wrapper">
|
||||
<view class="modal-loading">
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-title">
|
||||
{{title}}
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,158 @@
|
||||
.TUI-message-input-container {
|
||||
background-color: #F1F1F1;
|
||||
}
|
||||
|
||||
.TUI-message-input {
|
||||
display: flex;
|
||||
padding-bottom: 16rpx;
|
||||
background-color: #F1F1F1;
|
||||
width: 100vw;
|
||||
overflow: scroll;
|
||||
}
|
||||
.TUI-commom-function {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 750rpx;
|
||||
height: 106rpx;
|
||||
background-color: #F1F1F1;
|
||||
align-items: center;
|
||||
}
|
||||
.TUI-commom-function-item {
|
||||
display: flex;
|
||||
width: 136rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: #FFFFFF;
|
||||
height: 48rpx;
|
||||
margin-left: 16rpx;
|
||||
border-radius: 24rpx;
|
||||
background-color: #00C8DC;
|
||||
}
|
||||
.TUI-commom-function-item:first-child{
|
||||
margin-left: 48rpx;
|
||||
}
|
||||
.TUI-message-input-functions {
|
||||
display: flex;
|
||||
}
|
||||
.TUI-message-input-main {
|
||||
height: 30px;
|
||||
background-color: #fff;
|
||||
flex: 1;
|
||||
margin: 0 10rpx;
|
||||
padding: 0 5rpx;
|
||||
border-radius: 5rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.TUI-message-input-main-focus {
|
||||
height: auto;
|
||||
background-color: #fff;
|
||||
flex: 1;
|
||||
margin: 0 10rpx;
|
||||
padding: 0 5rpx;
|
||||
border-radius: 5rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.TUI-message-input-area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 300rpx;
|
||||
/* 最多显示10行 */
|
||||
line-height: 30rpx;
|
||||
overflow: scroll;
|
||||
}
|
||||
.TUI-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
.TUI-Extensions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100vw;
|
||||
height: 450rpx;
|
||||
margin-left: 14rpx;
|
||||
margin-right: 14rpx;
|
||||
}
|
||||
|
||||
.TUI-Extension-slot {
|
||||
width: 128rpx;
|
||||
height: 170rpx;
|
||||
margin-left: 26rpx;
|
||||
margin-right: 26rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.TUI-Extension-icon {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
.TUI-sendMessage-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
|
||||
.TUI-Emoji-area {
|
||||
width: 100vw;
|
||||
height: 450rpx;
|
||||
}
|
||||
|
||||
.TUI-Extension-slot-name {
|
||||
line-height: 34rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.record-modal {
|
||||
height: 300rpx;
|
||||
width: 60vw;
|
||||
background-color: #000;
|
||||
opacity: 0.8;
|
||||
position: fixed;
|
||||
top: 670rpx;
|
||||
z-index: 9999;
|
||||
left: 20vw;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.record-modal .wrapper {
|
||||
display: flex;
|
||||
height: 200rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 10vw;
|
||||
}
|
||||
.record-modal .wrapper .modal-loading {
|
||||
opacity: 1;
|
||||
width: 40rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 4rpx;
|
||||
background-color: #006fff;
|
||||
animation: loading 2s
|
||||
cubic-bezier(0.17, 0.37, 0.43, 0.67) infinite;
|
||||
}
|
||||
.modal-title {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
@keyframes loading{
|
||||
0% {
|
||||
transform: translate(0,0)
|
||||
}
|
||||
50%
|
||||
{
|
||||
transform :translate(30vw,0);
|
||||
background-color: #f5634a;
|
||||
width :40px;
|
||||
}
|
||||
100%
|
||||
{transform: translate(0,0);
|
||||
}
|
||||
}
|
||||