wo-user-toolkit-uniapp/index.js

512 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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
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
export default {
// 用直观的色彩,代替 uview 的命名方式:
RED: 'error',
GREEN: 'success',
BLUE: 'primary',
YELLOW: 'warning',
GREY: 'info',
BLACK_TOAST: 'default',
WHITE_BUTTON: 'default',
BACKEND: 'SERVER', // 通过变量来动态切换后台类型:服务器 SERVER或云服务 CLOUD。应当根据实际需要在前端所用的 unitool 里覆盖。
// 快速输出详尽提示,可用来取代 console.log
colog(...message) {
if (process.env.NODE_ENV === 'development') {
// 在首页时getApp() 或 getCurrentPages() 有可能获取不到。
const pageName = getCurrentPages().length > 0 ? getCurrentPages().pop().route : 'App'
console.log(`${pageName}`, ...message, ' 】】')
}
},
thisPage() {
return getCurrentPages()[getCurrentPages().length - 1] // 准备挂在到 window 下使用,因此不必探测 this?.$store 了,肯定不存在。
},
localizeText(i18nText) {
// 如果直接挂载到 Vue.prototype 下,那么可以直接访问 this.i18nText。但如果通过 this.$T.localeText 访问,那么 this.i18nText 就报错了。因此安全起见,先获取当前 page
const pageNow = this?.$store ? this // 对于组件内定义的 i18nText要使用 this 来获得组建内的 i18nText而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: getCurrentPages()[getCurrentPages().length - 1]
// 支持 localizeText({...}) 来解析参数
if (i18nText && typeof(i18nText)==='object' && pageNow.$store?.state?.i18n?.mylang) {
return i18nText[pageNow.$store.state.i18n.mylang] || ''
}
// 支持 localizeText() 来解析 页面.vue 的顶层 i18nText
if (!i18nText && typeof(pageNow.i18nText)==='object' && pageNow.$store?.state?.i18n?.mylang) {
return pageNow.i18nText[pageNow.$store.state.i18n.mylang] || ''
}
if (typeof i18nText === 'string'){
return i18nText
}
return ''
},
localeText() {
const pageNow = this?.$store ? this // 对于组件内定义的 i18nText要使用 this 来获得组件内的 i18nText而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: getCurrentPages()[getCurrentPages().length - 1]
return pageNow.i18nText?.[pageNow.$store.state.i18n.mylang] || {}
},
setBarTitles ({ windowTitle, pageTitle } = {}) {
const pageNow = getCurrentPages()[getCurrentPages().length - 1]
const mylang = getApp().$store.state.i18n.mylang
// #ifdef H5
document.title = windowTitle || pagesJson?.appInfo?.i18nText?.[mylang] // 必须放在 setNavigationBarTitle 之后,否则会被其覆盖掉。
// #endif
uni.setNavigationBarTitle({
title:
pageTitle ||
pageNow.i18nText?.[mylang]?.tPageTitle || // 页面.vue 的 i18nText 对象
pageNow.i18nPageTitle?.[mylang] || // 页面.vue 的 i18nPageTitle 变量
pagesJson?.pages?.find((page) => page.path === pageNow.route)?.i18nPageTitle?.[mylang], // pages.json 的页面配置里
})
// 必须要在有 tab 的页面里 setTabBarItem 才有效果
//const midIndex = parseInt(pagesJson?.tabBar?.list?.length/2) // 如果存在midButton,实际上tabBar.list.length必须为偶数。不过为了心安再parseInt一下。
pagesJson?.tabBar?.list?.forEach((tab, tabIndex) => {
if (tab.i18nText && tab.i18nText[mylang]) {
uni.setTabBarItem({
// #ifdef H5
index: tabIndex, // + ((pagesJson?.tabBar?.midButton?.iconPath && tabIndex >= midIndex)?1:0), // H5 里,如果使用了 midButtontabBarItem的index出现错位需hack调整。推测在H5里 midButton 作为一个普通tab被插入到 tabBar 里,导致 tabBar 的 index 和 pagesJson.tabBar.list 的 index 错位了。[20211031] 注意到,从 HBuilderX 3.2.12.20211029 起,在 H5 里也没有错位了。
// #endif
// #ifndef H5
index: tabIndex,
// #endif
text: tab.i18nText[mylang],
})
}
})
// uni.showTabBar({})
},
makeServerUrl(route = '') {
if (typeof route !== 'string') route = '' // 防止 route 为 null, undefined 等由于后台数据库默认值而造成的异常。
route = route.replace('\\', '/')
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.replace(/^\//, '')}`
},
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])
}
}
process.env.NODE_ENV === 'production' || console.log('👇 Request 👇 ', { method, url, header, data }, ' 👇 👇')
let [error, response] = await uni.request({ method, url, header, data })
process.env.NODE_ENV === 'production' || console.log('👆 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 = {} }) {
process.env.NODE_ENV === 'production' || console.log('👇 BackendRequest 👇 ', { backend, apiWho, apiTodo, apiWhat }, ' 👇 👇')
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)) {
// 后台云服务无法连接
result = { _state: 'CLIENT_BACKEND_BROKEN', error }
} else {
// 后台云服务返回异常
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', error }
} else if (error.errMsg === 'request:fail timeout') {
// 后台服务器超时
result = { _state: 'CLIENT_BACKEND_TIMEOUT', error }
} else {
// 后台服务器返回异常
result = { _state: 'CLIENT_BACKEND_EXCEPTION', error }
}
} else {
result = resultServer
}
}
process.env.NODE_ENV === 'production' || console.log('👆 BackendResult 👆 ', JSON.stringify(result), ' 👆 👆') // 不知为何,直接用 result 会输出一个奇怪的对象,要主动添加 JSON.stringify 才按照期望输出。
return result
},
async pickupFile2Server({
mediaType = 'image',
count = 1,
sizeType = ['original', 'compressed'],
sourceType = ['album', 'camera'],
url = 'api/FileTransfer/receiveFile', // 默认后台用这个接口来接受文件
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 { _state: 'UNKNOWN_MEDIATYPE' }
}
if (filePath) {
for (let key in formData) {
// multer 不会自动处理 JSON 数据,必须前后端配合处理
formData[key] = JSON.stringify(formData[key])
}
header._passtoken = uni.getStorageSync('_passtoken')
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') { // 不知为何uni.uploadFile返回的 data 是字符串而不是对象
try{
data = JSON.parse(data)
}catch(exp){
return { _state: 'CLIENT_FAIL_RESPONSE_JSON_MALFORMED'}
}
}
if (data?._state==='SUCCESS' && data?.path) {
return { _state: 'SUCCESS', fileUrl: this.makeServerUrl(data.path), filePath: data.path, ...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()
if (fileID) {
return { _state: 'SUCCESS', fileUrl: 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 resultCloud
}else if (backend==='SERVER'){
const resultServer = await this.pickupFile2Server({mediaType, count, sizeType, sourceType, maxDuration, url, header, formData, name})
return resultServer
}else {
return { _state: 'CLEINT_FAIL_UNKNOWN_BACKEND_TYPE', backend }
}
},
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 pageNow = this?.$store ? this // 对于组件内定义的 i18nText要使用 this 来获得组建内的 i18nText而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: getCurrentPages()[getCurrentPages().length - 1]
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)
},
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)
},
}