Browse Source

项目初始化

master
liaoxinyu 4 months ago
commit
931675ac3c
  1. 20
      .gitignore
  2. 33
      .hbuilderx/launch.json
  3. 51
      .vite/_cert.pem
  4. 1
      .vite/deps/package.json
  5. 881
      App.vue
  6. 201
      LICENSE
  7. 62
      README.md
  8. 38
      androidPrivacy.json
  9. 54
      api/compass.js
  10. 45
      api/emoji.js
  11. 48
      api/friend.js
  12. 15
      api/index.js
  13. 65
      api/login.js
  14. 366
      api/message.js
  15. 865
      common/appUpdate.js
  16. 65
      common/config.js
  17. 72
      common/scan.js
  18. 193
      common/socket.js
  19. BIN
      components/.DS_Store
  20. 53
      components/Empty.vue
  21. 75
      components/Tags.vue
  22. 63
      components/breadcrum.vue
  23. 79
      components/cu-custom.vue
  24. 433
      components/get-qrcode.vue
  25. 130
      components/im-touch.vue
  26. 81
      components/im-user.vue
  27. 43
      components/message/im-image.vue
  28. 1311
      components/message/im-input.vue
  29. 35
      components/message/im-item.vue
  30. 50
      components/message/im-tab.vue
  31. 301
      components/message/user-select.vue
  32. 461
      components/mosowe-canvas-image/mosowe-canvas-image.vue
  33. 195
      components/mosowe-canvas-image/readme.md
  34. 1623
      components/mosowe-canvas-image/wxqrcode.js
  35. 416
      components/sendtoImg.vue
  36. 92
      components/status.vue
  37. 1371
      components/yq-avatar/yq-avatar.vue
  38. BIN
      favicon.ico
  39. 0
      hybrid/html/groupchat.html
  40. BIN
      hybrid/html/image/guaduan.png
  41. BIN
      hybrid/html/image/jieting.png
  42. BIN
      hybrid/html/image/speaker-off.png
  43. BIN
      hybrid/html/image/speaker.png
  44. BIN
      hybrid/html/image/video.png
  45. BIN
      hybrid/html/image/voice-off.png
  46. BIN
      hybrid/html/image/voice.png
  47. BIN
      hybrid/html/image/wallpaper.png
  48. 613
      hybrid/html/index.html
  49. 275
      hybrid/html/js/jsonly.js
  50. 1
      hybrid/html/js/uni.webview.js
  51. 11
      hybrid/html/js/utils.js
  52. 1
      hybrid/html/js/vue.js
  53. 3480
      hybrid/html/rtc/adapter-latest.js
  54. 702
      hybrid/html/rtc/chrome/chrome_shim.js
  55. 50
      hybrid/html/rtc/chrome/getdisplaymedia.js
  56. 188
      hybrid/html/rtc/chrome/getusermedia.js
  57. 433
      hybrid/html/rtc/common_shim.js
  58. 296
      hybrid/html/rtc/firefox/firefox_shim.js
  59. 36
      hybrid/html/rtc/firefox/getdisplaymedia.js
  60. 67
      hybrid/html/rtc/firefox/getusermedia.js
  61. 353
      hybrid/html/rtc/safari/safari_shim.js
  62. 263
      hybrid/html/rtc/utils.js
  63. BIN
      hybrid/html/voice/calling.mp3
  64. 20
      index.html
  65. 54
      main.js
  66. 210
      manifest.json
  67. 144
      mixins/chat.js
  68. BIN
      nativeplugins/lemonjk-FileSelect/android/uniplugin_fileSelect-release.aar
  69. BIN
      nativeplugins/lemonjk-FileSelect/ios/DCTestUniPlugin.framework/DCTestUniPlugin
  70. BIN
      nativeplugins/lemonjk-FileSelect/ios/DCTestUniPlugin.framework/Info.plist
  71. 43
      nativeplugins/lemonjk-FileSelect/package.json
  72. 412
      package-lock.json
  73. 26
      package.json
  74. 247
      pages.json
  75. 156
      pages/compass/Informationdetails.vue
  76. 410
      pages/compass/friendscircledetails.vue
  77. 103
      pages/compass/index.vue
  78. 547
      pages/compass/moments.vue
  79. 313
      pages/compass/personalcircleoffriends.vue
  80. 445
      pages/compass/sendtoMoments.vue
  81. 101
      pages/contacts/blacklist.vue
  82. 327
      pages/contacts/detail.vue
  83. 150
      pages/contacts/friend.vue
  84. 106
      pages/contacts/group.vue
  85. 447
      pages/contacts/index.vue
  86. 88
      pages/contacts/search.vue
  87. 435
      pages/index/index.vue
  88. 225
      pages/index/qrcode.vue
  89. 34
      pages/index/scan.vue
  90. 112
      pages/index/search.vue
  91. 354
      pages/index/userSelection.vue
  92. 57
      pages/login/404.vue
  93. 214
      pages/login/index.vue
  94. 183
      pages/login/register.vue
  95. 41
      pages/mapselect/mapselect.vue
  96. 262
      pages/message/call.vue
  97. 1919
      pages/message/chat.vue
  98. 965
      pages/message/detail.vue
  99. 225
      pages/message/emoji.vue
  100. 349
      pages/message/group/groupUser.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" : "standard",
"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"}

881
App.vue

@ -0,0 +1,881 @@
<script>
// vue3 getCurrentInstance
import { getCurrentInstance } from 'vue'
import pinia from '@/store/index'
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login';
import permision from "@/utils/permission.js"
import config from "@/common/config";
import emoji from '@/utils/emoji.js';
// #ifdef APP-PLUS
import db from './utils/db';
import homeData from './service/homeData';
import getSystemInfo from './service/getSystemInfo';
import getMessageList from './service/getMessageList';
import groupInfo from './service/groupInfo';
import groupUserList from './service/groupUserList';
import getUserInfo from './service/getUserInfo';
// #endif
// #ifdef H5
import VConsole from 'vconsole';
// #endif
let vConsole = null; //H5
const msgStore = useMsgStore(pinia)
const userStore = useloginStore(pinia);
let keepAlive = null
let network_log = null
let networkCheckInterval = null
let emojiMap = []
// #ifdef APP-PLUS
import appUpdate from '@/common/appUpdate.js';
// 使ios
// keepAlive = uni.requireNativePlugin('Ba-KeepAlive');
// #endif
export default {
async created() {
// #ifdef APP-PLUS
await db.createDatabase()
await homeData.createTalbe();
await getSystemInfo.createTalbe();
await getMessageList.createTalbe();
await groupInfo.createTalbe();
await groupUserList.createTalbe();
await getUserInfo.createTalbe();
// #endif
// console.info('1111111');
},
onLaunch: function() {
this.startNetworkMonitor();
this.network_log = uni.getStorageSync('network_log')
// this.network_log = msgStore.network_log
const _this = this
if(this.network_log=='none') {
_this.getGroupData()
}else{
//
userStore.getGlobalConfig();
// console.log(config);
// _this.getGroupData()
}
// APP
let setting=uni.getStorageSync('appSetting') ?? '';
if(!setting){
userStore.setAppSetting({
voiceStatus:true,
vibrateStatus:false,
circleAvatar:false
});
}else{
userStore.setAppSetting(setting);
}
uni.getSystemInfo({
success: function(e) {
let paddingB=0;
// appContext
const {appContext} = getCurrentInstance();
appContext.config.globalProperties.StatusBar = e.statusBarHeight;
// #ifndef MP
if (e.platform == 'android') {
appContext.config.globalProperties.CustomBar = e.statusBarHeight + 50;
} else {
appContext.config.globalProperties.CustomBar = e.statusBarHeight + 45;
};
// #endif
// #ifdef MP-WEIXIN
let custom = wx.getMenuButtonBoundingClientRect();
appContext.config.globalProperties.Custom = custom;
appContext.config.globalProperties.CustomBar = custom.bottom + custom.top - e.statusBarHeight;
// #endif
// #ifdef MP-ALIPAY
appContext.config.globalProperties.CustomBar = e.statusBarHeight + e.titleBarHeight;
// #endif
// #ifdef MP
try {
var res = uni.getSystemInfoSync();
res.model = res.model.replace(' ', '');
res.model = res.model.toLowerCase();
var res1 = res.model.indexOf('iphonex');
if(res1 > 5){res1 = -1;}
var res2 = res.model.indexOf('iphone1');
if(res2 > 5){res2 = -1;}
if(res1 != -1 || res2 != -1){
paddingB = uni.upx2px(50);
}
} catch (e){return null;}
// #endif
// #ifdef H5
paddingB = uni.upx2px(100);
// #endif
//
appContext.config.globalProperties.inlineTools=paddingB;
//
appContext.config.globalProperties.navBarHeight=uni.upx2px(100);
}
})
//H5
// #ifdef H5
this.loadVConsole();
// #endif
// #ifdef APP-PLUS
//
appUpdate();
if (uni.getSystemInfoSync().platform === 'android' && keepAlive) {
// this.register();
}
// app
uni.onPushMessage((res) => {
let data=res.data;
if(res.type=='click'){
let playload=data.payload;
let toUser=playload.toContactId ? playload.toContactId : '';
//
if(playload.type=='webrtc'){
return;
}
uni.navigateTo({
url:"/pages/message/chat?id=" + toUser
})
}
})
// #endif
},
onUnload() {
this.scoketClose()
this.socketIo.traderDetailIndex = 100 // tabIndex
},
onShow: function() {
// #ifdef APP-PLUS
// ,
if (uni.getSystemInfoSync().platform === 'android' && keepAlive) {
// this.isRunning();
}
// #endif
if(!this.socketIo.checkStatus()){
// console.log('ws线');
uni.$emit('socketStatus',false);
this.getWebsocketData();
}
this.appStatus=true;
var userInfo = uni.getStorageSync('userInfo');
if(userInfo){
this.getNoticeCount()
}
// console.log('App Show')
let emojiMap=[];
//
emoji.forEach(function (item) {
let child=item.children;
if(child.length>0){
child.forEach(function (val) {
let name=val.name;
let src=val.src;
emojiMap[name]=src;
})
}
});
this.emojiMap=emojiMap;
},
methods:{
startNetworkMonitor() {
//
this.checkAndSaveNetworkStatus();
// 5
this.networkCheckInterval = setInterval(() => {
this.checkAndSaveNetworkStatus();
// if(this.network_log!=='none'&&this.$route.path!="/pages/login/404"){
// userStore.getGlobalConfig();
// }else if(this.$route.path=="/pages/login/404"){
// clearInterval(this.networkCheckInterval)
// }
}, 5000);
},
checkAndSaveNetworkStatus(){
uni.getNetworkType({
success (res) {
msgStore.network_log = res.networkType;
// userStore.getGlobalConfig();
uni.setStorageSync('network_log', res.networkType);
// uni.setStorageSync('network_log', 'none');
}
});
},
async getGroupData(){
let groups = await getSystemInfo.getList();
groups.forEach(item => {
item.sysInfo = JSON.parse(item.sysInfo);
item.chatInfo = JSON.parse(item.chatInfo);
item.fileUpload = JSON.parse(item.fileUpload);
item.compass = JSON.parse(item.compass);
item.demon_mode = JSON.parse(item.demon_mode);
})
let [result] = groups
console.info('处理后的数据:', result)
uni.setStorageSync('globalConfig',result)
},
//
loadVConsole() { //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(); //
},
getNoticeCount(){
this.$api.compaApi.getNoticeCount().then(res => {
msgStore.getCount(res.data.count);
})
},
getWebsocketData() {
//
var userInfo = uni.getStorageSync('userInfo');
var _this = this;
//
const data = {
type: "ping"
};
//
this.socketIo.connectSocketInit(data);
uni.$off("getPositonsOrder");
// ,
uni.$on("getPositonsOrder", (res) => {
var userInfo = uni.getStorageSync('userInfo');
let data=res.data;
this.connect = true;
switch (res['type']) {
// ping
case 'ping':
this.socketIo.send({
type: "pong"
});
break;
case 'pong':
userStore.$patch({
multiport: res.multiport
})
break;
//
case 'init':
// clientId
uni.setStorageSync('client_id', res.client_id);
if(userInfo){
this.$api.LoginApi.bindUid({
client_id: res['client_id'],
user_id: userInfo.user_id,
cid:uni.getStorageSync('cid')
}).then(e => {
this.socketIo.send({
type: "bindUid",
user_id: userInfo.user_id,
token:uni.getStorageSync("authToken")
});
}).catch(error => {})
}
break;
//线线
case "isOnline":
msgStore.updateContacts({
id: data.id,
is_online:data.is_online
});
// 线,,
if(!data.is_online && msgStore.webrtcLock==data.id){
msgStore.webrtcLock=false;
}
break;
case "offline":
let clientId=uni.getStorageSync('client_id');
let globalConfig=uni.getStorageSync('globalConfig');
//
if(globalConfig.sysInfo.multipleLogin==1){
break;
}else if(data.id==userInfo.user_id && data.client_id!=clientId && data.isMobile){
uni.showToast({
title: "您的账号在其他设备登录,已被迫下线!",
icon: "none",
duration: 2500
})
userStore.logout();
}
break;
case "updateConfig":
uni.setStorageSync('globalConfig',data);
userStore.globalConfig=data;
if(data.sysInfo.state==0){
uni.navigateTo({
url:'/pages/login/404'
})
}
break;
case 'simple':
case 'group':
// ,
if (data.fromUser.id != userInfo.user_id) {
const contact = msgStore.getContact(data.toContactId,data);
//
if (data.toContactId=='system' || contact.is_notice == 1) {
this.playSound();
}
// #ifdef APP-PLUS
var self=this;
const list = []
let imgSrcs = []
let emojiSrcs = []
list.push(data)
list.forEach((res)=>{
res.content = this.emojiToHtml(res.content)
const parts = res.fromUser.avatar.split('/')
let lastPart = parts.pop() || parts.pop() || ''
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart));
res.fromUser.imgname = isNumber ? lastPart+'.png' : lastPart;
if(res.type=='image'||res.type=='video'||res.type=='emoji'){
const parts1 = res.content.split('/')
let lastPart1 = parts1.pop() || parts1.pop() || ''
res.imgname = lastPart1
uni.downloadFile({ url: res.content,success: (downloadResult) => {
self.saveToPermanentStorage(downloadResult.tempFilePath);
}})
}else if(res.type=='text'){
const regex = /<img\s+[^>]*src="([^"]+)"/g;
let match;
while ((match = regex.exec(res.content)) !== null) {
imgSrcs.push(match[1]);
}
if(imgSrcs.length!==0){
imgSrcs.forEach((img)=>{
const parts = img.split('/')
let lastPart = parts.pop() || parts.pop() || ''
emojiSrcs.push(lastPart)
uni.downloadFile({ url: img,success: (downloadResult) => {
self.saveToPermanentStorage(downloadResult.tempFilePath);
}})
})
res.imgname = emojiSrcs
}
}
})
// console.log(list);
this.insertdata(list)
// #endif
}
this.appendMessage(res);
break;
case "setChatTop":
msgStore.updateContacts({
id: data.id,
is_top: data.is_top
});
break;
case "setIsNotice":
msgStore.updateContacts({
id: data.id,
is_notice: data.is_notice
});
break;
//
case "addGroup":
msgStore.appendContacts(data);
this.$api.LoginApi.bindGroup({ client_id: uni.getStorageSync('client_id'), group_id: data.id });
// uni.$emit('initContacts', true)
break;
//
case "setManager":
case "addGroupUser":
case "removeUser":
if(res['type']=='removeUser' && data.user_id == userInfo.user_id){
msgStore.deleteContacts({
id: data.group_id
})
}else{
msgStore.updateContacts({
id: data.group_id,
avatar: data.avatar
});
uni.$emit('updateGroup',res);
}
break;
//
case "undoMessage":
const tabid = uni.getStorageSync('tabid')
if(data.is_last){
msgStore.updateContacts({
id: data.toContactId,
lastContent: data.content
});
}else if(data.chat_identify&&data.from_user==tabid){
msgStore.checkMsg(data);
msgStore.appendMsg(data);
}
break;
//
case "delUserAllMsg":
// #ifdef APP-PLUS
this.deleteList1('user')
// #endif
break;
//
case "delGroupAllMsg":
// #ifdef APP-PLUS
this.deleteList1('group')
// #endif
break;
case "delMessageAll":
// #ifdef APP-PLUS
if(data.form_user){
this.deleteList(data)
// console.log('group_id',data)
}else if(data.group_id){
data.group_id = "'"+'group-' + data.group_id+"'"
this.deleteList(data)
}
// #endif
break;
case "delSystemAllMsg":
this.delAllMsg()
break;
//
case "editGroupName":
msgStore.updateContacts({
id: data.id,
displayName: data.displayName
});
break;
case "removeGroup":
msgStore.deleteContacts({
id: data.group_id
})
break;
//
case "setNotice":
msgStore.updateContacts({
id: data.group_id,
notice: data.notice
});
break;
//
case "groupSetting":
msgStore.updateContacts({
id: data.group_id,
setting: data.setting
});
break;
case "appendContact":
msgStore.appendContacts(data);
break;
case "postsNotice":
_this.getNoticeCount()
break;
case 'webrtc':
let platform='h5';
//#ifdef H5
platform='h5';
//#endif
//#ifdef APP-PLUS
platform= 'app';
//#endif
if(data.fromUser.id==userInfo.user_id){
// webrtc
if([902,903,905,906,907].includes(parseInt(data.extends.code))){
msgStore.webrtcLock=false;
}
//
if(data.extends.isMobile==1 || data.extends.event=='calling'){
if(data.extends.event=='calling'){
this.appendMessage(res);
}
return;
}
}
// 线,
if(data.extends.event == 'offer' || data.extends.event == 'answer' ){
msgStore.webrtcLock=true;
}else if(data.extends.event == 'hangup'){
msgStore.webrtcLock=false;
}
if(data.extends.event == 'calling'){
this.appendMessage(res);
const allroutes = getCurrentPages();
const cureentRoute = allroutes[allroutes.length - 1].route;
// ,线
if (cureentRoute == 'pages/message/call') {
this.$api.msgApi.sendToMsg({
toContactId:data.fromUser.user_id,
type:data.extends.type,
event:'busy',
status:data.extends.status,
code:907,
id:data.id,
msg_id:data.msg_id,
})
}else{
//
//#ifdef APP-PLUS || H5
msgStore.webrtcLock=data.fromUser.user_id;
uni.navigateTo({
url: '/pages/message/call?msg_id='+data.id+'&type='+data.extends.type+'&status='+data.extends.status+'&id='+data.fromUser.user_id+'&name='+data.fromUser.realname+'&avatar='+encodeURI(data.fromUser.avatar)
})
//#endif
}
}else{
uni.$emit('webrtcConn',data);
}
break;
default:
break;
}
})
//
uni.$on("connectError", () => {
this.connect = false
this.scoketError = true
uni.$emit('socketStatus',false);
})
},
async insertdata(data){
// console.log(data,'231');
await getMessageList.batchInsertOrUpdate(data)
},
async deleteList(data){
await getMessageList.deleteList(data)
},
async deleteList1(val){
const list = {group_id:val=='user'?0:1}
await getMessageList.deleteallList(list)
},
async delAllMsg(){
await getMessageList.delAllMsg()
this.deleteImageFolder()
},
deleteImageFolder() {
plus.io.resolveLocalFileSystemURL('_doc', function(docDir) {
docDir.getDirectory('image', { create: false }, function(imageDir) {
//
imageDir.removeRecursively(function() {
console.log('删除成功');
}, function(error) {
console.error('删除失败:', error);
});
}, function(error) {
// 1
if (error.code === 1) {
console.log('目录不存在,无需删除');
} else {
console.error('获取目录失败:', error);
}
});
}, function(error) {
console.error('无法访问 _doc 目录:', error);
});
},
emojiToHtml(str){
if(!str){
return;
}
let emojiMap=this.emojiMap;
return str.replace(/\[!(\w+)\]/gi, function (str, match) {
var file = match;
return emojiMap[file] ? "<img style=\"width:18px;height:18px;margin-right:5px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]");
});
},
appendMessage(res){
let data=res.data;
let userInfo = uni.getStorageSync('userInfo');
//
let toUser = data.toContactId;
if(data.toContactId == userInfo.user_id){
toUser = data.toUser
}
let contact=msgStore.getContact(toUser,data);
if(data.toContactId=='system'){
//
msgStore.$patch({
sysUnread: msgStore.sysUnread += 1,
})
}else{
// ,
let addUnread = 1;
//
if(data.fromUser.id==userInfo.user_id){
addUnread = 0;
}
let at=0;
// at@
if(data.at.includes(userInfo.user_id)){
at=1;
}
if (contact) {
contact.lastContent = data.content;
contact.lastSendTime = data.sendTime*1000;
contact.type = data.type;
contact.unread += addUnread;
contact.is_at += at;
//
msgStore.updateContacts(contact);
} else {
let newContact = {
id: data.toContactId,
displayName: data.fromUser.displayName,
avatar: data.fromUser.avatar,
lastContent: data.content,
lastSendTime: data.sendTime*1000,
is_group: data.is_group,
unread: addUnread,
is_top: 0,
dep_id: 0,
is_at:at
}
msgStore.appendContacts(newContact)
}
msgStore.catchSocketAction(res);
}
// #ifdef APP-PLUS
let appStatus=this.appStatus;
// app,
if(!appStatus && data.fromUser.id!=userInfo.user_id && contact.is_notice==1){
this.createPushMsg(data,contact);
}
// #endif
},
createPushMsg(data,contact){
console.info("创建通知栏");
var regex = /<[^>]+>/g; // HTML
let content=data.content.replace(regex, ''); // HTML
if(!['text','event','location','contact','create'].includes(data.type)){
let callVideo=data.extends.type ?? 0;
content=this.$util.getMsgType(data.type,callVideo);
}
if(data.is_group==1){
content=data.fromUser.displayName+':'+content;
}
let message={
title:contact.displayName,
content:content,
payload:data,
}
let systemInfo = uni.getSystemInfoSync();
// ,
if (systemInfo.platform === 'android') {
uni.downloadFile({
url: contact.avatar,
success: (res) => {
if (res.statusCode === 200) {
message.icon=res.tempFilePath
uni.createPushMessage(message)
}
}
});
} else if (systemInfo.platform === 'ios') {
uni.createPushMessage(message)
}
},
playSound() {
let setting=uni.getStorageSync('appSetting') ?? '';
if(setting.voiceStatus){
let _this = this
// _this.playing = true
const innerAudioContext = uni.createInnerAudioContext();
innerAudioContext.autoplay = true;
innerAudioContext.src = config.apiUrl+'/static/voice/notify.mp3';
innerAudioContext.onError((res) => {
//
})
innerAudioContext.onStop((res) => {
// _this.playing=false
_this.$forceUpdate()
})
}
if(setting.vibrateStatus){
uni.vibrateLong({
success: function () {
console.log('手机震动');
}
});
}
},
// App
saveToPermanentStorage(tempPath) {
return new Promise((resolve, reject) => {
//
plus.io.resolveLocalFileSystemURL(
'_doc',
(docDir) => {
//
docDir.getDirectory(
'image',
{ create: true, exclusive: false },
(entry) => {
//
const fileName = this.getFileName(tempPath);
const fileName1 = this.getFileName(docDir.fullPath + 'image/' +fileName);
// console.log(fileName);
// console.log(fileName1);
//
entry.getFile(fileName1,{ create: false }, //
(fileEntry) => {
// console.log('');
//
reject(new Error('File already exists: ' + fileName));
},
(error) => {
// console.log(error);
//
if (error.code === 14) { // 1
this.copyFile(tempPath, entry, fileName, resolve, reject);
} else {
reject(error);
}
}
);
},
(error) => {
reject(error);
}
);
},
(error) => {
reject(error);
}
);
});
},
//
copyFile(tempPath, targetDir, fileName, resolve, reject) {
plus.io.resolveLocalFileSystemURL(
tempPath,
(tempEntry) => {
tempEntry.copyTo(
targetDir,
fileName,
(newEntry) => {
resolve(newEntry.toLocalURL());
},
(error) => {
reject(error);
}
);
},
(error) => {
reject(error);
}
);
},
//
getFileName(path) {
const index = path.lastIndexOf('/');
let fileName = path.substr(index + 1);
fileName = fileName.replace(/\(\d+\)(?=\.[^./]+$)/, '');
return fileName;
},
},
onHide: function() {
this.appStatus=false;
console.log('App Hide')
}
}
</script>
<style lang="scss">
@import url("static/css/iconfont.css");
@import url("static/css/main.css");
@import url("static/css/icon.css");
@import url("static/css/reset.css");
/*每个页面公共css */
@import '@/uni_modules/uni-scss/index.scss';
/* #ifndef APP-NVUE */
@import '@/static/customicons.css';
//
page {
background-color: #f5f5f5;
}
/* #endif */
.example-info {
font-size: 14px;
color: #333;
padding: 10px;
}
</style>

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

54
api/compass.js

@ -0,0 +1,54 @@
import {postRequest,postJsonRequest,apiUrl} from '@/utils/request.js';
let compaApi = {}
// 朋友圈帖子列表
compaApi.wechatMomentsList = (params) =>{
return postJsonRequest('enterprise/posts/index', params)
}
// 发布朋友圈
compaApi.wechatMomentsadd = (params) =>{
return postJsonRequest('enterprise/posts/add', params)
}
// 朋友圈点赞/取消点赞
compaApi.onlikes = (params) =>{
return postJsonRequest('enterprise/posts/like', params)
}
// 评论/回复
compaApi.oncomments = (params) =>{
return postJsonRequest('enterprise/posts/comment', params)
}
// 获取上次保存草稿数据
compaApi.getLastPosts = (params) =>{
return postJsonRequest('enterprise/posts/getLastPosts', params)
}
// 删除帖子
compaApi.Deleteapost = (params) =>{
return postJsonRequest('enterprise/posts/del', params)
}
// 我的朋友圈
compaApi.myPosts = (params) =>{
return postJsonRequest('enterprise/posts/myPosts', params)
}
// 我的朋友圈
compaApi.detailsList = (params) =>{
return postJsonRequest('enterprise/posts/details', params)
}
// 获取未读消息数量
compaApi.getNoticeCount = (params) =>{
return postJsonRequest('enterprise/posts/getNoticeCount', params)
}
// 消息通知列表接口
compaApi.getNoticeList = (params) =>{
return postJsonRequest('enterprise/posts/getNoticeList', params)
}
export default compaApi;

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;

15
api/index.js

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

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;

366
api/message.js

@ -0,0 +1,366 @@
// 统一请求路径前缀在libs/axios.js中修改
import {
postRequest,
postJsonRequest,
apiUrl
} from '@/utils/request.js';
let msgApi = {}
msgApi.uploadUrl=apiUrl+'/common/upload/uploadFile';
// 上传头像
msgApi.uploadAvatar=apiUrl+'/common/upload/uploadAvatar';
/**
* @desc 网络图片上传
* @param {*} 参数
*/
msgApi.uploadFileImage = (params) => {
return postJsonRequest('/common/upload/uploadFileImage', params)
}
/**
* @desc 个人聊天定时删除消息
* @param {*} 参数
*/
msgApi.friendClearMsgDay = (params) => {
return postJsonRequest('/enterprise/friend/setClearMsgDay', params)
}
/**
* @desc 群聊天定时删除消息
* @param {*} 参数
*/
msgApi.groupClearMsgDay = (params) => {
return postJsonRequest('/enterprise/group/setClearMsgDay', params)
}
/**
* @desc 单聊一键清除所有消息
* @param {*} 参数
*/
msgApi.friendremoveAllMessage = (params) => {
return postJsonRequest('/enterprise/friend/removeAllMessage', params)
}
/**
* @desc 群聊一键清除所有消息
* @param {*} 参数
*/
msgApi.groupremoveAllMessage = (params) => {
return postJsonRequest('/enterprise/group/removeAllMessage', params)
}
/**
* @desc 普通消息列表小程序
* @param {*} 参数
*/
msgApi.initContacts = (params) => {
return postJsonRequest('/enterprise/im/getContacts', params)
}
/**
* @desc AI对话
* @param {*} 参数
*/
msgApi.sendChat = (params) => {
return postJsonRequest('/enterprise/chat/sendChat', params)
}
/**
* @desc 普通消息列表小程序
* @param {*} 参数
*/
msgApi.getMessageList = (params) => {
return postJsonRequest('/enterprise/im/getMessageList', params)
}
/**
* @desc 设置聊天置顶
* @param {*} 参数
*/
msgApi.setChatTopAPI = (params) => {
return postJsonRequest('/enterprise/im/setChatTop', params)
}
/**
* @desc 获取好友信息
* @param {*} 参数
*/
msgApi.getFriendInfo = (params) => {
return postJsonRequest('enterprise/friend/getFriendInfo', params)
}
/**
* @desc 删除聊天
* @param {*} 参数
*/
msgApi.delChatAPI = (params) => {
return postJsonRequest('/enterprise/im/delChat', params)
}
/**
* @desc 发送文本聊天消息
* @param {*} 参数
*/
msgApi.sendMessage = (params) => {
return postJsonRequest('/enterprise/im/sendMessage', params)
}
/**
* @desc 转发聊天消息
* @param {*} 参数
*/
msgApi.forwardMessage = (params) => {
return postJsonRequest('/enterprise/im/forwardMessage', params)
}
/**
* @desc 设置消息已读
* @param {*} 参数
*/
msgApi.setMsgIsRead = (params) => {
return postJsonRequest('/enterprise/im/setMsgIsRead', params)
}
/**
* @desc 设置艾特消息已读
* @param {*} 参数
*/
msgApi.readAtMsg = (params) => {
return postJsonRequest('/enterprise/im/readAtMsg', params)
}
/**
* 撤回消息
* @param {*} data
*/
msgApi.undoMessage = (params) => {
return postJsonRequest('/enterprise/im/undoMessage', params)
}
/**
* 删除消息
* @param {*} data
*/
msgApi.delMessage = (params) => {
return postJsonRequest('/enterprise/im/delMessage', params)
}
/**
* 删除消息个人
* @param {*} data
*/
msgApi.removeMessage = (params) => {
return postJsonRequest('/enterprise/im/removeMessage', params)
}
/**
* 加入黑名单
* @param {*} data
*/
msgApi.isBlacklist = (params) => {
return postJsonRequest('/enterprise/friend/isBlacklist', params)
}
/**
* 发送ws消息
* @param {*} data
*/
msgApi.sendToMsg = (params) => {
return postJsonRequest('/enterprise/im/sendToMsg', params)
}
/**
* 消息免打扰
* @param {*} data
*/
msgApi.isNoticeAPI = (params) => {
return postJsonRequest('/enterprise/im/isNotice', params)
}
/**
* 更新业务卡片
* @param {*} data
*/
msgApi.updateCard = (params) => {
return postJsonRequest('/enterprise/im/updateCard', params)
}
/**
* 同意或者忽略团队
* @param {*} data
*/
msgApi.joinGroup = (params) => {
return postJsonRequest('/enterprise/group/joinGroup', params)
}
/**
* 获取群成员
* @param {*} data
*/
msgApi.groupUserList = (params) => {
return postJsonRequest('/enterprise/group/groupUserList', params)
}
/**
* 修改群公告
* @param {*} data
*/
msgApi.setNotice = (params) => {
return postJsonRequest('/enterprise/group/setNotice', params)
}
/**
* 群管理信息
* @param {*} data
*/
msgApi.groupInfo = (params) => {
return postJsonRequest('/enterprise/group/groupInfo', params)
}
/**
* 加入群聊
* @param {*} data
*/
msgApi.joinGroup = (params) => {
return postJsonRequest('/enterprise/group/joinGroup', params)
}
/**
* 修改群管理信息
* @param {*} data
*/
msgApi.groupSetting = (params) => {
return postJsonRequest('/enterprise/group/groupSetting', params)
}
/**
* 转让管理权限
* @param {*} data
*/
msgApi.changeOwner = (params) => {
return postJsonRequest('/enterprise/group/changeOwner', params)
}
/**
* 获取全部人员
* @param {*} data
*/
msgApi.getAllUser = (params) => {
return postJsonRequest('/enterprise/group/getAllUser', params)
}
/**
* 创建群聊
* @param {*} data
*/
msgApi.addGroup = (params) => {
return postJsonRequest('/enterprise/group/add', params)
}
/**
* 绑定群聊
* @param {*} data
*/
msgApi.bindGroup = (params) => {
return postJsonRequest('/common/index/bindGroup', params)
}
/**
* 修改群聊名字
* @param {*} data
*/
msgApi.editGroupName = (params) => {
return postJsonRequest('/enterprise/group/editGroupName', params)
}
/**
* 添加群成员
* @param {*} data
*/
msgApi.addGroupUser = (params) => {
return postJsonRequest('/enterprise/group/addGroupUser', params)
}
/**
* 删除群成员
* @param {*} data
*/
msgApi.removeUser = (params) => {
return postJsonRequest('/enterprise/group/removeUser', params)
}
/**
* 删除群聊
* @param {*} data
*/
msgApi.removeGroup = (params) => {
return postJsonRequest('/enterprise/group/removeGroup', params)
}
/**
* 删除群聊聊天记录
* @param {*} data
*/
msgApi.clearMessage = (params) => {
return postJsonRequest('/enterprise/group/clearMessage', params)
}
/**
* 设置/取消管理员
* @param {*} data
*/
msgApi.setManager = (params) => {
return postJsonRequest('/enterprise/group/setManager', params)
}
/**
* 设置禁言
* @param {*} data
*/
msgApi.setNoSpeak = (params) => {
return postJsonRequest('/enterprise/group/setNoSpeak', params)
}
/**
* 获取成员信息
* @param {*} data
*/
msgApi.getUserInfo = (params) => {
return postJsonRequest('/enterprise/im/getUserInfo', params)
}
// 搜索用户
msgApi.searchUser= (params) =>{
return postJsonRequest('enterprise/im/searchUser', params)
}
// 修改用户信息
msgApi.updateUserInfo= (params) =>{
return postJsonRequest('enterprise/im/updateUserInfo', params)
}
// 修改账号
msgApi.editAccount= (params) =>{
return postJsonRequest('enterprise/im/editAccount', params)
}
// 修改密码
msgApi.editPassword= (params) =>{
return postJsonRequest('enterprise/im/editPassword', params)
}
// 获取联系人信息
msgApi.contactInfo= (params) =>{
return postJsonRequest('enterprise/im/getContactInfo', params)
}
// 获取公告信息
msgApi.getAdminNotice= (params) =>{
return postJsonRequest('enterprise/im/getAdminNotice', params)
}
// 获取聊天原图
msgApi.viewOriginalImage= (params) =>{
return postJsonRequest('enterprise/files/viewOriginalImage', params)
}
export default msgApi;

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

65
common/config.js

@ -0,0 +1,65 @@
import CryptoJS from "crypto-js";
// 如果为开启ssl证书,请修改为http协议
// let scheme ="https"; //协议头
let scheme ="http"; //协议头
// 请将下面的域名替换成自己的服务器域名,如果有端口要把端口加上
let host = '192.168.66.16:8007';
// 为了避免h5页面被其他人盗用,可以自由选择加密方式,详细教程查看readme.md文件
let hostToken="sha5602584d3bc770dd47e9680a99d0efe6bi";
// 是否开启H5的调试模式
let isVConsole = false
// 以下内容请勿修改
// 以下内容请勿修改
// 以下内容请勿修改
// 以下内容请勿修改
// #ifdef MP-WEIXIN
let env = wx.getAccountInfoSync()
if (env.miniProgram.envVersion == 'develop') {
isVConsole = true
}
// #endif
// #ifdef APP-PLUS || H5
if (process.env.NODE_ENV === 'development') {
isVConsole = true
}
// #endif
let apiUrl = scheme + '://' + host;
let wssUrl = (scheme == 'https' ? 'wss' :'ws') + '://' + host + '/wss';
let hostMd5=CryptoJS.MD5(CryptoJS.MD5(host).toString()).toString();
if('sha'+hostMd5+'bi'!=hostToken){
apiUrl = 'false';
wssUrl = 'false';
}
// app更新的配置
const updateConfig = {
url:apiUrl+'/common/pub/checkVersion', //检查版本的接口
bgColor:'', //升级主色,按钮背景颜色
iconUrl:'' //升级小图标
}
/*
* | 参数名称 | 一定返回 | 类型 | 描述
* | -------------|--------- | --------- | ------------- |
* | versionCode | y | int | 版本号 20240331 |
* | versionName | y | String | 版本名称 :4.0.1 |
* | versionInfo | y | String | 版本信息 修复了bug |
* | updateType | y | String | forcibly = 强制更新, solicit = 弹窗确认更新, silent = 静默更新 |
* | downloadUrl | y | String | 版本下载链接IOS安装包更新请放跳转store应用商店链接,安卓apk和wgt文件放文件下载链接 |
*/
export default {
apiUrl,
wssUrl,
isVConsole,
updateConfig
}

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>

81
components/im-user.vue

@ -0,0 +1,81 @@
<template>
<view>
<!-- #ifdef APP-PLUS -->
<view v-for="(items,indexs) in imglist" :key="indexs" v-if="network_log=='none'">
<view class="cu-avatar lg" v-if="info.imgname === items.name" :class="appSetting.circleAvatar?'round':'radius'" @tap="openUserInfo(info)" :style="[{backgroundImage:'url('+ items.path +')'}]"></view>
</view>
<view v-else class="cu-avatar lg" :class="appSetting.circleAvatar?'round':'radius'" @tap="openUserInfo(info)" :style="[{backgroundImage:'url('+ info.avatar +')'}]"></view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view class="cu-avatar lg" :class="appSetting.circleAvatar?'round':'radius'" @tap="openUserInfo(info)" :style="[{backgroundImage:'url('+ info.avatar +')'}]"></view>
<!-- #endif -->
</view>
</template>
<script>
const userInfo=uni.getStorageSync('userInfo');
const appSetting=uni.getStorageSync('appSetting');
import { useMsgStore } from '@/store/message';
// #ifdef APP-PLUS
import {getSavedImages} from '@/utils/LocalFileSystemURL.js'
// #endif
import { storeToRefs } from 'pinia';
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
const {network_log} = storeToRefs(msgStore);
export default{
name : "im-touch",
props : {
info:{type:Object, default:function(){return {};}},
circleAvatar:{type:Boolean, default:false},
profile:{type:Boolean, default:false},
},
data() {
return {
toucheTimer : 0,
fingerRes : [],
distance : 0,
taptimer : 100,
appSetting:appSetting,
imglist:[],
network_log:network_log
}
},
created() {
// #ifdef APP-PLUS
this.getImagePath();
// #endif
// console.info(this.info,'');
},
methods:{
async getImagePath(){
this.imglist = await getSavedImages()
this.imglist.map(item => {
item.path = plus.io.convertLocalFileSystemURL(item.path)
});
// console.info(this.imglist,'');
},
//
openUserInfo(item){
let friend=msgStore.getContact(item.user_id);
// if(!this.profile && !friend){
// uni.showToast({
// title:'',
// icon:'none'
// })
// return false;
// }
// if(item.id==userInfo.user_id) return;
// uni.redirectTo({
// url:"/pages/contacts/detail?id="+this.info.id
// })
uni.navigateTo({
url:"/pages/contacts/detail?id="+this.info.id
})
}
}
}
</script>
<style scoped></style>

43
components/message/im-image.vue

@ -0,0 +1,43 @@
<template name="im-image">
<template v-if="src && info && info.fixMode">
<image v-if="info.fixMode<=2" :src="src" class="radius" :mode="info.fixMode==1 ? 'widthFix' : 'heightFix' " :style="info.fixMode==1 ? 'width:200px' : 'height:240px'" @tap="showImgs" :data-img="src" ></image>
<image v-if="info.fixMode==3" :src="src" class="radius" mode="scaleToFill" :style="[{width:info.width+'px',height:info.height+'px'}]" @tap="showImgs" :data-img="src" ></image>
</template>
<template v-else-if="!src && info && info.width">
<image :src="src" class="radius" mode="scaleToFill" :style="$util.imageCoverStyle(info.width,info.height)" @tap="showImgs" :data-img="src" ></image>
</template>
<template v-else>
<image :src="src" style="width:200px" class="radius" mode="widthFix" @tap="showImgs" :data-img="src" ></image>
</template>
</template>
<script>
export default {
name : "im-image",
props : {
info:{type:Object, default:function(){return {};}},
src:{type:String, default:''},
isview:{type:Object, default:function(){return {};}},
},
emits:['showImgs','viewImgs'],
data() {
return {
}
},
created : function(){
},
methods:{
showImgs(e){
if(this.isview.is_view==0){
this.$api.msgApi.viewOriginalImage({file_id:this.isview.file_id,msg_id:this.isview.msg_id}).then(res => {
// console.log(res);
const valobj = {img:res.data.src,is_view:this.isview.is_view}
this.$emit('showImgs',valobj);
this.$emit('viewImgs',{src:res.data.src,msg_id:this.isview.msg_id});
})
}else{
this.$emit('showImgs',e);
}
},
}
}
</script>

1311
components/message/im-input.vue

File diff suppressed because it is too large

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>

301
components/message/user-select.vue

@ -0,0 +1,301 @@
<template>
<view>
<view class="cu-bar bg-white search">
<view class="search-form round">
<text class="cuIcon-search"></text>
<input type="text" v-model="keywords" placeholder="搜索联系人" confirm-type="search"/>
</view>
</view>
<view class="gui-padding" style="padding-bottom: 120rpx;" v-if="!keywords">
<view class="cu-list menu">
<view class="cu-item arrow" v-if="isAuth" @tap="atAll">
<view class='cu-avatar mr-15 group-bg sm'>
</view>
<view class="content">
<text class="c-333">所有人</text>
</view>
</view>
</view>
<uni-data-checkbox :multiple="multiple" class="user-list-select" @change="chooseUser" v-model="changeUser" :localdata="lists"></uni-data-checkbox>
<Empty v-if="!lists.length" noDatatext="无联系人" textcolor="#999" ></Empty>
</view>
<view class="gui-padding" style="padding-bottom: 120rpx;" v-if="keywords">
<uni-data-checkbox :multiple="multiple" class="user-list-select" @change="chooseSearchUser" v-model="searcheUser" :localdata="searchList"></uni-data-checkbox>
<Empty v-if="!searchList.length" noDatatext="未搜索到数据" textcolor="#999" ></Empty>
</view>
<view class="cu-bar bg-white tabbar border shop footer-opt">
<scroll-view class="scroll-view_H" scroll-x="true" :scroll-anchoring="true" :scroll-left="scrollLeft">
<view class="user-list-avatar">
<template v-for="(item,index) in selectUser" :key="index">
<image class="user-avatar" :src="item.avatar" @tap="removeUser(item.id,item.user_id)"></image>
</template>
</view>
<view class="select-num pd-10">已选{{selectUser.length}}</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { storeToRefs } from 'pinia';
import pinia from '@/store/index'
import { useloginStore } from '@/store/login';
const userStore = useloginStore(pinia)
export default {
name : "user-select",
props : {
type:{type:Number, default:0},//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 if(this.type == 5){
this.getAllUser()
this.changeUser = this.user_ids
this.selectUser = uni.getStorageSync('selectUser')
} else if(this.type == 6){
this.getAllUser()
this.changeUser = this.user_ids
this.selectUser = uni.getStorageSync('selectUser1')
}else {
this.getAllUser()
}
let lists=JSON.parse(JSON.stringify(this.userList));
this.lists=lists;
},
methods: {
getAllUser() {
const allContact=uni.getStorageSync('allContacts');
let contact=[];
if(this.type==3){
contact = allContact.filter((item)=>{
return item.id!=this.contact_id;
})
contact.forEach(item => {
item.disable = false;
let name=item.displayName;
if(item.is_group==1){
name+="(群聊)";
}
item.text=name;
item.value=item.id;
})
}else{
contact = allContact.filter((item)=>{
return item.is_group==0;
})
this.contacList=JSON.parse(JSON.stringify(contact));
contact.forEach(item => {
item.disable = false;
item.text=item.displayName;
item.value=item.id;
if (this.user_ids.length && this.type==2) {
if (this.user_ids.includes(item.id)) {
item.disable = true
}
}
})
if(this.type==1 && this.contact_id){
this.changeUser.push(parseInt(this.contact_id));
this.selectUser=contact.filter((item)=>{
return item.id==this.contact_id;
})
}
}
this.userList = contact
this.lists = contact
},
getGroupUser(){
this.userList = []
this.$api.msgApi.groupUserList({
group_id: this.contact_id
}).then(res => {
const isAuth=res.data.filter(item => (item.role == 1 || item.role == 2) && item.userInfo.id== this.userInfo.user_id)
if(isAuth.length) this.isAuth=true;
const allUser=res.data;
allUser.forEach((item,index)=>{
item.realname=item.userInfo.displayName;
item.displayName=item.userInfo.displayName;
item.avatar=item.userInfo.avatar;
item.name_py=item.userInfo.name_py;
item.disable = false;
item.text=item.userInfo.displayName;
item.value=item.userInfo.id;
})
const userList=res.data.filter(item => item.userInfo.id != this.userInfo.user_id)
this.lists=userList;
console.log(this.lists);
this.userList = userList;
})
},
//
chooseUser(e){
if(this.multiple){
this.selectUser=e.detail.data;
}else{
this.selectUser=[e.detail.data];
}
setTimeout(()=>{
this.scrollLeft+=300;
},50)
},
//
atAll(){
this.$emit('setData',{realname:'所有人',user_id:0});
},
//
chooseSearchUser(e){
if(this.multiple){
this.selectUser=this.$util.mergeArrays(e.detail.data,this.selectUser);
}else{
this.selectUser=[e.detail.data];
}
setTimeout(()=>{
this.scrollLeft+=300;
},50)
},
//
removeUser(id,user_id){
this.selectUser=this.selectUser.filter((e)=>{
return e.id!=id;
})
this.changeUser=this.changeUser.filter((e)=>{
return e!=user_id;
})
},
//
search: function(e) {
let contact=JSON.parse(JSON.stringify(this.lists));
this.searchList=this.$util.searchObject(contact,['displayName','name_py'],this.keywords);
},
//
confirm: function(e) {
let arr = []
if (e) { //
var brr = this.userList.filter(value => {
//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>

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

@ -0,0 +1,461 @@
<!-- mosowe-canvas-image -->
<template>
<view class='mosowe-canvas-image'>
<view class="slot-view" @click="createCanvas">
<slot></slot>
</view>
<view class="canvas-wrap-box">
<!-- 主面板绘制 -->
<canvas class="canvas-wrap" canvas-id="canvas" :style="'width: '+ width +'px; height: '+ height +'px;'"></canvas>
<!-- 这个是用来绘制圆形图片的 -->
<canvas class="canvas-wrap" canvas-id="canvas-arc" :style="'width: '+ canvasArcWidth +'px; height: '+ canvasArcHeight +'px;'"></canvas>
</view>
</view>
</template>
<script>
import QR from './wxqrcode.js';
export default {
name: 'mosowe-canvas-image',
components: {},
props: {
imgType: { //
type: String,
default: 'jpg',
validator: () => {
return ['jpg', 'png'];
}
},
compress: { //
type: Boolean,
default: false
},
compressSize: { // 2M
type: [Number, String],
default: 1024*1024*2
},
showPreview: { //
type: Boolean,
default: false
},
height: { // canvas
type: [String, Number],
default: 200
},
width: { // canvas
type: [String, Number],
default: 200
},
lists: {
type: Array,
default: () => {
return [];
}
}
},
data () {
return {
canvas: null,
listsIndex: 0,
listsLength: 0,
canvasArc: null,
canvasArcWidth: 100,
canvasArcHeight: 100,
compressQuality: 20,
compressQualityH5: 5,
};
},
watch: {},
//
beforeCreate () {},
//
created () {
this.canvas = uni.createCanvasContext('canvas', this);
this.canvasArc = uni.createCanvasContext('canvas-arc', this);
},
//
beforeMount () {},
//
mounted () {},
//
beforeUpdate () {},
//
updated () {},
//
beforeDestroy () {},
//
destroyed () {},
//
methods: {
//
createCanvas () {
this.clearCanvas();
if (this.lists.length === 0) {
uni.showToast({
title: 'lists不能为空',
icon: 'none'
});
return;
}
this.listsIndex = 0;
this.listsLength = this.lists.length - 1;
uni.showLoading({
title: '正在生成图片...',
mask: true
});
setTimeout(()=>{
uni.hideLoading();
},10000);
this.dataDrawCanvas();
},
//
dataDrawCanvas () {
let item = this.lists[this.listsIndex];
if (item.type === 'image') { //
if (item.content.indexOf('https://') > -1||item.content.indexOf('http://') > -1) { // https://
// #ifndef H5
// H5
this.downloadImageNotH5(item);
// #endif
// #ifdef H5
// H5
this.downloadImageH5(item);
// #endif
} else { //
if (this.compress && item.hasOwnProperty('file') && item.file.size > this.compressSize) { // 2M
this.compressImage(item);
} else {
if (item.arc) {
this.drawImageArc(item);
} else {
this.drawImage(item);
}
}
}
} else if (item.type === 'text') { //
this.drawText(item);
} else if (item.type === 'rect') { // 线
this.drawRect(item);
} else if (item.type === 'arc') { //
this.drawArc(item);
} else if (item.type === 'qr') { //
this.drawQR(item);
}
},
// #ifndef H5
// 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) {
console.log(item.content);
if (/\.(jpg|png)$/i.test(item.content)) {
//
this.checkDrawOver();
}else{
let image = null;
image = new Image();
image.setAttribute('crossOrigin', 'anonymous');
image.crossOrigin = 'Anonymous';
image.src = item.content;
image.onload = () => {
let canvas = document.createElement('canvas');
canvas.width = item.width;
canvas.height = item.height;
let ctx = canvas.getContext('2d');
ctx.drawImage(
image,
0,
0,
item.width,
item.height
);
let dataURL = canvas.toDataURL('image/png');
if (item.arc) { //
item.content = dataURL;
this.drawImageArc(item);
} else {
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1;
this.canvas.drawImage(
dataURL,
item.x,
item.y,
item.hasOwnProperty('width') ? item.width : this.width,
item.hasOwnProperty('height') ? item.height : this.height
);
this.checkDrawOver();
}
};
}
},
// #endif
//
compressImage (item) {
uni.showLoading({
title: '压缩中...',
mask: true
});
// H5
// #ifndef H5
uni.compressImage({
src: item.content,
quality: this.compressQuality,
success: (res) => {
uni.showLoading({
title: '正在生成图片...',
mask: true
});
item.content = res.tempFilePath;
if (item.arc) {
this.drawImageArc(item);
} else {
this.drawImage(item);
}
},
fail: (res) => {
console.log(res);
uni.showToast({
title: '压缩失败',
icon: 'none'
});
}
});
// #endif
// H5
// #ifdef H5
let image = new Image();
image.setAttribute('crossOrigin', 'anonymous');
image.crossOrigin = 'Anonymous';
image.src = item.content;
image.onload = () => {
let canvas = document.createElement('canvas');
canvas.width = item.width;
canvas.height = item.height;
let ctx = canvas.getContext('2d');
ctx.drawImage(
image,
0,
0,
item.width,
item.height
);
let dataURL = canvas.toDataURL('image/png');
item.content = dataURL;
if (item.arc) {
this.drawImageArc(item);
} else {
this.drawImage(item);
}
};
// #endif
},
// canvas,png
drawImageArc (item) {
this.canvasArc.clearRect(0, 0, this.canvasArcWidth, this.canvasArcHeight);
this.canvasArcWidth = item.arcR * 2;
this.canvasArcHeight = item.arcR * 2;
this.canvasArc.save();
let arcT = setTimeout(() => {
clearTimeout(arcT);
this.canvasArc.arc(item.arcR, item.arcR, item.arcR, 0, 2 * Math.PI);
this.canvasArc.clip();
// this.canvasArc.closePath();
this.canvasArc.drawImage(
item.content,
item.arcX,
item.arcY,
item.width,
item.height
);
this.canvasArc.draw(false, setTimeout(() => {
let t = setTimeout(() => {
clearTimeout(t);
uni.canvasToTempFilePath({
x: 0,
y: 0,
width: item.arcR * 2,
height: item.arcR * 2,
fileType: 'png',
canvasId: 'canvas-arc',
success: (res) => {
item.width = item.arcR * 2;
item.height = item.arcR * 2;
item.content = res.tempFilePath;
this.drawImage(item);
},
fail: (res) => {
console.log(res);
},
complete: () => {
this.canvasArc.restore();
this.canvasArc.fillRect(0, 0, 0, 0);
this.canvasArc.clearRect(0, 0, this.canvasArcWidth, this.canvasArcHeight);
}
}, this);
}, 100);
}));
}, 100);
},
//
drawImage (item) {
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1;
this.canvas.drawImage(
item.content,
item.x,
item.y,
item.hasOwnProperty('width') ? item.width : this.width,
item.hasOwnProperty('height') ? item.height : this.height
);
this.checkDrawOver();
},
//
drawText (item) {
this.canvas.setFillStyle(item.hasOwnProperty('color') ? item.color : '#000000');
this.canvas.setFontSize(item.hasOwnProperty('size')? item.size : 20);
this.canvas.setTextAlign(item.hasOwnProperty('align') ? item.align: 'left');
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1;
if (item.maxWidth) {
this.canvas.fillText(item.content, item.x, item.y, item.maxWidth);
} else {
this.canvas.fillText(item.content, item.x, item.y);
}
this.checkDrawOver();
},
// 线
drawRect (item) {
this.canvas.setFillStyle(item.hasOwnProperty('color') ? item.color : '#000000');
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1;
this.canvas.fillRect(item.x, item.y, item.width, item.height);
this.checkDrawOver();
},
//
drawArc (item) {
this.canvas.arc(item.arcX, item.arcY, item.arcR, 0, 2 * Math.PI);
this.canvas.setFillStyle(item.hasOwnProperty('color') ? item.color : '#000000');
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1;
this.canvas.fill();
this.canvas.closePath();
this.checkDrawOver();
},
//
drawQR (item) {
let len=item.content.length;
let typeNumber=Math.ceil(len/16);
if(typeNumber<4){
typeNumber=4;
}
item['qr'] = QR.createQrCodeImg(item.content, {
size: parseInt(300),
typeNumber:typeNumber
});
this.canvas.globalAlpha = item.hasOwnProperty('globalAlpha') ? item.globalAlpha : 1;
this.canvas.drawImage(
item.qr,
item.x,
item.y,
item.hasOwnProperty('width') ? item.width : this.width,
item.hasOwnProperty('height') ? item.height : this.height
);
this.checkDrawOver();
},
//
checkDrawOver () {
if (this.listsIndex < this.listsLength) { // lists
this.listsIndex++;
this.dataDrawCanvas();
} else {
this.canvasImage();
}
},
//
canvasImage () {
this.listsIndex = 0;
this.canvas.draw(false, () => {
setTimeout(() => {
uni.canvasToTempFilePath({
x: 0,
y: 0,
width: Number(this.width),
height: Number(this.height),
fileType: this.imgType,
canvasId: 'canvas',
success: (res) => {
this.$emit('canvasImage', res.tempFilePath);
if (this.showPreview) {
this.showPreviewFn(res.tempFilePath);
}
},
fail: (res) => {
console.log(res);
},
complete: () => {
uni.hideLoading();
}
}, this);
}, 500);
});
},
//
showPreviewFn (img) {
uni.previewImage({
current: 0,
urls: [img]
});
},
//
clearCanvas () {
this.canvas.clearRect(0, 0, this.width, this.height);
},
}
};
</script>
<style lang='scss' scoped>
.mosowe-canvas-image{
overflow: hidden;
.canvas-wrap-box{
overflow: hidden;
height: 0;
width: 0;
position: fixed;
left:200%;
top: 0;
}
.canvas-wrap {
overflow: hidden;
height: 0;
width: 0;
}
}
</style>

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

416
components/sendtoImg.vue

@ -0,0 +1,416 @@
<template>
<view class="burst-wrap">
<view class="burst-wrap-bg">
<view>
<!-- 信息提交 -->
<view class="burst-info">
<view class="uni-uploader-body">
<view class="uni-uploader__files">
<!-- 图片 -->
<block v-for="(image,index) in imageList" :key="index">
<view class="uni-uploader__file">
<view class="icon iconfont icon-cuo">
<!-- @tap="delect(index)" -->
<uni-icons type="closeempty" color="#000" size="15" class="closeempty_icons" @tap="delect(index)"></uni-icons>
</view>
<image class="uni-uploader__img" :src="image" :data-src="image" @tap="previewImage"></image>
</view>
</block>
<!-- 视频 -->
<view v-if="src1.url" style="width: 100%;height: 100%;">
<view>
<!-- <video :src="src" id="myVideo" class="video" @play="playVideoFullscreen"></video>
<cover-view style="position: absolute;width: 160rpx;height: 160rpx;z-index: 999;color: #fff;display: flex;">
<cover-view style="width: 80%;" @click="playVideoFullscreen"></cover-view>
<cover-view style="width: 20%;height: 40rpx;font-size: 45rpx;" @click="delectVideo">×</cover-view>
</cover-view> -->
<view class='course-video' :style="src1.url ? $util.imageCoverStyle(270,480) : ''">
<view class="relative-shadow flex justify-center align-center" @tap="handlePlay(src1)">
<view class="cuIcon-video icon-center f-28 c-white"></view>
</view>
<image :src="src1.poster" mode="aspectFit" style="position: absolute;top: 0px;width: 100%;height: 100%;"></image>
<uni-icons @click="delectVideo" type="closeempty" size="30" style="position: absolute;top: 0px;right: 0px;z-index: 1;"></uni-icons>
</view>
</view>
</view>
<view class="uni-uploader__input-box" v-if="VideoOfImagesShow">
<view class="uni-uploader__input" @tap="chooseVideoImage">
<image src="/static/image/jiahao.png" mode="widthFix" style="width: 60rpx;"></image>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import config from '@/common/config.js'
import { chat } from '@/mixins/chat.js'
import imImage from '@/components/message/im-image.vue';
var sourceType = [
['camera'],
['album'],
['camera', 'album']
]
export default {
components:{
imImage
},
mixins:[chat],
props: {
img_arr1: { type: Array, default: ()=>[] }
},
data() {
return {
imageList:[],//
src1:{},//
sourceTypeIndex: 2,
checkedValue:true,
checkedIndex:0,
sourceType: ['拍摄', '相册', '拍摄或相册'],
cameraList: [{
value: 'back',
name: '后置摄像头',
checked: 'true'
},
{
value: 'front',
name: '前置摄像头'
},
],
cameraIndex: 0,
VideoOfImagesShow:true,
}
},
watch: {
img_arr1: {
immediate: true, //
handler(newVal) {
if(newVal.length > 0) {
// this.handleImageLoad(newVal); //
newVal.forEach((res)=>{
if(res.type==1){
this.imageList.push(config.apiUrl+res.src)
}else if(res.type==2){
this.src1 = {url:config.apiUrl+res.src,poster:config.apiUrl+res.privacy};
this.VideoOfImagesShow = false;
}
})
}
}
}
},
onUnload() {
this.src1 = {},
this.sourceTypeIndex = 2,
this.sourceType = ['拍摄', '相册', '拍摄或相册'];
},
methods: {
chooseVideoImage(){
if(this.imageList.length>0){
uni.showActionSheet({
title:"选择上传类型",
itemList: ['图片'],
success: (res) => {
console.log(res)
if(res.tapIndex == 0){
this.chooseImages()
} else {
this.chooseVideo()
}
}
})
}else{
uni.showActionSheet({
title:"选择上传类型",
itemList: ['图片','视频'],
success: (res) => {
console.log(res)
if(res.tapIndex == 0){
this.chooseImages()
} else {
this.chooseVideo()
}
}
})
}
},
playVideoFullscreen(){
//
const videoContext = uni.createVideoContext('myVideo')
//
videoContext.requestFullScreen({direction: 0});
},
chooseImages(){
//
uni.chooseImage({
count: 9, //9
// sizeType: ['original', 'compressed'], //
sourceType: ['album','camera'], //
success:(res)=> {
let igmFile = res.tempFilePaths;
console.log(igmFile,'123');
// 9
const remainingSlots = 9 - this.imageList.length;
if (remainingSlots <= 0) {
uni.showToast({ title: '最多上传9张图片', icon: 'none' });
return;
}
//
const filesToUpload = igmFile.slice(0, remainingSlots);
filesToUpload.map(fileurl => {
uni.uploadFile({
url:config.apiUrl+'/common/upload/uploadPosts',
method:"POST",
header:{
'Authorization':uni.getStorageSync('authToken')
},
filePath:fileurl,
name:'file',
success: (res) =>{
let imgUrls = JSON.parse(res.data); //
imgUrls.data.forEach((item)=>{
if (this.imageList.length < 9) {
this.imageList = this.imageList.concat(item.url);
}
})
this.$emit('send',this.imageList)
if(this.imageList.length>=9) {
this.VideoOfImagesShow = false;
} else {
this.VideoOfImagesShow = true;
}
}
})
})
},
});
},
chooseVideo(){
//
uni.chooseVideo({
maxDuration:60,
count: 9,
camera: this.cameraList[this.cameraIndex].value,
sourceType: ['album', 'camera'],
success: (responent) => {
let videoFile = responent.tempFilePath;
uni.uploadFile({
url:config.apiUrl+'/common/upload/uploadPosts',
method:"POST",
header:{
'Authorization':uni.getStorageSync('authToken')
},
filePath:videoFile,
name:'file',
success: (res) => {
let videoUrls = JSON.parse(res.data) //
// console.info(videoUrls,'12345');
// let videoUrls = res.data //
// this.imagesUrlPath = this.imagesUrlPath.concat(videoUrls);
this.src1 = videoUrls.data[0]; //
this.$emit('videourl',this.src1)
if(this.src1.url) {
this.itemList = ['图片']
this.VideoOfImagesShow = false;
} else {
this.itemList = ['图片','视频']
}
}
})
// this.src = responent.tempFilePath; //
}
})
},
previewImage(e) {
//
var current = e.target.dataset.src
uni.previewImage({
current: current,
urls: [current]
})
},
delect(index){
uni.showModal({
title: "提示",
content: "是否要删除该图片",
success: (res) => {
if (res.confirm) {
this.imageList.splice(index, 1)
if(this.imageList.length>=9) {
this.VideoOfImagesShow = false;
} else {
this.VideoOfImagesShow = true;
}
}
}
})
},
delectVideo(){
uni.showModal({
title:"提示",
content:"是否要删除此视频",
success:(res) =>{
if(res.confirm){
this.src1 = {}
this.VideoOfImagesShow = true;
}
}
})
}
}
}
</script>
<style scoped>
.burst-wrap{
width: 100%;
height: 100px;
margin-top: 20px;
}
/* .burst-wrap .burst-wrap-bg{
position: relative;
width: 100%;
height: 320rpx;
background:linear-gradient(90deg,rgba(251,91,80,1) 0%,rgba(240,45,51,1) 100%);
border-bottom-right-radius: 80rpx;
border-bottom-left-radius: 80rpx;
} */
.burst-wrap .burst-wrap-bg>view{
width: 100%;
height: 100%;
/* margin: 0 auto;
position: absolute;
top: 65rpx;
left: 0;
right: 0; */
}
.form-item{
width: 100%;
}
.form-item textarea{
display: block;
height: 220rpx;
width: 100%;
color: #AAAAAA;
font-size: 28rpx;
}
.uni-uploader__files{
display: flex;
flex-wrap: wrap;
}
.uni-uploader__file{
z-index: 999;
width: 160rpx;
height: 160rpx;
margin-right: 6px;
margin-top: 5px;
position: relative;
}
.uploader_video{
width: 160rpx;
height: 160rpx;
margin-right: 6px;
}
.uni-uploader__img {
width: 160rpx;
height: 160rpx;
}
.icon-cuo {
top: 5rpx;
right: 5px;
width: 20px;
height: 20px;
z-index: 999;
/* color: #FFFFFF; */
position: absolute;
border-radius: 20px;
/* background: linear-gradient(90deg,rgba(209, 209, 209, 1.0) 0%,rgba(209, 209, 209, 1.0) 100%); */
}
.video{
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
}
.login-input-box{
position: relative;
border-bottom: 1rpx solid #EEEEEE;
}
.login-input-box .forget,.login-input-box .phone{
position: absolute;
top: 0;
height: 100%;
z-index: 100;
}
.login-input-box .phone{
width: 100rpx;
left: 0;
color: #666666;
font-weight: bold;
}
.login-input-box .phone-input{
padding-left: 100rpx;
}
.address-wrap,.open-info{
margin-top: 20rpx;
}
.open-info>view:last-child{
font-size: 28rpx;
color: #999999;
}
.address-wrap .address {
background: #F2F2F2;
border-radius: 40rpx;
padding: 0 20rpx;
}
.user-set-btn{
margin: 40rpx;
background: linear-gradient(90deg,rgba(251,91,80,1) 0%,rgba(240,45,51,1) 100%);
color: #FFFFFF;
text-align: center;
height: 88rpx;
line-height: 88rpx;
}
.uni-uploader__input-box{
width: 160rpx;
height: 160rpx;
margin-top: 5px;
}
.uni-uploader__input{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ebebeb;
}
.closeempty_icons{
display: flex;
line-height: 20px;
justify-content: center;
}
.course-video{
overflow: hidden;
position: relative;
}
.relative-shadow{
z-index:1;
width:100%;
height:100%;
height: 240px;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
}
</style>

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

0
hybrid/html/groupchat.html

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

210
manifest.json

@ -0,0 +1,210 @@
{
"name" : "Raingad-IM",
"appid" : "__UNI__A902A60",
"description" : "聊天小应用",
"versionName" : "5.5.2",
"versionCode" : 20250107,
"transformPx" : false,
"app-plus" : {
/* 5+App */
"usingComponents" : true,
"nvueCompiler" : "uni-app",
"compilerVersion" : 3,
"nvueStyleCompiler" : "uni-app",
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {
"VideoPlayer" : {},
"Record" : {},
"UIWebview" : {},
"Camera" : {},
"Barcode" : {},
"Push" : {},
"SQLite" : {}
},
/* */
"distribute" : {
/* */
"android" : {
/* android */
"permissions" : [
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.MANAGE_APP_TOKENS\"/>",
"<uses-permission android:name=\"android.permission.MANAGE_DOCUMENTS\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.INSTALL_PACKAGES\"/>",
"<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 21,
"targetSdkVersion" : ""
},
"ios" : {
"dSYMs" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "需要发送图片消息",
"NSPhotoLibraryAddUsageDescription" : "需要下载图片或者视频",
"NSCameraUsageDescription" : "需要拍照照片或者视频发送给好友",
"NSMicrophoneUsageDescription" : "需要和好友语音通话或者发送语音消息",
"NSLocationWhenInUseUsageDescription" : "APP将要获取你的位置信息,用于保持APP的活跃,并不会记录您的位置。是否允许?",
"NSLocationAlwaysUsageDescription" : "App将要在后台持续获取您的位置,用于保持APP的活跃,并不会记录您的位置。是否允许?",
"NSLocationAlwaysAndWhenInUseUsageDescription" : "APP将要获取你的位置信息,用于保持APP的活跃,并不会记录您的位置。是否允许?"
},
"UIBackgroundModes" : "audio,location"
},
"huawei" : {
"permissions" : [
"ohos.permission.GET_NETWORK_INFO" // HarmonyOS
]
},
/* ios */
"sdkConfigs" : {
"ad" : {},
"push" : {},
"maps" : {
"amap" : {
"name" : "",
"appkey_ios" : "",
"appkey_android" : ""
}
},
"geolocation" : {
"system" : {
"__platform__" : [ "ios", "android" ]
}
}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
},
"splashscreen" : {
"useOriginalMsgbox" : true
}
},
"nativePlugins" : {
"lemonjk-FileSelect" : {
"__plugin_info__" : {
"name" : "FileSelect",
"description" : "文件选取插件",
"platforms" : "Android,iOS",
"url" : "",
"android_package_name" : "",
"ios_bundle_id" : "",
"isCloud" : false,
"bought" : -1,
"pid" : "",
"parameters" : {}
}
}
}
},
// "Ba-KeepAlive" : {
// "__plugin_info__" : {
// "name" : "安卓保活(采用多种主流技术) Ba-KeepAlive",
// "description" : "原生保活插件,支持市面上大部分机型,Android4.4到13.0 。为定位、推送、websocket、定时任务、蓝牙、聊天等保驾护航(**注意:**不保证支持所有机型和场景,建议先试用再购买)",
// "platforms" : "Android",
// "url" : "https://ext.dcloud.net.cn/plugin?id=9423",
// "android_package_name" : "uni.UNID1E78EC",
// "ios_bundle_id" : "",
// "isCloud" : true,
// "bought" : 1,
// "pid" : "9423",
// "parameters" : {}
// }
// }
/* SDK */
"quickapp" : {},
/* */
"mp-weixin" : {
/* */
"appid" : "wxd36ac7a23fbfcfea",
"setting" : {
"urlCheck" : false,
"minified" : true,
"ignoreDevUnusedFiles" : false,
"ignoreUploadUnusedFiles" : false
},
"lazyCodeLoading" : "requiredComponents",
"usingComponents" : true,
"permission" : {},
"unipush" : {
"enable" : false
},
"libVersion" : "latest"
},
"vueVersion" : "3",
"h5" : {
"router" : {
"mode" : "hash",
"base" : "./"
},
"devServer" : {
"https" : false,
"port" : ""
},
"title" : "raingad-IM",
"unipush" : {
"enable" : false
},
"sdkConfigs" : {
"maps" : {
"amap" : {
"key" : "",
"securityJsCode" : "",
"serviceHost" : ""
}
}
}
},
"fallbackLocale" : "zh-Hans"
}

144
mixins/chat.js

@ -0,0 +1,144 @@
import config from '@/common/config.js'
export const chat = {
data() {
return {
network_log:'',
apiUrl: config.apiUrl,
emojiMap:''
}
},
created: function() {
this.network_log = uni.getStorageSync('network_log')
},
methods: {
// 播放视频,禁止多个同时播放
handlePlay (item,iteme) {
// console.log(item);
if(this.network_log == 'none'){
uni.navigateTo({
url: '/pages/message/video?name='+item.fileName+'&src='+encodeURI(iteme.path),
animationType:"slide-in-bottom"
});
}else if(item.src||item.url){
const parts = item.src?item.src.split('/'):item.url.split('/');
let lastPart = parts.pop() || parts.pop() || '';
const url = item.src?this.apiUrl+item.src:item.url
uni.navigateTo({
url: '/pages/message/video?name='+lastPart+'&src='+encodeURI(url),
animationType:"slide-in-bottom",
});
}else{
uni.navigateTo({
url: '/pages/message/video?name='+item.fileName+'&src='+encodeURI(item.content),
animationType:"slide-in-bottom"
});
}
},
// 文件预览
previewFile(item){
if(this.islongPress){
return;
}
this.curMsg=item;
this.modelName='preview';
},
preview(val){
let item=this.curMsg;
let audioExt=['mp3','wav','acc'];
let extension = item.content.split('.').pop().toLowerCase();
if(audioExt.includes(extension) || val==2){
uni.navigateTo({
url: '/pages/mine/webview?title=文件预览&src='+encodeURIComponent(item.preview),
animationType:"slide-in-bottom"
});
return;
}
// #ifdef APP-PLUS || MP-WEIXIN
let exts=['doc', 'xls', 'ppt', 'pdf', 'docx', 'xlsx', 'pptx'];
if(exts.includes(extension)){
uni.showLoading({title: '文件加载中'});
uni.downloadFile({
url: item.content,
success: function (res) {
uni.hideLoading();
var filePath = res.tempFilePath;
uni.openDocument({
filePath: filePath,
showMenu: true,
success: function (res) {
console.info('打开文档成功');
}
});
},
fail() {
uni.hideLoading();
}
});
}else{
uni.showToast({
title:'该文件不支持预览!',
icon:'none'
})
}
// #endif
// #ifdef H5
const tempLink = document.createElement("a");
tempLink.style.display = "none";
tempLink.href = item.download;
tempLink.setAttribute("download", item.fileName);
tempLink.setAttribute("target", "_blank");
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
// #endif
},
// 图片预览
showImgs : function(e){
var imgs = [];
var imgs1 = [];
var imgsCurrent = e.is_view==0?e.img:e.currentTarget.dataset.img;
for (var i = 0; i < this.messageList.length; i++) {
if (this.messageList[i].type == 'image' || this.messageList[i].type == 'emoji') {
imgs.push(this.messageList[i].content);
}
}
if(e.is_view==0){
imgs1.push(imgsCurrent);
uni.previewImage({urls : imgs1});
}else{
uni.previewImage({urls : imgs, current : imgsCurrent});
}
},
openLocation(item){
uni.openLocation({
latitude: item.latitude,
longitude: item.longitude,
success: function () {
console.log('success');
}
});
},
// 打开用户详情
openContact(item){
uni.navigateTo({
url:"/pages/contacts/detail?id="+item.id
})
},
// 自动解析消息中的表情
emojiToHtml(str){
let emojiMap=this.emojiMap;
return str.replace(/\[!(\w+)\]/gi, function (str, match) {
var file = match;
return emojiMap[file] ? "<img class='mr-5' style=\"width:18px;height:18px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]");
});
},
fileSize(size){
return this.$util.getFileSize(size);
},
sendTime:function(mstime){
return this.$util.timeFormat(mstime);
},
}
}

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

412
package-lock.json

@ -0,0 +1,412 @@
{
"name": "Raingad-IM",
"version": "5.5.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/runtime": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="
},
"@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"requires": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
}
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
}
},
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
},
"copy-text-to-clipboard": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz",
"integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q=="
},
"core-js": {
"version": "3.42.0",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.42.0.tgz",
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g=="
},
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"requires": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
}
},
"define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"requires": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
}
},
"define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"requires": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
}
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
},
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"requires": {
"es-errors": "^1.3.0"
}
},
"eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
},
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
},
"get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"requires": {
"es-define-property": "^1.0.0"
}
},
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"requires": {
"has-symbols": "^1.0.3"
}
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
},
"highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="
},
"is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"requires": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
}
},
"is-date-object": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz",
"integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"requires": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
}
},
"is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"requires": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
}
},
"jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="
},
"linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"requires": {
"uc.micro": "^2.0.0"
}
},
"markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"requires": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
}
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
},
"mutation-observer": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/mutation-observer/-/mutation-observer-1.0.3.tgz",
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
},
"object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"requires": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
}
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
},
"pinia": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz",
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
"requires": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
}
},
"punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="
},
"quill": {
"version": "1.3.7",
"resolved": "https://registry.npmmirror.com/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"requires": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmmirror.com/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"requires": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
}
},
"regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
"integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"requires": {
"call-bind": "^1.0.8",
"define-properties": "^1.2.1",
"es-errors": "^1.3.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"set-function-name": "^2.0.2"
}
},
"set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"requires": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
}
},
"set-function-name": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"requires": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.2"
}
},
"uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
},
"vconsole": {
"version": "3.15.1",
"resolved": "https://registry.npmmirror.com/vconsole/-/vconsole-3.15.1.tgz",
"integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==",
"requires": {
"@babel/runtime": "^7.17.2",
"copy-text-to-clipboard": "^3.0.1",
"core-js": "^3.11.0",
"mutation-observer": "^1.0.3"
}
},
"vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="
}
}
}

26
package.json

@ -0,0 +1,26 @@
{
"name": "Raingad-IM",
"version": "5.5.2",
"description": "一款基于vue3.0的uniapp即时聊天工具",
"logo": "/static/image/logo.png",
"author": "Raingad",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gitee.com/raingad/instant-chat-uniapp.git"
},
"keywords": [],
"license": "ISC",
"dependencies": {
"crypto-js": "^4.2.0",
"highlight.js": "^11.11.1",
"jsqr": "^1.4.0",
"markdown-it": "^14.1.0",
"pinia": "^2.0.23",
"quill": "^1.3.7",
"vconsole": "^3.15.1"
}
}

247
pages.json

@ -0,0 +1,247 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/message/chat",
"style": {
"navigationBarTitleText": "聊天"
// "enablePullDownRefresh": true
}
},
{
"path": "pages/message/emoji",
"style": {
"navigationBarTitleText": "表情"
}
},
{
"path": "pages/contacts/index",
"style": {
"navigationBarTitleText": "联系人"
}
},
{
"path": "pages/message/detail",
"style": {
"navigationBarTitleText": "聊天信息"
}
},
{
"path": "pages/message/record",
"style": {
"navigationBarTitleText": "聊天记录"
}
},
{
"path": "pages/contacts/detail",
"style": {
"navigationBarTitleText": "用户信息"
}
},
{
"path": "pages/contacts/friend",
"style": {
"navigationBarTitleText": "新朋友"
}
},
{
"path": "pages/contacts/search",
"style": {
"navigationBarTitleText": "搜索朋友"
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/login/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/message/call",
"style": {
"navigationBarTitleText": "通话"
}
},
{
"path": "pages/message/video",
"style": {
"navigationBarTitleText": "视频播放"
}
},
{
"path": "pages/index/userSelection",
"style": {
"navigationBarTitleText": "用户选择"
}
},
{
"path": "pages/message/group/groupUser",
"style": {
"navigationBarTitleText": "群聊成员管理"
}
},
{
"path": "pages/index/qrcode",
"style": {
"navigationBarTitleText": "群二维码"
}
},
{
"path": "pages/message/group/info",
"style": {
"navigationBarTitleText": "群信息"
}
},
{
"path": "pages/contacts/group",
"style": {
"navigationBarTitleText": "群聊列表"
}
},
{
"path": "pages/index/search",
"style": {
"navigationBarTitleText": "搜索"
}
},
{
"path": "pages/index/scan",
"style": {
"navigationBarTitleText": "扫描"
}
},
{
"path": "pages/mine/webview",
"style": {
"navigationBarTitleText": "浏览器",
"navigationStyle":"default"
}
},
{
"path": "pages/compass/moments",
"style": {
"navigationBarTitleText": "朋友圈"
}
},
{
"path": "pages/mine/profile",
"style": {
"navigationBarTitleText": "个人信息"
}
},
{
"path": "pages/mine/secure",
"style": {
"navigationBarTitleText": "安全"
}
},
{
"path": "pages/mine/about",
"style": {
"navigationBarTitleText": "关于"
}
},
{
"path": "pages/mine/setting",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "pages/mine/doc",
"style": {
"navigationBarTitleText": "帮助文档"
}
},
{
"path" : "pages/compass/sendtoMoments",
"style" : {
"navigationBarTitleText" : ""
}
},
{
"path" : "pages/compass/personalcircleoffriends",
"style" :
{
"navigationBarTitleText" : ""
}
},
{
"path" : "pages/compass/friendscircledetails",
"style" :
{
"navigationBarTitleText" : ""
}
},
{
"path" : "pages/mapselect/mapselect",
"style" :
{
"navigationBarTitleText" : ""
}
},
{
"path" : "pages/login/404",
"style" :
{
"navigationBarTitleText" : ""
}
},
{
"path" : "pages/compass/Informationdetails",
"style" :
{
"navigationBarTitleText" : ""
}
},
{
"path" : "pages/contacts/blacklist",
"style" :
{
"navigationBarTitleText" : ""
}
}
],
"globalStyle": {
"navigationStyle": "custom",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"app-plus": {
"background": "#efeff4"
}
},
"tabBar": {
"custom": true,
"color": "#333",
"selectedColor": "#1AAD19",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"list": [{
"pagePath": "pages/index/index",
"iconPath": "static/image/tabbar/demo.png",
"selectedIconPath": "static/image/tabbar/demo.png",
"text": "首页"
},
{
"pagePath": "pages/contacts/index",
"iconPath": "static/image/tabbar/demo.png",
"selectedIconPath": "static/image/tabbar/demo.png",
"text": "通讯录"
}
]
}
}

156
pages/compass/Informationdetails.vue

@ -0,0 +1,156 @@
<template>
<cu-custom bgColor="bg-white" :isBack="true">
<template #backText></template>
<template #content>消息</template>
</cu-custom>
<!-- #ifdef APP-PLUS -->
<scroll-view scroll-y style="height: 88vh;" @scrolltolower="scrollbot">
<!-- #endif -->
<!-- #ifdef H5 -->
<scroll-view scroll-y style="height: 93vh;" @scrolltolower="scrollbot">
<!-- #endif -->
<view v-for="(item,index) in filteredList" :key="index" class="flex align-center" @click="JumpDetails(item)">
<view style="padding: 25rpx 25rpx 25rpx 20rpx;">
<image :src="item.option_user.avatar" mode="widthFix" style="width: 80rpx;border-radius: 10rpx;"></image>
</view>
<view class="flex align-center" style="border-bottom: 1px solid #e7e7e7;padding: 25rpx 0px;width: 100%;margin-right: 30rpx;justify-content: space-between;">
<view>
<view style="color: #576b95;font-size: 16px;margin-bottom: 10rpx;">{{item.option_user.nickname}}</view>
<view v-if="item.content">
<mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap;font-size: 13px;" :content="emojiToHtml(item.content)"/>
</view>
<view v-if="item.type==1">
<uni-icons type="heart" size="15" color="#576b95"></uni-icons>
</view>
<view style="font-size: 10px;margin-top: 10rpx;">{{item.create_time}}</view>
</view>
<view v-if="item.file.src">
<image :src="apiUrl+item.file.src" mode="widthFix" style="width: 80rpx;border-radius: 10rpx;"></image>
</view>
</view>
</view>
<view v-if="!showEarlier && hasUnread" @click="showEarlier = true" style="text-align: center; padding: 20rpx; color: #576b95;">
查看更早的消息...
</view>
<uni-load-more v-if="showEarlier" :status="status" :content-text="contentText"></uni-load-more>
</scroll-view>
</template>
<script>
import pinia from '@/store/index';
import config from "@/common/config";
import emoji from '@/utils/emoji.js';
import { useMsgStore } from '@/store/message';
const msgStore = useMsgStore(pinia)
export default {
data() {
return {
list:[],
apiUrl:config.apiUrl,
emojiMap:[],
page:1,
limit:20,
status: 'more',//
//
contentText: {
contentdown: '查看更多',
contentrefresh: '加载中....',
contentnomore: '没有更多咯'
},
showEarlier: false, //
hasUnread: false //
}
},
computed: {
filteredList() {
if (this.showEarlier) {
// is_read=1
return this.list.sort((a, b) => new Date(b.create_time) - new Date(a.create_time))
} else {
// is_read=0
const unread = this.list.filter(item => item.is_read === 0);
return unread.length ? unread : this.list;
}
}
},
created() {
let emojiMap=[];
//
emoji.forEach(function (item) {
let child=item.children;
if(child.length>0){
child.forEach(function (val) {
let name=val.name;
let src=val.src;
emojiMap[name]=src;
})
}
});
this.emojiMap=emojiMap;
},
onShow() {
this.getNoticeList()
},
methods: {
getNoticeList(){
let params = {page:this.page,limit:this.limit}
this.status = 'loading'; //
this.$api.compaApi.getNoticeList(params).then(res => {
//
if (res.data.length <= res.count) {
this.status = 'noMore'; //
} else {
this.status = 'more'; //
}
this.list = res.data
msgStore.getCount(0);
this.hasUnread = this.list.some(item => item.is_read === 0)
//
this.list.sort((a, b) => new Date(b.create_time) - new Date(a.create_time))
//
if (!this.hasUnread) {
this.showEarlier = true;
}
})
},
JumpDetails(item){
uni.navigateTo({
url:`/pages/compass/friendscircledetails?posts_id=${item.posts_id}&userid=${item.posts_user_id}`
})
},
scrollbot(){
if (this.status == 'noMore') {
return;
}
this.page++
this.getNoticeList()
// console.log('');
},
//
emojiToHtml(str){
if(!str){
return;
}
let emojiMap=this.emojiMap;
return str.replace(/\[!(\w+)\]/gi, function (str, match) {
var file = match;
return emojiMap[file] ? "<img class='mr-5' style=\"width:18px;height:18px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]");
});
},
}
}
</script>
<style scoped lang="scss">
.load-more-button {
font-size: 28rpx;
color: #576b95;
padding: 30rpx 0;
border-top: 1px solid #e7e7e7;
}
</style>

410
pages/compass/friendscircledetails.vue

@ -0,0 +1,410 @@
<template>
<cu-custom bgColor="bg-white" :isBack="true">
<template #backText></template>
<template #content>详情</template>
</cu-custom>
<scroll-view scroll-y style="height: 92vh;">
<view class="cu-card dynamic no-card">
<view class="cu-item shadow">
<view class="cu-list menu-avatar">
<view class="cu-item" style="margin-top: 20px;" v-if="list.user">
<view class="cu-avatar round lg" :style="[{backgroundImage:'url('+list.user.avatar+')'}]"></view>
<view class="content flex-sub">
<view>{{list.user.nickname}}</view>
<view class="text-gray text-sm flex justify-between">
{{list.create_time}}
</view>
</view>
</view>
</view>
<view class="text-content">
<!-- <mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap" :content="emojiToHtml(list.content)"/> -->
<view class="text-content" v-if="contenthtml(list.content)" @click="handleLink(list.content)" v-html="list.content"></view>
<view class="text-content" v-html="list.content" v-else></view>
</view>
<view class="grid" style="margin: 0px 24rpx;">
<view class="grid" style="margin: 2rpx 0px;padding: 0px 8rpx;" v-for="(itemss,indexs) in list.files" :key="indexs">
<!-- flex-sub -->
<view class="bg-img" v-if="itemss.type==1">
<image :src="apiUrl+itemss.src" mode="aspectFill" style="width: 200rpx;height: 200rpx;" :data-src="apiUrl+itemss.src" @tap="previewImage"></image>
</view>
<view class='course-video' :style="list.files ? $util.imageCoverStyle(270,480) : ''" v-if="itemss.type==2">
<view class="relative-shadow" @tap="handlePlay(itemss)" style="">
<view class="cuIcon-video icon-center f-28 c-white"></view>
</view>
<im-image style="position: absolute;top: 0px;" :src="apiUrl+itemss.privacy" ></im-image>
</view>
</view>
</view>
<view v-if="list.address" @tap="openLocation(list.location)" class="im-location-msg im-flex im-rows im-nowrap im-align-items-center radius-8 pd-10">
<view class="f-20 cuIcon-location pr-5"></view>
<view>
<view class="f-10 mb-5">{{list.address}}</view>
</view>
</view>
<view class="text-gray text-sm text-right padding">
<text @click="onLike(list.id)" :class="['cuIcon-appreciatefill', list.is_like === 1 ? 'text-red' : 'margin-lr-xs']"></text> {{list.likes?list.likes.length:0}}
<text @click="onComment(null)" class="cuIcon-messagefill margin-lr-xs"></text> {{list.comment?list.comment.length:0}}
<text v-if="list.user_id==userInfo.user_id" class="cuIcon-delete" style="margin-left: 5px;" @click="Delete(list.id)">删除</text>
</view>
<view v-show="boll" style="margin: 5px 15px;position: relative;box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2);">
<view style="padding-bottom: 45px;">
<editor id="editor" class="bg-white c-333" style="min-height: 50px;height: 50px;padding:0px 10px ;" :adjust-position="false" maxlength="300" cursor-spacing="10"
@input="changeMsgText" @ready="onEditorReady" :read-only="readOnly"> </editor>
</view>
<view style="position: absolute;bottom: 5px;right: 10px;">
<view style="display: flex;justify-content: space-between;">
<view>
<view class="im-menus cuIcon-emoji f-28 ml-5 mr-10" hover-class="tap" @click.stop="showAppBox"></view>
<scroll-view scroll-y class="icon" v-if="isFocus">
<view class="im-flex im-wrap im-justify-content-start im-align-items-center pd-10">
<view v-for="(item,index) in currentEmojiList" class="im-emoji-item" :key="index">
<image :src="item.src" style="width:44rpx;;height:44rpx" mode="aspectFit" lazy-load @tap="chooseEmoji(item)"></image>
</view>
</view>
</scroll-view>
</view>
<button class="cu-btn bg-green shadow " @click="sendTextMsg(list.id)">发送</button>
</view>
</view>
</view>
<view class="flex Likeview" style="flex-wrap: wrap;" v-if="list.likes?.length!==0">
<uni-icons type="heart" size="15" color="#576b95"></uni-icons>
<view v-for="(itemws,indexws) in list.likes" :key="indexws" class="flex align-center" style="margin: 5px;">
<!-- <view style="margin-left: 2px;">{{itemws.nickname}}</view> -->
<image :src="itemws.avatar" mode="widthFix" style="width: 60rpx;margin-left: 2px;"></image>
</view>
</view>
<view style="margin-bottom: 10px;" v-if="list.comment?.length!==0">
<view class="cu-list menu-avatar comment" style="margin-top: 0px;" v-for="(iteme,indexw) in list.comment" :key="indexw">
<view class="comment_view">
<view class="content">
<view class="flex align-center" style="gap: 12rpx;" @click.stop="onComment(iteme.id)">
<view>
<view class="flex" v-if="iteme.reply_user_name!==''">
<view>{{iteme.nickname}} <text style="color: #000;margin-right: 5px;">回复</text></view>
<view class="flex-sub"> {{iteme.reply_user_name}}</view>
</view>
<view v-else>{{iteme.nickname}}</view>
</view>
<view class="text-gray text-content text-df" style="flex: 1; min-width: 0; word-break: break-word;">
<!-- <mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap;color: #000;" :content="emojiToHtml(iteme.content)"/> -->
<view class="text-content content_img" style="color: #000;" v-if="contenthtml(iteme.content)" @click="handleLink(iteme.content)" v-html="iteme.content"></view>
<view class="text-content" style="color: #000;" v-html="iteme.content" v-else></view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script>
import { useloginStore } from '@/store/login';
import config from '@/common/config.js';
import emoji from '@/utils/emoji.js';
import Edit from '@/utils/edit.js';
import { chat } from '@/mixins/chat.js';
import imImage from '@/components/message/im-image.vue';
const loginStore = useloginStore()
export default {
components:{
imImage
},
mixins:[chat],
data() {
return {
list: {},
boll:false,
edit: null,
userid:'',
inputMsg:'',
posts_id:0,
emojiMap:[],
isFocus:false,
readOnly:false,
currentEmojiList:[],
apiUrl: config.apiUrl,
userInfo:loginStore.userInfo,
}
},
created(){
this.currentEmojiList=emoji[0]['children'];
let emojiMap=[];
//
emoji.forEach(function (item) {
let child=item.children;
if(child.length>0){
child.forEach(function (val) {
let name=val.name;
let src=val.src;
emojiMap[name]=src;
})
}
});
this.emojiMap=emojiMap;
},
onLoad(option) {
this.posts_id = option.posts_id
if(this.userInfo.user_id==option.userid){
this.userid = ""
}else{
this.userid = option.userid
}
},
onShow() {
if(this.userid){
this.init(this.userid)
}else{
this.init()
}
},
methods: {
showAppBox(){
this.isFocus = !this.isFocus;
},
//
chooseEmoji(item){
// #ifdef H5
this.editorCtx.insertImage({
src: item.src,
alt: item.title,
width: 18,
height: 18,
nowrap:true,
extClass:'emoji-image',
success: ()=>{
},
complete: ()=> {
this.editorCtx.blur();
},
});
// #endif
// #ifndef H5
this.readOnly= true
setTimeout(()=>{
this.editorCtx.insertImage({
src: item.src,
alt: item.title,
width: 18,
height: 18,
nowrap:true,
extClass:'emoji-image',
success: function() {
},
complete: ()=> {
this.readOnly= false
},
});
},10);
// #endif
},
onEditorReady() {
// #ifdef MP-BAIDU
this.editorCtx = requireDynamicLib('editorLib').createEditorContext('editor');
// #endif
// #ifdef APP-PLUS || MP-WEIXIN || H5
const query = uni.createSelectorQuery().in(this);
query.select('#editor').context((res) => {
this.edit = new Edit({context: res.context,maxCount: 300});
this.editorCtx = res.context
}).exec()
// #endif
},
previewImage(e) {
//
var current = e.target.dataset.src
uni.previewImage({
current: current,
urls: [current]
})
},
changeMsgText(e){
const txt=e.detail.text.replace(/\n/g, '');
if(txt=='' && e.detail.html=='<p><br></p>'){
this.inputMsg='';
}else{
this.inputMsg=e.detail.html;
}
},
onLike(id){
this.$api.compaApi.onlikes({posts_id:id}).then(res => {
if(this.userid){
this.init(this.userid)
}else{
this.init()
}
// this.list = res.data
// console.log(res);
})
},
onComment(id){
this.boll = !this.boll
if(id){
uni.setStorageSync('pid',id)
}
},
Delete(id){
const _this = this
uni.showModal({
title: "提示",
content: "是否要删除该图片",
success: (res) => {
if (res.confirm) {
_this.$api.compaApi.Deleteapost({posts_id:id}).then(res => {
if(_this.userid){
_this.init(_this.userid)
}else{
_this.init()
}
})
}else if (res.cancel) {
console.log('用户点击取消');
}
}
})
},
contenthtml(val){
return val && (val.includes('http://') || val.includes('https://'));
},
handleLink(url){
const textOnly = url.replace(/<[^>]+>/g, '');
const urlRegex = /https?:\/\/[^\s"']+/g;
const urls = textOnly.match(urlRegex) || [];
if(urls.length > 0){
urls.forEach((res,index)=>{
// #ifdef H5
window.open(urls[index], '_blank');
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(urls[index]);
// #endif
})
}else{
return
}
},
init(id){
this.$api.compaApi.detailsList({posts_id:+this.posts_id,friend_user_id:id?+id:''}).then(res => {
const [latitudeStr, longitudeStr] = res.data.location?.split('+');
const location = {
latitude: +latitudeStr?+latitudeStr:0,
longitude: +longitudeStr?+longitudeStr:0
};
res.data.location = location
this.list = res.data
})
},
sendTextMsg(id1){
const _this = this
const pid = uni.getStorageSync('pid')
this.editorCtx.getContents({
success:(e)=>{
let msg=e.html;
const text = /^\s*$/.test(msg.replace(/<[^>]+>/g, ""));
const hasImg = /<img\b/i.test(msg);
if(msg==''||(text && !hasImg)){
uni.showToast({ title: "内容不能为空", icon: "none" });
msg=''
_this.editorCtx.clear();
return
}
let params = {posts_id:id1,content:msg,pid:pid?pid:""}
// console.log(params);
_this.$api.compaApi.oncomments(params).then(res => {
uni.removeStorageSync('pid')
if(_this.userid){
_this.init(_this.userid)
}else{
_this.init()
}
_this.editorCtx.clear();
this.boll = false;
// _this.list = res.data
// console.log(res);
})
}
})
},
}
}
</script>
<style scoped lang="scss">
.im-input{
height:100%;
font-size: 28rpx;
min-height:150rpx;
max-height: 300rpx;
padding:14rpx 14rpx;
border-radius:10rpx;
word-break: break-all;
// margin:0 8rpx !important;
}
.im-location-msg{
color:#2B2E3D;
margin: 5px 10px;
text-align: left !important;
}
.relative-shadow{
z-index:1;
width:100%;
height:100%;
height: 240px;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
}
.Likeview{
padding: 5px;
display: flex;
color: #576b95;
align-items: center;
background-color: #f7f7f7;
margin: 0rem 0.9375rem 0rem 2rem;
}
.course-video{
overflow: hidden;
position: relative;
}
.comment_view{
height: auto;
color: #576b95;
padding: 20rpx 30rpx;
background-color: #f7f7f7;
margin: 0rem 0.9375rem 0rem 2rem;
}
.icon{
top: -440rpx;
left: -170rpx;
width: 400rpx;
height: 400rpx;
position: absolute;
border-radius: 10rpx;
background-color: #fff;
box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2);
.im-emoji-item{
padding:22rpx;
}
}
::v-deep .ql-editor{
padding-top: 10px;
}
::v-deep .content_img img{
width: 18px;
margin-right: 5px;
}
</style>

103
pages/compass/index.vue

@ -0,0 +1,103 @@
<template>
<view :style="{paddingBottom: paddingB+'px'}">
<view class="cu-list menu" :class="['sm-border','card-menu margin-top']" v-if="compass().mode == 1">
<block v-for="(item,index) in sortedList" :key="item.order">
<view class="cu-item" :class="'arrow'" @tap="openApp(item)">
<view class="content">
<image :src="item.icon" class="png" mode="aspectFit"></image>
<text class="text-grey">{{item.name}}</text>
</view>
<view class="content_num" v-if="item.name=='朋友圈'&&count>0">{{count>99?'99+':count}}</view>
</view>
</block>
</view>
<view class="cu-list grid" :class="['col-3',' margin-top']" v-if="compass().mode == 2">
<block v-for="(item,index) in sortedList" :key="item.order">
<view class="cu-item" @tap="openApp(item)">
<view>
<image :src="item.icon" style="height:100rpx;width: 100rpx;"></image>
<view class="cu-tag badge" v-if="item.badge!=0">
<block v-if="item.badge!=1">{{item.badge>99?'99+':item.badge}}</block>
</view>
</view>
<text>{{item.name}}</text>
</view>
</block>
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
import { storeToRefs } from 'pinia';
import { useMsgStore } from '@/store/message';
const loginStore = useloginStore(pinia)
const msgStore = useMsgStore(pinia)
const {NoticeCount} = storeToRefs(msgStore);
export default {
data() {
return {
isCard: true,
userInfo:loginStore.userInfo,
paddingB:0,
count:NoticeCount
};
},
computed:{
sortedList() {
return this.compass()?.list
.filter(item => item.status == 1)
.sort((a, b) => a.order - b.order)
}
},
created:function(){
// #ifdef H5
this.paddingB=this.inlineTools;
// #endif
// #ifndef H5
this.paddingB=this.navBarHeight+this.inlineTools;
// #endif
},
methods: {
compass(){
return loginStore.globalConfig.compass
},
openApp(item) {
let url=item.url;
if(item.type==2){
url='/pages/mine/webview?title='+item.name+'&src='+encodeURIComponent(item.url)
}
uni.navigateTo({
url:url
})
},
}
}
</script>
<style lang="scss">
.im-friend-header{
width:100%;height:400rpx;position: relative;
.im-friend-bg{
width:100%;height:300rpx;overflow: hidden;
.im-friend-image{
width:100%;height:300rpx;
}
}
}
.im-user{
position: absolute;right:60rpx;top:210rpx;overflow: hidden;
}
.content_num{
color: #fff;
padding: 1px 5px;
border-radius: 50px;
text-align: center;
background-color: red;
}
</style>

547
pages/compass/moments.vue

@ -0,0 +1,547 @@
<template>
<cu-custom bgColor="bg-white" :isBack="true">
<template #backText></template>
<template #content>朋友圈</template>
</cu-custom>
<!-- #ifdef APP-PLUS -->
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing"
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 88vh;">
<!-- #endif -->
<!-- #ifdef H5 -->
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing"
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 93vh;">
<!-- #endif -->
<!-- -->
<view @click="onicons" :style="{paddingBottom: paddingB+'px'}">
<view class="im-friend-header">
<view class="im-friend-bg">
<image class="im-friend-image" src="/static/image/user-card-bg.jpg" mode="widthFix"></image>
</view>
<view class="im-user im-flex im-justify-content-start align-center">
<text class="text-white mr-5">{{userInfo.realname}}</text>
<image class="radius-10" style="width:120rpx;height:120rpx" :src="userInfo.avatar" mode="widthFix"></image>
</view>
<view style="position: absolute;top: 15px;right: 18px;" @click="tomoments">
<image class="radius-10" style="width:50rpx;height:50rpx" src="/static/image/moments.png" mode="widthFix"></image>
</view>
</view>
<view class="text-center" style="background-color: #fff;padding: 10px;" @click="tonotice" v-if="notice>0">
<view style="width: 100px;height: 30px;background-color: #f0f0f0;line-height: 30px;margin: auto;border-radius: 5px;color: #6b72ff;">{{notice}}条新信息</view>
</view>
<view v-for="(item,index) in list" :key="index">
<view class="cu-card dynamic no-card">
<view class="cu-item shadow">
<view class="cu-list menu-avatar">
<view class="cu-item" style="padding-top: 20px;">
<view class="cu-avatar round lg" :style="[{backgroundImage:'url('+item.user.avatar+')'}]"></view>
<view class="content flex-sub">
<view>{{item.user.nickname}}</view>
<view class="text-gray text-sm flex justify-between">
{{item.create_time}}
</view>
</view>
</view>
</view>
<view class="text-content" v-if="contenthtml(item.content)" @click="handleLink(item.content)" v-html="item.content"></view>
<view class="text-content" v-html="item.content" v-else></view>
<view class="grid" style="margin: 0px 24rpx;">
<view class="grid" style="margin: 2rpx 0px;padding: 0px 8rpx;" v-for="(itemss,indexs) in item.files" :key="indexs">
<!-- flex-sub -->
<view class="bg-img" v-if="itemss.type==1">
<image :src="apiUrl+itemss.src" mode="aspectFill" style="width: 200rpx;height: 200rpx;" :data-src="apiUrl+itemss.src" @tap="previewImage"></image>
</view>
<view class='course-video' :style="item.files ? $util.imageCoverStyle(270,480) : ''" v-if="itemss.type==2">
<view class="relative-shadow" @tap="handlePlay(itemss)" style="">
<view class="cuIcon-video icon-center f-28 c-white"></view>
</view>
<im-image style="position: absolute;top: 0px;" :src="apiUrl+itemss.privacy" ></im-image>
<!-- :info="item.files" -->
</view>
</view>
</view>
<view v-if="item.location_address" @tap="openLocation(item.location)" class="im-location-msg im-flex im-rows im-nowrap im-align-items-center radius-8 pd-10">
<view class="f-20 cuIcon-location pr-5"></view>
<view>
<view class="f-10 mb-5">{{item.location_address}}</view>
<!-- <view class="c-999 f-12">{{item.extends && item.extends.address}}</view> -->
</view>
</view>
<view class="text-gray text-sm text-right padding">
<!-- <text class="cuIcon-attentionfill margin-lr-xs"></text> 10 -->
<!-- +item.is_like -->
<text @click="onLike(item.id)" :class="['cuIcon-appreciatefill', item.is_like === 1 ? 'text-red' : 'margin-lr-xs']"></text> {{item.likes.length}}
<text @click="onComment(index)" class="cuIcon-messagefill margin-lr-xs"></text> {{item.comment.length}}
<text v-if="item.user_id==userInfo.user_id" class="cuIcon-delete" style="margin-left: 5px;" @click="Delete(item.id)">删除</text>
</view>
<view v-show="numindex === index&&boll" style="margin: 5px 15px 5px 15px;position: relative;box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2);">
<view style="padding-bottom: 45px;">
<!-- <uni-easyinput id="input" type="textarea" autoHeight v-model="content" placeholder="评论"></uni-easyinput> -->
<!-- id="editor1" -->
<editor :id="'editor' + index" class=" bg-white c-333" style="min-height: 50px;height: 50px;padding:0px 10px;" :adjust-position="false"
maxlength="200" cursor-spacing="10" @input="changeMsgText" @ready="onEditorReady(index)" :read-only="readOnly"> </editor>
<!-- @focus="InputFocus" @blur="InputBlur" -->
</view>
<view style="position: absolute;bottom: 5px;right: 10px;">
<view style="display: flex;justify-content: space-between;">
<view>
<view class="im-menus cuIcon-emoji f-28 ml-5 mr-10" hover-class="tap" @click.stop="showAppBox"></view>
<scroll-view scroll-y class="icon" v-if="isFocus">
<view class="im-flex im-wrap im-justify-content-start im-align-items-center pd-10">
<view v-for="(item,index) in currentEmojiList" class="im-emoji-item" :key="index">
<image :src="item.src" style="width:44rpx;;height:44rpx" mode="aspectFit" lazy-load @tap="chooseEmoji(item)"></image>
</view>
</view>
</scroll-view>
</view>
<button class="cu-btn bg-green shadow " @touchend.prevent="sendTextMsg(item.id)">发送</button>
</view>
</view>
</view>
<view class="flex Likeview" v-if="item.likes.length!=0">
<view v-for="(itemws,indexws) in item.likes" :key="indexws" class="flex align-center" style="margin-right: 5px;">
<uni-icons type="heart" size="15" color="#576b95"></uni-icons>
<view style="margin-left: 2px;">{{itemws.nickname}}</view>
</view>
</view>
<view style="margin-bottom: 10px;">
<view class="cu-list menu-avatar comment" style="margin-top: 0px;" v-for="(iteme,indexw) in item.comment" :key="indexw">
<view class="comment_view">
<view class="content">
<view class="flex align-center" style="gap: 12rpx;" @click.stop="onComment(index,iteme.id)">
<view>
<view class="flex" v-if="iteme.reply_user_name!==''">
<view>{{iteme.nickname}} <text style="color: #000;margin-right: 5px;">回复</text></view>
<view class="flex-sub"> {{iteme.reply_user_name}}</view>
</view>
<view v-else>{{iteme.nickname}}</view>
</view>
<view class="text-gray text-content text-df" style="flex: 1; min-width: 0; word-break: break-word;">
<view class="text-content content_img" style="color: #000;" v-if="contenthtml(iteme.content)" @click.stop="handleLink(iteme.content,index,iteme.id)" v-html="iteme.content"></view>
<view class="text-content" style="color: #000;" v-html="iteme.content" v-else></view>
<!-- <mp-html container-style="overflow: hidden;display:inline;white-space: pre-wrap;color: #000;" :content="emojiToHtml(iteme.content)"/> -->
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-if="list.length==0" style="display: flex;justify-content: center;align-items: center;margin-top: 40px;">
<view>
<image src="@/static/image/empty.png" mode="widthFix" style="width: 560rpx;"></image>
<view style="text-align: center;color: #ccc;font-size: 40rpx;margin-right: 10px;">暂无数据</view>
</view>
</view>
</view>
<uni-load-more :status="status" :content-text="contentText"></uni-load-more>
</scroll-view>
</template>
<script>
import { useloginStore } from '@/store/login'
import config from '@/common/config.js'
import emoji from '@/utils/emoji.js'
import Edit from '@/utils/edit.js'
import { chat } from '@/mixins/chat.js'
import imImage from '@/components/message/im-image.vue';
import pinia from '@/store/index'
import { storeToRefs } from 'pinia';
import { useMsgStore } from '@/store/message';
const msgStore = useMsgStore(pinia)
const {NoticeCount} = storeToRefs(msgStore);
const loginStore = useloginStore()
export default {
components:{
imImage
},
mixins:[chat],
data() {
return {
notice:NoticeCount,
isCard: true,
apiUrl: config.apiUrl,
userInfo:loginStore.userInfo,
paddingB:0,
page:1,
limit:20,
content:'',
numindex:-1,
boll:false,
list:[],
// editorCtx:null,
editorCtx:{},
currentEmojiList:[],
emojiMap:[],
isFocus:false,
edit: null,
readOnly:false,
refreshing: false,
status: 'more',//
inputMsg:'',
//
contentText: {
contentdown: '查看更多',
contentrefresh: '加载中....',
contentnomore: '没有更多咯'
},
};
},
created(){
// #ifdef H5
this.paddingB=this.inlineTools;
// #endif
// #ifndef H5
this.paddingB=this.navBarHeight+this.inlineTools;
// #endif
this.currentEmojiList=emoji[0]['children'];
let emojiMap=[];
//
emoji.forEach(function (item) {
let child=item.children;
if(child.length>0){
child.forEach(function (val) {
let name=val.name;
let src=val.src;
emojiMap[name]=src;
})
}
});
this.emojiMap=emojiMap;
},
onShow() {
this.page = 1
this.wechatMomentsList()
},
methods: {
onRefresh() {
this.page = 1
this.wechatMomentsList()
if (this.refreshing) return;
this.refreshing = true;
setTimeout(() => {
this.refreshing = false;
}, 1000)
},
contenthtml(val){
return val && (val.includes('http://') || val.includes('https://'));
},
handleLink(url,index,id){
const textOnly = url.replace(/<[^>]+>/g, '');
const urlRegex = /https?:\/\/[^\s"']+/g;
const urls = textOnly.match(urlRegex) || [];
// console.log(urls);
if(urls.length > 0){
urls.forEach((res,index)=>{
// #ifdef H5
window.open(urls[index], '_blank');
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(urls[index]);
// #endif
})
}else{
this.onComment(index,id)
return
}
},
IsCard(e) {
this.isCard = e.detail.value
},
wechatMomentsList(){
let params = {page:this.page,limit:this.limit}
this.status = 'loading'; //
this.$api.compaApi.wechatMomentsList(params).then(res => {
res.data.forEach((res)=>{
const [latitudeStr, longitudeStr] = res.location.split('+');
const location = {
latitude: +latitudeStr,
longitude: +longitudeStr
};
res.location = location
res.content = res.content.replace(/\n/g, '<br>')
})
if (this.page === 1) {
this.list = res.data
} else {
this.list = this.list.concat(res.data);
}
//
if (this.list.length < res.count) {
this.status = 'more'; //
} else {
this.status = 'noMore'; //
}
// console.log(res);
})
},
scrollbot(){
if (this.status == 'noMore') {
return;
}
this.page++
this.wechatMomentsList()
// console.log('');
},
tomoments(){
uni.navigateTo({
url:'/pages/compass/sendtoMoments'
})
},
tonotice(){
uni.navigateTo({
url:'/pages/compass/Informationdetails'
})
},
onLike(id){
this.$api.compaApi.onlikes({posts_id:id}).then(res => {
this.wechatMomentsList()
// this.list = res.data
// console.log(res);
})
},
onComment(index,id){
//
if (this.numindex === index) {
this.boll = false
uni.removeStorageSync('pid')
this.numindex = -1
return
}
this.numindex = index
this.boll = !this.boll
if(id&&this.boll == true){
uni.setStorageSync('pid',id)
}
},
showAppBox(){
this.isFocus = !this.isFocus;
},
onicons(){
this.isFocus = false;
},
//
chooseEmoji(item){
// #ifdef H5
this.editorCtx[this.numindex].insertImage({
src: item.src,
alt: item.title,
width: 18,
height: 18,
nowrap:true,
extClass:'emoji-image',
success: ()=>{
},
complete: ()=> {
this.editorCtx[this.numindex].blur();
},
});
// console.log(this.editorCtx[this.numindex]);
// #endif
// #ifndef H5
this.readOnly= true
setTimeout(()=>{
this.editorCtx[this.numindex].insertImage({
src: item.src,
alt: item.title,
width: 18,
height: 18,
nowrap:true,
extClass:'emoji-image',
success: function() {
},
complete: ()=> {
this.readOnly= false
},
});
},10);
// #endif
},
onEditorReady(index) {
const editorId = 'editor' + index;
// #ifdef MP-BAIDU
// this.editorCtx = requireDynamicLib('editorLib').createEditorContext('editor1');
this.editorCtx[index] = requireDynamicLib('editorLib').createEditorContext(editorId);
// #endif
// #ifdef APP-PLUS || MP-WEIXIN || H5
const query = uni.createSelectorQuery().in(this);
// query.select('#editor1').context((res) => {
query.select('#editor' + index).context((res) => {
this.edit = new Edit({context: res.context,maxCount: 300});
this.editorCtx[index] = res.context
}).exec()
// #endif
},
changeMsgText(e){
const txt=e.detail.text.replace(/\n/g, '');
if(txt=='' && e.detail.html=='<p><br></p>'){
this.inputMsg='';
}else{
this.inputMsg=e.detail.html;
}
},
//
emojiToHtml(str){
if(!str){
return;
}
let emojiMap=this.emojiMap;
return str.replace(/\[!(\w+)\]/gi, function (str, match) {
var file = match;
return emojiMap[file] ? "<img class='mr-5' style=\"width:18px;height:18px\" emoji-name=\"".concat(match, "\" src=\"").concat(emojiMap[file], "\" />") : "[!".concat(match, "]");
});
},
previewImage(e) {
//
var current = e.target.dataset.src
uni.previewImage({
current: current,
urls: [current]
})
},
Delete(id){
const _this = this
uni.showModal({
title: "提示",
content: "是否要删除该内容",
success: (res) => {
if (res.confirm) {
_this.$api.compaApi.Deleteapost({posts_id:id}).then(res => {
_this.wechatMomentsList()
})
}else if (res.cancel) {
console.log('用户点击取消');
}
}
})
},
sendTextMsg(id1){
const _this = this
const pid = uni.getStorageSync('pid')
const currentIndex = this.numindex
this.editorCtx[currentIndex].getContents({
success:(e)=>{
let msg=e.html;
// console.log('',e);
let params = {posts_id:id1,content:msg,pid:pid?pid:""}
const text = /^\s*$/.test(params.content.replace(/<[^>]+>/g, ""));
const hasImg = /<img\b/i.test(params.content);
if(params.content==''|| (text && !hasImg)){
uni.showToast({title: '内容不能为空!',icon:'error'});
return
}
_this.$api.compaApi.oncomments(params).then(res => {
uni.removeStorageSync('pid')
_this.wechatMomentsList()
_this.inputMsg = ""
_this.boll = false;
_this.editorCtx[currentIndex].clear();
// _this.list = res.data
// console.log(_this.editorCtx.clear());
})
}
})
},
}
}
</script>
<style scoped lang="scss">
.im-friend-header{
width:100%;height:400rpx;position: relative;
.im-friend-bg{
width:100%;height:400rpx;overflow: hidden;
.im-friend-image{
width:100%;height:400rpx;
}
}
}
.im-user{
position: absolute;
right:60rpx;
top:210rpx;
overflow: hidden;
}
.icon{
top: -440rpx;
left: -170rpx;
width: 400rpx;
height: 400rpx;
position: absolute;
border-radius: 10rpx;
background-color: #fff;
box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2);
.im-emoji-item{
padding:22rpx;
}
}
.im-input{
height:100%;
font-size: 28rpx;
min-height:150rpx;
max-height: 300rpx;
padding:14rpx 14rpx;
border-radius:10rpx;
word-break: break-all;
margin:0 8rpx !important;
}
.im-location-msg{
color:#2B2E3D;
margin: 5px 10px;
text-align: left !important;
}
.relative-shadow{
z-index:1;
width:100%;
height:100%;
height: 240px;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
}
.Likeview{
padding: 5px;
display: flex;
color: #576b95;
align-items: center;
background-color: #f7f7f7;
margin: 0rem 0.9375rem 0rem 2rem;
}
.course-video{
overflow: hidden;
position: relative;
}
.comment_view{
height: auto;
color: #576b95;
padding: 20rpx 30rpx;
background-color: #f7f7f7;
margin: 0rem 0.9375rem 0rem 2rem;
}
::v-deep .ql-editor{
padding-top: 10px;
}
::v-deep .content_img img{
width: 18px;
margin-right: 5px;
}
</style>

313
pages/compass/personalcircleoffriends.vue

@ -0,0 +1,313 @@
<template>
<cu-custom bgColor="bg-white" :isBack="true">
<template #backText></template>
<template #content>朋友圈</template>
</cu-custom>
<!-- #ifdef APP-PLUS -->
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing"
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 88vh;">
<!-- #endif -->
<!-- #ifdef H5 -->
<scroll-view scroll-y @scrolltolower="scrollbot" :lower-threshold="0" refresher-enabled="true" :refresher-triggered="refreshing"
:refresher-threshold="50" @refresherrefresh="onRefresh" style="height: 93vh;">
<!-- #endif -->
<view>
<view class="im-friend-header">
<view class="im-friend-bg">
<image class="im-friend-image" src="/static/image/user-card-bg.jpg" style="width: 750rpx;height: 460rpx;"></image>
</view>
<view class="im-user im-flex im-justify-content-start align-center">
<text class="text-white mr-5">{{name}}</text>
<image class="radius-10" style="width:120rpx;height:120rpx" :src="urlimg" mode="widthFix"></image>
</view>
</view>
</view>
<view class="viewtop" v-if="userInfo.user_id==userid">
<view class="flex view_mar">
<view class="view_text">今天</view>
<view class="view_icons">
<uni-icons @tap="tomoments" type="camera-filled" size="30" color="#8a8a8a" style="padding: 30px;"></uni-icons>
</view>
</view>
</view>
<view v-for="(yearGroup, year) in groupedList" :key="year" class="year-group">
<!-- 年份标题 -->
<view class="year-title" @click="toggleYear(year)">
<text class="year-text">{{ year }}</text>
</view>
<!-- 动态内容展开时显示 -->
<view v-if="!collapsedYears.includes(year)" class="moments-list">
<view v-for="(item, index) in yearGroup" :key="item.id" class="moment-item" @click="JumpDetails(item.id)">
<view class="meta-info">
<text class="time">{{ formatTime(item.create_time) }}</text>
</view>
<view class="content1">
<view v-if="item.files" v-for="(items, indexs) in item.files" :key="indexs">
<view v-if="items.type==1"><image :src="configurl+items.src" :data-src="configurl+items.src" class="post-image" mode="aspectFill" @tap.stop="previewImage"/></view>
<view v-if="items.type==2">
<view class='course-video' style="width: 80px;height: 90px;">
<view class="relative-shadow" @tap.stop="handlePlay(items)">
<view class="cuIcon-video icon-center f-28 c-white"></view>
<!-- <view class="video-duration f-10 c-white" v-if="item.extends && item.extends.duration">{{$util.videoFormatTime(item.extends.duration)}}</view> -->
</view>
<im-image v-if="configurl+items.privacy" :src="configurl+items.privacy"></im-image>
</view>
<!-- <image :src="configurl+items.privacy" :data-src="configurl+items.privacy" class="post-image1" mode="aspectFill" @tap.stop="previewImage"/> -->
</view>
</view>
<view style="margin-left: 20rpx;">
<mp-html container-style="overflow: hidden;display:inline;word-break: break-word;" :content="emojiToHtml(item.content)"/>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script>
import { useloginStore } from '@/store/login'
import config from '@/common/config.js'
import {chat} from '@/mixins/chat.js'
import imImage from '@/components/message/im-image.vue';
const loginStore = useloginStore()
export default {
components: {
imImage
},
mixins: [chat],
data() {
return {
page: 1,
limit: 20,
list: [],
// groupedList: {},
collapsedYears: [],
status: 'more', //
refreshing: false,
configurl:config.apiUrl,
urlimg:'',
name:'',
userid:'',
//
contentText: {
contentdown: '查看更多',
contentrefresh: '加载中....',
contentnomore: '没有更多咯'
},
userInfo:loginStore.userInfo,
}
},
computed: {
// ...
groupedList() {
return this.groupDataByYear(this.list)
}
},
// watch: {
// list: {
// handler(newVal) {
// this.groupedList = this.groupDataByYear(newVal)
// },
// immediate: true,
// deep: true
// }
// },
onLoad(val) {
this.urlimg = val.avatar
this.name = val.realname
this.userid = val.user_id
},
onShow() {
if(this.userInfo.user_id==this.userid){
this.init()
}else{
this.init(this.userid)
}
},
methods: {
JumpDetails(id){
uni.navigateTo({
url:`/pages/compass/friendscircledetails?posts_id=${id}&userid=${this.userid}`
})
},
tomoments(){
uni.navigateTo({
url:'/pages/compass/sendtoMoments'
})
},
formatTime(timeStr) {
const date = new Date(timeStr)
return `${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
},
groupDataByYear(data) {
return data.reduce((acc, item) => {
const year = new Date(item.create_time).getFullYear()
if (!acc[year]) acc[year] = []
acc[year].push(item)
return acc
}, {})
},
previewImage(e) {
//
var current = e.target.dataset.src
uni.previewImage({
current: current,
urls: [current]
})
},
init(id){
if (this.page > 1) {
this.page = 1
}
let params = {page: this.page,limit: this.limit,friend_user_id: id?+id:''}
this.status = 'loading'; //
this.$api.compaApi.myPosts(params).then(res => {
res.data.forEach((res) => {
const [latitudeStr, longitudeStr] = res.location.split('+');
const location = {
latitude: +latitudeStr,
longitude: +longitudeStr
};
res.location = location
})
if (this.page == 1) {
this.list.length = 0
this.list = res.data
} else {
this.list = this.list.concat(res.data);
}
//
if (res.data.length < res.count) {
this.status = 'noMore'; //
} else {
this.status = 'more'; //
}
})
},
onRefresh() {
this.page = 1
// this.wechatMomentsList()
if (this.refreshing) return;
this.refreshing = true;
setTimeout(() => {
this.refreshing = false;
}, 1000)
},
scrollbot(){
if (this.status == 'noMore') {
return;
}
this.page++
// this.wechatMomentsList()
// console.log('');
},
}
}
</script>
<style scoped lang="scss">
.im-friend-header{
width:100%;height:400rpx;position: relative;
.im-friend-bg{
width:100%;height:400rpx;overflow: hidden;
.im-friend-image{
width:100%;height:400rpx;
}
}
}
.im-user{
position: absolute;
right:60rpx;
top:210rpx;
overflow: hidden;
}
.icon{
top: -440rpx;
left: -170rpx;
width: 400rpx;
height: 400rpx;
position: absolute;
border-radius: 10rpx;
background-color: #fff;
box-shadow:0px 0px 5px rgba(0, 0, 0, 0.2);
.im-emoji-item{
padding:22rpx;
}
}
.viewtop{
padding: 0px 30rpx;
.view_mar{
margin-top: 100rpx;
.view_text{
font-size: 34rpx;
font-weight: bold;
}
.view_icons{
display: flex;
margin-left: 40rpx;
align-items: center;
justify-content: center;
background-color: #c3c3c3;
}
}
}
.year-group {
padding: 0px 30rpx;
margin-top: 40rpx;
.year-title{
font-size: 38rpx;
}
.moment-item{
display: flex;
margin-top: 40rpx;
.meta-info{
font-size: 36rpx;
margin-right: 40rpx;
margin-left: 20rpx;
}
}
}
.content1{
display: flex;
.post-image{
width: 160rpx;
height: 160rpx;
}
.post-image1{
width: 160rpx;
height: 160rpx;
}
}
.course-video{
overflow: hidden;
position: relative;
}
.relative-shadow{
z-index:1;
width:100%;
height:100%;
position: absolute;
background: #8383833d;
}
.icon-center{
position: absolute;
top: 50%;
z-index: 4;
transform: translate(-50%, -50%);
left: 50%;
padding: 0 4rpx 0 6rpx;
}
</style>

445
pages/compass/sendtoMoments.vue

@ -0,0 +1,445 @@
<template>
<cu-custom bgColor="bg-white">
<template #backText>
<uni-icons type="left" size="22" @click="BackPage"></uni-icons>
</template>
<template #right>
<!-- disabled="true" -->
<view class="right_view"><button class="mini-btn" type="primary" size="mini" style="padding: 0px 10px;" @click="publish">发表</button></view>
</template>
</cu-custom>
<view style="padding: 10px 15px;">
<view>
<uni-easyinput type="textarea" :inputBorder="false" v-model="params.content" placeholder="这一刻的想法...."></uni-easyinput>
</view>
<view style="height: 600rpx;">
<sendtoImg @send="params.img_arr = $event" @videourl="video_file1 = $event" :img_arr1="img_arr1" :video_file1="video_file1"></sendtoImg>
</view>
<view>
<view class="icons_view" @click="chooseLocation">
<view style="display: flex;align-items: center;">
<uni-icons type="location" size="30"></uni-icons>
<view style="font-size: 18px;margin-left: 10px;">{{positioningList.content?positioningList.content:'所在位置'}}</view>
</view>
<view><uni-icons type="right" size="20"></uni-icons></view>
</view>
<!-- <view style="display: flex;align-items: center;justify-content: space-between;border-top: 1px solid #e5e5e5;padding: 10px 0px;">
<view style="display: flex;align-items: center;">
<uni-icons type="person" size="30"></uni-icons> <view style="font-size: 18px;"></view>
</view>
<view><uni-icons type="right" size="18"></uni-icons></view>
</view> -->
<view class="icons_view" style="border-bottom: 2px solid #e5e5e5;" @click="toggle">
<view style="display: flex;align-items: center;">
<uni-icons type="person" size="30"></uni-icons> <view style="font-size: 18px;margin-left: 10px;"></view>
</view>
<view style="display: flex;align-items: center;">
<view style="font-size: 18px;color: #a3a3a3;">{{params.privacy==1?'公开':params.privacy==2?'部分可见':params.privacy==3?'私密':'不给谁看'}}</view>
<uni-icons type="right" size="20"></uni-icons>
</view>
</view>
</view>
</view>
<uni-popup ref="popup" background-color="#fff" style="z-index: 999999;">
<view class="popup-content">
<cu-custom bgColor="bg-white">
<template #backText>
<uni-icons type="left" size="22" @click="BackPage1"></uni-icons>
</template>
<template #content>
<view>谁可以看</view>
</template>
<template #right>
<!-- disabled="true" -->
<view class="right_view"><button class="mini-btn" type="primary" size="mini" style="padding: 0px 10px;" @click="complete">完成</button></view>
</template>
</cu-custom>
<view>
<view style="padding: 30rpx 60rpx;">
<radio-group @change="radioChange">
<label>
<view style="display: flex;align-items: center;margin-bottom: 30px;">
<radio value="1" :checked="params.privacy==1" style="transform:scale(0.7)"/>
<view>
<view style="font-size: 30rpx;">公开</view>
<view style="font-size: 22rpx;color: #a3a3a3;">所有朋友可见</view>
</view>
</view>
</label>
<label>
<view style="display: flex;align-items: center;margin-bottom: 30px;">
<radio value="3" :checked="params.privacy==3" style="transform:scale(0.7)" />
<view>
<view style="font-size: 30rpx;">私密</view>
<view style="font-size: 22rpx;color: #a3a3a3;">仅自己可见</view>
</view>
</view>
</label>
<label>
<view style="margin-bottom: 30px;">
<view style="display: flex;align-items: center;">
<radio value="2" :checked="params.privacy==2" style="transform:scale(0.7)" />
<view>
<view style="font-size: 30rpx;">部分可见</view>
</view>
</view>
<view style="padding: 40rpx 60rpx;" v-if="params.privacy==2">
<view @click="editUser">
<view style="color: #004f9f;font-size: 30rpx;">选择朋友</view>
<block v-for="(item,index) in user1" :key="index" v-if="user1.length!==0" style="display: flex;">
<text style="font-size: 22rpx;margin-right: 5px;">{{item.displayName?item.displayName:item.nickname}}</text>
</block>
<view v-else style="font-size: 22rpx;">选中的朋友可见</view>
</view>
</view>
</view>
</label>
<label>
<view>
<view style="display: flex;align-items: center;">
<radio value="4" :checked="params.privacy==4" style="transform:scale(0.7)" />
<view>
<view style="font-size: 30rpx;">不给谁看</view>
</view>
</view>
<view style="padding: 40rpx 60rpx;" v-if="params.privacy==4">
<view @click="editUser1">
<view style="color: #004f9f;font-size: 30rpx;">选择朋友</view>
<block v-for="(item,index) in user2" :key="index" v-if="user2.length!==0" style="display: flex;">
<text style="font-size: 22rpx;margin-right: 5px;">{{item.displayName?item.displayName:item.nickname}}</text>
</block>
<view v-else style="font-size: 22rpx;">选中的朋友不可见</view>
</view>
</view>
</view>
</label>
</radio-group>
</view>
</view>
</view>
</uni-popup>
</template>
<script>
import sendtoImg from '@/components/sendtoImg.vue'
export default {
components: {
sendtoImg
},
data() {
return {
params:{
content:"",
privacy:1,
location:'',
status:1,
img_arr:[],
file_id:"",
user_ids:[],
posts_id:0,
video_file:""
},
video_file1:{},
video_file2:{},
positioningList:{},
user1:[],
user2:[],
img_arr1:[],
bols:false
}
},
created() {
// console.log(this.params.privacy);
this.getLastPosts()
},
onShow() {
if(uni.getStorageSync('selectUser')){
this.user1 = uni.getStorageSync('selectUser')
}
if(uni.getStorageSync('selectUser1')){
this.user2 = uni.getStorageSync('selectUser1')
}
},
methods: {
radioChange(e){
this.params.privacy = +e.detail.value
// console.log(this.params.privacy);
},
toggle() {
this.$refs.popup.open('bottom')
},
complete(){
this.$refs.popup.close()
if(this.params.privacy==2){
this.user2 = [];
uni.removeStorageSync('selectUser');
}else if(this.params.privacy==4){
this.user1 = [];
uni.removeStorageSync('selectUser1');
}else{
this.user1 = [];
this.user2 = [];
uni.removeStorageSync('selectUser');
uni.removeStorageSync('selectUser1');
}
},
BackPage1(){
if(this.bols==false){
this.params.privacy = 1;
this.user1 = [];
this.user2 = [];
uni.removeStorageSync('selectUser');
uni.removeStorageSync('selectUser1');
}
this.$refs.popup.close();
},
editUser(){
if(uni.getStorageSync('selectUser')){
this.user1 = uni.getStorageSync('selectUser')
const list = []
this.user1.forEach((res)=>{
list.push(res.user_id)
})
uni.navigateTo({
url: `/pages/index/userSelection?type=${5}&user_ids=${JSON.stringify(list)}`
})
}else{
uni.navigateTo({
url: `/pages/index/userSelection?type=${5}`
// url: '/pages/index/userSelection?type='+type+'&contact_id=' + this.contact.id
})
}
},
editUser1(){
if(uni.getStorageSync('selectUser1')){
this.user2 = uni.getStorageSync('selectUser1')
const list1 = []
this.user2.forEach((res)=>{
list1.push(res.user_id)
})
uni.navigateTo({
url: `/pages/index/userSelection?type=${6}&user_ids=${JSON.stringify(list1)}`
})
}else{
uni.navigateTo({
url: `/pages/index/userSelection?type=${6}`
// url: '/pages/index/userSelection?type='+type+'&contact_id=' + this.contact.id
})
}
},
transformToArrayObject(arr) {
//
const resultObj = {};
//
arr.forEach((value, index) => {
resultObj[index] = value;
});
//
return [resultObj];
},
BackPage(){
const _this = this
console.log(this.video_file1.url);
if(this.params.content!==''||this.params.img_arr.length!=0||this.video_file1.url){
uni.showModal({
content: '保留此次编辑',
cancelText: '不保留',
confirmText: '保留',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
_this.params.status = 2;
_this.publish()
} else if (res.cancel) {
_this.user1 = [];
_this.user2 = [];
uni.removeStorageSync('selectUser');
uni.removeStorageSync('selectUser1');
if(_this.params.posts_id){
_this.Delete(_this.params.posts_id)
}else{
uni.navigateBack({
delta: 1
});
}
}
}
});
}else{
uni.showModal({
content: '退出此次编辑?',
cancelText: '取消',
confirmText: '退出',
confirmColor: "red",
success: function (res) {
if (res.confirm) {
_this.user1 = [];
_this.user2 = [];
uni.removeStorageSync('selectUser');
uni.removeStorageSync('selectUser1');
uni.navigateBack({
delta: 1
});
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}
},
Delete(id){
console.log(id);
this.$api.compaApi.Deleteapost({posts_id:id}).then(res => {
uni.navigateBack({
delta: 1
});
})
},
chooseLocation(){
uni.chooseLocation({
success: (res) => {
// console.log('' + res.name);
// console.log('' + res.address);
// console.log('' + res.latitude);
// console.log('' + res.longitude);
let message={
type:'location',
status:'going',
content:'[位置] '+res.name,
name:res.name,
latitud: res.latitude+ '+' +res.longitude,
extends:res
};
this.positioningList = message
// this.$emit('send',Object.assign(this.msgItem(), message),false);
}
})
},
getLastPosts(){
this.$api.compaApi.getLastPosts().then(res => {
if(res.code==0&&res.data!==null){
console.log(res);
this.params.content = res.data.content
// this.params.img_arr = res.data.files
this.positioningList.latitud = res.data.location
this.positioningList.content = res.data.location_address
this.params.posts_id = res.data.posts_id
this.params.privacy = res.data.privacy
if(res.data.files[0].type==1){
this.img_arr1 = res.data.files
}else{
this.img_arr1 = res.data.files
res.data.files.forEach((res1)=>{
this.video_file1 = {url:res1.src,poster:res1.privacy,type:res1.type}
})
}
this.bols = true
if(res.data.privacy==2){
this.user1 = res.data.privacy_user
uni.setStorageSync('selectUser', this.user1);
}else if(res.data.privacy==4){
this.user2 = res.data.privacy_user
uni.setStorageSync('selectUser1', this.user2);
}
// console.log(res);
// this.list = res.data
}
})
},
publish(){
let list1 = []
let list2 = []
let listUrl = []
if(this.params.privacy == 2){
this.user1.forEach((res)=>{
list1.push(res.user_id)
})
} else if(this.params.privacy == 4){
this.user2.forEach((res)=>{
list2.push(res.user_id)
})
}
if(this.img_arr1&&this.img_arr1[0]?.type==1){
this.img_arr1.forEach(res=>{
listUrl.push(res.src)
})
}
if(this.params.content==''&&this.params.img_arr.length==0&&this.video_file1.url==undefined){
uni.showToast({title: '文字或图片、视频必须上传一个,并且文字不能是空格',icon:'none'});
return
}
const data = {
privacy: this.params.privacy,
content: this.params.content||'',
status: this.params.status,
location: this.positioningList.latitud || '',
address: this.positioningList.content || '',
img_arr: this.params.img_arr.length==0?listUrl:this.params.img_arr||'',
video_file: this.video_file1.url || '',
poster_file: this.video_file1.poster || '',
user_ids: this.params.privacy == 2 ? list1 : this.params.privacy == 4 ? list2 : [],
posts_id: this.params.posts_id||''
}
// console.log('',data);
this.$api.compaApi.wechatMomentsadd(data).then(res => {
// this.list = res.data
// console.log(res);
if(res.code==0){
list1 = []
list2 = []
if(this.params.status==2){
uni.showToast({title: '保存成功'});
}else{
uni.showToast({title: '添加成功'});
}
uni.navigateBack({
delta: 1
});
}
})
}
}
}
</script>
<style scoped lang="scss">
.right_view{
height: 30px;
margin-right: 10px;
}
uni-button[disabled][type='primary']{
background-color:#e7e7e7;
color: #c6c6c6;
}
uni-button:after{
border: 0px;
}
::v-deep .bg-white{
background-color: #f1f1f1 !important;
}
::v-deep .uni-easyinput__content{
background-color: #f1f1f1 !important;
}
.icons_view{
display: flex;
padding: 10px 10px;
align-items: center;
border-top: 2px solid #e5e5e5;
justify-content: space-between;
}
::v-deep .uni-modal__bd{
color: #000;
}
.popup-content {
height: 100vh;
align-items: center;
background-color: #fff;
justify-content: center;
}
</style>

101
pages/contacts/blacklist.vue

@ -0,0 +1,101 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>黑名单列表</template>
<!-- <template #right>
<view class="f-20 ml-10 mr-10" @tap="search()">
<text class="cuIcon-search"></text>
</view>
</template> -->
</cu-custom>
<view class="cu-list menu-avatar no-padding">
<view class="flex List-item" v-for="(items,sub) in groupList" :key="sub">
<view class="flex align-center" style="padding: 10px;">
<view class='cu-avatar lg radius mr-10' :style="[{backgroundImage:'url('+items.avatar+')'}]">
</view>
<view>
<view class="c-333">{{items.displayName}}</view>
</view>
</view>
<view class="flex">
<view class="fc-danger mr-10" @tap="setis_blacklist(items)">移除黑名单</view>
<view class="action fc-primary mr-10" @tap='openDetails(items)'>
<view>发送信息</view>
</view>
</view>
</view>
<Empty v-if="!groupList.length" noDatatext="暂无数据" textcolor="#999" ></Empty>
</view>
</view>
</template>
<script>
import { storeToRefs } from 'pinia';
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
const userStore = useloginStore(pinia);
const msgStore = useMsgStore(pinia)
const {contacts} = storeToRefs(msgStore);
/**
* 初始的引导页
*/
export default {
name : "group",
data() {
return {
groupList:[],
userInfo:userStore.userInfo
};
},
created() {
},
mounted(){
this.initContacts();
},
methods: {
initContacts(arr){
this.$api.msgApi.initContacts().then(res => {
this.groupList = res.data.filter(item=>item.is_blacklist==1)
})
},
setis_blacklist(items){
this.$api.msgApi.isBlacklist({friend_user_id:items.user_id}).then(res => {
if (res.code == 0) {
uni.showToast({
title: '移除成功!'
})
this.initContacts();
}
})
},
//
openDetails(items){
uni.navigateTo({
url:"/pages/message/chat?id="+items.id
})
},
search(){
uni.navigateTo({
url:"/pages/index/search?type=4"
})
}
}
}
</script>
<style scoped>
.fc-primary{
color: #409eff;
}
.fc-danger{
color: #f56c6c;
}
.List-item{
justify-content: space-between;
align-items: center;
}
</style>

327
pages/contacts/detail.vue

@ -0,0 +1,327 @@
<template>
<view>
<cu-custom bgColor="bg-white" :isBack="true">
<template #backText></template>
<template #content>个人信息</template>
</cu-custom>
<view class="padding flex justify-start align-center">
<block v-for="(items,indexs) in imglist" :key="indexs" v-if="detail.imgname&&network_log == 'none'">
<view class='cu-avatar lg radius mr-15' v-if="detail.imgname === items.name" @tap="showAvatar(detail)" :style="'background-image:url('+items.path+')'"></view>
</block>
<view v-else class='cu-avatar lg radius mr-15' @tap="showAvatar(detail)" :style="'background-image:url('+detail.avatar+')'"></view>
<view class='im-flex im-justify-content-start im-columns'>
<view class="mb-5">{{detail.friend?.nickname?detail.friend.nickname:detail.realname}}</view>
<view class="text-gray">{{detail.account}}</view>
</view>
</view>
<view class="cu-list menu">
<view class="cu-item" v-if="globalConfig.sysInfo.runMode==2 && detail.friend && userInfo.user_id!=detail.user_id" @tap="setNickname">
<view class="content">
<text class="cuIcon-edit text-green"></text>
<text>备注</text>
</view>
<view class="action">
<text class="text-grey text-sm">{{detail.friend.nickname || '未设置'}}</text>
<text class="text-grey text-sm ml-5 cuIcon-write"></text>
</view>
</view>
<view class="cu-item">
<view class="content">
<text class="cuIcon-mail text-green"></text>
<text>昵称</text>
</view>
<view class="action">
<text class="text-grey text-sm">{{detail.realname}}</text>
</view>
</view>
<view class="cu-item">
<view class="content">
<text class="cuIcon-mail text-green"></text>
<text>邮箱</text>
</view>
<view class="action">
<text class="text-grey text-sm">{{detail.email ?? ''}}</text>
</view>
</view>
<view class="cu-item">
<view class="content">
<text class="cuIcon-safe text-green"></text>
<text>性别</text>
</view>
<view class="action">
<text class="text-grey text-sm">{{ sex(detail.sex)}}</text>
</view>
</view>
<view class="cu-item" v-if="parseInt(globalConfig.sysInfo.ipregion)">
<view class="content">
<text class="cuIcon-location text-green"></text>
<text>IP</text>
</view>
<view class="action">
<text class="text-grey text-sm" v-if="detail.last_login_ip">{{ detail.last_login_ip || "未知"}} {{detail.location || "未知"}}</text>
<text class="text-grey text-sm" v-else>未知</text>
</view>
</view>
<view class="cu-item" @click="toFriendsCircle(detail)">
<view class="content flex">
<view class="flex justify-center" style="width: 1.6em;margin-right: 0.3125rem;"><uni-icons type="pyq" size="15" color="#45ba5b"></uni-icons></view>
<view>朋友圈</view>
</view>
<view class="action">
<text class="text-grey cuIcon-right"></text>
</view>
</view>
</view>
<template class="" v-if="userInfo.user_id!=detail.user_id">
<view class="padding flex flex-direction" v-if="globalConfig.sysInfo.runMode==1 || detail.friend">
<button class="cu-btn bg-green mt-10 lg" @tap="sendMsg(detail)">发消息</button>
<button class="cu-btn bg-blue mt-10 lg" v-if="validatePhone" @tap="callPhone()">打电话</button>
<!-- #ifdef APP | H5 -->
<button class="cu-btn bg-grey mt-10 lg" @tap="modelName='callRtc'" v-if="parseInt(globalConfig.chatInfo.webrtc) && parseInt(globalConfig.chatInfo.simpleChat)">音视频通话</button>
<!-- #endif -->
<button class="cu-btn bg-red mt-10 lg" @tap="delFriend()" v-if="globalConfig.sysInfo.runMode==2">删除好友</button>
</view>
<view class="padding flex flex-direction" v-if="globalConfig.sysInfo.runMode==2 && !detail.friend">
<button class="cu-btn bg-green lg" @tap="addFriend()">加好友</button>
</view>
</template>
<view class="cu-modal bottom-modal" :class="modelName=='callRtc'?'show':''" @tap="modelName=''">
<view class="cu-dialog">
<view class="manage-content">
<view class="cu-list menu bg-white">
<view class="cu-item" @tap="calling(0)">
<view class="content padding-tb-sm">
<text class="cuIcon-dianhua"></text>
<text>语音通话</text>
</view>
</view>
<view class="cu-item" @tap="calling(1)">
<view class="content padding-tb-sm">
<text class=" cuIcon-record"></text>
<text>视频通话</text>
</view>
</view>
<view class="parting-line-5"></view>
<view class="cu-item" @tap="modelName=''">
<view class="content padding-tb-sm">
<text class="c-red">取消</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login';
// #ifdef APP-PLUS
import {getSavedImages} from '@/utils/LocalFileSystemURL.js'
// #endif
import getUserInfo from '@/service/getUserInfo';
import pinia from '@/store/index'
import { storeToRefs } from 'pinia';
const msgStore = useMsgStore(pinia)
const userStore = useloginStore(pinia)
const {network_log} = storeToRefs(msgStore);
export default {
data() {
return {
modelName:'',
detail:{},
userInfo:userStore.userInfo,
globalConfig:userStore.globalConfig,
network_log:'',
imglist:[],
ids:0
}
},
computed: {
validatePhone(){
let reg = /^1[3456789]\d{9}$/;
return reg.test(this.detail.account);
}
},
onLoad(options) {
const _this = this
this.network_log = uni.getStorageSync('network_log')
if(_this.network_log == 'none'){
this.getImagePath()
this.getUserInfoList(options.id)
}else{
this.ids = options.id
this.getInfo()
// this.getImagePath()
// this.getUserInfoList(options.id)
}
},
methods: {
getInfo(){
this.$api.msgApi.getUserInfo({user_id:this.ids}).then((res)=>{
if(res.code==0){
this.detail=res.data;
const list = []
list.push(res.data)
this.insertUserInfo(list)
}
})
},
async insertUserInfo(val){
await getUserInfo.batchInsertOrUpdate(val)
},
async getUserInfoList(user_id){
try {
const list = await getUserInfo.getList({user_id:user_id});
list.forEach((item)=>{
const parts = item.avatar.split('/');
let lastPart = parts.pop() || parts.pop() || '';
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart));
item.imgname = isNumber ? lastPart+'.png' : lastPart;
item.friend = JSON.parse(item.friend);
item.setting = JSON.parse(item.setting);
})
this.detail = list[0];
console.log('获取个人信息数据',list.length,list);
} catch (error) {
console.error(error);
}
},
async getImagePath(){
this.imglist = await getSavedImages()
this.imglist.map(item => {
item.path = plus.io.convertLocalFileSystemURL(item.path)
});
// console.info(this.imglist,'');
},
showAvatar(detail){
let imgs=[];
imgs.push(detail.avatar);
uni.previewImage({urls : imgs})
},
sendMsg(info){
uni.reLaunch({
url:"/pages/message/chat?id="+info.user_id
})
},
toFriendsCircle(info){
uni.navigateTo({
url:"/pages/compass/personalcircleoffriends?user_id="+info.user_id+"&avatar="+info.avatar+"&realname="+info.realname
})
},
sex(value) {
let arr = ['女', '男','未知']
return arr[value] || '未知';
},
callPhone(){
uni.makePhoneCall({
phoneNumber: this.detail.account
});
},
calling(is_video){
if(msgStore.webrtcLock){
return uni.showToast({
title:'其他终端正在通话中',
icon:'none'
})
}
this.modelName='';
let msg_id=this.$util.getUuid();
uni.navigateTo({
url: '/pages/message/call?msg_id='+msg_id+'&type='+is_video+'&status=1&id='+this.detail.user_id+'&name='+this.detail.realname+'&avatar='+encodeURI(this.detail.avatar)
})
},
delFriend(){
uni.showModal({
title: '确定要删除该好友吗?',
success: (res)=>{
if (res.confirm) {
let data={ id: this.detail.user_id};
this.$api.friendApi.delFriend(data).then((res)=>{
if(res.code==0){
msgStore.deleteContacts(data);
// console.log(data,'123456');
uni.reLaunch({
url: '/pages/index/index'
})
}
})
}
},
})
},
addFriend(){
uni.showModal({
title: '请输入验证信息',
editable:true,
success: (res)=>{
if (res.confirm) {
if(res.content==''){
return uni.showToast({
title:'请输入备注!',
icon:'error'
})
}
this.$api.friendApi.addFriend({user_id:this.detail.user_id,remark:res.content}).then((e)=>{
if(e.code==0){
uni.showToast({
title:e.msg,
icon:'none'
})
this.getInfo()
}
})
}
}
});
},
setNickname(){
let friend_id=this.detail.friend.friend_id ?? '';
if(!this.detail.friend){
return uni.showToast({
title:'无法设置',
icon:'error'
})
}
uni.showModal({
title: '请输入备注信息',
editable:true,
success: (res)=>{
if (res.confirm) {
// if(res.content==''){
// return uni.showToast({
// title:'',
// icon:'error'
// })
// }
this.$api.friendApi.setNickname({friend_id:friend_id,nickname:res.content}).then((e)=>{
if(e.code==0){
this.detail.friend.nickname=res.content;
//
msgStore.updateContacts({
id:this.detaild.user_id,
displayName:res.content
})
uni.showToast({
title:e.msg,
icon:'none'
})
}
})
}
}
});
}
}
}
</script>
<style lang="scss" scoped>
</style>

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:'添加好友成功',
icon:'none'
})
msgStore.sysUnread--;
this.getList();
}
})
}
})
}
}
}
</script>
<style scoped>
</style>

106
pages/contacts/group.vue

@ -0,0 +1,106 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>群聊列表</template>
<template #right>
<view class="f-20 ml-10 mr-10" @tap="search()">
<text class="cuIcon-search"></text>
</view>
</template>
</cu-custom>
<view class="cu-list menu-avatar no-padding">
<view class="cu-item" v-for="(items,sub) in groupList" :key="sub" @tap='openDetails(items)'>
<block v-for="(itemse,indexs) in imglist" :key="indexs" v-if="network_log == 'none'">
<view v-if="items.imgname == itemse.name" class='cu-avatar lg radius mr-15' :style="[{backgroundImage:'url('+ itemse.path +')'}]"></view>
</block>
<view v-else class='cu-avatar lg radius mr-15' :style="[{backgroundImage:'url('+items.avatar+')'}]">
</view>
<view class="content">
<view class="c-333">{{items.displayName}}</view>
</view>
<view class="action">
<text class="c-999 cuIcon-peoplefill" v-if="items.owner_id==userInfo.user_id"></text>
</view>
</view>
<Empty v-if="!groupList.length" noDatatext="暂无群聊" textcolor="#999" ></Empty>
</view>
</view>
</template>
<script>
import { storeToRefs } from 'pinia';
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
// #ifdef APP-PLUS
import {getSavedImages} from '@/utils/LocalFileSystemURL.js'
// #endif
const userStore = useloginStore(pinia);
const msgStore = useMsgStore(pinia)
const {contacts,network_log} = storeToRefs(msgStore);
/**
* 初始的引导页
*/
export default {
name : "group",
data() {
return {
groupList:[],
userInfo:userStore.userInfo,
imglist:[],
network_log:network_log,
};
},
created() {},
mounted(){
this.initContacts(this.msgs);
// #ifdef APP-PLUS
this.getImagePath()
// #endif
},
methods: {
initContacts(arr){
const allContacts=uni.getStorageSync('allContacts');
const contacts=allContacts.filter((item)=>{
return item.is_group==1;
})
//
const sorted = contacts.sort((a, b) => {
if (a.index === '#') {
return 1;
}
if (b.index === '#') {
return -1;
}
return a.index.localeCompare(b.index, 'zh');
});
this.groupList=sorted;
},
async getImagePath(){
this.imglist = await getSavedImages()
this.imglist.map(item => {
item.path = plus.io.convertLocalFileSystemURL(item.path)
});
// console.info('',this.imglist);
},
//
openDetails(items){
uni.navigateTo({
url:"/pages/message/chat?id="+items.id
})
},
search(){
uni.navigateTo({
url:"/pages/index/search?type=3"
})
}
}
}
</script>
<style scoped>
</style>

447
pages/contacts/index.vue

@ -0,0 +1,447 @@
<template>
<view>
<scroll-view scroll-y class="indexes" :scroll-into-view="'indexes-'+ listCurID" :style="[{height:'calc(100vh - '+ (CustomBar+inlineTools+StatusBar) + 'px)'}]"
:scroll-with-animation="true" :enable-back-to-top="true" v-if="TabCur==0">
<view style="padding-bottom:30rpx">
<view class="cu-list menu mt-10">
<view class="cu-item arrow" @tap="openFriend" v-if="globalConfig.sysInfo.runMode==2">
<view class='cu-avatar mr-15 invite-bg' :class="appSetting.circleAvatar?'round':'radius'">
</view>
<view class="content">
<text class="c-333">新邀请</text>
</view>
<view class="action">
<view class="cu-tag round bg-red sm" v-if="unread>0">{{unread}}</view>
</view>
</view>
<view class="cu-item arrow" @tap="openGroup">
<view class='cu-avatar mr-15 group-bg' :class="appSetting.circleAvatar?'round':'radius'">
</view>
<view class="content">
<text class="c-333">群聊</text>
</view>
</view>
<view class="cu-item arrow" @tap="openBlacklist">
<view class='cu-avatar mr-15 blacklist' :class="appSetting.circleAvatar?'round':'radius'"></view>
<view class="content">
<text class="c-333">黑名单</text>
</view>
</view>
<view class="cu-item arrow" @tap="openChat(item.id)" v-for="(item,index) in systemContact" :key="index">
<block v-for="(items,indexs) in imglist" :key="indexs" v-if="network_log == 'none'">
<view class='cu-avatar mr-15' v-if="item.imgname === items.name" :style="{background:'url('+items.path+')',backgroundSize: 'contain'}" :class="appSetting.circleAvatar?'round':'radius'"></view>
</block>
<view v-else class='cu-avatar mr-15' :style="{background:'url('+item.avatar+')',backgroundSize: 'contain'}" :class="appSetting.circleAvatar?'round':'radius'"></view>
<view class="content">
<text class="c-333">{{item.displayName}}</text>
</view>
</view>
</view>
<block v-for="(item,index) in contacts" :key="index">
<view :class="'indexItem-' + item.name" :id="'indexes-' + item.name" :data-index="item.name">
<view class="padding">{{item.name}}</view>
<view class="cu-list menu no-padding">
<view class="cu-item" v-for="(items,sub) in item.children" :key="sub" @tap='openDetails(items)'>
<block v-for="(iteme,indexs) in imglist" :key="indexs" v-if="network_log == 'none'">
<view class="cu-avatar mr-15" v-if="items.imgname === iteme.name" :class="appSetting.circleAvatar?'round':'radius'" :style="[{backgroundImage:'url('+ iteme.path +')'}]">
<view class="online-status-small" v-if="items.is_online && items.is_group==0 && globalConfig.chatInfo.online==1" ></view>
</view>
</block>
<view v-else class='cu-avatar mr-15' :class="appSetting.circleAvatar?'round':'radius'" :style="[{backgroundImage:'url('+items.avatar+')'}]">
<view class="online-status-small" v-if="items.is_online && items.is_group==0 && globalConfig.chatInfo.online==1" ></view>
</view>
<view class="content">
<view class="c-333">{{items.displayName}}</view>
</view>
</view>
</view>
</view>
</block>
<view class="text-center m-20 text-grey">{{total}} 个朋友</view>
<Empty v-if="!contacts.length" noDatatext="暂无联系人" textcolor="#999" ></Empty>
</view>
</scroll-view>
<view class="indexBar" :style="[{height:'calc(100vh - ' + CustomBar + 'px - 50px)'}]" v-if="TabCur==0">
<view class="indexBar-box" @touchstart="tStart" @touchend="tEnd" @touchmove.stop="tMove">
<view class="indexBar-item" v-for="(item,index) in contacts" :key="index" :id="index" @touchstart="getCur" @touchend="setCur"> {{item.name}}</view>
</view>
</view>
<view class="cu-bar bg-white search fixed" :style="[{top:CustomBar + 'px'}]" v-if="TabCur==1">
<view class="search-form round">
<text class="cuIcon-search"></text>
<input type="text" v-model="keywords" placeholder="输入搜索的关键词" confirm-type="search"/>
</view>
</view>
<scroll-view scroll-y class="indexes" :style="[{top:'50px',height:'calc(100vh - '+ (CustomBar+inlineTools+StatusBar) + 'px)'}]"
:scroll-with-animation="true" :enable-back-to-top="true" v-if="TabCur==1">
<view class="">
<breadcurm :tree='tree' @openBread="openDep"></breadcurm>
</view>
<view class="im-department-list">
<navigator class="im-flex im-justify-content-start im-align-items-center mt-10" @tap="openDep(item)"
v-for="(item, depindex) in depList" :key="depindex">
<view class="im-folder-bar mr-10">
<text class="cuIcon-file color-blue"></text>
</view>
<view class="im-list-body im-border-b">
<view class="im-list-title word">
{{item.name}}
</view>
</view>
</navigator>
<navigator class="im-flex im-justify-content-start im-align-items-center mt-10" v-for="(item, index) in userList"
:key="item.id" :url="'/pages/contacts/detail?user_id=' + item.id">
<view class="im-folder-bar im-image mr-10">
<image :src="item.avatar" mode="widthFix"></image>
</view>
<view class="im-list-body im-border-b">
<view class="im-list-title word">
{{item.realname}}
</view>
</view>
</navigator>
<Empty v-if="depList.length==0 && userList.length==0"></Empty>
</view>
</scroll-view>
<!--选择显示-->
<view v-show="!hidden" class="indexToast">
{{listCur}}
</view>
</view>
</template>
<script>
import breadcurm from "@/components/breadcrum.vue"
import statusPoint from '@/components/status.vue'
import { storeToRefs } from 'pinia';
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login';
// #ifdef APP-PLUS
import {getSavedImages} from '@/utils/LocalFileSystemURL.js'
// #endif
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
const userStore = useloginStore(pinia)
const {contacts,sysUnread,network_log} = storeToRefs(msgStore);
/**
* 初始的引导页
*/
export default {
components: {
breadcurm,
statusPoint
},
name : "contacts",
props:{
TabCur:{type:Number, default:0}
},
data() {
return {
//#ifdef H5
tabbarH:50,
//#endif
//#ifndef H5
tabbarH: 100,
//#endif
hidden: true,
listCurID: '',
list: [],
systemContact:[],
network_log:network_log,
listCur: '',
total:0,
scrollLeft: 0,
msgs: contacts,
unread:sysUnread,
globalConfig:userStore.globalConfig,
appSetting:userStore.appSetting,
tree:[
{
id:1,
name:'技术部'
},
{
id:2,
name:'产品部'
},
{
id:3,
name:'信息部'
},
],
depList:[
{
id:1,
name:"技术部"
},
{
id:2,
name:"财务部"
}
],
userList:[
{
id:1,
realname:"张三",
avatar:'https://api.multiavatar.com/raingad1.png?apikey=zdvXV3W4MjwhP9'
},
{
id:2,
realname:"李四",
avatar:'https://api.multiavatar.com/raingad2.png?apikey=zdvXV3W4MjwhP9'
}
],
tabList:[
"普通通讯录",
"企业通讯录"
],
contacts:[],
imglist:[]
};
},
watch:{
msgs(val){
this.initContacts(val);
}
},
created() {
this.listCur = this.contacts[0];
// #ifdef APP-PLUS
this.getImagePath()
// #endif
},
mounted(){
this.initContacts(this.msgs);
},
methods: {
async getImagePath(){
this.imglist = await getSavedImages()
this.imglist.map(item => {
item.path = plus.io.convertLocalFileSystemURL(item.path)
});
// console.info(this.imglist,'');
},
initContacts(arr){
const allContacts=JSON.parse(JSON.stringify(arr));
const contacts=allContacts.filter((item)=>{
return item.is_group==0;
})
this.systemContact=allContacts.filter((item)=>{
return item.is_group==2||item.is_group==4;
})
this.total=contacts.length;
//
const sorted = contacts.sort((a, b) => {
if (a.index === '#') {
return 1;
}
if (b.index === '#') {
return -1;
}
return a.index.localeCompare(b.index, 'zh');
});
//
const result = sorted.reduce((acc, cur) => {
const index = cur.index;
const existingIndex = acc.findIndex(item => item.name === index);
if (existingIndex === -1) {
acc.push({ name: index, children: [cur] });
} else {
acc[existingIndex].children.push(cur);
}
return acc;
}, []);
this.contacts=result;
},
openDep(item){
},
openChat(id){
uni.navigateTo({
url:"/pages/message/chat?id=" + id,
animationType:"slide-in-right"
})
},
//
openDetails(items){
uni.navigateTo({
url:"/pages/contacts/detail?id="+items.id
})
},
openGroup(){
uni.navigateTo({
url:"/pages/contacts/group"
})
},
openBlacklist(){
uni.navigateTo({
url:"/pages/contacts/blacklist"
})
},
openFriend(){
uni.navigateTo({
url:"/pages/contacts/friend"
})
},
tabSelect(e) {
this.TabCur = e.currentTarget.dataset.id;
this.scrollLeft = (e.currentTarget.dataset.id - 1) * 60
},
//
getCur(e) {
this.hidden = false;
this.listCur = this.contacts[e.target.id].name;
},
setCur(e) {
this.hidden = true;
this.listCur = this.listCur
},
//Item
tMove(e) {
let y = e.touches[0].clientY,
offsettop = this.boxTop,
that = this;
//,
if (y > offsettop) {
let num = parseInt((y - offsettop) / 20);
this.listCur = that.contacts[num].name
};
},
//
tStart() {
this.hidden = false
},
//
tEnd() {
this.hidden = true;
this.listCurID = this.listCur
},
indexSelect(e) {
let that = this;
let barHeight = this.barHeight;
let list = this.contacts;
let scrollY = Math.ceil(list.length * e.detail.y / barHeight);
for (let i = 0; i < list.length; i++) {
if (scrollY < i + 1) {
that.listCur = list[i].name;
that.movableY = i * 20
return false
}
}
}
}
}
</script>
<style scoped>
page {
padding-top: 100upx;
}
.indexes {
position: relative;
margin-bottom:20rpx
}
.indexBar {
position: fixed;
right: 0px;
bottom: 0px;
padding: 20upx 20upx 20upx 60upx;
display: flex;
align-items: center;
}
.indexBar .indexBar-box {
width: 40upx;
height: auto;
background: #fff;
display: flex;
flex-direction: column;
box-shadow: 0 0 20upx rgba(0, 0, 0, 0.1);
border-radius: 10upx;
}
.indexBar-item {
flex: 1;
width: 40upx;
height: 40upx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24upx;
color: #888;
}
movable-view.indexBar-item {
width: 40upx;
height: 40upx;
z-index: 9;
position: relative;
}
movable-view.indexBar-item::before {
content: "";
display: block;
position: absolute;
left: 0;
top: 10upx;
height: 20upx;
width: 4upx;
background-color: #f37b1d;
}
.indexToast {
position: fixed;
top: 0;
right: 80upx;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
width: 100upx;
height: 100upx;
border-radius: 10upx;
margin: auto;
color: #fff;
line-height: 100upx;
text-align: center;
font-size: 48upx;
}
.gui-list-image {
width: 70rpx !important;
height: 70rpx !important;
border-radius: 8rpx;
object-fit: cover
}
.im-department-list {
padding: 0 20rpx 40rpx
}
.im-folder-bar {
background-color: #aaccff52;
border-radius: 8rpx;
width: 70rpx;
height: 70rpx;
text-align: center;
line-height: 70rpx;
overflow: hidden;
}
.im-image {
}
.im-folder-bar .iconfont {
font-size: 44rpx !important;
}
.group-bg{
background-image: url(@/static/image/group.png);
}
.invite-bg{
background-image: url(@/static/image/invite.png);
}
.blacklist{
background-image: url(@/static/image/blacklist-red.png);
}
</style>

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>

435
pages/index/index.vue

@ -0,0 +1,435 @@
<template>
<view>
<cu-custom :bgColor="PageCur=='mine'?'bg-mine':'bg-white'">
<template #backText>
<view v-if="PageCur=='message' || PageCur=='contacts'" class="f-20 ml-10 mr-10" @tap="search()">
<text class="cuIcon-search" style="margin-left: -10px;"></text>
</view>
</template>
<template #content>{{PageName}}</template>
<template #right>
<view v-if="PageCur=='message'" class="f-20 ml-10 mr-10" @tap="modelName='add'">
<text class="cuIcon-add f-28" id="add"></text>
</view>
</template>
</cu-custom>
<view>
<message v-show="PageCur=='message'"></message>
<contacts v-show="PageCur=='contacts'" :TabCur="TabCur"></contacts>
<compass v-show="PageCur=='compass'"></compass>
<mine v-show="PageCur=='mine'"></mine>
</view>
<view class="cu-bar tabbar bg-white shadow foot">
<view class="action" @click="NavChange(item)" v-for="(item,index) in navList" :key="index" data-cur="message">
<view class='cuIcon-cu-image' style="position: relative;">
<image :src="'/static/image/tabbar/' + [item.name] + [PageCur==item.name?'-active':''] + '.svg'"></image>
<view class="cu-tag badge" v-if="item.notice>0">{{item.notice}}</view>
<view style="position: absolute;top: 0px;right: 2px;" v-if="item.bol>0">
<view class="notice-back"></view>
</view>
</view>
<view :class="PageCur==item.name?'text-green':'text-black'">{{item.title}}</view>
</view>
</view>
<view class="add-modal" :class="modelName=='add' ? 'show' : 'none'" @tap="modelName=''" >
<view class="add-dialog" :style="{top:top+20+'px'}">
<view class="add-item" @tap="initContacts();">
<view class="content padding-tb-sm">
<text class="cuIcon-refresh"></text>
<text class="text">更新消息</text>
</view>
</view>
<view class="add-item" @tap="addFriend()" v-if='globalConfig.sysInfo.runMode==2'>
<view class="content padding-tb-sm">
<text class="cuIcon-friendadd"></text>
<text class="text">添加朋友</text>
</view>
</view>
<view class="add-item" @tap="addGroup()">
<view class="content padding-tb-sm">
<text class=" cuIcon-friend"></text>
<text class="text">创建群聊</text>
</view>
</view>
<view class="add-item" @tap="scan()">
<view class="content padding-tb-sm" style="text-align: left;">
<text class=" cuIcon-scan mr-5"></text>
<text class="text"> </text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import message from '@/pages/message';
import contacts from '@/pages/contacts';
import compass from '@/pages/compass';
import mine from '@/pages/mine';
import { storeToRefs } from 'pinia';
import { useMsgStore } from '@/store/message';
import { useloginStore } from '@/store/login';
import pinia from '@/store/index'
import scan from '@/common/scan.js'
import homeData from '../../service/homeData';
const msgStore = useMsgStore(pinia)
const loginStore = useloginStore(pinia)
const { unread,sysUnread,NoticeCount } = storeToRefs(msgStore);
export default {
components: {
message,
contacts,
compass,
mine
},
data() {
let navList=[
{
name:'message',
title:'消息',
notice:unread
},
{
name:'contacts',
title:'通讯录',
notice:sysUnread
}
]
let compass={
name:'compass',
title:'探索',
notice:0,
bol:NoticeCount
};
if(loginStore.globalConfig && loginStore.globalConfig.compass){
if(loginStore.globalConfig.compass.status==1){
navList.push(compass);
}
}
let mine={
name:'mine',
title:'我的',
notice:0
}
navList.push(mine);
return {
globalConfig:loginStore.globalConfig,
PageCur: 'message',
PageName: '消息',
TabCur:0,
modelName:false,
navList:navList,
top:10,
indexs:0,
IntervalChat:null
}
},
onBackPress(options) {
if (getCurrentPages().length > 1) {
return false;
}
try {
const main = plus.android.runtimeMainActivity();
main.moveTaskToBack(false); //
return true; // 退
} catch (e) {
return false; //
}
},
mounted(){
// #ifndef MP
uni.hideTabBar();
// #endif
// ws,,
uni.$on('socketStatus',(e)=>{
if(e){
console.log('触发了一次');
this.initContacts();
}
})
uni.$off('initContacts');
uni.$on('initContacts',(e)=>{
this.initContacts();
})
let query = this.$util.getQuery(this).select("#add").boundingClientRect();
query.exec((res)=> {
let top = res[0].top;
let height = res[0].height;
this.top = top+height
});
},
onShow(){
const _this = this
const tabid = uni.getStorageSync('tabid')
if(tabid){
uni.removeStorageSync('tabid')
}
uni.getNetworkType({
success (res) {
if(res.networkType == 'none'){
_this.getGroupData()
}else{
_this.initContacts(0);
// _this.startPolling()
// _this.getGroupData()
}
// console.log(res.networkType,'1111');
}
});
},
onHide() {
//
this.stopPolling()
},
methods: {
startPolling(){
this.IntervalChat = setInterval(()=>{
this.initContacts();
},5000)
},
stopPolling(){
clearInterval(this.IntervalChat)
this.IntervalChat = null
},
closeModel(){
this.modelName=false;
},
scan(){
scan.scanQr();
},
NavChange: function(item) {
this.PageCur = item.name
this.PageName = item.title
},
showContacts(){
this.TabCur==1 ? this.TabCur=0 :this.TabCur=1
},
initContacts(num){
if(num){
this.indexs = num
}
this.modelName='';
this.$api.msgApi.initContacts().then(res => {
this.indexs++
//
msgStore.sysUnread=res.count;
msgStore.initContacts(res.data);
// if(this.indexs === 1&&num===0){
//
res.data.forEach((val)=>{
if(val.id==-2){
val.lastContent = val.lastContent.replace(/'/g, "''");
}
})
this.syncGroupData(res.data)
const userInfo = uni.getStorageSync('userInfo')
const list = [...res.data]
list.push({avatar:userInfo.avatar})
list.forEach(res => {
// #ifdef APP-PLUS
uni.downloadFile({ url: res.avatar,success: (downloadResult) => {
this.saveToPermanentStorage(downloadResult.tempFilePath);
}})
// #endif
})
// }
// this.getGroupData()
})
},
// App
saveToPermanentStorage(tempPath) {
return new Promise((resolve, reject) => {
//
plus.io.resolveLocalFileSystemURL(
'_doc',
(docDir) => {
//
docDir.getDirectory(
'img',
{ create: true, exclusive: false },
(entry) => {
//
const fileName = this.getFileName(tempPath);
const fileName1 = this.getFileName(docDir.fullPath + 'img/' +fileName);
//
entry.getFile(fileName1,{ create: false }, //
(fileEntry) => {
// console.log('');
//
reject(new Error('File already exists: ' + fileName));
},
(error) => {
// console.log(error);
//
if (error.code === 14) { // 1
this.copyFile(tempPath, entry, fileName, resolve, reject);
} else {
reject(error);
}
}
);
},
(error) => {
reject(error);
}
);
},
(error) => {
reject(error);
}
);
});
},
//
copyFile(tempPath, targetDir, fileName, resolve, reject) {
plus.io.resolveLocalFileSystemURL(
tempPath,
(tempEntry) => {
tempEntry.copyTo(
targetDir,
fileName,
(newEntry) => {
resolve(newEntry.toLocalURL());
},
(error) => {
reject(error);
}
);
},
(error) => {
reject(error);
}
);
},
//
getFileName(path) {
const index = path.lastIndexOf('/');
let fileName = path.substr(index + 1);
fileName = fileName.replace(/\(\d+\)(?=\.[^./]+$)/, '');
return fileName;
},
async syncGroupData(data) {
// #ifdef APP-PLUS
// 1.
let apiResponse = data;
// 3.
await homeData.deleteallList()
await homeData.batchInsertOrUpdate(apiResponse);
// #endif
},
async getGroupData(){
// #ifdef APP-PLUS
const groups = await homeData.getList();
msgStore.initContacts(groups)
// console.log(':', groups)
// #endif
},
addGroup(){
uni.navigateTo({
url: '/pages/index/userSelection?type=1'
})
},
addFriend(){
uni.navigateTo({
url: '/pages/contacts/search'
})
},
search(){
const type = this.PageCur=="message" ? 1 : 2;
uni.navigateTo({
url: '/pages/index/search?type='+type
})
}
}
}
</script>
<style scoped lang="scss">
.add-modal{
position: fixed;
top: 0;
right: 0;
z-index: -99999;
.add-dialog{
display: flex;
flex-direction: column;
background-color: #4f4f4f;
color: #fff;
border-radius: 10rpx;
justify-content: space-around;
align-items: center;
position: fixed;
right: 10rpx;
width: 240rpx;
padding:20rpx;
.add-item{
.text{
margin-left: 10rpx;
}
}
}
.add-dialog::after {
content: "";
top: -10rpx;
transform: rotate(45deg);
position: absolute;
display: inline-block;
overflow: hidden;
width: 30rpx;
height: 30rpx;
right: 20rpx;
left: initial;
background: #4f4f4f;
}
}
.show{
position: fixed;
top: 0;
z-index: 9999;
height: 100vh;
width: 100vw;
opacity:1
}
.none{
position: fixed;
top: 0;
right: 0;
z-index: -99999;
opacity: 0;
}
.notice-back{
width: 6px;
height: 6px;
position: relative;
border-radius: 50%;
background-color: red;
vertical-align: middle;
}
.notice-back::after{
content: "";
width: 100%;
height: 100%;
top: 0px;
right: 0px;
position: absolute;
border-radius: 50%;
background: inherit;
animation: notiback 1.2s ease-in-out infinite;
}
@keyframes notiback{
0% {transform: scale(0.5);opacity: 1;}
30%{opacity: 0.7;}
100%{ transform: scale(2.5);opacity: 0;}
}
</style>

225
pages/index/qrcode.vue

@ -0,0 +1,225 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>{{title}}</template>
</cu-custom>
<view class="mt-20 im-flex im-align-items-center">
<image :src="canvasUrl" mode="widthFix" style="margin:0 auto"></image>
</view>
<view class="padding flex flex-direction mt-10" v-if="canvasUrl" style="position: relative;">
<button class="cu-btn bg-green lg" @tap="saveHeadImgFile">
<!-- #ifdef H5 -->
下载图片
<!-- #endif -->
<!-- #ifndef H5 -->
保存到相册
<!-- #endif -->
</button>
<!-- #ifdef H5 -->
<image :src="contact.avatar" mode="widthFix" style="width: 120rpx;position: absolute;top: -850rpx;left: 320rpx;" />
<!-- #endif -->
</view>
<mosowe-canvas-image ref="mosoweCanvasComponents" :lists="lists" :height="height" :width="width" @canvasImage="canvasImage" ></mosowe-canvas-image>
</view>
</template>
<script>
import mosoweCanvasImage from '@/components/mosowe-canvas-image/mosowe-canvas-image.vue';
import pinia from '@/store/index'
import { useloginStore } from '@/store/login';
const userStore = useloginStore(pinia);
export default {
data() {
return {
canvasUrl: '',
group_id:0,
lists:[],
width:500,
height:700,
title:'我的二维码',
contact:{}
}
},
components:{
mosoweCanvasImage
},
onLoad(options) {
this.group_id = options.group_id?options.group_id:''
// ID,
if(this.group_id){
this.title="群二维码";
this.getGroupInfo()
}else{
this.createUserQr();
}
},
methods: {
createUserQr(){
let userInfo=userStore.userInfo;
let qrUrl=userInfo.qrUrl ?? '';
if(!qrUrl){
uni.showToast({
title:'请重新登陆后再生成二维码',
icon:'none'
})
uni.navigateBack();
return;
}
this.lists=[
{
type: 'rect',
color: '#ffffff',
width: this.width,
height: this.height,
x: 0,
y: 0
},
{
type: 'image',
content: userInfo.avatar,
width: 100,
height: 100,
x: 70,
y: 60
},
{
type: 'text',
content: userInfo.displayName,
x: 190,
y: 96,
maxWidth:400,
color: '#000',
size: 28,
},
{
type: 'text',
content: userInfo.account,
x: 190,
y: 135,
maxWidth:400,
color: '#999',
size: 20,
},
{
type: 'qr',
content: qrUrl,
width: 360,
height: 360,
x: 70,
y: 200,
},
{
type: 'text',
content: '扫一扫上面的二维码图案,加我为好友',
x: 105,
y: 620,
color: '#999',
maxWidth:400,
size: 18,
},
];
setTimeout(()=>{
this.$refs.mosoweCanvasComponents.createCanvas();
},100)
},
getGroupInfo() {
this.userList = []
this.$api.msgApi.groupInfo({
group_id: this.group_id
}).then((res) => {
let data=res.data;
this.contact=data;
let groupName=data.name;
let length=groupName.length;
console.log(this.contact);
if(length>12){
groupName=groupName.substring(0,12)+"...";
}
this.lists=[
{
type: 'rect',
color: '#ffffff',
width: this.width,
height: this.height,
x: 0,
y: 0
},
{
type: 'image',
content: data.avatar,
// content: "http://192.168.66.16:8007/avatar//120/20",
width: 100,
height: 100,
x: 200,
y: 40
},
{
type: 'text',
content: '群聊:'+groupName,
x: this.width/2,
align:'center',
y: 180,
maxWidth:400,
color: '#000',
size: 24,
},
{
type: 'qr',
content: data.qrUrl,
width: 360,
height: 360,
x: 70,
y: 220,
},
{
type: 'text',
content: '该二维码7天内('+data.qrExpire+'前)有效',
x: 105,
y: 640,
color: '#999',
maxWidth:400,
size: 18,
},
];
setTimeout(()=>{
this.$refs.mosoweCanvasComponents.createCanvas();
},100)
})
},
canvasImage (e) {
this.canvasUrl = e;
},
// base64
saveHeadImgFile() {
// #ifdef H5
const tempLink = document.createElement("a");
tempLink.style.display = "none";
tempLink.href = this.canvasUrl;
tempLink.setAttribute("download", this.contact.name+".jpg");
tempLink.setAttribute("target", "_blank");
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
// #endif
// #ifndef H5
uni.saveImageToPhotosAlbum({
filePath: this.canvasUrl,
success: () => {
uni.showToast({title:'图片保存成功',icon:'none'})
}
})
// #endif
}
}
}
</script>
<style scoped>
.list-image {
width: 80rpx;
height: 80rpx;
font-size: 0;
}
</style>

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>

354
pages/index/userSelection.vue

@ -0,0 +1,354 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>{{title}}</template>
<template #right>
<view class="mr-10 f-16" @tap="save">{{type==3 ? '转发' : '完成'}}</view>
</template>
</cu-custom>
<user-select :type="type" :contact_id="contact_id" :user_ids="user_ids" ref="userSelect"></user-select>
</view>
</template>
<script>
import userSelect from '@/components/message/user-select.vue';
import { useMsgStore } from '@/store/message';
import { storeToRefs } from 'pinia';
import pinia from '@/store/index'
const msgStore = useMsgStore(pinia)
const {appendContacts} = storeToRefs(msgStore);
export default {
components: {
userSelect
},
data() {
return {
title:'发起群聊',
contact_id:'',
selectUser:[],
userList: [],
changeUser: [], //
user_ids: [], //
type: 1,
relayState: false,
scrollLeft:300,
content:'',
curMsg:{}
}
},
watch: {
relayState(val) {
if (val == true) {
uni.showToast({
icon: 'success',
title: '转发成功'
})
setTimeout(() => {
uni.navigateBack()
}, 2000)
}
}
},
onLoad(options) {
var pages = getCurrentPages(); //
var beforePage = pages[pages.length - 2]; //
this.type = options.type ? options.type : 1;
this.contact_id = options.contact_id ? options.contact_id : '';
this.content = options.content ? options.content : '';
if (options.type == 2) {
this.title="添加成员";
//
// #ifdef H5
this.user_ids = beforePage.user_ids
// #endif
// #ifndef H5
this.user_ids = beforePage.$vm.user_ids
// #endif
console.log(this.user_ids);
}else if (options.type == 3) {
this.title="转发聊天";
//
// #ifdef H5
this.curMsg = beforePage.curMsg
// #endif
// #ifndef H5
this.curMsg = beforePage.$vm.curMsg
// #endif
} else if (options.type == 4) {
this.title="选择提醒的人";
} else if(options.type == 5){
this.user_ids = JSON.parse(options.user_ids)
// console.log(this.user_ids);
}else if(options.type == 6){
if(options.user_ids){
this.user_ids = JSON.parse(options.user_ids)
}
// console.log(this.user_ids);
}else{
this.title="发起群聊";
}
},
methods: {
//
relay() {
let user_ids = this.changeUser.map(it => {
return it.id
})
if (!user_ids.length) {
uni.showToast({
title: "请选择至少一名人员",
icon: "none"
})
} else if (user_ids.length > 5) {
uni.showToast({
title: "转发的人数不能超过5人!",
icon: "none"
})
} else {
var pages = getCurrentPages(); //
var beforePage = pages[pages.length - 2]; //
let toContactId = ''
let fromUser = ''
//
// #ifdef H5
fromUser = beforePage.fromUser
toContactId = beforePage.contact.id
// #endif
// #ifndef H5
fromUser = beforePage.$vm.fromUser
// #endif
let selectedItem = ''
if (this.type == 'relayCrm' || this.type == 'relayProject') {
// #ifdef H5
selectedItem = beforePage.message
// #endif
// #ifndef H5
selectedItem = beforePage.$vm.message
// #endif
} else {
selectedItem = uni.getStorageSync('selectedItem')
}
user_ids.forEach(it => {
let msg = {
id: this.$util.getUuid(),
is_group: 0,
fromUser,
extends: selectedItem.extends ? selectedItem.extends : '',
type: selectedItem.type,
toContactId: it,
content: selectedItem.content,
sendTime: new Date().getTime()
}
this.$api.msgApi.sendMessage(msg)
.then((res) => {
if (res.code !== 200) return
this.relayState = true
})
})
}
},
//
addGroupUser(user_ids) {
var pages = getCurrentPages(); //
var beforePage = pages[pages.length - 2]; //
//
// #ifdef H5
beforePage.getsimpleMessage = false
let group_id = beforePage.group_id
// #endif
// #ifndef H5
beforePage.$vm.getsimpleMessage = false
let group_id = beforePage.$vm.group_id
// #endif
this.$api.msgApi.addGroupUser({
user_ids,
id: group_id
}).then(res => {
if (res.code == 200) {
uni.navigateBack()
}
})
},
//
addGroup(user_ids) {
this.$api.msgApi.addGroup({
user_ids
}).then(res => {
if (res.code == 200) {
setTimeout(() => {
uni.navigateBack()
}, 2000)
}
})
},
save(){
uni.showLoading({
title: '保存中...',
mask:true
});
this.changeUser=this.$refs.userSelect.changeUser;
this.selectUser=this.$refs.userSelect.selectUser;
// console.log(this.changeUser);
if(!this.changeUser.length){
return uni.showToast({
title:'请选择人员',
icon:'none'
})
}
try{
if(this.type==1){
this.$api.msgApi.addGroup({user_ids:this.changeUser}).then(res =>{
uni.hideLoading();
const data = res.data;
msgStore.appendContacts(data);
uni.navigateTo({
url:'/pages/message/chat?id='+data.id
})
})
}else if(this.type==2){
this.$api.msgApi.addGroupUser({user_ids:this.changeUser,id:this.contact_id}).then(res =>{
this.closePage();
})
}else if(this.type==3){
if(this.changeUser.length>5){
return uni.showToast({
title:'单次转发不能超过5人',
icon:'none'
})
}
this.$api.msgApi.forwardMessage({user_ids:this.changeUser,msg_id:this.curMsg.msg_id,content:this.content}).then(res =>{
this.closePage();
})
}else if(this.type==4){
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('getAtList',this.selectUser);
this.closePage();
}else if(this.type==5){
uni.setStorageSync('selectUser',this.selectUser)
this.closePage();
}else if(this.type==6){
uni.setStorageSync('selectUser1',this.selectUser)
this.closePage();
}else{
this.$api.msgApi.removeUser({user_ids:this.changeUser,id:this.contact_id}).then(res =>{
this.closePage();
})
}
}catch(e){
console.info(e);
this.closePage();
}
},
//
closePage(){
uni.hideLoading();
uni.navigateBack();
},
//
confirm: function(e) {
let arr = []
if (e) { //
var brr = this.userList.filter(value => {
//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>

57
pages/login/404.vue

@ -0,0 +1,57 @@
<template>
<view class="flex justify-center align-center" style="height: 100vh;background-color: #fff;">
<view>
<image src="../../static/image/123.gif" mode="widthFix"></image>
<view style="text-align: center;">
<view class="text1">抱歉!</view>
<view style="margin-bottom: 40rpx;">{{globalConfig.sysInfo.closeTips}}</view>
<view style="display: flex;justify-content: center;">
<a class="bullshit-return-home" href="/">返回首页</a>
</view>
</view>
</view>
</view>
</template>
<script>
import pinia from '@/store/index'
import { useloginStore } from '@/store/login';
const userStore = useloginStore(pinia);
export default {
data() {
return {
globalConfig:userStore.globalConfig,
}
},
created() {
},
methods: {
}
}
</script>
<style scoped>
.text1{
margin-bottom: 40rpx;
font-size: 64rpx;
font-weight: bold;
line-height: 80rpx;
color: #175CFF;
}
.bullshit-return-home{
display:block;
float:left;
width:110px;
height:36px;
font-size:14px;
line-height:36px;
color:#fff;
text-align:center;
cursor:pointer;
background:#175cff;
border-radius:100px;
text-decoration: none;
}
</style>

214
pages/login/index.vue

@ -0,0 +1,214 @@
<template>
<view>
<view style="height:150rpx;"></view>
<view class="padding im-flex im-rows im-justify-content-center mb-10">
<view class="im-flex im-rows im-justify-content-center">
<image class="login-logo " :src="globalConfig.sysInfo.logo ?? packData.logo" mode="fixWidth"></image>
</view>
</view>
<view class="im-flex im-rows im-justify-content-center">{{globalConfig.sysInfo.name ?? packData.name}}</view>
<form>
<view class="cu-form-group margin-top">
<view class="title">账号</view>
<input placeholder="账号" maxlength="32" name="input" v-model="loginForm.account"/>
</view>
<view class="cu-form-group" v-if="!forget">
<view class="title">密码</view>
<input placeholder="请输入密码" maxlength="32" type="password" name="input" v-model="loginForm.password"/>
</view>
<view class="cu-form-group" v-else>
<view class="title">验证码</view>
<input placeholder="请输入验证码" maxlength="6" name="input" v-model="loginForm.code"/>
<button class='cu-btn bg-blue shadow' @tap="sendCode">发送验证码</button>
</view>
</form>
<view class='forget'>
<view><switch class="switch" :checked="loginForm.rememberMe" :class="loginForm.rememberMe?'checked':''" @change="switchChange" style="transform:scale(0.7)" />记住我</view>
<view class="text-blue" @tap="forget=!forget;loginForm.code=''">{{forget ? '密码登陆' : '忘记密码'}}</view>
</view>
<view class="flex flex-direction im-login-btn">
<button class="cu-btn lg bg-blue" @tap="login()">登录</button>
</view>
<view class="flex flex-direction im-reg-btn" v-if="globalConfig && globalConfig.sysInfo.regtype==1">
<button class=" cu-btn lg bg-white" @tap="register()">注册</button>
</view>
<view class="m-20 c-666" v-if="globalConfig && globalConfig.demon_mode ">
<view class="f-16 remark-title mb-10">站点仅用于演示演示账号</view>
<view class="c-999">账号13800000002~13800000020</view>
<view class="c-999">密码123456</view>
</view>
<view class="footer-version c-999">
{{globalConfig.sysInfo.name ?? packData.name}} for {{packData.version}}
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
import packageData from "../../package.json"
import getSystemInfo from '../../service/getSystemInfo';
import { useMsgStore } from '@/store/message';
const loginStore = useloginStore(pinia)
const msgStore = useMsgStore(pinia)
export default {
data() {
return {
loginForm:{
account:'',
password:'',
code:'',
client_id:'',
rememberMe:false
},
forget:false,
packData:packageData,
globalConfig:loginStore.globalConfig
}
},
watch:{
forget(val){
if(val){
this.loginForm.password='123456';
}
}
},
onLoad(options){
// token,
const token=options.token;
if(token){
this.doLogin({token:token});
}
},
mounted() {
if(this.globalConfig && this.globalConfig.demon_mode){
const random = Math.floor(Math.random() * 19 + 2)
this.loginForm.account=13800000000+random;
this.loginForm.password='123456';
}
let LoginAccount=uni.getStorageSync('LoginAccount');
if(LoginAccount){
this.loginForm=LoginAccount;
}
},
methods: {
switchChange(e) {
this.loginForm.rememberMe=e.detail.value;
},
sendCode(){
if(!this.loginForm.account){
uni.showToast({
title: '请输入账号!',
icon: "none"
});
return false;
}
let data={
account:this.loginForm.account,
type:1
}
this.$api.LoginApi.sendCode(data).then((res)=>{
uni.showToast({
title: res.msg,
icon: "none"
});
})
},
register(){
uni.navigateTo({
url:"/pages/login/register"
})
},
login(){
if(this.loginForm.rememberMe){
uni.setStorageSync('LoginAccount',this.loginForm);
}else{
uni.removeStorageSync('LoginAccount');
}
if(this.loginForm.account==""){
uni.showToast({
title: '请输入账号!',
icon: "none"
});
return false;
}
if(this.loginForm.password==""){
uni.showToast({
title: '请输入密码!',
icon: "none"
});
return false;
}
let client_id=uni.getStorageSync('client_id');
if(client_id){
this.loginForm.client_id=client_id;
}
this.doLogin(this.loginForm);
const config = uni.getStorageSync('globalConfig')
getSystemInfo.batchInsertOrUpdate(config);
},
doLogin(data){
this.$api.LoginApi.login(data).then(res => {
if (res.code == 0) {
uni.setStorageSync('authToken', res.data.authToken)
const parts = res.data.userInfo.avatar.split('/')
let lastPart = parts.pop() || parts.pop() || ''
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart));
res.data.userInfo.imgname = isNumber ? lastPart+'.png' : lastPart
let userInfo=res.data.userInfo;
// wss
this.socketIo.send({
type: "bindUid",
user_id: userInfo.user_id,
token:res.data.authToken
});
loginStore.login(userInfo);
this.getNoticeCount()
uni.reLaunch({
url: '/pages/index/index'
})
}
})
},
getNoticeCount(){
this.$api.compaApi.getNoticeCount().then(res => {
msgStore.getCount(res.data.count);
})
},
}
}
</script>
<style scoped>
.login-logo {
width: 180rpx;
height: 180rpx;
font-size: 80rpx;
text-align: center;
line-height: 120rpx;
border-radius: 18rpx;
}
.footer-version{
width:100%;
text-align: center;
position: fixed;
bottom: 40rpx;
}
.remark-title{
font-weight: 600;
}
.im-reg-btn{
padding:30rpx;
}
.im-login-btn{
padding:0 30rpx;
}
.forget{
display: flex;
justify-content: space-between;
padding:30rpx;
}
</style>

183
pages/login/register.vue

@ -0,0 +1,183 @@
<template>
<view>
<cu-custom bgColor="bg-gradual-blue" :isBack="true">
<template #backText></template>
<template #content>账号注册</template>
</cu-custom>
<view style="height:100rpx;"></view>
<view class="padding im-flex im-rows im-justify-content-center mb-10">
<view class="im-flex im-rows im-justify-content-center">
<image class="login-logo " :src="globalConfig.sysInfo.logo ?? packData.logo" mode="fixWidth"></image>
</view>
</view>
<view class="im-flex im-rows im-justify-content-center">{{globalConfig.sysInfo.name ?? packData.name}}</view>
<form>
<view class="cu-form-group margin-top">
<view class="title">账号</view>
<input :placeholder="placeholder" class="uni-input" maxlength="32" name="input" v-model="regForm.account" @input="handleInput"/>
</view>
<view class="cu-form-group margin-top">
<view class="title">用户名/昵称</view>
<input placeholder="请输入用户名或昵称" maxlength="32" name="input" v-model="regForm.realname"/>
</view>
<view class="cu-form-group" v-if="parseInt(globalConfig.sysInfo.regauth)">
<view class="title">验证码</view>
<input placeholder="请输入验证码" maxlength="6" name="input" v-model="regForm.code"/>
<button class='cu-btn bg-blue shadow' @tap="sendCode">发送验证码</button>
</view>
<view class="cu-form-group">
<view class="title">密码</view>
<input placeholder="请输入密码" maxlength="32" type="password" name="input" v-model="regForm.password"/>
</view>
<view class="cu-form-group">
<view class="title">重复密码</view>
<input placeholder="请重复输入密码" maxlength="32" type="password" name="input" v-model="regForm.repass"/>
</view>
</form>
<view class="flex flex-direction im-login-btn">
<button class="cu-btn lg bg-blue" @tap="login()">注册</button>
</view>
<view class="footer-version c-999">
{{globalConfig.sysInfo.name ?? packData.name}} for {{packData.version}}
</view>
</view>
</template>
<script>
import { useloginStore } from '@/store/login'
import pinia from '@/store/index'
import packageData from "../../package.json"
const loginStore = useloginStore(pinia)
export default {
data() {
return {
regForm:{
account:'',
realname:'',
password:'',
repass:'',
code:''
},
placeholder:'请输入账号:4-32个字符',
forget:false,
packData:packageData,
globalConfig:loginStore.globalConfig
}
},
watch:{
// forget(val){
// if(val){
// this.regForm.password='123456';
// }
// }
},
mounted() {
let regauth=this.globalConfig.sysInfo.regauth ?? 0;
if(regauth==1){
this.placeholder='请输入手机号';
}else if(regauth==2){
this.placeholder='请输入邮箱账号';
}else if(regauth==3){
this.placeholder='请输入手机号/邮箱';
}
},
methods: {
handleInput(event) {
let value = event.detail.value;
let filteredValue = value.replace(/[\u4e00-\u9fa5]/g, '');
this.regForm.account = filteredValue;
},
sendCode(){
if(!this.regForm.account){
uni.showToast({
title: '请输入账号!',
icon: "none"
});
return false;
}
let data={
account:this.regForm.account,
type:2
}
this.$api.LoginApi.sendCode(data).then((res)=>{
uni.showToast({
title: res.msg,
icon: "none"
});
})
},
login(){
if(this.regForm.account==""){
uni.showToast({
title: '请输入账号!',
icon: "none"
});
return false;
}
if(this.regForm.realname==""){
uni.showToast({
title: '请输入用户名或者昵称!',
icon: "none"
});
return false;
}
if(this.regForm.password=="" || this.regForm.password.length<6 || this.regForm.password.length>16){
uni.showToast({
title: '请输入6-16位密码!',
icon: "none"
});
return false;
}
if(this.regForm.password!=this.regForm.repass){
uni.showToast({
title: '两次密码输入不相同!',
icon: "none"
});
return false;
}
this.$api.LoginApi.register(this.regForm).then(res => {
if (res.code == 0) {
setTimeout(()=>{
uni.reLaunch({
url: '/pages/login/index'
})
},2000)
}
})
},
}
}
</script>
<style scoped>
.login-logo {
width: 180rpx;
height: 180rpx;
font-size: 80rpx;
text-align: center;
line-height: 120rpx;
border-radius: 18rpx;
}
.footer-version{
width:100%;
text-align: center;
position: fixed;
bottom: 40rpx;
}
.remark-title{
font-weight: 600;
}
.im-reg-btn{
padding:30rpx;
}
.im-login-btn{
padding:30rpx;
}
.forget{
padding:30rpx;
text-align: right;
}
</style>

41
pages/mapselect/mapselect.vue

@ -0,0 +1,41 @@
<template>
<map-Point @commitCheck="commitCheck" :mapKey='mapKey' :Radius='Radius' :showResetting='showResetting' :listIco='listIco' :orientationIco='orientationIco' :resettingIco='resettingIco' :configData='configData'></map-Point>
</template>
<script>
export default {
data() {
return {
mapKey:'99e0c683598315f86d872c5ea3cf0a7b',
address: '',
latitude: '',
longitude: '',
listIco:'/uni_modules/map-Point/static/item-inx.png',
orientationIco:'/uni_modules/map-Point/static/map-inx.png',
resettingIco:"/uni_modules/map-Point/static/position.png",
showResetting:true,
Radius:'',
// jsSdk
configData:{
debug:false,// ,apialertpclogpc
appId:'',//
timestamp: '', //
nonceStr: '', //
signature: '', //
}
};
},
methods: {
commitCheck(e) {
console.log(e, 565);
uni.$emit('commitCheck', e);
uni.navigateBack({
delta: 1
});
}
}
};
</script>
<style>
</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>

1919
pages/message/chat.vue

File diff suppressed because it is too large

965
pages/message/detail.vue

@ -0,0 +1,965 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>聊天信息</template>
</cu-custom>
<view>
<view class="bg-white">
<view class="user-list im-flex im-justify-content-start im-align-items-center im-a im-wrap" v-if="is_group<2">
<view class="user-info mt-20" v-for="(item,index) in userList" :key="index" align="center">
<block v-for="(iteme,indexs) in imglist" :key="indexs" v-if="network_log=='none'">
<image class="user-avatar" v-if="item.userInfo.imgname === iteme.name" :src="iteme.path" @tap="openChatDetail(item.userInfo)"></image>
</block>
<image v-else class="user-avatar" :src="item.userInfo.avatar" @tap="openChatDetail(item.userInfo)"></image>
<view class="text-center user-name text-overflow">{{item.userInfo.displayName}}</view>
</view>
<view class="user-info mt-20" v-if="isAuth || is_group==0 || contact.setting.invite=='1'">
<view class="user-opt radius-8" style='margin:auto' @tap='editUser(2)'>
<view class="icon cuIcon-add f-24"></view>
</view>
<view class="f-11 mt-5">添加成员</view>
</view>
<view class="user-info mt-20" v-if="isAuth">
<view class="user-opt radius-8" style='margin:auto' @tap='manageUser()'>
<view class="icon cuIcon-move f-24"></view>
</view>
<view class="f-11 mt-5">移除成员</view>
</view>
</view>
<navigator v-if="is_group==1 " class="mt-10" :url="`/pages/message/group/groupUser?group_id=${contact_id}&group_id1=${contact.group_id}`">
<view class="text-center pb-15 pt-15 im-flex im-justify-content-center im-align-items-center">
<text class="gui-list-title-text gui-list-one-line gui-primary-color">查看全部群成员</text>
<text class="gui-list-title-desc gui-color-gray">{{groupUserCount}}</text>
<text class="cuIcon-right"></text>
</view>
</navigator>
</view>
<view class="cu-list menu mt-15 bg-white" v-if="is_group==1">
<view class="cu-item" @click="open">
<view class="content padding-tb-sm">
<view> 群聊名称 </view>
</view>
<view class="action">
<view class="text-grey">{{contact.displayName}} <text class="cuIcon-right"></text></view>
</view>
</view>
<view class="cu-item" @click="openQr" v-if="contact.setting.invite">
<view class="content padding-tb-sm">
<view> 群二维码 </view>
</view>
<view class="action">
<view class="text-grey"><text class="cuIcon-qr_code f-18"></text> <text class="cuIcon-right"></text></view>
</view>
</view>
<view class="cu-item" @tap="openModel('notice')">
<view class="content padding-tb-sm">
<view> 群公告 </view>
</view>
<view class="action" style="width:80%">
<view class="text-grey im-flex im-justify-content-end">
<view class="text-overflow notice-line">
<!-- {{contact.notice ?? '暂无公告'}} -->
{{contact.notice==null?'暂无公告':contact.notice}}
</view>
<text class="cuIcon-right"></text>
</view>
</view>
</view>
<view class="cu-item" v-if="isAuth" @tap="openModel('manage')">
<view class="content padding-tb-sm">
<view> 群管理 </view>
</view>
<view class="action">
<view class="text-grey"><text class="cuIcon-right"></text></view>
</view>
</view>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog mode="input" :value="contact.displayName" title="修改群名称" :duration="2000" :before-close="true" @close="closePop" @confirm="editGroupName">
</uni-popup-dialog>
</uni-popup>
</view>
<view class="cu-list menu mt-15 bg-white">
<view class="cu-item">
<view class="content padding-tb-sm">
<view> 消息免打扰 </view>
</view>
<view class="action">
<switch class="switch" @change="setIsNotice" :class="contactUser.is_notice==0?'checked':''" :checked="contactUser.is_notice==0?true:false"></switch>
</view>
<!-- <view class="action" v-else>
<switch class="switch" @change="setIsNotice" :class="!contact.is_notice?'checked':''" :checked="!contact.is_notice?true:false"></switch>
</view> -->
</view>
<view class="cu-item">
<view class="content padding-tb-sm">
<view> 置顶聊天 </view>
</view>
<view class="action">
<switch class="switch" @change="setIsTop" :class="contactUser.is_top?'checked':''" :checked="contactUser.is_top?true:false"></switch>
</view>
<!-- <view class="action" v-else>
<switch class="switch" @change="setIsTop" :class="contact.is_top?'checked':''" :checked="contact.is_top?true:false"></switch>
</view> -->
</view>
<view class="cu-item" v-if="contact.user_id">
<view class="content padding-tb-sm">
<view> 加入黑名单 </view>
</view>
<view class="action">
<switch class="switch" @change="setis_blacklist($event,contact.user_id)" :class="contactUser.is_blacklist?'checked':''" :checked="contactUser.is_blacklist?true:false"></switch>
</view>
</view>
<view class="cu-item menu bg-white" @click="onDeleterecord" v-if="userConfig.chatInfo.messageOneClickDel=='1'">
<view class="content padding-tb-sm">
<view> 一键清除聊天记录 </view>
</view>
<view class="action">
<view class="text-grey"><text class="cuIcon-right"></text></view>
</view>
</view>
<view class="cu-list menu bg-white" @click="showRecord" v-if="userConfig.chatInfo.userMsgClear=='1'">
<view class="cu-item">
<view class="content padding-tb-sm">
<view> 自动清除聊天记录 </view>
</view>
<view class="action">
<view class="text-grey"><text class="cuIcon-right"></text></view>
</view>
</view>
</view>
</view>
<!-- #ifndef H5 -->
<view class="cu-list menu mt-15 bg-white" @tap="modelName='setBg'">
<view class="cu-item">
<view class="content padding-tb-sm">
<view> 设置当前聊天背景 </view>
</view>
<view class="action">
<view class="text-grey"><text class="cuIcon-right"></text></view>
</view>
</view>
</view>
<!-- #endif -->
<navigator class="mt-10" :url="`/pages/message/record?id=${contact_id}`">
<view class="cu-list menu mt-15 bg-white">
<view class="cu-item">
<view class="content padding-tb-sm">
<view> 查看聊天记录 </view>
</view>
<view class="action">
<view class="text-grey"><text class="cuIcon-right"></text></view>
</view>
</view>
</view>
</navigator>
<view class="cu-list menu mt-15 bg-white" v-if="is_group==1 && isAdmin" @tap="clearMessage">
<view class="cu-item text-center delete-btn">
<text class="c-orange">清空聊天记录</text>
</view>
</view>
<view class="cu-list menu mt-15 bg-white" v-if="is_group==1" @tap="removeGroup">
<view class="cu-item text-center delete-btn">
<text class="c-red">{{isAdmin ? '解散群聊' : '退出群聊' }}</text>
</view>
</view>
<view class="parting-line-20"></view>
<view class="cu-modal bottom-modal" :class="modelName=='notice'&&contact.setting.manage=='0'||modelName=='notice'&&contact.role<3?'show':''">
<view class="cu-dialog">
<view class="cu-bar bg-white">
<view class="action text-gray" @tap="closeModel">取消</view>
<view class="action text-green" @tap="editNotice">保存</view>
</view>
<view class="notice-content">
<textarea class="im-textarea" maxlength="-1" v-model="contact.notice" placeholder="请输入公告内容..."></textarea>
<!-- :disabled="!isAuth" -->
</view>
</view>
</view>
<view class="cu-modal bottom-modal" :class="modelName=='setBg'?'show':''" @tap="modelName=''">
<view class="cu-dialog" @tap.stop=''>
<view class="cu-bar">
<view class="action" >设置当前聊天背景</view>
<view class="action cuIcon-close f-18" @tap="modelName=''"></view>
</view>
<view class="cu-list menu mb-15 bg-white">
<view class="cu-item" @click="chooseImg()">
<view class="content padding-tb-sm">
<view>选取背景图片</view>
</view>
<view class="action">
<view class="text-grey"><text class="cuIcon-right"></text></view>
</view>
</view>
</view>
<view v-if="bgInfo.image">
<view><image :src="bgInfo.image" style="width:200px" mode="widthFix"></image></view>
<button class="cu-btn bg-red mt-10" @tap="removeBg">移除背景图片</button>
</view>
<view class="cu-list menu mt-15 mb-15 bg-white">
<view class="cu-item">
<view class="content padding-tb-sm">
<view>背景虚化</view>
</view>
<view class="action">
<switch class="switch" @change="setFilter" :class="bgInfo.filter?'checked':''" :checked="bgInfo.filter == true ? true :false"></switch>
</view>
</view>
</view>
<uni-notice-bar text="修改后重新进入聊天才能生效" class="mb-15"/>
</view>
</view>
<view class="cu-modal bottom-modal" :class="modelName=='manage'?'show':''">
<view class="cu-dialog">
<view class="cu-bar bg-white">
<view class="action text-gray" @tap="closeModel">取消</view>
<view class="action text-green" @tap="saveManage">保存</view>
</view>
<view class="manage-content">
<view class="cu-list menu mt-15 bg-white">
<view class="cu-item">
<view class="content padding-tb-sm">
<view>仅群主和群管理员可以管理</view>
<view class="text-gray text-sm"> 启用后其他成员不能修改群名称编辑公告等</view>
</view>
<view class="action">
<switch class="switch" @change="setManage" :class="contact.setting.manage=='1'?'checked':''" :checked="contact.setting.manage=='1'?true:false"></switch>
</view>
</view>
<view class="cu-item">
<view class="content padding-tb-sm">
<view>允许群成员邀请</view>
<view class="text-gray text-sm">启用后其他成员可以邀请其他人加入群聊</view>
</view>
<view class="action">
<switch class="switch" @change="setInvite" :class="contact.setting.invite=='1'?'checked':''" :checked="contact.setting.invite=='1'?true:false"></switch>
</view>
</view>
<view class="cu-item">
<view class="content padding-tb-sm">
<view>允许成员查看历史消息</view>
<view class="text-gray text-sm">启用后新入群的成员可以查看所有的历史记录</view>
</view>
<view class="action">
<switch class="switch" @change="setHistory" :class="contact.setting.history=='1'?'checked':''" :checked="contact.setting.history=='1'?true:false"></switch>
</view>
</view>
<view class="cu-item">
<view class="content padding-tb-sm">
<view>允许添加群成员为好友</view>
<view class="text-gray text-sm">启用后成员可以互相查看资料并添加为好友或发消息</view>
</view>
<view class="action">
<switch class="switch" @change="setProfile" :class="contact.setting.profile=='1'?'checked':''" :checked="contact.setting.profile=='1'?true:false"></switch>
</view>
</view>
<uni-section title="群聊禁言" type="line">
<radio-group class="block" @change="setSpeak">
<view class="cu-form-group" v-for="(item,indexs) in radioList" :key="indexs">
<view class="title">{{item.label}}</view>
<radio :class="contact.setting.nospeak==item.value?'checked':''" :checked="contact.setting.nospeak==item.value?true:false" :value="item.value.toString()"></radio>
</view>
</radio-group>
</uni-section>
</view>
</view>
</view>
</view>
<view class="cu-modal bottom-modal" :class="bolRecord=='manage'?'show':''">
<view class="cu-dialog">
<view class="cu-bar bg-white">
<view class="action text-gray" @tap="RecordModel">取消</view>
<view class="action text-green" @tap="RecordManage">保存</view>
</view>
<view class="manage-content">
<radio-group class="block" @change="setuserMsgClear">
<label class="cu-form-group" v-for="(tag,tagindexs) in userConfig.chatInfo.userMsgClearDay" :key="tagindexs">
<view class="title">{{tag.title}}</view>
<radio :checked="MsgClear==tag.value?true:false" :value="tag.value"></radio>
<!-- :class="MsgClear==tag.title?'checked':''" -->
</label>
</radio-group>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { useMsgStore } from '@/store/message';
// #ifdef APP-PLUS
import groupUserList from '@/service/groupUserList';
import {getSavedImages1} from '@/utils/LocalFileSystemURL.js'
import getMessageList from '@/service/getMessageList';
// #endif
import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useloginStore } from '@/store/login';
const userStore = useloginStore(pinia)
const msgStore = useMsgStore(pinia)
const {network_log} = storeToRefs(msgStore);
export default {
components: {
},
data() {
return {
pageLoading: true,
contact_id: null, //id,
is_group:0,
groupUserCount:0,
modelName:false,
userList: [], //
allUser:[],
userInfo:userStore.userInfo,
userConfig:userStore.globalConfig,
groupInfo:uni.getStorageSync('setgroupInfo'),
groupInfo1:uni.getStorageSync('setgroupInfo'),
chatRecordlist: [{
text: '文本',
icon: "icon-wenben",
type: 'text'
},
{
text: '图片',
icon: "icon-zhaopian",
type: 'image'
}, {
text: '文件',
icon: "icon-wenjian",
type: 'file'
}, {
text: '视频',
icon: "icon-shipin",
type: 'video'
}, {
text: '项目',
icon: "icon-xiangmu_2",
type: 'project'
}, {
text: '客户',
icon: "icon-kehu",
type: 'leads'
},
],
radioList: [{
label: "关闭",
value: 0
},
{
label: "仅管理员可发言",
value: 1
},
{
label: "仅群主可发言",
value: 2
},
],
isAuth: false, //
contact: null, //
isAdmin:false, //
isManage: false, //
user_ids: [],
user:[],//
bgInfo:{
image:'',
filter:false
},
imglist:[],
network_log:network_log,
contactUser:{},
bolRecord:false,
MsgClear:0,
MsgClearDay:0
}
},
onShow() {
const _this = this
if(this.network_log == 'none'){
_this.Getchatinformation()
_this.getImagePath()
}else{
_this.getUserlist()
// _this.Getchatinformation()
// _this.getImagePath()
}
},
onLoad: function(options) {
let bgInfo=uni.getStorageSync('chat-bg-info'+options.id);
if(bgInfo){
this.bgInfo=bgInfo;
}
this.is_group = options.is_group;
this.contact_id = options.id;
let contact=msgStore.getContact(this.contact_id);
if(!contact){
uni.showToast({
title:'联系人不存在',
icon:'none',
duration:1500,
complete:(res)=>{
uni.reLaunch({
url: '/pages/index/index'
})
}
})
return;
}
if(options.notice!=='undefined'){
this.contact=contact;
this.contact.notice = uni.getStorageSync('notice');
}else{
this.contact=contact;
}
if(this.is_group==0){
contact.userInfo={
id:contact.user_id,
account:contact.account,
displayName:contact.displayName,
avatar:contact.avatar
}
this.allUser.push(contact);
this.userList.push(contact);
}
if(this.is_group==1){
this.MsgClear = this.groupInfo.clear_msg_day;
}else{
this.getFriendInfo()
}
},
methods: {
getFriendInfo(){
this.$api.msgApi.getFriendInfo({friend_user_id:this.contact_id}).then(res => {
this.contactUser = res.data;
this.MsgClearDay = this.contactUser.clear_msg_day;
this.MsgClear = this.contactUser.clear_msg_day;
})
},
showRecord(){
this.bolRecord = 'manage';
},
RecordModel(){
if(this.is_group==1){
this.MsgClear = this.groupInfo.clear_msg_day;
}else{
this.MsgClear = this.contactUser.clear_msg_day;
}
this.bolRecord = false;
},
RecordManage(){
if(this.is_group==1){
this.$api.msgApi.groupClearMsgDay({id: this.contact.id,day: this.MsgClearDay}).then(res=>{
this.groupInfo.clear_msg_day = this.MsgClearDay
msgStore.checkMsg(res.data);
msgStore.appendMsg(res.data);
uni.setStorageSync('setgroupInfo',this.groupInfo);
this.deleteList(res.data)
})
}else{
this.$api.msgApi.friendClearMsgDay({to_user: this.contact.id,day: this.MsgClearDay}).then(res=>{
this.getFriendInfo()
this.deleteList(res.data)
msgStore.checkMsg(res.data);
msgStore.appendMsg(res.data);
})
}
this.bolRecord = false;
},
async deleteList(data){
await getMessageList.deleteList(data)
},
async deleteallList1(data){
await getMessageList.deleteallList1(data)
},
openModel(model){
this.modelName=model;
},
closeModel(){
this.modelName=false;
},
saveManage(){
if(!this.isAuth) return;
this.$api.msgApi.groupSetting({
id: this.contact.id,
setting: this.contact.setting
})
this.modelName=false;
},
setManage(e){
this.contact.setting.manage=e.detail.value ? '1' : '0';
},
setInvite(e){
this.contact.setting.invite=e.detail.value ? '1' : '0';
},
setHistory(e){
this.contact.setting.history=e.detail.value ? '1' : '0';
},
setProfile(e){
this.contact.setting.profile=e.detail.value ? '1' : '0';
},
setSpeak(e){
this.contact.setting.nospeak=e.detail.value;
},
setuserMsgClear(e){
this.MsgClear = e.detail.value;
// const val = this.userConfig.chatInfo.userMsgClearDay.filter(item=>item.value==e.detail.value)
this.MsgClearDay = e.detail.value;
},
setIsNotice(e){
// if(this.contact.user_id){
console.log(e.detail.value);
this.contactUser.is_notice=e.detail.value ? 0 : 1;
this.contact.is_notice=e.detail.value ? 0 : 1;
// }else{
// this.contact.is_notice=e.detail.value ? 0 : 1;
// }
this.$api.msgApi.isNoticeAPI({
id: this.contact.id,
is_group:this.contact.is_group,
is_notice:this.contact.is_notice
})
},
setIsTop(e){
// if(this.contact.user_id){
this.contactUser.is_top=e.detail.value ? 1 : 0;
this.contact.is_top=e.detail.value ? 1 : 0;
// }else{
// this.contact.is_top=e.detail.value ? 1 : 0;
// }
this.$api.msgApi.setChatTopAPI({
id: this.contact.id,
is_group:this.contact.is_group,
is_top:this.contact.is_top
})
},
setis_blacklist(e,id){
this.contactUser.is_blacklist=e.detail.value ? 1 : 0;
this.$api.msgApi.isBlacklist({friend_user_id:id}).then( res =>{
if(res.code==400){
this.contactUser.is_blacklist = 0
}
})
},
editNotice(){
// if(!this.isAuth) return;
this.$api.msgApi.setNotice({
id: this.contact.id,
notice: this.contact.notice
})
this.modelName=false;
},
open() {
this.$refs.popup.open()
},
openQr() {
uni.navigateTo({
url: '/pages/index/qrcode?group_id='+ this.contact.id
})
},
editGroupName(e){
this.$api.msgApi.editGroupName({id:this.contact.id,displayName:e}).then( res =>{
if(res.code!==400){
this.contact.displayName=e;
}
this.$refs.popup.close()
})
},
closePop(){
this.$refs.popup.close()
},
//
removeGroup() {
// 退
let txt="退出群聊";
if(this.isAdmin) txt="解散群聊";
uni.showModal({
title: '确定要'+txt+'吗?',
success: e => {
if (e.confirm) {
if(this.isAdmin){
this.$api.msgApi.removeGroup({id:this.contact.id}).then((res)=>{
//
uni.reLaunch({
url: '/pages/index/index'
})
})
}else{
this.$api.msgApi.removeUser({id:this.contact.id,user_id:this.userInfo.user_id}).then((res)=>{
//
uni.reLaunch({
url: '/pages/index/index'
})
})
}
}
}
});
},
clearMessage() {
// 退
if(!this.isAdmin) {
uni.showToast({
title:'无权操作',
icon:'none'
})
};
uni.showModal({
title: '删除消息会从当前聊天记录中被删除,确定继续吗?',
success: e => {
if (e.confirm) {
this.$api.msgApi.clearMessage({id:this.contact.id}).then((res)=>{
uni.showToast({
title:'清除成功',
icon:'none'
})
})
}
}
});
},
//
editUser(type) {
this.user_ids = this.allUser.map(item => item.user_id)
if(this.contact.is_group==0){
type=1
}
uni.navigateTo({
url: '/pages/index/userSelection?type='+type+'&contact_id=' + this.contact.id
})
},
//
manageUser() {
uni.navigateTo({
url: '/pages/message/group/groupUser?group_id=' + this.contact.id
})
},
//
goChatRecord(type) {
uni.navigateTo({
url: '/package/message/pages/chatRecord/chatRecord?type=' + type + '&toContactId=' + this.contact_id + '&is_group=1'
})
},
//
getUserlist() {
if(this.is_group==0) return;
this.userList = []
this.$api.msgApi.groupUserList({
group_id: this.contact_id,
limit:20000,
}).then(res => {
this.user = res.data
this.Insertchatmessage(res.data)
res.data.forEach(res => {
// #ifdef APP-PLUS
uni.downloadFile({ url: res.userInfo.avatar,success: (downloadResult) => {
this.saveToPermanentStorage(downloadResult.tempFilePath);
}})
// #endif
})
if (res.code !== 0) return
//
const admin=res.data.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id)
if(admin.length) this.isAdmin=true;
//
const manage=res.data.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id)
if(manage.length) this.manage=true;
//
if(admin.length || manage.length) this.isAuth=true;
this.allUser=JSON.parse(JSON.stringify(res.data));
if (res.data.length > 18) {
if (this.isAuth) {
// this.userList = res.data.splice(0, 18)
this.userList = res.data.splice(0, 20000)
}else if(this.contact.setting.invite){
// this.userList = res.data.splice(0, 19)
this.userList = res.data.splice(0, 20000)
} else {
// this.userList = res.data.splice(0, 20)
this.userList = res.data.splice(0, 20000)
}
} else {
this.userList = res.data
}
this.groupUserCount=res.count;
this.pageLoading = false;
})
},
async Getchatinformation(){
// #ifdef APP-PLUS
const groups = await groupUserList.getList({group_id: this.contact.group_id});
groups.map((res)=>{
res.userInfo = JSON.parse(res.userInfo)
})
this.user = groups
if (groups.length == 0) return
//
const admin=groups.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id)
if(admin.length) this.isAdmin=true;
//
const manage=groups.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id)
if(manage.length) this.manage=true;
//
if(admin.length || manage.length) this.isAuth=true;
this.allUser=JSON.parse(JSON.stringify(groups));
this.groupUserCount=groups.length;
this.userList = groups
this.pageLoading = false;
console.info('获取聊天信息数据',groups.length,groups);
// #endif
},
async Insertchatmessage(val){
// #ifdef APP-PLUS
val.forEach((item)=>{
const parts = item.userInfo.avatar.split('/')
let lastPart = parts.pop() || parts.pop() || ''
const isNumber = !isNaN(lastPart)&&!isNaN(parseFloat(lastPart));
item.userInfo.imgname = isNumber ? lastPart+'.png' : lastPart;
})
// console.log('',val);
await groupUserList.batchInsertOrUpdate(val);
// #endif
},
// App
saveToPermanentStorage(tempPath) {
return new Promise((resolve, reject) => {
//
plus.io.resolveLocalFileSystemURL(
'_doc',
(docDir) => {
//
docDir.getDirectory(
'img1',
{ create: true, exclusive: false },
(entry) => {
//
const fileName = this.getFileName(tempPath);
const fileName1 = this.getFileName(docDir.fullPath + 'img1/' +fileName);
//
entry.getFile(fileName1,{ create: false }, //
(fileEntry) => {
// console.log('');
//
reject(new Error('File already exists: ' + fileName));
},
(error) => {
// console.log(error);
//
if (error.code === 14) { // 1
this.copyFile(tempPath, entry, fileName, resolve, reject);
} else {
reject(error);
}
}
);
},
(error) => {
reject(error);
}
);
},
(error) => {
reject(error);
}
);
});
},
//
copyFile(tempPath, targetDir, fileName, resolve, reject) {
plus.io.resolveLocalFileSystemURL(
tempPath,
(tempEntry) => {
tempEntry.copyTo(
targetDir,
fileName,
(newEntry) => {
resolve(newEntry.toLocalURL());
},
(error) => {
reject(error);
}
);
},
(error) => {
reject(error);
}
);
},
//
getFileName(path) {
const index = path.lastIndexOf('/');
let fileName = path.substr(index + 1);
fileName = fileName.replace(/\(\d+\)(?=\.[^./]+$)/, '');
return fileName;
},
//
async getImagePath(){
this.imglist = await getSavedImages1()
this.imglist.map(item => {
item.path = plus.io.convertLocalFileSystemURL(item.path)
});
console.info('读取地址',this.imglist);
},
//
openChatDetail(item){
if(this.userInfo.user_id==item.id) return;
let friend=msgStore.getContact(item.id);
// this.contact_id
if(this.contact.role<3 || this.contact.setting.profile=='1' || friend){
uni.navigateTo({
url:"/pages/contacts/detail?id="+item.id
})
}else{
uni.showToast({
title:'已开启用户隐私!',
icon:'none'
})
return false;
}
},
setFilter(e){
this.bgInfo.filter=e.detail.value ? true : false;
uni.setStorageSync('chat-bg-info'+this.contact.id,this.bgInfo)
},
chooseImg(){
uni.chooseImage({
count : 1,
sizeType : ['compressed'],
sourceType : ['album', 'camera'],
success : (res)=>{
const tempFiles = res.tempFiles;
tempFiles.forEach((item) => {
uni.saveFile({
tempFilePath:item.path,
success:(res)=>{
this.bgInfo.image=res.savedFilePath;
uni.setStorageSync('chat-bg-info'+this.contact.id,this.bgInfo)
uni.showToast({
title:'设置成功,重新进入聊天后生效',
icon:'none'
})
}
})
})
}
});
},
removeBg(){
this.bgInfo.image='';
uni.setStorageSync('chat-bg-info'+this.contact.id,'')
},
onDeleterecord(){
const removeval = {group_id:this.contact.is_group==1?this.contact.id:'',form_user:this.userInfo.user_id,to_user:this.contact.id}
uni.showModal({
title: '提示',
content: `是否要删除${this.contact.is_group==1?'群聊':'个人'}消息,需注意点击后所有人的聊天记录会消失`,
success:(res)=>{
if (res.confirm) {
if(this.contact.is_group==1){
this.$api.msgApi.groupremoveAllMessage({id: this.contact.id}).then(res => {
if(res.code==0){
uni.showToast({
title:'删除成功',
icon:'none'
})
this.deleteallList1(removeval)
}
})
}else{
this.$api.msgApi.friendremoveAllMessage({to_user: this.contact.id}).then(res => {
if(res.code==0){
uni.showToast({
title:'删除成功',
icon:'none'
})
this.deleteallList1(removeval)
}
})
}
}
}
});
}
}
}
</script>
<style lang="scss" scoped>
.user-list{
padding:0 20rpx 20rpx;
.user-info{
width:142rpx;
height:130rpx;
text-align:center;
.user-avatar{
width:100rpx;
height:100rpx;
border-radius: 16rpx;
}
.user-name{
width:100rpx;
margin:0 auto;
font-size: 22rpx;
}
.user-opt{
border:1px dashed #999;
height:98rpx;
width:98rpx;
display: flex;
align-items: center;
justify-content: center;
.icon{
color:#999 !important;
}
}
}
}
.delete-btn{
justify-content:center !important ;
}
.notice-content{
width:100%;
min-height:480rpx;
.im-textarea{
width:100%;
min-height:480rpx;
padding:20rpx;
text-align:left;
}
}
.notice-line{
width:70%;
text-align: right;
}
</style>

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>

349
pages/message/group/groupUser.vue

@ -0,0 +1,349 @@
<template>
<view>
<cu-custom bgColor="bg-main-bar" :isBack="true">
<template #backText></template>
<template #content>群成员</template>
</cu-custom>
<view class="cu-bar bg-white search fixed" :style="[{top:CustomBar + 'px'}]">
<view class="search-form round">
<text class="cuIcon-search"></text>
<input type="text" v-model="keywords" placeholder="输入搜索的关键词" confirm-type="search"/>
</view>
</view>
<view style="margin-top: 104rpx;">
<view class="cu-list menu-avatar">
<view class="cu-item" v-for="(item,index) in userList" :key="index">
<block v-for="(iteme,indexs) in imglist" :key="indexs" v-if="network_log == 'none'">
<view class="cu-avatar round lg" v-if="item.userInfo.imgname === iteme.name" :style="'background-image:url('+iteme.path+');'" @tap="openChatDetail(item)"></view>
</block>
<view v-else class="cu-avatar round lg" :style="'background-image:url('+item.userInfo.avatar+');'" @tap="openChatDetail(item)"></view>
<view class="content">
<view class="text-grey">
<view class="text-cut">{{item.userInfo.displayName}}</view>
<view v-if="item.role<3" class="cu-tag round sm" :class="item.role==1 ? 'bg-red' : 'bg-orange'">{{item.role ==1?"群主":item.role==2?'管理员':''}}</view>
<view v-if="item.user_id==userInfo.user_id" class="cu-tag round sm"></view>
</view>
<view v-if="noSpeakExp(item.no_speak_time)" class="text-red text-xs">禁言至{{noSpeakExp(item.no_speak_time)}}</view>
</view>
<view class="action" @tap="openModel(item)">
<view class="text-grey text-sm"> <text class="cuIcon-more f-24" v-if="item.role>1 && isAuth" ></text></view>
</view>
</view>
<Empty v-if="!userList.length" noDatatext="未搜索到数据" textcolor="#999" ></Empty>
</view>
</view>
<view class="cu-modal bottom-modal" :class="modelName=='userOpt'?'show':''">
<view class="cu-dialog">
<view class="manage-content">
<view class="cu-list menu bg-white">
<view class="cu-item" v-if="curUser">
<view class="content im-flex im-justify-content-center im-align-items-center">
<view class="cu-avatar round sm" :style="'background-image:url('+(curUser.avatar)+');'"></view>
<view class="text-cut ml-5">{{curUser.realname}}</view>
<view v-if="curUser.role==2" class="cu-tag round sm bg-orange">管理员</view>
</view>
</view>
<view class="cu-item" @tap="changeOwner()" v-if="isAdmin">
<view class="content padding-tb-sm">
<!-- <text class="c-orange">转让管理权限</text> -->
<text class="c-orange">转让群主</text>
</view>
</view>
<view class="cu-item" @tap="setManage()" v-if="isAdmin">
<view class="content padding-tb-sm">
<text>{{curUser.role==2 ? '取消管理员' : '设为管理员'}}</text>
</view>
</view>
<view class="cu-item" @tap="showNoSpeak()" v-if="isAdmin">
<view class="content padding-tb-sm">
<text>设置禁言</text>
</view>
</view>
<view class="cu-item" @tap="removeUser()">
<view class="content padding-tb-sm">
<text>移出群聊</text>
</view>
</view>
<view class="parting-line-5"></view>
<view class="cu-item" @tap="modelName=''">
<view class="content padding-tb-sm">
<text class="c-red">取消</text>
</view>
</view>
</view>
</view>
</view>
</view>
<uni-popup ref="nospeak" type="share" safeArea backgroundColor="#fff">
<uni-list>
<uni-list-item clickable v-for="(item,index) in noSpeakList" :key="index" :title="item.name" @click="setTime(item.id)" >
<template v-slot:footer>
<text class="cuIcon-check c-green" v-if="noSpeakTimer==item.id"></text>
</template>
</uni-list-item>
<uni-list-item title="自定义" clickable @click="setTime(0)" >
<template v-slot:footer>
<uni-number-box :min="1" :max="365" :value="noSpeakDay" />&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)
// #ifdef APP-PLUS
import {getSavedImages1} from '@/utils/LocalFileSystemURL.js'
import groupUserList from '@/service/groupUserList';
// #endif
import { storeToRefs } from 'pinia';
const {network_log} = storeToRefs(msgStore);
export default {
data() {
return {
keywords:'',
group_id: '',
group_id1: null,
modelName:'',
userList: [],
allUser:[],
isAdmin:false,
isManage:false,
isAuth:false,
noSpeakTimer:0,
noSpeakDay:1,
userInfo:userStore.userInfo,
curUser:{}, //
noSpeakList:[{
name:'10分钟',
id:1
},{
name:'1小时',
id:2
},{
name:'3小时',
id:3
},{
name:'1天',
id:4
}],
imglist:[],
network_log:network_log
}
},
watch:{
keywords(val){
if(val==''){
this.userList=this.allUser;
}else{
this.search();
}
},
},
onLoad(options) {
this.group_id = options.group_id?options.group_id:''
this.group_id1 = options.group_id1?options.group_id1:null
const _this = this
if(this.network_log == 'none'){
_this.Getchatinformation()
_this.getImagePath()
}else{
_this.getGroupuserlist()
// _this.Getchatinformation()
// _this.getImagePath()
}
},
methods: {
getGroupuserlist() {
this.userList = []
this.$api.msgApi.groupUserList({
group_id: this.group_id
}).then(res => {
const admin=res.data.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id)
if(admin.length) this.isAdmin=true;
const manage=res.data.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id)
if(manage.length) this.manage=true;
if(admin.length || manage.length) this.isAuth=true;
const allUser=res.data;
allUser.forEach((item)=>{
item.realname=item.userInfo.displayName;
item.name_py=item.userInfo.name_py;
})
this.allUser=allUser;
this.userList = res.data;
})
},
async Getchatinformation(){
// #ifdef APP-PLUS
const groups = await groupUserList.getList({group_id: this.group_id1});
groups.map((res)=>{
res.userInfo = JSON.parse(res.userInfo)
})
const admin=groups.filter(item => item.role == 1 && item.userInfo.id== this.userInfo.user_id)
if(admin.length) this.isAdmin=true;
const manage=groups.filter(item => item.role == 2 && item.userInfo.id== this.userInfo.user_id)
if(manage.length) this.manage=true;
if(admin.length || manage.length) this.isAuth=true;
const allUser=groups;
allUser.forEach((item)=>{
item.realname=item.userInfo.displayName;
item.name_py=item.userInfo.name_py;
})
this.allUser=allUser;
this.userList = groups;
console.info('获取群成员数据',groups.length,groups);
// #endif
},
//
async getImagePath(){
this.imglist = await getSavedImages1()
this.imglist.map(item => {
item.path = plus.io.convertLocalFileSystemURL(item.path)
});
console.info('读取地址',this.imglist);
},
openModel(item){
item.realname=item.userInfo.displayName;
item.avatar=item.userInfo.avatar;
this.curUser=item;
this.modelName='userOpt';
},
//
setManage(){
const role=this.curUser.role==2 ? 3 : 2;
this.$api.msgApi.setManager({
id:this.group_id,
user_id:this.curUser.user_id,
role:role
}).then((res)=>{
if(res.code==0){
this.getGroupuserlist();
}
this.modelName='';
})
},
//
removeUser(){
this.modelName='';
uni.showModal({
title: '确定要删除该成员吗?',
success: e => {
if (e.confirm) {
this.$api.msgApi.removeUser({
id:this.group_id,
user_id:this.curUser.user_id,
}).then((res)=>{
if(res.code==0){
this.getGroupuserlist();
}
})
}
}
});
},
changeOwner(){
this.modelName='';
uni.showModal({
// title: '?',
title: '确定将群主管理权限转移给该成员吗?',
success: e => {
if (e.confirm) {
this.$api.msgApi.changeOwner({
id:this.group_id,
user_id:this.curUser.user_id,
}).then((res)=>{
if(res.code==0){
uni.reLaunch({
url:'/pages/index/index'
})
}
})
}
}
});
},
search(){
const allUser=JSON.parse(JSON.stringify(this.allUser));
this.userList=this.$util.searchObject(allUser,['realname','name_py'],this.keywords);
},
openChatDetail(item){
if(this.userInfo.user_id==item.user_id) return;
let friend=msgStore.getContact(item.user_id);
let curContact=msgStore.getContact(this.group_id);
if(curContact.role<3 || curContact.setting.profile=='1' || friend){
uni.navigateTo({
url:"/pages/contacts/detail?id="+item.user_id
})
}else{
uni.showToast({
title:'已开启用户隐私!',
icon:'none'
})
return false;
}
},
showNoSpeak(){
this.modelName=''
this.$refs.nospeak.open()
},
setTime(val){
console.log(val);
this.noSpeakTimer=val;
},
setNoSpeak(){
this.$refs.nospeak.close();
this.$api.msgApi.setNoSpeak({
id:this.group_id,
user_id:this.curUser.user_id,
noSpeakDay:this.noSpeakDay,
noSpeakTimer:this.noSpeakTimer
}).then((res)=>{
this.noSpeakTimer=0;
this.noSpeakDay=1;
if(res.code==0){
this.getGroupuserlist();
}
})
},
noSpeakExp(time){
if(time * 1000>new Date().getTime()){
return this.$util.date('m-d H:i',time);
}else{
return false;
}
}
}
}
</script>
<style scoped lang="scss">
.list-image {
width: 80rpx;
height: 80rpx;
font-size: 0;
}
.share {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
}
.uni-popup{
z-index:9999 !important;
}
::v-deep uni-modal{
z-index: 1025;
}
</style>

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

Loading…
Cancel
Save