merges wo-user-toolkit-uniapp into this wo-core-toolkit
This commit is contained in:
parent
fcaf990c51
commit
4e45a4e642
4
index.js
Normal file
4
index.js
Normal file
@ -0,0 +1,4 @@
|
||||
const coretool = require('./tool_core')
|
||||
const unitool = require('./tool_uniapp')
|
||||
|
||||
module.exports = { ...coretool, ...unitool }
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wo-core-toolkit",
|
||||
"version": "0.1.0",
|
||||
"main": "coretool.js",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
|
@ -1,8 +1,8 @@
|
||||
/* 基础小工具,可通用于服务端和用户端
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
BASEPORT_API_SERVER: 7000,
|
||||
@ -46,6 +46,14 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
parse_json_or_keep (value) {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
},
|
||||
|
||||
// 按顺序展开,哪怕嵌套。
|
||||
stringify_by_keyorder (obj, { cmp, cycles = false, space = '', replacer, schemaColumns, excludeKeys = [] } = {}) {
|
||||
/* 这个解决方法不考虑缺省值,不能把嵌套对象也按顺序展开。*/
|
||||
@ -420,4 +428,14 @@ module.exports = {
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
shuffle_array (array = []) {
|
||||
if (Array.isArray(array)) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[array[i], array[j]] = [array[j], array[i]]
|
||||
}
|
||||
}
|
||||
return array
|
||||
},
|
||||
}
|
926
tool_uniapp.js
Normal file
926
tool_uniapp.js
Normal file
@ -0,0 +1,926 @@
|
||||
const crypto = require('crypto')
|
||||
const path = require('path')
|
||||
|
||||
//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
|
||||
|
||||
const my = {
|
||||
get_mylang () {
|
||||
// globalThis.getCurrentPages?.() 在 topWindow/App.vue 里有可能为空,所以用 getApp().$store 更安全. 20230513: 发现在微信小程序模拟器里,getApp().$store.state 未定义,所以还是用 globalThis.wo?.ss
|
||||
return globalThis.wo?.ss?.i18n?.mylang || globalThis.getApp?.()?.$store?.state?.i18n?.mylang
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BASE_TYPE_DEFAULT: 'SERVER', // one of { SERVER: 服务器, UNICLOUD_FUNC: 云函数, UNICLOUD_OBJECT: 云对象 }
|
||||
|
||||
// 用直观的色彩,代替语义化的类型:
|
||||
// uView 中大量组件都使用 type 来设置颜色,而 uni-ui 中只有少量组件用到。
|
||||
c2t: {
|
||||
RED: 'error',
|
||||
GREEN: 'success',
|
||||
BLUE: 'primary',
|
||||
YELLOW: 'warn',
|
||||
GREY: 'info',
|
||||
|
||||
// uAlertTips: { YELLOW: 'warning', BLUE: 'primary', RED: 'error', GREY: 'info', GREEN: 'success' },
|
||||
// uBadge: { RED: 'error', BLUE: 'primary', YELLOW: 'warning', GREY: 'info', GREEN: 'success' },
|
||||
// uButton: { WHITE: 'default', BLUE: 'primary', YELLOW: 'warning', RED: 'error', GREY: 'info', GREEN: 'success' },
|
||||
// uNoticeBar: { YELLOW: 'warning', BLUE: 'primary', RED: 'error', GREY: 'info', GREEN: 'success', TRANS: 'none' },
|
||||
// uTag: { BLUE: 'primary', RED: 'error', YELLOW: 'warning', GREY: 'info', GREEN: 'success' },
|
||||
// uToast: { BLACK: 'default', BLUE: 'primary', YELLOW: 'warning', RED: 'error', GREY: 'info', GREEN: 'success' },
|
||||
// uTopTips: { BLUE: 'primary', RED: 'error', YELLOW: 'warning', GREY: 'info', GREEN: 'success' },
|
||||
|
||||
// uniBadge: { GREY: 'default', YELLOW: 'warning', BLUE: 'primary', GREEN: 'success', RED: 'error' },
|
||||
// uniButton: { GREY: 'default', RED: 'warn', RGB: 'primary' },
|
||||
// uniPopupDialog: { GREEN: 'success', YELLOW: 'warn', RED: 'error', GREY: 'info' },
|
||||
// uniPopupMessage: { GREEN: 'success', YELLOW: 'warn', RED: 'error', GREY: 'info' },
|
||||
// uniTag: { GREY: 'default', YELLOW: 'warning', BLUE: 'primary', GREEN: 'success', RED: 'error', PURPLE: 'royal' },
|
||||
},
|
||||
|
||||
is_text_file (fileName = '') {
|
||||
const ext = /\./.test(fileName) ? fileName.split('.').pop().toLowerCase() : ''
|
||||
return (wo.envar.textExtensionList || ['txt', 'text']).includes(ext)
|
||||
},
|
||||
is_image_file (fileName = '') {
|
||||
const ext = /\./.test(fileName) ? fileName.split('.').pop().toLowerCase() : ''
|
||||
return (wo.envar.imageExtensionList || ['jpg', 'jpeg', 'png', 'gif', 'webp', 'image']).includes(ext)
|
||||
},
|
||||
is_video_file (fileName = '') {
|
||||
const ext = /\./.test(fileName) ? fileName.split('.').pop().toLowerCase() : ''
|
||||
return (wo.envar.videoExtensionList || ['avi', 'mp4', 'mov', 'wmv', 'video']).includes(ext)
|
||||
},
|
||||
|
||||
thisPage () {
|
||||
return this.__page__
|
||||
? this // constructor.name==='VueComponent' 只在 development 环境有用,在 production 环境会被简化成 'o'。
|
||||
: globalThis.getCurrentPages?.()?.pop?.() || {} // [20220401] 发现在 topWindow 里,或者在 App.vue 里, getCurrentPages() 是 undefined 和 空数组 [],因此在这里默认 {} 做保护。
|
||||
},
|
||||
|
||||
localizeText (i18nText, { langCode, precise = false } = {}) {
|
||||
i18nText =
|
||||
i18nText || // 如果传入i18n参数 ({zhCN:'...', enUS:'...'})
|
||||
this.i18nText || // 1) 如果挂载到具体页面的 computed { lote: wo?.localizeText } 那么 this 就是当前页面,直接取用 this.i18nText 即可。2) 对于组件内定义的 i18nText,要使用 this 来获得组件内的 i18nText
|
||||
getCurrentPages()?.pop()?.i18nText // 如果不是挂载到 Vue.prototype 而是 挂载到 wo 下调用,那么 this.i18nText 就不存在了。因此通过 pageNow.i18nText 访问。
|
||||
if (['string', 'number', 'boolean'].includes(typeof i18nText)) {
|
||||
// 必须先检测是否标量值,如果直接返回 i18nText 可能返回{}等,导致依赖于返回空值的前端出错
|
||||
return i18nText
|
||||
} else if (typeof i18nText === 'object' && i18nText) {
|
||||
return (
|
||||
i18nText?.[langCode] ||
|
||||
i18nText?.[my.get_mylang()] ||
|
||||
(precise ? '' : i18nText?.earTH || i18nText?.defLAN || i18nText?.gloBAL || i18nText?.enUS || Object.values(i18nText)[0] || '')
|
||||
)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
localeText () {
|
||||
// 专供绑定到 computed { lote: wo?.localeText } 使用,这时 this 就是当前页面。
|
||||
return this.i18nText?.[my.get_mylang()] || {}
|
||||
},
|
||||
|
||||
setBarTitles ({ windowTitle, pageTitle, wo = globalThis.wo } = {}) {
|
||||
const langNow = my.get_mylang()
|
||||
const pageNow = globalThis.getCurrentPages?.()?.pop?.()
|
||||
// 在ios/android app里,pageNow.route 是正确的,但是 pageNow.xxx 等自定义属性 都 undefined,必须 pageNow.$vm.$data.xxx 才可以。
|
||||
// 注意,$vm.$data 不包括 computed 属性,而 pageNow 里包括。因此不能把 i18nPageTitle 放在 computed 里。
|
||||
|
||||
const navibarTitle = this.localizeText(
|
||||
pageTitle ||
|
||||
pageNow?.$vm?.$data?.i18nPageTitle || // 页面.vue 的 i18nPageTitle 变量
|
||||
pageNow?.$vm?.$data?.i18nText?.[langNow]?.tPageTitle || // 页面.vue 的 i18nText 对象
|
||||
pageNow?.$vm?.$data?.pageTitle ||
|
||||
wo?.pageSet?.[pageNow?.route?.substring?.(6)]?.i18nPageTitle ||
|
||||
wo?.pagesJson?.pages?.find((page) => page.path === pageNow?.route)?.i18nPageTitle || // pages.json 的页面配置里
|
||||
''
|
||||
)
|
||||
|
||||
windowTitle =
|
||||
windowTitle || wo?.envar?.callnames?.[langNow] || wo?.pagesJson?.appInfo?.i18nText?.[langNow] || wo?.pagesJson?.globalStyle?.navigationBarTitleText || ''
|
||||
|
||||
if (wo.envar.clientInfo.deviceType === 'pc') {
|
||||
uni.setNavigationBarTitle({ title: windowTitle + (navibarTitle ? ` - ${navibarTitle}` : '') })
|
||||
} else {
|
||||
uni.setNavigationBarTitle({ title: navibarTitle })
|
||||
}
|
||||
|
||||
//#ifdef WEB
|
||||
//// 设置窗口标题栏 document.title
|
||||
//// navibarTitle 也会被用于浏览器的标签标题,可用 document.title 去覆盖。必须放在 setNavigationBarTitle 之后。
|
||||
//// 但这个方案,在电脑上,还是会显示 navibarTitle 在浏览器窗口顶栏,不知为何。
|
||||
if (wo.envar.clientInfo.deviceType !== 'pc' && /MicroMessenger/i.test(globalThis.window?.navigator?.userAgent)) {
|
||||
//// 微信浏览器里,本身就显示了标题栏,和自有的导航栏形成功能重叠和混淆。
|
||||
//// 设置标题栏为空或覆盖
|
||||
document.title = windowTitle
|
||||
//// 或者设置导航栏隐藏。但这样导致,用户容易误点微信浏览器标题栏的 X 关掉页面,所以还是显示导航栏吧。
|
||||
// document.getElementsByTagName('uni-page-head')?.[0]?.remove() // 或者 [0]?.style?.display = 'none'
|
||||
}
|
||||
//#endif
|
||||
|
||||
if (wo.envar.clientInfo.deviceType === 'pc') {
|
||||
uni.hideTabBar()
|
||||
} else {
|
||||
// 必须要在有 tab 的页面里 setTabBarItem 才有效果
|
||||
//const midIndex = parseInt(wo?.pagesJson?.tabBar?.list?.length/2) // 如果存在midButton,实际上tabBar.list.length必须为偶数。不过为了心安,再parseInt一下。
|
||||
wo?.pagesJson?.tabBar?.list?.forEach((tab, tabIndex) => {
|
||||
if (tab.i18nText && tab.i18nText[langNow]) {
|
||||
uni.setTabBarItem({
|
||||
// #ifdef WEB
|
||||
index: tabIndex, // + ((wo?.pagesJson?.tabBar?.midButton?.iconPath && tabIndex >= midIndex)?1:0), // H5 里,如果使用了 midButton,tabBarItem的index出现错位,需hack调整。推测,在H5里 midButton 作为一个普通tab被插入到 tabBar 里,导致 tabBar 的 index 和 wo?.pagesJson.tabBar.list 的 index 错位了。[20211031] 注意到,从 HBuilderX 3.2.12.20211029 起,在 H5 里也没有错位了。
|
||||
// #endif
|
||||
// #ifndef WEB
|
||||
index: tabIndex,
|
||||
// #endif
|
||||
text: tab.i18nText[langNow],
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
make_server_url (route, envar = globalThis.wo?.envar || {}) {
|
||||
if (typeof route === 'string') route = route.replace('\\', '/')
|
||||
else if (route?.apiWho && route?.apiTodo) {
|
||||
const { apiVersion = 'api', apiWho, apiTodo } = route
|
||||
route = `${apiVersion}/${apiWho}/${apiTodo}`
|
||||
} else {
|
||||
// 防止 route 为 null, undefined 等由于后台数据库默认值而造成的异常。
|
||||
route = ''
|
||||
}
|
||||
// 已是完整url
|
||||
if (/^https?:\/\//.test(route)) {
|
||||
return route
|
||||
}
|
||||
// 本地图片
|
||||
if (/^\/static\//.test(route)) {
|
||||
return route
|
||||
}
|
||||
// 纯数字和字母的cid
|
||||
if (/^[\da-zA-Z]+$/.test(route) && envar.ipfsLens) {
|
||||
return `${envar.ipfsLens.replace(/\/$/, '')}/${route.replace(/^\//, '')}`
|
||||
}
|
||||
//// base url / 后台服务器url 需要组装。包括了 route === '_filestore/xxx' 的情况
|
||||
route = route.replace(/^\//, '')
|
||||
// 已有现成后端服务域名
|
||||
if (envar.servUrl) {
|
||||
return `${envar.servUrl.replace(/\/$/, '')}/${route}`
|
||||
} else {
|
||||
// 需要组装后端服务域名
|
||||
const hostname = envar.servHostname /*|| globalThis.window?.location?.hostname*/ || 'localhost'
|
||||
const port = envar.servPort /*|| globalThis.window?.location?.port*/ || ''
|
||||
const protocol = hostname === 'localhost' ? 'http' : envar.servProtocol || (process.env.NODE_ENV === 'production' ? 'https' : 'http')
|
||||
return `${protocol}://${hostname}${port ? ':' : ''}${port}/${route}`
|
||||
}
|
||||
},
|
||||
|
||||
make_bgurl (image) {
|
||||
return image ? `url(${this.make_server_url(image)})` : ''
|
||||
},
|
||||
|
||||
/** 统一 uni.request 和 uniCloud.callFunction 的调用方法,提供统一、透明的后台调用
|
||||
* 返回值:{ _state, 成功结果或错误结果 },其中 _state 除了后台返回的,还可以是
|
||||
* - CLIENT_WOBASE_BROKEN: 前端发现后台断线
|
||||
* - CLIENT_WOBASE_TIMEOUT: 前端发现后台超时
|
||||
* - CLINET_WOBASE_EXCEPTION: 前端发现后台异常
|
||||
**/
|
||||
async callBase ({
|
||||
baseType = globalThis.wo?.envar?.baseTypeDefault || this.BASE_TYPE_DEFAULT,
|
||||
httpMethod = 'POST',
|
||||
apiVersion = 'api',
|
||||
apiWho,
|
||||
apiTodo,
|
||||
apiWhat = {},
|
||||
timeout,
|
||||
}) {
|
||||
const thisRoute = globalThis.getCurrentPages?.()?.pop?.()?.route || 'VueApp' // 立刻保存 route,因为在调用后台后,可能已切换到了其他页面。
|
||||
const startTime = new Date().toJSON()
|
||||
let apiurl = undefined
|
||||
apiWhat._clientInfo = {
|
||||
...globalThis.wo?.envar?.clientInfo,
|
||||
lang: globalThis.wo?.ss?.i18n?.mylang,
|
||||
// #ifdef WEB
|
||||
requrl: globalThis.location?.href,
|
||||
// #endif
|
||||
}
|
||||
apiWhat._passtoken = uni.getStorageSync('_passtoken')
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(
|
||||
{
|
||||
_at: startTime,
|
||||
_io: 'CREQ',
|
||||
_path: `${thisRoute} => ${apiWho}/${apiTodo}`,
|
||||
apiurl,
|
||||
baseType,
|
||||
apiWhat,
|
||||
timeout,
|
||||
}
|
||||
// `%c ${startTime} (IN) ${thisRoute} :: ${apiTodo}`,
|
||||
// 'background: #87cefa; border-radius: 0.5em;color: white; font-weight: bold; padding: 2px 0.5em;',
|
||||
// {
|
||||
// baseType,
|
||||
// apiWho,
|
||||
// apiTodo,
|
||||
// apiWhat,
|
||||
// timeout,
|
||||
// apiurl,
|
||||
// }
|
||||
)
|
||||
}
|
||||
let result = {}
|
||||
if (baseType === 'UNICLOUD_OBJECT') {
|
||||
const uniObj = uniCloud.importObject(apiWho)
|
||||
try {
|
||||
result = await uniObj[apiTodo](apiWhat)
|
||||
} catch (error) {
|
||||
result = { _state: 'CLINET_WOBASE_EXCEPTION', error }
|
||||
}
|
||||
} else if (baseType === 'UNICLOUD_FUNC') {
|
||||
let { /* success, header, requestedId, */ result: resultCloud = {} } = await uniCloud
|
||||
.callFunction({
|
||||
name: apiWho,
|
||||
data: {
|
||||
apiTodo,
|
||||
apiWhat,
|
||||
// uniIdToken // uniCloud自动getStorageSync('uni_id_token')并传递为 uniIdToken;也可自行组装传入 uniIdToken
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
// {errMsg, stack} = error
|
||||
if (/request:fail/.test(error.errMsg)) {
|
||||
// 后台云服务无法连接
|
||||
return { _state: 'CLIENT_WOBASE_BROKEN', error }
|
||||
} else {
|
||||
// 后台云服务返回异常
|
||||
return { _state: 'CLIENT_WOBASE_EXCEPTION', error }
|
||||
}
|
||||
})
|
||||
result = resultCloud
|
||||
} else if (baseType === 'SERVER') {
|
||||
if (httpMethod === 'GET') {
|
||||
// 如果不是 POST 方法,要额外把参数JSON化
|
||||
for (let key in apiWhat) {
|
||||
apiWhat[key] = JSON.stringify(apiWhat[key])
|
||||
}
|
||||
}
|
||||
apiurl = this.make_server_url(`${apiVersion}/${apiWho}/${apiTodo}`)
|
||||
let [error, { statusCode, header, errMsg, data: resultServer = {} } = {}] = await uni
|
||||
.request({
|
||||
method: httpMethod,
|
||||
url: apiurl,
|
||||
data: apiWhat,
|
||||
timeout,
|
||||
})
|
||||
.catch((expt) => {
|
||||
return [undefined, { data: { _state: 'CLIENT_WOBASE_EXCEPTION' } }]
|
||||
})
|
||||
if (error) {
|
||||
if (error.errMsg === 'request:fail') {
|
||||
// 后台服务器无法连接
|
||||
result = { _state: 'CLIENT_WOBASE_BROKEN', error }
|
||||
} else if (error.errMsg === 'request:fail timeout') {
|
||||
// 后台服务器超时
|
||||
result = { _state: 'CLIENT_WOBASE_TIMEOUT', error }
|
||||
} else {
|
||||
// 后台服务器返回异常
|
||||
result = { _state: 'CLIENT_WOBASE_EXCEPTION', error }
|
||||
}
|
||||
} else {
|
||||
result = resultServer
|
||||
}
|
||||
} else {
|
||||
result = { _state: 'CLIENT_WOBASE_TYPE_UNKNOWN' }
|
||||
}
|
||||
|
||||
if (result?._passtoken) {
|
||||
uni.setStorageSync('_passtoken', result._passtoken)
|
||||
}
|
||||
if (result?.uni_id_token) {
|
||||
uni.setStorageSync('uni_id_token', result.uni_id_token)
|
||||
}
|
||||
if (result?.clid) {
|
||||
uni.setStorageSync('clid', result.clid)
|
||||
}
|
||||
|
||||
// 注意1,resultServer 和 resultCloud 推荐遵循同样的格式 { _state, error | data },这样方便前端做统一判断。
|
||||
// 注意2,虽然预设了 resultServer 和 resultCloud = {},但如果后台返回了 null,那么 resultServer/resultCloud 也是 null。
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(
|
||||
{
|
||||
_at: new Date().toJSON(),
|
||||
_io: 'CRES',
|
||||
_path: `${thisRoute} => ${apiWho}/${apiTodo}`,
|
||||
result,
|
||||
}
|
||||
// `%c ${new Date().toJSON()} (OUT) ${thisRoute} :: ${apiTodo}`,
|
||||
// 'background: #4169e1; border-radius: 0.5em;color: white; font-weight: bold; padding: 2px 0.5em;',
|
||||
// result
|
||||
)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
async pickupFile2Server ({
|
||||
fileDragged, // 形如 { filePath: "blob:http://localhost:8080/218e3007-f74b-440d-a2db-8f1d9aed746f", name: 'abc.txt', size: 2375, type: "text/plain", lastModified: 1732291306772 }
|
||||
mediaType = 'image', // could be: image, video, array of supported extensions, anything else
|
||||
count = 1,
|
||||
sizeType = ['original', 'compressed'],
|
||||
sourceType = ['album', 'camera'],
|
||||
url = 'api/Fileloader/receiveFile', // 默认后台用这个接口来接受文件
|
||||
header = {},
|
||||
formData = {},
|
||||
name = 'file',
|
||||
} = {}) {
|
||||
// 有的管理后台不需要登录就允许上传,例如 cmctoy。因此不要在这里依赖登录状态。
|
||||
|
||||
let filePath, fileSize, filePicked
|
||||
if ('undefined' !== typeof fileDragged?.size) {
|
||||
// size 为 0 的时候也该要做这些处理
|
||||
fileSize = fileDragged.size
|
||||
filePath = fileDragged.filePath
|
||||
filePicked = fileDragged
|
||||
if (!mediaType) {
|
||||
mediaType = this.is_image_file(fileDragged.name) ? 'image' : this.is_video_file(fileDragged.name) ? 'video' : 'file'
|
||||
}
|
||||
} else if (mediaType === 'image') {
|
||||
let [errorChoose, { tempFilePaths, tempFiles } = {}] = await uni.chooseImage({ count, sizeType, sourceType })
|
||||
if (errorChoose) {
|
||||
return {
|
||||
_state: 'CER_FAIL_CHOOSE',
|
||||
_msg: '', // { zhCN: '图像选择失败。请稍后再试,或向客服投诉。', enUS: 'Image choose failed. Please try again later, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
fileSize = tempFiles?.[0]?.size
|
||||
filePicked = tempFiles?.[0]
|
||||
filePath = tempFilePaths?.[0]
|
||||
} else if (mediaType === 'video') {
|
||||
let [errorChoose, { tempFilePath, tempFile, size, duration }] = await uni.chooseVideo({ sourceType })
|
||||
if (errorChoose) {
|
||||
return {
|
||||
_state: 'CER_FAIL_CHOOSE',
|
||||
_msg: '', // { zhCN: '视频选择失败。请稍后再试,或向客服投诉。', enUS: 'Video choose failed. Please try again later, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
fileSize = size
|
||||
filePicked = tempFile
|
||||
filePath = tempFilePath
|
||||
} else {
|
||||
// #ifdef WEB
|
||||
// https://uniapp.dcloud.net.cn/api/media/file.html
|
||||
let [errorChoose, { tempFilePaths, tempFiles } = {}] = await uni.chooseFile({
|
||||
count,
|
||||
extension: Array.isArray(mediaType) ? mediaType : undefined,
|
||||
type: Array.isArray(mediaType) ? undefined : 'all',
|
||||
}) // 20240429 但是测试下来 extension 参数无效
|
||||
if (errorChoose) {
|
||||
return {
|
||||
_state: 'CER_FAIL_CHOOSE',
|
||||
_msg: '', // { zhCN: '文件选择失败。请稍后再试,或向客服投诉。', enUS: 'File choose failed. Please try again later, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
fileSize = tempFiles?.[0]?.size
|
||||
filePicked = tempFiles?.[0]
|
||||
filePath = tempFilePaths?.[0]
|
||||
// #endif
|
||||
// #ifndef WEB
|
||||
return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '请切换到网页端上传文件!', enUS: 'Please switch to WebApp to upload files.' } }
|
||||
// #endif
|
||||
}
|
||||
|
||||
const fileName = filePicked?.name || filePath?.split?.('/')?.pop?.() // filePicked.name is available in WEB only. on the other hand, filePath 在 WEB 上并不是文件路径名,而是类似 "blob:http://localhost:8080/f0d3e54d-0694-4803-8097-641d76a10b0d“。在 iOS 上是 "_doc/uniapp_temp_1598593902955/compressed/1598593925815.png", 有时还包含从 file:/// 开始的完整路径名。
|
||||
if (!fileSize) {
|
||||
return {
|
||||
_state: 'CER_EMPTY_FILE',
|
||||
_msg: { zhCN: '文件为空,无法上传:\n' + fileName, enUS: 'Empty files cannot be uploaded:\n' + fileName },
|
||||
}
|
||||
} else if (fileSize > (globalThis.wo?.envar?.fileSizeLimit || 10485760)) {
|
||||
let sizeLimitMB = parseInt((globalThis.wo?.envar?.fileSizeLimit || 10485760) / 1048576) + 'MB'
|
||||
return {
|
||||
_state: 'CER_FILE_TOO_LARGE',
|
||||
_msg: { zhCN: `文件大于 ${sizeLimitMB},无法上传`, enUS: `The file exceeds ${sizeLimitMB} and cannot be uploaded` },
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
// #ifndef APP
|
||||
// 20240830 luk: 在 App 上,就相信 iOS/Android,不检查文件后缀名。
|
||||
(mediaType === 'image' && !this.is_image_file(fileName)) ||
|
||||
(mediaType === 'video' && !this.is_video_file(fileName)) ||
|
||||
// #endif
|
||||
(Array.isArray(mediaType) && !mediaType?.includes?.(/\./.test(fileName) ? '.' + fileName?.split?.('.')?.pop?.()?.toLowerCase?.() : ''))
|
||||
) {
|
||||
return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '不支持的文件类型:\n' + fileName, enUS: 'Unsupported file type:\n' + fileName } }
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
return {
|
||||
_state: 'CER_FAIL_CHOOSE',
|
||||
_msg: '', // { zhCN: '文件选择失败。请稍后再试,或向客服投诉。', enUS: 'File choose failed. Please try again later, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
|
||||
for (let key in formData) {
|
||||
// multer 不会自动处理 JSON 数据,必须前后端配合处理
|
||||
formData[key] = JSON.stringify(formData[key])
|
||||
}
|
||||
// 在 Fileloader/fileloader.js 里,已经不再依赖 _passtoken,而且 header 在被 nginx 或 cloudflare (没搞清楚是谁干的)代理之后也被过滤掉了,因此不再使用这一句: header._passtoken = uni.getStorageSync('_passtoken')
|
||||
formData._passtoken = uni.getStorageSync('_passtoken') // 20230527 加回这一句,让后台可以根据验证用户来决定怎样处理文件。
|
||||
formData.filenameOriginal = fileName // 20241020 不知为何,通过拖拽的文件上传后,后台得到的 file.originalname 形如 "file-1729354713176" 而不是真正的原始文件名,也不包含后缀。为了保持一致,这里主动把真实的文件名(不管是拖拽还是点击的)发给后台来处理。
|
||||
|
||||
this.showLoading({ title: { zhCN: '上传中...', enUS: 'Uploading...' } })
|
||||
let [errorUpload, { data, statusCode } = {}] = await uni.uploadFile({ url: this.make_server_url(url), filePath, name, header, formData })
|
||||
// 后台 Multer 处理 req.file = { destination, filename, originalname, path, mimetype, size }, 其中 path 包括了 destination 和 filename 的文件相对路径。
|
||||
// url 指向的后台方法进一步处理后,通过 uni.uploadFile 存在 data 里返回结果: { ...file, cid?, ipfsUrl?, baseUrl? }
|
||||
this.hideLoading()
|
||||
|
||||
if (typeof data === 'string') {
|
||||
// 不知为何,uni.uploadFile返回的 data 是字符串而不是对象
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
// 注释下面几句,改由后台来处理 filenameOriginal 以把正确的 data.originalname 返回给前端
|
||||
// if (fileDragged?.name) {
|
||||
// // 20241020 不知为何,通过拖拽的文件上传后,后台得到的 以及返回的 data.originalname 形如 "file-1729354713176" 而不是真正的原始文件名,也不包含后缀。为了保持一致,这里强行重新设置
|
||||
// data.originalnameSystem = data.originalname
|
||||
// data.originalname = fileDragged.name
|
||||
// }
|
||||
} catch (exp) {
|
||||
return {
|
||||
_state: 'BER_FAIL_RESPONSE_JSON_MALFORMED',
|
||||
_msg: { zhCN: '文件上传失败。请稍后再试,或向客服报告。', enUS: 'File upload failed. Please try again, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data?._state === 'SUCCESS' && data?.path) {
|
||||
// 后台送来的 baseUrl 在开发环境下,不一定符合前端实际,因为后台只知道预设的 servUrl 例如 https://pexserver.test.tic.cc:7739/... ,而开发环境下实际上是 http://localhost:7739/... 所以再设置一个 fileUrl 来根据 location.origin 调整。// todo: 改名叫 clientUrl 或 userUrl 或 baseUrl4Client,与 baseUrl 对应
|
||||
return { _state: 'SUCCESS', fileUrl: this.make_server_url(data.path), ...data } // { path, destination, filename, fileUrl, cid?, ipfsUrl?, baseUrl?, ...file } 注意,data.path 不包含起头的 '/'
|
||||
} else {
|
||||
return {
|
||||
_state: 'BER_FAIL_UPLOAD_FILE',
|
||||
_msg: { zhCN: '文件上传失败。请稍后再试,或向客服报告。', enUS: 'File upload failed. Please try again, or report to customer service.' },
|
||||
error: errorUpload,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async pickupFile2Cloud ({
|
||||
fileDragged,
|
||||
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, fileSize, filePicked
|
||||
let random = crypto.randomBytes(16).toString('hex')
|
||||
if ('undefined' !== typeof fileDragged?.size) {
|
||||
fileSize = fileDragged.size
|
||||
filePath = fileDragged.filePath
|
||||
filePicked = fileDragged
|
||||
cloudPath = `WEB_${wo.envar.clientInfo.osName}_${random}_${path.extname(fileDragged.name || { image: '.jpg', video: '.mp4' }[mediaType] || '')}` // tempFile and name are H5 only
|
||||
} else if (mediaType === 'image') {
|
||||
let [errorChoose, { tempFilePaths, tempFiles } = {}] = await uni.chooseImage({ count, sizeType, sourceType })
|
||||
if (errorChoose) {
|
||||
return {
|
||||
_state: 'CER_FAIL_CHOOSE',
|
||||
_msg: '', // { zhCN: '图像选择失败。请稍后再试,或向客服投诉。', enUS: 'Image choose failed. Please try again later, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
fileSize = tempFiles?.[0]?.size
|
||||
filePicked = tempFiles?.[0]
|
||||
filePath = tempFilePaths?.[0] // 在 H5 上并不是文件路径名,而是类似 "blob:http://localhost:8080/f0d3e54d-0694-4803-8097-641d76a10b0d“。// 在 iOS 上是 "_doc/uniapp_temp_1598593902955/compressed/1598593925815.png", 有时还包含从 file:/// 开始的完整路径名
|
||||
// #ifndef WEB
|
||||
// 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_${wo.envar.clientInfo.osName}_${random}${path.extname(filePath || '.jpg')}`
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
cloudPath = `WEB_${wo.envar.clientInfo.osName}_${random}${path.extname(tempFiles?.[0]?.name || '.jpg')}` // 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 })
|
||||
if (errorChoose) {
|
||||
return {
|
||||
_state: 'CER_FAIL_CHOOSE',
|
||||
_msg: '', // { zhCN: '视频选择失败。请稍后再试,或向客服投诉。', enUS: 'Video choose failed. Please try again later, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
fileSize = size
|
||||
filePicked = tempFile
|
||||
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 WEB
|
||||
cloudPath = `APP_${wo.envar.clientInfo.osName}_${random}_dur${duration}${path.extname(filePath || '.mp4')}`
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
cloudPath = `WEB_${wo.envar.clientInfo.osName}_${random}_dur${duration}${path.extname(name || '.mp4')}` // tempFile and name are H5 only
|
||||
// #endif
|
||||
// iOS 上测试,filePath 为 *.MOV,而阿里云只允许 *.mp4, 所以默认添加 .mp4 后缀。参见 https://uniapp.dcloud.net.cn/uniCloud/storage?id=clouduploadfile
|
||||
// 20200915测试,阿里云支持上传 *.mov 了。
|
||||
} else {
|
||||
// #ifdef WEB
|
||||
// https://uniapp.dcloud.net.cn/uniCloud/storage.html#uploadfile
|
||||
let { errMsg, tempFilePaths, tempFiles } = await uniCloud.chooseAndUploadFile({
|
||||
type: Array.isArray(mediaType) ? undefined : 'all', // valid for H5 only
|
||||
extension: Array.isArray(mediaType) ? mediaType : undefined,
|
||||
count: 1,
|
||||
// extention: [],
|
||||
// onChooseFile: ({ errMsg, tempFilePaths, tempFiles }) => { },
|
||||
// onUploadProgress: ({ index, loaded, total, tempFilePath, tempFile }) => { }
|
||||
})
|
||||
if (errMsg !== 'chooseAndUploadFile:ok') {
|
||||
return {
|
||||
_state: 'CER_FAIL_CHOOSE',
|
||||
_msg: '', // { zhCN: '文件选择失败。请稍后再试,或向客服投诉。', enUS: 'File choose failed. Please try again later, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
fileSize = tempFiles?.[0]?.size
|
||||
filePicked = tempFiles?.[0]
|
||||
filePath = tempFilePaths?.[0]
|
||||
cloudPath = `WEB_${wo.envar.clientInfo.osName}_${random}${path.extname(tempFiles?.[0]?.name || '')}` // name is available in H5 only. 只包含文件名和后缀名,不包含路径。
|
||||
// #endif
|
||||
// #ifndef WEB
|
||||
return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '请切换到网页端上传文件!', enUS: 'Please switch to WebApp to upload files.' } }
|
||||
// #endif
|
||||
}
|
||||
|
||||
const fileName = filePicked?.name || filePath?.split?.('/')?.pop?.() // filePicked.name is available in WEB only. on the other hand, filePath 在 WEB 上并不是文件路径名,而是类似 "blob:http://localhost:8080/f0d3e54d-0694-4803-8097-641d76a10b0d“。在 iOS 上是 "_doc/uniapp_temp_1598593902955/compressed/1598593925815.png", 有时还包含从 file:/// 开始的完整路径名。
|
||||
|
||||
if (!fileSize) {
|
||||
return { _state: 'CER_EMPTY_FILE', _msg: { zhCN: '文件为空,无法上传:\n' + fileName, enUS: 'Empty files cannot be uploaded:\n' + fileName } }
|
||||
} else if (fileSize > (globalThis.wo?.envar?.fileSizeLimit || 10485760)) {
|
||||
let sizeLimitMB = parseInt((globalThis.wo?.envar?.fileSizeLimit || 10485760) / 1048576) + 'MB'
|
||||
return {
|
||||
_state: 'CER_FILE_TOO_LARGE',
|
||||
_msg: { zhCN: `文件大于 ${sizeLimitMB},无法上传`, enUS: `The file exceeds ${sizeLimitMB} and cannot be uploaded` },
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
// #ifndef APP
|
||||
// 20240830 luk: 在 App 上,就相信 iOS/Android,不检查文件后缀名。
|
||||
(mediaType === 'image' && !this.is_image_file(fileName)) ||
|
||||
(mediaType === 'video' && !this.is_video_file(fileName)) ||
|
||||
// #endif
|
||||
(Array.isArray(mediaType) && !mediaType?.includes?.(/\./.test(fileName) ? '.' + fileName?.split?.('.')?.pop?.()?.toLowerCase?.() : ''))
|
||||
) {
|
||||
return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '不支持的文件类型:\n' + fileName, enUS: 'Unsupported file type:\n' + fileName } }
|
||||
}
|
||||
|
||||
if (mediaType !== 'video' && mediaType !== 'image') {
|
||||
// 这一句应该在上面的 uniCloud.chooseAndUploadFile() 分支里,不过为了合用 fileSize 和 fileExt 的判断,就放在这里。
|
||||
return { _state: 'SUCCESS', fileUrl: filePicked?.url }
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
return {
|
||||
_state: 'BER_FAIL_CHOOSE_FILE',
|
||||
_msg: { zhCN: '文件上传失败。请稍后再试,或向客服报告。', enUS: 'File upload failed. Please try again, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
|
||||
this.showLoading({ title: { zhCN: '上传中...', enUS: 'Uploading...' } })
|
||||
const { fileID, requestId } = await uniCloud.uploadFile({
|
||||
filePath: filePath,
|
||||
cloudPath: (process.env.NODE_ENV !== 'production' ? 'dev_' : '') + cloudPath, // 关键是要具有文件格式后缀名,这样可以保持阿里云下载链接也用这个后缀名。
|
||||
//fileType: mediaType, // = image, video, audio. Looks like only necessary for for 支付宝小程序: https://uniapp.dcloud.net.cn/uniCloud/storage.html#uploadfile
|
||||
onUploadProgress: function (progressEvent) {
|
||||
var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
},
|
||||
})
|
||||
this.hideLoading()
|
||||
|
||||
if (fileID) {
|
||||
return { _state: 'SUCCESS', fileUrl: fileID, requestId }
|
||||
} else {
|
||||
return {
|
||||
_state: 'BER_FAIL_UPLOAD_FILE',
|
||||
_msg: { zhCN: '文件上传失败。请稍后再试,或向客服报告。', enUS: 'File upload failed. Please try again, or report to customer service.' },
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async pickupFile ({
|
||||
baseType = globalThis.wo?.envar?.baseTypeDefault || this.BASE_TYPE_DEFAULT,
|
||||
fileDragged,
|
||||
mediaType = 'image', // could be image, video, array of supported extensions, anything else // 20240502 todo: rename to pickupFileType
|
||||
count = 1,
|
||||
sizeType = ['original', 'compressed'],
|
||||
sourceType = ['album', 'camera'],
|
||||
maxDuration,
|
||||
url,
|
||||
header = {},
|
||||
formData = {},
|
||||
name = 'file',
|
||||
} = {}) {
|
||||
if (/^UNICLOUD/.test(baseType)) {
|
||||
return await this.pickupFile2Cloud({ fileDragged, mediaType, count, sizeType, sourceType, maxDuration })
|
||||
} else if (baseType === 'SERVER') {
|
||||
return await this.pickupFile2Server({ fileDragged, mediaType, count, sizeType, sourceType, maxDuration, url, header, formData, name })
|
||||
} else {
|
||||
return { _state: 'CLEINT_FAIL_UNKNOWN_WOBASE_TYPE', baseType, _msg: { zhCN: '不支持的后台类型。', enUS: 'Unsupported base type.' } }
|
||||
}
|
||||
},
|
||||
|
||||
open_url ({ url, title, inWebview } = {}) {
|
||||
url = this.localizeText?.(url)
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
if (wo.envar.inPc) {
|
||||
window.open(url, '_blank')
|
||||
} else if (inWebview) {
|
||||
wo.ss.webviewUrl = url
|
||||
wo.ss.webviewTitle = title
|
||||
uni.navigateTo({ url: 'show-webview' })
|
||||
} else {
|
||||
// #ifdef APP
|
||||
plus.runtime.openURL(url)
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
open_url_in_browser ({ url }) {
|
||||
url = this.localizeText?.(url) || url
|
||||
// #ifdef APP
|
||||
plus.runtime.openURL(url)
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
},
|
||||
open_url_in_webview ({ url, title }) {
|
||||
url = this.localizeText?.(url) || url
|
||||
if (wo.envar.inPc) {
|
||||
window.open(url, '_blank')
|
||||
} else {
|
||||
wo.ss.webviewUrl = url
|
||||
wo.ss.webviewTitle = title
|
||||
uni.navigateTo({ url: 'show-webview' })
|
||||
}
|
||||
},
|
||||
|
||||
parse_ua_to_browser () {
|
||||
// #ifdef WEB
|
||||
let userAgent = globalThis.window?.navigator?.userAgent?.toLowerCase?.()
|
||||
return /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' // 只要在 H5, 即使不认识也要返回一个名称。
|
||||
// #endif
|
||||
return '' // 如果不在 H5。
|
||||
},
|
||||
|
||||
/*
|
||||
* 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
|
||||
})
|
||||
* uToptips.show({
|
||||
type=default (by default)/primary/success/error/warning/info,
|
||||
position:center/top/bottom,
|
||||
callback // 发生在 toast 之后
|
||||
})
|
||||
*/
|
||||
showToast ({ type = 'success', image, title, duration = 2000, wo = globalThis.wo, sysToast = false, ...rest } = {}) {
|
||||
title = this.localizeText(title)
|
||||
if (!title) {
|
||||
return
|
||||
}
|
||||
const mypopup =
|
||||
globalThis.getCurrentPages?.()?.pop()?.mypopup ||
|
||||
globalThis.getCurrentPages?.()?.pop()?.$refs?.mypopup ||
|
||||
globalThis.getApp().globalData?.mypopup ||
|
||||
wo?.mypopup
|
||||
const mytoast =
|
||||
globalThis.getCurrentPages?.()?.pop()?.mytoast ||
|
||||
globalThis.getCurrentPages?.()?.pop()?.$refs?.mytoast ||
|
||||
globalThis.getApp().globalData?.mytoast ||
|
||||
wo?.mytoast
|
||||
if (!sysToast && mypopup) {
|
||||
wo.ss.popMessage = title
|
||||
wo.ss.popType = type // success/error/warning/info
|
||||
wo.ss.popDuration = duration
|
||||
mypopup.open()
|
||||
} else if (!sysToast && mytoast) {
|
||||
// 来自 <ucToast> 或 <u-toast> 或 <u-top-tips> // rename to popToast?
|
||||
mytoast?.show?.({ type, title, duration, ...rest })
|
||||
} else {
|
||||
// #ifdef APP
|
||||
uni.showToast({ icon: 'none', title, duration, ...rest })
|
||||
// plus.nativeUI.toast( title, { align: center/left/right, verticalAlign: bottom/center/top, duration:long/short, icon, iconWidth, iconHeight, style: block/inline, type:text/richtext })
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
uni.showToast({ icon: 'none', image, title, duration, ...rest })
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
|
||||
showModal (option = {}) {
|
||||
option.title = this.localizeText(option.title)
|
||||
option.content =
|
||||
(uni.getSystemInfoSync().uniPlatform === 'app' ? '\n' : '') + this.localizeText(option.content)?.substring?.(0, option.contentLength || 300)
|
||||
if (option.content) option.content += '\n\n'
|
||||
option.cancelText = this.localizeText(option.cancelText || { zhCN: '取消', enUS: 'Cancel', jaJP: 'キャンセル' })
|
||||
option.confirmText = this.localizeText(
|
||||
option.confirmText ||
|
||||
(option.showCanel === true || option.showCancel === undefined
|
||||
? { zhCN: '确认', enUS: 'Confirm', jaJP: '確認' }
|
||||
: { zhCN: '好的', enUS: 'OK', jaJP: 'OK' })
|
||||
)
|
||||
// #ifdef APP
|
||||
if (option.content) option.content = '\n' + option.content
|
||||
else option.content = ' ' // 20240229 从前在手机app里,content必须存在。现在似乎没关<-系了,但不确定。
|
||||
// #endif
|
||||
uni.showModal(option) // uni.showModal 的 showCancel 默认为 true
|
||||
},
|
||||
|
||||
showLoading ({ title = { zhCN: '加载中...', enUS: 'Loading...' }, mask = false } = {}) {
|
||||
title = this.localizeText(title)
|
||||
// #ifndef APP
|
||||
uni.showLoading({ title, mask }) // 原函数的 mask 默认为 false
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
// 在安卓应用里,uni.showLoading() 重复调用,导致不断闪烁跳动。
|
||||
// plus.nativeUI.showWaiting() 调用多了则导致死机。
|
||||
// 还好,showWaiting() 返回 waiting 对象,可以 waiting.setTitle()
|
||||
return plus.nativeUI.showWaiting(title, { modal: mask }) // 原函数的 mask 默认为 true
|
||||
// #endif
|
||||
},
|
||||
hideLoading () {
|
||||
// #ifndef APP
|
||||
uni.hideLoading()
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
plus.nativeUI.closeWaiting()
|
||||
// #endif
|
||||
},
|
||||
|
||||
// precision 要有默认值,以防无法连接后台时,这个方法会导致 part-header.vue 出错。
|
||||
formatMoney (amount, { precision = 2 } = {}) {
|
||||
// parseInt(NaN/undefined/false/null/'') 都返回 NaN,而 Number(false/null/'')===0,因此用 parseInt 来过滤无效输入。
|
||||
// 或者可以 if (!['number', 'string'].includes(typeof amount) && [NaN, undefined, false, null, ''].includes(amount))
|
||||
if (Number.isNaN(parseInt(amount))) {
|
||||
return ''
|
||||
}
|
||||
// Number(amount).toFixed(precision) // toFixed 虽然方便,但是会自动四舍五入。
|
||||
return `${parseInt(Number(amount) * Math.pow(10, precision)) / Math.pow(10, precision)}`
|
||||
},
|
||||
|
||||
formatPercent (value, precision = 2) {
|
||||
return Number(value * 100 || 0).toFixed(precision)
|
||||
},
|
||||
|
||||
formatDate (date, format) {
|
||||
if (typeof date === 'string' && date) {
|
||||
// new Date('') ==> Invalid Date
|
||||
if (/^\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
|
||||
} else if (/^\d{13}$/.test(date)) {
|
||||
// new Date('1702179364450') ==> Invalid Date,所以要先转成纯数字
|
||||
date = parseInt(date)
|
||||
} else if (/^\d{10}$/.test(date)) {
|
||||
date = parseInt(date) * 1000
|
||||
}
|
||||
date = new Date(date)
|
||||
} else if (typeof date === 'number') {
|
||||
date = new Date(date)
|
||||
}
|
||||
|
||||
if (!(date instanceof Date) || !date?.toJSON?.()) {
|
||||
return ''
|
||||
}
|
||||
|
||||
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标签
|
||||
// <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)
|
||||
},
|
||||
|
||||
goto_page (pageName, { type = 'navigateTo', wo = globalThis.wo } = {}) {
|
||||
if (pageName) {
|
||||
if (wo?.pagesJson?.tabBar?.list?.find((item) => item?.pagePath?.substr(6) === pageName)) {
|
||||
// 注意,即使在 PC 上 topWindow 代替了 tabBar 时,从标签页转化而来的菜单页,也是用 switchTab 跳转。
|
||||
uni.switchTab({ url: pageName })
|
||||
} else if (type === 'navigateTo') {
|
||||
uni.navigateTo({ url: pageName })
|
||||
} else {
|
||||
uni.redirectTo({ url: pageName })
|
||||
}
|
||||
} else {
|
||||
uni.navigateBack()
|
||||
}
|
||||
},
|
||||
|
||||
next_focus (currentFocus, focusList) {
|
||||
focusList = focusList || globalThis.getCurrentPages?.()?.pop()?.focusList
|
||||
if (focusList) {
|
||||
for (let n in focusList) {
|
||||
// 不论对数组或对象都有效,n 是数组的index 或对象的key
|
||||
focusList[n] = false
|
||||
}
|
||||
setTimeout(() => {
|
||||
focusList[(currentFocus + 1) % Object.keys(focusList).length] = true
|
||||
}, 200) // 如果没有 setTimeout 至少 200ms, 焦点短暂跳到下一个后,又会消失
|
||||
}
|
||||
},
|
||||
|
||||
copy_to_clipboard (text, { promptLength = 50, hidePrompt = false, sysToast = false } = {}) {
|
||||
text = this.localizeText?.(text) || text
|
||||
const self = this
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
if (!hidePrompt) {
|
||||
if (text.length > promptLength) {
|
||||
text = String(text).substring(0, promptLength) + ' ...'
|
||||
}
|
||||
self.showToast?.({
|
||||
type: 'success',
|
||||
title: `${this.localizeText?.({ zhCN: '已拷贝\n', enUS: 'Copied\n' }) || ''}${text}`,
|
||||
sysToast,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue
Block a user