@ -0,0 +1,20 @@ |
|||
# Build and Release Folders |
|||
bin-debug/ |
|||
bin-release/ |
|||
[Oo]bj/ |
|||
[Bb]in/ |
|||
|
|||
# Other files and folders |
|||
.settings/ |
|||
/unpackage/ |
|||
node_modules |
|||
# Executables |
|||
*.swf |
|||
*.air |
|||
*.ipa |
|||
*.apk |
|||
*.zip |
|||
|
|||
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` |
|||
# should NOT be excluded as they contain compiler settings and other important |
|||
# information for Eclipse / Flash Builder. |
|||
@ -0,0 +1,33 @@ |
|||
{ |
|||
// launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/ |
|||
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数 |
|||
"version" : "0.0", |
|||
"configurations" : [ |
|||
{ |
|||
"app-plus" : { |
|||
"launchtype" : "remote" |
|||
}, |
|||
"default" : { |
|||
"launchtype" : "local" |
|||
}, |
|||
"mp-weixin" : { |
|||
"launchtype" : "local" |
|||
}, |
|||
"type" : "uniCloud" |
|||
}, |
|||
{ |
|||
"openVueDevtools" : false, |
|||
"playground" : "custom", |
|||
"type" : "uni-app:app-ios" |
|||
}, |
|||
{ |
|||
"openVueDevtools" : false, |
|||
"playground" : "standard", |
|||
"type" : "uni-app:app-android" |
|||
}, |
|||
{ |
|||
"openVueDevtools" : false, |
|||
"type" : "uni-app:h5" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
-----BEGIN RSA PRIVATE KEY----- |
|||
MIIEowIBAAKCAQEA17mXxivjza/+LCpiNPKPTiiit0dcFiBAkOGN297tSiJjavsv |
|||
Pji04nGeBbi+z/2qb1+8/5r92QDy/Krp3xdHIuEGIwDll6UEa3Wcj3StqBnycAWc |
|||
JDlZ9dur7bpoVLF6O1/IeexfSIkqVkNmaKDS2kXKpllrgyx19ql3vXekFuFCwLIh |
|||
Ku6yLlcfEaKnWmd3h4vNV0X+q3ckK56w5IyPkxe6gOKpPbQ5TEB8J+w9X7u3MCKl |
|||
PfZPmLyJ53UzacPnkfH6SO+eHO+WSsauYtfnxfduuG7BzYndH6JO3A9m9GwIsM/o |
|||
N3WuMAk3DoFXK5dqnFhocXlsPizQOt+MZLYvLQIDAQABAoIBABo6VlmhvHCllSGJ |
|||
hacqVoIKLr5Zzrhh83ep9LVmxTLeO3gmUfgerN9bMPtBGvHuxWoFGdV54qMfmmtd |
|||
3FFjYyK6eSSIV2G4jnECO6a1aOenP/Keu/0re+SIcL7WixjA+zt8ycMJGgyXoK4c |
|||
Q7c01m3zTlArTwcDwNPchtJiWXDueQw0tjO2zdD2DA/hSohVUcgynJ6kDez1lQ35 |
|||
8p2LI0M4LGjn2NL5XN4PyarTvnslvG5Ik6Oo6wSTySqcddWHJm9+ozuVVqDrVqOY |
|||
R0bdm8g9PWDoPDiCLbx58c3MtFRoejBFhm3cQp16DEB1fbL20nnTQspXG5hvsYpP |
|||
uZEXbAECgYEA+pYztQ0+13BYoTxSMQMOUscxLigJXUcm3JnA8w95qG5jwkwosh52 |
|||
dqDNGAUmpeliokR0rI3xozXDsldX+onDGyExHHi8u9f03B7NMseoTK6edGjJEprl |
|||
syoAoGwg/GY6yle3yqEkRhchWxNYDewe+W1b6AK8JG7XMQEfU08r3rkCgYEA3GKZ |
|||
E212OyKgADaOVBqxwBN9bECxbne8KqsWRMnWN+cps0mqfPFrhDr62oqxwioh1ZAE |
|||
RWciBF7l5PrbaJ5NAjPuh6MzU6zBbJVkDqddySsbHExh69Uia81J1UG21GpPnjII |
|||
/h74dan6DYHJuxjFYDLqGrn81TCkd1JiikdfOhUCgYB4fTBvpebJgGOdY3vBxU5l |
|||
zxF+uBGIoGW3PNbiCFbe/fVJv4Tx4GPltnvnSNLEg+vBSlkvfzDo4Tkvz3+mIAeI |
|||
S/VpU0SsrbI3BTh1ajsqY+wc3SWRpJk+BLw4ZsWVlzI9iN/+tmzSptyLBkoYp6hd |
|||
FpBShr4gZotiLL/7Nt5JQQKBgQCZlKCGeGbHSRblbx96nuu2Jh7mnKLJj+lydq3b |
|||
HCkL5i0aQ0DrNzas/IkqWTMNU10mveksEHYVQ6jEDMlwO7kAyv30ShgPvLlCmU0U |
|||
JTBna4HGE7i9p1cIdxR36AaoOrnnTYkUxrJxFRYr6YGSv+10X6bjHy+BxhcnDCOd |
|||
p6VGDQKBgH+BVVOQ2I5ddO6ZICapfm6O3n32RPuSsZavvEE7T73kY0jY+gAf2q1j |
|||
KIZ9kH851D0q2ofmgly2lo9ctuV+4uq55wT07Bgz5UwTCoOQ8VE7sBC1aB6TP0ot |
|||
whW3ZXHQomC9aM+c11IcTsQAJ6suZfT7uZcazWhD8PN+BiluxTlm |
|||
-----END RSA PRIVATE KEY----- |
|||
-----BEGIN CERTIFICATE----- |
|||
MIID9zCCAt+gAwIBAgIJA2BOZCd6yrSRMA0GCSqGSIb3DQEBCwUAMGkxFDASBgNV |
|||
BAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx |
|||
EzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl |
|||
c3QwHhcNMjIxMDIwMTI0MDUxWhcNMjIxMTE5MTI0MDUxWjBpMRQwEgYDVQQDEwtl |
|||
eGFtcGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYD |
|||
VQQHEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIIB |
|||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA17mXxivjza/+LCpiNPKPTiii |
|||
t0dcFiBAkOGN297tSiJjavsvPji04nGeBbi+z/2qb1+8/5r92QDy/Krp3xdHIuEG |
|||
IwDll6UEa3Wcj3StqBnycAWcJDlZ9dur7bpoVLF6O1/IeexfSIkqVkNmaKDS2kXK |
|||
pllrgyx19ql3vXekFuFCwLIhKu6yLlcfEaKnWmd3h4vNV0X+q3ckK56w5IyPkxe6 |
|||
gOKpPbQ5TEB8J+w9X7u3MCKlPfZPmLyJ53UzacPnkfH6SO+eHO+WSsauYtfnxfdu |
|||
uG7BzYndH6JO3A9m9GwIsM/oN3WuMAk3DoFXK5dqnFhocXlsPizQOt+MZLYvLQID |
|||
AQABo4GhMIGeMAsGA1UdDwQEAwIC9DAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYB |
|||
BQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDCDBcBgNVHREEVTBTgglsb2NhbGhvc3SC |
|||
FWxvY2FsaG9zdC5sb2NhbGRvbWFpboIGbHZoLm1lgggqLmx2aC5tZYIFWzo6MV2H |
|||
BH8AAAGHEP6AAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBAEEPfOUj |
|||
4+CbTnJwjZIqz+4Qo9X7RnMTkuuckTWC/f4h8IXSuqF3ximcnFWrDu5uEw3x1wLM |
|||
VHedWIhTUw908dHW4flTTJfI/hQg1samK7XefVfGx7/0uRJi2tiPf5ztI45/z6cC |
|||
0dQxGZEu4t7mfu0sbfjvKtngDCdF5ZWKy4jjEWdW7CsJ7BSWuyp4dP/FVW9ntPz8 |
|||
m+qYYTIUI3QHQe1RoMmOKK+1Nqs7YkfB/avebt+xdv2VOekefqmYudpwDMbaQ+Zk |
|||
boqW/+u1WBpsWeXob+Fhmp3nM+j3G6KyNkPRbGBZcgfkdGrPdM8AvOrdGEcrK3rT |
|||
QKRyGCwDZqQjLgg= |
|||
-----END CERTIFICATE----- |
|||
@ -0,0 +1 @@ |
|||
{"type":"module"} |
|||
@ -0,0 +1,881 @@ |
|||
<script> |
|||
// 引入vue3 getCurrentInstance |
|||
import { getCurrentInstance } from 'vue' |
|||
import pinia from '@/store/index' |
|||
import { useMsgStore } from '@/store/message'; |
|||
import { useloginStore } from '@/store/login'; |
|||
import permision from "@/utils/permission.js" |
|||
import config from "@/common/config"; |
|||
import emoji from '@/utils/emoji.js'; |
|||
// #ifdef APP-PLUS |
|||
import db from './utils/db'; |
|||
import homeData from './service/homeData'; |
|||
import getSystemInfo from './service/getSystemInfo'; |
|||
import getMessageList from './service/getMessageList'; |
|||
import groupInfo from './service/groupInfo'; |
|||
import groupUserList from './service/groupUserList'; |
|||
import getUserInfo from './service/getUserInfo'; |
|||
// #endif |
|||
// #ifdef H5 |
|||
import VConsole from 'vconsole'; |
|||
// #endif |
|||
let vConsole = null; //移动H5调试器 |
|||
const msgStore = useMsgStore(pinia) |
|||
const userStore = useloginStore(pinia); |
|||
let keepAlive = null |
|||
let network_log = null |
|||
let networkCheckInterval = null |
|||
let emojiMap = [] |
|||
// #ifdef APP-PLUS |
|||
import appUpdate from '@/common/appUpdate.js'; |
|||
// 安卓设备引入保活插件,不使用保活或者编译ios的时候注释掉,只需要注释这一行即可。 |
|||
// keepAlive = uni.requireNativePlugin('Ba-KeepAlive'); |
|||
// #endif |
|||
export default { |
|||
async created() { |
|||
// #ifdef APP-PLUS |
|||
await db.createDatabase() |
|||
|
|||
await homeData.createTalbe(); |
|||
await getSystemInfo.createTalbe(); |
|||
await getMessageList.createTalbe(); |
|||
await groupInfo.createTalbe(); |
|||
await groupUserList.createTalbe(); |
|||
await getUserInfo.createTalbe(); |
|||
// #endif |
|||
// console.info('1111111'); |
|||
}, |
|||
onLaunch: function() { |
|||
this.startNetworkMonitor(); |
|||
this.network_log = uni.getStorageSync('network_log') |
|||
// this.network_log = msgStore.network_log |
|||
|
|||
const _this = this |
|||
if(this.network_log=='none') { |
|||
_this.getGroupData() |
|||
}else{ |
|||
// 获取全局配置 |
|||
userStore.getGlobalConfig(); |
|||
// console.log(config); |
|||
// _this.getGroupData() |
|||
} |
|||
|
|||
// 初始化APP设置 |
|||
let setting=uni.getStorageSync('appSetting') ?? ''; |
|||
if(!setting){ |
|||
userStore.setAppSetting({ |
|||
voiceStatus:true, |
|||
vibrateStatus:false, |
|||
circleAvatar:false |
|||
}); |
|||
}else{ |
|||
userStore.setAppSetting(setting); |
|||
} |
|||
|
|||
uni.getSystemInfo({ |
|||
success: function(e) { |
|||
let paddingB=0; |
|||
// 获取 appContext 上下文 |
|||
const {appContext} = getCurrentInstance(); |
|||
appContext.config.globalProperties.StatusBar = e.statusBarHeight; |
|||
// #ifndef MP |
|||
if (e.platform == 'android') { |
|||
appContext.config.globalProperties.CustomBar = e.statusBarHeight + 50; |
|||
} else { |
|||
appContext.config.globalProperties.CustomBar = e.statusBarHeight + 45; |
|||
}; |
|||
// #endif |
|||
// #ifdef MP-WEIXIN |
|||
|
|||
let custom = wx.getMenuButtonBoundingClientRect(); |
|||
appContext.config.globalProperties.Custom = custom; |
|||
appContext.config.globalProperties.CustomBar = custom.bottom + custom.top - e.statusBarHeight; |
|||
// #endif |
|||
|
|||
// #ifdef MP-ALIPAY |
|||
appContext.config.globalProperties.CustomBar = e.statusBarHeight + e.titleBarHeight; |
|||
// #endif |
|||
|
|||
// #ifdef MP |
|||
try { |
|||
|
|||
var res = uni.getSystemInfoSync(); |
|||
res.model = res.model.replace(' ', ''); |
|||
res.model = res.model.toLowerCase(); |
|||
var res1 = res.model.indexOf('iphonex'); |
|||
if(res1 > 5){res1 = -1;} |
|||
var res2 = res.model.indexOf('iphone1'); |
|||
if(res2 > 5){res2 = -1;} |
|||
if(res1 != -1 || res2 != -1){ |
|||
paddingB = uni.upx2px(50); |
|||
} |
|||
|
|||
} catch (e){return null;} |
|||
// #endif |
|||
// #ifdef H5 |
|||
paddingB = uni.upx2px(100); |
|||
// #endif |
|||
// 获取设备底部高度 |
|||
appContext.config.globalProperties.inlineTools=paddingB; |
|||
// 设置全局底部导航栏高度 |
|||
appContext.config.globalProperties.navBarHeight=uni.upx2px(100); |
|||
} |
|||
}) |
|||
|
|||
//启用H5调试模式 |
|||
// #ifdef H5 |
|||
this.loadVConsole(); |
|||
// #endif |
|||
|
|||
// #ifdef APP-PLUS |
|||
// 检查应用更新 |
|||
appUpdate(); |
|||
if (uni.getSystemInfoSync().platform === 'android' && keepAlive) { |
|||
// this.register(); |
|||
} |
|||
// 只有app才有推送权限 |
|||
uni.onPushMessage((res) => { |
|||
let data=res.data; |
|||
if(res.type=='click'){ |
|||
let playload=data.payload; |
|||
let toUser=playload.toContactId ? playload.toContactId : ''; |
|||
// 如果是音视频通话则不 |
|||
if(playload.type=='webrtc'){ |
|||
return; |
|||
} |
|||
uni.navigateTo({ |
|||
url:"/pages/message/chat?id=" + toUser |
|||
}) |
|||
} |
|||
}) |
|||
// #endif |
|||
}, |
|||
onUnload() { |
|||
this.scoketClose() |
|||
this.socketIo.traderDetailIndex = 100 // 初始化 tabIndex |
|||
}, |
|||
onShow: function() { |
|||
// #ifdef APP-PLUS |
|||
// 判断平台,如果是安卓就检测保活插件 |
|||
if (uni.getSystemInfoSync().platform === 'android' && keepAlive) { |
|||
// this.isRunning(); |
|||
} |
|||
// #endif |
|||
|
|||
if(!this.socketIo.checkStatus()){ |
|||
// console.log('ws断线了,重新链接!'); |
|||
uni.$emit('socketStatus',false); |
|||
this.getWebsocketData(); |
|||
} |
|||
this.appStatus=true; |
|||
var userInfo = uni.getStorageSync('userInfo'); |
|||
if(userInfo){ |
|||
this.getNoticeCount() |
|||
} |
|||
// console.log('App Show') |
|||
let emojiMap=[]; |
|||
// 解析所有表情 |
|||
emoji.forEach(function (item) { |
|||
let child=item.children; |
|||
if(child.length>0){ |
|||
child.forEach(function (val) { |
|||
let name=val.name; |
|||
let src=val.src; |
|||
emojiMap[name]=src; |
|||
}) |
|||
} |
|||
}); |
|||
this.emojiMap=emojiMap; |
|||
}, |
|||
|
|||
methods:{ |
|||
startNetworkMonitor() { |
|||
// 立即执行第一次检测 |
|||
this.checkAndSaveNetworkStatus(); |
|||
|
|||
// 设置定时器(每5秒检测一次) |
|||
this.networkCheckInterval = setInterval(() => { |
|||
this.checkAndSaveNetworkStatus(); |
|||
// if(this.network_log!=='none'&&this.$route.path!="/pages/login/404"){ |
|||
// userStore.getGlobalConfig(); |
|||
// }else if(this.$route.path=="/pages/login/404"){ |
|||
// clearInterval(this.networkCheckInterval) |
|||
// } |
|||
}, 5000); |
|||
}, |
|||
checkAndSaveNetworkStatus(){ |
|||
uni.getNetworkType({ |
|||
success (res) { |
|||
msgStore.network_log = res.networkType; |
|||
// userStore.getGlobalConfig(); |
|||
uni.setStorageSync('network_log', res.networkType); |
|||
// uni.setStorageSync('network_log', 'none'); |
|||
} |
|||
}); |
|||
}, |
|||
async getGroupData(){ |
|||
let groups = await getSystemInfo.getList(); |
|||
groups.forEach(item => { |
|||
item.sysInfo = JSON.parse(item.sysInfo); |
|||
item.chatInfo = JSON.parse(item.chatInfo); |
|||
item.fileUpload = JSON.parse(item.fileUpload); |
|||
item.compass = JSON.parse(item.compass); |
|||
item.demon_mode = JSON.parse(item.demon_mode); |
|||
}) |
|||
let [result] = groups |
|||
console.info('处理后的数据:', result) |
|||
uni.setStorageSync('globalConfig',result) |
|||
}, |
|||
// 开启调试模式 |
|||
loadVConsole() { //初始化vConsole,用于H5调试用 |
|||
if (config.isVConsole) { //开启调试时 |
|||
let systemInfo = uni.getSystemInfoSync(); |
|||
if (!(systemInfo.uniPlatform == 'app' || systemInfo.uniPlatform == 'web')) { // 当为app或者H5时 |
|||
return; |
|||
} |
|||
vConsole = new VConsole({ |
|||
defaultPlugins: ['system', 'network', 'element', 'storage'], |
|||
// 可以在此设定要默认加载的面板 |
|||
maxLogNumber: 1000, |
|||
// disableLogScrolling: true, |
|||
onReady: () => { |
|||
console.log('vConsole: onReady'); |
|||
// 置顶最高层级 |
|||
var vcSwitch = document.getElementsByClassName('vc-switch')[0]; |
|||
vcSwitch.style.zIndex = '9999999999'; |
|||
var vcMask = document.getElementsByClassName('vc-mask')[0]; |
|||
vcMask.style.zIndex = '9999999999'; |
|||
var vcPanel = document.getElementsByClassName('vc-panel')[0]; |
|||
vcPanel.style.zIndex = '9999999999'; |
|||
}, |
|||
onClearLog: () => { |
|||
console.log('vConsole: onClearLog'); |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
destroyVConsole() { |
|||
// 结束调试后,可移除掉 |
|||
vConsole.destroy(); |
|||
}, |
|||
isRunning() { //是否正在运行 |
|||
keepAlive.isRunning((res) => { |
|||
if(!res.isRunning){ |
|||
this.restart(); |
|||
}else{ |
|||
console.info('保活组件运行中'); |
|||
} |
|||
}); |
|||
}, |
|||
restart() { //重启 |
|||
keepAlive.restart((res) => { |
|||
console.info('重启成功!'); |
|||
}); |
|||
}, |
|||
register() { //注册 |
|||
keepAlive.register({ |
|||
channelId: 'Ba-KeepAlive', |
|||
channelName: "Ba-KeepAlive", |
|||
title: "消息服务正在运行中", //可以修改为自己想展示的文字 |
|||
content: '用于接收消息的常驻通知,请保留!', //可以修改为自己想展示的文字 |
|||
}, |
|||
(res) => { |
|||
console.log('保活注册成功'); |
|||
}); |
|||
}, |
|||
scoketClose() { |
|||
this.socketIo.connectNum = 1 |
|||
const data = { |
|||
type: "close" |
|||
}; |
|||
this.socketIo.send(data); // 这是给后端发送特定数据 关闭推送 |
|||
this.socketIo.Close(); // 主动 关闭连接 , 不会重连 |
|||
}, |
|||
getNoticeCount(){ |
|||
this.$api.compaApi.getNoticeCount().then(res => { |
|||
msgStore.getCount(res.data.count); |
|||
}) |
|||
}, |
|||
getWebsocketData() { |
|||
// 获取用户信息 |
|||
var userInfo = uni.getStorageSync('userInfo'); |
|||
var _this = this; |
|||
// 要发送的数据包 |
|||
const data = { |
|||
type: "ping" |
|||
}; |
|||
// 打开连接 |
|||
this.socketIo.connectSocketInit(data); |
|||
uni.$off("getPositonsOrder"); |
|||
// 接收数据,全局监听 |
|||
uni.$on("getPositonsOrder", (res) => { |
|||
var userInfo = uni.getStorageSync('userInfo'); |
|||
let data=res.data; |
|||
this.connect = true; |
|||
switch (res['type']) { |
|||
// 服务端ping客户端 |
|||
case 'ping': |
|||
this.socketIo.send({ |
|||
type: "pong" |
|||
}); |
|||
break; |
|||
case 'pong': |
|||
userStore.$patch({ |
|||
multiport: res.multiport |
|||
}) |
|||
break; |
|||
// 登录 更新用户列表 |
|||
case 'init': |
|||
// 设置全局clientId |
|||
uni.setStorageSync('client_id', res.client_id); |
|||
if(userInfo){ |
|||
this.$api.LoginApi.bindUid({ |
|||
client_id: res['client_id'], |
|||
user_id: userInfo.user_id, |
|||
cid:uni.getStorageSync('cid') |
|||
}).then(e => { |
|||
this.socketIo.send({ |
|||
type: "bindUid", |
|||
user_id: userInfo.user_id, |
|||
token:uni.getStorageSync("authToken") |
|||
}); |
|||
}).catch(error => {}) |
|||
} |
|||
|
|||
break; |
|||
//上线、下线通知 |
|||
case "isOnline": |
|||
msgStore.updateContacts({ |
|||
id: data.id, |
|||
is_online:data.is_online |
|||
}); |
|||
// 如果是下线,并且和通话的是用一个人,就将通话锁定关闭 |
|||
if(!data.is_online && msgStore.webrtcLock==data.id){ |
|||
msgStore.webrtcLock=false; |
|||
} |
|||
break; |
|||
case "offline": |
|||
let clientId=uni.getStorageSync('client_id'); |
|||
let globalConfig=uni.getStorageSync('globalConfig'); |
|||
// 如果开启了多设备同时登录,则不走后面的逻辑 |
|||
if(globalConfig.sysInfo.multipleLogin==1){ |
|||
break; |
|||
}else if(data.id==userInfo.user_id && data.client_id!=clientId && data.isMobile){ |
|||
uni.showToast({ |
|||
title: "您的账号在其他设备登录,已被迫下线!", |
|||
icon: "none", |
|||
duration: 2500 |
|||
}) |
|||
userStore.logout(); |
|||
} |
|||
break; |
|||
case "updateConfig": |
|||
uni.setStorageSync('globalConfig',data); |
|||
userStore.globalConfig=data; |
|||
if(data.sysInfo.state==0){ |
|||
uni.navigateTo({ |
|||
url:'/pages/login/404' |
|||
}) |
|||
} |
|||
break; |
|||
case 'simple': |
|||
case 'group': |
|||
// 只要不是自己发的,才可以播放声音 |
|||
if (data.fromUser.id != userInfo.user_id) { |
|||
const contact = msgStore.getContact(data.toContactId,data); |
|||
// 如果开启了声音才播放 |
|||
if (data.toContactId=='system' || contact.is_notice == 1) { |
|||
this.playSound(); |
|||
} |
|||
// #ifdef APP-PLUS |
|||
var self=this; |
|||
const list = [] |
|||
let imgSrcs = [] |
|||
let emojiSrcs = [] |
|||
list.push(data) |
|||
list.forEach((res)=>{ |
|||
res.content = this.emojiToHtml(res.content) |
|||
const parts = res.fromUser.avatar.split('/') |
|||
let lastPart = parts.pop() || parts.pop() || '' |
|||
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart)); |
|||
res.fromUser.imgname = isNumber ? lastPart+'.png' : lastPart; |
|||
|
|||
|
|||
if(res.type=='image'||res.type=='video'||res.type=='emoji'){ |
|||
const parts1 = res.content.split('/') |
|||
let lastPart1 = parts1.pop() || parts1.pop() || '' |
|||
res.imgname = lastPart1 |
|||
uni.downloadFile({ url: res.content,success: (downloadResult) => { |
|||
self.saveToPermanentStorage(downloadResult.tempFilePath); |
|||
}}) |
|||
}else if(res.type=='text'){ |
|||
const regex = /<img\s+[^>]*src="([^"]+)"/g; |
|||
let match; |
|||
while ((match = regex.exec(res.content)) !== null) { |
|||
imgSrcs.push(match[1]); |
|||
} |
|||
if(imgSrcs.length!==0){ |
|||
imgSrcs.forEach((img)=>{ |
|||
const parts = img.split('/') |
|||
let lastPart = parts.pop() || parts.pop() || '' |
|||
emojiSrcs.push(lastPart) |
|||
uni.downloadFile({ url: img,success: (downloadResult) => { |
|||
self.saveToPermanentStorage(downloadResult.tempFilePath); |
|||
}}) |
|||
}) |
|||
res.imgname = emojiSrcs |
|||
} |
|||
} |
|||
}) |
|||
|
|||
// console.log(list); |
|||
this.insertdata(list) |
|||
// #endif |
|||
} |
|||
this.appendMessage(res); |
|||
break; |
|||
case "setChatTop": |
|||
msgStore.updateContacts({ |
|||
id: data.id, |
|||
is_top: data.is_top |
|||
}); |
|||
break; |
|||
case "setIsNotice": |
|||
msgStore.updateContacts({ |
|||
id: data.id, |
|||
is_notice: data.is_notice |
|||
}); |
|||
break; |
|||
// 新增加了群聊 |
|||
case "addGroup": |
|||
msgStore.appendContacts(data); |
|||
this.$api.LoginApi.bindGroup({ client_id: uni.getStorageSync('client_id'), group_id: data.id }); |
|||
// uni.$emit('initContacts', true) |
|||
break; |
|||
// 设置群管理员 |
|||
case "setManager": |
|||
case "addGroupUser": |
|||
case "removeUser": |
|||
if(res['type']=='removeUser' && data.user_id == userInfo.user_id){ |
|||
msgStore.deleteContacts({ |
|||
id: data.group_id |
|||
}) |
|||
}else{ |
|||
msgStore.updateContacts({ |
|||
id: data.group_id, |
|||
avatar: data.avatar |
|||
}); |
|||
uni.$emit('updateGroup',res); |
|||
} |
|||
|
|||
break; |
|||
// 撤回消息 |
|||
case "undoMessage": |
|||
const tabid = uni.getStorageSync('tabid') |
|||
if(data.is_last){ |
|||
msgStore.updateContacts({ |
|||
id: data.toContactId, |
|||
lastContent: data.content |
|||
}); |
|||
}else if(data.chat_identify&&data.from_user==tabid){ |
|||
msgStore.checkMsg(data); |
|||
msgStore.appendMsg(data); |
|||
} |
|||
break; |
|||
// 删除用户消息 |
|||
case "delUserAllMsg": |
|||
// #ifdef APP-PLUS |
|||
this.deleteList1('user') |
|||
// #endif |
|||
break; |
|||
// 删除群主消息 |
|||
case "delGroupAllMsg": |
|||
// #ifdef APP-PLUS |
|||
this.deleteList1('group') |
|||
// #endif |
|||
break; |
|||
case "delMessageAll": |
|||
// #ifdef APP-PLUS |
|||
if(data.form_user){ |
|||
this.deleteList(data) |
|||
// console.log('group_id',data) |
|||
}else if(data.group_id){ |
|||
data.group_id = "'"+'group-' + data.group_id+"'" |
|||
this.deleteList(data) |
|||
} |
|||
// #endif |
|||
break; |
|||
case "delSystemAllMsg": |
|||
this.delAllMsg() |
|||
break; |
|||
// 修改群组名称 |
|||
case "editGroupName": |
|||
msgStore.updateContacts({ |
|||
id: data.id, |
|||
displayName: data.displayName |
|||
}); |
|||
break; |
|||
case "removeGroup": |
|||
msgStore.deleteContacts({ |
|||
id: data.group_id |
|||
}) |
|||
break; |
|||
// 发布公告 |
|||
case "setNotice": |
|||
msgStore.updateContacts({ |
|||
id: data.group_id, |
|||
notice: data.notice |
|||
}); |
|||
break; |
|||
// 群聊设置 |
|||
case "groupSetting": |
|||
msgStore.updateContacts({ |
|||
id: data.group_id, |
|||
setting: data.setting |
|||
}); |
|||
break; |
|||
case "appendContact": |
|||
msgStore.appendContacts(data); |
|||
break; |
|||
case "postsNotice": |
|||
_this.getNoticeCount() |
|||
break; |
|||
case 'webrtc': |
|||
let platform='h5'; |
|||
//#ifdef H5 |
|||
platform='h5'; |
|||
//#endif |
|||
//#ifdef APP-PLUS |
|||
platform= 'app'; |
|||
//#endif |
|||
if(data.fromUser.id==userInfo.user_id){ |
|||
// 挂断的情况下解锁webrtc |
|||
if([902,903,905,906,907].includes(parseInt(data.extends.code))){ |
|||
msgStore.webrtcLock=false; |
|||
} |
|||
// 如果是当前设备发出的消息则不处理 |
|||
if(data.extends.isMobile==1 || data.extends.event=='calling'){ |
|||
if(data.extends.event=='calling'){ |
|||
this.appendMessage(res); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
} |
|||
// 如果是多端在线,要将在通话中的用户锁定 |
|||
if(data.extends.event == 'offer' || data.extends.event == 'answer' ){ |
|||
msgStore.webrtcLock=true; |
|||
}else if(data.extends.event == 'hangup'){ |
|||
msgStore.webrtcLock=false; |
|||
} |
|||
if(data.extends.event == 'calling'){ |
|||
this.appendMessage(res); |
|||
const allroutes = getCurrentPages(); |
|||
const cureentRoute = allroutes[allroutes.length - 1].route; |
|||
// 如果当前已经在通话中,通知对方忙线中 |
|||
if (cureentRoute == 'pages/message/call') { |
|||
this.$api.msgApi.sendToMsg({ |
|||
toContactId:data.fromUser.user_id, |
|||
type:data.extends.type, |
|||
event:'busy', |
|||
status:data.extends.status, |
|||
code:907, |
|||
id:data.id, |
|||
msg_id:data.msg_id, |
|||
}) |
|||
}else{ |
|||
|
|||
// 小程序不支持音视频聊天 |
|||
//#ifdef APP-PLUS || H5 |
|||
msgStore.webrtcLock=data.fromUser.user_id; |
|||
uni.navigateTo({ |
|||
url: '/pages/message/call?msg_id='+data.id+'&type='+data.extends.type+'&status='+data.extends.status+'&id='+data.fromUser.user_id+'&name='+data.fromUser.realname+'&avatar='+encodeURI(data.fromUser.avatar) |
|||
}) |
|||
//#endif |
|||
} |
|||
|
|||
|
|||
}else{ |
|||
uni.$emit('webrtcConn',data); |
|||
} |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
}) |
|||
// 错误时做些什么 |
|||
uni.$on("connectError", () => { |
|||
this.connect = false |
|||
this.scoketError = true |
|||
uni.$emit('socketStatus',false); |
|||
}) |
|||
}, |
|||
async insertdata(data){ |
|||
// console.log(data,'231'); |
|||
await getMessageList.batchInsertOrUpdate(data) |
|||
}, |
|||
async deleteList(data){ |
|||
await getMessageList.deleteList(data) |
|||
}, |
|||
async deleteList1(val){ |
|||
const list = {group_id:val=='user'?0:1} |
|||
await getMessageList.deleteallList(list) |
|||
}, |
|||
async delAllMsg(){ |
|||
await getMessageList.delAllMsg() |
|||
this.deleteImageFolder() |
|||
}, |
|||
deleteImageFolder() { |
|||
plus.io.resolveLocalFileSystemURL('_doc', function(docDir) { |
|||
docDir.getDirectory('image', { create: false }, function(imageDir) { |
|||
// 目录存在,删除它(包括所有内容) |
|||
imageDir.removeRecursively(function() { |
|||
console.log('删除成功'); |
|||
}, function(error) { |
|||
console.error('删除失败:', error); |
|||
}); |
|||
}, function(error) { |
|||
// 目录不存在(错误码 1),也算成功 |
|||
if (error.code === 1) { |
|||
console.log('目录不存在,无需删除'); |
|||
} else { |
|||
console.error('获取目录失败:', error); |
|||
} |
|||
}); |
|||
}, function(error) { |
|||
console.error('无法访问 _doc 目录:', error); |
|||
}); |
|||
}, |
|||
emojiToHtml(str){ |
|||
if(!str){ |
|||
return; |
|||
} |
|||
let emojiMap=this.emojiMap; |
|||
return str.replace(/\[!(\w+)\]/gi, function (str, match) { |
|||
var file = match; |
|||
return emojiMap[file] ? "<img style=\"width:18px;height:18px;margin-right:5px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]"); |
|||
}); |
|||
}, |
|||
appendMessage(res){ |
|||
let data=res.data; |
|||
let userInfo = uni.getStorageSync('userInfo'); |
|||
// 确定接受人是谁 |
|||
let toUser = data.toContactId; |
|||
if(data.toContactId == userInfo.user_id){ |
|||
toUser = data.toUser |
|||
} |
|||
let contact=msgStore.getContact(toUser,data); |
|||
if(data.toContactId=='system'){ |
|||
// 系统消息只需要把未读数增加一 |
|||
msgStore.$patch({ |
|||
sysUnread: msgStore.sysUnread += 1, |
|||
}) |
|||
}else{ |
|||
// 判断是否是自己,自己发的看接收人是谁,去更新接受人的信息 |
|||
let addUnread = 1; |
|||
// 自己发送的不需要加 |
|||
if(data.fromUser.id==userInfo.user_id){ |
|||
addUnread = 0; |
|||
} |
|||
let at=0; |
|||
// 如果at参数包含了我自己,就要增加@的数量 |
|||
if(data.at.includes(userInfo.user_id)){ |
|||
at=1; |
|||
} |
|||
if (contact) { |
|||
contact.lastContent = data.content; |
|||
contact.lastSendTime = data.sendTime*1000; |
|||
contact.type = data.type; |
|||
contact.unread += addUnread; |
|||
contact.is_at += at; |
|||
// 更新联系人信息 |
|||
msgStore.updateContacts(contact); |
|||
} else { |
|||
let newContact = { |
|||
id: data.toContactId, |
|||
displayName: data.fromUser.displayName, |
|||
avatar: data.fromUser.avatar, |
|||
lastContent: data.content, |
|||
lastSendTime: data.sendTime*1000, |
|||
is_group: data.is_group, |
|||
unread: addUnread, |
|||
is_top: 0, |
|||
dep_id: 0, |
|||
is_at:at |
|||
} |
|||
msgStore.appendContacts(newContact) |
|||
} |
|||
msgStore.catchSocketAction(res); |
|||
} |
|||
// #ifdef APP-PLUS |
|||
let appStatus=this.appStatus; |
|||
// 如果app在后台运行,不是自己发的,并且开启了通知,就创建通知栏 |
|||
if(!appStatus && data.fromUser.id!=userInfo.user_id && contact.is_notice==1){ |
|||
this.createPushMsg(data,contact); |
|||
} |
|||
// #endif |
|||
}, |
|||
createPushMsg(data,contact){ |
|||
console.info("创建通知栏"); |
|||
var regex = /<[^>]+>/g; // 定义正则表达式,匹配所有的HTML标签 |
|||
let content=data.content.replace(regex, ''); // 将匹配到的HTML标签替换为空字符串 |
|||
if(!['text','event','location','contact','create'].includes(data.type)){ |
|||
let callVideo=data.extends.type ?? 0; |
|||
content=this.$util.getMsgType(data.type,callVideo); |
|||
} |
|||
if(data.is_group==1){ |
|||
content=data.fromUser.displayName+':'+content; |
|||
} |
|||
let message={ |
|||
title:contact.displayName, |
|||
content:content, |
|||
payload:data, |
|||
} |
|||
|
|||
let systemInfo = uni.getSystemInfoSync(); |
|||
// 判断平台,如果是安卓就去下载头像并展示头像 |
|||
if (systemInfo.platform === 'android') { |
|||
uni.downloadFile({ |
|||
url: contact.avatar, |
|||
success: (res) => { |
|||
if (res.statusCode === 200) { |
|||
message.icon=res.tempFilePath |
|||
uni.createPushMessage(message) |
|||
} |
|||
} |
|||
}); |
|||
} else if (systemInfo.platform === 'ios') { |
|||
uni.createPushMessage(message) |
|||
} |
|||
}, |
|||
playSound() { |
|||
let setting=uni.getStorageSync('appSetting') ?? ''; |
|||
if(setting.voiceStatus){ |
|||
let _this = this |
|||
// _this.playing = true |
|||
const innerAudioContext = uni.createInnerAudioContext(); |
|||
innerAudioContext.autoplay = true; |
|||
innerAudioContext.src = config.apiUrl+'/static/voice/notify.mp3'; |
|||
innerAudioContext.onError((res) => { |
|||
//如果音频没有正常播放 |
|||
}) |
|||
innerAudioContext.onStop((res) => { |
|||
// _this.playing=false |
|||
_this.$forceUpdate() |
|||
}) |
|||
} |
|||
if(setting.vibrateStatus){ |
|||
uni.vibrateLong({ |
|||
success: function () { |
|||
console.log('手机震动'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
}, |
|||
// App端持久化存储实现 |
|||
saveToPermanentStorage(tempPath) { |
|||
return new Promise((resolve, reject) => { |
|||
// 获取应用文档目录(持久化存储) |
|||
plus.io.resolveLocalFileSystemURL( |
|||
'_doc', |
|||
(docDir) => { |
|||
// 创建目标路径 |
|||
docDir.getDirectory( |
|||
'image', |
|||
{ create: true, exclusive: false }, |
|||
(entry) => { |
|||
// 从临时路径获取文件名 |
|||
const fileName = this.getFileName(tempPath); |
|||
const fileName1 = this.getFileName(docDir.fullPath + 'image/' +fileName); |
|||
// console.log(fileName); |
|||
// console.log(fileName1); |
|||
// 新增:检查文件是否存在 |
|||
entry.getFile(fileName1,{ create: false }, // 不创建新文件 |
|||
(fileEntry) => { |
|||
// console.log('文件已存在,拒绝操作'); |
|||
// 文件已存在,拒绝操作 |
|||
reject(new Error('File already exists: ' + fileName)); |
|||
}, |
|||
(error) => { |
|||
// console.log(error); |
|||
// 文件不存在(或发生其他错误),继续复制操作 |
|||
if (error.code === 14) { // 1表示文件不存在(不同平台错误码可能不同) |
|||
this.copyFile(tempPath, entry, fileName, resolve, reject); |
|||
} else { |
|||
reject(error); |
|||
} |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}); |
|||
}, |
|||
|
|||
// 提取复制逻辑为独立方法 |
|||
copyFile(tempPath, targetDir, fileName, resolve, reject) { |
|||
plus.io.resolveLocalFileSystemURL( |
|||
tempPath, |
|||
(tempEntry) => { |
|||
tempEntry.copyTo( |
|||
targetDir, |
|||
fileName, |
|||
(newEntry) => { |
|||
resolve(newEntry.toLocalURL()); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
|
|||
// 获取文件名工具方法 |
|||
getFileName(path) { |
|||
const index = path.lastIndexOf('/'); |
|||
let fileName = path.substr(index + 1); |
|||
fileName = fileName.replace(/\(\d+\)(?=\.[^./]+$)/, ''); |
|||
return fileName; |
|||
}, |
|||
}, |
|||
onHide: function() { |
|||
this.appStatus=false; |
|||
console.log('App Hide') |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
|
|||
<style lang="scss"> |
|||
@import url("static/css/iconfont.css"); |
|||
@import url("static/css/main.css"); |
|||
@import url("static/css/icon.css"); |
|||
@import url("static/css/reset.css"); |
|||
/*每个页面公共css */ |
|||
@import '@/uni_modules/uni-scss/index.scss'; |
|||
/* #ifndef APP-NVUE */ |
|||
@import '@/static/customicons.css'; |
|||
// 设置整个项目的背景色 |
|||
page { |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
/* #endif */ |
|||
.example-info { |
|||
font-size: 14px; |
|||
color: #333; |
|||
padding: 10px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,201 @@ |
|||
Apache License |
|||
Version 2.0, January 2004 |
|||
http://www.apache.org/licenses/ |
|||
|
|||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
|||
|
|||
1. Definitions. |
|||
|
|||
"License" shall mean the terms and conditions for use, reproduction, |
|||
and distribution as defined by Sections 1 through 9 of this document. |
|||
|
|||
"Licensor" shall mean the copyright owner or entity authorized by |
|||
the copyright owner that is granting the License. |
|||
|
|||
"Legal Entity" shall mean the union of the acting entity and all |
|||
other entities that control, are controlled by, or are under common |
|||
control with that entity. For the purposes of this definition, |
|||
"control" means (i) the power, direct or indirect, to cause the |
|||
direction or management of such entity, whether by contract or |
|||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
|||
outstanding shares, or (iii) beneficial ownership of such entity. |
|||
|
|||
"You" (or "Your") shall mean an individual or Legal Entity |
|||
exercising permissions granted by this License. |
|||
|
|||
"Source" form shall mean the preferred form for making modifications, |
|||
including but not limited to software source code, documentation |
|||
source, and configuration files. |
|||
|
|||
"Object" form shall mean any form resulting from mechanical |
|||
transformation or translation of a Source form, including but |
|||
not limited to compiled object code, generated documentation, |
|||
and conversions to other media types. |
|||
|
|||
"Work" shall mean the work of authorship, whether in Source or |
|||
Object form, made available under the License, as indicated by a |
|||
copyright notice that is included in or attached to the work |
|||
(an example is provided in the Appendix below). |
|||
|
|||
"Derivative Works" shall mean any work, whether in Source or Object |
|||
form, that is based on (or derived from) the Work and for which the |
|||
editorial revisions, annotations, elaborations, or other modifications |
|||
represent, as a whole, an original work of authorship. For the purposes |
|||
of this License, Derivative Works shall not include works that remain |
|||
separable from, or merely link (or bind by name) to the interfaces of, |
|||
the Work and Derivative Works thereof. |
|||
|
|||
"Contribution" shall mean any work of authorship, including |
|||
the original version of the Work and any modifications or additions |
|||
to that Work or Derivative Works thereof, that is intentionally |
|||
submitted to Licensor for inclusion in the Work by the copyright owner |
|||
or by an individual or Legal Entity authorized to submit on behalf of |
|||
the copyright owner. For the purposes of this definition, "submitted" |
|||
means any form of electronic, verbal, or written communication sent |
|||
to the Licensor or its representatives, including but not limited to |
|||
communication on electronic mailing lists, source code control systems, |
|||
and issue tracking systems that are managed by, or on behalf of, the |
|||
Licensor for the purpose of discussing and improving the Work, but |
|||
excluding communication that is conspicuously marked or otherwise |
|||
designated in writing by the copyright owner as "Not a Contribution." |
|||
|
|||
"Contributor" shall mean Licensor and any individual or Legal Entity |
|||
on behalf of whom a Contribution has been received by Licensor and |
|||
subsequently incorporated within the Work. |
|||
|
|||
2. Grant of Copyright License. Subject to the terms and conditions of |
|||
this License, each Contributor hereby grants to You a perpetual, |
|||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|||
copyright license to reproduce, prepare Derivative Works of, |
|||
publicly display, publicly perform, sublicense, and distribute the |
|||
Work and such Derivative Works in Source or Object form. |
|||
|
|||
3. Grant of Patent License. Subject to the terms and conditions of |
|||
this License, each Contributor hereby grants to You a perpetual, |
|||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|||
(except as stated in this section) patent license to make, have made, |
|||
use, offer to sell, sell, import, and otherwise transfer the Work, |
|||
where such license applies only to those patent claims licensable |
|||
by such Contributor that are necessarily infringed by their |
|||
Contribution(s) alone or by combination of their Contribution(s) |
|||
with the Work to which such Contribution(s) was submitted. If You |
|||
institute patent litigation against any entity (including a |
|||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
|||
or a Contribution incorporated within the Work constitutes direct |
|||
or contributory patent infringement, then any patent licenses |
|||
granted to You under this License for that Work shall terminate |
|||
as of the date such litigation is filed. |
|||
|
|||
4. Redistribution. You may reproduce and distribute copies of the |
|||
Work or Derivative Works thereof in any medium, with or without |
|||
modifications, and in Source or Object form, provided that You |
|||
meet the following conditions: |
|||
|
|||
(a) You must give any other recipients of the Work or |
|||
Derivative Works a copy of this License; and |
|||
|
|||
(b) You must cause any modified files to carry prominent notices |
|||
stating that You changed the files; and |
|||
|
|||
(c) You must retain, in the Source form of any Derivative Works |
|||
that You distribute, all copyright, patent, trademark, and |
|||
attribution notices from the Source form of the Work, |
|||
excluding those notices that do not pertain to any part of |
|||
the Derivative Works; and |
|||
|
|||
(d) If the Work includes a "NOTICE" text file as part of its |
|||
distribution, then any Derivative Works that You distribute must |
|||
include a readable copy of the attribution notices contained |
|||
within such NOTICE file, excluding those notices that do not |
|||
pertain to any part of the Derivative Works, in at least one |
|||
of the following places: within a NOTICE text file distributed |
|||
as part of the Derivative Works; within the Source form or |
|||
documentation, if provided along with the Derivative Works; or, |
|||
within a display generated by the Derivative Works, if and |
|||
wherever such third-party notices normally appear. The contents |
|||
of the NOTICE file are for informational purposes only and |
|||
do not modify the License. You may add Your own attribution |
|||
notices within Derivative Works that You distribute, alongside |
|||
or as an addendum to the NOTICE text from the Work, provided |
|||
that such additional attribution notices cannot be construed |
|||
as modifying the License. |
|||
|
|||
You may add Your own copyright statement to Your modifications and |
|||
may provide additional or different license terms and conditions |
|||
for use, reproduction, or distribution of Your modifications, or |
|||
for any such Derivative Works as a whole, provided Your use, |
|||
reproduction, and distribution of the Work otherwise complies with |
|||
the conditions stated in this License. |
|||
|
|||
5. Submission of Contributions. Unless You explicitly state otherwise, |
|||
any Contribution intentionally submitted for inclusion in the Work |
|||
by You to the Licensor shall be under the terms and conditions of |
|||
this License, without any additional terms or conditions. |
|||
Notwithstanding the above, nothing herein shall supersede or modify |
|||
the terms of any separate license agreement you may have executed |
|||
with Licensor regarding such Contributions. |
|||
|
|||
6. Trademarks. This License does not grant permission to use the trade |
|||
names, trademarks, service marks, or product names of the Licensor, |
|||
except as required for reasonable and customary use in describing the |
|||
origin of the Work and reproducing the content of the NOTICE file. |
|||
|
|||
7. Disclaimer of Warranty. Unless required by applicable law or |
|||
agreed to in writing, Licensor provides the Work (and each |
|||
Contributor provides its Contributions) on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|||
implied, including, without limitation, any warranties or conditions |
|||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
|||
PARTICULAR PURPOSE. You are solely responsible for determining the |
|||
appropriateness of using or redistributing the Work and assume any |
|||
risks associated with Your exercise of permissions under this License. |
|||
|
|||
8. Limitation of Liability. In no event and under no legal theory, |
|||
whether in tort (including negligence), contract, or otherwise, |
|||
unless required by applicable law (such as deliberate and grossly |
|||
negligent acts) or agreed to in writing, shall any Contributor be |
|||
liable to You for damages, including any direct, indirect, special, |
|||
incidental, or consequential damages of any character arising as a |
|||
result of this License or out of the use or inability to use the |
|||
Work (including but not limited to damages for loss of goodwill, |
|||
work stoppage, computer failure or malfunction, or any and all |
|||
other commercial damages or losses), even if such Contributor |
|||
has been advised of the possibility of such damages. |
|||
|
|||
9. Accepting Warranty or Additional Liability. While redistributing |
|||
the Work or Derivative Works thereof, You may choose to offer, |
|||
and charge a fee for, acceptance of support, warranty, indemnity, |
|||
or other liability obligations and/or rights consistent with this |
|||
License. However, in accepting such obligations, You may act only |
|||
on Your own behalf and on Your sole responsibility, not on behalf |
|||
of any other Contributor, and only if You agree to indemnify, |
|||
defend, and hold each Contributor harmless for any liability |
|||
incurred by, or claims asserted against, such Contributor by reason |
|||
of your accepting any such warranty or additional liability. |
|||
|
|||
END OF TERMS AND CONDITIONS |
|||
|
|||
APPENDIX: How to apply the Apache License to your work. |
|||
|
|||
To apply the Apache License to your work, attach the following |
|||
boilerplate notice, with the fields enclosed by brackets "[]" |
|||
replaced with your own identifying information. (Don't include |
|||
the brackets!) The text should be enclosed in the appropriate |
|||
comment syntax for the file format. We also recommend that a |
|||
file or class name and description of purpose be included on the |
|||
same "printed page" as the copyright notice for easier |
|||
identification within third-party archives. |
|||
|
|||
Copyright [yyyy] [name of copyright owner] |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
@ -0,0 +1,62 @@ |
|||
# Raingad-IM for uniapp |
|||
|
|||
技术栈:`vue3` + `pinia` + `color-UI` |
|||
|
|||
#### 目录结构 |
|||
``` |
|||
Raingad |
|||
├─api 接口目录 |
|||
│ ├─index.js 总的接口文件 |
|||
│ └─message.js 消息接口 |
|||
│ |
|||
├─common 公共目录 |
|||
│ ├─socket.js websocket配置 |
|||
│ └─config.js 服务器地址配置 |
|||
│ |
|||
│─components 符合vue组件规范的uni-app组件目录 |
|||
│ └─comp-a.vue 可复用的a组件 |
|||
│ |
|||
├─pages 业务页面文件存放的目录 |
|||
│ ├─index |
|||
│ │ └─index.vue index页面 |
|||
│ ├─message |
|||
│ │ └─index.vue 消息列表页面 |
|||
│ ├─ ... 更多目录 |
|||
│ └─contact |
|||
│ └─index.vue 联系人列表页面 |
|||
│ |
|||
├─static 存放应用引用的本地静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此 |
|||
│ |
|||
├─uni_modules 存放[uni_module](/uni_modules)。 |
|||
│ |
|||
├─hybrid App端存放本地html文件的目录,详见 |
|||
│ |
|||
├─nativeplugins 远程插件目录 |
|||
│ |
|||
├─uniCloud 云函数目录,里面有unipush的推送功能,如果不需要可以删除(已删除) |
|||
│ |
|||
├─unpackage 非工程代码,一般存放运行或发行的编译结果 |
|||
│ |
|||
├─main.js Vue初始化入口文件 |
|||
├─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期 |
|||
├─manifest.json 配置应用名称、appid、logo、版本等打包信息,详见 |
|||
├─pages.json 配置页面路由、导航条、选项卡等页面类信息,详见 |
|||
└─uni.scss 这里是uni-app内置的常用样式变量 |
|||
``` |
|||
|
|||
#### 5.3+ 重要更新 |
|||
修改接口的时候,为了避免自己打包的H5被盗用,我们加入了域名加密认证。 |
|||
- 在 `/common/config.js` 中需要添加一个加密信息:hostToken,可以搜索定位到该字符串 |
|||
- hostToken信息需要将自己的域名md5加密 **两次**(域名md5加密一次后得到的值再md5加密一次)在拼接两个字符串,得到我们的Token。不会加密的去百度搜索md5在线加密,将域名输入后加密得到第一次加密后的Token,把加密后的值再重复加密一次即可。 |
|||
|
|||
加密格式如下: |
|||
|
|||
> "sha"+域名加密两次后的md5 + "bi" |
|||
|
|||
假如我们的域名是 `im.xxxxx.com` ,那么加密后就应该是:**sha** f9cb4d7d77719f068e7233e81690f39a **bi** |
|||
|
|||
加密好了把该token填写到hostToken中,这样就可以避免别人下载我们的h5页面,修改域名来使用我们的源码,如果不影响可以将相应的代码屏蔽掉或者使用其他更先进的算法。 |
|||
|
|||
#### 安装打包请直接参考文档 |
|||
文档全部迁移至以下地址: |
|||
所有文档地址: [接口文档地址](https://apifox.com/apidoc/shared-e563aed5-7578-4620-913f-f6746ece6067) 访问密码: raingad-im-doc |
|||
@ -0,0 +1,38 @@ |
|||
{ |
|||
"version" : "1", |
|||
"prompt" : "template", |
|||
"title" : "服务协议和隐私政策", |
|||
"message" : " 请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/> 你可阅读<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", |
|||
"buttonAccept" : "同意并接受", |
|||
"buttonRefuse" : "暂不同意", |
|||
"hrefLoader" : "system", |
|||
"backToExit" : "false", |
|||
"second" : { |
|||
"title" : "确认提示", |
|||
"message" : " 进入应用前,你需先同意<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>,否则将退出应用。", |
|||
"buttonAccept" : "同意并继续", |
|||
"buttonRefuse" : "退出应用" |
|||
}, |
|||
"disagreeMode" : { |
|||
"support" : false, |
|||
"loadNativePlugins" : false, |
|||
"visitorEntry" : false, |
|||
"showAlways" : false |
|||
}, |
|||
"styles" : { |
|||
"backgroundColor" : "#ffffff", |
|||
"borderRadius" : "5px", |
|||
"title" : { |
|||
"color" : "#000000" |
|||
}, |
|||
"buttonAccept" : { |
|||
"color" : "#00ff00" |
|||
}, |
|||
"buttonRefuse" : { |
|||
"color" : "#999999" |
|||
}, |
|||
"buttonVisitor" : { |
|||
"color" : "#999999" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
import {postRequest,postJsonRequest,apiUrl} from '@/utils/request.js'; |
|||
let compaApi = {} |
|||
|
|||
// 朋友圈帖子列表
|
|||
compaApi.wechatMomentsList = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/index', params) |
|||
} |
|||
|
|||
// 发布朋友圈
|
|||
compaApi.wechatMomentsadd = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/add', params) |
|||
} |
|||
|
|||
// 朋友圈点赞/取消点赞
|
|||
compaApi.onlikes = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/like', params) |
|||
} |
|||
|
|||
// 评论/回复
|
|||
compaApi.oncomments = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/comment', params) |
|||
} |
|||
|
|||
// 获取上次保存草稿数据
|
|||
compaApi.getLastPosts = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/getLastPosts', params) |
|||
} |
|||
|
|||
// 删除帖子
|
|||
compaApi.Deleteapost = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/del', params) |
|||
} |
|||
|
|||
// 我的朋友圈
|
|||
compaApi.myPosts = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/myPosts', params) |
|||
} |
|||
|
|||
// 我的朋友圈
|
|||
compaApi.detailsList = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/details', params) |
|||
} |
|||
|
|||
// 获取未读消息数量
|
|||
compaApi.getNoticeCount = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/getNoticeCount', params) |
|||
} |
|||
|
|||
// 消息通知列表接口
|
|||
compaApi.getNoticeList = (params) =>{ |
|||
return postJsonRequest('enterprise/posts/getNoticeList', params) |
|||
} |
|||
|
|||
export default compaApi; |
|||
@ -0,0 +1,45 @@ |
|||
// 统一请求路径前缀在libs/axios.js中修改
|
|||
import { |
|||
postRequest, |
|||
postJsonRequest, |
|||
apiUrl |
|||
} from '@/utils/request.js'; |
|||
let emojiApi = {} |
|||
|
|||
emojiApi.uploadEmoji=apiUrl+'/common/upload/uploadEmoji'; |
|||
|
|||
/** |
|||
* @desc 表情列表 |
|||
* @param {*} 参数 |
|||
*/ |
|||
emojiApi.emojiList = (params) => { |
|||
return postJsonRequest('/enterprise/emoji/index', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 添加表情 |
|||
* @param {*} 参数 |
|||
*/ |
|||
emojiApi.addEmoji = (params) => { |
|||
return postJsonRequest('/enterprise/emoji/add', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 删除表情 |
|||
* @param {*} 参数 |
|||
*/ |
|||
emojiApi.delEmoji = (params) => { |
|||
return postJsonRequest('/enterprise/emoji/del', params) |
|||
} |
|||
|
|||
|
|||
/** |
|||
* @desc 移动表情 |
|||
* @param {*} 参数 |
|||
*/ |
|||
emojiApi.moveEmoji = (params) => { |
|||
return postJsonRequest('/enterprise/emoji/move', params) |
|||
} |
|||
|
|||
|
|||
export default emojiApi; |
|||
@ -0,0 +1,48 @@ |
|||
// 统一请求路径前缀在libs/axios.js中修改
|
|||
import { |
|||
postJsonRequest, |
|||
apiUrl |
|||
} from '@/utils/request.js'; |
|||
let friendApi = {} |
|||
|
|||
/** |
|||
* @desc 删除好友 |
|||
* @param {*} 参数 |
|||
*/ |
|||
friendApi.delFriend = (params) => { |
|||
return postJsonRequest('/enterprise/friend/del', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 添加好友 |
|||
* @param {*} 参数 |
|||
*/ |
|||
friendApi.addFriend = (params) => { |
|||
return postJsonRequest('/enterprise/friend/add', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 新朋友列表 |
|||
* @param {*} 参数 |
|||
*/ |
|||
friendApi.applyList = (params) => { |
|||
return postJsonRequest('/enterprise/friend/index', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 同意或者拒绝请求 |
|||
* @param {*} 参数 |
|||
*/ |
|||
friendApi.acceptApply = (params) => { |
|||
return postJsonRequest('/enterprise/friend/update', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 设置好有备注 |
|||
* @param {*} 参数 |
|||
*/ |
|||
friendApi.setNickname = (params) => { |
|||
return postJsonRequest('/enterprise/friend/setNickname', params) |
|||
} |
|||
|
|||
export default friendApi; |
|||
@ -0,0 +1,15 @@ |
|||
// 引入其他模块的接口
|
|||
|
|||
import msgApi from '@/api/message.js'; // 消息
|
|||
import LoginApi from '@/api/login.js'; //登录相关
|
|||
import friendApi from '@/api/friend.js'; //登录相关
|
|||
import emojiApi from '@/api/emoji.js'; //登录相关
|
|||
import compaApi from '@/api/compass.js'; //朋友圈
|
|||
// 导出接口
|
|||
export default { |
|||
msgApi, |
|||
LoginApi, |
|||
friendApi, |
|||
emojiApi, |
|||
compaApi |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
// 统一请求路径前缀在libs/axios.js中修改
|
|||
import { |
|||
postJsonRequest, |
|||
apiUrl |
|||
} from '@/utils/request.js'; |
|||
let LoginApi = {} |
|||
|
|||
/** |
|||
* @desc 登录接口 |
|||
* @param {*} 参数 |
|||
*/ |
|||
LoginApi.login = (params) => { |
|||
return postJsonRequest('/common/Pub/login', params) |
|||
} |
|||
|
|||
|
|||
/** |
|||
* @desc 退出接口 |
|||
* @param {*} 参数 |
|||
*/ |
|||
LoginApi.logout = (params) => { |
|||
return postJsonRequest('/common/Pub/logout', params) |
|||
} |
|||
|
|||
/** |
|||
* 注册用户 |
|||
* @param {*} data |
|||
*/ |
|||
LoginApi.register = (params) => { |
|||
return postJsonRequest('/common/pub/register', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 绑定client_id |
|||
* @param {*} 参数 |
|||
*/ |
|||
LoginApi.bindUid = (params) => { |
|||
return postJsonRequest('/common/Pub/bindUid', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 绑定群聊client_id |
|||
* @param {*} 参数 |
|||
*/ |
|||
LoginApi.bindGroup = (params) => { |
|||
return postJsonRequest('common/pub/bindGroup', params) |
|||
} |
|||
|
|||
/** |
|||
* 获取全局配置 |
|||
* @param {*} data |
|||
*/ |
|||
LoginApi.getSystemInfo = (params) => { |
|||
return postJsonRequest('/common/pub/getSystemInfo', params) |
|||
} |
|||
|
|||
/** |
|||
* 发送验证码 |
|||
* @param {*} data |
|||
*/ |
|||
LoginApi.sendCode = (params) => { |
|||
return postJsonRequest('/common/pub/sendCode', params) |
|||
} |
|||
|
|||
export default LoginApi; |
|||
@ -0,0 +1,366 @@ |
|||
// 统一请求路径前缀在libs/axios.js中修改
|
|||
import { |
|||
postRequest, |
|||
postJsonRequest, |
|||
apiUrl |
|||
} from '@/utils/request.js'; |
|||
let msgApi = {} |
|||
|
|||
msgApi.uploadUrl=apiUrl+'/common/upload/uploadFile'; |
|||
// 上传头像
|
|||
msgApi.uploadAvatar=apiUrl+'/common/upload/uploadAvatar'; |
|||
|
|||
/** |
|||
* @desc 网络图片上传 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.uploadFileImage = (params) => { |
|||
return postJsonRequest('/common/upload/uploadFileImage', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 个人聊天定时删除消息 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.friendClearMsgDay = (params) => { |
|||
return postJsonRequest('/enterprise/friend/setClearMsgDay', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 群聊天定时删除消息 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.groupClearMsgDay = (params) => { |
|||
return postJsonRequest('/enterprise/group/setClearMsgDay', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 单聊一键清除所有消息 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.friendremoveAllMessage = (params) => { |
|||
return postJsonRequest('/enterprise/friend/removeAllMessage', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 群聊一键清除所有消息 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.groupremoveAllMessage = (params) => { |
|||
return postJsonRequest('/enterprise/group/removeAllMessage', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 普通消息列表(小程序) |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.initContacts = (params) => { |
|||
return postJsonRequest('/enterprise/im/getContacts', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc AI对话 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.sendChat = (params) => { |
|||
return postJsonRequest('/enterprise/chat/sendChat', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 普通消息列表(小程序) |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.getMessageList = (params) => { |
|||
return postJsonRequest('/enterprise/im/getMessageList', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 设置聊天置顶 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.setChatTopAPI = (params) => { |
|||
return postJsonRequest('/enterprise/im/setChatTop', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 获取好友信息 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.getFriendInfo = (params) => { |
|||
return postJsonRequest('enterprise/friend/getFriendInfo', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 删除聊天 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.delChatAPI = (params) => { |
|||
return postJsonRequest('/enterprise/im/delChat', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 发送文本聊天消息 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.sendMessage = (params) => { |
|||
return postJsonRequest('/enterprise/im/sendMessage', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 转发聊天消息 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.forwardMessage = (params) => { |
|||
return postJsonRequest('/enterprise/im/forwardMessage', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 设置消息已读 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.setMsgIsRead = (params) => { |
|||
return postJsonRequest('/enterprise/im/setMsgIsRead', params) |
|||
} |
|||
|
|||
/** |
|||
* @desc 设置艾特消息已读 |
|||
* @param {*} 参数 |
|||
*/ |
|||
msgApi.readAtMsg = (params) => { |
|||
return postJsonRequest('/enterprise/im/readAtMsg', params) |
|||
} |
|||
|
|||
/** |
|||
* 撤回消息 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.undoMessage = (params) => { |
|||
return postJsonRequest('/enterprise/im/undoMessage', params) |
|||
} |
|||
|
|||
/** |
|||
* 删除消息 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.delMessage = (params) => { |
|||
return postJsonRequest('/enterprise/im/delMessage', params) |
|||
} |
|||
|
|||
/** |
|||
* 删除消息个人 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.removeMessage = (params) => { |
|||
return postJsonRequest('/enterprise/im/removeMessage', params) |
|||
} |
|||
|
|||
/** |
|||
* 加入黑名单 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.isBlacklist = (params) => { |
|||
return postJsonRequest('/enterprise/friend/isBlacklist', params) |
|||
} |
|||
|
|||
/** |
|||
* 发送ws消息 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.sendToMsg = (params) => { |
|||
return postJsonRequest('/enterprise/im/sendToMsg', params) |
|||
} |
|||
|
|||
/** |
|||
* 消息免打扰 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.isNoticeAPI = (params) => { |
|||
return postJsonRequest('/enterprise/im/isNotice', params) |
|||
} |
|||
|
|||
/** |
|||
* 更新业务卡片 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.updateCard = (params) => { |
|||
return postJsonRequest('/enterprise/im/updateCard', params) |
|||
} |
|||
|
|||
/** |
|||
* 同意或者忽略团队 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.joinGroup = (params) => { |
|||
return postJsonRequest('/enterprise/group/joinGroup', params) |
|||
} |
|||
|
|||
/** |
|||
* 获取群成员 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.groupUserList = (params) => { |
|||
return postJsonRequest('/enterprise/group/groupUserList', params) |
|||
} |
|||
|
|||
/** |
|||
* 修改群公告 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.setNotice = (params) => { |
|||
return postJsonRequest('/enterprise/group/setNotice', params) |
|||
} |
|||
/** |
|||
* 群管理信息 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.groupInfo = (params) => { |
|||
return postJsonRequest('/enterprise/group/groupInfo', params) |
|||
} |
|||
|
|||
/** |
|||
* 加入群聊 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.joinGroup = (params) => { |
|||
return postJsonRequest('/enterprise/group/joinGroup', params) |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 修改群管理信息 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.groupSetting = (params) => { |
|||
return postJsonRequest('/enterprise/group/groupSetting', params) |
|||
} |
|||
|
|||
/** |
|||
* 转让管理权限 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.changeOwner = (params) => { |
|||
return postJsonRequest('/enterprise/group/changeOwner', params) |
|||
} |
|||
|
|||
/** |
|||
* 获取全部人员 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.getAllUser = (params) => { |
|||
return postJsonRequest('/enterprise/group/getAllUser', params) |
|||
} |
|||
|
|||
/** |
|||
* 创建群聊 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.addGroup = (params) => { |
|||
return postJsonRequest('/enterprise/group/add', params) |
|||
} |
|||
/** |
|||
* 绑定群聊 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.bindGroup = (params) => { |
|||
return postJsonRequest('/common/index/bindGroup', params) |
|||
} |
|||
|
|||
/** |
|||
* 修改群聊名字 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.editGroupName = (params) => { |
|||
return postJsonRequest('/enterprise/group/editGroupName', params) |
|||
} |
|||
/** |
|||
* 添加群成员 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.addGroupUser = (params) => { |
|||
return postJsonRequest('/enterprise/group/addGroupUser', params) |
|||
} |
|||
|
|||
/** |
|||
* 删除群成员 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.removeUser = (params) => { |
|||
return postJsonRequest('/enterprise/group/removeUser', params) |
|||
} |
|||
|
|||
/** |
|||
* 删除群聊 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.removeGroup = (params) => { |
|||
return postJsonRequest('/enterprise/group/removeGroup', params) |
|||
} |
|||
|
|||
/** |
|||
* 删除群聊聊天记录 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.clearMessage = (params) => { |
|||
return postJsonRequest('/enterprise/group/clearMessage', params) |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 设置/取消管理员 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.setManager = (params) => { |
|||
return postJsonRequest('/enterprise/group/setManager', params) |
|||
} |
|||
|
|||
/** |
|||
* 设置禁言 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.setNoSpeak = (params) => { |
|||
return postJsonRequest('/enterprise/group/setNoSpeak', params) |
|||
} |
|||
|
|||
/** |
|||
* 获取成员信息 |
|||
* @param {*} data |
|||
*/ |
|||
msgApi.getUserInfo = (params) => { |
|||
return postJsonRequest('/enterprise/im/getUserInfo', params) |
|||
} |
|||
|
|||
// 搜索用户
|
|||
msgApi.searchUser= (params) =>{ |
|||
return postJsonRequest('enterprise/im/searchUser', params) |
|||
} |
|||
|
|||
// 修改用户信息
|
|||
msgApi.updateUserInfo= (params) =>{ |
|||
return postJsonRequest('enterprise/im/updateUserInfo', params) |
|||
} |
|||
|
|||
// 修改账号
|
|||
msgApi.editAccount= (params) =>{ |
|||
return postJsonRequest('enterprise/im/editAccount', params) |
|||
} |
|||
|
|||
// 修改密码
|
|||
msgApi.editPassword= (params) =>{ |
|||
return postJsonRequest('enterprise/im/editPassword', params) |
|||
} |
|||
|
|||
// 获取联系人信息
|
|||
msgApi.contactInfo= (params) =>{ |
|||
return postJsonRequest('enterprise/im/getContactInfo', params) |
|||
} |
|||
|
|||
// 获取公告信息
|
|||
msgApi.getAdminNotice= (params) =>{ |
|||
return postJsonRequest('enterprise/im/getAdminNotice', params) |
|||
} |
|||
|
|||
// 获取聊天原图
|
|||
msgApi.viewOriginalImage= (params) =>{ |
|||
return postJsonRequest('enterprise/files/viewOriginalImage', params) |
|||
} |
|||
export default msgApi; |
|||
@ -0,0 +1,865 @@ |
|||
|
|||
import config from "@/common/config.js" |
|||
import { postJsonRequest } from '@/utils/request.js'; |
|||
|
|||
const updateConfig=config.updateConfig; |
|||
const platform = uni.getSystemInfoSync().platform; |
|||
// 主颜色
|
|||
const $mainColor = updateConfig.bgColor ? updateConfig.bgColor : "FF5B78"; |
|||
// 弹窗图标url
|
|||
const $iconUrl = updateConfig.iconUrl ? updateConfig.iconUrl : "/static/image/rocket.png"; |
|||
|
|||
const $checkUpdateUrl=updateConfig.url ? updateConfig.url : ""; |
|||
|
|||
|
|||
// 获取当前应用的版本号
|
|||
export const getCurrentNo = (callback) => { |
|||
// 获取本地应用资源版本号
|
|||
plus.runtime.getProperty(plus.runtime.appid,(inf) => { |
|||
callback && callback({ |
|||
versionCode: inf.versionCode, |
|||
versionName: inf.version |
|||
}); |
|||
}); |
|||
} |
|||
// 从服务器下载应用资源包(wgt文件)
|
|||
const getDownload = (data)=>{ |
|||
let dtask; |
|||
if(data.updateType == 'forcibly' || data.updateType == 'solicit'){ |
|||
let popupData = { |
|||
progress: true, |
|||
buttonNum: 2 |
|||
}; |
|||
if(data.updateType == 'forcibly'){ |
|||
popupData.buttonNum = 0; |
|||
} |
|||
let lastProgressValue = 0; |
|||
let popupObj = downloadPopup(popupData); |
|||
dtask = plus.downloader.createDownload(data.downloadUrl, { |
|||
filename: "_doc/update/" |
|||
}, function(download, status) { |
|||
if (status == 200) { |
|||
popupObj.change({ |
|||
progressValue: 100, |
|||
progressTip:"正在安装文件...", |
|||
progress: true, |
|||
buttonNum: 0 |
|||
}); |
|||
plus.runtime.install(download.filename, {}, function() { |
|||
popupObj.change({ |
|||
contentText: "应用资源更新完成!", |
|||
buttonNum: 1, |
|||
progress: false |
|||
}); |
|||
}, function(e) { |
|||
popupObj.cancel(); |
|||
plus.nativeUI.alert("安装文件失败[" + e.code + "]:" + e.message); |
|||
}); |
|||
} else { |
|||
popupObj.change({ |
|||
contentText: "文件下载失败...", |
|||
buttonNum: 1, |
|||
progress: false |
|||
}); |
|||
} |
|||
}); |
|||
dtask.start(); |
|||
dtask.addEventListener("statechanged", function(task, status) { |
|||
switch (task.state) { |
|||
case 1: // 开始
|
|||
popupObj.change({ |
|||
progressValue:0, |
|||
progressTip:"准备下载...", |
|||
progress: true |
|||
}); |
|||
break; |
|||
case 2: // 已连接到服务器
|
|||
popupObj.change({ |
|||
progressValue:0, |
|||
progressTip:"开始下载...", |
|||
progress: true |
|||
}); |
|||
break; |
|||
case 3: |
|||
const progress = parseInt(task.downloadedSize / task.totalSize * 100); |
|||
if(progress - lastProgressValue >= 2){ |
|||
lastProgressValue = progress; |
|||
popupObj.change({ |
|||
progressValue:progress, |
|||
progressTip: "已下载" + progress + "%", |
|||
progress: true |
|||
}); |
|||
} |
|||
break; |
|||
} |
|||
}); |
|||
// 取消下载
|
|||
popupObj.cancelDownload = function(){ |
|||
dtask && dtask.abort(); |
|||
uni.showToast({ |
|||
title: "已取消下载", |
|||
icon:"none" |
|||
}); |
|||
} |
|||
// 重启APP
|
|||
popupObj.reboot = function(){ |
|||
plus.runtime.restart(); |
|||
} |
|||
} else if(data.updateType == "silent"){ |
|||
dtask = plus.downloader.createDownload(data.downloadUrl, { |
|||
filename: "_doc/update/" |
|||
}, function(download, status) { |
|||
if (status == 200) { |
|||
plus.runtime.install(download.filename, {}, function() { |
|||
console.log("应用资源更新完成"); |
|||
}, function(e) { |
|||
plus.nativeUI.alert("安装文件失败[" + e.code + "]:" + e.message); |
|||
}); |
|||
} else { |
|||
plus.nativeUI.alert("文件下载失败..."); |
|||
} |
|||
}); |
|||
dtask.start(); |
|||
} |
|||
} |
|||
// 文字换行
|
|||
const drawtext = (text, maxWidth)=>{ |
|||
let textArr = text.split(""); |
|||
let len = textArr.length; |
|||
// 上个节点
|
|||
let previousNode = 0; |
|||
// 记录节点宽度
|
|||
let nodeWidth = 0; |
|||
// 文本换行数组
|
|||
let rowText = []; |
|||
// 如果是字母,侧保存长度
|
|||
let letterWidth = 0; |
|||
// 汉字宽度
|
|||
let chineseWidth = 14; |
|||
// otherFont宽度
|
|||
let otherWidth = 7; |
|||
for (let i = 0; i < len; i++) { |
|||
if (/[\u4e00-\u9fa5]|[\uFE30-\uFFA0]/g.test(textArr[i])) { |
|||
if(letterWidth > 0){ |
|||
if(nodeWidth + chineseWidth + letterWidth * otherWidth > maxWidth){ |
|||
rowText.push({ |
|||
type: "text", |
|||
content: text.substring(previousNode, i) |
|||
}); |
|||
previousNode = i; |
|||
nodeWidth = chineseWidth; |
|||
letterWidth = 0; |
|||
} else { |
|||
nodeWidth += chineseWidth + letterWidth * otherWidth; |
|||
letterWidth = 0; |
|||
} |
|||
} else { |
|||
if(nodeWidth + chineseWidth > maxWidth){ |
|||
rowText.push({ |
|||
type: "text", |
|||
content: text.substring(previousNode, i) |
|||
}); |
|||
previousNode = i; |
|||
nodeWidth = chineseWidth; |
|||
}else{ |
|||
nodeWidth += chineseWidth; |
|||
} |
|||
} |
|||
} else { |
|||
if(/\n/g.test(textArr[i])){ |
|||
rowText.push({ |
|||
type: "break", |
|||
content: text.substring(previousNode, i) |
|||
}); |
|||
previousNode = i + 1; |
|||
nodeWidth = 0; |
|||
letterWidth = 0; |
|||
}else if(textArr[i] == "\\" && textArr[i + 1] == "n"){ |
|||
rowText.push({ |
|||
type: "break", |
|||
content: text.substring(previousNode, i) |
|||
}); |
|||
previousNode = i + 2; |
|||
nodeWidth = 0; |
|||
letterWidth = 0; |
|||
}else if(/[a-zA-Z0-9]/g.test(textArr[i])){ |
|||
letterWidth += 1; |
|||
if(nodeWidth + letterWidth * otherWidth > maxWidth){ |
|||
rowText.push({ |
|||
type: "text", |
|||
content: text.substring(previousNode, i + 1 - letterWidth) |
|||
}); |
|||
previousNode = i + 1 - letterWidth; |
|||
nodeWidth = letterWidth * otherWidth; |
|||
letterWidth = 0; |
|||
} |
|||
} else{ |
|||
if(nodeWidth + otherWidth > maxWidth){ |
|||
rowText.push({ |
|||
type: "text", |
|||
content: text.substring(previousNode, i) |
|||
}); |
|||
previousNode = i; |
|||
nodeWidth = otherWidth; |
|||
}else{ |
|||
nodeWidth += otherWidth; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (previousNode < len) { |
|||
rowText.push({ |
|||
type: "text", |
|||
content: text.substring(previousNode, len) |
|||
}); |
|||
} |
|||
return rowText; |
|||
} |
|||
// 是否更新弹窗
|
|||
const updatePopup = (data, callback) => { |
|||
// 弹窗遮罩层
|
|||
let maskLayer = new plus.nativeObj.View("maskLayer", { //先创建遮罩层
|
|||
top: '0px', |
|||
left: '0px', |
|||
height: '100%', |
|||
width: '100%', |
|||
backgroundColor: 'rgba(0,0,0,0.5)' |
|||
}); |
|||
|
|||
// 以下为计算菜单的nview绘制布局,为固定算法,使用者无关关心
|
|||
const screenWidth = plus.screen.resolutionWidth; |
|||
const screenHeight = plus.screen.resolutionHeight; |
|||
//弹窗容器宽度
|
|||
const popupViewWidth = screenWidth * 0.7; |
|||
// 弹窗容器的Padding
|
|||
const viewContentPadding = 20; |
|||
// 弹窗容器的宽度
|
|||
const viewContentWidth = parseInt(popupViewWidth - (viewContentPadding * 2)); |
|||
// 描述的列表
|
|||
const descriptionList = drawtext(data.versionInfo, viewContentWidth); |
|||
// 弹窗容器高度
|
|||
let popupViewHeight = 80 + 20 + 20 + 90 + 10; |
|||
|
|||
let popupViewContentList = [{ |
|||
src: $iconUrl, |
|||
id: "logo", |
|||
tag: "img", |
|||
position: { |
|||
top: "0px", |
|||
left: (popupViewWidth - 124) / 2 + "px", |
|||
width: "124px", |
|||
height: "80px", |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'font', |
|||
id: 'title', |
|||
text: "发现新版本:" + data.versionName, |
|||
textStyles: { |
|||
size: '18px', |
|||
color: "#333", |
|||
weight: "bold", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
top: '90px', |
|||
left: viewContentPadding + "px", |
|||
width: viewContentWidth + "px", |
|||
height: "30px", |
|||
} |
|||
}]; |
|||
const textHeight = 18; |
|||
let contentTop = 130; |
|||
descriptionList.forEach((item,index) => { |
|||
if(index > 0){ |
|||
popupViewHeight += textHeight; |
|||
contentTop += textHeight; |
|||
} |
|||
popupViewContentList.push({ |
|||
tag: 'font', |
|||
id: 'content' + index + 1, |
|||
text: item.content, |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#666", |
|||
lineSpacing: "50%", |
|||
align: "left" |
|||
}, |
|||
position: { |
|||
top: contentTop + "px", |
|||
left: viewContentPadding + "px", |
|||
width: viewContentWidth + "px", |
|||
height: textHeight + "px", |
|||
} |
|||
}); |
|||
if(item.type == "break"){ |
|||
contentTop += 10; |
|||
popupViewHeight += 10; |
|||
} |
|||
}); |
|||
|
|||
if(data.updateType == "forcibly"){ |
|||
popupViewContentList.push({ |
|||
tag: 'rect', //绘制底边按钮
|
|||
rectStyles:{ |
|||
radius: "6px", |
|||
color: $mainColor |
|||
}, |
|||
position:{ |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: viewContentWidth + "px", |
|||
height: "30px" |
|||
} |
|||
}); |
|||
popupViewContentList.push({ |
|||
tag: 'font', |
|||
id: 'confirmText', |
|||
text: "立即升级", |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#FFF", |
|||
lineSpacing: "0%", |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: viewContentWidth + "px", |
|||
height: "30px" |
|||
} |
|||
}); |
|||
} else { |
|||
// 绘制底边按钮
|
|||
popupViewContentList.push({ |
|||
tag: 'rect', |
|||
id: 'cancelBox', |
|||
rectStyles: { |
|||
radius: "3px", |
|||
borderColor: "#f1f1f1", |
|||
borderWidth: "1px", |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px", |
|||
} |
|||
}); |
|||
popupViewContentList.push({ |
|||
tag: 'rect', |
|||
id: 'confirmBox', |
|||
rectStyles: { |
|||
radius: "3px", |
|||
color: $mainColor, |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: ((viewContentWidth - viewContentPadding) / 2 + viewContentPadding * 2) + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px", |
|||
} |
|||
}); |
|||
popupViewContentList.push({ |
|||
tag: 'font', |
|||
id: 'cancelText', |
|||
text: "暂不升级", |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#666", |
|||
lineSpacing: "0%", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px", |
|||
} |
|||
}); |
|||
popupViewContentList.push({ |
|||
tag: 'font', |
|||
id: 'confirmText', |
|||
text: "立即升级", |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#FFF", |
|||
lineSpacing: "0%", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: ((viewContentWidth - viewContentPadding) / 2 + viewContentPadding * 2) + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px", |
|||
} |
|||
}); |
|||
} |
|||
// 弹窗内容
|
|||
let popupView = new plus.nativeObj.View("popupView", { //创建底部图标菜单
|
|||
tag: "rect", |
|||
top: (screenHeight - popupViewHeight) / 2 + "px", |
|||
left: '15%', |
|||
height: popupViewHeight + "px", |
|||
width: "70%" |
|||
}); |
|||
// 绘制白色背景
|
|||
popupView.drawRect({ |
|||
color: "#FFFFFF", |
|||
radius: "8px" |
|||
}, { |
|||
top: "40px", |
|||
height: popupViewHeight - 40 + "px", |
|||
}); |
|||
|
|||
popupView.draw(popupViewContentList); |
|||
popupView.addEventListener("click", function(e) { |
|||
let maxTop = popupViewHeight - viewContentPadding; |
|||
let maxLeft = popupViewWidth - viewContentPadding; |
|||
let buttonWidth = (viewContentWidth - viewContentPadding) / 2; |
|||
if (e.clientY > maxTop - 30 && e.clientY < maxTop) { |
|||
if(data.updateType == "forcibly"){ |
|||
if(e.clientX > viewContentPadding && e.clientX < maxLeft){ |
|||
// 立即升级
|
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
callback && callback(); |
|||
} |
|||
} else { |
|||
// 暂不升级
|
|||
if (e.clientX > viewContentPadding && e.clientX < maxLeft - buttonWidth - viewContentPadding) { |
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
} else if (e.clientX > maxLeft - buttonWidth && e.clientX < maxLeft) { |
|||
// 立即升级
|
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
callback && callback(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
}); |
|||
if(data.updateType == "solicit"){ |
|||
// 点击遮罩层
|
|||
maskLayer.addEventListener("click", function() { //处理遮罩层点击
|
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
}); |
|||
} |
|||
// 显示弹窗
|
|||
maskLayer.show(); |
|||
popupView.show(); |
|||
} |
|||
// 文件下载的弹窗绘图
|
|||
const downloadPopupDrawing =(data) => { |
|||
// 以下为计算菜单的nview绘制布局,为固定算法,使用者无关关心
|
|||
const screenWidth = plus.screen.resolutionWidth; |
|||
const screenHeight = plus.screen.resolutionHeight; |
|||
//弹窗容器宽度
|
|||
const popupViewWidth = screenWidth * 0.7; |
|||
// 弹窗容器的Padding
|
|||
const viewContentPadding = 20; |
|||
// 弹窗容器的宽度
|
|||
const viewContentWidth = popupViewWidth - (viewContentPadding * 2); |
|||
// 弹窗容器高度
|
|||
let popupViewHeight = viewContentPadding * 3 + 60; |
|||
let progressTip = data.progressTip || "准备下载..."; |
|||
let contentText = data.contentText || "正在为您更新,请耐心等待"; |
|||
let elementList = [ |
|||
{ |
|||
tag: 'rect', //背景色
|
|||
color: '#FFFFFF', |
|||
rectStyles:{ |
|||
radius: "8px" |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'font', |
|||
id: 'title', |
|||
text: "升级APP", |
|||
textStyles: { |
|||
size: '16px', |
|||
color: "#333", |
|||
weight: "bold", |
|||
verticalAlign: "middle", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
top: viewContentPadding + 'px', |
|||
height: "30px", |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'font', |
|||
id: 'content', |
|||
text: contentText, |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#333", |
|||
verticalAlign: "middle", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
top: viewContentPadding * 2 + 30 + 'px', |
|||
height: "20px", |
|||
} |
|||
} |
|||
]; |
|||
// 是否有进度条
|
|||
if(data.progress){ |
|||
popupViewHeight += viewContentPadding + 40; |
|||
elementList = elementList.concat([ |
|||
{ |
|||
tag: 'font', |
|||
id: 'progressValue', |
|||
text: progressTip, |
|||
textStyles: { |
|||
size: '14px', |
|||
color: $mainColor, |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
top: viewContentPadding * 4 + 20 + 'px', |
|||
height: "30px" |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'rect', //绘制进度条背景
|
|||
id: 'progressBg', |
|||
rectStyles:{ |
|||
radius: "4px", |
|||
borderColor: "#f1f1f1", |
|||
borderWidth: "1px", |
|||
}, |
|||
position:{ |
|||
top: viewContentPadding * 4 + 60 + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: viewContentWidth + "px", |
|||
height: "8px" |
|||
} |
|||
}, |
|||
]); |
|||
} |
|||
if (data.buttonNum == 2) { |
|||
popupViewHeight += viewContentPadding + 30; |
|||
elementList = elementList.concat([ |
|||
{ |
|||
tag: 'rect', //绘制底边按钮
|
|||
rectStyles:{ |
|||
radius: "3px", |
|||
borderColor: "#f1f1f1", |
|||
borderWidth: "1px", |
|||
}, |
|||
position:{ |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px" |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'rect', //绘制底边按钮
|
|||
rectStyles:{ |
|||
radius: "3px", |
|||
color: $mainColor |
|||
}, |
|||
position:{ |
|||
bottom: viewContentPadding + 'px', |
|||
left: ((viewContentWidth - viewContentPadding) / 2 + viewContentPadding * 2) + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px" |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'font', |
|||
id: 'cancelText', |
|||
text: "取消下载", |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#666", |
|||
lineSpacing: "0%", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px", |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'font', |
|||
id: 'confirmText', |
|||
text: "后台下载", |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#FFF", |
|||
lineSpacing: "0%", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: ((viewContentWidth - viewContentPadding) / 2 + viewContentPadding * 2) + "px", |
|||
width: (viewContentWidth - viewContentPadding) / 2 + "px", |
|||
height: "30px", |
|||
} |
|||
} |
|||
]); |
|||
} |
|||
if (data.buttonNum == 1) { |
|||
popupViewHeight += viewContentPadding + 40; |
|||
elementList = elementList.concat([ |
|||
{ |
|||
tag: 'rect', //绘制底边按钮
|
|||
rectStyles:{ |
|||
radius: "6px", |
|||
color: $mainColor |
|||
}, |
|||
position:{ |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: viewContentWidth + "px", |
|||
height: "40px" |
|||
} |
|||
}, |
|||
{ |
|||
tag: 'font', |
|||
id: 'confirmText', |
|||
text: "关闭", |
|||
textStyles: { |
|||
size: '14px', |
|||
color: "#FFF", |
|||
lineSpacing: "0%", |
|||
}, |
|||
position: { |
|||
bottom: viewContentPadding + 'px', |
|||
left: viewContentPadding + "px", |
|||
width: viewContentWidth + "px", |
|||
height: "40px" |
|||
} |
|||
} |
|||
]); |
|||
} |
|||
return { |
|||
popupViewHeight:popupViewHeight, |
|||
popupViewWidth:popupViewWidth, |
|||
screenHeight:screenHeight, |
|||
viewContentWidth:viewContentWidth, |
|||
viewContentPadding:viewContentPadding, |
|||
elementList: elementList |
|||
}; |
|||
} |
|||
// 文件下载的弹窗
|
|||
const downloadPopup=(data)=>{ |
|||
// 弹窗遮罩层
|
|||
let maskLayer = new plus.nativeObj.View("maskLayer", { //先创建遮罩层
|
|||
top: '0px', |
|||
left: '0px', |
|||
height: '100%', |
|||
width: '100%', |
|||
backgroundColor: 'rgba(0,0,0,0.5)' |
|||
}); |
|||
let popupViewData = downloadPopupDrawing(data); |
|||
// 弹窗内容
|
|||
let popupView = new plus.nativeObj.View("popupView", { //创建底部图标菜单
|
|||
tag: "rect", |
|||
top: (popupViewData.screenHeight - popupViewData.popupViewHeight) / 2 + "px", |
|||
left: '15%', |
|||
height: popupViewData.popupViewHeight + "px", |
|||
width: "70%", |
|||
}); |
|||
let progressValue = 0; |
|||
let progressTip = 0; |
|||
let contentText = 0; |
|||
let buttonNum = 2; |
|||
if(data.buttonNum >= 0){ |
|||
buttonNum = data.buttonNum; |
|||
} |
|||
popupView.draw(popupViewData.elementList); |
|||
let callbackData = { |
|||
change: function(res) { |
|||
let progressElement = []; |
|||
if(res.progressValue){ |
|||
progressValue = res.progressValue; |
|||
// 绘制进度条
|
|||
progressElement.push({ |
|||
tag: 'rect', //绘制进度条背景
|
|||
id: 'progressValueBg', |
|||
rectStyles:{ |
|||
radius: "4px", |
|||
color: $mainColor |
|||
}, |
|||
position:{ |
|||
top: popupViewData.viewContentPadding * 4 + 60 + 'px', |
|||
left: popupViewData.viewContentPadding + "px", |
|||
width: popupViewData.viewContentWidth * (res.progressValue / 100) + "px", |
|||
height: "8px" |
|||
} |
|||
}); |
|||
} |
|||
if(res.progressTip){ |
|||
progressTip = res.progressTip; |
|||
progressElement.push({ |
|||
tag: 'font', |
|||
id: 'progressValue', |
|||
text: res.progressTip, |
|||
textStyles: { |
|||
size: '14px', |
|||
color: $mainColor, |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
top: popupViewData.viewContentPadding * 4 + 20 + 'px', |
|||
height: "30px" |
|||
} |
|||
}); |
|||
} |
|||
if(res.contentText){ |
|||
contentText = res.contentText; |
|||
progressElement.push({ |
|||
tag: 'font', |
|||
id: 'content', |
|||
text: res.contentText, |
|||
textStyles: { |
|||
size: '16px', |
|||
color: "#333", |
|||
whiteSpace: "normal" |
|||
}, |
|||
position: { |
|||
top: popupViewData.viewContentPadding * 2 + 30 + 'px', |
|||
height: "30px", |
|||
} |
|||
}); |
|||
} |
|||
if(res.buttonNum >= 0 && buttonNum != res.buttonNum){ |
|||
buttonNum = res.buttonNum; |
|||
popupView.reset(); |
|||
popupViewData = downloadPopupDrawing(Object.assign({ |
|||
progressValue:progressValue, |
|||
progressTip:progressTip, |
|||
contentText:contentText, |
|||
},res)); |
|||
let newElement = []; |
|||
popupViewData.elementList.map((item,index) => { |
|||
let have = false; |
|||
progressElement.forEach((childItem,childIndex) => { |
|||
if(item.id == childItem.id){ |
|||
have = true; |
|||
} |
|||
}); |
|||
if(!have){ |
|||
newElement.push(item); |
|||
} |
|||
}); |
|||
progressElement = newElement.concat(progressElement); |
|||
popupView.setStyle({ |
|||
tag: "rect", |
|||
top: (popupViewData.screenHeight - popupViewData.popupViewHeight) / 2 + "px", |
|||
left: '15%', |
|||
height: popupViewData.popupViewHeight + "px", |
|||
width: "70%", |
|||
}); |
|||
popupView.draw(progressElement); |
|||
}else{ |
|||
popupView.draw(progressElement); |
|||
} |
|||
}, |
|||
cancel: function() { |
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
} |
|||
} |
|||
popupView.addEventListener("click", function(e) { |
|||
let maxTop = popupViewData.popupViewHeight - popupViewData.viewContentPadding; |
|||
let maxLeft = popupViewData.popupViewWidth - popupViewData.viewContentPadding; |
|||
if (e.clientY > maxTop - 40 && e.clientY < maxTop) { |
|||
if(buttonNum == 1){ |
|||
// 单按钮
|
|||
if (e.clientX > popupViewData.viewContentPadding && e.clientX < maxLeft) { |
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
callbackData.reboot(); |
|||
} |
|||
}else if(buttonNum == 2){ |
|||
// 双按钮
|
|||
let buttonWidth = (popupViewData.viewContentWidth - popupViewData.viewContentPadding) / 2; |
|||
if (e.clientX > popupViewData.viewContentPadding && e.clientX < maxLeft - buttonWidth - popupViewData.viewContentPadding) { |
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
callbackData.cancelDownload(); |
|||
} else if (e.clientX > maxLeft - buttonWidth && e.clientX < maxLeft) { |
|||
maskLayer.hide(); |
|||
popupView.hide(); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
// 显示弹窗
|
|||
maskLayer.show(); |
|||
popupView.show(); |
|||
// 改变进度条
|
|||
return callbackData; |
|||
} |
|||
|
|||
export default (isPrompt = false)=>{ |
|||
|
|||
getCurrentNo(versionInfo => { |
|||
let httpData = { |
|||
release: versionInfo.versionCode, |
|||
// 版本名称
|
|||
version: versionInfo.versionName, |
|||
// setupPage参数说明(判断用户是不是从设置页面点击的更新,如果是设置页面点击的更新,有不要用静默更新了,不然用户点击没反应很奇怪的)
|
|||
setupPage: isPrompt |
|||
}; |
|||
if (platform == "android") { |
|||
httpData.type = 1101; |
|||
} else { |
|||
httpData.type = 1102; |
|||
} |
|||
/* 接口入参说明 |
|||
* version: 应用当前版本号(已自动获取) |
|||
* versionName: 应用当前版本名称(已自动获取) |
|||
* type:平台(1101是安卓,1102是IOS) |
|||
*/ |
|||
/****************以下是示例*******************/ |
|||
// 可以用自己项目的请求方法(接口自己找后台要,插件不提供)
|
|||
postJsonRequest($checkUpdateUrl,httpData).then((e)=>{ |
|||
let res=e.data; |
|||
if (res && res.downloadUrl) { |
|||
if (res.updateType == "forcibly" || res.updateType == "silent") { |
|||
if (/\.wgt$/i.test(res.downloadUrl)) { |
|||
getDownload(res); |
|||
} else if(/\.html$/i.test(res.downloadUrl)){ |
|||
plus.runtime.openURL(res.downloadUrl); |
|||
} else { |
|||
if (platform == "android") { |
|||
getDownload(res); |
|||
} else { |
|||
plus.runtime.openURL(res.downloadUrl); |
|||
} |
|||
} |
|||
} else if(res.updateType == "solicit"){ |
|||
updatePopup(res, function() { |
|||
if (/\.wgt$/i.test(res.downloadUrl)) { |
|||
getDownload(res); |
|||
} else if(/\.html$/i.test(res.downloadUrl)){ |
|||
plus.runtime.openURL(res.downloadUrl); |
|||
} else { |
|||
if (platform == "android") { |
|||
getDownload(res); |
|||
} else { |
|||
plus.runtime.openURL(res.downloadUrl); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} else if (isPrompt) { |
|||
uni.showToast({ |
|||
title: "暂无新版本", |
|||
icon: "none" |
|||
}); |
|||
} |
|||
}) |
|||
}); |
|||
} |
|||
|
|||
@ -0,0 +1,65 @@ |
|||
import CryptoJS from "crypto-js"; |
|||
|
|||
// 如果为开启ssl证书,请修改为http协议
|
|||
// let scheme ="https"; //协议头
|
|||
let scheme ="http"; //协议头
|
|||
// 请将下面的域名替换成自己的服务器域名,如果有端口要把端口加上
|
|||
let host = '192.168.66.16:8007'; |
|||
|
|||
// 为了避免h5页面被其他人盗用,可以自由选择加密方式,详细教程查看readme.md文件
|
|||
let hostToken="sha5602584d3bc770dd47e9680a99d0efe6bi"; |
|||
|
|||
// 是否开启H5的调试模式
|
|||
let isVConsole = false |
|||
|
|||
// 以下内容请勿修改
|
|||
// 以下内容请勿修改
|
|||
// 以下内容请勿修改
|
|||
// 以下内容请勿修改
|
|||
|
|||
// #ifdef MP-WEIXIN
|
|||
let env = wx.getAccountInfoSync() |
|||
|
|||
if (env.miniProgram.envVersion == 'develop') { |
|||
isVConsole = true |
|||
} |
|||
// #endif
|
|||
// #ifdef APP-PLUS || H5
|
|||
if (process.env.NODE_ENV === 'development') { |
|||
isVConsole = true |
|||
} |
|||
// #endif
|
|||
|
|||
let apiUrl = scheme + '://' + host; |
|||
let wssUrl = (scheme == 'https' ? 'wss' :'ws') + '://' + host + '/wss'; |
|||
|
|||
let hostMd5=CryptoJS.MD5(CryptoJS.MD5(host).toString()).toString(); |
|||
if('sha'+hostMd5+'bi'!=hostToken){ |
|||
apiUrl = 'false'; |
|||
wssUrl = 'false'; |
|||
} |
|||
|
|||
// app更新的配置
|
|||
const updateConfig = { |
|||
url:apiUrl+'/common/pub/checkVersion', //检查版本的接口
|
|||
bgColor:'', //升级主色,按钮背景颜色
|
|||
iconUrl:'' //升级小图标
|
|||
} |
|||
|
|||
/* 检查版本需要返回的数据说明 |
|||
* | 参数名称 | 一定返回 | 类型 | 描述 |
|||
* | -------------|--------- | --------- | ------------- | |
|||
* | versionCode | y | int | 版本号 :20240331 | |
|||
* | versionName | y | String | 版本名称 :4.0.1 | |
|||
* | versionInfo | y | String | 版本信息 : 修复了bug | |
|||
* | updateType | y | String | forcibly = 强制更新, solicit = 弹窗确认更新, silent = 静默更新 | |
|||
* | downloadUrl | y | String | 版本下载链接(IOS安装包更新请放跳转store应用商店链接,安卓apk和wgt文件放文件下载链接) | |
|||
*/ |
|||
|
|||
|
|||
export default { |
|||
apiUrl, |
|||
wssUrl, |
|||
isVConsole, |
|||
updateConfig |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
import { |
|||
apiUrl, |
|||
postJsonRequest, |
|||
} from '@/utils/request.js'; |
|||
|
|||
const verifyQr=(url)=>{ |
|||
let pathinfo=url.replace(apiUrl,''); |
|||
let pathParts = pathinfo.split('/'); // 使用斜杠字符分割字符串
|
|||
let lastPart = pathParts[pathParts.length - 1]; // 获取最后一组数据
|
|||
postJsonRequest(pathinfo,{realToken:lastPart}).then((res)=>{ |
|||
if(res.code==0){ |
|||
switch(res.data.action){ |
|||
case 'groupInfo': |
|||
uni.navigateTo({ |
|||
url: '/pages/message/group/info?group_id='+ res.data.id |
|||
}) |
|||
break; |
|||
case 'userInfo': |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+res.data.id |
|||
}) |
|||
break; |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const scanQr=()=>{ |
|||
// #ifndef H5
|
|||
uni.scanCode({ |
|||
success: function (res) { |
|||
checkQr(res.result); |
|||
} |
|||
}); |
|||
// #endif
|
|||
// #ifdef H5
|
|||
uni.navigateTo({ |
|||
url:'/pages/index/scan' |
|||
}) |
|||
// #endif
|
|||
} |
|||
|
|||
const checkQr=(data)=>{ |
|||
// 如果识别出二维码是跟服务器的地址一样,就请求该接口
|
|||
if(data.includes(apiUrl)){ |
|||
verifyQr(data); |
|||
}else{ |
|||
uni.showModal({ |
|||
title: '已识别内容', |
|||
content: data, |
|||
confirmText:'复制内容', |
|||
success: function (e) { |
|||
if (e.confirm) { |
|||
uni.setClipboardData({ |
|||
data: data, |
|||
success: function () { |
|||
uni.showToast({ |
|||
title:'复制成功', |
|||
icon:'none' |
|||
}) |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export default { |
|||
scanQr, |
|||
checkQr |
|||
} |
|||
@ -0,0 +1,193 @@ |
|||
import api from '@/common/config.js' // 接口Api,图片地址等等配置,可根据自身情况引入,也可以直接在下面url填入你的 webSocket连接地址
|
|||
class socketIO { |
|||
constructor(data, time, url) { |
|||
this.socketTask = null |
|||
this.is_open_socket = false //避免重复连接
|
|||
this.url = url ? url : api.wssUrl //连接地址
|
|||
this.data = data ? data : null |
|||
this.connectNum = 1 // 重连次数
|
|||
this.traderDetailIndex = 100 // traderDetailIndex ==2 重连
|
|||
this.accountStateIndex = 100 // accountStateIndex ==1 重连
|
|||
this.followFlake = false // followFlake == true 重连
|
|||
this.init=false; |
|||
//心跳检测
|
|||
this.timeout = time ? time : 25000 //多少秒执行检测
|
|||
this.heartbeatInterval = null //检测服务器端是否还活着
|
|||
this.reconnectTimeOut = null //重连之后多久再次重连
|
|||
this.networkStatus=true |
|||
} |
|||
|
|||
CALLBACK = (res) => { |
|||
if(res.isConnected){ |
|||
this.traderDetailIndex=2; |
|||
this.connectSocketInit({type:'ping'}) |
|||
} |
|||
} |
|||
|
|||
// 进入这个页面的时候创建websocket连接【整个页面随时使用】
|
|||
connectSocketInit(data) { |
|||
this.data = data |
|||
this.socketTask = uni.connectSocket({ |
|||
url: this.url, |
|||
success: () => { |
|||
// console.info("正准备建立websocket中...");
|
|||
// 返回实例
|
|||
return this.socketTask |
|||
}, |
|||
}); |
|||
this.socketTask.onOpen((res) => { |
|||
uni.$emit('socketStatus',true); |
|||
if(!this.networkStatus){ |
|||
// 连接成功后取消网络监听
|
|||
uni.offNetworkStatusChange(this.CALLBACK); |
|||
} |
|||
this.networkStatus=true; |
|||
this.connectNum = 1 |
|||
// console.info("WebSocket连接正常!");
|
|||
this.send(data) |
|||
clearInterval(this.reconnectTimeOut) |
|||
clearInterval(this.heartbeatInterval) |
|||
this.is_open_socket = true; |
|||
this.start(); |
|||
// 注:只有连接正常打开中 ,才能正常收到消息
|
|||
this.socketTask.onMessage((e) => { |
|||
// 字符串转json
|
|||
let res = JSON.parse(e.data); |
|||
if (res) { |
|||
uni.$emit('getPositonsOrder', res); |
|||
} |
|||
}); |
|||
}) |
|||
if(!this.init){ |
|||
// 监听连接失败,这里代码我注释掉的原因是因为如果服务器关闭后,和下面的onclose方法一起发起重连操作,这样会导致重复连接
|
|||
uni.onSocketError((res) => { |
|||
console.info(res,'WebSocket连接打开失败,请检查!'); |
|||
this.socketTask = null |
|||
this.is_open_socket = false; |
|||
clearInterval(this.heartbeatInterval) |
|||
clearInterval(this.reconnectTimeOut) |
|||
if (this.connectNum < 10) { |
|||
this.traderDetailIndex = 2 |
|||
this.reconnect(); |
|||
this.connectNum += 1 |
|||
} else { |
|||
uni.$emit('connectError'); |
|||
this.networkStatus=false |
|||
uni.onNetworkStatusChange(this.CALLBACK); |
|||
this.connectNum = 1 |
|||
} |
|||
}); |
|||
this.init=true; |
|||
} |
|||
// 这里仅是事件监听【如果socket关闭了会执行】
|
|||
this.socketTask.onClose(() => { |
|||
console.info("已经被关闭了-------") |
|||
clearInterval(this.heartbeatInterval) |
|||
clearInterval(this.reconnectTimeOut) |
|||
this.is_open_socket = false; |
|||
this.socketTask = null |
|||
if (this.connectNum < 5) { |
|||
this.reconnect(); |
|||
} else { |
|||
uni.$emit('connectError'); |
|||
this.networkStatus=false; |
|||
uni.onNetworkStatusChange(this.CALLBACK); |
|||
this.connectNum = 1 |
|||
} |
|||
|
|||
}) |
|||
} |
|||
// 主动关闭socket连接
|
|||
Close() { |
|||
if (!this.is_open_socket) { |
|||
return |
|||
} |
|||
this.socketTask.close({ |
|||
success() { |
|||
uni.showToast({ |
|||
title: 'SocketTask 关闭成功', |
|||
icon: "none" |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
//发送消息
|
|||
send(data) { |
|||
// 注:只有连接正常打开中 ,才能正常成功发送消息
|
|||
if (this.socketTask) { |
|||
this.socketTask.send({ |
|||
data: JSON.stringify(data), |
|||
async success() { |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// 检测状态
|
|||
checkStatus(){ |
|||
// console.info("检查状态")
|
|||
clearInterval(this.reconnectTimeOut) |
|||
if(!this.socketTask || [2,3].includes(this.socketTask.readyState)){ |
|||
// console.info("未链接!")
|
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
//开启心跳检测
|
|||
start() { |
|||
this.heartbeatInterval = setInterval(() => { |
|||
this.send({ |
|||
"type": "ping" |
|||
}); |
|||
}, this.timeout) |
|||
} |
|||
//重新连接
|
|||
reconnect() { |
|||
//停止发送心跳
|
|||
console.info('检查是否手动断开,并重新连接') |
|||
clearInterval(this.heartbeatInterval) |
|||
//如果不是人为关闭的话,进行重连
|
|||
if (!this.is_open_socket && (this.traderDetailIndex == 2 || this.accountStateIndex == 0 || this |
|||
.followFlake)) { |
|||
console.info("5秒后重新连接...") |
|||
this.reconnectTimeOut = setInterval(() => { |
|||
this.connectSocketInit(this.data); |
|||
}, 15000) |
|||
} |
|||
} |
|||
/** |
|||
* @description 将 scoket 数据进行过滤 |
|||
* @param {array} array |
|||
* @param {string} type 区分 弹窗 openposition 分为跟随和我的 |
|||
*/ |
|||
arrayFilter(array, type = 'normal', signalId = 0) { |
|||
let arr1 = [] |
|||
let arr2 = [] |
|||
let obj = { |
|||
arr1: [], |
|||
arr2: [] |
|||
} |
|||
arr1 = array.filter(v => v.flwsig == true) |
|||
arr2 = array.filter(v => v.flwsig == false) |
|||
if (type == 'normal') { |
|||
if (signalId) { |
|||
arr1 = array.filter(v => v.flwsig == true && v.sigtraderid == signalId) |
|||
return arr1 |
|||
} else { |
|||
return arr1.concat(arr2) |
|||
} |
|||
} else { |
|||
if (signalId > 0) { |
|||
arr1 = array.filter(v => v.flwsig == true && v.sigtraderid == signalId) |
|||
obj.arr1 = arr1 |
|||
} else { |
|||
obj.arr1 = arr1 |
|||
} |
|||
obj.arr2 = arr2 |
|||
|
|||
return obj |
|||
} |
|||
} |
|||
} |
|||
export default socketIO |
|||
@ -0,0 +1,53 @@ |
|||
<template> |
|||
<view class="lz-empty-box"> |
|||
<view v-if="showImage" class="im-flex im-align-items-center im-justify-content-center"> |
|||
<!-- 请根据您的项目要求制作并更换为空图片 --> |
|||
<image class="empty-img" :src="src"></image> |
|||
</view> |
|||
<view class="text-center"> |
|||
<text :class="showImage ? ' mt-20' : '' " :style='{color:textcolor}'>{{noDatatext}}</text> |
|||
</view> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'Empty', |
|||
props: { |
|||
src: { |
|||
type: String, |
|||
default: '/static/image/empty.png' |
|||
}, // 图片路径 |
|||
showImage: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
noDatatext: { |
|||
type: String, |
|||
default: () => { |
|||
return '暂无数据' |
|||
} |
|||
}, |
|||
textcolor: { |
|||
type: String, |
|||
default: "#333333" |
|||
} |
|||
}, |
|||
data() { |
|||
return {}; |
|||
} |
|||
|
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
.lz-empty-box { |
|||
margin: 40rpx auto; |
|||
align:center |
|||
} |
|||
|
|||
.empty-img { |
|||
margin-top: 40rpx; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,75 @@ |
|||
<template> |
|||
<view class="mr-5 radius-4 im-tags lh-15x" :class="[ |
|||
size=='mini' ? 'im-tags-mini' : '', |
|||
size=='small' ? 'im-tags-samll' : '', |
|||
size=='medium' ? 'im-tags-medium' : '', |
|||
type == 'info' ? 'im-tags-info' : '' , |
|||
type == 'success' ? 'im-tags-success' : '' , |
|||
type == 'warning' ? 'im-tags-warning' : '', |
|||
type == 'danger' ? 'im-tags-danger' : '',]" |
|||
>{{text}}</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "tags", |
|||
props: { |
|||
size:{type: String,default:'mini'}, // 图片路径 |
|||
type:{type: String,default:'primary'}, |
|||
text:{type: String, default:()=>{return '暂无'}} |
|||
}, |
|||
data() { |
|||
return {} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.im-tags{ |
|||
color: #175CFF; |
|||
background-color: #D4E1FF; |
|||
padding:10rpx 20rpx; |
|||
font-size: 24rpx; |
|||
border: 1px solid #b5cbff; |
|||
} |
|||
|
|||
.im-tags-success{ |
|||
background-color: #f0f9eb; |
|||
border-color: #e1f3d8; |
|||
color: #67c23a; |
|||
} |
|||
|
|||
.im-tags-info { |
|||
background-color: #f4f4f5; |
|||
border-color: #e9e9eb; |
|||
color: #909399; |
|||
} |
|||
|
|||
.im-tags-warning { |
|||
background-color: #fdf6ec; |
|||
border-color: #faecd8; |
|||
color: #e6a23c; |
|||
} |
|||
|
|||
.im-tags-danger { |
|||
background-color: #fef0f0; |
|||
border-color: #fde2e2; |
|||
color: #f56c6c; |
|||
} |
|||
|
|||
|
|||
.im-tags-mini{ |
|||
padding:0 8rpx; |
|||
font-size: 22rpx; |
|||
} |
|||
|
|||
.im-tags-small{ |
|||
padding:6rpx 12rpx; |
|||
font-size: 22rpx; |
|||
} |
|||
.im-tags-medium{ |
|||
padding:8rpx 16rpx; |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,63 @@ |
|||
<template> |
|||
<view> |
|||
<scroll-view class="folder-wap" :scroll-x="true" :scroll-left="99999999"> |
|||
<view class="im-flex im-justify-content-start im-align-items-center"> |
|||
<view class="tab-item im-flex im-justify-content-start im-align-items-center" v-for="(item, index) in tree" @tap="open(item)" :key='index'> |
|||
<view class="mar10 lz-tree-name font-color-999" :class="{ 'font-color-333': index == tree.length - 1 }">{{ item.name }}</view> |
|||
<slot name="icon"> |
|||
<text class="font-color-999" :class="[icon ? icon :'cuIcon-right']" v-if="index < tree.length - 1"></text> |
|||
</slot> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "breadcrum", |
|||
props: { |
|||
tree:{type: Array,default:()=>{return {}}}, |
|||
icon:{type: String,default:()=>{return 'cuIcon-right'}} |
|||
}, |
|||
data() { |
|||
return {} |
|||
}, |
|||
methods:{ |
|||
open(item){ |
|||
this.$emit('openBread',item); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.folder-wap { |
|||
box-sizing: border-box; |
|||
width: 100%; |
|||
white-space: nowrap; |
|||
padding:20rpx; |
|||
.tab-item { |
|||
padding-bottom: 10rpx; |
|||
box-sizing: border-box; |
|||
.lz-tree-name{ |
|||
margin:0 20rpx; |
|||
} |
|||
.iconfont{ |
|||
font-size:24rpx !important; |
|||
} |
|||
&:last-child { |
|||
margin-right: 32rpx; |
|||
.lz-tree-name{ |
|||
margin-right:80rpx; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.font-color-333{ |
|||
color:#303133 !important; |
|||
} |
|||
.font-color-999{ |
|||
color:#909399 |
|||
} |
|||
</style> |
|||
@ -0,0 +1,79 @@ |
|||
<template> |
|||
<view> |
|||
<view class="cu-custom" :style="[{height:CustomBar + 'px'}]"> |
|||
<view class="cu-bar fixed" :style="style" :class="[bgImage!=''?'none-bg text-white bg-img':'',bgColor]"> |
|||
<view class="action" @tap="BackPage" v-if="isBack"> |
|||
<text class="cuIcon-back"></text> |
|||
<slot name="backText"></slot> |
|||
</view> |
|||
<view class="action" v-else> |
|||
<slot name="backText"></slot> |
|||
</view> |
|||
<view class="content" :style="[{top:StatusBar + 'px'}]"> |
|||
<slot name="content"></slot> |
|||
</view> |
|||
<view class="right"> |
|||
<slot name="right"></slot> |
|||
</view> |
|||
|
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
StatusBar: this.StatusBar, |
|||
CustomBar: this.CustomBar |
|||
}; |
|||
}, |
|||
name: 'custom', |
|||
computed: { |
|||
style() { |
|||
var StatusBar= this.StatusBar; |
|||
var CustomBar= this.CustomBar; |
|||
var bgImage = this.bgImage; |
|||
var style = `height:${CustomBar}px;padding-top:${StatusBar}px;`; |
|||
if (this.bgImage) { |
|||
style = `${style}background-image:url(${bgImage});`; |
|||
} |
|||
return style |
|||
} |
|||
}, |
|||
props: { |
|||
bgColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
isBack: { |
|||
type: [Boolean, String], |
|||
default: false |
|||
}, |
|||
bgImage: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
}, |
|||
methods: { |
|||
BackPage() { |
|||
const allroutes = getCurrentPages(); |
|||
const cureentRoute = allroutes[allroutes.length - 1].route; |
|||
// 如果当前在聊天页面,自动返回到首页,避免层级过深 |
|||
if (cureentRoute == 'pages/message/chat') { |
|||
uni.switchTab({ |
|||
url: '/pages/index/index' |
|||
}) |
|||
return; |
|||
}else if (allroutes.length < 2 && 'undefined' !== typeof __wxConfig) { |
|||
let url = '/' + __wxConfig.pages[0] |
|||
return uni.redirectTo({url}) |
|||
} |
|||
uni.navigateBack({ |
|||
delta: 1 |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
@ -0,0 +1,433 @@ |
|||
<template> |
|||
<view class="canvasBox"> |
|||
<template v-if="isUse"> |
|||
<view class="box"> |
|||
<view class="line"></view> |
|||
<view class="angle"></view> |
|||
</view> |
|||
<view class="box2" v-if="isUseTorch"> |
|||
<view class="track" @click="openTrack"> |
|||
<svg |
|||
t="1653920715959" |
|||
class="icon" |
|||
viewBox="0 0 1024 1024" |
|||
version="1.1" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
p-id="1351" |
|||
width="32" |
|||
height="32" |
|||
> |
|||
<path |
|||
d="M651.353043 550.479503H378.752795L240.862609 364.315031c-3.688944-4.897391-5.660621-10.876025-5.660621-17.045466v-60.040745c0-15.773416 12.847702-28.621118 28.621118-28.621118h502.459627c15.773416 0 28.621118 12.847702 28.621118 28.621118v59.977143c0 6.105839-1.971677 12.084472-5.660621 17.045466l-137.890187 186.228074zM378.752795 598.308571v398.024348c0 15.328199 12.402484 27.667081 27.667081 27.667081h217.266087c15.328199 0 27.667081-12.402484 27.66708-27.667081V598.308571H378.752795z m136.300124 176.942112c-14.564969 0-26.331429-11.76646-26.331428-26.331428v-81.283975c0-14.564969 11.76646-26.331429 26.331428-26.331429 14.564969 0 26.331429 11.76646 26.331429 26.331429v81.283975c0 14.564969-11.76646 26.331429-26.331429 26.331428zM512 222.608696c-17.554286 0-31.801242-14.246957-31.801242-31.801243V31.801242c0-17.554286 14.246957-31.801242 31.801242-31.801242s31.801242 14.246957 31.801242 31.801242v159.006211c0 17.554286-14.246957 31.801242-31.801242 31.801243zM280.932174 205.881242c-9.47677 0-18.889938-4.197764-25.122981-12.275279L158.242981 67.991056a31.864845 31.864845 0 0 1 5.597019-44.648944 31.864845 31.864845 0 0 1 44.648944 5.597018l97.502609 125.551305a31.864845 31.864845 0 0 1-5.597019 44.648944c-5.787826 4.579379-12.656894 6.741863-19.46236 6.741863zM723.987081 205.881242c-6.805466 0-13.674534-2.162484-19.462361-6.678261a31.794882 31.794882 0 0 1-5.597018-44.648944l97.566211-125.551304a31.794882 31.794882 0 0 1 44.648944-5.597019 31.794882 31.794882 0 0 1 5.597019 44.648944l-97.566211 125.551305c-6.360248 8.077516-15.709814 12.27528-25.186584 12.275279z" |
|||
fill="#ffffff" |
|||
p-id="1352" |
|||
></path> |
|||
</svg> |
|||
{{ trackStatus ? '关闭闪光灯' : '打开闪光灯' }} |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="mask1 mask" :style="'height:' + maskHeight + 'px;'"></view> |
|||
<view |
|||
class="mask2 mask" |
|||
:style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'" |
|||
></view> |
|||
<view class="mask3 mask" :style="'height:' + maskHeight + 'px;'"></view> |
|||
<view |
|||
class="mask4 mask" |
|||
:style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'" |
|||
></view> |
|||
</template> |
|||
<template v-else> |
|||
<slot name="error"> |
|||
<view class="error"> |
|||
<view class="on1">相机权限被拒绝,请尝试如下操作:</view> |
|||
<view>· 刷新页面后重试;</view> |
|||
<view>· 在系统中检测当前App或浏览器的相机权限是否被禁用;</view> |
|||
<view>· 如果依然不能体验,建议在微信中打开链接;</view> |
|||
</view> |
|||
</slot> |
|||
</template> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import jsQR from "jsqr" |
|||
export default { |
|||
props: { |
|||
continue: { |
|||
type: Boolean, |
|||
default: false // false 监听一次 true 持续监听 |
|||
}, |
|||
exact: { |
|||
type: String, |
|||
default: 'environment' // environment 后摄像头 user 前摄像头 |
|||
}, |
|||
size: { |
|||
type: String, |
|||
default: 'whole' // whole 全屏 balf 半屏 |
|||
}, |
|||
definition: { |
|||
type: Boolean, |
|||
default: false // fasle 正常 true 高清 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
windowWidth: 0, |
|||
windowHeight: 0, |
|||
video: null, |
|||
canvas2d: null, |
|||
canvas2d2: null, |
|||
canvasWidth: 200, |
|||
canvasHeight: 200, |
|||
maskWidth: 0, |
|||
maskHeight: 0, |
|||
inter: 0, |
|||
track: null, |
|||
isUseTorch: false, |
|||
trackStatus: false, |
|||
isParse: false, |
|||
isUse: true |
|||
} |
|||
}, |
|||
mounted() { |
|||
if (origin.indexOf('https') === -1) throw '请在 https 环境中使用摄像头组件。' |
|||
|
|||
this.windowWidth = document.documentElement.clientWidth || document.body.clientWidth |
|||
this.windowHeight = document.documentElement.clientHeight || document.body.clientHeight |
|||
this.windowHeight = this.size === 'whole' ? this.windowHeight : this.windowHeight / 2 |
|||
this.isParse = true |
|||
|
|||
this.$nextTick(() => { |
|||
this.createMsk() |
|||
this.openScan() |
|||
}) |
|||
}, |
|||
destroyed() { |
|||
this.closeCamera() |
|||
}, |
|||
methods: { |
|||
openScan() { |
|||
const width = this.transtion(this.windowHeight) |
|||
const height = this.transtion(this.windowWidth) |
|||
const videoParam = { |
|||
audio: false, |
|||
video: { |
|||
facingMode: { exact: this.exact }, |
|||
width, |
|||
height |
|||
} |
|||
} |
|||
navigator.mediaDevices |
|||
.getUserMedia(videoParam) |
|||
.then(stream => { |
|||
this.video = document.createElement('video') |
|||
this.video.width = this.windowWidth |
|||
this.video.height = this.windowHeight |
|||
|
|||
const canvas = document.createElement('canvas') |
|||
canvas.id = 'canvas' |
|||
canvas.width = this.transtion(this.canvasWidth) |
|||
canvas.height = this.transtion(this.canvasHeight) |
|||
canvas.style = 'display:none;' |
|||
//canvas.style = 'position: fixed;top: 0;z-index: 999;left:0' |
|||
this.canvas2d = canvas.getContext('2d') |
|||
|
|||
// 设置当前宽高 满屏 |
|||
const canvasBox = document.querySelector('.canvasBox') |
|||
canvasBox.append(this.video) |
|||
canvasBox.append(canvas) |
|||
canvasBox.style = `width:${this.windowWidth}px;height:${this.windowHeight}px;` |
|||
|
|||
// 创建第二个canvas |
|||
const canvas2 = document.createElement('canvas') |
|||
canvas2.id = 'canvas2' |
|||
canvas2.width = this.canvasWidth |
|||
canvas2.height = this.canvasHeight |
|||
canvas2.style = 'position: absolute;top: 50%;left: 50%;z-index: 20;transform: translate(-50%, -50%);' |
|||
this.canvas2d2 = canvas2.getContext('2d') |
|||
canvasBox.append(canvas2) |
|||
|
|||
this.video.srcObject = stream |
|||
this.video.setAttribute('playsinline', true) |
|||
this.video.play() |
|||
this.tick() |
|||
|
|||
this.track = stream.getVideoTracks()[0] |
|||
setTimeout(() => { |
|||
this.isUseTorch = this.track.getCapabilities().torch || null |
|||
}, 500) |
|||
}) |
|||
.catch(err => { |
|||
this.isUse = false |
|||
this.$emit('error', err) |
|||
}) |
|||
}, |
|||
|
|||
closeCamera() { |
|||
this.isParse = false |
|||
if (this.video && this.video.srcObject) { |
|||
this.video.srcObject.getTracks().forEach(track => { |
|||
track.stop() |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
tick() { |
|||
if (!this.isParse) return |
|||
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) { |
|||
this.canvas2d.drawImage( |
|||
this.video, |
|||
this.transtion(this.maskWidth), |
|||
this.transtion(this.maskHeight), |
|||
this.transtion(200), |
|||
this.transtion(200), |
|||
0, |
|||
0, |
|||
this.transtion(this.canvasWidth), |
|||
this.transtion(this.canvasHeight) |
|||
) |
|||
|
|||
const imageData = this.canvas2d.getImageData( |
|||
0, |
|||
0, |
|||
this.transtion(this.canvasWidth), |
|||
this.transtion(this.canvasHeight) |
|||
) |
|||
|
|||
const code = jsQR(imageData.data, imageData.width, imageData.height, { |
|||
inversionAttempts: 'dontInvert' |
|||
}) |
|||
|
|||
this.canvas2d2.clearRect(0, 0, this.canvasWidth, this.canvasHeight) |
|||
if (code) { |
|||
this.drawLine(code.location.topLeftCorner, code.location.topRightCorner) |
|||
this.drawLine(code.location.topRightCorner, code.location.bottomRightCorner) |
|||
this.drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner) |
|||
this.drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner) |
|||
if (code.data) { |
|||
this.getData(code.data) |
|||
} |
|||
} |
|||
} |
|||
requestAnimationFrame(this.tick) |
|||
}, |
|||
drawLine(begin, end, color = '#FF3B58') { |
|||
this.canvas2d2.beginPath() |
|||
this.canvas2d2.moveTo(this.nutranstion(begin.x), this.nutranstion(begin.y)) |
|||
this.canvas2d2.lineTo(this.nutranstion(end.x), this.nutranstion(end.y)) |
|||
this.canvas2d2.lineWidth = 4 |
|||
this.canvas2d2.strokeStyle = color |
|||
this.canvas2d2.stroke() |
|||
}, |
|||
|
|||
getData(data) { |
|||
this.$emit('success', data) |
|||
if (!this.continue) { |
|||
this.closeCamera() |
|||
} |
|||
}, |
|||
|
|||
openTrack() { |
|||
this.trackStatus = !this.trackStatus |
|||
this.track.applyConstraints({ |
|||
advanced: [{ torch: this.trackStatus }] |
|||
}) |
|||
}, |
|||
|
|||
createMsk() { |
|||
this.maskWidth = this.windowWidth / 2 - this.canvasWidth / 2 |
|||
this.maskHeight = this.windowHeight / 2 - this.canvasHeight / 2 |
|||
}, |
|||
|
|||
transtion(number) { |
|||
return this.definition ? number * 2.8 : number * 1.8 |
|||
}, |
|||
nutranstion(number) { |
|||
return this.definition ? number / 2.8 : number / 1.8 |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
page { |
|||
background-color: #333333; |
|||
} |
|||
|
|||
.canvasBox { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
position: relative; |
|||
|
|||
background-image: linear-gradient( |
|||
0deg, |
|||
transparent 24%, |
|||
rgba(32, 255, 77, 0.1) 25%, |
|||
rgba(32, 255, 77, 0.1) 26%, |
|||
transparent 27%, |
|||
transparent 74%, |
|||
rgba(32, 255, 77, 0.1) 75%, |
|||
rgba(32, 255, 77, 0.1) 76%, |
|||
transparent 77%, |
|||
transparent |
|||
), |
|||
linear-gradient( |
|||
90deg, |
|||
transparent 24%, |
|||
rgba(32, 255, 77, 0.1) 25%, |
|||
rgba(32, 255, 77, 0.1) 26%, |
|||
transparent 27%, |
|||
transparent 74%, |
|||
rgba(32, 255, 77, 0.1) 75%, |
|||
rgba(32, 255, 77, 0.1) 76%, |
|||
transparent 77%, |
|||
transparent |
|||
); |
|||
background-size: 3rem 3rem; |
|||
background-position: -1rem -1rem; |
|||
z-index: 10; |
|||
background-color: #1110; |
|||
} |
|||
|
|||
.box { |
|||
width: 200px; |
|||
height: 200px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
overflow: hidden; |
|||
border: 0.1rem solid rgba(0, 255, 51, 0.2); |
|||
z-index: 11; |
|||
} |
|||
|
|||
.line { |
|||
height: calc(100% - 2px); |
|||
width: 100%; |
|||
background: linear-gradient(180deg, rgba(0, 255, 51, 0) 43%, #00ff33 211%); |
|||
border-bottom: 3px solid #00ff33; |
|||
transform: translateY(-100%); |
|||
animation: radar-beam 2s infinite alternate; |
|||
animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99); |
|||
animation-delay: 1.4s; |
|||
} |
|||
|
|||
.box:after, |
|||
.box:before, |
|||
.angle:after, |
|||
.angle:before { |
|||
content: ''; |
|||
display: block; |
|||
position: absolute; |
|||
width: 3vw; |
|||
height: 3vw; |
|||
z-index: 12; |
|||
border: 0.2rem solid transparent; |
|||
} |
|||
|
|||
.box:after, |
|||
.box:before { |
|||
top: 0; |
|||
border-top-color: #00ff33; |
|||
} |
|||
|
|||
.angle:after, |
|||
.angle:before { |
|||
bottom: 0; |
|||
border-bottom-color: #00ff33; |
|||
} |
|||
|
|||
.box:before, |
|||
.angle:before { |
|||
left: 0; |
|||
border-left-color: #00ff33; |
|||
} |
|||
|
|||
.box:after, |
|||
.angle:after { |
|||
right: 0; |
|||
border-right-color: #00ff33; |
|||
} |
|||
|
|||
@keyframes radar-beam { |
|||
0% { |
|||
transform: translateY(-100%); |
|||
} |
|||
|
|||
100% { |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.msg { |
|||
text-align: center; |
|||
padding: 20rpx 0; |
|||
} |
|||
|
|||
.box2 { |
|||
width: 300px; |
|||
height: 200px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
z-index: 20; |
|||
} |
|||
|
|||
.track { |
|||
position: absolute; |
|||
bottom: -100px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
z-index: 20; |
|||
color: #fff; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
|
|||
.mask { |
|||
position: absolute; |
|||
z-index: 10; |
|||
background-color: rgba(0, 0, 0, 0.55); |
|||
} |
|||
|
|||
.mask1 { |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
} |
|||
|
|||
.mask2 { |
|||
right: 0; |
|||
} |
|||
|
|||
.mask3 { |
|||
right: 0; |
|||
left: 0; |
|||
bottom: 0; |
|||
} |
|||
|
|||
.mask4 { |
|||
left: 0; |
|||
} |
|||
|
|||
.error { |
|||
color: #fff; |
|||
padding: 40rpx; |
|||
font-size: 24rpx; |
|||
background-color: #333333; |
|||
position: fixed; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 550rpx; |
|||
border-radius: 20rpx; |
|||
} |
|||
|
|||
.error .on1 { |
|||
font-size: 30rpx; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,130 @@ |
|||
<template> |
|||
<view |
|||
@touchstart="touchstart" |
|||
@touchmove="touchmove" |
|||
@touchend="touchend"> |
|||
<slot></slot> |
|||
</view> |
|||
</template> |
|||
<script> |
|||
export default{ |
|||
name : "im-touch", |
|||
props : { |
|||
datas:{type:Array, default:function(){return [];}} |
|||
}, |
|||
data() { |
|||
return { |
|||
toucheTimer : 0, |
|||
fingerRes : [], |
|||
distance : 0, |
|||
taptimer : 100 |
|||
} |
|||
}, |
|||
methods:{ |
|||
toInt : function(arr){ |
|||
var res = []; |
|||
arr.forEach((item)=>{ |
|||
item.pageX = parseInt(item.pageX); |
|||
item.pageY = parseInt(item.pageY); |
|||
res.push(item); |
|||
}); |
|||
return res; |
|||
}, |
|||
touchstart : function(e){ |
|||
this.toucheTimer = new Date().getTime(); |
|||
this.fingerRes = this.toInt(e.changedTouches); |
|||
if(this.fingerRes.length > 2){return ;} |
|||
var moves = [], i = 0; |
|||
this.fingerRes.forEach((finger)=>{ |
|||
var xTouch = finger.pageX; |
|||
var yTouch = finger.pageY; |
|||
moves.push([xTouch, yTouch]); |
|||
i++; |
|||
}); |
|||
this.$emit('thStart', moves, this.datas); |
|||
}, |
|||
touchmove : function(e){ |
|||
if(this.toucheTimer < 50){return ;} |
|||
var timer = new Date().getTime() - this.toucheTimer; |
|||
if(timer < this.taptimer){return ;} |
|||
var touches = this.toInt(e.changedTouches); |
|||
if(touches.length > 2){return ;} |
|||
if(touches.length == 1){ |
|||
var i = 0, moves = []; |
|||
touches.forEach((finger)=>{ |
|||
var xTouch = finger.pageX - this.fingerRes[i].pageX; |
|||
var yTouch = finger.pageY - this.fingerRes[i].pageY; |
|||
moves.push([xTouch, yTouch]); |
|||
i++; |
|||
}); |
|||
this.$emit('thMove', moves, this.datas); |
|||
} |
|||
else if(touches.length == 2){ |
|||
if(this.distance == 0){ |
|||
this.distance = parseInt(this.getDistance(touches[0].pageX,touches[0].pageY, touches[1].pageX, touches[1].pageY)); |
|||
}else{ |
|||
var distance1 = parseInt(this.getDistance(touches[0].pageX,touches[0].pageY, touches[1].pageX, touches[1].pageY)); |
|||
var scale = distance1 / this.distance; |
|||
scale = Math.floor(scale * 100) / 100; |
|||
this.$emit('scale', scale, this.datas); |
|||
} |
|||
} |
|||
}, |
|||
touchend : function (e){ |
|||
var timer = new Date().getTime() - this.toucheTimer; |
|||
if(timer < this.taptimer){ |
|||
this.$emit('tapme'); |
|||
return ; |
|||
} |
|||
var touches = this.toInt(e.changedTouches); |
|||
this.distance = 0; |
|||
if(touches.length == 1){ |
|||
var i = 0, moves = []; |
|||
touches.forEach((finger)=>{ |
|||
var xTouch = finger.pageX - this.fingerRes[i].pageX; |
|||
var yTouch = finger.pageY - this.fingerRes[i].pageY; |
|||
moves.push([xTouch, yTouch]); |
|||
i++; |
|||
}); |
|||
moves.push(timer); |
|||
this.$emit('thEnd', moves, this.datas); |
|||
// 根据时间及距离决定滑动时间 |
|||
if(timer < 300){ |
|||
var mx = Math.abs(moves[0][0]); |
|||
var my = Math.abs(moves[0][1]); |
|||
if(mx > my){ |
|||
if(mx >= 50){ |
|||
if(moves[0][0] > 0){ |
|||
this.$emit('swipe', 'right', this.datas); |
|||
}else{ |
|||
this.$emit('swipe', 'left', this.datas); |
|||
} |
|||
} |
|||
}else{ |
|||
if(my >= 50){ |
|||
if(moves[0][1] > 0){ |
|||
this.$emit('swipe', 'down', this.datas); |
|||
}else{ |
|||
this.$emit('swipe', 'up', this.datas); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
getDistance : function (lat1, lng1, lat2, lng2){ |
|||
var radLat1 = lat1*Math.PI / 180.0; |
|||
var radLat2 = lat2*Math.PI / 180.0; |
|||
var a = radLat1 - radLat2; |
|||
var b = lng1*Math.PI / 180.0 - lng2*Math.PI / 180.0; |
|||
var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2) + Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2))); |
|||
s = s * 6378.137; |
|||
return Math.round(s * 10000) / 10000; |
|||
}, |
|||
tapme : function(){ |
|||
this.isTap = true; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
<style scoped></style> |
|||
@ -0,0 +1,81 @@ |
|||
<template> |
|||
<view> |
|||
<!-- #ifdef APP-PLUS --> |
|||
<view v-for="(items,indexs) in imglist" :key="indexs" v-if="network_log=='none'"> |
|||
<view class="cu-avatar lg" v-if="info.imgname === items.name" :class="appSetting.circleAvatar?'round':'radius'" @tap="openUserInfo(info)" :style="[{backgroundImage:'url('+ items.path +')'}]"></view> |
|||
</view> |
|||
<view v-else class="cu-avatar lg" :class="appSetting.circleAvatar?'round':'radius'" @tap="openUserInfo(info)" :style="[{backgroundImage:'url('+ info.avatar +')'}]"></view> |
|||
<!-- #endif --> |
|||
|
|||
<!-- #ifdef H5 --> |
|||
<view class="cu-avatar lg" :class="appSetting.circleAvatar?'round':'radius'" @tap="openUserInfo(info)" :style="[{backgroundImage:'url('+ info.avatar +')'}]"></view> |
|||
<!-- #endif --> |
|||
</view> |
|||
</template> |
|||
<script> |
|||
const userInfo=uni.getStorageSync('userInfo'); |
|||
const appSetting=uni.getStorageSync('appSetting'); |
|||
import { useMsgStore } from '@/store/message'; |
|||
// #ifdef APP-PLUS |
|||
import {getSavedImages} from '@/utils/LocalFileSystemURL.js' |
|||
// #endif |
|||
import { storeToRefs } from 'pinia'; |
|||
import pinia from '@/store/index' |
|||
const msgStore = useMsgStore(pinia) |
|||
const {network_log} = storeToRefs(msgStore); |
|||
export default{ |
|||
name : "im-touch", |
|||
props : { |
|||
info:{type:Object, default:function(){return {};}}, |
|||
circleAvatar:{type:Boolean, default:false}, |
|||
profile:{type:Boolean, default:false}, |
|||
}, |
|||
data() { |
|||
return { |
|||
toucheTimer : 0, |
|||
fingerRes : [], |
|||
distance : 0, |
|||
taptimer : 100, |
|||
appSetting:appSetting, |
|||
imglist:[], |
|||
network_log:network_log |
|||
} |
|||
}, |
|||
created() { |
|||
// #ifdef APP-PLUS |
|||
this.getImagePath(); |
|||
// #endif |
|||
// console.info(this.info,'读取地址'); |
|||
}, |
|||
methods:{ |
|||
async getImagePath(){ |
|||
this.imglist = await getSavedImages() |
|||
this.imglist.map(item => { |
|||
item.path = plus.io.convertLocalFileSystemURL(item.path) |
|||
}); |
|||
// console.info(this.imglist,'读取地址'); |
|||
}, |
|||
// 打开用户详情 |
|||
openUserInfo(item){ |
|||
let friend=msgStore.getContact(item.user_id); |
|||
// if(!this.profile && !friend){ |
|||
// uni.showToast({ |
|||
// title:'已开启用户隐私!', |
|||
// icon:'none' |
|||
// }) |
|||
// return false; |
|||
// } |
|||
// if(item.id==userInfo.user_id) return; |
|||
|
|||
// uni.redirectTo({ |
|||
// url:"/pages/contacts/detail?id="+this.info.id |
|||
// }) |
|||
|
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+this.info.id |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
<style scoped></style> |
|||
@ -0,0 +1,43 @@ |
|||
<template name="im-image"> |
|||
<template v-if="src && info && info.fixMode"> |
|||
<image v-if="info.fixMode<=2" :src="src" class="radius" :mode="info.fixMode==1 ? 'widthFix' : 'heightFix' " :style="info.fixMode==1 ? 'width:200px' : 'height:240px'" @tap="showImgs" :data-img="src" ></image> |
|||
<image v-if="info.fixMode==3" :src="src" class="radius" mode="scaleToFill" :style="[{width:info.width+'px',height:info.height+'px'}]" @tap="showImgs" :data-img="src" ></image> |
|||
</template> |
|||
<template v-else-if="!src && info && info.width"> |
|||
<image :src="src" class="radius" mode="scaleToFill" :style="$util.imageCoverStyle(info.width,info.height)" @tap="showImgs" :data-img="src" ></image> |
|||
</template> |
|||
<template v-else> |
|||
<image :src="src" style="width:200px" class="radius" mode="widthFix" @tap="showImgs" :data-img="src" ></image> |
|||
</template> |
|||
</template> |
|||
<script> |
|||
export default { |
|||
name : "im-image", |
|||
props : { |
|||
info:{type:Object, default:function(){return {};}}, |
|||
src:{type:String, default:''}, |
|||
isview:{type:Object, default:function(){return {};}}, |
|||
}, |
|||
emits:['showImgs','viewImgs'], |
|||
data() { |
|||
return { |
|||
} |
|||
}, |
|||
created : function(){ |
|||
}, |
|||
methods:{ |
|||
showImgs(e){ |
|||
if(this.isview.is_view==0){ |
|||
this.$api.msgApi.viewOriginalImage({file_id:this.isview.file_id,msg_id:this.isview.msg_id}).then(res => { |
|||
// console.log(res); |
|||
const valobj = {img:res.data.src,is_view:this.isview.is_view} |
|||
this.$emit('showImgs',valobj); |
|||
this.$emit('viewImgs',{src:res.data.src,msg_id:this.isview.msg_id}); |
|||
}) |
|||
}else{ |
|||
this.$emit('showImgs',e); |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
@ -0,0 +1,35 @@ |
|||
<template name="im-item"> |
|||
<view v-if="item.type=='diy'" :item="item" :index="index" :isSelf="isSelf"> |
|||
自定义 |
|||
</view> |
|||
<view v-else> |
|||
暂不支持该消息类型 |
|||
</view> |
|||
</template> |
|||
<script> |
|||
// import imVoice from '@/components/message/im-card-voice.vue'; |
|||
export default { |
|||
|
|||
name : "im-item", |
|||
components: { |
|||
// imVoice |
|||
}, |
|||
props : { |
|||
item:{type:Object, default:function(){return {};}}, |
|||
index:{type:Number, default:0}, |
|||
isSelf:{type:Boolean, default:false}, |
|||
}, |
|||
data() { |
|||
return { |
|||
} |
|||
}, |
|||
created : function(){ |
|||
}, |
|||
methods:{ |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
<style scoped> |
|||
|
|||
</style> |
|||
@ -0,0 +1,50 @@ |
|||
<template name="im-tab"> |
|||
<view class="tab-main im-flex im-justify-content-start im-align-items-center bg-gray" :style="{height:height+'rpx',borderRadius:height/2+'rpx'}"> |
|||
<view class="tab-item" :class="active==index ? 'active' : ''" v-for="(item,index) in values" @click="changeItem(item,index)" :style="{height:itemHeight+'rpx',borderRadius:itemHeight/2+'rpx',lineHeight:itemHeight-8+'rpx'}" :key="index"> |
|||
{{item.name}} <text v-if="item.count>0">{{item.count>99 ? '99+' : item.count}}</text> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
<script> |
|||
export default { |
|||
|
|||
name : "im-tab", |
|||
components: { |
|||
}, |
|||
props : { |
|||
values:{type:Array, default:function(){return [];}}, |
|||
height:{type:Number,default:72} |
|||
}, |
|||
data() { |
|||
return { |
|||
active:0, |
|||
itemHeight:48 |
|||
} |
|||
}, |
|||
created : function(){ |
|||
this.itemHeight=this.height-16; |
|||
}, |
|||
methods:{ |
|||
changeItem(item,index){ |
|||
this.active=index; |
|||
this.$emit('change',item,index) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
<style lang="scss" scoped> |
|||
.tab-main{ |
|||
padding:10rpx; |
|||
.tab-item{ |
|||
padding: 6rpx 12rpx; |
|||
min-width:150rpx; |
|||
text-align: center; |
|||
vertical-align: middle; |
|||
&.active{ |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04); |
|||
background-color: #fff; |
|||
color:#18bc37; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,301 @@ |
|||
<template> |
|||
<view> |
|||
<view class="cu-bar bg-white search"> |
|||
<view class="search-form round"> |
|||
<text class="cuIcon-search"></text> |
|||
<input type="text" v-model="keywords" placeholder="搜索联系人" confirm-type="search"/> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="gui-padding" style="padding-bottom: 120rpx;" v-if="!keywords"> |
|||
<view class="cu-list menu"> |
|||
<view class="cu-item arrow" v-if="isAuth" @tap="atAll"> |
|||
<view class='cu-avatar mr-15 group-bg sm'> |
|||
</view> |
|||
<view class="content"> |
|||
<text class="c-333">所有人</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<uni-data-checkbox :multiple="multiple" class="user-list-select" @change="chooseUser" v-model="changeUser" :localdata="lists"></uni-data-checkbox> |
|||
<Empty v-if="!lists.length" noDatatext="无联系人" textcolor="#999" ></Empty> |
|||
</view> |
|||
<view class="gui-padding" style="padding-bottom: 120rpx;" v-if="keywords"> |
|||
<uni-data-checkbox :multiple="multiple" class="user-list-select" @change="chooseSearchUser" v-model="searcheUser" :localdata="searchList"></uni-data-checkbox> |
|||
<Empty v-if="!searchList.length" noDatatext="未搜索到数据" textcolor="#999" ></Empty> |
|||
</view> |
|||
<view class="cu-bar bg-white tabbar border shop footer-opt"> |
|||
<scroll-view class="scroll-view_H" scroll-x="true" :scroll-anchoring="true" :scroll-left="scrollLeft"> |
|||
<view class="user-list-avatar"> |
|||
<template v-for="(item,index) in selectUser" :key="index"> |
|||
<image class="user-avatar" :src="item.avatar" @tap="removeUser(item.id,item.user_id)"></image> |
|||
</template> |
|||
</view> |
|||
<view class="select-num pd-10">已选{{selectUser.length}}人</view> |
|||
</scroll-view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { storeToRefs } from 'pinia'; |
|||
import pinia from '@/store/index' |
|||
import { useloginStore } from '@/store/login'; |
|||
const userStore = useloginStore(pinia) |
|||
export default { |
|||
name : "user-select", |
|||
props : { |
|||
type:{type:Number, default:0},//1创建群聊,2邀请群成员,3:转发,4:提及某人,5:选择联系人 |
|||
contact_id:{type:String, default:''}, |
|||
multiple:{type:Boolean, default:true}, |
|||
user_ids:{type:Object, default:{}} |
|||
}, |
|||
data() { |
|||
return { |
|||
lists: [], |
|||
keywords:'', |
|||
searchList:[], |
|||
selectUser:[], |
|||
userList: [], |
|||
changeUser: [], //选中的数据 |
|||
scrollLeft:300, |
|||
contacList:[], |
|||
searcheUser:[], //搜索选中人员 |
|||
isAuth:false, |
|||
userInfo:userStore.userInfo, |
|||
} |
|||
}, |
|||
watch: { |
|||
keywords(val){ |
|||
this.search(); |
|||
// 如果关键词为空,搜索选中的人员为已经选中的人员 |
|||
if(val!=''){ |
|||
this.searcheUser=this.changeUser; |
|||
} |
|||
}, |
|||
searcheUser(val){ |
|||
const arr=this.changeUser.concat(val); |
|||
this.changeUser=[...new Set(arr)]; |
|||
} |
|||
}, |
|||
mounted() { |
|||
if (this.type == 4) { |
|||
this.getGroupUser() |
|||
} else if(this.type == 5){ |
|||
this.getAllUser() |
|||
this.changeUser = this.user_ids |
|||
this.selectUser = uni.getStorageSync('selectUser') |
|||
} else if(this.type == 6){ |
|||
this.getAllUser() |
|||
this.changeUser = this.user_ids |
|||
this.selectUser = uni.getStorageSync('selectUser1') |
|||
}else { |
|||
this.getAllUser() |
|||
} |
|||
let lists=JSON.parse(JSON.stringify(this.userList)); |
|||
this.lists=lists; |
|||
|
|||
}, |
|||
methods: { |
|||
getAllUser() { |
|||
const allContact=uni.getStorageSync('allContacts'); |
|||
let contact=[]; |
|||
if(this.type==3){ |
|||
contact = allContact.filter((item)=>{ |
|||
return item.id!=this.contact_id; |
|||
}) |
|||
contact.forEach(item => { |
|||
item.disable = false; |
|||
let name=item.displayName; |
|||
if(item.is_group==1){ |
|||
name+="(群聊)"; |
|||
} |
|||
item.text=name; |
|||
item.value=item.id; |
|||
}) |
|||
}else{ |
|||
contact = allContact.filter((item)=>{ |
|||
return item.is_group==0; |
|||
}) |
|||
this.contacList=JSON.parse(JSON.stringify(contact)); |
|||
contact.forEach(item => { |
|||
item.disable = false; |
|||
item.text=item.displayName; |
|||
item.value=item.id; |
|||
if (this.user_ids.length && this.type==2) { |
|||
if (this.user_ids.includes(item.id)) { |
|||
item.disable = true |
|||
} |
|||
} |
|||
}) |
|||
if(this.type==1 && this.contact_id){ |
|||
this.changeUser.push(parseInt(this.contact_id)); |
|||
this.selectUser=contact.filter((item)=>{ |
|||
return item.id==this.contact_id; |
|||
}) |
|||
} |
|||
} |
|||
this.userList = contact |
|||
this.lists = contact |
|||
}, |
|||
getGroupUser(){ |
|||
this.userList = [] |
|||
this.$api.msgApi.groupUserList({ |
|||
group_id: this.contact_id |
|||
}).then(res => { |
|||
const isAuth=res.data.filter(item => (item.role == 1 || item.role == 2) && item.userInfo.id== this.userInfo.user_id) |
|||
if(isAuth.length) this.isAuth=true; |
|||
const allUser=res.data; |
|||
allUser.forEach((item,index)=>{ |
|||
item.realname=item.userInfo.displayName; |
|||
item.displayName=item.userInfo.displayName; |
|||
item.avatar=item.userInfo.avatar; |
|||
item.name_py=item.userInfo.name_py; |
|||
item.disable = false; |
|||
item.text=item.userInfo.displayName; |
|||
item.value=item.userInfo.id; |
|||
}) |
|||
const userList=res.data.filter(item => item.userInfo.id != this.userInfo.user_id) |
|||
this.lists=userList; |
|||
console.log(this.lists); |
|||
this.userList = userList; |
|||
}) |
|||
}, |
|||
// 选择人员 |
|||
chooseUser(e){ |
|||
if(this.multiple){ |
|||
this.selectUser=e.detail.data; |
|||
}else{ |
|||
this.selectUser=[e.detail.data]; |
|||
} |
|||
setTimeout(()=>{ |
|||
this.scrollLeft+=300; |
|||
},50) |
|||
}, |
|||
// 提醒所有人 |
|||
atAll(){ |
|||
this.$emit('setData',{realname:'所有人',user_id:0}); |
|||
}, |
|||
// 选择搜索列表的成员 |
|||
chooseSearchUser(e){ |
|||
if(this.multiple){ |
|||
this.selectUser=this.$util.mergeArrays(e.detail.data,this.selectUser); |
|||
}else{ |
|||
this.selectUser=[e.detail.data]; |
|||
} |
|||
setTimeout(()=>{ |
|||
this.scrollLeft+=300; |
|||
},50) |
|||
}, |
|||
// 移除成员 |
|||
removeUser(id,user_id){ |
|||
this.selectUser=this.selectUser.filter((e)=>{ |
|||
return e.id!=id; |
|||
}) |
|||
this.changeUser=this.changeUser.filter((e)=>{ |
|||
return e!=user_id; |
|||
}) |
|||
}, |
|||
// 搜索成员 |
|||
search: function(e) { |
|||
let contact=JSON.parse(JSON.stringify(this.lists)); |
|||
this.searchList=this.$util.searchObject(contact,['displayName','name_py'],this.keywords); |
|||
}, |
|||
// 监听提交 |
|||
confirm: function(e) { |
|||
let arr = [] |
|||
if (e) { //这个值为输入框输入的值 |
|||
var brr = this.userList.filter(value => { |
|||
//遍历数组,返回值为true保留并复制到新数组,false则过滤掉 |
|||
let data = value.realname ? value.realname : value.userInfo.displayName |
|||
if (data.includes(e.trim())) { |
|||
arr.push(value) |
|||
} |
|||
return data.includes(e.trim()); |
|||
}); |
|||
this.lists = arr |
|||
} |
|||
|
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.group-bg{ |
|||
background-image: url(@/static/image/group.png); |
|||
} |
|||
.search-warp { |
|||
width: 750rpx; |
|||
padding: 15rpx 50rpx; |
|||
} |
|||
::v-deep .checklist-group{ |
|||
display: grid !important; |
|||
.checklist-box{ |
|||
padding:20rpx; |
|||
.checkbox__inner{ |
|||
width:40rpx !important; |
|||
height:40rpx !important; |
|||
overflow:hidden; |
|||
.checkbox__inner-icon{ |
|||
position: absolute; |
|||
top: -8px !important; |
|||
left: -4px !important; |
|||
height: 20px !important; |
|||
width: 20px !important; |
|||
border-right-width: 2px !important; |
|||
border-bottom-width: 2px !important; |
|||
} |
|||
} |
|||
|
|||
|
|||
.checklist-content{ |
|||
margin-left:20rpx; |
|||
.checklist-text{ |
|||
font-size:36rpx !important; |
|||
} |
|||
|
|||
} |
|||
} |
|||
.is-checked{ |
|||
.radio__inner{ |
|||
border-color:#18bc37 !important; |
|||
.radio__inner-icon{ |
|||
background-color: #18bc37 !important; |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
.footer-opt{ |
|||
position: fixed; |
|||
bottom:0; |
|||
left:0; |
|||
width:100%; |
|||
} |
|||
|
|||
.scroll-view_H { |
|||
white-space: nowrap; |
|||
width: 100%; |
|||
} |
|||
|
|||
.user-list-avatar{ |
|||
float: left; |
|||
margin-top:10rpx; |
|||
.user-avatar{ |
|||
width:70rpx; |
|||
height:70rpx; |
|||
flex: 0 0 auto; |
|||
border-radius: 8rpx; |
|||
margin-left: 15rpx; |
|||
display: inline-block; |
|||
&:last-child{ |
|||
margin-right: 15rpx; |
|||
} |
|||
} |
|||
.select-num{ |
|||
padding:10rpx; |
|||
} |
|||
|
|||
} |
|||
</style> |
|||
@ -0,0 +1,461 @@ |
|||
<!-- mosowe-canvas-image --> |
|||
<template> |
|||
<view class='mosowe-canvas-image'> |
|||
<view class="slot-view" @click="createCanvas"> |
|||
<slot></slot> |
|||
</view> |
|||
<view class="canvas-wrap-box"> |
|||
|
|||
<!-- 主面板绘制 --> |
|||
<canvas class="canvas-wrap" canvas-id="canvas" :style="'width: '+ width +'px; height: '+ height +'px;'"></canvas> |
|||
<!-- 这个是用来绘制圆形图片的 --> |
|||
<canvas class="canvas-wrap" canvas-id="canvas-arc" :style="'width: '+ canvasArcWidth +'px; height: '+ canvasArcHeight +'px;'"></canvas> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import QR from './wxqrcode.js'; |
|||
export default { |
|||
name: 'mosowe-canvas-image', |
|||
components: {}, |
|||
props: { |
|||
imgType: { // 图片类型 |
|||
type: String, |
|||
default: 'jpg', |
|||
validator: () => { |
|||
return ['jpg', 'png']; |
|||
} |
|||
}, |
|||
compress: { // 是否开启压缩 |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
compressSize: { // 压缩界限,超过界限压缩,默认2M |
|||
type: [Number, String], |
|||
default: 1024*1024*2 |
|||
}, |
|||
showPreview: { // 生成图像后是否预览 |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
height: { // canvas高度 |
|||
type: [String, Number], |
|||
default: 200 |
|||
}, |
|||
width: { // canvas宽度 |
|||
type: [String, Number], |
|||
default: 200 |
|||
}, |
|||
lists: { |
|||
type: Array, |
|||
default: () => { |
|||
return []; |
|||
} |
|||
} |
|||
}, |
|||
data () { |
|||
return { |
|||
canvas: null, |
|||
listsIndex: 0, |
|||
listsLength: 0, |
|||
canvasArc: null, |
|||
canvasArcWidth: 100, |
|||
canvasArcHeight: 100, |
|||
compressQuality: 20, |
|||
compressQualityH5: 5, |
|||
}; |
|||
}, |
|||
watch: {}, |
|||
// 组件实例化之前 |
|||
beforeCreate () {}, |
|||
// 组件创建完成 |
|||
created () { |
|||
this.canvas = uni.createCanvasContext('canvas', this); |
|||
this.canvasArc = uni.createCanvasContext('canvas-arc', this); |
|||
}, |
|||
// 组件挂载之前 |
|||
beforeMount () {}, |
|||
// 组件挂载之后 |
|||
mounted () {}, |
|||
// 组件数据更新时 |
|||
beforeUpdate () {}, |
|||
// 组价更新 |
|||
updated () {}, |
|||
// 组件销毁前 |
|||
beforeDestroy () {}, |
|||
// 组件销毁后 |
|||
destroyed () {}, |
|||
// 页面方法 |
|||
methods: { |
|||
// 开始绘制 |
|||
createCanvas () { |
|||
this.clearCanvas(); |
|||
if (this.lists.length === 0) { |
|||
uni.showToast({ |
|||
title: 'lists不能为空', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
this.listsIndex = 0; |
|||
this.listsLength = this.lists.length - 1; |
|||
uni.showLoading({ |
|||
title: '正在生成图片...', |
|||
mask: true |
|||
}); |
|||
setTimeout(()=>{ |
|||
uni.hideLoading(); |
|||
},10000); |
|||
this.dataDrawCanvas(); |
|||
}, |
|||
// 数据绘制 |
|||
dataDrawCanvas () { |
|||
let item = this.lists[this.listsIndex]; |
|||
if (item.type === 'image') { // 图片 |
|||
if (item.content.indexOf('https://') > -1||item.content.indexOf('http://') > -1) { // https://网络图片 |
|||
// #ifndef H5 |
|||
// 非H5 |
|||
this.downloadImageNotH5(item); |
|||
// #endif |
|||
// #ifdef H5 |
|||
// H5 |
|||
this.downloadImageH5(item); |
|||
// #endif |
|||
} else { // 本地选择图片 |
|||
if (this.compress && item.hasOwnProperty('file') && item.file.size > this.compressSize) { // 大于限制2M压缩 |
|||
this.compressImage(item); |
|||
} else { |
|||
if (item.arc) { |
|||
this.drawImageArc(item); |
|||
} else { |
|||
this.drawImage(item); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} else if (item.type === 'text') { // 文本 |
|||
this.drawText(item); |
|||
} else if (item.type === 'rect') { // 矩形(线条) |
|||
this.drawRect(item); |
|||
} else if (item.type === 'arc') { // 圆形 |
|||
this.drawArc(item); |
|||
} else if (item.type === 'qr') { // 二维码 |
|||
this.drawQR(item); |
|||
} |
|||
}, |
|||
// #ifndef H5 |
|||
// https图片下载本地并绘制,非H5 |
|||
downloadImageNotH5 (item) { |
|||
uni.downloadFile({ |
|||
url: item.content, |
|||
header: { |
|||
'Access-Control-Allow-Origin': '*', |
|||
}, |
|||
success: (res) => { |
|||
item.content = res.tempFilePath; |
|||
if (item.arc) { |
|||
this.drawImageArc(item); |
|||
} else { |
|||
this.drawImage(item); |
|||
} |
|||
}, |
|||
fail: (res) => { |
|||
console.log(res); |
|||
} |
|||
}); |
|||
}, |
|||
// #endif |
|||
// #ifdef H5 |
|||
// https图片下载本地并绘制,H5 |
|||
downloadImageH5 (item) { |
|||
console.log(item.content); |
|||
if (/\.(jpg|png)$/i.test(item.content)) { |
|||
// 符合条件时跳过处理 |
|||
this.checkDrawOver(); |
|||
}else{ |
|||
let image = null; |
|||
image = new Image(); |
|||
image.setAttribute('crossOrigin', 'anonymous'); |
|||
image.crossOrigin = 'Anonymous'; |
|||
image.src = item.content; |
|||
image.onload = () => { |
|||
let canvas = document.createElement('canvas'); |
|||
canvas.width = item.width; |
|||
canvas.height = item.height; |
|||
let ctx = canvas.getContext('2d'); |
|||
ctx.drawImage( |
|||
image, |
|||
0, |
|||
0, |
|||
item.width, |
|||
item.height |
|||
); |
|||
let dataURL = canvas.toDataURL('image/png'); |
|||
if (item.arc) { // 绘制圆形 |
|||
item.content = dataURL; |
|||
this.drawImageArc(item); |
|||
} else { |
|||
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1; |
|||
this.canvas.drawImage( |
|||
dataURL, |
|||
item.x, |
|||
item.y, |
|||
item.hasOwnProperty('width') ? item.width : this.width, |
|||
item.hasOwnProperty('height') ? item.height : this.height |
|||
); |
|||
this.checkDrawOver(); |
|||
} |
|||
|
|||
}; |
|||
} |
|||
}, |
|||
// #endif |
|||
// 图片压缩 |
|||
compressImage (item) { |
|||
uni.showLoading({ |
|||
title: '压缩中...', |
|||
mask: true |
|||
}); |
|||
// 非H5压缩 |
|||
// #ifndef H5 |
|||
uni.compressImage({ |
|||
src: item.content, |
|||
quality: this.compressQuality, |
|||
success: (res) => { |
|||
uni.showLoading({ |
|||
title: '正在生成图片...', |
|||
mask: true |
|||
}); |
|||
item.content = res.tempFilePath; |
|||
if (item.arc) { |
|||
this.drawImageArc(item); |
|||
} else { |
|||
this.drawImage(item); |
|||
} |
|||
}, |
|||
fail: (res) => { |
|||
console.log(res); |
|||
uni.showToast({ |
|||
title: '压缩失败', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
}); |
|||
// #endif |
|||
// H5压缩 |
|||
// #ifdef H5 |
|||
let image = new Image(); |
|||
image.setAttribute('crossOrigin', 'anonymous'); |
|||
image.crossOrigin = 'Anonymous'; |
|||
image.src = item.content; |
|||
image.onload = () => { |
|||
let canvas = document.createElement('canvas'); |
|||
canvas.width = item.width; |
|||
canvas.height = item.height; |
|||
let ctx = canvas.getContext('2d'); |
|||
ctx.drawImage( |
|||
image, |
|||
0, |
|||
0, |
|||
item.width, |
|||
item.height |
|||
); |
|||
let dataURL = canvas.toDataURL('image/png'); |
|||
item.content = dataURL; |
|||
if (item.arc) { |
|||
this.drawImageArc(item); |
|||
} else { |
|||
this.drawImage(item); |
|||
} |
|||
}; |
|||
// #endif |
|||
}, |
|||
// 圆形图片另外绘制canvas,png格式 |
|||
drawImageArc (item) { |
|||
this.canvasArc.clearRect(0, 0, this.canvasArcWidth, this.canvasArcHeight); |
|||
this.canvasArcWidth = item.arcR * 2; |
|||
this.canvasArcHeight = item.arcR * 2; |
|||
this.canvasArc.save(); |
|||
let arcT = setTimeout(() => { |
|||
clearTimeout(arcT); |
|||
this.canvasArc.arc(item.arcR, item.arcR, item.arcR, 0, 2 * Math.PI); |
|||
this.canvasArc.clip(); |
|||
// this.canvasArc.closePath(); |
|||
|
|||
this.canvasArc.drawImage( |
|||
item.content, |
|||
item.arcX, |
|||
item.arcY, |
|||
item.width, |
|||
item.height |
|||
); |
|||
this.canvasArc.draw(false, setTimeout(() => { |
|||
let t = setTimeout(() => { |
|||
clearTimeout(t); |
|||
uni.canvasToTempFilePath({ |
|||
x: 0, |
|||
y: 0, |
|||
width: item.arcR * 2, |
|||
height: item.arcR * 2, |
|||
fileType: 'png', |
|||
canvasId: 'canvas-arc', |
|||
success: (res) => { |
|||
item.width = item.arcR * 2; |
|||
item.height = item.arcR * 2; |
|||
item.content = res.tempFilePath; |
|||
this.drawImage(item); |
|||
}, |
|||
fail: (res) => { |
|||
console.log(res); |
|||
}, |
|||
complete: () => { |
|||
this.canvasArc.restore(); |
|||
this.canvasArc.fillRect(0, 0, 0, 0); |
|||
this.canvasArc.clearRect(0, 0, this.canvasArcWidth, this.canvasArcHeight); |
|||
} |
|||
}, this); |
|||
}, 100); |
|||
})); |
|||
}, 100); |
|||
}, |
|||
// 图片绘制 |
|||
drawImage (item) { |
|||
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1; |
|||
this.canvas.drawImage( |
|||
item.content, |
|||
item.x, |
|||
item.y, |
|||
item.hasOwnProperty('width') ? item.width : this.width, |
|||
item.hasOwnProperty('height') ? item.height : this.height |
|||
); |
|||
this.checkDrawOver(); |
|||
}, |
|||
// 文本绘制 |
|||
drawText (item) { |
|||
this.canvas.setFillStyle(item.hasOwnProperty('color') ? item.color : '#000000'); |
|||
this.canvas.setFontSize(item.hasOwnProperty('size')? item.size : 20); |
|||
this.canvas.setTextAlign(item.hasOwnProperty('align') ? item.align: 'left'); |
|||
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1; |
|||
|
|||
if (item.maxWidth) { |
|||
this.canvas.fillText(item.content, item.x, item.y, item.maxWidth); |
|||
} else { |
|||
this.canvas.fillText(item.content, item.x, item.y); |
|||
} |
|||
this.checkDrawOver(); |
|||
}, |
|||
|
|||
// 矩形(线条)绘制 |
|||
drawRect (item) { |
|||
this.canvas.setFillStyle(item.hasOwnProperty('color') ? item.color : '#000000'); |
|||
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1; |
|||
this.canvas.fillRect(item.x, item.y, item.width, item.height); |
|||
this.checkDrawOver(); |
|||
}, |
|||
|
|||
// 圆形绘制 |
|||
drawArc (item) { |
|||
this.canvas.arc(item.arcX, item.arcY, item.arcR, 0, 2 * Math.PI); |
|||
this.canvas.setFillStyle(item.hasOwnProperty('color') ? item.color : '#000000'); |
|||
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1; |
|||
this.canvas.fill(); |
|||
this.canvas.closePath(); |
|||
this.checkDrawOver(); |
|||
}, |
|||
|
|||
// 二维码绘制 |
|||
drawQR (item) { |
|||
let len=item.content.length; |
|||
let typeNumber=Math.ceil(len/16); |
|||
if(typeNumber<4){ |
|||
typeNumber=4; |
|||
} |
|||
item['qr'] = QR.createQrCodeImg(item.content, { |
|||
size: parseInt(300), |
|||
typeNumber:typeNumber |
|||
}); |
|||
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1; |
|||
this.canvas.drawImage( |
|||
item.qr, |
|||
item.x, |
|||
item.y, |
|||
item.hasOwnProperty('width') ? item.width : this.width, |
|||
item.hasOwnProperty('height') ? item.height : this.height |
|||
); |
|||
this.checkDrawOver(); |
|||
}, |
|||
|
|||
|
|||
// 判断是否绘制完 |
|||
checkDrawOver () { |
|||
if (this.listsIndex < this.listsLength) { // lists未画完 |
|||
this.listsIndex++; |
|||
this.dataDrawCanvas(); |
|||
} else { |
|||
this.canvasImage(); |
|||
} |
|||
}, |
|||
|
|||
// 绘制到画布并生成图片 |
|||
canvasImage () { |
|||
this.listsIndex = 0; |
|||
this.canvas.draw(false, () => { |
|||
setTimeout(() => { |
|||
uni.canvasToTempFilePath({ |
|||
x: 0, |
|||
y: 0, |
|||
width: Number(this.width), |
|||
height: Number(this.height), |
|||
fileType: this.imgType, |
|||
canvasId: 'canvas', |
|||
success: (res) => { |
|||
this.$emit('canvasImage', res.tempFilePath); |
|||
if (this.showPreview) { |
|||
this.showPreviewFn(res.tempFilePath); |
|||
} |
|||
}, |
|||
fail: (res) => { |
|||
console.log(res); |
|||
}, |
|||
complete: () => { |
|||
uni.hideLoading(); |
|||
} |
|||
}, this); |
|||
}, 500); |
|||
}); |
|||
}, |
|||
// 预览图 |
|||
showPreviewFn (img) { |
|||
uni.previewImage({ |
|||
current: 0, |
|||
urls: [img] |
|||
}); |
|||
}, |
|||
// 清空画布 |
|||
clearCanvas () { |
|||
this.canvas.clearRect(0, 0, this.width, this.height); |
|||
}, |
|||
|
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang='scss' scoped> |
|||
.mosowe-canvas-image{ |
|||
overflow: hidden; |
|||
.canvas-wrap-box{ |
|||
overflow: hidden; |
|||
height: 0; |
|||
width: 0; |
|||
position: fixed; |
|||
left:200%; |
|||
top: 0; |
|||
} |
|||
.canvas-wrap { |
|||
overflow: hidden; |
|||
height: 0; |
|||
width: 0; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,195 @@ |
|||
## mosowe-canvas-image:一个可以制作多用途图片的插件(海报,二维码,分享图) |
|||
|
|||
### v1.2.0: |
|||
1. 添加透明度`globalAlpha`,值0-1; |
|||
2. 修改只能画一个圆形图片问题,画圆形图片时耗时较多,因为额外增加了一个canvas处理圆形图片,请注意`arcX`与`arcY`的说明; |
|||
|
|||
### v1.1.0: |
|||
|
|||
1. 添加本地图片渲染,已支持本地相册/相机 + https的网络图片,建议画布宽高:750*1330 |
|||
2. 添加导出图片的类型:`imgType`,可选值,jpg、png |
|||
3. 添加本地图片压缩功能:`compress`是否开启压缩,`compressSize`:压缩程度,默认2M,lists列表图片项需增加传参`file`, |
|||
4. 解决H5网络图片“canvasToTempFilePath:fail SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported”错误问题,图片服务器需支持“Access-Control-Allow-Origin: *” |
|||
5. 使用非solt插槽触发事件时,请确认`$ref`是否正确 |
|||
6. 示例增加参数编辑,使用前体验插件 |
|||
|
|||
#### 平台支持: |
|||
|
|||
| APP | H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 字节跳动小程序 | QQ小程序 | |
|||
| :--: | :--: | :--------: | :----------: | :--------: | :------------: | :------: | |
|||
| √ | √ | √ | × | 未测 | 未测 | 未测 | |
|||
|
|||
#### 插件功能 |
|||
|
|||
1. 支持多图片绘制,多文本绘制,圆形图片绘制; |
|||
2. 支持矩形(线条)绘制; |
|||
3. 支持圆形绘制; |
|||
4. 支持二维码生成,项目用不上可以去插件内去除,毕竟这个插件携带的比较大,单纯用来生成二维码图片也是阔以的; |
|||
5. 支持绘图后预览。 |
|||
|
|||
多用于海报图,分享图; |
|||
|
|||
注意H5跨域问题及小程序白名单配置; |
|||
|
|||
图片是网络图片:https://....(require及import引入不了3Kb以上的绝对路径图片,若有大神知道处理方法,望不吝赐教,谢谢!) |
|||
|
|||
#### 属性 |
|||
|
|||
| 名称 | 类型 | 默认值 | 说明 | 版本 | |
|||
| ------------ | ---------------- | ------------ | -------------------------------------------------------- | ----- | |
|||
| width | Number \String | 200 | canvas画布宽度,也是导出图片宽度,单位px,值中不要带单位 | 1.0.0 | |
|||
| height | Number \String | 200 | canvas画布高度,也是导出图片高度,单位px,值中不要带单位 | 1.0.0 | |
|||
| showPreview | Boolean | false | 绘制完成后是否打开预览 | 1.0.0 | |
|||
| lists | Array | [] | 绘制的元素列表:图片,文字,矩形(线条),圆形,二维码 | 1.0.0 | |
|||
| imgType | String | jpg | 生成图片格式,可选:png | 1.1.0 | |
|||
| compress | Boolean | false | 是否开启图片压缩 | 1.1.0 | |
|||
| compressSize | Number\String | 2097152 (2M) | 超过多少M压缩 | 1.1.0 | |
|||
|
|||
|
|||
|
|||
|
|||
#### lists\<item>属性 |
|||
|
|||
注意:图文先后顺序,底层的图片靠前,最上层的在最后,圆形图片放在最后,因为一旦绘制圆形,后续的元素都只在该圆形内显示,而超过圆形范围的将看不见。 |
|||
|
|||
| 名称 | 类型 | 必填 | 说明 |版本| |
|||
| :------- | :----- | :--- | :----------------------------------------------------------- |:----| |
|||
| type | String | 是 | 元素类型:`image`图片,`text`文本,`rect`矩形(线条),`arc`圆形,`qr`二维码 |1.0.0| |
|||
| content | String | 否 | image:图片路径(必填),text:文字(必填),qr:转二维码的数据(必填),rect及arc:非必填 |1.0.0| |
|||
| x | Number | 是 | X轴坐标,绘制圆形图片时:x = arcX - arcR |1.0.0| |
|||
| y | Number | 是 | Y轴坐标,绘制圆形图片时:y = arcY - arcR |1.0.0| |
|||
| width | Number | 否 | 图片、矩形(线条)、二维码宽度 |1.0.0| |
|||
| height | Number | 否 | 图片、矩形(线条)、二维码高度 |1.0.0| |
|||
| arc | Boolen | 否 | type=image时:是否绘制圆形图片 |1.0.0| |
|||
| arcX | Number | 否 | type=arc时:绘制圆形时中心点X轴坐标,type=image时,图片在圆形canvas的X坐标,多为负数,版本`1.2.0` |1.2.0| |
|||
| arcY | Number | 否 | type=arc时:绘制圆形时中心点Y轴坐标,type=image时,图片在圆形canvas的Y坐标,多为负数,版本`1.2.0` |1.2.0| |
|||
| arcR | Number | 否 | type=image、arc时:绘制圆形的半径 |1.0.0| |
|||
| color | String | 否 | 绘制文本、矩形(线条)的颜色,默认:#000000 |1.0.0| |
|||
| size | Number | 否 | 绘制文本的字号大小,默认:20 |1.0.0| |
|||
| align | String | 否 | 绘制文本的对齐方式,默认:left |1.0.0| |
|||
| maxWidth | Number | 否 | 绘制文本的最大宽度,文字长度超过该值会被压缩 |1.0.0| |
|||
| file | file | 否 | 选择本地图片的file文件,版本`1.1.0` |1.1.0| |
|||
| globalAlpha | Number | 否 | 透明度:0~1,默认1,版本`1.2.0` |1.2.0| |
|||
|
|||
#### slots |
|||
|
|||
| 名称 | 说明 | |
|||
| :------ | :--------------------------------- | |
|||
| default | 自定义插槽,点击此区会触发绘图事件 | |
|||
|
|||
#### 事件 |
|||
|
|||
| 名称 | 回调参数 | 说明 | |
|||
| ----------- | -------- | ------------------------------------ | |
|||
| canvasImage | url | 绘制成功后返回的本地地址,H5为base64 | |
|||
|
|||
|
|||
|
|||
#### 使用方式 |
|||
|
|||
若`page.json`中配置了`"easycom": true`,则无需`script`引入就可以使用,没有则需要引入。 |
|||
|
|||
1. 无slot:组件标签添加`ref`属性,采用父级调用子组件`createCanvas()`方法使用,见后文示例; |
|||
2. 有slot:slot区点击就会执行 |
|||
|
|||
#### 示例 |
|||
|
|||
```javascript |
|||
// js |
|||
data () { |
|||
return { |
|||
canvasUrl: '', |
|||
lists: [ |
|||
{ |
|||
type: 'image', |
|||
content: 'https://www.zhonglixunqing.cn/images/uniapp/1.jpg', |
|||
width: 200, |
|||
height: 100, |
|||
x: 50, |
|||
y: 20, |
|||
}, |
|||
{ |
|||
type: 'image', |
|||
content: 'https://www.zhonglixunqing.cn/images/uniapp/2.jpg', |
|||
width: 80, |
|||
height: 80, |
|||
x: 20, |
|||
y: 200, |
|||
arc: false, |
|||
arcX: 0, |
|||
arcY: 0, |
|||
arcR: 0 |
|||
}, |
|||
{ |
|||
type: 'text', |
|||
content: '扫一扫,获取更多信息', |
|||
x: 120, |
|||
y: 250, |
|||
color: '#ff0000', |
|||
size: 10, |
|||
// maxWidth: 100, |
|||
// align: 'left', |
|||
}, |
|||
{ |
|||
type: 'rect', |
|||
width: 1, |
|||
height: 100, |
|||
x: 0, |
|||
y: 10, |
|||
color: '#ff0000', |
|||
}, |
|||
{ |
|||
type: 'image', |
|||
content: 'https://www.zhonglixunqing.cn/images/uniapp/3.jpg', |
|||
width: 100, |
|||
height: 100, |
|||
x: 200, |
|||
y: 200, |
|||
arc: true, |
|||
arcX: 250, |
|||
arcY: 250, |
|||
arcR: 50 |
|||
}, |
|||
] |
|||
}; |
|||
}, |
|||
methods: { |
|||
beginCanvas () { |
|||
this.$refs.mosoweCanvasComponents.createCanvas(); |
|||
}, |
|||
_canvasImage (e) { |
|||
this.canvasUrl = e; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
插件外独立按钮触发: |
|||
|
|||
```html |
|||
<button type="default" @click="beginCanvas">开始绘图</button> |
|||
<image :src="canvasUrl" mode="widthFix"></image> |
|||
<mosowe-canvas-image |
|||
ref="mosoweCanvasComponents" |
|||
@canvasImage="_canvasImage" |
|||
:lists="lists" |
|||
height="300" |
|||
width="300" |
|||
showPreview /> |
|||
``` |
|||
|
|||
slot插槽触发: |
|||
|
|||
```html |
|||
<mosowe-canvas-image |
|||
:lists="lists" |
|||
height="300" |
|||
width="300" |
|||
showPreview > |
|||
<view class="in_btn"> |
|||
slot按钮的 |
|||
</view> |
|||
</mosowe-canvas-image> |
|||
``` |
|||
|
|||
#### 预览地址 |
|||
|
|||
@ -0,0 +1,416 @@ |
|||
<template> |
|||
<view class="burst-wrap"> |
|||
<view class="burst-wrap-bg"> |
|||
<view> |
|||
<!-- 信息提交 --> |
|||
<view class="burst-info"> |
|||
<view class="uni-uploader-body"> |
|||
<view class="uni-uploader__files"> |
|||
<!-- 图片 --> |
|||
<block v-for="(image,index) in imageList" :key="index"> |
|||
<view class="uni-uploader__file"> |
|||
<view class="icon iconfont icon-cuo"> |
|||
<!-- @tap="delect(index)" --> |
|||
<uni-icons type="closeempty" color="#000" size="15" class="closeempty_icons" @tap="delect(index)"></uni-icons> |
|||
</view> |
|||
<image class="uni-uploader__img" :src="image" :data-src="image" @tap="previewImage"></image> |
|||
</view> |
|||
</block> |
|||
<!-- 视频 --> |
|||
<view v-if="src1.url" style="width: 100%;height: 100%;"> |
|||
<view> |
|||
<!-- <video :src="src" id="myVideo" class="video" @play="playVideoFullscreen"></video> |
|||
<cover-view style="position: absolute;width: 160rpx;height: 160rpx;z-index: 999;color: #fff;display: flex;"> |
|||
<cover-view style="width: 80%;" @click="playVideoFullscreen"></cover-view> |
|||
<cover-view style="width: 20%;height: 40rpx;font-size: 45rpx;" @click="delectVideo">×</cover-view> |
|||
</cover-view> --> |
|||
<view class='course-video' :style="src1.url ? $util.imageCoverStyle(270,480) : ''"> |
|||
<view class="relative-shadow flex justify-center align-center" @tap="handlePlay(src1)"> |
|||
<view class="cuIcon-video icon-center f-28 c-white"></view> |
|||
</view> |
|||
<image :src="src1.poster" mode="aspectFit" style="position: absolute;top: 0px;width: 100%;height: 100%;"></image> |
|||
<uni-icons @click="delectVideo" type="closeempty" size="30" style="position: absolute;top: 0px;right: 0px;z-index: 1;"></uni-icons> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="uni-uploader__input-box" v-if="VideoOfImagesShow"> |
|||
<view class="uni-uploader__input" @tap="chooseVideoImage"> |
|||
<image src="/static/image/jiahao.png" mode="widthFix" style="width: 60rpx;"></image> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import config from '@/common/config.js' |
|||
import { chat } from '@/mixins/chat.js' |
|||
import imImage from '@/components/message/im-image.vue'; |
|||
var sourceType = [ |
|||
['camera'], |
|||
['album'], |
|||
['camera', 'album'] |
|||
] |
|||
export default { |
|||
components:{ |
|||
imImage |
|||
}, |
|||
mixins:[chat], |
|||
props: { |
|||
img_arr1: { type: Array, default: ()=>[] } |
|||
}, |
|||
data() { |
|||
return { |
|||
imageList:[],//图片 |
|||
src1:{},//视频存放 |
|||
sourceTypeIndex: 2, |
|||
checkedValue:true, |
|||
checkedIndex:0, |
|||
sourceType: ['拍摄', '相册', '拍摄或相册'], |
|||
cameraList: [{ |
|||
value: 'back', |
|||
name: '后置摄像头', |
|||
checked: 'true' |
|||
}, |
|||
{ |
|||
value: 'front', |
|||
name: '前置摄像头' |
|||
}, |
|||
], |
|||
cameraIndex: 0, |
|||
VideoOfImagesShow:true, |
|||
} |
|||
}, |
|||
watch: { |
|||
img_arr1: { |
|||
immediate: true, // 立即触发初始值 |
|||
handler(newVal) { |
|||
if(newVal.length > 0) { |
|||
// this.handleImageLoad(newVal); // 你的业务逻辑 |
|||
newVal.forEach((res)=>{ |
|||
if(res.type==1){ |
|||
this.imageList.push(config.apiUrl+res.src) |
|||
}else if(res.type==2){ |
|||
this.src1 = {url:config.apiUrl+res.src,poster:config.apiUrl+res.privacy}; |
|||
this.VideoOfImagesShow = false; |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
onUnload() { |
|||
this.src1 = {}, |
|||
this.sourceTypeIndex = 2, |
|||
this.sourceType = ['拍摄', '相册', '拍摄或相册']; |
|||
}, |
|||
methods: { |
|||
chooseVideoImage(){ |
|||
if(this.imageList.length>0){ |
|||
uni.showActionSheet({ |
|||
title:"选择上传类型", |
|||
itemList: ['图片'], |
|||
success: (res) => { |
|||
console.log(res) |
|||
if(res.tapIndex == 0){ |
|||
this.chooseImages() |
|||
} else { |
|||
this.chooseVideo() |
|||
} |
|||
} |
|||
}) |
|||
}else{ |
|||
uni.showActionSheet({ |
|||
title:"选择上传类型", |
|||
itemList: ['图片','视频'], |
|||
success: (res) => { |
|||
console.log(res) |
|||
if(res.tapIndex == 0){ |
|||
this.chooseImages() |
|||
} else { |
|||
this.chooseVideo() |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
playVideoFullscreen(){ |
|||
// 创建视频上下文 |
|||
const videoContext = uni.createVideoContext('myVideo') |
|||
|
|||
// 进入全屏 |
|||
videoContext.requestFullScreen({direction: 0}); |
|||
}, |
|||
chooseImages(){ |
|||
// 上传图片 |
|||
uni.chooseImage({ |
|||
count: 9, //默认9 |
|||
// sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 |
|||
sourceType: ['album','camera'], //从相册选择 |
|||
success:(res)=> { |
|||
let igmFile = res.tempFilePaths; |
|||
console.log(igmFile,'123'); |
|||
// 计算还能添加多少张(最多9张) |
|||
const remainingSlots = 9 - this.imageList.length; |
|||
if (remainingSlots <= 0) { |
|||
uni.showToast({ title: '最多上传9张图片', icon: 'none' }); |
|||
return; |
|||
} |
|||
|
|||
// 只处理剩余可添加的数量 |
|||
const filesToUpload = igmFile.slice(0, remainingSlots); |
|||
|
|||
filesToUpload.map(fileurl => { |
|||
uni.uploadFile({ |
|||
url:config.apiUrl+'/common/upload/uploadPosts', |
|||
method:"POST", |
|||
header:{ |
|||
'Authorization':uni.getStorageSync('authToken') |
|||
}, |
|||
filePath:fileurl, |
|||
name:'file', |
|||
success: (res) =>{ |
|||
let imgUrls = JSON.parse(res.data); //微信和头条支持 |
|||
imgUrls.data.forEach((item)=>{ |
|||
if (this.imageList.length < 9) { |
|||
this.imageList = this.imageList.concat(item.url); |
|||
} |
|||
}) |
|||
this.$emit('send',this.imageList) |
|||
if(this.imageList.length>=9) { |
|||
this.VideoOfImagesShow = false; |
|||
} else { |
|||
this.VideoOfImagesShow = true; |
|||
} |
|||
} |
|||
}) |
|||
}) |
|||
}, |
|||
}); |
|||
}, |
|||
chooseVideo(){ |
|||
// 上传视频 |
|||
uni.chooseVideo({ |
|||
maxDuration:60, |
|||
count: 9, |
|||
camera: this.cameraList[this.cameraIndex].value, |
|||
sourceType: ['album', 'camera'], |
|||
success: (responent) => { |
|||
let videoFile = responent.tempFilePath; |
|||
uni.uploadFile({ |
|||
url:config.apiUrl+'/common/upload/uploadPosts', |
|||
method:"POST", |
|||
header:{ |
|||
'Authorization':uni.getStorageSync('authToken') |
|||
}, |
|||
filePath:videoFile, |
|||
name:'file', |
|||
success: (res) => { |
|||
let videoUrls = JSON.parse(res.data) //微信和头条支持 |
|||
// console.info(videoUrls,'12345'); |
|||
// let videoUrls = res.data //百度支持 |
|||
// this.imagesUrlPath = this.imagesUrlPath.concat(videoUrls); |
|||
this.src1 = videoUrls.data[0]; //微信 |
|||
this.$emit('videourl',this.src1) |
|||
if(this.src1.url) { |
|||
this.itemList = ['图片'] |
|||
this.VideoOfImagesShow = false; |
|||
} else { |
|||
this.itemList = ['图片','视频'] |
|||
} |
|||
|
|||
} |
|||
}) |
|||
// this.src = responent.tempFilePath; //头条 |
|||
} |
|||
}) |
|||
}, |
|||
previewImage(e) { |
|||
//预览图片 |
|||
var current = e.target.dataset.src |
|||
uni.previewImage({ |
|||
current: current, |
|||
urls: [current] |
|||
}) |
|||
}, |
|||
delect(index){ |
|||
uni.showModal({ |
|||
title: "提示", |
|||
content: "是否要删除该图片", |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
this.imageList.splice(index, 1) |
|||
if(this.imageList.length>=9) { |
|||
this.VideoOfImagesShow = false; |
|||
} else { |
|||
this.VideoOfImagesShow = true; |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
delectVideo(){ |
|||
uni.showModal({ |
|||
title:"提示", |
|||
content:"是否要删除此视频", |
|||
success:(res) =>{ |
|||
if(res.confirm){ |
|||
this.src1 = {} |
|||
this.VideoOfImagesShow = true; |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.burst-wrap{ |
|||
width: 100%; |
|||
height: 100px; |
|||
margin-top: 20px; |
|||
} |
|||
/* .burst-wrap .burst-wrap-bg{ |
|||
position: relative; |
|||
width: 100%; |
|||
height: 320rpx; |
|||
background:linear-gradient(90deg,rgba(251,91,80,1) 0%,rgba(240,45,51,1) 100%); |
|||
border-bottom-right-radius: 80rpx; |
|||
border-bottom-left-radius: 80rpx; |
|||
} */ |
|||
.burst-wrap .burst-wrap-bg>view{ |
|||
width: 100%; |
|||
height: 100%; |
|||
/* margin: 0 auto; |
|||
position: absolute; |
|||
top: 65rpx; |
|||
left: 0; |
|||
right: 0; */ |
|||
} |
|||
|
|||
.form-item{ |
|||
width: 100%; |
|||
} |
|||
.form-item textarea{ |
|||
display: block; |
|||
height: 220rpx; |
|||
width: 100%; |
|||
color: #AAAAAA; |
|||
font-size: 28rpx; |
|||
} |
|||
.uni-uploader__files{ |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
} |
|||
.uni-uploader__file{ |
|||
z-index: 999; |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
margin-right: 6px; |
|||
margin-top: 5px; |
|||
position: relative; |
|||
} |
|||
.uploader_video{ |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
margin-right: 6px; |
|||
} |
|||
.uni-uploader__img { |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
} |
|||
.icon-cuo { |
|||
top: 5rpx; |
|||
right: 5px; |
|||
width: 20px; |
|||
height: 20px; |
|||
z-index: 999; |
|||
/* color: #FFFFFF; */ |
|||
position: absolute; |
|||
border-radius: 20px; |
|||
/* background: linear-gradient(90deg,rgba(209, 209, 209, 1.0) 0%,rgba(209, 209, 209, 1.0) 100%); */ |
|||
} |
|||
.video{ |
|||
width: 100%; |
|||
height: 100%; |
|||
position: absolute; |
|||
z-index: -1; |
|||
} |
|||
|
|||
.login-input-box{ |
|||
position: relative; |
|||
border-bottom: 1rpx solid #EEEEEE; |
|||
} |
|||
.login-input-box .forget,.login-input-box .phone{ |
|||
position: absolute; |
|||
top: 0; |
|||
height: 100%; |
|||
z-index: 100; |
|||
} |
|||
.login-input-box .phone{ |
|||
width: 100rpx; |
|||
left: 0; |
|||
color: #666666; |
|||
font-weight: bold; |
|||
} |
|||
.login-input-box .phone-input{ |
|||
padding-left: 100rpx; |
|||
} |
|||
.address-wrap,.open-info{ |
|||
margin-top: 20rpx; |
|||
} |
|||
.open-info>view:last-child{ |
|||
font-size: 28rpx; |
|||
color: #999999; |
|||
} |
|||
.address-wrap .address { |
|||
background: #F2F2F2; |
|||
border-radius: 40rpx; |
|||
padding: 0 20rpx; |
|||
} |
|||
.user-set-btn{ |
|||
margin: 40rpx; |
|||
background: linear-gradient(90deg,rgba(251,91,80,1) 0%,rgba(240,45,51,1) 100%); |
|||
color: #FFFFFF; |
|||
text-align: center; |
|||
height: 88rpx; |
|||
line-height: 88rpx; |
|||
} |
|||
.uni-uploader__input-box{ |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
margin-top: 5px; |
|||
} |
|||
.uni-uploader__input{ |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: #ebebeb; |
|||
} |
|||
.closeempty_icons{ |
|||
display: flex; |
|||
line-height: 20px; |
|||
justify-content: center; |
|||
} |
|||
.course-video{ |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
.relative-shadow{ |
|||
z-index:1; |
|||
width:100%; |
|||
height:100%; |
|||
height: 240px; |
|||
display: flex; |
|||
position: absolute; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,92 @@ |
|||
<!-- 原子脉冲状态显示器 ,引用于scui--> |
|||
<template> |
|||
<view class="sc-state" :class="[{ 'sc-status-processing': pulse }, 'sc-state-bg--' + type]"></view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
type: { type: String, default: "primary" }, |
|||
pulse: { type: [Boolean,Number], default: true } |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
|
|||
.sc-state { |
|||
display: inline-block; |
|||
background: '#409EFF'; |
|||
width: 6px; |
|||
height: 6px; |
|||
margin:0 5px 3px; |
|||
border-radius: 50%; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
.sc-status-processing { |
|||
position: relative; |
|||
|
|||
&:after { |
|||
position: absolute; |
|||
top: 0px; |
|||
left: 0px; |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 50%; |
|||
background: inherit; |
|||
content: ''; |
|||
animation: warns 1.2s ease-in-out infinite; |
|||
} |
|||
} |
|||
|
|||
.sc-state-bg--primary { |
|||
background: #409EFF; |
|||
} |
|||
|
|||
.sc-state-bg--success { |
|||
background: #67C23A; |
|||
} |
|||
|
|||
.sc-state-bg--warning { |
|||
background: #E6A23C; |
|||
} |
|||
|
|||
.sc-state-bg--danger { |
|||
background: #F56C6C; |
|||
} |
|||
|
|||
.sc-state-bg--info { |
|||
background: #909399; |
|||
} |
|||
@-webkit-keyframes warns { |
|||
0% { |
|||
transform: scale(0.5); |
|||
opacity: 1; |
|||
} |
|||
|
|||
30% { |
|||
opacity: 0.7; |
|||
} |
|||
|
|||
100% { |
|||
transform: scale(2.5); |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
@keyframes warns { |
|||
0% { |
|||
transform: scale(0.5); |
|||
opacity: 1; |
|||
} |
|||
|
|||
30% { |
|||
opacity: 0.7; |
|||
} |
|||
|
|||
100% { |
|||
transform: scale(2.5); |
|||
opacity: 0; |
|||
} |
|||
}</style> |
|||
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 24 KiB |
@ -0,0 +1,613 @@ |
|||
<html lang="zh"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> |
|||
<script type="text/javascript" src="./rtc/adapter-latest.js"></script> |
|||
<script type="text/javascript" src='./js/uni.webview.js'></script> |
|||
<script type="text/javascript" src='./js/utils.js'></script> |
|||
<script type="text/javascript" src='./js/jsonly.js'></script> |
|||
<style> |
|||
body{ |
|||
padding:0; |
|||
margin:0; |
|||
background-image: url('image/wallpaper.png'); |
|||
background-size: contain; |
|||
} |
|||
|
|||
.webrtc-box{ |
|||
background: #666; |
|||
border-radius: 6px; |
|||
width:100%; |
|||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
|||
} |
|||
.localvideo{ |
|||
width:100vw; |
|||
height:100vh; |
|||
object-fit: cover; |
|||
} |
|||
.remotevideo{ |
|||
min-height: 160px; |
|||
width: 100px; |
|||
position: fixed; |
|||
top: 40px; |
|||
right: 15px; |
|||
z-index:10; |
|||
object-fit: cover; |
|||
} |
|||
|
|||
.call-user-box{ |
|||
position:fixed; |
|||
bottom: 20px; |
|||
width:100%; |
|||
} |
|||
|
|||
.call-user{ |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
margin-bottom:50px; |
|||
} |
|||
.call-user .avatar{ |
|||
width:60px; |
|||
height:60px; |
|||
object-fit: contain; |
|||
border-radius: 50%; |
|||
overflow: hidden; |
|||
} |
|||
.call-user .text{ |
|||
font-size:16px; |
|||
margin-top:15px; |
|||
color:#f6f6f6 |
|||
} |
|||
|
|||
.call-time{ |
|||
color:#f6f6f6; |
|||
font-size: 24px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.calling-button{ |
|||
display: flex; |
|||
justify-content: space-around; |
|||
padding: 20px; |
|||
} |
|||
.calling-button .button{ |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
.calling-button .button .image{ |
|||
width:60px; |
|||
height:60px; |
|||
margin-bottom: 10px; |
|||
} |
|||
.calling-button .button .text{ |
|||
color:#f6f6f6; |
|||
} |
|||
.calling-button .switch-btn .text{ |
|||
font-size:12px !important; |
|||
} |
|||
.calling-button .button .image-icon{ |
|||
width:40px; |
|||
height:40px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div id="app"> |
|||
<div class="webrtc-box"> |
|||
<audio id="music1"> |
|||
<source src="./voice/calling.mp3"> |
|||
</audio> |
|||
<video v-show="localStream && is_video" class="localvideo" ref="localvideo" x5-video-player-fullscreen="true" autoplay x5-playsinline playsinline webkit-playsinline @click="displayBtn = !displayBtn" poster="./image/wallpaper.png"></video> |
|||
<video v-show="remoteStream && is_video" class="remotevideo" ref="remotevideo" x5-video-player-fullscreen="true" autoplay x5-playsinline playsinline webkit-playsinline @click="changeVideo()" poster="./image/wallpaper.png"></video> |
|||
<div class="call-user-box" v-if="displayBtn"> |
|||
<div class="call-user" v-if="contact"> |
|||
<img class="avatar" v-if="status!=2 || !is_video" :src="contact.avatar" alt=""> |
|||
<div class="text"> |
|||
<b v-if="!is_video && status==2">{{contact.displayName}}</b> |
|||
<span v-if="status!=2"> |
|||
<span v-if="status==3"> {{contact.displayName}} 正在请求与您{{is_video ? '视频' : '语音'}}通话</span> |
|||
<span v-else>您正对 <b>{{contact.displayName}}</b> 发起{{is_video ? '视频' : '语音'}}通话</span> |
|||
</span> |
|||
|
|||
</div> |
|||
</div> |
|||
<div class="call-time" v-if="callTime && status==2"> |
|||
{{setCallTime()}} |
|||
</div> |
|||
<div class="calling-button"> |
|||
<div class="button switch-btn" v-if="status<3" > |
|||
<img class="image-icon" :src="'./image/voice'+(voiceStatus ? '' : '-off')+'.png'" @click="switchVoice()"/> |
|||
<div class="text">{{ voiceStatus ? '关闭' : '开启'}}麦克风</div> |
|||
</div> |
|||
<div class="button" v-if="status!=0" > |
|||
<img class="image" src="./image/guaduan.png" @click="hangup(true)"/> |
|||
<div class="text">挂断</div> |
|||
</div> |
|||
<div class="button" v-if="status==3" > |
|||
<img class="image" src="./image/jieting.png" @click="answer()"/> |
|||
<div class="text">接听</div> |
|||
</div> |
|||
<div class="button switch-btn" v-if="status<3" > |
|||
<template v-if="is_video"> |
|||
<img class="image-icon" :src="'./image/video.png'" @click="exchangeVideo()"/> |
|||
<div class="text">切换摄像头</div> |
|||
</template> |
|||
<template v-else> |
|||
<img class="image-icon" :src="'./image/speaker'+(speaker ? '' : '-off')+'.png'" @click="speakBtn()"/> |
|||
<div class="text">{{ speaker ? '关闭' : '开启'}}扬声器</div> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
<script type="text/javascript" src='./js/vue.js'></script> |
|||
<script> |
|||
const params=parseUrl(window.location.href); |
|||
const opt=JSON.parse(decodeURIComponent(params.stun)); |
|||
const config = { |
|||
'iceServers': [{ |
|||
'urls': ['stun:stun.xten.com', 'stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302', |
|||
'stun:stun2.l.google.com:19302', 'stun:stun3.l.google.com:19302', 'stun:stun4.l.google.com:19302' |
|||
] |
|||
},{ |
|||
'urls': opt.stun ? opt.stun : ['stun:stun.callwithus.com'], // 自己搭建服务器地址 |
|||
"username":opt.stunUser ? opt.stunUser : '', |
|||
"credential":opt.stunPass ? opt.stunPass : '' |
|||
} |
|||
], |
|||
}; |
|||
const Counter = { |
|||
data() { |
|||
return { |
|||
displayBtn:true, |
|||
platform:params.platform, |
|||
status: 0, //状态0,默认,1:拨号中,2通话中,3来电中,4忙线 |
|||
pc: null, //pc实力化 |
|||
localVideo: "", //本地视频的DOM |
|||
remoteVideo: "", //远程视频的DOM |
|||
remoteStream: null, // 远端视频流 |
|||
localStream: null, // 本地视频流 |
|||
is_video: 0, //是否为视频通话 |
|||
videoStatus: true, //视频开启状态 |
|||
voiceStatus: true, //语音开启状态 |
|||
cutdown: 40, //拨号超时 |
|||
timer: null, //计时器 |
|||
offerParams:{}, |
|||
plus:null, |
|||
streamType:1, //视频通话展示方式 |
|||
facingMode:'user',//前置摄像头还是后置摄像头 user-前置 environment-后置 |
|||
headset : true, //麦克风 打开true 关闭false, |
|||
senders: null, // 数据流 |
|||
speaker:true, // 听筒 false 扬声器true |
|||
callTime:0, //通话时间 |
|||
callTimeDis:'', //通话时间展示 |
|||
timerIntervalId:null, //通话计时器 |
|||
contact:{ |
|||
id:params.target_id, |
|||
displayName:params.name, |
|||
avatar:params.avatar |
|||
} |
|||
}; |
|||
}, |
|||
mounted() { |
|||
this.pc = new RTCPeerConnection(config); |
|||
this.pc.ontrack = (event) => { |
|||
console.log(event,'接收视频流'); |
|||
if(this.localVideo){ |
|||
this.remoteStream = event.streams[0]; |
|||
setTimeout(()=>{ |
|||
this.streamType=2; |
|||
},50) |
|||
} |
|||
}; |
|||
|
|||
if (this.platform === 'app') { |
|||
document.addEventListener('plusready', () => { |
|||
console.log('设置扬声器') |
|||
this.plus = plus.audio.createPlayer(); |
|||
this.plus.setRoute(plus.audio.ROUTE_SPEAKER); |
|||
}); |
|||
} |
|||
|
|||
this.localVideo = this.$refs.localvideo; |
|||
this.remoteVideo = this.$refs.remotevideo; |
|||
|
|||
window.addEventListener('message', (e) => { |
|||
this.callMessagecallback(e) |
|||
}, false); |
|||
|
|||
window.getUniAppMessage = (arg) => { |
|||
const data = { |
|||
data: jsonly(arg) |
|||
} |
|||
this.callMessagecallback(data) |
|||
} |
|||
this.is_video = params.type==1 ? true : false; |
|||
|
|||
this.offerParams = this.is_video ? { |
|||
offerToRecieveAudio: 1, |
|||
offerToRecieveVideo: 1 |
|||
} : { |
|||
offerToRecieveAudio: 1, |
|||
offerToRecieveVideo: 0 |
|||
} |
|||
this.status=params.status |
|||
// 如果状态为1,表示拨打电话,并且calling状态为1的时候才是直接拨打; |
|||
if(this.status==1){ |
|||
if(params.calling==1){ |
|||
this.called(this.is_video) |
|||
} |
|||
}else{ |
|||
this.playMusicCall('state'); |
|||
} |
|||
}, |
|||
watch:{ |
|||
streamType(val){ |
|||
// 切换镜头位置 |
|||
if(val==1){ |
|||
this.localVideo.srcObject = this.localStream; |
|||
this.remoteVideo.srcObject = this.remoteStream; |
|||
this.localVideo.muted=true; |
|||
this.remoteVideo.muted=false; |
|||
}else{ |
|||
this.localVideo.srcObject = this.remoteStream; |
|||
this.remoteVideo.srcObject = this.localStream; |
|||
this.localVideo.muted=false; |
|||
this.remoteVideo.muted=true; |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
// 开始通话计时 |
|||
startTime() { |
|||
this.timerIntervalId=setInterval(()=>{ |
|||
this.callTime++ |
|||
},1000) |
|||
}, |
|||
// 设置通话时间 |
|||
setCallTime(){ |
|||
let time=this.callTime; |
|||
const hours = Math.floor(time / 3600); |
|||
const minutes = Math.floor((time - (hours * 3600)) / 60); |
|||
const seconds = time - (hours * 3600) - (minutes * 60); |
|||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
|||
}, |
|||
// 视频电话初始化本地视频 |
|||
initLocalStream(call_id, is_video) { |
|||
let video=false; |
|||
navigator.mediaDevices.enumerateDevices().then(devices => { |
|||
const videoDevices = devices.filter(device => device.kind === 'videoinput'); |
|||
if(videoDevices.length && is_video){ |
|||
video = { |
|||
width: window.screen.height, |
|||
height: window.screen.width |
|||
} |
|||
} |
|||
navigator.mediaDevices.getUserMedia({ |
|||
video: video, |
|||
audio: {echoCancellation: true} |
|||
}).then((stream) => { |
|||
this.localStream = stream; |
|||
// 同步音频 |
|||
stream.getTracks().forEach((track) => { |
|||
this.pc.addTrack(track, stream); |
|||
}); |
|||
this.localVideo.srcObject = this.localStream; |
|||
// 把自己的视频静音 |
|||
this.localVideo.muted = true; |
|||
if(call_id){ |
|||
this.postMsg({ |
|||
event:'calling', |
|||
status:3, |
|||
code:901 |
|||
}); |
|||
// 计时器,如果一段时间没有接听则自动挂断 |
|||
this.timer = setInterval(() => { |
|||
this.cutdown--; |
|||
if (this.cutdown == 0) { |
|||
this.hangup(true); |
|||
} |
|||
}, 1000) |
|||
}else{ |
|||
// 告诉对方已经接听电话 |
|||
this.postMsg({ event: 'acceptRtc',code:904}); |
|||
} |
|||
// 监听远程媒体流 |
|||
}).catch((e) => { |
|||
this.postMsg({ |
|||
event: 'mediaDevices', |
|||
}) |
|||
}); |
|||
}); |
|||
}, |
|||
// 拨打电话 |
|||
called(is_video) { |
|||
this.is_video = is_video; |
|||
this.initLocalStream(true, is_video); |
|||
this.playMusicCall('state'); |
|||
}, |
|||
// 接听电话 |
|||
answer() { |
|||
this.status = 2; |
|||
this.initLocalStream(false, this.is_video); |
|||
this.playMusicCall('close'); |
|||
this.startTime(); |
|||
}, |
|||
// 挂断电话 |
|||
hangup(btn) { |
|||
clearInterval(this.timer); |
|||
clearInterval(this.timerIntervalId); |
|||
if(this.status!=2){ |
|||
this.playMusicCall('close'); |
|||
} |
|||
if (this.status) { |
|||
this.closeLocalMedia(); //关闭本地媒体 |
|||
this.remoteStream=null; //关闭远程媒体 |
|||
} |
|||
// 通话取消 |
|||
let code=902; |
|||
// 通话中挂断 |
|||
if(this.status==2 ){ |
|||
code=906 |
|||
// 拒绝挂断 |
|||
}else if(this.status==3 ){ |
|||
code=903 |
|||
//对方忙线中 |
|||
}else if(this.status==4 ){ |
|||
code=907 |
|||
} |
|||
this.postMsg({ |
|||
event:'hangup', |
|||
isbtn:btn, |
|||
callTime:this.callTime, |
|||
code:code |
|||
}) |
|||
}, |
|||
// 关闭本地媒体 |
|||
closeLocalMedia() { |
|||
if (this.localStream && this.localStream.getTracks()) { |
|||
this.localStream.getTracks().forEach((track) => { |
|||
track.stop(); |
|||
}); |
|||
} |
|||
this.localStream = null; |
|||
}, |
|||
// 打开或关闭声音 |
|||
switchVoice() { |
|||
if (this.localStream == null) { |
|||
alert('请打开音视频'); |
|||
return false; |
|||
} |
|||
const tracks = this.localStream.getTracks(); |
|||
if (this.voiceStatus) { |
|||
tracks.forEach(track => { |
|||
if (track.kind === 'audio') { |
|||
track.enabled = false |
|||
} |
|||
}); |
|||
this.voiceStatus = false; |
|||
} else { |
|||
tracks.forEach(track => { |
|||
if (track.kind === 'audio') { |
|||
track.enabled = true |
|||
} |
|||
}); |
|||
this.voiceStatus = true; |
|||
} |
|||
}, |
|||
// 临时开、关视频 |
|||
switchVideo() { |
|||
if (this.localStream == null) { |
|||
alert('请打开音视频'); |
|||
return false; |
|||
} |
|||
const tracks = this.localStream.getTracks(); |
|||
if (this.videoStatus) { |
|||
tracks.forEach(track => { |
|||
if (track.kind === 'video') { |
|||
track.enabled = false |
|||
} |
|||
}); |
|||
this.videoStatus = false; |
|||
} else { |
|||
tracks.forEach(track => { |
|||
if (track.kind === 'video') { |
|||
track.enabled = true |
|||
} |
|||
}); |
|||
this.videoStatus = true; |
|||
} |
|||
}, |
|||
// 切换前后摄像头 |
|||
exchangeVideo() { |
|||
this.localStream.getTracks().forEach(track => track.stop()); |
|||
if (this.facingMode == 'user') this.facingMode = 'environment' |
|||
else this.facingMode = 'user' |
|||
navigator.mediaDevices.getUserMedia({ |
|||
video: { |
|||
width: window.screen.height, |
|||
height: window.screen.width, |
|||
facingMode: { |
|||
exact: this.facingMode |
|||
} |
|||
}, |
|||
audio: { |
|||
echoCancellation: true, |
|||
} |
|||
}).then((mediastream) => { |
|||
this.senders = this.pc.getSenders() |
|||
let videoTrack = mediastream.getVideoTracks()[0]; |
|||
let audioTrack = mediastream.getAudioTracks()[0]; |
|||
var sender = this.senders.find((s) => { |
|||
return s.track.kind == 'video'; |
|||
}); |
|||
var sender2 = this.senders.find((s) => { |
|||
return s.track.kind == 'audio'; |
|||
}); |
|||
sender.replaceTrack(videoTrack); |
|||
sender2.replaceTrack(audioTrack); |
|||
if (this.streamType === 2) this.remoteVideo.srcObject = mediastream; |
|||
else this.localVideo.srcObject = mediastream |
|||
this.localStream = mediastream |
|||
if(this.voiceStatus==false){ |
|||
this.voiceStatus=true; |
|||
this.switchVoice(); |
|||
} |
|||
if(this.speaker){ |
|||
this.speaker = !this.speaker |
|||
}else{ |
|||
this.speaker = !this.speaker |
|||
} |
|||
}) |
|||
}, |
|||
// 播放响铃 |
|||
playMusicCall(type) { |
|||
var audio = document.getElementById("music1"); |
|||
if(type=='close' && !audio.paused){ |
|||
audio.pause(); // 暂停 |
|||
return; |
|||
} |
|||
if (type === "state") { |
|||
audio.loop = true; |
|||
} else { |
|||
audio.loop = false; |
|||
} |
|||
|
|||
if (audio.paused) { |
|||
audio.play(); // 播放 |
|||
} else { |
|||
audio.pause(); // 暂停 |
|||
} |
|||
}, |
|||
// 向uniapp发送消息,页面通讯 |
|||
postMsg(data) { |
|||
if (this.platform === 'app') { |
|||
uni.postMessage({ |
|||
data: data |
|||
}) |
|||
} else { |
|||
window.parent.postMessage(data) |
|||
} |
|||
}, |
|||
// 接收websocket发送过来的消息,由uniapp接收后传输到当前页面 |
|||
callMessagecallback(msg){ |
|||
let e=msg.data; |
|||
switch (e.event) { |
|||
case "calling": |
|||
console.log('发起通话...'); |
|||
this.called(this.is_video); |
|||
break; |
|||
case "hangup": |
|||
this.hangup(false); |
|||
break; |
|||
case "busy": |
|||
this.status=4; |
|||
this.hangup(false); |
|||
break; |
|||
case "acceptRtc": //已经接听,创建offer并发送 |
|||
this.status = 2; |
|||
clearInterval(this.timer); |
|||
this.startTime(); |
|||
this.playMusicCall(); |
|||
this.createOffer() |
|||
break; |
|||
case "turndown": |
|||
break; |
|||
case "answer": |
|||
//同步answer信息... |
|||
this.pc.setRemoteDescription(new RTCSessionDescription({ |
|||
type: 'answer', |
|||
sdp: e.sdp |
|||
})); |
|||
break; |
|||
case "iceCandidate": |
|||
setTimeout(()=>{ |
|||
// 添加ice完成通话连接 |
|||
if (typeof(e.iceCandidate) === 'object') { |
|||
this.pc.addIceCandidate(new RTCIceCandidate(e.iceCandidate)); |
|||
} else { |
|||
this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(e.iceCandidate))); |
|||
} |
|||
},100) |
|||
break; |
|||
case "offer": |
|||
this.pc.setRemoteDescription(new RTCSessionDescription({ |
|||
type: 'offer', |
|||
sdp: e.sdp |
|||
})); |
|||
this.createAnswer(); |
|||
break; |
|||
|
|||
} |
|||
}, |
|||
// 创建offer-sdp |
|||
createOffer() { |
|||
this.pc.createOffer(this.offerParams).then((offer) => { |
|||
this.pc.setLocalDescription(offer); |
|||
this.postMsg({ |
|||
event: 'offer', |
|||
sdp: offer.sdp |
|||
}, '*'); |
|||
}); |
|||
// 创建offer需要监听ice流 |
|||
this.onicecandidate(); |
|||
}, |
|||
// 创建应答sdp |
|||
createAnswer() { |
|||
this.pc.createAnswer(this.offerParams).then((answer) => { |
|||
this.pc.setLocalDescription(answer); |
|||
this.postMsg({ |
|||
event: 'answer', |
|||
sdp: answer.sdp |
|||
}, '*'); |
|||
this.onicecandidate(); |
|||
}); |
|||
}, |
|||
onicecandidate(){ |
|||
this.pc.onicecandidate = (event) => { |
|||
var iceCandidate = event.candidate; |
|||
if (iceCandidate) { |
|||
this.postMsg({ |
|||
event: 'iceCandidate', |
|||
iceCandidate: JSON.parse(JSON.stringify(iceCandidate)) |
|||
}, '*'); |
|||
} |
|||
}; |
|||
}, |
|||
//切换视频显示位置 |
|||
changeVideo(){ |
|||
this.streamType==1 ? this.streamType=2 : this.streamType=1; |
|||
}, |
|||
//打开关闭扬声器 h5端就是静音 ROUTE_EARPIECE 听筒 ROUTE_SPEAKER 扬声器 |
|||
speakBtn() { |
|||
let isMuted=false; |
|||
let speaker=plus.audio.ROUTE_SPEAKER; |
|||
this.localVideo.muted = true; |
|||
if (this.speaker) { //扬声器 => 听筒 |
|||
isMuted=true; |
|||
speaker=plus.audio.ROUTE_EARPIECE |
|||
} |
|||
if (this.platform === 'h5') { |
|||
this.localVideo.muted=isMuted; |
|||
} |
|||
if (this.platform === 'app') { |
|||
this.plus.setRoute(speaker); |
|||
this.localVideo.muted = false; |
|||
} |
|||
this.speaker = !this.speaker |
|||
} |
|||
} |
|||
} |
|||
|
|||
const app = Vue.createApp(Counter); |
|||
app.mount('#app'); |
|||
</script> |
|||
</html> |
|||
@ -0,0 +1,275 @@ |
|||
var at, // The index of the current character
|
|||
ch, // The current character
|
|||
escapee = { |
|||
'"': '"', |
|||
'\\': '\\', |
|||
'/': '/', |
|||
b: '\b', |
|||
f: '\f', |
|||
n: '\n', |
|||
r: '\r', |
|||
t: '\t' |
|||
}, |
|||
text, |
|||
|
|||
error = function(m) { |
|||
// Call error when something is wrong.
|
|||
throw { |
|||
name: 'SyntaxError', |
|||
message: m, |
|||
at: at, |
|||
text: text |
|||
}; |
|||
}, |
|||
|
|||
next = function(c) { |
|||
// If a c parameter is provided, verify that it matches the current character.
|
|||
if (c && c !== ch) { |
|||
error("Expected '" + c + "' instead of '" + ch + "'"); |
|||
} |
|||
|
|||
// Get the next character. When there are no more characters,
|
|||
// return the empty string.
|
|||
|
|||
ch = text.charAt(at); |
|||
at += 1; |
|||
return ch; |
|||
}, |
|||
|
|||
number = function() { |
|||
// Parse a number value.
|
|||
var number, |
|||
string = ''; |
|||
|
|||
if (ch === '-') { |
|||
string = '-'; |
|||
next('-'); |
|||
} |
|||
while (ch >= '0' && ch <= '9') { |
|||
string += ch; |
|||
next(); |
|||
} |
|||
if (ch === '.') { |
|||
string += '.'; |
|||
while (next() && ch >= '0' && ch <= '9') { |
|||
string += ch; |
|||
} |
|||
} |
|||
if (ch === 'e' || ch === 'E') { |
|||
string += ch; |
|||
next(); |
|||
if (ch === '-' || ch === '+') { |
|||
string += ch; |
|||
next(); |
|||
} |
|||
while (ch >= '0' && ch <= '9') { |
|||
string += ch; |
|||
next(); |
|||
} |
|||
} |
|||
number = +string; |
|||
if (!isFinite(number)) { |
|||
error("Bad number"); |
|||
} else { |
|||
return number; |
|||
} |
|||
}, |
|||
|
|||
string = function() { |
|||
// Parse a string value.
|
|||
var hex, |
|||
i, |
|||
string = '', |
|||
uffff; |
|||
|
|||
// When parsing for string values, we must look for " and \ characters.
|
|||
if (ch === '"') { |
|||
while (next()) { |
|||
if (ch === '"') { |
|||
next(); |
|||
return string; |
|||
} else if (ch === '\\') { |
|||
next(); |
|||
if (ch === 'u') { |
|||
uffff = 0; |
|||
for (i = 0; i < 4; i += 1) { |
|||
hex = parseInt(next(), 16); |
|||
if (!isFinite(hex)) { |
|||
break; |
|||
} |
|||
uffff = uffff * 16 + hex; |
|||
} |
|||
string += String.fromCharCode(uffff); |
|||
} else if (typeof escapee[ch] === 'string') { |
|||
string += escapee[ch]; |
|||
} else { |
|||
break; |
|||
} |
|||
} else { |
|||
string += ch; |
|||
} |
|||
} |
|||
} |
|||
error("Bad string"); |
|||
}, |
|||
|
|||
white = function() { |
|||
|
|||
// Skip whitespace.
|
|||
|
|||
while (ch && ch <= ' ') { |
|||
next(); |
|||
} |
|||
}, |
|||
|
|||
word = function() { |
|||
|
|||
// true, false, or null.
|
|||
|
|||
switch (ch) { |
|||
case 't': |
|||
next('t'); |
|||
next('r'); |
|||
next('u'); |
|||
next('e'); |
|||
return true; |
|||
case 'f': |
|||
next('f'); |
|||
next('a'); |
|||
next('l'); |
|||
next('s'); |
|||
next('e'); |
|||
return false; |
|||
case 'n': |
|||
next('n'); |
|||
next('u'); |
|||
next('l'); |
|||
next('l'); |
|||
return null; |
|||
} |
|||
error("Unexpected '" + ch + "'"); |
|||
}, |
|||
|
|||
value, // Place holder for the value function.
|
|||
|
|||
array = function() { |
|||
|
|||
// Parse an array value.
|
|||
|
|||
var array = []; |
|||
|
|||
if (ch === '[') { |
|||
next('['); |
|||
white(); |
|||
if (ch === ']') { |
|||
next(']'); |
|||
return array; // empty array
|
|||
} |
|||
while (ch) { |
|||
array.push(value()); |
|||
white(); |
|||
if (ch === ']') { |
|||
next(']'); |
|||
return array; |
|||
} |
|||
next(','); |
|||
white(); |
|||
} |
|||
} |
|||
error("Bad array"); |
|||
}, |
|||
|
|||
object = function() { |
|||
|
|||
// Parse an object value.
|
|||
|
|||
var key, |
|||
object = {}; |
|||
|
|||
if (ch === '{') { |
|||
next('{'); |
|||
white(); |
|||
if (ch === '}') { |
|||
next('}'); |
|||
return object; // empty object
|
|||
} |
|||
while (ch) { |
|||
key = string(); |
|||
white(); |
|||
next(':'); |
|||
if (Object.hasOwnProperty.call(object, key)) { |
|||
error('Duplicate key "' + key + '"'); |
|||
} |
|||
object[key] = value(); |
|||
white(); |
|||
if (ch === '}') { |
|||
next('}'); |
|||
return object; |
|||
} |
|||
next(','); |
|||
white(); |
|||
} |
|||
} |
|||
error("Bad object"); |
|||
}; |
|||
|
|||
value = function() { |
|||
|
|||
// Parse a JSON value. It could be an object, an array, a string, a number,
|
|||
// or a word.
|
|||
|
|||
white(); |
|||
switch (ch) { |
|||
case '{': |
|||
return object(); |
|||
case '[': |
|||
return array(); |
|||
case '"': |
|||
return string(); |
|||
case '-': |
|||
return number(); |
|||
default: |
|||
return ch >= '0' && ch <= '9' ? number() : word(); |
|||
} |
|||
}; |
|||
|
|||
// Return the json_parse function. It will have access to all of the above
|
|||
// functions and variables.
|
|||
|
|||
const jsonly = function(source, reviver) { |
|||
var result; |
|||
|
|||
text = source; |
|||
at = 0; |
|||
ch = ' '; |
|||
result = value(); |
|||
white(); |
|||
if (ch) { |
|||
error("Syntax error"); |
|||
} |
|||
|
|||
// If there is a reviver function, we recursively walk the new structure,
|
|||
// passing each name/value pair to the reviver function for possible
|
|||
// transformation, starting with a temporary root object that holds the result
|
|||
// in an empty key. If there is not a reviver function, we simply return the
|
|||
// result.
|
|||
|
|||
return typeof reviver === 'function' ? (function walk(holder, key) { |
|||
var k, v, value = holder[key]; |
|||
if (value && typeof value === 'object') { |
|||
for (k in value) { |
|||
if (Object.prototype.hasOwnProperty.call(value, k)) { |
|||
v = walk(value, k); |
|||
if (v !== undefined) { |
|||
value[k] = v; |
|||
} else { |
|||
delete value[k]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return reviver.call(holder, key, value); |
|||
}({ |
|||
'': result |
|||
}, '')) : result; |
|||
}; |
|||
@ -0,0 +1,11 @@ |
|||
//序列化url,将url链接后面的get参数序列化成json对象
|
|||
function parseUrl(url){ |
|||
var param=url.substring(url.indexOf("?")+1); |
|||
var paramArr=param.split("&"); |
|||
var urlArr={}; |
|||
for (let i = 0; i < paramArr.length; i++) { |
|||
urlArr[paramArr[i].split("=")[0]] = decodeURI(paramArr[i].split("=")[1]); |
|||
// 将数组元素中'='左边的内容作为对象的属性名,'='右边的内容作为对象对应属性的属性值
|
|||
} |
|||
return urlArr; |
|||
} |
|||
@ -0,0 +1,702 @@ |
|||
/* |
|||
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
import * as utils from '../utils.js'; |
|||
|
|||
export {shimGetUserMedia} from './getusermedia'; |
|||
export {shimGetDisplayMedia} from './getdisplaymedia'; |
|||
|
|||
export function shimMediaStream(window) { |
|||
window.MediaStream = window.MediaStream || window.webkitMediaStream; |
|||
} |
|||
|
|||
export function shimOnTrack(window) { |
|||
if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in |
|||
window.RTCPeerConnection.prototype)) { |
|||
Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { |
|||
get() { |
|||
return this._ontrack; |
|||
}, |
|||
set(f) { |
|||
if (this._ontrack) { |
|||
this.removeEventListener('track', this._ontrack); |
|||
} |
|||
this.addEventListener('track', this._ontrack = f); |
|||
}, |
|||
enumerable: true, |
|||
configurable: true |
|||
}); |
|||
const origSetRemoteDescription = |
|||
window.RTCPeerConnection.prototype.setRemoteDescription; |
|||
window.RTCPeerConnection.prototype.setRemoteDescription = |
|||
function setRemoteDescription() { |
|||
if (!this._ontrackpoly) { |
|||
this._ontrackpoly = (e) => { |
|||
// onaddstream does not fire when a track is added to an existing
|
|||
// stream. But stream.onaddtrack is implemented so we use that.
|
|||
e.stream.addEventListener('addtrack', te => { |
|||
let receiver; |
|||
if (window.RTCPeerConnection.prototype.getReceivers) { |
|||
receiver = this.getReceivers() |
|||
.find(r => r.track && r.track.id === te.track.id); |
|||
} else { |
|||
receiver = {track: te.track}; |
|||
} |
|||
|
|||
const event = new Event('track'); |
|||
event.track = te.track; |
|||
event.receiver = receiver; |
|||
event.transceiver = {receiver}; |
|||
event.streams = [e.stream]; |
|||
this.dispatchEvent(event); |
|||
}); |
|||
e.stream.getTracks().forEach(track => { |
|||
let receiver; |
|||
if (window.RTCPeerConnection.prototype.getReceivers) { |
|||
receiver = this.getReceivers() |
|||
.find(r => r.track && r.track.id === track.id); |
|||
} else { |
|||
receiver = {track}; |
|||
} |
|||
const event = new Event('track'); |
|||
event.track = track; |
|||
event.receiver = receiver; |
|||
event.transceiver = {receiver}; |
|||
event.streams = [e.stream]; |
|||
this.dispatchEvent(event); |
|||
}); |
|||
}; |
|||
this.addEventListener('addstream', this._ontrackpoly); |
|||
} |
|||
return origSetRemoteDescription.apply(this, arguments); |
|||
}; |
|||
} else { |
|||
// even if RTCRtpTransceiver is in window, it is only used and
|
|||
// emitted in unified-plan. Unfortunately this means we need
|
|||
// to unconditionally wrap the event.
|
|||
utils.wrapPeerConnectionEvent(window, 'track', e => { |
|||
if (!e.transceiver) { |
|||
Object.defineProperty(e, 'transceiver', |
|||
{value: {receiver: e.receiver}}); |
|||
} |
|||
return e; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export function shimGetSendersWithDtmf(window) { |
|||
// Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
|
|||
if (typeof window === 'object' && window.RTCPeerConnection && |
|||
!('getSenders' in window.RTCPeerConnection.prototype) && |
|||
'createDTMFSender' in window.RTCPeerConnection.prototype) { |
|||
const shimSenderWithDtmf = function(pc, track) { |
|||
return { |
|||
track, |
|||
get dtmf() { |
|||
if (this._dtmf === undefined) { |
|||
if (track.kind === 'audio') { |
|||
this._dtmf = pc.createDTMFSender(track); |
|||
} else { |
|||
this._dtmf = null; |
|||
} |
|||
} |
|||
return this._dtmf; |
|||
}, |
|||
_pc: pc |
|||
}; |
|||
}; |
|||
|
|||
// augment addTrack when getSenders is not available.
|
|||
if (!window.RTCPeerConnection.prototype.getSenders) { |
|||
window.RTCPeerConnection.prototype.getSenders = function getSenders() { |
|||
this._senders = this._senders || []; |
|||
return this._senders.slice(); // return a copy of the internal state.
|
|||
}; |
|||
const origAddTrack = window.RTCPeerConnection.prototype.addTrack; |
|||
window.RTCPeerConnection.prototype.addTrack = |
|||
function addTrack(track, stream) { |
|||
let sender = origAddTrack.apply(this, arguments); |
|||
if (!sender) { |
|||
sender = shimSenderWithDtmf(this, track); |
|||
this._senders.push(sender); |
|||
} |
|||
return sender; |
|||
}; |
|||
|
|||
const origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; |
|||
window.RTCPeerConnection.prototype.removeTrack = |
|||
function removeTrack(sender) { |
|||
origRemoveTrack.apply(this, arguments); |
|||
const idx = this._senders.indexOf(sender); |
|||
if (idx !== -1) { |
|||
this._senders.splice(idx, 1); |
|||
} |
|||
}; |
|||
} |
|||
const origAddStream = window.RTCPeerConnection.prototype.addStream; |
|||
window.RTCPeerConnection.prototype.addStream = function addStream(stream) { |
|||
this._senders = this._senders || []; |
|||
origAddStream.apply(this, [stream]); |
|||
stream.getTracks().forEach(track => { |
|||
this._senders.push(shimSenderWithDtmf(this, track)); |
|||
}); |
|||
}; |
|||
|
|||
const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; |
|||
window.RTCPeerConnection.prototype.removeStream = |
|||
function removeStream(stream) { |
|||
this._senders = this._senders || []; |
|||
origRemoveStream.apply(this, [stream]); |
|||
|
|||
stream.getTracks().forEach(track => { |
|||
const sender = this._senders.find(s => s.track === track); |
|||
if (sender) { // remove sender
|
|||
this._senders.splice(this._senders.indexOf(sender), 1); |
|||
} |
|||
}); |
|||
}; |
|||
} else if (typeof window === 'object' && window.RTCPeerConnection && |
|||
'getSenders' in window.RTCPeerConnection.prototype && |
|||
'createDTMFSender' in window.RTCPeerConnection.prototype && |
|||
window.RTCRtpSender && |
|||
!('dtmf' in window.RTCRtpSender.prototype)) { |
|||
const origGetSenders = window.RTCPeerConnection.prototype.getSenders; |
|||
window.RTCPeerConnection.prototype.getSenders = function getSenders() { |
|||
const senders = origGetSenders.apply(this, []); |
|||
senders.forEach(sender => sender._pc = this); |
|||
return senders; |
|||
}; |
|||
|
|||
Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { |
|||
get() { |
|||
if (this._dtmf === undefined) { |
|||
if (this.track.kind === 'audio') { |
|||
this._dtmf = this._pc.createDTMFSender(this.track); |
|||
} else { |
|||
this._dtmf = null; |
|||
} |
|||
} |
|||
return this._dtmf; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export function shimGetStats(window) { |
|||
if (!window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
|
|||
const origGetStats = window.RTCPeerConnection.prototype.getStats; |
|||
window.RTCPeerConnection.prototype.getStats = function getStats() { |
|||
const [selector, onSucc, onErr] = arguments; |
|||
|
|||
// If selector is a function then we are in the old style stats so just
|
|||
// pass back the original getStats format to avoid breaking old users.
|
|||
if (arguments.length > 0 && typeof selector === 'function') { |
|||
return origGetStats.apply(this, arguments); |
|||
} |
|||
|
|||
// When spec-style getStats is supported, return those when called with
|
|||
// either no arguments or the selector argument is null.
|
|||
if (origGetStats.length === 0 && (arguments.length === 0 || |
|||
typeof selector !== 'function')) { |
|||
return origGetStats.apply(this, []); |
|||
} |
|||
|
|||
const fixChromeStats_ = function(response) { |
|||
const standardReport = {}; |
|||
const reports = response.result(); |
|||
reports.forEach(report => { |
|||
const standardStats = { |
|||
id: report.id, |
|||
timestamp: report.timestamp, |
|||
type: { |
|||
localcandidate: 'local-candidate', |
|||
remotecandidate: 'remote-candidate' |
|||
}[report.type] || report.type |
|||
}; |
|||
report.names().forEach(name => { |
|||
standardStats[name] = report.stat(name); |
|||
}); |
|||
standardReport[standardStats.id] = standardStats; |
|||
}); |
|||
|
|||
return standardReport; |
|||
}; |
|||
|
|||
// shim getStats with maplike support
|
|||
const makeMapStats = function(stats) { |
|||
return new Map(Object.keys(stats).map(key => [key, stats[key]])); |
|||
}; |
|||
|
|||
if (arguments.length >= 2) { |
|||
const successCallbackWrapper_ = function(response) { |
|||
onSucc(makeMapStats(fixChromeStats_(response))); |
|||
}; |
|||
|
|||
return origGetStats.apply(this, [successCallbackWrapper_, |
|||
selector]); |
|||
} |
|||
|
|||
// promise-support
|
|||
return new Promise((resolve, reject) => { |
|||
origGetStats.apply(this, [ |
|||
function(response) { |
|||
resolve(makeMapStats(fixChromeStats_(response))); |
|||
}, reject]); |
|||
}).then(onSucc, onErr); |
|||
}; |
|||
} |
|||
|
|||
export function shimSenderReceiverGetStats(window) { |
|||
if (!(typeof window === 'object' && window.RTCPeerConnection && |
|||
window.RTCRtpSender && window.RTCRtpReceiver)) { |
|||
return; |
|||
} |
|||
|
|||
// shim sender stats.
|
|||
if (!('getStats' in window.RTCRtpSender.prototype)) { |
|||
const origGetSenders = window.RTCPeerConnection.prototype.getSenders; |
|||
if (origGetSenders) { |
|||
window.RTCPeerConnection.prototype.getSenders = function getSenders() { |
|||
const senders = origGetSenders.apply(this, []); |
|||
senders.forEach(sender => sender._pc = this); |
|||
return senders; |
|||
}; |
|||
} |
|||
|
|||
const origAddTrack = window.RTCPeerConnection.prototype.addTrack; |
|||
if (origAddTrack) { |
|||
window.RTCPeerConnection.prototype.addTrack = function addTrack() { |
|||
const sender = origAddTrack.apply(this, arguments); |
|||
sender._pc = this; |
|||
return sender; |
|||
}; |
|||
} |
|||
window.RTCRtpSender.prototype.getStats = function getStats() { |
|||
const sender = this; |
|||
return this._pc.getStats().then(result => |
|||
/* Note: this will include stats of all senders that |
|||
* send a track with the same id as sender.track as |
|||
* it is not possible to identify the RTCRtpSender. |
|||
*/ |
|||
utils.filterStats(result, sender.track, true)); |
|||
}; |
|||
} |
|||
|
|||
// shim receiver stats.
|
|||
if (!('getStats' in window.RTCRtpReceiver.prototype)) { |
|||
const origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; |
|||
if (origGetReceivers) { |
|||
window.RTCPeerConnection.prototype.getReceivers = |
|||
function getReceivers() { |
|||
const receivers = origGetReceivers.apply(this, []); |
|||
receivers.forEach(receiver => receiver._pc = this); |
|||
return receivers; |
|||
}; |
|||
} |
|||
utils.wrapPeerConnectionEvent(window, 'track', e => { |
|||
e.receiver._pc = e.srcElement; |
|||
return e; |
|||
}); |
|||
window.RTCRtpReceiver.prototype.getStats = function getStats() { |
|||
const receiver = this; |
|||
return this._pc.getStats().then(result => |
|||
utils.filterStats(result, receiver.track, false)); |
|||
}; |
|||
} |
|||
|
|||
if (!('getStats' in window.RTCRtpSender.prototype && |
|||
'getStats' in window.RTCRtpReceiver.prototype)) { |
|||
return; |
|||
} |
|||
|
|||
// shim RTCPeerConnection.getStats(track).
|
|||
const origGetStats = window.RTCPeerConnection.prototype.getStats; |
|||
window.RTCPeerConnection.prototype.getStats = function getStats() { |
|||
if (arguments.length > 0 && |
|||
arguments[0] instanceof window.MediaStreamTrack) { |
|||
const track = arguments[0]; |
|||
let sender; |
|||
let receiver; |
|||
let err; |
|||
this.getSenders().forEach(s => { |
|||
if (s.track === track) { |
|||
if (sender) { |
|||
err = true; |
|||
} else { |
|||
sender = s; |
|||
} |
|||
} |
|||
}); |
|||
this.getReceivers().forEach(r => { |
|||
if (r.track === track) { |
|||
if (receiver) { |
|||
err = true; |
|||
} else { |
|||
receiver = r; |
|||
} |
|||
} |
|||
return r.track === track; |
|||
}); |
|||
if (err || (sender && receiver)) { |
|||
return Promise.reject(new DOMException( |
|||
'There are more than one sender or receiver for the track.', |
|||
'InvalidAccessError')); |
|||
} else if (sender) { |
|||
return sender.getStats(); |
|||
} else if (receiver) { |
|||
return receiver.getStats(); |
|||
} |
|||
return Promise.reject(new DOMException( |
|||
'There is no sender or receiver for the track.', |
|||
'InvalidAccessError')); |
|||
} |
|||
return origGetStats.apply(this, arguments); |
|||
}; |
|||
} |
|||
|
|||
export function shimAddTrackRemoveTrackWithNative(window) { |
|||
// shim addTrack/removeTrack with native variants in order to make
|
|||
// the interactions with legacy getLocalStreams behave as in other browsers.
|
|||
// Keeps a mapping stream.id => [stream, rtpsenders...]
|
|||
window.RTCPeerConnection.prototype.getLocalStreams = |
|||
function getLocalStreams() { |
|||
this._shimmedLocalStreams = this._shimmedLocalStreams || {}; |
|||
return Object.keys(this._shimmedLocalStreams) |
|||
.map(streamId => this._shimmedLocalStreams[streamId][0]); |
|||
}; |
|||
|
|||
const origAddTrack = window.RTCPeerConnection.prototype.addTrack; |
|||
window.RTCPeerConnection.prototype.addTrack = |
|||
function addTrack(track, stream) { |
|||
if (!stream) { |
|||
return origAddTrack.apply(this, arguments); |
|||
} |
|||
this._shimmedLocalStreams = this._shimmedLocalStreams || {}; |
|||
|
|||
const sender = origAddTrack.apply(this, arguments); |
|||
if (!this._shimmedLocalStreams[stream.id]) { |
|||
this._shimmedLocalStreams[stream.id] = [stream, sender]; |
|||
} else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { |
|||
this._shimmedLocalStreams[stream.id].push(sender); |
|||
} |
|||
return sender; |
|||
}; |
|||
|
|||
const origAddStream = window.RTCPeerConnection.prototype.addStream; |
|||
window.RTCPeerConnection.prototype.addStream = function addStream(stream) { |
|||
this._shimmedLocalStreams = this._shimmedLocalStreams || {}; |
|||
|
|||
stream.getTracks().forEach(track => { |
|||
const alreadyExists = this.getSenders().find(s => s.track === track); |
|||
if (alreadyExists) { |
|||
throw new DOMException('Track already exists.', |
|||
'InvalidAccessError'); |
|||
} |
|||
}); |
|||
const existingSenders = this.getSenders(); |
|||
origAddStream.apply(this, arguments); |
|||
const newSenders = this.getSenders() |
|||
.filter(newSender => existingSenders.indexOf(newSender) === -1); |
|||
this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); |
|||
}; |
|||
|
|||
const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; |
|||
window.RTCPeerConnection.prototype.removeStream = |
|||
function removeStream(stream) { |
|||
this._shimmedLocalStreams = this._shimmedLocalStreams || {}; |
|||
delete this._shimmedLocalStreams[stream.id]; |
|||
return origRemoveStream.apply(this, arguments); |
|||
}; |
|||
|
|||
const origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; |
|||
window.RTCPeerConnection.prototype.removeTrack = |
|||
function removeTrack(sender) { |
|||
this._shimmedLocalStreams = this._shimmedLocalStreams || {}; |
|||
if (sender) { |
|||
Object.keys(this._shimmedLocalStreams).forEach(streamId => { |
|||
const idx = this._shimmedLocalStreams[streamId].indexOf(sender); |
|||
if (idx !== -1) { |
|||
this._shimmedLocalStreams[streamId].splice(idx, 1); |
|||
} |
|||
if (this._shimmedLocalStreams[streamId].length === 1) { |
|||
delete this._shimmedLocalStreams[streamId]; |
|||
} |
|||
}); |
|||
} |
|||
return origRemoveTrack.apply(this, arguments); |
|||
}; |
|||
} |
|||
|
|||
export function shimAddTrackRemoveTrack(window, browserDetails) { |
|||
if (!window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
// shim addTrack and removeTrack.
|
|||
if (window.RTCPeerConnection.prototype.addTrack && |
|||
browserDetails.version >= 65) { |
|||
return shimAddTrackRemoveTrackWithNative(window); |
|||
} |
|||
|
|||
// also shim pc.getLocalStreams when addTrack is shimmed
|
|||
// to return the original streams.
|
|||
const origGetLocalStreams = window.RTCPeerConnection.prototype |
|||
.getLocalStreams; |
|||
window.RTCPeerConnection.prototype.getLocalStreams = |
|||
function getLocalStreams() { |
|||
const nativeStreams = origGetLocalStreams.apply(this); |
|||
this._reverseStreams = this._reverseStreams || {}; |
|||
return nativeStreams.map(stream => this._reverseStreams[stream.id]); |
|||
}; |
|||
|
|||
const origAddStream = window.RTCPeerConnection.prototype.addStream; |
|||
window.RTCPeerConnection.prototype.addStream = function addStream(stream) { |
|||
this._streams = this._streams || {}; |
|||
this._reverseStreams = this._reverseStreams || {}; |
|||
|
|||
stream.getTracks().forEach(track => { |
|||
const alreadyExists = this.getSenders().find(s => s.track === track); |
|||
if (alreadyExists) { |
|||
throw new DOMException('Track already exists.', |
|||
'InvalidAccessError'); |
|||
} |
|||
}); |
|||
// Add identity mapping for consistency with addTrack.
|
|||
// Unless this is being used with a stream from addTrack.
|
|||
if (!this._reverseStreams[stream.id]) { |
|||
const newStream = new window.MediaStream(stream.getTracks()); |
|||
this._streams[stream.id] = newStream; |
|||
this._reverseStreams[newStream.id] = stream; |
|||
stream = newStream; |
|||
} |
|||
origAddStream.apply(this, [stream]); |
|||
}; |
|||
|
|||
const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; |
|||
window.RTCPeerConnection.prototype.removeStream = |
|||
function removeStream(stream) { |
|||
this._streams = this._streams || {}; |
|||
this._reverseStreams = this._reverseStreams || {}; |
|||
|
|||
origRemoveStream.apply(this, [(this._streams[stream.id] || stream)]); |
|||
delete this._reverseStreams[(this._streams[stream.id] ? |
|||
this._streams[stream.id].id : stream.id)]; |
|||
delete this._streams[stream.id]; |
|||
}; |
|||
|
|||
window.RTCPeerConnection.prototype.addTrack = |
|||
function addTrack(track, stream) { |
|||
if (this.signalingState === 'closed') { |
|||
throw new DOMException( |
|||
'The RTCPeerConnection\'s signalingState is \'closed\'.', |
|||
'InvalidStateError'); |
|||
} |
|||
const streams = [].slice.call(arguments, 1); |
|||
if (streams.length !== 1 || |
|||
!streams[0].getTracks().find(t => t === track)) { |
|||
// this is not fully correct but all we can manage without
|
|||
// [[associated MediaStreams]] internal slot.
|
|||
throw new DOMException( |
|||
'The adapter.js addTrack polyfill only supports a single ' + |
|||
' stream which is associated with the specified track.', |
|||
'NotSupportedError'); |
|||
} |
|||
|
|||
const alreadyExists = this.getSenders().find(s => s.track === track); |
|||
if (alreadyExists) { |
|||
throw new DOMException('Track already exists.', |
|||
'InvalidAccessError'); |
|||
} |
|||
|
|||
this._streams = this._streams || {}; |
|||
this._reverseStreams = this._reverseStreams || {}; |
|||
const oldStream = this._streams[stream.id]; |
|||
if (oldStream) { |
|||
// this is using odd Chrome behaviour, use with caution:
|
|||
// https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
|
|||
// Note: we rely on the high-level addTrack/dtmf shim to
|
|||
// create the sender with a dtmf sender.
|
|||
oldStream.addTrack(track); |
|||
|
|||
// Trigger ONN async.
|
|||
Promise.resolve().then(() => { |
|||
this.dispatchEvent(new Event('negotiationneeded')); |
|||
}); |
|||
} else { |
|||
const newStream = new window.MediaStream([track]); |
|||
this._streams[stream.id] = newStream; |
|||
this._reverseStreams[newStream.id] = stream; |
|||
this.addStream(newStream); |
|||
} |
|||
return this.getSenders().find(s => s.track === track); |
|||
}; |
|||
|
|||
// replace the internal stream id with the external one and
|
|||
// vice versa.
|
|||
function replaceInternalStreamId(pc, description) { |
|||
let sdp = description.sdp; |
|||
Object.keys(pc._reverseStreams || []).forEach(internalId => { |
|||
const externalStream = pc._reverseStreams[internalId]; |
|||
const internalStream = pc._streams[externalStream.id]; |
|||
sdp = sdp.replace(new RegExp(internalStream.id, 'g'), |
|||
externalStream.id); |
|||
}); |
|||
return new RTCSessionDescription({ |
|||
type: description.type, |
|||
sdp |
|||
}); |
|||
} |
|||
function replaceExternalStreamId(pc, description) { |
|||
let sdp = description.sdp; |
|||
Object.keys(pc._reverseStreams || []).forEach(internalId => { |
|||
const externalStream = pc._reverseStreams[internalId]; |
|||
const internalStream = pc._streams[externalStream.id]; |
|||
sdp = sdp.replace(new RegExp(externalStream.id, 'g'), |
|||
internalStream.id); |
|||
}); |
|||
return new RTCSessionDescription({ |
|||
type: description.type, |
|||
sdp |
|||
}); |
|||
} |
|||
['createOffer', 'createAnswer'].forEach(function(method) { |
|||
const nativeMethod = window.RTCPeerConnection.prototype[method]; |
|||
const methodObj = {[method]() { |
|||
const args = arguments; |
|||
const isLegacyCall = arguments.length && |
|||
typeof arguments[0] === 'function'; |
|||
if (isLegacyCall) { |
|||
return nativeMethod.apply(this, [ |
|||
(description) => { |
|||
const desc = replaceInternalStreamId(this, description); |
|||
args[0].apply(null, [desc]); |
|||
}, |
|||
(err) => { |
|||
if (args[1]) { |
|||
args[1].apply(null, err); |
|||
} |
|||
}, arguments[2] |
|||
]); |
|||
} |
|||
return nativeMethod.apply(this, arguments) |
|||
.then(description => replaceInternalStreamId(this, description)); |
|||
}}; |
|||
window.RTCPeerConnection.prototype[method] = methodObj[method]; |
|||
}); |
|||
|
|||
const origSetLocalDescription = |
|||
window.RTCPeerConnection.prototype.setLocalDescription; |
|||
window.RTCPeerConnection.prototype.setLocalDescription = |
|||
function setLocalDescription() { |
|||
if (!arguments.length || !arguments[0].type) { |
|||
return origSetLocalDescription.apply(this, arguments); |
|||
} |
|||
arguments[0] = replaceExternalStreamId(this, arguments[0]); |
|||
return origSetLocalDescription.apply(this, arguments); |
|||
}; |
|||
|
|||
// TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier
|
|||
|
|||
const origLocalDescription = Object.getOwnPropertyDescriptor( |
|||
window.RTCPeerConnection.prototype, 'localDescription'); |
|||
Object.defineProperty(window.RTCPeerConnection.prototype, |
|||
'localDescription', { |
|||
get() { |
|||
const description = origLocalDescription.get.apply(this); |
|||
if (description.type === '') { |
|||
return description; |
|||
} |
|||
return replaceInternalStreamId(this, description); |
|||
} |
|||
}); |
|||
|
|||
window.RTCPeerConnection.prototype.removeTrack = |
|||
function removeTrack(sender) { |
|||
if (this.signalingState === 'closed') { |
|||
throw new DOMException( |
|||
'The RTCPeerConnection\'s signalingState is \'closed\'.', |
|||
'InvalidStateError'); |
|||
} |
|||
// We can not yet check for sender instanceof RTCRtpSender
|
|||
// since we shim RTPSender. So we check if sender._pc is set.
|
|||
if (!sender._pc) { |
|||
throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + |
|||
'does not implement interface RTCRtpSender.', 'TypeError'); |
|||
} |
|||
const isLocal = sender._pc === this; |
|||
if (!isLocal) { |
|||
throw new DOMException('Sender was not created by this connection.', |
|||
'InvalidAccessError'); |
|||
} |
|||
|
|||
// Search for the native stream the senders track belongs to.
|
|||
this._streams = this._streams || {}; |
|||
let stream; |
|||
Object.keys(this._streams).forEach(streamid => { |
|||
const hasTrack = this._streams[streamid].getTracks() |
|||
.find(track => sender.track === track); |
|||
if (hasTrack) { |
|||
stream = this._streams[streamid]; |
|||
} |
|||
}); |
|||
|
|||
if (stream) { |
|||
if (stream.getTracks().length === 1) { |
|||
// if this is the last track of the stream, remove the stream. This
|
|||
// takes care of any shimmed _senders.
|
|||
this.removeStream(this._reverseStreams[stream.id]); |
|||
} else { |
|||
// relying on the same odd chrome behaviour as above.
|
|||
stream.removeTrack(sender.track); |
|||
} |
|||
this.dispatchEvent(new Event('negotiationneeded')); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
export function shimPeerConnection(window, browserDetails) { |
|||
if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { |
|||
// very basic support for old versions.
|
|||
window.RTCPeerConnection = window.webkitRTCPeerConnection; |
|||
} |
|||
if (!window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
|
|||
// shim implicit creation of RTCSessionDescription/RTCIceCandidate
|
|||
if (browserDetails.version < 53) { |
|||
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] |
|||
.forEach(function(method) { |
|||
const nativeMethod = window.RTCPeerConnection.prototype[method]; |
|||
const methodObj = {[method]() { |
|||
arguments[0] = new ((method === 'addIceCandidate') ? |
|||
window.RTCIceCandidate : |
|||
window.RTCSessionDescription)(arguments[0]); |
|||
return nativeMethod.apply(this, arguments); |
|||
}}; |
|||
window.RTCPeerConnection.prototype[method] = methodObj[method]; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// Attempt to fix ONN in plan-b mode.
|
|||
export function fixNegotiationNeeded(window, browserDetails) { |
|||
utils.wrapPeerConnectionEvent(window, 'negotiationneeded', e => { |
|||
const pc = e.target; |
|||
if (browserDetails.version < 72 || (pc.getConfiguration && |
|||
pc.getConfiguration().sdpSemantics === 'plan-b')) { |
|||
if (pc.signalingState !== 'stable') { |
|||
return; |
|||
} |
|||
} |
|||
return e; |
|||
}); |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
/* |
|||
* Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
export function shimGetDisplayMedia(window, getSourceId) { |
|||
if (window.navigator.mediaDevices && |
|||
'getDisplayMedia' in window.navigator.mediaDevices) { |
|||
return; |
|||
} |
|||
if (!(window.navigator.mediaDevices)) { |
|||
return; |
|||
} |
|||
// getSourceId is a function that returns a promise resolving with
|
|||
// the sourceId of the screen/window/tab to be shared.
|
|||
if (typeof getSourceId !== 'function') { |
|||
console.error('shimGetDisplayMedia: getSourceId argument is not ' + |
|||
'a function'); |
|||
return; |
|||
} |
|||
window.navigator.mediaDevices.getDisplayMedia = |
|||
function getDisplayMedia(constraints) { |
|||
return getSourceId(constraints) |
|||
.then(sourceId => { |
|||
const widthSpecified = constraints.video && constraints.video.width; |
|||
const heightSpecified = constraints.video && |
|||
constraints.video.height; |
|||
const frameRateSpecified = constraints.video && |
|||
constraints.video.frameRate; |
|||
constraints.video = { |
|||
mandatory: { |
|||
chromeMediaSource: 'desktop', |
|||
chromeMediaSourceId: sourceId, |
|||
maxFrameRate: frameRateSpecified || 3 |
|||
} |
|||
}; |
|||
if (widthSpecified) { |
|||
constraints.video.mandatory.maxWidth = widthSpecified; |
|||
} |
|||
if (heightSpecified) { |
|||
constraints.video.mandatory.maxHeight = heightSpecified; |
|||
} |
|||
return window.navigator.mediaDevices.getUserMedia(constraints); |
|||
}); |
|||
}; |
|||
} |
|||
@ -0,0 +1,188 @@ |
|||
/* |
|||
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
import * as utils from '../utils.js'; |
|||
const logging = utils.log; |
|||
|
|||
export function shimGetUserMedia(window, browserDetails) { |
|||
const navigator = window && window.navigator; |
|||
|
|||
if (!navigator.mediaDevices) { |
|||
return; |
|||
} |
|||
|
|||
const constraintsToChrome_ = function(c) { |
|||
if (typeof c !== 'object' || c.mandatory || c.optional) { |
|||
return c; |
|||
} |
|||
const cc = {}; |
|||
Object.keys(c).forEach(key => { |
|||
if (key === 'require' || key === 'advanced' || key === 'mediaSource') { |
|||
return; |
|||
} |
|||
const r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; |
|||
if (r.exact !== undefined && typeof r.exact === 'number') { |
|||
r.min = r.max = r.exact; |
|||
} |
|||
const oldname_ = function(prefix, name) { |
|||
if (prefix) { |
|||
return prefix + name.charAt(0).toUpperCase() + name.slice(1); |
|||
} |
|||
return (name === 'deviceId') ? 'sourceId' : name; |
|||
}; |
|||
if (r.ideal !== undefined) { |
|||
cc.optional = cc.optional || []; |
|||
let oc = {}; |
|||
if (typeof r.ideal === 'number') { |
|||
oc[oldname_('min', key)] = r.ideal; |
|||
cc.optional.push(oc); |
|||
oc = {}; |
|||
oc[oldname_('max', key)] = r.ideal; |
|||
cc.optional.push(oc); |
|||
} else { |
|||
oc[oldname_('', key)] = r.ideal; |
|||
cc.optional.push(oc); |
|||
} |
|||
} |
|||
if (r.exact !== undefined && typeof r.exact !== 'number') { |
|||
cc.mandatory = cc.mandatory || {}; |
|||
cc.mandatory[oldname_('', key)] = r.exact; |
|||
} else { |
|||
['min', 'max'].forEach(mix => { |
|||
if (r[mix] !== undefined) { |
|||
cc.mandatory = cc.mandatory || {}; |
|||
cc.mandatory[oldname_(mix, key)] = r[mix]; |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
if (c.advanced) { |
|||
cc.optional = (cc.optional || []).concat(c.advanced); |
|||
} |
|||
return cc; |
|||
}; |
|||
|
|||
const shimConstraints_ = function(constraints, func) { |
|||
if (browserDetails.version >= 61) { |
|||
return func(constraints); |
|||
} |
|||
constraints = JSON.parse(JSON.stringify(constraints)); |
|||
if (constraints && typeof constraints.audio === 'object') { |
|||
const remap = function(obj, a, b) { |
|||
if (a in obj && !(b in obj)) { |
|||
obj[b] = obj[a]; |
|||
delete obj[a]; |
|||
} |
|||
}; |
|||
constraints = JSON.parse(JSON.stringify(constraints)); |
|||
remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); |
|||
remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); |
|||
constraints.audio = constraintsToChrome_(constraints.audio); |
|||
} |
|||
if (constraints && typeof constraints.video === 'object') { |
|||
// Shim facingMode for mobile & surface pro.
|
|||
let face = constraints.video.facingMode; |
|||
face = face && ((typeof face === 'object') ? face : {ideal: face}); |
|||
const getSupportedFacingModeLies = browserDetails.version < 66; |
|||
|
|||
if ((face && (face.exact === 'user' || face.exact === 'environment' || |
|||
face.ideal === 'user' || face.ideal === 'environment')) && |
|||
!(navigator.mediaDevices.getSupportedConstraints && |
|||
navigator.mediaDevices.getSupportedConstraints().facingMode && |
|||
!getSupportedFacingModeLies)) { |
|||
delete constraints.video.facingMode; |
|||
let matches; |
|||
if (face.exact === 'environment' || face.ideal === 'environment') { |
|||
matches = ['back', 'rear']; |
|||
} else if (face.exact === 'user' || face.ideal === 'user') { |
|||
matches = ['front']; |
|||
} |
|||
if (matches) { |
|||
// Look for matches in label, or use last cam for back (typical).
|
|||
return navigator.mediaDevices.enumerateDevices() |
|||
.then(devices => { |
|||
devices = devices.filter(d => d.kind === 'videoinput'); |
|||
let dev = devices.find(d => matches.some(match => |
|||
d.label.toLowerCase().includes(match))); |
|||
if (!dev && devices.length && matches.includes('back')) { |
|||
dev = devices[devices.length - 1]; // more likely the back cam
|
|||
} |
|||
if (dev) { |
|||
constraints.video.deviceId = face.exact ? {exact: dev.deviceId} : |
|||
{ideal: dev.deviceId}; |
|||
} |
|||
constraints.video = constraintsToChrome_(constraints.video); |
|||
logging('chrome: ' + JSON.stringify(constraints)); |
|||
return func(constraints); |
|||
}); |
|||
} |
|||
} |
|||
constraints.video = constraintsToChrome_(constraints.video); |
|||
} |
|||
logging('chrome: ' + JSON.stringify(constraints)); |
|||
return func(constraints); |
|||
}; |
|||
|
|||
const shimError_ = function(e) { |
|||
if (browserDetails.version >= 64) { |
|||
return e; |
|||
} |
|||
return { |
|||
name: { |
|||
PermissionDeniedError: 'NotAllowedError', |
|||
PermissionDismissedError: 'NotAllowedError', |
|||
InvalidStateError: 'NotAllowedError', |
|||
DevicesNotFoundError: 'NotFoundError', |
|||
ConstraintNotSatisfiedError: 'OverconstrainedError', |
|||
TrackStartError: 'NotReadableError', |
|||
MediaDeviceFailedDueToShutdown: 'NotAllowedError', |
|||
MediaDeviceKillSwitchOn: 'NotAllowedError', |
|||
TabCaptureError: 'AbortError', |
|||
ScreenCaptureError: 'AbortError', |
|||
DeviceCaptureError: 'AbortError' |
|||
}[e.name] || e.name, |
|||
message: e.message, |
|||
constraint: e.constraint || e.constraintName, |
|||
toString() { |
|||
return this.name + (this.message && ': ') + this.message; |
|||
} |
|||
}; |
|||
}; |
|||
|
|||
const getUserMedia_ = function(constraints, onSuccess, onError) { |
|||
shimConstraints_(constraints, c => { |
|||
navigator.webkitGetUserMedia(c, onSuccess, e => { |
|||
if (onError) { |
|||
onError(shimError_(e)); |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
navigator.getUserMedia = getUserMedia_.bind(navigator); |
|||
|
|||
// Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
|
|||
// function which returns a Promise, it does not accept spec-style
|
|||
// constraints.
|
|||
if (navigator.mediaDevices.getUserMedia) { |
|||
const origGetUserMedia = navigator.mediaDevices.getUserMedia. |
|||
bind(navigator.mediaDevices); |
|||
navigator.mediaDevices.getUserMedia = function(cs) { |
|||
return shimConstraints_(cs, c => origGetUserMedia(c).then(stream => { |
|||
if (c.audio && !stream.getAudioTracks().length || |
|||
c.video && !stream.getVideoTracks().length) { |
|||
stream.getTracks().forEach(track => { |
|||
track.stop(); |
|||
}); |
|||
throw new DOMException('', 'NotFoundError'); |
|||
} |
|||
return stream; |
|||
}, e => Promise.reject(shimError_(e)))); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,433 @@ |
|||
/* |
|||
* Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
|
|||
import SDPUtils from 'sdp'; |
|||
import * as utils from './utils'; |
|||
|
|||
export function shimRTCIceCandidate(window) { |
|||
// foundation is arbitrarily chosen as an indicator for full support for
|
|||
// https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
|
|||
if (!window.RTCIceCandidate || (window.RTCIceCandidate && 'foundation' in |
|||
window.RTCIceCandidate.prototype)) { |
|||
return; |
|||
} |
|||
|
|||
const NativeRTCIceCandidate = window.RTCIceCandidate; |
|||
window.RTCIceCandidate = function RTCIceCandidate(args) { |
|||
// Remove the a= which shouldn't be part of the candidate string.
|
|||
if (typeof args === 'object' && args.candidate && |
|||
args.candidate.indexOf('a=') === 0) { |
|||
args = JSON.parse(JSON.stringify(args)); |
|||
args.candidate = args.candidate.substr(2); |
|||
} |
|||
|
|||
if (args.candidate && args.candidate.length) { |
|||
// Augment the native candidate with the parsed fields.
|
|||
const nativeCandidate = new NativeRTCIceCandidate(args); |
|||
const parsedCandidate = SDPUtils.parseCandidate(args.candidate); |
|||
const augmentedCandidate = Object.assign(nativeCandidate, |
|||
parsedCandidate); |
|||
|
|||
// Add a serializer that does not serialize the extra attributes.
|
|||
augmentedCandidate.toJSON = function toJSON() { |
|||
return { |
|||
candidate: augmentedCandidate.candidate, |
|||
sdpMid: augmentedCandidate.sdpMid, |
|||
sdpMLineIndex: augmentedCandidate.sdpMLineIndex, |
|||
usernameFragment: augmentedCandidate.usernameFragment, |
|||
}; |
|||
}; |
|||
return augmentedCandidate; |
|||
} |
|||
return new NativeRTCIceCandidate(args); |
|||
}; |
|||
window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; |
|||
|
|||
// Hook up the augmented candidate in onicecandidate and
|
|||
// addEventListener('icecandidate', ...)
|
|||
utils.wrapPeerConnectionEvent(window, 'icecandidate', e => { |
|||
if (e.candidate) { |
|||
Object.defineProperty(e, 'candidate', { |
|||
value: new window.RTCIceCandidate(e.candidate), |
|||
writable: 'false' |
|||
}); |
|||
} |
|||
return e; |
|||
}); |
|||
} |
|||
|
|||
export function shimMaxMessageSize(window, browserDetails) { |
|||
if (!window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
|
|||
if (!('sctp' in window.RTCPeerConnection.prototype)) { |
|||
Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { |
|||
get() { |
|||
return typeof this._sctp === 'undefined' ? null : this._sctp; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
const sctpInDescription = function(description) { |
|||
if (!description || !description.sdp) { |
|||
return false; |
|||
} |
|||
const sections = SDPUtils.splitSections(description.sdp); |
|||
sections.shift(); |
|||
return sections.some(mediaSection => { |
|||
const mLine = SDPUtils.parseMLine(mediaSection); |
|||
return mLine && mLine.kind === 'application' |
|||
&& mLine.protocol.indexOf('SCTP') !== -1; |
|||
}); |
|||
}; |
|||
|
|||
const getRemoteFirefoxVersion = function(description) { |
|||
// TODO: Is there a better solution for detecting Firefox?
|
|||
const match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); |
|||
if (match === null || match.length < 2) { |
|||
return -1; |
|||
} |
|||
const version = parseInt(match[1], 10); |
|||
// Test for NaN (yes, this is ugly)
|
|||
return version !== version ? -1 : version; |
|||
}; |
|||
|
|||
const getCanSendMaxMessageSize = function(remoteIsFirefox) { |
|||
// Every implementation we know can send at least 64 KiB.
|
|||
// Note: Although Chrome is technically able to send up to 256 KiB, the
|
|||
// data does not reach the other peer reliably.
|
|||
// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419
|
|||
let canSendMaxMessageSize = 65536; |
|||
if (browserDetails.browser === 'firefox') { |
|||
if (browserDetails.version < 57) { |
|||
if (remoteIsFirefox === -1) { |
|||
// FF < 57 will send in 16 KiB chunks using the deprecated PPID
|
|||
// fragmentation.
|
|||
canSendMaxMessageSize = 16384; |
|||
} else { |
|||
// However, other FF (and RAWRTC) can reassemble PPID-fragmented
|
|||
// messages. Thus, supporting ~2 GiB when sending.
|
|||
canSendMaxMessageSize = 2147483637; |
|||
} |
|||
} else if (browserDetails.version < 60) { |
|||
// Currently, all FF >= 57 will reset the remote maximum message size
|
|||
// to the default value when a data channel is created at a later
|
|||
// stage. :(
|
|||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831
|
|||
canSendMaxMessageSize = |
|||
browserDetails.version === 57 ? 65535 : 65536; |
|||
} else { |
|||
// FF >= 60 supports sending ~2 GiB
|
|||
canSendMaxMessageSize = 2147483637; |
|||
} |
|||
} |
|||
return canSendMaxMessageSize; |
|||
}; |
|||
|
|||
const getMaxMessageSize = function(description, remoteIsFirefox) { |
|||
// Note: 65536 bytes is the default value from the SDP spec. Also,
|
|||
// every implementation we know supports receiving 65536 bytes.
|
|||
let maxMessageSize = 65536; |
|||
|
|||
// FF 57 has a slightly incorrect default remote max message size, so
|
|||
// we need to adjust it here to avoid a failure when sending.
|
|||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697
|
|||
if (browserDetails.browser === 'firefox' |
|||
&& browserDetails.version === 57) { |
|||
maxMessageSize = 65535; |
|||
} |
|||
|
|||
const match = SDPUtils.matchPrefix(description.sdp, |
|||
'a=max-message-size:'); |
|||
if (match.length > 0) { |
|||
maxMessageSize = parseInt(match[0].substr(19), 10); |
|||
} else if (browserDetails.browser === 'firefox' && |
|||
remoteIsFirefox !== -1) { |
|||
// If the maximum message size is not present in the remote SDP and
|
|||
// both local and remote are Firefox, the remote peer can receive
|
|||
// ~2 GiB.
|
|||
maxMessageSize = 2147483637; |
|||
} |
|||
return maxMessageSize; |
|||
}; |
|||
|
|||
const origSetRemoteDescription = |
|||
window.RTCPeerConnection.prototype.setRemoteDescription; |
|||
window.RTCPeerConnection.prototype.setRemoteDescription = |
|||
function setRemoteDescription() { |
|||
this._sctp = null; |
|||
// Chrome decided to not expose .sctp in plan-b mode.
|
|||
// As usual, adapter.js has to do an 'ugly worakaround'
|
|||
// to cover up the mess.
|
|||
if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) { |
|||
const {sdpSemantics} = this.getConfiguration(); |
|||
if (sdpSemantics === 'plan-b') { |
|||
Object.defineProperty(this, 'sctp', { |
|||
get() { |
|||
return typeof this._sctp === 'undefined' ? null : this._sctp; |
|||
}, |
|||
enumerable: true, |
|||
configurable: true, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
if (sctpInDescription(arguments[0])) { |
|||
// Check if the remote is FF.
|
|||
const isFirefox = getRemoteFirefoxVersion(arguments[0]); |
|||
|
|||
// Get the maximum message size the local peer is capable of sending
|
|||
const canSendMMS = getCanSendMaxMessageSize(isFirefox); |
|||
|
|||
// Get the maximum message size of the remote peer.
|
|||
const remoteMMS = getMaxMessageSize(arguments[0], isFirefox); |
|||
|
|||
// Determine final maximum message size
|
|||
let maxMessageSize; |
|||
if (canSendMMS === 0 && remoteMMS === 0) { |
|||
maxMessageSize = Number.POSITIVE_INFINITY; |
|||
} else if (canSendMMS === 0 || remoteMMS === 0) { |
|||
maxMessageSize = Math.max(canSendMMS, remoteMMS); |
|||
} else { |
|||
maxMessageSize = Math.min(canSendMMS, remoteMMS); |
|||
} |
|||
|
|||
// Create a dummy RTCSctpTransport object and the 'maxMessageSize'
|
|||
// attribute.
|
|||
const sctp = {}; |
|||
Object.defineProperty(sctp, 'maxMessageSize', { |
|||
get() { |
|||
return maxMessageSize; |
|||
} |
|||
}); |
|||
this._sctp = sctp; |
|||
} |
|||
|
|||
return origSetRemoteDescription.apply(this, arguments); |
|||
}; |
|||
} |
|||
|
|||
export function shimSendThrowTypeError(window) { |
|||
if (!(window.RTCPeerConnection && |
|||
'createDataChannel' in window.RTCPeerConnection.prototype)) { |
|||
return; |
|||
} |
|||
|
|||
// Note: Although Firefox >= 57 has a native implementation, the maximum
|
|||
// message size can be reset for all data channels at a later stage.
|
|||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831
|
|||
|
|||
function wrapDcSend(dc, pc) { |
|||
const origDataChannelSend = dc.send; |
|||
dc.send = function send() { |
|||
const data = arguments[0]; |
|||
const length = data.length || data.size || data.byteLength; |
|||
if (dc.readyState === 'open' && |
|||
pc.sctp && length > pc.sctp.maxMessageSize) { |
|||
throw new TypeError('Message too large (can send a maximum of ' + |
|||
pc.sctp.maxMessageSize + ' bytes)'); |
|||
} |
|||
return origDataChannelSend.apply(dc, arguments); |
|||
}; |
|||
} |
|||
const origCreateDataChannel = |
|||
window.RTCPeerConnection.prototype.createDataChannel; |
|||
window.RTCPeerConnection.prototype.createDataChannel = |
|||
function createDataChannel() { |
|||
const dataChannel = origCreateDataChannel.apply(this, arguments); |
|||
wrapDcSend(dataChannel, this); |
|||
return dataChannel; |
|||
}; |
|||
utils.wrapPeerConnectionEvent(window, 'datachannel', e => { |
|||
wrapDcSend(e.channel, e.target); |
|||
return e; |
|||
}); |
|||
} |
|||
|
|||
|
|||
/* shims RTCConnectionState by pretending it is the same as iceConnectionState. |
|||
* See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12
|
|||
* for why this is a valid hack in Chrome. In Firefox it is slightly incorrect |
|||
* since DTLS failures would be hidden. See |
|||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1265827
|
|||
* for the Firefox tracking bug. |
|||
*/ |
|||
export function shimConnectionState(window) { |
|||
if (!window.RTCPeerConnection || |
|||
'connectionState' in window.RTCPeerConnection.prototype) { |
|||
return; |
|||
} |
|||
const proto = window.RTCPeerConnection.prototype; |
|||
Object.defineProperty(proto, 'connectionState', { |
|||
get() { |
|||
return { |
|||
completed: 'connected', |
|||
checking: 'connecting' |
|||
}[this.iceConnectionState] || this.iceConnectionState; |
|||
}, |
|||
enumerable: true, |
|||
configurable: true |
|||
}); |
|||
Object.defineProperty(proto, 'onconnectionstatechange', { |
|||
get() { |
|||
return this._onconnectionstatechange || null; |
|||
}, |
|||
set(cb) { |
|||
if (this._onconnectionstatechange) { |
|||
this.removeEventListener('connectionstatechange', |
|||
this._onconnectionstatechange); |
|||
delete this._onconnectionstatechange; |
|||
} |
|||
if (cb) { |
|||
this.addEventListener('connectionstatechange', |
|||
this._onconnectionstatechange = cb); |
|||
} |
|||
}, |
|||
enumerable: true, |
|||
configurable: true |
|||
}); |
|||
|
|||
['setLocalDescription', 'setRemoteDescription'].forEach((method) => { |
|||
const origMethod = proto[method]; |
|||
proto[method] = function() { |
|||
if (!this._connectionstatechangepoly) { |
|||
this._connectionstatechangepoly = e => { |
|||
const pc = e.target; |
|||
if (pc._lastConnectionState !== pc.connectionState) { |
|||
pc._lastConnectionState = pc.connectionState; |
|||
const newEvent = new Event('connectionstatechange', e); |
|||
pc.dispatchEvent(newEvent); |
|||
} |
|||
return e; |
|||
}; |
|||
this.addEventListener('iceconnectionstatechange', |
|||
this._connectionstatechangepoly); |
|||
} |
|||
return origMethod.apply(this, arguments); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
export function removeExtmapAllowMixed(window, browserDetails) { |
|||
/* remove a=extmap-allow-mixed for webrtc.org < M71 */ |
|||
if (!window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) { |
|||
return; |
|||
} |
|||
if (browserDetails.browser === 'safari' && browserDetails.version >= 605) { |
|||
return; |
|||
} |
|||
const nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription; |
|||
window.RTCPeerConnection.prototype.setRemoteDescription = |
|||
function setRemoteDescription(desc) { |
|||
if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) { |
|||
const sdp = desc.sdp.split('\n').filter((line) => { |
|||
return line.trim() !== 'a=extmap-allow-mixed'; |
|||
}).join('\n'); |
|||
// Safari enforces read-only-ness of RTCSessionDescription fields.
|
|||
if (window.RTCSessionDescription && |
|||
desc instanceof window.RTCSessionDescription) { |
|||
arguments[0] = new window.RTCSessionDescription({ |
|||
type: desc.type, |
|||
sdp, |
|||
}); |
|||
} else { |
|||
desc.sdp = sdp; |
|||
} |
|||
} |
|||
return nativeSRD.apply(this, arguments); |
|||
}; |
|||
} |
|||
|
|||
export function shimAddIceCandidateNullOrEmpty(window, browserDetails) { |
|||
// Support for addIceCandidate(null or undefined)
|
|||
// as well as addIceCandidate({candidate: "", ...})
|
|||
// https://bugs.chromium.org/p/chromium/issues/detail?id=978582
|
|||
// Note: must be called before other polyfills which change the signature.
|
|||
if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { |
|||
return; |
|||
} |
|||
const nativeAddIceCandidate = |
|||
window.RTCPeerConnection.prototype.addIceCandidate; |
|||
if (!nativeAddIceCandidate || nativeAddIceCandidate.length === 0) { |
|||
return; |
|||
} |
|||
window.RTCPeerConnection.prototype.addIceCandidate = |
|||
function addIceCandidate() { |
|||
if (!arguments[0]) { |
|||
if (arguments[1]) { |
|||
arguments[1].apply(null); |
|||
} |
|||
return Promise.resolve(); |
|||
} |
|||
// Firefox 68+ emits and processes {candidate: "", ...}, ignore
|
|||
// in older versions.
|
|||
// Native support for ignoring exists for Chrome M77+.
|
|||
// Safari ignores as well, exact version unknown but works in the same
|
|||
// version that also ignores addIceCandidate(null).
|
|||
if (((browserDetails.browser === 'chrome' && browserDetails.version < 78) |
|||
|| (browserDetails.browser === 'firefox' |
|||
&& browserDetails.version < 68) |
|||
|| (browserDetails.browser === 'safari')) |
|||
&& arguments[0] && arguments[0].candidate === '') { |
|||
return Promise.resolve(); |
|||
} |
|||
return nativeAddIceCandidate.apply(this, arguments); |
|||
}; |
|||
} |
|||
|
|||
// Note: Make sure to call this ahead of APIs that modify
|
|||
// setLocalDescription.length
|
|||
export function shimParameterlessSetLocalDescription(window, browserDetails) { |
|||
if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { |
|||
return; |
|||
} |
|||
const nativeSetLocalDescription = |
|||
window.RTCPeerConnection.prototype.setLocalDescription; |
|||
if (!nativeSetLocalDescription || nativeSetLocalDescription.length === 0) { |
|||
return; |
|||
} |
|||
window.RTCPeerConnection.prototype.setLocalDescription = |
|||
function setLocalDescription() { |
|||
let desc = arguments[0] || {}; |
|||
if (typeof desc !== 'object' || (desc.type && desc.sdp)) { |
|||
return nativeSetLocalDescription.apply(this, arguments); |
|||
} |
|||
// The remaining steps should technically happen when SLD comes off the
|
|||
// RTCPeerConnection's operations chain (not ahead of going on it), but
|
|||
// this is too difficult to shim. Instead, this shim only covers the
|
|||
// common case where the operations chain is empty. This is imperfect, but
|
|||
// should cover many cases. Rationale: Even if we can't reduce the glare
|
|||
// window to zero on imperfect implementations, there's value in tapping
|
|||
// into the perfect negotiation pattern that several browsers support.
|
|||
desc = {type: desc.type, sdp: desc.sdp}; |
|||
if (!desc.type) { |
|||
switch (this.signalingState) { |
|||
case 'stable': |
|||
case 'have-local-offer': |
|||
case 'have-remote-pranswer': |
|||
desc.type = 'offer'; |
|||
break; |
|||
default: |
|||
desc.type = 'answer'; |
|||
break; |
|||
} |
|||
} |
|||
if (desc.sdp || (desc.type !== 'offer' && desc.type !== 'answer')) { |
|||
return nativeSetLocalDescription.apply(this, [desc]); |
|||
} |
|||
const func = desc.type === 'offer' ? this.createOffer : this.createAnswer; |
|||
return func.apply(this) |
|||
.then(d => nativeSetLocalDescription.apply(this, [d])); |
|||
}; |
|||
} |
|||
@ -0,0 +1,296 @@ |
|||
/* |
|||
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
|
|||
import * as utils from '../utils'; |
|||
export {shimGetUserMedia} from './getusermedia'; |
|||
export {shimGetDisplayMedia} from './getdisplaymedia'; |
|||
|
|||
export function shimOnTrack(window) { |
|||
if (typeof window === 'object' && window.RTCTrackEvent && |
|||
('receiver' in window.RTCTrackEvent.prototype) && |
|||
!('transceiver' in window.RTCTrackEvent.prototype)) { |
|||
Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { |
|||
get() { |
|||
return {receiver: this.receiver}; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export function shimPeerConnection(window, browserDetails) { |
|||
if (typeof window !== 'object' || |
|||
!(window.RTCPeerConnection || window.mozRTCPeerConnection)) { |
|||
return; // probably media.peerconnection.enabled=false in about:config
|
|||
} |
|||
if (!window.RTCPeerConnection && window.mozRTCPeerConnection) { |
|||
// very basic support for old versions.
|
|||
window.RTCPeerConnection = window.mozRTCPeerConnection; |
|||
} |
|||
|
|||
if (browserDetails.version < 53) { |
|||
// shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
|
|||
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] |
|||
.forEach(function(method) { |
|||
const nativeMethod = window.RTCPeerConnection.prototype[method]; |
|||
const methodObj = {[method]() { |
|||
arguments[0] = new ((method === 'addIceCandidate') ? |
|||
window.RTCIceCandidate : |
|||
window.RTCSessionDescription)(arguments[0]); |
|||
return nativeMethod.apply(this, arguments); |
|||
}}; |
|||
window.RTCPeerConnection.prototype[method] = methodObj[method]; |
|||
}); |
|||
} |
|||
|
|||
const modernStatsTypes = { |
|||
inboundrtp: 'inbound-rtp', |
|||
outboundrtp: 'outbound-rtp', |
|||
candidatepair: 'candidate-pair', |
|||
localcandidate: 'local-candidate', |
|||
remotecandidate: 'remote-candidate' |
|||
}; |
|||
|
|||
const nativeGetStats = window.RTCPeerConnection.prototype.getStats; |
|||
window.RTCPeerConnection.prototype.getStats = function getStats() { |
|||
const [selector, onSucc, onErr] = arguments; |
|||
return nativeGetStats.apply(this, [selector || null]) |
|||
.then(stats => { |
|||
if (browserDetails.version < 53 && !onSucc) { |
|||
// Shim only promise getStats with spec-hyphens in type names
|
|||
// Leave callback version alone; misc old uses of forEach before Map
|
|||
try { |
|||
stats.forEach(stat => { |
|||
stat.type = modernStatsTypes[stat.type] || stat.type; |
|||
}); |
|||
} catch (e) { |
|||
if (e.name !== 'TypeError') { |
|||
throw e; |
|||
} |
|||
// Avoid TypeError: "type" is read-only, in old versions. 34-43ish
|
|||
stats.forEach((stat, i) => { |
|||
stats.set(i, Object.assign({}, stat, { |
|||
type: modernStatsTypes[stat.type] || stat.type |
|||
})); |
|||
}); |
|||
} |
|||
} |
|||
return stats; |
|||
}) |
|||
.then(onSucc, onErr); |
|||
}; |
|||
} |
|||
|
|||
export function shimSenderGetStats(window) { |
|||
if (!(typeof window === 'object' && window.RTCPeerConnection && |
|||
window.RTCRtpSender)) { |
|||
return; |
|||
} |
|||
if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { |
|||
return; |
|||
} |
|||
const origGetSenders = window.RTCPeerConnection.prototype.getSenders; |
|||
if (origGetSenders) { |
|||
window.RTCPeerConnection.prototype.getSenders = function getSenders() { |
|||
const senders = origGetSenders.apply(this, []); |
|||
senders.forEach(sender => sender._pc = this); |
|||
return senders; |
|||
}; |
|||
} |
|||
|
|||
const origAddTrack = window.RTCPeerConnection.prototype.addTrack; |
|||
if (origAddTrack) { |
|||
window.RTCPeerConnection.prototype.addTrack = function addTrack() { |
|||
const sender = origAddTrack.apply(this, arguments); |
|||
sender._pc = this; |
|||
return sender; |
|||
}; |
|||
} |
|||
window.RTCRtpSender.prototype.getStats = function getStats() { |
|||
return this.track ? this._pc.getStats(this.track) : |
|||
Promise.resolve(new Map()); |
|||
}; |
|||
} |
|||
|
|||
export function shimReceiverGetStats(window) { |
|||
if (!(typeof window === 'object' && window.RTCPeerConnection && |
|||
window.RTCRtpSender)) { |
|||
return; |
|||
} |
|||
if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { |
|||
return; |
|||
} |
|||
const origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; |
|||
if (origGetReceivers) { |
|||
window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { |
|||
const receivers = origGetReceivers.apply(this, []); |
|||
receivers.forEach(receiver => receiver._pc = this); |
|||
return receivers; |
|||
}; |
|||
} |
|||
utils.wrapPeerConnectionEvent(window, 'track', e => { |
|||
e.receiver._pc = e.srcElement; |
|||
return e; |
|||
}); |
|||
window.RTCRtpReceiver.prototype.getStats = function getStats() { |
|||
return this._pc.getStats(this.track); |
|||
}; |
|||
} |
|||
|
|||
export function shimRemoveStream(window) { |
|||
if (!window.RTCPeerConnection || |
|||
'removeStream' in window.RTCPeerConnection.prototype) { |
|||
return; |
|||
} |
|||
window.RTCPeerConnection.prototype.removeStream = |
|||
function removeStream(stream) { |
|||
utils.deprecated('removeStream', 'removeTrack'); |
|||
this.getSenders().forEach(sender => { |
|||
if (sender.track && stream.getTracks().includes(sender.track)) { |
|||
this.removeTrack(sender); |
|||
} |
|||
}); |
|||
}; |
|||
} |
|||
|
|||
export function shimRTCDataChannel(window) { |
|||
// rename DataChannel to RTCDataChannel (native fix in FF60):
|
|||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1173851
|
|||
if (window.DataChannel && !window.RTCDataChannel) { |
|||
window.RTCDataChannel = window.DataChannel; |
|||
} |
|||
} |
|||
|
|||
export function shimAddTransceiver(window) { |
|||
// https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
|
|||
// Firefox ignores the init sendEncodings options passed to addTransceiver
|
|||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
|
|||
if (!(typeof window === 'object' && window.RTCPeerConnection)) { |
|||
return; |
|||
} |
|||
const origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver; |
|||
if (origAddTransceiver) { |
|||
window.RTCPeerConnection.prototype.addTransceiver = |
|||
function addTransceiver() { |
|||
this.setParametersPromises = []; |
|||
const initParameters = arguments[1]; |
|||
const shouldPerformCheck = initParameters && |
|||
'sendEncodings' in initParameters; |
|||
if (shouldPerformCheck) { |
|||
// If sendEncodings params are provided, validate grammar
|
|||
initParameters.sendEncodings.forEach((encodingParam) => { |
|||
if ('rid' in encodingParam) { |
|||
const ridRegex = /^[a-z0-9]{0,16}$/i; |
|||
if (!ridRegex.test(encodingParam.rid)) { |
|||
throw new TypeError('Invalid RID value provided.'); |
|||
} |
|||
} |
|||
if ('scaleResolutionDownBy' in encodingParam) { |
|||
if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) { |
|||
throw new RangeError('scale_resolution_down_by must be >= 1.0'); |
|||
} |
|||
} |
|||
if ('maxFramerate' in encodingParam) { |
|||
if (!(parseFloat(encodingParam.maxFramerate) >= 0)) { |
|||
throw new RangeError('max_framerate must be >= 0.0'); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
const transceiver = origAddTransceiver.apply(this, arguments); |
|||
if (shouldPerformCheck) { |
|||
// Check if the init options were applied. If not we do this in an
|
|||
// asynchronous way and save the promise reference in a global object.
|
|||
// This is an ugly hack, but at the same time is way more robust than
|
|||
// checking the sender parameters before and after the createOffer
|
|||
// Also note that after the createoffer we are not 100% sure that
|
|||
// the params were asynchronously applied so we might miss the
|
|||
// opportunity to recreate offer.
|
|||
const {sender} = transceiver; |
|||
const params = sender.getParameters(); |
|||
if (!('encodings' in params) || |
|||
// Avoid being fooled by patched getParameters() below.
|
|||
(params.encodings.length === 1 && |
|||
Object.keys(params.encodings[0]).length === 0)) { |
|||
params.encodings = initParameters.sendEncodings; |
|||
sender.sendEncodings = initParameters.sendEncodings; |
|||
this.setParametersPromises.push(sender.setParameters(params) |
|||
.then(() => { |
|||
delete sender.sendEncodings; |
|||
}).catch(() => { |
|||
delete sender.sendEncodings; |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
return transceiver; |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export function shimGetParameters(window) { |
|||
if (!(typeof window === 'object' && window.RTCRtpSender)) { |
|||
return; |
|||
} |
|||
const origGetParameters = window.RTCRtpSender.prototype.getParameters; |
|||
if (origGetParameters) { |
|||
window.RTCRtpSender.prototype.getParameters = |
|||
function getParameters() { |
|||
const params = origGetParameters.apply(this, arguments); |
|||
if (!('encodings' in params)) { |
|||
params.encodings = [].concat(this.sendEncodings || [{}]); |
|||
} |
|||
return params; |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export function shimCreateOffer(window) { |
|||
// https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
|
|||
// Firefox ignores the init sendEncodings options passed to addTransceiver
|
|||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
|
|||
if (!(typeof window === 'object' && window.RTCPeerConnection)) { |
|||
return; |
|||
} |
|||
const origCreateOffer = window.RTCPeerConnection.prototype.createOffer; |
|||
window.RTCPeerConnection.prototype.createOffer = function createOffer() { |
|||
if (this.setParametersPromises && this.setParametersPromises.length) { |
|||
return Promise.all(this.setParametersPromises) |
|||
.then(() => { |
|||
return origCreateOffer.apply(this, arguments); |
|||
}) |
|||
.finally(() => { |
|||
this.setParametersPromises = []; |
|||
}); |
|||
} |
|||
return origCreateOffer.apply(this, arguments); |
|||
}; |
|||
} |
|||
|
|||
export function shimCreateAnswer(window) { |
|||
// https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
|
|||
// Firefox ignores the init sendEncodings options passed to addTransceiver
|
|||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
|
|||
if (!(typeof window === 'object' && window.RTCPeerConnection)) { |
|||
return; |
|||
} |
|||
const origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer; |
|||
window.RTCPeerConnection.prototype.createAnswer = function createAnswer() { |
|||
if (this.setParametersPromises && this.setParametersPromises.length) { |
|||
return Promise.all(this.setParametersPromises) |
|||
.then(() => { |
|||
return origCreateAnswer.apply(this, arguments); |
|||
}) |
|||
.finally(() => { |
|||
this.setParametersPromises = []; |
|||
}); |
|||
} |
|||
return origCreateAnswer.apply(this, arguments); |
|||
}; |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
/* |
|||
* Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
|
|||
export function shimGetDisplayMedia(window, preferredMediaSource) { |
|||
if (window.navigator.mediaDevices && |
|||
'getDisplayMedia' in window.navigator.mediaDevices) { |
|||
return; |
|||
} |
|||
if (!(window.navigator.mediaDevices)) { |
|||
return; |
|||
} |
|||
window.navigator.mediaDevices.getDisplayMedia = |
|||
function getDisplayMedia(constraints) { |
|||
if (!(constraints && constraints.video)) { |
|||
const err = new DOMException('getDisplayMedia without video ' + |
|||
'constraints is undefined'); |
|||
err.name = 'NotFoundError'; |
|||
// from https://heycam.github.io/webidl/#idl-DOMException-error-names
|
|||
err.code = 8; |
|||
return Promise.reject(err); |
|||
} |
|||
if (constraints.video === true) { |
|||
constraints.video = {mediaSource: preferredMediaSource}; |
|||
} else { |
|||
constraints.video.mediaSource = preferredMediaSource; |
|||
} |
|||
return window.navigator.mediaDevices.getUserMedia(constraints); |
|||
}; |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
/* |
|||
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
|
|||
import * as utils from '../utils'; |
|||
|
|||
export function shimGetUserMedia(window, browserDetails) { |
|||
const navigator = window && window.navigator; |
|||
const MediaStreamTrack = window && window.MediaStreamTrack; |
|||
|
|||
navigator.getUserMedia = function(constraints, onSuccess, onError) { |
|||
// Replace Firefox 44+'s deprecation warning with unprefixed version.
|
|||
utils.deprecated('navigator.getUserMedia', |
|||
'navigator.mediaDevices.getUserMedia'); |
|||
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); |
|||
}; |
|||
|
|||
if (!(browserDetails.version > 55 && |
|||
'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { |
|||
const remap = function(obj, a, b) { |
|||
if (a in obj && !(b in obj)) { |
|||
obj[b] = obj[a]; |
|||
delete obj[a]; |
|||
} |
|||
}; |
|||
|
|||
const nativeGetUserMedia = navigator.mediaDevices.getUserMedia. |
|||
bind(navigator.mediaDevices); |
|||
navigator.mediaDevices.getUserMedia = function(c) { |
|||
if (typeof c === 'object' && typeof c.audio === 'object') { |
|||
c = JSON.parse(JSON.stringify(c)); |
|||
remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); |
|||
remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); |
|||
} |
|||
return nativeGetUserMedia(c); |
|||
}; |
|||
|
|||
if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { |
|||
const nativeGetSettings = MediaStreamTrack.prototype.getSettings; |
|||
MediaStreamTrack.prototype.getSettings = function() { |
|||
const obj = nativeGetSettings.apply(this, arguments); |
|||
remap(obj, 'mozAutoGainControl', 'autoGainControl'); |
|||
remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); |
|||
return obj; |
|||
}; |
|||
} |
|||
|
|||
if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { |
|||
const nativeApplyConstraints = |
|||
MediaStreamTrack.prototype.applyConstraints; |
|||
MediaStreamTrack.prototype.applyConstraints = function(c) { |
|||
if (this.kind === 'audio' && typeof c === 'object') { |
|||
c = JSON.parse(JSON.stringify(c)); |
|||
remap(c, 'autoGainControl', 'mozAutoGainControl'); |
|||
remap(c, 'noiseSuppression', 'mozNoiseSuppression'); |
|||
} |
|||
return nativeApplyConstraints.apply(this, [c]); |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,353 @@ |
|||
/* |
|||
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
'use strict'; |
|||
import * as utils from '../utils'; |
|||
|
|||
export function shimLocalStreamsAPI(window) { |
|||
if (typeof window !== 'object' || !window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { |
|||
window.RTCPeerConnection.prototype.getLocalStreams = |
|||
function getLocalStreams() { |
|||
if (!this._localStreams) { |
|||
this._localStreams = []; |
|||
} |
|||
return this._localStreams; |
|||
}; |
|||
} |
|||
if (!('addStream' in window.RTCPeerConnection.prototype)) { |
|||
const _addTrack = window.RTCPeerConnection.prototype.addTrack; |
|||
window.RTCPeerConnection.prototype.addStream = function addStream(stream) { |
|||
if (!this._localStreams) { |
|||
this._localStreams = []; |
|||
} |
|||
if (!this._localStreams.includes(stream)) { |
|||
this._localStreams.push(stream); |
|||
} |
|||
// Try to emulate Chrome's behaviour of adding in audio-video order.
|
|||
// Safari orders by track id.
|
|||
stream.getAudioTracks().forEach(track => _addTrack.call(this, track, |
|||
stream)); |
|||
stream.getVideoTracks().forEach(track => _addTrack.call(this, track, |
|||
stream)); |
|||
}; |
|||
|
|||
window.RTCPeerConnection.prototype.addTrack = |
|||
function addTrack(track, ...streams) { |
|||
if (streams) { |
|||
streams.forEach((stream) => { |
|||
if (!this._localStreams) { |
|||
this._localStreams = [stream]; |
|||
} else if (!this._localStreams.includes(stream)) { |
|||
this._localStreams.push(stream); |
|||
} |
|||
}); |
|||
} |
|||
return _addTrack.apply(this, arguments); |
|||
}; |
|||
} |
|||
if (!('removeStream' in window.RTCPeerConnection.prototype)) { |
|||
window.RTCPeerConnection.prototype.removeStream = |
|||
function removeStream(stream) { |
|||
if (!this._localStreams) { |
|||
this._localStreams = []; |
|||
} |
|||
const index = this._localStreams.indexOf(stream); |
|||
if (index === -1) { |
|||
return; |
|||
} |
|||
this._localStreams.splice(index, 1); |
|||
const tracks = stream.getTracks(); |
|||
this.getSenders().forEach(sender => { |
|||
if (tracks.includes(sender.track)) { |
|||
this.removeTrack(sender); |
|||
} |
|||
}); |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export function shimRemoteStreamsAPI(window) { |
|||
if (typeof window !== 'object' || !window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { |
|||
window.RTCPeerConnection.prototype.getRemoteStreams = |
|||
function getRemoteStreams() { |
|||
return this._remoteStreams ? this._remoteStreams : []; |
|||
}; |
|||
} |
|||
if (!('onaddstream' in window.RTCPeerConnection.prototype)) { |
|||
Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { |
|||
get() { |
|||
return this._onaddstream; |
|||
}, |
|||
set(f) { |
|||
if (this._onaddstream) { |
|||
this.removeEventListener('addstream', this._onaddstream); |
|||
this.removeEventListener('track', this._onaddstreampoly); |
|||
} |
|||
this.addEventListener('addstream', this._onaddstream = f); |
|||
this.addEventListener('track', this._onaddstreampoly = (e) => { |
|||
e.streams.forEach(stream => { |
|||
if (!this._remoteStreams) { |
|||
this._remoteStreams = []; |
|||
} |
|||
if (this._remoteStreams.includes(stream)) { |
|||
return; |
|||
} |
|||
this._remoteStreams.push(stream); |
|||
const event = new Event('addstream'); |
|||
event.stream = stream; |
|||
this.dispatchEvent(event); |
|||
}); |
|||
}); |
|||
} |
|||
}); |
|||
const origSetRemoteDescription = |
|||
window.RTCPeerConnection.prototype.setRemoteDescription; |
|||
window.RTCPeerConnection.prototype.setRemoteDescription = |
|||
function setRemoteDescription() { |
|||
const pc = this; |
|||
if (!this._onaddstreampoly) { |
|||
this.addEventListener('track', this._onaddstreampoly = function(e) { |
|||
e.streams.forEach(stream => { |
|||
if (!pc._remoteStreams) { |
|||
pc._remoteStreams = []; |
|||
} |
|||
if (pc._remoteStreams.indexOf(stream) >= 0) { |
|||
return; |
|||
} |
|||
pc._remoteStreams.push(stream); |
|||
const event = new Event('addstream'); |
|||
event.stream = stream; |
|||
pc.dispatchEvent(event); |
|||
}); |
|||
}); |
|||
} |
|||
return origSetRemoteDescription.apply(pc, arguments); |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export function shimCallbacksAPI(window) { |
|||
if (typeof window !== 'object' || !window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
const prototype = window.RTCPeerConnection.prototype; |
|||
const origCreateOffer = prototype.createOffer; |
|||
const origCreateAnswer = prototype.createAnswer; |
|||
const setLocalDescription = prototype.setLocalDescription; |
|||
const setRemoteDescription = prototype.setRemoteDescription; |
|||
const addIceCandidate = prototype.addIceCandidate; |
|||
|
|||
prototype.createOffer = |
|||
function createOffer(successCallback, failureCallback) { |
|||
const options = (arguments.length >= 2) ? arguments[2] : arguments[0]; |
|||
const promise = origCreateOffer.apply(this, [options]); |
|||
if (!failureCallback) { |
|||
return promise; |
|||
} |
|||
promise.then(successCallback, failureCallback); |
|||
return Promise.resolve(); |
|||
}; |
|||
|
|||
prototype.createAnswer = |
|||
function createAnswer(successCallback, failureCallback) { |
|||
const options = (arguments.length >= 2) ? arguments[2] : arguments[0]; |
|||
const promise = origCreateAnswer.apply(this, [options]); |
|||
if (!failureCallback) { |
|||
return promise; |
|||
} |
|||
promise.then(successCallback, failureCallback); |
|||
return Promise.resolve(); |
|||
}; |
|||
|
|||
let withCallback = function(description, successCallback, failureCallback) { |
|||
const promise = setLocalDescription.apply(this, [description]); |
|||
if (!failureCallback) { |
|||
return promise; |
|||
} |
|||
promise.then(successCallback, failureCallback); |
|||
return Promise.resolve(); |
|||
}; |
|||
prototype.setLocalDescription = withCallback; |
|||
|
|||
withCallback = function(description, successCallback, failureCallback) { |
|||
const promise = setRemoteDescription.apply(this, [description]); |
|||
if (!failureCallback) { |
|||
return promise; |
|||
} |
|||
promise.then(successCallback, failureCallback); |
|||
return Promise.resolve(); |
|||
}; |
|||
prototype.setRemoteDescription = withCallback; |
|||
|
|||
withCallback = function(candidate, successCallback, failureCallback) { |
|||
const promise = addIceCandidate.apply(this, [candidate]); |
|||
if (!failureCallback) { |
|||
return promise; |
|||
} |
|||
promise.then(successCallback, failureCallback); |
|||
return Promise.resolve(); |
|||
}; |
|||
prototype.addIceCandidate = withCallback; |
|||
} |
|||
|
|||
export function shimGetUserMedia(window) { |
|||
const navigator = window && window.navigator; |
|||
|
|||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { |
|||
// shim not needed in Safari 12.1
|
|||
const mediaDevices = navigator.mediaDevices; |
|||
const _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); |
|||
navigator.mediaDevices.getUserMedia = (constraints) => { |
|||
return _getUserMedia(shimConstraints(constraints)); |
|||
}; |
|||
} |
|||
|
|||
if (!navigator.getUserMedia && navigator.mediaDevices && |
|||
navigator.mediaDevices.getUserMedia) { |
|||
navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) { |
|||
navigator.mediaDevices.getUserMedia(constraints) |
|||
.then(cb, errcb); |
|||
}.bind(navigator); |
|||
} |
|||
} |
|||
|
|||
export function shimConstraints(constraints) { |
|||
if (constraints && constraints.video !== undefined) { |
|||
return Object.assign({}, |
|||
constraints, |
|||
{video: utils.compactObject(constraints.video)} |
|||
); |
|||
} |
|||
|
|||
return constraints; |
|||
} |
|||
|
|||
export function shimRTCIceServerUrls(window) { |
|||
if (!window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
// migrate from non-spec RTCIceServer.url to RTCIceServer.urls
|
|||
const OrigPeerConnection = window.RTCPeerConnection; |
|||
window.RTCPeerConnection = |
|||
function RTCPeerConnection(pcConfig, pcConstraints) { |
|||
if (pcConfig && pcConfig.iceServers) { |
|||
const newIceServers = []; |
|||
for (let i = 0; i < pcConfig.iceServers.length; i++) { |
|||
let server = pcConfig.iceServers[i]; |
|||
if (!server.hasOwnProperty('urls') && |
|||
server.hasOwnProperty('url')) { |
|||
utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); |
|||
server = JSON.parse(JSON.stringify(server)); |
|||
server.urls = server.url; |
|||
delete server.url; |
|||
newIceServers.push(server); |
|||
} else { |
|||
newIceServers.push(pcConfig.iceServers[i]); |
|||
} |
|||
} |
|||
pcConfig.iceServers = newIceServers; |
|||
} |
|||
return new OrigPeerConnection(pcConfig, pcConstraints); |
|||
}; |
|||
window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; |
|||
// wrap static methods. Currently just generateCertificate.
|
|||
if ('generateCertificate' in OrigPeerConnection) { |
|||
Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { |
|||
get() { |
|||
return OrigPeerConnection.generateCertificate; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export function shimTrackEventTransceiver(window) { |
|||
// Add event.transceiver member over deprecated event.receiver
|
|||
if (typeof window === 'object' && window.RTCTrackEvent && |
|||
'receiver' in window.RTCTrackEvent.prototype && |
|||
!('transceiver' in window.RTCTrackEvent.prototype)) { |
|||
Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { |
|||
get() { |
|||
return {receiver: this.receiver}; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export function shimCreateOfferLegacy(window) { |
|||
const origCreateOffer = window.RTCPeerConnection.prototype.createOffer; |
|||
window.RTCPeerConnection.prototype.createOffer = |
|||
function createOffer(offerOptions) { |
|||
if (offerOptions) { |
|||
if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { |
|||
// support bit values
|
|||
offerOptions.offerToReceiveAudio = |
|||
!!offerOptions.offerToReceiveAudio; |
|||
} |
|||
const audioTransceiver = this.getTransceivers().find(transceiver => |
|||
transceiver.receiver.track.kind === 'audio'); |
|||
if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { |
|||
if (audioTransceiver.direction === 'sendrecv') { |
|||
if (audioTransceiver.setDirection) { |
|||
audioTransceiver.setDirection('sendonly'); |
|||
} else { |
|||
audioTransceiver.direction = 'sendonly'; |
|||
} |
|||
} else if (audioTransceiver.direction === 'recvonly') { |
|||
if (audioTransceiver.setDirection) { |
|||
audioTransceiver.setDirection('inactive'); |
|||
} else { |
|||
audioTransceiver.direction = 'inactive'; |
|||
} |
|||
} |
|||
} else if (offerOptions.offerToReceiveAudio === true && |
|||
!audioTransceiver) { |
|||
this.addTransceiver('audio', {direction: 'recvonly'}); |
|||
} |
|||
|
|||
if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { |
|||
// support bit values
|
|||
offerOptions.offerToReceiveVideo = |
|||
!!offerOptions.offerToReceiveVideo; |
|||
} |
|||
const videoTransceiver = this.getTransceivers().find(transceiver => |
|||
transceiver.receiver.track.kind === 'video'); |
|||
if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { |
|||
if (videoTransceiver.direction === 'sendrecv') { |
|||
if (videoTransceiver.setDirection) { |
|||
videoTransceiver.setDirection('sendonly'); |
|||
} else { |
|||
videoTransceiver.direction = 'sendonly'; |
|||
} |
|||
} else if (videoTransceiver.direction === 'recvonly') { |
|||
if (videoTransceiver.setDirection) { |
|||
videoTransceiver.setDirection('inactive'); |
|||
} else { |
|||
videoTransceiver.direction = 'inactive'; |
|||
} |
|||
} |
|||
} else if (offerOptions.offerToReceiveVideo === true && |
|||
!videoTransceiver) { |
|||
this.addTransceiver('video', {direction: 'recvonly'}); |
|||
} |
|||
} |
|||
return origCreateOffer.apply(this, arguments); |
|||
}; |
|||
} |
|||
|
|||
export function shimAudioContext(window) { |
|||
if (typeof window !== 'object' || window.AudioContext) { |
|||
return; |
|||
} |
|||
window.AudioContext = window.webkitAudioContext; |
|||
} |
|||
|
|||
@ -0,0 +1,263 @@ |
|||
/* |
|||
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. |
|||
* |
|||
* Use of this source code is governed by a BSD-style license |
|||
* that can be found in the LICENSE file in the root of the source |
|||
* tree. |
|||
*/ |
|||
/* eslint-env node */ |
|||
'use strict'; |
|||
|
|||
let logDisabled_ = true; |
|||
let deprecationWarnings_ = true; |
|||
|
|||
/** |
|||
* Extract browser version out of the provided user agent string. |
|||
* |
|||
* @param {!string} uastring userAgent string. |
|||
* @param {!string} expr Regular expression used as match criteria. |
|||
* @param {!number} pos position in the version string to be returned. |
|||
* @return {!number} browser version. |
|||
*/ |
|||
export function extractVersion(uastring, expr, pos) { |
|||
const match = uastring.match(expr); |
|||
return match && match.length >= pos && parseInt(match[pos], 10); |
|||
} |
|||
|
|||
// Wraps the peerconnection event eventNameToWrap in a function
|
|||
// which returns the modified event object (or false to prevent
|
|||
// the event).
|
|||
export function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { |
|||
if (!window.RTCPeerConnection) { |
|||
return; |
|||
} |
|||
const proto = window.RTCPeerConnection.prototype; |
|||
const nativeAddEventListener = proto.addEventListener; |
|||
proto.addEventListener = function(nativeEventName, cb) { |
|||
if (nativeEventName !== eventNameToWrap) { |
|||
return nativeAddEventListener.apply(this, arguments); |
|||
} |
|||
const wrappedCallback = (e) => { |
|||
const modifiedEvent = wrapper(e); |
|||
if (modifiedEvent) { |
|||
if (cb.handleEvent) { |
|||
cb.handleEvent(modifiedEvent); |
|||
} else { |
|||
cb(modifiedEvent); |
|||
} |
|||
} |
|||
}; |
|||
this._eventMap = this._eventMap || {}; |
|||
if (!this._eventMap[eventNameToWrap]) { |
|||
this._eventMap[eventNameToWrap] = new Map(); |
|||
} |
|||
this._eventMap[eventNameToWrap].set(cb, wrappedCallback); |
|||
return nativeAddEventListener.apply(this, [nativeEventName, |
|||
wrappedCallback]); |
|||
}; |
|||
|
|||
const nativeRemoveEventListener = proto.removeEventListener; |
|||
proto.removeEventListener = function(nativeEventName, cb) { |
|||
if (nativeEventName !== eventNameToWrap || !this._eventMap |
|||
|| !this._eventMap[eventNameToWrap]) { |
|||
return nativeRemoveEventListener.apply(this, arguments); |
|||
} |
|||
if (!this._eventMap[eventNameToWrap].has(cb)) { |
|||
return nativeRemoveEventListener.apply(this, arguments); |
|||
} |
|||
const unwrappedCb = this._eventMap[eventNameToWrap].get(cb); |
|||
this._eventMap[eventNameToWrap].delete(cb); |
|||
if (this._eventMap[eventNameToWrap].size === 0) { |
|||
delete this._eventMap[eventNameToWrap]; |
|||
} |
|||
if (Object.keys(this._eventMap).length === 0) { |
|||
delete this._eventMap; |
|||
} |
|||
return nativeRemoveEventListener.apply(this, [nativeEventName, |
|||
unwrappedCb]); |
|||
}; |
|||
|
|||
Object.defineProperty(proto, 'on' + eventNameToWrap, { |
|||
get() { |
|||
return this['_on' + eventNameToWrap]; |
|||
}, |
|||
set(cb) { |
|||
if (this['_on' + eventNameToWrap]) { |
|||
this.removeEventListener(eventNameToWrap, |
|||
this['_on' + eventNameToWrap]); |
|||
delete this['_on' + eventNameToWrap]; |
|||
} |
|||
if (cb) { |
|||
this.addEventListener(eventNameToWrap, |
|||
this['_on' + eventNameToWrap] = cb); |
|||
} |
|||
}, |
|||
enumerable: true, |
|||
configurable: true |
|||
}); |
|||
} |
|||
|
|||
export function disableLog(bool) { |
|||
if (typeof bool !== 'boolean') { |
|||
return new Error('Argument type: ' + typeof bool + |
|||
'. Please use a boolean.'); |
|||
} |
|||
logDisabled_ = bool; |
|||
return (bool) ? 'adapter.js logging disabled' : |
|||
'adapter.js logging enabled'; |
|||
} |
|||
|
|||
/** |
|||
* Disable or enable deprecation warnings |
|||
* @param {!boolean} bool set to true to disable warnings. |
|||
*/ |
|||
export function disableWarnings(bool) { |
|||
if (typeof bool !== 'boolean') { |
|||
return new Error('Argument type: ' + typeof bool + |
|||
'. Please use a boolean.'); |
|||
} |
|||
deprecationWarnings_ = !bool; |
|||
return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); |
|||
} |
|||
|
|||
export function log() { |
|||
if (typeof window === 'object') { |
|||
if (logDisabled_) { |
|||
return; |
|||
} |
|||
if (typeof console !== 'undefined' && typeof console.log === 'function') { |
|||
console.log.apply(console, arguments); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Shows a deprecation warning suggesting the modern and spec-compatible API. |
|||
*/ |
|||
export function deprecated(oldMethod, newMethod) { |
|||
if (!deprecationWarnings_) { |
|||
return; |
|||
} |
|||
console.warn(oldMethod + ' is deprecated, please use ' + newMethod + |
|||
' instead.'); |
|||
} |
|||
|
|||
/** |
|||
* Browser detector. |
|||
* |
|||
* @return {object} result containing browser and version |
|||
* properties. |
|||
*/ |
|||
export function detectBrowser(window) { |
|||
// Returned result object.
|
|||
const result = {browser: null, version: null}; |
|||
|
|||
// Fail early if it's not a browser
|
|||
if (typeof window === 'undefined' || !window.navigator) { |
|||
result.browser = 'Not a browser.'; |
|||
return result; |
|||
} |
|||
|
|||
const {navigator} = window; |
|||
|
|||
if (navigator.mozGetUserMedia) { // Firefox.
|
|||
result.browser = 'firefox'; |
|||
result.version = extractVersion(navigator.userAgent, |
|||
/Firefox\/(\d+)\./, 1); |
|||
} else if (navigator.webkitGetUserMedia || |
|||
(window.isSecureContext === false && window.webkitRTCPeerConnection && |
|||
!window.RTCIceGatherer)) { |
|||
// Chrome, Chromium, Webview, Opera.
|
|||
// Version matches Chrome/WebRTC version.
|
|||
// Chrome 74 removed webkitGetUserMedia on http as well so we need the
|
|||
// more complicated fallback to webkitRTCPeerConnection.
|
|||
result.browser = 'chrome'; |
|||
result.version = extractVersion(navigator.userAgent, |
|||
/Chrom(e|ium)\/(\d+)\./, 2); |
|||
} else if (window.RTCPeerConnection && |
|||
navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { // Safari.
|
|||
result.browser = 'safari'; |
|||
result.version = extractVersion(navigator.userAgent, |
|||
/AppleWebKit\/(\d+)\./, 1); |
|||
result.supportsUnifiedPlan = window.RTCRtpTransceiver && |
|||
'currentDirection' in window.RTCRtpTransceiver.prototype; |
|||
} else { // Default fallthrough: not supported.
|
|||
result.browser = 'Not a supported browser.'; |
|||
return result; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* Checks if something is an object. |
|||
* |
|||
* @param {*} val The something you want to check. |
|||
* @return true if val is an object, false otherwise. |
|||
*/ |
|||
function isObject(val) { |
|||
return Object.prototype.toString.call(val) === '[object Object]'; |
|||
} |
|||
|
|||
/** |
|||
* Remove all empty objects and undefined values |
|||
* from a nested object -- an enhanced and vanilla version |
|||
* of Lodash's `compact`. |
|||
*/ |
|||
export function compactObject(data) { |
|||
if (!isObject(data)) { |
|||
return data; |
|||
} |
|||
|
|||
return Object.keys(data).reduce(function(accumulator, key) { |
|||
const isObj = isObject(data[key]); |
|||
const value = isObj ? compactObject(data[key]) : data[key]; |
|||
const isEmptyObject = isObj && !Object.keys(value).length; |
|||
if (value === undefined || isEmptyObject) { |
|||
return accumulator; |
|||
} |
|||
return Object.assign(accumulator, {[key]: value}); |
|||
}, {}); |
|||
} |
|||
|
|||
/* iterates the stats graph recursively. */ |
|||
export function walkStats(stats, base, resultSet) { |
|||
if (!base || resultSet.has(base.id)) { |
|||
return; |
|||
} |
|||
resultSet.set(base.id, base); |
|||
Object.keys(base).forEach(name => { |
|||
if (name.endsWith('Id')) { |
|||
walkStats(stats, stats.get(base[name]), resultSet); |
|||
} else if (name.endsWith('Ids')) { |
|||
base[name].forEach(id => { |
|||
walkStats(stats, stats.get(id), resultSet); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/* filter getStats for a sender/receiver track. */ |
|||
export function filterStats(result, track, outbound) { |
|||
const streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; |
|||
const filteredResult = new Map(); |
|||
if (track === null) { |
|||
return filteredResult; |
|||
} |
|||
const trackStats = []; |
|||
result.forEach(value => { |
|||
if (value.type === 'track' && |
|||
value.trackIdentifier === track.id) { |
|||
trackStats.push(value); |
|||
} |
|||
}); |
|||
trackStats.forEach(trackStat => { |
|||
result.forEach(stats => { |
|||
if (stats.type === streamStatsType && stats.trackId === trackStat.id) { |
|||
walkStats(result, stats, filteredResult); |
|||
} |
|||
}); |
|||
}); |
|||
return filteredResult; |
|||
} |
|||
|
|||
@ -0,0 +1,20 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<script> |
|||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || |
|||
CSS.supports('top: constant(a)')) |
|||
document.write( |
|||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + |
|||
(coverSupport ? ', viewport-fit=cover' : '') + '" />') |
|||
</script> |
|||
<title></title> |
|||
<!--preload-links--> |
|||
<!--app-context--> |
|||
</head> |
|||
<body> |
|||
<div id="app"><!--app-html--></div> |
|||
<script type="module" src="/main.js"></script> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,54 @@ |
|||
|
|||
// #ifdef H5
|
|||
import quill from "quill"; |
|||
window.Quill = quill; |
|||
// #endif
|
|||
|
|||
// #ifndef VUE3
|
|||
import Vue from 'vue' |
|||
import App from './App' |
|||
|
|||
Vue.config.productionTip = false |
|||
|
|||
import cuCustom from './components/cu-custom' |
|||
Vue.component('cu-custom',cuCustom) |
|||
|
|||
|
|||
App.mpType = 'app' |
|||
|
|||
const app = new Vue({ |
|||
...App |
|||
}) |
|||
app.$mount() |
|||
// #endif
|
|||
|
|||
|
|||
|
|||
// #ifdef VUE3
|
|||
import { createSSRApp } from 'vue' |
|||
import App from './App.vue' |
|||
import cuCustom from './components/cu-custom' |
|||
import util from '@/utils/utils.js' |
|||
import store from './store' |
|||
import '@/utils/request' |
|||
import api from '@/api/index.js'; |
|||
import socketIO from '@/common/socket.js'; |
|||
import Empty from "@/components/Empty.vue" //通用空状态
|
|||
import Tags from "@/components/Tags.vue" //通用标签
|
|||
|
|||
export function createApp() { |
|||
const app = createSSRApp(App) |
|||
app.config.globalProperties.appStatus=true; |
|||
app.config.globalProperties.$util = util; |
|||
app.config.globalProperties.$api = api; |
|||
app.config.globalProperties.$store = store; |
|||
app.config.globalProperties.socketIo = new socketIO() |
|||
app.component('cu-custom',cuCustom) |
|||
app.component('Empty',Empty) |
|||
app.component('Tags',Tags) |
|||
app.use(store) |
|||
return { |
|||
app |
|||
} |
|||
} |
|||
// #endif
|
|||
@ -0,0 +1,210 @@ |
|||
{ |
|||
"name" : "Raingad-IM", |
|||
"appid" : "__UNI__A902A60", |
|||
"description" : "聊天小应用", |
|||
"versionName" : "5.5.2", |
|||
"versionCode" : 20250107, |
|||
"transformPx" : false, |
|||
"app-plus" : { |
|||
/* 5+App特有相关 */ |
|||
"usingComponents" : true, |
|||
"nvueCompiler" : "uni-app", |
|||
"compilerVersion" : 3, |
|||
"nvueStyleCompiler" : "uni-app", |
|||
"splashscreen" : { |
|||
"alwaysShowBeforeRender" : true, |
|||
"waiting" : true, |
|||
"autoclose" : true, |
|||
"delay" : 0 |
|||
}, |
|||
"modules" : { |
|||
"VideoPlayer" : {}, |
|||
"Record" : {}, |
|||
"UIWebview" : {}, |
|||
"Camera" : {}, |
|||
"Barcode" : {}, |
|||
"Push" : {}, |
|||
"SQLite" : {} |
|||
}, |
|||
/* 模块配置 */ |
|||
"distribute" : { |
|||
/* 应用发布信息 */ |
|||
"android" : { |
|||
/* android打包配置 */ |
|||
"permissions" : [ |
|||
"<uses-feature android:name=\"android.hardware.camera\"/>", |
|||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>", |
|||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>", |
|||
"<uses-permission android:name=\"android.permission.CAMERA\"/>", |
|||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", |
|||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", |
|||
"<uses-permission android:name=\"android.permission.MANAGE_APP_TOKENS\"/>", |
|||
"<uses-permission android:name=\"android.permission.MANAGE_DOCUMENTS\"/>", |
|||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", |
|||
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>", |
|||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>", |
|||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>", |
|||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", |
|||
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>", |
|||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>", |
|||
"<uses-permission android:name=\"android.permission.INSTALL_PACKAGES\"/>", |
|||
"<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>" |
|||
], |
|||
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ], |
|||
"minSdkVersion" : 21, |
|||
"targetSdkVersion" : "" |
|||
}, |
|||
"ios" : { |
|||
"dSYMs" : false, |
|||
"privacyDescription" : { |
|||
"NSPhotoLibraryUsageDescription" : "需要发送图片消息", |
|||
"NSPhotoLibraryAddUsageDescription" : "需要下载图片或者视频", |
|||
"NSCameraUsageDescription" : "需要拍照照片或者视频发送给好友", |
|||
"NSMicrophoneUsageDescription" : "需要和好友语音通话或者发送语音消息", |
|||
"NSLocationWhenInUseUsageDescription" : "APP将要获取你的位置信息,用于保持APP的活跃,并不会记录您的位置。是否允许?", |
|||
"NSLocationAlwaysUsageDescription" : "App将要在后台持续获取您的位置,用于保持APP的活跃,并不会记录您的位置。是否允许?", |
|||
"NSLocationAlwaysAndWhenInUseUsageDescription" : "APP将要获取你的位置信息,用于保持APP的活跃,并不会记录您的位置。是否允许?" |
|||
}, |
|||
"UIBackgroundModes" : "audio,location" |
|||
}, |
|||
"huawei" : { |
|||
"permissions" : [ |
|||
"ohos.permission.GET_NETWORK_INFO" // HarmonyOS 网络状态权限 |
|||
] |
|||
}, |
|||
/* ios打包配置 */ |
|||
"sdkConfigs" : { |
|||
"ad" : {}, |
|||
"push" : {}, |
|||
"maps" : { |
|||
"amap" : { |
|||
"name" : "", |
|||
"appkey_ios" : "", |
|||
"appkey_android" : "" |
|||
} |
|||
}, |
|||
"geolocation" : { |
|||
"system" : { |
|||
"__platform__" : [ "ios", "android" ] |
|||
} |
|||
} |
|||
}, |
|||
"icons" : { |
|||
"android" : { |
|||
"hdpi" : "unpackage/res/icons/72x72.png", |
|||
"xhdpi" : "unpackage/res/icons/96x96.png", |
|||
"xxhdpi" : "unpackage/res/icons/144x144.png", |
|||
"xxxhdpi" : "unpackage/res/icons/192x192.png" |
|||
}, |
|||
"ios" : { |
|||
"appstore" : "unpackage/res/icons/1024x1024.png", |
|||
"ipad" : { |
|||
"app" : "unpackage/res/icons/76x76.png", |
|||
"app@2x" : "unpackage/res/icons/152x152.png", |
|||
"notification" : "unpackage/res/icons/20x20.png", |
|||
"notification@2x" : "unpackage/res/icons/40x40.png", |
|||
"proapp@2x" : "unpackage/res/icons/167x167.png", |
|||
"settings" : "unpackage/res/icons/29x29.png", |
|||
"settings@2x" : "unpackage/res/icons/58x58.png", |
|||
"spotlight" : "unpackage/res/icons/40x40.png", |
|||
"spotlight@2x" : "unpackage/res/icons/80x80.png" |
|||
}, |
|||
"iphone" : { |
|||
"app@2x" : "unpackage/res/icons/120x120.png", |
|||
"app@3x" : "unpackage/res/icons/180x180.png", |
|||
"notification@2x" : "unpackage/res/icons/40x40.png", |
|||
"notification@3x" : "unpackage/res/icons/60x60.png", |
|||
"settings@2x" : "unpackage/res/icons/58x58.png", |
|||
"settings@3x" : "unpackage/res/icons/87x87.png", |
|||
"spotlight@2x" : "unpackage/res/icons/80x80.png", |
|||
"spotlight@3x" : "unpackage/res/icons/120x120.png" |
|||
} |
|||
} |
|||
}, |
|||
"splashscreen" : { |
|||
"useOriginalMsgbox" : true |
|||
} |
|||
}, |
|||
"nativePlugins" : { |
|||
"lemonjk-FileSelect" : { |
|||
"__plugin_info__" : { |
|||
"name" : "FileSelect", |
|||
"description" : "文件选取插件", |
|||
"platforms" : "Android,iOS", |
|||
"url" : "", |
|||
"android_package_name" : "", |
|||
"ios_bundle_id" : "", |
|||
"isCloud" : false, |
|||
"bought" : -1, |
|||
"pid" : "", |
|||
"parameters" : {} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
// "Ba-KeepAlive" : { |
|||
// "__plugin_info__" : { |
|||
// "name" : "安卓保活(采用多种主流技术) Ba-KeepAlive", |
|||
// "description" : "原生保活插件,支持市面上大部分机型,Android4.4到13.0 。为定位、推送、websocket、定时任务、蓝牙、聊天等保驾护航(**注意:**不保证支持所有机型和场景,建议先试用再购买)", |
|||
// "platforms" : "Android", |
|||
// "url" : "https://ext.dcloud.net.cn/plugin?id=9423", |
|||
// "android_package_name" : "uni.UNID1E78EC", |
|||
// "ios_bundle_id" : "", |
|||
// "isCloud" : true, |
|||
// "bought" : 1, |
|||
// "pid" : "9423", |
|||
// "parameters" : {} |
|||
// } |
|||
// } |
|||
/* SDK配置 */ |
|||
"quickapp" : {}, |
|||
/* 快应用特有相关 */ |
|||
"mp-weixin" : { |
|||
/* 小程序特有相关 */ |
|||
"appid" : "wxd36ac7a23fbfcfea", |
|||
"setting" : { |
|||
"urlCheck" : false, |
|||
"minified" : true, |
|||
"ignoreDevUnusedFiles" : false, |
|||
"ignoreUploadUnusedFiles" : false |
|||
}, |
|||
"lazyCodeLoading" : "requiredComponents", |
|||
"usingComponents" : true, |
|||
"permission" : {}, |
|||
"unipush" : { |
|||
"enable" : false |
|||
}, |
|||
"libVersion" : "latest" |
|||
}, |
|||
"vueVersion" : "3", |
|||
"h5" : { |
|||
"router" : { |
|||
"mode" : "hash", |
|||
"base" : "./" |
|||
}, |
|||
"devServer" : { |
|||
"https" : false, |
|||
"port" : "" |
|||
}, |
|||
"title" : "raingad-IM", |
|||
"unipush" : { |
|||
"enable" : false |
|||
}, |
|||
"sdkConfigs" : { |
|||
"maps" : { |
|||
"amap" : { |
|||
"key" : "", |
|||
"securityJsCode" : "", |
|||
"serviceHost" : "" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"fallbackLocale" : "zh-Hans" |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
import config from '@/common/config.js' |
|||
export const chat = { |
|||
data() { |
|||
return { |
|||
network_log:'', |
|||
apiUrl: config.apiUrl, |
|||
emojiMap:'' |
|||
} |
|||
}, |
|||
created: function() { |
|||
this.network_log = uni.getStorageSync('network_log') |
|||
}, |
|||
methods: { |
|||
// 播放视频,禁止多个同时播放
|
|||
handlePlay (item,iteme) { |
|||
// console.log(item);
|
|||
if(this.network_log == 'none'){ |
|||
uni.navigateTo({ |
|||
url: '/pages/message/video?name='+item.fileName+'&src='+encodeURI(iteme.path), |
|||
animationType:"slide-in-bottom" |
|||
}); |
|||
}else if(item.src||item.url){ |
|||
const parts = item.src?item.src.split('/'):item.url.split('/'); |
|||
let lastPart = parts.pop() || parts.pop() || ''; |
|||
const url = item.src?this.apiUrl+item.src:item.url |
|||
uni.navigateTo({ |
|||
url: '/pages/message/video?name='+lastPart+'&src='+encodeURI(url), |
|||
animationType:"slide-in-bottom", |
|||
}); |
|||
}else{ |
|||
uni.navigateTo({ |
|||
url: '/pages/message/video?name='+item.fileName+'&src='+encodeURI(item.content), |
|||
animationType:"slide-in-bottom" |
|||
}); |
|||
} |
|||
}, |
|||
// 文件预览
|
|||
previewFile(item){ |
|||
if(this.islongPress){ |
|||
return; |
|||
} |
|||
this.curMsg=item; |
|||
this.modelName='preview'; |
|||
}, |
|||
preview(val){ |
|||
let item=this.curMsg; |
|||
let audioExt=['mp3','wav','acc']; |
|||
let extension = item.content.split('.').pop().toLowerCase(); |
|||
if(audioExt.includes(extension) || val==2){ |
|||
uni.navigateTo({ |
|||
url: '/pages/mine/webview?title=文件预览&src='+encodeURIComponent(item.preview), |
|||
animationType:"slide-in-bottom" |
|||
}); |
|||
return; |
|||
} |
|||
// #ifdef APP-PLUS || MP-WEIXIN
|
|||
let exts=['doc', 'xls', 'ppt', 'pdf', 'docx', 'xlsx', 'pptx']; |
|||
if(exts.includes(extension)){ |
|||
uni.showLoading({title: '文件加载中'}); |
|||
uni.downloadFile({ |
|||
url: item.content, |
|||
success: function (res) { |
|||
uni.hideLoading(); |
|||
var filePath = res.tempFilePath; |
|||
uni.openDocument({ |
|||
filePath: filePath, |
|||
showMenu: true, |
|||
success: function (res) { |
|||
console.info('打开文档成功'); |
|||
} |
|||
}); |
|||
}, |
|||
fail() { |
|||
uni.hideLoading(); |
|||
} |
|||
}); |
|||
}else{ |
|||
uni.showToast({ |
|||
title:'该文件不支持预览!', |
|||
icon:'none' |
|||
}) |
|||
} |
|||
// #endif
|
|||
|
|||
// #ifdef H5
|
|||
const tempLink = document.createElement("a"); |
|||
tempLink.style.display = "none"; |
|||
tempLink.href = item.download; |
|||
tempLink.setAttribute("download", item.fileName); |
|||
tempLink.setAttribute("target", "_blank"); |
|||
document.body.appendChild(tempLink); |
|||
tempLink.click(); |
|||
document.body.removeChild(tempLink); |
|||
// #endif
|
|||
}, |
|||
// 图片预览
|
|||
showImgs : function(e){ |
|||
var imgs = []; |
|||
var imgs1 = []; |
|||
var imgsCurrent = e.is_view==0?e.img:e.currentTarget.dataset.img; |
|||
for (var i = 0; i < this.messageList.length; i++) { |
|||
if (this.messageList[i].type == 'image' || this.messageList[i].type == 'emoji') { |
|||
imgs.push(this.messageList[i].content); |
|||
} |
|||
} |
|||
|
|||
if(e.is_view==0){ |
|||
imgs1.push(imgsCurrent); |
|||
uni.previewImage({urls : imgs1}); |
|||
}else{ |
|||
uni.previewImage({urls : imgs, current : imgsCurrent}); |
|||
} |
|||
}, |
|||
openLocation(item){ |
|||
uni.openLocation({ |
|||
latitude: item.latitude, |
|||
longitude: item.longitude, |
|||
success: function () { |
|||
console.log('success'); |
|||
} |
|||
}); |
|||
}, |
|||
// 打开用户详情
|
|||
openContact(item){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+item.id |
|||
}) |
|||
}, |
|||
// 自动解析消息中的表情
|
|||
emojiToHtml(str){ |
|||
let emojiMap=this.emojiMap; |
|||
return str.replace(/\[!(\w+)\]/gi, function (str, match) { |
|||
var file = match; |
|||
return emojiMap[file] ? "<img class='mr-5' style=\"width:18px;height:18px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]"); |
|||
}); |
|||
}, |
|||
fileSize(size){ |
|||
return this.$util.getFileSize(size); |
|||
}, |
|||
sendTime:function(mstime){ |
|||
return this.$util.timeFormat(mstime); |
|||
}, |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
{ |
|||
"name": "FileSelect", |
|||
"id": "lemonjk-FileSelect", |
|||
"version": "3.1.0", |
|||
"description": "文件选取插件", |
|||
"_dp_type": "nativeplugin", |
|||
"_dp_nativeplugin": { |
|||
"android": { |
|||
"plugins": [{ |
|||
"type": "module", |
|||
"name": "lemonjk-FileSelect", |
|||
"class": "com.lemonjk.fileselect.FileSelectModule" |
|||
}], |
|||
"compileOptions": { |
|||
"sourceCompatibility": "1.8", |
|||
"targetCompatibility": "1.8" |
|||
}, |
|||
"dependencies": [ |
|||
"androidx.documentfile:documentfile:1.0.1" |
|||
], |
|||
"integrateType": "aar", |
|||
"permissions": [ |
|||
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>" |
|||
], |
|||
"abis": [ |
|||
"armeabi-v7a", |
|||
"arm64-v8a" |
|||
], |
|||
"minSdkVersion": 21 |
|||
}, |
|||
"ios": { |
|||
"plugins": [ |
|||
{ |
|||
"type": "module", |
|||
"name": "lemonjk-FileSelect", |
|||
"class": "TestModule" |
|||
} |
|||
], |
|||
"integrateType": "framework", |
|||
"deploymentTarget": "11.0" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,412 @@ |
|||
{ |
|||
"name": "Raingad-IM", |
|||
"version": "5.5.2", |
|||
"lockfileVersion": 1, |
|||
"requires": true, |
|||
"dependencies": { |
|||
"@babel/runtime": { |
|||
"version": "7.27.1", |
|||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.1.tgz", |
|||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==" |
|||
}, |
|||
"@vue/devtools-api": { |
|||
"version": "6.6.4", |
|||
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", |
|||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" |
|||
}, |
|||
"argparse": { |
|||
"version": "2.0.1", |
|||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", |
|||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" |
|||
}, |
|||
"call-bind": { |
|||
"version": "1.0.8", |
|||
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", |
|||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", |
|||
"requires": { |
|||
"call-bind-apply-helpers": "^1.0.0", |
|||
"es-define-property": "^1.0.0", |
|||
"get-intrinsic": "^1.2.4", |
|||
"set-function-length": "^1.2.2" |
|||
} |
|||
}, |
|||
"call-bind-apply-helpers": { |
|||
"version": "1.0.2", |
|||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", |
|||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", |
|||
"requires": { |
|||
"es-errors": "^1.3.0", |
|||
"function-bind": "^1.1.2" |
|||
} |
|||
}, |
|||
"call-bound": { |
|||
"version": "1.0.4", |
|||
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", |
|||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", |
|||
"requires": { |
|||
"call-bind-apply-helpers": "^1.0.2", |
|||
"get-intrinsic": "^1.3.0" |
|||
} |
|||
}, |
|||
"clone": { |
|||
"version": "2.1.2", |
|||
"resolved": "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz", |
|||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" |
|||
}, |
|||
"copy-text-to-clipboard": { |
|||
"version": "3.2.0", |
|||
"resolved": "https://registry.npmmirror.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz", |
|||
"integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==" |
|||
}, |
|||
"core-js": { |
|||
"version": "3.42.0", |
|||
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.42.0.tgz", |
|||
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==" |
|||
}, |
|||
"crypto-js": { |
|||
"version": "4.2.0", |
|||
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz", |
|||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" |
|||
}, |
|||
"deep-equal": { |
|||
"version": "1.1.2", |
|||
"resolved": "https://registry.npmmirror.com/deep-equal/-/deep-equal-1.1.2.tgz", |
|||
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", |
|||
"requires": { |
|||
"is-arguments": "^1.1.1", |
|||
"is-date-object": "^1.0.5", |
|||
"is-regex": "^1.1.4", |
|||
"object-is": "^1.1.5", |
|||
"object-keys": "^1.1.1", |
|||
"regexp.prototype.flags": "^1.5.1" |
|||
} |
|||
}, |
|||
"define-data-property": { |
|||
"version": "1.1.4", |
|||
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", |
|||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", |
|||
"requires": { |
|||
"es-define-property": "^1.0.0", |
|||
"es-errors": "^1.3.0", |
|||
"gopd": "^1.0.1" |
|||
} |
|||
}, |
|||
"define-properties": { |
|||
"version": "1.2.1", |
|||
"resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", |
|||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", |
|||
"requires": { |
|||
"define-data-property": "^1.0.1", |
|||
"has-property-descriptors": "^1.0.0", |
|||
"object-keys": "^1.1.1" |
|||
} |
|||
}, |
|||
"dunder-proto": { |
|||
"version": "1.0.1", |
|||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", |
|||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", |
|||
"requires": { |
|||
"call-bind-apply-helpers": "^1.0.1", |
|||
"es-errors": "^1.3.0", |
|||
"gopd": "^1.2.0" |
|||
} |
|||
}, |
|||
"entities": { |
|||
"version": "4.5.0", |
|||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", |
|||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" |
|||
}, |
|||
"es-define-property": { |
|||
"version": "1.0.1", |
|||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", |
|||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" |
|||
}, |
|||
"es-errors": { |
|||
"version": "1.3.0", |
|||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", |
|||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" |
|||
}, |
|||
"es-object-atoms": { |
|||
"version": "1.1.1", |
|||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", |
|||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", |
|||
"requires": { |
|||
"es-errors": "^1.3.0" |
|||
} |
|||
}, |
|||
"eventemitter3": { |
|||
"version": "2.0.3", |
|||
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-2.0.3.tgz", |
|||
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" |
|||
}, |
|||
"extend": { |
|||
"version": "3.0.2", |
|||
"resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", |
|||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" |
|||
}, |
|||
"fast-diff": { |
|||
"version": "1.1.2", |
|||
"resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.1.2.tgz", |
|||
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" |
|||
}, |
|||
"function-bind": { |
|||
"version": "1.1.2", |
|||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", |
|||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" |
|||
}, |
|||
"functions-have-names": { |
|||
"version": "1.2.3", |
|||
"resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", |
|||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" |
|||
}, |
|||
"get-intrinsic": { |
|||
"version": "1.3.0", |
|||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", |
|||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", |
|||
"requires": { |
|||
"call-bind-apply-helpers": "^1.0.2", |
|||
"es-define-property": "^1.0.1", |
|||
"es-errors": "^1.3.0", |
|||
"es-object-atoms": "^1.1.1", |
|||
"function-bind": "^1.1.2", |
|||
"get-proto": "^1.0.1", |
|||
"gopd": "^1.2.0", |
|||
"has-symbols": "^1.1.0", |
|||
"hasown": "^2.0.2", |
|||
"math-intrinsics": "^1.1.0" |
|||
} |
|||
}, |
|||
"get-proto": { |
|||
"version": "1.0.1", |
|||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", |
|||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", |
|||
"requires": { |
|||
"dunder-proto": "^1.0.1", |
|||
"es-object-atoms": "^1.0.0" |
|||
} |
|||
}, |
|||
"gopd": { |
|||
"version": "1.2.0", |
|||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", |
|||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" |
|||
}, |
|||
"has-property-descriptors": { |
|||
"version": "1.0.2", |
|||
"resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", |
|||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", |
|||
"requires": { |
|||
"es-define-property": "^1.0.0" |
|||
} |
|||
}, |
|||
"has-symbols": { |
|||
"version": "1.1.0", |
|||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", |
|||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" |
|||
}, |
|||
"has-tostringtag": { |
|||
"version": "1.0.2", |
|||
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", |
|||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", |
|||
"requires": { |
|||
"has-symbols": "^1.0.3" |
|||
} |
|||
}, |
|||
"hasown": { |
|||
"version": "2.0.2", |
|||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", |
|||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", |
|||
"requires": { |
|||
"function-bind": "^1.1.2" |
|||
} |
|||
}, |
|||
"highlight.js": { |
|||
"version": "11.11.1", |
|||
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", |
|||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" |
|||
}, |
|||
"is-arguments": { |
|||
"version": "1.2.0", |
|||
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz", |
|||
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", |
|||
"requires": { |
|||
"call-bound": "^1.0.2", |
|||
"has-tostringtag": "^1.0.2" |
|||
} |
|||
}, |
|||
"is-date-object": { |
|||
"version": "1.1.0", |
|||
"resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", |
|||
"integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", |
|||
"requires": { |
|||
"call-bound": "^1.0.2", |
|||
"has-tostringtag": "^1.0.2" |
|||
} |
|||
}, |
|||
"is-regex": { |
|||
"version": "1.2.1", |
|||
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", |
|||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", |
|||
"requires": { |
|||
"call-bound": "^1.0.2", |
|||
"gopd": "^1.2.0", |
|||
"has-tostringtag": "^1.0.2", |
|||
"hasown": "^2.0.2" |
|||
} |
|||
}, |
|||
"jsqr": { |
|||
"version": "1.4.0", |
|||
"resolved": "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz", |
|||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==" |
|||
}, |
|||
"linkify-it": { |
|||
"version": "5.0.0", |
|||
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz", |
|||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", |
|||
"requires": { |
|||
"uc.micro": "^2.0.0" |
|||
} |
|||
}, |
|||
"markdown-it": { |
|||
"version": "14.1.0", |
|||
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz", |
|||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", |
|||
"requires": { |
|||
"argparse": "^2.0.1", |
|||
"entities": "^4.4.0", |
|||
"linkify-it": "^5.0.0", |
|||
"mdurl": "^2.0.0", |
|||
"punycode.js": "^2.3.1", |
|||
"uc.micro": "^2.1.0" |
|||
} |
|||
}, |
|||
"math-intrinsics": { |
|||
"version": "1.1.0", |
|||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", |
|||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" |
|||
}, |
|||
"mdurl": { |
|||
"version": "2.0.0", |
|||
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz", |
|||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" |
|||
}, |
|||
"mutation-observer": { |
|||
"version": "1.0.3", |
|||
"resolved": "https://registry.npmmirror.com/mutation-observer/-/mutation-observer-1.0.3.tgz", |
|||
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==" |
|||
}, |
|||
"object-is": { |
|||
"version": "1.1.6", |
|||
"resolved": "https://registry.npmmirror.com/object-is/-/object-is-1.1.6.tgz", |
|||
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", |
|||
"requires": { |
|||
"call-bind": "^1.0.7", |
|||
"define-properties": "^1.2.1" |
|||
} |
|||
}, |
|||
"object-keys": { |
|||
"version": "1.1.1", |
|||
"resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", |
|||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" |
|||
}, |
|||
"parchment": { |
|||
"version": "1.1.4", |
|||
"resolved": "https://registry.npmmirror.com/parchment/-/parchment-1.1.4.tgz", |
|||
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" |
|||
}, |
|||
"pinia": { |
|||
"version": "2.3.1", |
|||
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", |
|||
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", |
|||
"requires": { |
|||
"@vue/devtools-api": "^6.6.3", |
|||
"vue-demi": "^0.14.10" |
|||
} |
|||
}, |
|||
"punycode.js": { |
|||
"version": "2.3.1", |
|||
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", |
|||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" |
|||
}, |
|||
"quill": { |
|||
"version": "1.3.7", |
|||
"resolved": "https://registry.npmmirror.com/quill/-/quill-1.3.7.tgz", |
|||
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", |
|||
"requires": { |
|||
"clone": "^2.1.1", |
|||
"deep-equal": "^1.0.1", |
|||
"eventemitter3": "^2.0.3", |
|||
"extend": "^3.0.2", |
|||
"parchment": "^1.1.4", |
|||
"quill-delta": "^3.6.2" |
|||
} |
|||
}, |
|||
"quill-delta": { |
|||
"version": "3.6.3", |
|||
"resolved": "https://registry.npmmirror.com/quill-delta/-/quill-delta-3.6.3.tgz", |
|||
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", |
|||
"requires": { |
|||
"deep-equal": "^1.0.1", |
|||
"extend": "^3.0.2", |
|||
"fast-diff": "1.1.2" |
|||
} |
|||
}, |
|||
"regexp.prototype.flags": { |
|||
"version": "1.5.4", |
|||
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", |
|||
"integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", |
|||
"requires": { |
|||
"call-bind": "^1.0.8", |
|||
"define-properties": "^1.2.1", |
|||
"es-errors": "^1.3.0", |
|||
"get-proto": "^1.0.1", |
|||
"gopd": "^1.2.0", |
|||
"set-function-name": "^2.0.2" |
|||
} |
|||
}, |
|||
"set-function-length": { |
|||
"version": "1.2.2", |
|||
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", |
|||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", |
|||
"requires": { |
|||
"define-data-property": "^1.1.4", |
|||
"es-errors": "^1.3.0", |
|||
"function-bind": "^1.1.2", |
|||
"get-intrinsic": "^1.2.4", |
|||
"gopd": "^1.0.1", |
|||
"has-property-descriptors": "^1.0.2" |
|||
} |
|||
}, |
|||
"set-function-name": { |
|||
"version": "2.0.2", |
|||
"resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", |
|||
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", |
|||
"requires": { |
|||
"define-data-property": "^1.1.4", |
|||
"es-errors": "^1.3.0", |
|||
"functions-have-names": "^1.2.3", |
|||
"has-property-descriptors": "^1.0.2" |
|||
} |
|||
}, |
|||
"uc.micro": { |
|||
"version": "2.1.0", |
|||
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", |
|||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" |
|||
}, |
|||
"vconsole": { |
|||
"version": "3.15.1", |
|||
"resolved": "https://registry.npmmirror.com/vconsole/-/vconsole-3.15.1.tgz", |
|||
"integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==", |
|||
"requires": { |
|||
"@babel/runtime": "^7.17.2", |
|||
"copy-text-to-clipboard": "^3.0.1", |
|||
"core-js": "^3.11.0", |
|||
"mutation-observer": "^1.0.3" |
|||
} |
|||
}, |
|||
"vue-demi": { |
|||
"version": "0.14.10", |
|||
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", |
|||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
{ |
|||
"name": "Raingad-IM", |
|||
"version": "5.5.2", |
|||
"description": "一款基于vue3.0的uniapp即时聊天工具", |
|||
"logo": "/static/image/logo.png", |
|||
"author": "Raingad", |
|||
"main": "main.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://gitee.com/raingad/instant-chat-uniapp.git" |
|||
}, |
|||
"keywords": [], |
|||
"license": "ISC", |
|||
"dependencies": { |
|||
"crypto-js": "^4.2.0", |
|||
"highlight.js": "^11.11.1", |
|||
"jsqr": "^1.4.0", |
|||
"markdown-it": "^14.1.0", |
|||
"pinia": "^2.0.23", |
|||
"quill": "^1.3.7", |
|||
"vconsole": "^3.15.1" |
|||
} |
|||
} |
|||
@ -0,0 +1,247 @@ |
|||
{ |
|||
"pages": [ |
|||
{ |
|||
"path": "pages/index/index", |
|||
"style": { |
|||
"navigationBarTitleText": "首页" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/chat", |
|||
"style": { |
|||
"navigationBarTitleText": "聊天" |
|||
// "enablePullDownRefresh": true |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/emoji", |
|||
"style": { |
|||
"navigationBarTitleText": "表情" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/contacts/index", |
|||
"style": { |
|||
"navigationBarTitleText": "联系人" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/detail", |
|||
"style": { |
|||
"navigationBarTitleText": "聊天信息" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/record", |
|||
"style": { |
|||
"navigationBarTitleText": "聊天记录" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/contacts/detail", |
|||
"style": { |
|||
"navigationBarTitleText": "用户信息" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/contacts/friend", |
|||
"style": { |
|||
"navigationBarTitleText": "新朋友" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/contacts/search", |
|||
"style": { |
|||
"navigationBarTitleText": "搜索朋友" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/login/index", |
|||
"style": { |
|||
"navigationBarTitleText": "登录" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/login/register", |
|||
"style": { |
|||
"navigationBarTitleText": "注册" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/call", |
|||
"style": { |
|||
"navigationBarTitleText": "通话" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/video", |
|||
"style": { |
|||
"navigationBarTitleText": "视频播放" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/index/userSelection", |
|||
"style": { |
|||
"navigationBarTitleText": "用户选择" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/group/groupUser", |
|||
"style": { |
|||
"navigationBarTitleText": "群聊成员管理" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/index/qrcode", |
|||
"style": { |
|||
"navigationBarTitleText": "群二维码" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/message/group/info", |
|||
"style": { |
|||
"navigationBarTitleText": "群信息" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/contacts/group", |
|||
"style": { |
|||
"navigationBarTitleText": "群聊列表" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/index/search", |
|||
"style": { |
|||
"navigationBarTitleText": "搜索" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/index/scan", |
|||
"style": { |
|||
"navigationBarTitleText": "扫描" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/mine/webview", |
|||
"style": { |
|||
"navigationBarTitleText": "浏览器", |
|||
"navigationStyle":"default" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/compass/moments", |
|||
"style": { |
|||
"navigationBarTitleText": "朋友圈" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/mine/profile", |
|||
"style": { |
|||
"navigationBarTitleText": "个人信息" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/mine/secure", |
|||
"style": { |
|||
"navigationBarTitleText": "安全" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/mine/about", |
|||
"style": { |
|||
"navigationBarTitleText": "关于" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/mine/setting", |
|||
"style": { |
|||
"navigationBarTitleText": "设置" |
|||
} |
|||
}, |
|||
{ |
|||
"path": "pages/mine/doc", |
|||
"style": { |
|||
"navigationBarTitleText": "帮助文档" |
|||
} |
|||
}, |
|||
{ |
|||
"path" : "pages/compass/sendtoMoments", |
|||
"style" : { |
|||
"navigationBarTitleText" : "" |
|||
} |
|||
}, |
|||
{ |
|||
"path" : "pages/compass/personalcircleoffriends", |
|||
"style" : |
|||
{ |
|||
"navigationBarTitleText" : "" |
|||
} |
|||
}, |
|||
{ |
|||
"path" : "pages/compass/friendscircledetails", |
|||
"style" : |
|||
{ |
|||
"navigationBarTitleText" : "" |
|||
} |
|||
}, |
|||
{ |
|||
"path" : "pages/mapselect/mapselect", |
|||
"style" : |
|||
{ |
|||
"navigationBarTitleText" : "" |
|||
} |
|||
}, |
|||
{ |
|||
"path" : "pages/login/404", |
|||
"style" : |
|||
{ |
|||
"navigationBarTitleText" : "" |
|||
} |
|||
}, |
|||
{ |
|||
"path" : "pages/compass/Informationdetails", |
|||
"style" : |
|||
{ |
|||
"navigationBarTitleText" : "" |
|||
} |
|||
}, |
|||
{ |
|||
"path" : "pages/contacts/blacklist", |
|||
"style" : |
|||
{ |
|||
"navigationBarTitleText" : "" |
|||
} |
|||
} |
|||
], |
|||
"globalStyle": { |
|||
"navigationStyle": "custom", |
|||
"navigationBarTextStyle": "black", |
|||
"navigationBarTitleText": "uni-app", |
|||
"navigationBarBackgroundColor": "#F8F8F8", |
|||
"backgroundColor": "#F8F8F8", |
|||
"app-plus": { |
|||
"background": "#efeff4" |
|||
} |
|||
}, |
|||
"tabBar": { |
|||
"custom": true, |
|||
"color": "#333", |
|||
"selectedColor": "#1AAD19", |
|||
"borderStyle": "black", |
|||
"backgroundColor": "#FFFFFF", |
|||
"list": [{ |
|||
"pagePath": "pages/index/index", |
|||
"iconPath": "static/image/tabbar/demo.png", |
|||
"selectedIconPath": "static/image/tabbar/demo.png", |
|||
"text": "首页" |
|||
}, |
|||
|
|||
{ |
|||
"pagePath": "pages/contacts/index", |
|||
"iconPath": "static/image/tabbar/demo.png", |
|||
"selectedIconPath": "static/image/tabbar/demo.png", |
|||
"text": "通讯录" |
|||
} |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
<template> |
|||
<cu-custom bgColor="bg-white" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>消息</template> |
|||
</cu-custom> |
|||
<!-- #ifdef APP-PLUS --> |
|||
<scroll-view scroll-y style="height: 88vh;" @scrolltolower="scrollbot"> |
|||
<!-- #endif --> |
|||
<!-- #ifdef H5 --> |
|||
<scroll-view scroll-y style="height: 93vh;" @scrolltolower="scrollbot"> |
|||
<!-- #endif --> |
|||
<view v-for="(item,index) in filteredList" :key="index" class="flex align-center" @click="JumpDetails(item)"> |
|||
<view style="padding: 25rpx 25rpx 25rpx 20rpx;"> |
|||
<image :src="item.option_user.avatar" mode="widthFix" style="width: 80rpx;border-radius: 10rpx;"></image> |
|||
</view> |
|||
<view class="flex align-center" style="border-bottom: 1px solid #e7e7e7;padding: 25rpx 0px;width: 100%;margin-right: 30rpx;justify-content: space-between;"> |
|||
<view> |
|||
<view style="color: #576b95;font-size: 16px;margin-bottom: 10rpx;">{{item.option_user.nickname}}</view> |
|||
<view v-if="item.content"> |
|||
<mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap;font-size: 13px;" :content="emojiToHtml(item.content)"/> |
|||
</view> |
|||
<view v-if="item.type==1"> |
|||
<uni-icons type="heart" size="15" color="#576b95"></uni-icons> |
|||
</view> |
|||
<view style="font-size: 10px;margin-top: 10rpx;">{{item.create_time}}</view> |
|||
</view> |
|||
<view v-if="item.file.src"> |
|||
<image :src="apiUrl+item.file.src" mode="widthFix" style="width: 80rpx;border-radius: 10rpx;"></image> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="!showEarlier && hasUnread" @click="showEarlier = true" style="text-align: center; padding: 20rpx; color: #576b95;"> |
|||
查看更早的消息... |
|||
</view> |
|||
<uni-load-more v-if="showEarlier" :status="status" :content-text="contentText"></uni-load-more> |
|||
</scroll-view> |
|||
</template> |
|||
|
|||
<script> |
|||
import pinia from '@/store/index'; |
|||
import config from "@/common/config"; |
|||
import emoji from '@/utils/emoji.js'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
|
|||
const msgStore = useMsgStore(pinia) |
|||
export default { |
|||
data() { |
|||
return { |
|||
list:[], |
|||
apiUrl:config.apiUrl, |
|||
emojiMap:[], |
|||
page:1, |
|||
limit:20, |
|||
status: 'more',//状态 |
|||
//显示的状态 |
|||
contentText: { |
|||
contentdown: '查看更多', |
|||
contentrefresh: '加载中....', |
|||
contentnomore: '没有更多咯' |
|||
}, |
|||
showEarlier: false, // 控制是否显示更早消息 |
|||
hasUnread: false // 标记是否存在未读消息 |
|||
} |
|||
}, |
|||
computed: { |
|||
filteredList() { |
|||
if (this.showEarlier) { |
|||
// 显示所有已读消息(is_read=1) |
|||
return this.list.sort((a, b) => new Date(b.create_time) - new Date(a.create_time)) |
|||
} else { |
|||
// 优先显示未读消息(is_read=0) |
|||
const unread = this.list.filter(item => item.is_read === 0); |
|||
return unread.length ? unread : this.list; |
|||
} |
|||
} |
|||
}, |
|||
created() { |
|||
let emojiMap=[]; |
|||
// 解析所有表情 |
|||
emoji.forEach(function (item) { |
|||
let child=item.children; |
|||
if(child.length>0){ |
|||
child.forEach(function (val) { |
|||
let name=val.name; |
|||
let src=val.src; |
|||
emojiMap[name]=src; |
|||
}) |
|||
} |
|||
}); |
|||
this.emojiMap=emojiMap; |
|||
}, |
|||
onShow() { |
|||
this.getNoticeList() |
|||
}, |
|||
methods: { |
|||
getNoticeList(){ |
|||
let params = {page:this.page,limit:this.limit} |
|||
this.status = 'loading'; // 加载中 |
|||
this.$api.compaApi.getNoticeList(params).then(res => { |
|||
// 判断是否还有更多数据 |
|||
if (res.data.length <= res.count) { |
|||
this.status = 'noMore'; // 没有更多数据 |
|||
} else { |
|||
this.status = 'more'; // 还有更多数据 |
|||
} |
|||
|
|||
this.list = res.data |
|||
msgStore.getCount(0); |
|||
this.hasUnread = this.list.some(item => item.is_read === 0) |
|||
// 默认按时间倒序排列(确保最新消息在顶部) |
|||
this.list.sort((a, b) => new Date(b.create_time) - new Date(a.create_time)) |
|||
|
|||
// 如果没有未读消息,直接显示所有已读 |
|||
if (!this.hasUnread) { |
|||
this.showEarlier = true; |
|||
} |
|||
|
|||
|
|||
}) |
|||
}, |
|||
JumpDetails(item){ |
|||
uni.navigateTo({ |
|||
url:`/pages/compass/friendscircledetails?posts_id=${item.posts_id}&userid=${item.posts_user_id}` |
|||
}) |
|||
}, |
|||
scrollbot(){ |
|||
if (this.status == 'noMore') { |
|||
return; |
|||
} |
|||
this.page++ |
|||
this.getNoticeList() |
|||
// console.log('已触发底部事件'); |
|||
}, |
|||
// 自动解析消息中的表情 |
|||
emojiToHtml(str){ |
|||
if(!str){ |
|||
return; |
|||
} |
|||
let emojiMap=this.emojiMap; |
|||
return str.replace(/\[!(\w+)\]/gi, function (str, match) { |
|||
var file = match; |
|||
return emojiMap[file] ? "<img class='mr-5' style=\"width:18px;height:18px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]"); |
|||
}); |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.load-more-button { |
|||
font-size: 28rpx; |
|||
color: #576b95; |
|||
padding: 30rpx 0; |
|||
border-top: 1px solid #e7e7e7; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,410 @@ |
|||
<template> |
|||
<cu-custom bgColor="bg-white" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>详情</template> |
|||
</cu-custom> |
|||
<scroll-view scroll-y style="height: 92vh;"> |
|||
<view class="cu-card dynamic no-card"> |
|||
<view class="cu-item shadow"> |
|||
<view class="cu-list menu-avatar"> |
|||
<view class="cu-item" style="margin-top: 20px;" v-if="list.user"> |
|||
<view class="cu-avatar round lg" :style="[{backgroundImage:'url('+list.user.avatar+')'}]"></view> |
|||
<view class="content flex-sub"> |
|||
<view>{{list.user.nickname}}</view> |
|||
<view class="text-gray text-sm flex justify-between"> |
|||
{{list.create_time}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="text-content"> |
|||
<!-- <mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap" :content="emojiToHtml(list.content)"/> --> |
|||
<view class="text-content" v-if="contenthtml(list.content)" @click="handleLink(list.content)" v-html="list.content"></view> |
|||
<view class="text-content" v-html="list.content" v-else></view> |
|||
</view> |
|||
<view class="grid" style="margin: 0px 24rpx;"> |
|||
<view class="grid" style="margin: 2rpx 0px;padding: 0px 8rpx;" v-for="(itemss,indexs) in list.files" :key="indexs"> |
|||
<!-- flex-sub --> |
|||
<view class="bg-img" v-if="itemss.type==1"> |
|||
<image :src="apiUrl+itemss.src" mode="aspectFill" style="width: 200rpx;height: 200rpx;" :data-src="apiUrl+itemss.src" @tap="previewImage"></image> |
|||
</view> |
|||
<view class='course-video' :style="list.files ? $util.imageCoverStyle(270,480) : ''" v-if="itemss.type==2"> |
|||
<view class="relative-shadow" @tap="handlePlay(itemss)" style=""> |
|||
<view class="cuIcon-video icon-center f-28 c-white"></view> |
|||
</view> |
|||
<im-image style="position: absolute;top: 0px;" :src="apiUrl+itemss.privacy" ></im-image> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="list.address" @tap="openLocation(list.location)" class="im-location-msg im-flex im-rows im-nowrap im-align-items-center radius-8 pd-10"> |
|||
<view class="f-20 cuIcon-location pr-5"></view> |
|||
<view> |
|||
<view class="f-10 mb-5">{{list.address}}</view> |
|||
</view> |
|||
|
|||
</view> |
|||
<view class="text-gray text-sm text-right padding"> |
|||
<text @click="onLike(list.id)" :class="['cuIcon-appreciatefill', list.is_like === 1 ? 'text-red' : 'margin-lr-xs']"></text> {{list.likes?list.likes.length:0}} |
|||
<text @click="onComment(null)" class="cuIcon-messagefill margin-lr-xs"></text> {{list.comment?list.comment.length:0}} |
|||
<text v-if="list.user_id==userInfo.user_id" class="cuIcon-delete" style="margin-left: 5px;" @click="Delete(list.id)">删除</text> |
|||
</view> |
|||
|
|||
<view v-show="boll" style="margin: 5px 15px;position: relative;box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2);"> |
|||
<view style="padding-bottom: 45px;"> |
|||
<editor id="editor" class="bg-white c-333" style="min-height: 50px;height: 50px;padding:0px 10px ;" :adjust-position="false" maxlength="300" cursor-spacing="10" |
|||
@input="changeMsgText" @ready="onEditorReady" :read-only="readOnly"> </editor> |
|||
</view> |
|||
<view style="position: absolute;bottom: 5px;right: 10px;"> |
|||
<view style="display: flex;justify-content: space-between;"> |
|||
<view> |
|||
<view class="im-menus cuIcon-emoji f-28 ml-5 mr-10" hover-class="tap" @click.stop="showAppBox"></view> |
|||
<scroll-view scroll-y class="icon" v-if="isFocus"> |
|||
<view class="im-flex im-wrap im-justify-content-start im-align-items-center pd-10"> |
|||
<view v-for="(item,index) in currentEmojiList" class="im-emoji-item" :key="index"> |
|||
<image :src="item.src" style="width:44rpx;;height:44rpx" mode="aspectFit" lazy-load @tap="chooseEmoji(item)"></image> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
<button class="cu-btn bg-green shadow " @click="sendTextMsg(list.id)">发送</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="flex Likeview" style="flex-wrap: wrap;" v-if="list.likes?.length!==0"> |
|||
<uni-icons type="heart" size="15" color="#576b95"></uni-icons> |
|||
<view v-for="(itemws,indexws) in list.likes" :key="indexws" class="flex align-center" style="margin: 5px;"> |
|||
<!-- <view style="margin-left: 2px;">{{itemws.nickname}}</view> --> |
|||
<image :src="itemws.avatar" mode="widthFix" style="width: 60rpx;margin-left: 2px;"></image> |
|||
</view> |
|||
</view> |
|||
|
|||
<view style="margin-bottom: 10px;" v-if="list.comment?.length!==0"> |
|||
<view class="cu-list menu-avatar comment" style="margin-top: 0px;" v-for="(iteme,indexw) in list.comment" :key="indexw"> |
|||
<view class="comment_view"> |
|||
<view class="content"> |
|||
<view class="flex align-center" style="gap: 12rpx;" @click.stop="onComment(iteme.id)"> |
|||
<view> |
|||
<view class="flex" v-if="iteme.reply_user_name!==''"> |
|||
<view>{{iteme.nickname}} <text style="color: #000;margin-right: 5px;">回复</text></view> |
|||
<view class="flex-sub"> {{iteme.reply_user_name}}:</view> |
|||
</view> |
|||
<view v-else>{{iteme.nickname}}:</view> |
|||
</view> |
|||
<view class="text-gray text-content text-df" style="flex: 1; min-width: 0; word-break: break-word;"> |
|||
<!-- <mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap;color: #000;" :content="emojiToHtml(iteme.content)"/> --> |
|||
<view class="text-content content_img" style="color: #000;" v-if="contenthtml(iteme.content)" @click="handleLink(iteme.content)" v-html="iteme.content"></view> |
|||
<view class="text-content" style="color: #000;" v-html="iteme.content" v-else></view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useloginStore } from '@/store/login'; |
|||
import config from '@/common/config.js'; |
|||
import emoji from '@/utils/emoji.js'; |
|||
import Edit from '@/utils/edit.js'; |
|||
import { chat } from '@/mixins/chat.js'; |
|||
import imImage from '@/components/message/im-image.vue'; |
|||
|
|||
const loginStore = useloginStore() |
|||
export default { |
|||
components:{ |
|||
imImage |
|||
}, |
|||
mixins:[chat], |
|||
data() { |
|||
return { |
|||
list: {}, |
|||
boll:false, |
|||
edit: null, |
|||
userid:'', |
|||
inputMsg:'', |
|||
posts_id:0, |
|||
emojiMap:[], |
|||
isFocus:false, |
|||
readOnly:false, |
|||
currentEmojiList:[], |
|||
apiUrl: config.apiUrl, |
|||
userInfo:loginStore.userInfo, |
|||
} |
|||
}, |
|||
created(){ |
|||
this.currentEmojiList=emoji[0]['children']; |
|||
|
|||
let emojiMap=[]; |
|||
// 解析所有表情 |
|||
emoji.forEach(function (item) { |
|||
let child=item.children; |
|||
if(child.length>0){ |
|||
child.forEach(function (val) { |
|||
let name=val.name; |
|||
let src=val.src; |
|||
emojiMap[name]=src; |
|||
}) |
|||
} |
|||
}); |
|||
this.emojiMap=emojiMap; |
|||
}, |
|||
onLoad(option) { |
|||
this.posts_id = option.posts_id |
|||
if(this.userInfo.user_id==option.userid){ |
|||
this.userid = "" |
|||
}else{ |
|||
this.userid = option.userid |
|||
} |
|||
}, |
|||
onShow() { |
|||
if(this.userid){ |
|||
this.init(this.userid) |
|||
}else{ |
|||
this.init() |
|||
} |
|||
}, |
|||
methods: { |
|||
showAppBox(){ |
|||
this.isFocus = !this.isFocus; |
|||
}, |
|||
// 选择表情 |
|||
chooseEmoji(item){ |
|||
// #ifdef H5 |
|||
this.editorCtx.insertImage({ |
|||
src: item.src, |
|||
alt: item.title, |
|||
width: 18, |
|||
height: 18, |
|||
nowrap:true, |
|||
extClass:'emoji-image', |
|||
success: ()=>{ |
|||
}, |
|||
complete: ()=> { |
|||
this.editorCtx.blur(); |
|||
}, |
|||
}); |
|||
// #endif |
|||
// #ifndef H5 |
|||
this.readOnly= true |
|||
setTimeout(()=>{ |
|||
this.editorCtx.insertImage({ |
|||
src: item.src, |
|||
alt: item.title, |
|||
width: 18, |
|||
height: 18, |
|||
nowrap:true, |
|||
extClass:'emoji-image', |
|||
success: function() { |
|||
}, |
|||
complete: ()=> { |
|||
this.readOnly= false |
|||
}, |
|||
}); |
|||
},10); |
|||
// #endif |
|||
}, |
|||
onEditorReady() { |
|||
// #ifdef MP-BAIDU |
|||
this.editorCtx = requireDynamicLib('editorLib').createEditorContext('editor'); |
|||
// #endif |
|||
|
|||
// #ifdef APP-PLUS || MP-WEIXIN || H5 |
|||
const query = uni.createSelectorQuery().in(this); |
|||
query.select('#editor').context((res) => { |
|||
this.edit = new Edit({context: res.context,maxCount: 300}); |
|||
this.editorCtx = res.context |
|||
}).exec() |
|||
// #endif |
|||
}, |
|||
previewImage(e) { |
|||
//预览图片 |
|||
var current = e.target.dataset.src |
|||
uni.previewImage({ |
|||
current: current, |
|||
urls: [current] |
|||
}) |
|||
}, |
|||
changeMsgText(e){ |
|||
const txt=e.detail.text.replace(/\n/g, ''); |
|||
if(txt=='' && e.detail.html=='<p><br></p>'){ |
|||
this.inputMsg=''; |
|||
}else{ |
|||
this.inputMsg=e.detail.html; |
|||
} |
|||
}, |
|||
onLike(id){ |
|||
this.$api.compaApi.onlikes({posts_id:id}).then(res => { |
|||
if(this.userid){ |
|||
this.init(this.userid) |
|||
}else{ |
|||
this.init() |
|||
} |
|||
// this.list = res.data |
|||
// console.log(res); |
|||
}) |
|||
}, |
|||
onComment(id){ |
|||
this.boll = !this.boll |
|||
if(id){ |
|||
uni.setStorageSync('pid',id) |
|||
} |
|||
}, |
|||
Delete(id){ |
|||
const _this = this |
|||
uni.showModal({ |
|||
title: "提示", |
|||
content: "是否要删除该图片", |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
_this.$api.compaApi.Deleteapost({posts_id:id}).then(res => { |
|||
if(_this.userid){ |
|||
_this.init(_this.userid) |
|||
}else{ |
|||
_this.init() |
|||
} |
|||
}) |
|||
}else if (res.cancel) { |
|||
console.log('用户点击取消'); |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
contenthtml(val){ |
|||
return val && (val.includes('http://') || val.includes('https://')); |
|||
}, |
|||
handleLink(url){ |
|||
const textOnly = url.replace(/<[^>]+>/g, ''); |
|||
const urlRegex = /https?:\/\/[^\s"']+/g; |
|||
const urls = textOnly.match(urlRegex) || []; |
|||
if(urls.length > 0){ |
|||
urls.forEach((res,index)=>{ |
|||
// #ifdef H5 |
|||
window.open(urls[index], '_blank'); |
|||
// #endif |
|||
// #ifdef APP-PLUS |
|||
plus.runtime.openURL(urls[index]); |
|||
// #endif |
|||
}) |
|||
}else{ |
|||
return |
|||
} |
|||
|
|||
}, |
|||
init(id){ |
|||
this.$api.compaApi.detailsList({posts_id:+this.posts_id,friend_user_id:id?+id:''}).then(res => { |
|||
const [latitudeStr, longitudeStr] = res.data.location?.split('+'); |
|||
const location = { |
|||
latitude: +latitudeStr?+latitudeStr:0, |
|||
longitude: +longitudeStr?+longitudeStr:0 |
|||
}; |
|||
res.data.location = location |
|||
this.list = res.data |
|||
}) |
|||
}, |
|||
sendTextMsg(id1){ |
|||
const _this = this |
|||
const pid = uni.getStorageSync('pid') |
|||
this.editorCtx.getContents({ |
|||
success:(e)=>{ |
|||
let msg=e.html; |
|||
const text = /^\s*$/.test(msg.replace(/<[^>]+>/g, "")); |
|||
const hasImg = /<img\b/i.test(msg); |
|||
if(msg==''||(text && !hasImg)){ |
|||
uni.showToast({ title: "内容不能为空", icon: "none" }); |
|||
msg='' |
|||
_this.editorCtx.clear(); |
|||
return |
|||
} |
|||
let params = {posts_id:id1,content:msg,pid:pid?pid:""} |
|||
// console.log(params); |
|||
_this.$api.compaApi.oncomments(params).then(res => { |
|||
uni.removeStorageSync('pid') |
|||
if(_this.userid){ |
|||
_this.init(_this.userid) |
|||
}else{ |
|||
_this.init() |
|||
} |
|||
_this.editorCtx.clear(); |
|||
this.boll = false; |
|||
// _this.list = res.data |
|||
// console.log(res); |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.im-input{ |
|||
height:100%; |
|||
font-size: 28rpx; |
|||
min-height:150rpx; |
|||
max-height: 300rpx; |
|||
padding:14rpx 14rpx; |
|||
border-radius:10rpx; |
|||
word-break: break-all; |
|||
// margin:0 8rpx !important; |
|||
} |
|||
.im-location-msg{ |
|||
color:#2B2E3D; |
|||
margin: 5px 10px; |
|||
text-align: left !important; |
|||
} |
|||
.relative-shadow{ |
|||
z-index:1; |
|||
width:100%; |
|||
height:100%; |
|||
height: 240px; |
|||
display: flex; |
|||
position: absolute; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
.Likeview{ |
|||
padding: 5px; |
|||
display: flex; |
|||
color: #576b95; |
|||
align-items: center; |
|||
background-color: #f7f7f7; |
|||
margin: 0rem 0.9375rem 0rem 2rem; |
|||
} |
|||
.course-video{ |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
.comment_view{ |
|||
height: auto; |
|||
color: #576b95; |
|||
padding: 20rpx 30rpx; |
|||
background-color: #f7f7f7; |
|||
margin: 0rem 0.9375rem 0rem 2rem; |
|||
} |
|||
.icon{ |
|||
top: -440rpx; |
|||
left: -170rpx; |
|||
width: 400rpx; |
|||
height: 400rpx; |
|||
position: absolute; |
|||
border-radius: 10rpx; |
|||
background-color: #fff; |
|||
box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2); |
|||
|
|||
.im-emoji-item{ |
|||
padding:22rpx; |
|||
} |
|||
} |
|||
::v-deep .ql-editor{ |
|||
padding-top: 10px; |
|||
} |
|||
::v-deep .content_img img{ |
|||
width: 18px; |
|||
margin-right: 5px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,103 @@ |
|||
<template> |
|||
<view :style="{paddingBottom: paddingB+'px'}"> |
|||
<view class="cu-list menu" :class="['sm-border','card-menu margin-top']" v-if="compass().mode == 1"> |
|||
<block v-for="(item,index) in sortedList" :key="item.order"> |
|||
<view class="cu-item" :class="'arrow'" @tap="openApp(item)"> |
|||
<view class="content"> |
|||
<image :src="item.icon" class="png" mode="aspectFit"></image> |
|||
<text class="text-grey">{{item.name}}</text> |
|||
</view> |
|||
<view class="content_num" v-if="item.name=='朋友圈'&&count>0">{{count>99?'99+':count}}</view> |
|||
</view> |
|||
</block> |
|||
</view> |
|||
<view class="cu-list grid" :class="['col-3',' margin-top']" v-if="compass().mode == 2"> |
|||
<block v-for="(item,index) in sortedList" :key="item.order"> |
|||
<view class="cu-item" @tap="openApp(item)"> |
|||
<view> |
|||
<image :src="item.icon" style="height:100rpx;width: 100rpx;"></image> |
|||
<view class="cu-tag badge" v-if="item.badge!=0"> |
|||
<block v-if="item.badge!=1">{{item.badge>99?'99+':item.badge}}</block> |
|||
</view> |
|||
</view> |
|||
<text>{{item.name}}</text> |
|||
</view> |
|||
</block> |
|||
</view> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useloginStore } from '@/store/login' |
|||
import pinia from '@/store/index' |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
const loginStore = useloginStore(pinia) |
|||
|
|||
const msgStore = useMsgStore(pinia) |
|||
const {NoticeCount} = storeToRefs(msgStore); |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
isCard: true, |
|||
userInfo:loginStore.userInfo, |
|||
paddingB:0, |
|||
count:NoticeCount |
|||
}; |
|||
}, |
|||
computed:{ |
|||
sortedList() { |
|||
return this.compass()?.list |
|||
.filter(item => item.status == 1) |
|||
.sort((a, b) => a.order - b.order) |
|||
} |
|||
}, |
|||
created:function(){ |
|||
// #ifdef H5 |
|||
this.paddingB=this.inlineTools; |
|||
// #endif |
|||
|
|||
// #ifndef H5 |
|||
this.paddingB=this.navBarHeight+this.inlineTools; |
|||
// #endif |
|||
}, |
|||
methods: { |
|||
compass(){ |
|||
return loginStore.globalConfig.compass |
|||
}, |
|||
openApp(item) { |
|||
let url=item.url; |
|||
if(item.type==2){ |
|||
url='/pages/mine/webview?title='+item.name+'&src='+encodeURIComponent(item.url) |
|||
} |
|||
uni.navigateTo({ |
|||
url:url |
|||
}) |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.im-friend-header{ |
|||
width:100%;height:400rpx;position: relative; |
|||
.im-friend-bg{ |
|||
width:100%;height:300rpx;overflow: hidden; |
|||
.im-friend-image{ |
|||
width:100%;height:300rpx; |
|||
} |
|||
} |
|||
} |
|||
.im-user{ |
|||
position: absolute;right:60rpx;top:210rpx;overflow: hidden; |
|||
} |
|||
.content_num{ |
|||
color: #fff; |
|||
padding: 1px 5px; |
|||
border-radius: 50px; |
|||
text-align: center; |
|||
background-color: red; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,547 @@ |
|||
<template> |
|||
<cu-custom bgColor="bg-white" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>朋友圈</template> |
|||
</cu-custom> |
|||
<!-- #ifdef APP-PLUS --> |
|||
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing" |
|||
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 88vh;"> |
|||
<!-- #endif --> |
|||
<!-- #ifdef H5 --> |
|||
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing" |
|||
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 93vh;"> |
|||
<!-- #endif --> |
|||
<!-- --> |
|||
<view @click="onicons" :style="{paddingBottom: paddingB+'px'}"> |
|||
<view class="im-friend-header"> |
|||
<view class="im-friend-bg"> |
|||
<image class="im-friend-image" src="/static/image/user-card-bg.jpg" mode="widthFix"></image> |
|||
</view> |
|||
<view class="im-user im-flex im-justify-content-start align-center"> |
|||
<text class="text-white mr-5">{{userInfo.realname}}</text> |
|||
<image class="radius-10" style="width:120rpx;height:120rpx" :src="userInfo.avatar" mode="widthFix"></image> |
|||
</view> |
|||
<view style="position: absolute;top: 15px;right: 18px;" @click="tomoments"> |
|||
<image class="radius-10" style="width:50rpx;height:50rpx" src="/static/image/moments.png" mode="widthFix"></image> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="text-center" style="background-color: #fff;padding: 10px;" @click="tonotice" v-if="notice>0"> |
|||
<view style="width: 100px;height: 30px;background-color: #f0f0f0;line-height: 30px;margin: auto;border-radius: 5px;color: #6b72ff;">{{notice}}条新信息</view> |
|||
</view> |
|||
<view v-for="(item,index) in list" :key="index"> |
|||
<view class="cu-card dynamic no-card"> |
|||
<view class="cu-item shadow"> |
|||
<view class="cu-list menu-avatar"> |
|||
<view class="cu-item" style="padding-top: 20px;"> |
|||
<view class="cu-avatar round lg" :style="[{backgroundImage:'url('+item.user.avatar+')'}]"></view> |
|||
<view class="content flex-sub"> |
|||
<view>{{item.user.nickname}}</view> |
|||
<view class="text-gray text-sm flex justify-between"> |
|||
{{item.create_time}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="text-content" v-if="contenthtml(item.content)" @click="handleLink(item.content)" v-html="item.content"></view> |
|||
<view class="text-content" v-html="item.content" v-else></view> |
|||
|
|||
<view class="grid" style="margin: 0px 24rpx;"> |
|||
<view class="grid" style="margin: 2rpx 0px;padding: 0px 8rpx;" v-for="(itemss,indexs) in item.files" :key="indexs"> |
|||
<!-- flex-sub --> |
|||
<view class="bg-img" v-if="itemss.type==1"> |
|||
<image :src="apiUrl+itemss.src" mode="aspectFill" style="width: 200rpx;height: 200rpx;" :data-src="apiUrl+itemss.src" @tap="previewImage"></image> |
|||
</view> |
|||
<view class='course-video' :style="item.files ? $util.imageCoverStyle(270,480) : ''" v-if="itemss.type==2"> |
|||
<view class="relative-shadow" @tap="handlePlay(itemss)" style=""> |
|||
<view class="cuIcon-video icon-center f-28 c-white"></view> |
|||
</view> |
|||
<im-image style="position: absolute;top: 0px;" :src="apiUrl+itemss.privacy" ></im-image> |
|||
<!-- :info="item.files" --> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="item.location_address" @tap="openLocation(item.location)" class="im-location-msg im-flex im-rows im-nowrap im-align-items-center radius-8 pd-10"> |
|||
<view class="f-20 cuIcon-location pr-5"></view> |
|||
<view> |
|||
<view class="f-10 mb-5">{{item.location_address}}</view> |
|||
<!-- <view class="c-999 f-12">{{item.extends && item.extends.address}}</view> --> |
|||
</view> |
|||
|
|||
</view> |
|||
<view class="text-gray text-sm text-right padding"> |
|||
<!-- <text class="cuIcon-attentionfill margin-lr-xs"></text> 10 --> |
|||
<!-- +item.is_like --> |
|||
<text @click="onLike(item.id)" :class="['cuIcon-appreciatefill', item.is_like === 1 ? 'text-red' : 'margin-lr-xs']"></text> {{item.likes.length}} |
|||
<text @click="onComment(index)" class="cuIcon-messagefill margin-lr-xs"></text> {{item.comment.length}} |
|||
<text v-if="item.user_id==userInfo.user_id" class="cuIcon-delete" style="margin-left: 5px;" @click="Delete(item.id)">删除</text> |
|||
</view> |
|||
|
|||
<view v-show="numindex === index&&boll" style="margin: 5px 15px 5px 15px;position: relative;box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2);"> |
|||
<view style="padding-bottom: 45px;"> |
|||
<!-- <uni-easyinput id="input" type="textarea" autoHeight v-model="content" placeholder="评论"></uni-easyinput> --> |
|||
<!-- id="editor1" --> |
|||
<editor :id="'editor' + index" class=" bg-white c-333" style="min-height: 50px;height: 50px;padding:0px 10px;" :adjust-position="false" |
|||
maxlength="200" cursor-spacing="10" @input="changeMsgText" @ready="onEditorReady(index)" :read-only="readOnly"> </editor> |
|||
<!-- @focus="InputFocus" @blur="InputBlur" --> |
|||
</view> |
|||
<view style="position: absolute;bottom: 5px;right: 10px;"> |
|||
<view style="display: flex;justify-content: space-between;"> |
|||
<view> |
|||
<view class="im-menus cuIcon-emoji f-28 ml-5 mr-10" hover-class="tap" @click.stop="showAppBox"></view> |
|||
<scroll-view scroll-y class="icon" v-if="isFocus"> |
|||
<view class="im-flex im-wrap im-justify-content-start im-align-items-center pd-10"> |
|||
<view v-for="(item,index) in currentEmojiList" class="im-emoji-item" :key="index"> |
|||
<image :src="item.src" style="width:44rpx;;height:44rpx" mode="aspectFit" lazy-load @tap="chooseEmoji(item)"></image> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
<button class="cu-btn bg-green shadow " @touchend.prevent="sendTextMsg(item.id)">发送</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="flex Likeview" v-if="item.likes.length!=0"> |
|||
<view v-for="(itemws,indexws) in item.likes" :key="indexws" class="flex align-center" style="margin-right: 5px;"> |
|||
<uni-icons type="heart" size="15" color="#576b95"></uni-icons> |
|||
<view style="margin-left: 2px;">{{itemws.nickname}}</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view style="margin-bottom: 10px;"> |
|||
<view class="cu-list menu-avatar comment" style="margin-top: 0px;" v-for="(iteme,indexw) in item.comment" :key="indexw"> |
|||
<view class="comment_view"> |
|||
<view class="content"> |
|||
<view class="flex align-center" style="gap: 12rpx;" @click.stop="onComment(index,iteme.id)"> |
|||
<view> |
|||
<view class="flex" v-if="iteme.reply_user_name!==''"> |
|||
<view>{{iteme.nickname}} <text style="color: #000;margin-right: 5px;">回复</text></view> |
|||
<view class="flex-sub"> {{iteme.reply_user_name}}:</view> |
|||
</view> |
|||
<view v-else>{{iteme.nickname}}:</view> |
|||
</view> |
|||
<view class="text-gray text-content text-df" style="flex: 1; min-width: 0; word-break: break-word;"> |
|||
<view class="text-content content_img" style="color: #000;" v-if="contenthtml(iteme.content)" @click.stop="handleLink(iteme.content,index,iteme.id)" v-html="iteme.content"></view> |
|||
<view class="text-content" style="color: #000;" v-html="iteme.content" v-else></view> |
|||
<!-- <mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap;color: #000;" :content="emojiToHtml(iteme.content)"/> --> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="list.length==0" style="display: flex;justify-content: center;align-items: center;margin-top: 40px;"> |
|||
<view> |
|||
<image src="@/static/image/empty.png" mode="widthFix" style="width: 560rpx;"></image> |
|||
<view style="text-align: center;color: #ccc;font-size: 40rpx;margin-right: 10px;">暂无数据</view> |
|||
</view> |
|||
</view> |
|||
|
|||
</view> |
|||
<uni-load-more :status="status" :content-text="contentText"></uni-load-more> |
|||
</scroll-view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useloginStore } from '@/store/login' |
|||
import config from '@/common/config.js' |
|||
import emoji from '@/utils/emoji.js' |
|||
import Edit from '@/utils/edit.js' |
|||
import { chat } from '@/mixins/chat.js' |
|||
import imImage from '@/components/message/im-image.vue'; |
|||
|
|||
import pinia from '@/store/index' |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
const msgStore = useMsgStore(pinia) |
|||
const {NoticeCount} = storeToRefs(msgStore); |
|||
|
|||
const loginStore = useloginStore() |
|||
export default { |
|||
components:{ |
|||
imImage |
|||
}, |
|||
mixins:[chat], |
|||
data() { |
|||
return { |
|||
notice:NoticeCount, |
|||
isCard: true, |
|||
apiUrl: config.apiUrl, |
|||
userInfo:loginStore.userInfo, |
|||
paddingB:0, |
|||
page:1, |
|||
limit:20, |
|||
content:'', |
|||
numindex:-1, |
|||
boll:false, |
|||
list:[], |
|||
// editorCtx:null, |
|||
editorCtx:{}, |
|||
currentEmojiList:[], |
|||
emojiMap:[], |
|||
isFocus:false, |
|||
edit: null, |
|||
readOnly:false, |
|||
refreshing: false, |
|||
status: 'more',//状态 |
|||
inputMsg:'', |
|||
//显示的状态 |
|||
contentText: { |
|||
contentdown: '查看更多', |
|||
contentrefresh: '加载中....', |
|||
contentnomore: '没有更多咯' |
|||
}, |
|||
}; |
|||
}, |
|||
created(){ |
|||
// #ifdef H5 |
|||
this.paddingB=this.inlineTools; |
|||
// #endif |
|||
|
|||
// #ifndef H5 |
|||
this.paddingB=this.navBarHeight+this.inlineTools; |
|||
// #endif |
|||
this.currentEmojiList=emoji[0]['children']; |
|||
|
|||
let emojiMap=[]; |
|||
// 解析所有表情 |
|||
emoji.forEach(function (item) { |
|||
let child=item.children; |
|||
if(child.length>0){ |
|||
child.forEach(function (val) { |
|||
let name=val.name; |
|||
let src=val.src; |
|||
emojiMap[name]=src; |
|||
}) |
|||
} |
|||
}); |
|||
this.emojiMap=emojiMap; |
|||
}, |
|||
onShow() { |
|||
this.page = 1 |
|||
this.wechatMomentsList() |
|||
}, |
|||
methods: { |
|||
onRefresh() { |
|||
this.page = 1 |
|||
this.wechatMomentsList() |
|||
if (this.refreshing) return; |
|||
this.refreshing = true; |
|||
setTimeout(() => { |
|||
this.refreshing = false; |
|||
}, 1000) |
|||
}, |
|||
contenthtml(val){ |
|||
return val && (val.includes('http://') || val.includes('https://')); |
|||
}, |
|||
handleLink(url,index,id){ |
|||
const textOnly = url.replace(/<[^>]+>/g, ''); |
|||
const urlRegex = /https?:\/\/[^\s"']+/g; |
|||
const urls = textOnly.match(urlRegex) || []; |
|||
// console.log(urls); |
|||
if(urls.length > 0){ |
|||
urls.forEach((res,index)=>{ |
|||
// #ifdef H5 |
|||
window.open(urls[index], '_blank'); |
|||
// #endif |
|||
// #ifdef APP-PLUS |
|||
plus.runtime.openURL(urls[index]); |
|||
// #endif |
|||
}) |
|||
}else{ |
|||
this.onComment(index,id) |
|||
return |
|||
} |
|||
|
|||
}, |
|||
IsCard(e) { |
|||
this.isCard = e.detail.value |
|||
}, |
|||
wechatMomentsList(){ |
|||
let params = {page:this.page,limit:this.limit} |
|||
this.status = 'loading'; // 加载中 |
|||
this.$api.compaApi.wechatMomentsList(params).then(res => { |
|||
res.data.forEach((res)=>{ |
|||
const [latitudeStr, longitudeStr] = res.location.split('+'); |
|||
const location = { |
|||
latitude: +latitudeStr, |
|||
longitude: +longitudeStr |
|||
}; |
|||
res.location = location |
|||
res.content = res.content.replace(/\n/g, '<br>') |
|||
}) |
|||
if (this.page === 1) { |
|||
this.list = res.data |
|||
} else { |
|||
this.list = this.list.concat(res.data); |
|||
} |
|||
// 判断是否还有更多数据 |
|||
if (this.list.length < res.count) { |
|||
this.status = 'more'; // 还有更多数据 |
|||
} else { |
|||
this.status = 'noMore'; // 没有更多数据 |
|||
} |
|||
|
|||
// console.log(res); |
|||
}) |
|||
}, |
|||
scrollbot(){ |
|||
if (this.status == 'noMore') { |
|||
return; |
|||
} |
|||
this.page++ |
|||
this.wechatMomentsList() |
|||
// console.log('已触发底部事件'); |
|||
}, |
|||
tomoments(){ |
|||
uni.navigateTo({ |
|||
url:'/pages/compass/sendtoMoments' |
|||
}) |
|||
}, |
|||
tonotice(){ |
|||
uni.navigateTo({ |
|||
url:'/pages/compass/Informationdetails' |
|||
}) |
|||
}, |
|||
onLike(id){ |
|||
this.$api.compaApi.onlikes({posts_id:id}).then(res => { |
|||
this.wechatMomentsList() |
|||
// this.list = res.data |
|||
// console.log(res); |
|||
}) |
|||
}, |
|||
onComment(index,id){ |
|||
// 如果点击的是同一个项,关闭编辑 |
|||
if (this.numindex === index) { |
|||
this.boll = false |
|||
uni.removeStorageSync('pid') |
|||
this.numindex = -1 |
|||
return |
|||
} |
|||
|
|||
this.numindex = index |
|||
this.boll = !this.boll |
|||
if(id&&this.boll == true){ |
|||
uni.setStorageSync('pid',id) |
|||
} |
|||
}, |
|||
showAppBox(){ |
|||
this.isFocus = !this.isFocus; |
|||
}, |
|||
onicons(){ |
|||
this.isFocus = false; |
|||
}, |
|||
// 选择表情 |
|||
chooseEmoji(item){ |
|||
// #ifdef H5 |
|||
this.editorCtx[this.numindex].insertImage({ |
|||
src: item.src, |
|||
alt: item.title, |
|||
width: 18, |
|||
height: 18, |
|||
nowrap:true, |
|||
extClass:'emoji-image', |
|||
success: ()=>{ |
|||
}, |
|||
complete: ()=> { |
|||
this.editorCtx[this.numindex].blur(); |
|||
}, |
|||
}); |
|||
// console.log(this.editorCtx[this.numindex]); |
|||
// #endif |
|||
// #ifndef H5 |
|||
this.readOnly= true |
|||
setTimeout(()=>{ |
|||
this.editorCtx[this.numindex].insertImage({ |
|||
src: item.src, |
|||
alt: item.title, |
|||
width: 18, |
|||
height: 18, |
|||
nowrap:true, |
|||
extClass:'emoji-image', |
|||
success: function() { |
|||
}, |
|||
complete: ()=> { |
|||
this.readOnly= false |
|||
}, |
|||
}); |
|||
},10); |
|||
// #endif |
|||
}, |
|||
onEditorReady(index) { |
|||
const editorId = 'editor' + index; |
|||
// #ifdef MP-BAIDU |
|||
// this.editorCtx = requireDynamicLib('editorLib').createEditorContext('editor1'); |
|||
this.editorCtx[index] = requireDynamicLib('editorLib').createEditorContext(editorId); |
|||
// #endif |
|||
|
|||
// #ifdef APP-PLUS || MP-WEIXIN || H5 |
|||
const query = uni.createSelectorQuery().in(this); |
|||
// query.select('#editor1').context((res) => { |
|||
query.select('#editor' + index).context((res) => { |
|||
this.edit = new Edit({context: res.context,maxCount: 300}); |
|||
this.editorCtx[index] = res.context |
|||
}).exec() |
|||
// #endif |
|||
}, |
|||
changeMsgText(e){ |
|||
const txt=e.detail.text.replace(/\n/g, ''); |
|||
if(txt=='' && e.detail.html=='<p><br></p>'){ |
|||
this.inputMsg=''; |
|||
}else{ |
|||
this.inputMsg=e.detail.html; |
|||
} |
|||
}, |
|||
// 自动解析消息中的表情 |
|||
emojiToHtml(str){ |
|||
if(!str){ |
|||
return; |
|||
} |
|||
let emojiMap=this.emojiMap; |
|||
return str.replace(/\[!(\w+)\]/gi, function (str, match) { |
|||
var file = match; |
|||
return emojiMap[file] ? "<img class='mr-5' style=\"width:18px;height:18px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]"); |
|||
}); |
|||
}, |
|||
previewImage(e) { |
|||
//预览图片 |
|||
var current = e.target.dataset.src |
|||
uni.previewImage({ |
|||
current: current, |
|||
urls: [current] |
|||
}) |
|||
}, |
|||
Delete(id){ |
|||
const _this = this |
|||
uni.showModal({ |
|||
title: "提示", |
|||
content: "是否要删除该内容", |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
_this.$api.compaApi.Deleteapost({posts_id:id}).then(res => { |
|||
_this.wechatMomentsList() |
|||
}) |
|||
}else if (res.cancel) { |
|||
console.log('用户点击取消'); |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
sendTextMsg(id1){ |
|||
const _this = this |
|||
const pid = uni.getStorageSync('pid') |
|||
const currentIndex = this.numindex |
|||
this.editorCtx[currentIndex].getContents({ |
|||
success:(e)=>{ |
|||
let msg=e.html; |
|||
// console.log('消息发送',e); |
|||
let params = {posts_id:id1,content:msg,pid:pid?pid:""} |
|||
const text = /^\s*$/.test(params.content.replace(/<[^>]+>/g, "")); |
|||
const hasImg = /<img\b/i.test(params.content); |
|||
if(params.content==''|| (text && !hasImg)){ |
|||
uni.showToast({title: '内容不能为空!',icon:'error'}); |
|||
return |
|||
} |
|||
_this.$api.compaApi.oncomments(params).then(res => { |
|||
uni.removeStorageSync('pid') |
|||
_this.wechatMomentsList() |
|||
_this.inputMsg = "" |
|||
_this.boll = false; |
|||
_this.editorCtx[currentIndex].clear(); |
|||
// _this.list = res.data |
|||
// console.log(_this.editorCtx.clear()); |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.im-friend-header{ |
|||
width:100%;height:400rpx;position: relative; |
|||
.im-friend-bg{ |
|||
width:100%;height:400rpx;overflow: hidden; |
|||
.im-friend-image{ |
|||
width:100%;height:400rpx; |
|||
} |
|||
} |
|||
} |
|||
.im-user{ |
|||
position: absolute; |
|||
right:60rpx; |
|||
top:210rpx; |
|||
overflow: hidden; |
|||
} |
|||
.icon{ |
|||
top: -440rpx; |
|||
left: -170rpx; |
|||
width: 400rpx; |
|||
height: 400rpx; |
|||
position: absolute; |
|||
border-radius: 10rpx; |
|||
background-color: #fff; |
|||
box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2); |
|||
|
|||
.im-emoji-item{ |
|||
padding:22rpx; |
|||
} |
|||
} |
|||
.im-input{ |
|||
height:100%; |
|||
font-size: 28rpx; |
|||
min-height:150rpx; |
|||
max-height: 300rpx; |
|||
padding:14rpx 14rpx; |
|||
border-radius:10rpx; |
|||
word-break: break-all; |
|||
margin:0 8rpx !important; |
|||
} |
|||
.im-location-msg{ |
|||
color:#2B2E3D; |
|||
margin: 5px 10px; |
|||
text-align: left !important; |
|||
} |
|||
.relative-shadow{ |
|||
z-index:1; |
|||
width:100%; |
|||
height:100%; |
|||
height: 240px; |
|||
display: flex; |
|||
position: absolute; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
.Likeview{ |
|||
padding: 5px; |
|||
display: flex; |
|||
color: #576b95; |
|||
align-items: center; |
|||
background-color: #f7f7f7; |
|||
margin: 0rem 0.9375rem 0rem 2rem; |
|||
} |
|||
.course-video{ |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
.comment_view{ |
|||
height: auto; |
|||
color: #576b95; |
|||
padding: 20rpx 30rpx; |
|||
background-color: #f7f7f7; |
|||
margin: 0rem 0.9375rem 0rem 2rem; |
|||
} |
|||
::v-deep .ql-editor{ |
|||
padding-top: 10px; |
|||
} |
|||
|
|||
::v-deep .content_img img{ |
|||
width: 18px; |
|||
margin-right: 5px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,313 @@ |
|||
<template> |
|||
<cu-custom bgColor="bg-white" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>朋友圈</template> |
|||
</cu-custom> |
|||
<!-- #ifdef APP-PLUS --> |
|||
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing" |
|||
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 88vh;"> |
|||
<!-- #endif --> |
|||
<!-- #ifdef H5 --> |
|||
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing" |
|||
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 93vh;"> |
|||
<!-- #endif --> |
|||
<view> |
|||
<view class="im-friend-header"> |
|||
<view class="im-friend-bg"> |
|||
<image class="im-friend-image" src="/static/image/user-card-bg.jpg" style="width: 750rpx;height: 460rpx;"></image> |
|||
</view> |
|||
<view class="im-user im-flex im-justify-content-start align-center"> |
|||
<text class="text-white mr-5">{{name}}</text> |
|||
<image class="radius-10" style="width:120rpx;height:120rpx" :src="urlimg" mode="widthFix"></image> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="viewtop" v-if="userInfo.user_id==userid"> |
|||
<view class="flex view_mar"> |
|||
<view class="view_text">今天</view> |
|||
<view class="view_icons"> |
|||
<uni-icons @tap="tomoments" type="camera-filled" size="30" color="#8a8a8a" style="padding: 30px;"></uni-icons> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view v-for="(yearGroup, year) in groupedList" :key="year" class="year-group"> |
|||
<!-- 年份标题 --> |
|||
<view class="year-title" @click="toggleYear(year)"> |
|||
<text class="year-text">{{ year }}年</text> |
|||
</view> |
|||
|
|||
<!-- 动态内容(展开时显示) --> |
|||
<view v-if="!collapsedYears.includes(year)" class="moments-list"> |
|||
<view v-for="(item, index) in yearGroup" :key="item.id" class="moment-item" @click="JumpDetails(item.id)"> |
|||
<view class="meta-info"> |
|||
<text class="time">{{ formatTime(item.create_time) }}</text> |
|||
</view> |
|||
<view class="content1"> |
|||
<view v-if="item.files" v-for="(items, indexs) in item.files" :key="indexs"> |
|||
<view v-if="items.type==1"><image :src="configurl+items.src" :data-src="configurl+items.src" class="post-image" mode="aspectFill" @tap.stop="previewImage"/></view> |
|||
<view v-if="items.type==2"> |
|||
<view class='course-video' style="width: 80px;height: 90px;"> |
|||
<view class="relative-shadow" @tap.stop="handlePlay(items)"> |
|||
<view class="cuIcon-video icon-center f-28 c-white"></view> |
|||
<!-- <view class="video-duration f-10 c-white" v-if="item.extends && item.extends.duration">{{$util.videoFormatTime(item.extends.duration)}}</view> --> |
|||
</view> |
|||
<im-image v-if="configurl+items.privacy" :src="configurl+items.privacy"></im-image> |
|||
</view> |
|||
|
|||
<!-- <image :src="configurl+items.privacy" :data-src="configurl+items.privacy" class="post-image1" mode="aspectFill" @tap.stop="previewImage"/> --> |
|||
</view> |
|||
</view> |
|||
<view style="margin-left: 20rpx;"> |
|||
<mp-html container-style="overflow: hidden;display:inline;word-break: break-word;" :content="emojiToHtml(item.content)"/> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useloginStore } from '@/store/login' |
|||
import config from '@/common/config.js' |
|||
import {chat} from '@/mixins/chat.js' |
|||
import imImage from '@/components/message/im-image.vue'; |
|||
|
|||
const loginStore = useloginStore() |
|||
export default { |
|||
components: { |
|||
imImage |
|||
}, |
|||
mixins: [chat], |
|||
data() { |
|||
return { |
|||
page: 1, |
|||
limit: 20, |
|||
list: [], |
|||
// groupedList: {}, |
|||
collapsedYears: [], |
|||
status: 'more', //状态 |
|||
refreshing: false, |
|||
configurl:config.apiUrl, |
|||
urlimg:'', |
|||
name:'', |
|||
userid:'', |
|||
//显示的状态 |
|||
contentText: { |
|||
contentdown: '查看更多', |
|||
contentrefresh: '加载中....', |
|||
contentnomore: '没有更多咯' |
|||
}, |
|||
userInfo:loginStore.userInfo, |
|||
} |
|||
}, |
|||
computed: { |
|||
// 计算属性保持不变... |
|||
groupedList() { |
|||
return this.groupDataByYear(this.list) |
|||
} |
|||
}, |
|||
// watch: { |
|||
// list: { |
|||
// handler(newVal) { |
|||
// this.groupedList = this.groupDataByYear(newVal) |
|||
// }, |
|||
// immediate: true, |
|||
// deep: true |
|||
// } |
|||
// }, |
|||
onLoad(val) { |
|||
this.urlimg = val.avatar |
|||
this.name = val.realname |
|||
this.userid = val.user_id |
|||
}, |
|||
onShow() { |
|||
if(this.userInfo.user_id==this.userid){ |
|||
this.init() |
|||
}else{ |
|||
this.init(this.userid) |
|||
} |
|||
}, |
|||
methods: { |
|||
JumpDetails(id){ |
|||
uni.navigateTo({ |
|||
url:`/pages/compass/friendscircledetails?posts_id=${id}&userid=${this.userid}` |
|||
}) |
|||
}, |
|||
tomoments(){ |
|||
uni.navigateTo({ |
|||
url:'/pages/compass/sendtoMoments' |
|||
}) |
|||
}, |
|||
formatTime(timeStr) { |
|||
const date = new Date(timeStr) |
|||
return `${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}` |
|||
}, |
|||
groupDataByYear(data) { |
|||
return data.reduce((acc, item) => { |
|||
const year = new Date(item.create_time).getFullYear() |
|||
if (!acc[year]) acc[year] = [] |
|||
acc[year].push(item) |
|||
return acc |
|||
}, {}) |
|||
}, |
|||
previewImage(e) { |
|||
//预览图片 |
|||
var current = e.target.dataset.src |
|||
uni.previewImage({ |
|||
current: current, |
|||
urls: [current] |
|||
}) |
|||
}, |
|||
init(id){ |
|||
if (this.page > 1) { |
|||
this.page = 1 |
|||
} |
|||
let params = {page: this.page,limit: this.limit,friend_user_id: id?+id:''} |
|||
this.status = 'loading'; // 加载中 |
|||
this.$api.compaApi.myPosts(params).then(res => { |
|||
res.data.forEach((res) => { |
|||
const [latitudeStr, longitudeStr] = res.location.split('+'); |
|||
const location = { |
|||
latitude: +latitudeStr, |
|||
longitude: +longitudeStr |
|||
}; |
|||
res.location = location |
|||
}) |
|||
if (this.page == 1) { |
|||
this.list.length = 0 |
|||
this.list = res.data |
|||
} else { |
|||
this.list = this.list.concat(res.data); |
|||
} |
|||
// 判断是否还有更多数据 |
|||
if (res.data.length < res.count) { |
|||
this.status = 'noMore'; // 没有更多数据 |
|||
} else { |
|||
this.status = 'more'; // 还有更多数据 |
|||
} |
|||
}) |
|||
}, |
|||
onRefresh() { |
|||
this.page = 1 |
|||
// this.wechatMomentsList() |
|||
if (this.refreshing) return; |
|||
this.refreshing = true; |
|||
setTimeout(() => { |
|||
this.refreshing = false; |
|||
}, 1000) |
|||
}, |
|||
scrollbot(){ |
|||
if (this.status == 'noMore') { |
|||
return; |
|||
} |
|||
this.page++ |
|||
// this.wechatMomentsList() |
|||
// console.log('已触发底部事件'); |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.im-friend-header{ |
|||
width:100%;height:400rpx;position: relative; |
|||
.im-friend-bg{ |
|||
width:100%;height:400rpx;overflow: hidden; |
|||
.im-friend-image{ |
|||
width:100%;height:400rpx; |
|||
} |
|||
} |
|||
} |
|||
.im-user{ |
|||
position: absolute; |
|||
right:60rpx; |
|||
top:210rpx; |
|||
overflow: hidden; |
|||
} |
|||
.icon{ |
|||
top: -440rpx; |
|||
left: -170rpx; |
|||
width: 400rpx; |
|||
height: 400rpx; |
|||
position: absolute; |
|||
border-radius: 10rpx; |
|||
background-color: #fff; |
|||
box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2); |
|||
|
|||
.im-emoji-item{ |
|||
padding:22rpx; |
|||
} |
|||
} |
|||
.viewtop{ |
|||
padding: 0px 30rpx; |
|||
|
|||
.view_mar{ |
|||
margin-top: 100rpx; |
|||
|
|||
.view_text{ |
|||
font-size: 34rpx; |
|||
font-weight: bold; |
|||
} |
|||
.view_icons{ |
|||
display: flex; |
|||
margin-left: 40rpx; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: #c3c3c3; |
|||
} |
|||
} |
|||
} |
|||
.year-group { |
|||
padding: 0px 30rpx; |
|||
margin-top: 40rpx; |
|||
|
|||
.year-title{ |
|||
font-size: 38rpx; |
|||
} |
|||
|
|||
.moment-item{ |
|||
display: flex; |
|||
margin-top: 40rpx; |
|||
|
|||
.meta-info{ |
|||
font-size: 36rpx; |
|||
margin-right: 40rpx; |
|||
margin-left: 20rpx; |
|||
} |
|||
} |
|||
} |
|||
.content1{ |
|||
display: flex; |
|||
|
|||
.post-image{ |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
} |
|||
.post-image1{ |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
} |
|||
} |
|||
.course-video{ |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
.relative-shadow{ |
|||
z-index:1; |
|||
width:100%; |
|||
height:100%; |
|||
position: absolute; |
|||
background: #8383833d; |
|||
} |
|||
.icon-center{ |
|||
position: absolute; |
|||
top: 50%; |
|||
z-index: 4; |
|||
transform: translate(-50%, -50%); |
|||
left: 50%; |
|||
padding: 0 4rpx 0 6rpx; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,445 @@ |
|||
<template> |
|||
<cu-custom bgColor="bg-white"> |
|||
<template #backText> |
|||
<uni-icons type="left" size="22" @click="BackPage"></uni-icons> |
|||
</template> |
|||
<template #right> |
|||
<!-- disabled="true" --> |
|||
<view class="right_view"><button class="mini-btn" type="primary" size="mini" style="padding: 0px 10px;" @click="publish">发表</button></view> |
|||
</template> |
|||
</cu-custom> |
|||
<view style="padding: 10px 15px;"> |
|||
<view> |
|||
<uni-easyinput type="textarea" :inputBorder="false" v-model="params.content" placeholder="这一刻的想法...."></uni-easyinput> |
|||
</view> |
|||
<view style="height: 600rpx;"> |
|||
<sendtoImg @send="params.img_arr = $event" @videourl="video_file1 = $event" :img_arr1="img_arr1" :video_file1="video_file1"></sendtoImg> |
|||
</view> |
|||
<view> |
|||
<view class="icons_view" @click="chooseLocation"> |
|||
<view style="display: flex;align-items: center;"> |
|||
<uni-icons type="location" size="30"></uni-icons> |
|||
<view style="font-size: 18px;margin-left: 10px;">{{positioningList.content?positioningList.content:'所在位置'}}</view> |
|||
</view> |
|||
<view><uni-icons type="right" size="20"></uni-icons></view> |
|||
</view> |
|||
<!-- <view style="display: flex;align-items: center;justify-content: space-between;border-top: 1px solid #e5e5e5;padding: 10px 0px;"> |
|||
<view style="display: flex;align-items: center;"> |
|||
<uni-icons type="person" size="30"></uni-icons> <view style="font-size: 18px;">提醒谁看</view> |
|||
</view> |
|||
<view><uni-icons type="right" size="18"></uni-icons></view> |
|||
</view> --> |
|||
<view class="icons_view" style="border-bottom: 2px solid #e5e5e5;" @click="toggle"> |
|||
<view style="display: flex;align-items: center;"> |
|||
<uni-icons type="person" size="30"></uni-icons> <view style="font-size: 18px;margin-left: 10px;">谁可以看</view> |
|||
</view> |
|||
<view style="display: flex;align-items: center;"> |
|||
<view style="font-size: 18px;color: #a3a3a3;">{{params.privacy==1?'公开':params.privacy==2?'部分可见':params.privacy==3?'私密':'不给谁看'}}</view> |
|||
<uni-icons type="right" size="20"></uni-icons> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<uni-popup ref="popup" background-color="#fff" style="z-index: 999999;"> |
|||
<view class="popup-content"> |
|||
<cu-custom bgColor="bg-white"> |
|||
<template #backText> |
|||
<uni-icons type="left" size="22" @click="BackPage1"></uni-icons> |
|||
</template> |
|||
<template #content> |
|||
<view>谁可以看</view> |
|||
</template> |
|||
<template #right> |
|||
<!-- disabled="true" --> |
|||
<view class="right_view"><button class="mini-btn" type="primary" size="mini" style="padding: 0px 10px;" @click="complete">完成</button></view> |
|||
</template> |
|||
</cu-custom> |
|||
<view> |
|||
<view style="padding: 30rpx 60rpx;"> |
|||
<radio-group @change="radioChange"> |
|||
<label> |
|||
<view style="display: flex;align-items: center;margin-bottom: 30px;"> |
|||
<radio value="1" :checked="params.privacy==1" style="transform:scale(0.7)"/> |
|||
<view> |
|||
<view style="font-size: 30rpx;">公开</view> |
|||
<view style="font-size: 22rpx;color: #a3a3a3;">所有朋友可见</view> |
|||
</view> |
|||
</view> |
|||
</label> |
|||
<label> |
|||
<view style="display: flex;align-items: center;margin-bottom: 30px;"> |
|||
<radio value="3" :checked="params.privacy==3" style="transform:scale(0.7)" /> |
|||
<view> |
|||
<view style="font-size: 30rpx;">私密</view> |
|||
<view style="font-size: 22rpx;color: #a3a3a3;">仅自己可见</view> |
|||
</view> |
|||
</view> |
|||
</label> |
|||
<label> |
|||
<view style="margin-bottom: 30px;"> |
|||
<view style="display: flex;align-items: center;"> |
|||
<radio value="2" :checked="params.privacy==2" style="transform:scale(0.7)" /> |
|||
<view> |
|||
<view style="font-size: 30rpx;">部分可见</view> |
|||
</view> |
|||
</view> |
|||
<view style="padding: 40rpx 60rpx;" v-if="params.privacy==2"> |
|||
<view @click="editUser"> |
|||
<view style="color: #004f9f;font-size: 30rpx;">选择朋友</view> |
|||
<block v-for="(item,index) in user1" :key="index" v-if="user1.length!==0" style="display: flex;"> |
|||
<text style="font-size: 22rpx;margin-right: 5px;">{{item.displayName?item.displayName:item.nickname}}</text> |
|||
</block> |
|||
<view v-else style="font-size: 22rpx;">选中的朋友可见</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</label> |
|||
<label> |
|||
<view> |
|||
<view style="display: flex;align-items: center;"> |
|||
<radio value="4" :checked="params.privacy==4" style="transform:scale(0.7)" /> |
|||
<view> |
|||
<view style="font-size: 30rpx;">不给谁看</view> |
|||
</view> |
|||
</view> |
|||
<view style="padding: 40rpx 60rpx;" v-if="params.privacy==4"> |
|||
<view @click="editUser1"> |
|||
<view style="color: #004f9f;font-size: 30rpx;">选择朋友</view> |
|||
<block v-for="(item,index) in user2" :key="index" v-if="user2.length!==0" style="display: flex;"> |
|||
<text style="font-size: 22rpx;margin-right: 5px;">{{item.displayName?item.displayName:item.nickname}}</text> |
|||
</block> |
|||
<view v-else style="font-size: 22rpx;">选中的朋友不可见</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</label> |
|||
</radio-group> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</uni-popup> |
|||
</template> |
|||
|
|||
<script> |
|||
import sendtoImg from '@/components/sendtoImg.vue' |
|||
export default { |
|||
components: { |
|||
sendtoImg |
|||
}, |
|||
data() { |
|||
return { |
|||
params:{ |
|||
content:"", |
|||
privacy:1, |
|||
location:'', |
|||
status:1, |
|||
img_arr:[], |
|||
file_id:"", |
|||
user_ids:[], |
|||
posts_id:0, |
|||
video_file:"" |
|||
}, |
|||
video_file1:{}, |
|||
video_file2:{}, |
|||
positioningList:{}, |
|||
user1:[], |
|||
user2:[], |
|||
img_arr1:[], |
|||
bols:false |
|||
} |
|||
}, |
|||
created() { |
|||
// console.log(this.params.privacy); |
|||
this.getLastPosts() |
|||
}, |
|||
onShow() { |
|||
if(uni.getStorageSync('selectUser')){ |
|||
this.user1 = uni.getStorageSync('selectUser') |
|||
} |
|||
if(uni.getStorageSync('selectUser1')){ |
|||
this.user2 = uni.getStorageSync('selectUser1') |
|||
} |
|||
}, |
|||
methods: { |
|||
radioChange(e){ |
|||
this.params.privacy = +e.detail.value |
|||
// console.log(this.params.privacy); |
|||
}, |
|||
toggle() { |
|||
this.$refs.popup.open('bottom') |
|||
}, |
|||
complete(){ |
|||
this.$refs.popup.close() |
|||
if(this.params.privacy==2){ |
|||
this.user2 = []; |
|||
uni.removeStorageSync('selectUser'); |
|||
}else if(this.params.privacy==4){ |
|||
this.user1 = []; |
|||
uni.removeStorageSync('selectUser1'); |
|||
}else{ |
|||
this.user1 = []; |
|||
this.user2 = []; |
|||
uni.removeStorageSync('selectUser'); |
|||
uni.removeStorageSync('selectUser1'); |
|||
} |
|||
}, |
|||
BackPage1(){ |
|||
if(this.bols==false){ |
|||
this.params.privacy = 1; |
|||
this.user1 = []; |
|||
this.user2 = []; |
|||
uni.removeStorageSync('selectUser'); |
|||
uni.removeStorageSync('selectUser1'); |
|||
} |
|||
this.$refs.popup.close(); |
|||
}, |
|||
editUser(){ |
|||
if(uni.getStorageSync('selectUser')){ |
|||
this.user1 = uni.getStorageSync('selectUser') |
|||
const list = [] |
|||
this.user1.forEach((res)=>{ |
|||
list.push(res.user_id) |
|||
}) |
|||
uni.navigateTo({ |
|||
url: `/pages/index/userSelection?type=${5}&user_ids=${JSON.stringify(list)}` |
|||
}) |
|||
}else{ |
|||
uni.navigateTo({ |
|||
url: `/pages/index/userSelection?type=${5}` |
|||
// url: '/pages/index/userSelection?type='+type+'&contact_id=' + this.contact.id |
|||
}) |
|||
} |
|||
}, |
|||
editUser1(){ |
|||
if(uni.getStorageSync('selectUser1')){ |
|||
this.user2 = uni.getStorageSync('selectUser1') |
|||
const list1 = [] |
|||
this.user2.forEach((res)=>{ |
|||
list1.push(res.user_id) |
|||
}) |
|||
uni.navigateTo({ |
|||
url: `/pages/index/userSelection?type=${6}&user_ids=${JSON.stringify(list1)}` |
|||
}) |
|||
}else{ |
|||
uni.navigateTo({ |
|||
url: `/pages/index/userSelection?type=${6}` |
|||
// url: '/pages/index/userSelection?type='+type+'&contact_id=' + this.contact.id |
|||
}) |
|||
} |
|||
}, |
|||
transformToArrayObject(arr) { |
|||
// 创建空对象 |
|||
const resultObj = {}; |
|||
|
|||
// 遍历数组,用索引作为键 |
|||
arr.forEach((value, index) => { |
|||
resultObj[index] = value; |
|||
}); |
|||
|
|||
// 将对象包裹在数组中 |
|||
return [resultObj]; |
|||
}, |
|||
BackPage(){ |
|||
const _this = this |
|||
console.log(this.video_file1.url); |
|||
if(this.params.content!==''||this.params.img_arr.length!=0||this.video_file1.url){ |
|||
uni.showModal({ |
|||
content: '保留此次编辑', |
|||
cancelText: '不保留', |
|||
confirmText: '保留', |
|||
success: function (res) { |
|||
if (res.confirm) { |
|||
console.log('用户点击确定'); |
|||
_this.params.status = 2; |
|||
_this.publish() |
|||
} else if (res.cancel) { |
|||
_this.user1 = []; |
|||
_this.user2 = []; |
|||
uni.removeStorageSync('selectUser'); |
|||
uni.removeStorageSync('selectUser1'); |
|||
if(_this.params.posts_id){ |
|||
_this.Delete(_this.params.posts_id) |
|||
}else{ |
|||
uni.navigateBack({ |
|||
delta: 1 |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
}else{ |
|||
uni.showModal({ |
|||
content: '退出此次编辑?', |
|||
cancelText: '取消', |
|||
confirmText: '退出', |
|||
confirmColor: "red", |
|||
success: function (res) { |
|||
if (res.confirm) { |
|||
_this.user1 = []; |
|||
_this.user2 = []; |
|||
uni.removeStorageSync('selectUser'); |
|||
uni.removeStorageSync('selectUser1'); |
|||
uni.navigateBack({ |
|||
delta: 1 |
|||
}); |
|||
} else if (res.cancel) { |
|||
console.log('用户点击取消'); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
Delete(id){ |
|||
console.log(id); |
|||
this.$api.compaApi.Deleteapost({posts_id:id}).then(res => { |
|||
uni.navigateBack({ |
|||
delta: 1 |
|||
}); |
|||
}) |
|||
}, |
|||
chooseLocation(){ |
|||
uni.chooseLocation({ |
|||
success: (res) => { |
|||
// console.log('位置名称:' + res.name); |
|||
// console.log('详细地址:' + res.address); |
|||
// console.log('纬度:' + res.latitude); |
|||
// console.log('经度:' + res.longitude); |
|||
let message={ |
|||
type:'location', |
|||
status:'going', |
|||
content:'[位置] '+res.name, |
|||
name:res.name, |
|||
latitud: res.latitude+ '+' +res.longitude, |
|||
extends:res |
|||
}; |
|||
this.positioningList = message |
|||
// this.$emit('send',Object.assign(this.msgItem(), message),false); |
|||
} |
|||
}) |
|||
}, |
|||
getLastPosts(){ |
|||
this.$api.compaApi.getLastPosts().then(res => { |
|||
if(res.code==0&&res.data!==null){ |
|||
console.log(res); |
|||
this.params.content = res.data.content |
|||
// this.params.img_arr = res.data.files |
|||
this.positioningList.latitud = res.data.location |
|||
this.positioningList.content = res.data.location_address |
|||
this.params.posts_id = res.data.posts_id |
|||
this.params.privacy = res.data.privacy |
|||
if(res.data.files[0].type==1){ |
|||
this.img_arr1 = res.data.files |
|||
}else{ |
|||
this.img_arr1 = res.data.files |
|||
res.data.files.forEach((res1)=>{ |
|||
this.video_file1 = {url:res1.src,poster:res1.privacy,type:res1.type} |
|||
}) |
|||
} |
|||
this.bols = true |
|||
if(res.data.privacy==2){ |
|||
this.user1 = res.data.privacy_user |
|||
uni.setStorageSync('selectUser', this.user1); |
|||
}else if(res.data.privacy==4){ |
|||
this.user2 = res.data.privacy_user |
|||
uni.setStorageSync('selectUser1', this.user2); |
|||
} |
|||
// console.log(res); |
|||
// this.list = res.data |
|||
} |
|||
}) |
|||
}, |
|||
publish(){ |
|||
let list1 = [] |
|||
let list2 = [] |
|||
let listUrl = [] |
|||
|
|||
if(this.params.privacy == 2){ |
|||
this.user1.forEach((res)=>{ |
|||
list1.push(res.user_id) |
|||
}) |
|||
} else if(this.params.privacy == 4){ |
|||
this.user2.forEach((res)=>{ |
|||
list2.push(res.user_id) |
|||
}) |
|||
} |
|||
|
|||
if(this.img_arr1&&this.img_arr1[0]?.type==1){ |
|||
this.img_arr1.forEach(res=>{ |
|||
listUrl.push(res.src) |
|||
}) |
|||
} |
|||
if(this.params.content==''&&this.params.img_arr.length==0&&this.video_file1.url==undefined){ |
|||
uni.showToast({title: '文字或图片、视频必须上传一个,并且文字不能是空格',icon:'none'}); |
|||
return |
|||
} |
|||
|
|||
const data = { |
|||
privacy: this.params.privacy, |
|||
content: this.params.content||'', |
|||
status: this.params.status, |
|||
location: this.positioningList.latitud || '', |
|||
address: this.positioningList.content || '', |
|||
img_arr: this.params.img_arr.length==0?listUrl:this.params.img_arr||'', |
|||
video_file: this.video_file1.url || '', |
|||
poster_file: this.video_file1.poster || '', |
|||
user_ids: this.params.privacy == 2 ? list1 : this.params.privacy == 4 ? list2 : [], |
|||
posts_id: this.params.posts_id||'' |
|||
} |
|||
// console.log('传值',data); |
|||
this.$api.compaApi.wechatMomentsadd(data).then(res => { |
|||
// this.list = res.data |
|||
// console.log(res); |
|||
if(res.code==0){ |
|||
list1 = [] |
|||
list2 = [] |
|||
if(this.params.status==2){ |
|||
uni.showToast({title: '保存成功'}); |
|||
}else{ |
|||
uni.showToast({title: '添加成功'}); |
|||
} |
|||
uni.navigateBack({ |
|||
delta: 1 |
|||
}); |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.right_view{ |
|||
height: 30px; |
|||
margin-right: 10px; |
|||
} |
|||
uni-button[disabled][type='primary']{ |
|||
background-color:#e7e7e7; |
|||
color: #c6c6c6; |
|||
} |
|||
uni-button:after{ |
|||
border: 0px; |
|||
} |
|||
::v-deep .bg-white{ |
|||
background-color: #f1f1f1 !important; |
|||
} |
|||
::v-deep .uni-easyinput__content{ |
|||
background-color: #f1f1f1 !important; |
|||
} |
|||
.icons_view{ |
|||
display: flex; |
|||
padding: 10px 10px; |
|||
align-items: center; |
|||
border-top: 2px solid #e5e5e5; |
|||
justify-content: space-between; |
|||
} |
|||
::v-deep .uni-modal__bd{ |
|||
color: #000; |
|||
} |
|||
.popup-content { |
|||
height: 100vh; |
|||
align-items: center; |
|||
background-color: #fff; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,101 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>黑名单列表</template> |
|||
<!-- <template #right> |
|||
<view class="f-20 ml-10 mr-10" @tap="search()"> |
|||
<text class="cuIcon-search"></text> |
|||
</view> |
|||
</template> --> |
|||
</cu-custom> |
|||
<view class="cu-list menu-avatar no-padding"> |
|||
<view class="flex List-item" v-for="(items,sub) in groupList" :key="sub"> |
|||
<view class="flex align-center" style="padding: 10px;"> |
|||
<view class='cu-avatar lg radius mr-10' :style="[{backgroundImage:'url('+items.avatar+')'}]"> |
|||
</view> |
|||
<view> |
|||
<view class="c-333">{{items.displayName}}</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="flex"> |
|||
<view class="fc-danger mr-10" @tap="setis_blacklist(items)">移除黑名单</view> |
|||
<view class="action fc-primary mr-10" @tap='openDetails(items)'> |
|||
<view>发送信息</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<Empty v-if="!groupList.length" noDatatext="暂无数据" textcolor="#999" ></Empty> |
|||
</view> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
import { useloginStore } from '@/store/login' |
|||
import pinia from '@/store/index' |
|||
const userStore = useloginStore(pinia); |
|||
const msgStore = useMsgStore(pinia) |
|||
const {contacts} = storeToRefs(msgStore); |
|||
/** |
|||
* 初始的引导页 |
|||
*/ |
|||
export default { |
|||
name : "group", |
|||
data() { |
|||
return { |
|||
groupList:[], |
|||
userInfo:userStore.userInfo |
|||
}; |
|||
}, |
|||
created() { |
|||
}, |
|||
mounted(){ |
|||
this.initContacts(); |
|||
}, |
|||
methods: { |
|||
initContacts(arr){ |
|||
this.$api.msgApi.initContacts().then(res => { |
|||
this.groupList = res.data.filter(item=>item.is_blacklist==1) |
|||
}) |
|||
}, |
|||
setis_blacklist(items){ |
|||
this.$api.msgApi.isBlacklist({friend_user_id:items.user_id}).then(res => { |
|||
if (res.code == 0) { |
|||
uni.showToast({ |
|||
title: '移除成功!' |
|||
}) |
|||
this.initContacts(); |
|||
} |
|||
}) |
|||
}, |
|||
// 打开聊天 |
|||
openDetails(items){ |
|||
uni.navigateTo({ |
|||
url:"/pages/message/chat?id="+items.id |
|||
}) |
|||
}, |
|||
search(){ |
|||
uni.navigateTo({ |
|||
url:"/pages/index/search?type=4" |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.fc-primary{ |
|||
color: #409eff; |
|||
} |
|||
.fc-danger{ |
|||
color: #f56c6c; |
|||
} |
|||
.List-item{ |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,327 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-white" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>个人信息</template> |
|||
</cu-custom> |
|||
<view class="padding flex justify-start align-center"> |
|||
<block v-for="(items,indexs) in imglist" :key="indexs" v-if="detail.imgname&&network_log == 'none'"> |
|||
<view class='cu-avatar lg radius mr-15' v-if="detail.imgname === items.name" @tap="showAvatar(detail)" :style="'background-image:url('+items.path+')'"></view> |
|||
</block> |
|||
<view v-else class='cu-avatar lg radius mr-15' @tap="showAvatar(detail)" :style="'background-image:url('+detail.avatar+')'"></view> |
|||
|
|||
<view class='im-flex im-justify-content-start im-columns'> |
|||
<view class="mb-5">{{detail.friend?.nickname?detail.friend.nickname:detail.realname}}</view> |
|||
<view class="text-gray">{{detail.account}}</view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-list menu"> |
|||
<view class="cu-item" v-if="globalConfig.sysInfo.runMode==2 && detail.friend && userInfo.user_id!=detail.user_id" @tap="setNickname"> |
|||
<view class="content"> |
|||
<text class="cuIcon-edit text-green"></text> |
|||
<text>备注</text> |
|||
</view> |
|||
<view class="action"> |
|||
<text class="text-grey text-sm">{{detail.friend.nickname || '未设置'}}</text> |
|||
<text class="text-grey text-sm ml-5 cuIcon-write"></text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item"> |
|||
<view class="content"> |
|||
<text class="cuIcon-mail text-green"></text> |
|||
<text>昵称</text> |
|||
</view> |
|||
<view class="action"> |
|||
<text class="text-grey text-sm">{{detail.realname}}</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item"> |
|||
<view class="content"> |
|||
<text class="cuIcon-mail text-green"></text> |
|||
<text>邮箱</text> |
|||
</view> |
|||
<view class="action"> |
|||
<text class="text-grey text-sm">{{detail.email ?? ''}}</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item"> |
|||
<view class="content"> |
|||
<text class="cuIcon-safe text-green"></text> |
|||
<text>性别</text> |
|||
</view> |
|||
<view class="action"> |
|||
<text class="text-grey text-sm">{{ sex(detail.sex)}}</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" v-if="parseInt(globalConfig.sysInfo.ipregion)"> |
|||
<view class="content"> |
|||
<text class="cuIcon-location text-green"></text> |
|||
<text>IP</text> |
|||
</view> |
|||
<view class="action"> |
|||
<text class="text-grey text-sm" v-if="detail.last_login_ip">{{ detail.last_login_ip || "未知"}} ({{detail.location || "未知"}})</text> |
|||
<text class="text-grey text-sm" v-else>未知</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @click="toFriendsCircle(detail)"> |
|||
<view class="content flex"> |
|||
<view class="flex justify-center" style="width: 1.6em;margin-right: 0.3125rem;"><uni-icons type="pyq" size="15" color="#45ba5b"></uni-icons></view> |
|||
<view>朋友圈</view> |
|||
</view> |
|||
<view class="action"> |
|||
<text class="text-grey cuIcon-right"></text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<template class="" v-if="userInfo.user_id!=detail.user_id"> |
|||
<view class="padding flex flex-direction" v-if="globalConfig.sysInfo.runMode==1 || detail.friend"> |
|||
<button class="cu-btn bg-green mt-10 lg" @tap="sendMsg(detail)">发消息</button> |
|||
<button class="cu-btn bg-blue mt-10 lg" v-if="validatePhone" @tap="callPhone()">打电话</button> |
|||
<!-- #ifdef APP | H5 --> |
|||
<button class="cu-btn bg-grey mt-10 lg" @tap="modelName='callRtc'" v-if="parseInt(globalConfig.chatInfo.webrtc) && parseInt(globalConfig.chatInfo.simpleChat)">音视频通话</button> |
|||
<!-- #endif --> |
|||
|
|||
<button class="cu-btn bg-red mt-10 lg" @tap="delFriend()" v-if="globalConfig.sysInfo.runMode==2">删除好友</button> |
|||
</view> |
|||
<view class="padding flex flex-direction" v-if="globalConfig.sysInfo.runMode==2 && !detail.friend"> |
|||
<button class="cu-btn bg-green lg" @tap="addFriend()">加好友</button> |
|||
</view> |
|||
</template> |
|||
<view class="cu-modal bottom-modal" :class="modelName=='callRtc'?'show':''" @tap="modelName=''"> |
|||
<view class="cu-dialog"> |
|||
<view class="manage-content"> |
|||
<view class="cu-list menu bg-white"> |
|||
<view class="cu-item" @tap="calling(0)"> |
|||
<view class="content padding-tb-sm"> |
|||
<text class="cuIcon-dianhua"></text> |
|||
<text>语音通话</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @tap="calling(1)"> |
|||
<view class="content padding-tb-sm"> |
|||
<text class=" cuIcon-record"></text> |
|||
<text>视频通话</text> |
|||
</view> |
|||
</view> |
|||
<view class="parting-line-5"></view> |
|||
<view class="cu-item" @tap="modelName=''"> |
|||
<view class="content padding-tb-sm"> |
|||
<text class="c-red">取消</text> |
|||
</view> |
|||
</view> |
|||
|
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useMsgStore } from '@/store/message'; |
|||
import { useloginStore } from '@/store/login'; |
|||
// #ifdef APP-PLUS |
|||
import {getSavedImages} from '@/utils/LocalFileSystemURL.js' |
|||
// #endif |
|||
import getUserInfo from '@/service/getUserInfo'; |
|||
import pinia from '@/store/index' |
|||
import { storeToRefs } from 'pinia'; |
|||
const msgStore = useMsgStore(pinia) |
|||
const userStore = useloginStore(pinia) |
|||
const {network_log} = storeToRefs(msgStore); |
|||
export default { |
|||
data() { |
|||
return { |
|||
modelName:'', |
|||
detail:{}, |
|||
userInfo:userStore.userInfo, |
|||
globalConfig:userStore.globalConfig, |
|||
network_log:'', |
|||
imglist:[], |
|||
ids:0 |
|||
} |
|||
}, |
|||
computed: { |
|||
validatePhone(){ |
|||
let reg = /^1[3456789]\d{9}$/; |
|||
return reg.test(this.detail.account); |
|||
} |
|||
}, |
|||
onLoad(options) { |
|||
const _this = this |
|||
this.network_log = uni.getStorageSync('network_log') |
|||
if(_this.network_log == 'none'){ |
|||
this.getImagePath() |
|||
this.getUserInfoList(options.id) |
|||
}else{ |
|||
this.ids = options.id |
|||
this.getInfo() |
|||
// this.getImagePath() |
|||
// this.getUserInfoList(options.id) |
|||
} |
|||
}, |
|||
methods: { |
|||
getInfo(){ |
|||
this.$api.msgApi.getUserInfo({user_id:this.ids}).then((res)=>{ |
|||
if(res.code==0){ |
|||
this.detail=res.data; |
|||
const list = [] |
|||
list.push(res.data) |
|||
this.insertUserInfo(list) |
|||
} |
|||
}) |
|||
}, |
|||
async insertUserInfo(val){ |
|||
await getUserInfo.batchInsertOrUpdate(val) |
|||
}, |
|||
async getUserInfoList(user_id){ |
|||
try { |
|||
const list = await getUserInfo.getList({user_id:user_id}); |
|||
list.forEach((item)=>{ |
|||
const parts = item.avatar.split('/'); |
|||
let lastPart = parts.pop() || parts.pop() || ''; |
|||
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart)); |
|||
item.imgname = isNumber ? lastPart+'.png' : lastPart; |
|||
item.friend = JSON.parse(item.friend); |
|||
item.setting = JSON.parse(item.setting); |
|||
}) |
|||
this.detail = list[0]; |
|||
console.log('获取个人信息数据',list.length,list); |
|||
} catch (error) { |
|||
console.error(error); |
|||
} |
|||
}, |
|||
async getImagePath(){ |
|||
this.imglist = await getSavedImages() |
|||
this.imglist.map(item => { |
|||
item.path = plus.io.convertLocalFileSystemURL(item.path) |
|||
}); |
|||
// console.info(this.imglist,'读取地址'); |
|||
}, |
|||
showAvatar(detail){ |
|||
let imgs=[]; |
|||
imgs.push(detail.avatar); |
|||
uni.previewImage({urls : imgs}) |
|||
}, |
|||
sendMsg(info){ |
|||
uni.reLaunch({ |
|||
url:"/pages/message/chat?id="+info.user_id |
|||
}) |
|||
}, |
|||
toFriendsCircle(info){ |
|||
uni.navigateTo({ |
|||
url:"/pages/compass/personalcircleoffriends?user_id="+info.user_id+"&avatar="+info.avatar+"&realname="+info.realname |
|||
}) |
|||
}, |
|||
sex(value) { |
|||
let arr = ['女', '男','未知'] |
|||
return arr[value] || '未知'; |
|||
}, |
|||
callPhone(){ |
|||
uni.makePhoneCall({ |
|||
phoneNumber: this.detail.account |
|||
}); |
|||
}, |
|||
calling(is_video){ |
|||
if(msgStore.webrtcLock){ |
|||
return uni.showToast({ |
|||
title:'其他终端正在通话中', |
|||
icon:'none' |
|||
}) |
|||
} |
|||
this.modelName=''; |
|||
let msg_id=this.$util.getUuid(); |
|||
uni.navigateTo({ |
|||
url: '/pages/message/call?msg_id='+msg_id+'&type='+is_video+'&status=1&id='+this.detail.user_id+'&name='+this.detail.realname+'&avatar='+encodeURI(this.detail.avatar) |
|||
}) |
|||
}, |
|||
delFriend(){ |
|||
uni.showModal({ |
|||
title: '确定要删除该好友吗?', |
|||
success: (res)=>{ |
|||
if (res.confirm) { |
|||
let data={ id: this.detail.user_id}; |
|||
this.$api.friendApi.delFriend(data).then((res)=>{ |
|||
if(res.code==0){ |
|||
msgStore.deleteContacts(data); |
|||
// console.log(data,'123456'); |
|||
uni.reLaunch({ |
|||
url: '/pages/index/index' |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
}, |
|||
addFriend(){ |
|||
uni.showModal({ |
|||
title: '请输入验证信息', |
|||
editable:true, |
|||
success: (res)=>{ |
|||
if (res.confirm) { |
|||
if(res.content==''){ |
|||
return uni.showToast({ |
|||
title:'请输入备注!', |
|||
icon:'error' |
|||
}) |
|||
} |
|||
this.$api.friendApi.addFriend({user_id:this.detail.user_id,remark:res.content}).then((e)=>{ |
|||
if(e.code==0){ |
|||
uni.showToast({ |
|||
title:e.msg, |
|||
icon:'none' |
|||
}) |
|||
this.getInfo() |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
setNickname(){ |
|||
let friend_id=this.detail.friend.friend_id ?? ''; |
|||
if(!this.detail.friend){ |
|||
return uni.showToast({ |
|||
title:'无法设置', |
|||
icon:'error' |
|||
}) |
|||
} |
|||
uni.showModal({ |
|||
title: '请输入备注信息', |
|||
editable:true, |
|||
success: (res)=>{ |
|||
if (res.confirm) { |
|||
// if(res.content==''){ |
|||
// return uni.showToast({ |
|||
// title:'请输入好友备注!', |
|||
// icon:'error' |
|||
// }) |
|||
// } |
|||
this.$api.friendApi.setNickname({friend_id:friend_id,nickname:res.content}).then((e)=>{ |
|||
if(e.code==0){ |
|||
this.detail.friend.nickname=res.content; |
|||
// 修改备注后修改联系人 |
|||
msgStore.updateContacts({ |
|||
id:this.detaild.user_id, |
|||
displayName:res.content |
|||
}) |
|||
uni.showToast({ |
|||
title:e.msg, |
|||
icon:'none' |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
|
|||
</style> |
|||
@ -0,0 +1,150 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-gradual-blue" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>新邀请</template> |
|||
<template #right> |
|||
<view class="f-20 ml-10 mr-10" @tap="searchFriend()"> |
|||
<text class="cuIcon-add"></text> |
|||
</view> |
|||
</template> |
|||
</cu-custom> |
|||
<uni-segmented-control :current="current" :values="items" @clickItem="onClickItem" styleType="text"></uni-segmented-control> |
|||
<view class="cu-list menu"> |
|||
<view class="cu-item" v-if="!params.is_mine" v-for="(x,index) in list" :key="index"> |
|||
<view class='cu-avatar md radius mr-15' :style="[{backgroundImage:'url('+x.create_user_info.avatar+')'}]"> |
|||
</view> |
|||
<view class="content padding-tb-sm" @tap='openDetails(x.create_user_info.user_id)'> |
|||
<view class="text-grey" v-if="!params.is_mine"> |
|||
<text class="text-blue">{{x.create_user_info.realname}} </text> 申请添加您为好友 |
|||
</view> |
|||
<view class="text-gray text-sm lh-15x">{{x.remark}}</view> |
|||
</view> |
|||
<view class="action ml-10"> |
|||
<text class="text-red" v-if="x.status==0">已拒绝</text> |
|||
<text class="text-blue" v-if="x.status==1" @tap="sendMsg(x.create_user_info.user_id)">发消息</text> |
|||
<button class="cu-btn round sm bg-green" v-if="x.status==2" @tap="optApply(x)"> |
|||
操作 |
|||
</button> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" v-if="params.is_mine" v-for="(x,index) in list" :key="index"> |
|||
<view class='cu-avatar md radius mr-15' :style="[{backgroundImage:'url('+x.user_id_info.avatar+')'}]"> |
|||
</view> |
|||
<view class="content" @tap='openDetails(x.user_id_info.user_id)'> |
|||
<view class="text-grey"> |
|||
请求添加<text class="text-blue"> {{x.user_id_info.realname}} </text> 为好友 |
|||
</view> |
|||
</view> |
|||
<view class="action ml-10"> |
|||
<text class="text-red" v-if="x.status==0">已拒绝</text> |
|||
<text class="text-blue" v-if="x.status==1" @tap="sendMsg(x.user_id_info.user_id)">发消息</text> |
|||
<text class="text-orange" v-if="x.status==2">待同意</text> |
|||
</view> |
|||
</view> |
|||
<view class="m-10" v-if="list.length"> |
|||
<uni-pagination :current="params.page" :total="total" :pageSize="params.limit" @change="changePage"/> |
|||
</view> |
|||
|
|||
<Empty v-if="!list.length" noDatatext="暂无群聊" textcolor="#999" ></Empty> |
|||
</view> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
import pinia from '@/store/index' |
|||
const msgStore = useMsgStore(pinia) |
|||
const {contacts} = storeToRefs(msgStore); |
|||
/** |
|||
* 初始的引导页 |
|||
*/ |
|||
export default { |
|||
name : "group", |
|||
data() { |
|||
return { |
|||
items: ['我收到的', '我发起的'], |
|||
current: 0, |
|||
list: [], |
|||
total:0, |
|||
params: { |
|||
page: 1, |
|||
limit: 10, |
|||
is_mine:0 |
|||
} |
|||
}; |
|||
}, |
|||
created() { |
|||
}, |
|||
mounted(){ |
|||
this.getList(); |
|||
}, |
|||
methods: { |
|||
getList(){ |
|||
this.$api.friendApi.applyList(this.params).then((res)=>{ |
|||
if(res.code==0){ |
|||
this.list = res.data; |
|||
this.total = res.count; |
|||
} |
|||
}) |
|||
}, |
|||
changePage(e){ |
|||
this.params.page=e.current; |
|||
this.getList(); |
|||
}, |
|||
onClickItem(e) { |
|||
this.params.is_mine = e.currentIndex; |
|||
this.current = e.currentIndex; |
|||
this.params.page = 1; |
|||
this.getList(); |
|||
}, |
|||
sendMsg(id){ |
|||
uni.navigateTo({ |
|||
url:"/pages/message/chat?id="+id |
|||
}) |
|||
}, |
|||
openDetails(id){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+id |
|||
}) |
|||
}, |
|||
searchFriend(){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/search" |
|||
}) |
|||
}, |
|||
optApply(x){ |
|||
uni.showModal({ |
|||
title: '提示', |
|||
content:"你确定同意该好友的请求吗", |
|||
cancelText:"拒绝", |
|||
cancelColor:'#e54d42', |
|||
confirmText:"接受", |
|||
success: (res)=>{ |
|||
let status=0; |
|||
if (res.confirm) { |
|||
status=1 |
|||
} |
|||
this.$api.friendApi.acceptApply({friend_id:x.friend_id,status:status}).then((e)=>{ |
|||
if(e.code==0){ |
|||
uni.showToast({ |
|||
title:'添加好友成功', |
|||
icon:'none' |
|||
}) |
|||
msgStore.sysUnread--; |
|||
this.getList(); |
|||
} |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
</style> |
|||
@ -0,0 +1,106 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>群聊列表</template> |
|||
<template #right> |
|||
<view class="f-20 ml-10 mr-10" @tap="search()"> |
|||
<text class="cuIcon-search"></text> |
|||
</view> |
|||
</template> |
|||
</cu-custom> |
|||
<view class="cu-list menu-avatar no-padding"> |
|||
<view class="cu-item" v-for="(items,sub) in groupList" :key="sub" @tap='openDetails(items)'> |
|||
<block v-for="(itemse,indexs) in imglist" :key="indexs" v-if="network_log == 'none'"> |
|||
<view v-if="items.imgname == itemse.name" class='cu-avatar lg radius mr-15' :style="[{backgroundImage:'url('+ itemse.path +')'}]"></view> |
|||
</block> |
|||
<view v-else class='cu-avatar lg radius mr-15' :style="[{backgroundImage:'url('+items.avatar+')'}]"> |
|||
</view> |
|||
<view class="content"> |
|||
<view class="c-333">{{items.displayName}}</view> |
|||
</view> |
|||
<view class="action"> |
|||
<text class="c-999 cuIcon-peoplefill" v-if="items.owner_id==userInfo.user_id"></text> |
|||
</view> |
|||
|
|||
</view> |
|||
<Empty v-if="!groupList.length" noDatatext="暂无群聊" textcolor="#999" ></Empty> |
|||
</view> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
import { useloginStore } from '@/store/login' |
|||
import pinia from '@/store/index' |
|||
// #ifdef APP-PLUS |
|||
import {getSavedImages} from '@/utils/LocalFileSystemURL.js' |
|||
// #endif |
|||
const userStore = useloginStore(pinia); |
|||
const msgStore = useMsgStore(pinia) |
|||
const {contacts,network_log} = storeToRefs(msgStore); |
|||
/** |
|||
* 初始的引导页 |
|||
*/ |
|||
export default { |
|||
name : "group", |
|||
data() { |
|||
return { |
|||
groupList:[], |
|||
userInfo:userStore.userInfo, |
|||
imglist:[], |
|||
network_log:network_log, |
|||
}; |
|||
}, |
|||
created() {}, |
|||
mounted(){ |
|||
this.initContacts(this.msgs); |
|||
// #ifdef APP-PLUS |
|||
this.getImagePath() |
|||
// #endif |
|||
}, |
|||
methods: { |
|||
initContacts(arr){ |
|||
const allContacts=uni.getStorageSync('allContacts'); |
|||
const contacts=allContacts.filter((item)=>{ |
|||
return item.is_group==1; |
|||
}) |
|||
// 将联系人进行排序 |
|||
const sorted = contacts.sort((a, b) => { |
|||
if (a.index === '#') { |
|||
return 1; |
|||
} |
|||
if (b.index === '#') { |
|||
return -1; |
|||
} |
|||
return a.index.localeCompare(b.index, 'zh'); |
|||
}); |
|||
this.groupList=sorted; |
|||
}, |
|||
async getImagePath(){ |
|||
this.imglist = await getSavedImages() |
|||
this.imglist.map(item => { |
|||
item.path = plus.io.convertLocalFileSystemURL(item.path) |
|||
}); |
|||
// console.info('读取地址',this.imglist); |
|||
}, |
|||
// 打开聊天 |
|||
openDetails(items){ |
|||
uni.navigateTo({ |
|||
url:"/pages/message/chat?id="+items.id |
|||
}) |
|||
}, |
|||
search(){ |
|||
uni.navigateTo({ |
|||
url:"/pages/index/search?type=3" |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
</style> |
|||
@ -0,0 +1,447 @@ |
|||
<template> |
|||
<view> |
|||
<scroll-view scroll-y class="indexes" :scroll-into-view="'indexes-'+ listCurID" :style="[{height:'calc(100vh - '+ (CustomBar+inlineTools+StatusBar) + 'px)'}]" |
|||
:scroll-with-animation="true" :enable-back-to-top="true" v-if="TabCur==0"> |
|||
<view style="padding-bottom:30rpx"> |
|||
<view class="cu-list menu mt-10"> |
|||
<view class="cu-item arrow" @tap="openFriend" v-if="globalConfig.sysInfo.runMode==2"> |
|||
<view class='cu-avatar mr-15 invite-bg' :class="appSetting.circleAvatar?'round':'radius'"> |
|||
</view> |
|||
<view class="content"> |
|||
<text class="c-333">新邀请</text> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="cu-tag round bg-red sm" v-if="unread>0">{{unread}}</view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item arrow" @tap="openGroup"> |
|||
<view class='cu-avatar mr-15 group-bg' :class="appSetting.circleAvatar?'round':'radius'"> |
|||
</view> |
|||
<view class="content"> |
|||
<text class="c-333">群聊</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item arrow" @tap="openBlacklist"> |
|||
<view class='cu-avatar mr-15 blacklist' :class="appSetting.circleAvatar?'round':'radius'"></view> |
|||
<view class="content"> |
|||
<text class="c-333">黑名单</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item arrow" @tap="openChat(item.id)" v-for="(item,index) in systemContact" :key="index"> |
|||
<block v-for="(items,indexs) in imglist" :key="indexs" v-if="network_log == 'none'"> |
|||
<view class='cu-avatar mr-15' v-if="item.imgname === items.name" :style="{background:'url('+items.path+')',backgroundSize: 'contain'}" :class="appSetting.circleAvatar?'round':'radius'"></view> |
|||
</block> |
|||
<view v-else class='cu-avatar mr-15' :style="{background:'url('+item.avatar+')',backgroundSize: 'contain'}" :class="appSetting.circleAvatar?'round':'radius'"></view> |
|||
<view class="content"> |
|||
<text class="c-333">{{item.displayName}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<block v-for="(item,index) in contacts" :key="index"> |
|||
<view :class="'indexItem-' + item.name" :id="'indexes-' + item.name" :data-index="item.name"> |
|||
<view class="padding">{{item.name}}</view> |
|||
<view class="cu-list menu no-padding"> |
|||
<view class="cu-item" v-for="(items,sub) in item.children" :key="sub" @tap='openDetails(items)'> |
|||
<block v-for="(iteme,indexs) in imglist" :key="indexs" v-if="network_log == 'none'"> |
|||
<view class="cu-avatar mr-15" v-if="items.imgname === iteme.name" :class="appSetting.circleAvatar?'round':'radius'" :style="[{backgroundImage:'url('+ iteme.path +')'}]"> |
|||
<view class="online-status-small" v-if="items.is_online && items.is_group==0 && globalConfig.chatInfo.online==1" ></view> |
|||
</view> |
|||
</block> |
|||
<view v-else class='cu-avatar mr-15' :class="appSetting.circleAvatar?'round':'radius'" :style="[{backgroundImage:'url('+items.avatar+')'}]"> |
|||
<view class="online-status-small" v-if="items.is_online && items.is_group==0 && globalConfig.chatInfo.online==1" ></view> |
|||
</view> |
|||
|
|||
<view class="content"> |
|||
<view class="c-333">{{items.displayName}}</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</block> |
|||
<view class="text-center m-20 text-grey">{{total}} 个朋友</view> |
|||
<Empty v-if="!contacts.length" noDatatext="暂无联系人" textcolor="#999" ></Empty> |
|||
</view> |
|||
</scroll-view> |
|||
|
|||
<view class="indexBar" :style="[{height:'calc(100vh - ' + CustomBar + 'px - 50px)'}]" v-if="TabCur==0"> |
|||
<view class="indexBar-box" @touchstart="tStart" @touchend="tEnd" @touchmove.stop="tMove"> |
|||
<view class="indexBar-item" v-for="(item,index) in contacts" :key="index" :id="index" @touchstart="getCur" @touchend="setCur"> {{item.name}}</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="cu-bar bg-white search fixed" :style="[{top:CustomBar + 'px'}]" v-if="TabCur==1"> |
|||
<view class="search-form round"> |
|||
<text class="cuIcon-search"></text> |
|||
<input type="text" v-model="keywords" placeholder="输入搜索的关键词" confirm-type="search"/> |
|||
</view> |
|||
</view> |
|||
<scroll-view scroll-y class="indexes" :style="[{top:'50px',height:'calc(100vh - '+ (CustomBar+inlineTools+StatusBar) + 'px)'}]" |
|||
:scroll-with-animation="true" :enable-back-to-top="true" v-if="TabCur==1"> |
|||
<view class=""> |
|||
<breadcurm :tree='tree' @openBread="openDep"></breadcurm> |
|||
</view> |
|||
<view class="im-department-list"> |
|||
<navigator class="im-flex im-justify-content-start im-align-items-center mt-10" @tap="openDep(item)" |
|||
v-for="(item, depindex) in depList" :key="depindex"> |
|||
<view class="im-folder-bar mr-10"> |
|||
<text class="cuIcon-file color-blue"></text> |
|||
</view> |
|||
<view class="im-list-body im-border-b"> |
|||
<view class="im-list-title word"> |
|||
{{item.name}} |
|||
</view> |
|||
</view> |
|||
</navigator> |
|||
<navigator class="im-flex im-justify-content-start im-align-items-center mt-10" v-for="(item, index) in userList" |
|||
:key="item.id" :url="'/pages/contacts/detail?user_id=' + item.id"> |
|||
<view class="im-folder-bar im-image mr-10"> |
|||
<image :src="item.avatar" mode="widthFix"></image> |
|||
</view> |
|||
<view class="im-list-body im-border-b"> |
|||
<view class="im-list-title word"> |
|||
{{item.realname}} |
|||
</view> |
|||
</view> |
|||
</navigator> |
|||
<Empty v-if="depList.length==0 && userList.length==0"></Empty> |
|||
</view> |
|||
</scroll-view> |
|||
|
|||
<!--选择显示--> |
|||
<view v-show="!hidden" class="indexToast"> |
|||
{{listCur}} |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import breadcurm from "@/components/breadcrum.vue" |
|||
import statusPoint from '@/components/status.vue' |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
import { useloginStore } from '@/store/login'; |
|||
// #ifdef APP-PLUS |
|||
import {getSavedImages} from '@/utils/LocalFileSystemURL.js' |
|||
// #endif |
|||
import pinia from '@/store/index' |
|||
const msgStore = useMsgStore(pinia) |
|||
const userStore = useloginStore(pinia) |
|||
const {contacts,sysUnread,network_log} = storeToRefs(msgStore); |
|||
/** |
|||
* 初始的引导页 |
|||
*/ |
|||
export default { |
|||
components: { |
|||
breadcurm, |
|||
statusPoint |
|||
}, |
|||
name : "contacts", |
|||
props:{ |
|||
TabCur:{type:Number, default:0} |
|||
}, |
|||
data() { |
|||
return { |
|||
//#ifdef H5 |
|||
tabbarH:50, |
|||
//#endif |
|||
//#ifndef H5 |
|||
tabbarH: 100, |
|||
//#endif |
|||
hidden: true, |
|||
listCurID: '', |
|||
list: [], |
|||
systemContact:[], |
|||
network_log:network_log, |
|||
listCur: '', |
|||
total:0, |
|||
scrollLeft: 0, |
|||
msgs: contacts, |
|||
unread:sysUnread, |
|||
globalConfig:userStore.globalConfig, |
|||
appSetting:userStore.appSetting, |
|||
tree:[ |
|||
{ |
|||
id:1, |
|||
name:'技术部' |
|||
}, |
|||
{ |
|||
id:2, |
|||
name:'产品部' |
|||
}, |
|||
{ |
|||
id:3, |
|||
name:'信息部' |
|||
}, |
|||
], |
|||
depList:[ |
|||
{ |
|||
id:1, |
|||
name:"技术部" |
|||
}, |
|||
{ |
|||
id:2, |
|||
name:"财务部" |
|||
} |
|||
], |
|||
userList:[ |
|||
{ |
|||
id:1, |
|||
realname:"张三", |
|||
avatar:'https://api.multiavatar.com/raingad1.png?apikey=zdvXV3W4MjwhP9' |
|||
}, |
|||
{ |
|||
id:2, |
|||
realname:"李四", |
|||
avatar:'https://api.multiavatar.com/raingad2.png?apikey=zdvXV3W4MjwhP9' |
|||
} |
|||
], |
|||
tabList:[ |
|||
"普通通讯录", |
|||
"企业通讯录" |
|||
], |
|||
contacts:[], |
|||
imglist:[] |
|||
}; |
|||
}, |
|||
watch:{ |
|||
msgs(val){ |
|||
this.initContacts(val); |
|||
} |
|||
}, |
|||
created() { |
|||
this.listCur = this.contacts[0]; |
|||
// #ifdef APP-PLUS |
|||
this.getImagePath() |
|||
// #endif |
|||
}, |
|||
mounted(){ |
|||
this.initContacts(this.msgs); |
|||
}, |
|||
methods: { |
|||
async getImagePath(){ |
|||
this.imglist = await getSavedImages() |
|||
this.imglist.map(item => { |
|||
item.path = plus.io.convertLocalFileSystemURL(item.path) |
|||
}); |
|||
// console.info(this.imglist,'读取地址'); |
|||
}, |
|||
initContacts(arr){ |
|||
const allContacts=JSON.parse(JSON.stringify(arr)); |
|||
const contacts=allContacts.filter((item)=>{ |
|||
return item.is_group==0; |
|||
}) |
|||
this.systemContact=allContacts.filter((item)=>{ |
|||
return item.is_group==2||item.is_group==4; |
|||
}) |
|||
this.total=contacts.length; |
|||
// 将联系人进行排序 |
|||
const sorted = contacts.sort((a, b) => { |
|||
if (a.index === '#') { |
|||
return 1; |
|||
} |
|||
if (b.index === '#') { |
|||
return -1; |
|||
} |
|||
return a.index.localeCompare(b.index, 'zh'); |
|||
}); |
|||
// 重组联系人 |
|||
const result = sorted.reduce((acc, cur) => { |
|||
const index = cur.index; |
|||
const existingIndex = acc.findIndex(item => item.name === index); |
|||
if (existingIndex === -1) { |
|||
acc.push({ name: index, children: [cur] }); |
|||
} else { |
|||
acc[existingIndex].children.push(cur); |
|||
} |
|||
return acc; |
|||
}, []); |
|||
this.contacts=result; |
|||
}, |
|||
openDep(item){ |
|||
|
|||
}, |
|||
openChat(id){ |
|||
uni.navigateTo({ |
|||
url:"/pages/message/chat?id=" + id, |
|||
animationType:"slide-in-right" |
|||
}) |
|||
}, |
|||
// 打开聊天详情 |
|||
openDetails(items){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+items.id |
|||
}) |
|||
}, |
|||
openGroup(){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/group" |
|||
}) |
|||
}, |
|||
openBlacklist(){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/blacklist" |
|||
}) |
|||
}, |
|||
openFriend(){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/friend" |
|||
}) |
|||
}, |
|||
tabSelect(e) { |
|||
this.TabCur = e.currentTarget.dataset.id; |
|||
this.scrollLeft = (e.currentTarget.dataset.id - 1) * 60 |
|||
}, |
|||
//获取文字信息 |
|||
getCur(e) { |
|||
this.hidden = false; |
|||
this.listCur = this.contacts[e.target.id].name; |
|||
}, |
|||
setCur(e) { |
|||
this.hidden = true; |
|||
this.listCur = this.listCur |
|||
}, |
|||
//滑动选择Item |
|||
tMove(e) { |
|||
|
|||
let y = e.touches[0].clientY, |
|||
offsettop = this.boxTop, |
|||
that = this; |
|||
//判断选择区域,只有在选择区才会生效 |
|||
if (y > offsettop) { |
|||
let num = parseInt((y - offsettop) / 20); |
|||
this.listCur = that.contacts[num].name |
|||
}; |
|||
|
|||
}, |
|||
//触发全部开始选择 |
|||
tStart() { |
|||
this.hidden = false |
|||
}, |
|||
//触发结束选择 |
|||
tEnd() { |
|||
this.hidden = true; |
|||
this.listCurID = this.listCur |
|||
}, |
|||
indexSelect(e) { |
|||
let that = this; |
|||
let barHeight = this.barHeight; |
|||
let list = this.contacts; |
|||
let scrollY = Math.ceil(list.length * e.detail.y / barHeight); |
|||
for (let i = 0; i < list.length; i++) { |
|||
if (scrollY < i + 1) { |
|||
that.listCur = list[i].name; |
|||
that.movableY = i * 20 |
|||
return false |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
page { |
|||
padding-top: 100upx; |
|||
} |
|||
.indexes { |
|||
position: relative; |
|||
margin-bottom:20rpx |
|||
} |
|||
.indexBar { |
|||
position: fixed; |
|||
right: 0px; |
|||
bottom: 0px; |
|||
padding: 20upx 20upx 20upx 60upx; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.indexBar .indexBar-box { |
|||
width: 40upx; |
|||
height: auto; |
|||
background: #fff; |
|||
display: flex; |
|||
flex-direction: column; |
|||
box-shadow: 0 0 20upx rgba(0, 0, 0, 0.1); |
|||
border-radius: 10upx; |
|||
} |
|||
.indexBar-item { |
|||
flex: 1; |
|||
width: 40upx; |
|||
height: 40upx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 24upx; |
|||
color: #888; |
|||
} |
|||
movable-view.indexBar-item { |
|||
width: 40upx; |
|||
height: 40upx; |
|||
z-index: 9; |
|||
position: relative; |
|||
} |
|||
movable-view.indexBar-item::before { |
|||
content: ""; |
|||
display: block; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 10upx; |
|||
height: 20upx; |
|||
width: 4upx; |
|||
background-color: #f37b1d; |
|||
} |
|||
.indexToast { |
|||
position: fixed; |
|||
top: 0; |
|||
right: 80upx; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
width: 100upx; |
|||
height: 100upx; |
|||
border-radius: 10upx; |
|||
margin: auto; |
|||
color: #fff; |
|||
line-height: 100upx; |
|||
text-align: center; |
|||
font-size: 48upx; |
|||
} |
|||
|
|||
.gui-list-image { |
|||
width: 70rpx !important; |
|||
height: 70rpx !important; |
|||
border-radius: 8rpx; |
|||
object-fit: cover |
|||
} |
|||
|
|||
.im-department-list { |
|||
padding: 0 20rpx 40rpx |
|||
} |
|||
|
|||
.im-folder-bar { |
|||
background-color: #aaccff52; |
|||
border-radius: 8rpx; |
|||
width: 70rpx; |
|||
height: 70rpx; |
|||
text-align: center; |
|||
line-height: 70rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.im-image { |
|||
|
|||
} |
|||
|
|||
.im-folder-bar .iconfont { |
|||
font-size: 44rpx !important; |
|||
} |
|||
|
|||
.group-bg{ |
|||
background-image: url(@/static/image/group.png); |
|||
} |
|||
.invite-bg{ |
|||
background-image: url(@/static/image/invite.png); |
|||
} |
|||
.blacklist{ |
|||
background-image: url(@/static/image/blacklist-red.png); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,88 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>{{title}}</template> |
|||
</cu-custom> |
|||
<view class="cu-bar bg-white search fixed" :style="[{top:CustomBar + 'px'}]"> |
|||
<view class="search-form round"> |
|||
<text class="cuIcon-search"></text> |
|||
<input type="text" v-model="keywords" placeholder="请输入用户账号进行搜索" confirm-type="search"/> |
|||
|
|||
</view> |
|||
<view class="action"> |
|||
<button class="cu-btn round bg-green" @tap="search()">搜索</button> |
|||
</view> |
|||
</view> |
|||
|
|||
<view style="margin-top:120rpx"> |
|||
<view> |
|||
<view class="cu-list menu no-padding"> |
|||
<view class="cu-item" v-for="(items,sub) in contacts" :key="sub" @tap='openDetails(items)'> |
|||
<view class='cu-avatar radius mr-15' :style="[{backgroundImage:'url('+items.avatar+')'}]"> |
|||
</view> |
|||
<view class="content"> |
|||
<view class="c-333">{{items.realname}}</view> |
|||
</view> |
|||
<view class="action ml-10"> |
|||
<view class="text-blue" v-if="items.friend" @tap.stop="sendMsg(items.user_id)">发消息</view> |
|||
<view class="text-blue" v-if="!items.friend">查看</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="!contacts.length"> |
|||
<Empty :noDatatext="noText" textcolor="#999" ></Empty> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* 初始的引导页 |
|||
*/ |
|||
export default { |
|||
name : "search", |
|||
data() { |
|||
return { |
|||
title:'搜索朋友', |
|||
keywords:'', |
|||
contacts:[], |
|||
type:1, |
|||
noText:'暂无数据' |
|||
}; |
|||
}, |
|||
methods: { |
|||
search(){ |
|||
if(this.keywords==''){ |
|||
return uni.showToast({ |
|||
title:"请输入用户账号进行搜索", |
|||
icon:'none' |
|||
}) |
|||
} |
|||
this.noText="未搜索到数据"; |
|||
this.$api.msgApi.searchUser({keywords:this.keywords}).then((res)=>{ |
|||
if(res.code==0){ |
|||
this.contacts=res.data; |
|||
} |
|||
}) |
|||
}, |
|||
// 打开用户详情 |
|||
openDetails(items){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+items.user_id |
|||
}) |
|||
}, |
|||
sendMsg(id){ |
|||
uni.navigateTo({ |
|||
url:"/pages/message/chat?id="+id |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
</style> |
|||
@ -0,0 +1,435 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom :bgColor="PageCur=='mine'?'bg-mine':'bg-white'"> |
|||
<template #backText> |
|||
<view v-if="PageCur=='message' || PageCur=='contacts'" class="f-20 ml-10 mr-10" @tap="search()"> |
|||
<text class="cuIcon-search" style="margin-left: -10px;"></text> |
|||
</view> |
|||
</template> |
|||
<template #content>{{PageName}}</template> |
|||
<template #right> |
|||
<view v-if="PageCur=='message'" class="f-20 ml-10 mr-10" @tap="modelName='add'"> |
|||
<text class="cuIcon-add f-28" id="add"></text> |
|||
</view> |
|||
|
|||
</template> |
|||
</cu-custom> |
|||
<view> |
|||
<message v-show="PageCur=='message'"></message> |
|||
<contacts v-show="PageCur=='contacts'" :TabCur="TabCur"></contacts> |
|||
<compass v-show="PageCur=='compass'"></compass> |
|||
<mine v-show="PageCur=='mine'"></mine> |
|||
</view> |
|||
<view class="cu-bar tabbar bg-white shadow foot"> |
|||
<view class="action" @click="NavChange(item)" v-for="(item,index) in navList" :key="index" data-cur="message"> |
|||
<view class='cuIcon-cu-image' style="position: relative;"> |
|||
<image :src="'/static/image/tabbar/' + [item.name] + [PageCur==item.name?'-active':''] + '.svg'"></image> |
|||
<view class="cu-tag badge" v-if="item.notice>0">{{item.notice}}</view> |
|||
<view style="position: absolute;top: 0px;right: 2px;" v-if="item.bol>0"> |
|||
<view class="notice-back"></view> |
|||
</view> |
|||
</view> |
|||
<view :class="PageCur==item.name?'text-green':'text-black'">{{item.title}}</view> |
|||
</view> |
|||
</view> |
|||
<view class="add-modal" :class="modelName=='add' ? 'show' : 'none'" @tap="modelName=''" > |
|||
<view class="add-dialog" :style="{top:top+20+'px'}"> |
|||
<view class="add-item" @tap="initContacts();"> |
|||
<view class="content padding-tb-sm"> |
|||
<text class="cuIcon-refresh"></text> |
|||
<text class="text">更新消息</text> |
|||
</view> |
|||
</view> |
|||
<view class="add-item" @tap="addFriend()" v-if='globalConfig.sysInfo.runMode==2'> |
|||
<view class="content padding-tb-sm"> |
|||
<text class="cuIcon-friendadd"></text> |
|||
<text class="text">添加朋友</text> |
|||
</view> |
|||
</view> |
|||
<view class="add-item" @tap="addGroup()"> |
|||
<view class="content padding-tb-sm"> |
|||
<text class=" cuIcon-friend"></text> |
|||
<text class="text">创建群聊</text> |
|||
</view> |
|||
</view> |
|||
<view class="add-item" @tap="scan()"> |
|||
<view class="content padding-tb-sm" style="text-align: left;"> |
|||
<text class=" cuIcon-scan mr-5"></text> |
|||
<text class="text">扫 一 扫</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import message from '@/pages/message'; |
|||
import contacts from '@/pages/contacts'; |
|||
import compass from '@/pages/compass'; |
|||
import mine from '@/pages/mine'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
import { useloginStore } from '@/store/login'; |
|||
import pinia from '@/store/index' |
|||
import scan from '@/common/scan.js' |
|||
import homeData from '../../service/homeData'; |
|||
|
|||
const msgStore = useMsgStore(pinia) |
|||
const loginStore = useloginStore(pinia) |
|||
const { unread,sysUnread,NoticeCount } = storeToRefs(msgStore); |
|||
export default { |
|||
components: { |
|||
message, |
|||
contacts, |
|||
compass, |
|||
mine |
|||
}, |
|||
data() { |
|||
let navList=[ |
|||
{ |
|||
name:'message', |
|||
title:'消息', |
|||
notice:unread |
|||
}, |
|||
{ |
|||
name:'contacts', |
|||
title:'通讯录', |
|||
notice:sysUnread |
|||
} |
|||
] |
|||
let compass={ |
|||
name:'compass', |
|||
title:'探索', |
|||
notice:0, |
|||
bol:NoticeCount |
|||
}; |
|||
if(loginStore.globalConfig && loginStore.globalConfig.compass){ |
|||
if(loginStore.globalConfig.compass.status==1){ |
|||
navList.push(compass); |
|||
} |
|||
} |
|||
let mine={ |
|||
name:'mine', |
|||
title:'我的', |
|||
notice:0 |
|||
} |
|||
navList.push(mine); |
|||
return { |
|||
globalConfig:loginStore.globalConfig, |
|||
PageCur: 'message', |
|||
PageName: '消息', |
|||
TabCur:0, |
|||
modelName:false, |
|||
navList:navList, |
|||
top:10, |
|||
indexs:0, |
|||
IntervalChat:null |
|||
} |
|||
}, |
|||
onBackPress(options) { |
|||
if (getCurrentPages().length > 1) { |
|||
return false; |
|||
} |
|||
try { |
|||
const main = plus.android.runtimeMainActivity(); |
|||
main.moveTaskToBack(false); // 将任务移动到后台 |
|||
return true; // 拦截返回按键,防止退出应用 |
|||
} catch (e) { |
|||
return false; // 出现错误时允许默认返回行为 |
|||
} |
|||
}, |
|||
mounted(){ |
|||
// #ifndef MP |
|||
uni.hideTabBar(); |
|||
// #endif |
|||
|
|||
// 监听ws状态,如果重新连接了,要更新联系人 |
|||
uni.$on('socketStatus',(e)=>{ |
|||
if(e){ |
|||
console.log('触发了一次'); |
|||
this.initContacts(); |
|||
} |
|||
}) |
|||
uni.$off('initContacts'); |
|||
uni.$on('initContacts',(e)=>{ |
|||
this.initContacts(); |
|||
}) |
|||
let query = this.$util.getQuery(this).select("#add").boundingClientRect(); |
|||
query.exec((res)=> { |
|||
let top = res[0].top; |
|||
let height = res[0].height; |
|||
this.top = top+height |
|||
}); |
|||
}, |
|||
onShow(){ |
|||
const _this = this |
|||
const tabid = uni.getStorageSync('tabid') |
|||
if(tabid){ |
|||
uni.removeStorageSync('tabid') |
|||
} |
|||
uni.getNetworkType({ |
|||
success (res) { |
|||
if(res.networkType == 'none'){ |
|||
_this.getGroupData() |
|||
}else{ |
|||
_this.initContacts(0); |
|||
// _this.startPolling() |
|||
// _this.getGroupData() |
|||
} |
|||
// console.log(res.networkType,'1111'); |
|||
} |
|||
}); |
|||
}, |
|||
onHide() { |
|||
// 组件销毁时清除定时器 |
|||
this.stopPolling() |
|||
}, |
|||
methods: { |
|||
startPolling(){ |
|||
this.IntervalChat = setInterval(()=>{ |
|||
this.initContacts(); |
|||
},5000) |
|||
}, |
|||
stopPolling(){ |
|||
clearInterval(this.IntervalChat) |
|||
this.IntervalChat = null |
|||
}, |
|||
closeModel(){ |
|||
this.modelName=false; |
|||
}, |
|||
scan(){ |
|||
scan.scanQr(); |
|||
}, |
|||
NavChange: function(item) { |
|||
this.PageCur = item.name |
|||
this.PageName = item.title |
|||
}, |
|||
showContacts(){ |
|||
this.TabCur==1 ? this.TabCur=0 :this.TabCur=1 |
|||
}, |
|||
initContacts(num){ |
|||
if(num){ |
|||
this.indexs = num |
|||
} |
|||
this.modelName=''; |
|||
this.$api.msgApi.initContacts().then(res => { |
|||
this.indexs++ |
|||
// 设置消息未读数和系统消息未读数 |
|||
msgStore.sysUnread=res.count; |
|||
msgStore.initContacts(res.data); |
|||
// if(this.indexs === 1&&num===0){ |
|||
// 新增持久化存储检查 |
|||
res.data.forEach((val)=>{ |
|||
if(val.id==-2){ |
|||
val.lastContent = val.lastContent.replace(/'/g, "''"); |
|||
} |
|||
}) |
|||
this.syncGroupData(res.data) |
|||
const userInfo = uni.getStorageSync('userInfo') |
|||
const list = [...res.data] |
|||
list.push({avatar:userInfo.avatar}) |
|||
list.forEach(res => { |
|||
// #ifdef APP-PLUS |
|||
uni.downloadFile({ url: res.avatar,success: (downloadResult) => { |
|||
this.saveToPermanentStorage(downloadResult.tempFilePath); |
|||
}}) |
|||
// #endif |
|||
}) |
|||
// } |
|||
// this.getGroupData() |
|||
}) |
|||
}, |
|||
|
|||
// App端持久化存储实现 |
|||
saveToPermanentStorage(tempPath) { |
|||
return new Promise((resolve, reject) => { |
|||
// 获取应用文档目录(持久化存储) |
|||
plus.io.resolveLocalFileSystemURL( |
|||
'_doc', |
|||
(docDir) => { |
|||
// 创建目标路径 |
|||
docDir.getDirectory( |
|||
'img', |
|||
{ create: true, exclusive: false }, |
|||
(entry) => { |
|||
// 从临时路径获取文件名 |
|||
const fileName = this.getFileName(tempPath); |
|||
const fileName1 = this.getFileName(docDir.fullPath + 'img/' +fileName); |
|||
// 新增:检查文件是否存在 |
|||
entry.getFile(fileName1,{ create: false }, // 不创建新文件 |
|||
(fileEntry) => { |
|||
// console.log('文件已存在,拒绝操作'); |
|||
// 文件已存在,拒绝操作 |
|||
reject(new Error('File already exists: ' + fileName)); |
|||
}, |
|||
(error) => { |
|||
// console.log(error); |
|||
// 文件不存在(或发生其他错误),继续复制操作 |
|||
if (error.code === 14) { // 1表示文件不存在(不同平台错误码可能不同) |
|||
this.copyFile(tempPath, entry, fileName, resolve, reject); |
|||
} else { |
|||
reject(error); |
|||
} |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}); |
|||
}, |
|||
|
|||
// 提取复制逻辑为独立方法 |
|||
copyFile(tempPath, targetDir, fileName, resolve, reject) { |
|||
plus.io.resolveLocalFileSystemURL( |
|||
tempPath, |
|||
(tempEntry) => { |
|||
tempEntry.copyTo( |
|||
targetDir, |
|||
fileName, |
|||
(newEntry) => { |
|||
resolve(newEntry.toLocalURL()); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
|
|||
// 获取文件名工具方法 |
|||
getFileName(path) { |
|||
const index = path.lastIndexOf('/'); |
|||
let fileName = path.substr(index + 1); |
|||
fileName = fileName.replace(/\(\d+\)(?=\.[^./]+$)/, ''); |
|||
return fileName; |
|||
}, |
|||
|
|||
async syncGroupData(data) { |
|||
// #ifdef APP-PLUS |
|||
// 1. 获取接口数据(示例) |
|||
let apiResponse = data; |
|||
// 3. 执行批量插入 |
|||
await homeData.deleteallList() |
|||
await homeData.batchInsertOrUpdate(apiResponse); |
|||
// #endif |
|||
}, |
|||
async getGroupData(){ |
|||
// #ifdef APP-PLUS |
|||
const groups = await homeData.getList(); |
|||
msgStore.initContacts(groups) |
|||
// console.log('处理后的数据:', groups) |
|||
// #endif |
|||
}, |
|||
addGroup(){ |
|||
uni.navigateTo({ |
|||
url: '/pages/index/userSelection?type=1' |
|||
}) |
|||
}, |
|||
addFriend(){ |
|||
uni.navigateTo({ |
|||
url: '/pages/contacts/search' |
|||
}) |
|||
}, |
|||
search(){ |
|||
const type = this.PageCur=="message" ? 1 : 2; |
|||
uni.navigateTo({ |
|||
url: '/pages/index/search?type='+type |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.add-modal{ |
|||
position: fixed; |
|||
top: 0; |
|||
right: 0; |
|||
z-index: -99999; |
|||
|
|||
.add-dialog{ |
|||
display: flex; |
|||
flex-direction: column; |
|||
background-color: #4f4f4f; |
|||
color: #fff; |
|||
border-radius: 10rpx; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
position: fixed; |
|||
right: 10rpx; |
|||
width: 240rpx; |
|||
padding:20rpx; |
|||
.add-item{ |
|||
.text{ |
|||
margin-left: 10rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.add-dialog::after { |
|||
content: ""; |
|||
top: -10rpx; |
|||
transform: rotate(45deg); |
|||
position: absolute; |
|||
display: inline-block; |
|||
overflow: hidden; |
|||
width: 30rpx; |
|||
height: 30rpx; |
|||
right: 20rpx; |
|||
left: initial; |
|||
background: #4f4f4f; |
|||
} |
|||
} |
|||
.show{ |
|||
position: fixed; |
|||
top: 0; |
|||
z-index: 9999; |
|||
height: 100vh; |
|||
width: 100vw; |
|||
opacity:1 |
|||
} |
|||
.none{ |
|||
position: fixed; |
|||
top: 0; |
|||
right: 0; |
|||
z-index: -99999; |
|||
opacity: 0; |
|||
} |
|||
|
|||
.notice-back{ |
|||
width: 6px; |
|||
height: 6px; |
|||
position: relative; |
|||
border-radius: 50%; |
|||
background-color: red; |
|||
vertical-align: middle; |
|||
} |
|||
.notice-back::after{ |
|||
content: ""; |
|||
width: 100%; |
|||
height: 100%; |
|||
top: 0px; |
|||
right: 0px; |
|||
position: absolute; |
|||
border-radius: 50%; |
|||
background: inherit; |
|||
animation: notiback 1.2s ease-in-out infinite; |
|||
} |
|||
@keyframes notiback{ |
|||
0% {transform: scale(0.5);opacity: 1;} |
|||
30%{opacity: 0.7;} |
|||
100%{ transform: scale(2.5);opacity: 0;} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,225 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>{{title}}</template> |
|||
</cu-custom> |
|||
<view class="mt-20 im-flex im-align-items-center"> |
|||
<image :src="canvasUrl" mode="widthFix" style="margin:0 auto"></image> |
|||
</view> |
|||
<view class="padding flex flex-direction mt-10" v-if="canvasUrl" style="position: relative;"> |
|||
<button class="cu-btn bg-green lg" @tap="saveHeadImgFile"> |
|||
<!-- #ifdef H5 --> |
|||
下载图片 |
|||
<!-- #endif --> |
|||
<!-- #ifndef H5 --> |
|||
保存到相册 |
|||
<!-- #endif --> |
|||
</button> |
|||
<!-- #ifdef H5 --> |
|||
<image :src="contact.avatar" mode="widthFix" style="width: 120rpx;position: absolute;top: -850rpx;left: 320rpx;" /> |
|||
<!-- #endif --> |
|||
</view> |
|||
<mosowe-canvas-image ref="mosoweCanvasComponents" :lists="lists" :height="height" :width="width" @canvasImage="canvasImage" ></mosowe-canvas-image> |
|||
</view> |
|||
</template> |
|||
<script> |
|||
import mosoweCanvasImage from '@/components/mosowe-canvas-image/mosowe-canvas-image.vue'; |
|||
import pinia from '@/store/index' |
|||
import { useloginStore } from '@/store/login'; |
|||
const userStore = useloginStore(pinia); |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
canvasUrl: '', |
|||
group_id:0, |
|||
lists:[], |
|||
width:500, |
|||
height:700, |
|||
title:'我的二维码', |
|||
contact:{} |
|||
} |
|||
}, |
|||
components:{ |
|||
mosoweCanvasImage |
|||
}, |
|||
onLoad(options) { |
|||
this.group_id = options.group_id?options.group_id:'' |
|||
// 如果有团队ID就生成群聊二维码,没有就生成个人二维码 |
|||
if(this.group_id){ |
|||
this.title="群二维码"; |
|||
this.getGroupInfo() |
|||
}else{ |
|||
this.createUserQr(); |
|||
} |
|||
|
|||
}, |
|||
methods: { |
|||
createUserQr(){ |
|||
let userInfo=userStore.userInfo; |
|||
let qrUrl=userInfo.qrUrl ?? ''; |
|||
if(!qrUrl){ |
|||
uni.showToast({ |
|||
title:'请重新登陆后再生成二维码', |
|||
icon:'none' |
|||
}) |
|||
uni.navigateBack(); |
|||
return; |
|||
} |
|||
this.lists=[ |
|||
{ |
|||
type: 'rect', |
|||
color: '#ffffff', |
|||
width: this.width, |
|||
height: this.height, |
|||
x: 0, |
|||
y: 0 |
|||
}, |
|||
{ |
|||
type: 'image', |
|||
content: userInfo.avatar, |
|||
width: 100, |
|||
height: 100, |
|||
x: 70, |
|||
y: 60 |
|||
}, |
|||
{ |
|||
type: 'text', |
|||
content: userInfo.displayName, |
|||
x: 190, |
|||
y: 96, |
|||
maxWidth:400, |
|||
color: '#000', |
|||
size: 28, |
|||
}, |
|||
{ |
|||
type: 'text', |
|||
content: userInfo.account, |
|||
x: 190, |
|||
y: 135, |
|||
maxWidth:400, |
|||
color: '#999', |
|||
size: 20, |
|||
}, |
|||
{ |
|||
type: 'qr', |
|||
content: qrUrl, |
|||
width: 360, |
|||
height: 360, |
|||
x: 70, |
|||
y: 200, |
|||
}, |
|||
{ |
|||
type: 'text', |
|||
content: '扫一扫上面的二维码图案,加我为好友', |
|||
x: 105, |
|||
y: 620, |
|||
color: '#999', |
|||
maxWidth:400, |
|||
size: 18, |
|||
}, |
|||
]; |
|||
setTimeout(()=>{ |
|||
this.$refs.mosoweCanvasComponents.createCanvas(); |
|||
},100) |
|||
}, |
|||
getGroupInfo() { |
|||
this.userList = [] |
|||
this.$api.msgApi.groupInfo({ |
|||
group_id: this.group_id |
|||
}).then((res) => { |
|||
let data=res.data; |
|||
this.contact=data; |
|||
let groupName=data.name; |
|||
let length=groupName.length; |
|||
console.log(this.contact); |
|||
if(length>12){ |
|||
groupName=groupName.substring(0,12)+"..."; |
|||
} |
|||
this.lists=[ |
|||
{ |
|||
type: 'rect', |
|||
color: '#ffffff', |
|||
width: this.width, |
|||
height: this.height, |
|||
x: 0, |
|||
y: 0 |
|||
}, |
|||
{ |
|||
type: 'image', |
|||
content: data.avatar, |
|||
// content: "http://192.168.66.16:8007/avatar/吕布/120/20", |
|||
width: 100, |
|||
height: 100, |
|||
x: 200, |
|||
y: 40 |
|||
}, |
|||
{ |
|||
type: 'text', |
|||
content: '群聊:'+groupName, |
|||
x: this.width/2, |
|||
align:'center', |
|||
y: 180, |
|||
maxWidth:400, |
|||
color: '#000', |
|||
size: 24, |
|||
}, |
|||
{ |
|||
type: 'qr', |
|||
content: data.qrUrl, |
|||
width: 360, |
|||
height: 360, |
|||
x: 70, |
|||
y: 220, |
|||
}, |
|||
{ |
|||
type: 'text', |
|||
content: '该二维码7天内('+data.qrExpire+'前)有效', |
|||
x: 105, |
|||
y: 640, |
|||
color: '#999', |
|||
maxWidth:400, |
|||
size: 18, |
|||
}, |
|||
]; |
|||
setTimeout(()=>{ |
|||
this.$refs.mosoweCanvasComponents.createCanvas(); |
|||
},100) |
|||
}) |
|||
}, |
|||
canvasImage (e) { |
|||
this.canvasUrl = e; |
|||
}, |
|||
// 将base64 图片保存到本地系统相册 |
|||
saveHeadImgFile() { |
|||
// #ifdef H5 |
|||
const tempLink = document.createElement("a"); |
|||
tempLink.style.display = "none"; |
|||
tempLink.href = this.canvasUrl; |
|||
tempLink.setAttribute("download", this.contact.name+".jpg"); |
|||
tempLink.setAttribute("target", "_blank"); |
|||
document.body.appendChild(tempLink); |
|||
tempLink.click(); |
|||
document.body.removeChild(tempLink); |
|||
// #endif |
|||
// #ifndef H5 |
|||
uni.saveImageToPhotosAlbum({ |
|||
filePath: this.canvasUrl, |
|||
success: () => { |
|||
uni.showToast({title:'图片保存成功',icon:'none'}) |
|||
} |
|||
}) |
|||
// #endif |
|||
|
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
<style scoped> |
|||
.list-image { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
font-size: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,34 @@ |
|||
<template> |
|||
<!-- HTML --> |
|||
<get-qrcode @success='qrcodeSucess' @error="qrcodeError" ></get-qrcode> |
|||
</template> |
|||
|
|||
<script> |
|||
import scan from '@/common/scan.js' |
|||
import getQrcode from '@/components/get-qrcode.vue' |
|||
// 嫌路径长的话可以单独复制出来 |
|||
export default { |
|||
components: { |
|||
getQrcode |
|||
}, |
|||
methods: { |
|||
qrcodeSucess(data) { |
|||
scan.checkQr(data); |
|||
uni.navigateBack({}) |
|||
}, |
|||
qrcodeError(err) { |
|||
console.log(err) |
|||
uni.showModal({ |
|||
title: '摄像头授权失败', |
|||
content: '摄像头授权失败,请检测当前浏览器是否有摄像头权限。', |
|||
success: () => { |
|||
uni.navigateBack() |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
|||
@ -0,0 +1,112 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>{{title}}</template> |
|||
</cu-custom> |
|||
<view class="cu-bar bg-white search fixed" :style="[{top:CustomBar + 'px'}]"> |
|||
<view class="search-form round"> |
|||
<text class="cuIcon-search"></text> |
|||
<input type="text" v-model="keywords" placeholder="搜索联系人" confirm-type="search"/> |
|||
</view> |
|||
</view> |
|||
|
|||
<view style="margin-top:120rpx"> |
|||
<view v-if="type<3 && contacts.length>0"> |
|||
<view class="padding">联系人</view> |
|||
<view class="cu-list menu-avatar no-padding"> |
|||
<view class="cu-item" v-for="(items,sub) in contacts" :key="sub" @tap='openDetails(items)'> |
|||
<view class='cu-avatar lg radius mr-15' :style="[{backgroundImage:'url('+items.avatar+')'}]"> |
|||
</view> |
|||
<view class="content"> |
|||
<view class="c-333">{{items.displayName}}</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="groupList.length>0"> |
|||
<view class="padding">群聊</view> |
|||
<view class="cu-list menu-avatar no-padding"> |
|||
<view class="cu-item" v-for="(items,sub) in groupList" :key="sub" @tap='openDetails(items)'> |
|||
<view class='cu-avatar lg radius mr-15' :style="[{backgroundImage:'url('+items.avatar+')'}]"> |
|||
</view> |
|||
<view class="content"> |
|||
<view class="c-333">{{items.displayName}}</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="!groupList.length && !contacts.length"> |
|||
<Empty noDatatext="暂无数据" textcolor="#999" ></Empty> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* 初始的引导页 |
|||
*/ |
|||
export default { |
|||
name : "search", |
|||
data() { |
|||
return { |
|||
title:'搜索聊天', |
|||
keywords:'', |
|||
allContacts:[], |
|||
contacts:[], |
|||
groupList:[], |
|||
type:1 |
|||
}; |
|||
}, |
|||
watch:{ |
|||
keywords(val){ |
|||
this.search(); |
|||
} |
|||
}, |
|||
onLoad(options){ |
|||
this.type=options.type; |
|||
if(this.type==2){ |
|||
this.title="搜索联系人"; |
|||
}else if(this.type==3){ |
|||
this.title="搜索群聊"; |
|||
}else{ |
|||
this.title="搜索聊天"; |
|||
} |
|||
}, |
|||
mounted(){ |
|||
this.allContacts=uni.getStorageSync('allContacts'); |
|||
}, |
|||
methods: { |
|||
search(){ |
|||
const arr=this.$util.searchObject(this.allContacts,['displayName','name_py','account'],this.keywords); |
|||
const contacts=[]; |
|||
const groupList=[]; |
|||
arr.forEach((item)=>{ |
|||
if(item.is_group==1){ |
|||
groupList.push(item); |
|||
}else{ |
|||
contacts.push(item); |
|||
} |
|||
}) |
|||
this.groupList=groupList; |
|||
this.contacts=contacts; |
|||
}, |
|||
// 打开聊天 |
|||
openDetails(items){ |
|||
if(this.type==2 && items.is_gourp==0){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+items.id |
|||
}) |
|||
} |
|||
uni.navigateTo({ |
|||
url:"/pages/message/chat?id="+items.id |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
</style> |
|||
@ -0,0 +1,354 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>{{title}}</template> |
|||
<template #right> |
|||
<view class="mr-10 f-16" @tap="save">{{type==3 ? '转发' : '完成'}}</view> |
|||
</template> |
|||
</cu-custom> |
|||
<user-select :type="type" :contact_id="contact_id" :user_ids="user_ids" ref="userSelect"></user-select> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import userSelect from '@/components/message/user-select.vue'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import pinia from '@/store/index' |
|||
const msgStore = useMsgStore(pinia) |
|||
const {appendContacts} = storeToRefs(msgStore); |
|||
export default { |
|||
components: { |
|||
userSelect |
|||
}, |
|||
data() { |
|||
return { |
|||
title:'发起群聊', |
|||
contact_id:'', |
|||
selectUser:[], |
|||
userList: [], |
|||
changeUser: [], //选中的数据 |
|||
user_ids: [], // |
|||
type: 1, |
|||
relayState: false, |
|||
scrollLeft:300, |
|||
content:'', |
|||
curMsg:{} |
|||
} |
|||
}, |
|||
watch: { |
|||
relayState(val) { |
|||
if (val == true) { |
|||
uni.showToast({ |
|||
icon: 'success', |
|||
title: '转发成功' |
|||
}) |
|||
setTimeout(() => { |
|||
uni.navigateBack() |
|||
}, 2000) |
|||
} |
|||
|
|||
} |
|||
}, |
|||
onLoad(options) { |
|||
var pages = getCurrentPages(); //当前页 |
|||
var beforePage = pages[pages.length - 2]; //上个页面 |
|||
this.type = options.type ? options.type : 1; |
|||
this.contact_id = options.contact_id ? options.contact_id : ''; |
|||
this.content = options.content ? options.content : ''; |
|||
if (options.type == 2) { |
|||
this.title="添加成员"; |
|||
// 调用上一页的方法刷新 |
|||
// #ifdef H5 |
|||
this.user_ids = beforePage.user_ids |
|||
// #endif |
|||
// #ifndef H5 |
|||
this.user_ids = beforePage.$vm.user_ids |
|||
// #endif |
|||
console.log(this.user_ids); |
|||
}else if (options.type == 3) { |
|||
this.title="转发聊天"; |
|||
// 调用上一页的方法刷新 |
|||
// #ifdef H5 |
|||
this.curMsg = beforePage.curMsg |
|||
// #endif |
|||
// #ifndef H5 |
|||
this.curMsg = beforePage.$vm.curMsg |
|||
// #endif |
|||
} else if (options.type == 4) { |
|||
this.title="选择提醒的人"; |
|||
} else if(options.type == 5){ |
|||
this.user_ids = JSON.parse(options.user_ids) |
|||
// console.log(this.user_ids); |
|||
}else if(options.type == 6){ |
|||
if(options.user_ids){ |
|||
this.user_ids = JSON.parse(options.user_ids) |
|||
} |
|||
// console.log(this.user_ids); |
|||
}else{ |
|||
this.title="发起群聊"; |
|||
} |
|||
|
|||
|
|||
}, |
|||
methods: { |
|||
// 转发 |
|||
relay() { |
|||
let user_ids = this.changeUser.map(it => { |
|||
return it.id |
|||
}) |
|||
if (!user_ids.length) { |
|||
uni.showToast({ |
|||
title: "请选择至少一名人员", |
|||
icon: "none" |
|||
}) |
|||
} else if (user_ids.length > 5) { |
|||
uni.showToast({ |
|||
title: "转发的人数不能超过5人!", |
|||
icon: "none" |
|||
}) |
|||
} else { |
|||
var pages = getCurrentPages(); //当前页 |
|||
var beforePage = pages[pages.length - 2]; //上个页面 |
|||
let toContactId = '' |
|||
let fromUser = '' |
|||
// 调用上一页的方法刷新 |
|||
// #ifdef H5 |
|||
fromUser = beforePage.fromUser |
|||
toContactId = beforePage.contact.id |
|||
// #endif |
|||
// #ifndef H5 |
|||
fromUser = beforePage.$vm.fromUser |
|||
// #endif |
|||
let selectedItem = '' |
|||
if (this.type == 'relayCrm' || this.type == 'relayProject') { |
|||
// #ifdef H5 |
|||
selectedItem = beforePage.message |
|||
// #endif |
|||
// #ifndef H5 |
|||
selectedItem = beforePage.$vm.message |
|||
// #endif |
|||
} else { |
|||
selectedItem = uni.getStorageSync('selectedItem') |
|||
} |
|||
user_ids.forEach(it => { |
|||
let msg = { |
|||
id: this.$util.getUuid(), |
|||
is_group: 0, |
|||
fromUser, |
|||
extends: selectedItem.extends ? selectedItem.extends : '', |
|||
type: selectedItem.type, |
|||
toContactId: it, |
|||
content: selectedItem.content, |
|||
sendTime: new Date().getTime() |
|||
} |
|||
|
|||
this.$api.msgApi.sendMessage(msg) |
|||
.then((res) => { |
|||
if (res.code !== 200) return |
|||
this.relayState = true |
|||
}) |
|||
}) |
|||
} |
|||
}, |
|||
// 添加群成员 |
|||
addGroupUser(user_ids) { |
|||
var pages = getCurrentPages(); //当前页 |
|||
var beforePage = pages[pages.length - 2]; //上个页面 |
|||
// 调用上一页的方法刷新 |
|||
// #ifdef H5 |
|||
beforePage.getsimpleMessage = false |
|||
let group_id = beforePage.group_id |
|||
// #endif |
|||
// #ifndef H5 |
|||
beforePage.$vm.getsimpleMessage = false |
|||
let group_id = beforePage.$vm.group_id |
|||
// #endif |
|||
this.$api.msgApi.addGroupUser({ |
|||
user_ids, |
|||
id: group_id |
|||
}).then(res => { |
|||
if (res.code == 200) { |
|||
uni.navigateBack() |
|||
} |
|||
}) |
|||
}, |
|||
// 添加群聊 |
|||
addGroup(user_ids) { |
|||
this.$api.msgApi.addGroup({ |
|||
user_ids |
|||
}).then(res => { |
|||
if (res.code == 200) { |
|||
setTimeout(() => { |
|||
uni.navigateBack() |
|||
}, 2000) |
|||
|
|||
|
|||
|
|||
} |
|||
}) |
|||
}, |
|||
save(){ |
|||
uni.showLoading({ |
|||
title: '保存中...', |
|||
mask:true |
|||
}); |
|||
this.changeUser=this.$refs.userSelect.changeUser; |
|||
this.selectUser=this.$refs.userSelect.selectUser; |
|||
// console.log(this.changeUser); |
|||
if(!this.changeUser.length){ |
|||
return uni.showToast({ |
|||
title:'请选择人员', |
|||
icon:'none' |
|||
}) |
|||
} |
|||
try{ |
|||
if(this.type==1){ |
|||
this.$api.msgApi.addGroup({user_ids:this.changeUser}).then(res =>{ |
|||
uni.hideLoading(); |
|||
const data = res.data; |
|||
msgStore.appendContacts(data); |
|||
uni.navigateTo({ |
|||
url:'/pages/message/chat?id='+data.id |
|||
}) |
|||
}) |
|||
}else if(this.type==2){ |
|||
this.$api.msgApi.addGroupUser({user_ids:this.changeUser,id:this.contact_id}).then(res =>{ |
|||
this.closePage(); |
|||
}) |
|||
}else if(this.type==3){ |
|||
if(this.changeUser.length>5){ |
|||
return uni.showToast({ |
|||
title:'单次转发不能超过5人', |
|||
icon:'none' |
|||
}) |
|||
} |
|||
this.$api.msgApi.forwardMessage({user_ids:this.changeUser,msg_id:this.curMsg.msg_id,content:this.content}).then(res =>{ |
|||
this.closePage(); |
|||
}) |
|||
}else if(this.type==4){ |
|||
const eventChannel = this.getOpenerEventChannel(); |
|||
eventChannel.emit('getAtList',this.selectUser); |
|||
this.closePage(); |
|||
}else if(this.type==5){ |
|||
uni.setStorageSync('selectUser',this.selectUser) |
|||
this.closePage(); |
|||
}else if(this.type==6){ |
|||
uni.setStorageSync('selectUser1',this.selectUser) |
|||
this.closePage(); |
|||
}else{ |
|||
this.$api.msgApi.removeUser({user_ids:this.changeUser,id:this.contact_id}).then(res =>{ |
|||
this.closePage(); |
|||
}) |
|||
} |
|||
}catch(e){ |
|||
console.info(e); |
|||
this.closePage(); |
|||
} |
|||
|
|||
}, |
|||
// 关闭加载动画返回上一个页面 |
|||
closePage(){ |
|||
uni.hideLoading(); |
|||
uni.navigateBack(); |
|||
}, |
|||
// 监听提交 |
|||
confirm: function(e) { |
|||
let arr = [] |
|||
if (e) { //这个值为输入框输入的值 |
|||
var brr = this.userList.filter(value => { |
|||
//遍历数组,返回值为true保留并复制到新数组,false则过滤掉 |
|||
let data = value.realname ? value.realname : value.userInfo.displayName |
|||
if (data.includes(e.trim())) { |
|||
arr.push(value) |
|||
} |
|||
return data.includes(e.trim()); |
|||
}); |
|||
this.lists = arr |
|||
} |
|||
|
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.group-bg{ |
|||
background-image: url(@/static/image/group.png); |
|||
} |
|||
.search-warp { |
|||
width: 750rpx; |
|||
padding: 15rpx 50rpx; |
|||
} |
|||
::v-deep .checklist-group{ |
|||
display: grid !important; |
|||
.checklist-box{ |
|||
padding:20rpx; |
|||
.checkbox__inner{ |
|||
width:40rpx !important; |
|||
height:40rpx !important; |
|||
overflow:hidden; |
|||
.checkbox__inner-icon{ |
|||
position: absolute; |
|||
top: -8px !important; |
|||
left: -4px !important; |
|||
height: 20px !important; |
|||
width: 20px !important; |
|||
border-right-width: 2px !important; |
|||
border-bottom-width: 2px !important; |
|||
} |
|||
} |
|||
.checklist-content{ |
|||
margin-left:20rpx; |
|||
.checklist-text{ |
|||
font-size:36rpx !important; |
|||
} |
|||
|
|||
} |
|||
} |
|||
.is-checked{ |
|||
.checkbox__inner{ |
|||
background-color: #18bc37 !important; |
|||
border-color: #18bc37 !important; |
|||
} |
|||
.checklist-content{ |
|||
.checklist-text{ |
|||
color: #18bc37 !important; |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
.footer-opt{ |
|||
position: fixed; |
|||
bottom:0; |
|||
left:0; |
|||
width:100%; |
|||
} |
|||
|
|||
.scroll-view_H { |
|||
white-space: nowrap; |
|||
width: 100%; |
|||
} |
|||
|
|||
.user-list-avatar{ |
|||
float: left; |
|||
margin-top:10rpx; |
|||
.user-avatar{ |
|||
width:70rpx; |
|||
height:70rpx; |
|||
flex: 0 0 auto; |
|||
border-radius: 8rpx; |
|||
margin-left: 15rpx; |
|||
display: inline-block; |
|||
&:last-child{ |
|||
margin-right: 15rpx; |
|||
} |
|||
} |
|||
.select-num{ |
|||
padding:10rpx; |
|||
} |
|||
|
|||
} |
|||
</style> |
|||
@ -0,0 +1,57 @@ |
|||
<template> |
|||
<view class="flex justify-center align-center" style="height: 100vh;background-color: #fff;"> |
|||
<view> |
|||
<image src="../../static/image/123.gif" mode="widthFix"></image> |
|||
<view style="text-align: center;"> |
|||
<view class="text1">抱歉!</view> |
|||
<view style="margin-bottom: 40rpx;">{{globalConfig.sysInfo.closeTips}}</view> |
|||
<view style="display: flex;justify-content: center;"> |
|||
<a class="bullshit-return-home" href="/">返回首页</a> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import pinia from '@/store/index' |
|||
import { useloginStore } from '@/store/login'; |
|||
const userStore = useloginStore(pinia); |
|||
export default { |
|||
data() { |
|||
return { |
|||
globalConfig:userStore.globalConfig, |
|||
} |
|||
}, |
|||
created() { |
|||
|
|||
}, |
|||
methods: { |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.text1{ |
|||
margin-bottom: 40rpx; |
|||
font-size: 64rpx; |
|||
font-weight: bold; |
|||
line-height: 80rpx; |
|||
color: #175CFF; |
|||
} |
|||
.bullshit-return-home{ |
|||
display:block; |
|||
float:left; |
|||
width:110px; |
|||
height:36px; |
|||
font-size:14px; |
|||
line-height:36px; |
|||
color:#fff; |
|||
text-align:center; |
|||
cursor:pointer; |
|||
background:#175cff; |
|||
border-radius:100px; |
|||
text-decoration: none; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,214 @@ |
|||
<template> |
|||
<view> |
|||
<view style="height:150rpx;"></view> |
|||
<view class="padding im-flex im-rows im-justify-content-center mb-10"> |
|||
<view class="im-flex im-rows im-justify-content-center"> |
|||
<image class="login-logo " :src="globalConfig.sysInfo.logo ?? packData.logo" mode="fixWidth"></image> |
|||
|
|||
</view> |
|||
</view> |
|||
<view class="im-flex im-rows im-justify-content-center">{{globalConfig.sysInfo.name ?? packData.name}}</view> |
|||
<form> |
|||
<view class="cu-form-group margin-top"> |
|||
<view class="title">账号</view> |
|||
<input placeholder="账号" maxlength="32" name="input" v-model="loginForm.account"/> |
|||
</view> |
|||
<view class="cu-form-group" v-if="!forget"> |
|||
<view class="title">密码</view> |
|||
<input placeholder="请输入密码" maxlength="32" type="password" name="input" v-model="loginForm.password"/> |
|||
</view> |
|||
<view class="cu-form-group" v-else> |
|||
<view class="title">验证码</view> |
|||
<input placeholder="请输入验证码" maxlength="6" name="input" v-model="loginForm.code"/> |
|||
<button class='cu-btn bg-blue shadow' @tap="sendCode">发送验证码</button> |
|||
</view> |
|||
</form> |
|||
<view class='forget'> |
|||
<view><switch class="switch" :checked="loginForm.rememberMe" :class="loginForm.rememberMe?'checked':''" @change="switchChange" style="transform:scale(0.7)" />记住我</view> |
|||
<view class="text-blue" @tap="forget=!forget;loginForm.code=''">{{forget ? '密码登陆' : '忘记密码'}}</view> |
|||
</view> |
|||
<view class="flex flex-direction im-login-btn"> |
|||
<button class="cu-btn lg bg-blue" @tap="login()">登录</button> |
|||
</view> |
|||
<view class="flex flex-direction im-reg-btn" v-if="globalConfig && globalConfig.sysInfo.regtype==1"> |
|||
<button class=" cu-btn lg bg-white" @tap="register()">注册</button> |
|||
</view> |
|||
<view class="m-20 c-666" v-if="globalConfig && globalConfig.demon_mode "> |
|||
<view class="f-16 remark-title mb-10">站点仅用于演示,演示账号</view> |
|||
<view class="c-999">账号:13800000002~13800000020</view> |
|||
<view class="c-999">密码:123456</view> |
|||
</view> |
|||
<view class="footer-version c-999"> |
|||
{{globalConfig.sysInfo.name ?? packData.name}} for {{packData.version}} |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useloginStore } from '@/store/login' |
|||
import pinia from '@/store/index' |
|||
import packageData from "../../package.json" |
|||
import getSystemInfo from '../../service/getSystemInfo'; |
|||
import { useMsgStore } from '@/store/message'; |
|||
|
|||
const loginStore = useloginStore(pinia) |
|||
const msgStore = useMsgStore(pinia) |
|||
export default { |
|||
data() { |
|||
return { |
|||
loginForm:{ |
|||
account:'', |
|||
password:'', |
|||
code:'', |
|||
client_id:'', |
|||
rememberMe:false |
|||
}, |
|||
forget:false, |
|||
packData:packageData, |
|||
globalConfig:loginStore.globalConfig |
|||
} |
|||
}, |
|||
watch:{ |
|||
forget(val){ |
|||
if(val){ |
|||
this.loginForm.password='123456'; |
|||
} |
|||
} |
|||
}, |
|||
onLoad(options){ |
|||
// 检查是否有token,如果有就自动登录 |
|||
const token=options.token; |
|||
if(token){ |
|||
this.doLogin({token:token}); |
|||
} |
|||
}, |
|||
mounted() { |
|||
if(this.globalConfig && this.globalConfig.demon_mode){ |
|||
const random = Math.floor(Math.random() * 19 + 2) |
|||
this.loginForm.account=13800000000+random; |
|||
this.loginForm.password='123456'; |
|||
} |
|||
let LoginAccount=uni.getStorageSync('LoginAccount'); |
|||
if(LoginAccount){ |
|||
this.loginForm=LoginAccount; |
|||
} |
|||
}, |
|||
methods: { |
|||
switchChange(e) { |
|||
this.loginForm.rememberMe=e.detail.value; |
|||
}, |
|||
sendCode(){ |
|||
if(!this.loginForm.account){ |
|||
uni.showToast({ |
|||
title: '请输入账号!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
let data={ |
|||
account:this.loginForm.account, |
|||
type:1 |
|||
} |
|||
this.$api.LoginApi.sendCode(data).then((res)=>{ |
|||
uni.showToast({ |
|||
title: res.msg, |
|||
icon: "none" |
|||
}); |
|||
}) |
|||
}, |
|||
register(){ |
|||
uni.navigateTo({ |
|||
url:"/pages/login/register" |
|||
}) |
|||
}, |
|||
login(){ |
|||
if(this.loginForm.rememberMe){ |
|||
uni.setStorageSync('LoginAccount',this.loginForm); |
|||
}else{ |
|||
uni.removeStorageSync('LoginAccount'); |
|||
} |
|||
if(this.loginForm.account==""){ |
|||
uni.showToast({ |
|||
title: '请输入账号!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
if(this.loginForm.password==""){ |
|||
uni.showToast({ |
|||
title: '请输入密码!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
let client_id=uni.getStorageSync('client_id'); |
|||
if(client_id){ |
|||
this.loginForm.client_id=client_id; |
|||
} |
|||
this.doLogin(this.loginForm); |
|||
|
|||
const config = uni.getStorageSync('globalConfig') |
|||
getSystemInfo.batchInsertOrUpdate(config); |
|||
}, |
|||
doLogin(data){ |
|||
this.$api.LoginApi.login(data).then(res => { |
|||
if (res.code == 0) { |
|||
uni.setStorageSync('authToken', res.data.authToken) |
|||
const parts = res.data.userInfo.avatar.split('/') |
|||
let lastPart = parts.pop() || parts.pop() || '' |
|||
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart)); |
|||
res.data.userInfo.imgname = isNumber ? lastPart+'.png' : lastPart |
|||
let userInfo=res.data.userInfo; |
|||
// 登录成功后绑定wss |
|||
this.socketIo.send({ |
|||
type: "bindUid", |
|||
user_id: userInfo.user_id, |
|||
token:res.data.authToken |
|||
}); |
|||
loginStore.login(userInfo); |
|||
this.getNoticeCount() |
|||
uni.reLaunch({ |
|||
url: '/pages/index/index' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
getNoticeCount(){ |
|||
this.$api.compaApi.getNoticeCount().then(res => { |
|||
msgStore.getCount(res.data.count); |
|||
}) |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.login-logo { |
|||
width: 180rpx; |
|||
height: 180rpx; |
|||
font-size: 80rpx; |
|||
text-align: center; |
|||
line-height: 120rpx; |
|||
border-radius: 18rpx; |
|||
} |
|||
.footer-version{ |
|||
width:100%; |
|||
text-align: center; |
|||
position: fixed; |
|||
bottom: 40rpx; |
|||
} |
|||
.remark-title{ |
|||
font-weight: 600; |
|||
} |
|||
.im-reg-btn{ |
|||
padding:30rpx; |
|||
} |
|||
.im-login-btn{ |
|||
padding:0 30rpx; |
|||
} |
|||
.forget{ |
|||
display: flex; |
|||
justify-content: space-between; |
|||
padding:30rpx; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,183 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-gradual-blue" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>账号注册</template> |
|||
</cu-custom> |
|||
<view style="height:100rpx;"></view> |
|||
<view class="padding im-flex im-rows im-justify-content-center mb-10"> |
|||
<view class="im-flex im-rows im-justify-content-center"> |
|||
<image class="login-logo " :src="globalConfig.sysInfo.logo ?? packData.logo" mode="fixWidth"></image> |
|||
|
|||
</view> |
|||
</view> |
|||
<view class="im-flex im-rows im-justify-content-center">{{globalConfig.sysInfo.name ?? packData.name}}</view> |
|||
<form> |
|||
<view class="cu-form-group margin-top"> |
|||
<view class="title">账号</view> |
|||
<input :placeholder="placeholder" class="uni-input" maxlength="32" name="input" v-model="regForm.account" @input="handleInput"/> |
|||
</view> |
|||
<view class="cu-form-group margin-top"> |
|||
<view class="title">用户名/昵称</view> |
|||
<input placeholder="请输入用户名或昵称" maxlength="32" name="input" v-model="regForm.realname"/> |
|||
</view> |
|||
<view class="cu-form-group" v-if="parseInt(globalConfig.sysInfo.regauth)"> |
|||
<view class="title">验证码</view> |
|||
<input placeholder="请输入验证码" maxlength="6" name="input" v-model="regForm.code"/> |
|||
<button class='cu-btn bg-blue shadow' @tap="sendCode">发送验证码</button> |
|||
</view> |
|||
<view class="cu-form-group"> |
|||
<view class="title">密码</view> |
|||
<input placeholder="请输入密码" maxlength="32" type="password" name="input" v-model="regForm.password"/> |
|||
</view> |
|||
<view class="cu-form-group"> |
|||
<view class="title">重复密码</view> |
|||
<input placeholder="请重复输入密码" maxlength="32" type="password" name="input" v-model="regForm.repass"/> |
|||
</view> |
|||
</form> |
|||
<view class="flex flex-direction im-login-btn"> |
|||
<button class="cu-btn lg bg-blue" @tap="login()">注册</button> |
|||
</view> |
|||
<view class="footer-version c-999"> |
|||
{{globalConfig.sysInfo.name ?? packData.name}} for {{packData.version}} |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useloginStore } from '@/store/login' |
|||
import pinia from '@/store/index' |
|||
import packageData from "../../package.json" |
|||
const loginStore = useloginStore(pinia) |
|||
export default { |
|||
data() { |
|||
return { |
|||
regForm:{ |
|||
account:'', |
|||
realname:'', |
|||
password:'', |
|||
repass:'', |
|||
code:'' |
|||
}, |
|||
placeholder:'请输入账号:4-32个字符', |
|||
forget:false, |
|||
packData:packageData, |
|||
globalConfig:loginStore.globalConfig |
|||
} |
|||
}, |
|||
watch:{ |
|||
// forget(val){ |
|||
// if(val){ |
|||
// this.regForm.password='123456'; |
|||
// } |
|||
// } |
|||
}, |
|||
mounted() { |
|||
let regauth=this.globalConfig.sysInfo.regauth ?? 0; |
|||
if(regauth==1){ |
|||
this.placeholder='请输入手机号'; |
|||
}else if(regauth==2){ |
|||
this.placeholder='请输入邮箱账号'; |
|||
}else if(regauth==3){ |
|||
this.placeholder='请输入手机号/邮箱'; |
|||
} |
|||
}, |
|||
methods: { |
|||
handleInput(event) { |
|||
let value = event.detail.value; |
|||
let filteredValue = value.replace(/[\u4e00-\u9fa5]/g, ''); |
|||
this.regForm.account = filteredValue; |
|||
}, |
|||
sendCode(){ |
|||
if(!this.regForm.account){ |
|||
uni.showToast({ |
|||
title: '请输入账号!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
let data={ |
|||
account:this.regForm.account, |
|||
type:2 |
|||
} |
|||
this.$api.LoginApi.sendCode(data).then((res)=>{ |
|||
uni.showToast({ |
|||
title: res.msg, |
|||
icon: "none" |
|||
}); |
|||
}) |
|||
}, |
|||
login(){ |
|||
if(this.regForm.account==""){ |
|||
uni.showToast({ |
|||
title: '请输入账号!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
if(this.regForm.realname==""){ |
|||
uni.showToast({ |
|||
title: '请输入用户名或者昵称!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
if(this.regForm.password=="" || this.regForm.password.length<6 || this.regForm.password.length>16){ |
|||
uni.showToast({ |
|||
title: '请输入6-16位密码!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
|
|||
if(this.regForm.password!=this.regForm.repass){ |
|||
uni.showToast({ |
|||
title: '两次密码输入不相同!', |
|||
icon: "none" |
|||
}); |
|||
return false; |
|||
} |
|||
this.$api.LoginApi.register(this.regForm).then(res => { |
|||
if (res.code == 0) { |
|||
setTimeout(()=>{ |
|||
uni.reLaunch({ |
|||
url: '/pages/login/index' |
|||
}) |
|||
},2000) |
|||
} |
|||
}) |
|||
|
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.login-logo { |
|||
width: 180rpx; |
|||
height: 180rpx; |
|||
font-size: 80rpx; |
|||
text-align: center; |
|||
line-height: 120rpx; |
|||
border-radius: 18rpx; |
|||
} |
|||
.footer-version{ |
|||
width:100%; |
|||
text-align: center; |
|||
position: fixed; |
|||
bottom: 40rpx; |
|||
} |
|||
.remark-title{ |
|||
font-weight: 600; |
|||
} |
|||
.im-reg-btn{ |
|||
padding:30rpx; |
|||
} |
|||
.im-login-btn{ |
|||
padding:30rpx; |
|||
} |
|||
.forget{ |
|||
padding:30rpx; |
|||
text-align: right; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,41 @@ |
|||
<template> |
|||
<map-Point @commitCheck="commitCheck" :mapKey='mapKey' :Radius='Radius' :showResetting='showResetting' :listIco='listIco' :orientationIco='orientationIco' :resettingIco='resettingIco' :configData='configData'></map-Point> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
mapKey:'99e0c683598315f86d872c5ea3cf0a7b', |
|||
address: '', |
|||
latitude: '', |
|||
longitude: '', |
|||
listIco:'/uni_modules/map-Point/static/item-inx.png', |
|||
orientationIco:'/uni_modules/map-Point/static/map-inx.png', |
|||
resettingIco:"/uni_modules/map-Point/static/position.png", |
|||
showResetting:true, |
|||
Radius:'', |
|||
// 微信公众号jsSdk配置 |
|||
configData:{ |
|||
debug:false,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 |
|||
appId:'',// 必填,公众号的唯一标识 |
|||
timestamp: '', // 必填,生成签名的时间戳 |
|||
nonceStr: '', // 必填,生成签名的随机串 |
|||
signature: '', // 必填,签名 |
|||
} |
|||
}; |
|||
}, |
|||
methods: { |
|||
commitCheck(e) { |
|||
console.log(e, 565); |
|||
uni.$emit('commitCheck', e); |
|||
uni.navigateBack({ |
|||
delta: 1 |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
|||
@ -0,0 +1,262 @@ |
|||
<template> |
|||
<view class=""> |
|||
<web-view @message="message" :src="html"></web-view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import permision from "@/utils/permission.js" |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useloginStore } from '@/store/login'; |
|||
import pinia from '@/store/index' |
|||
import config from "@/common/config"; |
|||
const userStore = useloginStore(pinia) |
|||
const {userInfo} = storeToRefs(userStore); |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
html: '', |
|||
postMsg:null, |
|||
webview:null, |
|||
Frames: null, |
|||
mainFrame: null, |
|||
contact:null, |
|||
type:false, |
|||
wsData:null, |
|||
main_id:null, |
|||
globalConfig:userStore.globalConfig |
|||
} |
|||
}, |
|||
onReady() { |
|||
// #ifdef APP-PLUS |
|||
var currentWebview = this.$scope.$getAppWebview() //获取当前页面的webview对象 |
|||
var that=this; |
|||
setTimeout(function() { |
|||
that.webview = currentWebview.children()[0] |
|||
}, 600); //如果是页面初始化调用时,需要延时一下 |
|||
this.postMsg = this.appsendH5 |
|||
// #endif |
|||
}, |
|||
onLoad: function (option) { |
|||
let platform='h5'; |
|||
let calling=0; |
|||
//#ifdef H5 |
|||
platform='h5'; |
|||
calling=1; |
|||
//#endif |
|||
//#ifdef APP-PLUS |
|||
platform= 'app'; |
|||
// 如果是app需要用户授权 |
|||
this.checkAuth(option).then((e)=>{ |
|||
// 如果是拨打方拿到权限之后发起通话 |
|||
if(option.status==1){ |
|||
setTimeout(() => { |
|||
this.postMsg({event:'calling'}); |
|||
}, 800) |
|||
} |
|||
}); |
|||
//#endif |
|||
let stun=encodeURIComponent(JSON.stringify({ |
|||
stun:this.globalConfig.chatInfo.stun ?? '', |
|||
stunUser:this.globalConfig.chatInfo.stunUser ?? '', |
|||
stunPass:this.globalConfig.chatInfo.stunPass ?? '', |
|||
})); |
|||
this.html='/hybrid/html/index.html?id='+userInfo.value.user_id+'&status='+option.status+'&calling='+calling+'&name='+option.name+'&target_id='+option.id+'&type='+option.type+'&platform='+platform+'&avatar='+option.avatar+'&stun='+stun; |
|||
this.main_id=option.msg_id; |
|||
this.type= option.type; |
|||
this.status = option.status |
|||
this.contact={ |
|||
id:option.id, |
|||
displayName:option.name, |
|||
avatar:option.avatar |
|||
} |
|||
// #ifdef H5 |
|||
setTimeout(() => { |
|||
this.Frames = document.getElementsByTagName('iframe'); |
|||
this.mainFrame = this.Frames[0].contentWindow; |
|||
this.postMsg = this.h5sendH5 |
|||
}, 500) |
|||
|
|||
window.onmessage = (e) => { |
|||
this.msgCallback(e) |
|||
} |
|||
//#endif |
|||
|
|||
uni.$off('webrtcConn'); |
|||
// 接收websocket传输过来的消息,并发送给webview |
|||
uni.$on('webrtcConn',(e)=>{ |
|||
if(e.fromUser.id==userInfo.value.user_id){ |
|||
if(e.extends.event=="otherOpt"){ |
|||
this.wsData=null; |
|||
this.main_id=null; |
|||
this.postMsg({event:'hangup'},'*'); |
|||
} |
|||
return; |
|||
} |
|||
// 如果msgID不相同,表示在忙线中 |
|||
if(this.main_id && this.main_id!=e.id){ |
|||
this.$api.msgApi.sendToMsg({ |
|||
toContactId:e.fromUser.user_id, |
|||
type:e.extends.type, |
|||
event:'busy', |
|||
status:e.extends.status, |
|||
code:907, |
|||
id:e.id, |
|||
msg_id:e.msg_id, |
|||
}) |
|||
return; |
|||
}else{ |
|||
this.wsData=e; |
|||
if(this.main_id && this.main_id==e.id){ |
|||
this.postMsg(JSON.parse(JSON.stringify(e.extends)),'*'); |
|||
} |
|||
} |
|||
|
|||
}) |
|||
}, |
|||
methods: { |
|||
async checkAuth(option){ |
|||
// #ifdef APP-PLUS |
|||
let record=await this.requestPermission('record'); |
|||
if(!record){ |
|||
return false; |
|||
} |
|||
if(option.type==1){ |
|||
let camera=await this.requestPermission('camera'); |
|||
if(!camera){ |
|||
return false; |
|||
} |
|||
} |
|||
// #endif |
|||
}, |
|||
//音视频权限获取 |
|||
async requestPermission(auth) { |
|||
let isIos=false; |
|||
// #ifdef APP-PLUS |
|||
isIos = (plus.os.name == "iOS") |
|||
// #endif |
|||
let andriodAuth=''; |
|||
if(auth=='record'){ |
|||
andriodAuth='android.permission.RECORD_AUDIO'; |
|||
}else if(auth=='camera'){ |
|||
andriodAuth='android.permission.CAMERA'; |
|||
} |
|||
if(isIos){ |
|||
// let iosRes = await permision.judgeIosPermission(auth); |
|||
return true; |
|||
}else{ |
|||
let andRes = await permision.requestAndroidPermission(andriodAuth); |
|||
return andRes==1 ? true : false; |
|||
} |
|||
}, |
|||
// 接收webview传输过来的消息 |
|||
message(event) { |
|||
const msg = { |
|||
data: event.detail.data[0] |
|||
} |
|||
this.msgCallback(msg) |
|||
}, |
|||
// app端向webview传输消息 |
|||
appsendH5(params) { |
|||
if (params.iceCandidate && params.iceCandidate.length>0){ |
|||
params.iceCandidate = JSON.parse(params.iceCandidate) |
|||
} |
|||
this.webview.evalJS("getUniAppMessage('" + JSON.stringify(params) + "')") |
|||
}, |
|||
// h5端向webview传输消息 |
|||
h5sendH5(params) { |
|||
this.mainFrame.postMessage(params, '*') |
|||
}, |
|||
msgCallback(e){ |
|||
let iceCandidate = ''; |
|||
let msg_id=''; |
|||
if(this.wsData){ |
|||
msg_id=this.wsData.msg_id ?? ''; |
|||
} |
|||
let api=true; |
|||
switch(e.data['event']){ |
|||
case 'hangup': |
|||
this.closeCall(); |
|||
if(e.data['code']==907){ |
|||
uni.showToast({ |
|||
title: '对方忙~~', |
|||
icon:'none' |
|||
}) |
|||
} |
|||
if(!e.data.isbtn){ |
|||
api=false; |
|||
} |
|||
break; |
|||
case 'iceCandidate': |
|||
console.log('监听同步ice') |
|||
let niceCandidate = {} |
|||
niceCandidate['candidate'] = e.data['iceCandidate']['candidate'] |
|||
niceCandidate['sdpMLineIndex'] = e.data['iceCandidate']['sdpMLineIndex'] |
|||
niceCandidate['sdpMid'] = e.data['iceCandidate']['sdpMid'] |
|||
iceCandidate=JSON.stringify(niceCandidate) |
|||
break; |
|||
case "mediaDevices": |
|||
api=false; |
|||
uni.showToast({ |
|||
title: '请检查音视频', |
|||
icon:'none' |
|||
}) |
|||
this.closeCall(); |
|||
} |
|||
if(api){ |
|||
this.$api.msgApi.sendToMsg({ |
|||
id:this.main_id, |
|||
msg_id:msg_id, |
|||
toContactId:this.contact.id, |
|||
type:this.type, |
|||
event:e.data['event'], |
|||
status:e.data['status'] ?? '', |
|||
code:e.data['code'] ?? '', |
|||
callTime:e.data['callTime'] ?? '', |
|||
sdp:e.data['sdp'] ?? '', |
|||
iceCandidate:iceCandidate, |
|||
}).then((res)=>{ |
|||
if(e.data['event']=='calling'){ |
|||
this.wsData=res.data; |
|||
} |
|||
if(res.data.extends.code==907){ |
|||
uni.showToast({ |
|||
title:'对方不在线', |
|||
icon:'none' |
|||
}) |
|||
this.closeCall(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
}, |
|||
//关闭通话页面 |
|||
closeCall(){ |
|||
const innerAudioContext = uni.createInnerAudioContext(); |
|||
innerAudioContext.autoplay = true; |
|||
innerAudioContext.src = config.apiUrl+'/static/voice/guaduan.mp3'; |
|||
innerAudioContext.onStop((res) => { |
|||
this.$forceUpdate() |
|||
}) |
|||
let pages = getCurrentPages(); |
|||
if (pages.length > 1) { |
|||
uni.navigateBack(); |
|||
} else { |
|||
uni.reLaunch({ |
|||
url: '/pages/index/index' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.container { |
|||
padding: 20px; |
|||
font-size: 14px; |
|||
line-height: 24px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,965 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>聊天信息</template> |
|||
</cu-custom> |
|||
<view> |
|||
<view class="bg-white"> |
|||
<view class="user-list im-flex im-justify-content-start im-align-items-center im-a im-wrap" v-if="is_group<2"> |
|||
<view class="user-info mt-20" v-for="(item,index) in userList" :key="index" align="center"> |
|||
<block v-for="(iteme,indexs) in imglist" :key="indexs" v-if="network_log=='none'"> |
|||
<image class="user-avatar" v-if="item.userInfo.imgname === iteme.name" :src="iteme.path" @tap="openChatDetail(item.userInfo)"></image> |
|||
</block> |
|||
<image v-else class="user-avatar" :src="item.userInfo.avatar" @tap="openChatDetail(item.userInfo)"></image> |
|||
|
|||
<view class="text-center user-name text-overflow">{{item.userInfo.displayName}}</view> |
|||
</view> |
|||
<view class="user-info mt-20" v-if="isAuth || is_group==0 || contact.setting.invite=='1'"> |
|||
<view class="user-opt radius-8" style='margin:auto' @tap='editUser(2)'> |
|||
<view class="icon cuIcon-add f-24"></view> |
|||
</view> |
|||
<view class="f-11 mt-5">添加成员</view> |
|||
</view> |
|||
<view class="user-info mt-20" v-if="isAuth"> |
|||
<view class="user-opt radius-8" style='margin:auto' @tap='manageUser()'> |
|||
<view class="icon cuIcon-move f-24"></view> |
|||
</view> |
|||
<view class="f-11 mt-5">移除成员</view> |
|||
</view> |
|||
</view> |
|||
<navigator v-if="is_group==1 " class="mt-10" :url="`/pages/message/group/groupUser?group_id=${contact_id}&group_id1=${contact.group_id}`"> |
|||
<view class="text-center pb-15 pt-15 im-flex im-justify-content-center im-align-items-center"> |
|||
<text class="gui-list-title-text gui-list-one-line gui-primary-color">查看全部群成员</text> |
|||
<text class="gui-list-title-desc gui-color-gray">{{groupUserCount}}人</text> |
|||
<text class="cuIcon-right"></text> |
|||
</view> |
|||
</navigator> |
|||
|
|||
|
|||
</view> |
|||
<view class="cu-list menu mt-15 bg-white" v-if="is_group==1"> |
|||
<view class="cu-item" @click="open"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 群聊名称 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey">{{contact.displayName}} <text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @click="openQr" v-if="contact.setting.invite"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 群二维码 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey"><text class="cuIcon-qr_code f-18"></text> <text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @tap="openModel('notice')"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 群公告 </view> |
|||
</view> |
|||
<view class="action" style="width:80%"> |
|||
<view class="text-grey im-flex im-justify-content-end"> |
|||
<view class="text-overflow notice-line"> |
|||
<!-- {{contact.notice ?? '暂无公告'}} --> |
|||
{{contact.notice==null?'暂无公告':contact.notice}} |
|||
</view> |
|||
<text class="cuIcon-right"></text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" v-if="isAuth" @tap="openModel('manage')"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 群管理 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey"><text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
<uni-popup ref="popup" type="dialog"> |
|||
<uni-popup-dialog mode="input" :value="contact.displayName" title="修改群名称" :duration="2000" :before-close="true" @close="closePop" @confirm="editGroupName"> |
|||
|
|||
</uni-popup-dialog> |
|||
</uni-popup> |
|||
</view> |
|||
|
|||
<view class="cu-list menu mt-15 bg-white"> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 消息免打扰 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setIsNotice" :class="contactUser.is_notice==0?'checked':''" :checked="contactUser.is_notice==0?true:false"></switch> |
|||
</view> |
|||
<!-- <view class="action" v-else> |
|||
<switch class="switch" @change="setIsNotice" :class="!contact.is_notice?'checked':''" :checked="!contact.is_notice?true:false"></switch> |
|||
</view> --> |
|||
</view> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 置顶聊天 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setIsTop" :class="contactUser.is_top?'checked':''" :checked="contactUser.is_top?true:false"></switch> |
|||
</view> |
|||
<!-- <view class="action" v-else> |
|||
<switch class="switch" @change="setIsTop" :class="contact.is_top?'checked':''" :checked="contact.is_top?true:false"></switch> |
|||
</view> --> |
|||
</view> |
|||
<view class="cu-item" v-if="contact.user_id"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 加入黑名单 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setis_blacklist($event,contact.user_id)" :class="contactUser.is_blacklist?'checked':''" :checked="contactUser.is_blacklist?true:false"></switch> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item menu bg-white" @click="onDeleterecord" v-if="userConfig.chatInfo.messageOneClickDel=='1'"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 一键清除聊天记录 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey"><text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-list menu bg-white" @click="showRecord" v-if="userConfig.chatInfo.userMsgClear=='1'"> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 自动清除聊天记录 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey"><text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<!-- #ifndef H5 --> |
|||
<view class="cu-list menu mt-15 bg-white" @tap="modelName='setBg'"> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 设置当前聊天背景 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey"><text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<!-- #endif --> |
|||
|
|||
<navigator class="mt-10" :url="`/pages/message/record?id=${contact_id}`"> |
|||
<view class="cu-list menu mt-15 bg-white"> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view> 查看聊天记录 </view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey"><text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</navigator> |
|||
<view class="cu-list menu mt-15 bg-white" v-if="is_group==1 && isAdmin" @tap="clearMessage"> |
|||
<view class="cu-item text-center delete-btn"> |
|||
<text class="c-orange">清空聊天记录</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="cu-list menu mt-15 bg-white" v-if="is_group==1" @tap="removeGroup"> |
|||
<view class="cu-item text-center delete-btn"> |
|||
<text class="c-red">{{isAdmin ? '解散群聊' : '退出群聊' }}</text> |
|||
</view> |
|||
</view> |
|||
<view class="parting-line-20"></view> |
|||
<view class="cu-modal bottom-modal" :class="modelName=='notice'&&contact.setting.manage=='0'||modelName=='notice'&&contact.role<3?'show':''"> |
|||
<view class="cu-dialog"> |
|||
<view class="cu-bar bg-white"> |
|||
<view class="action text-gray" @tap="closeModel">取消</view> |
|||
<view class="action text-green" @tap="editNotice">保存</view> |
|||
</view> |
|||
<view class="notice-content"> |
|||
<textarea class="im-textarea" maxlength="-1" v-model="contact.notice" placeholder="请输入公告内容..."></textarea> |
|||
<!-- :disabled="!isAuth" --> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-modal bottom-modal" :class="modelName=='setBg'?'show':''" @tap="modelName=''"> |
|||
<view class="cu-dialog" @tap.stop=''> |
|||
<view class="cu-bar"> |
|||
<view class="action" >设置当前聊天背景</view> |
|||
<view class="action cuIcon-close f-18" @tap="modelName=''"></view> |
|||
</view> |
|||
<view class="cu-list menu mb-15 bg-white"> |
|||
<view class="cu-item" @click="chooseImg()"> |
|||
<view class="content padding-tb-sm"> |
|||
<view>选取背景图片</view> |
|||
</view> |
|||
<view class="action"> |
|||
<view class="text-grey"><text class="cuIcon-right"></text></view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="bgInfo.image"> |
|||
<view><image :src="bgInfo.image" style="width:200px" mode="widthFix"></image></view> |
|||
<button class="cu-btn bg-red mt-10" @tap="removeBg">移除背景图片</button> |
|||
</view> |
|||
<view class="cu-list menu mt-15 mb-15 bg-white"> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view>背景虚化</view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setFilter" :class="bgInfo.filter?'checked':''" :checked="bgInfo.filter == true ? true :false"></switch> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<uni-notice-bar text="修改后重新进入聊天才能生效" class="mb-15"/> |
|||
</view> |
|||
</view> |
|||
<view class="cu-modal bottom-modal" :class="modelName=='manage'?'show':''"> |
|||
<view class="cu-dialog"> |
|||
<view class="cu-bar bg-white"> |
|||
<view class="action text-gray" @tap="closeModel">取消</view> |
|||
<view class="action text-green" @tap="saveManage">保存</view> |
|||
</view> |
|||
<view class="manage-content"> |
|||
<view class="cu-list menu mt-15 bg-white"> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view>仅群主和群管理员可以管理</view> |
|||
<view class="text-gray text-sm"> 启用后,其他成员不能修改群名称,编辑公告等</view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setManage" :class="contact.setting.manage=='1'?'checked':''" :checked="contact.setting.manage=='1'?true:false"></switch> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view>允许群成员邀请</view> |
|||
<view class="text-gray text-sm">启用后,其他成员可以邀请其他人加入群聊</view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setInvite" :class="contact.setting.invite=='1'?'checked':''" :checked="contact.setting.invite=='1'?true:false"></switch> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view>允许成员查看历史消息</view> |
|||
<view class="text-gray text-sm">启用后,新入群的成员可以查看所有的历史记录</view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setHistory" :class="contact.setting.history=='1'?'checked':''" :checked="contact.setting.history=='1'?true:false"></switch> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item"> |
|||
<view class="content padding-tb-sm"> |
|||
<view>允许添加群成员为好友</view> |
|||
<view class="text-gray text-sm">启用后,成员可以互相查看资料并添加为好友或发消息</view> |
|||
</view> |
|||
<view class="action"> |
|||
<switch class="switch" @change="setProfile" :class="contact.setting.profile=='1'?'checked':''" :checked="contact.setting.profile=='1'?true:false"></switch> |
|||
</view> |
|||
</view> |
|||
<uni-section title="群聊禁言" type="line"> |
|||
<radio-group class="block" @change="setSpeak"> |
|||
<view class="cu-form-group" v-for="(item,indexs) in radioList" :key="indexs"> |
|||
<view class="title">{{item.label}}</view> |
|||
<radio :class="contact.setting.nospeak==item.value?'checked':''" :checked="contact.setting.nospeak==item.value?true:false" :value="item.value.toString()"></radio> |
|||
</view> |
|||
</radio-group> |
|||
</uni-section> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="cu-modal bottom-modal" :class="bolRecord=='manage'?'show':''"> |
|||
<view class="cu-dialog"> |
|||
<view class="cu-bar bg-white"> |
|||
<view class="action text-gray" @tap="RecordModel">取消</view> |
|||
<view class="action text-green" @tap="RecordManage">保存</view> |
|||
</view> |
|||
<view class="manage-content"> |
|||
<radio-group class="block" @change="setuserMsgClear"> |
|||
<label class="cu-form-group" v-for="(tag,tagindexs) in userConfig.chatInfo.userMsgClearDay" :key="tagindexs"> |
|||
<view class="title">{{tag.title}}</view> |
|||
<radio :checked="MsgClear==tag.value?true:false" :value="tag.value"></radio> |
|||
<!-- :class="MsgClear==tag.title?'checked':''" --> |
|||
</label> |
|||
</radio-group> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useMsgStore } from '@/store/message'; |
|||
// #ifdef APP-PLUS |
|||
import groupUserList from '@/service/groupUserList'; |
|||
import {getSavedImages1} from '@/utils/LocalFileSystemURL.js' |
|||
import getMessageList from '@/service/getMessageList'; |
|||
// #endif |
|||
import pinia from '@/store/index'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useloginStore } from '@/store/login'; |
|||
const userStore = useloginStore(pinia) |
|||
const msgStore = useMsgStore(pinia) |
|||
const {network_log} = storeToRefs(msgStore); |
|||
export default { |
|||
components: { |
|||
}, |
|||
data() { |
|||
return { |
|||
pageLoading: true, |
|||
contact_id: null, //聊天id, |
|||
is_group:0, |
|||
groupUserCount:0, |
|||
modelName:false, |
|||
userList: [], //群成员 |
|||
allUser:[], |
|||
userInfo:userStore.userInfo, |
|||
userConfig:userStore.globalConfig, |
|||
groupInfo:uni.getStorageSync('setgroupInfo'), |
|||
groupInfo1:uni.getStorageSync('setgroupInfo'), |
|||
chatRecordlist: [{ |
|||
text: '文本', |
|||
icon: "icon-wenben", |
|||
type: 'text' |
|||
|
|||
}, |
|||
{ |
|||
text: '图片', |
|||
icon: "icon-zhaopian", |
|||
type: 'image' |
|||
|
|||
}, { |
|||
text: '文件', |
|||
icon: "icon-wenjian", |
|||
type: 'file' |
|||
|
|||
}, { |
|||
text: '视频', |
|||
icon: "icon-shipin", |
|||
type: 'video' |
|||
|
|||
}, { |
|||
text: '项目', |
|||
icon: "icon-xiangmu_2", |
|||
type: 'project' |
|||
|
|||
}, { |
|||
text: '客户', |
|||
icon: "icon-kehu", |
|||
type: 'leads' |
|||
|
|||
}, |
|||
], |
|||
radioList: [{ |
|||
label: "关闭", |
|||
value: 0 |
|||
}, |
|||
{ |
|||
label: "仅管理员可发言", |
|||
value: 1 |
|||
}, |
|||
{ |
|||
label: "仅群主可发言", |
|||
value: 2 |
|||
}, |
|||
], |
|||
isAuth: false, //判断自己是否时群管理或者群主 |
|||
contact: null, //联系人相关信息 |
|||
isAdmin:false, //如果为真,自己就是群主 |
|||
isManage: false, // 如果为真,自己就是管理 |
|||
user_ids: [], |
|||
user:[],//全部群成员 |
|||
bgInfo:{ |
|||
image:'', |
|||
filter:false |
|||
}, |
|||
imglist:[], |
|||
network_log:network_log, |
|||
contactUser:{}, |
|||
bolRecord:false, |
|||
MsgClear:0, |
|||
MsgClearDay:0 |
|||
} |
|||
}, |
|||
onShow() { |
|||
const _this = this |
|||
if(this.network_log == 'none'){ |
|||
_this.Getchatinformation() |
|||
_this.getImagePath() |
|||
}else{ |
|||
_this.getUserlist() |
|||
// _this.Getchatinformation() |
|||
// _this.getImagePath() |
|||
} |
|||
}, |
|||
onLoad: function(options) { |
|||
let bgInfo=uni.getStorageSync('chat-bg-info'+options.id); |
|||
if(bgInfo){ |
|||
this.bgInfo=bgInfo; |
|||
} |
|||
this.is_group = options.is_group; |
|||
this.contact_id = options.id; |
|||
let contact=msgStore.getContact(this.contact_id); |
|||
if(!contact){ |
|||
uni.showToast({ |
|||
title:'联系人不存在', |
|||
icon:'none', |
|||
duration:1500, |
|||
complete:(res)=>{ |
|||
uni.reLaunch({ |
|||
url: '/pages/index/index' |
|||
}) |
|||
} |
|||
}) |
|||
return; |
|||
} |
|||
|
|||
if(options.notice!=='undefined'){ |
|||
this.contact=contact; |
|||
this.contact.notice = uni.getStorageSync('notice'); |
|||
}else{ |
|||
this.contact=contact; |
|||
} |
|||
|
|||
if(this.is_group==0){ |
|||
contact.userInfo={ |
|||
id:contact.user_id, |
|||
account:contact.account, |
|||
displayName:contact.displayName, |
|||
avatar:contact.avatar |
|||
} |
|||
this.allUser.push(contact); |
|||
this.userList.push(contact); |
|||
} |
|||
if(this.is_group==1){ |
|||
this.MsgClear = this.groupInfo.clear_msg_day; |
|||
}else{ |
|||
this.getFriendInfo() |
|||
} |
|||
}, |
|||
methods: { |
|||
getFriendInfo(){ |
|||
this.$api.msgApi.getFriendInfo({friend_user_id:this.contact_id}).then(res => { |
|||
this.contactUser = res.data; |
|||
this.MsgClearDay = this.contactUser.clear_msg_day; |
|||
this.MsgClear = this.contactUser.clear_msg_day; |
|||
}) |
|||
}, |
|||
showRecord(){ |
|||
this.bolRecord = 'manage'; |
|||
}, |
|||
RecordModel(){ |
|||
if(this.is_group==1){ |
|||
this.MsgClear = this.groupInfo.clear_msg_day; |
|||
}else{ |
|||
this.MsgClear = this.contactUser.clear_msg_day; |
|||
} |
|||
this.bolRecord = false; |
|||
}, |
|||
RecordManage(){ |
|||
if(this.is_group==1){ |
|||
this.$api.msgApi.groupClearMsgDay({id: this.contact.id,day: this.MsgClearDay}).then(res=>{ |
|||
this.groupInfo.clear_msg_day = this.MsgClearDay |
|||
msgStore.checkMsg(res.data); |
|||
msgStore.appendMsg(res.data); |
|||
uni.setStorageSync('setgroupInfo',this.groupInfo); |
|||
this.deleteList(res.data) |
|||
}) |
|||
}else{ |
|||
this.$api.msgApi.friendClearMsgDay({to_user: this.contact.id,day: this.MsgClearDay}).then(res=>{ |
|||
this.getFriendInfo() |
|||
this.deleteList(res.data) |
|||
msgStore.checkMsg(res.data); |
|||
msgStore.appendMsg(res.data); |
|||
}) |
|||
} |
|||
this.bolRecord = false; |
|||
}, |
|||
async deleteList(data){ |
|||
await getMessageList.deleteList(data) |
|||
}, |
|||
async deleteallList1(data){ |
|||
await getMessageList.deleteallList1(data) |
|||
}, |
|||
openModel(model){ |
|||
this.modelName=model; |
|||
}, |
|||
closeModel(){ |
|||
this.modelName=false; |
|||
}, |
|||
saveManage(){ |
|||
if(!this.isAuth) return; |
|||
this.$api.msgApi.groupSetting({ |
|||
id: this.contact.id, |
|||
setting: this.contact.setting |
|||
}) |
|||
this.modelName=false; |
|||
}, |
|||
setManage(e){ |
|||
this.contact.setting.manage=e.detail.value ? '1' : '0'; |
|||
}, |
|||
setInvite(e){ |
|||
this.contact.setting.invite=e.detail.value ? '1' : '0'; |
|||
}, |
|||
setHistory(e){ |
|||
this.contact.setting.history=e.detail.value ? '1' : '0'; |
|||
}, |
|||
setProfile(e){ |
|||
this.contact.setting.profile=e.detail.value ? '1' : '0'; |
|||
}, |
|||
setSpeak(e){ |
|||
this.contact.setting.nospeak=e.detail.value; |
|||
}, |
|||
setuserMsgClear(e){ |
|||
this.MsgClear = e.detail.value; |
|||
// const val = this.userConfig.chatInfo.userMsgClearDay.filter(item=>item.value==e.detail.value) |
|||
this.MsgClearDay = e.detail.value; |
|||
}, |
|||
setIsNotice(e){ |
|||
// if(this.contact.user_id){ |
|||
console.log(e.detail.value); |
|||
this.contactUser.is_notice=e.detail.value ? 0 : 1; |
|||
this.contact.is_notice=e.detail.value ? 0 : 1; |
|||
// }else{ |
|||
// this.contact.is_notice=e.detail.value ? 0 : 1; |
|||
// } |
|||
|
|||
this.$api.msgApi.isNoticeAPI({ |
|||
id: this.contact.id, |
|||
is_group:this.contact.is_group, |
|||
is_notice:this.contact.is_notice |
|||
}) |
|||
}, |
|||
setIsTop(e){ |
|||
// if(this.contact.user_id){ |
|||
this.contactUser.is_top=e.detail.value ? 1 : 0; |
|||
this.contact.is_top=e.detail.value ? 1 : 0; |
|||
// }else{ |
|||
// this.contact.is_top=e.detail.value ? 1 : 0; |
|||
// } |
|||
|
|||
this.$api.msgApi.setChatTopAPI({ |
|||
id: this.contact.id, |
|||
is_group:this.contact.is_group, |
|||
is_top:this.contact.is_top |
|||
}) |
|||
}, |
|||
setis_blacklist(e,id){ |
|||
this.contactUser.is_blacklist=e.detail.value ? 1 : 0; |
|||
this.$api.msgApi.isBlacklist({friend_user_id:id}).then( res =>{ |
|||
if(res.code==400){ |
|||
this.contactUser.is_blacklist = 0 |
|||
} |
|||
}) |
|||
}, |
|||
editNotice(){ |
|||
// if(!this.isAuth) return; |
|||
this.$api.msgApi.setNotice({ |
|||
id: this.contact.id, |
|||
notice: this.contact.notice |
|||
}) |
|||
this.modelName=false; |
|||
}, |
|||
open() { |
|||
this.$refs.popup.open() |
|||
}, |
|||
openQr() { |
|||
uni.navigateTo({ |
|||
url: '/pages/index/qrcode?group_id='+ this.contact.id |
|||
}) |
|||
}, |
|||
editGroupName(e){ |
|||
this.$api.msgApi.editGroupName({id:this.contact.id,displayName:e}).then( res =>{ |
|||
if(res.code!==400){ |
|||
this.contact.displayName=e; |
|||
} |
|||
this.$refs.popup.close() |
|||
}) |
|||
}, |
|||
closePop(){ |
|||
this.$refs.popup.close() |
|||
}, |
|||
//移除群聊 |
|||
removeGroup() { |
|||
// 如果是群主就解散群聊,否则就退出群聊 |
|||
let txt="退出群聊"; |
|||
if(this.isAdmin) txt="解散群聊"; |
|||
uni.showModal({ |
|||
title: '确定要'+txt+'吗?', |
|||
success: e => { |
|||
if (e.confirm) { |
|||
if(this.isAdmin){ |
|||
this.$api.msgApi.removeGroup({id:this.contact.id}).then((res)=>{ |
|||
// 删除之后返回首页 |
|||
uni.reLaunch({ |
|||
url: '/pages/index/index' |
|||
}) |
|||
}) |
|||
}else{ |
|||
this.$api.msgApi.removeUser({id:this.contact.id,user_id:this.userInfo.user_id}).then((res)=>{ |
|||
// 删除之后返回首页 |
|||
uni.reLaunch({ |
|||
url: '/pages/index/index' |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
} |
|||
} |
|||
}); |
|||
|
|||
}, |
|||
clearMessage() { |
|||
// 如果是群主就解散群聊,否则就退出群聊 |
|||
if(!this.isAdmin) { |
|||
uni.showToast({ |
|||
title:'无权操作', |
|||
icon:'none' |
|||
}) |
|||
}; |
|||
uni.showModal({ |
|||
title: '删除消息会从当前聊天记录中被删除,确定继续吗?', |
|||
success: e => { |
|||
if (e.confirm) { |
|||
this.$api.msgApi.clearMessage({id:this.contact.id}).then((res)=>{ |
|||
uni.showToast({ |
|||
title:'清除成功', |
|||
icon:'none' |
|||
}) |
|||
}) |
|||
} |
|||
} |
|||
}); |
|||
|
|||
}, |
|||
// 添加群成员 |
|||
editUser(type) { |
|||
this.user_ids = this.allUser.map(item => item.user_id) |
|||
if(this.contact.is_group==0){ |
|||
type=1 |
|||
} |
|||
uni.navigateTo({ |
|||
url: '/pages/index/userSelection?type='+type+'&contact_id=' + this.contact.id |
|||
}) |
|||
}, |
|||
// 管理群成员 |
|||
manageUser() { |
|||
uni.navigateTo({ |
|||
url: '/pages/message/group/groupUser?group_id=' + this.contact.id |
|||
}) |
|||
}, |
|||
// 跳转到聊天记录 |
|||
goChatRecord(type) { |
|||
uni.navigateTo({ |
|||
url: '/package/message/pages/chatRecord/chatRecord?type=' + type + '&toContactId=' + this.contact_id + '&is_group=1' |
|||
}) |
|||
}, |
|||
// 获取群成员列表 |
|||
getUserlist() { |
|||
if(this.is_group==0) return; |
|||
this.userList = [] |
|||
this.$api.msgApi.groupUserList({ |
|||
group_id: this.contact_id, |
|||
limit:20000, |
|||
}).then(res => { |
|||
this.user = res.data |
|||
this.Insertchatmessage(res.data) |
|||
res.data.forEach(res => { |
|||
// #ifdef APP-PLUS |
|||
uni.downloadFile({ url: res.userInfo.avatar,success: (downloadResult) => { |
|||
this.saveToPermanentStorage(downloadResult.tempFilePath); |
|||
}}) |
|||
// #endif |
|||
}) |
|||
if (res.code !== 0) return |
|||
// 判断自己是否为群主 |
|||
const admin=res.data.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id) |
|||
if(admin.length) this.isAdmin=true; |
|||
// 判断自己是否是群管理 |
|||
const manage=res.data.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id) |
|||
if(manage.length) this.manage=true; |
|||
// 判断是否有管理权限 |
|||
if(admin.length || manage.length) this.isAuth=true; |
|||
this.allUser=JSON.parse(JSON.stringify(res.data)); |
|||
if (res.data.length > 18) { |
|||
if (this.isAuth) { |
|||
// this.userList = res.data.splice(0, 18) |
|||
this.userList = res.data.splice(0, 20000) |
|||
}else if(this.contact.setting.invite){ |
|||
// this.userList = res.data.splice(0, 19) |
|||
this.userList = res.data.splice(0, 20000) |
|||
} else { |
|||
// this.userList = res.data.splice(0, 20) |
|||
this.userList = res.data.splice(0, 20000) |
|||
} |
|||
} else { |
|||
this.userList = res.data |
|||
} |
|||
this.groupUserCount=res.count; |
|||
this.pageLoading = false; |
|||
}) |
|||
}, |
|||
async Getchatinformation(){ |
|||
// #ifdef APP-PLUS |
|||
const groups = await groupUserList.getList({group_id: this.contact.group_id}); |
|||
groups.map((res)=>{ |
|||
res.userInfo = JSON.parse(res.userInfo) |
|||
}) |
|||
this.user = groups |
|||
|
|||
if (groups.length == 0) return |
|||
// 判断自己是否为群主 |
|||
const admin=groups.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id) |
|||
if(admin.length) this.isAdmin=true; |
|||
// 判断自己是否是群管理 |
|||
const manage=groups.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id) |
|||
if(manage.length) this.manage=true; |
|||
// 判断是否有管理权限 |
|||
if(admin.length || manage.length) this.isAuth=true; |
|||
this.allUser=JSON.parse(JSON.stringify(groups)); |
|||
|
|||
this.groupUserCount=groups.length; |
|||
this.userList = groups |
|||
this.pageLoading = false; |
|||
|
|||
console.info('获取聊天信息数据',groups.length,groups); |
|||
// #endif |
|||
}, |
|||
async Insertchatmessage(val){ |
|||
// #ifdef APP-PLUS |
|||
val.forEach((item)=>{ |
|||
const parts = item.userInfo.avatar.split('/') |
|||
let lastPart = parts.pop() || parts.pop() || '' |
|||
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart)); |
|||
item.userInfo.imgname = isNumber ? lastPart+'.png' : lastPart; |
|||
}) |
|||
// console.log('插入聊天信息数据',val); |
|||
await groupUserList.batchInsertOrUpdate(val); |
|||
// #endif |
|||
}, |
|||
|
|||
// App端持久化存储实现 |
|||
saveToPermanentStorage(tempPath) { |
|||
return new Promise((resolve, reject) => { |
|||
// 获取应用文档目录(持久化存储) |
|||
plus.io.resolveLocalFileSystemURL( |
|||
'_doc', |
|||
(docDir) => { |
|||
// 创建目标路径 |
|||
docDir.getDirectory( |
|||
'img1', |
|||
{ create: true, exclusive: false }, |
|||
(entry) => { |
|||
// 从临时路径获取文件名 |
|||
const fileName = this.getFileName(tempPath); |
|||
const fileName1 = this.getFileName(docDir.fullPath + 'img1/' +fileName); |
|||
// 新增:检查文件是否存在 |
|||
entry.getFile(fileName1,{ create: false }, // 不创建新文件 |
|||
(fileEntry) => { |
|||
// console.log('文件已存在,拒绝操作'); |
|||
// 文件已存在,拒绝操作 |
|||
reject(new Error('File already exists: ' + fileName)); |
|||
}, |
|||
(error) => { |
|||
// console.log(error); |
|||
// 文件不存在(或发生其他错误),继续复制操作 |
|||
if (error.code === 14) { // 1表示文件不存在(不同平台错误码可能不同) |
|||
this.copyFile(tempPath, entry, fileName, resolve, reject); |
|||
} else { |
|||
reject(error); |
|||
} |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}); |
|||
}, |
|||
|
|||
// 提取复制逻辑为独立方法 |
|||
copyFile(tempPath, targetDir, fileName, resolve, reject) { |
|||
plus.io.resolveLocalFileSystemURL( |
|||
tempPath, |
|||
(tempEntry) => { |
|||
tempEntry.copyTo( |
|||
targetDir, |
|||
fileName, |
|||
(newEntry) => { |
|||
resolve(newEntry.toLocalURL()); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
(error) => { |
|||
reject(error); |
|||
} |
|||
); |
|||
}, |
|||
|
|||
// 获取文件名工具方法 |
|||
getFileName(path) { |
|||
const index = path.lastIndexOf('/'); |
|||
let fileName = path.substr(index + 1); |
|||
fileName = fileName.replace(/\(\d+\)(?=\.[^./]+$)/, ''); |
|||
return fileName; |
|||
}, |
|||
// 获取图片地址 |
|||
async getImagePath(){ |
|||
this.imglist = await getSavedImages1() |
|||
this.imglist.map(item => { |
|||
item.path = plus.io.convertLocalFileSystemURL(item.path) |
|||
}); |
|||
console.info('读取地址',this.imglist); |
|||
}, |
|||
|
|||
// 打开联系人详情 |
|||
openChatDetail(item){ |
|||
if(this.userInfo.user_id==item.id) return; |
|||
let friend=msgStore.getContact(item.id); |
|||
// this.contact_id |
|||
if(this.contact.role<3 || this.contact.setting.profile=='1' || friend){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+item.id |
|||
}) |
|||
}else{ |
|||
uni.showToast({ |
|||
title:'已开启用户隐私!', |
|||
icon:'none' |
|||
}) |
|||
return false; |
|||
} |
|||
}, |
|||
setFilter(e){ |
|||
this.bgInfo.filter=e.detail.value ? true : false; |
|||
uni.setStorageSync('chat-bg-info'+this.contact.id,this.bgInfo) |
|||
}, |
|||
chooseImg(){ |
|||
uni.chooseImage({ |
|||
count : 1, |
|||
sizeType : ['compressed'], |
|||
sourceType : ['album', 'camera'], |
|||
success : (res)=>{ |
|||
const tempFiles = res.tempFiles; |
|||
tempFiles.forEach((item) => { |
|||
uni.saveFile({ |
|||
tempFilePath:item.path, |
|||
success:(res)=>{ |
|||
this.bgInfo.image=res.savedFilePath; |
|||
uni.setStorageSync('chat-bg-info'+this.contact.id,this.bgInfo) |
|||
uni.showToast({ |
|||
title:'设置成功,重新进入聊天后生效', |
|||
icon:'none' |
|||
}) |
|||
} |
|||
}) |
|||
}) |
|||
} |
|||
}); |
|||
}, |
|||
removeBg(){ |
|||
this.bgInfo.image=''; |
|||
uni.setStorageSync('chat-bg-info'+this.contact.id,'') |
|||
}, |
|||
onDeleterecord(){ |
|||
const removeval = {group_id:this.contact.is_group==1?this.contact.id:'',form_user:this.userInfo.user_id,to_user:this.contact.id} |
|||
uni.showModal({ |
|||
title: '提示', |
|||
content: `是否要删除${this.contact.is_group==1?'群聊':'个人'}消息,需注意点击后所有人的聊天记录会消失`, |
|||
success:(res)=>{ |
|||
if (res.confirm) { |
|||
if(this.contact.is_group==1){ |
|||
this.$api.msgApi.groupremoveAllMessage({id: this.contact.id}).then(res => { |
|||
if(res.code==0){ |
|||
uni.showToast({ |
|||
title:'删除成功', |
|||
icon:'none' |
|||
}) |
|||
this.deleteallList1(removeval) |
|||
} |
|||
}) |
|||
}else{ |
|||
this.$api.msgApi.friendremoveAllMessage({to_user: this.contact.id}).then(res => { |
|||
if(res.code==0){ |
|||
uni.showToast({ |
|||
title:'删除成功', |
|||
icon:'none' |
|||
}) |
|||
this.deleteallList1(removeval) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.user-list{ |
|||
padding:0 20rpx 20rpx; |
|||
.user-info{ |
|||
width:142rpx; |
|||
height:130rpx; |
|||
text-align:center; |
|||
.user-avatar{ |
|||
width:100rpx; |
|||
height:100rpx; |
|||
border-radius: 16rpx; |
|||
} |
|||
.user-name{ |
|||
width:100rpx; |
|||
margin:0 auto; |
|||
font-size: 22rpx; |
|||
} |
|||
.user-opt{ |
|||
border:1px dashed #999; |
|||
height:98rpx; |
|||
width:98rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
.icon{ |
|||
color:#999 !important; |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
.delete-btn{ |
|||
justify-content:center !important ; |
|||
} |
|||
|
|||
.notice-content{ |
|||
width:100%; |
|||
min-height:480rpx; |
|||
.im-textarea{ |
|||
width:100%; |
|||
min-height:480rpx; |
|||
padding:20rpx; |
|||
text-align:left; |
|||
} |
|||
} |
|||
.notice-line{ |
|||
width:70%; |
|||
text-align: right; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,225 @@ |
|||
<template> |
|||
<cu-custom :isBack="true" bgColor="bg-white"> |
|||
<template #content>表情管理({{emojiList.length}})</template> |
|||
<template #right> |
|||
<view class="ml-10 mr-10" @tap="manage()"> |
|||
{{manageName}} |
|||
</view> |
|||
</template> |
|||
</cu-custom> |
|||
<view> |
|||
<scroll-view scroll-y class="bg-white" style="padding-bottom:100rpx"> |
|||
<uni-grid :column="5" :highlight="true" @change="change" style="padding:10rpx"> |
|||
<uni-grid-item> |
|||
<view class="grid-item-box" :index="0"> |
|||
<view class="upload-emoji" @tap="uploadEmoji"><text class="cuIcon-add c-999" style="vertical-align: sub;"></text></view> |
|||
</view> |
|||
</uni-grid-item> |
|||
<uni-grid-item v-for="(item, index) in emojiList" :index="index+1" :key="index"> |
|||
<view class="grid-item-box"> |
|||
<image :src="item.src" style="width:100rpx;height:100rpx" :fade-show="false" mode="aspectFit" lazy-load></image> |
|||
<view class="emoji-check-box" v-if="isManage" :class="item.isCheck ? 'text-green cuIcon-roundcheckfill' : 'cuIcon-round'"></view> |
|||
</view> |
|||
</uni-grid-item> |
|||
</uni-grid> |
|||
</scroll-view> |
|||
<view class="btn-opt bg-white im-flex im-align-items-center im-space-between pd-10" v-if="checkList.length>0"> |
|||
<button class="cu-btn bg-green" @tap="moveEmoji()">移动到最前面</button> |
|||
<button class="cu-btn bg-red" @tap="delEmoji()">删除({{checkList.length}})</button> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
emojiList:[], |
|||
isManage:false, |
|||
manageName:'整理', |
|||
checkList:[] |
|||
} |
|||
}, |
|||
onLoad(option){ |
|||
this.url=decodeURI(option.src); |
|||
this.name=option.name; |
|||
}, |
|||
mounted(){ |
|||
this.getEmojiList(); |
|||
}, |
|||
methods: { |
|||
getEmojiList(){ |
|||
this.$api.emojiApi.emojiList({}).then((res)=>{ |
|||
if(res.code==0){ |
|||
this.emojiList=res.data; |
|||
} |
|||
}) |
|||
}, |
|||
// 上传表情 |
|||
uploadEmoji(){ |
|||
uni.chooseImage({ |
|||
count : 9, |
|||
sizeType : ['compressed'], |
|||
sourceType : ['album', 'camera'], |
|||
success : (res)=>{ |
|||
const tempFiles = res.tempFiles; |
|||
tempFiles.forEach((item) => { |
|||
if(item.size>2*1024*1024){ |
|||
return uni.showToast({ |
|||
title: '表情大小不能超过2MB', |
|||
icon:'error' |
|||
}) |
|||
} |
|||
uni.uploadFile({ |
|||
url: this.$api.emojiApi.uploadEmoji, |
|||
filePath: item.path, |
|||
name: 'file', |
|||
header: { |
|||
'Authorization': uni.getStorageSync('authToken'), |
|||
}, |
|||
success: (e) => { |
|||
let res=JSON.parse(e.data); |
|||
if(res.code==0){ |
|||
this.updateEmoji(); |
|||
} |
|||
}, |
|||
fail: (res) => { |
|||
} |
|||
}) |
|||
}) |
|||
} |
|||
}); |
|||
}, |
|||
manage(){ |
|||
this.isManage =!this.isManage; |
|||
this.manageName = this.isManage ? '取消' : '整理'; |
|||
if(!this.isManage){ |
|||
this.checkList=[]; |
|||
this.emojiList.forEach((item,index)=>{ |
|||
this.emojiList[index].isCheck=false; |
|||
}) |
|||
} |
|||
}, |
|||
change(e) { |
|||
let { index } = e.detail; |
|||
if(!this.isManage || index==0) return; |
|||
let isCheck=this.emojiList[index-1].isCheck; |
|||
let id=this.emojiList[index-1].id; |
|||
if(!isCheck){ |
|||
this.checkList.push(id); |
|||
}else{ |
|||
this.checkList = this.checkList.filter(item => item !== id); |
|||
} |
|||
this.emojiList[index-1].isCheck=!isCheck; |
|||
}, |
|||
// 移动表情 |
|||
moveEmoji(){ |
|||
if(this.checkList.length<=0){ |
|||
return; |
|||
} |
|||
this.$api.emojiApi.moveEmoji({ids:this.checkList}).then((res)=>{ |
|||
this.updateEmoji() |
|||
}) |
|||
}, |
|||
// 删除表情 |
|||
delEmoji(){ |
|||
if(this.checkList.length<=0){ |
|||
return; |
|||
} |
|||
uni.showModal({ |
|||
title: '确定要删除选中的表情吗?', |
|||
success: (res)=>{ |
|||
if (res.confirm) { |
|||
this.$api.emojiApi.delEmoji({ids:this.checkList}).then((res)=>{ |
|||
this.updateEmoji() |
|||
}) |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
}, |
|||
updateEmoji(){ |
|||
this.getEmojiList(); |
|||
this.checkList=[]; |
|||
uni.$emit('updateEmoji',true) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
page{ |
|||
background-color: #fff; |
|||
} |
|||
.upload-emoji{ |
|||
height:70rpx; |
|||
font-size:50rpx; |
|||
text-align: center; |
|||
} |
|||
.grid-dynamic-box { |
|||
margin-bottom: 15px; |
|||
} |
|||
|
|||
.btn-opt{ |
|||
position: fixed; |
|||
bottom: 0; |
|||
width:100%; |
|||
height:100rpx; |
|||
border-top: solid 1px #D2D2D2; |
|||
z-index:1000; |
|||
} |
|||
|
|||
.grid-item-box { |
|||
flex: 1; |
|||
// position: relative; |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.grid-item-box-row { |
|||
flex: 1; |
|||
// position: relative; |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 15px 0; |
|||
} |
|||
|
|||
.grid-dot { |
|||
position: absolute; |
|||
top: 5px; |
|||
right: 15px; |
|||
} |
|||
|
|||
.swiper { |
|||
height: 420px; |
|||
} |
|||
|
|||
.emoji-check-box{ |
|||
position:absolute; |
|||
right:5rpx; |
|||
top:5rpx; |
|||
} |
|||
|
|||
/* #ifdef H5 */ |
|||
@media screen and (min-width: 768px) and (max-width: 1425px) { |
|||
.swiper { |
|||
height: 630px; |
|||
} |
|||
} |
|||
|
|||
@media screen and (min-width: 1425px) { |
|||
.swiper { |
|||
height: 830px; |
|||
} |
|||
} |
|||
|
|||
/* #endif */ |
|||
</style> |
|||
@ -0,0 +1,349 @@ |
|||
<template> |
|||
<view> |
|||
<cu-custom bgColor="bg-main-bar" :isBack="true"> |
|||
<template #backText></template> |
|||
<template #content>群成员</template> |
|||
</cu-custom> |
|||
<view class="cu-bar bg-white search fixed" :style="[{top:CustomBar + 'px'}]"> |
|||
<view class="search-form round"> |
|||
<text class="cuIcon-search"></text> |
|||
<input type="text" v-model="keywords" placeholder="输入搜索的关键词" confirm-type="search"/> |
|||
</view> |
|||
</view> |
|||
<view style="margin-top: 104rpx;"> |
|||
<view class="cu-list menu-avatar"> |
|||
<view class="cu-item" v-for="(item,index) in userList" :key="index"> |
|||
<block v-for="(iteme,indexs) in imglist" :key="indexs" v-if="network_log == 'none'"> |
|||
<view class="cu-avatar round lg" v-if="item.userInfo.imgname === iteme.name" :style="'background-image:url('+iteme.path+');'" @tap="openChatDetail(item)"></view> |
|||
</block> |
|||
<view v-else class="cu-avatar round lg" :style="'background-image:url('+item.userInfo.avatar+');'" @tap="openChatDetail(item)"></view> |
|||
|
|||
<view class="content"> |
|||
<view class="text-grey"> |
|||
<view class="text-cut">{{item.userInfo.displayName}}</view> |
|||
<view v-if="item.role<3" class="cu-tag round sm" :class="item.role==1 ? 'bg-red' : 'bg-orange'">{{item.role ==1?"群主":item.role==2?'管理员':''}}</view> |
|||
<view v-if="item.user_id==userInfo.user_id" class="cu-tag round sm">我</view> |
|||
|
|||
</view> |
|||
<view v-if="noSpeakExp(item.no_speak_time)" class="text-red text-xs">禁言至:{{noSpeakExp(item.no_speak_time)}}</view> |
|||
</view> |
|||
<view class="action" @tap="openModel(item)"> |
|||
<view class="text-grey text-sm"> <text class="cuIcon-more f-24" v-if="item.role>1 && isAuth" ></text></view> |
|||
</view> |
|||
</view> |
|||
<Empty v-if="!userList.length" noDatatext="未搜索到数据" textcolor="#999" ></Empty> |
|||
</view> |
|||
</view> |
|||
<view class="cu-modal bottom-modal" :class="modelName=='userOpt'?'show':''"> |
|||
<view class="cu-dialog"> |
|||
<view class="manage-content"> |
|||
<view class="cu-list menu bg-white"> |
|||
<view class="cu-item" v-if="curUser"> |
|||
<view class="content im-flex im-justify-content-center im-align-items-center"> |
|||
<view class="cu-avatar round sm" :style="'background-image:url('+(curUser.avatar)+');'"></view> |
|||
<view class="text-cut ml-5">{{curUser.realname}}</view> |
|||
<view v-if="curUser.role==2" class="cu-tag round sm bg-orange">管理员</view> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @tap="changeOwner()" v-if="isAdmin"> |
|||
<view class="content padding-tb-sm"> |
|||
<!-- <text class="c-orange">转让管理权限</text> --> |
|||
<text class="c-orange">转让群主</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @tap="setManage()" v-if="isAdmin"> |
|||
<view class="content padding-tb-sm"> |
|||
<text>{{curUser.role==2 ? '取消管理员' : '设为管理员'}}</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @tap="showNoSpeak()" v-if="isAdmin"> |
|||
<view class="content padding-tb-sm"> |
|||
<text>设置禁言</text> |
|||
</view> |
|||
</view> |
|||
<view class="cu-item" @tap="removeUser()"> |
|||
<view class="content padding-tb-sm"> |
|||
<text>移出群聊</text> |
|||
</view> |
|||
</view> |
|||
<view class="parting-line-5"></view> |
|||
<view class="cu-item" @tap="modelName=''"> |
|||
<view class="content padding-tb-sm"> |
|||
<text class="c-red">取消</text> |
|||
</view> |
|||
</view> |
|||
|
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<uni-popup ref="nospeak" type="share" safeArea backgroundColor="#fff"> |
|||
<uni-list> |
|||
<uni-list-item clickable v-for="(item,index) in noSpeakList" :key="index" :title="item.name" @click="setTime(item.id)" > |
|||
<template v-slot:footer> |
|||
<text class="cuIcon-check c-green" v-if="noSpeakTimer==item.id"></text> |
|||
</template> |
|||
</uni-list-item> |
|||
<uni-list-item title="自定义" clickable @click="setTime(0)" > |
|||
<template v-slot:footer> |
|||
<uni-number-box :min="1" :max="365" :value="noSpeakDay" /> 天 |
|||
</template> |
|||
</uni-list-item> |
|||
</uni-list> |
|||
<view class="padding flex flex-direction mt-10"> |
|||
<button class="cu-btn bg-green lg" @tap="setNoSpeak()">确定</button> |
|||
</view> |
|||
</uni-popup> |
|||
</view> |
|||
</template> |
|||
<script> |
|||
import { useMsgStore } from '@/store/message'; |
|||
import pinia from '@/store/index' |
|||
const msgStore = useMsgStore(pinia) |
|||
import { useloginStore } from '@/store/login'; |
|||
const userStore = useloginStore(pinia) |
|||
// #ifdef APP-PLUS |
|||
import {getSavedImages1} from '@/utils/LocalFileSystemURL.js' |
|||
import groupUserList from '@/service/groupUserList'; |
|||
// #endif |
|||
import { storeToRefs } from 'pinia'; |
|||
const {network_log} = storeToRefs(msgStore); |
|||
export default { |
|||
data() { |
|||
return { |
|||
keywords:'', |
|||
group_id: '', |
|||
group_id1: null, |
|||
modelName:'', |
|||
userList: [], |
|||
allUser:[], |
|||
isAdmin:false, |
|||
isManage:false, |
|||
isAuth:false, |
|||
noSpeakTimer:0, |
|||
noSpeakDay:1, |
|||
userInfo:userStore.userInfo, |
|||
curUser:{}, //选中的用户 |
|||
noSpeakList:[{ |
|||
name:'10分钟', |
|||
id:1 |
|||
},{ |
|||
name:'1小时', |
|||
id:2 |
|||
},{ |
|||
name:'3小时', |
|||
id:3 |
|||
},{ |
|||
name:'1天', |
|||
id:4 |
|||
}], |
|||
imglist:[], |
|||
network_log:network_log |
|||
} |
|||
}, |
|||
watch:{ |
|||
keywords(val){ |
|||
if(val==''){ |
|||
this.userList=this.allUser; |
|||
}else{ |
|||
this.search(); |
|||
} |
|||
}, |
|||
}, |
|||
onLoad(options) { |
|||
this.group_id = options.group_id?options.group_id:'' |
|||
this.group_id1 = options.group_id1?options.group_id1:null |
|||
const _this = this |
|||
if(this.network_log == 'none'){ |
|||
_this.Getchatinformation() |
|||
_this.getImagePath() |
|||
}else{ |
|||
_this.getGroupuserlist() |
|||
// _this.Getchatinformation() |
|||
// _this.getImagePath() |
|||
} |
|||
}, |
|||
methods: { |
|||
getGroupuserlist() { |
|||
this.userList = [] |
|||
this.$api.msgApi.groupUserList({ |
|||
group_id: this.group_id |
|||
}).then(res => { |
|||
const admin=res.data.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id) |
|||
if(admin.length) this.isAdmin=true; |
|||
const manage=res.data.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id) |
|||
if(manage.length) this.manage=true; |
|||
if(admin.length || manage.length) this.isAuth=true; |
|||
const allUser=res.data; |
|||
allUser.forEach((item)=>{ |
|||
item.realname=item.userInfo.displayName; |
|||
item.name_py=item.userInfo.name_py; |
|||
}) |
|||
this.allUser=allUser; |
|||
this.userList = res.data; |
|||
}) |
|||
}, |
|||
|
|||
async Getchatinformation(){ |
|||
// #ifdef APP-PLUS |
|||
const groups = await groupUserList.getList({group_id: this.group_id1}); |
|||
groups.map((res)=>{ |
|||
res.userInfo = JSON.parse(res.userInfo) |
|||
}) |
|||
const admin=groups.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id) |
|||
if(admin.length) this.isAdmin=true; |
|||
const manage=groups.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id) |
|||
if(manage.length) this.manage=true; |
|||
if(admin.length || manage.length) this.isAuth=true; |
|||
const allUser=groups; |
|||
allUser.forEach((item)=>{ |
|||
item.realname=item.userInfo.displayName; |
|||
item.name_py=item.userInfo.name_py; |
|||
}) |
|||
this.allUser=allUser; |
|||
this.userList = groups; |
|||
|
|||
console.info('获取群成员数据',groups.length,groups); |
|||
// #endif |
|||
}, |
|||
// 获取图片地址 |
|||
async getImagePath(){ |
|||
this.imglist = await getSavedImages1() |
|||
this.imglist.map(item => { |
|||
item.path = plus.io.convertLocalFileSystemURL(item.path) |
|||
}); |
|||
console.info('读取地址',this.imglist); |
|||
}, |
|||
|
|||
openModel(item){ |
|||
item.realname=item.userInfo.displayName; |
|||
item.avatar=item.userInfo.avatar; |
|||
this.curUser=item; |
|||
this.modelName='userOpt'; |
|||
}, |
|||
// 设置取消管理员 |
|||
setManage(){ |
|||
const role=this.curUser.role==2 ? 3 : 2; |
|||
this.$api.msgApi.setManager({ |
|||
id:this.group_id, |
|||
user_id:this.curUser.user_id, |
|||
role:role |
|||
}).then((res)=>{ |
|||
if(res.code==0){ |
|||
this.getGroupuserlist(); |
|||
} |
|||
this.modelName=''; |
|||
}) |
|||
}, |
|||
// 移出成员 |
|||
removeUser(){ |
|||
this.modelName=''; |
|||
uni.showModal({ |
|||
title: '确定要删除该成员吗?', |
|||
success: e => { |
|||
if (e.confirm) { |
|||
this.$api.msgApi.removeUser({ |
|||
id:this.group_id, |
|||
user_id:this.curUser.user_id, |
|||
}).then((res)=>{ |
|||
if(res.code==0){ |
|||
this.getGroupuserlist(); |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
changeOwner(){ |
|||
this.modelName=''; |
|||
uni.showModal({ |
|||
// title: '确定将管理权限转移给该成员吗?', |
|||
title: '确定将群主管理权限转移给该成员吗?', |
|||
success: e => { |
|||
if (e.confirm) { |
|||
this.$api.msgApi.changeOwner({ |
|||
id:this.group_id, |
|||
user_id:this.curUser.user_id, |
|||
}).then((res)=>{ |
|||
if(res.code==0){ |
|||
uni.reLaunch({ |
|||
url:'/pages/index/index' |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
search(){ |
|||
const allUser=JSON.parse(JSON.stringify(this.allUser)); |
|||
this.userList=this.$util.searchObject(allUser,['realname','name_py'],this.keywords); |
|||
}, |
|||
openChatDetail(item){ |
|||
if(this.userInfo.user_id==item.user_id) return; |
|||
let friend=msgStore.getContact(item.user_id); |
|||
let curContact=msgStore.getContact(this.group_id); |
|||
if(curContact.role<3 || curContact.setting.profile=='1' || friend){ |
|||
uni.navigateTo({ |
|||
url:"/pages/contacts/detail?id="+item.user_id |
|||
}) |
|||
}else{ |
|||
uni.showToast({ |
|||
title:'已开启用户隐私!', |
|||
icon:'none' |
|||
}) |
|||
return false; |
|||
} |
|||
}, |
|||
showNoSpeak(){ |
|||
this.modelName='' |
|||
this.$refs.nospeak.open() |
|||
}, |
|||
setTime(val){ |
|||
console.log(val); |
|||
this.noSpeakTimer=val; |
|||
}, |
|||
setNoSpeak(){ |
|||
this.$refs.nospeak.close(); |
|||
this.$api.msgApi.setNoSpeak({ |
|||
id:this.group_id, |
|||
user_id:this.curUser.user_id, |
|||
noSpeakDay:this.noSpeakDay, |
|||
noSpeakTimer:this.noSpeakTimer |
|||
}).then((res)=>{ |
|||
this.noSpeakTimer=0; |
|||
this.noSpeakDay=1; |
|||
if(res.code==0){ |
|||
this.getGroupuserlist(); |
|||
} |
|||
}) |
|||
}, |
|||
noSpeakExp(time){ |
|||
if(time * 1000>new Date().getTime()){ |
|||
return this.$util.date('m-d H:i',time); |
|||
}else{ |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
<style scoped lang="scss"> |
|||
.list-image { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
font-size: 0; |
|||
} |
|||
.share { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
flex-direction: column; |
|||
} |
|||
.uni-popup{ |
|||
z-index:9999 !important; |
|||
} |
|||
::v-deep uni-modal{ |
|||
z-index: 1025; |
|||
} |
|||
</style> |
|||