Compare commits

...

1 Commits

Author SHA1 Message Date
feige996
faeeef8655 feat: 引入devTools, 测试各种异常 2025-06-15 16:36:08 +08:00
92 changed files with 13546 additions and 2 deletions

View File

@ -48,4 +48,26 @@ export default defineUniPages({
},
],
},
subPackages: [
{
root: 'devTools/page',
pages: [
{
path: 'index',
style: {
navigationStyle: 'custom',
// #ifdef APP-PLUS
softinputMode: 'adjustResize',
// backgroundColor: 'transparent',
animationDuration: 1,
animationType: 'none',
popGesture: 'none',
bounce: 'none',
titleNView: false,
// #endif
},
},
],
},
],
})

9
src/devTools/READEME.MD Normal file
View File

@ -0,0 +1,9 @@
# UniDevTools - 调试工具
支持 Vue2+Vue3 的跨平台调试工具
> 文档&安装教程 https://dev.api0.cn/
当前版本v3.8
更新日期2025.5.5

69
src/devTools/config.js Normal file
View File

@ -0,0 +1,69 @@
let config = {
status: true, //调试工具总开关
route: '/devTools/page/index', // 调试页面的路由,不建议更改
bubble: {
//调试弹窗气泡设置
status: true, // 气泡标签是否显示,生产环境建议关闭
text: 'DevTools', // 气泡上展示的文字
color: '#ffffff', // 气泡文字颜色
bgColor: 'rgba(250, 53, 52,0.7)', // 气泡背景颜色
},
// 注意: 以下配置不建议更改
pageStatistics: {
// 页面统计开关
status: true, // 统计状态开关
size: 1024 * 100, // 缓存上限单位byte
dayOnlineRowMax: 30, // 活跃数据缓存天数
},
console: {
//console日志配置
status: true, //功能总开关
isOutput: true, //打印的日志是否对外输出到浏览器调试界面,建议在生产环境时关闭
cache: {
status: true, //是否启用本地缓存
size: 512 * 1024, //缓存上限单位byte
rowSize: 1024 * 4, //单条记录缓存上限单位byte
},
},
uniBus: {
// uni event bus 监听设置
status: true,
cache: {
status: true,
size: 1024 * 512, // bus调用日志上限 byte
rowSize: 1024 * 10,
countMaxSize: 1024 * 10, // bus统计上限 byte
},
},
error: {
//报错拦截配置
status: true,
cache: {
status: true,
size: 512 * 1024,
rowSize: 1024 * 4,
},
},
network: {
//请求拦截配置
status: true,
cache: {
status: true,
size: 512 * 1024,
rowSize: 1024 * 4,
},
},
logs: {
//运行日志
status: true,
cache: {
status: true,
size: 512 * 1024,
rowSize: 1024 * 4,
},
},
}
export default config

View File

@ -0,0 +1,156 @@
<template>
<view
v-if="isMp && options && options.status && options.bubble.status"
class="mpDevBubble"
:style="{
left: `${tagConfig.x}px`,
top: `${tagConfig.y}px`,
'background-color': options.bubble.bgColor,
'box-shadow': `0px 0px 6px ${options.bubble.bgColor}`,
}"
@touchstart.stop="touchstart"
@touchmove.stop="touchmove"
@touchend.stop="touchend"
>
<text
class="mpDevBubbleText"
:style="{
color: options.bubble.color,
'font-size': '20rpx',
}"
>
{{ options.bubble.text }}
</text>
</view>
</template>
<script>
import devOptions from '../libs/devOptions'
let options = devOptions.getOptions()
let sysInfo = uni.getSystemInfoSync()
let tagConfig = uni.getStorageSync('devTools_tagConfig')
if (!tagConfig) {
tagConfig = {}
}
tagConfig = Object.assign(
{
x: sysInfo.screenWidth - 150,
y: sysInfo.screenHeight - 240,
},
tagConfig,
)
//
let dragLimit = {
min: { x: 0, y: 0 },
max: {
x: sysInfo.screenWidth - 70,
y: sysInfo.screenHeight - 24,
},
}
tagConfig.x = Math.min(Math.max(tagConfig.x, dragLimit.min.x), dragLimit.max.x)
tagConfig.y = Math.min(Math.max(tagConfig.y, dragLimit.min.y), dragLimit.max.y)
let isTouch = false
let touchStartPoint = {
clientX: 0,
clientY: 0,
tagX: tagConfig.x,
tagY: tagConfig.y,
hasMove: false,
}
let isMp = false
// #ifdef MP
isMp = true
// #endif
export default {
data() {
return {
isMp,
/**
* 标签参数
*/
options,
/**
* 标签坐标信息配置
*/
tagConfig,
}
},
mounted() {
// console.log("zzzzzzzzzzzzzzzz");
},
methods: {
touchstart(e) {
if (isTouch) return
if (e.preventDefault) {
e.preventDefault()
}
let clientX = e.touches[0].clientX
let clientY = e.touches[0].clientY
touchStartPoint.clientX = clientX
touchStartPoint.clientY = clientY
touchStartPoint.tagX = tagConfig.x
touchStartPoint.tagY = tagConfig.y
touchStartPoint.hasMove = false
isTouch = true
},
touchmove(e) {
if (!isTouch) return
if (e.preventDefault) {
e.preventDefault()
}
let clientX = e.touches[0].clientX
let clientY = e.touches[0].clientY
touchStartPoint.hasMove = true
let offsetX = touchStartPoint.clientX - clientX
let offsetY = touchStartPoint.clientY - clientY
let tx = touchStartPoint.tagX - offsetX
let ty = touchStartPoint.tagY - offsetY
tx = Math.min(Math.max(tx, dragLimit.min.x), dragLimit.max.x)
ty = Math.min(Math.max(ty, dragLimit.min.y), dragLimit.max.y)
tagConfig.x = tx
tagConfig.y = ty
this.tagConfig.x = tx
this.tagConfig.y = ty
},
touchend(e) {
if (!isTouch) return
if (e.preventDefault) {
e.preventDefault()
}
isTouch = false
uni.setStorageSync('devTools_tagConfig', tagConfig)
if (!touchStartPoint.hasMove) {
let pages = getCurrentPages()
let route = options.route.substring(1, options.route.length - 2)
if (pages[pages.length - 1].route == route) {
// debug
return
}
this.$devTools.show()
}
},
},
}
</script>
<style lang="scss" scoped>
.mpDevBubble {
box-sizing: border-box;
position: fixed;
z-index: 9999999;
width: 70px;
height: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 6px;
font-size: 10px;
}
</style>

View File

@ -0,0 +1,142 @@
/**
*! 创建h5页面上拖动的气泡标签
*/
function createH5Bubble(options, devTools) {
let tagConfig = localStorage.getItem('devTools_tagConfig')
if (!tagConfig) {
tagConfig = {}
} else {
tagConfig = JSON.parse(tagConfig)
}
tagConfig = Object.assign(
{
show: options.bubble.status,
x: window.innerWidth - 90,
y: window.innerHeight - 90,
},
tagConfig,
)
tagConfig.show = options.bubble.status
// 拖动范围限制
let dragLimit = {
min: { x: 0, y: 0 },
max: {
x: window.innerWidth - 70,
y: window.innerHeight - 24,
},
}
tagConfig.x = Math.min(Math.max(tagConfig.x, dragLimit.min.x), dragLimit.max.x)
tagConfig.y = Math.min(Math.max(tagConfig.y, dragLimit.min.y), dragLimit.max.y)
let tag = document.createElement('div')
tag.style = `
box-sizing: border-box;
position: fixed;
z-index: 9999999;
left: ${tagConfig.x}px;
top: ${tagConfig.y}px;
width: 70px;
height: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 6px;
background-color: ${options.bubble.bgColor};
color: ${options.bubble.color};
font-size: 10px;
cursor: grab;
box-shadow: 0px 0px 6px ${options.bubble.bgColor};
backdrop-filter: blur(1px);
`
tag.innerHTML = options.bubble.text
tag.setAttribute('id', 'debugTag')
if (tagConfig.show) {
document.body.appendChild(tag)
}
/**
* 标签单击事件
*/
function tagClick() {
let pages = getCurrentPages()
let route = options.route.substring(1, options.route.length - 2)
if (pages[pages.length - 1].route == route) {
// 已经处于debug页面不响应点击事件
return
}
devTools.show()
}
let isTouch = false
let touchStartPoint = {
clientX: 0,
clientY: 0,
tagX: tagConfig.x,
tagY: tagConfig.y,
hasMove: false,
}
function touchStart(e) {
if (isTouch) return
if (e.preventDefault) {
e.preventDefault()
}
let clientX = e.clientX ? e.clientX : e.targetTouches[0].clientX
let clientY = e.clientX ? e.clientY : e.targetTouches[0].clientY
touchStartPoint.clientX = clientX
touchStartPoint.clientY = clientY
touchStartPoint.tagX = tagConfig.x
touchStartPoint.tagY = tagConfig.y
touchStartPoint.hasMove = false
isTouch = true
}
function touchMove(e) {
if (!isTouch) return
if (e.preventDefault) {
e.preventDefault()
}
let clientX = e.clientX ? e.clientX : e.targetTouches[0].clientX
let clientY = e.clientX ? e.clientY : e.targetTouches[0].clientY
touchStartPoint.hasMove = true
let offsetX = touchStartPoint.clientX - clientX
let offsetY = touchStartPoint.clientY - clientY
let tx = touchStartPoint.tagX - offsetX
let ty = touchStartPoint.tagY - offsetY
tx = Math.min(Math.max(tx, dragLimit.min.x), dragLimit.max.x)
ty = Math.min(Math.max(ty, dragLimit.min.y), dragLimit.max.y)
tag.style.left = `${tx}px`
tag.style.top = `${ty}px`
tagConfig.x = tx
tagConfig.y = ty
}
function touchEnd(e) {
if (!isTouch) return
if (e.preventDefault) {
e.preventDefault()
}
isTouch = false
localStorage.setItem('devTools_tagConfig', JSON.stringify(tagConfig))
if (!touchStartPoint.hasMove) {
tagClick()
}
}
tag.addEventListener('touchstart', touchStart)
tag.addEventListener('touchmove', touchMove)
tag.addEventListener('touchend', touchEnd)
tag.addEventListener('mousedown', touchStart)
document.addEventListener('mousemove', touchMove)
document.addEventListener('mouseup', touchEnd)
localStorage.setItem('devTools_tagConfig', JSON.stringify(tagConfig))
}
export default createH5Bubble

View File

@ -0,0 +1,131 @@
import devOptions from './devOptions'
/**
* dev工具缓存管理
*/
export default {
/**
* 存储的键开始名称
*/
cacheKey: 'devTools_v3_',
options: null,
/**
* 临时缓存对象
*/
tempCache: {
errorReport: [],
logReport: [],
console: [],
request: [],
uniBus: [],
},
/**
* 临时数据存放
*/
tempData: {},
/**
* 向缓存内写入数据
*/
set(key, value) {
try {
if (['errorReport', 'logReport', 'console', 'request', 'uniBus'].indexOf(key) != -1) {
let setting = this.getLongListSetting(key)
if (!setting.status) return
if (!setting.cache.status) {
// !不使用缓存
this.tempCache[key] = value
return
}
}
key = `${this.cacheKey}${key}`
// #ifdef APP-NVUE
let pages = getCurrentPages()
if (pages[pages.length - 1].route == 'devTools/page/index') {
// devtools 页面直接走设置缓存
return uni.setStorageSync(key, value)
}
// #endif
this.tempData[key] = value
} catch (error) {
console.log('devCache.set error', error)
}
},
/**
* 同步读取缓存数据
*/
get(key) {
try {
if (['errorReport', 'logReport', 'console', 'request', 'uniBus'].indexOf(key) != -1) {
let setting = this.getLongListSetting(key)
if (!setting.status) return []
if (!setting.cache.status) {
// !不使用缓存
return this.tempCache[key]
}
}
key = `${this.cacheKey}${key}`
// #ifdef APP-NVUE
let pages = getCurrentPages()
if (pages[pages.length - 1].route == 'devTools/page/index') {
// devtools 页面直接走设置缓存
return uni.getStorageSync(key)
}
// #endif
if (this.tempData.hasOwnProperty(key)) {
return this.tempData[key]
} else {
let value = uni.getStorageSync(key)
this.tempData[key] = value
return value
}
} catch (error) {
console.log('devCache.get error', error)
return ''
}
},
getLongListSetting(key) {
let optionsKey = {
errorReport: 'error',
logReport: 'logs',
console: 'console',
request: 'network',
uniBus: 'uniBus',
}
if (this.options) return this.options[optionsKey[key]]
this.options = devOptions.getOptions()
return this.options[optionsKey[key]]
},
/**
* 同步本地缓存
*/
syncLocalCache() {
let that = this
setTimeout(
() => {
try {
let waitSetKeys = Object.keys(that.tempData)
for (let i = 0; i < waitSetKeys.length; i++) {
const key = waitSetKeys[i]
uni.setStorage({
key,
data: that.tempData[key],
success() {
// console.log("set " + key + " success,length=" + that.tempData[key].length);
delete that.tempData[key]
},
})
}
} catch (error) {
console.log('devCache error: ', error)
}
setTimeout(() => {
that.syncLocalCache()
}, 500)
},
Math.round(Math.random() * 3 * 1000) + 2000,
)
},
}

View File

@ -0,0 +1,184 @@
import devCache from './devCache'
/**
* 设置各端大小 kb
*/
const defSize = (h5, app, mp) => {
let r = 0
// #ifdef H5
r = h5
// #endif
// #ifdef MP
r = mp
// #endif
// #ifdef APP-PLUS || APP-NVUE
r = app
// #endif
return Math.ceil(r * 1024)
}
/**
* 获取配置
*/
export default {
/**
* 配置缓存key
*/
cacheKey: 'options_v8',
/**
* 默认配置项
*/
defaultOptions: {
version: 3.81,
status: false, //调试工具总开关
route: '/devTools/page/index', // 调试页面的路由,不建议更改
bubble: {
//调试弹窗气泡设置
status: false, // 气泡标签是否显示,生产环境建议关闭
text: '调试工具', // 气泡上展示的文字
color: '#ffffff', // 气泡文字颜色
bgColor: 'rgba(250, 53, 52,0.7)', // 气泡背景颜色
},
console: {
status: true, // 开关
isOutput: true, //打印的日志是否对外输出到浏览器调试界面,建议在生产环境时开启
cache: {
status: true, //是否启用console缓存
size: defSize(512, 1024 * 2, 512),
rowSize: defSize(5.12, 20, 10),
},
},
error: {
status: true,
cache: {
status: true,
size: defSize(512, 1024 * 2, 512),
rowSize: defSize(5.12, 20, 10),
},
},
network: {
status: true,
cache: {
status: true,
size: defSize(512, 1024 * 2, 512),
rowSize: defSize(5.12, 20, 10),
},
},
logs: {
status: true,
cache: {
status: true,
size: defSize(512, 1024 * 2, 512),
rowSize: defSize(0.4, 0.4, 0.4),
},
},
// 页面统计开关
pageStatistics: {
status: true, // 统计状态开关
size: defSize(200, 1024 * 2, 512),
// #ifdef H5
dayOnlineRowMax: 30, // 日活跃时间的保存条数
// #endif
// #ifdef APP-PLUS || APP-NVUE
dayOnlineRowMax: 90, // 日活跃时间的保存条数
// #endif
// #ifdef MP-WEIXIN
dayOnlineRowMax: 60, // 日活跃时间的保存条数
// #endif
},
// uni event bus 监听设置
uniBus: {
status: true,
cache: {
status: true,
size: defSize(512, 1024 * 2, 512),
rowSize: defSize(5.12, 20, 10),
countMaxSize: defSize(512, 1024 * 2, 512), // bus统计上限 kb
},
},
},
/**
* 获取配置信息
*/
getOptions() {
try {
let options = devCache.get(this.cacheKey)
if (!options) {
return {
status: false, //默认关闭调试工具
}
}
let r = String(options.route)
// ! 增加 devRoute 参数
options.devRoute = r.substring(1, r.length)
return options
} catch (error) {
console.log('devOptions.getOptions error: ', error)
return {
status: false, //默认关闭调试工具
}
}
},
/**
* 保存配置项
*/
setOptions(options) {
try {
if (!options) {
options = this.defaultOptions
}
if (options.status) {
if (!options.route || typeof options.route != 'string' || options.route.indexOf('/') != 0) {
return this.outputError(`devTools 调试工具配置出错: [route] 参数配置错误!`)
}
}
let data = deepMerge(this.defaultOptions, options)
devCache.set(this.cacheKey, data)
} catch (error) {
console.log('devOptions.setOptions error: ', error)
}
},
/**
* 弹出错误信息
*/
outputError(msg) {
console.log(
'%c' + msg,
`
padding: 4px;
background-color: red;
color: #fff;
font-size: 15px;
`,
)
},
}
/**
* 深度合并对象
*/
function deepMerge(target, ...sources) {
try {
if (!sources.length) return target // 如果没有源对象则直接返回目标对象
const source = sources[0]
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && typeof target[key] !== 'undefined') {
target[key] = deepMerge({}, target[key], source[key]) // 若属性值为对象类型且目标对象已存在该属性则递归调用deepMerge函数进行合并
} else {
target[key] = source[key] // 否则将源对象的属性赋值到目标对象上
}
}
}
return deepMerge(target, ...sources.slice(1)) // 处理完第一个源对象后再次调用deepMerge函数处理其他源对象
} catch (error) {
console.log('deepMerge error', error)
return {}
}
}

View File

@ -0,0 +1,118 @@
/**
* 绘制调试工具
*/
/**
* 入口文件
*/
function init(options, devTools) {
let sysInfo = uni.getSystemInfoSync()
let tagConfig = uni.getStorageSync('devTools_tagConfig')
if (!tagConfig) {
tagConfig = {}
}
tagConfig = Object.assign(
{
show: options.bubble.status,
x: sysInfo.screenWidth - 90,
y: sysInfo.screenHeight - 90,
},
tagConfig,
)
tagConfig.show = options.bubble.status
// 拖动范围限制
let dragLimit = {
min: { x: 0, y: 0 },
max: {
x: sysInfo.screenWidth - 70,
y: sysInfo.screenHeight - 24,
},
}
let view = new plus.nativeObj.View('debugTag', {
top: tagConfig.y + 'px',
left: tagConfig.x + 'px',
height: '24px',
width: '70px',
backgroundColor: options.bubble.bgColor,
})
view.drawText(
options.bubble.text,
{},
{
size: '12px',
color: options.bubble.color,
weight: 'bold',
},
)
if (tagConfig.show) {
view.show()
}
let isTouch = false
let touchStart = {
l: 0,
t: 0,
x: 0,
y: 0,
time: 0,
hasMove: false,
}
view.addEventListener('touchstart', (e) => {
isTouch = true
touchStart.l = e.clientX
touchStart.t = e.clientY
touchStart.time = new Date().getTime()
touchStart.hasMove = false
})
view.addEventListener('touchmove', (e) => {
if (!isTouch) return
if (!touchStart.hasMove) {
touchStart.hasMove = true
}
let x = e.screenX - touchStart.l
let y = e.screenY - touchStart.t
x = Math.min(Math.max(x, dragLimit.min.x), dragLimit.max.x)
y = Math.min(Math.max(y, dragLimit.min.y), dragLimit.max.y)
view.setStyle({
top: y + 'px',
left: x + 'px',
})
touchStart.x = x
touchStart.y = y
})
view.addEventListener('touchend', (e) => {
isTouch = false
if (!touchStart.hasMove || touchStart.time > new Date().getTime() - 300) {
// 单击事件
let pages = getCurrentPages()
let route = options.route.substring(1, options.route.length - 2)
if (pages[pages.length - 1].route == route) {
// 已经处于debug页面不响应点击事件
return
}
devTools.show()
} else {
//拖拽结束事件
tagConfig.x = touchStart.x
tagConfig.y = touchStart.y
uni.setStorageSync('devTools_tagConfig', tagConfig)
}
})
uni.setStorageSync('devTools_tagConfig', tagConfig)
}
export default init

View File

@ -0,0 +1,60 @@
import devCache from './devCache'
import devOptions from './devOptions'
import jsonCompress from './jsonCompress'
/**
* ! vue报错捕获
*/
/**
* * vue错误日志上报
* @param {'ve'|'vw'|'oe'|'n'} type 错误类型
*/
function errorReport(msg, trace, type = 'n') {
try {
if (!msg) return false
if (msg instanceof Error) {
msg = msg.message
}
let options = devOptions.getOptions()
if (!options.error.status) return
let page = '未知'
try {
let pages = getCurrentPages()
let item = pages[pages.length - 1]
if (item && item.route) {
page = item.route
}
} catch (error) {}
let logs = devCache.get('errorReport')
if (!logs) logs = []
if (logs.length >= options.error.cache.rowMax) {
logs = logs.splice(0, options.error.cache.rowMax)
}
msg = String(msg)
msg = jsonCompress.compressObject(msg, options.error.cache.rowSize / 2)
trace = String(trace)
trace = jsonCompress.compressObject(trace, options.error.cache.rowSize / 2)
logs.unshift({
t: new Date().getTime(),
m: msg,
tr: trace,
p: page,
type,
})
console.error('__ignoreReport__', msg, trace)
logs = jsonCompress.compressArray(logs, 'end', options.error.cache.size)
devCache.set('errorReport', logs)
} catch (error) {
console.log('errorReport error: ', error)
}
}
export default errorReport

View File

@ -0,0 +1,333 @@
/**
* json压缩工具
*/
export default {
/**
* 压缩js对象成json字符串并控制json字节大小多余部分裁剪
*/
compressObject(obj = {}, maxSize = 1024 * 10.24) {
try {
if (obj === undefined || obj === null) return obj
if (typeof obj == 'string') {
return this.truncateStrBySize(obj, maxSize)
}
if (typeof obj == 'number') {
return obj
}
let t = new Date().getTime()
const type = typeof obj
if (type === 'symbol') {
obj = 'Symbol->' + obj.toString()
} else if (type === 'bigint') {
obj = 'bigint->' + obj.toString()
} else if (typeof Error != 'undefined' && obj instanceof Error) {
obj = `Error->${obj.name}\n${obj.message}\n${obj.stack}`
} else if (typeof Date != 'undefined' && obj instanceof Date) {
obj = 'Date->' + obj.toISOString()
} else if (typeof obj == 'function') {
obj = 'Function->' + obj.toString()
} else if (typeof RegExp != 'undefined' && obj instanceof RegExp) {
obj = 'RegExp->' + obj.toString()
} else if (typeof Map != 'undefined' && obj instanceof Map) {
obj = `Map->(${obj.size}) { ${Array.from(obj.entries())
.map(([key, value]) => `${convertToString(key)} => ${convertToString(value)}`)
.join(', ')} }`
} else if (typeof Set != 'undefined' && obj instanceof Set) {
obj = `Set->(${obj.size}) { ${Array.from(obj.values())
.map((value) => convertToString(value))
.join(', ')} }`
} else if (typeof Blob != 'undefined' && obj instanceof Blob) {
obj = `Blob->{ size: ${obj.size}, type: ${obj.type} }`
} else if (typeof File != 'undefined' && obj instanceof File) {
obj = `File->{ name: "${obj.name}", size: ${obj.size}, type: ${obj.type}, lastModified: ${new Date(obj.lastModified).toISOString()} }`
} else if (typeof URL != 'undefined' && obj instanceof URL) {
obj = `URL->{ href: "${obj.href}", protocol: "${obj.protocol}", host: "${obj.host}", pathname: "${obj.pathname}", search: "${obj.search}", hash: "${obj.hash}" }`
} else if (typeof FormData != 'undefined' && obj instanceof FormData) {
const entries = []
obj.forEach((key, item) => {
entries.push(key)
})
obj = `FormData->{ ${entries.join(', ')} }`
} else if (typeof Location != 'undefined' && obj instanceof Location) {
obj = `Location->{ href: "${obj.href}", protocol: "${obj.protocol}", host: "${obj.host}", pathname: "${obj.pathname}", search: "${obj.search}", hash: "${obj.hash}" }`
} else if (typeof Document != 'undefined' && obj instanceof Document) {
obj = `Document->{ title: "${obj.title}", URL: "${obj.URL}" }`
} else if (typeof Window !== 'undefined' && obj instanceof Window) {
obj = `Window->{ location: ${this.compressObject(obj.location)}, document: ${this.compressObject(obj.document)} }`
} else if (typeof Element != 'undefined' && obj instanceof Element) {
obj = `Element->{ tagName: "${obj.tagName}", id: "${obj.id}", class: "${obj.className}" }`
} else if (typeof HTMLCanvasElement != 'undefined' && obj instanceof HTMLCanvasElement) {
obj = `Canvas->{ width: ${obj.width}, height: ${obj.height} }`
} else if (typeof HTMLAudioElement != 'undefined' && obj instanceof HTMLAudioElement) {
obj = `Audio->{ src: "${obj.src}", duration: ${obj.duration} }`
} else if (typeof HTMLVideoElement != 'undefined' && obj instanceof HTMLVideoElement) {
obj = `Video->{ src: "${obj.src}", width: ${obj.videoWidth}, height: ${obj.videoHeight}, duration: ${obj.duration} }`
} else if (typeof Storage != 'undefined' && obj instanceof Storage) {
obj = `Storage->{ length: ${obj.length} }`
} else if (
typeof Worker != 'undefined' &&
typeof ServiceWorker != 'undefined' &&
typeof SharedWorker != 'undefined' &&
(obj instanceof Worker || obj instanceof ServiceWorker || obj instanceof SharedWorker)
) {
obj = `Worker->${obj.constructor.name} { scriptURL: "${obj.scriptURL}" }`
} else if (typeof WebSocket != 'undefined' && obj instanceof WebSocket) {
obj = `WebSocket->{ url: "${obj.url}", readyState: ${obj.readyState} }`
} else if (typeof XMLHttpRequest != 'undefined' && obj instanceof XMLHttpRequest) {
obj = `XMLHttpRequest->{ readyState: ${obj.readyState}, status: ${obj.status} }`
} else if (typeof EventSource != 'undefined' && obj instanceof EventSource) {
obj = `EventSource->{ url: "${obj.url}", readyState: ${obj.readyState} }`
} else if (typeof MediaStream != 'undefined' && obj instanceof MediaStream) {
obj = `MediaStream->{ id: "${obj.id}", active: ${obj.active} }`
} else if (typeof RTCPeerConnection != 'undefined' && obj instanceof RTCPeerConnection) {
obj = `RTCPeerConnection->{ connectionState: "${obj.connectionState}" }`
} else if (typeof AudioContext != 'undefined' && obj instanceof AudioContext) {
obj = `AudioContext->{ state: "${obj.state}" }`
} else if (typeof Element != 'undefined' && obj instanceof Element) {
obj = `Element->{ tagName: "${obj.tagName}", id: "${obj.id}", class: "${obj.className}" }`
} else if (typeof HTMLCanvasElement != 'undefined' && obj instanceof HTMLCanvasElement) {
obj = `Canvas->{ width: ${obj.width}, height: ${obj.height} }`
} else if (typeof HTMLAudioElement != 'undefined' && obj instanceof HTMLAudioElement) {
obj = `Audio->{ src: "${obj.src}", duration: ${obj.duration} }`
} else if (typeof HTMLVideoElement != 'undefined' && obj instanceof HTMLVideoElement) {
obj = `Video->{ src: "${obj.src}", width: ${obj.videoWidth}, height: ${obj.videoHeight}, duration: ${obj.duration} }`
} else if (typeof Geolocation != 'undefined' && obj instanceof Geolocation) {
obj = `Geolocation->{ }`
} else if (typeof Performance != 'undefined' && obj instanceof Performance) {
obj = `Performance->{ now: ${obj.now()} }`
} else if (typeof Event != 'undefined' && obj instanceof Event) {
obj = `Event->{ type: "${obj.type}", target: "${obj.target}" }`
}
if (typeof obj == 'string') {
return this.truncateStrBySize(obj, maxSize)
}
if (typeof obj != 'object') {
return obj
}
if (maxSize < 2) return {}
let addEndOut = false
if (maxSize > 50) {
let objSize = this.calculateStringByteSize(obj)
if (objSize > maxSize) {
maxSize = maxSize - 50
addEndOut = true
}
}
let sizeCount = 2
let str = this.safeJsonStringify(obj, (key, value) => {
let keySize = this.calculateStringByteSize(key)
if (typeof value == 'object') {
if (sizeCount + keySize + 6 > maxSize) {
return
}
sizeCount = sizeCount + keySize + 6
return value
}
let valueSize = this.calculateStringByteSize(value)
let rowSize = keySize + valueSize + 6
if (rowSize + sizeCount > maxSize) return
sizeCount = sizeCount + rowSize
return value
})
let outPut = JSON.parse(str)
if (addEndOut) {
if (Array.isArray(outPut)) {
outPut.push('(已截断其余部分)')
} else if (typeof outPut == 'object') {
outPut['*注意'] = '(已截断其余部分)'
}
}
// console.log("compressObject use time: " + (new Date().getTime() - t));
return outPut
} catch (error) {
console.log('compressObject error', error)
return ''
}
},
/**
* 压缩数组不超过特定大小
* @param {any[]} [arr=[]] 需要处理的数组
* @param {string} [delType='start'] 数组超出后删除的开始位置
* @param {number} [maxSize=1024 * 972] 数组最大字节数
*/
compressArray(arr = [], delType = 'start', maxSize = 1024 * 972) {
let t = new Date().getTime()
try {
if (!arr || arr.length == 0 || !arr[0]) return []
let i = 0
while (true) {
i = i + 1
if (i > 999999) return arr
if (!arr || arr.length == 0) {
return []
}
if (this.calculateStringByteSize(arr) <= maxSize) {
// consoleLog("compressArray t=>" + (new Date().getTime() - t) + " i=>" + i)
return arr
}
if (delType == 'start') {
arr.splice(0, 1)
} else {
arr.splice(arr.length - 1, 1)
}
}
} catch (error) {
console.log('compressArray error', error)
return []
}
},
/**
* 计算对象或字符串占用的字节大小传入对象将自动转json
*/
calculateStringByteSize(str) {
try {
let type = typeof str
if (type == 'bigint' || type == 'number' || type == 'boolean') {
return str.toString().length
} else if (type == 'function') {
str = str.toString().length
} else if (str === null || str === undefined) {
return 0
} else {
try {
str = this.safeJsonStringify(str)
if (str && str.hasOwnProperty) {
return str.length
} else {
return 1024 * 20
}
} catch (error) {
console.log('calculateStringByteSize error', error)
return 1024 * 20
}
}
let size = 0
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i)
if (charCode < 0x0080) {
size += 1
} else if (charCode < 0x0800) {
size += 2
} else if (charCode >= 0xd800 && charCode <= 0xdfff) {
size += 4
i++
} else {
size += 3
}
}
return size
} catch (error) {
console.log('calculateStringByteSize error', error)
return 1024 * 1024
}
},
/**
* 安全的js对象转字符串
*/
safeJsonStringify(obj, handleValue) {
if (!obj) return '{}'
try {
if (handleValue) {
return JSON.stringify(obj, (key, value) => {
return handleValue(key, value)
})
} else {
return JSON.stringify(obj, (key, value) => {
return value
})
}
} catch (error) {
// 尝试解析json失败可能是变量循环引用的问题继续尝试增加WeakSet解析
}
try {
let seen = new WeakSet()
let jsonStr = JSON.stringify(obj, (key, value) => {
if (typeof value == 'object') {
try {
if (value instanceof File) {
value = 'js:File'
}
if (
value &&
value.constructor &&
value.constructor.name &&
typeof value.constructor.name == 'string'
) {
let className = value.constructor.name
if (className == 'VueComponent') {
return 'js:Object:VueComponent'
}
}
} catch (error) {}
}
if (typeof value == 'function') {
try {
value = value.toString()
} catch (error) {
value = 'js:function'
}
}
if (typeof value === 'object' && value !== null) {
// 处理循环引用问题
if (seen.has(value)) {
return
}
seen.add(value)
}
if (handleValue && typeof handleValue == 'function') {
try {
return handleValue(key, value)
} catch (error) {
console.log('handleValue error', error)
}
return
}
return value
})
seen = null
return jsonStr
} catch (error) {
return '{}'
}
},
/**
* 根据限制的字节大小截取字符串
*/
truncateStrBySize(str = '', size = 20 * 1024) {
try {
if (size < 1) return ''
if (this.calculateStringByteSize(str) <= size) return str
let endStr = ''
if (size > 30) {
endStr = '(已截断多余部分)'
size = size - 30
}
let low = 0,
high = str.length,
mid
while (low < high) {
mid = Math.floor((low + high) / 2)
let currentSize = this.calculateStringByteSize(str.substring(0, mid))
if (currentSize > size) {
// 如果大于限制值,减小高边界
high = mid
} else {
// 如果小于或等于限制值,增加低边界
low = mid + 1
}
}
// 返回截断的字符串注意low-1是因为low是最后一次检查超出大小时的位置
return str.substring(0, low - 1) + endStr
} catch (error) {
console.log('truncateStrBySize error', error)
return ''
}
},
}

View File

@ -0,0 +1,63 @@
import devCache from './devCache'
import devOptions from './devOptions'
import jsonCompress from './jsonCompress'
/**
* ! 运行日志提交工具
*/
/**
* 日志上报
*/
function logReport(msg) {
try {
if (!msg) return false
let options = devOptions.getOptions()
if (!options.status) {
console.error('日志上报失败dev工具未启用 msg:' + msg)
return
}
if (!options.logs.status) {
console.error('日志上报失败dev logs未启用 msg:' + msg)
return
}
try {
let pages = getCurrentPages()
if (pages[pages.length - 1].route == options.devRoute) {
// 不记录调试工具报出的日志
return false
}
} catch (error) {}
if (typeof msg == 'object') {
try {
msg = JSON.stringify(msg)
} catch (error) {
msg = 'logReport:error'
}
}
let log = {
t: new Date().getTime(),
m: '',
}
let logSize = jsonCompress.calculateStringByteSize(log)
msg = String(msg)
msg = jsonCompress.compressObject(msg, options.logs.cache.rowSize - logSize)
log.m = msg
let logs = devCache.get('logReport')
if (!logs) logs = []
logs.unshift(log)
logs = jsonCompress.compressArray(logs, 'end', options.logs.cache.size)
devCache.set('logReport', logs)
} catch (error) {
console.log('logReport error', error)
}
}
export default logReport

View File

@ -0,0 +1,124 @@
import devCache from './devCache'
export default {
pageRouteMap: [],
pageRouteKeyMap: {},
/**
* 安装路径分析插件
*/
install() {
let allRoutes = this.getAllRoutes()
let pageRouteKeyMap = devCache.get('pageRouteKeyMap')
if (!pageRouteKeyMap || typeof pageRouteKeyMap != 'object') {
pageRouteKeyMap = {}
}
let lastNo = 0
Object.keys(pageRouteKeyMap).forEach((key) => {
let item = Number(pageRouteKeyMap[key])
if (item > lastNo) {
lastNo = item
}
})
allRoutes.forEach((item) => {
if (!pageRouteKeyMap[item.path]) {
pageRouteKeyMap[item.path] = lastNo + 1
lastNo = lastNo + 1
}
})
devCache.set('pageRouteKeyMap', pageRouteKeyMap)
this.pageRouteKeyMap = pageRouteKeyMap
let pageRouteMap = devCache.get('pageRouteMap')
if (!pageRouteMap || typeof pageRouteMap != 'object') {
pageRouteMap = {}
}
Object.keys(pageRouteMap).forEach((key) => {
try {
let n = Number(pageRouteMap[key])
if (!Number.isInteger(n) || n < 0) {
pageRouteMap[key] = 1
}
} catch (error) {}
})
this.pageRouteMap = pageRouteMap
this.saveData()
},
/**
* 获取APP注册的所有路由
* @returns {{path: string}[]} 返回路由列表
*/
getAllRoutes() {
let pages = []
// #ifdef H5 || APP-PLUS
try {
__uniRoutes.map((item) => {
let path = item.alias ? item.alias : item.path
pages.push({ path })
})
} catch (error) {
pages = []
}
// #endif
// #ifdef MP-WEIXIN
try {
let wxPages = __wxConfig.pages
wxPages.map((item) => {
pages.push({
path: '/' + item,
})
})
} catch (error) {
pages = []
}
// #endif
return pages
},
/**
* 写入路由列表
*/
pushPageRouteMap(list = []) {
if (!list || list.length == 0) {
list = getCurrentPages()
}
let key = ''
list.forEach((item) => {
let path = item.route.indexOf('/') == 0 ? item.route : '/' + item.route
let keyItem = this.pageRouteKeyMap[path]
if (!keyItem) {
keyItem = path
}
if (key == '') {
key = keyItem + ''
} else {
key = key + ',' + keyItem
}
})
if (this.pageRouteMap[key]) {
this.pageRouteMap[key] = this.pageRouteMap[key] + 1
} else {
this.pageRouteMap[key] = 1
}
},
/**
* 保存路由缓存
*/
saveData() {
let that = this
setTimeout(
() => {
devCache.set('pageRouteMap', that.pageRouteMap)
setTimeout(() => {
that.saveData()
}, 500)
},
Math.round(Math.random() * 3 * 1000) + 2000,
)
},
}

View File

@ -0,0 +1,71 @@
/**
* !页面统计访问次数停留时长
*/
import devCache from './devCache'
import devOptions from './devOptions'
import jsonCompress from './jsonCompress'
import { timeFormat } from './timeFormat'
/**
* 页面注销时提交
*/
function pageStatisticsReport(route, activeTime) {
try {
if (!route) return false
let options = devOptions.getOptions()
if (!options.pageStatistics.status) return //! 配置文件关闭页面统计
let logs = devCache.get('pageCount')
if (!logs) logs = []
let pageIndex = logs.findIndex((x) => x.route == route)
if (pageIndex == -1) {
logs.push({
route,
activeTimeCount: activeTime,
})
} else {
logs[pageIndex].activeTimeCount = activeTime + logs[pageIndex].activeTimeCount
}
logs = jsonCompress.compressArray(logs, 'end', options.pageStatistics.size)
devCache.set('pageCount', logs)
let now = new Date().getTime()
let date = timeFormat(now, 'yyyy-mm-dd')
let dayOnline = devCache.get('dayOnline')
if (!dayOnline) dayOnline = []
let i = dayOnline.findIndex((x) => x.date == date)
if (i == -1) {
dayOnline.unshift({
date,
activeTimeCount: activeTime,
page: [
{
r: route,
t: activeTime,
},
],
})
} else {
dayOnline[i].activeTimeCount = dayOnline[i].activeTimeCount + activeTime
let pi = dayOnline[i].page.findIndex((x) => x.r == route)
if (pi == -1) {
dayOnline[i].page.push({
r: route,
t: activeTime,
})
} else {
dayOnline[i].page[pi].t = dayOnline[i].page[pi].t + activeTime
}
}
if (dayOnline.length > options.pageStatistics.dayOnlineRowMax) {
dayOnline = dayOnline.splice(0, options.pageStatistics.dayOnlineRowMax)
}
dayOnline = jsonCompress.compressArray(dayOnline, 'end', options.pageStatistics.size)
devCache.set('dayOnline', dayOnline)
} catch (error) {
console.log('pageStatistics error', error)
}
}
export default pageStatisticsReport

View File

@ -0,0 +1,94 @@
// padStart 的 polyfill因为某些机型或情况还无法支持es7的padStart比如电脑版的微信小程序
// 所以这里做一个兼容polyfill的兼容处理
try {
if (!String.prototype.padStart) {
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
String.prototype.padStart = function (maxLength, fillString = ' ') {
if (Object.prototype.toString.call(fillString) !== '[object String]')
throw new TypeError('fillString must be String')
let str = this
// 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉
if (str.length >= maxLength) return String(str)
let fillLength = maxLength - str.length,
times = Math.ceil(fillLength / fillString.length)
while ((times >>= 1)) {
fillString += fillString
if (times === 1) {
fillString += fillString
}
}
return fillString.slice(0, fillLength) + str
}
}
} catch (error) {
console.log('timeFormat fillString error', error)
}
// 其他更多是格式化有如下:
// yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合
export function timeFormat(dateTime = null, fmt = 'yyyy-mm-dd hh:MM:ss') {
try {
// 如果为null,则格式化当前时间
if (!dateTime) dateTime = Number(new Date())
// 如果dateTime长度为10或者13则为秒和毫秒的时间戳如果超过13位则为其他的时间格式
if (dateTime.toString().length == 10) dateTime *= 1000
let date = new Date(dateTime)
let ret
let opt = {
'y+': date.getFullYear().toString(), // 年
'm+': (date.getMonth() + 1).toString(), // 月
'd+': date.getDate().toString(), // 日
'h+': date.getHours().toString(), // 时
'M+': date.getMinutes().toString(), // 分
's+': date.getSeconds().toString(), // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
}
for (let k in opt) {
ret = new RegExp('(' + k + ')').exec(fmt)
if (ret) {
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'))
}
}
return fmt
} catch (error) {
console.log('timeFormat error', error)
return 'unknown error'
}
}
export function timeFromNow(timestamp) {
try {
const now = new Date().getTime()
let diff = timestamp - now
// 确定是过去还是未来
const suffix = diff > 0 ? '后' : '前'
diff = Math.abs(diff)
// 计算时间差异
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
const months = Math.floor(days / 30)
const years = Math.floor(days / 365)
// 根据时间差异返回相应的字符串
if (seconds < 60) {
return `${seconds}${suffix}`
} else if (minutes < 60) {
return `${minutes}分钟${suffix}`
} else if (hours < 24) {
return `${hours}小时${suffix}`
} else if (days < 30) {
return `${days}${suffix}`
} else if (months < 12) {
return `${months}个月${suffix}`
} else {
return `${years}${suffix}`
}
} catch (error) {
console.log('timeFromNow error', error)
}
}

View File

@ -0,0 +1,413 @@
import devCache from '../libs/devCache'
import devOptions from '../libs/devOptions'
import jsonCompress from '../libs/jsonCompress'
export default {
logList: [],
options: null,
/**
* 挂载打印拦截器
*/
install() {
let that = this
this.options = devOptions.getOptions()
if (!this.options.console.status) return
this.logList = devCache.get('console')
if (!this.logList) this.logList = []
this.syncReqData() //同步缓存
if (uni.__log__) {
// ! VUE3在app端时有这个特殊的方法
that.mountUniConsole()
} else {
that.mountJsConsole()
}
//! 删除指定记录
uni.$on('devTools_delConsoleItem', (item) => {
let i = that.logList.findIndex((x) => {
let t = JSON.stringify(x.list)
return (
t == JSON.stringify(item.list) &&
x.time == item.time &&
x.page == item.page &&
x.type == item.type
)
})
if (i != -1) {
that.logList.splice(i, 1)
}
that.saveData()
})
//! 清空console日志
uni.$on('devTools_delConsoleAll', () => {
that.logList = []
that.saveData()
})
},
/**
* 同步请求信息到缓存数据中
*/
syncReqData() {
let that = this
setTimeout(() => {
try {
that.saveData()
} catch (error) {
console.log('console.syncReqData error', error)
}
that.syncReqData()
}, 3000)
},
/**
* 同步数据到缓存
*/
saveData() {
let that = this
that.logList = jsonCompress.compressArray(that.logList, 'end', that.options.console.cache.size)
devCache.set('console', that.logList)
},
/**
* 挂载监听js自带的console函数
*/
mountJsConsole() {
let that = this
try {
let l = console.log
try {
globalThis.consoleLog = function () {
console.log(...arguments)
}
} catch (error) {}
try {
window.consoleLog = function () {
console.log(...arguments)
}
} catch (error) {}
console.log = function () {
replaceConsole('log', arguments)
}
let e = console.error
function _error() {
try {
let args = [...arguments]
if (
args[0] &&
typeof args[0] == 'string' &&
(args[0] == '__ignoreReport__' || //! 忽略错误日志上报
args[0].indexOf('__ignoreReport__') == 0)
) {
let _args = []
if (args.length > 1) {
for (let i = 0; i < args.length; i++) {
if (i != 0) {
_args.push(args[i])
}
}
} else {
_args[0] = args[0]
_args[0] = _args[0].replace('__ignoreReport__', '')
}
if (that.options.console.isOutput) {
e(..._args)
}
return
}
replaceConsole('error', args)
} catch (error) {
e('监听console.error出错', error)
}
}
console.error = _error
let w = console.warn
console.warn = function () {
replaceConsole('warn', arguments)
}
let i = console.info
console.info = function () {
replaceConsole('info', arguments)
}
/**
* 替换系统打印函数
*/
function replaceConsole(type, args) {
try {
let data = []
if (args && args.length > 0) {
let argList = args
// #ifdef APP-PLUS
if (args.length == 1) {
argList = args[0].split('---COMMA---')
let endItem = argList[argList.length - 1]
if (
endItem &&
typeof endItem == 'string' &&
endItem.indexOf(' at ') > -1 &&
endItem.indexOf(':') > -1
) {
// 可能包含路径信息
let endList = endItem.split(' at ')
if (endList.length == 2) {
argList.pop()
argList.push(endList[0])
argList.push('at ' + endList[1])
}
}
argList = argList.map((item, index) => {
try {
if (typeof item == 'string') {
if (item.indexOf('---BEGIN') > -1) {
let isJson = item.indexOf('---BEGIN:JSON---') > -1
item = item.replace(/---BEGIN:.*?---/g, '')
item = item.replace(/---END:.*?---/g, '')
if (isJson) {
item = JSON.parse(item)
}
} else if (item == '---NULL---') {
item = null
} else if (item == '---UNDEFINED---') {
item = undefined
}
}
} catch (error) {
console.log('replaceConsole 尝试解析对象出错:', error)
}
return item
})
}
// #endif
let oneSize = that.options.console.cache.rowSize / argList.length
for (let i = 0; i < argList.length; i++) {
let row = jsonCompress.compressObject(argList[i], oneSize)
data.push(row)
}
} else {
data = []
}
let page = '未知'
try {
let pages = getCurrentPages()
let item = pages[pages.length - 1]
if (item && item.route) {
page = item.route
}
} catch (error) {}
that.logList.unshift({
list: data,
time: new Date().getTime(),
page,
type,
})
if (that.options.console.isOutput) {
if (type == 'error') {
e(...args)
} else if (type == 'warn') {
w(...args)
} else if (type == 'info') {
i(...args)
} else {
l(...args)
}
}
} catch (error) {
if (that.options.console.isOutput) {
e('监听console出错', error)
}
}
}
} catch (error) {
console.log('console.install error', error)
}
},
/**
* 挂载监听uni自带的打印函数
*/
mountUniConsole() {
let that = this
try {
let uniSysConsole = uni.__log__
try {
globalThis.consoleLog = function () {
uni.__log__('log', '未知来源', ...arguments)
}
} catch (error) {}
try {
window.consoleLog = function () {
uni.__log__('log', '未知来源', ...arguments)
}
} catch (error) {}
uni.__log__ = function (type, line, ...args) {
try {
// 处理特殊情况 "__ignoreReport__" 忽略错误日志上报
if (type == 'error') {
if (
args[0] &&
typeof args[0] == 'string' &&
(args[0] == '__ignoreReport__' || //! 忽略错误日志上报
args[0].indexOf('__ignoreReport__') == 0)
) {
let _args = []
if (args.length > 1) {
for (let i = 0; i < args.length; i++) {
if (i != 0) {
_args.push(args[i])
}
}
} else {
_args[0] = args[0]
_args[0] = _args[0].replace('__ignoreReport__', '')
}
if (that.options.console.isOutput) {
uniSysConsole(type, line, ..._args)
}
return
}
}
let data = []
if (args && args.length > 0) {
let argList = args
let oneSize = that.options.console.cache.rowSize / argList.length
for (let i = 0; i < argList.length; i++) {
let row = jsonCompress.compressObject(argList[i], oneSize)
data.push(row)
}
} else {
data = []
}
let page = '未知'
try {
let pages = getCurrentPages()
let item = pages[pages.length - 1]
if (item && item.route) {
page = item.route
}
} catch (error) {}
data.push(line)
that.logList.unshift({
list: data,
time: new Date().getTime(),
page,
type,
})
if (that.options.console.isOutput) {
uniSysConsole(type, line, ...data)
}
} catch (error) {
if (that.options.console.isOutput) {
uniSysConsole('error', '监听console出错', error)
}
}
}
/**
* 替换系统打印函数
*/
function replaceConsole(type, args) {
try {
let data = []
if (args && args.length > 0) {
let argList = args
// #ifdef APP-PLUS
if (args.length == 1) {
argList = args[0].split('---COMMA---')
let endItem = argList[argList.length - 1]
if (
endItem &&
typeof endItem == 'string' &&
endItem.indexOf(' at ') > -1 &&
endItem.indexOf(':') > -1
) {
// 可能包含路径信息
let endList = endItem.split(' at ')
if (endList.length == 2) {
argList.pop()
argList.push(endList[0])
argList.push('at ' + endList[1])
}
}
argList = argList.map((item, index) => {
try {
if (typeof item == 'string') {
if (item.indexOf('---BEGIN') > -1) {
let isJson = item.indexOf('---BEGIN:JSON---') > -1
item = item.replace(/---BEGIN:.*?---/g, '')
item = item.replace(/---END:.*?---/g, '')
if (isJson) {
item = JSON.parse(item)
}
} else if (item == '---NULL---') {
item = null
} else if (item == '---UNDEFINED---') {
item = undefined
}
}
} catch (error) {
console.log('replaceConsole 尝试解析对象出错:', error)
}
return item
})
}
// #endif
let oneSize = that.options.console.cache.rowSize / argList.length
for (let i = 0; i < argList.length; i++) {
let row = jsonCompress.compressObject(argList[i], oneSize)
data.push(row)
}
} else {
data = []
}
let page = '未知'
try {
let pages = getCurrentPages()
let item = pages[pages.length - 1]
if (item && item.route) {
page = item.route
}
} catch (error) {}
that.logList.unshift({
list: data,
time: new Date().getTime(),
page,
type,
})
if (that.options.console.isOutput) {
if (type == 'error') {
e(...args)
} else if (type == 'warn') {
w(...args)
} else if (type == 'info') {
i(...args)
} else {
l(...args)
}
}
} catch (error) {
if (that.options.console.isOutput) {
e('监听console出错', error)
}
}
}
} catch (error) {
console.log('console.install error', error)
}
},
}

View File

@ -0,0 +1,33 @@
import devCache from '../libs/devCache'
import console from './console'
import request from './request'
import storage from './storage'
import uniBus from './uniBus'
import uniListen from './uniListen'
/**
* dev调试工具初始化
*/
export default function devToolsProxyInstall(options) {
try {
if (options.network && options.network.status) {
request.install()
}
if (options.console && options.console.status) {
console.install()
}
if (options.logs && options.logs.status) {
uniListen.install()
}
storage.install()
if (options.uniBus && options.uniBus.status) {
uniBus.install()
}
devCache.syncLocalCache()
} catch (error) {
console.log('devToolsProxyInstall error', error)
}
}

View File

@ -0,0 +1,246 @@
import devCache from '../libs/devCache'
import devOptions from '../libs/devOptions'
import jsonCompress from '../libs/jsonCompress'
export default {
/**
* 请求日志示例
*/
ajaxLogData: {
id: 0, //请求id
type: 0, // 0发起请求中 1请求成功 2请求失败
sendTime: 0, //发送请求的时间
responseTime: 0, //响应时间
useTime: 0, //请求总耗时
url: '', //请求地址
header: '', //请求头
method: 'get', //请求方式
data: '', //请求参数
responseBody: '', //响应主体
responseHeader: '', //响应头
responseStatus: '', //响应编码
responseMsg: '', //响应报错信息
},
options: null,
/**
* 请求的数据列表
*/
ajaxData: [],
/**
* 挂载请求拦截器
*/
install() {
let that = this
try {
this.options = devOptions.getOptions()
if (!this.options.network.status) return
this.ajaxData = devCache.get('request')
if (!this.ajaxData) this.ajaxData = []
this.syncReqData() //同步缓存
uni.addInterceptor('request', {
/**
* 入参
*/
invoke(args) {
try {
args._id_ =
new Date().getTime() + '_' + Number(Math.random().toString().replace('0.', ''))
let copyData = JSON.parse(JSON.stringify(that.ajaxLogData))
copyData.id = args._id_
copyData.sendTime = new Date().getTime()
copyData.url = that.dataCopy(args.url)
copyData.header = that.dataCopy(args.header)
if (!args.method) {
copyData.method = 'get'
} else {
copyData.method = that.dataCopy(args.method)
}
let cSize = jsonCompress.calculateStringByteSize(copyData)
if (cSize > that.options.network.cache.rowSize) {
copyData = jsonCompress.compressObject(copyData, that.options.network.cache.rowSize)
} else {
let data = jsonCompress.compressObject(
args.data,
that.options.network.cache.rowSize - cSize,
)
try {
data = JSON.parse(data)
} catch (error) {}
copyData.data = data
}
that.ajaxData.unshift(copyData)
} catch (error) {
console.error('request拦截器invoke出错', error)
}
},
success(response, request) {
return new Promise(async (yes, err) => {
//! 延迟请求返回,模拟弱网环境
let speedLimit = uni.getStorageSync('devtools_uniResLimitType')
if (speedLimit) {
let delayDuration = {
'2g': [30, 60],
'3g-': [10, 30],
'3g': [3, 10],
'4g': [0.5, 3],
}
let sleepParam = delayDuration[speedLimit]
if (sleepParam) {
let sleepTime = rNum(sleepParam[0], sleepParam[1])
await sleep(sleepTime * 1000)
response.errMsg = response.errMsg + ` | [devtools模拟弱网延迟:${sleepTime}s]`
}
}
// ! 随机响应失败概率
let resTimeout = uni.getStorageSync('devtools_uniResTimeout')
let isFail = false
if (resTimeout && resTimeout > 1) {
let targetPro = Number(resTimeout)
let randPro = rNum(0, 100)
if (randPro < targetPro) {
// 命中失败
response.statusCode = rNum(400, 600) //生成随机400 ~ 600之间的状态码
response.errMsg =
response.errMsg +
` | [devtools随机超时报错当前命中的概率阶层为:${targetPro}%,生成的随机数为:${randPro}]`
response.data = '[devTools]模拟请求失败结果!'
isFail = true
}
}
// ! 记录响应内容
try {
let item = that.ajaxData.find((x) => x.id == request._id_)
if (!item) return
item.responseBodySize = jsonCompress.calculateStringByteSize(response.data)
item.responseMsg = response.errMsg
item.responseStatus = response.statusCode
item.responseHeader = response.header
item.type = 1
item.responseTime = new Date().getTime()
item.useTime = ((item.responseTime - item.sendTime) / 1000).toFixed(3)
let size = jsonCompress.calculateStringByteSize(item)
if (size > that.options.network.cache.rowSize) {
item.responseBody = '[内容太长已截断多余部分]'
let data = jsonCompress.compressObject(item, that.options.network.cache.rowSize)
that.ajaxData[that.ajaxData.findIndex((x) => x.id == request._id_)] = data
} else {
let json = response.data
try {
json = JSON.parse(JSON.stringify(json))
} catch (error) {}
item.responseBody = jsonCompress.compressObject(
json,
that.options.network.cache.rowSize - size,
)
}
} catch (error) {
console.error('request拦截器success出错', error)
}
if (isFail) {
err(response.data)
} else {
yes(response)
}
})
},
fail(err, request) {
try {
let item = that.ajaxData.find((x) => x.id == request._id_)
if (!item) return
item.type = 2
item.responseTime = new Date().getTime()
item.useTime = ((item.responseTime - item.sendTime) / 1000).toFixed(3)
item.responseMsg = err.errMsg
} catch (error) {
console.error('request拦截器fail出错', error)
}
},
complete(res) {},
})
// ! 删除指定请求记录
uni.$on('devTools_delNetworkItemById', (id) => {
let i = this.ajaxData.findIndex((x) => x.id == id)
if (i != -1) {
this.ajaxData.splice(i, 1)
}
this.saveData()
})
// ! 清空请求记录
uni.$on('devTools_delNetworkAll', () => {
this.ajaxData = []
this.saveData()
})
} catch (error) {
console.log('request.install error', error)
}
},
/**
* 同步请求信息到缓存数据中
*/
syncReqData() {
let that = this
setTimeout(() => {
try {
that.saveData()
} catch (error) {
console.log('request.syncReqData', error)
}
that.syncReqData()
}, 4000)
},
/**
* 保存数据到缓存中
*/
saveData() {
let that = this
that.ajaxData = jsonCompress.compressArray(that.ajaxData, that.options.network.cache.size)
devCache.set('request', that.ajaxData)
},
/**
* 复制对象
*/
dataCopy(data) {
try {
if (typeof data == 'object') {
return JSON.parse(JSON.stringify([data]))[0]
} else {
return data
}
} catch (error) {
console.log('request.dataCopy', error)
return ''
}
},
}
/**
* 随机生成n~m的数支持两位小数
*/
function rNum(n, m) {
return Number((Math.random() * (m - n) + n).toFixed(2))
}
/**
* 休眠指定时长
*/
function sleep(t) {
return new Promise((y) => {
setTimeout(y, t)
})
}

View File

@ -0,0 +1,96 @@
import devCache from '../libs/devCache'
export default {
/**
* 挂载缓存监听
*/
install() {
try {
// #ifdef MP
let that = this
let _setStorage = uni.setStorage
uni.setStorage = setStorage
function setStorage() {
try {
if (arguments[0] && arguments[0].key && arguments[0].key.indexOf('devTools_') != 0) {
that.addCacheKey(arguments[0].key)
}
} catch (error) {}
return _setStorage(...arguments)
}
let _setStorageSync = uni.setStorageSync
uni.setStorageSync = setStorageSync
function setStorageSync() {
try {
if (arguments[0] && arguments[0].indexOf('devTools_') != 0) {
that.addCacheKey(arguments[0])
}
} catch (error) {}
return _setStorageSync(...arguments)
}
let _removeStorage = uni.removeStorage
uni.removeStorage = removeStorage
function removeStorage() {
try {
if (arguments[0] && arguments[0].key && arguments[0].key.indexOf('devTools_') != 0) {
that.delCacheKey(arguments[0].key)
}
} catch (error) {}
return _removeStorage(...arguments)
}
let _removeStorageSync = uni.removeStorageSync
uni.removeStorageSync = removeStorageSync
function removeStorageSync() {
try {
if (arguments[0] && arguments[0].indexOf('devTools_') != 0) {
that.delCacheKey(arguments[0])
}
} catch (error) {}
return _removeStorageSync(...arguments)
}
// #endif
} catch (error) {
console.log('devTools storage.install error', error)
}
},
/**
* 添加缓存key
*/
addCacheKey(key) {
try {
if (key && typeof key == 'string') {
let storageList = devCache.get('storage')
if (!storageList) storageList = []
if (storageList.indexOf(key) == -1) {
storageList.push(key)
devCache.set('storage', storageList)
}
}
} catch (error) {
console.log('devTools storage.addCacheKey error', error)
}
},
/**
* 删除指定缓存key
*/
delCacheKey(key) {
try {
if (key && typeof key == 'string') {
let storageList = devCache.get('storage')
if (!storageList) storageList = []
let index = storageList.indexOf(key)
if (index > -1) {
storageList.splice(index, 1)
devCache.set('storage', storageList)
}
}
} catch (error) {
console.log('devTools storage.delCacheKey error', error)
}
},
}

View File

@ -0,0 +1,160 @@
import devCache from '../libs/devCache'
import devOptions from '../libs/devOptions'
import jsonCompress from '../libs/jsonCompress'
export default {
logList: [],
busCount: [],
options: null,
/**
* 挂载打印拦截器
*/
install() {
try {
let that = this
this.options = devOptions.getOptions()
if (!this.options.uniBus.status) return
this.logList = devCache.get('uniBus')
if (!this.logList) this.logList = []
this.busCount = devCache.get('busCount')
if (!this.busCount) this.busCount = []
this.syncReqData() //同步缓存
let now = () => new Date().getTime()
const _on = uni.$on
uni.$on = function () {
try {
let n = arguments[0]
if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) {
that.logList.unshift({
t: now(),
e: jsonCompress.compressObject(`on>${n}`, that.options.uniBus.cache.rowMax),
})
addCount(n, 'on')
}
} catch (error) {
console.error('uni.$on出错', error)
}
_on(...arguments)
}
const _once = uni.$once
uni.$once = function () {
try {
let n = arguments[0]
if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) {
that.logList.unshift({
t: now(),
e: jsonCompress.compressObject(`once>${n}`, that.options.uniBus.cache.rowMax),
})
addCount(n, 'once')
}
} catch (error) {
console.error('uni.$once出错', error)
}
_once(...arguments)
}
const _emit = uni.$emit
uni.$emit = function () {
try {
let n = arguments[0]
let p = arguments[1]
if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) {
that.logList.unshift({
t: now(),
e: jsonCompress.compressObject(
`emit>${n}` + (p ? '>' + JSON.stringify(p) : ''),
that.options.uniBus.cache.rowMax,
),
})
addCount(n, 'emit')
}
} catch (error) {
console.error('uni.$emit出错', error)
}
_emit(...arguments)
}
const _off = uni.$off
uni.$off = function () {
try {
let n = arguments[0]
if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) {
that.logList.unshift({
t: now(),
e: jsonCompress.compressObject(
`off>${n}` + arguments[0],
that.options.uniBus.cache.rowMax,
),
})
addCount(n, 'off')
}
} catch (error) {
console.error('uni.$off出错', error)
}
_off(...arguments)
}
/**
* 统计总次数
*/
function addCount(name, type = 'on') {
let i = that.busCount.findIndex((x) => x.e == name)
if (i == -1) {
let item = {
e: name,
on: 0,
off: 0,
emit: 0,
once: 0,
}
item[type] = item[type] + 1
that.busCount.push(item)
} else {
that.busCount[i][type] = that.busCount[i][type] + 1
}
}
// ! 清空全部记录
uni.$on('devTools_delUniBusAll', () => {
that.logList = []
that.busCount = []
})
} catch (error) {
console.log('devTools uniBus.install error', error)
}
},
/**
* 同步请求信息到缓存数据中
*/
syncReqData() {
let that = this
setTimeout(() => {
try {
that.logList = jsonCompress.compressArray(
that.logList,
'end',
that.options.uniBus.cache.rowMax,
)
devCache.set('uniBus', that.logList)
that.busCount = jsonCompress.compressArray(
that.busCount,
'end',
that.options.uniBus.cache.countMaxSize,
)
devCache.set('busCount', that.busCount)
} catch (error) {
console.log('devTools uniBus.syncReqData error', error)
}
that.syncReqData()
}, 5000)
},
}

View File

@ -0,0 +1,195 @@
import logReport from '../libs/logReport'
export default {
/**
* 挂载uni大部分api监听器
*/
install() {
try {
this.addDefUniApiListen()
this.onNetworkStatusChange()
this.scanCodeListen()
this.onLocaleChange()
} catch (error) {
console.log('uniListen error', error)
}
},
/**
* 批量挂载api调用日志
*/
addDefUniApiListen() {
/**
* 需要挂载监听的api列表
*/
let diyListenApi = {
downloadFile(args) {
logReport('downloadFile>' + (args && args.url ? args.url : ''))
},
connectSocket(args) {
logReport('connectSocket>' + args.url)
},
makePhoneCall(args) {
logReport('makePhoneCall>' + args.phoneNumber)
},
addPhoneContact(args) {
logReport('addPhoneContact>' + args.name)
},
showToast(args) {
logReport('showToast>' + args.title)
},
showModal(args) {
logReport('showModal>' + args.title + '>' + args.content)
},
setLocale(args) {
logReport('setLocale>' + args)
},
saveFile(args) {
logReport('saveFile>' + args.tempFilePath)
},
login(args) {
logReport('login>' + JSON.stringify(args))
},
share(args) {
logReport('share>' + JSON.stringify(args))
},
shareWithSystem(args) {
logReport('shareWithSystem>' + JSON.stringify(args))
},
requestPayment(args) {
logReport('requestPayment>' + JSON.stringify(args))
},
authorize(args) {
logReport('requestPayment>' + args.scope)
},
navigateToMiniProgram(args) {
logReport('navigateToMiniProgram>' + args.appId + '>' + args.path)
},
openDocument(args) {
logReport('openDocument>' + args.filePath)
},
}
/**
* 需要监听打印日志的api名称列表
*/
let waitListenApiNames = [
'uploadFile',
'closeSocket',
'getLocation',
'chooseLocation',
'openLocation',
'chooseImage',
'previewImage',
'saveImageToPhotosAlbum',
'chooseFile',
'chooseVideo',
'chooseMedia',
'saveVideoToPhotosAlbum',
'openVideoEditor',
'openAppAuthorizeSetting',
'startAccelerometer',
'startCompass',
'startGyroscope',
'setScreenBrightness',
'vibrate',
'vibrateLong',
'vibrateShort',
'openBluetoothAdapter',
'startBeaconDiscovery',
'startSoterAuthentication',
'hideKeyboard',
'showActionSheet',
'startPullDownRefresh',
'showShareMenu',
'startFacialRecognitionVerify',
'openSetting',
'chooseAddress',
'chooseInvoiceTitle',
'openEmbeddedMiniProgram',
]
for (const key in diyListenApi) {
uni.addInterceptor(key, {
invoke(_args) {
try {
diyListenApi[key](_args)
} catch (error) {
console.error('addInterceptor=>' + key, error)
}
},
})
}
waitListenApiNames.map((key) => {
uni.addInterceptor(key, {
invoke(args) {
try {
logReport(key)
} catch (error) {
console.error('addInterceptor>' + key, error)
}
},
})
})
},
/**
* 添加网络状态监听
*/
onNetworkStatusChange() {
uni.onNetworkStatusChange((res) => {
try {
logReport(
'onNetworkStatusChange>isConnected:' +
(res.isConnected ? 'true' : 'false') +
'>networkType:' +
res.networkType,
)
} catch (error) {
console.log('onNetworkStatusChange', error)
}
})
},
/**
* 添加系统主题切换监听
*/
onThemeChange() {
uni.onThemeChange((res) => {
try {
logReport('onThemeChange>' + res.theme)
} catch (error) {
console.log('onThemeChange', error)
}
})
},
/**
* 监听扫码结果
*/
scanCodeListen() {
uni.addInterceptor('scanCode', {
success(res) {
try {
logReport(
'scanCodeSuccess>' +
JSON.stringify({
scanType: res.scanType,
result: res.result,
}),
)
} catch (error) {
console.log('scanCode', error)
}
},
})
},
/**
* 监听系统语言切换
*/
onLocaleChange() {
uni.onLocaleChange((locale) => {
try {
logReport('onLocaleChange>' + locale)
} catch (error) {
console.log('onLocaleChange', error)
}
})
},
}

View File

@ -0,0 +1,160 @@
import devOptions from '../libs/devOptions'
import logReport from '../libs/logReport'
import pageLinkList from '../libs/pageLinkList'
import pageStatisticsReport from '../libs/pageStatistics'
/**
* ! Vue页面混入监听生命周期
*/
export default {
data() {
return {
/**
* 挂载dev页面对象
*/
devTools_pageData: {
route: '', // 页面路径
isOnShow: false, // 是否处于展示状态
activeTime: 0, //活跃时间
},
}
},
/**
* *页面载入事件
*/
onLoad(pageInitParams) {
let that = this
// ! 注入 Eruda
let isInjectEruda = uni.getStorageSync('devTools_isInjectEruda') == 'yes'
if (isInjectEruda) {
let ErudaCode = `
if(!window.isInjectEruda){
window.isInjectEruda = true;
var script = document.createElement('script');
script.src="https://cdn.jsdelivr.net/npm/eruda";
document.body.append(script);
script.onload = function () {
eruda.init();
}
}
`
let fun = 'e' + ['v'][0] + 'a' + ['l'][0]
try {
// #ifdef H5
window[fun](ErudaCode)
// #endif
// #ifdef APP-PLUS
let endPageWebView = getCurrentPages().pop()
if (endPageWebView) {
let nowPageWebview = endPageWebView.$getAppWebview()
if (nowPageWebview && !nowPageWebview.nvue) {
nowPageWebview[fun + 'JS'](ErudaCode)
}
}
// #endif
} catch (error) {
console.log('devTools mixin onLoad injectEruda error ', error)
}
}
// ! 注入 vConsole
let isInjectVConsole = uni.getStorageSync('devTools_isInjectVConsole') == 'yes'
if (isInjectVConsole) {
let vConsoleCode = `
if(!window.isInjectVConsole){
window.isInjectVConsole = true;
var script = document.createElement('script');
script.src="https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js";
document.body.append(script);
script.onload = function () {
let vConsoleObj = new window.VConsole();
}
}
`
let fun = 'e' + ['v'][0] + 'a' + ['l'][0]
try {
// #ifdef H5
window[fun](vConsoleCode)
// #endif
// #ifdef APP-PLUS
let endPageWebView = getCurrentPages().pop()
if (endPageWebView) {
let nowPageWebview = endPageWebView.$getAppWebview()
if (nowPageWebview && !nowPageWebview.nvue) {
nowPageWebview[fun + 'JS'](vConsoleCode)
}
}
// #endif
} catch (error) {
console.log('devTools mixin onLoad injectVConsole error ', error)
}
}
try {
let pages = getCurrentPages()
let pageItem = pages && pages.length > 0 ? pages[pages.length - 1] : null
if (pageItem) {
let devSetting = devOptions.getOptions()
if (pageItem.route == devSetting.devRoute) {
that.devTools_pageData = false
} else {
that.devTools_pageData.route = pageItem.route
logReport(
`onLoad>${pageItem.route}>` + (pageInitParams ? JSON.stringify(pageInitParams) : ''),
)
setInterval(() => {
if (that.devTools_pageData && that.devTools_pageData.isOnShow) {
that.devTools_pageData.activeTime = that.devTools_pageData.activeTime + 1
}
}, 1000)
}
}
pageLinkList.pushPageRouteMap(pages)
} catch (error) {
console.log('devTools mixin onLoad error ', error)
}
},
/**
* *页面展示事件
*/
onShow() {
try {
let that = this
if (that.devTools_pageData) {
that.devTools_pageData.isOnShow = true
that.devTools_pageData.activeTime = 0
}
} catch (error) {
console.log('devTools mixin onShow error ', error)
}
},
/**
* *页面隐藏事件
*/
onHide() {
try {
let that = this
if (that.devTools_pageData) {
that.devTools_pageData.isOnShow = false
pageStatisticsReport(that.devTools_pageData.route, that.devTools_pageData.activeTime)
that.devTools_pageData.activeTime = 0
}
} catch (error) {
console.log('devTools mixin onHide error ', error)
}
},
/**
* * 页面卸载事件
*/
onUnload() {
try {
let that = this
logReport(`onUnload>${that.devTools_pageData.route}`)
that.devTools_pageData = null
} catch (error) {
console.log('devTools mixin onUnload error ', error)
}
},
}

177
src/devTools/index.js Normal file
View File

@ -0,0 +1,177 @@
import drawView from './core/libs/drawView'
import logReport from './core/libs/logReport'
import errorReport from './core/libs/errorReport'
import devOptions from './core/libs/devOptions'
import createH5Bubble from './core/libs/createH5Bubble'
import vueMixin from './core/proxy/vueMixin'
import devToolsProxyInstall from './core/proxy/index'
import pageLinkList from './core/libs/pageLinkList'
/**
* @type {Vue}
*/
let that
const devTools = {
options: null,
/**
* 挂载安装APP页面
*/
install(vm, options) {
try {
that = vm
let _this = this
if (vm && vm.config && vm.config.globalProperties) {
vm.config.globalProperties.$logReport = logReport
} else {
vm.prototype.$logReport = logReport
}
//! 初始化配置项
devOptions.setOptions(options)
options = devOptions.getOptions()
_this.options = options
if (!options || !options.status) {
return console.log(
'%c devTools 调试工具未运行!',
'padding: 4px;background-color: red;color: #fff;font-size: 15px;',
)
}
//! 挂载dev工具
if (vm && vm.config && vm.config.globalProperties) {
vm.config.globalProperties.$devTools = devTools
} else {
vm.prototype.$devTools = devTools
}
if (options.error.status) {
//! 挂载vue报错
vm.config.errorHandler = (err, vm, trace) => {
errorReport(err, trace, 've')
}
//! 挂载vue警告
vm.config.warnHandler = (err, vm, trace) => {
errorReport(err, trace, 'vw')
}
}
//!混入生命周期监听器
vm.mixin(vueMixin)
//!绘制环境变量小标签
// #ifdef APP-PLUS
drawView(options, devTools)
// #endif
// #ifdef H5
createH5Bubble(options, devTools)
// #endif
//!调试工具全局拦截器挂载
devToolsProxyInstall(options)
//! 注册dev弹窗打开事件
uni.$on('devTools_showDialog', () => {
_this.show()
})
//! 注册dev弹窗关闭事件
uni.$on('devTools_closeDialog', (options) => {
_this.hide(options)
})
//! 挂载uni对象
uni.$dev = {
show() {
_this.show()
},
hide() {
_this.hide()
},
errorReport,
logReport,
}
//! 注册jsRunner执行事件
uni.$on('devTools_jsRunner', (code) => {
let result = undefined
try {
let fun = ('ev' + '__混淆__' + 'al').replace('__混淆__', '')
result = globalThis[fun](code)
// result = eval(code);
} catch (error) {
if (error && error.message) {
result = error.message
}
}
uni.$emit('devTools_jsRunnerCallback', result)
})
// ! 页面路由列表
pageLinkList.install()
} catch (error) {
console.log('devTools install error', error)
}
},
/**
* 打开调试弹窗
*/
show() {
let pages = getCurrentPages()
//! 已经打开了调试工具,不要重复显示
if (pages[pages.length - 1].route == this.options.devRoute) {
return false
}
uni.navigateTo({
url: this.options.route,
animationType: 'none',
animationDuration: 0,
})
},
/**
* 隐藏调试弹窗
*/
hide(options) {
// #ifdef APP-PLUS
uni.$emit('devTools_closeDevToolsPanel')
let isBack = false
uni.$once('devTools_panelHideSuccess', () => {
if (!isBack) {
isBack = true
uni.navigateBack()
}
})
setTimeout(() => {
if (!isBack) {
isBack = true
uni.navigateBack()
}
}, 500)
// #endif
// #ifndef APP-PLUS
uni.navigateBack()
// #endif
if (options && options.navigateToUrl) {
let t = 600
// #ifndef APP-PLUS
t = 200
// #endif
setTimeout(() => {
uni.navigateTo({
url: options.navigateToUrl,
})
}, t)
}
},
errorReport,
logReport,
}
export default devTools

View File

@ -0,0 +1,914 @@
<template>
<view
v-if="isShow"
class="bottomTools"
:style="{
'padding-bottom': pb,
}"
>
<!-- Error -->
<template v-if="tabTitle == 'Error'">
<view class="miniBtn mr warn" @click="emptyLogs('error')">
<text class="miniBtnText">清空 x</text>
</view>
<btnTabs :list="errorTypeList" :value="errorTypeIndex" @indexChange="errorTypeIndexChange" />
</template>
<!-- Console -->
<template v-if="tabTitle == 'Console'">
<view class="miniBtn mr warn" @click="emptyLogs('console')">
<text class="miniBtnText">清空 x</text>
</view>
<btnTabs
:list="consoleTypeList"
:value="consoleTypeListIndex"
@indexChange="consoleTypeIndexChange"
/>
</template>
<!-- Network -->
<template v-if="tabTitle == 'Network'">
<view class="miniBtn mr warn" @click="emptyLogs('network')">
<text class="miniBtnText">清空 x</text>
</view>
<MenuBtn
:list="networkFilterType"
:value="networkTypeListIndex"
@indexChange="networkTypeIndexChange"
title="筛选:"
/>
<view class="mr"></view>
<RequestSpeedLimit />
<view class="mr"></view>
<RequestTimeoutMock />
</template>
<!-- Pages -->
<template v-if="tabTitle == 'Pages'">
<view class="miniBtn mr warn" @click="emptyLogs('pages_1')">
<text class="miniBtnText">清空停留统计</text>
</view>
<view class="miniBtn mr warn" @click="emptyLogs('pages_2')">
<text class="miniBtnText">清空日活统计</text>
</view>
<view class="miniBtn mr primary" @click="goPage">
<text class="miniBtnText">跳转页面</text>
</view>
</template>
<!-- Logs -->
<template v-if="tabTitle == 'Logs'">
<view class="miniBtn mr warn" @click="emptyLogs('logs')">
<text class="miniBtnText">清空 x</text>
</view>
</template>
<!-- Storage -->
<template v-if="tabTitle == 'Storage'">
<view class="miniBtn mr warn" @click="emptyLogs('storage')">
<text class="miniBtnText">清空 x</text>
</view>
<!-- #ifdef H5 -->
<btnTabs
:list="storageFilterTypeList"
:value="storageTypeListIndex"
@indexChange="storageTypeIndexChange"
/>
<!-- #endif -->
<view class="miniBtn primary ml" @click="addStorage">
<text class="miniBtnText">新增数据+</text>
</view>
</template>
<!-- UniBus -->
<template v-if="tabTitle == 'UniBus'">
<view class="miniBtn mr warn" @click="emptyLogs('UniBus')">
<text class="miniBtnText">清空 x</text>
</view>
<btnTabs :list="busFilterType" :value="busTypeListIndex" @indexChange="busTypeIndexChange" />
</template>
<!-- FileSys -->
<template v-if="tabTitle == 'FileSys'">
<!-- #ifdef APP-PLUS || MP-WEIXIN -->
<scroll-view scroll-x class="dirList">
<view class="dirScrollItem">
<text class="dirName" style="color: #999" @click="$emit('goChildDir', '_goIndex_0')">
{{ options.fileSysDirType }}
</text>
<text class="delimiter">/</text>
<view v-for="(item, index) in options.fileSysDirList" :key="index" class="dirItem">
<text v-if="index != 0" class="delimiter">/</text>
<text class="dirName" @click="$emit('goChildDir', '_goIndex_' + (index + 1))">
{{ item }}
</text>
</view>
</view>
</scroll-view>
<view class="miniBtn mr warn" @click="emptyFolder">
<text class="miniBtnText">清空 x</text>
</view>
<!-- #ifdef APP-PLUS -->
<btnTabs
:list="dirTypeList"
:value="fileTypeListIndex"
@indexChange="$emit('changeFileDirType', dirTypeList[$event].type)"
/>
<view style="width: 20rpx"></view>
<!-- #endif -->
<view class="miniBtn primary" @click="createDir">
<text class="miniBtnText">新建文件()</text>
</view>
<!-- #endif -->
</template>
<!-- JsRunner -->
<template v-if="tabTitle == 'JsRunner'">
<view class="jsRunnerTools">
<view class="runOptions">
<view class="radiusList">
<text class="runType">运行环境:</text>
<radio-group
@change="jsRunType = $event.detail.value"
class="radiusList"
style="display: flex; flex-direction: row"
>
<view
v-for="(item, index) in jsRunTypeList"
:key="index"
class="radiusItem"
@click="jsRunType = item"
>
<radio
class="radiusRadio"
:value="item"
:checked="jsRunType == item"
color="#ff2d55"
/>
<text
class="radiusText"
:style="{
color: jsRunType == item ? '#ff2d55' : '#333',
}"
>
{{ item }}
</text>
</view>
</radio-group>
</view>
<view class="hisEmpty" @click="$emit('emptyCodeHis')" v-if="options.codeHisLength > 0">
<image class="hisEmptyIcon" src="@/devTools/page/static/delete.png" />
<text class="hisEmptyText">清空记录</text>
</view>
<view class="logList" @click="showHisCode">
<text class="hisText">历史代码</text>
<image class="unfold" src="@/devTools/page/static/unfold.png" />
</view>
</view>
<view class="code">
<textarea
v-model="waitSendCode"
class="codeInput"
placeholder="js code ..."
maxlength="-1"
/>
<view class="codeSend" @click="runJs">
<text class="codeSendText">run</text>
</view>
</view>
</view>
</template>
<!-- Vuex -->
<template v-if="tabTitle == 'Vuex'">
<btnTabs
:list="stateTypeList"
:value="stateTypeListIndex"
@indexChange="stateTypeIndexChange"
/>
</template>
<codeHisPicker ref="codeHisPicker" />
</view>
</template>
<script>
import devCache from '../../core/libs/devCache'
import appDelDir from './libs/appDelDir'
import btnTabs from './ui/btnTabs.vue'
import codeHisPicker from './ui/codeHisPicker.vue'
import MenuBtn from './ui/menuBtn.vue'
import RequestSpeedLimit from './ui/requestSpeedLimit.vue'
import RequestTimeoutMock from './ui/requestTimeoutMock.vue'
export default {
components: {
btnTabs,
codeHisPicker,
MenuBtn,
RequestSpeedLimit,
RequestTimeoutMock,
},
props: {
/**
* 列表索引
*/
tabIndex: {
type: Number,
default: 0,
},
/**
* 当前标题
*/
tabTitle: {
type: String,
default: '',
},
/**
* 配置项
*/
options: {
type: Object,
default: () => ({
errorFilterType: '',
consoleFilterType: '',
networkFilterType: '',
busFilterType: '',
fileSysDirList: [],
fileSysDirType: '',
storageType: '',
}),
},
/**
* 是否渲染
*/
isShow: {
type: Boolean,
default: false,
},
/**
* Vuex变量类型
*/
stateType: {
type: String,
default: 'vuex',
},
},
data() {
let pb = '20px'
// #ifdef H5
pb = '8px'
// #endif
let sys = uni.getSystemInfoSync()
if (sys.platform == 'ios') {
pb = '40px'
}
let jsRunTypeList = []
// #ifdef H5
jsRunTypeList = ['h5']
// #endif
// #ifdef APP-PLUS
jsRunTypeList = ['nvue', 'webview']
// #endif
return {
/**
* 底部边距
*/
pb,
/**
* 错误类型列表
*/
errorTypeList: [
{ title: '全部', type: '' },
{ title: 'error', type: 've' },
{ title: 'warn', type: 'vw' },
{ title: 'jsError', type: 'oe' },
{ title: 'unknown', type: 'n' },
],
/**
* console过滤类型
*/
consoleTypeList: [
{ title: '全部', type: '' },
{ title: 'log', type: 'log' },
{ title: 'info', type: 'info' },
{ title: 'warn', type: 'warn' },
{ title: 'error', type: 'error' },
],
/**
* 请求过滤类型
*/
networkFilterType: [
{ title: '全部请求', type: '', msg: '不使用筛选' },
{ title: '请求失败', type: '请求失败', msg: '仅展示请求失败的记录' },
{ title: '1s+', type: '1s+', msg: '仅展示响应超过1秒的记录' },
{ title: '5s+', type: '5s+', msg: '仅展示响应超过5秒的记录' },
{ title: '10s+', type: '10s+', msg: '仅展示响应超过10秒的记录' },
{ title: '500KB+', type: '500KB+', msg: '仅展示响应内容超过500KB的记录' },
{ title: '1MB+', type: '1MB+', msg: '仅展示响应内容超过1MB的记录' },
{ title: 'get', type: 'get', msg: '仅展示get请求' },
{ title: 'post', type: 'post', msg: '仅展示post请求' },
{ title: 'other', type: 'other', msg: '除了get和post的其他请求' },
],
/**
* uni bus 过滤类型
*/
busFilterType: [
{ title: '全部', type: '' },
{ title: 'on', type: 'on' },
{ title: 'once', type: 'once' },
{ title: 'emit', type: 'emit' },
{ title: 'off', type: 'off' },
],
/**
* 文件类型
*/
dirTypeList: [
{ title: '_doc', type: 'PRIVATE_DOC' },
{ title: '_www', type: 'PRIVATE_WWW' },
{ title: '公共文档', type: 'PUBLIC_DOCUMENTS' },
{ title: '公共下载', type: 'PUBLIC_DOWNLOADS' },
],
/**
* 缓存类型
*/
storageFilterTypeList: [
{ title: 'localStorage', type: 'localStorage' },
{ title: 'sessionStorage', type: 'sessionStorage' },
{ title: 'cookie', type: 'cookie' },
],
/**
* 等待执行的js代码
*/
waitSendCode: '',
/**
* js运行类型
*/
jsRunType: jsRunTypeList[0],
jsRunTypeList,
/**
* Vuex变量类型
*/
stateTypeList: [
{ title: 'vuex', type: 'vuex' },
{ title: 'pinia', type: 'pinia' },
{ title: 'globalData', type: 'globalData' },
],
}
},
computed: {
/**
* 错误筛选分类index
*/
errorTypeIndex() {
return this.errorTypeList.findIndex((x) => x.type == this.options.errorFilterType)
},
/**
* 日志分类索引
*/
consoleTypeListIndex() {
return this.consoleTypeList.findIndex((x) => x.type == this.options.consoleFilterType)
},
/**
* 网络筛选索引
*/
networkTypeListIndex() {
return this.networkFilterType.findIndex((x) => x.type == this.options.networkFilterType)
},
/**
* bus分类索引
*/
busTypeListIndex() {
return this.busFilterType.findIndex((x) => x.type == this.options.busFilterType)
},
/**
* 文件分类索引
*/
fileTypeListIndex() {
return this.dirTypeList.findIndex((x) => x.type == this.options.fileSysDirType)
},
/**
* 缓存类型索引
*/
storageTypeListIndex() {
return this.storageFilterTypeList.findIndex((x) => x.type == this.options.storageType)
},
/**
* Vuex变量类型
*/
stateTypeListIndex() {
return this.stateTypeList.findIndex((x) => x.type == this.stateType)
},
},
methods: {
/**
* 过滤类型改变
*/
filterTypeChange(type) {
this.$emit('filterTypeChange', type)
},
/**
* 错误类型索引改变
*/
errorTypeIndexChange(e) {
this.filterTypeChange(this.errorTypeList[e].type)
},
/**
* 日志分类索引改变
*/
consoleTypeIndexChange(e) {
this.filterTypeChange(this.consoleTypeList[e].type)
},
/**
* 网络状态筛选改变事件
*/
networkTypeIndexChange(e) {
this.filterTypeChange(this.networkFilterType[e].type)
},
/**
* bus筛选改变事件
*/
busTypeIndexChange(e) {
this.filterTypeChange(this.busFilterType[e].type)
},
/**
* 文件分类改变事件
*/
fileTypeIndexChange(e) {
this.$emit('changeFileDirType', this.dirTypeList[e].type)
},
/**
* 缓存类型改变事件
*/
storageTypeIndexChange(e) {
this.$emit('changeStorageType', this.storageFilterTypeList[e].type)
},
/**
* Vuex变量类型改变事件
*/
stateTypeIndexChange(e) {
this.$emit('changeStateType', this.stateTypeList[e].type)
},
/**
* 清空日志
*/
emptyLogs(type) {
let that = this
let title = {
error: '报错记录',
console: 'console',
network: '请求日志',
pages_1: '页面停留统计',
pages_2: '页面日活统计',
logs: 'Logs',
UniBus: 'UniBus',
storage: 'Storage',
}
// #ifdef H5
if (type == 'storage') {
title[type] = this.options.storageType
}
// #endif
uni.showModal({
title: '警告',
content: `是否确认清空${title[type]}全部数据?`,
success(e) {
if (e.confirm) {
uni.showLoading({
title: '处理中...',
})
if (type == 'error') {
devCache.set('errorReport', [])
} else if (type == 'console') {
uni.$emit('devTools_delConsoleAll')
} else if (type == 'network') {
uni.$emit('devTools_delNetworkAll')
} else if (type == 'logs') {
devCache.set('logReport', [])
} else if (type == 'UniBus') {
uni.$emit('devTools_delUniBusAll')
} else if (type == 'pages_1') {
devCache.set('pageCount', [])
} else if (type == 'pages_2') {
devCache.set('dayOnline', [])
} else if (type == 'storage') {
that.delStorage()
}
setTimeout(() => {
that.$emit('getPage')
}, 5500)
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '清空成功!',
icon: 'success',
})
}, 5000)
}
},
})
},
/**
* 清空全部缓存
*/
delStorage() {
// #ifdef APP-PLUS
let keys = plus.storage.getAllKeys()
for (let i = 0; i < keys.length; i++) {
const key = String(keys[i])
if (key.indexOf('devTools_') == 0) {
continue
}
uni.removeStorageSync(key)
}
// #endif
// #ifdef H5
if (this.options.storageType == 'localStorage') {
for (let i = 0; i < localStorage.length; i++) {
let key = String(localStorage.key(i))
if (key.indexOf('devTools_') == 0) {
continue
}
uni.removeStorageSync(key)
}
} else if (this.options.storageType == 'sessionStorage') {
for (let i = 0; i < sessionStorage.length; i++) {
let key = String(sessionStorage.key(i))
if (key.indexOf('devTools_') == 0) {
continue
}
sessionStorage.removeItem(key)
}
} else if (this.options.storageType == 'cookie') {
let keys = []
document.cookie.split(';').forEach((cookieStr) => {
const [name, value] = cookieStr.trim().split('=')
keys.push(name)
})
keys.map((k) => {
document.cookie =
`${k}=;expires=` + new Date(new Date().getTime() + 200).toGMTString() + ';path=/'
})
}
// #endif
// #ifdef MP
let keyList = devCache.get('storage')
if (!keyList) keyList = []
for (let i = 0; i < keyList.length; i++) {
const key = keyList[i]
if (key.indexOf('devTools_') == 0) {
continue
}
uni.removeStorageSync(key)
}
// #endif
},
/**
* 清空文件夹
*/
emptyFolder() {
let that = this
if (that.options.fileSysDirType == 'PRIVATE_WWW') {
return uni.showToast({
title: '该目录不可删除',
icon: 'none',
})
}
uni.showModal({
title: '提示',
content: '是否确认清空全部文件?',
success(res) {
if (res.confirm) {
uni.showLoading({
title: '清空中',
})
let path = ''
switch (that.options.fileSysDirType) {
case 'wx':
path = wx.env.USER_DATA_PATH
break
case 'PRIVATE_DOC':
path = '_doc'
break
case 'PUBLIC_DOCUMENTS':
path = '_documents'
break
case 'PUBLIC_DOWNLOADS':
path = '_downloads'
break
default:
break
}
// #ifdef APP-PLUS
appDelDir(path + '/', false)
.then(() => {
uni.hideLoading()
uni.showToast({
title: '清空成功!',
icon: 'success',
})
that.$emit('getPage')
})
.catch(() => {
uni.hideLoading()
uni.showToast({
title: '清空失败!',
icon: 'none',
})
})
// #endif
// #ifdef MP-WEIXIN
let fs = wx.getFileSystemManager()
fs.rmdir({
dirPath: path + '/',
recursive: true,
success() {
uni.hideLoading()
uni.showToast({
title: '清空成功!',
icon: 'success',
})
that.$emit('getPage')
},
fail() {
uni.hideLoading()
uni.showToast({
title: '清空失败!',
icon: 'none',
})
},
})
// #endif
}
},
})
},
/**
* 创建文件夹
*/
createDir() {
let that = this
let menu = [
{
text: `新建文件`,
click() {
that.$emit('editDirName', {
isEdit: false,
isDir: false,
})
},
},
{
text: `新建文件夹`,
click() {
that.$emit('editDirName', {
isEdit: false,
isDir: true,
})
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
/**
* 新增缓存
*/
addStorage() {
uni.$emit('devTools_showAddStorageDialog')
},
/**
* 执行js
*/
runJs() {
let that = this
if (this.waitSendCode == '') {
return uni.showToast({
title: '请先输入需要执行的js代码',
icon: 'none',
})
} else {
let code = String(this.waitSendCode)
this.$emit('runJs', { code, type: that.jsRunType })
this.waitSendCode = ''
}
},
/**
* 获取历史代码运行记录
*/
showHisCode() {
let that = this
let his = devCache.get('codeRunHis')
if (!his) his = []
if (his.length == 0) {
return uni.showToast({
title: '暂无记录!',
icon: 'none',
})
}
that.$refs.codeHisPicker.show(his).then((res) => {
that.waitSendCode = res
})
},
/**
* 跳转指定页面
*/
goPage() {
uni.$emit('devTools_showRouteDialog')
},
},
}
</script>
<style lang="scss" scoped>
.bottomTools {
position: fixed;
z-index: 3;
bottom: 0;
left: 0;
width: 750rpx;
border-top: 1px solid rgba(0, 0, 0, 0.05);
background-color: #fff;
padding-top: 15rpx;
padding-left: 20rpx;
padding-right: 20rpx;
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
.mr {
margin-right: 20rpx;
}
.mt {
margin-left: 20rpx;
}
.ml {
margin-left: 20rpx;
}
.miniBtn {
height: 40rpx;
border-radius: 8rpx;
padding: 0 10rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
&.warn {
background-color: rgb(255, 251, 229);
}
&.primary {
background-color: #ecf5ff;
}
.miniBtnText {
font-size: 20rpx;
height: 40rpx;
line-height: 40rpx;
}
}
}
.dirList {
display: flex;
flex-direction: row;
align-items: center;
width: 710rpx;
height: 34rpx;
margin-bottom: 10rpx;
/* #ifndef APP-PLUS */
white-space: nowrap;
/* #endif */
.dirScrollItem {
display: flex;
flex-direction: row;
align-items: center;
height: 34rpx;
}
.dirItem {
display: flex;
flex-direction: row;
align-items: center;
}
.delimiter {
color: #999;
margin: 0 6rpx;
font-size: 22rpx;
line-height: 34rpx;
}
.dirName {
color: #333;
font-size: 22rpx;
line-height: 34rpx;
height: 34rpx;
padding: 0 6rpx;
background-color: rgba(0, 0, 0, 0.02);
border-radius: 6rpx;
}
}
.jsRunnerTools {
display: flex;
flex-direction: column;
width: 710rpx;
.runOptions {
display: flex;
flex-direction: row;
align-items: center;
width: 710rpx;
justify-content: space-between;
padding-bottom: 10rpx;
.radiusList {
display: flex;
flex-direction: row;
align-items: center;
.runType {
font-size: 20rpx;
line-height: 24rpx;
color: #333;
}
.radiusItem {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 10rpx;
.radiusText {
font-size: 20rpx;
line-height: 24rpx;
color: #333;
}
.radiusRadio {
transform: scale(0.5);
}
}
}
.hisEmpty {
display: flex;
flex-direction: row;
align-items: center;
.hisEmptyIcon {
width: 16rpx;
height: 16rpx;
}
.hisEmptyText {
font-size: 20rpx;
margin-left: 5rpx;
color: #ff2d55;
}
}
.logList {
// margin-right: 120rpx;
display: flex;
flex-direction: row;
align-items: center;
.hisText {
font-size: 20rpx;
color: #777;
line-height: 24rpx;
margin-right: 5rpx;
}
.unfold {
width: 24rpx;
height: 24rpx;
}
}
}
.code {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 710rpx;
.codeInput {
width: 590rpx;
height: 100rpx;
font-size: 20rpx;
line-height: 28rpx;
color: #333;
padding: 10rpx;
background-color: rgba(0, 0, 0, 0.03);
border-radius: 15rpx;
}
.codeSend {
width: 100rpx;
height: 100rpx;
border-radius: 15rpx;
border: 1px solid #ff2d55;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.codeSendText {
color: #ff2d55;
font-size: 24rpx;
font-weight: bold;
}
}
.codeSend:active {
background-color: rgba(0, 0, 0, 0.03);
}
}
}
.menuBtn {
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<view>
<view
class="editMask"
v-if="isShow"
:style="{
height: height + 'px',
}"
@click.stop
>
<view class="editDialog" @click.stop>
<text class="title">新增缓存</text>
<input type="text" placeholder="请输入Key" class="input" v-model="key" />
<input type="text" placeholder="请输入Value" class="input" v-model="value" />
<view class="btnGroup">
<view class="btnItem left" @click="hide">
<text class="btnText">取消</text>
</view>
<view class="btnItem right" @click="save">
<text class="btnText">提交</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
let success, error
export default {
props: {
/**
* 默认缓存类型
*/
storageType: {
type: String,
default: 'localStorage',
},
},
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 键名称
*/
key: '',
/**
*
*/
value: '',
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
}
},
mounted() {
let that = this
/**
* 挂载弹窗打开事件
*/
uni.$on('devTools_showAddStorageDialog', () => {
that.key = ''
that.value = ''
that.isShow = true
})
},
unmounted() {
uni.$off('devTools_showAddStorageDialog')
},
methods: {
/**
* 关闭弹窗
*/
hide() {
this.isShow = false
},
/**
* 保存
*/
save() {
let that = this
if (that.key == '') {
return uni.showToast({
title: '请输入key',
icon: 'none',
})
}
if (that.storageType == 'localStorage') {
uni.setStorageSync(that.key, that.value)
} else if (that.storageType == 'sessionStorage') {
sessionStorage.setItem(that.key, that.value)
} else if (that.storageType == 'cookie') {
let key = encodeURIComponent(that.key)
let val = encodeURIComponent(that.value)
let cookie =
`${key}=${val}; path=/; expires=` +
new Date(new Date().getTime() + 86400 * 1000 * 365).toGMTString()
document.cookie = cookie
}
uni.showToast({
title: '添加成功!',
icon: 'success',
})
that.hide()
setTimeout(() => {
that.$emit('getPage')
}, 300)
},
},
}
</script>
<style lang="scss" scoped>
.editMask {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.3);
width: 750rpx;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(1px);
/* #endif */
position: fixed;
left: 0;
top: 0;
z-index: 999;
.editDialog {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 690rpx;
background-color: #fff;
border-radius: 20rpx;
padding: 20rpx;
margin-bottom: 300rpx;
.title {
font-size: 28rpx;
color: #333;
height: 50rpx;
line-height: 50rpx;
}
.input {
margin-top: 20rpx;
margin-bottom: 20rpx;
width: 640rpx;
height: 70rpx;
padding: 5rpx;
border-radius: 8rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.btnGroup {
display: flex;
flex-direction: row;
width: 640rpx;
justify-content: space-between;
.btnItem {
height: 64rpx;
border-radius: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.btnText {
font-size: 24rpx;
color: #fff;
}
&.left {
width: 160rpx;
background-color: #8799a3;
}
&.right {
width: 450rpx;
background-color: #3cbb45;
}
}
}
}
}
</style>

View File

@ -0,0 +1,356 @@
<template>
<view>
<view
class="editMask"
v-if="isShow"
:style="{
height: height + 'px',
}"
@click.stop
>
<view class="editDialog" @click.stop>
<text class="title">{{ title }}</text>
<input type="text" placeholder="请输入" class="input" v-model="value" />
<view class="btnGroup">
<view class="btn left" @click="hide">
<text class="btnText">取消</text>
</view>
<view class="btn right" @click="save">
<text class="btnText">提交</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
let success, error
export default {
props: {
/**
* 路径列表
*/
dirList: {
type: Array,
default: () => [],
},
/**
* 路径类型
*/
dirType: {
type: String,
default: '',
},
},
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 是否为操作文件夹
*/
isDir: false,
/**
* 是否编辑模式
*/
isEdit: true,
/**
* 输入框的值
*/
value: '',
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
/**
* 更改前名称
*/
oldValue: '',
}
},
computed: {
/**
* 获取标题
*/
title() {
if (this.isEdit) {
return this.isDir ? '更改文件夹名称' : '更改文件名称'
} else {
return this.isDir ? '创建文件夹' : '创建文件'
}
},
},
methods: {
/**
* 展示弹窗
*/
show(options) {
let that = this
return new Promise((yes, err) => {
success = yes
error = err
this.isDir = options.isDir
this.isEdit = options.isEdit
this.value = String(options.name ? options.name : '')
this.oldValue = String(options.name ? options.name : '')
that.isShow = true
})
},
/**
* 关闭弹窗
*/
hide() {
this.isShow = false
},
/**
* 获取当前文件绝对路径
*/
getPath() {
let that = this
let path = ''
switch (that.dirType) {
case 'wx':
path = wx.env.USER_DATA_PATH
break
case 'PRIVATE_DOC':
path = '_doc'
break
case 'PRIVATE_WWW':
path = '_www'
break
case 'PUBLIC_DOCUMENTS':
path = '_documents'
break
case 'PUBLIC_DOWNLOADS':
path = '_downloads'
break
default:
break
}
that.dirList.map((x) => {
path += '/' + x
})
return path + '/'
},
/**
* 保存
*/
save() {
let that = this
that.value = that.value.replace(' ', '')
if (that.value == '') {
return uni.showToast({
title: '请输入...',
icon: 'none',
})
}
let path = that.getPath()
function buildSuccess() {
uni.showToast({
title: '操作成功!',
icon: 'success',
})
that.isShow = false
that.$emit('getPage')
}
function buildError(e) {
let msg = ''
if (e && e.message) {
msg = e.message
}
uni.showToast({
title: '重命名失败!' + msg,
icon: 'none',
})
}
// #ifdef MP-WEIXIN
if (1) {
let fs = wx.getFileSystemManager()
if (that.isEdit) {
if (that.isDir) {
// !
// !
} else {
// !
fs.rename({
oldPath: path + that.oldValue,
newPath: path + that.value,
success: buildSuccess,
fail: buildError,
})
}
} else {
if (that.isDir) {
// !
fs.mkdir({
dirPath: path + that.value,
recursive: false,
success: buildSuccess,
fail: buildError,
})
} else {
// !
// fs.open({
// filePath: path + that.value,
// flag: "wx+",
// success({ fd }) {
// fs.closeSync({ fd });
// buildSuccess();
// },
// fail: buildError,
// });
fs.writeFile({
filePath: path + that.value,
data: '',
encoding: 'utf8',
success() {
buildSuccess()
},
fail: buildError,
})
}
}
return
}
// #endif
if (that.isEdit) {
if (that.isDir) {
// !
plus.io.resolveLocalFileSystemURL(
path + that.oldValue,
(entry) => {
plus.io.resolveLocalFileSystemURL(
path,
(faEntry) => {
entry.moveTo(faEntry, that.value, buildSuccess, buildError)
},
buildError,
)
},
buildError,
)
} else {
// !
plus.io.resolveLocalFileSystemURL(
path + that.oldValue,
(entry) => {
plus.io.resolveLocalFileSystemURL(
path,
(faEntry) => {
entry.moveTo(faEntry, that.value, buildSuccess, buildError)
},
buildError,
)
},
buildError,
)
}
} else {
if (that.isDir) {
// !
plus.io.resolveLocalFileSystemURL(
path,
(entry) => {
entry.getDirectory(
that.value,
{ create: true, exclusive: false },
buildSuccess,
buildError,
)
},
buildError,
)
} else {
// !
plus.io.resolveLocalFileSystemURL(
path,
(entry) => {
entry.getFile(that.value, { create: true }, buildSuccess, buildError)
},
buildError,
)
}
}
},
},
}
</script>
<style lang="scss" scoped>
.editMask {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.3);
width: 750rpx;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(1px);
/* #endif */
position: fixed;
left: 0;
top: 0;
z-index: 999;
.editDialog {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 690rpx;
background-color: #fff;
border-radius: 20rpx;
padding: 20rpx;
margin-bottom: 300rpx;
.title {
font-size: 28rpx;
line-height: 28rpx;
color: #333;
}
.input {
margin-top: 20rpx;
margin-bottom: 20rpx;
width: 640rpx;
height: 70rpx;
padding: 5rpx;
border-radius: 8rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.btnGroup {
display: flex;
flex-direction: row;
width: 640rpx;
justify-content: space-between;
.btn {
height: 64rpx;
border-radius: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.btnText {
font-size: 24rpx;
color: #fff;
}
}
.left {
width: 160rpx;
background-color: #8799a3;
}
.right {
width: 450rpx;
background-color: #3cbb45;
}
}
}
}
</style>

View File

@ -0,0 +1,202 @@
<template>
<view>
<view
class="dialogMask"
v-if="isShow"
:style="{
height: height + 'px',
}"
@click.stop="hide"
>
<view class="dialogContent" @click.stop>
<view class="dialogHead" @click="hide">
<view>
<text class="title">{{ item.date + ' ' + item.timeCount }}</text>
</view>
<view>
<image src="@/devTools/page/static/unfold.png" class="fold" />
</view>
</view>
<scroll-view scroll-y class="scrollList">
<view
v-for="(row, index) in item.page"
:key="index"
class="pageLogItem"
@click.stop="showMenu(row)"
>
<text class="p">页面{{ row.r }}</text>
<text class="t">活跃{{ row.timeCount }}</text>
</view>
<view style="height: 100rpx"></view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
let success, error
export default {
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
/**
* 详情列表
*/
item: {
date: '',
timeCount: '',
page: [
{
r: '',
timeCount: '',
},
],
},
}
},
methods: {
/**
* 展示弹窗
*/
show(item) {
let that = this
return new Promise((yes, err) => {
success = yes
error = err
that.item.date = item.date
that.item.timeCount = item.timeCount
that.item.page = item.page
that.isShow = true
})
},
/**
* 关闭弹窗
*/
hide() {
this.isShow = false
error()
},
/**
* 保存
*/
save() {
this.isShow = false
success(this.value)
},
/**
* 展示菜单
*/
showMenu(row) {
let that = this
let r = String(row.r).substring(0, 10) + '...'
let menu = [
{
text: `复制路径(${r})`,
click() {
uni.setClipboardData({
data: row.r,
})
},
},
{
text: `复制时间(${row.timeCount})`,
click() {
uni.setClipboardData({
data: row.timeCount,
})
},
},
{
text: `跳转至此页面`,
click() {
uni.$emit('devTools_showRouteDialog', row.r)
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.dialogMask {
display: flex;
flex-direction: column-reverse;
background-color: rgba(0, 0, 0, 0.3);
width: 750rpx;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(1px);
/* #endif */
position: fixed;
left: 0;
top: 0;
z-index: 999;
.dialogContent {
display: flex;
flex-direction: column;
width: 750rpx;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
.dialogHead {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 80rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
width: 750rpx;
.title {
margin-left: 20rpx;
font-size: 24rpx;
line-height: 24rpx;
color: #333;
}
.fold {
width: 20rpx;
height: 20rpx;
margin-right: 20rpx;
}
}
.scrollList {
width: 750rpx;
height: 750rpx;
.pageLogItem {
width: 750rpx;
display: flex;
flex-direction: column;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
.p {
font-size: 20rpx;
color: #333;
}
.t {
margin-top: 4rpx;
font-size: 20rpx;
color: #333;
}
}
}
}
}
</style>

View File

@ -0,0 +1,178 @@
<template>
<view>
<view
class="editMask"
v-if="isShow"
:style="{
height: height + 'px',
}"
@click.stop
>
<view class="editDialog" @click.stop>
<view>
<text class="title">{{ title }}</text>
</view>
<textarea v-model="value" type="text" placeholder="请输入..." class="textarea" />
<view class="btnGroup">
<view class="btnItem left" @click="hide">
<text class="btnText">取消</text>
</view>
<view class="btnItem right" @click="save">
<text class="btnText">提交</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
let success, error
export default {
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 标题
*/
title: '',
/**
* 输入框的值
*/
value: '',
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
}
},
mounted() {
let that = this
/**
* 使用uni.$on打开弹窗
*/
uni.$on('devTools_showEditDialog', (options) => {
that
.show(options.title, options.value)
.then((val) => {
uni.$emit('devTools_editDialogSaveSuccess', val)
})
.catch(() => {
uni.$emit('devTools_editDialogClose')
})
})
},
beforeDestroy() {
uni.$off('devTools_showEditDialog')
},
methods: {
/**
* 展示弹窗
*/
show(title = '标题', value = '') {
let that = this
return new Promise((yes, err) => {
success = yes
error = err
that.title = title
that.value = value ? value : ''
that.isShow = true
})
},
/**
* 关闭弹窗
*/
hide() {
this.isShow = false
error()
},
/**
* 保存
*/
save() {
this.isShow = false
success(this.value)
},
},
}
</script>
<style lang="scss" scoped>
.editMask {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.3);
width: 750rpx;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(1px);
/* #endif */
position: fixed;
left: 0;
top: 0;
z-index: 999;
.editDialog {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 690rpx;
background-color: #fff;
border-radius: 20rpx;
padding: 20rpx;
.title {
font-size: 28rpx;
line-height: 28rpx;
color: #333;
/* #ifndef APP-PLUS */
max-width: 600rpx;
word-wrap: break-word;
/* #endif */
/* #ifdef APP-PLUS */
width: 600rpx;
/* #endif */
}
.textarea {
margin-top: 20rpx;
margin-bottom: 20rpx;
width: 640rpx;
/* #ifndef APP-PLUS */
min-height: 200rpx;
max-height: 750rpx;
/* #endif */
background-color: rgba(0, 0, 0, 0.02);
padding: 10rpx;
}
.btnGroup {
display: flex;
flex-direction: row;
width: 640rpx;
justify-content: space-between;
.btnItem {
height: 64rpx;
border-radius: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.btnText {
font-size: 24rpx;
color: #fff;
}
&.left {
width: 160rpx;
background-color: #8799a3;
}
&.right {
width: 450rpx;
background-color: #3cbb45;
}
}
}
}
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<view>
<view
class="routeDialogMask"
v-if="isShow"
:style="{
height: height + 'px',
}"
@click.stop
>
<view class="routeDialog" @click.stop>
<view>
<text class="title">跳转至指定页面</text>
</view>
<textarea v-model="path" type="text" placeholder="请输入..." class="textarea" />
<view class="btnGroup">
<view class="btnItem left" @click="hide">
<text class="btnText">取消</text>
</view>
<view class="btnItem right" @click="goPath">
<text class="btnText">跳转</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 输入框的值
*/
path: '',
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
}
},
mounted() {
let that = this
/**
* 使用uni.$on打开弹窗
*/
uni.$on('devTools_showRouteDialog', (path) => {
that.path = path ? path : ''
that.isShow = true
})
},
beforeDestroy() {
uni.$off('devTools_showRouteDialog')
},
methods: {
/**
* 关闭弹窗
*/
hide() {
this.isShow = false
},
/**
* 执行跳转
*/
goPath() {
let that = this
let path = String(that.path)
path = path.replace(/[\r\n\s]+/g, '')
if (path.substring(0, 1) !== '/') {
return uni.showToast({
title: '页面路径需以“/”开头!',
icon: 'none',
})
}
if (path.length < 2) {
return uni.showToast({
title: '请输入正确的路径!',
icon: 'none',
})
}
uni.navigateTo({
url: path,
})
},
},
}
</script>
<style lang="scss" scoped>
.routeDialogMask {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.3);
width: 750rpx;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(1px);
/* #endif */
position: fixed;
left: 0;
top: 0;
z-index: 999;
.routeDialog {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 690rpx;
background-color: #fff;
border-radius: 20rpx;
padding: 20rpx;
.title {
font-size: 28rpx;
line-height: 28rpx;
color: #333;
/* #ifndef APP-PLUS */
word-wrap: break-word;
max-width: 600rpx;
/* #endif */
/* #ifdef APP-PLUS */
width: 600rpx;
/* #endif */
}
.textarea {
margin-top: 20rpx;
margin-bottom: 20rpx;
width: 640rpx;
/* #ifndef APP-PLUS */
min-height: 200rpx;
max-height: 750rpx;
/* #endif */
background-color: rgba(0, 0, 0, 0.02);
padding: 10rpx;
}
.btnGroup {
display: flex;
flex-direction: row;
width: 640rpx;
justify-content: space-between;
.btnItem {
height: 64rpx;
border-radius: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.btnText {
font-size: 24rpx;
color: #fff;
}
&.left {
width: 160rpx;
background-color: #8799a3;
}
&.right {
width: 450rpx;
background-color: #3cbb45;
}
}
}
}
}
</style>

View File

@ -0,0 +1,443 @@
<template>
<view>
<view
class="dialogMask"
v-if="isShow"
:style="{
height: height + 'px',
}"
@click.stop
>
<view class="dialogContent" @click.stop>
<view class="dialogHead" @click="hide(2)">
<view>
<text class="title">请求构建工具</text>
</view>
<view>
<image src="@/devTools/page/static/unfold.png" class="fold" />
</view>
</view>
<scroll-view
@click.stop
scroll-y
class="scrollList"
:style="{
height: dialogHeight + 'px',
}"
>
<subTitleBar title="请求地址(url)" :showArrow="false" />
<view class="inputRow">
<input
type="text"
placeholder="请输入url地址"
class="input"
v-model="request.url"
maxlength="-1"
/>
<text class="del" @click="request.url = ''">x</text>
</view>
<subTitleBar title="请求方式[method]" :showArrow="false" />
<view class="inputRow">
<picker
@change="request.method = requestMethods[$event.detail.value]"
:value="requestMethods.findIndex((x) => x == request.method)"
:range="requestMethods"
>
<view class="method">
<text class="methodName">{{ request.method }}</text>
<image class="unfold" src="@/devTools/page/static/unfold.png" />
</view>
</picker>
</view>
<subTitleBar title="请求参数[data](json对象)" :showArrow="false" />
<view class="inputRow">
<textarea
placeholder="请输入JSON对象"
class="textarea"
v-model="request.data"
maxlength="-1"
/>
<text class="del" @click="request.data = ''">x</text>
</view>
<subTitleBar title="请求头(header)" :showArrow="false" />
<view class="inputRow">
<textarea
placeholder="请输入JSON对象"
class="textarea"
v-model="request.header"
maxlength="-1"
/>
<text class="del" @click="request.header = ''">x</text>
</view>
<view :class="[send.status ? 'loading' : '']" class="sendBtn" @click="sendRequest">
<text v-if="send.status" class="sendBtnText">
发送中[{{ send.t }}ms]
<text class="msg">(点击可取消)</text>
</text>
<text v-else class="sendBtnText">发送</text>
</view>
<template v-if="ajaxHasRes">
<subTitleBar title="响应结果:" :showArrow="false" />
<view class="inputRow">
<objectAnalysis :data="ajaxRes" :width="710" :isOpenFirst="true" />
</view>
</template>
<view style="height: 100rpx"></view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import objectAnalysis from '../listItem/objectAnalysis.vue'
import subTitleBar from '../ui/subTitleBar.vue'
let success, error
/**
* 转json字符串并格式化
*/
function toJsonStr(obj) {
return JSON.stringify(obj, null, 2)
}
export default {
components: {
subTitleBar,
objectAnalysis,
},
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
dialogHeight: Math.ceil(uni.getSystemInfoSync().windowHeight * 0.85),
requestMethods: ['get', 'post', 'put', 'delete', 'connect', 'head', 'options', 'trace'],
/**
* 请求构建对象
*/
request: {
url: '', //
header: '', //
method: 'get', //
data: '', //
},
/**
* 是否有响应结果
*/
ajaxHasRes: false,
/**
* 响应结果对象
*/
ajaxRes: {},
/**
* 是否状态
*/
send: {
status: false, //
t: 0, // ms
time: 0, //
},
}
},
mounted() {
let that = this
setInterval(() => {
if (that.send.status) {
that.send.t = new Date().getTime() - that.send.time
}
}, 1000 / 24)
},
methods: {
/**
* 展示弹窗
*/
show(item, needSend = false) {
let that = this
if (that.send.status) {
that.send.status = false
}
if (that.ajaxHasRes) {
that.ajaxHasRes = false
}
return new Promise((yes, err) => {
success = yes
error = err
if (item && item.url && item.method) {
that.request.url = item.url
if (typeof item.method == 'string') {
that.request.method = item.method.toLocaleLowerCase()
} else {
that.request.method = 'get'
}
try {
let data = toJsonStr(item.data)
if (Object.keys(data).length == 0) {
data = ''
} else {
that.request.data = data
}
} catch (error) {
that.request.data = ''
}
try {
that.request.header = toJsonStr(item.header)
} catch (error) {
that.request.header = toJsonStr({
'content-type': 'application/x-www-form-urlencoded',
})
}
} else {
that.request.url = ''
that.request.data = ''
that.request.method = 'get'
that.ajaxHasRes = false
that.request.header = toJsonStr({
'content-type': 'application/x-www-form-urlencoded',
})
}
that.send.status = false
that.isShow = true
if (needSend) {
that.sendRequest()
}
})
},
/**
* 关闭弹窗
*/
hide() {
this.isShow = false
error()
},
/**
* 发送请求
*/
sendRequest() {
let that = this
if (that.send.status) {
return uni.showModal({
title: '提示',
content: '请求还在进行,是否确认取消请求?',
success(res) {
if (res.confirm) {
if (that.send.status) {
that.send.status = false
that.ajaxHasRes = false
}
}
},
})
}
let tw = (m) =>
uni.showToast({
icon: 'none',
title: m,
})
if (that.request.url == '' || typeof that.request.url != 'string') return tw('请输入url')
if (that.request.url.indexOf('http') != 0) return tw('请输入正确的url地址')
if (that.request.url.indexOf('://') == -1) return tw('请输入正确的url地址')
if (that.request.url.length < 10) return tw('请输入正确的url地址')
let data = {}
if (that.request.data != '') {
try {
data = JSON.parse(that.request.data)
} catch (error) {
return tw('请求参数json解析失败')
}
}
let header = {}
if (that.request.header) {
try {
header = JSON.parse(that.request.header)
} catch (error) {
return tw('请求头json解析失败')
}
}
header['Devtoolssend'] = 1
that.send.t = 0
that.send.time = new Date().getTime()
that.send.status = true
that.ajaxHasRes = false
uni.request({
url: that.request.url,
method: that.request.method,
data,
header,
success(res) {
if (!that.send.status || !that.isShow) return
that.send.status = false
res['请求用时'] = new Date().getTime() - that.send.time + 'ms'
that.$set(that, 'ajaxRes', res)
that.ajaxHasRes = true
uni.showToast({
title: '请求响应成功',
icon: 'success',
})
},
fail(msg, request) {
if (!that.send.status || !that.isShow) return
let res = {
fail: msg,
request,
请求用时: new Date().getTime() - that.send.time + 'ms',
}
that.send.status = false
that.$set(that, 'ajaxRes', res)
that.ajaxHasRes = true
uni.showToast({
title: '请求响应失败',
icon: 'error',
})
},
})
},
},
}
</script>
<style lang="scss" scoped>
.dialogMask {
display: flex;
flex-direction: column-reverse;
background-color: rgba(0, 0, 0, 0.3);
width: 750rpx;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(1px);
/* #endif */
position: fixed;
left: 0;
top: 0;
z-index: 999;
.dialogContent {
display: flex;
flex-direction: column;
width: 750rpx;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
.dialogHead {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 80rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
width: 750rpx;
.title {
margin-left: 20rpx;
font-size: 24rpx;
line-height: 28rpx;
height: 28rpx;
color: #333;
}
.fold {
width: 20rpx;
height: 20rpx;
margin-right: 20rpx;
}
}
.scrollList {
width: 750rpx;
.inputRow {
width: 750rpx;
padding: 0rpx 20rpx;
position: relative;
.input {
width: 710rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
padding-left: 5rpx;
padding-top: 5rpx;
padding-bottom: 5rpx;
padding-right: 50rpx;
font-size: 24rpx;
border-radius: 6rpx;
height: 60rpx;
}
.textarea {
width: 710rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
padding-left: 5rpx;
padding-top: 5rpx;
padding-bottom: 5rpx;
padding-right: 50rpx;
font-size: 24rpx;
border-radius: 6rpx;
height: 140rpx;
}
.del {
position: absolute;
right: 24rpx;
top: 10rpx;
height: 40rpx;
background-color: #fff;
padding: 0 10rpx;
font-size: 24rpx;
color: #999;
line-height: 40rpx;
}
.method {
display: flex;
flex-direction: row;
align-items: center;
.methodName {
font-size: 24rpx;
color: #333;
}
.unfold {
margin-left: 10rpx;
width: 24rpx;
height: 24rpx;
}
}
}
.sendBtn {
width: 710rpx;
margin-left: 20rpx;
margin-top: 30rpx;
margin-bottom: 30rpx;
height: 60rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: 10rpx;
background-color: rgb(255, 45, 85);
&.loading {
background-color: rgba(255, 45, 85, 0.5);
}
.sendBtnText {
color: #fff;
font-size: 26rpx;
}
.msg {
color: #fff;
font-size: 20rpx;
margin-left: 20rpx;
}
}
}
}
}
</style>

View File

@ -0,0 +1,361 @@
<template>
<view>
<view
class="dialogMask"
v-if="isShow"
:style="{
height: height + 'px',
}"
>
<view class="dialogContent" @click.stop>
<view class="dialogHead" @click="hide">
<view>
<text class="title">{{ title }}</text>
</view>
<view>
<image src="@/devTools/page/static/unfold.png" class="fold" />
</view>
</view>
<scroll-view
scroll-y
class="scrollList"
:style="{
height: dialogHeight + 'px',
}"
>
<textarea
:style="{
height: dialogHeight - (canSave ? 90 : 40) + 'px',
}"
v-model="value"
type="text"
placeholder="请输入..."
class="fileEditInput"
maxlength="-1"
/>
<view class="saveBtn" v-if="canSave" @click="saveFile">
<text class="saveBtnText">保存</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
let success, error
export default {
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
dialogHeight: uni.getSystemInfoSync().windowHeight * 0.8,
/**
* 弹窗标题
*/
title: '',
/**
* 文本内容
*/
value: '',
/**
* 是否为文件编辑模式
*/
isFileEdit: true,
/**
* 文件路径
*/
path: '',
/**
* 是否允许保存
*/
canSave: false,
/**
* 是否为新建文件
*/
isNewFile: false,
}
},
mounted() {
let that = this
uni.$on('devTools_showTextEditDialog', (options) => {
that
.show(options)
.then((val) => {
uni.$emit('devTools_showTextEditDialogSave', val)
})
.catch(() => {
uni.$emit('devTools_showTextEditDialogHide')
})
})
},
unmounted() {
uni.$off('devTools_showTextEditDialog')
},
methods: {
/**
* 展示弹窗
*/
show(options) {
let that = this
return new Promise((yes, err) => {
success = yes
error = err
that.title = options.title
that.canSave = Boolean(options.canSave)
that.isShow = true
if (options.isFileEdit === false) {
//
that.isFileEdit = false
try {
that.value = JSON.stringify(JSON.parse(options.value), null, 2)
} catch (error) {
that.value = options.value
}
return
}
that.isFileEdit = true
that.value = '文件读取中...'
that.path = options.path
that.isNewFile = Boolean(options.isNewFile)
// #ifdef APP-PLUS
if (that.isNewFile) {
that.value = ''
} else {
plus.io.resolveLocalFileSystemURL(
that.path,
(entry) => {
// entrytest.html
entry.file((file) => {
var fileReader = new plus.io.FileReader()
fileReader.readAsText(file, 'utf-8')
fileReader.onloadend = function (evt) {
let res = ''
try {
res = JSON.stringify(JSON.parse(evt.target.result), null, 2)
} catch (error) {
res = evt.target.result
}
that.value = res
}
fileReader.onerror = function () {
that.value = `[${that.path}]文件读取失败_code_2`
}
})
},
function (e) {
that.value = `[${that.path}]文件读取失败!` + e.message
},
)
}
// #endif
// #ifdef MP-WEIXIN
let fs = wx.getFileSystemManager()
if (options.size != 0) {
fs.readFile({
filePath: that.path,
encoding: 'utf8',
position: 0,
length: options.size,
success({ data }) {
try {
that.value = JSON.stringify(JSON.parse(data), null, 2)
} catch (error) {
that.value = data
}
},
fail(e) {
console.log(e)
that.value = `[${that.path}]文件读取失败!` + e
},
})
} else {
that.value = ''
}
// #endif
})
},
/**
* 关闭弹窗
*/
hide() {
this.isShow = false
error()
},
/**
* 保存
*/
save() {
this.isShow = false
success(this.value)
},
/**
* 保存文件
*/
saveFile() {
let that = this
if (!that.isFileEdit) {
//
that.isShow = false
success(that.value)
return
}
uni.showLoading({
title: '保存中',
})
// #ifdef APP-PLUS
let fileName = that.path.split('/').pop()
let path = that.path.substring(0, that.path.length - fileName.length)
plus.io.resolveLocalFileSystemURL(
path,
(entry) => {
entry.getFile(
fileName,
{
create: true,
},
(fileEntry) => {
fileEntry.createWriter((writer) => {
writer.onwrite = (e) => {
uni.hideLoading()
uni.showToast({
title: '文件保存成功!',
icon: 'success',
})
that.isShow = false
that.$emit('getPage')
}
writer.onerror = () => {
uni.hideLoading()
uni.showToast({
title: '文件保存失败_写入文件失败',
icon: 'none',
})
}
writer.write(that.value)
})
},
)
},
() => {
uni.hideLoading()
uni.showToast({
title: '文件保存失败_打开目录失败',
icon: 'none',
})
},
)
// #endif
// #ifdef MP-WEIXIN
let fs = wx.getFileSystemManager()
fs.writeFile({
filePath: that.path,
encoding: 'utf-8',
data: that.value,
success() {
uni.hideLoading()
uni.showToast({
title: '文件保存成功!',
icon: 'success',
})
that.isShow = false
that.$emit('getPage')
},
fail() {
uni.hideLoading()
uni.showToast({
title: '文件保存失败_打开目录失败',
icon: 'none',
})
},
})
// #endif
},
},
}
</script>
<style lang="scss" scoped>
.dialogMask {
display: flex;
flex-direction: column-reverse;
background-color: rgba(0, 0, 0, 0.3);
width: 750rpx;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(1px);
/* #endif */
position: fixed;
left: 0;
top: 0;
z-index: 999;
.dialogContent {
display: flex;
flex-direction: column;
width: 750rpx;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
.dialogHead {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 80rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
width: 750rpx;
.title {
margin-left: 20rpx;
font-size: 24rpx;
line-height: 24rpx;
color: #333;
}
.fold {
width: 20rpx;
height: 20rpx;
margin-right: 20rpx;
}
}
.scrollList {
width: 750rpx;
padding: 20rpx;
.fileEditInput {
font-size: 20rpx;
}
.saveBtn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-top: 20rpx;
height: 35px;
width: 710rpx;
border-radius: 10rpx;
background-color: #ff2d55;
.saveBtnText {
color: #fff;
font-size: 24rpx;
line-height: 24rpx;
}
}
}
}
}
</style>

View File

@ -0,0 +1,77 @@
import dirReader from './dirReader'
/**
* 遍历删除整个文件夹/结尾
*/
export default function appDelDir(path, needDelSelf = true) {
return new Promise(async (yes, err) => {
let dirList = await dirReader.getDirFileList(path)
for (let i = 0; i < dirList.length; i++) {
let item = dirList[i]
try {
if (item.type == 'dir') {
let info = await getMeteInfo(path + item.name + '/')
if (info.directoryCount > 0 || info.fileCount > 0) {
await appDelDir(path + item.name + '/')
} else {
await delDir(path + item.name + '/')
}
} else {
await delFile(path + item.name)
}
} catch (error) {}
}
try {
if (needDelSelf) {
await delDir(path)
}
} catch (error) {}
yes()
})
}
function delFile(path) {
return new Promise((yes, err) => {
plus.io.resolveLocalFileSystemURL(
path,
(entry) => {
entry.remove(yes, err)
},
err,
)
})
}
function delDir(path) {
return new Promise((yes, err) => {
plus.io.resolveLocalFileSystemURL(
path,
(entry) => {
entry.remove(yes, err)
},
err,
)
})
}
/**
* 获取文件夹内信息
* @returns {Promise<PlusIoMetadata>}
*/
function getMeteInfo(path) {
return new Promise((yes, err) => {
plus.io.resolveLocalFileSystemURL(
path,
(entry) => {
if (entry.isDirectory) {
entry.getMetadata((metadata) => {
yes(metadata)
}, err)
} else {
err()
}
},
err,
)
})
}

View File

@ -0,0 +1,216 @@
const iconConfig = [
{
type: ['', undefined, null],
mime: '',
icon: '/devTools/page/static/fileSys/weizhiwenjian.png',
},
{
type: ['dwg'],
mime: 'dwg',
icon: '/devTools/page/static/fileSys/DWG.png',
},
{
type: ['xls', 'xlsx', 'csv'],
mime: 'xls',
icon: '/devTools/page/static/fileSys/excel.png',
},
{
type: ['exe'],
mime: 'exe',
icon: '/devTools/page/static/fileSys/EXE.png',
},
{
type: ['gif'],
mime: 'gif',
icon: '/devTools/page/static/fileSys/GIF.png',
},
{
type: ['html'],
mime: 'html',
icon: '/devTools/page/static/fileSys/HTML.png',
},
{
type: ['pdf'],
mime: 'pdf',
icon: '/devTools/page/static/fileSys/pdf.png',
},
{
type: ['ppt'],
mime: 'ppt',
icon: '/devTools/page/static/fileSys/pptl.png',
},
{
type: ['psd'],
mime: 'psd',
icon: '/devTools/page/static/fileSys/PSD.png',
},
{
type: ['rvt'],
mime: 'rvt',
icon: '/devTools/page/static/fileSys/RVT.png',
},
{
type: ['mp4', 'avi', 'wmv', 'mpg', 'mpeg', 'mov', 'flv', '3gp', 'mp3gp', 'mkv', 'rmvb'],
mime: 'mp4',
icon: '/devTools/page/static/fileSys/shipin.png',
},
{
type: ['skp'],
mime: 'skp',
icon: '/devTools/page/static/fileSys/SKP.png',
},
{
type: ['svg'],
mime: 'svg',
icon: '/devTools/page/static/fileSys/SVG.png',
},
{
type: ['png', 'jpeg', 'jpg', 'webp', 'bmp'],
mime: 'img',
icon: '/devTools/page/static/fileSys/tupian.png',
},
{
type: ['txt', 'sql', 'js', 'css', 'log', 'json'],
mime: 'txt',
icon: '/devTools/page/static/fileSys/txt.png',
},
{
type: ['word'],
mime: 'word',
icon: '/devTools/page/static/fileSys/word.png',
},
{
type: ['zip', 'rar', 'gz', '7z'],
mime: 'zip',
icon: '/devTools/page/static/fileSys/yasuo.png',
},
{
type: ['mp3', 'wma', 'wav', 'aac', 'flac'],
mime: '',
icon: '/devTools/page/static/fileSys/yinpin.png',
},
]
export default {
/**
* 获取文件和目录列表
*/
getDirFileList(path) {
return new Promise((yes) => {
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(
path,
function (entry) {
if (entry.isDirectory) {
let reader = entry.createReader()
reader.readEntries(
async (entries) => {
let dirList = []
let fileList = []
for (let i = 0; i < entries.length; i++) {
/**
* @type {PlusIoDirectoryEntry}
*/
const item = entries[i]
let meta = await getMetaData(item)
let row = {
type: item.isDirectory ? 'dir' : 'file',
name: item.name,
fileType: getFileType(item.name),
...meta,
}
if (item.isDirectory) {
dirList.push(row)
} else {
fileList.push(row)
}
}
dirList = dirList.sort((a, b) => a.time > b.time)
fileList = fileList.sort((a, b) => a.time > b.time)
yes([...dirList, ...fileList])
},
(e) => {
console.log('readEntries error', e)
uni.showToast({
title: '文件读取失败: ' + e.message,
icon: 'none',
})
yes([])
},
)
} else {
uni.showToast({
title: '路径读取失败_b',
icon: 'none',
})
yes([])
}
},
() => {
uni.showToast({
title: '路径读取失败_a',
icon: 'none',
})
yes([])
},
)
// #endif
})
},
/**
* 获取文件图片
*/
getFileIcon(type) {
for (let i = 0; i < iconConfig.length; i++) {
const item = iconConfig[i]
for (let _i = 0; _i < item.type.length; _i++) {
const typeName = item.type[_i]
if (type == typeName) {
return item.icon
}
}
}
return '/devTools/page/static/fileSys/weizhiwenjian.png'
},
}
/**
* @param {PlusIoDirectoryEntry} entry
*/
function getMetaData(entry) {
return new Promise((yes) => {
entry.getMetadata(
function (metadata) {
yes({
size: metadata.size,
time: metadata.modificationTime.getTime(),
fileCount: metadata.fileCount,
directoryCount: metadata.directoryCount,
})
},
function () {
yes({
size: 0,
time: 0,
fileCount: 0,
directoryCount: 0,
})
},
)
})
}
function getFileType(name) {
if (typeof name == 'string') {
let tList = name.split('.')
if (tList.length > 1) {
return tList.pop().toLocaleLowerCase()
} else {
return ''
}
} else {
return ''
}
}

View File

@ -0,0 +1,15 @@
export default {
/**
* 获取字节大小,b转kb mb
*/
getByteSize(size) {
if (null == size || size == '') return '0 B'
var unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var index = 0
var srcsize = parseFloat(size)
index = Math.floor(Math.log(srcsize) / Math.log(1024))
var size = srcsize / Math.pow(1024, index)
size = size.toFixed(2) //保留的小数位数
return size + unitArr[index]
},
}

View File

@ -0,0 +1,132 @@
/**
* 获取当前运行时环境信息
*/
export default function getRuntimeInfo() {
return new Promise(async (yes) => {
let data = {
系统信息: uni.getSystemInfoSync(),
设备基础信息: uni.getDeviceInfo ? uni.getDeviceInfo() : null,
窗口信息: uni.getWindowInfo ? uni.getWindowInfo() : null,
APP基础信息: uni.getAppBaseInfo ? uni.getAppBaseInfo() : null,
APP授权设置: uni.getAppAuthorizeSetting ? uni.getAppAuthorizeSetting() : null,
设备设置: uni.getSystemSetting ? uni.getSystemSetting() : null,
网络状态: await getNetworkType(),
启动参数: uni.getLaunchOptionsSync(),
// #ifdef APP-PLUS
其他信息: await getAppOtherInfo(),
// #endif
}
yes(data)
})
}
/**
* 获取网络状态
*/
function getNetworkType() {
return new Promise((yes, err) => {
if (!uni.getNetworkType) {
return yes(null)
}
uni.getNetworkType({
success(res) {
yes(res.networkType)
},
fail() {
yes('error')
},
})
})
}
/**
* 获取APP端设备其他信息
*/
function getAppOtherInfo() {
return new Promise(async (yes) => {
let info = {}
let getDevice = () =>
new Promise((yes) => {
plus.device.getInfo({
success: yes,
fail() {
yes('plus.device.getInfo() fail')
},
})
})
let getOAID = () =>
new Promise((yes) => {
plus.device.getOAID({
success: yes,
fail() {
yes('plus.device.getOAID() fail')
},
})
})
let getAAID = () =>
new Promise((yes) => {
plus.device.getOAID({
success: yes,
fail() {
yes('plus.device.getOAID() fail')
},
})
})
let getDeviceId = () =>
new Promise((yes) => {
try {
let id = plus.device.getDeviceId()
yes(id)
} catch (error) {
yes('plus.device.getDeviceId() fail')
}
})
try {
info.getDevice = await getDevice()
info.getOAID = await getOAID()
info.getAAID = await getAAID()
info.getDeviceId = await getDeviceId()
yes(info)
} catch (error) {
console.log('getDeviceInfoFail', error)
yes('获取设备信息失败!')
}
plus.device.getInfo({
success(e) {
info = Object.assign(info, e)
plus.device.getOAID({
success(e) {
info = Object.assign(info, e)
plus.device.getVAID({
success(e) {},
fail() {
yes(
Object.assign(info, {
errMsg: 'plus.device.getVAID 获取失败!',
}),
)
},
})
},
fail() {
yes(
Object.assign(info, {
errMsg: 'plus.device.getOAID 获取失败!',
}),
)
},
})
},
fail() {
yes({ errMsg: 'plus.device.getInfo 获取失败!' })
},
})
})
}

View File

@ -0,0 +1,271 @@
<template>
<view class="consoleItem" :class="['type-' + item.type]" @longpress.stop="consoleLongpress">
<view class="content">
<view v-for="(row, index) in item.list" :key="index">
<template v-if="isObj(row)">
<objectAnalysis
:data="row"
:width="610"
:canLongpress="false"
@onLongpress="consoleLongpress"
/>
</template>
<template v-else>
<view>
<text class="context" :class="[getTypeClass(row)]">
{{ getText(row) }}
</text>
</view>
</template>
</view>
<view class="msgBar">
<text class="time">{{ item.date }}</text>
<text class="logType" :class="'type-' + item.type">
{{ item.type }}
</text>
<text class="page">{{ item.page }}</text>
</view>
</view>
<view class="tools">
<view class="copyBtn" @click="copyList">
<image src="@/devTools/page/static/copy.png" class="copyIcon" />
</view>
</view>
</view>
</template>
<script>
import objectAnalysis from './objectAnalysis.vue'
export default {
components: {
objectAnalysis,
},
props: {
/**
* console单行数据
*/
item: {
type: Object,
default() {
return {
list: [],
date: '', //
page: '', //
type: '', //
}
},
},
},
methods: {
/**
* 是否为对象类型
*/
isObj(data) {
return typeof data == 'object' && data != null && data != undefined
},
/**
* 获取对应的类型样式
*/
getTypeClass(obj) {
try {
let type = typeof obj
if (type == 'string') {
if (obj.indexOf('at ') == 0) {
return 't-line'
} else if (obj === undefined || obj == 'undefined') {
return 't-undefined'
} else if (obj === null || obj == 'null') {
return 't-null'
} else if (obj == 'true' || obj == 'false') {
return 't-boolean'
} else if (Number.isFinite(Number(obj))) {
return 't-number'
}
}
return 't-' + type
} catch (error) {}
return 't-string'
},
/**
* 获取数据文字
*/
getText(data) {
switch (typeof data) {
case 'string':
// return data.replace(/\n/g, "");
return data
case 'boolean':
return data ? 'true' : 'false'
case 'undefined':
return 'undefined'
case 'function':
return 'js:function'
case 'symbol':
return 'js:symbol'
default:
return data
}
},
/**
* 复制列表
*/
copyList() {
uni.setClipboardData({
data: JSON.stringify(this.item.list),
})
},
/**
* 长按事件
*/
consoleLongpress() {
let that = this
let menu = [
{
text: `复制日志信息`,
click() {
uni.setClipboardData({
data: JSON.stringify(that.item),
})
},
},
{
text: `删除此记录`,
click() {
uni.$emit('devTools_delConsoleItem', that.item)
uni.showToast({
title: '删除成功!',
icon: 'success',
})
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.consoleItem:active {
background-color: rgba(0, 0, 0, 0.03);
}
.consoleItem {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&.type-warn {
background-color: rgb(255, 251, 229);
}
&.type-error {
background-color: rgb(255, 240, 240);
}
&.type-info {
background-color: rgba(0, 0, 0, 0.02);
}
.content {
width: 610rpx;
display: flex;
flex-direction: column;
.context {
font-size: 20rpx;
color: #333;
line-height: 24rpx;
&.t-number {
color: rgb(8, 66, 160);
}
&.t-boolean {
color: rgb(133, 2, 255);
}
&.t-string {
color: #333;
}
&.t-undefined {
color: rgba(0, 0, 0, 0.2);
}
&.t-null {
color: rgba(0, 0, 0, 0.2);
}
&.t-line {
color: rgba(0, 0, 0, 0.5);
}
}
.msgBar {
display: flex;
flex-direction: row;
margin-top: 4rpx;
.time {
font-size: 16rpx;
color: #888;
/* #ifndef APP-PLUS */
min-width: 90rpx;
/* #endif */
}
.page {
font-size: 16rpx;
color: #888;
margin-left: 20rpx;
}
.logType {
margin-left: 20rpx;
font-size: 16rpx;
padding: 0px 6rpx;
border-radius: 2px;
}
.type-log {
color: #fff;
background-color: #a8abb3;
}
.type-info {
color: #fff;
background-color: #747474;
}
.type-warn {
color: #fff;
background-color: #ff9900;
}
.type-error {
color: #fff;
background-color: #fa3534;
}
}
}
.tools {
width: 100rpx;
display: flex;
flex-direction: row-reverse;
margin-top: 6rpx;
.copy {
font-size: 20rpx;
color: #333;
line-height: 24rpx;
}
.copy:active {
background-color: red;
}
}
}
.copyBtn:active {
background-color: rgba(103, 194, 58, 0.6);
}
.copyBtn {
padding: 5rpx;
border-radius: 999rpx;
overflow: hidden;
background-color: #67c23a;
.copyIcon {
width: 20rpx;
height: 20rpx;
}
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<view class="dayOnlineItem" @click.stop="$emit('click')" @longpress.stop="logLongpress">
<view class="info">
<text class="text-xs">{{ item.date }}</text>
<text class="text-xs margin-left">{{ item.timeCount }}</text>
</view>
<view class="arrow">
<image class="icon" src="@/devTools/page/static/fold.png" />
</view>
</view>
</template>
<script>
export default {
props: {
/**
* logs单行数据
*/
item: {
type: Object,
default() {
return {
date: '', //
timeCount: '', //
page: [], //
}
},
},
},
methods: {
/**
* 长按事件
*/
logLongpress() {
let that = this
let menu = [
{
text: `复制日志信息`,
click() {
uni.setClipboardData({
data: JSON.stringify(that.item),
})
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.dayOnlineItem:active {
background-color: rgba(0, 0, 0, 0.03);
}
.dayOnlineItem {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.info {
width: 610rpx;
display: flex;
flex-direction: row;
align-items: center;
.text-xs {
font-size: 20rpx;
}
.margin-left {
margin-left: 30rpx;
}
}
.arrow {
.icon {
width: 20rpx;
height: 20rpx;
transform: rotate(90deg);
}
}
}
</style>

View File

@ -0,0 +1,265 @@
<template>
<view class="errorItem" :class="['type-' + item.type]" @longpress.stop="errorLongpress">
<view class="content">
<view v-for="(row, index) in [item.m, item.tr]" :key="index">
<template v-if="isObj(row)">
<objectAnalysis
:data="row"
:width="610"
:canLongpress="false"
@onLongpress="errorLongpress"
/>
</template>
<template v-else>
<view>
<text class="context">{{ getText(row) }}</text>
</view>
</template>
</view>
<view class="msgBar">
<text class="time">{{ item.date }}</text>
<text class="logType" :class="['type-' + item.type]">
{{ getType(item.type) }}
</text>
<text class="page">{{ item.p }}</text>
</view>
</view>
<view class="tools">
<view class="copyBtn" @click="copyList">
<image src="@/devTools/page/static/copy.png" class="copyIcon" />
</view>
</view>
</view>
</template>
<script>
import devCache from '../../../core/libs/devCache'
import objectAnalysis from './objectAnalysis.vue'
export default {
components: {
objectAnalysis,
},
props: {
/**
* console单行数据
*/
item: {
type: Object,
default() {
return {
m: '',
tr: '',
date: '', //
p: '', //
type: '', //
}
},
},
},
methods: {
/**
* 是否为对象类型
*/
isObj(data) {
return typeof data == 'object'
},
/**
* 获取数据文字
*/
getText(data) {
switch (typeof data) {
case 'string':
return data
case 'boolean':
return data ? 'true' : 'false'
case 'undefined':
return 'undefined'
case 'function':
return 'js:function'
case 'symbol':
return 'js:symbol'
default:
return data
}
},
/**
* 复制列表
*/
copyList() {
let that = this
uni.setClipboardData({
data: JSON.stringify([that.item.m, that.item.tr]),
})
},
/**
* 获取类型
*/
getType(type) {
let t = {
ve: 'vue error',
vw: 'vue warn',
oe: 'App.vue onError',
n: 'unknown',
}
return t[type]
},
/**
* 长按事件
*/
errorLongpress() {
let that = this
let menu = [
{
text: `复制日志信息`,
click() {
uni.setClipboardData({
data: JSON.stringify(that.item),
})
},
},
{
text: `删除此记录`,
click() {
uni.$emit('devTools_delError', that.item)
let logs = devCache.get('errorReport')
if (!logs) logs = []
let i = logs.findIndex(
(x) =>
x.type == that.item.type &&
x.t == that.item.t &&
x.m == that.item.m &&
x.tr == that.item.tr &&
x.p == that.item.p,
)
if (i != -1) {
logs.splice(i, 1)
devCache.set('errorReport', logs)
}
uni.showToast({
title: '删除成功!',
icon: 'success',
})
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.errorItem:active {
background-color: rgba(0, 0, 0, 0.03);
}
.errorItem {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&.type-vw {
background-color: rgb(255, 251, 229);
}
&.type-ve {
background-color: rgb(255, 240, 240);
}
&.type-oe {
background-color: rgb(255, 240, 240);
}
.content {
width: 670rpx;
display: flex;
flex-direction: column;
.context {
font-size: 20rpx;
color: #333;
line-height: 24rpx;
}
.msgBar {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.time {
font-size: 16rpx;
color: #888;
/* #ifndef APP-PLUS */
min-width: 90rpx;
/* #endif */
}
.page {
font-size: 16rpx;
color: #888;
margin-left: 20rpx;
lines: 1;
overflow: hidden;
/* #ifndef APP-PLUS */
max-width: 450rpx;
white-space: nowrap;
/* #endif */
/* #ifdef APP-PLUS */
width: 450rpx;
/* #endif */
text-overflow: ellipsis;
}
.logType {
margin-left: 20rpx;
font-size: 16rpx;
padding: 0px 6rpx;
border-radius: 2px;
}
.type-ve {
color: #fff;
background-color: #fd0add;
}
.type-n {
color: #fff;
background-color: #82848a;
}
.type-vw {
color: #fff;
background-color: #ff9900;
}
.type-oe {
color: #fff;
background-color: #ff0000;
}
}
}
.tools {
width: 40rpx;
display: flex;
flex-direction: row-reverse;
margin-top: 6rpx;
.copy {
font-size: 20rpx;
color: #333;
line-height: 24rpx;
}
.copy:active {
background-color: red;
}
}
}
.copyBtn:active {
background-color: rgba(103, 194, 58, 0.6);
}
.copyBtn {
padding: 5rpx;
border-radius: 999rpx;
overflow: hidden;
background-color: #67c23a;
.copyIcon {
width: 20rpx;
height: 20rpx;
}
}
</style>

View File

@ -0,0 +1,380 @@
<template>
<view class="fileItem" @click="fileClick" @longpress.stop="longpress">
<view class="icon">
<image class="iconImg" :src="icon" mode="aspectFit" />
</view>
<view class="fileInfo">
<text
class="fileName"
:style="{
color: item.type == 'back' ? '#999' : '#333',
}"
>
{{ item.name }}
</text>
<view v-if="item.type != 'back'" class="fileMeta">
<text class="textItem">{{ item.date }}</text>
<text v-if="item.type == 'file'" class="textItem">
{{ item.sizeText }}
</text>
</view>
</view>
</view>
</template>
<script>
import appDelDir from '../libs/appDelDir'
export default {
props: {
/**
* 当前文件路径类型
*/
dirType: {
type: String,
default: '',
},
/**
* 文件路径列表
*/
dirList: {
type: Array,
default: () => [],
},
/**
* 单个列表对象
*/
item: {
type: Object,
default() {
return {
type: '', // dir back file
name: '',
fileType: '', //
size: '', //
icon: '', //
time: '', //
date: '',
fileCount: 0, //
directoryCount: 0, //
}
},
},
},
computed: {
icon() {
// #ifdef APP-PLUS
// IOS访
if (
uni.getSystemInfoSync().platform == 'android' &&
this.item.type == 'file' &&
['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].indexOf(this.item.fileType) > -1
) {
let path = this.getPath()
return path
}
// #endif
return this.item.icon
},
},
data() {
return {}
},
methods: {
/**
* 点击事件
*/
fileClick() {
let that = this
if (this.item.type == 'dir') {
this.$emit('goChildDir', this.item.name)
} else if (this.item.type == 'back') {
this.$emit('goChildDir', '__back__')
} else {
if (
//? 使
['txt', 'sql', 'js', 'css', 'html', 'log', 'json'].indexOf(this.item.fileType) != -1
) {
this.openInEdit()
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].indexOf(this.item.fileType) != -1) {
let path = that.getPath()
uni.previewImage({
urls: [path],
})
} else {
this.longpress()
}
}
},
/**
* 使用文本编辑器打开
*/
openInEdit() {
let that = this
let path = that.getPath()
that.$emit('openEditFileDialog', {
title: that.item.name,
canSave: that.dirType != 'PRIVATE_WWW',
path,
size: that.item.size,
})
},
/**
* 获取当前文件绝对路径
*/
getPath() {
let that = this
let path = ''
switch (that.dirType) {
case 'wx':
path = wx.env.USER_DATA_PATH
break
case 'PRIVATE_DOC':
path = '_doc'
break
case 'PRIVATE_WWW':
path = '_www'
break
case 'PUBLIC_DOCUMENTS':
path = '_documents'
break
case 'PUBLIC_DOWNLOADS':
path = '_downloads'
break
default:
break
}
that.dirList.map((x) => {
path += '/' + x
})
path = path + '/' + that.item.name
return path
},
/**
* 长按事件
*/
longpress() {
let that = this
let path = that.getPath()
let menu = [
{
text: `复制绝对路径`,
click() {
// #ifdef APP-PLUS
path = plus.io.convertLocalFileSystemURL(path)
// #endif
uni.setClipboardData({
data: path,
})
},
},
{
text: `删除`,
click() {
uni.showModal({
title: '警告',
content:
'是否确认删除' +
(that.item.type == 'dir' ? '文件夹:' : '文件:') +
that.item.name +
'?',
success(res) {
if (res.confirm) {
uni.showLoading({
title: '删除中',
})
function delSuccess() {
uni.hideLoading()
uni.showToast({
title: '删除成功!',
icon: 'success',
})
that.$emit('getPage')
}
function delError() {
uni.hideLoading()
uni.showToast({
title: '删除失败',
icon: 'none',
})
}
// #ifdef MP-WEIXIN
if (1) {
let fs = wx.getFileSystemManager()
if (that.item.type == 'file') {
// !
fs.unlink({
filePath: path,
success: delSuccess,
fail: delError,
})
} else {
// !
fs.rmdir({
dirPath: path,
recursive: true,
success: delSuccess,
fail: delError,
})
}
return
}
// #endif
if (that.item.type == 'file') {
// !
plus.io.resolveLocalFileSystemURL(
path,
(entry) => {
// entrytest.html
entry.remove(delSuccess, delError)
},
delError,
)
} else {
// !
appDelDir(path + '/')
.then(delSuccess)
.catch(delError)
}
}
},
})
},
},
]
let isMp = false
// #ifdef MP-WEIXIN
isMp = true
// #endif
if (!isMp || that.item.type != 'dir') {
menu.push({
text: '重命名',
click() {
that.$emit('editDirName', {
isDir: that.item.type == 'dir',
isEdit: true,
name: that.item.name,
})
},
})
}
// #ifdef APP-PLUS
if (that.item.type == 'file') {
menu.push({
text: '调用外部程序打开此文件',
click() {
plus.runtime.openFile(path, null, (e) => {
uni.showToast({
title: '文档打开失败!' + e.message,
icon: 'none',
})
})
},
})
}
// #endif
// #ifdef MP-WEIXIN
if (['doc', 'xls', 'ppt', 'pdf', 'docx', 'xlsx', 'pptx'].indexOf(that.item.fileType) != -1) {
menu.unshift({
text: '打开该文档',
click() {
let path = that.getPath()
uni.openDocument({
filePath: path,
fail() {
uni.showToast({
title: '文档打开失败!',
icon: 'none',
})
},
})
},
})
}
// #endif
if (that.item.type == 'file') {
menu.unshift({
text: `用文本编辑器打开`,
click() {
that.openInEdit()
},
})
}
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.fileItem {
display: flex;
flex-direction: row;
align-items: center;
padding: 0rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
width: 750rpx;
/* #ifndef APP-PLUS */
min-height: 70rpx;
/* #endif */
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
.icon {
width: 50rpx;
height: 50rpx;
border-radius: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.05);
.iconImg {
width: 40rpx;
height: 40rpx;
}
}
.fileInfo {
margin-left: 10rpx;
width: 650rpx;
display: flex;
flex-direction: column;
.fileName {
width: 650rpx;
lines: 1;
overflow: hidden;
font-size: 24rpx;
color: #333;
line-height: 28rpx;
}
.fileMeta {
margin-top: 5rpx;
display: flex;
flex-direction: row;
align-items: center;
width: 650rpx;
.textItem {
margin-right: 20rpx;
font-size: 20rpx;
line-height: 26rpx;
color: #999;
}
}
}
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<view class="storageList">
<objectAnalysis v-if="isLoaded" :data="data" :isOpenFirst="true" :width="710" />
<view v-else class="dataLoading">
<text class="status">加载中</text>
</view>
</view>
</template>
<script>
import objectAnalysis from './objectAnalysis.vue'
import getRuntimeInfo from '../libs/getRuntimeInfo'
export default {
components: {
objectAnalysis,
},
data() {
return {
/**
* 是否完成加载
*/
isLoaded: false,
/**
* 缓存里的数据
*/
data: {},
}
},
methods: {
/**
* 加载数据
*/
async getData() {
let that = this
that.isLoaded = false
let data = await getRuntimeInfo()
setTimeout(() => {
that.data = data
that.isLoaded = true
}, 500)
},
},
}
</script>
<style lang="scss" scoped>
.storageList {
padding: 20rpx;
width: 750rpx;
}
.dataLoading {
width: 750rpx;
height: 400rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.status {
font-size: 20rpx;
color: #888;
line-height: 20rpx;
}
}
</style>

View File

@ -0,0 +1,197 @@
<template>
<view class="jsRunnerItem">
<view class="codeInput">
<image src="@/devTools/page/static/fold.png" class="fold-left" />
<view class="codeView">
<text class="codeText">{{ item.code }}</text>
</view>
</view>
<view class="codeResult">
<image src="@/devTools/page/static/fold.png" class="fold-right" />
<view class="codeResultView">
<template v-if="item.isEnd">
<template v-if="isObj(item.result)">
<objectAnalysis
:data="item.result"
:showObjProto="true"
:width="610"
:canLongpress="false"
/>
</template>
<template v-else>
<view>
<text class="context" :class="[getTypeClass(item.result)]" selectable>
{{ getText(item.result) }}
</text>
</view>
</template>
</template>
<text v-else class="loadingOutput">...</text>
</view>
</view>
</view>
</template>
<script>
import objectAnalysis from './objectAnalysis.vue'
export default {
components: {
objectAnalysis,
},
props: {
/**
* 单行数据
*/
item: {
type: Object,
default() {
return {
id: '',
code: '',
result: '',
isEnd: false,
}
},
},
},
methods: {
/**
* 是否为对象类型
*/
isObj(data) {
return typeof data == 'object' && data != null && data != undefined
},
/**
* 获取对应的类型样式
*/
getTypeClass(obj) {
try {
let type = typeof obj
if (type == 'string') {
if (obj.indexOf('at ') == 0) {
return 't-line'
} else if (obj === undefined || obj == 'undefined') {
return 't-undefined'
} else if (obj === null || obj == 'null') {
return 't-null'
} else if (obj == 'true' || obj == 'false') {
return 't-boolean'
} else if (Number.isFinite(Number(obj))) {
return 't-number'
}
} else if (type == 'function') {
return 't-function'
}
return 't-' + type
} catch (error) {}
return 't-string'
},
/**
* 获取数据文字
*/
getText(data) {
if (data === null) {
return 'null'
}
switch (typeof data) {
case 'string':
// return data.replace(/\n/g, "");
return data
case 'boolean':
return data ? 'true' : 'false'
case 'undefined':
return 'undefined'
case 'function':
return data.toString()
case 'symbol':
return 'js:symbol'
default:
return data
}
},
},
}
</script>
<style lang="scss" scoped>
.jsRunnerItem:active {
// background-color: rgba(0, 0, 0, 0.03);
}
.jsRunnerItem {
display: flex;
flex-direction: column;
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.codeInput {
width: 710rpx;
display: flex;
flex-direction: row;
margin-bottom: 10rpx;
.codeView {
margin-left: 10rpx;
width: 670rpx;
display: flex;
flex-direction: column;
.codeText {
font-size: 20rpx;
color: #777;
line-height: 26rpx;
}
}
}
.codeResult {
width: 710rpx;
display: flex;
flex-direction: row;
border-top: 1px solid rgba(0, 0, 0, 0.02);
padding-top: 10rpx;
.codeResultView {
margin-left: 10rpx;
width: 670rpx;
display: flex;
flex-direction: column;
.loadingOutput {
font-size: 20rpx;
color: rgba(0, 0, 0, 0.1);
line-height: 26rpx;
}
.context {
font-size: 20rpx;
color: rgb(227, 54, 46);
line-height: 26rpx;
&.t-number {
color: rgb(8, 66, 160);
}
&.t-boolean {
color: rgb(133, 2, 255);
}
&.t-string {
color: rgb(227, 54, 46);
}
&.t-undefined {
color: rgba(0, 0, 0, 0.2);
}
&.t-null {
color: rgba(0, 0, 0, 0.2);
}
&.t-line {
color: rgba(0, 0, 0, 0.5);
}
&.t-function {
color: rgb(121, 38, 117);
}
}
}
}
}
.fold-left {
transform: rotate(90deg);
width: 24rpx;
height: 24rpx;
}
.fold-right {
transform: rotate(-90deg);
width: 24rpx;
height: 24rpx;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<view class="logItem" @longpress.stop="logLongpress">
<view class="content">
<view>
<text class="text-xs">{{ item.m }}</text>
</view>
<view class="msgBar">
<text class="time">{{ item.date }}</text>
</view>
</view>
<view class="tools">
<view class="copyBtn" @click="copyItem">
<image src="@/devTools/page/static/copy.png" class="copyIcon" />
</view>
</view>
</view>
</template>
<script>
import devCache from '../../../core/libs/devCache'
export default {
props: {
/**
* logs单行数据
*/
item: {
type: Object,
default() {
return {
date: '',
m: '', //
}
},
},
},
methods: {
/**
* 复制
*/
copyItem() {
uni.setClipboardData({
data: this.item.m,
})
},
/**
* 长按事件
*/
logLongpress() {
let that = this
let menu = [
{
text: `复制日志信息`,
click() {
uni.setClipboardData({
data: JSON.stringify(that.item),
})
},
},
{
text: `删除此记录`,
click() {
uni.$emit('devTools_delLog', that.item)
let logs = devCache.get('logReport')
if (!logs) logs = []
let i = logs.findIndex((x) => x.m == that.item.m && x.t == that.item.t)
if (i != -1) {
logs.splice(i, 1)
devCache.set('logReport', logs)
}
uni.showToast({
title: '删除成功!',
icon: 'success',
})
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.logItem:active {
background-color: rgba(0, 0, 0, 0.03);
}
.logItem {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.content {
width: 610rpx;
display: flex;
flex-direction: column;
.context {
font-size: 20rpx;
color: #333;
line-height: 24rpx;
}
.msgBar {
display: flex;
flex-direction: row;
.time {
font-size: 16rpx;
color: #888;
}
.page {
font-size: 16rpx;
color: #888;
margin-left: 20rpx;
}
}
}
}
.copyBtn:active {
background-color: rgba(103, 194, 58, 0.6);
}
.copyBtn {
padding: 5rpx;
border-radius: 999rpx;
overflow: hidden;
background-color: #67c23a;
.copyIcon {
width: 20rpx;
height: 20rpx;
}
}
.text-xs {
font-size: 20rpx;
}
</style>

View File

@ -0,0 +1,330 @@
<template>
<view class="networkItem" :class="['type-' + item.type]" @longpress.stop="networkLongpress">
<view class="content">
<view class="head">
<view class="method" :class="'type-' + item.method">
<text class="methodText" :class="'type-' + item.method">
{{ item.method }}
</text>
</view>
<view class="path">
<text class="pageText">{{ getPath(item.url) }}</text>
</view>
</view>
<objectAnalysis
:data="getItem(item)"
:width="710"
:canLongpress="false"
@onLongpress="networkLongpress"
/>
<view class="msgBar">
<text class="data">
{{ item.date }}
</text>
<text
class="time"
:style="{
width: '100rpx',
color: getTimeColor,
}"
>
{{ item.useTime }}s
</text>
<text class="status" :class="'s-' + item.type" style="width: 120rpx">
{{ getTypeName(item.type) }}
</text>
<text
v-if="item.type == 1"
class="time"
:style="{
color: getSizeColor,
}"
>
{{ getByteSize }}
</text>
</view>
</view>
</view>
</template>
<script>
import objectAnalysis from './objectAnalysis.vue'
export default {
components: {
objectAnalysis,
},
props: {
/**
* console单行数据
*/
item: {
type: Object,
default() {
return {
id: 0, //id
type: 0, // 0 1 2
date: '', //
sendTime: 0, //
responseTime: 0, //
useTime: 0, //
url: '', //
header: '', //
method: 'get', //
data: '', //
responseBody: '', //
responseHeader: '', //
responseStatus: '', //
responseMsg: '', //
responseBodySize: 0, //
}
},
},
},
computed: {
/**
* 获取请求时间的颜色
*/
getTimeColor() {
if (this.item.useTime == 0) {
return '#eeeeee'
} else if (this.item.useTime > 3) {
return '#fa3534'
} else if (this.item.useTime > 1) {
return '#ff9900'
} else {
return '#909399'
}
},
/**
* 获取字节大小,b转kb mb
*/
getByteSize() {
let size = Number(this.item.responseBodySize)
if (null == size || size == '') return '0.00 KB'
var unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var index = 0
var srcsize = parseFloat(size)
index = Math.floor(Math.log(srcsize) / Math.log(1024))
size = srcsize / Math.pow(1024, index)
size = size.toFixed(2) //
if (Number(this.item.responseBodySize) < 1024) {
return (size / 1000).toFixed(2) + 'KB'
}
return size + unitArr[index]
},
/**
* 获取响应大小的颜色
*/
getSizeColor() {
let size = this.item.responseBodySize
if (size == 0) {
return '#fa3534'
} else if (size > 256 * 1024) {
return '#ff9900'
} else if (size > 1024 * 1024) {
return '#fa3534'
} else {
return '#909399'
}
},
},
methods: {
/**
* 通过url获取路径
*/
getPath(url) {
if (!url) return '无'
function getPathFromUrl(url) {
const pathStart = url.indexOf('//') + 2
const pathEnd = url.indexOf('?', pathStart) >= 0 ? url.indexOf('?', pathStart) : url.length
return url.substring(url.indexOf('/', pathStart), pathEnd)
}
return getPathFromUrl(url)
},
/**
* 获取请求类型名称
*/
getTypeName(type) {
return ['请求中...', '请求完成', '请求失败'][Number(type)]
},
/**
* 精简item
*/
getItem(item) {
return {
data: item.data,
responseMsg: item.responseMsg,
responseStatus: item.responseStatus,
method: item.method,
url: item.url,
header: item.header,
responseBody: item.responseBody,
responseHeader: item.responseHeader,
responseBodySize: this.getByteSize,
}
},
/**
* 长按事件
*/
networkLongpress() {
let that = this
let menu = [
{
text: `重发此请求`,
click() {
that.$emit('goSendRequest', that.item)
},
},
{
text: `复制请求日志信息`,
click() {
uni.setClipboardData({
data: JSON.stringify(that.item),
})
},
},
{
text: `在请求构建工具中打开`,
click() {
that.$emit('goOpenRequest', that.item)
},
},
{
text: `删除此记录`,
click() {
uni.$emit('devTools_delNetworkItemById', that.item.id)
uni.showToast({
title: '删除成功!',
icon: 'success',
})
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.networkItem:active {
background-color: rgba(0, 0, 0, 0.03);
}
.networkItem {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&.type-0 {
background-color: rgb(255, 251, 229);
}
&.type-2 {
background-color: rgb(255, 240, 240);
}
.content {
width: 710rpx;
display: flex;
flex-direction: column;
.head {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 5rpx;
.method {
background-color: #e2e2e2;
color: #333;
border-radius: 4rpx;
padding: 4rpx 6rpx;
border-radius: 10rpx;
&.type-get {
background-color: rgba(168, 25, 197, 0.1);
}
&.type-post {
background-color: rgba(255, 217, 0, 0.1);
}
.methodText {
color: #333;
font-size: 22rpx;
line-height: 22rpx;
/* #ifndef APP-PLUS */
max-width: 650rpx;
/* #endif */
&.type-get {
// color: #fff;
}
&.type-post {
// color: #fff;
}
}
}
.path {
width: 620rpx;
/* #ifdef APP-PLUS */
width: 620rpx;
/* #endif */
/* #ifndef APP-PLUS */
max-width: 620rpx;
/* #endif */
lines: 1;
margin-left: 6rpx;
overflow: hidden;
/* #ifdef H5 */
//
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/* #endif */
.pageText {
font-size: 24rpx;
color: #333;
}
}
}
.context {
font-size: 20rpx;
color: #333;
line-height: 24rpx;
}
.msgBar {
display: flex;
flex-direction: row;
margin-top: 5rpx;
.data {
font-size: 16rpx;
color: #888;
/* #ifndef APP-PLUS */
min-width: 90rpx;
/* #endif */
}
.time {
font-size: 16rpx;
color: #333;
margin-left: 20rpx;
}
.status {
margin-left: 20rpx;
font-size: 16rpx;
&.s-0 {
color: #fa3534;
}
&.s-1 {
color: #909399;
}
&.s-2 {
color: #ff9900;
}
}
}
}
}
</style>

View File

@ -0,0 +1,731 @@
<template>
<view
class="objectAnalysis"
:style="{
width: width + 'rpx',
}"
>
<view
class="objectTitle"
:class="canLongpress ? 'objectTitleActive' : ''"
@click="titleClick"
@longpress.stop="faLongpress"
>
<image class="foldItem" v-if="isOpen" src="@/devTools/page/static/fold.png" />
<image class="foldItem" v-else src="@/devTools/page/static/unfold.png" />
<text
class="title"
:style="{
width: width - 50 + 'rpx',
}"
>
{{ title }}
</text>
</view>
<view
class="objectList"
v-if="isOpen"
:style="{
width: width + 'rpx',
}"
>
<view
v-for="(item, index) in list"
:key="item.i"
class="listItem"
:style="{
marginLeft: item.l + 'rpx',
}"
@click="rowClick(item, index)"
@longpress.stop="rowLongpress($event, item, index)"
>
<template v-if="item.t == 'array' || item.t == 'object'">
<image class="foldItem" v-if="item.o" src="@/devTools/page/static/fold.png" />
<image class="foldItem" v-else src="@/devTools/page/static/unfold.png" />
</template>
<view v-else class="emptyFold"></view>
<text
class="objKey"
:class="'t-' + item.t"
:style="{
maxWidth: (width - item.l - 40) / 2 + 'rpx',
}"
>
{{ item.k }}:
</text>
<text class="objValue" :class="'t-' + item.t">
{{ item.vt }}
</text>
</view>
</view>
</view>
</template>
<script>
import jsonCompress from '../../../core/libs/jsonCompress'
function getType(v) {
return Object.prototype.toString.call(v).slice(8, -1).toLocaleLowerCase()
}
function randId() {
return Math.ceil(Math.random() * 1000000000000000)
}
export default {
props: {
/**
* 需要解析的对象数据
*/
data: '',
/**
* 组件宽度 rpx
*/
width: {
type: Number,
default: 610,
},
/**
* 是否默认展开第一层
*/
isOpenFirst: {
type: Boolean,
default: false,
},
/**
* 是否自定义长按的菜单
*/
isDiyMenu: {
type: Boolean,
default: false,
},
/**
* 是否响应最外层的长按事件
*/
canLongpress: {
type: Boolean,
default: true,
},
/**
* 是否展示完整的对象类型
*/
showObjProto: {
type: Boolean,
default: false,
},
},
data() {
return {
/**
* 对象类型 array object map unknown
*/
type: 'unknown',
/**
* 对象标题
*/
title: '',
/**
* 是否为开启节点状态
*/
isOpen: false,
/**
* 渲染的列表
*/
list: [
{
t: 'text', //
k: '键名', // key
v: '名称', // value
vt: '', //view
i: 's', //id
p: '0', //id
o: true, //
l: 0, //
d: null, //
},
],
}
},
mounted() {
let that = this
try {
let { title } = that.getObjType(this.data)
that.list = []
that.title = title
if (that.isOpenFirst) {
that.titleClick()
}
} catch (error) {
console.log('objectAnalysis error', error)
}
},
methods: {
/**
* 标题点击事件
*/
titleClick() {
if (this.isOpen) {
this.isOpen = false
} else {
if (this.list.length == 0) {
this.analysisData(this.data)
}
this.isOpen = true
}
},
/**
* 解析渲染数组
*/
analysisData(data, pid = 0) {
let list = []
let l = this.getParentNum(pid)
let keys = []
keys = Reflect.ownKeys(data)
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
let value = data[key]
if (key == '__proto__' || key == '__ob__') {
continue
}
let o = {
t: typeof value,
k: key,
v: value,
vt: '',
i: randId(),
p: pid,
o: false,
l: l * 5,
d: value,
}
try {
let t = typeof value
if (t == 'function') {
try {
o.vt = value.toString()
} catch (error) {
if (error && error.message) {
o.vt = '[js:function]' + error.message
} else {
o.vt = '[js:function]'
}
}
o.v = o.vt
o.d = ''
} else if (t == 'object') {
if (this.showObjProto) {
let str = 'unknown'
if (value === null) {
o.t = 'null'
str = 'null'
} else if (Array.isArray(value)) {
o.t = 'array'
let l = 0
try {
l = value.length
} catch (error) {}
str =
Object.prototype.toString.call(value).slice(8, -1) +
(l > 0 ? ` (${l})[...]` : ' (0)[]')
} else {
o.t = 'object'
let childList = []
try {
childList = Reflect.ownKeys(value)
} catch (error) {}
str =
Object.prototype.toString.call(value).slice(8, -1) +
(childList.length == 0 ? ' {}' : ' {...}')
}
o.vt = str
} else {
let type = getType(value)
let title = ''
try {
title = JSON.stringify(value)
if (title.length > 50) {
title = title.slice(0, 50) + '...'
}
if (type == 'array' && value.length > 0) {
title = '(' + value.length + ')' + title
}
} catch (error) {
title = '对象解析失败:' + error
}
o.t = type
o.vt = title
o.v = value
}
} else if (t == 'symbol') {
o.t = 'symbol'
try {
if (value.toString) {
o.vt = value.toString()
} else {
o.vt = '[js:symbol]'
}
} catch (error) {
let msg = ''
if (error && error.message) {
msg = error.message
} else {
msg = '[js:symbol解析失败]'
}
o.vt = msg
}
} else if (t == 'string') {
if (value.length > 200) {
o.vt = `"` + value.slice(0, 200) + '...' + '"'
} else {
o.vt = `"${value}"`
}
} else if (t == 'number') {
if (Number.isFinite(value)) {
o.vt = value.toString()
} else {
o.vt = isNaN(value) ? 'NaN' : 'Infinity'
}
} else if (t == 'boolean') {
o.vt = value ? 'true' : 'false'
} else if (t == 'undefined') {
o.vt = 'undefined'
} else {
o.vt = '[js:unknown type]'
}
} catch (error) {
let msg = ''
if (error && error.message) {
msg = error.message
} else {
msg = '[js对象解析失败]'
}
o.vt = msg
}
list.push(o)
}
if (pid == 0) {
this.list = list
} else {
let faIndex = this.list.findIndex((x) => x.i == pid) + 1
for (let i = 0; i < list.length; i++) {
this.list.splice(faIndex, 0, list[i])
faIndex++
}
}
},
/**
* 获取节点的父类数量
*/
getParentNum(pid) {
let that = this
let count = 0
if (pid == 0) {
return count
} else {
let p = Number(pid)
while (true) {
count = count + 1
let fa = that.list.find((x) => x.i == p)
if (!fa || fa.i == 0) {
break
} else {
p = Number(fa.p)
}
}
}
return count
},
/**
* 行对象点击事件
*/
rowClick(item, index) {
let that = this
const nodeItem = that.list[index]
if (item.t == 'object' || item.t == 'array') {
if (item.o) {
nodeItem.o = false
that.hideListByPid(item.i)
} else {
nodeItem.o = true
that.analysisData(nodeItem.d, item.i)
}
} else if (item.t == 'string' && item.v.length > 100) {
//
uni.$emit('devTools_showTextEditDialog', {
title: item.k,
canSave: false,
isFileEdit: false,
value: item.v,
})
}
},
/**
* 根据父类id删除数组内元素
*/
hideListByPid(pid = 0) {
let that = this
while (true) {
let i = that.list.findIndex((x) => x.p == pid)
if (i == -1) {
break
}
if (that.list[i].o) {
that.hideListByPid(that.list[i].i)
}
that.list.splice(i, 1)
}
},
/**
* 长按事件
*/
rowLongpress(e, item, index) {
// #ifdef APP-PLUS
if (e && e.stopPropagation) {
e.stopPropagation()
}
// #endif
let that = this
if (that.isDiyMenu) {
that.$emit('diyMenu', { item, index })
} else {
let k = this.toString(item.k)
if (k.length > 20) {
k = k.slice(0, 20) + '...'
}
let v = this.toString(item.v)
if (v.length > 20) {
v = v.slice(0, 20) + '...'
}
uni.showActionSheet({
itemList: [`复制键(${k})`, `复制值(${v})`],
success({ tapIndex }) {
if (tapIndex == 0) {
uni.setClipboardData({
data: that.toString(item.k),
})
} else {
uni.setClipboardData({
data: that.toString(item.v),
})
}
},
})
}
},
/**
* 尝试转字符串
*/
toString(data) {
try {
if (data === undefined) return 'undefined'
if (data === null) return 'null'
if (typeof data == 'boolean') return data ? 'true' : 'false'
if (typeof data == 'object') {
return JSON.stringify(data)
}
return data.toString()
} catch (error) {
return '尝试解析失败!' + error
}
},
/**
* 获取列表
*/
getList() {
return this.list
},
/**
* 获取父级key类别
*/
getFaKeyList(itemId) {
let keyList = []
let item = this.list.find((x) => x.i == itemId)
if (!item) return keyList
keyList = [item.k]
if (item.p == 0) return keyList
while (true) {
item = this.list.find((x) => x.i == item.p)
if (!item) break
keyList.unshift(item.k)
if (item.p == 0) {
break
}
}
return keyList
},
/**
* 长按复制一整个对象
*/
faLongpress(e) {
// #ifdef APP-PLUS
if (e && e.stopPropagation) {
e.stopPropagation()
}
// #endif
let that = this
if (that.canLongpress) {
uni.setClipboardData({
data: JSON.stringify(that.data),
})
} else {
that.$emit('onLongpress')
}
},
/**
* 获取对象单行数据
*/
getObjType(obj) {
try {
let title = 'unknown'
let type = typeof obj
let data = obj
switch (typeof data) {
case 'symbol':
title = '[js:symbol]'
try {
if (data.toString) {
title = data.toString()
} else {
title = '[js:symbol]'
}
} catch (error) {
let msg = ''
if (error && error.message) {
msg = error.message
} else {
msg = '[js:symbol解析失败]'
}
title = msg
}
break
case 'string':
title = data
break
case 'object':
if (this.showObjProto) {
try {
let objType = Object.prototype.toString.call(data).slice(8, -1)
title = {}
let keys = Reflect.ownKeys(data)
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
if (key == '__proto__' || key == '__ob__') {
continue
}
try {
let value = data[key]
let t = typeof value
if (t == 'function') {
continue
}
if (t == 'object') {
let str = 'unknown'
if (value === null) {
str = 'null'
} else if (Array.isArray(value)) {
str = Object.prototype.toString.call(value).slice(8, -1) + ' [...]'
} else {
str = Object.prototype.toString.call(value).slice(8, -1) + ' {...}'
}
title[key] = str
continue
}
title[key] = data[key]
} catch (error) {
if (error && error.message) {
title[key] = error.message
} else {
title[key] = '[js对象解析失败]'
}
}
}
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
try {
let value = data[key]
let t = typeof value
if (t == 'function') {
try {
title[key] = value.toString()
} catch (error) {
if (error && error.message) {
title[key] = '[js:function]' + error.message
} else {
title[key] = '[js:function]'
}
}
}
} catch (error) {
if (error && error.message) {
title[key] = error.message
}
}
}
if (title.toJSON) {
title.toJSON = '[js:function]'
}
if (objType == 'Array') {
title = objType + ' ' + jsonCompress.safeJsonStringify(title)
} else {
title = objType + ' ' + jsonCompress.safeJsonStringify(title)
}
if (Array.isArray(data)) {
type = 'array'
} else {
type = 'object'
}
} catch (error) {
let msg = 'unknown'
if (error && error.message) {
msg = error.message
}
title = '对象解析出错:' + msg
}
} else {
try {
title = JSON.stringify(data)
if (title.length > 50) {
title = title.slice(0, 50) + '...'
}
} catch (error) {
title = '对象解析失败:' + error
}
}
break
case 'function':
try {
title = data.toString()
} catch (error) {
title = '[js:function]'
}
break
default:
title = data
break
}
return { title, type }
} catch (error) {
console.log('getObjType error', error)
return {
title: 'unknown',
type: 'unknown',
}
}
},
},
}
</script>
<style lang="scss" scoped>
.objectAnalysis {
display: flex;
flex-direction: column;
.objectTitle {
display: flex;
flex-direction: row;
align-items: center;
.title {
font-size: 20rpx;
line-height: 20rpx;
color: rgb(89, 74, 154);
lines: 1;
overflow: hidden;
/* #ifdef H5 */
//
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/* #endif */
}
}
.objectTitleActive:active {
background-color: rgba(0, 0, 0, 0.08);
}
.objectList {
background-color: rgba(0, 0, 0, 0.02);
border-radius: 8rpx;
/* #ifndef APP-PLUS */
min-height: 50rpx;
/* #endif */
padding: 10rpx;
.listItem:active {
background-color: rgba(0, 0, 0, 0.08);
}
.listItem {
display: flex;
flex-direction: row;
align-items: center;
.emptyFold {
width: 20rpx;
height: 20rpx;
margin-right: 6rpx;
}
.objKey {
font-size: 20rpx;
line-height: 28rpx;
color: rgb(121, 38, 117);
lines: 1;
overflow: hidden;
/* #ifdef H5 */
//
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/* #endif */
}
.objValue {
line-height: 28rpx;
margin-left: 5rpx;
color: #333;
font-size: 20rpx;
lines: 1;
overflow: hidden;
/* #ifdef H5 */
//
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/* #endif */
&.t-number {
color: rgb(8, 66, 160);
}
&.t-boolean {
color: rgb(133, 2, 255);
}
&.t-string {
color: rgb(227, 54, 46);
}
&.t-array {
color: rgba(0, 0, 0, 0.5);
}
&.t-object {
color: rgba(0, 0, 0, 0.5);
}
&.t-undefined {
color: rgba(0, 0, 0, 0.2);
}
&.t-null {
color: rgba(0, 0, 0, 0.2);
}
}
}
}
}
.foldItem {
width: 20rpx;
height: 20rpx;
background-color: #eee;
border-radius: 4rpx;
margin-right: 6rpx;
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<view class="pageItem" @click.stop="showMenu">
<view class="content">
<view>
<text class="text-xs">页面路由{{ item.route }}</text>
</view>
<view>
<text class="text-xs">停留时长{{ item.timeCount }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
/**
* logs单行数据
*/
item: {
type: Object,
default() {
return {
route: '',
timeCount: '',
}
},
},
},
methods: {
/**
* 展示菜单
*/
showMenu() {
let that = this
let r = String(that.item.route).substring(0, 10) + '...'
let menu = [
{
text: `复制路径(${r})`,
click() {
uni.setClipboardData({
data: that.item.route,
})
},
},
{
text: `复制时间(${that.item.timeCount})`,
click() {
uni.setClipboardData({
data: that.item.timeCount,
})
},
},
{
text: `跳转至此页面`,
click() {
uni.$emit('devTools_showRouteDialog', that.item.route)
},
},
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.pageItem:active {
background-color: rgba(0, 0, 0, 0.03);
}
.pageItem {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.content {
width: 610rpx;
display: flex;
flex-direction: column;
.context {
font-size: 20rpx;
color: #333;
line-height: 24rpx;
}
}
}
.text-xs {
font-size: 20rpx;
}
</style>

View File

@ -0,0 +1,194 @@
<template>
<view v-if="isShow" class="pagesList">
<!-- <objectAnalysis
v-if="isLoaded"
:data="pages"
:isOpenFirst="true"
:width="710"
/> -->
<template v-if="isLoaded">
<view
v-for="(item, index) in pages"
:key="index"
class="pageItem"
@longpress.stop="longpress(item)"
>
<text v-if="pages.length == index + 1" class="t-red">当前</text>
<view class="routeInfo">
<text class="path">{{ item.route }}</text>
<text class="options">{{ item.options }}</text>
</view>
</view>
</template>
<view v-else class="dataLoading">
<text class="status">加载中</text>
</view>
</view>
</template>
<script>
import objectAnalysis from './objectAnalysis.vue'
export default {
components: {
objectAnalysis,
},
props: {
/**
* 是否渲染
*/
isShow: {
type: Boolean,
default: true,
},
},
data() {
return {
/**
* 是否完成加载
*/
isLoaded: false,
/**
* 页面路由数据
*/
pages: [],
}
},
methods: {
/**
* 加载数据
*/
getData() {
let that = this
that.isLoaded = false
let pageList = getCurrentPages().map((x) => {
let options = ''
if (x.options) {
Object.keys(x.options).map((key) => {
options = options + (options == '' ? '' : '&') + key + '=' + x.options[key]
})
}
return {
route: x.route,
options,
}
})
pageList.pop()
that.pages = pageList
that.isLoaded = true
},
/**
* 长按事件
*/
longpress(item) {
let that = this
let menu = [
{
text: `复制路径`,
click() {
uni.setClipboardData({
data: item.route,
})
},
},
...(item.options
? [
{
text: `复制参数`,
click() {
uni.setClipboardData({
data: item.options,
})
},
},
{
text: `复制路径+参数`,
click() {
uni.setClipboardData({
data: item.route + item.options ? '?' + item.options : '',
})
},
},
]
: []),
]
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.pagesList {
width: 750rpx;
display: flex;
flex-direction: column;
.pageItem {
display: flex;
flex-direction: row;
align-items: center;
padding: 15rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
.t-red {
font-size: 20rpx;
color: #fff;
padding: 3rpx 8rpx;
background-color: #ff2d55;
border-radius: 10rpx;
margin-right: 10rpx;
height: 34rpx;
}
.routeInfo {
display: flex;
flex-direction: column;
width: 580rpx;
.path {
font-size: 26rpx;
line-height: 30rpx;
color: #333;
width: 580rpx;
/* #ifndef APP-PLUS */
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
/* #endif */
}
.options {
margin-top: 4rpx;
font-size: 20rpx;
line-height: 26rpx;
color: #888;
width: 580rpx;
/* #ifndef APP-PLUS */
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
/* #endif */
}
}
}
}
.dataLoading {
width: 750rpx;
height: 100rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.status {
font-size: 20rpx;
color: #888;
line-height: 20rpx;
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<view class="routeItem" @click.stop="showMenu">
<view>
<text class="routeText">{{ item.path }}</text>
</view>
</view>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => ({}),
},
},
data() {
return {}
},
methods: {
/**
* 展示菜单
*/
showMenu() {
let that = this
let menuList = []
let p = that.item.path.substr(0, 20)
if (p.length == 20) {
p = p + '...'
}
menuList.push(`复制路径(${p})`)
let isTabBar = false
if (that.item.meta && that.item.meta.isTabBar) {
isTabBar = true
}
if (!isTabBar) {
menuList.push('跳转至此页面')
}
uni.showActionSheet({
itemList: menuList,
success({ tapIndex }) {
if (tapIndex == 0) {
uni.setClipboardData({
data: that.item.path,
})
} else {
uni.$emit('devTools_showRouteDialog', that.item.path)
}
},
})
},
},
}
</script>
<style lang="scss" scoped>
.routeItem {
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
.routeText {
width: 710rpx;
font-size: 20rpx;
line-height: 26rpx;
color: #333;
}
}
.routeItem:active {
background-color: rgba(0, 0, 0, 0.05);
}
</style>

View File

@ -0,0 +1,746 @@
<template>
<view class="settingView">
<view v-if="loading" class="loading">
<text class="loadingText">加载中</text>
</view>
<template v-else>
<subTitleBar
:isOpen="exportIsShow"
@click="exportIsShow = !exportIsShow"
title="导出全部日志"
/>
<template v-if="exportIsShow">
<view class="delBtn" @click="exportJsonFile">
<text class="delBtnText">导出日志文件(.json)</text>
</view>
</template>
<view class="divisionLine"></view>
<subTitleBar
:isOpen="cacheListIsShow"
title="清空全部缓存"
@click="cacheListIsShow = !cacheListIsShow"
/>
<template v-if="cacheListIsShow">
<view
v-for="(item, index) in cacheSelectList"
:key="index"
@click.stop="doSelectCache(index)"
class="checkboxItem"
>
<checkbox :value="item.check ? '1' : '0'" :checked="item.check" color="#ff2d55" />
<text
class="name"
:style="{
color: item.count ? '#333' : '#888',
}"
>
{{ item.name }}
</text>
<text v-if="item.key == 'file'"></text>
<text v-else-if="item.count" class="count">({{ item.count }})</text>
<text v-else class="empty">()</text>
</view>
<view class="delBtn" @click="delCache">
<text class="delBtnText">清空选中</text>
</view>
</template>
<view class="divisionLine"></view>
<subTitleBar
:isOpen="configIsShow"
title="DevTools当前配置参数"
@click="configIsShow = !configIsShow"
/>
<view v-if="configIsShow" class="objectAnalysisView">
<objectAnalysis :isOpenFirst="true" :data="config" :width="710" />
</view>
<view class="divisionLine"></view>
<subTitleBar :showArrow="false" title="关于" />
<view class="about">
<view>
<text class="row">Copyright©2024 福州重塑网络科技有限公司 前端团队</text>
</view>
<view @click="goUrl('https://dev.api0.cn')" style="display: flex; flex-direction: row">
<text class="row">在线文档</text>
<text class="row" style="color: #ff2d55">https://dev.api0.cn</text>
</view>
<view>
<text class="row">当前版本v{{ config.version }}</text>
</view>
</view>
<view style="height: 100rpx"></view>
</template>
</view>
</template>
<script>
import devCache from '../../../core/libs/devCache'
import devOptions from '../../../core/libs/devOptions'
import jsonCompress from '../../../core/libs/jsonCompress'
import appDelDir from '../libs/appDelDir'
import subTitleBar from '../ui/subTitleBar.vue'
import objectAnalysis from './objectAnalysis.vue'
import getRuntimeInfo from '../libs/getRuntimeInfo'
export default {
components: {
subTitleBar,
objectAnalysis,
},
data() {
return {
/**
* 是否加载中
*/
loading: false,
/**
* 缓存列表是否展示
*/
cacheListIsShow: false,
/**
* 缓存列表
*/
cacheSelectList: [],
/**
* 配置文件是否显示
*/
configIsShow: false,
/**
* 当前配置
*/
config: devOptions.getOptions(),
/**
* 是否显示导出日志按钮
*/
exportIsShow: false,
}
},
methods: {
/**
* 加载页面
*/
async getPage() {
let that = this
that.loading = true
that.cacheSelectList = await that.countCache()
that.loading = false
},
/**
* 统计缓存信息
*/
countCache() {
let that = this
return new Promise(async (yes) => {
let cacheSelectList = []
// dev
let keys = {
errorReport: 'Error错误日志',
console: 'Console打印日志',
request: 'Request请求日志',
logReport: 'Logs日志',
uniBus: 'UniBus函数日志',
}
Object.keys(keys).map((key) => {
let logs = devCache.get(key)
cacheSelectList.push({
name: keys[key],
check: logs.length > 0,
count: logs.length,
key,
})
})
// #ifdef H5
let indexDBList = await this.getIndexDBList()
let cookieLength = document.cookie.split(';').length
if (document.cookie == '') {
cookieLength = 0
}
// #endif
cacheSelectList = cacheSelectList.concat([
that.countStorageCache(),
// #ifdef H5
{
key: 'sessionStorage',
name: 'SessionStorage临时缓存',
check: sessionStorage.length > 0,
count: sessionStorage.length,
},
// #endif
// #ifdef APP-PLUS
{
key: 'file',
name: 'FileSys本地文件(_doc)',
check: false,
count: '未知 // TODO',
},
// #endif
// #ifdef MP-WEIXIN
{
name: 'FileSys本地文件(FileSystemManager)',
check: false,
key: 'file',
count: '未知 // TODO',
},
// #endif
{
key: 'pageCount',
name: 'Pages页面停留统计',
check: devCache.get('pageCount').length > 0,
count: devCache.get('pageCount').length,
},
{
key: 'dayOnline',
name: 'Pages日活时间统计',
check: devCache.get('dayOnline').length > 0,
count: devCache.get('dayOnline').length,
},
// #ifdef H5
{
key: 'cookie',
name: 'Cookie',
check: cookieLength > 0,
count: cookieLength,
},
{
key: 'IndexDB',
name: 'IndexDB',
check: indexDBList.length > 0,
count: indexDBList.length,
},
// #endif
])
yes(cacheSelectList)
})
},
/**
* 统计本地缓存
*/
countStorageCache() {
let n = 0
// #ifdef APP-PLUS
let keys = plus.storage.getAllKeys()
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key.indexOf('devTools_') == 0) {
// devTools_ key
continue
}
n++
}
// #endif
// #ifdef H5
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i)
if (key.indexOf('devTools_') == 0) {
continue
}
n++
}
// #endif
// #ifdef MP
let keyList = devCache.get('storage')
if (!keyList) keyList = []
for (let i = 0; i < keyList.length; i++) {
const key = keyList[i]
if (key.indexOf('devTools_') == 0) {
continue
}
n++
}
// #endif
return {
key: 'localStorage',
name: 'localStorage本地缓存',
check: n > 0,
count: n,
}
},
/**
* 获取indexDB列表
*/
getIndexDBList() {
return new Promise((yes) => {
try {
indexedDB.databases().then((list) => {
yes(list)
})
} catch (error) {
console.log('getIndexDBList error', error)
yes([])
}
})
},
/**
* 选择清空的缓存项目
*/
doSelectCache(index) {
this.cacheSelectList[index].check = !this.cacheSelectList[index].check
},
/**
* 清空缓存
*/
delCache() {
let that = this
let selectedKey = []
that.cacheSelectList.map((item) => {
if (item.check) {
selectedKey.push(item.key)
}
})
let keyDelFun = {
errorReport() {
devCache.set('errorReport', [])
},
console() {
uni.$emit('devTools_delConsoleAll')
},
request() {
uni.$emit('devTools_delNetworkAll')
},
logReport() {
devCache.set('logReport', [])
},
uniBus() {
uni.$emit('devTools_delUniBusAll')
},
localStorage() {
// #ifdef APP-PLUS
let keys = plus.storage.getAllKeys()
for (let i = 0; i < keys.length; i++) {
const key = String(keys[i])
if (key.indexOf('devTools_') == 0) {
continue
}
uni.removeStorageSync(key)
}
// #endif
// #ifdef H5
for (let i = 0; i < localStorage.length; i++) {
let key = String(localStorage.key(i))
if (key.indexOf('devTools_') == 0) {
continue
}
setTimeout(
() => {
localStorage.removeItem(key)
},
i * 2 + 1,
)
}
// #endif
// #ifdef MP
let keyList = devCache.get('storage')
if (!keyList) keyList = []
for (let i = 0; i < keyList.length; i++) {
const key = keyList[i]
if (key.indexOf('devTools_') == 0) {
continue
}
uni.removeStorageSync(key)
}
// #endif
},
sessionStorage() {
for (let i = 0; i < sessionStorage.length; i++) {
let key = String(sessionStorage.key(i))
if (key.indexOf('devTools_') == 0) {
continue
}
sessionStorage.removeItem(key)
}
},
file() {
// #ifdef APP-PLUS
appDelDir('_doc/')
// #endif
// #ifdef MP-WEIXIN
let fs = wx.getFileSystemManager()
fs.rmdir({
dirPath: wx.env.USER_DATA_PATH + '/',
recursive: true,
})
// #endif
},
pageCount() {
devCache.set('pageCount', [])
},
dayOnline() {
devCache.set('dayOnline', [])
},
cookie() {
let keys = []
document.cookie.split(';').forEach((cookieStr) => {
const [name, value] = cookieStr.trim().split('=')
keys.push(name)
})
keys.map((k) => {
document.cookie =
`${k}=;expires=` + new Date(new Date().getTime() + 200).toGMTString() + ';path=/'
})
},
IndexDB() {
indexedDB.databases().then((list) => {
list.map((item) => {
indexedDB.deleteDatabase(item.name)
})
})
},
}
if (selectedKey.length == 0) {
return uni.showToast({
title: '请先勾选需要清空的项目!',
icon: 'none',
})
}
uni.showLoading({
title: '清空中...',
mask: true,
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '清空成功!',
icon: 'success',
})
that.getPage()
}, 5100)
selectedKey.map((key) => {
keyDelFun[key]()
})
},
/**
* 导出日志文件到json
*/
async exportJsonFile() {
let that = this
// #ifdef MP
if (1) {
uni.showToast({
title: '小程序平台不支持导出日志,建议直接上传至服务器!',
icon: 'none',
})
return
}
// #endif
uni.showLoading({
title: '打包中...',
})
try {
let devOp = devOptions.getOptions()
let waitExportObject = {
exportOptions: {
version: devOp.version,
config: devOp,
exportTime: new Date().getTime(),
// #ifdef APP-PLUS
platform: 'app',
// #endif
// #ifdef H5
platform: 'h5',
// #endif
// #ifdef MP
platform: 'mp',
// #endif
// #ifdef MP-WEIXIN
platform: 'wx',
// #endif
// #ifdef MP-QQ
platform: 'qq',
// #endif
},
error: devCache.get('errorReport'),
console: devCache.get('console'),
network: devCache.get('request'),
pageCount: devCache.get('pageCount'),
dayOnline: devCache.get('dayOnline'),
logs: devCache.get('logReport'), // !
info: await getRuntimeInfo(), // !
uniBus: devCache.get('uniBus'),
busCount: devCache.get('busCount'),
pageRouteMap: devCache.get('pageRouteMap'),
pageRouteKeyMap: devCache.get('pageRouteKeyMap'),
storage: {},
sessionStorage: {},
cookie: {},
...that.getCache(),
}
try {
if (that.$store.state) {
waitExportObject.vuex = that.$store.state
}
} catch (error) {}
try {
if (uni.Pinia) {
waitExportObject.pinia = uni.Pinia.getActivePinia().state.value
} else if (that.$pinia.state.value) {
waitExportObject.pinia = that.$pinia.state.value
}
} catch (error) {}
try {
if (getApp().globalData) {
waitExportObject.globalData = getApp().globalData
}
} catch (error) {}
let data = jsonCompress.safeJsonStringify(waitExportObject)
data = JSON.parse(data)
data = JSON.stringify(data, null, 2)
let t = new Date().getTime()
let exportFileName = `export_devtools_log_${t}.json`
// #ifdef H5
const blob = new Blob([data], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.style = 'display: none'
a.download = exportFileName
a.href = url
document.body.appendChild(a)
a.click()
uni.showToast({
title: '导出成功!',
icon: 'success',
})
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(
'_downloads/',
(entry) => {
entry.getFile(
exportFileName,
{
create: true,
},
(fileEntry) => {
fileEntry.createWriter((writer) => {
writer.onwrite = (e) => {
uni.hideLoading()
uni.showModal({
title: '导出成功',
content: '文件导出成功!已保存至公共下载路径,文件名称:' + exportFileName,
})
}
writer.onerror = () => {
uni.hideLoading()
uni.showToast({
title: '日志导出失败_写入文件失败',
icon: 'none',
})
}
writer.write(data)
})
},
)
},
(err) => {
console.log('err', err)
uni.hideLoading()
uni.showToast({
title: '文件保存失败_打开目录失败',
icon: 'none',
})
},
)
// #endif
uni.hideLoading()
} catch (error) {
if (error && error.message) {
console.log('导出失败!', error.message)
} else {
console.log('导出失败!', error)
}
uni.hideLoading()
uni.showToast({
title: '导出失败!',
icon: 'error',
})
}
},
/**
* 获取缓存数据
*/
getCache() {
let data = {
storage: {},
sessionStorage: {},
cookie: {},
}
// #ifdef APP-PLUS
let keys = plus.storage.getAllKeys()
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key.indexOf('devTools_') == 0) {
// devTools_ key
continue
}
data.storage[key] = uni.getStorageSync(key)
}
// #endif
// #ifdef H5
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i)
if (key.indexOf('devTools_') == 0) {
continue
}
let value = uni.getStorageSync(key)
data.storage[key] = value
}
for (let i = 0; i < sessionStorage.length; i++) {
let key = sessionStorage.key(i)
if (key.indexOf('devTools_') == 0) {
continue
}
let value = sessionStorage.getItem(key)
data.sessionStorage[key] = value
}
document.cookie.split(';').forEach((cookieStr) => {
const [name, value] = cookieStr.trim().split('=')
data.cookie[name] = value
})
// #endif
// #ifdef MP
let keyList = devCache.get('storage')
if (!keyList) keyList = []
for (let i = 0; i < keyList.length; i++) {
const key = keyList[i]
if (key.indexOf('devTools_') == 0) {
continue
}
let value = uni.getStorageSync(key)
if (value) {
data.storage[key] = value
}
}
// #endif
return data
},
/**
* 跳转指定URL
*/
goUrl(url) {
// #ifdef H5
window.open(url)
// #endif
// #ifdef MP
uni.setClipboardData({
data: url,
})
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(url)
// #endif
},
},
}
</script>
<style lang="scss" scoped>
.settingView {
display: flex;
flex-direction: column;
width: 750rpx;
.loading {
width: 750rpx;
height: 300rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.loadingText {
font-size: 24rpx;
color: #888;
}
}
.divisionLine {
width: 750rpx;
height: 1px;
background-color: rgba(0, 0, 0, 0.1);
}
.checkboxItem {
display: flex;
flex-direction: row;
padding: 10rpx 20rpx;
width: 750rpx;
align-items: center;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
.name {
font-size: 24rpx;
margin-left: 5rpx;
}
.count {
font-size: 20rpx;
margin-left: 10rpx;
color: #ff2d55;
}
.empty {
font-size: 20rpx;
margin-left: 10rpx;
color: #999;
}
}
.delBtn {
width: 710rpx;
margin-left: 20rpx;
border-radius: 20rpx;
height: 70rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background-color: rgb(255, 45, 85);
margin-top: 20rpx;
margin-bottom: 30rpx;
&:active {
background-color: rgba(255, 45, 85, 0.8);
}
.delBtnText {
font-size: 24rpx;
color: #fff;
}
}
}
.objectAnalysisView {
width: 710rpx;
margin-left: 20rpx;
margin-bottom: 20rpx;
}
.about {
width: 710rpx;
margin-left: 20rpx;
display: flex;
flex-direction: column;
.row {
margin-bottom: 10rpx;
font-size: 24rpx;
color: #888;
}
}
</style>

View File

@ -0,0 +1,313 @@
<template>
<view class="storageList">
<objectAnalysis
v-if="isLoaded && !isEmpty"
:data="storageData"
:isOpenFirst="true"
:width="710"
:isDiyMenu="true"
@diyMenu="diyMenu"
/>
<view v-if="!isLoaded" class="dataLoading">
<text class="status">加载中</text>
</view>
<view v-if="isLoaded && isEmpty" class="dataLoading">
<text class="status">无缓存数据</text>
</view>
<view v-if="isLoaded && !isEmpty" class="moreTools">
<text class="tips">Tips长按最外层key可复制编辑或删除缓存</text>
</view>
</view>
</template>
<script>
// #ifdef MP
import devCache from '../../../core/libs/devCache'
// #endif
import objectAnalysis from './objectAnalysis.vue'
export default {
components: {
objectAnalysis,
},
data() {
return {
/**
* 是否完成加载
*/
isLoaded: false,
/**
* 缓存里的数据
*/
storageData: {},
/**
* 数据是否为空
*/
isEmpty: false,
}
},
methods: {
/**
* 加载数据
*/
getData(storageType) {
let that = this
that.isLoaded = false
let data = {}
// #ifdef APP-PLUS
let keys = plus.storage.getAllKeys()
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key.indexOf('devTools_') == 0) {
// devTools_ key
continue
}
data[key] = uni.getStorageSync(key)
}
// #endif
// #ifdef H5
if (storageType == 'localStorage') {
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i)
if (key.indexOf('devTools_') == 0) {
continue
}
let value = uni.getStorageSync(key)
data[key] = value
}
} else if (storageType == 'sessionStorage') {
for (let i = 0; i < sessionStorage.length; i++) {
let key = sessionStorage.key(i)
if (key.indexOf('devTools_') == 0) {
continue
}
let value = sessionStorage.getItem(key)
data[key] = value
}
} else if (storageType == 'cookie') {
document.cookie.split(';').forEach((cookieStr) => {
const [name, value] = cookieStr.trim().split('=')
data[name] = value
})
}
// #endif
// #ifdef MP
let keyList = devCache.get('storage')
if (!keyList) keyList = []
for (let i = 0; i < keyList.length; i++) {
const key = keyList[i]
if (key.indexOf('devTools_') == 0) {
continue
}
let value = uni.getStorageSync(key)
if (value) {
data[key] = value
}
}
// #endif
setTimeout(() => {
that.storageData = data
if (Object.keys(data).length == 0) {
that.isEmpty = true
} else {
that.isEmpty = false
}
that.isLoaded = true
}, 500)
},
/**
* 自定义长按事件
*/
diyMenu({ item, index }) {
let that = this
let menu = [
{
text: `复制键(key)`,
click() {
uni.setClipboardData({
data: item.k,
})
},
},
{
text: `复制值(value)`,
click() {
uni.setClipboardData({
data: item.v,
})
},
},
]
if (item.p == 0) {
// key
menu.unshift({
text: '删除该键',
click() {
// #ifdef H5
if (that.storageType == 'localStorage') {
uni.removeStorageSync(item.k)
} else if (that.storageType == 'sessionStorage') {
sessionStorage.removeItem(item.k)
} else if (that.storageType == 'cookie') {
document.cookie =
`${item.k}=;expires=` +
new Date(new Date().getTime() + 200).toGMTString() +
';path=/'
}
// #endif
// #ifndef H5
uni.removeStorageSync(item.k)
// #endif
uni.showToast({
title: '删除成功!',
icon: 'success',
})
if (that.storageType == 'cookie') {
// cookie
setTimeout(() => {
that.getData()
}, 1500)
} else {
that.getData()
}
},
})
menu.unshift({
text: '编辑值',
click() {
let key = item.k
let value = ''
if (that.storageType == 'sessionStorage') {
value = sessionStorage.getItem(key)
} else if (that.storageType == 'cookie') {
document.cookie.split(';').forEach((cookieStr) => {
const [name, v] = cookieStr.trim().split('=')
if (name == key) {
value = v
}
})
} else {
value = uni.getStorageSync(key)
}
if (typeof value == 'object') {
value = JSON.stringify(value)
} else if (value === false) {
value = 'false'
} else if (value === true) {
value = 'true'
} else if (!value) {
value = ''
}
uni.$emit('devTools_showEditDialog', {
title: `key:${key}`,
value,
})
uni.$on('devTools_editDialogClose', () => {
uni.$off('devTools_editDialogSaveSuccess')
uni.$off('devTools_editDialogClose')
})
uni.$on('devTools_editDialogSaveSuccess', (val) => {
uni.$off('devTools_editDialogSaveSuccess')
uni.$off('devTools_editDialogClose')
let oldValue = uni.getStorageSync(key)
if (oldValue === false || oldValue === true) {
if (val == 'true' || val == 'false') {
val = val == 'true'
}
}
// #ifdef H5
if (that.storageType == 'localStorage') {
uni.setStorageSync(key, val)
} else if (that.storageType == 'sessionStorage') {
sessionStorage.setItem(key, val)
} else if (that.storageType == 'cookie') {
key = encodeURIComponent(key)
val = encodeURIComponent(val)
let cookie =
`${key}=${val}; path=/; expires=` +
new Date(new Date().getTime() + 86400 * 1000 * 365).toGMTString()
document.cookie = cookie
}
// #endif
// #ifndef H5
uni.setStorageSync(key, val)
// #endif
uni.showToast({
icon: 'success',
title: '保存成功',
})
setTimeout(() => {
that.getData()
}, 300)
})
},
})
}
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
},
}
</script>
<style lang="scss" scoped>
.storageList {
padding: 20rpx;
width: 750rpx;
}
.dataLoading {
width: 750rpx;
height: 400rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.status {
font-size: 20rpx;
color: #888;
line-height: 20rpx;
}
}
.moreTools {
display: flex;
flex-direction: column;
.tips {
font-size: 20rpx;
color: #888;
margin-top: 20rpx;
}
.delBtn {
margin-top: 50rpx;
border-radius: 8rpx;
padding: 10rpx;
background-color: rgba(0, 0, 0, 0.02);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 375rpx;
&:active {
background-color: rgba(0, 0, 0, 0.1);
}
.delBtnText {
font-size: 20rpx;
line-height: 20rpx;
color: #f37b1d;
}
}
}
</style>

View File

@ -0,0 +1,292 @@
<template>
<view>
<subTitleBar
:isOpen="isShowSetting"
@click="changeStatus('isShowSetting')"
title="DevTools扩展配置项"
/>
<template v-if="isShowSetting">
<!-- #ifdef APP-PLUS || H5 -->
<view class="settingItem">
<view class="settingHead">
<text class="settingTitle">页面自动注入Eruda调试工具</text>
<text class="settingSubtitle">(强大的节点选择等工具重启APP后生效)</text>
</view>
<switch
:checked="isInjectEruda"
@change="switchChange($event, 'isInjectEruda')"
color="#ff2d55"
/>
</view>
<view class="settingItem">
<view class="settingHead">
<text class="settingTitle">页面自动注入vConsole调试工具</text>
<text class="settingSubtitle">(腾讯开源的h5调试工具重启APP后生效)</text>
</view>
<switch
:checked="isInjectVConsole"
@change="switchChange($event, 'isInjectVConsole')"
color="#ff2d55"
/>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="settingItem">
<view class="settingHead">
<text class="settingTitle">小程序VConsole开关</text>
<text class="settingSubtitle">设置是否打开调试开关此开关对正式版也能生效</text>
</view>
<switch
:checked="isOpenMpDevTag"
@change="switchChange($event, 'isOpenMpDevTag')"
color="#ff2d55"
/>
</view>
<!-- #endif -->
</template>
<subTitleBar
:isOpen="isShowBtnPanel"
@click="changeStatus('isShowBtnPanel')"
title="常用工具"
/>
<view v-if="isShowBtnPanel" class="btnPanel">
<!-- #ifdef APP-PLUS || H5 -->
<view class="btnItem btn-def" @click="restart">
<text class="btnText">重启APP</text>
</view>
<!-- #endif -->
<view class="btnItem btn-def" @click="goPage">
<text class="btnText">跳转指定页面</text>
</view>
<view class="btnItem btn-def" @click="$emit('goOpenRequest')">
<text class="btnText">发起网络请求</text>
</view>
<view class="btnItem btn-def" @click="delLocalStorage">
<text class="btnText">清空localStorage缓存</text>
</view>
</view>
<subTitleBar
:isOpen="isShowDiyTools"
@click="changeStatus('isShowDiyTools')"
title="自定义Tools"
/>
<tools v-if="isShowDiyTools" ref="tools" />
</view>
</template>
<script>
import tools from '../../../tools.vue'
import subTitleBar from '../ui/subTitleBar.vue'
export default {
components: {
tools,
subTitleBar,
},
data() {
return {
/**
* 是否显示系统工具栏
*/
isShowSetting: false,
/**
* 是否自动注入Eruda
*/
isInjectEruda: uni.getStorageSync('devTools_isInjectEruda') == 'yes',
/**
* 是否自动注入vConsole
*/
isInjectVConsole: uni.getStorageSync('devTools_isInjectVConsole') == 'yes',
/**
* 是否显示 用户自定义tools
*/
isShowDiyTools: true,
/**
* 是否打开小程序调试工具
*/
isOpenMpDevTag: uni.getStorageSync('devTools_isOpenMpDevTag') == 'yes',
/**
* 常用工具栏开关
*/
isShowBtnPanel: true,
}
},
methods: {
/**
* 更改选中状态
*/
changeStatus(key) {
this[key] = !this[key]
},
/**
* 开关选择器改变事件
*/
switchChange(e, key) {
let status = e.detail.value
this[key] = status
uni.setStorageSync('devTools_' + key, status ? 'yes' : 'no')
if (key == 'isOpenMpDevTag') {
wx.setEnableDebug({
enableDebug: status,
})
}
},
/**
* 重启APP
*/
restart() {
// #ifdef APP-PLUS
plus.runtime.restart()
// #endif
// #ifdef H5
location.href = '/'
// #endif
},
/**
* 跳转指定页面
*/
goPage() {
uni.$emit('devTools_showRouteDialog')
},
/**
* 清空LocalStorage
*/
delLocalStorage() {
uni.showModal({
title: '操作确认',
content: '是否确认清空LocalStorage缓存',
success(res) {
if (res.confirm) {
// #ifdef APP-PLUS
let keys = plus.storage.getAllKeys()
for (let i = 0; i < keys.length; i++) {
const key = String(keys[i])
if (key.indexOf('devTools_') == 0) {
continue
}
uni.removeStorageSync(key)
}
// #endif
// #ifdef H5
for (let i = 0; i < localStorage.length; i++) {
let key = String(localStorage.key(i))
if (key.indexOf('devTools_') == 0) {
continue
}
setTimeout(
() => {
localStorage.removeItem(key)
},
i * 2 + 1,
)
}
// #endif
// #ifdef MP
let keyList = devCache.get('storage')
if (!keyList) keyList = []
for (let i = 0; i < keyList.length; i++) {
const key = keyList[i]
if (key.indexOf('devTools_') == 0) {
continue
}
uni.removeStorageSync(key)
}
// #endif
uni.showToast({
icon: 'success',
title: '清空成功!',
})
}
},
})
},
},
}
</script>
<style lang="scss" scoped>
.padding-sm {
padding: 20rpx;
}
.settingItem:active {
background-color: rgba(0, 0, 0, 0.05);
}
.settingItem {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 750rpx;
padding: 15rpx 0;
padding-right: 15rpx;
.settingHead {
display: flex;
flex-direction: column;
margin-left: 20rpx;
.settingTitle {
font-size: 24rpx;
line-height: 28rpx;
color: #333;
}
.settingSubtitle {
margin-top: 4rpx;
font-size: 20rpx;
line-height: 26rpx;
color: #777;
}
}
}
.btnPanel {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding-left: 20rpx;
padding-right: 20rpx;
padding-top: 20rpx;
.btnItem {
margin-right: 20rpx;
margin-bottom: 20rpx;
border-radius: 10rpx;
/* #ifndef APP-PLUS */
min-width: 120rpx;
/* #endif */
height: 60rpx;
border: 1px solid rgba(0, 0, 0, 0.1);
background-color: rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 15rpx;
.btnText {
font-size: 20rpx;
line-height: 30rpx;
color: #333;
}
}
.btn-red:active {
background-color: rgba(255, 45, 85, 0.5);
}
.btn-red {
border: 1px solid rgba(255, 45, 85, 1);
background-color: rgba(255, 45, 85, 1);
.btnText {
color: #fff;
}
}
.btn-def:active {
background-color: rgba(0, 0, 0, 0.1);
}
.btn-def {
border: 1px solid rgba(0, 0, 0, 0.1);
background-color: rgba(0, 0, 0, 0.04);
.btnText {
color: #333;
}
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<view class="storageList">
<objectAnalysis
v-if="isLoaded"
:data="storageData"
:isOpenFirst="true"
:width="710"
:isDiyMenu="true"
@diyMenu="diyMenu"
ref="objectAnalysis"
/>
<view v-else class="dataLoading">
<text class="status">加载中</text>
</view>
<text v-if="isLoaded" class="tipsText">长按非对象类型的数据可编辑</text>
</view>
</template>
<script>
import objectAnalysis from './objectAnalysis.vue'
export default {
components: {
objectAnalysis,
},
props: {
/**
* 全局变量类型
*/
stateType: {
type: String,
default: 'vuex',
},
},
data() {
return {
/**
* 是否完成加载
*/
isLoaded: false,
/**
* 缓存里的数据
*/
storageData: {},
}
},
methods: {
/**
* 加载数据
*/
getData() {
let that = this
let data = {}
if (that.stateType == 'vuex') {
try {
data = JSON.parse(JSON.stringify(that.$store.state))
} catch (error) {}
} else if (that.stateType == 'pinia') {
try {
if (uni.Pinia) {
data = JSON.parse(JSON.stringify(uni.Pinia.getActivePinia().state.value))
} else if (that.$pinia) {
data = JSON.parse(JSON.stringify(that.$pinia.state.value))
}
} catch (error) {}
} else if (that.stateType == 'globalData') {
try {
data = JSON.parse(JSON.stringify(getApp().globalData))
} catch (error) {}
}
that.isLoaded = false
setTimeout(() => {
that.storageData = data
that.isLoaded = true
}, 500)
},
/**
* 自定义长按事件
*/
diyMenu({ item, index }) {
let that = this
let menu = [
{
text: `复制键(key)`,
click() {
uni.setClipboardData({
data: that.toString(item.k),
})
},
},
{
text: `复制值(value)`,
click() {
uni.setClipboardData({
data: that.toString(item.v),
})
},
},
]
if (typeof item.v != 'object') {
menu.push({
text: '编辑值',
click() {
let keyList = that.$refs.objectAnalysis.getFaKeyList(item.i)
let title = ''
if (keyList.length == 0) {
title = 'key'
} else {
keyList.map((x) => {
title = (title == '' ? '' : title + '.') + x
})
}
let isBool = typeof item.v == 'boolean'
if (isBool) {
item.v = item.v ? 'true' : 'false'
}
if (item.v === undefined || item.v === null) {
item.v = ''
}
uni.$emit('devTools_showEditDialog', {
title,
value: item.v,
})
uni.$on('devTools_editDialogClose', () => {
uni.$off('devTools_editDialogSaveSuccess')
uni.$off('devTools_editDialogClose')
})
uni.$on('devTools_editDialogSaveSuccess', (val) => {
uni.$off('devTools_editDialogSaveSuccess')
uni.$off('devTools_editDialogClose')
if (isBool && (val == 'true' || val == 'false')) {
val = val == 'true'
}
let data
if (that.stateType == 'vuex') {
data = that.$store.state
} else if (that.stateType == 'pinia') {
if (uni.Pinia) {
data = uni.Pinia.getActivePinia().state.value
} else if (that.$pinia) {
data = that.$pinia.state.value
}
} else if (that.stateType == 'globalData') {
data = getApp().globalData
}
let lastKey = keyList.pop()
keyList.map((x) => {
data = data[x]
})
that.$set(data, lastKey, val)
that.getData()
})
},
})
}
uni.showActionSheet({
itemList: menu.map((x) => x.text),
success({ tapIndex }) {
menu[tapIndex].click()
},
})
},
getFaKeyList() {},
},
}
</script>
<style lang="scss" scoped>
.storageList {
padding: 20rpx;
width: 750rpx;
}
.dataLoading {
width: 750rpx;
height: 400rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.status {
font-size: 20rpx;
color: #888;
line-height: 20rpx;
}
}
.tipsText {
font-size: 20rpx;
color: #8799a3;
margin-top: 40rpx;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
// #ifdef APP-PLUS
const animation = weex.requireModule('animation')
// #endif
export default {
methods: {
/**
* 显示面板
*/
panelShow() {
let that = this
let sys = uni.getSystemInfoSync()
animation.transition(that.$refs.mask, {
styles: {
opacity: 1,
height: sys.windowHeight + 'px',
},
duration: 200, //ms
timingFunction: 'linear',
delay: 0, //ms
})
let height = Math.ceil(sys.windowHeight * 0.8)
animation.transition(
that.$refs.panel,
{
styles: {
opacity: 1,
transform: `transform: translate(0px,${height}px)`,
},
duration: 1, //ms
timingFunction: 'linear',
delay: 0, //ms
},
(res) => {
animation.transition(that.$refs.panel, {
styles: {
transform: `transform: translate(0px,0px)`,
},
duration: 200, //ms
timingFunction: 'linear',
delay: 0, //ms
})
},
)
},
/**
* 关闭面板
*/
panelHide() {
let that = this
animation.transition(that.$refs.mask, {
styles: {
opacity: 0,
},
duration: 200, //ms
timingFunction: 'linear',
delay: 0, //ms
})
let height = uni.upx2px(1000)
animation.transition(
that.$refs.panel,
{
styles: {
transform: `transform: translate(0px,${height}px)`,
},
duration: 200, //ms
timingFunction: 'linear',
delay: 0, //ms
},
() => {
uni.$emit('devTools_panelHideSuccess')
},
)
},
},
}

View File

@ -0,0 +1,77 @@
export default {
data() {
return {
windowInfo: getWindowInfo(),
}
},
computed: {
/**
* 小程序和H5的标题
*/
navbarStyle() {
let windowInfo = getWindowInfo()
return {
statusBarHeightPx: windowInfo.system.statusBarHeight + 'px',
navbarHeightPx: windowInfo.navbar.bodyHeightPx + 'px',
}
},
},
methods: {
back() {
uni.navigateBack()
},
},
}
/**
* 获取当前窗口数据
*
*/
function getWindowInfo() {
let systemInfo = uni.getSystemInfoSync()
systemInfo.pixelRatio = Math.round(systemInfo.pixelRatio * 100) / 100
let windowInfo = {
system: systemInfo,
capsule: {
bottom: 78,
height: 30,
left: 283,
right: 363,
top: 48,
width: 0,
},
navbar: {
heightPx: uni.upx2px(100) + systemInfo.statusBarHeight,
bodyHeightPx: uni.upx2px(100),
bodyWidthPx: systemInfo.windowWidth,
capsuleWidthPx: 0,
capsuleRightPx: 0,
},
height: systemInfo.windowHeight * (750 / systemInfo.windowWidth),
width: 750,
statusBarHeight: systemInfo.statusBarHeight * (750 / systemInfo.windowWidth),
safeBottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
safeBottomRpx:
(systemInfo.windowHeight - systemInfo.safeArea.bottom) * (750 / systemInfo.windowWidth),
/**
* 原生端 底部导航栏高度
*/
footNavbarHeight: uni.upx2px(100) + (systemInfo.windowHeight - systemInfo.safeArea.bottom),
}
// #ifdef MP-QQ || MP-WEIXIN
let capsuleInfo = uni.getMenuButtonBoundingClientRect()
windowInfo.capsule = capsuleInfo
let capsuleToStatusBarPx = capsuleInfo.top - systemInfo.statusBarHeight //胶囊和状态栏之间的高度
windowInfo.navbar.bodyHeightPx = capsuleInfo.height + capsuleToStatusBarPx * 2
windowInfo.navbar.heightPx = windowInfo.navbar.bodyHeightPx + systemInfo.statusBarHeight
let capsuleWidthPx = (systemInfo.windowWidth - capsuleInfo.right) * 2 + capsuleInfo.width
windowInfo.navbar.bodyWidthPx = systemInfo.windowWidth - capsuleWidthPx
windowInfo.navbar.capsuleWidthPx = capsuleWidthPx
windowInfo.navbar.capsuleRightPx = capsuleWidthPx - (systemInfo.windowWidth - capsuleInfo.right)
// #endif
return windowInfo
}

View File

@ -0,0 +1,68 @@
<template>
<view class="btnTabs" v-if="list.length > 0">
<block v-for="(item, index) in list" :key="item.title">
<view
class="btnTabsItem"
:style="{
'background-color': '#f9f9f9',
}"
@click="$emit('indexChange', index)"
>
<text
class="tabsText"
:style="{
color: index == value ? '#ff2d55' : '#333333',
}"
>
{{ item.title }}
</text>
</view>
<view v-if="index != list.length - 1" :key="index" class="splitLine"></view>
</block>
</view>
</template>
<script>
export default {
props: {
/**
* 按钮列表
*/
list: {
type: Array,
default: () => [],
},
/**
* 当前选中的按钮索引
*/
value: {
type: Number,
default: 0,
},
},
}
</script>
<style lang="scss" scoped>
.btnTabs {
display: flex;
flex-direction: row;
border-radius: 8rpx;
overflow: hidden;
height: 40rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
.btnTabsItem {
display: flex;
height: 40rpx;
padding: 0 8rpx;
.tabsText {
font-size: 20rpx;
line-height: 40rpx;
height: 40rpx;
}
}
.splitLine {
width: 1px;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.05);
}
}
</style>

View File

@ -0,0 +1,161 @@
<template>
<view
class="codeHisPicker"
v-if="isShow"
@click="close"
:style="{
height: height + 'px',
}"
>
<view class="codeList" @click.stop>
<view class="head">
<view class="title">
<text class="titleText">历史运行代码</text>
</view>
<view class="subTitle">
<text class="subTitleText">(保留100条运行记录)</text>
</view>
</view>
<scroll-view scroll-y class="codeScroll">
<view class="hisItem" v-for="(item, index) in list" :key="index" @click="selectedRow(item)">
<text class="hisItemCode">
{{ item }}
</text>
</view>
<view style="height: 100rpx"></view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
/**
* 是否展示
*/
isShow: false,
/**
* 筛选的列表
*/
list: [],
/**
* 默认选中的索引
*/
index: 0,
/**
* 选中的回调事件
*/
callback: null,
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().screenHeight,
}
},
methods: {
/**
* 展示弹窗
*/
show(list = []) {
let that = this
that.index = 0
that.list = list
that.isShow = true
return new Promise((yes) => {
that.callback = yes
})
},
/**
* 选择改变事件
*/
pickerChange(e) {
that.callback = ''
console.log('e', e)
},
/**
* 关闭弹窗
*/
close() {
this.isShow = false
},
/**
* 选择单行代码
*/
selectedRow(row) {
this.callback(row)
this.close()
},
},
}
</script>
<style lang="scss" scoped>
.codeHisPicker {
position: fixed;
bottom: 0;
left: 0;
width: 750rpx;
background-color: rgba(0, 0, 0, 0.3);
/* #ifndef APP-PLUS */
backdrop-filter: blur(1px);
/* #endif */
display: flex;
flex-direction: column-reverse;
z-index: 999;
.codeList {
width: 750rpx;
border-radius: 20rpx 20rpx 0 0;
background-color: #fff;
.head {
padding: 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.title {
.titleText {
font-size: 24rpx;
line-height: 28rpx;
color: #333;
}
}
.subTitle {
display: flex;
flex-direction: row;
align-items: center;
.subTitleText {
font-size: 20rpx;
line-height: 28rpx;
color: #777;
}
}
}
.codeScroll {
height: 750rpx;
width: 750rpx;
.hisItem {
width: 750rpx;
padding: 10rpx 20rpx;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
.hisItemCode {
font-size: 20rpx;
color: #333;
line-height: 26rpx;
overflow: hidden;
text-overflow: ellipsis;
/* #ifdef H5 */
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
/* #endif */
lines: 3;
}
}
.hisItem:active {
background-color: rgba(0, 0, 0, 0.03);
}
}
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<div><slot></slot></div>
</template>
<script>
export default {}
</script>
<style></style>

View File

@ -0,0 +1,218 @@
<template>
<view>
<view class="menuBtn" v-if="list.length > 0" @click="showMenu">
<text class="menuText">{{ title }}</text>
<text class="menuText">{{ list[value].title }}</text>
<image src="@/devTools/page/static/menu.png" class="menuIcon" />
</view>
<view
v-if="showMenuList"
class="menuMock"
:style="{
height: height + 'px',
}"
@click.stop
>
<view class="closeBtn" @click="close">
<text class="closeText">关闭</text>
</view>
<scroll-view
:style="{
maxHeight: height * 0.7 + 'px',
}"
scroll-y
class="scrollList"
>
<view
v-for="(item, index) in list"
:key="item.title"
class="menuSelectItem"
:class="[
index > 0 ? 'tl' : '',
index == 0 ? 'i-s' : '',
index == list.length ? 'i-e' : '',
]"
@click="menuSelect(index)"
>
<text
class="menuSelectText"
:style="{
color: index == value ? '#ff2d55' : '#333333',
}"
>
{{ item.title }}
</text>
<view v-if="item.msg">
<text class="menuSelectMsg">{{ item.msg }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
props: {
/**
* 按钮列表
*/
list: {
type: Array,
default: () => [],
},
/**
* 当前选中的按钮索引
*/
value: {
type: Number,
default: 0,
},
/**
* 标题
*/
title: {
type: String,
default: '',
},
},
data() {
return {
/**
* 屏幕高度
*/
height: uni.getSystemInfoSync().windowHeight,
/**
* 是否展示菜单列表
*/
showMenuList: false,
}
},
methods: {
/**
* 展示菜单
*/
showMenu() {
this.showMenuList = true
},
/**
* 点击菜单选择事件
*/
menuSelect(index) {
this.$emit('indexChange', index)
this.showMenuList = false
},
/**
* 关闭菜单弹窗
*/
close() {
this.showMenuList = false
},
},
}
</script>
<style lang="scss" scoped>
.menuBtn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: 8rpx;
height: 40rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
padding: 0 8rpx;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
.menuText {
font-size: 20rpx;
line-height: 40rpx;
color: #333;
}
.menuIcon {
margin-left: 8rpx;
width: 28rpx;
height: 28rpx;
}
}
.menuMock {
position: fixed;
z-index: 90;
width: 750rpx;
left: 0;
top: 0;
flex: 1;
/* #ifndef APP-PLUS */
height: 100vh;
backdrop-filter: blur(2px);
/* #endif */
display: flex;
flex-direction: column-reverse;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
.closeBtn {
width: 710rpx;
height: 80rpx;
margin-bottom: 80rpx;
border-radius: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background-color: #fff;
transition: background-color 200ms ease-in-out;
&:active {
background-color: #dcdcdc;
}
.closeText {
font-size: 24rpx;
color: #333;
line-height: 24rpx;
}
}
}
.scrollList {
display: flex;
flex-direction: column;
align-items: center;
width: 710rpx;
margin-bottom: 30rpx;
border-radius: 20rpx;
overflow: hidden;
.menuSelectItem {
width: 710rpx;
/* #ifndef APP-PLUS */
min-height: 80rpx;
/* #endif */
padding: 15rpx 0;
background-color: #fff;
transition: background-color 200ms ease-in-out;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
&.tl {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
&.i-s {
border-radius: 20rpx 20rpx 0 0;
}
&.i-e {
border-radius: 0 0 20rpx 20rpx;
}
&:active {
background-color: #dcdcdc;
}
.menuSelectText {
font-size: 24rpx;
line-height: 24rpx;
color: #333;
}
.menuSelectMsg {
font-size: 20rpx;
line-height: 20rpx;
margin-top: 10rpx;
color: #999999;
}
}
}
</style>

View File

@ -0,0 +1,158 @@
<template>
<view>
<!-- #ifdef APP-PLUS -->
<swiper
:current="tabIndex"
:style="{
height: scrollHeight + 'px',
}"
@change="$emit('tabIndexChange', $event.detail.current)"
>
<swiper-item v-for="(item, index) in tabList" :key="index + 'tabList'">
<list
:style="{
height: scrollHeight + 'px',
}"
class="contentList"
show-scrollbar
:index="index"
:id="`contentList_${index}`"
:fixFreezing="true"
ref="mob_list"
>
<refresh
v-if="item.canRefreshing"
class="refreshView"
@refresh="$emit('refresh', index)"
@pullingdown="$emit('pullingdown', { event: $event, index })"
:display="item.isRefreshing ? 'show' : 'hide'"
>
<view class="content">
<template v-if="item.refreshType == 'waitPullUp'">
<text class="statusText">下拉刷新</text>
</template>
<template v-if="item.refreshType == 'waitRelease'">
<text class="statusText">松手刷新</text>
</template>
<template v-if="item.refreshType == 'refreshing'">
<text class="statusText">刷新中...</text>
</template>
<template v-if="item.refreshType == 'success'">
<text class="statusText">刷新成功</text>
</template>
<template v-if="item.refreshType == 'error'">
<text class="statusText">刷新失败</text>
</template>
</view>
</refresh>
<slot :item="item" :index="index"></slot>
<cell ref="mob_list_end">
<view></view>
</cell>
</list>
</swiper-item>
</swiper>
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<scroll-view
scroll-y
:style="{
height: scrollHeight + 'px',
}"
:scroll-top="scrollTop"
>
<slot :item="tabList[tabIndex]" :index="tabIndex"></slot>
</scroll-view>
<!-- #endif -->
</view>
</template>
<script>
export default {
props: {
/**
* 分类索引
*/
tabIndex: {
type: Number,
default: 0,
},
/**
* tab列表
*/
tabList: {
type: Array,
default: () => [],
},
/**
* 滚动高度
*/
scrollHeight: {
type: Number,
default: 1000,
},
},
data() {
return {
/**
* 滚动位置
*/
scrollTop: 0,
}
},
methods: {
/**
* 滚动到列表底部
*/
scrollToBottom() {
let that = this
// #ifdef APP-PLUS
const dom = weex.requireModule('dom')
dom.scrollToElement(that.$refs.mob_list_end[that.tabIndex])
// #endif
// #ifndef APP-PLUS
that.scrollTop = 999999999 + Math.random()
// #endif
},
},
}
</script>
<style lang="scss" scoped>
.contentList {
display: flex;
flex-direction: column;
align-items: center;
width: 750rpx;
.cell {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 20rpx;
width: 750rpx;
}
}
.refreshView {
background-color: #fff;
width: 750rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: absolute;
top: 0px;
left: 0px;
height: 100rpx;
.content {
height: 100rpx;
width: 750rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
.statusText {
color: #8799a3;
font-size: 24rpx;
margin-left: 10rpx;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<view>
<MenuBtn :list="typeList" :value="index" @indexChange="change" title="网速:" />
</view>
</template>
<script>
import MenuBtn from './menuBtn.vue'
export default {
components: {
MenuBtn,
},
data() {
return {
/**
* 弱网模拟状态
*/
typeList: [
{ title: '不限', type: '', msg: '正常响应请求' },
{ title: '2G龟速', type: '2g', msg: '随机延迟30~60秒后响应' },
{ title: '3G慢速', type: '3g-', msg: '随机延迟10~30秒后响应' },
{ title: '3G略快', type: '3g', msg: '随机延迟3~10秒后响应' },
{ title: '4G弱网', type: '4g', msg: '随机延迟0.5~3秒后响应' },
],
/**
* 当前选中的索引
*/
index: 0,
}
},
mounted() {
let cache = uni.getStorageSync('devtools_uniResLimitType')
let index = this.typeList.findIndex((x) => x.type == cache)
if (index == -1) index = 0
this.index = index
},
methods: {
/**
* 选项发生改变事件
*/
change(index) {
uni.setStorageSync('devtools_uniResLimitType', this.typeList[index].type)
this.index = index
},
},
}
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,50 @@
<template>
<view>
<MenuBtn :list="typeList" :value="index" @indexChange="change" title="超时:" />
</view>
</template>
<script>
import MenuBtn from './menuBtn.vue'
export default {
components: {
MenuBtn,
},
data() {
return {
/**
* 随机响应超时
*/
typeList: [
{ title: '无', type: '', msg: '正常响应请求' },
{ title: '5%', type: '5', msg: '随机5%的概率请求响应超时或报错' },
{ title: '10%', type: '10', msg: '随机10%的概率请求响应超时或报错' },
{ title: '30%', type: '30', msg: '随机30%的概率请求响应超时或报错' },
{ title: '50%', type: '50', msg: '随机50%的概率请求响应超时或报错' },
{ title: '70%', type: '70', msg: '随机70%的概率请求响应超时或报错' },
{ title: '90%', type: '90', msg: '随机90%的概率请求响应超时或报错' },
{ title: '100%', type: '100', msg: '所有请求均响应超时或报错' },
],
/**
* 当前选中的索引
*/
index: 0,
}
},
mounted() {
let cache = uni.getStorageSync('devtools_uniResTimeout')
let index = this.typeList.findIndex((x) => x.type == cache)
if (index == -1) index = 0
this.index = index
},
methods: {
/**
* 选项发生改变事件
*/
change(index) {
uni.setStorageSync('devtools_uniResTimeout', this.typeList[index].type)
this.index = index
},
},
}
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,86 @@
<template>
<view class="subTitleBar" @click.stop="click">
<view class="left">
<view class="titleLine"></view>
<text class="titleText">{{ title }}</text>
</view>
<view v-if="showArrow" class="right">
<image v-if="isOpen" src="@/devTools/page/static/fold.png" class="arrow" />
<image v-else src="@/devTools/page/static/unfold.png" class="arrow" />
</view>
</view>
</template>
<script>
export default {
emits: ['click'],
props: {
/**
* 标题名称
*/
title: {
type: String,
default: '标题',
},
/**
* 是否显示右侧箭头
*/
showArrow: {
type: Boolean,
default: true,
},
/**
* 是否为开启状态
*/
isOpen: {
type: Boolean,
default: false,
},
},
data() {
return {}
},
methods: {
click() {
this.$emit('click')
},
},
}
</script>
<style lang="scss" scoped>
.subTitleBar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
width: 750rpx;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
.left {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 20rpx;
.titleLine {
width: 4rpx;
height: 24rpx;
border-radius: 4px;
background-color: #ff2d55;
}
.titleText {
color: #333;
margin-left: 10rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
.right {
margin-right: 20rpx;
.arrow {
width: 30rpx;
height: 30rpx;
}
}
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<view class="nvue">
<mainView ref="mainView" />
</view>
</template>
<script>
import mainView from "./components/main.vue";
export default {
components: {
mainView,
},
onLoad(options) {
let that = this;
that.$nextTick(() => {
that.$refs.mainView.pageOnLoad(options);
});
},
onBackPress(e) {
if (e.from == "navigateBack") {
return false;
}
this.$refs.mainView.hide();
return true;
},
};
</script>
<style lang="scss" scoped>
.nvue {
width: 750rpx;
/* #ifndef APP-PLUS */
height: 100vh;
/* #endif */
/* #ifdef APP-PLUS */
flex: 1;
/* #endif */
}
/* #ifdef H5 */
@media only screen and (pointer: fine) {
.showScrollbars {
::-webkit-scrollbar-thumb:horizontal {
/*水平滚动条的样式*/
width: 4px;
background-color: rgba(0, 0, 0, 0.1);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-track-piece {
background-color: #fff;
/*滚动条的背景颜色*/
-webkit-border-radius: 0;
/*滚动条的圆角宽度*/
}
::-webkit-scrollbar {
width: 10px;
/*滚动条的宽度*/
height: 5px;
/*滚动条的高度*/
display: block !important;
}
::-webkit-scrollbar-thumb:vertical {
display: none;
}
::-webkit-scrollbar-thumb:hover {
/*滚动条的hover样式*/
height: 50px;
background-color: rgba(0, 0, 0, 0.25);
-webkit-border-radius: 4px;
}
}
}
/* #endif */
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

22
src/devTools/tools.vue Normal file
View File

@ -0,0 +1,22 @@
<template>
<view class="tools">
<!-- 在这里可以开发自己的DIY工具 -->
<text style="margin-top: 50rpx; color: grey; font-size: 24rpx">Empty</text>
</view>
</template>
<script>
export default {
data() {
return {}
},
methods: {},
}
</script>
<style lang="scss" scoped>
.tools {
display: flex;
flex-direction: column;
align-items: center;
width: 750rpx;
}
</style>

View File

@ -7,6 +7,11 @@ import App from './App.vue'
import { prototypeInterceptor, requestInterceptor, routeInterceptor } from './interceptors'
import store from './store'
import devTools from './devTools/index.js'
import devToolsConfig from './devTools/config.js'
import mpDevBubble from './devTools/core/components/mpDevBubble.vue'
import devToolsVueMixin from './devTools/core/proxy/vueMixin.js'
export function createApp() {
const app = createSSRApp(App)
app.use(store)
@ -14,6 +19,14 @@ export function createApp() {
app.use(requestInterceptor)
app.use(prototypeInterceptor)
app.use(VueQueryPlugin)
//混入DevTools生命周期监听
app.mixin(devToolsVueMixin)
//挂载Devtools
app.use(devTools, devToolsConfig)
//注册小程序端专用的拖动浮标组件
app.component('mpDevBubble', mpDevBubble)
return {
app,

View File

@ -44,6 +44,26 @@
}
]
},
"subPackages": [
{
"root": "devTools/page",
"pages": [
{
"path": "index",
"style": {
"navigationStyle": "custom",
"softinputMode": "adjustResize",
"animationDuration": 1,
"animationType": "none",
"popGesture": "none",
"bounce": "none",
"titleNView": false
}
}
]
}
],
"__esModule": true,
"pages": [
{
"path": "pages/index/index",
@ -97,6 +117,5 @@
"navigationBarTitleText": "修改密码"
}
}
],
"subPackages": []
]
}

View File

@ -9,6 +9,7 @@
<template>
<view>
<mpDevBubble />
<fg-navbar>关于</fg-navbar>
<view
class="bg-white overflow-hidden pt-2 px-4"