Browse Source

项目初始化

master
liaoxinyu 1 year ago
commit
d1031a4da2
  1. 20
      .gitignore
  2. 33
      .hbuilderx/launch.json
  3. 51
      .vite/_cert.pem
  4. 1
      .vite/deps/package.json
  5. 579
      App.vue
  6. 201
      LICENSE
  7. 62
      README.md
  8. 38
      androidPrivacy.json
  9. 45
      api/emoji.js
  10. 48
      api/friend.js
  11. 13
      api/index.js
  12. 65
      api/login.js
  13. 291
      api/message.js
  14. 865
      common/appUpdate.js
  15. 64
      common/config.js
  16. 72
      common/scan.js
  17. 193
      common/socket.js
  18. BIN
      components/.DS_Store
  19. 53
      components/Empty.vue
  20. 75
      components/Tags.vue
  21. 63
      components/breadcrum.vue
  22. 79
      components/cu-custom.vue
  23. 433
      components/get-qrcode.vue
  24. 130
      components/im-touch.vue
  25. 46
      components/im-user.vue
  26. 33
      components/message/im-image.vue
  27. 957
      components/message/im-input.vue
  28. 35
      components/message/im-item.vue
  29. 50
      components/message/im-tab.vue
  30. 292
      components/message/user-select.vue
  31. 455
      components/mosowe-canvas-image/mosowe-canvas-image.vue
  32. 195
      components/mosowe-canvas-image/readme.md
  33. 1623
      components/mosowe-canvas-image/wxqrcode.js
  34. 92
      components/status.vue
  35. 1371
      components/yq-avatar/yq-avatar.vue
  36. BIN
      favicon.ico
  37. BIN
      hybrid/html/image/guaduan.png
  38. BIN
      hybrid/html/image/jieting.png
  39. BIN
      hybrid/html/image/speaker-off.png
  40. BIN
      hybrid/html/image/speaker.png
  41. BIN
      hybrid/html/image/video.png
  42. BIN
      hybrid/html/image/voice-off.png
  43. BIN
      hybrid/html/image/voice.png
  44. BIN
      hybrid/html/image/wallpaper.png
  45. 613
      hybrid/html/index.html
  46. 275
      hybrid/html/js/jsonly.js
  47. 1
      hybrid/html/js/uni.webview.js
  48. 11
      hybrid/html/js/utils.js
  49. 1
      hybrid/html/js/vue.js
  50. 3480
      hybrid/html/rtc/adapter-latest.js
  51. 702
      hybrid/html/rtc/chrome/chrome_shim.js
  52. 50
      hybrid/html/rtc/chrome/getdisplaymedia.js
  53. 188
      hybrid/html/rtc/chrome/getusermedia.js
  54. 433
      hybrid/html/rtc/common_shim.js
  55. 296
      hybrid/html/rtc/firefox/firefox_shim.js
  56. 36
      hybrid/html/rtc/firefox/getdisplaymedia.js
  57. 67
      hybrid/html/rtc/firefox/getusermedia.js
  58. 353
      hybrid/html/rtc/safari/safari_shim.js
  59. 263
      hybrid/html/rtc/utils.js
  60. BIN
      hybrid/html/voice/calling.mp3
  61. 20
      index.html
  62. 54
      main.js
  63. 192
      manifest.json
  64. 118
      mixins/chat.js
  65. BIN
      nativeplugins/lemonjk-FileSelect/android/uniplugin_fileSelect-release.aar
  66. BIN
      nativeplugins/lemonjk-FileSelect/ios/DCTestUniPlugin.framework/DCTestUniPlugin
  67. BIN
      nativeplugins/lemonjk-FileSelect/ios/DCTestUniPlugin.framework/Info.plist
  68. 43
      nativeplugins/lemonjk-FileSelect/package.json
  69. 722
      package-lock.json
  70. 24
      package.json
  71. 199
      pages.json
  72. 79
      pages/compass/index.vue
  73. 140
      pages/compass/moments.vue
  74. 247
      pages/contacts/detail.vue
  75. 150
      pages/contacts/friend.vue
  76. 89
      pages/contacts/group.vue
  77. 410
      pages/contacts/index.vue
  78. 88
      pages/contacts/search.vue
  79. 254
      pages/index/index.vue
  80. 220
      pages/index/qrcode.vue
  81. 34
      pages/index/scan.vue
  82. 112
      pages/index/search.vue
  83. 337
      pages/index/userSelection.vue
  84. 198
      pages/login/index.vue
  85. 182
      pages/login/register.vue
  86. 262
      pages/message/call.vue
  87. 1347
      pages/message/chat.vue
  88. 638
      pages/message/detail.vue
  89. 225
      pages/message/emoji.vue
  90. 291
      pages/message/group/groupUser.vue
  91. 88
      pages/message/group/info.vue
  92. 349
      pages/message/index.vue
  93. 517
      pages/message/record.vue
  94. 79
      pages/message/video.vue
  95. 110
      pages/mine/about.vue
  96. 114
      pages/mine/doc.vue
  97. 168
      pages/mine/index.vue
  98. 184
      pages/mine/profile.vue
  99. 238
      pages/mine/secure.vue
  100. 92
      pages/mine/setting.vue

20
.gitignore

@ -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.

33
.hbuilderx/launch.json

@ -0,0 +1,33 @@
{
// launch.json configurations app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtypelocalremote, localremote
"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" : "custom",
"type" : "uni-app:app-android"
},
{
"openVueDevtools" : false,
"type" : "uni-app:h5"
}
]
}

51
.vite/_cert.pem

@ -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-----

1
.vite/deps/package.json

@ -0,0 +1 @@
{"type":"module"}

579
App.vue

@ -0,0 +1,579 @@
<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";
// #ifdef H5
import VConsole from 'vconsole';
// #endif
let vConsole = null; //H5
const msgStore = useMsgStore(pinia)
const userStore = useloginStore(pinia);
let keepAlive = null
// #ifdef APP-PLUS
import appUpdate from '@/common/appUpdate.js';
// 使ios
keepAlive = uni.requireNativePlugin('Ba-KeepAlive');
// #endif
export default {
onLaunch: function() {
// APP
let setting=uni.getStorageSync('appSetting') ?? '';
if(!setting){
userStore.setAppSetting({
voiceStatus:true,
vibrateStatus:false,
circleAvatar:false
});
}else{
userStore.setAppSetting(setting);
}
//
userStore.getGlobalConfig();
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;
console.log('App Show')
},
methods:{
//
loadVConsole() { //vConsoleH5
if (config.isVConsole) { //
let systemInfo = uni.getSystemInfoSync();
if (!(systemInfo.uniPlatform == 'app' || systemInfo.uniPlatform == 'web')) { // appH5
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(); //
},
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){
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;
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();
}
}
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 });
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":
if(data.is_last){
msgStore.updateContacts({
id: data.toContactId,
lastContent: data.content
});
}
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 '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);
})
},
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'].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('手机震动');
}
});
}
}
},
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>

201
LICENSE

@ -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.

62
README.md

@ -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

38
androidPrivacy.json

@ -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"
}
}
}

45
api/emoji.js

@ -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;

48
api/friend.js

@ -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;

13
api/index.js

@ -0,0 +1,13 @@
// 引入其他模块的接口
import msgApi from '@/api/message.js'; // 消息
import LoginApi from '@/api/login.js'; //登录相关
import friendApi from '@/api/friend.js'; //登录相关
import emojiApi from '@/api/emoji.js'; //登录相关
// 导出接口
export default {
msgApi,
LoginApi,
friendApi,
emojiApi
}

65
api/login.js

@ -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;

291
api/message.js

@ -0,0 +1,291 @@
// 统一请求路径前缀在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.initContacts = (params) => {
return postJsonRequest('/enterprise/im/getContacts', 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.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)
}
/**
* 发送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)
}
export default msgApi;

865
common/appUpdate.js

@ -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"
});
}
})
});
}

64
common/config.js

@ -0,0 +1,64 @@
import CryptoJS from "crypto-js";
// 如果为开启ssl证书,请修改为http协议
let scheme ="https"; //协议头
// 请将下面的域名替换成自己的服务器域名,如果有端口要把端口加上
let host = 'im.raingad.com';
// 为了避免h5页面被其他人盗用,可以自由选择加密方式,详细教程查看readme.md文件
let hostToken="shab0725962100bcedc1d0019ff780f2435bi";
// 是否开启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
}

72
common/scan.js

@ -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
}

193
common/socket.js

@ -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

BIN
components/.DS_Store

Binary file not shown.

53
components/Empty.vue

@ -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>

75
components/Tags.vue

@ -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>

63
components/breadcrum.vue

@ -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>

79
components/cu-custom.vue

@ -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>

433
components/get-qrcode.vue

@ -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>

130
components/im-touch.vue

@ -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>

46
components/im-user.vue

@ -0,0 +1,46 @@
<template>
<view class="cu-avatar lg" :class="appSetting.circleAvatar?'round':'radius'" @tap="openUserInfo(info)" :style="[{backgroundImage:'url('+ info.avatar +')'}]"></view>
</template>
<script>
const userInfo=uni.getStorageSync('userInfo');
const appSetting=uni.getStorageSync('appSetting');
import { useMsgStore } from '@/store/message';
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
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
}
},
methods:{
//
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
})
}
}
}
</script>
<style scoped></style>

33
components/message/im-image.vue

@ -0,0 +1,33 @@
<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:''},
},
emits:['showImgs'],
data() {
return {
}
},
created : function(){
},
methods:{
showImgs(e){
this.$emit('showImgs',e);
},
}
}
</script>

957
components/message/im-input.vue

@ -0,0 +1,957 @@
<template name="im-input">
<view id="more-oprate">
<view class="im-footer bg-gray" :style="[{paddingBottom:paddingB+'px',bottom:InputBottom+'px',minHeight:footerHeight + 'px'}]">
<view class="im-menus f-28" v-show="!isFocus || contact.is_group!=1" style="margin-bottom: 8rpx;" :class="[recShow ? 'cuIcon-keyboard' : 'cuIcon-sound']" hover-class="tap" @tap="showRec"></view>
<view class="im-menus f-24" v-show="isFocus && contact.is_group==1" style="margin-bottom: 13rpx;" @tap="modelName='userModel'">@</view>
<view class="im-flex1 im-msgarea">
<editor id="editor" class="solid-bottom bg-white im-input c-333" :adjust-position="false" maxlength="300" cursor-spacing="10"
@focus="InputFocus" @blur="InputBlur" @input="changeMsgText" @ready="onEditorReady" :read-only="readOnly" v-show="recShow==false"> </editor>
<view class="toolBox" v-show="recShow==true">
<view class="recorder" :class="{active:isUseRecorder}" @touchstart.prevent="startRecorder"
@touchend.prevent="endRecorder" @touchmove.prevent="moveRecorder" @touchcancel="cancelRecorder">
{{isUseRecorder ? '松开结束' : '按住说话'}}
</view>
</view>
<view class='im-flex im-space-between message-quote radius-6 im-align-items-center' v-if="quote">
<view class='text-overflow'>{{quote.content}}</view>
<view class="cuIcon-close" @tap="closeQuote"></view>
</view>
</view>
<mumu-recorder ref='recorderRef' @success='handlerSuccess' @error='handlerError' v-if="isH5"></mumu-recorder>
<view class="im-flex im-justify-content-start im-align-items-center" style="margin-bottom: 8rpx;" >
<view class="im-menus cuIcon-emoji f-28 ml-5" hover-class="tap" @tap="showAppBox(1)"></view>
<view class="im-menus cuIcon-roundadd f-28 mr-10" hover-class="tap" v-if="!inputMsg" @tap="showAppBox(2)"></view>
<view v-if="inputMsg">
<button class="cu-btn bg-green shadow mr-10" @touchend.prevent="sendTextMsg">发送</button>
</view>
</view>
<!-- 表情窗口 -->
<view class="im-flex im-columns im-emoji-box" :style="[{height:boxHeight+'px'}]" v-if="appBox==1">
<scroll-view scroll-x class="bg-gray nav im-emoji-header" scroll-with-animation :scroll-left="scrollLeft">
<view class="cu-item" :class="index==TabCur?'text-green':''" v-for="(item,index) in emojiList" :key="index" @tap="tabSelect" :data-id="index" :data-name="item.name">
<view :class="[item.icon]" class="f-20"></view>
</view>
</scroll-view>
<scroll-view scroll-y class="bg-white im-emoji-body">
<view class="im-flex im-wrap im-justify-content-start im-align-items-center pd-10" :class="emojiName=='favors'?' cu-list grid col-5':''">
<view v-if="emojiName=='favors'" class="im-emoji-item">
<view class="upload-emoji" @tap="uploadEmoji"><text class="cuIcon-add c-999" style="vertical-align: sub;"></text></view>
</view>
<view v-for="(item,index) in currentEmojiList" class="im-emoji-item" :key="index">
<image :src="item.src" style="width:100rpx;height:100rpx" mode="aspectFit" :fade-show="false" lazy-load @tap="chooseDiyEmoji(item)" v-if="emojiName=='favors'"></image>
<image :src="item.src" style="width:44rpx;;height:44rpx" mode="aspectFit" lazy-load @tap="chooseEmoji(item)" v-else></image>
</view>
</view>
</scroll-view>
</view>
<!-- 工具栏窗口 -->
<view class="im-flex im-app-box im-flex im-justify-content-start im-wrap im-align-content-start pd-20" :style="[{height:boxHeight+'px'}]" v-if="appBox==2">
<view class="im-flex im-columns im-align-items-center mt-10 im-app-item" @tap="chooseImg">
<view class="bg-white cuIcon-album f-24 radius-10 im-app-item-icon"></view>
<view class="mt-5">照片</view>
</view>
<view class="im-flex im-columns im-align-items-center mt-10 im-app-item" @tap="chooseVideo">
<view class="bg-white cuIcon-video f-24 radius-10 im-app-item-icon"></view>
<view class="mt-5">视频</view>
</view>
<view class="im-flex im-columns im-align-items-center mt-10 im-app-item" @tap="chooseFile">
<view class="bg-white cuIcon-file f-24 radius-10 im-app-item-icon"></view>
<view class="mt-5">文件</view>
</view>
<view class="im-flex im-columns im-align-items-center mt-10 im-app-item" v-if='!contact.is_group && (isH5 || isApp) && parseInt(globalConfig.chatInfo.webrtc)' @tap="calling(0)">
<view class="bg-white cuIcon-dianhua f-24 radius-10 im-app-item-icon"></view>
<view class="mt-5">语音通话</view>
</view>
<view class="im-flex im-columns im-align-items-center mt-10 im-app-item" v-if='!contact.is_group && (isH5 || isApp) && parseInt(globalConfig.chatInfo.webrtc)' @tap="calling(1)">
<view class="bg-white cuIcon-record f-24 radius-10 im-app-item-icon"></view>
<view class="mt-5">视频通话</view>
</view>
<view class="im-flex im-columns im-align-items-center mt-10 im-app-item" @tap="chooseLocation">
<view class="bg-white cuIcon-location f-24 radius-10 im-app-item-icon"></view>
<view class="mt-5">位置</view>
</view>
<view class="im-flex im-columns im-align-items-center mt-10 im-app-item" @tap="modelName='contactModel'">
<view class="bg-white cuIcon-addressbook f-24 radius-10 im-app-item-icon"></view>
<view class="mt-5">名片</view>
</view>
</view>
</view>
<view class="voice-model c-white radius-10 im-flex im-columns im-align-items-center pd-10" v-show="isUseRecorder">
<view class="cuIcon-voicefill mt-15 mb-5 f-28" :class="[isCancel ? 'c-red' : 'voice-icon']"></view>
<view :class="[isCancel ? 'c-red' : '']">
{{isCancel ? '松开取消' : '正在录音'}}
</view>
</view>
<view class="cu-modal bottom-modal" :class="modelName=='userModel'?'show':''" @tap="closeModel()">
<view class="cu-dialog" v-if="modelName=='userModel'">
<view class="cu-bar bg-white">
<view class="action text-gray" @tap="closeModel()">取消</view>
<view class="f-16">选择提醒的人</view>
<view class="action text-green" @tap="at()">完成</view>
</view>
<view class="manage-content" style="height:500px" @tap.stop=''>
<scroll-view style="height:500px" scroll-y="true">
<user-select v-if="contact.is_group==1" :type="4" :contact_id="contact.id" ref="userSelect" @setData="setAtList"></user-select>
</scroll-view>
</view>
</view>
</view>
<view class="cu-modal bottom-modal" :class="modelName=='contactModel'?'show':''" @tap="closeModel()">
<view class="cu-dialog" v-if="modelName=='contactModel'">
<view class="cu-bar bg-white">
<view class="action text-gray" @tap="closeModel()">取消</view>
<view class="f-16">选择分享的联系人</view>
<view class="action text-green" @tap="sendContactCard()">完成</view>
</view>
<view class="manage-content" style="height:500px" @tap.stop=''>
<scroll-view style="height:500px" scroll-y="true">
<user-select :type="1" :multiple="false" :contact_id="contact.id" ref="contactSelect"></user-select>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script>
import MumuRecorder from '@/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue'
import userSelect from '@/components/message/user-select.vue';
import Edit from '@/utils/edit.js'
import emoji from '@/utils/emoji.js'
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login';
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia);
const userStore = useloginStore(pinia);
export default {
name : "im-input",
components: { MumuRecorder,userSelect },
props : {
boxStatus:{type:Number, default:0},
contact:{type:Object, default:{}}
},
data() {
return {
editorCtx:null,
InputBottom : 0,
paddingB : 0,
footerHeight : 50,
boxHeight : 300,
uploading : false,
recShow : false,
inputMsg : "",
recorderManager : null,
recing : false,
recLength : 1,
recTimer : null,
tmpVoice : '',
isUseRecorder: false,
playItemIndex: -1,
currentAudio: '',
mainHeight:0,
isCancel:false,
isH5:false,
isApp:false,
appBox:0,
TabCur: 0,
emojiName:'',
scrollLeft: 0,
emojiList:[],
currentEmojiList:[],
isFocus:false,
globalConfig:userStore.globalConfig,
userInfo:userStore.userInfo,
readOnly:false,
edit: null,
modelName:false,
isAt:false,
quote:'',
boardHeight:0,
}
},
watch:{
boxStatus(val){
this.appBox=0;
this.InputBottom=0;
},
appBox(val){
// ,0;
if(val==0 && !this.isFocus){
this.InputBottom=0;
}
},
InputBottom(val){
this.$emit('setPad',val);
}
},
created : function(){
// #ifndef H5
//
uni.onKeyboardHeightChange(res => {
if(this.appBox==0 || res.height>0){
this.boardHeight=res.height;
}
})
// #endif
uni.$on('updateEmoji',(res) => {
this.getEmojiList()
})
this.currentEmojiList=emoji[0]['children'];
this.getEmojiList();
uni.getSystemInfo({
success: res => {
let windowHeight = res.windowHeight;
this.mainHeight=windowHeight;
}
});
// #ifdef H5
this.isH5=true;
// this.paddingB=15;
// this.footerHeight=65;
// #endif
// #ifdef APP-PLUS
this.isApp=true;
// #endif
// #ifndef H5
this.isH5=false;
this.recorderManager = uni.getRecorderManager();
this.recorderManager.onStop((res) => {
this.tmpVoice = res.tempFilePath;
this.recing = false;
if(this.recLengt<1){
// 1
return this.checkRecorder(this.recLength);
}else{
//
this.sendVoiceMsg();
}
});
this.recorderManager.onError(() => {
uni.showToast({ title: '录音失败', icon: 'none' });
this.recing = false;
});
// #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){
let paddingB = uni.upx2px(50);
this.paddingB =paddingB;
this.footerHeight=55+paddingB;
}
} catch (e){return null;}
// #endif
},
methods:{
msgItem(){
return {
id:this.$util.getUuid(),
sendTime:new Date().getTime(),
status: "going",
type: "text",
content: "",
is_read: 0,
is_group: 0,
file_id: 0,
file_cate: 0,
fileName: "",
fileSize: 0,
extends: null
}
},
// @
at(){
this.isAt=true;
let data=this.$refs.userSelect.selectUser;
this.closeModel();
this.edit.addLink({
prefix: '@',
data:data
})
setTimeout(()=>{
this.getFocus();
},100)
},
// @
sendContactCard(){
let data=this.$refs.contactSelect.selectUser;
if(data.length!=1){
return uni.showToast({
icon:'none',
title:'必须选择一位联系人'
})
}
this.closeModel();
let contact=data[0];
let message={
type:'contact',
status:'going',
content:'[个人名片] '+contact.displayName,
extends:{
avatar:contact.avatar,
displayName:contact.displayName,
id:contact.id
}
};
this.$emit('send',Object.assign(this.msgItem(), message),false);
},
//
closeModel(){
if(this.modelName=="userModel"){
this.$refs.userSelect.selectUser=[];
this.$refs.userSelect.changeUser=[];
}
this.modelName='';
},
setAtList(item){
this.isAt=true;
this.closeModel();
this.edit.addLink({
prefix: '@',
data: item
})
},
//
getFocus(){
this.editorCtx.format('fontFamily', 'inherit');
this.isFocus=true;
},
chooseDiyEmoji(item){
let message={
type:'emoji',
content:item.src,
file_id:item.file_id,
status:'going'
}
this.$emit('send',Object.assign(this.msgItem(), message),false);
},
//
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();
this.showAppBox(1)
},
});
// #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
},
tabSelect(e) {
this.TabCur = e.currentTarget.dataset.id;
this.emojiName = e.currentTarget.dataset.name;
this.scrollLeft = (e.currentTarget.dataset.id - 1) * 60;
this.currentEmojiList=this.emojiList[this.TabCur]['children'];
},
showAppBox(val){
if(this.appBox==val){
this.appBox=0;
this.InputBottom=0;
}else{
this.appBox=val;
this.InputBottom=this.boxHeight;
this.recShow = false;
}
},
//
showRec : function(){
this.InputBottom=0;
this.appBox=0;
this.recShow == true ? this.recShow = false : this.recShow = true
},
//
sendVoiceMsg: function(){
if (this.tmpVoice == '') {
uni.showToast({ title: "录制已取消", icon: "none" });
return;
}
let res={
localUrl:this.tmpVoice,
duration:this.recLength
}
this.handlerSuccess(res);
this.tmpVoice = '';
this.recLength = 0;
},
//
sendTextMsg: function () {
if(this.appBox!=1){
this.isFocus=true;
}
this.editorCtx.getContents({
success:(e)=>{
let msg=e.html;
if (msg == '<p><br></p>') {return false;}
// @
this.edit.getLink().then((e)=>{
let message={
type:'text',
content:msg,
extends:this.quote
}
// qute
if(this.quote.msg_id){
message.pid=this.quote.msg_id;
message.extends=this.quote;
}
const userList = Array.from(new Set(e));
message.at=userList;
this.inputMsg = '';
this.closeQuote();
this.editorCtx.clear();
if(this.appBox!=1){
this.getFocus();
setTimeout(()=>{
this.isFocus=true;
},10)
}
this.$emit('send',Object.assign(this.msgItem(), message),'');
});
},
fail:(e)=>{
this.inputMsg = '';
this.editorCtx.clear();
this.editorCtx.format('fontFamily', 'inherit');
console.info('错误');
}
})
},
uploadEmoji(){
uni.navigateTo({
url: '/pages/message/emoji'
})
return;
},
//
chooseImg(){
let message={
type:'image',
status:'going'
};
uni.chooseImage({
count : 9,
sizeType : ['compressed'],
sourceType : ['album', 'camera'],
success : (res)=>{
const tempFiles = res.tempFiles;
tempFiles.forEach((item) => {
message.content=item.path;
message.fileName=item.name;
message.fileSize=item.size;
this.$emit('send',Object.assign(this.msgItem(), message),item.path);
})
}
});
},
//
chooseVideo: function () {
let message={
type:'video',
status:'going'
};
uni.chooseVideo({
sourceType: ['camera', 'album'],
success: (res) => {
if(res.duration>60){
return uni.showToast({
title: '视频长度不能超过60秒',
icon:'error'
})
}
const tempFilePaths = res.tempFilePath;
let fixMode=(res.width > res.height) ? 1 : 2;
let arr={
duration:Math.ceil(res.duration),
width:res.width,
height:res.height,
fixMode:fixMode,
poster:''
};
message.fileName=res.name;
message.fileSize=res.size;
message.extends=arr;
message.content=tempFilePaths;
this.$emit('send',Object.assign(this.msgItem(), message),tempFilePaths);
}
});
},
//
chooseFile:function(){
let self=this;
// #ifdef H5
uni.chooseFile({
count: 5, //100
success: function (res) {
self.appendFile(res);
}
});
// #endif
// #ifdef MP
wx.chooseMessageFile({
count: 5, //100
success: function (res) {
self.appendFile(res);
}
});
// #endif
// #ifdef APP-PLUS
const lemonjkFileSelect = uni.requireNativePlugin('lemonjk-FileSelect');
lemonjkFileSelect.showPicker({
pathScope: "/Download", // showPicker
mimeType: "*/*",
utisType:"public.data",
multi:'yes',
}, result => {
// ,
if(result.code==1001){
uni.showModal({
title:"需要文件访问权限",
content:"您还未授权本应用读取文件。为保证您可以正常上传文件,请在权限设置页面打开文件访问权限(不同手机厂商表述可能略有差异)请根据自己手机品牌设置",
confirmText:"去授权",
cancelText:"算了",
success(e) {
if(e.confirm){
//
lemonjkFileSelect.gotoSetting();
}
}
})
}
let type='file';
let imageExts=['jpg','jpeg','png','bmp','gif'];
let videoExts=['mp4','3gp','avi','m2v','mkv','mov'];
result.files.forEach((item)=>{
if(imageExts.includes(item.fileExtension)){
type='image';
}else if(videoExts.includes(item.fileExtension)){
type='video';
}else{
type='file';
}
let filePath='file://'+item.filePath;
const message={
type:type,
status:'going',
fileName:item.FileName,
fileSize:item.fileSize,
content:filePath
};
this.$emit('send',Object.assign(this.msgItem(), message),filePath);
})
})
// #endif
},
//
appendFile(res){
const tempFiles=res.tempFiles;
tempFiles.forEach((item) => {
let path=item.path;
// #ifdef APP-PLUS
path='file://'+ item.path;
// #endif
let message={
type:'file',
status:'going',
fileName:item.name,
fileSize:item.size,
content:path
};
// #ifdef H5
let type=item.type;
if(type.indexOf("image/")!=-1){
message.type="image";
}
if(type.indexOf("video/")!=-1){
message.type="video";
}
// #endif
this.$emit('send',Object.assign(this.msgItem(), message),path);
})
},
//
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,
extends:res
};
this.$emit('send',Object.assign(this.msgItem(), message),false);
}
})
},
calling(is_video){
// #ifdef MP
return uni.showToast({
title:'小程序暂不支持',
icon:'none'
})
// #endif
if(!parseInt(this.globalConfig.chatInfo.webrtc)){
return uni.showToast({
title:'未开启音视频通话',
icon:'none'
})
}
if(!parseInt(this.globalConfig.chatInfo.simpleChat)){
return uni.showToast({
title:'系统已关闭私聊',
icon:'none'
})
}
if(msgStore.webrtcLock){
return uni.showToast({
title:'其他终端正在通话中',
icon:'none'
})
}
let msg_id=this.$util.getUuid();
uni.navigateTo({
url: '/pages/message/call?msg_id='+msg_id+'&type='+is_video+'&status=1&id='+this.contact.id+'&name='+this.contact.displayName+'&avatar='+encodeURI(this.contact.avatar)
})
},
changeMsgText(e){
// @@
if(this.isAt){
this.edit.eventLink(e.detail);
if(this.edit.isSetContents){
setTimeout(()=>{
this.getFocus()
},200);
}
}
const txt=e.detail.text.replace(/\n/g, '');
if(txt=='' && e.detail.html=='<p><br></p>'){
this.inputMsg='';
}else{
this.inputMsg=e.detail.html;
}
},
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
},
InputFocus(e) {
this.isFocus=true;
if(this.appBox>0){
//
this.appBox=0;
}
this.InputBottom = 0;
},
getInputTop(){
const query=this.$util.getQuery(this);
setTimeout(()=>{
query.select('#editor').boundingClientRect();
query.exec(data =>{
if(this.boardHeight>0){
//editordtop,editorInputBottom
if((this.mainHeight-this.boardHeight)>data[0].top){
this.InputBottom = 0;
}else{
this.InputBottom =this.boardHeight;
}
}
})
},10)
},
InputBlur(e) {
if(!this.appBox && !this.isFocus){
this.InputBottom = 0;
}
setTimeout(()=>{
this.isFocus=false;
},10)
},
//
startRecorder() {
console.log('录音开始...')
// #ifdef H5
this.$refs.recorderRef.start()
// #endif
// #ifndef H5
this.recorderManager.start({duration:60000, format:'mp3' });
this.recLength = 0;
this.recTimer = setInterval(()=>{this.recLength++;}, 1000);
// #endif
this.isUseRecorder = true
},
//
endRecorder() {
console.log('录音结束')
// #ifdef H5
this.$refs.recorderRef.stop()
// #endif
// #ifndef H5
this.recorderManager.stop();
clearInterval(this.recTimer);
// #endif
this.isUseRecorder = false
},
//
cancelRecorder(){
this.endRecorder();
this.isCancel=true;
},
//
moveRecorder(e){
let touch=e.touches[0];
if(touch.clientY<this.mainHeight-80){
this.isCancel=true;
}else{
this.isCancel=false;
}
},
handlerSuccess(res) {
this.checkRecorder(res.duration);
if(this.isCancel){
this.isCancel=false;
return console.log('录音已取消');
}
let message={
type:'voice',
content:res.localUrl,
fileName:this.$util.getUuid()+'.mp3',
extends:{
duration: res.duration
}
}
this.$emit('send',Object.assign(this.msgItem(), message));
},
//
checkRecorder(duration){
if(duration<1 || isNaN(duration) || !duration){
this.recLength = 0;
this.tmpVoice = '';
this.recing = false;
this.isCancel=true;
return uni.showToast({
title: '录音时间太短',
icon: 'error'
})
}
},
handlerError(code) {
switch (code) {
case '201':
uni.showModal({
content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。'
})
break
default:
console.log('录音功能受限,请知晓!')
break
}
},
closeQuote(){
this.quote='';
},
//
quoteMessage(quote){
this.quote=quote;
// .@
if(this.contact.is_group==1 && quote.user_id!=this.userInfo.user_id){
this.setAtList({
user_id:quote.user_id,
realname:quote.realname,
})
}
},
getEmojiList(){
this.$api.emojiApi.emojiList({}).then((res)=>{
if(res.code==0){
emoji[1]['children']=res.data;
if(this.TabCur==1){
this.currentEmojiList=res.data;
}
}
this.emojiList=emoji;
})
}
}
}
</script>
<style lang="scss" scoped>
.im-footer{padding:0; width:100%; position:fixed; left:0; bottom:0;min-height:100rpx;display:flex; flex-wrap:nowrap; overflow:hidden; box-shadow:1px 1px 6px #999999; align-items:flex-end;z-index:101}
.im-footer .items{width:auto; line-height:88rpx; flex-shrink:0; font-size:28rpx; color:#2B2E3D;}
.im-menus{width:80rpx; height:80rpx; flex-shrink:0; line-height:80rpx; text-align:center;}
.im-input{padding:14rpx 14rpx; border-radius:10rpx;margin:0 8rpx !important;height:100%;min-height:44rpx;max-height: 300rpx;font-size: 28rpx;word-break: break-all;}
.im-msgarea{padding:12rpx 10rpx 12rpx 0;}
.im-record{width:100%; position:fixed; left:0; bottom:0; background:#FFFFFF; padding:30px 0; padding-bottom:100rpx; z-index:11; box-shadow:1px 1px 6px #999999;}
.im-record-close{width:100rpx; height:100rpx; position:absolute; top:0px; right:0px; z-index:100; text-align:center; line-height:100rpx; color:#888888; font-size:38rpx !important;}
.im-record-txt{text-align:center; font-size:26rpx; line-height:30px; padding-bottom:10px; color:#CCCCCC;}
.im-record-btn{width:60px; height:60px; margin:0 auto; border:5px solid #F1F2F3; border-radius:100%; background:#00B26A;}
.im-recording{background:#FF0000; animation:fade linear 2s infinite;}
@keyframes fade{from{opacity:0.1;} 50%{opacity:1;} to{opacity:0.1;}}
.im-record-txt text{color:#00B26A; padding:0 12px;}
.im-send-voice{margin-top:12px; font-size:28rpx; color:#00BA62; text-align:center;}
.im-send-voice text{margin:0 15px; color:#00BA62;}
.toolBox {
height:72rpx;
margin-bottom:3rpx;
.recorder{
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
padding: 14rpx;
border-radius: 10rpx;
font-size: 28rpx;
box-shadow: 2rpx 2rpx 6rpx rgba(0, 0, 0, 0.2);
position: relative;
margin: 0 6rpx !important;
height:100%;
&.active {
background-color: #67C23A;;
color:#fff;
}
}
}
.voice-model{
width:240rpx;
height:180rpx;
position: fixed;
top: 50%;
z-index: 2;
transform: translate(-50%, -50%);
left: 50%;
padding: 0 4rpx 0 6rpx;
background-color: #363636b3;
}
.im-emoji-box{
position:fixed;bottom:0;width:100%;background-color: #F1F2F3;z-index:3;
.im-emoji-header{
height:90rpx;
}
.im-emoji-body{
height:100%;padding-bottom: 100rpx;
.im-emoji-item{
padding:22rpx;
}
}
}
.im-app-box{
position:fixed;bottom:0;width:100%;background-color: #F1F2F3;z-index:3;
.im-app-item{
width:160rpx;height:160rpx;
.im-app-item-icon{
padding:20rpx 25rpx;
}
}
}
.message-quote{
padding:8rpx;
font-size:24rpx;
margin:16rpx -10rpx 0 10rpx;
background-color: #e3e3e3;
.text-overflow{
overflow: hidden !important;
text-overflow: ellipsis;
white-space: nowrap !important;
width:380rpx;
}
}
.voice-icon{
animation: twinkle 0.5s infinite alternate;
}
@keyframes twinkle {
0%{
opacity:0.9;
}
100%{
opacity:0.3;
}
}
.upload-emoji{
width:90rpx;
height:90rpx;
border:dashed 1px #999;
font-size:50rpx;
text-align: center;
border-radius: 6rpx;
}
</style>
<style>
.im-input /deep/ .ql-editor{
max-height:300rpx;
line-height: 1.5;
font-size: 30rpx !important;
word-break: break-all;
word-wrap: break-word;
padding-left: 3px;
}
.im-input /deep/ .ql-editor p img{
vertical-align: text-top;
}
</style>

35
components/message/im-item.vue

@ -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>

50
components/message/im-tab.vue

@ -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>

292
components/message/user-select.vue

@ -0,0 +1,292 @@
<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},//12345
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 {
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;
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 => {
//truefalse
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>

455
components/mosowe-canvas-image/mosowe-canvas-image.vue

@ -0,0 +1,455 @@
<!-- 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) { // 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
// httpsH5
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
// httpsH5
downloadImageH5 (item) {
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>

195
components/mosowe-canvas-image/readme.md

@ -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>
```
#### 预览地址

1623
components/mosowe-canvas-image/wxqrcode.js

File diff suppressed because it is too large

92
components/status.vue

@ -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>

1371
components/yq-avatar/yq-avatar.vue

File diff suppressed because it is too large

BIN
favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
hybrid/html/image/guaduan.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
hybrid/html/image/jieting.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
hybrid/html/image/speaker-off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
hybrid/html/image/speaker.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
hybrid/html/image/video.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
hybrid/html/image/voice-off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
hybrid/html/image/voice.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
hybrid/html/image/wallpaper.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

613
hybrid/html/index.html

@ -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>

275
hybrid/html/js/jsonly.js

@ -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;
};

1
hybrid/html/js/uni.webview.js

File diff suppressed because one or more lines are too long

11
hybrid/html/js/utils.js

@ -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;
}

1
hybrid/html/js/vue.js

File diff suppressed because one or more lines are too long

3480
hybrid/html/rtc/adapter-latest.js

File diff suppressed because it is too large

702
hybrid/html/rtc/chrome/chrome_shim.js

@ -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;
});
}

50
hybrid/html/rtc/chrome/getdisplaymedia.js

@ -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);
});
};
}

188
hybrid/html/rtc/chrome/getusermedia.js

@ -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))));
};
}
}

433
hybrid/html/rtc/common_shim.js

@ -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]));
};
}

296
hybrid/html/rtc/firefox/firefox_shim.js

@ -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);
};
}

36
hybrid/html/rtc/firefox/getdisplaymedia.js

@ -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);
};
}

67
hybrid/html/rtc/firefox/getusermedia.js

@ -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]);
};
}
}
}

353
hybrid/html/rtc/safari/safari_shim.js

@ -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;
}

263
hybrid/html/rtc/utils.js

@ -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;
}

BIN
hybrid/html/voice/calling.mp3

Binary file not shown.

20
index.html

@ -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>

54
main.js

@ -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

192
manifest.json

@ -0,0 +1,192 @@
{
"name" : "Raingad-IM",
"appid" : "__UNI__D1E78EC",
"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" : {}
},
/* */
"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"
},
/* ios */
"sdkConfigs" : {
"ad" : {},
"push" : {},
"maps" : {},
"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" : {}
}
},
"fallbackLocale" : "zh-Hans"
}

118
mixins/chat.js

@ -0,0 +1,118 @@
export const chat = {
data() {
return {
}
},
created: function() {
},
methods: {
// 播放视频,禁止多个同时播放
handlePlay (item) {
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 imgsCurrent = 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);
}
}
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);
},
}
}

BIN
nativeplugins/lemonjk-FileSelect/android/uniplugin_fileSelect-release.aar

Binary file not shown.

BIN
nativeplugins/lemonjk-FileSelect/ios/DCTestUniPlugin.framework/DCTestUniPlugin

Binary file not shown.

BIN
nativeplugins/lemonjk-FileSelect/ios/DCTestUniPlugin.framework/Info.plist

Binary file not shown.

43
nativeplugins/lemonjk-FileSelect/package.json

@ -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"
}
}
}

722
package-lock.json

@ -0,0 +1,722 @@
{
"name": "Raingad-IM",
"version": "5.5.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "Raingad-IM",
"version": "5.5.0",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.2.0",
"jsqr": "^1.4.0",
"pinia": "^2.0.23",
"quill": "^1.3.7",
"vconsole": "^3.15.1"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.5.tgz",
"integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ=="
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/copy-text-to-clipboard": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz",
"integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/core-js": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/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==",
"dependencies": {
"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"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"node_modules/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=="
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"dependencies": {
"has-symbols": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-date-object": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="
},
"node_modules/mutation-observer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
},
"node_modules/object-is": {
"version": "1.1.5",
"resolved": "https://registry.npmmirror.com/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
},
"node_modules/pinia": {
"version": "2.0.23",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.23.tgz",
"integrity": "sha512-N15hFf4o5STrxpNrib1IEb1GOArvPYf1zPvQVRGOO1G1d74Ak0J0lVyalX/SmrzdT4Q0nlEFjbURsmBmIGUR5Q==",
"dependencies": {
"@vue/devtools-api": "^6.4.4",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmmirror.com/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"dependencies": {
"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"
}
},
"node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmmirror.com/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
"integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"set-function-name": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-function-name": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.1.tgz",
"integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
"dependencies": {
"define-data-property": "^1.0.1",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/vconsole": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vconsole/-/vconsole-3.15.1.tgz",
"integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"copy-text-to-clipboard": "^3.0.1",
"core-js": "^3.11.0",
"mutation-observer": "^1.0.3"
}
},
"node_modules/vue": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==",
"peer": true
},
"node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
}
},
"dependencies": {
"@babel/runtime": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
"requires": {
"regenerator-runtime": "^0.14.0"
}
},
"@vue/devtools-api": {
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.5.tgz",
"integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ=="
},
"call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"requires": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
}
},
"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.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz",
"integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q=="
},
"core-js": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA=="
},
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/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.1",
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"requires": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
}
},
"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"
}
},
"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.2.2",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"requires": {
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
}
},
"gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"requires": {
"get-intrinsic": "^1.1.3"
}
},
"has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"requires": {
"get-intrinsic": "^1.2.2"
}
},
"has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"requires": {
"has-symbols": "^1.0.2"
}
},
"hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"requires": {
"function-bind": "^1.1.2"
}
},
"is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
}
},
"is-date-object": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
}
},
"jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="
},
"mutation-observer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
},
"object-is": {
"version": "1.1.5",
"resolved": "https://registry.npmmirror.com/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"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.0.23",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.23.tgz",
"integrity": "sha512-N15hFf4o5STrxpNrib1IEb1GOArvPYf1zPvQVRGOO1G1d74Ak0J0lVyalX/SmrzdT4Q0nlEFjbURsmBmIGUR5Q==",
"requires": {
"@vue/devtools-api": "^6.4.4",
"vue-demi": "*"
}
},
"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"
}
},
"regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
"integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"set-function-name": "^2.0.0"
}
},
"set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"requires": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
}
},
"set-function-name": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.1.tgz",
"integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
"requires": {
"define-data-property": "^1.0.1",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.0"
}
},
"vconsole": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/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": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==",
"peer": true
},
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
}
}
}

24
package.json

@ -0,0 +1,24 @@
{
"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",
"jsqr": "^1.4.0",
"pinia": "^2.0.23",
"quill": "^1.3.7",
"vconsole": "^3.15.1"
}
}

199
pages.json

@ -0,0 +1,199 @@
{
"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": "帮助文档"
}
}
],
"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": "通讯录"
}
]
}
}

79
pages/compass/index.vue

@ -0,0 +1,79 @@
<template>
<view :style="{paddingBottom: paddingB+'px'}">
<view class="cu-list menu" :class="['sm-border','card-menu margin-top']" v-if="compass.mode==1">
<view class="cu-item" :class="'arrow'" v-for="(item,index) in compass.list" :key="index" @tap="openApp(item)">
<view class="content">
<image :src="item.icon" class="png" mode="aspectFit"></image>
<text class="text-grey">{{item.name}}</text>
</view>
</view>
</view>
<view class="cu-list grid" :class="['col-3',' margin-top']" v-if="compass.mode==2">
<view class="cu-item" v-for="(item,index) in compass.list" :key="index" @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>
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
const loginStore = useloginStore(pinia)
export default {
data() {
return {
isCard: true,
userInfo:loginStore.userInfo,
paddingB:0
};
},
computed:{
compass:()=>{
return loginStore.globalConfig.compass
},
},
created:function(){
// #ifdef H5
this.paddingB=this.inlineTools;
// #endif
// #ifndef H5
this.paddingB=this.navBarHeight+this.inlineTools;
// #endif
},
methods: {
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;
}
</style>

140
pages/compass/moments.vue

@ -0,0 +1,140 @@
<template>
<cu-custom bgColor="bg-white" :isBack="true">
<template #backText></template>
<template #content>朋友圈</template>
</cu-custom>
<view :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>
<view class="m-10 text-center">此页面是静态模板</view>
<view class="cu-card dynamic no-card">
<view class="cu-item shadow">
<view class="cu-list menu-avatar">
<view class="cu-item">
<view class="cu-avatar round lg" style="background-image:url(https://api.multiavatar.com/raingad3.png?apikey=zdvXV3W4MjwhP9);"></view>
<view class="content flex-sub">
<view>凯尔</view>
<view class="text-gray text-sm flex justify-between">
2019年12月3日
</view>
</view>
</view>
</view>
<view class="text-content">
折磨生出苦难苦难又会加剧折磨凡间这无穷的循环将有我来终结
</view>
<view class="grid flex-sub padding-lr" :class="isCard?'col-3 grid-square':'col-1'">
<view class="bg-img" :class="isCard?'':'only-img'" style="background-image:url(https://ossweb-img.qq.com/images/lol/web201310/skin/big10006.jpg);"
v-for="(item,index) in isCard?9:1" :key="index">
</view>
</view>
<view class="text-gray text-sm text-right padding">
<text class="cuIcon-attentionfill margin-lr-xs"></text> 10
<text class="cuIcon-appreciatefill margin-lr-xs"></text> 20
<text class="cuIcon-messagefill margin-lr-xs"></text> 30
</view>
<view class="cu-list menu-avatar comment solids-top">
<view class="cu-item">
<view class="cu-avatar round" style="background-image:url(https://api.multiavatar.com/raingad5.png?apikey=zdvXV3W4MjwhP9);"></view>
<view class="content">
<view class="text-grey">莫甘娜</view>
<view class="text-gray text-content text-df">
凯尔你被自己的光芒变的盲目
</view>
<view class="bg-grey padding-sm radius margin-top-sm text-sm">
<view class="flex">
<view>凯尔</view>
<view class="flex-sub">妹妹你在帮他们给黑暗找借口吗?</view>
</view>
</view>
<view class="margin-top-sm flex justify-between">
<view class="text-gray text-df">2018年12月4日</view>
<view>
<text class="cuIcon-appreciatefill text-red"></text>
<text class="cuIcon-messagefill text-gray margin-left-sm"></text>
</view>
</view>
</view>
</view>
<view class="cu-item">
<view class="cu-avatar round" style="background-image:url(https://api.multiavatar.com/raingad2.png?apikey=zdvXV3W4MjwhP9);"></view>
<view class="content">
<view class="text-grey">凯尔</view>
<view class="text-gray text-content text-df">
妹妹如果不是为了飞翔我们要这翅膀有什么用?
</view>
<view class="bg-grey padding-sm radius margin-top-sm text-sm">
<view class="flex">
<view>莫甘娜</view>
<view class="flex-sub">如果不能立足于大地要这双脚又有何用?</view>
</view>
</view>
<view class="margin-top-sm flex justify-between">
<view class="text-gray text-df">2018年12月4日</view>
<view>
<text class="cuIcon-appreciate text-gray"></text>
<text class="cuIcon-messagefill text-gray margin-left-sm"></text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
const loginStore = useloginStore()
export default {
data() {
return {
isCard: true,
userInfo:loginStore.userInfo,
paddingB:0
};
},
created:function(){
// #ifdef H5
this.paddingB=this.inlineTools;
// #endif
// #ifndef H5
this.paddingB=this.navBarHeight+this.inlineTools;
// #endif
},
methods: {
IsCard(e) {
this.isCard = e.detail.value
},
}
}
</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;
}
</style>

247
pages/contacts/detail.vue

@ -0,0 +1,247 @@
<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">
<view 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.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.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>
<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';
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
const userStore = useloginStore(pinia)
export default {
data() {
return {
modelName:'',
detail:{},
userInfo:userStore.userInfo,
globalConfig:userStore.globalConfig
}
},
computed: {
validatePhone(){
let reg = /^1[3456789]\d{9}$/;
return reg.test(this.detail.account);
}
},
onLoad(options) {
this.$api.msgApi.getUserInfo({user_id:options.id}).then((res)=>{
if(res.code==0){
this.detail=res.data;
}
})
},
methods: {
showAvatar(detail){
let imgs=[];
imgs.push(detail.avatar);
uni.previewImage({urls : imgs})
},
sendMsg(info){
uni.reLaunch({
url:"/pages/message/chat?id="+info.user_id
})
},
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);
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'
})
}
})
}
}
});
},
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>

150
pages/contacts/friend.vue

@ -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:e.msg,
icon:'none'
})
msgStore.sysUnread--;
this.getList();
}
})
}
})
}
}
}
</script>
<style scoped>
</style>

89
pages/contacts/group.vue

@ -0,0 +1,89 @@
<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)'>
<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 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'
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(this.msgs);
},
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;
},
//
openDetails(items){
uni.navigateTo({
url:"/pages/message/chat?id="+items.id
})
},
search(){
uni.navigateTo({
url:"/pages/index/search?type=3"
})
}
}
}
</script>
<style scoped>
</style>

410
pages/contacts/index.vue

@ -0,0 +1,410 @@
<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="openChat(item.id)" v-for="(item,index) in systemContact" :key="index">
<view 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)'>
<view 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';
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
const userStore = useloginStore(pinia)
const {contacts,sysUnread} = 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:[],
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:[]
};
},
watch:{
msgs(val){
this.initContacts(val);
}
},
created() {
this.listCur = this.contacts[0];
},
mounted(){
this.initContacts(this.msgs);
},
methods: {
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;
})
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"
})
},
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);
}
</style>

88
pages/contacts/search.vue

@ -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>

254
pages/index/index.vue

@ -0,0 +1,254 @@
<template>
<view>
<cu-custom bgColor="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'>
<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>
<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'
const msgStore = useMsgStore(pinia)
const loginStore = useloginStore(pinia)
const { unread,sysUnread } = 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
};
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
}
},
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
this.initContacts();
// 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
});
},
methods: {
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(){
this.modelName='';
this.$api.msgApi.initContacts().then(res => {
//
msgStore.sysUnread=res.count;
msgStore.initContacts(res.data);
})
},
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;
}
</style>

220
pages/index/qrcode.vue

@ -0,0 +1,220 @@
<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">
<button class="cu-btn bg-green lg" @tap="saveHeadImgFile">
<!-- #ifdef H5 -->
下载图片
<!-- #endif -->
<!-- #ifndef H5 -->
保存到相册
<!-- #endif -->
</button>
</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;
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,
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>

34
pages/index/scan.vue

@ -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>

112
pages/index/search.vue

@ -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>

337
pages/index/userSelection.vue

@ -0,0 +1,337 @@
<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,
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 : '';
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
}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 {
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}).then(res =>{
this.closePage();
})
}else if(this.type==4){
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('getAtList',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 => {
//truefalse
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>

198
pages/login/index.vue

@ -0,0 +1,198 @@
<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">{{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"
const loginStore = useloginStore(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);
},
doLogin(data){
this.$api.LoginApi.login(data).then(res => {
if (res.code == 0) {
uni.setStorageSync('authToken', res.data.authToken)
let userInfo=res.data.userInfo;
// wss
this.socketIo.send({
type: "bindUid",
user_id: userInfo.user_id,
token:res.data.authToken
});
loginStore.login(userInfo);
uni.reLaunch({
url: '/pages/index/index'
})
}
})
}
}
}
</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>

182
pages/login/register.vue

@ -0,0 +1,182 @@
<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>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>

262
pages/message/call.vue

@ -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)
},
// appwebview
appsendH5(params) {
if (params.iceCandidate && params.iceCandidate.length>0){
params.iceCandidate = JSON.parse(params.iceCandidate)
}
this.webview.evalJS("getUniAppMessage('" + JSON.stringify(params) + "')")
},
// h5webview
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>

1347
pages/message/chat.vue

File diff suppressed because it is too large

638
pages/message/detail.vue

@ -0,0 +1,638 @@
<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">
<image 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">
<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}`">
<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 ?? '暂无公告'}}
</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="!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="contact.is_top?'checked':''" :checked="contact.is_top?true:false"></switch>
</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'?'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" :disabled="!isAuth" v-model="contact.notice" placeholder="请输入公告内容..."></textarea>
</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 in radioList">
<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>
</view>
</template>
<script>
import { useMsgStore } from '@/store/message';
import pinia from '@/store/index';
import { useloginStore } from '@/store/login';
const userStore = useloginStore(pinia)
const msgStore = useMsgStore(pinia)
export default {
components: {
},
data() {
return {
pageLoading: true,
contact_id: null, //id,
is_group:0,
groupUserCount:0,
modelName:false,
userList: [], //
allUser:[],
userInfo:userStore.userInfo,
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
}
}
},
onShow() {
this.getUserlist()
},
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;
}
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);
}
},
methods: {
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;
},
setIsNotice(e){
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){
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
})
},
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 =>{
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:20,
}).then(res => {
this.user = res.data
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)
}else if(this.contact.setting.invite){
this.userList = res.data.splice(0, 19)
} else {
this.userList = res.data.splice(0, 20)
}
} else {
this.userList = res.data
}
this.groupUserCount=res.count;
this.pageLoading = false;
})
},
//
openChatDetail(item){
if(this.userInfo.user_id==item.id) return;
let friend=msgStore.getContact(item.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,'')
}
}
}
</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>

225
pages/message/emoji.vue

@ -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>

291
pages/message/group/groupUser.vue

@ -0,0 +1,291 @@
<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 in userList">
<view 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>
</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" :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" />&nbsp;
</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)
export default {
data() {
return {
keywords:'',
group_id: '',
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
}]
}
},
watch:{
keywords(val){
if(val==''){
this.userList=this.allUser;
}else{
this.search();
}
},
},
onLoad(options) {
this.group_id = options.group_id?options.group_id:''
this.getGroupuserlist()
},
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;
})
},
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: '确定将管理权限转移给该成员吗?',
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>
.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;
}
</style>

88
pages/message/group/info.vue

@ -0,0 +1,88 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>群信息</template>
</cu-custom>
<view align="center" class="groupInfo">
<image :src="contact.avatar" mode="widthFix" style="width:120px;height:120px;border-radius: 12rpx;"></image>
<view class="f-14 mt-10">
{{contact.name}} ({{contact.groupUserCount}})
</view>
</view>
<view class="padding flex flex-direction mt-10" v-if="!contact.isJoin">
<button class="cu-btn bg-green lg" @tap="applyGroup">
加入群聊
</button>
</view>
<view class="padding flex flex-direction mt-10" v-else>
<button class="cu-btn bg-green lg" @tap="openChat">
进入聊天
</button>
</view>
</view>
</template>
<script>
import pinia from '@/store/index'
import { useMsgStore } from '@/store/message';
const msgStore = useMsgStore(pinia)
export default {
data() {
return {
contact:{},
group_id:0
}
},
onLoad(options) {
this.group_id = options.group_id?options.group_id:''
this.getGroupInfo()
},
methods: {
getGroupInfo() {
this.$api.msgApi.groupInfo({
group_id: this.group_id
}).then((res) => {
let data=res.data;
this.contact=data;
})
},
applyGroup() {
if(this.contact.setting.invite==0){
return uni.showToast({
title:'该群聊已经关闭加群申请',
icon:'none'
})
}
uni.showLoading({
title: '加入中'
});
try{
this.$api.msgApi.joinGroup({
group_id: this.group_id,
inviteUid:this.contact.inviteUid
}).then((res) => {
uni.hideLoading()
if(res.code==0){
this.contact.is_group=1;
msgStore.appendContacts(this.contact);
this.openChat();
}
})
}catch(e){
uni.hideLoading()
}
},
openChat(){
uni.navigateTo({
url:"/pages/message/chat?id="+this.group_id
})
}
}
}
</script>
<style scoped>
.groupInfo{
margin-top: 100rpx;
}
</style>

349
pages/message/index.vue

@ -0,0 +1,349 @@
<template>
<view>
<!-- <view class="socket-status pd-10 im-flex justify-between im-align-items-center" v-if="!socketStatus">
<view class="cuIcon-infofill text-red f-18"></view>
<view class="c-666 f-12"> WS通信已断开检查网络设置是否正常</view>
<view @tap="reconnect()">重连</view>
</view> -->
<view class="pr-10 pl-10 text-gray bg-white im-flex im-space-between im-align-items-center cu-bar fixed" :style="[{top:CustomBar + 'px'}]">
<im-tab class="mr-10" :values="values" @change="changeChat"></im-tab>
<view class="im-flex im-justify-content-start im-align-items-center" v-if="multiport && socketStatus">
<view class=" iconfont icon-web f-16 ml-5"></view>
<view class="f-14 ml-5">电脑在线</view>
</view>
<view class="socket-status pd-5 im-flex justify-between im-align-items-center radius-10 im-flex1" v-if="!socketStatus">
<view class="cuIcon-infofill text-red f-18"></view>
<view class="c-666 f-12">通信断开</view>
<view class="cuIcon-refresh" @tap="reconnect()"></view>
</view>
</view>
<view class="im-message-list" style="margin-top:100rpx">
<uni-notice-bar :speed="60" show-icon scrollable showClose :text="noticeContent.title" style="margin: 0;" @tap="openChat('admin_notice')" v-if="noticeContent.title" @close="closeNotice"/>
<view class="cu-list menu-avatar" :style="{paddingBottom: paddingB+'px'}" v-if="msgsIn.length>0">
<view class="cu-item second" :class="[modalName=='move-box-'+ index?'move-cur':'',item.is_top==1 ? 'top-contacts' : '']" v-for="(item, index) in msgsIn" :key="index"
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" @tap="openChat(item.id)" :data-target="'move-box-' + index">
<view class="cu-avatar lg" :class="appSetting.circleAvatar?'round':'radius'" :style="[{backgroundImage:'url('+ item.avatar +')'}]">
<view class="online-status" v-if="item.is_online && item.is_group==0 && globalConfig.chatInfo.online==1"></view>
</view>
<view class="content">
<view class="c-333">
<view class="text-overflow f-16" style="width:80%">
{{item.displayName}}
</view>
</view>
<view class="im-flex im-justify-content-start im-align-items-start lh-20x" style="height: 50rpx;overflow:hidden">
<view class="text-red f-12 mr-5" v-if="item.is_at">[{{item.is_at}}@] </view>
<mp-html :content="emojiToHtml(item.lastContent)" class="im-flex f-12 text-gray text-overflow no-click"/>
</view>
</view>
<view class="action">
<view class="text-grey text-xs" >{{from_time(item.lastSendTime)}}</view>
<view class="cu-tag round sm" :class="item.is_notice ? 'bg-red' : 'bg-notremind'" v-if="item.unread>0">{{item.unread}}</view>
<view class="c-999" v-if="item.is_notice==0 && item.unread==0">
<text class="cuIcon-musicforbidfill"></text>
</view>
</view>
<view class="move second">
<view class="bg-grey" v-if="item.is_top==1" @tap="btnTap(0,item)">取消置顶</view>
<view class="bg-blue" v-else @tap="btnTap(0,item)">置顶聊天</view>
<view class="bg-orange" v-if="item.is_notice==1" @tap="btnTap(2,item)">免扰</view>
<view class="bg-orange" v-else @tap="btnTap(2,item)">取消免扰</view>
</view>
</view>
</view>
<Empty v-else noDatatext="暂无聊天" textcolor="#999" />
</view>
</view>
</template>
<script>
import statusPoint from '@/components/status.vue'
import imTab from '@/components/message/im-tab.vue'
import emoji from '@/utils/emoji.js'
import { storeToRefs } from 'pinia';
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
const {contacts,unread,msgAt} = storeToRefs(msgStore);
const userStore = useloginStore(pinia);
const {multiport} = storeToRefs(userStore);
export default {
components: {
statusPoint,imTab
},
data() {
return {
navCurrent: 0,
msgs: contacts,
pageLoading: true,
multiport:multiport,
socketStatus:true,
damping : 0.29,
moveIndex : -1,
touchStart:false,
modalName: null,
listTouchStart: 0,
listTouchDirection: null,
emojiMap:[],
chatStatus:true,
paddingB:0,
msgAt:msgStore.msgAt,
appSetting:userStore.appSetting,
globalConfig:userStore.globalConfig,
active:0,
triggered:true,
noticeContent:{},
values:[
{
id:1,
name:'所有',
count:0
},
{
id:2,
name:'未读',
count:unread
},
{
id:3,
name:'@我',
count:msgAt
}
]
}
},
computed:{
msgsIn(){
let index=this.active;
let contactList = [];
if(index==1){
contactList = this.msgs.filter(obj => obj.unread>0);
}else if(index==2){
contactList = this.msgs.filter(obj => obj.is_at>0);
}else{
contactList = this.msgs.filter(obj => obj.lastContent);
}
return contactList;
}
},
mounted(){
this.moveIndex = -1;
this.getNotice();
// ws
uni.$on('socketStatus',(e)=>{
if(!e){
this.multiport=false;
}
this.socketStatus=e;
})
},
created:function(){
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;
// #ifdef H5
this.paddingB=this.inlineTools;
// #endif
// #ifndef H5
this.paddingB=this.navBarHeight+this.inlineTools;
// #endif
},
methods: {
initContacts(){
uni.$emit('initContacts',true)
this.triggered = true;
setTimeout(() => {
this.triggered = false;
}, 1000)
},
getNotice(){
this.$api.msgApi.getAdminNotice().then(res => {
if (res.code == 0) {
let data=res.data;
let oldNotice=uni.getStorageSync('notice_'+data.create_time);
if(!oldNotice){
this.noticeContent=res.data;
}
}
});
},
closeNotice(){
//
uni.setStorageSync('notice_'+this.noticeContent.create_time,this.noticeContent);
},
changeChat(item,index){
this.active=index;
},
btnTap: function(index, contact) {
//
if (index == 0) {
contact.is_top=contact.is_top==0 ? 1 : 0;
this.$api.msgApi.setChatTopAPI({
id: contact.id,
is_top:contact.is_top,
is_group: contact.is_group
}).then(res => {
if (res.code == 0) {
msgStore.updateContacts(contact);
}
});
}
// [ ]
else if (index == 1) {
uni.showModal({
title: '确定要删除吗?',
success: e => {
if (e.confirm) {
// this.$api.msgApi.delChatAPI({
// id: contact.id,
// is_group: contact.is_group
// }).then(res => {
// if (res.code == 0) {
// msgStore.deleteContacts(contact);
// }
// });
}
}
});
}
//
else if (index == 2) {
contact.is_notice=contact.is_notice==0 ? 1 : 0;
this.$api.msgApi.isNoticeAPI({
id: contact.id,
is_notice:contact.is_notice,
is_group: contact.is_group
}).then(res => {
if (res.code == 0) {
msgStore.updateContacts(contact);
}
});
}
},
reconnect(){
uni.showLoading({
title:'重连中...'
})
this.socketIo.connectSocketInit({type:'ping'});
setTimeout(()=>{
uni.hideLoading()
},1500)
},
//
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, "]");
});
},
// ListTouch
ListTouchStart(e) {
this.listTouchStart = e.touches[0].pageX
},
// ListTouch
ListTouchMove(e) {
let cux=e.touches[0].pageX;
let direction=cux- this.listTouchStart;
// 100,
if(Math.abs(direction)>100 && direction<0){
this.listTouchDirection='left';
}else{
this.listTouchDirection='right';
}
},
// ListTouch
ListTouchEnd(e) {
if (this.listTouchDirection == 'left') {
this.modalName = e.currentTarget.dataset.target
this.chatStatus=false;
} else {
this.modalName = null
}
this.listTouchDirection = null
},
openChat(id){
//
if(this.chatStatus){
uni.navigateTo({
url:"/pages/message/chat?id=" + id,
animationType:"slide-in-right"
})
}else{
this.chatStatus=true;
}
},
from_time(time){
return this.$util.timeFormat(time);
},
}
}
</script>
<style lang="scss">
.container {
padding: 20px;
font-size: 14px;
line-height: 24px;
}
.border-b{
border-bottom: 1px solid #eee;
}
.socket-status{
background-color: #fde0de;
height:72rpx;
border-radius: 36rpx;
padding:10rpx 16rpx;
}
.text-overflow ::v-deep uni-text , .text-overflow ::v-deep uni-text span{
overflow: hidden !important;
text-overflow: ellipsis;
white-space: nowrap !important;
width:300rpx;
}
.top-contacts{
background-color: #f5f5f5 !important;
}
.cu-list.menu-avatar>.cu-item{
padding-right:30rpx;
.content{
line-height: unset;
}
}
.no-click{
pointer-events:none;
height:50rpx;
flex:1;
}
.text-overflow ::v-deep ._block ._a{
color: #aaaaaa !important;
}
.bg-notremind{
background-color: #d1d1d1;
color: #ffffff;
}
.cu-bar.fixed, .nav.fixed{
z-index:900;
}
</style>

517
pages/message/record.vue

@ -0,0 +1,517 @@
<template>
<view style="width:100%">
<cu-custom bgColor="bg-main-bar" :isBack="true" class="cu-header">
<template #backText></template>
<template #content>聊天信息</template>
<template #right>
<view class="cuIcon-search mr-10 f-16" @tap="switchSearch()"></view>
</template>
</cu-custom>
<view class="cu-bar bg-white search fixed" :style="[{top:CustomBar + 'px',minHeight:'80rpx',justifyContent:'flex-start'}]">
<view v-for="(tab,index) in tabBars" :key="tab.id" class="uni-tab-item" :id="tab.id" :data-current="index" @click="ontabtap(tab.id)" v-if="!isSearch">
<text class="uni-tab-item-title" :class="params.type==tab.id ? 'uni-tab-item-title-active' : ''">{{tab.name}}</text>
</view>
<template v-else>
<view class="search-form round">
<text class="cuIcon-search"></text>
<input type="text" v-model="params.keywords" placeholder="请输入关键字搜索" confirm-type="search"/>
</view>
<view class="action">
<button class="cu-btn round bg-green" @tap="getMessageList()">搜索</button>
</view>
</template>
</view>
<scroll-view class="scroll-view-body" ref="scrollView" :scroll-y="true" :scroll-anchoring="true" :style="{height:scrollH+'rpx',position:'fixed',bottom:bottomHeight+'px'}">
<view class="cu-chat" :style="{paddingTop:'100rpx'}">
<template v-for="(item,index) in messageList" :key="index" :id="'chatItem_'+index">
<view class="cu-item">
<im-user :info="item.fromUser" :profile="isProfile" @longpress="at(item.fromUser)"></im-user>
<view class="main im-wrap">
<view class="f-12 c-666" style="width:100%;margin-bottom: 6rpx;">
<text>{{item.fromUser.realname}} &nbsp;&nbsp;</text>
<text class="f-11 c-999">{{sendTime(item.sendTime)}}</text>
</view>
<view class="im-flex im-rows-reverse self im-align-items-end">
<!-- 文字消息 -->
<view v-if="item.type=='text'">
<view class="content shadow">
<mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap" :content="emojiToHtml(item.content)"/>
</view>
<view class="message-quote radius-6" v-if="item.extends && item.extends.content">
{{item.extends.content}}
</view>
</view>
<!-- 图片消息 -->
<template v-else-if="item.type=='image'">
<im-image :src="item.content" :info="item.extends" @showImgs="showImgs"></im-image>
</template>
<!-- 语音消息 -->
<view v-else-if="item.type=='voice'" class="im-voice-msg im-flex im-rows im-nowrap im-align-items-center radius-20"
:class="[index == playIndex ? 'linear-green' : '', item.fromUser.id==user.user_id ? 'im-rows-reverse' : '' , ]" :data-voice="item.content" :data-index='index' @tap='playVoice'
:style="{'width':(item.extends.duration*3)+'px'}">
<text class="f-16 cuIcon-subscription rotate45" :class="[index == playIndex ? 'c-white' : '',item.fromUser.id==user.user_id ? 'rotate225' : '']"></text>
<text class="im-voice-msg-text" :class="[index == playIndex ? 'c-white' : '']">{{item.extends.duration}} "</text>
</view>
<!-- 视频消息 -->
<template v-else-if="item.type=='video'" >
<view class='course-video' :style="(item.extends && item.extends.width) ? $util.imageCoverStyle(item.extends.width,item.extends.height) : ''">
<view class="relative-shadow" @tap="handlePlay(item)">
<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="item.extends" :src="item.extends.poster" :info="item.extends"></im-image>
</view>
</template>
<!-- 文件消息 -->
<view v-else-if="item.type=='file'">
<view class="file-card bg-white radius-10 im-flex im-justify-content-start pd-10 im-align-items-center" @tap.stop="previewFile(item)">
<image :src="item.extUrl" style="width:64rpx;height:80rpx"></image>
<view class="im-flex im-columns ml-10">
<view class="text-overflow file-name">{{item.fileName}}</view>
<view class="text-gray file-size f-12">{{fileSize(item.fileSize)}}</view>
</view>
</view>
</view>
<!-- 音视频消息 -->
<view v-else-if="item.type=='webrtc'" class="im-voice-msg im-flex im-rows im-nowrap im-align-items-center radius-20" :class="[item.fromUser.id==user.user_id ? 'im-rows-reverse' : '' , ]">
<text class="f-16" :class="[item.extends.type == 1 ? 'cuIcon-record' : 'cuIcon-dianhua',item.fromUser.id==user.user_id ? 'rotate180' : '']"></text>
<text class="im-voice-msg-text">{{item.content}}</text>
</view>
<!-- 位置消息 -->
<view v-else-if="item.type=='location'" @tap="openLocation(item.extends)" class="im-location-msg im-flex im-rows im-nowrap im-align-items-center radius-8 pd-10">
<view class="f-24 cuIcon-location pr-5"></view>
<view>
<view class="f-14 mb-5">{{item.content}}</view>
<view class="c-999 f-12">{{item.extends && item.extends.address}}</view>
</view>
</view>
<!-- 名片消息 -->
<view v-else-if="item.type=='contact'" @tap="openContact(item.extends)" class="im-contact-msg radius-8 pt-10 pr-10 pl-10 pb-5">
<view class="im-flex im-rows im-nowrap im-align-items-center">
<view class='cu-avatar mr-10 radius' :style="[{backgroundImage:'url('+item.extends.avatar+')'}]">
</view>
<view class="c-333">{{item.extends.displayName}}</view>
</view>
<hr class="mt-10 c-999">
<view class="c-666 f-10">
个人名片
</view>
</view>
<!-- 动态表情消息 -->
<template v-else-if="item.type=='emoji'">
<image :src="item.content" class="radius" mode="aspectFit" @tap="showImgs" :data-img="item.content" style="width:300rpx;height:300rpx"></image>
</template>
<!-- 其他消息 -->
<imItem v-else :item="item" :index="index" :isSelf="true"></imItem>
</view>
</view>
</view>
</template>
</view>
<Empty v-if="!messageList.length" noDatatext="暂无记录" textcolor="#999" />
<view class="im-pagination">
<uni-pagination :total="dataTotal" :pageSize="params.limit" @change="changePage" title="标题文字" />
</view>
</scroll-view>
<!-- 文件预览 -->
<view class="cu-modal bottom-modal" :class="modelName=='preview'?'show':''" @tap="modelName=''">
<view class="cu-dialog" v-if="modelName=='preview'">
<view class="cu-list menu bg-white">
<view class="cu-item" @tap="preview(1)" >
<view class="content padding-tb-sm">
<text class="text-center">本地预览(需下载)</text>
<view class="text-gray text-sm">需下载仅支持office类型文件</view>
</view>
</view>
<view class="cu-item" @tap="preview(2)">
<view class="content padding-tb-sm">
<text>在线预览</text>
<view class="text-gray text-sm">支持常用的文件和文档</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import imItem from '@/components/message/im-item.vue';
import imUser from '@/components/im-user.vue';
import { chat } from '@/mixins/chat.js'
import { useloginStore } from '@/store/login';
import { useMsgStore } from '@/store/message';
import { storeToRefs } from 'pinia';
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
const {newMessage,msgList,getContact,appendMsg,checkMsg,unread} = storeToRefs(msgStore);
const userStore = useloginStore(pinia)
export default {
components: {
imItem,
imUser
},
mixins:[chat],
data() {
return {
user:userStore.userInfo,
modelName:'',
messageList:[],
tabBars: [{
name: '全部',
id: 'all'
}, {
name: '文本',
id: 'text'
}, {
name: '图片',
id: 'image'
}, {
name: '视频',
id: 'video'
}, {
name: '文件',
id: 'file'
}],
scrollInto:'',
isSearch:false,
dataTotal:0,
emojiMap:[],
isProfile:false,
bottomHeight:0,
paddingT:0,
params:{
toContactId: 0,
is_group:0,
type: "all",
keywords: "",
page: 1,
limit: 10
}
}
},
computed:{
scrollH:function(){
let sys = uni.getSystemInfoSync();
let winWidth = sys.windowWidth;
let winrate = 750/winWidth;
let bottomHeight=uni.upx2px(100);
this.bottomHeight=bottomHeight;
const query=this.getQuery();
setTimeout(() => {
query.select('.cu-header').boundingClientRect();
query.exec(data => {
this.paddingT=data[0].height;
});
}, 10)
// #ifdef H5
let winHeight =parseInt((sys.windowHeight - this.navBarHeight - this.paddingT)*winrate);
// #endif
// #ifdef APP-PLUS
let winHeight =parseInt((sys.windowHeight - (this.inlineTools + this.paddingT+bottomHeight))*winrate);
// #endif
// #ifndef H5 || APP-PLUS
this.bottomHeight+=this.inlineTools;
// ++线
let winHeight =parseInt((sys.windowHeight-(this.inlineTools + this.paddingT + this.navBarHeight))*winrate)
// #endif
return winHeight+18
}
},
onLoad: function(options) {
this.params.toContactId = options.id;
let contact=msgStore.getContact(options.id);
this.params.is_group=contact.is_group;
if(!contact){
uni.showToast({
title:'联系人不存在',
icon:'none',
duration:1500,
complete:(res)=>{
uni.reLaunch({
url: '/pages/index/index'
})
}
})
return;
}
this.contact=contact;
this.getMessageList();
if(contact.is_group==1 && (contact.role<3 || contact.setting.profile=='1')){
this.isProfile=true;
}
},
methods: {
getQuery(){
// #ifdef MP
const query = uni.createSelectorQuery().in(this);
// #endif
// #ifndef MP
const query = uni.createSelectorQuery();
// #endif
return query;
},
getMessageList() {
this.$api.msgApi.getMessageList(this.params).then(res => {
let data=res.data;
this.messageList = data;
this.dataTotal=res.count;
})
},
ontabtap(type) {
if (this.params.type === type) {
return;
}
this.params.type=type;
this.params.page=1;
this.scrollInto = type;
this.getMessageList();
},
changePage(e){
this.params.page=e.current;
this.getMessageList();
},
search(){
this.getMessageList();
},
switchSearch(){
this.params.keywords='';
this.isSearch=!this.isSearch;
if(!this.isSearch){
this.getMessageList()
}
},
}
}
</script>
<style lang="scss">
/* #ifndef APP-PLUS */
page {
width: 100%;
min-height: 100%;
display: flex;
}
/* #endif */
#tab-bar {
background-color: #ffffff;
}
.scroll-h {
width:100%;
height: 80rpx;
flex-direction: row;
/* #ifndef APP-PLUS */
white-space: nowrap;
/* #endif */
/* flex-wrap: nowrap; */
/* border-color: #cccccc;
border-bottom-style: solid;
border-bottom-width: 1px; */
}
.line-h {
height: 1rpx;
background-color: #cccccc;
}
.uni-tab-item {
/* #ifndef APP-PLUS */
display: inline-block;
/* #endif */
flex-wrap: nowrap;
padding-left: 34rpx;
padding-right: 34rpx;
}
.uni-tab-item-title {
color: #555;
font-size: 30rpx;
height: 80rpx;
line-height: 80rpx;
flex-wrap: nowrap;
/* #ifndef APP-PLUS */
white-space: nowrap;
/* #endif */
}
.uni-tab-item-title-active {
color: #007AFF;
}
page{
padding-bottom: 100upx;
}
#more-oprate{
min-height:100%;
justify-content: flex-end;
flex-direction: column;
}
.cu-chat{
background-color: #f1f1f1;
}
.cu-chat .cu-item.self {
justify-content: flex-start;
text-align: right;
}
.bg-light-green{
background-color: #95ec69;
}
.im{padding:30rpx;}
.im-system-msg{color:#FFFFFF; font-size:26rpx; line-height:38rpx; padding:5px 10px; display:block; border-radius:6rpx;}
.im-msg{margin-bottom:28px; display:flex; flex-direction:row; flex-wrap:nowrap;}
.im-voice-msg{height:80rpx; padding:0 20rpx; background-color:#E7F0F3; color:#2B2E3D; min-width:160rpx; max-width:400rpx;}
.im-voice-msg-text{font-size:22rpx; margin:0 5rpx;}
.im-location-msg{ background-color:#E7F0F3; color:#2B2E3D;text-align: left !important;}
.im-contact-msg{ width:360rpx; background-color:#E7F0F3; color:#2B2E3D;text-align: left !important;}
.cu-chat .cu-item{
padding:20rpx 20rpx 0 20rpx !important;
}
.cu-chat .cu-item:last-child{
padding:20rpx !important;
}
.cu-chat .cu-item>.main {
margin: 0 0.8rem;
display: flex;
align-items: center;
}
.cu-chat .cu-item>.main .content{
padding:10rpx 20rpx;
min-height:60rpx;
}
.course-video{
max-width:400rpx;
overflow: hidden;
position: relative;
max-height: 360rpx;
}
.video-model{
background-color: #3838388f;z-index:10000;width: 100%;height: 100%;position: fixed;top:0;overflow:hidden;;
}
.close-model{
position: absolute;top:180rpx;right:20rpx;background-color: #3838388f;padding:4rpx 10rpx
}
.video-box{width:100%}
.icon-center{
position: absolute;
top: 50%;
z-index: 4;
transform: translate(-50%, -50%);
left: 50%;
padding: 0 4rpx 0 6rpx;
}
.relative-shadow{
position: absolute;width:100%;height:100%;background: #83838387;z-index:1;
}
.file-card{
width:420rpx;
height:120rpx;
.file-icon{
width:60rpx;
height:80rpx;
}
.file-name{
text-align: left !important;
width:300rpx;
}
.file-size{
text-align: left !important;
margin-top:8rpx;
}
}
.icon-spin{
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.main .content ::v-deep uni-text ,
.main .content ::v-deep uni-text span,
.main .content ::v-deep text,
.main .content ::v-deep uni-rich-text{
word-wrap: break-word !important;
word-break: break-all !important;
}
.main .content ::v-deep ._block ._a{
pointer-events: none !important;
}
.text-container{
-webkit-user-select:text !important;
user-select:text !important;
font-size:48rpx;
word-wrap: break-word !important;
text-align: left;
line-height: 1.5;
letter-spacing: 1.2px;
color:#333;
}
.read-status{
font-weight: 600;
}
</style>
<style lang="scss" scoped>
.message-quote{
padding:8rpx;
font-size:24rpx;
margin-top:16rpx;
background-color: #e3e3e3;
overflow: hidden !important;
text-overflow: ellipsis;
white-space: nowrap !important;
max-width:380rpx;
text-align: left;
}
//
::v-deep .emoji-image{
vertical-align: text-top !important;
}
.cu-chat ::v-deep .cu-item {
padding: 20rpx;
}
.cu-chat ::v-deep .cu-item:last-child{
padding-bottom:60rpx;
}
.back-unread{
background-color: #e3e3e3;
padding:4rpx 10rpx;
border-radius: 50%;
font-size: 22rpx;
}
uni-page-body {
padding-bottom: 0;
}
.im-pagination{
z-index:10;
position: fixed;
bottom: 0;
padding:20rpx;
background-color: #fff;
width:100%;
}
.cu-bar.fixed, .nav.fixed{
z-index:998 !important;
}
</style>

79
pages/message/video.vue

@ -0,0 +1,79 @@
<template>
<cu-custom :isBack="true" style="color:white !important">
<template #backText>关闭视频</template>
</cu-custom>
<view class="video-model im-flex im-align-items-center" >
<video class="video-box" id="myVideo" :src="url" controls autoplay="autoplay" style="width:100%;height:100vh"></video>
<view class="opt-model im-flex im-align-items-center">
<button class="cu-btn round mr-10" @tap="download">保存到本地</button>
<button class="cu-btn round" @tap="closeModel">关闭</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
url:'',
name:''
}
},
onLoad(option){
this.url=decodeURI(option.src);
this.name=option.name;
},
mounted(){
},
methods: {
closeModel(){
uni.navigateBack();
},
download(){
// #ifndef H5
uni.downloadFile({
url: this.url,
success: (res) => {
if (res.statusCode === 200) {
uni.saveVideoToPhotosAlbum({
filePath: res.tempFilePath,
success: function () {
uni.showToast({
title:"已保存到相册",
icon:'none'
})
}
});
}
}
});
// #endif
// #ifdef H5
const tempLink = document.createElement("a");
tempLink.style.display = "none";
tempLink.href = this.url;
tempLink.setAttribute("download", this.name);
tempLink.setAttribute("target", "_blank");
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
// #endif
}
}
}
</script>
<style lang="scss">
.video-model{
background-color: #000;width: 100%;height: 100%;position: fixed;top:0;overflow:hidden;;
}
.opt-model{
position: absolute;bottom:100rpx;right:20rpx;padding:4rpx 10rpx;text-align: center;
.bm-btn{
width:120rpx;
}
}
.video-box{width:100%}
</style>

110
pages/mine/about.vue

@ -0,0 +1,110 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>关于IM</template>
</cu-custom>
<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="app-name">{{globalConfig.sysInfo.name ?? packData.name}}</view>
<view class="app-version mb-20 mt-10 c-999">Version {{packData.version}}</view>
<view class="padding">
<view class="mb-15">
<view class="cuIcon-title">支持单聊和群聊</view>
</view>
<view class="mb-15">
<view class="cuIcon-title">支持发送表情图片语音视频和文件消息</view>
</view>
<view class="mb-15">
<view class="cuIcon-title">单聊支持消息已读未读的状态显示在线状态显示</view>
</view>
<view class="mb-15">
<view class="cuIcon-title">群聊创建删除和群成员管理群公告群禁言等</view>
</view>
<view class="mb-15">
<view class="cuIcon-title">支持置顶联系人消息免打扰</view>
</view>
<view class="mb-15">
<view class="cuIcon-title">支持一对一音视频通话已打通web端和移动端</view>
</view>
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
const loginStore = useloginStore(pinia)
import packageData from "../../package.json"
export default {
data() {
return {
loginStore:loginStore,
globalConfig:loginStore.globalConfig,
packData:packageData
}
},
onShow() {
},
methods: {
logout(){
let client_id=uni.getStorageSync('client_id');
this.$api.LoginApi.logout({client_id:client_id}).then(res => {
if (res.code == 0) {
loginStore.logout()
}
})
},
showSetting(){
uni.showToast({
title:'请在web端进行资料设置',
icon:'none'
})
},
editInfo(){
uni.navigateTo({
url:"/pages/mine/profile"
})
},
setAvatar(){
uni.navigateTo({
url:"/pages/mine/avatar"
})
}
}
}
</script>
<style>
.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;
}
.app-name{
font-size:40rpx;
font-weight: blod;
text-align: center;
}
.app-version{
text-align: center;
}
</style>

114
pages/mine/doc.vue

@ -0,0 +1,114 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>帮助中心</template>
</cu-custom>
<view style="height:20rpx;"></view>
<uni-collapse accordion>
<uni-collapse-item :title="item.title" v-for="item in list" :open="item.id==1">
<view class="content f-14 pd-10 lh-15x">
<rich-text :nodes="item.content"></rich-text>
</view>
</uni-collapse-item>
</uni-collapse>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
const loginStore = useloginStore(pinia)
import packageData from "../../package.json"
export default {
data() {
return {
loginStore:loginStore,
globalConfig:loginStore.globalConfig,
packData:packageData,
list:[
{
id:1,
title:'如何开启手机通知',
content:`<div class='vditor-reset' id='preview'><ol>
<li>手机系统设置中当前APP允许通知需要开启通知设置底部有一个消息推送点击进去也要开启</li>
<li>手机系统设置中声音提醒需要开启手机系统设置需非静音/勿扰模式</li>
<li>Android系统需核实设备系统声音设置-&gt;通知铃声是否有选到的铃声文件若是系统版本大于等于17.2的iOS设备需在iOS手机系统-&gt;设置-&gt;声音与触感-&gt;默认提醒确认是否有选择提醒铃声</li>
<li>Android系统手机收不到消息提醒还需检查以下几项<br />
手机系统设置中APP-&gt;允许通知-&gt;是否设为重要是否设置为重要<br />
手机系统设置中锁屏后通知相关开关是否设置为锁屏后通知<br />
手机系统设置中如果有通知过滤规则需将当前APP的通知过滤规则设置为重要<br />
手机后台设置中网络是否可用<br />
如果手机在后台运行锁屏状态未运行状态时无法收到提醒需检查自启动设置<br />
请确认手机是否开启了省电策略功能可在手机系统设置中-&gt;应用管理-&gt;当前APP-&gt;省电策略-&gt;设置为无限制或在系统设置中-&gt;电池-&gt;当前APP-&gt;启动管理-&gt;允许后台活动允许关联启动允许自启动关闭自动管理不同手机型号可能路径不同</li>
<li>安卓手机必须要将应用锁定到任务栏保证当前APP一直在后台运行还需要将APP保持为后台联网状态</li>
<li>iOS系统手机提醒需前往设置-&gt;当前APP-&gt;通知处查看</li>
</ol>
</div>`
},
{
id:2,
title:'为什么账号显示为未认证',
content:`认证状态为真实的手机号或者邮箱注册的账户才会显示为认证状态,如果管理员没有开启手机号或者邮箱号注册,未认证状态是无法更改的!请联系管理员处理。`
}
]
}
},
onShow() {
},
methods: {
logout(){
let client_id=uni.getStorageSync('client_id');
this.$api.LoginApi.logout({client_id:client_id}).then(res => {
if (res.code == 0) {
loginStore.logout()
}
})
},
showSetting(){
uni.showToast({
title:'请在web端进行资料设置',
icon:'none'
})
},
editInfo(){
uni.navigateTo({
url:"/pages/mine/profile"
})
},
setAvatar(){
uni.navigateTo({
url:"/pages/mine/avatar"
})
}
}
}
</script>
<style>
.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;
}
.app-name{
font-size:40rpx;
font-weight: blod;
text-align: center;
}
.app-version{
text-align: center;
}
</style>

168
pages/mine/index.vue

@ -0,0 +1,168 @@
<template>
<view>
<view class="padding flex im-space-between im-align-items-center bg-white mb-10">
<view class="flex justify-start bg-white" @tap="editInfo()">
<view class='cu-avatar lg mr-15' :class="appSetting.circleAvatar?'round':'radius'" :style="[{backgroundImage:'url('+loginStore.userInfo.avatar+')'}]">
</view>
<view class='im-flex im-justify-content-start im-columns'>
<view class="mb-5 f-18 mb-10 im-flex im-align-items-center">
<view class="c-333">{{loginStore.userInfo.realname}}</view>
<view class='cu-tag ml-10 round light' :class="loginStore.userInfo.is_auth ? 'bg-orange' : 'bg-grey'">{{loginStore.userInfo.is_auth ? '已认证' : '未认证'}}</view>
</view>
<view class="text-gray mb-10">{{loginStore.userInfo.account}}</view>
</view>
</view>
<view class="cuIcon-qrcode f-24 text-gray" @tap="openQr"></view>
</view>
<view class="cu-list menu">
<view class="cu-item" @tap="scan">
<view class="content">
<text class="cuIcon-scan text-blue"></text>
<text>扫一扫</text>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<view class="cu-item" @tap="showSetting">
<view class="content">
<text class="cuIcon-settings text-grey"></text>
<text>通用设置</text>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<view class="cu-item" @tap="showsecure">
<view class="content">
<text class="cuIcon-safe text-orange"></text>
<text>账号安全</text>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<view class="cu-item" @tap="showDoc">
<view class="content">
<text class="cuIcon-question"></text>
<text>帮助中心</text>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<!-- #ifdef APP-PLUS -->
<view class="cu-item" @click="checkVersion()">
<view class="content">
<text class="cuIcon-hot text-blue"></text>
<text>检查更新</text>
</view>
<view class="action">
<text class="text-grey">{{verisonName}} </text>
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<!-- #endif -->
<view class="cu-item" @click="about()" v-if="globalConfig.demon_mode">
<view class="content">
<text class="cuIcon-info text-green"></text>
<text>关于IM</text>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
</view>
<view></view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
import scan from '@/common/scan.js'
// #ifdef APP-PLUS
import appUpdate from '@/common/appUpdate.js';
// #endif
const loginStore = useloginStore(pinia)
export default {
data() {
return {
loginStore:loginStore,
globalConfig:loginStore.globalConfig,
appSetting:loginStore.appSetting,
versionCode:'',
verisonName:''
}
},
mounted() {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid,(inf) => {
this.versionCode = inf.versionCode
this.verisonName = inf.version
});
// #endif
},
methods: {
logout(){
let client_id=uni.getStorageSync('client_id');
this.$api.LoginApi.logout({client_id:client_id}).then(res => {
if (res.code == 0) {
loginStore.logout()
}
})
},
about(){
if(this.globalConfig && this.globalConfig.demon_mode){
uni.navigateTo({
url:"/pages/mine/about"
})
}else{
uni.navigateTo({
url:"/pages/mine/about"
})
}
},
showSetting(){
uni.navigateTo({
url:"/pages/mine/setting"
})
},
showsecure(){
uni.navigateTo({
url:"/pages/mine/secure"
})
},
showDoc(){
uni.navigateTo({
url:"/pages/mine/doc"
})
},
editInfo(){
uni.navigateTo({
url:"/pages/mine/profile"
})
},
scan(){
scan.scanQr();
},
openQr(){
uni.navigateTo({
url:"/pages/index/qrcode"
})
},
checkVersion(){
appUpdate(true);
}
}
}
</script>
<style>
</style>

184
pages/mine/profile.vue

@ -0,0 +1,184 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>个人信息</template>
</cu-custom>
<form>
<view class="cu-form-group" style="height:140rpx">
<view class="title">头像</view>
<view class="im-flex im-align-items-center">
<avatar
selWidth="240px" selHeight="480upx" expWidth="240px" expHeight="240px" @upload="uploadAvatar" :avatarSrc="userInfo.avatar"
avatarStyle="width: 100rpx; height: 100rpx; border-radius: 100%;" :quality="1">
</avatar>
<text class="cuIcon-right ml-10 f-18 text-grey"></text></view>
</view>
<view class="cu-form-group">
<view class="title">账号</view>
<view class="text-gray">{{userInfo.account}}</view>
</view>
<view class="cu-form-group">
<view class="title">{{globalConfig.sysInfo.runMode==1 ? "姓名" : '昵称'}}</view>
<view class="text-gray" v-if="globalConfig.sysInfo.runMode==1 && globalConfig.sysInfo.diyName!=1">{{userInfo.realname}}</view>
<input class="uni-input" style="text-align: right;" v-if="globalConfig.sysInfo.runMode==2 || globalConfig.sysInfo.diyName==1" v-model="userInfo.realname" placeholder="请输入昵称" />
</view>
<view class="cu-form-group">
<view class="title">e-mail</view>
<input class="uni-input" style="text-align: right;" v-model="userInfo.email" placeholder="请输入email地址" />
</view>
<view class="cu-form-group">
<view class="title">性别</view>
<view>
<radio-group @change="changeSex">
<label class="radio mr-10" v-for="x in sexList"><radio name="sex" :value="x.id" :checked="userInfo.sex==x.id" /> {{x.name}}</label>
</radio-group>
</view>
</view>
<view class="cu-form-group align-start">
<view class="title">个性签名</view>
<textarea maxlength="-1" v-model="userInfo.motto" placeholder="请输入个性签名"></textarea>
</view>
</form>
<view class="padding flex flex-direction">
<button class="cu-btn bg-green lg" :style="saved ? 'border: solid 1px #dbdada;' : ''" :disabled="saved" @tap="saveInfo()" >保存</button>
</view>
<view class="padding flex flex-direction" style="position: fixed;bottom: 0;width:100%;align-items: center;">
<text class="text-red" @tap="logout()">注销登录</text>
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
import avatar from "@/components/yq-avatar/yq-avatar.vue";
const loginStore = useloginStore(pinia)
export default {
components: {
avatar
},
data() {
return {
loginStore:loginStore,
globalConfig:loginStore.globalConfig,
userInfo:{},
sexList:[
{
id:'2',
name:'未知'
},
{
id:'1',
name:'男'
},
{
id:'0',
name:'女'
},
],
saved:false
}
},
mounted() {
this.userInfo=JSON.parse(JSON.stringify(loginStore.userInfo));
},
methods: {
logout(){
let client_id=uni.getStorageSync('client_id');
this.$api.LoginApi.logout({client_id:client_id}).then(res => {
if (res.code == 0) {
loginStore.logout()
}
})
},
textareaBInput(e) {
this.userInfo.motto = e.detail.value
},
changeSex(evt){
this.userInfo.sex=parseInt(evt.detail.value);
},
saveInfo(){
if(this.userInfo.realname == ''){
uni.showToast({
title:'请输入昵称',
icon:'none'
})
return false
}
let pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if(this.userInfo.email!='' && !pattern.test(this.userInfo.email)){
uni.showToast({
title:'请输入正确的邮箱',
icon:'none'
})
return false
}
//
this.saved=true;
let params={
realname:this.userInfo.realname,
email:this.userInfo.email,
sex:this.userInfo.sex,
motto:this.userInfo.motto
}
this.$api.msgApi.updateUserInfo(params).then(res=>{
if(res.code == 0){
uni.showToast({
title:'保存成功',
icon:'none'
})
let data=JSON.parse(JSON.stringify(this.userInfo))
loginStore.login(data)
}
})
// 8
setTimeout(()=>{
this.saved=false;
},8000)
},
setAvatar(){
uni.navigateTo({
url:"/pages/mine/avatar"
})
},
uploadAvatar(res){
uni.showLoading({
title:'上传中...'
})
uni.uploadFile({
url: this.$api.msgApi.uploadAvatar,
filePath: res.path,
name: 'file',
header: {
'Authorization': uni.getStorageSync('authToken'),
},
formData: {
ext: 'png'
},
success: (e) => {
uni.hideLoading();
let res=JSON.parse(e.data);
if(res.code==0){
uni.showToast({
title:res.msg,
icon:'none'
})
this.userInfo.avatar=res.data
let data=JSON.parse(JSON.stringify(this.userInfo));
loginStore.login(data);
}
},
fail: (res) => {
uni.hideLoading();
}
})
}
}
}
</script>
<style>
</style>

238
pages/mine/secure.vue

@ -0,0 +1,238 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>账号安全</template>
</cu-custom>
<view class="cu-list menu mt-10">
<view class="cu-item" @tap="editAcc">
<view class="content">
<text class="cuIcon-settings text-grey"></text>
<text>我的账号</text>
</view>
<view class="action">
<text>{{userInfo.account}}</text>
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<view class="cu-item" @tap="modelName='show';editPass=true">
<view class="content">
<text class="cuIcon-lock text-green"></text>
<text>修改密码</text>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<view class="cu-item" v-if='!userInfo.is_auth' @tap="modelName='show';editPass=false">
<view class="content padding-tb-sm">
<view>
<text class="cuIcon-vip text-orange ml-5"></text> <text class="ml-10">认证账户</text></view>
<view class="text-gray text-sm">
<text class="cuIcon-infofill ml-5 mr-10"></text> 验证账户的真实性绑定后请使用新账户来登录</view>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
<view class="padding flex flex-direction mt-40">
<button class="cu-btn bg-red lg" @tap="logout()">注销登录</button>
</view>
</view>
<view class="cu-modal bottom-modal" :class="modelName=='show'?'show':''" @tap="modelName=''">
<view class="cu-dialog" @tap.stop=''>
<view class="cu-bar bg-white">
<view class="action text-gray" @tap="modelName=''">取消</view>
<view class="action text-green" @tap="save">保存</view>
</view>
<view class="manage-content mb-20">
<view class="cu-list menu mt-15 bg-white">
<view class="cu-form-group text-right" v-if="userInfo.is_auth">
<view class="title">验证码</view>
<input placeholder="输入验证码" name="input" v-model="code" />
<button class='cu-btn bg-green shadow cu-load' :class="loading?'loading':''" :disabled="loading" @tap="sendCode(true)">发送验证码</button>
</view>
<template v-if="!editPass">
<view class="text-gray m-15 text-left">
<text class="cuIcon-infofill ml-5 mr-10"></text> 验证账户的真实性绑定后请使用新账户来登录
</view>
<view class="cu-form-group text-right">
<view class="title">新账号</view>
<input placeholder="输入新的邮箱或者手机号" name="input" v-model="account" />
</view>
<view class="cu-form-group text-right">
<view class="title">新账号验证码</view>
<input placeholder="输入验证码" name="input" v-model="newCode" />
<button class='cu-btn bg-green shadow cu-load' :class="loading?'loading':''" :disabled="loading" @tap="sendCode(false)">发送验证码</button>
</view>
</template>
<template v-else>
<view class="cu-form-group text-right" v-if="!userInfo.is_auth">
<view class="title">原密码</view>
<input placeholder="输入原来的密码" name="input" v-model="originalPassword" />
</view>
<view class="cu-form-group text-right">
<view class="title">新密码</view>
<input placeholder="输入新的密码" name="input" v-model="password" />
</view>
<view class="cu-form-group text-right">
<view class="title">重复新密码</view>
<input placeholder="重复输入新密码" name="input" v-model="repass" />
</view>
</template>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
const loginStore = useloginStore(pinia)
export default {
data() {
return {
userInfo:loginStore.userInfo,
globalConfig:loginStore.globalConfig,
modelName:'',
code:'',
account:'',
newCode:'',
password:'',
originalPassword:'',
repass:'',
loading:false,
editPass:false
}
},
onShow() {
},
methods: {
logout(){
let client_id=uni.getStorageSync('client_id');
this.$api.LoginApi.logout({client_id:client_id}).then(res => {
if (res.code == 0) {
loginStore.logout()
}
})
},
editAcc(){
if(!this.userInfo.is_auth){
uni.showToast({
title: '请先认证账户!',
icon: 'none'
});
return false;
}
this.modelName='show';
this.editPass=false;
},
save(){
if(this.code=='' && this.userInfo.is_auth){
uni.showToast({
title: '请输入验证码',
icon: 'none'
});
return false;
}
if(this.editPass){
if(this.password=='' || this.password.length<6 || this.password.length>16){
uni.showToast({
title: '请输入6-16个字符串的密码',
icon: 'none'
});
return false;
}
if(this.password!=this.repass){
uni.showToast({
title: '两次密码不一致',
icon: 'none'
});
return false;
}
let params = {
password:this.password,
code:this.code,
originalPassword:this.originalPassword
}
this.$api.msgApi.editPassword(params).then(res=>{
if(res.code==0){
this.modelName = '';
this.password = '';
this.repass = '';
uni.showToast({
title: res.msg,
icon: 'none'
});
}
})
}else{
if(this.account==''){
uni.showToast({
title: '请输入新的账号',
icon: 'none'
});
return false;
}
if(this.newCode==''){
uni.showToast({
title: '请输入新账号的验证码',
icon: 'none'
});
return false;
}
let params = {
account:this.account,
code:this.code,
newCode:this.newCode
}
this.$api.msgApi.editAccount(params).then(res=>{
if(res.code==0){
this.modelName = '';
this.account = '';
this.code = '';
this.newCode = '';
uni.showToast({
title: "修改成功,请重新登陆",
icon: 'none'
});
}
})
}
},
sendCode(e){
let account=e ? this.userInfo.account : this.account;
let type = this.editPass ? 3 : 4;
if(account==''){
uni.showToast({
title: '请输入新的账号',
icon: 'none'
});
return false;
}
this.loading = true;
this.$api.LoginApi.sendCode({account:account,type:type}).then((res)=>{
uni.showToast({
title: res.msg,
icon: "none"
});
this.loading=false;
})
}
}
}
</script>
<style>
.cu-load {
display: block;
line-height: 68rpx;
text-align: center;
}
</style>

92
pages/mine/setting.vue

@ -0,0 +1,92 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>通用设置</template>
</cu-custom>
<view class="cu-bar bg-white solid-bottom margin-top">
<view class="action">新消息</view>
</view>
<view class="cu-list menu">
<view class="cu-item">
<view class="content">
<text>声音</text>
</view>
<view class="action">
<switch class="switch" @change="setVoice" :class="setting.voiceStatus?'checked':''" :checked="setting.voiceStatus"></switch>
</view>
</view>
<view class="cu-item">
<view class="content">
<text>震动</text>
</view>
<view class="action">
<switch class="switch" @change="setVibrate" :class="setting.vibrateStatus?'checked':''" :checked="setting.vibrateStatus"></switch>
</view>
</view>
</view>
<view class="cu-bar bg-white solid-bottom margin-top">
<view class="action">其他设置</view>
</view>
<view class="cu-list menu">
<view class="cu-item">
<view class="content">
<text>圆形头像</text>
</view>
<view class="action">
<switch class="switch" @change="setAvatar" :class="setting.circleAvatar?'checked':''" :checked="setting.circleAvatar"></switch>
</view>
</view>
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
const loginStore = useloginStore(pinia)
export default {
data() {
return {
loginStore:loginStore,
globalConfig:loginStore.globalConfig,
setting:{
voiceStatus:true,
vibrateStatus:false,
circleAvatar:false
}
}
},
created() {
let setting=uni.getStorageSync('appSetting') ?? '';
if(setting){
this.setting=setting;
}
},
methods: {
setVoice(e){
this.setting.voiceStatus=e.detail.value
this.saveSet();
},
setVibrate(e){
this.setting.vibrateStatus=e.detail.value
this.saveSet();
},
setAvatar(e){
this.setting.circleAvatar=e.detail.value
this.saveSet();
},
saveSet(){
loginStore.setAppSetting(this.setting)
}
}
}
</script>
<style>
</style>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save