From f64e3f5737b6bbba64b64e2e88b1724ed3a88b79 Mon Sep 17 00:00:00 2001 From: Luk Lu Date: Thu, 21 Apr 2022 20:15:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20navigate=5Fpage=20?= =?UTF-8?q?=E5=92=8C=20rediect=5Fpage=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usertool.js | 297 +++++++++++++++++++++++++++++----------------------- 1 file changed, 167 insertions(+), 130 deletions(-) diff --git a/usertool.js b/usertool.js index 4300196..5bd3da9 100644 --- a/usertool.js +++ b/usertool.js @@ -15,56 +15,56 @@ export default { // [todo] 能否把这些默认值放到 export 以外? BACKEND_DEFAULT: 'SERVER', // 服务器 SERVER 或云服务 UNICLOUD - thisPage() { - return this.__page__ ? this // 1) constructor.name==='VueComponent' 只在 development 环境有用,在 production 环境会被简化成 'o'。2)对于组件内定义的 i18nText,要使用 this 来获得组建内的 i18nText,而不是 getCurrentPages[...] 去访问全局页面的 i18nText。 - : ( getCurrentPages()[getCurrentPages().length - 1] // [20220401] 发现在 topWindow 里, getCurrentPages 是 undefined。 - || {} ) // 在 App.vue 中调用 getCurrentPages() 返回的是空数组 [],因此在这里默认 {} 做保护。 + thisPage () { + return this.__page__ + ? this // 1) constructor.name==='VueComponent' 只在 development 环境有用,在 production 环境会被简化成 'o'。2)对于组件内定义的 i18nText,要使用 this 来获得组建内的 i18nText,而不是 getCurrentPages[...] 去访问全局页面的 i18nText。 + : getCurrentPages()[getCurrentPages().length - 1] || {} // [20220401] 发现在 topWindow 里, getCurrentPages 是 undefined。 // 在 App.vue 中调用 getCurrentPages() 返回的是空数组 [],因此在这里默认 {} 做保护。 }, // 输出命令行提示,可用来取代 console.log/info/warn/error - cclog(...args) { + cclog (...args) { const pageName = this.thisPage()?.route || 'VueApp' - console.log('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:blue', ...args) + console.log('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:blue', ...args) }, - ccinfo(...args) { + ccinfo (...args) { const pageName = this.thisPage()?.route || 'VueApp' - console.info('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:green', ...args) + console.info('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:green', ...args) }, - ccwarn(...args) { + ccwarn (...args) { const pageName = this.thisPage().route || 'VueApp' - console.warn('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:orange', ...args) + console.warn('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:orange', ...args) }, - ccerr(...args) { + ccerr (...args) { const pageName = this.thisPage()?.route || 'VueApp' - console.error('%c '+JSON.stringify({time:new Date().toJSON(), page:pageName}), 'color:red', ...args) + console.error('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:red', ...args) }, - ccdebug(...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) + console.debug('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:cyan', ...args) } }, - localizeText(i18nText) { - i18nText = i18nText?.__page__ ? this.i18nText // 如果挂载到具体页面的 computed { lote: wo.localizeText } 那么 i18nText 会被自动赋值为 this 就是当前页面,直接取用 this.i18nText 即可。 - : ( i18nText // 如果传入i18n参数 ({zhCN:'...', enUS:'...'}) - || this.thisPage()?.i18nText) // 如果不是挂载到 Vue.prototype 而是 挂载到 wo 下调用,那么 this.i18nText 就报错了。因此通过 thisPage().i18nText 访问。 + localizeText (i18nText) { + i18nText = i18nText?.__page__ + ? this.i18nText // 如果挂载到具体页面的 computed { lote: wo.localizeText } 那么 i18nText 会被自动赋值为 this 就是当前页面,直接取用 this.i18nText 即可。 + : i18nText || this.thisPage()?.i18nText // 如果传入i18n参数 ({zhCN:'...', enUS:'...'}) // 如果不是挂载到 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 就是当前页面。 + localeText () { + // 专供绑定到 computed { lote: wo.localeText } 使用,这时 this 就是当前页面。 return this.i18nText?.[getApp().$store.state.i18n.mylang] || {} }, - setBarTitles ({ windowTitle, pageTitle, - pagesJson=this.pagesJson || wo?.pagesJson } = {} - ) { + setBarTitles ({ windowTitle, pageTitle, pagesJson = this.pagesJson || wo?.pagesJson, envar = this.envar || wo?.envar } = {}) { const mylang = getApp()?.$store?.state?.i18n?.mylang // 不要用 pageNow.$store,防止在 App.vue 里无法获取当前页面。 const pageNow = this.thisPage() // 需要兼顾在 App.vue 时无法获取当前页面的情况,因为如果在 topWindow 里调用本函数,getApp() 和 getCurrentPages()[getCurrentPages().length-1] 就是 undefined。 // #ifdef H5 - document.title = windowTitle || wo?.envar?.Sys_Brand_Name?.[mylang] || pagesJson?.appInfo?.i18nText?.[mylang] || pagesJson?.globalStyle?.navigationBarTitleText // 必须放在 setNavigationBarTitle 之后,否则会被其覆盖掉。 + document.title = + windowTitle || wo?.envar?.Sys_Brand_Name?.[mylang] || pagesJson?.appInfo?.i18nText?.[mylang] || pagesJson?.globalStyle?.navigationBarTitleText // 必须放在 setNavigationBarTitle 之后,否则会被其覆盖掉。 // #endif uni.setNavigationBarTitle({ @@ -81,7 +81,7 @@ export default { 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 里也没有错位了。 + 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, @@ -94,22 +94,24 @@ export default { // #ifdef H5 // 响应式方案:仅仅根据当前设备类型,如果是 PC 大屏幕,则始终显示 topWindow 并且隐藏顶部 navibar 和底部 tabBar。 - const envar = this.envar || wo?.envar - if (pagesJson?.topWindow || envar.Hide_Bars_On_PC) { // 如果页头不是通过 pagesJson.topWindow 而是作为组件来引入个别页面,那么定义配置参数 Hide_Bars_On_PC 来控制。 - if (uni.getSystemInfoSync().model==='PC') { + if (pagesJson?.topWindow || envar?.Hide_Bars_On_PC) { + // 如果页头不是通过 pagesJson.topWindow 而是作为组件来引入个别页面,那么定义配置参数 Hide_Bars_On_PC 来控制。 + if (uni.getSystemInfoSync().model === 'PC') { if (window.screen.width > (pagesJson?.topWindow?.matchMedia?.minWidth || 0)) { uni.hideTabBar() // 不知为何,同一个二级页面,如果第二次进入,就仍然会显示 navibar, 必须通过 setTimeout 执行才能彻底隐藏。 - setTimeout(()=>{ document.getElementsByTagName('uni-page-head')?.[0]?.remove() },0) + setTimeout(() => { + document.getElementsByTagName('uni-page-head')?.[0]?.remove() + }, 0) } - }else { + } else { document.getElementsByTagName('uni-top-window')?.[0]?.remove() // hide topWindow } } - // #endif + // #endif }, - makeServerUrl(route = '') { + makeServerUrl (route = '') { const envar = this.envar || wo?.envar || {} if (typeof route !== 'string') route = '' // 防止 route 为 null, undefined 等由于后台数据库默认值而造成的异常。 @@ -124,51 +126,43 @@ export default { protocol = envar.Base_Protocol || 'https' hostname = envar.Base_Hostname port = envar.Base_Port - }else{ + } else { protocol = envar.Base_Protocol_Dev || 'http' - hostname = envar.Base_Hostname_Dev + hostname = + envar.Base_Hostname_Dev || // #ifdef H5 - || window.location.hostname - // #endif - port = envar.Base_Port_Dev || envar.Base_Port + window.location.hostname + // #endif + port = + envar.Base_Port_Dev || + envar.Base_Port || // #ifdef H5 - || window.location.port.replace(':','') - // #endif + window.location.port.replace(':', '') + // #endif } return `${protocol}://${hostname}:${port}/${route.replace(/^\//, '')}` }, - makeBgUrl(path) { + 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 = {} + **/ + 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() @@ -185,8 +179,9 @@ export default { // uniIdToken // uniCloud自动getStorageSync('uni_id_token')并传递为 uniIdToken;也可自行组装传入 uniIdToken }, }) - .catch((error) => { // {errMsg, stack} = error - if (/request:fail/.test(error.errMsg)) { + .catch((error) => { + // {errMsg, stack} = error + if (/request:fail/.test(error.errMsg)) { // 后台云服务无法连接 return { _state: 'CLIENT_BACKEND_BROKEN', error } } else { @@ -228,15 +223,21 @@ export default { // 注意2,虽然预设了 resultServer 和 resultCloud = {},但如果后台返回了 null,那么 resultServer/resultCloud 也是 null。 if (process.env.NODE_ENV === 'development') { console.log( - '%c '+JSON.stringify({startTime:startTime, page:thisRoute, endTime:new Date().toJSON()}) + - ' %c '+ JSON.stringify({ backend, apiWho, apiTodo, apiWhat, url }) + - ' %c '+ JSON.stringify(result), - 'color:blue', 'background:skyblue', 'background:magenta') // 不知为何,直接用 result 会输出一个奇怪的对象,要主动添加 JSON.stringify 才按照期望输出。 + '%c ' + + JSON.stringify({ startTime: startTime, page: thisRoute, endTime: new Date().toJSON() }) + + ' %c ' + + JSON.stringify({ backend, apiWho, apiTodo, apiWhat, url }) + + ' %c ' + + JSON.stringify(result), + 'color:blue', + 'background:skyblue', + 'background:magenta' + ) // 不知为何,直接用 result 会输出一个奇怪的对象,要主动添加 JSON.stringify 才按照期望输出。 } return result }, - async pickupFile2Server({ + async pickupFile2Server ({ mediaType = 'image', count = 1, sizeType = ['original', 'compressed'], @@ -277,25 +278,26 @@ export default { // url 所在方法进一步处理后,通过 uploadFile 存在 data 里返回结果 uni.hideLoading() - if (typeof(data)==='string') { // 不知为何,uni.uploadFile返回的 data 是字符串而不是对象 - try{ + if (typeof data === 'string') { + // 不知为何,uni.uploadFile返回的 data 是字符串而不是对象 + try { data = JSON.parse(data) - }catch(exp){ - return { _state: 'CLIENT_FAIL_RESPONSE_JSON_MALFORMED'} + } catch (exp) { + return { _state: 'CLIENT_FAIL_RESPONSE_JSON_MALFORMED' } } } - if (data?._state==='SUCCESS' && data?.path) { + if (data?._state === 'SUCCESS' && data?.path) { return { _state: 'SUCCESS', fileUrl: this.makeServerUrl(data.path), filePath: data.path, ...data } - }else { + } else { return { _state: 'CLIENT_FAIL_UPLOAD_FILE', errorUpload } } - }else { + } else { return { _state: 'CLIENT_FAIL_CHOOSE_FILE' } } }, - async pickupFile2Cloud({ mediaType = 'image', count = 1, sizeType = ['original', 'compressed'], sourceType = ['album', 'camera'], maxDuration } = {}) { + 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' } @@ -356,23 +358,30 @@ export default { return { _state: 'CLIENT_FAIL_CHOOSE_FILE' } }, - async pickupFile({ + 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', + 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 }) + 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}) + } else if (backend === 'SERVER') { + const resultServer = await this.pickupFile2Server({ mediaType, count, sizeType, sourceType, maxDuration, url, header, formData, name }) return resultServer - }else { + } else { return { _state: 'CLEINT_FAIL_UNKNOWN_BACKEND_TYPE', backend } } }, - openUrl(url) { + openUrl (url) { // #ifdef APP-PLUS plus.runtime.openURL(url) // #endif @@ -381,7 +390,7 @@ export default { // #endif }, - getSystemInfo() { + getSystemInfo () { let systemInfo = uni.getSystemInfoSync() // model=PC|iPhone|iPad|Nexus 6|..., // platform=ios|android|mac|windows|linux|other, @@ -434,9 +443,9 @@ export default { callback // 发生在 toast 之后 }) */ - showToast({ tool, type, image, title, duration = 2000, ...rest }) { + showToast ({ tool, type, image, title, duration = 2000, ...rest }) { const pageNow = this.thisPage() - if (tool === 'uni' || ! pageNow?.$refs?.toast) { + if (tool === 'uni' || !pageNow?.$refs?.toast) { // #ifdef APP-PLUS uni.showToast({ icon: 'none', title, duration, ...rest }) // #endif @@ -449,15 +458,15 @@ export default { } }, - formatMoney(value, precision=2) { + formatMoney (value, precision = 2) { return Number(value || 0).toFixed(precision) // Number(undefined)===NaN }, - formatPercent(value, precision=2) { + formatPercent (value, precision = 2) { return Number(value * 100 || 0).toFixed(precision) }, - formatDate(date, format) { + 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类型的值 @@ -486,47 +495,75 @@ export default { 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) - }, + 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) + }, + + 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 }) + }, + // 在手机上跳转到标签页需要 switchTab,在PC上跳转到菜单页需要 navigateTo 或 redirectTo。因此在这里用 gotoPage 和 backtoPage 来弥补差异。 + navigate_page (pageName, { pagesJson = this.pagesJson || wo?.pagesJson, envar = this.envar || wo?.envar } = {}) { + if (! envar?.onPC && pagesJson?.tabBar?.list?.find((item) => item?.pagePath?.substr(6) === pageName)) { + uni.switchTab({ url: pageName }) + } else { + uni.navigateTo({ url: pageName }) + } + }, + redirect_page (pageName, { pagesJson = this.pagesJson || wo?.pagesJson, envar = this.envar || wo?.envar } = {}) { + if (! envar?.onPC && pagesJson?.tabBar?.list?.find((item) => item?.pagePath?.substr(6) === pageName)) { + uni.switchTab({ url: pageName }) + } else { + uni.redirectTo({ url: pageName }) + } + }, }