wo-user-toolkit-uniapp/unitool.js

587 lines
25 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
const BASE_TYPE_DEFAULT = 'SERVER' // one of { SERVER: 服务器, UNICLOUD_FUNC: 云函数, UNICLOUD_OBJECT: 云对象 }
export default {
// 用直观的色彩,代替 uview 的命名方式:
RED: 'error',
GREEN: 'success',
BLUE: 'primary',
YELLOW: 'warning',
GREY: 'info',
BLACK_TOAST: 'default',
WHITE_BUTTON: 'default',
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) {
const pageName = this.thisPage()?.route || 'VueApp'
console.log('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:blue;background:lightgrey', ...args)
},
ccinfo (...args) {
const pageName = this.thisPage()?.route || 'VueApp'
console.info('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:green;background:lightgrey', ...args)
},
ccwarn (...args) {
const pageName = this.thisPage().route || 'VueApp'
console.warn('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:orange;background:lightgrey', ...args)
},
ccerror (...args) {
const pageName = this.thisPage()?.route || 'VueApp'
console.error('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:red;background:lightgrey', ...args)
},
ccdebug (...args) {
if (process.env.NODE_ENV !== 'production') {
const pageName = this.thisPage()?.route || 'VueApp'
console.debug('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:cyan;background:lightgrey', ...args)
}
},
cctitle (...args) {
const pageName = this.thisPage().route || 'VueApp'
console.debug('%c ' + JSON.stringify({ time: new Date().toJSON(), page: pageName }), 'color:cyan;background:lightgrey', ...args)
},
localizeText (i18nText) {
i18nText = i18nText?.__page__
? this.i18nText // 如果挂载到具体页面的 computed { lote: wo.localizeText } 那么 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 就是当前页面。
return this.i18nText?.[getApp().$store.state.i18n.mylang] || {}
},
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。
uni.setNavigationBarTitle({
// 也会被用于浏览器的标签标题,因此要用 document.title 去覆盖
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 的页面配置里
})
// #ifdef H5
document.title = windowTitle || wo?.envar?.callname?.[mylang] || pagesJson?.appInfo?.i18nText?.[mylang] || pagesJson?.globalStyle?.navigationBarTitleText // 必须放在 setNavigationBarTitle 之后,否则会被其覆盖掉。
// #endif
// 必须要在有 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({})
// #ifdef H5
// 响应式方案:仅仅根据当前设备类型,如果是 PC 大屏幕,则始终显示 topWindow 并且隐藏顶部 navibar 和底部 tabBar。
if (pagesJson?.topWindow || !envar?.showBarsOnPC) {
// 如果页头不是通过 pagesJson.topWindow 而是作为组件来引入个别页面,那么定义配置参数 showBarsOnPC 来控制。
if (uni.getSystemInfoSync().model === 'PC') {
if (window.innerWidth > (pagesJson?.topWindow?.matchMedia?.minWidth || 0)) {
uni.hideTabBar()
// 不知为何,同一个二级页面,如果第二次进入,就仍然会显示 navibar, 必须通过 setTimeout 执行才能彻底隐藏。
setTimeout(() => {
document.getElementsByTagName('uni-page-head')?.[0]?.remove()
}, 0)
}
} else {
document.getElementsByTagName('uni-top-window')?.[0]?.remove() // 强制隐藏 topWindow否则在手机浏览器里topWindow 会遮挡掉 navibar。
}
}
// #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 hostname = envar.servHostname
let port = envar.servPort
// #ifdef H5
if (!hostname) {
hostname = window?.location?.hostname
}
if (!port) {
port = window?.location?.port?.replace(':', '')
}
// #endif
if (!hostname) {
// 如果没有配置 envar.servHostname又不是在 H5 环境,则最后的默认值是 localhost
hostname = 'localhost'
}
const protocol = hostname === 'localhost' ? 'http' : envar.servProtocol || (process.env.NODE_ENV === 'production' ? 'https' : 'http')
return `${protocol}://${hostname}:${port}/${route.replace(/^\//, '')}`
},
makeBgUrl (path) {
if (path) {
return `url(${this.makeServerUrl(path)})`
}
return ''
},
/** 统一 uni.request 和 uniCloud.callFunction 的调用方法,提供统一、透明的后台调用
* 返回值:{ _state, 成功结果或错误结果 },其中 _state 除了后台返回的,还可以是
* - CLIENT_WOBASE_BROKEN: 前端发现后台断线
* - CLIENT_WOBASE_TIMEOUT: 前端发现后台超时
* - CLINET_WOBASE_EXCEPTION: 前端发现后台异常
**/
async callBase ({
baseType = this.envar?.baseTypeDefault || wo?.envar?.baseTypeDefault || BASE_TYPE_DEFAULT,
httpMethod = 'POST',
apiVersion = 'api',
apiWho,
apiTodo,
apiWhat = {},
}) {
const thisRoute = this.thisPage()?.route || 'VueApp' // 立刻保存 this.thisPage().route因为在调用后台后可能已切换到了其他页面。
const startTime = new Date().toJSON()
let url = undefined
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,
_passtoken: uni.getStorageSync('_passtoken'),
// 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])
}
}
url = this.makeServerUrl(`${apiVersion}/${apiWho}/${apiTodo}`)
let [error, { statusCode, header, errMsg, data: resultServer = {} } = {}] = await uni.request({
method: httpMethod,
url: url,
header: { _passtoken: uni.getStorageSync('_passtoken') },
data: apiWhat,
})
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' }
}
// 注意1resultServer 和 resultCloud 推荐遵循同样的格式 { _state, error | data },这样方便前端做统一判断。
// 注意2虽然预设了 resultServer 和 resultCloud = {},但如果后台返回了 null那么 resultServer/resultCloud 也是 null。
if (process.env.NODE_ENV !== 'production') {
console.log(
'%c ' +
JSON.stringify({ startTime: startTime, page: thisRoute, endTime: new Date().toJSON() }) +
' %c ' +
JSON.stringify({ baseType, apiWho, apiTodo, apiWhat, url }) +
' %c ' +
JSON.stringify(result),
'color:blue;background:lightgrey',
'background:skyblue',
'background:magenta'
) // 不知为何,直接用 result 会输出一个奇怪的对象,要主动添加 JSON.stringify 才按照期望输出。
}
return result
},
async pickupFile2Server ({
mediaType = 'image',
count = 1,
sizeType = ['original', 'compressed'],
sourceType = ['album', 'camera'],
url = 'api/Fileloader/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 }, 其中 path 包括了 destination 和 filename 的文件完整路径。
// 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 !== 'production') {
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 ({
baseType = this.envar?.baseTypeDefault || wo?.envar?.baseTypeDefault || BASE_TYPE_DEFAULT,
mediaType = 'image',
count = 1,
sizeType = ['original', 'compressed'],
sourceType = ['album', 'camera'],
maxDuration,
url,
header = {},
formData = {},
name = 'file',
} = {}) {
if (/^UNICLOUD/.test(baseType)) {
const resultCloud = await this.pickupFile2Cloud({ mediaType, count, sizeType, sourceType, maxDuration })
return resultCloud
} else if (baseType === 'SERVER') {
const resultServer = await this.pickupFile2Server({ mediaType, count, sizeType, sourceType, maxDuration, url, header, formData, name })
return resultServer
} else {
return { _state: 'CLEINT_FAIL_UNKNOWN_WOBASE_TYPE', baseType }
}
},
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
})
* uToptips.show({
type=default (by default)/primary/success/error/warning/info,
position:center/top/bottom,
callback // 发生在 toast 之后
})
*/
showToast ({ tool, type, image, title, duration = 2000, ...rest }) {
if (tool !== 'uni') {
// 来自 <ucToast> 或 <u-toast> 或 <u-top-tips>
const toast = this.thisPage()?.$refs?.toast || getApp().globalData.toast || wo.toast
const popup = this.thisPage()?.$refs?.popup || getApp().globalData.popup || wo.popup
if (toast) {
toast.show({ type, title, duration, ...rest })
return
} else if (popup) {
wo.ss.popMessage = title
wo.ss.popType = type || 'success'
wo.ss.popDuration = duration || 2000
popup.open()
return
}
}
// #ifdef APP-PLUS
uni.showToast({ icon: 'none', title, duration, ...rest })
// #endif
// #ifdef H5
uni.showToast({ icon: 'none', image, title, duration, ...rest })
// #endif
},
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标签
// <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, { forget = false, pagesJson = this.pagesJson || wo?.pagesJson } = {}) {
if (pageName) {
if (pagesJson?.tabBar?.list?.find((item) => item?.pagePath?.substr(6) === pageName)) {
// 注意,即使在 PC 上 topWindow 代替了 tabBar 时,从标签页转化而来的菜单页,也是用 switchTab 跳转。
uni.switchTab({ url: pageName })
} else if (forget) {
uni.navigateTo({ url: pageName })
} else {
uni.redirectTo({ url: pageName })
}
} else {
uni.navigateBack()
}
},
next_focus (currentFocus, focusList = this.thisPage().focusList) {
if (focusList) {
for (let n in focusList) {
focusList[n] = false
}
setTimeout(() => {
focusList[(parseInt(currentFocus) + 1) % Object.keys(focusList).length] = true
}, 200) // 如果没有 setTimeout 至少 200ms, 焦点短暂跳到下一个后,又会消失
}
},
}