// uniapp people side tools
// #ifdef H5
// import device from 'current-device' // https://github.com/matthewhudson/current-device
// if (device.mobile()){
// }else if (device.desktop()){
// }else if (device.tablet()){
// }
// #endif
import './ican-H5Api.js' // 对齐H5Api: https://ext.dcloud.net.cn/plugin?id=415 // 注意要取消默认自带的 showToast https://uniapp.dcloud.io/api/system/clipboard?id=%e6%b3%a8%e6%84%8f
module.exports = {
// 用直观的色彩,代替 uview 的命名方式:
RED: 'error',
GREEN: 'success',
BLUE: 'primary',
YELLOW: 'warning',
GREY: 'info',
BLACK_TOAST: 'default',
WHITE_BUTTON: 'default',
BACKEND: 'SERVER', // 通过变量来动态切换后台类型:服务器 SERVER,或云服务 CLOUD。应当根据实际需要,在前端所用的 unitool 里覆盖。
// 快速输出详尽提示,可用来取代 console.log
clog(...message) {
console.log(
'【【【【【【【【【【',
getCurrentPages().length > 0 ? getCurrentPages().pop().route : 'pages/Welcome', // 在首页时,getApp() 或 getCurrentPages() 有可能获取不到。
...message,
'】】】】】】】】】】】'
)
},
thisPage(){
return getCurrentPages()[getCurrentPages().length - 1] // 准备挂在到 window 下使用,因此不必探测 this.$store 了,肯定不存在。
},
localizeText(i18nText) {
// 如果直接挂载到 Vue.prototype 下,那么可以直接访问 this.i18nText。但如果通过 this.$T.localeText 访问,那么 this.i18nText 就报错了。因此安全起见,先获取当前 page
const thisPage = this.$store ? this // 对于组件内定义的 i18nText,要使用 this 来获得组建内的 i18nText,而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: getCurrentPages()[getCurrentPages().length - 1]
if (i18nText && typeof(i18nText)==='object' && thisPage.$store?.state?.i18n?.mylang) {
return i18nText[thisPage.$store.state.i18n.mylang] || ''
}
if (!i18nText && typeof(thisPage.i18nText)==='object' && thisPage.$store?.state?.i18n?.mylang) {
return thisPage.i18nText[thisPage.$store.state.i18n.mylang] || ''
}
if (typeof i18nText === 'string'){
return i18nText
}
return ''
},
localeText() {
const thisPage = this.$store ? this // 对于组件内定义的 i18nText,要使用 this 来获得组建内的 i18nText,而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: getCurrentPages()[getCurrentPages().length - 1]
return thisPage.i18nText[thisPage.$store.state.i18n.mylang]
},
// setBarTitles 迁移到 user.i18n.uniapp 库,通过 this.$store.commit('i18n/setBarTitles') 来调用
// setBarTitles({ windowTitle, pageTitle } = {}) {
// let page = getCurrentPages()[getCurrentPages().length - 1]
// uni.setNavigationBarTitle({ title: pageTitle || page.i18nText[page.$store.state.i18n.mylang].tPageTitle })
// // #ifdef H5
// document.title = windowTitle || page.$store.getters['i18n/getAppName'] // 必须放在 setNavigationBarTitle 之后,否则会被其覆盖掉。
// // #endif
// if (page.$store._mutations['i18n/setTabbar']) page.$store.commit('i18n/setTabbar') // 必须要在有 tab 的页面里重置才有效果
// },
makeServerUrl(route = '') {
if (typeof route !== 'string') route = '' // 防止 route 为 null, undedefined 等由于后台数据库默认值而造成的异常。
if (/^https?:\/\//.test(route)) {
return route
}
let port = this.SERVER_PORT
// #ifdef H5
|| window.location.port
// #endif
let hostname
let protocol
if (process.env.NODE_ENV === 'production') {
hostname = this.SERVER_HOSTNAME
// #ifdef H5
|| window.location.hostname
// #endif
protocol = this.SERVER_PROTOCOL || 'https'
} else {
hostname =
// #ifdef H5
window.location.hostname ||
// #endif
this.SERVER_HOSTNAME_DEV // 在本机的手机模拟器里可以,在虚拟机的浏览器里也可以,但是运行到连接的iPhone里就无法连接,不知为何
protocol = 'http'
}
return `${protocol}://${hostname}:${port}/${route}`
},
makeBgUrl(path) {
if (path) {
return `url(${this.makeServerUrl(path)})`
}
return ''
},
// 再次封装 uni.request,输入参数和 uni.request 保持基本一致。主要为了插入 _passtoken,简化 url 的组装,以及输出提示。
async request({ method = 'POST', url, header = {}, data = {} }) {
url = this.makeServerUrl(url)
header._passtoken = uni.getStorageSync('_passtoken')
if (method === 'GET') {
// 如果不是 POST 方法,要额外把参数JSON化
for (let key in data) {
data[key] = JSON.stringify(data[key])
}
}
console.log('👇 👇 👇 👇 < Request > 👇 👇 👇 👇 ', { method, url, header, data }, '👆 👆 👆 👆 < /Request > 👆 👆 👆 👆')
let [error, response] = await uni.request({ method, url, header, data })
console.log('⬇️ ⬇️ ⬇️ ⬇️ < Response > ⬇️ ⬇️ ⬇️ ⬇️ ', response, '⬆️ ⬆️ ⬆️ ⬆️ < /Response > ⬆️ ⬆️ ⬆️ ⬆️')
return [error, response]
},
/** 统一 uni.request 和 uniCloud.callFunction 的调用方法,提供统一、透明的后台调用
* 返回值:{ _state, 成功结果或错误结果 },其中 _state 除了后台返回的,还可以是
* - CLIENT_BACKEND_BROKEN: 前端发现后台断线
* - CLIENT_BACKEND_TIMEOUT: 前端发现后台超时
* - CLINET_BACKEND_EXCEPTION: 前端发现后台异常
**/
async callBackend({ backend = this.BACKEND, httpMethod = 'POST', apiVersion = 'api', apiWho, apiTodo, apiWhat = {} }) {
console.log('👇 < BackendRequest > ', { apiWho, apiTodo, apiWhat }, ' < /BackendRequest > 👇')
let result = {}
if (backend === 'UNICLOUD') {
let { /* success, header, requestedId, */ result: resultCloud = {} } = await uniCloud
.callFunction({
name: apiWho,
data: {
apiTodo,
apiWhat,
_passtoken: uni.getStorageSync('_passtoken'),
// uniIdToken // uniCloud自动getStorageSync('uni_id_token')并传递为 uniIdToken;也可自行组装传入 uniIdToken
},
})
.catch((error) => { // {errMsg, stack} = error
if (/request:fail/.test(error.errMsg)) {
// 后台云服务无法连接
return { result: { _state: 'CLIENT_BACKEND_BROKEN' } }
} else {
// 后台云服务返回异常
return { result: { _state: 'CLIENT_BACKEND_EXCEPTION', error } }
}
})
result = resultCloud
} else {
if (httpMethod === 'GET') {
// 如果不是 POST 方法,要额外把参数JSON化
for (let key in apiWhat) {
apiWhat[key] = JSON.stringify(apiWhat[key])
}
}
let [error, { statusCode, header, errMsg, data: resultServer = {} } = {}] = await uni.request({
method: httpMethod,
url: this.makeServerUrl(`${apiVersion}/${apiWho}/${apiTodo}`),
header: { _passtoken: uni.getStorageSync('_passtoken') },
data: apiWhat,
})
if (error) {
if (error.errMsg === 'request:fail') {
// 后台服务器无法连接
result = { _state: 'CLIENT_BACKEND_BROKEN' }
} else if (error.errMsg === 'request:fail timeout') {
// 后台服务器超时
result = { _state: 'CLIENT_BACKEND_TIMEOUT' }
} else {
// 后台服务器返回异常
result = { _state: 'CLIENT_BACKEND_EXCEPTION', error }
}
} else {
result = resultServer
}
}
console.log('👆 < BackendResult > ️', JSON.stringify(result), '< /BackendResult > 👆') // 不知为何,直接用 result 会输出一个奇怪的对象,要主动添加 JSON.stringify 才按照期望输出。
return result
},
async pickupFile2Server({
mediaType = 'image',
count = 1,
sizeType = ['original', 'compressed'],
sourceType = ['album', 'camera'],
url,
header = {},
formData = {},
name = 'file',
} = {}) {
// 有的管理后台不需要登录就允许上传,例如 cmctoy。因此不要在这里依赖登录状态。
// if (uni.getStorageSync('_passtoken')) {
// header._passtoken = uni.getStorageSync('_passtoken')
// } else {
// return [{ _ERROR: 'USER_OFFLINE', errMsg: 'offline user cannot upload files' }, null]
// }
let filePath
if (mediaType === 'image') {
let [errorChoose, { tempFilePaths, tempFiles } = {}] = await uni.chooseImage({ count, sizeType, sourceType })
filePath = tempFilePaths[0]
} else if (mediaType === 'video') {
let [errorChoose, { tempFilePath }] = await uni.chooseVideo({ sourceType })
filePath = tempFilePath
} else {
return [{ _ERROR: 'UNKNOWN_MEDIATYPE' }, null]
}
if (filePath) {
for (let key in formData) {
// multer 不会自动处理 JSON 数据,必须前后端配合处理
formData[key] = JSON.stringify(formData[key])
}
uni.showLoading()
let [errorUpload, { data, statusCode } = {}] = await uni.uploadFile({ url: this.makeServerUrl(url), filePath, name, header, formData })
// 后台 Multer 处理 req.file = { destination, filename, originalname, path, mimetype, size },
// url 所在方法进一步处理后,通过 uploadFile 存在 data 里返回结果
uni.hideLoading()
if (typeof(data)==='string') {
try {
data = JSON.parse(data)
} catch (exception) {}
}
// return [errorUpload, response]
if (data) {
return { _state: 'SUCCESS', file: data }
}else {
return { _state: 'CLIENT_FAIL_UPLOAD_FILE', errorUpload }
}
}else {
return { _state: 'CLIENT_FAIL_CHOOSE_FILE' }
}
},
async pickupFile2Cloud({ mediaType = 'image', count = 1, sizeType = ['original', 'compressed'], sourceType = ['album', 'camera'], maxDuration } = {}) {
// 有的管理后台不需要登录就允许上传,例如 cmctoy。因此不要在这里依赖登录状态。
// if (!uni.getStorageSync('_passtoken')) {
// return { _state: 'USER_OFFLINE', errMsg: 'offline user cannot upload files' }
// }
let filePath,
cloudPath,
systemInfo = this.getSystemInfo()
if (mediaType === 'image') {
let [errorChoose, { tempFilePaths, tempFiles } = {}] = await uni.chooseImage({ count, sizeType, sourceType })
// uni.showModal({ title: 'tempFilePaths[0]=' + tempFilePaths[0] })
filePath = tempFilePaths[0] // 在 H5 上并不是文件路径名,而是类似 "blob:http://localhost:8080/f0d3e54d-0694-4803-8097-641d76a10b0d“。
// #ifndef H5
// let [errorGetImageInfo, { path, width, height, orientation, type }] = await uni.getImageInfo({ src: filePath })
// cloudPath = path // 完整路径,包含后缀名。形如 file:///var/mobile/Containers/Data/Application/55A76332-44F5-4D5F-A9F6-3F857D584883/Documents/Pandora/apps/D064A425A8BEC13F9D8F741B98B37BC5/doc/uniapp_temp_1598593902955/compressed/1598593925815.png
cloudPath = `APP_${systemInfo.platform}__${filePath}` // 在 iOS 上是 "_doc/uniapp_temp_1598593902955/compressed/1598593925815.png", 有时还包含从 file:/// 开始的完整路径名
// #endif
// #ifdef H5
cloudPath = `H5_${systemInfo.platform}_${systemInfo.browser}__${tempFiles[0].name}` // name is available in H5 only. 只包含文件名和后缀名,不包含路径。
// #endif
} else if (mediaType === 'video') {
let [errorChoose, { tempFilePath, tempFile, duration, size, width, height, name }] = await uni.chooseVideo({ sourceType, maxDuration })
// uni.showModal({ title: 'tempFilePath=' + tempFilePath })
filePath = tempFilePath // 在 iOS 上形如 "file:///var/mobile/Containers/Data/Application/55A76332-44F5-4D5F-A9F6-3F857D584883/Documents/Pandora/apps/26B43CD2F587D37FC6799108434A6F84/doc/uniapp_temp_1598596171580/gallery/IMG_3082.MOV"
// #ifndef H5
cloudPath = `APP_${systemInfo.platform}_dur${duration}__${filePath}`
// #endif
// #ifdef H5
cloudPath = `H5_${systemInfo.platform}_${systemInfo.browser}_dur${duration}__${name}` // tempFile and name are H5 only
// #endif
// iOS 上测试,filePath 为 *.MOV,而阿里云只允许 *.mp4, 所以添加 .mp4 后缀。参见 https://uniapp.dcloud.net.cn/uniCloud/storage?id=clouduploadfile
// 20200915测试,阿里云支持上传 *.mov 了。
if (!/\.(mp4|mov)$/i.test(cloudPath)) cloudPath = cloudPath + '.mp4'
} else {
return { _state: 'CLIENT_FAIL_UNKNOWN_MEDIA_TYPE' }
}
if (process.env.NODE_ENV === 'development') {
cloudPath = 'dev_' + cloudPath
}
if (filePath) {
uni.showLoading()
const { fileID, requestId } = await uniCloud.uploadFile({
filePath: filePath,
cloudPath: cloudPath, // 关键是要具有文件格式后缀名,这样可以保持阿里云下载链接也用这个后缀名。
fileType: mediaType, // = image, video, audio
onUploadProgress: function (progressEvent) {
// console.log(progressEvent)
var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
},
})
uni.hideLoading()
console.log('文件上传结果:', { fileID, requestId })
if (fileID) {
return { _state: 'SUCCESS', fileID, requestId }
}
}
return { _state: 'CLIENT_FAIL_CHOOSE_FILE' }
},
async pickupFile({
backend = this.BACKEND,
mediaType = 'image', count = 1, sizeType = ['original', 'compressed'], sourceType = ['album', 'camera'], maxDuration,
url, header = {}, formData = {}, name = 'file',
}) {
if (backend==='UNICLOUD'){
const resultCloud = await this.pickupFile2Cloud({mediaType, count, sizeType, sourceType, maxDuration })
return Object.assign(resultCloud, { fileUrl: resultCloud.fileID })
}else if (backend==='SERVER' && url){
const resultServer = await this.pickupFile2Server({mediaType, count, sizeType, sourceType, maxDuration, url, header, formData, name})
return Object.assign(resultServer, {fileUrl: resultServer.file ? this.makeServerUrl(resultServer.file.webpath || resultServer.file.path) : undefined})
}
},
openUrl(url) {
// #ifdef APP-PLUS
plus.runtime.openURL(url)
// #endif
// #ifdef H5
window.open(url, '_blank')
// #endif
},
getSystemInfo() {
let systemInfo = uni.getSystemInfoSync()
// model=PC|iPhone|iPad|Nexus 6|...,
// platform=ios|android|mac|windows|linux|other,
// system=iOS 11.0|Android 4.0|Other 0|... 等等
// #ifdef H5
systemInfo.environment = 'h5'
let userAgent = window.navigator.userAgent.toLowerCase()
systemInfo.browser =
/msie/.test(userAgent) && !/opera/.test(userAgent)
? 'msie'
: /opera/.test(userAgent)
? 'opera'
: /version.*safari/.test(userAgent)
? 'safari'
: /chrome/.test(userAgent)
? 'chrome'
: /gecko/.test(userAgent) && !/webkit/.test(userAgent)
? 'firefox'
: /micromessenger/.test(userAgent)
? 'wechat'
: 'unknown'
// #endif
// #ifdef APP-PLUS || APP-PLUS-NVUE
systemInfo.environment = 'app'
// #endif
// #ifdef MP
systemInfo.environment = 'mp'
// 细分成 WEIXIN, ...
// #endif
return systemInfo
},
/*
* uni.showToast({
icon=success (by default)/loading/none,
position(app only):center|top|bottom,
success, fail, complete // 函数调用后立刻发生,不是在toast之后
})
* ucToast.show({
type=info (by default)/success/error/warning|loading,
position:top/bottom
})
* u-toast.show({
type=default (by default)/primary/success/error/warning/info,
position:center/top/bottom,
callback // 发生在 toast 之后
})
*/
showToast({ tool, type, image, title, duration = 2000, ...rest }) {
const thisPage = this.$store ? this // 对于组件内定义的 i18nText,要使用 this 来获得组建内的 i18nText,而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: getCurrentPages()[getCurrentPages().length - 1]
if (tool === 'uni' || !(thisPage.$refs && thisPage.$refs.toast)) {
// #ifdef APP-PLUS
uni.showToast({ icon: 'none', title, duration, ...rest })
// #endif
// #ifdef H5
uni.showToast({ icon: 'none', image, title, duration, ...rest })
// #endif
} else {
// 根据 html 中不同的组件 或 而不同。
thisPage.$refs.toast.show({ type, title, duration, ...rest })
}
},
formatMoney(value, decimal) {
return Number(value || 0).toFixed(decimal || 2) // Number(undefined)===NaN
},
formatPercent(value, decimal) {
return Number(value * 100 || 0).toFixed(decimal || 2)
},
formatDate(date, format) {
if (!(date instanceof Date)) {
if (typeof date === 'string' && /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d$/.test(date)) {
// 这是从 typeorm 数据库得到的Date类型的值
date = date.replace(/-/g, '/') // safari 不支持 yyyy-mm-dd,必须改成 yyyy/mm/dd
}
date = new Date(date)
}
if (!date.toJSON()) {
date = new Date()
}
format = format && typeof format === 'string' ? format : 'yyyy-mm-dd HH:MM:SS'
let o = {
'm+': date.getMonth() + 1, //月份
'q+': Math.floor((date.getMonth() + 3) / 3), //季度
'd+': date.getDate(), //日
'H+': date.getHours(), //小时
'M+': date.getMinutes(), //分
'S+': date.getSeconds(), //秒
s: date.getMilliseconds(), //毫秒
}
if (/(y+)/.test(format)) format = format.replace(RegExp.$1, `${date.getFullYear()}`.substr(4 - RegExp.$1.length))
for (var k in o) {
if (new RegExp(`(${k})`).test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : `00${o[k]}`.substr(`${o[k]}`.length))
}
return format
},
hash(data, { hasher = 'sha256', salt, input = 'utf8', output = 'hex' } = {}) {
if (typeof data !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) data = JSON.stringify(data)
if (salt && typeof salt === 'string') data = data + salt
let inputEncoding = input // my.INPUT_LIST.indexOf(option.input)>=0?option.input:my.INPUT // 'utf8', 'ascii' or 'latin1' for string data, default to utf8 if not specified; ignored for Buffer, TypedArray, or DataView.
let outputEncoding = output === 'buf' ? undefined : output // (my.OUTPUT_LIST.indexOf(output)>=0?output:my.OUTPUT) // option.output: 留空=》默认输出hex格式;或者手动指定 'buf', hex', 'latin1' or 'base64'
return require('crypto').createHash(hasher).update(data, inputEncoding).digest(outputEncoding)
},
regcode2aiid(code) {
if (typeof code === 'string' && /^[a-zA-Z0-9]+$/.test(code)) {
const alphabet = 'e5fcdg3hqa4b1n0pij2rstuv67mwx89klyz'
const base = 16367
code = code.toLowerCase()
let len = code.length
let num = 0
for (let i = 0; i < len; i++) {
num += alphabet.indexOf(code[i]) * Math.pow(alphabet.length, i)
}
let aiid = num / (base - alphabet.length) - base
if (aiid >= 0 && Number.isInteger(aiid)) {
// 允许 aiid===0:当第一个用户(aiid==1)登录时,需要一个系统默认的邀请码。
return aiid
}
}
return null
},
getUserEndLanIp(callback) {
let recode = {};
let RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
// 如果不存在则使用一个iframe绕过
if (!RTCPeerConnection) {
// 因为这里用到了iframe,所以在调用这个方法的script上必须有一个iframe标签
//
let win = iframe.contentWindow;
RTCPeerConnection = win.RTCPeerConnection || win.mozRTCPeerConnection || win.webkitRTCPeerConnection;
}
//创建实例,生成连接
let pc = new RTCPeerConnection();
// 匹配字符串中符合ip地址的字段
function handleCandidate(candidate) {
let ip_regexp = /([0-9]{1,3}(\.[0-9]{1,3}){3}|([a-f0-9]{1,4}((:[a-f0-9]{1,4}){7}|:+[a-f0-9]{1,4}){6}))/;
let ip_isMatch = candidate.match(ip_regexp)[1];
if (!recode[ip_isMatch]) {
callback(ip_isMatch);
recode[ip_isMatch] = true;
}
}
//监听icecandidate事件
pc.onicecandidate = (ice) => {
if (ice.candidate) {
handleCandidate(ice.candidate.candidate);
}
};
//建立一个伪数据的通道
pc.createDataChannel('');
pc.createOffer((res) => {
pc.setLocalDescription(res);
}, () => {});
//延迟,让一切都能完成
setTimeout(() => {
let lines = pc.localDescription.sdp.split('\n');
lines.forEach(item => {
if (item.indexOf('a=candidate:') === 0) {
handleCandidate(item);
}
})
}, 1000)
},
}