wo-user-toolkit-uniapp/index.js
2021-09-21 19:45:40 +08:00

506 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 this.$store ? this // 对于组件内定义的 i18nText要使用 this 来获得组建内的 i18nText而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: getCurrentPages()[getCurrentPages().length - 1]
},
localizeText(i18nText) {
// 如果直接挂载到 Vue.prototype 下,那么可以直接访问 this.i18nText。但如果通过 this.$T.localeText 访问,那么 this.i18nText 就报错了。因此安全起见,先获取当前 page
const thisPage = this.thisPage()
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.thisPage() // 如果直接挂载到 Vue.prototype 下,那么可以直接访问 this.i18nText。但如果通过 this.$T.localeText 访问,那么 this.i18nText 就报错了。因此安全起见,先获取当前 page
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 (route && typeof route === 'object') {
route = `${route.api}/${route.class}/${route.method}`
}
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 }) {
let pageNow = this.thisPage()
if (tool === 'uni' || !(pageNow.$refs && pageNow.$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 中不同的组件 <ucToast/> 或 <toast/> 而不同。
pageNow.$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标签
// <iframe id="iframe" sandbox="allow-same-origin" style="display:none;"></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)
},
}