// 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',
// [todo] 能否把这些默认值放到 export 以外?
RESPONSIVE_TABBAR_AUTOHIDE_WIDTH_THRESHOLD_DEFAULT: 0,
BACKEND_DEFAULT: 'SERVER', // 服务器 SERVER 或云服务 UNICLOUD
thisPage() {
return this.constructor.name==='VueComponent' ? this // 对于组件内定义的 i18nText,要使用 this 来获得组建内的 i18nText,而不是 getCurrentPages[...] 去访问全局页面的 i18nText。
: ( getCurrentPages()[getCurrentPages().length - 1] // [20220401] 发现在 topWindow 里, getCurrentPages 是 undefined。
|| {} ) // 用 {} 做备份是为了在 App.vue 中使用时,getCurrentPages() 有可能获取不到。
},
// 输出命令行提示,可用来取代 console.log/info/warn/error
cclog(...args) {
const pageName = this.thisPage()?.route || 'VueApp'
console.log('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:blue', ...args)
},
ccinfo(...args) {
const pageName = this.thisPage()?.route || 'VueApp'
console.info('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:green', ...args)
},
ccwarn(...args) {
const pageName = this.thisPage().route || 'VueApp'
console.warn('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:orange', ...args)
},
ccerr(...args) {
const pageName = this.thisPage()?.route || 'VueApp'
console.error('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:red', ...args)
},
ccdebug(...args) {
if (process.env.NODE_ENV === 'development') {
const pageName = this.thisPage()?.route || 'VueApp'
console.debug('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:cyan', ...args)
}
},
localizeText(i18nText) {
i18nText = i18nText?.constructor?.name==='VueComponent' ? this.i18nText // 如果挂载到具体页面的 computed { lote: wo.localizeText } 那么 this 就是当前页面,直接取用 this.i18nText 即可。
: ( i18nText // 如果传入i18n参数 ({zhCN:'...', enUS:'...'})
|| this.thisPage()?.i18nText) // 如果不是挂载到 Vue.prototype 而是 挂载到 wo 下调用,那么 this.i18nText 就报错了。因此通过 thisPage().i18nText 访问。
const mylang = getApp().$store.state.i18n.mylang // this.thisPage() 有可能为空(例如在 topWindow 里,或者在 App.vue 里),所以用 getApp().$store 更安全
return i18nText?.[mylang] || (typeof i18nText === 'string' ? i18nText : '') // 必须检测是否string,如果直接返回 i18nText 可能返回{}等,导致依赖于返回空值的前端出错
},
localeText() { // 专供绑定到 computed { lote: wo.localeText } 使用,这时 this 就是当前页面。
return this.i18nText?.[getApp().$store.state.i18n.mylang] || {}
},
setBarTitles ({ windowTitle, pageTitle,
pagesJson=this.pagesJson || wo?.pagesJson } = {}
) {
const mylang = getApp().$store.state.i18n.mylang
const pageNow = getCurrentPages()[getCurrentPages().length - 1]
// #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 里,如果使用了 midButton,tabBarItem的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({})
// #ifdef H5
if (uni.getSystemInfoSync().model==='PC') {
const envar = this.envar || wo?.envar || {}
if (envar.Responsive_Tabbar==='AUTOHIDE') {
if (window.screen.width > (envar.Responsive_Tabbar_Autohide_Width_Threshold || this.RESPONSIVE_TABBAR_AUTOHIDE_WIDTH_THRESHOLD_DEFAULT)) {
uni.hideTabBar()
}
uni.onWindowResize(({size})=>{
if (size.windowWidth > (envar.Responsive_Tabbar_Autohide_Width_Threshold || this.RESPONSIVE_TABBAR_AUTOHIDE_WIDTH_THRESHOLD_DEFAULT)) {
uni.hideTabBar()
}else{
uni.showTabBar()
}
})
}else if (envar.Responsive_Tabbar==='ALWAYSHIDE') {
uni.hideTabBar()
}
}
// #endif
},
makeServerUrl(route = '') {
const envar = this.envar || wo?.envar || {}
if (typeof route !== 'string') route = '' // 防止 route 为 null, undefined 等由于后台数据库默认值而造成的异常。
route = route.replace('\\', '/')
if (/^https?:\/\//.test(route)) {
return route
}
let protocol, hostname, port
if (process.env.NODE_ENV === 'production') {
protocol = envar.Base_Protocol || 'https'
hostname = envar.Base_Hostname
port = envar.Base_Port
}else{
protocol = envar.Base_Protocol_Dev || 'http'
hostname = envar.Base_Hostname_Dev
// #ifdef H5
|| window.location.hostname
// #endif
port = envar.Base_Port_Dev || envar.Base_Port
// #ifdef H5
|| window.location.port.replace(':','')
// #endif
}
return `${protocol}://${hostname}:${port}/${route.replace(/^\//, '')}`
},
makeBgUrl(path) {
if (path) {
return `url(${this.makeServerUrl(path)})`
}
return ''
},
relaunchForAll ({envar=this.envar || wo?.envar || {}} = {}) {
uni.reLaunch({ url: envar?.Start_Page_For_All })
},
relaunchForOnline ({envar=this.envar || wo?.envar || {}} = {}) {
process.env.NODE_ENV === 'production' &&
wo.ss.User.onlineUser.uuid &&
uni.reLaunch({ url: envar?.Start_Page_For_Online })
},
relaunchForOffline ({envar=this.envar || wo?.envar || {}} ={}) {
process.env.NODE_ENV === 'production' &&
! wo.ss.User.onlineUser.uuid &&
uni.reLaunch({ url: envar?.Start_Page_For_Offline })
},
/** 统一 uni.request 和 uniCloud.callFunction 的调用方法,提供统一、透明的后台调用
* 返回值:{ _state, 成功结果或错误结果 },其中 _state 除了后台返回的,还可以是
* - CLIENT_BACKEND_BROKEN: 前端发现后台断线
* - CLIENT_BACKEND_TIMEOUT: 前端发现后台超时
* - CLINET_BACKEND_EXCEPTION: 前端发现后台异常
**/
async callBackend({
backend = this.envar?.Backend_Default || wo?.envar?.Backend_Default || this.BACKEND_DEFAULT,
httpMethod = 'POST',
apiVersion = 'api', apiWho, apiTodo, apiWhat = {}
}) {
const thisRoute = this.thisPage()?.route || 'VueApp' // 立刻保存 this.thisPage().route,因为在调用后台后,可能已切换到了其他页面。
const startTime = new Date().toJSON()
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
}
}
// 注意1,resultServer 和 resultCloud 推荐遵循同样的格式 { _state, error | data },这样方便前端做统一判断。
// 注意2,虽然预设了 resultServer 和 resultCloud = {},但如果后台返回了 null,那么 resultServer/resultCloud 也是 null。
if (process.env.NODE_ENV === 'development') {
console.log(
'%c '+JSON.stringify({startTime:startTime, page:thisRoute}) +
' %c '+ JSON.stringify({ backend, apiWho, apiTodo, apiWhat }) +
' %c '+ JSON.stringify(result) +
' %c '+ JSON.stringify({endTime:new Date().toJSON()}),
'color:blue', 'background:skyblue', 'background:magenta', 'color:magenta') // 不知为何,直接用 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) {
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.envar?.Backend_Default || wo?.envar?.Backend_Default || this.BACKEND_DEFAULT,
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.thisPage()
if (tool === 'uni' || ! 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 中不同的组件 或 而不同。
pageNow.$refs.toast.show({ type, title, duration, ...rest })
}
},
formatMoney(value, precision=2) {
return Number(value || 0).toFixed(precision) // Number(undefined)===NaN
},
formatPercent(value, precision=2) {
return Number(value * 100 || 0).toFixed(precision)
},
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
},
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)
},
}