dedsudiyu 3 months ago
commit
8a6380129c
100 changed files with 15253 additions and 0 deletions
  1. 9 0
      .hbuilderx/launch.json
  2. 85 0
      App.vue
  3. 29 0
      api/commonality/permission.js
  4. 371 0
      api/index.js
  5. 48 0
      api/request/config.js
  6. 3 0
      api/request/imagesUrl.styl
  7. 455 0
      api/request/request.js
  8. 21 0
      api/request/util.js
  9. 44 0
      component/public.js
  10. 70 0
      component/system-info.js
  11. 9 0
      devTools/READEME.MD
  12. 62 0
      devTools/config.js
  13. 156 0
      devTools/core/components/mpDevBubble.vue
  14. 142 0
      devTools/core/libs/createH5Bubble.js
  15. 129 0
      devTools/core/libs/devCache.js
  16. 191 0
      devTools/core/libs/devOptions.js
  17. 117 0
      devTools/core/libs/drawView.js
  18. 64 0
      devTools/core/libs/errorReport.js
  19. 334 0
      devTools/core/libs/jsonCompress.js
  20. 67 0
      devTools/core/libs/logReport.js
  21. 125 0
      devTools/core/libs/pageLinkList.js
  22. 74 0
      devTools/core/libs/pageStatistics.js
  23. 94 0
      devTools/core/libs/timeFormat.js
  24. 430 0
      devTools/core/proxy/console.js
  25. 36 0
      devTools/core/proxy/index.js
  26. 259 0
      devTools/core/proxy/request.js
  27. 113 0
      devTools/core/proxy/storage.js
  28. 151 0
      devTools/core/proxy/uniBus.js
  29. 192 0
      devTools/core/proxy/uniListen.js
  30. 163 0
      devTools/core/proxy/vueMixin.js
  31. 182 0
      devTools/index.js
  32. 992 0
      devTools/page/components/bottomTools.vue
  33. 203 0
      devTools/page/components/dialog/addStorage.vue
  34. 365 0
      devTools/page/components/dialog/createDir.vue
  35. 214 0
      devTools/page/components/dialog/dayOnlinePageList.vue
  36. 192 0
      devTools/page/components/dialog/editDialog.vue
  37. 183 0
      devTools/page/components/dialog/routeDialog.vue
  38. 501 0
      devTools/page/components/dialog/sendRequest.vue
  39. 374 0
      devTools/page/components/dialog/textFileEditDialog.vue
  40. 86 0
      devTools/page/components/libs/appDelDir.js
  41. 225 0
      devTools/page/components/libs/dirReader.js
  42. 15 0
      devTools/page/components/libs/fileSize.js
  43. 134 0
      devTools/page/components/libs/getRuntimeInfo.js
  44. 290 0
      devTools/page/components/listItem/consoleItem.vue
  45. 96 0
      devTools/page/components/listItem/dayOnlineItem.vue
  46. 281 0
      devTools/page/components/listItem/errorItem.vue
  47. 390 0
      devTools/page/components/listItem/fileSysItem.vue
  48. 70 0
      devTools/page/components/listItem/infoList.vue
  49. 212 0
      devTools/page/components/listItem/jsRunnerItem.vue
  50. 151 0
      devTools/page/components/listItem/logItem.vue
  51. 344 0
      devTools/page/components/listItem/networkItem.vue
  52. 733 0
      devTools/page/components/listItem/objectAnalysis.vue
  53. 102 0
      devTools/page/components/listItem/pageItem.vue
  54. 205 0
      devTools/page/components/listItem/pages.vue
  55. 77 0
      devTools/page/components/listItem/routeItem.vue
  56. 782 0
      devTools/page/components/listItem/setting.vue
  57. 318 0
      devTools/page/components/listItem/storageList.vue
  58. 307 0
      devTools/page/components/listItem/tools.vue
  59. 196 0
      devTools/page/components/listItem/vuexList.vue
  60. 1842 0
      devTools/page/components/main.vue
  61. 91 0
      devTools/page/components/mixins/animationControl.js
  62. 83 0
      devTools/page/components/mixins/mp.js
  63. 75 0
      devTools/page/components/ui/btnTabs.vue
  64. 172 0
      devTools/page/components/ui/codeHisPicker.vue
  65. 7 0
      devTools/page/components/ui/h5Cell.vue
  66. 224 0
      devTools/page/components/ui/menuBtn.vue
  67. 167 0
      devTools/page/components/ui/mobileSwiperScroll.vue
  68. 52 0
      devTools/page/components/ui/requestSpeedLimit.vue
  69. 55 0
      devTools/page/components/ui/requestTimeoutMock.vue
  70. 100 0
      devTools/page/components/ui/subTitleBar.vue
  71. 77 0
      devTools/page/index.nvue
  72. BIN
      devTools/page/static/copy.png
  73. BIN
      devTools/page/static/delete.png
  74. BIN
      devTools/page/static/fileSys/AI.png
  75. BIN
      devTools/page/static/fileSys/DWG.png
  76. BIN
      devTools/page/static/fileSys/EXE.png
  77. BIN
      devTools/page/static/fileSys/GIF.png
  78. BIN
      devTools/page/static/fileSys/HTML.png
  79. BIN
      devTools/page/static/fileSys/PSD.png
  80. BIN
      devTools/page/static/fileSys/RVT.png
  81. BIN
      devTools/page/static/fileSys/SKP.png
  82. BIN
      devTools/page/static/fileSys/SVG.png
  83. BIN
      devTools/page/static/fileSys/excel.png
  84. BIN
      devTools/page/static/fileSys/pdf.png
  85. BIN
      devTools/page/static/fileSys/pptl.png
  86. BIN
      devTools/page/static/fileSys/shipin.png
  87. BIN
      devTools/page/static/fileSys/tupian.png
  88. BIN
      devTools/page/static/fileSys/txt.png
  89. BIN
      devTools/page/static/fileSys/weizhiwenjian.png
  90. BIN
      devTools/page/static/fileSys/wenjianjia.png
  91. BIN
      devTools/page/static/fileSys/word.png
  92. BIN
      devTools/page/static/fileSys/yasuo.png
  93. BIN
      devTools/page/static/fileSys/yinpin.png
  94. BIN
      devTools/page/static/fold.png
  95. BIN
      devTools/page/static/menu.png
  96. BIN
      devTools/page/static/refresh.png
  97. BIN
      devTools/page/static/unfold.png
  98. 25 0
      devTools/tools.vue
  99. 20 0
      index.html
  100. 0 0
      main.js

+ 9 - 0
.hbuilderx/launch.json

@@ -0,0 +1,9 @@
+{
+    "version" : "1.0",
+    "configurations" : [
+        {
+            "openVueDevtools" : true,
+            "type" : "uni-app:h5"
+        }
+    ]
+}

+ 85 - 0
App.vue

@@ -0,0 +1,85 @@
+<script>
+	export default {
+		onLaunch: function() {
+			console.log('App Launch')
+		},
+		onShow: function() {
+			console.log('App Show')
+		},
+		onHide: function() {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="stylus">
+	/*** 每个页面公共css ***/
+	/*** H5方法 ***/
+	/* #ifdef WEB */
+	html,body{
+		width: 100%;
+		height: 100%;
+		flex:1;
+		display: flex;
+		flex-direction: column;
+		overflow: hidden;
+		background: #f5f5f5;
+		font-family: PingFang SC;
+		font-weight: 400;
+		font-size: 30rpx;
+		color: #333;
+		uni-app{
+			flex:1;
+			display: flex;
+			flex-direction: column;
+			overflow: hidden;
+			uni-page{
+				flex:1;
+				display: flex;
+				flex-direction: column;
+				overflow: hidden;
+				uni-page-wrapper{
+					flex:1;
+					display: flex;
+					flex-direction: column;
+					overflow: hidden;
+					uni-page-body{
+						flex:1;
+						display: flex;
+						flex-direction: column;
+						overflow: hidden;
+					}
+				}
+			}
+		}
+	}
+	/* #endif */
+	
+	/*** 微信方法 ***/
+	/* #ifdef MP-WEIXIN */
+	page {
+		height: 100%;
+		width: 100%;
+		display: flex;
+		flex-direction: column;
+		overflow: hidden;
+		background: #f5f5f5;
+		font-family: PingFang SC;
+		font-weight: 400;
+		font-size: 30rpx;
+		color: #333;
+	}
+	view {
+		margin: 0;
+		padding: 0;
+	}
+	/* #endif */
+	
+	img {
+		margin: 0;
+		padding: 0;
+		display: block;
+	}
+	/*每个页面公共css */
+	// @import './styles/index.styl'
+</style>

+ 29 - 0
api/commonality/permission.js

@@ -0,0 +1,29 @@
+import { apiResquest,apiResquestForm,apiResquestJsonList,apiResquestFormVideo,apiResquestTimer } from '@/api/request/request.js'
+
+/*查询用户列表-下拉列表
+* userName : '', 名称
+* userType: '',  0.系统 1.老师 2.学生
+*/
+export const systemUserDropList = (data) => {
+	return apiResquest({
+		url: `/system/user/dropList`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//根据权限查询实验室列表
+export const systemSubjectGetListByPower = (data) => {
+	return apiResquest({
+		url: `/system/subject/getListByPower`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//根据权限查询实验室列表
+export const laboratorySubRelInfoSelectInfoByPage = (data) => {
+	return apiResquest({
+		url: `/laboratory/subRelInfo/selectInfoByPage`,
+		method: 'POST',
+		data: { ...data }
+	})
+};

+ 371 - 0
api/index.js

@@ -0,0 +1,371 @@
+import { apiResquest,apiResquestForm,apiResquestJsonList,apiResquestFormVideo,apiResquestTimer } from '@/api/request/request.js'
+
+//实验室-列表-获取学院列表下拉列表
+export const systemDeptDropList = (data) => {
+	return apiResquest({
+		url: `/system/dept/dropList`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//查询楼栋楼层
+export const laboratoryBigViewGetBuildByBigView = (data) => {
+	return apiResquest({
+		url: `/laboratory/bigView/getBuildByBigView`,
+		method: 'GET',
+		data: data,
+	})
+};
+
+
+/*根据校区/楼栋/楼层查询实验室列表
+  deptId:'校区查询'
+  buildId:'楼栋查询'
+  floorId:'楼层查询'
+*/
+//实验室-列表
+export const laboratorySubRelInfoGetListByFloor = (data) => {
+	return apiResquest({
+		url: `/laboratory/subRelInfo/getListByFloor`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+
+//根据学院id查询实验室列表
+export const laboratorySubRelInfoGetRelList = (data) => {
+	return apiResquest({
+		url: `/laboratory/subRelInfo/getRelList`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//根据学院id查询实验室列表-带权限
+export const laboratorySubRelInfoGetRelListByCondition = (data) => {
+	return apiResquest({
+		url: `/laboratory/subRelInfo/getRelListByCondition`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//字典
+export const getDicts = (dictType) => {
+	return apiResquest({
+		url: '/system/dict/item/option?dictCode=' + dictType,
+		method: 'get',
+	})
+};
+
+
+/***开展检查***/
+//开展检查-获取巡查计划管理列表
+export const checkManageList  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkManage/appList`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//开展检查-根据实验室id查询该实验室有关计划
+export const getCheckPlanBySubId = (data) => {
+    return apiResquestForm({
+        url: '/zd-security/checkManage/getCheckPlanBySubId',
+        method: 'GET',
+        data: data
+    })
+};
+//开展检查--新增--有计划的时候
+export const checkManageUpdate  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkManage/appUpdate`,
+        method: 'PUT',
+        data: {...data}
+    })
+};
+//开展检查--新增-该实验室没有计划的时候
+export const checkManageAdd  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkManage/add`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//开展检查-通过id查询巡查管理
+export const findCheckManage = (data) => {
+    return apiResquestForm({
+        url: '/zd-security/checkManage/appFindCheckManage',
+        method: 'GET',
+        data: data
+    })
+};
+//开展检查-根据计划id查询附件列表
+export const findByPlanIdAttachment = (data) => {
+    return apiResquestForm({
+        url: '/zd-security/upload/findByCheckPlanId',
+        method: 'GET',
+        data: data
+    })
+};
+//开展检查-根据实验室id查询该实验室详情
+export const subjectFindSubjectInfo = (data) => {
+    return apiResquestForm({
+        url: '/zd-laboratory/subject/findSubjectInfo',
+        method: 'GET',
+        data: data
+    })
+};
+//项目检查库-树状列表
+export const checkOptionListNew = (data) => {
+    return apiResquestForm({
+        url: '/zd-security/checkOption/list',
+        method: 'GET',
+        data: data
+    })
+};
+
+//专项检查-根据选择的设备id生成检查项
+export const getHazardInfoByJoinIds  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkManage/getHazardInfoByJoinIds`,
+        method: 'GET',
+        data: data,
+    })
+};
+//专项检查-根据选择的检查项查询当前项出现的次数
+export const countHazardNum  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkHazard/countHazardNum`,
+        method: 'GET',
+        data: data,
+    })
+};
+
+//专项检查-根据实验室id查询设备
+export const getHazardInfoBySubId  = (data) => {
+    return apiResquest({
+        url: `/laboratory/hazard/getHazardInfoBySubId`,
+        method: 'GET',
+        data: data,
+    })
+};
+//巡查计划--检查计划各检查状态数据数量
+export const getCheckStatusCount  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkPlan/getCheckStatusCount`,
+        method: 'GET',
+        data: data,
+    })
+};
+//各检查状态数据数量
+export const getManageStatusCount  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkManage/getManageStatusCount`,
+        method: 'GET',
+        data: data,
+    })
+};
+//数据统计
+export const dataStatistics  = (data) => {
+    return apiResquest({
+        url: `/security/DataStatistics/countApp`,
+        method: 'GET',
+        data: data,
+    })
+};
+/* 巡查计划------------------------------ */
+//巡查计划--列表
+export const checkPlanList  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkPlan/appList`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//巡查计划--添加
+export const checkPlanAdd  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkPlan/appAdd`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//巡查计划--全校学院/实验室数量
+export const getAllSubNum  = (data) => {
+    return apiResquest({
+        url: `/laboratory/subject/getAllSubNum`,
+        method: 'GET',
+        data: data,
+    })
+};
+//巡查计划-查询是否院级用户
+export const isSCollege  = (data) => {
+    return apiResquest({
+        url: `/security/collegCheck/isSCollege`,
+        method: 'GET',
+        data: data,
+    })
+};
+//巡查计划-选择学院-带实验室数量
+export const laboratorySubRelInfoConditionCollegeInfo  = (data) => {
+    return apiResquest({
+        url: `/laboratory/subRelInfo/conditionCollegeInfo`,
+        method: 'GET',
+        data: data,
+    })
+};
+//巡查计划-选择全校-带学院和实验室数量
+export const laboratorySubRelInfoGetAllSubNum  = (data) => {
+    return apiResquest({
+        url: `/laboratory/subRelInfo/getAllSubNum`,
+        method: 'GET',
+        data: data,
+    })
+};
+
+//专项检查/巡查计划-检查类型
+export const dangerList  = (data) => {
+    return apiResquest({
+        url: `/system/dict/data/listNotPower?dictType=lab_hazard_type`,
+        method: 'GET',
+        data: data,
+    })
+};
+//专项检查/巡查计划-设备查询
+export const findDeviceList  = (data) => {
+    return apiResquest({
+        url: `/laboratory/hazard/queryHazardList`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//专项检查/巡查计划-设备确定提交
+export const haveHazardInSub  = (data) => {
+    return apiResquest({
+        url: `/laboratory/hazard/haveHazardInSub`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//巡查计划--获取巡查组列表
+export const checkGroupList  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkGroup/appFindGroupList`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//巡查计划--巡查组详情
+export const checkGroupDetail  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkGroup/appFind`,
+        method: 'GET',
+        data: data,
+    })
+};
+//巡查计划--详情
+export const checkPlanById  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkPlan/appFindCheckPlan`,
+        method: 'GET',
+        data: data,
+    })
+};
+//巡查计划--修改
+export const checkPlanEdit  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkPlan/appUpdate`,
+        method: 'PUT',
+        data: {...data}
+    })
+};
+//巡查计划--添加
+export const findByCheckPlanId  = (data) => {
+    return apiResquest({
+        url: `/zd-security/checkManage/appFindByCheckPlanId`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+
+//开展检查-安全隐患列表
+export const checkHazardAppList = (data) => {
+    return apiResquestForm({
+        url: '/security/checkHazard/appCheckHazardList',
+        method: 'GET',
+        data: data
+    })
+};
+//开展检查-安全隐患详情
+export const getHazardById = (data) => {
+    return apiResquestForm({
+        url: '/security/checkHazard/getAppHazardById',
+        method: 'GET',
+        data: data
+    })
+};
+//开展检查-获取巡查计划管理列表
+export const checkHazardAdd  = (data) => {
+    return apiResquest({
+        url: `/security/checkRectify/addOrApprove`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//随手拍-----------------------------------------------------
+//随手拍--添加
+export const checkClapAdd  = (data) => {
+    return apiResquest({
+        url: `/security/checkClap/appAdd`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//随手拍--管理列表
+export const checkClapList  = (data) => {
+    return apiResquest({
+        url: `/security/checkClap/appList`,
+        method: 'GET',
+        data: data,
+    })
+};
+//随手拍--上报记录-列表
+export const checkClapMylist  = (data) => {
+    return apiResquest({
+        url: `/security/checkClap/mylist`,
+        method: 'GET',
+        data: data,
+    })
+};
+//随手拍--获取当前登录人身份信息
+export const getUserCollegCheck  = (data) => {
+    return apiResquest({
+        url: `/security/collegCheck/getloginUser`,
+        method: 'GET',
+        data: data,
+    })
+};
+
+//随手拍--详情
+export const checkClapById  = (data) => {
+    return apiResquest({
+        url: `/security/checkClap/appFindById`,
+        method: 'GET',
+        data: data,
+    })
+};
+//随手拍--整改
+export const checkClapRectify  = (data) => {
+    return apiResquest({
+        url: `/security/checkClap/clapApprove`,
+        method: 'POST',
+        data: {...data}
+    })
+};
+//根据名称查询实验室
+export const subjectList = (data) => {
+    return apiResquestForm({
+        url: '/laboratory/subject/list/listAdmin',
+        method: 'GET',
+        data: data
+    })
+};

+ 48 - 0
api/request/config.js

@@ -0,0 +1,48 @@
+const config = {
+	/************************************ 后台服务地址 ************************************/
+	
+	// base_url: 'http://192.168.1.43/api', //43服务器
+	// base_url: 'http://192.168.1.8/api',//1.8服务器
+	// base_url: 'http://192.168.1.88/labSystem',//1.88服务器
+	// base_url: 'http://192.168.1.9:8080',//柴
+	// base_url: 'http://192.168.1.24:8080',//林总
+	// base_url: 'http://192.168.1.7:8080',//刘波
+	// base_url: 'http://192.168.1.17:8080',//小飞
+	// base_url: 'http://192.168.1.43/api',//小飞
+	// base_url: 'http://192.168.1.20:8080',//志伟
+	// base_url: 'http://192.168.1.39:8080',//高升
+	// base_url: 'http://192.168.1.29:8080',//何成
+	// base_url: 'https://demo.zjznai.com/xzgd/',
+	// base_url: 'https://lab.zjznai.com/labNhSystem/',//43服务器高升测试
+	// base_url: 'https://lab.zjznai.com/labTest/',//1.8外网
+	// base_url: 'https://lab.zjznai.com/labapp/',//43服务器线上
+	base_url: 'https://lab.zjznai.com/appTest/',//88服务器线上
+	// base_url: 'https://lab.zjznai.com/labSystem/', //矿大地址
+	// base_url: 'https://lab.zjznai.com/jdlabSystem/', //交大地址
+	// base_url: 'https://lab.zjznai.com/jndxlabSystem/', //暨大地址
+	// base_url: 'https://lab.zjznai.com/api/', //暨大化材
+	// base_url: 'https://lab.zjznai.com/kdwclabSystem/', //矿大文昌地址
+    //base_url: 'https://znyj.zjznai.suda.edu.cn/labSystem/', //苏大临时地址
+	// base_url: 'https://labcontrol.nwafu.edu.cn/api/', //西北农林
+	// base_url: 'http://172.16.0.65/api/', //西北农林
+	//base_url: 'https://lab.zjznai.com/labapp/', //43测试
+	// base_url: 'https://metersphere.zjznai.com/labSystem/', //中国海关
+	
+	/************************************ 图片服务地址 ************************************/
+	//(imagesUrl.styl 文件内还需要配置一次css相关地址注意是否与imagesUrl.styl一致)
+	imagesUrl: 'https://zj-wechat.oss-cn-beijing.aliyuncs.com/xcx_images/xcx_v3/',
+	/************************************ 扫一扫 ************************************/
+	//1.MSDS说明书			2.安全制度			3.危险源详情 地址配置 
+	saoCodeTypeUrl: 'http://192.168.1.88/labSystem/admin/#/codeHtml?',
+	/************************************ 校级管理员身份ID ************************************/
+  schoolId:'1845666757636263938',
+	/************************************ 院级管理员身份ID ************************************/
+  collegeId:'1948620835518754817',
+	/************************************ 版本标识-用于区别各学校差异化代码 ************************************/
+	VERSION_DIFFERENCE_FIELD :'xiBeiNongLinDaXue',
+	/************************************ H5统一认证地址-用于H5版本统一认证 ************************************/
+	UNIFIED_CERTIFICATION_URL:'https://labcontrol.nwafu.edu.cn/api/auth/cas/logout/applet',
+}
+export {
+	config
+}

+ 3 - 0
api/request/imagesUrl.styl

@@ -0,0 +1,3 @@
+/****** css样式图片服务地址 ******/
+//config.js 文件内还需要配置一次相关地址注意是否与config.js一致
+$imagesUrl = "https://zj-wechat.oss-cn-beijing.aliyuncs.com/xcx_images/xcx_v3/"

+ 455 - 0
api/request/request.js

@@ -0,0 +1,455 @@
+import {
+	config
+} from './config.js'
+import {
+	tansParams
+} from "./util.js";
+
+export const apiResquest = (prams) => {
+	return new Promise((resolve, reject) => {
+		let url = config.base_url + prams.url;
+		uni.showLoading({
+			title: '加载中',
+			mask: true
+		});
+		// get请求映射params参数
+		if (prams.method === 'GET' && prams.data) {
+			url = url + '?' + tansParams(prams.data);
+			url = url.slice(0, -1);
+			prams.data = {};
+		}
+
+		return uni.request({
+			timeout:10000,
+			url: url,
+			data: {
+				...prams.data
+			},
+			method: prams.method,
+			header: {
+				'content-type': 'application/json;charset=utf-8',
+				'Authorization': uni.getStorageSync('token')
+			},
+			success: (res) => {
+				// 成功
+				uni.hideLoading()
+				if (res.statusCode == 200) {
+					if (res.data.code == 200) {
+						resolve(res);
+					} else if (res.data.code == 401) {
+						loginTimeout();
+					} else {
+						uni.showToast({
+							mask: true,
+							icon: "none",
+							position: "center",
+							title: res.data.message,
+							duration: 2000
+						});
+						resolve(res);
+					}
+				} else if (res.statusCode == 401) {
+					loginTimeout();
+				} else {
+					uni.showToast({
+						mask: true,
+						icon: "none",
+						position: "center",
+						title: '连接异常,请联系管理员.',
+						duration: 2000
+					});
+					resolve(res);
+				}
+			},
+			fail: (err) => {
+				// 失败
+				uni.hideLoading()
+				uni.showToast({
+					mask: true,
+					icon: "none",
+					position: "center",
+					title: '出错啦~请联系管理员!',
+					duration: 2000
+				});
+			},
+			complete: () => {
+				// 完成
+			}
+		});
+	})
+}
+
+export const apiResquestOutside = (prams) => {
+	return new Promise((resolve, reject) => {
+		let url =  prams.url;
+		uni.showLoading({
+			title: '加载中',
+			mask: true
+		});
+		// get请求映射params参数
+		if (prams.method === 'GET' && prams.data) {
+			url = url + '?' + tansParams(prams.data);
+			url = url.slice(0, -1);
+			prams.data = {};
+		}
+
+		return uni.request({
+			timeout:10000,
+			url: url,
+			data: {
+				...prams.data
+			},
+			method: prams.method,
+			header: {
+				'content-type': 'application/json;charset=utf-8',
+				'Authorization': uni.getStorageSync('token')
+			},
+			success: (res) => {
+				// 成功
+				uni.hideLoading()
+				if (res.statusCode == 200) {
+					if (res.data.code == 200) {
+						resolve(res);
+					} else if (res.data.code == 401) {
+						loginTimeout();
+					} else {
+						uni.showToast({
+							mask: true,
+							icon: "none",
+							position: "center",
+							title: res.data.message,
+							duration: 2000
+						});
+						resolve(res);
+					}
+				} else if (res.statusCode == 401) {
+					loginTimeout();
+				} else {
+					uni.showToast({
+						mask: true,
+						icon: "none",
+						position: "center",
+						title: '连接异常,请联系管理员.',
+						duration: 2000
+					});
+					resolve(res);
+				}
+			},
+			fail: (err) => {
+				// 失败
+				uni.hideLoading()
+				uni.showToast({
+					mask: true,
+					icon: "none",
+					position: "center",
+					title: '出错啦~请联系管理员!',
+					duration: 2000
+				});
+			},
+			complete: () => {
+				// 完成
+			}
+		});
+	})
+}
+export const apiResquestForm = (prams) => {
+	return new Promise((resolve, reject) => {
+		let url = config.base_url + prams.url;
+		uni.showLoading({
+			title: '加载中',
+			mask: true
+		});
+		// get请求映射params参数
+		if (prams.method === 'GET' && prams.data) {
+			url = url + '?' + tansParams(prams.data);
+			url = url.slice(0, -1);
+			prams.data = {};
+		}
+		return uni.request({
+			timeout:10000,
+			url: url,
+			data: {
+				...prams.data
+			},
+			method: prams.method,
+			header: {
+				'content-type': 'application/x-www-form-urlencoded',
+				'Authorization': uni.getStorageSync('token')
+			},
+			success: (res) => {
+				// 成功
+				uni.hideLoading()
+				if (res.statusCode == 200) {
+					if (res.data.code == 200) {
+						resolve(res);
+					} else if (res.data.code == 401) {
+						loginTimeout();
+					} else {
+						uni.showToast({
+							mask: true,
+							icon: "none",
+							position: "center",
+							title: res.data.message,
+							duration: 2000
+						});
+						resolve(res);
+					}
+				} else if (res.statusCode == 401) {
+					loginTimeout();
+				} else {
+					uni.showToast({
+						mask: true,
+						icon: "none",
+						position: "center",
+						title: '连接异常,请联系管理员.',
+						duration: 2000
+					});
+					resolve(res);
+				}
+			},
+			fail: (err) => {
+				// 失败
+				uni.hideLoading()
+				uni.showToast({
+					mask: true,
+					icon: "none",
+					position: "center",
+					title: '出错啦~请联系管理员!',
+					duration: 2000
+				});
+			},
+			complete: () => {
+				// 完成
+			}
+		});
+	})
+}
+export const apiResquestJsonList = (prams) => {
+	return new Promise((resolve, reject) => {
+		let url = config.base_url + prams.url;
+		uni.showLoading({
+			title: '加载中',
+			mask: true
+		});
+		// get请求映射params参数
+		if (prams.method === 'GET' && prams.data) {
+			url = url + '?' + tansParams(prams.data);
+			url = url.slice(0, -1);
+			prams.data = {};
+		}
+		return uni.request({
+			timeout:10000,
+			url: url,
+			data: prams.data,
+			method: prams.method,
+			header: {
+				'content-type': 'application/json',
+				'Authorization': uni.getStorageSync('token')
+			},
+			success: (res) => {
+				// 成功
+				uni.hideLoading()
+				if (res.statusCode == 200) {
+					if (res.data.code == 200) {
+						resolve(res);
+					} else if (res.data.code == 401) {
+						loginTimeout();
+					} else {
+						uni.showToast({
+							mask: true,
+							icon: "none",
+							position: "center",
+							title: res.data.message,
+							duration: 2000
+						});
+						resolve(res);
+					}
+				} else if (res.statusCode == 401) {
+					loginTimeout();
+				} else {
+					uni.showToast({
+						mask: true,
+						icon: "none",
+						position: "center",
+						title: '连接异常,请联系管理员.',
+						duration: 2000
+					});
+					resolve(res);
+				}
+			},
+			fail: (err) => {
+				// 失败
+				uni.hideLoading()
+				uni.showToast({
+					mask: true,
+					icon: "none",
+					position: "center",
+					title: '出错啦~请联系管理员!',
+					duration: 2000
+				});
+			},
+			complete: () => {
+				// 完成
+			}
+		});
+	})
+}
+export const apiResquestFormVideo = (prams) => {
+	return new Promise((resolve, reject) => {
+		let url = uni.getStorageSync('cameraExtranetAgent') + prams.url;
+		uni.showLoading({
+			title: '加载中',
+			mask: true
+		});
+		// get请求映射params参数
+		if (prams.method === 'GET' && prams.data) {
+			url = url + '?' + tansParams(prams.data);
+			url = url.slice(0, -1);
+			prams.data = {};
+		}
+		return uni.request({
+			timeout:10000,
+			url: url,
+			data: {
+				...prams.data
+			},
+			method: prams.method,
+			header: {
+				'content-type': 'application/x-www-form-urlencoded',
+				'Authorization': uni.getStorageSync('token')
+			},
+			success: (res) => {
+				// 成功
+				uni.hideLoading()
+				if (res.statusCode == 200) {
+					if (res.data.code == 200) {
+						resolve(res);
+					} else if (res.data.code == 401) {
+						loginTimeout();
+					} else {
+						uni.showToast({
+							mask: true,
+							icon: "none",
+							position: "center",
+							title: res.data.message,
+							duration: 2000
+						});
+						resolve(res);
+					}
+				} else if (res.statusCode == 401) {
+					loginTimeout();
+				} else {
+					uni.showToast({
+						mask: true,
+						icon: "none",
+						position: "center",
+						title: '连接异常,请联系管理员.',
+						duration: 2000
+					});
+					resolve(res);
+				}
+			},
+			fail: (err) => {
+				// 失败
+				uni.hideLoading()
+				uni.showToast({
+					mask: true,
+					icon: "none",
+					position: "center",
+					title: '出错啦~请联系管理员!',
+					duration: 2000
+				});
+			},
+			complete: () => {
+				// 完成
+			}
+		});
+	})
+}
+export const apiResquestTimer = (prams) => {
+	return new Promise((resolve, reject) => {
+		let url = config.base_url + prams.url;
+		// get请求映射params参数
+		if (prams.method === 'GET' && prams.data) {
+			url = url + '?' + tansParams(prams.data);
+			url = url.slice(0, -1);
+			prams.data = {};
+		}
+
+		return uni.request({
+			timeout:10000,
+			url: url,
+			data: {
+				...prams.data
+			},
+			method: prams.method,
+			header: {
+				'content-type': 'application/json;charset=utf-8',
+				'Authorization': uni.getStorageSync('token')
+			},
+			success: (res) => {
+				// 成功
+				uni.hideLoading()
+				if (res.statusCode == 200) {
+					if (res.data.code == 200) {
+						resolve(res);
+					} else if (res.data.code == 401) {
+						loginTimeout();
+					} else {
+						uni.showToast({
+							mask: true,
+							icon: "none",
+							position: "center",
+							title: res.data.message,
+							duration: 2000
+						});
+						resolve(res);
+					}
+				} else if (res.statusCode == 401) {
+					loginTimeout();
+				} else {
+					uni.showToast({
+						mask: true,
+						icon: "none",
+						position: "center",
+						title: '连接异常,请联系管理员.',
+						duration: 2000
+					});
+					resolve(res);
+				}
+			},
+			fail: (err) => {
+				// 失败
+				uni.hideLoading()
+				uni.showToast({
+					mask: true,
+					icon: "none",
+					position: "center",
+					title: '出错啦~请联系管理员!',
+					duration: 2000
+				});
+			},
+			complete: () => {
+				// 完成
+			}
+		});
+	})
+}
+//登录超时-退出至登录页面
+export function loginTimeout(params) {
+	uni.showToast({
+		mask: true,
+		icon: "none",
+		position: "center",
+		title: "登录超时,请重新登录~",
+		duration: 2000
+	});
+	uni.removeStorageSync('token');
+	uni.removeStorageSync('userId');
+	uni.removeStorageSync('userType');
+	setTimeout(function() {
+		uni.redirectTo({
+			url: '/pages/views/login/login',
+		});
+	}, 2000);
+}

+ 21 - 0
api/request/util.js

@@ -0,0 +1,21 @@
+export function tansParams(params) {
+    let result = ''
+    for (const propName of Object.keys(params)) {
+        const value = params[propName];
+        var part = encodeURIComponent(propName) + "=";
+        if (value !== null && typeof (value) !== "undefined") {
+            if (typeof value === 'object') {
+                for (const key of Object.keys(value)) {
+                    if (value[key] !== null && typeof (value[key]) !== 'undefined') {
+                        let params = propName + '[' + key + ']';
+                        var subPart = encodeURIComponent(params) + "=";
+                        result += subPart + encodeURIComponent(value[key]) + "&";
+                    }
+                }
+            } else {
+                result += part + encodeURIComponent(value) + "&";
+            }
+        }
+    }
+    return result
+}

+ 44 - 0
component/public.js

@@ -0,0 +1,44 @@
+// 日期格式化
+export function parseTime(time, pattern) {
+	if (arguments.length === 0 || !time) {
+		return null
+	}
+	if(time.indexOf('T')!== -1){
+    let newTime = time.split('T')
+    time = newTime[0] + ' ' + newTime[1]
+  }
+	const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+	let date
+	if (typeof time === 'object') {
+		date = time
+	} else {
+		if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+			time = parseInt(time)
+		} else if (typeof time === 'string') {
+			time = time.replace(new RegExp(/-/gm), '/');
+		}
+		if ((typeof time === 'number') && (time.toString().length === 10)) {
+			time = time * 1000
+		}
+		date = new Date(time)
+	}
+	const formatObj = {
+		y: date.getFullYear(),
+		m: date.getMonth() + 1,
+		d: date.getDate(),
+		h: date.getHours(),
+		i: date.getMinutes(),
+		s: date.getSeconds(),
+		a: date.getDay()
+	}
+	const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+		let value = formatObj[key]
+		// Note: getDay() returns 0 on Sunday
+		if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+		if (result.length > 0 && value < 10) {
+			value = '0' + value
+		}
+		return value || 0
+	})
+	return time_str
+}

+ 70 - 0
component/system-info.js

@@ -0,0 +1,70 @@
+/**
+ * 此js文件管理关于当前设备的机型系统信息
+ */
+const systemInfo = function() {
+	/****************** 所有平台共有的系统信息 ********************/
+	// 设备系统信息
+	let systemInfomations = uni.getSystemInfoSync()
+	// 机型适配比例系数
+	let scaleFactor = 750 / systemInfomations.windowWidth
+	// 当前机型-屏幕高度
+	let windowHeight = systemInfomations.windowHeight * scaleFactor //rpx
+	// 当前机型-屏幕宽度
+	let windowWidth = systemInfomations.windowWidth * scaleFactor //rpx
+	// 状态栏高度
+	let statusBarHeight = (systemInfomations.statusBarHeight) * scaleFactor //rpx
+ 
+	// 导航栏高度  注意:此导航栏高度只针对微信小程序有效 其他平台如自定义导航栏请使用:状态栏高度+自定义文本高度
+	let navHeight = 0 //rpx
+	// console.log(windowHeight,'哈哈哈哈哈');
+	
+	/****************** 微信小程序头部胶囊信息 ********************/
+	// #ifdef MP-WEIXIN
+	const menuButtonInfo = wx.getMenuButtonBoundingClientRect()
+	// 胶囊高度
+	let menuButtonHeight = menuButtonInfo.height * scaleFactor //rpx
+	// 胶囊宽度
+	let menuButtonWidth = menuButtonInfo.width * scaleFactor //rpx
+	// 胶囊上边界的坐标
+	let menuButtonTop = menuButtonInfo.top * scaleFactor //rpx
+	// 胶囊右边界的坐标
+	let menuButtonRight = menuButtonInfo.right * scaleFactor //rpx
+	// 胶囊下边界的坐标
+	let menuButtonBottom = menuButtonInfo.bottom * scaleFactor //rpx
+	// 胶囊左边界的坐标
+	let menuButtonLeft = menuButtonInfo.left * scaleFactor //rpx
+ 
+	// 微信小程序中导航栏高度 = 胶囊高度 + (顶部距离 - 状态栏高度) * 2
+	navHeight = menuButtonHeight + (menuButtonTop - statusBarHeight) * 2
+	// #endif
+ 
+ 
+	// #ifdef MP-WEIXIN
+	return {
+		scaleFactor,
+		windowHeight,
+		windowWidth,
+		statusBarHeight,
+		menuButtonHeight,
+		menuButtonWidth,
+		menuButtonTop,
+		menuButtonRight,
+		menuButtonBottom,
+		menuButtonLeft,
+		navHeight
+	}
+	// #endif
+ 
+	// #ifndef MP-WEIXIN
+	return {
+		scaleFactor,
+		windowHeight,
+		windowWidth,
+		statusBarHeight
+	}
+	// #endif
+}
+ 
+export {
+	systemInfo
+}

+ 9 - 0
devTools/READEME.MD

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

+ 62 - 0
devTools/config.js

@@ -0,0 +1,62 @@
+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;

+ 156 - 0
devTools/core/components/mpDevBubble.vue

@@ -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>

+ 142 - 0
devTools/core/libs/createH5Bubble.js

@@ -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;

+ 129 - 0
devTools/core/libs/devCache.js

@@ -0,0 +1,129 @@
+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);
+  },
+}
+

+ 191 - 0
devTools/core/libs/devOptions.js

@@ -0,0 +1,191 @@
+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 {}
+  }
+}

+ 117 - 0
devTools/core/libs/drawView.js

@@ -0,0 +1,117 @@
+/**
+ * 绘制调试工具
+ */
+
+/**
+ * 入口文件
+ */
+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;

+ 64 - 0
devTools/core/libs/errorReport.js

@@ -0,0 +1,64 @@
+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;

+ 334 - 0
devTools/core/libs/jsonCompress.js

@@ -0,0 +1,334 @@
+/**
+ * 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 "";
+    }
+  }
+}

+ 67 - 0
devTools/core/libs/logReport.js

@@ -0,0 +1,67 @@
+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;

+ 125 - 0
devTools/core/libs/pageLinkList.js

@@ -0,0 +1,125 @@
+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);
+  },
+}

+ 74 - 0
devTools/core/libs/pageStatistics.js

@@ -0,0 +1,74 @@
+
+/**
+ * !页面统计:访问次数、停留时长
+ */
+
+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;

+ 94 - 0
devTools/core/libs/timeFormat.js

@@ -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);
+	}
+}

+ 430 - 0
devTools/core/proxy/console.js

@@ -0,0 +1,430 @@
+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);
+    }
+  },
+
+}

+ 36 - 0
devTools/core/proxy/index.js

@@ -0,0 +1,36 @@
+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);
+  }
+
+}

+ 259 - 0
devTools/core/proxy/request.js

@@ -0,0 +1,259 @@
+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);
+  })
+}

+ 113 - 0
devTools/core/proxy/storage.js

@@ -0,0 +1,113 @@
+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);
+    }
+  },
+}

+ 151 - 0
devTools/core/proxy/uniBus.js

@@ -0,0 +1,151 @@
+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);
+  },
+}

+ 192 - 0
devTools/core/proxy/uniListen.js

@@ -0,0 +1,192 @@
+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);
+      }
+    })
+  },
+}

+ 163 - 0
devTools/core/proxy/vueMixin.js

@@ -0,0 +1,163 @@
+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);
+    }
+  },
+}

+ 182 - 0
devTools/index.js

@@ -0,0 +1,182 @@
+
+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;

+ 992 - 0
devTools/page/components/bottomTools.vue

@@ -0,0 +1,992 @@
+<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>

+ 203 - 0
devTools/page/components/dialog/addStorage.vue

@@ -0,0 +1,203 @@
+<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>

+ 365 - 0
devTools/page/components/dialog/createDir.vue

@@ -0,0 +1,365 @@
+<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>

+ 214 - 0
devTools/page/components/dialog/dayOnlinePageList.vue

@@ -0,0 +1,214 @@
+<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>

+ 192 - 0
devTools/page/components/dialog/editDialog.vue

@@ -0,0 +1,192 @@
+<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>

+ 183 - 0
devTools/page/components/dialog/routeDialog.vue

@@ -0,0 +1,183 @@
+<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>

+ 501 - 0
devTools/page/components/dialog/sendRequest.vue

@@ -0,0 +1,501 @@
+<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>

+ 374 - 0
devTools/page/components/dialog/textFileEditDialog.vue

@@ -0,0 +1,374 @@
+<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) => {
+              // 可通过entry对象操作test.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>

+ 86 - 0
devTools/page/components/libs/appDelDir.js

@@ -0,0 +1,86 @@
+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
+    );
+  })
+}

+ 225 - 0
devTools/page/components/libs/dirReader.js

@@ -0,0 +1,225 @@
+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 ""
+  }
+}

+ 15 - 0
devTools/page/components/libs/fileSize.js

@@ -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];
+  }
+}

+ 134 - 0
devTools/page/components/libs/getRuntimeInfo.js

@@ -0,0 +1,134 @@
+/**
+ * 获取当前运行时环境信息
+ */
+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 获取失败!" });
+      },
+    });
+  });
+}

+ 290 - 0
devTools/page/components/listItem/consoleItem.vue

@@ -0,0 +1,290 @@
+<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>

+ 96 - 0
devTools/page/components/listItem/dayOnlineItem.vue

@@ -0,0 +1,96 @@
+<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>

+ 281 - 0
devTools/page/components/listItem/errorItem.vue

@@ -0,0 +1,281 @@
+<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>

+ 390 - 0
devTools/page/components/listItem/fileSysItem.vue

@@ -0,0 +1,390 @@
+<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) => {
+                        // 可通过entry对象操作test.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>

+ 70 - 0
devTools/page/components/listItem/infoList.vue

@@ -0,0 +1,70 @@
+<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>

+ 212 - 0
devTools/page/components/listItem/jsRunnerItem.vue

@@ -0,0 +1,212 @@
+<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>

+ 151 - 0
devTools/page/components/listItem/logItem.vue

@@ -0,0 +1,151 @@
+<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>

+ 344 - 0
devTools/page/components/listItem/networkItem.vue

@@ -0,0 +1,344 @@
+<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>

+ 733 - 0
devTools/page/components/listItem/objectAnalysis.vue

@@ -0,0 +1,733 @@
+<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>

+ 102 - 0
devTools/page/components/listItem/pageItem.vue

@@ -0,0 +1,102 @@
+<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>

+ 205 - 0
devTools/page/components/listItem/pages.vue

@@ -0,0 +1,205 @@
+<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>

+ 77 - 0
devTools/page/components/listItem/routeItem.vue

@@ -0,0 +1,77 @@
+<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>

+ 782 - 0
devTools/page/components/listItem/setting.vue

@@ -0,0 +1,782 @@
+<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>

+ 318 - 0
devTools/page/components/listItem/storageList.vue

@@ -0,0 +1,318 @@
+<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>

+ 307 - 0
devTools/page/components/listItem/tools.vue

@@ -0,0 +1,307 @@
+<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>

+ 196 - 0
devTools/page/components/listItem/vuexList.vue

@@ -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
+ 1842 - 0
devTools/page/components/main.vue


+ 91 - 0
devTools/page/components/mixins/animationControl.js

@@ -0,0 +1,91 @@
+// #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")
+        }
+      )
+    },
+  }
+}

+ 83 - 0
devTools/page/components/mixins/mp.js

@@ -0,0 +1,83 @@
+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;
+}

+ 75 - 0
devTools/page/components/ui/btnTabs.vue

@@ -0,0 +1,75 @@
+<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>

+ 172 - 0
devTools/page/components/ui/codeHisPicker.vue

@@ -0,0 +1,172 @@
+<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>

+ 7 - 0
devTools/page/components/ui/h5Cell.vue

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

+ 224 - 0
devTools/page/components/ui/menuBtn.vue

@@ -0,0 +1,224 @@
+<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>

+ 167 - 0
devTools/page/components/ui/mobileSwiperScroll.vue

@@ -0,0 +1,167 @@
+<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>

+ 52 - 0
devTools/page/components/ui/requestSpeedLimit.vue

@@ -0,0 +1,52 @@
+<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>

+ 55 - 0
devTools/page/components/ui/requestTimeoutMock.vue

@@ -0,0 +1,55 @@
+<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>

+ 100 - 0
devTools/page/components/ui/subTitleBar.vue

@@ -0,0 +1,100 @@
+<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>

+ 77 - 0
devTools/page/index.nvue

@@ -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>

BIN
devTools/page/static/copy.png


BIN
devTools/page/static/delete.png


BIN
devTools/page/static/fileSys/AI.png


BIN
devTools/page/static/fileSys/DWG.png


BIN
devTools/page/static/fileSys/EXE.png


BIN
devTools/page/static/fileSys/GIF.png


BIN
devTools/page/static/fileSys/HTML.png


BIN
devTools/page/static/fileSys/PSD.png


BIN
devTools/page/static/fileSys/RVT.png


BIN
devTools/page/static/fileSys/SKP.png


BIN
devTools/page/static/fileSys/SVG.png


BIN
devTools/page/static/fileSys/excel.png


BIN
devTools/page/static/fileSys/pdf.png


BIN
devTools/page/static/fileSys/pptl.png


BIN
devTools/page/static/fileSys/shipin.png


BIN
devTools/page/static/fileSys/tupian.png


BIN
devTools/page/static/fileSys/txt.png


BIN
devTools/page/static/fileSys/weizhiwenjian.png


BIN
devTools/page/static/fileSys/wenjianjia.png


BIN
devTools/page/static/fileSys/word.png


BIN
devTools/page/static/fileSys/yasuo.png


BIN
devTools/page/static/fileSys/yinpin.png


BIN
devTools/page/static/fold.png


BIN
devTools/page/static/menu.png


BIN
devTools/page/static/refresh.png


BIN
devTools/page/static/unfold.png


+ 25 - 0
devTools/tools.vue

@@ -0,0 +1,25 @@
+<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>

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 0 - 0
main.js


Some files were not shown because too many files changed in this diff