achao %!s(int64=2) %!d(string=hai) anos
achega
5a6c9932ed
Modificáronse 100 ficheiros con 15330 adicións e 0 borrados
  1. 16 0
      .hbuilderx/launch.json
  2. 64 0
      App.vue
  3. 21 0
      LICENSE
  4. 19 0
      README.md
  5. 81 0
      admin.config.js
  6. 160 0
      changelog.md
  7. 199 0
      common/admin-icons.css
  8. 50 0
      common/theme.scss
  9. 542 0
      common/uni-icons.css
  10. 609 0
      common/uni.css
  11. 361 0
      components/download-excel/download-excel.vue
  12. 151 0
      components/download-excel/download.js
  13. 72 0
      components/fix-window/fix-window.vue
  14. 55 0
      components/show-info/show-info.vue
  15. 184 0
      components/uni-data-menu/uni-data-menu.vue
  16. 67 0
      components/uni-data-menu/util.js
  17. 49 0
      components/uni-menu-group/uni-menu-group.vue
  18. 133 0
      components/uni-menu-item/uni-menu-item.vue
  19. 48 0
      components/uni-menu-sidebar/uni-menu-sidebar.vue
  20. 29 0
      components/uni-nav-menu/mixins/rootParent.js
  21. 209 0
      components/uni-nav-menu/uni-nav-menu.vue
  22. 36 0
      components/uni-stat-breadcrumb/uni-stat-breadcrumb.vue
  23. 116 0
      components/uni-stat-panel/uni-stat-panel.vue
  24. 71 0
      components/uni-stat-table/uni-stat-table.vue
  25. 332 0
      components/uni-stat-tabs/uni-stat-tabs.vue
  26. 164 0
      components/uni-sub-menu/uni-sub-menu.vue
  27. 105 0
      i18n/en.json
  28. 8 0
      i18n/index.js
  29. 105 0
      i18n/zh-Hans.json
  30. 105 0
      i18n/zh-Hant.json
  31. 20 0
      index.html
  32. 66 0
      js_sdk/uni-admin/constants.js
  33. 42 0
      js_sdk/uni-admin/error.js
  34. 23 0
      js_sdk/uni-admin/fetchMock.js
  35. 15 0
      js_sdk/uni-admin/interceptor.js
  36. 27 0
      js_sdk/uni-admin/permission.js
  37. 28 0
      js_sdk/uni-admin/plugin.js
  38. 76 0
      js_sdk/uni-admin/request.js
  39. 38 0
      js_sdk/uni-admin/util.js
  40. 394 0
      js_sdk/uni-stat/util.js
  41. 117 0
      js_sdk/validator/cyt-logs.js
  42. 38 0
      js_sdk/validator/opendb-admin-log.js
  43. 69 0
      js_sdk/validator/opendb-admin-menus.js
  44. 123 0
      js_sdk/validator/opendb-app-list.js
  45. 157 0
      js_sdk/validator/opendb-app-versions.js
  46. 38 0
      js_sdk/validator/uni-id-log.js
  47. 84 0
      js_sdk/validator/uni-id-permissions.js
  48. 99 0
      js_sdk/validator/uni-id-roles.js
  49. 84 0
      js_sdk/validator/uni-id-tag.js
  50. 152 0
      js_sdk/validator/uni-id-users.js
  51. 264 0
      js_sdk/validator/uni-stat-app-crash-logs.js
  52. BIN=BIN
      log.zip
  53. 43 0
      main.js
  54. 82 0
      manifest.json
  55. 14 0
      mock/uni-stat/appOverview.json
  56. 17 0
      mock/uni-stat/appsDetail.json
  57. 11 0
      mock/uni-stat/db.js
  58. 87 0
      mock/uni-stat/event.json
  59. 508 0
      mock/uni-stat/pageContent.json
  60. 337 0
      mock/uni-stat/pageEnt.json
  61. 397 0
      mock/uni-stat/pageRes.json
  62. 132 0
      mock/uni-stat/pageRule.json
  63. 73 0
      mock/uni-stat/userActivity.json
  64. 93 0
      package.json
  65. 433 0
      pages.json
  66. 121 0
      pages/cyt-logs/add.vue
  67. 152 0
      pages/cyt-logs/edit.vue
  68. 240 0
      pages/cyt-logs/list.vue
  69. 111 0
      pages/demo/icons/icons.vue
  70. 132 0
      pages/demo/icons/uni-icons.js
  71. 146 0
      pages/demo/table/table.vue
  72. 193 0
      pages/demo/table/tableData.js
  73. 41 0
      pages/error/404.vue
  74. 82 0
      pages/index/fieldsMap.js
  75. 296 0
      pages/index/index.vue
  76. 123 0
      pages/opendb-admin-log/list.vue
  77. 490 0
      pages/system/app/add.vue
  78. 293 0
      pages/system/app/list.vue
  79. 273 0
      pages/system/app/mixin/publish_add_detail_mixin.js
  80. 161 0
      pages/system/app/uni-portal/uni-portal.vue
  81. 164 0
      pages/system/menu/add.vue
  82. 188 0
      pages/system/menu/edit.vue
  83. 360 0
      pages/system/menu/list.vue
  84. 98 0
      pages/system/permission/add.vue
  85. 128 0
      pages/system/permission/edit.vue
  86. 234 0
      pages/system/permission/list.vue
  87. 102 0
      pages/system/role/add.vue
  88. 132 0
      pages/system/role/edit.vue
  89. 236 0
      pages/system/role/list.vue
  90. 122 0
      pages/system/safety/list.vue
  91. 100 0
      pages/system/tag/add.vue
  92. 129 0
      pages/system/tag/edit.vue
  93. 230 0
      pages/system/tag/list.vue
  94. 193 0
      pages/system/user/add.vue
  95. 277 0
      pages/system/user/edit.vue
  96. 406 0
      pages/system/user/list.vue
  97. 476 0
      pages/uni-stat/channel/channel.vue
  98. 74 0
      pages/uni-stat/channel/fieldsMap.js
  99. 437 0
      pages/uni-stat/device/activity/activity.vue
  100. 48 0
      pages/uni-stat/device/activity/fieldsMap.js

+ 16 - 0
.hbuilderx/launch.json

@@ -0,0 +1,16 @@
+{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version": "0.0",
+    "configurations": [{
+     	"default" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"h5" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"type" : "uniCloud"
+     }
+    ]
+}

+ 64 - 0
App.vue

@@ -0,0 +1,64 @@
+<script>
+	import {
+		mapGetters,
+		mapActions,
+		mapMutations
+	} from 'vuex'
+	import config from '@/admin.config.js'
+	import {
+		version
+	} from './package.json'
+	export default {
+		created() {
+			this.clear = undefined
+		},
+		methods: {
+			...mapActions({
+				init: 'app/init'
+			}),
+			clearPlatform() {
+				const keysOfPlatform = uni.getStorageInfoSync().keys.filter(key => key.indexOf('platform') > -1)
+				keysOfPlatform.length && keysOfPlatform.forEach(key => uni.removeStorageSync(key))
+			}
+		},
+		onPageNotFound(msg) {
+			uni.redirectTo({
+				url: config.error.url
+			})
+		},
+		onLaunch: function() {
+			// #ifdef H5
+			console.log(
+				`%c uni-admin %c v${version} `,
+				'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px;  color: #fff',
+				'background:#007aff ;padding: 1px; border-radius: 0 3px 3px 0;  color: #fff; font-weight: bold;'
+			)
+			// #endif
+			// 线上示例使用
+			// console.log('%c uni-app官方团队诚邀优秀前端工程师加盟,一起打造更卓越的uni-app & uniCloud,欢迎投递简历到 hr2013@dcloud.io', 'color: red');
+			console.log('App Launch')
+			this.init()
+
+			// 登录成功回调
+			uni.$on('uni-id-pages-login-success', () => {
+				// this.setToken()
+				this.init()
+			})
+		},
+		onShow: function() {
+			console.log('App Show')
+			this.clear = setInterval(() => this.clearPlatform(), 15*60*1000)
+		},
+		onHide: function() {
+			console.log('App Hide')
+			this.clear && clearInterval(this.clear)
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import '@/common/uni.css';
+	@import '@/common/uni-icons.css';
+	@import '@/common/admin-icons.css';
+	@import '@/common/theme.scss';
+</style>

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 DCloud
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 19 - 0
README.md

@@ -0,0 +1,19 @@
+## uni-admin
+
+uni-admin,是基于 uni-app 和 uniCloud 的管理后台项目模版。
+
+对于uniCloud的开发者而言,其后台管理系统应该使用本框架。
+
+我们搭建了[uni-admin演示站点](http://hellouniadmin.dcloud.net.cn/admin/),你登录后即可快速体验uni-admin。
+
+uni-admin 是开源的,遵循 MIT 协议,你可以从[Github](https://github.com/dcloudio/uni-admin)或[码云](https://gitee.com/dcloud/uni-admin)获取源码,也可以从[DCloud插件市场](https://ext.dcloud.net.cn/plugin?id=3268)快捷下载。
+
+## 框架特征
+- 基于 uni-app 的宽屏适配,可自动适配 PC 宽屏和手机各端。了解[宽屏适配](https://uniapp.dcloud.io/adapt)
+- 基于 uniCloud,是 serverless 的云开发。了解[uniCloud](https://uniapp.dcloud.io/uniCloud/README)
+- 基于 uni-id,使用 uni-id 的用户账户、角色、权限系统。了解[uni-id](https://uniapp.dcloud.io/uniCloud/uni-id)
+
+## 看视频,15分钟掌握uni-admin
+<a target="_blank" href="https://www.bilibili.com/video/BV17p4y1a71x?p=13">
+    <img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/4332911b-6624-4587-8c77-78b68f1f8c78.jpg" alt="uni-admin视频教程" style="width: 60%;">
+</a>

+ 81 - 0
admin.config.js

@@ -0,0 +1,81 @@
+export default {
+	login: {
+		url: '/uni_modules/uni-id-pages/pages/login/login-withpwd' // 登录页面路径
+	},
+	index: {
+		url: '/pages/index/index' // 登录后跳转的第一个页面
+	},
+	error: {
+		url: '/pages/error/404' // 404 Not Found 错误页面路径
+	},
+	navBar: { // 顶部导航
+		logo: '/static/logo.png', // 左侧 Logo
+		langs: [{
+			text: '中文简体',
+			lang: 'zh-Hans'
+		}, {
+			text: '中文繁體',
+			lang: 'zh-Hant'
+		}, {
+			text: 'English',
+			lang: 'en'
+		}],
+		debug: {
+			enable: process.env.NODE_ENV !== 'production', //是否显示错误信息
+			engine: [{ // 搜索引擎配置(每条错误信息后,会自动生成搜索链接,点击后跳转至搜索引擎)
+				name: '百度',
+				url: 'https://www.baidu.com/baidu?wd=ERR_MSG'
+			}, {
+				name: '谷歌',
+				url: 'https://www.google.com/search?q=ERR_MSG'
+			}]
+		}
+	},
+	sideBar: { // 左侧菜单
+		// 配置静态菜单列表(放置在用户被授权的菜单列表下边)
+		staticMenu: [{
+			menu_id: "demo",
+			text: '静态功能演示',
+			icon: 'admin-icons-kaifashili',
+			url: "",
+			children: [{
+				menu_id: "icons",
+				text: '图标',
+				icon: 'admin-icons-icon',
+				value: '/pages/demo/icons/icons',
+			}, {
+				menu_id: "table",
+				text: '表格',
+				icon: 'admin-icons-table',
+				value: '/pages/demo/table/table',
+			}]
+		}, {
+			menu_id: "admim-doc-pulgin",
+			text: '文档与插件',
+			icon: 'admin-icons-eco',
+			url: "",
+			children: [{
+				menu_id: "admin-doc",
+				icon: 'admin-icons-doc',
+				text: 'uni-admin 框架文档',
+				value: 'https://uniapp.dcloud.net.cn/uniCloud/admin'
+			}, {
+				menu_id: "stat-doc",
+				icon: 'admin-icons-help',
+				text: 'uni 统计教程',
+				value: 'https://uniapp.dcloud.net.cn/uni-stat-v2.html'
+			}, {
+				menu_id: "admin-pulgin",
+				icon: 'admin-icons-pulgin',
+				text: 'uni-admin 插件',
+				value: 'https://ext.dcloud.net.cn/?cat1=7&cat2=74'
+			}]
+		}]
+	},
+	uniStat: {
+		// 上传 sourceMap 文件至腾讯云服务空间 ID。空值代表不启用 sourceMap 上报错误回溯源码功能
+		uploadSourceMapCloudSpaceId: '',
+		// 要上传到的腾讯云云存储访问地址
+		cloudSourceMapUrl: ''
+	}
+}

+ 160 - 0
changelog.md

@@ -0,0 +1,160 @@
+## 2.0.2(2022-09-20)
+- 升级 uni-id-pages 至 1.0.18
+- 优化 导航登录用户名的显示规则:用户昵称 > 用户名 > 手机号 > 邮箱
+## 2.0.1(2022-09-19)
+- 升级 uni-id-pages 至 1.0.17
+- 修改 导航登录用户名的显示规则:优先显示用户昵称,其次显示用户名
+- 增加 用户管理列表展示“用户昵称”字段
+- 增加 创建用户支持添加“用户昵称”字段
+## 2.0.0(2022-09-16)
+- 升级 uni-id-pages 至 1.0.13
+- 修复 应用中心修改应用无法修改的bug
+## 1.10.1(2022-09-08)
+- 修复 使用 uniIdRouter 时导致页面无法打开的Bug
+## 1.10.0(2022-09-08)
+- 升级 uni-id 至 4.0,移除 uni-id、uni-id-cf 插件,增加 uni-id-pages、uni-id-common 插件。[uni-id详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html)
+## 1.9.8(2022-08-15)
+- 修复 应用管理修改页面报错
+## 1.9.7(2022-08-08)
+- 改进 sourceMap 回溯源码功能使用方法,需要在 admin.config.js 中配置相关信息。[详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#upload-sourcemap)
+- 修复 js报错统计报错的Bug
+## 1.9.6(2022-08-02)
+- 修复 vue3 打包报错的Bug
+- 修复 升级中心发布 wgt 时原生 App 最低版本没有必填的Bug
+- 修复 升级中心发布 wgt 时显示Android应用市场的Bug
+## 1.9.5(2022-07-29)
+- 修复 运行到微信小程序控制台报错的Bug
+## 1.9.4(2022-07-28)
+- 新增 uni-admin uni统计支持上传 sourceMap,报错可准确回溯源码 [详情](https://uniapp.dcloud.io/uni-stat-v2.html#sourcemap-parse-error)
+## 1.9.3(2022-07-19)
+- 优化 uni-admin 应用管理模块可管理App下载地址、小程序二维码等更多应用信息 [详情](https://uniapp.dcloud.io/uniCloud/admin.html#app-manager)
+- 调整 uni-admin 内置 统一发布页(uni-portal)插件 [详情](https://uniapp.dcloud.io/uniCloud/admin.html#uni-portal)
+- 调整 uni-admin 内置 App升级中心(uni-upgrade-center)插件,并支持多应用商店更新 [详情](https://uniapp.dcloud.io/uniCloud/admin.html#uni-upgrade-center)
+- 升级前最好将旧版 uni-portal、uni-upgrade-center 插件备份并移出 uni_modules 目录
+## 1.9.2(2022-07-11)
+- 修复 留存统计跑批任务获取不到版本号的Bug
+## 1.9.1(2022-07-06)
+- 新增 opendb-device表,开通 uni-push2.0 与 uni统计2.0 自动上报 push_clientid 到 opendb-device表
+## 1.9.0(2022-07-05)
+- 【重要】uni-admin 优化 uni统计 版本记录复用uni升级中心的opendb-app-versions表,废弃uni-stat-app-versions表 [详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#upgrade)
+- 新增 uni统计 app崩溃页面,补充崩溃率统计
+- 修复 uni统计 js报错页面,错误率计算不准确的Bug
+- 修复 uni统计 切换版本或者修改时间等操作后,趋势图状态显示不正确的Bug
+- 修复 uni统计 部分页面首次进入时界面闪烁的问题
+## 1.8.5(2022-06-29)
+- 新增 支持 ios 安全区
+## 1.8.4(2022-06-01)
+- 新增 uni统计 可通过选择「应用版本」查询数据
+- 新增 uni统计 原生 app 崩溃页各项功能
+- 修复 uni统计 渠道页 table 表格最后一列空白的 bug
+- 修复 uni统计 场景分析页趋势图有数据却显示为 0 的 bug
+- 修复 系统设置权限只能加载 20 条的 bug
+## 1.8.3(2022-05-19)
+- 优化 「首页」逻辑调整,无 appid 时提示添加 app 记录,可跳转 app 管理的新增页
+- 优化 移除登录时多余的 init 的逻辑,提升登录速度
+- 优化 「页面统计」添加 「入口页」、「登录页」的提示文字
+- 修复 从「首页」跳转「概况」时,url 的 query 丢失的 bug
+## 1.8.2(2022-05-18)
+- 优化 uni 统计的「统计首页」菜单移动到应用「首页」,添加了设备概览、注册用户概览
+- 优化 uni 统计的「帮助」菜单移动到「文档与插件」
+- 修复 路由改变后面包屑未响应的 bug
+## 1.8.1(2022-05-17)
+- 修复 去掉多余的 schema
+## 1.8.0(2022-05-17)
+**重要更新:**
+- 新增 用户日志功能
+- 新增 内置 uni 统计报表体系,开源、免费、可私有化部署,[了解更多](https://uniapp.dcloud.net.cn/uni-stat-v2.html#uni%E7%BB%9F%E8%AE%A1),具体功能如下
+	- 统计首页
+	- 设备统计
+	- 用户统计
+	- 页面统计
+	- 渠道/场景值分析
+	- 自定义事件
+	- 错误统计
+## 1.7.13(2022-02-15)
+- 修复 新增菜单页‘内置图标’在 vue3 平台不显示的 bug
+- 修复 ‘新增一级菜单’ 按钮的文字错误
+## 1.7.12(2022-01-26)
+- 修复 uni-admin 的 'registerUser' 接口,注册用户含有多余字段 uid
+## 1.7.11(2022-01-19)
+- 修复 多个用户的用户名相同时,后注册的同名用户登录时提示“用户不存在”的 bug
+- 修复 偶发的验证码输出正确却提示“验证码错误”的 bug
+- 修复 刷新页面后验证码消的 bug
+## 1.7.10(2021-12-20)
+- 优化 支持 vue3 查找并注册的菜单(包括插件菜单)
+## 1.7.9(2021-12-07)
+- 新增 标签管理功能,可批量为用户添加或移除标签、通过标签过滤用户
+## 1.7.8(2021-11-30)
+- 修复 Android 平台切换语言闪退的 bug,该平台暂不支持切换语言
+## 1.7.7(2021-11-29)
+- 修复 uni-datetime-picker 国际化未默认英文的问题
+- 修复 uni-datetime-picker 范围选择在表格列头中渲染相同月份的问题
+## 1.7.6(2021-11-11)
+- 优化 修改密码功能不再支持查看明文密码
+- 修复 某些屏幕上,input 框中下划线 '_' 被隐藏的 bug
+## 1.7.5(2021-10-08)
+- 修复 用户管理与角色管理模糊搜索时关联的外键无法搜索的 bug
+## 1.7.4(2021-09-30)
+- 修复 topwindow 非 h5 端,key 使用表达式报错的 bug
+- 优化 topwindow 中英文混排不对齐的问题
+## 1.7.3(2021-09-27)
+- 修复 vue3 上加载 PostCSS 插件失败的 bug
+## 1.7.2(2021-09-17)
+- 优化 取消菜单管理请求数据条数限制
+- 优化 topwindow 菜单文字换行的问题
+- 修复 左侧菜单栏刷新失去打开状态的 bug
+## 1.7.1(2021-09-14)
+- 修复 vue3 下 i18n 未定义的 bug
+- 优化 抛出被 error.js 拦截的报错
+## 1.7.0(2021-08-31)
+- 新增 支持国际化 i18n
+- 优化 验证码图片边框样式调整
+## 1.6.2(2021-08-26)
+- 修复 非 admin 角色的用户无权限访问菜单表,动态菜单不显示的 bug
+	> 更新后,需上传 opendb-admin-menus.schema.json
+- 优化 list 页的表格样式
+## 1.6.1(2021-08-16)
+- 修复 uni-id-cf 中无用的node_modules造成的报错
+- 修复 uni.css 中样式穿透造成的 uni-file-picker 不可见的 bug
+## 1.6.0(2021-07-31)
+**重要更新:**
+- 新增 应用管理功能,管理用户可登录的应用(uni-id@3.3.1+ 支持)
+- 新增 升级系统管理 list 页的表格功能,支持数据排序、筛选、搜索等功能
+- 新增 同时适配 vue2 和 vue3(HBuilder X 3.2.0+ 支持 vue3)
+- 修复 刷新页面时,左侧菜单丢失高亮状态的 bug
+- 修复 修改密码失败的 bug
+## 1.5.8(2021-07-12)
+- 修复 侧边栏菜单查询数据条数一次不超过 20 条的 bug(限制是最大一次 500 条)
+## 1.5.7(2021-07-02)
+- 修复 菜单管理排序错误的 bug
+- 优化 框架设定非 admin 不能创建用户, 用户可自定义
+## 1.5.6(2021-06-28)
+- 修复 left-window 在小程序上的编译错误
+## 1.5.5(2021-06-21)
+- 修复 角色管理删除功能失效的 bug
+- 修复 权限管理删除功能失效的 bug
+## 1.5.4(2021-06-21)
+- 优化 云函数 uni-id-cf uni_module 化,更新更方便
+## 1.5.3(2021-06-17)
+- 优化 opendb-admin-menus.schema 读权限配置默认为 true
+	> 原因:侧边栏菜单管理功能使用了 clientDB, 默认全部读取,通过用户权限过滤
+## 1.4.6(2021-05-27)
+-  修复 未连接服务空间时登录页空白的 bug
+
+## 1.4.5(2021-05-18)
+- 新增 选择表格分页条数功能
+- 修复 切换分页条数当前分页不是1时获取数据出错的 bug
+## 1.4.4(2021-05-17)
+- 优化 导出 Excel 功能的代码
+- 优化 系统管理 list 页面样式
+- 优化 文案调整
+## 1.4.3(2021-05-14)
+- PC 端支持表格导出数据为 Excel
+## 1.4.2(2021-04-21)
+- 更新 uni-id 3.1.0
+  - 增加对用户名、邮箱、密码字段的两端去空格
+  - 默认忽略用户名、邮箱的大小写 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=case-sensitive)
+  - 修复 customToken导出async方法报错的Bug
+## 1.4.1(2021-04-16)
+- 更新 uni-tabel 1.0.3
+- 新增 根目录下 changelog.md

+ 199 - 0
common/admin-icons.css

@@ -0,0 +1,199 @@
+@font-face {
+	font-family: admin-icons;
+	src: url('~@/static/admin-icons.ttf') format('truetype');
+	font-weight: 400;
+	font-display: "auto";
+	font-style: normal
+}
+
+
+[class*="admin-icons-"],
+[class^=admin-icons-] {
+	font-family: admin-icons !important;
+	speak: none;
+	font-style: normal;
+	font-weight: 400;
+	font-variant: normal;
+	text-transform: none;
+	line-height: 1;
+	vertical-align: baseline;
+	display: inline-block;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale
+}
+
+
+
+.admin-icons-stat:before {
+	content: "\e64a";
+}
+
+.admin-icons-fl-xitong:before {
+  content: "\e623";
+}
+
+.admin-icons-tongji:before {
+  content: "\e64a";
+}
+
+.admin-icons-yonghutongji:before {
+  content: "\e661";
+}
+
+.admin-icons-dashboard:before {
+  content: "\e78b";
+}
+
+.admin-icons-qudaofenxi:before {
+  content: "\e6c6";
+}
+
+.admin-icons-shebeitongji:before {
+  content: "\e6fd";
+}
+
+.admin-icons-xitongguanli:before {
+  content: "\e671";
+}
+
+.admin-icons-kaifashili:before {
+  content: "\e614";
+}
+
+.admin-icons-yonghutongji1:before {
+  content: "\e769";
+}
+
+.admin-icons-shijianfenxi:before {
+  content: "\e604";
+}
+
+.admin-icons-ziyuan:before {
+  content: "\e619";
+}
+
+.admin-icons-cuowutongji:before {
+  content: "\e51f";
+}
+
+.admin-icons-shijianfenxi1:before {
+  content: "\e629";
+}
+
+
+.admin-icons-tongjishouye:before {
+  content: "\e679";
+}
+
+.admin-icons-yemiantongji:before {
+  content: "\e684";
+}
+
+
+.admin-icons-manager-user:before {
+  content: "\e610";
+}
+
+.admin-icons-manager-role:before {
+  content: "\e61a";
+}
+
+.admin-icons-manager-permission:before {
+  content: "\e637";
+}
+
+.admin-icons-manager-app:before {
+  content: "\e65b";
+}
+
+.admin-icons-manager-tag:before {
+  content: "\e83c";
+}
+
+.admin-icons-manager-menu:before {
+  content: "\e629";
+}
+
+.admin-icons-overview:before {
+  content: "\e609";
+}
+
+.admin-icons-activity:before {
+  content: "\e70e";
+}
+
+.admin-icons-trend:before {
+  content: "\e63c";
+}
+
+.admin-icons-retention:before {
+  content: "\e697";
+}
+
+.admin-icons-comparison:before {
+  content: "\e955";
+}
+
+.admin-icons-stickiness:before {
+  content: "\e770";
+}
+
+.admin-icons-page-ent:before {
+  content: "\e767";
+}
+
+.admin-icons-page-res:before {
+  content: "\e69b";
+}
+
+.admin-icons-scene:before {
+  content: "\e601";
+}
+
+.admin-icons-channel:before {
+  content: "\e603";
+}
+
+.admin-icons-error-js:before {
+  content: "\ec0c";
+}
+
+.admin-icons-error-app:before {
+  content: "\e617";
+}
+
+.admin-icons-help:before {
+  content: "\e65c";
+}
+
+.admin-icons-icon:before {
+  content: "\e503";
+}
+
+.admin-icons-table:before {
+  content: "\e639";
+}
+
+.admin-icons-eco:before {
+  content: "\e698";
+}
+
+.admin-icons-doc:before {
+  content: "\e656";
+}
+
+.admin-icons-pulgin:before {
+  content: "\e648";
+}
+
+.admin-icons-lang:before {
+  content: "\e618";
+}
+
+.admin-icons-user:before {
+  content: "\e68d";
+}
+
+.admin-icons-safety:before {
+  content: "\e769";
+}

+ 50 - 0
common/theme.scss

@@ -0,0 +1,50 @@
+@import '@/uni.scss';
+
+button[type="primary"] {
+	background-color: $uni-color-primary;
+	border-color: $uni-color-primary;
+	border-width: 0;
+}
+
+uni-button[type="primary"][plain] {
+	color: $uni-color-primary;
+	border-color: $uni-color-primary;
+}
+
+uni-button[disabled][type=primary] {
+	background-color: rgba($uni-color-primary, 0.6);
+}
+
+.button-hover[type=primary] {
+	background-color: darken($color: $uni-color-primary, $amount: 10%);
+}
+
+.uni-selector-select .uni-picker-item.selected{
+	color: $uni-color-primary;
+}
+
+.uni-tabs__item.is-active{
+	color: $uni-color-primary;
+}
+
+.uni-modal__btn_primary{
+	color: $uni-color-primary !important;
+}
+
+.uni-link {
+	color: $uni-color-primary;
+}
+
+.uni-switch-input-checked{
+	background-color: $uni-color-primary !important;
+	border-color: $uni-color-primary !important;
+}
+
+.uni-radio-input-checked{
+	background-color: $uni-color-primary !important;
+	border-color: $uni-color-primary !important;
+}
+
+.link-btn{
+	color: $uni-color-primary !important;
+}

+ 542 - 0
common/uni-icons.css

@@ -0,0 +1,542 @@
+@font-face {
+	font-family: uni-icons;
+	src: url('~@/uni_modules/uni-icons/components/uni-icons/uni.ttf') format('truetype');
+	font-weight: 400;
+	font-display: "auto";
+	font-style: normal
+}
+
+[class*=" uni-icons-"],
+[class^=uni-icons-] {
+	font-family: uni-icons !important;
+	speak: none;
+	font-style: normal;
+	font-weight: 400;
+	font-variant: normal;
+	text-transform: none;
+	line-height: 1;
+	vertical-align: baseline;
+	display: inline-block;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale
+}
+
+.uni-icons-shop:before {
+	content: "\e609";
+}
+
+.uni-icons-headphones:before {
+	content: "\e8bf";
+}
+
+.uni-icons-pulldown:before {
+	content: "\e588";
+}
+
+.uni-icons-scan:before {
+	content: "\e612";
+}
+
+.uni-icons-back:before {
+	content: "\e471";
+}
+
+.uni-icons-forward:before {
+	content: "\e470";
+}
+
+.uni-icons-refreshempty:before {
+	content: "\e461";
+}
+
+.uni-icons-checkbox-filled:before {
+	content: "\e442";
+}
+
+.uni-icons-checkbox:before {
+	content: "\e7fa";
+}
+
+.uni-icons-loop:before {
+	content: "\e565";
+}
+
+.uni-icons-arrowthindown:before {
+	content: "\e585";
+}
+
+.uni-icons-arrowthinleft:before {
+	content: "\e586";
+}
+
+.uni-icons-arrowthinright:before {
+	content: "\e587";
+}
+
+.uni-icons-arrowthinup:before {
+	content: "\e584";
+}
+
+.uni-icons-bars:before {
+	content: "\e563";
+}
+
+.uni-icons-cart-filled:before {
+	content: "\e7f4";
+}
+
+.uni-icons-cart:before {
+	content: "\e7f5";
+}
+
+.uni-icons-arrowleft:before {
+	content: "\e582";
+}
+
+.uni-icons-arrowdown:before {
+	content: "\e581";
+}
+
+.uni-icons-arrowright:before {
+	content: "\e583";
+}
+
+.uni-icons-arrowup:before {
+	content: "\e580";
+}
+
+.uni-icons-eye-filled:before {
+	content: "\e568";
+}
+
+.uni-icons-eye-slash-filled:before {
+	content: "\e822";
+}
+
+.uni-icons-eye-slash:before {
+	content: "\e823";
+}
+
+.uni-icons-eye:before {
+	content: "\e824";
+}
+
+.uni-icons-reload:before {
+	content: "\e462";
+}
+
+.uni-icons-hand-thumbsdown-filled:before {
+	content: "\e83b";
+}
+
+.uni-icons-hand-thumbsdown:before {
+	content: "\e83c";
+}
+
+.uni-icons-hand-thumbsup-filled:before {
+	content: "\e83d";
+}
+
+.uni-icons-heart-filled:before {
+	content: "\e83e";
+}
+
+.uni-icons-hand-thumbsup:before {
+	content: "\e83f";
+}
+
+.uni-icons-heart:before {
+	content: "\e840";
+}
+
+.uni-icons-mail-open-filled:before {
+	content: "\e84d";
+}
+
+.uni-icons-mail-open:before {
+	content: "\e84e";
+}
+
+.uni-icons-list:before {
+	content: "\e562";
+}
+
+.uni-icons-map-pin:before {
+	content: "\e85e";
+}
+
+.uni-icons-map-pin-ellipse:before {
+	content: "\e864";
+}
+
+.uni-icons-paperclip:before {
+	content: "\e567";
+}
+
+.uni-icons-images-filled:before {
+	content: "\e87a";
+}
+
+.uni-icons-images:before {
+	content: "\e87b";
+}
+
+.uni-icons-search:before {
+	content: "\e466";
+}
+
+.uni-icons-settings:before {
+	content: "\e560";
+}
+
+.uni-icons-cloud-download:before {
+	content: "\e8e4";
+}
+
+.uni-icons-cloud-upload-filled:before {
+	content: "\e8e5";
+}
+
+.uni-icons-cloud-upload:before {
+	content: "\e8e6";
+}
+
+.uni-icons-cloud-download-filled:before {
+	content: "\e8e9";
+}
+
+.uni-icons-more:before {
+	content: "\e507";
+}
+
+.uni-icons-more-filled:before {
+	content: "\e537";
+}
+
+.uni-icons-refresh:before {
+	content: "\e407";
+}
+
+.uni-icons-refresh-filled:before {
+	content: "\e437";
+}
+
+.uni-icons-undo-filled:before {
+	content: "\e7d6";
+}
+
+.uni-icons-undo:before {
+	content: "\e406";
+}
+
+.uni-icons-redo:before {
+	content: "\e405";
+}
+
+.uni-icons-redo-filled:before {
+	content: "\e7d9";
+}
+
+.uni-icons-camera:before {
+	content: "\e301";
+}
+
+.uni-icons-camera-filled:before {
+	content: "\e7ef";
+}
+
+.uni-icons-smallcircle-filled:before {
+	content: "\e801";
+}
+
+.uni-icons-circle:before {
+	content: "\e411";
+}
+
+.uni-icons-flag-filled:before {
+	content: "\e825";
+}
+
+.uni-icons-flag:before {
+	content: "\e508";
+}
+
+.uni-icons-gear-filled:before {
+	content: "\e532";
+}
+
+.uni-icons-gear:before {
+	content: "\e502";
+}
+
+.uni-icons-home:before {
+	content: "\e500";
+}
+
+.uni-icons-info:before {
+	content: "\e504";
+}
+
+.uni-icons-home-filled:before {
+	content: "\e530";
+}
+
+.uni-icons-info-filled:before {
+	content: "\e534";
+}
+
+.uni-icons-circle-filled:before {
+	content: "\e441";
+}
+
+.uni-icons-chat-filled:before {
+	content: "\e847";
+}
+
+.uni-icons-chat:before {
+	content: "\e263";
+}
+
+.uni-icons-checkmarkempty:before {
+	content: "\e472";
+}
+
+.uni-icons-locked-filled:before {
+	content: "\e856";
+}
+
+.uni-icons-locked:before {
+	content: "\e506";
+}
+
+.uni-icons-map-filled:before {
+	content: "\e85c";
+}
+
+.uni-icons-map:before {
+	content: "\e364";
+}
+
+.uni-icons-minus-filled:before {
+	content: "\e440";
+}
+
+.uni-icons-mic-filled:before {
+	content: "\e332";
+}
+
+.uni-icons-minus:before {
+	content: "\e410";
+}
+
+.uni-icons-micoff:before {
+	content: "\e360";
+}
+
+.uni-icons-mic:before {
+	content: "\e302";
+}
+
+.uni-icons-clear:before {
+	content: "\e434";
+}
+
+.uni-icons-smallcircle:before {
+	content: "\e868";
+}
+
+.uni-icons-close:before {
+	content: "\e404";
+}
+
+.uni-icons-closeempty:before {
+	content: "\e460";
+}
+
+.uni-icons-paperplane:before {
+	content: "\e503";
+}
+
+.uni-icons-paperplane-filled:before {
+	content: "\e86e";
+}
+
+.uni-icons-image:before {
+	content: "\e363";
+}
+
+.uni-icons-image-filled:before {
+	content: "\e877";
+}
+
+.uni-icons-location-filled:before {
+	content: "\e333";
+}
+
+.uni-icons-location:before {
+	content: "\e303";
+}
+
+.uni-icons-plus-filled:before {
+	content: "\e439";
+}
+
+.uni-icons-plus:before {
+	content: "\e409";
+}
+
+.uni-icons-plusempty:before {
+	content: "\e468";
+}
+
+.uni-icons-help-filled:before {
+	content: "\e535";
+}
+
+.uni-icons-help:before {
+	content: "\e505";
+}
+
+.uni-icons-navigate-filled:before {
+	content: "\e884";
+}
+
+.uni-icons-navigate:before {
+	content: "\e501";
+}
+
+.uni-icons-mic-slash-filled:before {
+	content: "\e892";
+}
+
+.uni-icons-sound:before {
+	content: "\e590";
+}
+
+.uni-icons-sound-filled:before {
+	content: "\e8a1";
+}
+
+.uni-icons-spinner-cycle:before {
+	content: "\e465";
+}
+
+.uni-icons-download-filled:before {
+	content: "\e8a4";
+}
+
+.uni-icons-videocam-filled:before {
+	content: "\e8af";
+}
+
+.uni-icons-upload:before {
+	content: "\e402";
+}
+
+.uni-icons-upload-filled:before {
+	content: "\e8b1";
+}
+
+.uni-icons-starhalf:before {
+	content: "\e463";
+}
+
+.uni-icons-star-filled:before {
+	content: "\e438";
+}
+
+.uni-icons-star:before {
+	content: "\e408";
+}
+
+.uni-icons-trash:before {
+	content: "\e401";
+}
+
+.uni-icons-compose:before {
+	content: "\e400";
+}
+
+.uni-icons-videocam:before {
+	content: "\e300";
+}
+
+.uni-icons-trash-filled:before {
+	content: "\e8dc";
+}
+
+.uni-icons-download:before {
+	content: "\e403";
+}
+
+.uni-icons-qq:before {
+	content: "\e264";
+}
+
+.uni-icons-weibo:before {
+	content: "\e260";
+}
+
+.uni-icons-weixin:before {
+	content: "\e261";
+}
+
+.uni-icons-pengyouquan:before {
+	content: "\e262";
+}
+
+.uni-icons-chatboxes:before {
+	content: "\e203";
+}
+
+.uni-icons-chatboxes-filled:before {
+	content: "\e233";
+}
+
+.uni-icons-email-filled:before {
+	content: "\e231";
+}
+
+.uni-icons-email:before {
+	content: "\e201";
+}
+
+.uni-icons-person-filled:before {
+	content: "\e131";
+}
+
+.uni-icons-contact-filled:before {
+	content: "\e130";
+}
+
+.uni-icons-person:before {
+	content: "\e101";
+}
+
+.uni-icons-contact:before {
+	content: "\e100";
+}
+
+.uni-icons-phone:before {
+	content: "\e200";
+}
+
+.uni-icons-personadd-filled:before {
+	content: "\e132";
+}
+
+.uni-icons-personadd:before {
+	content: "\e102";
+}
+
+.uni-icons-phone-filled:before {
+	content: "\e230";
+}
+
+.uni-icons-chatbubble-filled:before {
+	content: "\e232";
+}
+
+.uni-icons-chatbubble:before {
+	content: "\e202";
+}

+ 609 - 0
common/uni.css

@@ -0,0 +1,609 @@
+/* 全局公共样式 */
+
+body,
+html {
+	-webkit-user-select: auto;
+	user-select: auto;
+	font-size: 16px;
+}
+
+/* #ifdef H5 */
+
+.uni-app--showleftwindow uni-main {
+	position: relative;
+	background-color: #f5f5f5;
+}
+
+.uni-mask + .uni-left-window,
+.uni-mask + .uni-right-window {
+	position: fixed;
+}
+
+.uni-app--showleftwindow uni-page-head .uni-page-head {
+	color: #333 !important;
+	/* margin-right: 15px; */
+}
+
+uni-page-head .uni-btn-icon {
+	color: #333 !important;
+}
+
+.uni-app--showleftwindow
+	uni-page-head[uni-page-head-type="default"]
+	~ uni-page-wrapper {
+	height: auto;
+	padding-top: 44px;
+}
+
+.uni-app--showleftwindow uni-page-head ~ uni-page-wrapper uni-page-body {
+	/* padding-top: 44px; */
+}
+
+.uni-app--showleftwindow uni-page-wrapper {
+	position: absolute;
+	width: 100%;
+	top: 0;
+	bottom: 0;
+	padding: 15px;
+	overflow-y: auto;
+	box-sizing: border-box;
+	background-color: #f5f5f5;
+}
+
+.uni-app--showleftwindow uni-page-body {
+	width: 100%;
+	min-height: 100%;
+	box-sizing: border-box;
+	border-radius: 5px;
+	box-shadow: -1px -1px 5px 0 rgba(0, 0, 0, 0.1);
+	background-color: #fff;
+}
+
+.uni-app--showleftwindow .uni-container .uni-forms {
+	padding: 15px;
+	max-width: 650px;
+}
+
+/* #endif */
+
+/* #ifndef H5 */
+.uni-nav-menu {
+	height: 100vh;
+}
+
+/* #endif */
+
+.pointer {
+	cursor: pointer;
+}
+
+.uni-top-window {
+	z-index: 999;
+	overflow: visible;
+}
+
+.uni-tips {
+	font-size: 12px;
+	color: #666;
+}
+
+/* 容器 */
+.uni-container {
+	padding: 15px;
+	box-sizing: border-box;
+}
+
+/* 标题栏 */
+.uni-header {
+	padding: 0 15px;
+	display: flex;
+	min-height: 55px;
+	align-items: center;
+	justify-content: space-between;
+	border-bottom: 1px #f5f5f5 solid;
+	flex-wrap: wrap;
+}
+
+.uni-title {
+	margin-right: 10px;
+	font-size: 16px;
+	font-weight: 500;
+	color: #333;
+}
+
+.uni-sub-title {
+	margin-top: 3px;
+	font-size: 14px;
+	color: #999;
+}
+
+.uni-link {
+	color: #3a8ee6;
+	cursor: pointer;
+	text-decoration: underline;
+}
+
+.uni-group {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-wrap: wrap;
+	word-break: keep-all;
+}
+
+/* 按钮样式 */
+.uni-button-group {
+	margin-top: 30px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.uni-button {
+	padding: 10px 20px;
+	font-size: 14px;
+	border-radius: 4px;
+	line-height: 1;
+	margin: 0;
+	box-sizing: border-box;
+	overflow: initial;
+}
+
+.uni-group .uni-button {
+	margin: 10px;
+}
+
+.uni-group .uni-search {
+	margin: 10px;
+}
+
+.uni-group > .uni-button:first-child {
+	margin-left: 0;
+}
+
+.uni-button:hover,
+.uni-button:focus {
+	opacity: 0.9;
+}
+
+.uni-button:active {
+	opacity: 1;
+}
+
+.uni-button-full {
+	width: 100%;
+}
+
+/* 搜索框样式 */
+.uni-search {
+	width: 268px;
+	height: 28px;
+	line-height: 28px;
+	font-size: 12px;
+	color: #606266;
+	padding: 0 10px;
+	border: 1px #dcdfe6 solid;
+	/* margin-right: 10px; */
+	border-radius: 3px;
+}
+
+/* 分页容器 */
+.uni-pagination-box {
+	margin-top: 20px;
+}
+
+.uni-input-border,
+.uni-textarea-border {
+	width: 100%;
+	font-size: 14px;
+	color: #666;
+	border: 1px #e5e5e5 solid;
+	border-radius: 5px;
+	box-sizing: border-box;
+}
+
+.uni-input-border {
+	padding: 0 10px;
+	height: 35px;
+}
+
+.uni-textarea-border {
+	padding: 10px;
+	height: 80px;
+}
+
+.uni-disabled {
+	background-color: #f5f7fa;
+	color: #c0c4cc;
+}
+
+.uni-icon-password-eye {
+	position: absolute;
+	right: 8px;
+	top: 6px;
+	font-family: uniicons;
+	font-size: 20px;
+	font-weight: normal;
+	font-style: normal;
+	width: 24px;
+	height: 24px;
+	line-height: 24px;
+	color: #999999;
+}
+
+.uni-eye-active {
+	color: #007aff;
+}
+
+.uni-tabs__header {
+	position: relative;
+	background-color: #f5f7fa;
+	border-bottom: 1px solid #e4e7ed;
+}
+
+.uni-tabs__nav-wrap {
+	overflow: hidden;
+	margin-bottom: -1px;
+	position: relative;
+}
+
+.uni-tabs__nav-scroll {
+	overflow: hidden;
+}
+
+.uni-tabs__nav {
+	position: relative;
+	white-space: nowrap;
+}
+
+.uni-tabs__item {
+	position: relative;
+	padding: 0 20px;
+	height: 40px;
+	box-sizing: border-box;
+	line-height: 40px;
+	display: inline-block;
+	list-style: none;
+	font-size: 14px;
+	font-weight: 500;
+	color: #909399;
+	margin-top: -1px;
+	margin-left: -1px;
+	border: 1px solid transparent;
+	cursor: pointer;
+}
+
+.uni-tabs__item.is-active {
+	color: $uni-color-primary;
+	background-color: #fff;
+	border-right-color: #dcdfe6;
+	border-left-color: #dcdfe6;
+}
+
+.uni-form-item-tips {
+	color: #999;
+	font-size: 12px;
+	margin-top: 10px;
+	/* position: absolute; */
+	/* top: 40px; */
+}
+
+.uni-form-item-empty {
+	color: #999;
+	min-height: 36px;
+	line-height: 36px;
+}
+
+::v-deep .uni-forms-item__label .label-text {
+	color: #606266 !important;
+}
+
+::v-deep .flex-center-x .uni-forms-item__content {
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+}
+
+.link-btn {
+	line-height: 26px;
+	margin-top: 5px;
+	color: #007aff !important;
+	text-decoration: underline;
+	cursor: pointer;
+}
+
+/* button 重置样式 */
+::v-deep button[size="mini"] {
+	line-height: 2.4;
+	font-size: 12px;
+	border-radius: 3px;
+}
+
+button {
+	background: #fff;
+	border: 1px solid #dcdfe6;
+	color: #606266;
+	box-sizing: border-box;
+}
+
+button[type="primary"] {
+	background-color: #409eff;
+	border-color: #409eff;
+	border-width: 0;
+}
+
+button[type="warn"] {
+	background-color: #f56c6c;
+	border-color: #f56c6c;
+	border-width: 0;
+}
+
+button[type="default"] {
+	background: #fff;
+	border: 1px solid #dcdfe6;
+	color: #606266;
+	box-sizing: border-box;
+}
+
+button[type="primary"][plain] {
+	border-color: #409eff;
+	color: #409eff;
+}
+
+button[type="warn"][plain] {
+	border-color: #f56c6c;
+	color: #f56c6c;
+}
+
+button[type="default"][plain] {
+	border-color: #dcdfe6;
+	color: #606266;
+}
+
+button[plain] {
+	border-color: #dcdfe6;
+	color: #606266;
+}
+
+button:after {
+	border-width: 0;
+}
+
+.uni-input-placeholder {
+	color: #999;
+}
+
+.uni-pagination-box {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.select-picker {
+	margin-right: 20px;
+}
+
+.select-picker button {
+	margin-top: 5px;
+	line-height: 29px;
+	font-size: 14px;
+}
+
+.select-picker button text {
+	color: #999;
+}
+
+.select-picker-icon {
+	margin-left: 8px;
+}
+
+/* stat style start */
+.m-m {
+	margin: 15px !important;
+}
+
+.mb-s {
+	margin-bottom: 5px;
+}
+
+.mb-m {
+	margin-bottom: 15px !important;
+}
+
+.mb-l {
+	margin-bottom: 30px !important;
+}
+
+.ml-s {
+	margin-left: 5px;
+}
+
+.ml-m {
+	margin-left: 15px !important;
+}
+
+.ml-l {
+	margin-left: 30px !important;
+}
+
+.p-m {
+	padding: 15px;
+}
+
+.uni-charts-box {
+	width: 100%;
+	height: 350px;
+}
+
+.uni-stat--x {
+	border-radius: 4px;
+	box-shadow: -1px -1px 5px 0 rgba(0, 0, 0, 0.1);
+	margin-bottom: 15px;
+}
+
+.uni-stat__actived {
+	/* outline: 1px solid #2979ff; */
+}
+
+.flex {
+	display: flex;
+	flex-wrap: wrap;
+	align-items: center;
+}
+
+.label-text {
+	font-size: 14px;
+	font-weight: bold;
+	color: #555;
+	margin: auto 0;
+	margin-right: 5px;
+}
+
+.uni-stat-edit--x {
+	display: flex;
+	justify-content: space-between;
+}
+
+.uni-stat-edit--btn {
+	cursor: pointer;
+}
+
+.uni-stat-datetime-picker {
+	margin: 15px;
+}
+
+/* uni-popup modal start */
+.modal {
+	/* width: 100%; */
+	max-width: calc(100vw - 200px);
+	min-width: 600px;
+	margin: 0 auto;
+	background-color: #ffffff;
+}
+
+.modal-header {
+	padding: 20px 0;
+	text-align: center;
+	border-bottom: 1px solid #eee;
+}
+
+.modal-footer {
+	padding: 20px;
+	display: flex;
+	justify-content: flex-end;
+	align-items: center;
+	/* border-top: 1px solid #eee; */
+}
+
+.modal-content {
+	padding: 15px;
+	height: 600px;
+	box-sizing: border-box;
+}
+
+/* uni-popup modal end */
+
+.uni-stat-tooltip-s {
+	width: 160px;
+	white-space: normal;
+}
+
+/* #ifndef APP-NVUE */
+@media screen and (max-width: 500px) {
+	.hide-on-phone {
+		display: none !important;
+	}
+
+	.uni-charts-box {
+		width: 100%;
+		height: 220px;
+	}
+
+	.uni-group .uni-search {
+		height: 32px;
+		line-height: 32px;
+		width: 100%;
+		margin: 20px 20px 10px 20px;
+	}
+
+	.uni-header {
+		padding-left: 0px;
+		padding-right: 0px;
+		border: unset;
+	}
+
+	.uni-group {
+		width: 100%;
+	}
+
+	.uni-stat-breadcrumb-on-phone {
+		padding: 0 20px !important;
+		border-bottom: 1px #f5f5f5 solid;
+	}
+
+	.flex {
+		width: 100%;
+		display: flex;
+		flex-wrap: wrap;
+		align-items: center;
+	}
+}
+
+@media screen and (min-width: 500px) {
+	.dispaly-grid {
+		display: grid;
+		/* grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); */
+		grid-template-columns: 1fr 1fr;
+		/* grid-template-rows: 1fr 1fr; */
+		column-gap: 15px;
+	}
+
+	.pc-flex-wrap {
+		display: flex;
+		flex-wrap: wrap;
+		align-items: center;
+	}
+
+	.uni-stat-datetime-picker {
+		max-width: 350px;
+	}
+
+	::v-deep .uni-pagination-picker-show .uni-picker-container .uni-picker-custom {
+		width: 100px;
+		margin: 0 86px;
+	}
+
+	::v-deep .uni-pagination-picker-show .uni-picker-container .uni-picker-custom .uni-picker-select + div {
+		left: 50% !important;
+	}
+}
+
+/* #endif */
+
+/* #ifdef H5 */
+/* fix 弹出层被遮盖 */
+::v-deep .uni-table-scroll {
+	min-height: calc(100vh - 237px);
+	box-sizing: border-box;
+}
+::v-deep .uni-table .tr-table--border {
+	border-left: 1px #ebeef5 solid;
+}
+/* #endif */
+
+/* #ifdef H5 */
+/* fix 弹出层被遮盖 */
+::v-deep .uni-table-scroll {
+	min-height: calc(100vh - 237px);
+	box-sizing: border-box;
+}
+::v-deep .uni-table .tr-table--border {
+	border-left: 1px #ebeef5 solid;
+}
+/* #endif */
+
+/* #ifndef H5 */
+.fix-top-window {
+	margin-top: 85px;
+}
+/* #endif */

+ 361 - 0
components/download-excel/download-excel.vue

@@ -0,0 +1,361 @@
+<template>
+  <div :id="idName" @click="generate">
+    <slot> Download {{ name }} </slot>
+  </div>
+</template>
+
+<script>
+import download from "./download";
+
+export default {
+  name: "downloadExcel",
+  props: {
+    // mime type [xls, csv]
+    type: {
+      type: String,
+      default: "xls",
+    },
+    // Json to download
+    data: {
+      type: Array,
+      required: false,
+      default: null,
+    },
+    // fields inside the Json Object that you want to export
+    // if no given, all the properties in the Json are exported
+    fields: {
+      type: Object,
+      default: () => null,
+    },
+    // this prop is used to fix the problem with other components that use the
+    // variable fields, like vee-validate. exportFields works exactly like fields
+    exportFields: {
+      type: Object,
+      default: () => null,
+    },
+    // Use as fallback when the row has no field values
+    defaultValue: {
+      type: String,
+      required: false,
+      default: "",
+    },
+    // Title(s) for the data, could be a string or an array of strings (multiple titles)
+    header: {
+      default: null,
+    },
+    // Footer(s) for the data, could be a string or an array of strings (multiple footers)
+    footer: {
+      default: null,
+    },
+    // filename to export
+    name: {
+      type: String,
+      default: "data.xls",
+    },
+    fetch: {
+      type: Function,
+    },
+    meta: {
+      type: Array,
+      default: () => [],
+    },
+    worksheet: {
+      type: String,
+      default: "Sheet1",
+    },
+    //event before generate was called
+    beforeGenerate: {
+      type: Function,
+    },
+    //event before download pops up
+    beforeFinish: {
+      type: Function,
+    },
+    // Determine if CSV Data should be escaped
+    escapeCsv: {
+      type: Boolean,
+      default: true,
+    },
+    // long number stringify
+    stringifyLongNum: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  computed: {
+    // unique identifier
+    idName() {
+      var now = new Date().getTime();
+      return "export_" + now;
+    },
+
+    downloadFields() {
+      if (this.fields) return this.fields;
+
+      if (this.exportFields) return this.exportFields;
+    },
+  },
+  methods: {
+    async generate() {
+      if (typeof this.beforeGenerate === "function") {
+        await this.beforeGenerate();
+      }
+      let data = this.data;
+      if (typeof this.fetch === "function" || !data) data = await this.fetch();
+
+      if (!data || !data.length) {
+        return;
+      }
+
+      let json = this.getProcessedJson(data, this.downloadFields);
+      if (this.type === "html") {
+        // this is mainly for testing
+        return this.export(
+          this.jsonToXLS(json),
+          this.name.replace(".xls", ".html"),
+          "text/html"
+        );
+      } else if (this.type === "csv") {
+        return this.export(
+          this.jsonToCSV(json),
+          this.name.replace(".xls", ".csv"),
+          "application/csv"
+        );
+      }
+      return this.export(
+        this.jsonToXLS(json),
+        this.name,
+        "application/vnd.ms-excel"
+      );
+    },
+    /*
+		Use downloadjs to generate the download link
+		*/
+    export: async function (data, filename, mime) {
+      let blob = this.base64ToBlob(data, mime);
+      if (typeof this.beforeFinish === "function") await this.beforeFinish();
+      download(blob, filename, mime);
+    },
+    /*
+		jsonToXLS
+		---------------
+		Transform json data into an xml document with MS Excel format, sadly
+		it shows a prompt when it opens, that is a default behavior for
+		Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
+		*/
+    jsonToXLS(data) {
+      let xlsTemp =
+        '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>';
+      let xlsData = "<thead>";
+      const colspan = Object.keys(data[0]).length;
+      let _self = this;
+
+      //Header
+      const header = this.header || this.$attrs.title;
+      if (header) {
+        xlsData += this.parseExtraData(
+          header,
+          '<tr><th colspan="' + colspan + '">${data}</th></tr>'
+        );
+      }
+
+      //Fields
+      xlsData += "<tr>";
+      for (let key in data[0]) {
+        xlsData += "<th>" + key + "</th>";
+      }
+      xlsData += "</tr>";
+      xlsData += "</thead>";
+
+      //Data
+      xlsData += "<tbody>";
+      data.map(function (item, index) {
+        xlsData += "<tr>";
+        for (let key in item) {
+          xlsData +=
+            "<td>" +
+            _self.preprocessLongNum(
+              _self.valueReformattedForMultilines(item[key])
+            ) +
+            "</td>";
+        }
+        xlsData += "</tr>";
+      });
+      xlsData += "</tbody>";
+
+      //Footer
+      if (this.footer != null) {
+        xlsData += "<tfoot>";
+        xlsData += this.parseExtraData(
+          this.footer,
+          '<tr><td colspan="' + colspan + '">${data}</td></tr>'
+        );
+        xlsData += "</tfoot>";
+      }
+
+      return xlsTemp
+        .replace("${table}", xlsData)
+        .replace("${worksheet}", this.worksheet);
+    },
+    /*
+		jsonToCSV
+		---------------
+		Transform json data into an CSV file.
+		*/
+    jsonToCSV(data) {
+      let _self = this;
+      var csvData = [];
+
+      //Header
+      const header = this.header || this.$attrs.title;
+      if (header) {
+        csvData.push(this.parseExtraData(header, "${data}\r\n"));
+      }
+
+      //Fields
+      for (let key in data[0]) {
+        csvData.push(key);
+        csvData.push(",");
+      }
+      csvData.pop();
+      csvData.push("\r\n");
+      //Data
+      data.map(function (item) {
+        for (let key in item) {
+          let escapedCSV = item[key] + "";
+          // Escaped CSV data to string to avoid problems with numbers or other types of values
+          // this is controlled by the prop escapeCsv
+          if (_self.escapeCsv) {
+            escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string
+            if (escapedCSV.match(/[,"\n]/)) {
+              escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"';
+            }
+          }
+          csvData.push(escapedCSV);
+          csvData.push(",");
+        }
+        csvData.pop();
+        csvData.push("\r\n");
+      });
+      //Footer
+      if (this.footer != null) {
+        csvData.push(this.parseExtraData(this.footer, "${data}\r\n"));
+      }
+      return csvData.join("");
+    },
+    /*
+		getProcessedJson
+		---------------
+		Get only the data to export, if no fields are set return all the data
+		*/
+    getProcessedJson(data, header) {
+      let keys = this.getKeys(data, header);
+      let newData = [];
+      let _self = this;
+      data.map(function (item, index) {
+        let newItem = {};
+        for (let label in keys) {
+          let property = keys[label];
+          newItem[label] = _self.getValue(property, item);
+        }
+        newData.push(newItem);
+      });
+
+      return newData;
+    },
+    getKeys(data, header) {
+      if (header) {
+        return header;
+      }
+
+      let keys = {};
+      for (let key in data[0]) {
+        keys[key] = key;
+      }
+      return keys;
+    },
+    /*
+		parseExtraData
+		---------------
+		Parse title and footer attribute to the csv format
+		*/
+    parseExtraData(extraData, format) {
+      let parseData = "";
+      if (Array.isArray(extraData)) {
+        for (var i = 0; i < extraData.length; i++) {
+          if (extraData[i])
+            parseData += format.replace("${data}", extraData[i]);
+        }
+      } else {
+        parseData += format.replace("${data}", extraData);
+      }
+      return parseData;
+    },
+
+    getValue(key, item) {
+      const field = typeof key !== "object" ? key : key.field;
+      let indexes = typeof field !== "string" ? [] : field.split(".");
+      let value = this.defaultValue;
+
+      if (!field) value = item;
+      else if (indexes.length > 1)
+        value = this.getValueFromNestedItem(item, indexes);
+      else value = this.parseValue(item[field]);
+
+      if (key.hasOwnProperty("callback"))
+        value = this.getValueFromCallback(value, key.callback);
+
+      return value;
+    },
+
+    /*
+    convert values with newline \n characters into <br/>
+    */
+    valueReformattedForMultilines(value) {
+      if (typeof value == "string") return value.replace(/\n/gi, "<br/>");
+      else return value;
+    },
+    preprocessLongNum(value) {
+      if (this.stringifyLongNum) {
+        if (String(value).startsWith("0x")) {
+          return value;
+        }
+        if (!isNaN(value) && value != "") {
+          if (value > 99999999999 || value < 0.0000000000001) {
+            return '="' + value + '"';
+          }
+        }
+      }
+      return value;
+    },
+    getValueFromNestedItem(item, indexes) {
+      let nestedItem = item;
+      for (let index of indexes) {
+        if (nestedItem) nestedItem = nestedItem[index];
+      }
+      return this.parseValue(nestedItem);
+    },
+
+    getValueFromCallback(item, callback) {
+      if (typeof callback !== "function") return this.defaultValue;
+      const value = callback(item);
+      return this.parseValue(value);
+    },
+    parseValue(value) {
+      return value || value === 0 || typeof value === "boolean"
+        ? value
+        : this.defaultValue;
+    },
+    base64ToBlob(data, mime) {
+      let base64 = window.btoa(window.unescape(encodeURIComponent(data)));
+      let bstr = atob(base64);
+      let n = bstr.length;
+      let u8arr = new Uint8ClampedArray(n);
+      while (n--) {
+        u8arr[n] = bstr.charCodeAt(n);
+      }
+      return new Blob([u8arr], { type: mime });
+    },
+  }, // end methods
+};
+</script>

+ 151 - 0
components/download-excel/download.js

@@ -0,0 +1,151 @@
+//download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage
+// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
+// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
+// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
+// v4 adds AMD/UMD, commonJS, and plain browser support
+// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
+// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
+// https://github.com/rndme/download
+
+export default function download(data, strFileName, strMimeType) {
+
+		var self = window, // this script is only for browsers anyway...
+			defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
+			mimeType = strMimeType || defaultMime,
+			payload = data,
+			url = !strFileName && !strMimeType && payload,
+			anchor = document.createElement("a"),
+			toString = function(a){return String(a);},
+			myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
+			fileName = strFileName || "download",
+			blob,
+			reader;
+			myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
+	  
+		if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
+			payload=[payload, mimeType];
+			mimeType=payload[0];
+			payload=payload[1];
+		}
+
+
+		if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
+			fileName = url.split("/").pop().split("?")[0];
+			anchor.href = url; // assign href prop to temp anchor
+		  	if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
+        		var ajax=new XMLHttpRequest();
+        		ajax.open( "GET", url, true);
+        		ajax.responseType = 'blob';
+        		ajax.onload= function(e){ 
+				  download(e.target.response, fileName, defaultMime);
+				};
+        		setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
+			    return ajax;
+			} // end if valid url?
+		} // end if url?
+
+
+		//go ahead and download dataURLs right away
+		if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){
+		
+			if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
+				payload=dataUrlToBlob(payload);
+				mimeType=payload.type || defaultMime;
+			}else{			
+				return navigator.msSaveBlob ?  // IE10 can't do a[download], only Blobs:
+					navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
+					saver(payload) ; // everyone else can save dataURLs un-processed
+			}
+			
+		}else{//not data url, is it a string with special needs?
+			if(/([\x80-\xff])/.test(payload)){			  
+				var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length;
+				for(i;i<mx;++i) tempUiArr[i]= payload.charCodeAt(i);
+			 	payload=new myBlob([tempUiArr], {type: mimeType});
+			}		  
+		}
+		blob = payload instanceof myBlob ?
+			payload :
+			new myBlob([payload], {type: mimeType}) ;
+
+
+		function dataUrlToBlob(strUrl) {
+			var parts= strUrl.split(/[:;,]/),
+			type= parts[1],
+			decoder= parts[2] == "base64" ? atob : decodeURIComponent,
+			binData= decoder( parts.pop() ),
+			mx= binData.length,
+			i= 0,
+			uiArr= new Uint8Array(mx);
+
+			for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);
+
+			return new myBlob([uiArr], {type: type});
+		 }
+
+		function saver(url, winMode){
+
+			if ('download' in anchor) { //html5 A[download]
+				anchor.href = url;
+				anchor.setAttribute("download", fileName);
+				anchor.className = "download-js-link";
+				anchor.innerHTML = "downloading...";
+				anchor.style.display = "none";
+				document.body.appendChild(anchor);
+				setTimeout(function() {
+					anchor.click();
+					document.body.removeChild(anchor);
+					if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
+				}, 66);
+				return true;
+			}
+
+			// handle non-a[download] safari as best we can:
+			if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
+				if(/^data:/.test(url))	url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
+				if(!window.open(url)){ // popup blocked, offer direct download:
+					if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
+				}
+				return true;
+			}
+
+			//do iframe dataURL download (old ch+FF):
+			var f = document.createElement("iframe");
+			document.body.appendChild(f);
+
+			if(!winMode && /^data:/.test(url)){ // force a mime that will download:
+				url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
+			}
+			f.src=url;
+			setTimeout(function(){ document.body.removeChild(f); }, 333);
+
+		}//end saver
+
+
+
+
+		if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
+			return navigator.msSaveBlob(blob, fileName);
+		}
+
+		if(self.URL){ // simple fast and modern way using Blob and URL:
+			saver(self.URL.createObjectURL(blob), true);
+		}else{
+			// handle non-Blob()+non-URL browsers:
+			if(typeof blob === "string" || blob.constructor===toString ){
+				try{
+					return saver( "data:" +  mimeType   + ";base64,"  +  self.btoa(blob)  );
+				}catch(y){
+					return saver( "data:" +  mimeType   + "," + encodeURIComponent(blob)  );
+				}
+			}
+
+			// Blob but not URL support:
+			reader=new FileReader();
+			reader.onload=function(e){
+				saver(this.result);
+			};
+			reader.readAsDataURL(blob);
+		}
+		return true;
+	}; /* end download() */

+ 72 - 0
components/fix-window/fix-window.vue

@@ -0,0 +1,72 @@
+<template>
+	<view class="fix-window">
+		<top-window class="fix-window-top"/>
+		<view class="fix-window-button" @click="tiggerWindow"></view>
+		<view v-show="visible" class="fix-window--mask" @click="tiggerWindow"></view>
+		<left-window v-show="visible" class="fix-window--popup" />
+	</view>
+</template>
+
+<script>
+	import topWindow from '../../windows/topWindow.vue'
+	import leftWindow from '../../windows/leftWindow.vue'
+	export default {
+		components:{
+			topWindow,
+			leftWindow
+		},
+		data() {
+			return {
+				visible: false
+			};
+		},
+		methods: {
+			tiggerWindow() {
+				this.visible = !this.visible
+			}
+		}
+	}
+</script>
+
+<style>
+.fix-window {
+}
+.fix-window-button {
+	width: 30px;
+	height: 30px;
+	opacity: 0.5;
+	position: fixed;
+	top: 40px;
+	left: 20px;
+	z-index: 999;
+}
+
+.fix-window-top {
+	width: 100%;
+	position: fixed;
+	top: 25px;
+	left: 0;
+	z-index: 999;
+}
+
+.fix-window--mask {
+	position: fixed;
+	bottom: 0px;
+	top: 25px;
+	left: 0px;
+	right: 0px;
+	background-color: rgba(0, 0, 0, 0.4);
+	transition-duration: 0.3s;
+	z-index: 997;
+}
+
+.fix-window--popup {
+	position: fixed;
+	top: 85px;
+	left: 0;
+	/* transform: translate(-50%, -50%); */
+	transition-duration: 0.3s;
+	z-index: 998;
+}
+
+</style>

+ 55 - 0
components/show-info/show-info.vue

@@ -0,0 +1,55 @@
+<template>
+	<view style="position: relative;">
+		<uni-icons @mouseenter.native="mouseenter" @mouseleave.native="showStableInfo = false"
+			style="padding:0 10px;color: #a8a8a8;cursor: pointer;" type="info" />
+		<view v-if="showStableInfo" class="show-stable" :style="{top:`${top}px`,left:`${left}px`,width:`${width}px`}">
+			<text>{{content}}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			content: String,
+			top: {
+				type: [Number, String],
+				default: -60
+			},
+			left: {
+				type: [Number, String],
+				default: -100
+			},
+			width: {
+				type: [Number, String],
+				default: 200
+			}
+		},
+		data() {
+			return {
+				showStableInfo: false,
+				arrowStyle: {}
+			}
+		},
+		methods: {
+			mouseenter(e) {
+				this.showStableInfo = true
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	$main_color: #fff;
+	$main_back_color: #303133;
+
+	.show-stable {
+		position: absolute;
+		padding: 5px 10px;
+		background-color: $main_back_color;
+		color: $main_color;
+		border-radius: 4px;
+		border: 1px solid #e9e9eb;
+		z-index: 99999;
+	}
+</style>

+ 184 - 0
components/uni-data-menu/uni-data-menu.vue

@@ -0,0 +1,184 @@
+<template>
+	<view>
+		<uni-nav-menu :active="value" activeKey="value" :activeTextColor="activeTextColor" :uniqueOpened="uniqueOpened"
+			@select="onSelect">
+			<uni-menu-sidebar :data="userMenu"></uni-menu-sidebar>
+			<uni-menu-sidebar :data="staticMenu"></uni-menu-sidebar>
+		</uni-nav-menu>
+	</view>
+</template>
+
+<script>
+	import {
+		mapActions
+	} from 'vuex'
+	import {
+		buildMenus
+	} from './util.js'
+	export default {
+		data() {
+			return {
+				menus: [],
+				userMenu: [],
+				famliy: [],
+
+			};
+		},
+		mixins: [uniCloud.mixinDatacom],
+		props: {
+			// 当前激活菜单的 url
+			value: {
+				type: String,
+				default: ''
+			},
+			// 当前激活菜单的文字颜色
+			activeTextColor: {
+				type: String,
+				default: '#42B983'
+			},
+			// 是否只保持一个子菜单的展开
+			uniqueOpened: {
+				type: Boolean,
+				default: false
+			},
+			staticMenu: {
+				type: Array,
+				default () {
+					return []
+				}
+			}
+		},
+		watch: {
+			localdata: {
+				handler(newval) {
+					if (this.hasLocalData(newval)) {
+						this.userMenu = newval
+						console.log('this.userMenu',this.userMenu);
+					}
+				},
+				immediate: true
+			},
+			// TODO 暂时无需监听,需要看后面会出现什么问题
+			menus: {
+				immediate: true,
+				handler(newVal,oldVal) {
+					const item = this.menus.find(m => m.value === this.$route.path)
+					// 设置面包屑
+					if(item){
+						this.getMenuAncestor(item.menu_id, newVal)
+						item && this.setRoutes && this.setRoutes(this.famliy)
+					}
+				}
+			},
+			$route: {
+				immediate: false,
+				handler(val, old) {
+					if (val.fullPath !== old.fullPath) {
+						this.famliy = []
+						const menu = this.menus.find(m => m.value === val.path)
+						const menu_id = menu && menu.menu_id
+						this.getMenuAncestor(menu_id, this.menus)
+						this.setRoutes && this.setRoutes(this.famliy)
+					}
+				}
+			}
+		},
+		created() {
+			if (this.hasLocalData(this.localdata)) return
+			// this.load()
+		},
+		// computed:{
+		// 	userMenu() {
+		// 		return this.getUserMenu(this.menus)
+		// 	}
+		// },
+		methods: {
+			...mapActions({
+				setRoutes: 'app/setRoutes'
+			}),
+			getUserMenu(menuList) {
+				const {
+					permission,
+					role
+				} = uniCloud.getCurrentUserInfo()
+				// 标记叶子节点
+				menuList.map(item => {
+					if (!menuList.some(subMenuItem => subMenuItem.parent_id === item.menu_id)) {
+						item.isLeafNode = true
+					}
+				})
+
+				// 删除无权限访问的菜单
+				if (!role.includes('admin')) {
+					menuList = menuList.filter(item => {
+						if (item.isLeafNode) {
+							if (item.permission && item.permission.length) {
+								return item.permission.some(item => permission.indexOf(item) > -1)
+							}
+							return false
+						}
+						return true
+					})
+				}
+				return buildMenus(menuList)
+			},
+			onSelect(menu) {
+
+				this.famliy = []
+				this.getMenuAncestor(menu.menu_id, this.menus)
+				this.emit(menu)
+			},
+			emit(menu) {
+				this.$emit('select', menu, this.famliy)
+				this.$emit('input', menu.value)
+			},
+			hasLocalData(value) {
+				return Array.isArray(value) && value.length > 0
+			},
+			load() {
+				if (this.mixinDatacomLoading == true) {
+					return
+				}
+				this.mixinDatacomLoading = true
+				this.mixinDatacomGet().then((res) => {
+					this.mixinDatacomLoading = false
+					const {
+						data,
+						count
+					} = res.result
+					this.menus = data
+					this.userMenu = this.getUserMenu(this.menus)
+					console.log('this.userMenu',this.userMenu);
+				}).catch((err) => {
+					this.mixinDatacomLoading = false
+					this.mixinDatacomErrorMessage = err
+				})
+			},
+			getMenuAncestor(menuId, menus) {
+				menus.forEach(item => {
+					if (item.menu_id === menuId) {
+						const route = {
+							name: item.text
+						}
+						const path = item.value
+						if (path) {
+							route.to = {
+								path
+							}
+						}
+						this.famliy.unshift(route)
+						const parent_id = item.parent_id
+						if (parent_id) {
+							this.getMenuAncestor(parent_id, menus)
+						}
+					}
+				})
+				// return famliy
+			}
+		},
+	}
+</script>
+
+<style>
+
+</style>

+ 67 - 0
components/uni-data-menu/util.js

@@ -0,0 +1,67 @@
+function buildMenu(menu, menuList, menuIds) {
+	let nextLayer = []
+	for (let i = menu.length - 1; i > -1; i--) {
+		const currentMenu = menu[i]
+		const subMenu = menuList.filter(item => {
+			if (item.parent_id === currentMenu.menu_id) {
+				menuIds.push(item.menu_id)
+				return true
+			}
+		})
+		nextLayer = nextLayer.concat(subMenu)
+		currentMenu.children = subMenu
+	}
+	if (nextLayer.length) {
+		buildMenu(nextLayer, menuList, menuIds)
+	}
+}
+
+function getParentIds(menuItem, menuList) {
+	const parentArr = []
+	let currentItem = menuItem
+	while (currentItem && currentItem.parent_id) {
+		parentArr.push(currentItem.parent_id)
+		currentItem = menuList.find(item => item.menu_id === currentItem.parent_id)
+	}
+	return parentArr
+}
+
+function buildMenus(menuList, trim = true) {
+	// 保证父子级顺序
+	menuList = menuList.sort(function(a, b) {
+		const parentIdsA = getParentIds(a, menuList)
+		const parentIdsB = getParentIds(b, menuList)
+		if (parentIdsA.includes(b.menu_id)) {
+			return 1
+		}
+		return parentIdsA.length - parentIdsB.length || a.sort - b.sort
+	})
+	// 删除无subMenu且非子节点的菜单项
+	if (trim) {
+		for (let i = menuList.length - 1; i > -1; i--) {
+			const currentMenu = menuList[i]
+			const subMenu = menuList.filter(subMenuItem => subMenuItem.parent_id === currentMenu.menu_id)
+			if (!currentMenu.isLeafNode && !subMenu.length) {
+				menuList.splice(i, 1)
+			}
+		}
+	}
+	const menuIds = []
+	const menu = menuList.filter(item => {
+		if (!item.parent_id) {
+			menuIds.push(item.menu_id)
+			return true
+		}
+	})
+	buildMenu(menu, menuList, menuIds)
+	// 包含所有无效菜单
+	if (!trim && menuIds.length !== menuList.length) {
+		menu.push(...menuList.filter(item => !menuIds.includes(item.menu_id)))
+	}
+	return menu
+}
+
+export {
+	buildMenu,
+	buildMenus
+}

+ 49 - 0
components/uni-menu-group/uni-menu-group.vue

@@ -0,0 +1,49 @@
+<template>
+	<view class="uni-menu-group">
+		<view class="uni-menu-group__title" name="title" :style="{paddingLeft:paddingLeft}">{{title}}</view>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	import rootParent from '../uni-nav-menu/mixins/rootParent.js'
+	export default {
+		name: 'uniMenuGroup',
+		mixins:[rootParent],
+		props: {
+			title: String
+		},
+		data() {
+			return {
+
+			};
+		},
+		computed: {
+			paddingLeft() {
+				return 20+20 * this.rootMenu.SubMenu.length + 'px'
+			}
+		},
+		created() {
+			this.init()
+		},
+		methods: {
+			init() {
+				this.rootMenu = {
+					SubMenu: []
+				}
+				this.getParentAll('SubMenu', this)
+			}
+		}
+	}
+</script>
+
+<style>
+.uni-menu-group {
+	/* border: 1px red solid; */
+}
+.uni-menu-group__title {
+	line-height: 36px;
+	font-size: 12px;
+	color: #999;
+}
+</style>

+ 133 - 0
components/uni-menu-item/uni-menu-item.vue

@@ -0,0 +1,133 @@
+<template>
+	<view class="uni-menu-item"
+		:class="{
+			'is-active':active,
+			'is-disabled':disabled
+		}"
+		:style="{
+			paddingLeft:paddingLeft,
+			'background-color':active?activeBackgroundColor:''
+		}"
+	 @click="onClickItem">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	import rootParent from '../uni-nav-menu/mixins/rootParent.js'
+	export default {
+		name: 'uniMenuItem',
+		mixins: [rootParent],
+		props: {
+			// 唯一标识
+			index: {
+				type: [String,Object],
+				default(){
+					return ''
+				}
+			},
+			// TODO 是否禁用
+			disabled: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				active: false,
+				activeTextColor: '#42B983',
+				textColor: '#303133',
+				activeBackgroundColor: ''
+			};
+		},
+		computed: {
+			paddingLeft() {
+				return 20 + 20 * this.rootMenu.SubMenu.length + 'px'
+			}
+		},
+		created() {
+			this.init()
+		},
+		destroyed() {
+			if (this.$menuParent) {
+				const menuIndex = this.$menuParent.itemChildrens.findIndex(item => item === this)
+				this.$menuParent.itemChildrens.splice(menuIndex, 1)
+			}
+		},
+		methods: {
+			init() {
+				this.rootMenu = {
+					NavMenu: [],
+					SubMenu: []
+				}
+				this.indexPath = []
+				// 获取直系的所有父元素实例
+				this.getParentAll('SubMenu', this)
+				// 获取最外层父元素实例
+				this.$menuParent = this.getParent('uniNavMenu', this)
+				this.$subMenu = this.rootMenu.SubMenu
+
+				this.activeTextColor = this.$menuParent.activeTextColor
+				this.textColor = this.$menuParent.textColor
+				this.activeBackgroundColor = this.$menuParent.activeBackgroundColor
+
+				// 将当前插入到menu数组中
+				if (this.$menuParent) {
+					this.$menuParent.itemChildrens.push(this)
+					this.$menuParent.isActive(this)
+				}
+			},
+
+			// 点击 menuItem
+			onClickItem(e) {
+				if (this.disabled) return
+				// 关闭其他已经选中的 itemMenu
+				this.$menuParent.closeOtherActive(this)
+				this.active = true
+				this.indexPath.unshift(this.index)
+				this.indexPath.reverse()
+				if(e !== 'init'){
+					// this.$menuParent.activeIndex=this.index
+					this.$menuParent.select(this.index, this.indexPath)
+				}
+			}
+
+		}
+	}
+</script>
+
+<style lang="scss">
+	.uni-menu-item {
+		display: flex;
+		align-items: center;
+		padding: 0 20px;
+		height: 56px;
+		line-height: 56px;
+		color: #303133;
+		transition: all 0.3s;
+		cursor: pointer;
+		// border-bottom: 1px #f5f5f5 solid;
+	}
+
+	.uni-menu-item:hover {
+		outline: none;
+		background-color: #EBEBEB;
+		transition: all 0.3s;
+	}
+
+	.is-active {
+		color: $uni-color-primary;
+		// background-color: #ecf8f3;
+	}
+
+	.is-disabled {
+		// background-color: #f5f5f5;
+		color: #999;
+	}
+
+	.uni-menu-item.is-disabled:hover {
+		background-color: inherit;
+		color: #999;
+		cursor: not-allowed;
+	}
+</style>

+ 48 - 0
components/uni-menu-sidebar/uni-menu-sidebar.vue

@@ -0,0 +1,48 @@
+<template>
+	<view class="pointer">
+		<template v-for="(item,index) in data">
+			<template v-if="!item.children || !item.children.length">
+				<uni-menu-item :index="item">
+					<view :class="item.icon"></view>
+					<text :class="{title: item.icon}">{{item.text}}</text>
+				</uni-menu-item>
+			</template>
+			<uni-sub-menu v-else :index="item">
+				<template v-slot:title>
+					<view :class="item.icon"></view>
+					<text :class="{title: item.icon}">{{item.text}}</text>
+				</template>
+				<uni-menu-sidebar class="item-bg"  :data="item.children" :key="item._id" />
+			</uni-sub-menu>
+		</template>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'uniMenuSidebar',
+		props: {
+			data: {
+				type: Array,
+				default () {
+					return []
+				}
+			}
+		},
+		data() {
+			return {};
+		},
+		computed: {
+
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style lang="scss">
+	.title {
+		margin-left: 5px;
+	}
+</style>

+ 29 - 0
components/uni-nav-menu/mixins/rootParent.js

@@ -0,0 +1,29 @@
+export default {
+	methods:{
+		/**
+		 * 获取所有父元素
+		 * @param {Object} name
+		 * @param {Object} parent
+		 */
+		getParentAll(name, parent) {
+			parent = this.getParent(`uni${name}`, parent)
+			if (parent) {
+				this.rootMenu[name].push(parent)
+				this.getParentAll(name, parent)
+			}
+		},
+		/**
+		 * 获取父元素实例
+		 */
+		getParent(name, parent, type) {
+			parent = parent.$parent;
+			let parentName = parent.$options.name;
+			while (parentName !== name) {
+				parent = parent.$parent;
+				if (!parent) return false
+				parentName = parent.$options.name;
+			}
+			return parent;
+		}
+	}
+}

+ 209 - 0
components/uni-nav-menu/uni-nav-menu.vue

@@ -0,0 +1,209 @@
+<template>
+	<view class="uni-nav-menu" :style="{'background-color':backgroundColor}">
+		<slot>
+			<uni-menu-sidebar :data="data"></uni-menu-sidebar>
+		</slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'uniNavMenu',
+		props: {
+			data: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+
+			// 模式	可选值 horizontal / vertical
+			mode: {
+				type: String,
+				default: 'vertical'
+			},
+			// 是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)
+			collapse: {
+				type: Boolean,
+				default: false
+			},
+			// 菜单的背景色
+			backgroundColor: {
+				type: String,
+				default: '#fff'
+			},
+			// 菜单的文字颜色
+			textColor: {
+				type: String,
+				default: '#303133'
+			},
+			// 当前激活菜单的文字颜色
+			activeTextColor: {
+				type: String,
+				default: '#42B983'
+			},
+			// 当前激活菜单的背景色
+			activeBackgroundColor: {
+				type: String,
+				default: 'inherit'
+			},
+			// 如果 index 为 Object ,需要指定选中字段的名称
+			activeKey: {
+				type: String,
+				default: 'id'
+			},
+			// 当前激活菜单的 index
+			active: {
+				type: String,
+				default: ''
+			},
+			// 当前打开的 sub-menu 的 index 的数组
+			defaultOpeneds: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			// 是否只保持一个子菜单的展开
+			uniqueOpened: {
+				type: Boolean,
+				default: false
+			},
+			// TODO 子菜单打开的触发方式(只在 mode 为 horizontal 时有效) ,可选值 	 hover / click
+			menuTrigger: {
+				type: String,
+				default: 'hover'
+			},
+			router: {
+				type: Boolean,
+				default: false
+			},
+			// 是否开启折叠动画
+			collapseTransition: {
+				type: Boolean,
+				default: true
+			}
+		},
+		data() {
+			return {
+				activeIndex: this.active
+			};
+		},
+		watch: {
+			active(newVal) {
+				this.activeIndex=newVal
+			},
+			activeIndex(newVal, oldVal) {
+				if (this.itemChildrens.length > 0) {
+					let isActive = false
+					for(let i = 0 ; i < this.itemChildrens.length ;i++){
+						const item  = this.itemChildrens[i]
+						isActive = this.isActive(item)
+						if(isActive) break
+					}
+					if(!isActive){
+						this.closeAll()
+					}
+				}
+			}
+		},
+		created() {
+			this.itemChildrens = []
+			this.subChildrens = []
+			// this.activeIndex = this.active
+		},
+		methods: {
+			// menu 菜单激活回调
+			select(key, keyPath) {
+				this.$emit('select', key, keyPath)
+			},
+			// sub-menu 展开的回调
+			open(key, keyPath) {
+				this.$emit('open', key, keyPath)
+			},
+			// sub-menu 收起的回调
+			close(key, keyPath) {
+				this.$emit('close', key, keyPath)
+			},
+			// 判断当前选中,只有初始值会使用
+			isActive(subItem) {
+				let active = ''
+				let isActive = false
+				if(typeof(subItem.index) === 'object'){
+					active = subItem.index[this.activeKey] || ''
+				}else{
+					active = subItem.index
+				}
+				if (subItem.index && this.activeIndex === active) {
+					isActive = true
+					subItem.$subMenu.forEach((item, index) => {
+						if (!item.disabled && !subItem.disabled ) {
+							subItem.indexPath.push(item.index)
+							item.isOpen = true
+						}
+					})
+					if(!subItem.active){
+						subItem.onClickItem('init')
+					}
+				}
+				return isActive
+			},
+			// 打开关闭 sunMenu
+			selectMenu(subMenu){
+				// const subMenu = this.$menuParent
+				this.subChildrens.forEach((item,index)=>{
+					if(item === subMenu){
+						subMenu.isOpen = !subMenu.isOpen
+						subMenu.indexPath.push(subMenu.index)
+					}else{
+						if(item.isOpen && this.uniqueOpened) item.isOpen = false
+					}
+				})
+
+				subMenu.$subMenu.forEach((sub,idx)=>{
+						sub.isOpen = true
+						subMenu.indexPath.unshift(sub.index)
+				})
+				if(subMenu.isOpen){
+					this.open(subMenu.indexPath[subMenu.indexPath.length-1],subMenu.indexPath)
+				}else{
+					this.close(subMenu.indexPath[subMenu.indexPath.length-1],subMenu.indexPath)
+				}
+				subMenu.indexPath = []
+			},
+			// 关闭其他选中
+			closeOtherActive(itemMenu) {
+				// let parents = this.$menuParent
+				itemMenu.indexPath = []
+				itemMenu.$subMenu.forEach((item) => {
+					if (!item.disabled) {
+						itemMenu.indexPath.push(item.index)
+					}
+				})
+				this.itemChildrens.map((item) => {
+					if (item.active) {
+						item.active = false
+					}
+					return item
+				})
+			},
+			// 关闭所有
+			closeAll() {
+				this.subChildrens.forEach((item) => {
+					if (item.isOpen) {
+						item.isOpen = false
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.uni-nav-menu {
+		width: 240px;
+		// min-height: 500px;
+		background-color: #FFFFFF;
+		font-size: 14px;
+	}
+</style>

+ 36 - 0
components/uni-stat-breadcrumb/uni-stat-breadcrumb.vue

@@ -0,0 +1,36 @@
+<template>
+	<view class="uni-breadcrumb-x">
+		<uni-breadcrumb separator="/">
+			<uni-breadcrumb-item v-for="(route, index) in routes" :key="index" :to="route.to && route.to.path||''">{{route.name}}</uni-breadcrumb-item>
+		</uni-breadcrumb>
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex'
+
+	export default {
+		name: "uni-stat-breadcrumb",
+		data() {
+			return {
+
+			};
+		},
+		computed: {
+			...mapState('app', ['routes'])
+		}
+	}
+</script>
+
+<style>
+	.uni-breadcrumb-x {
+		flex: 1;
+		display: flex;
+		padding: 0 5px;
+		min-height: 55px;
+		line-height: 55px;
+		align-items: center;
+	}
+</style>

+ 116 - 0
components/uni-stat-panel/uni-stat-panel.vue

@@ -0,0 +1,116 @@
+<template>
+	<view class="uni-stat--sum-x mb-m">
+		<view v-for="(item, index) in items" :key="index" class="uni-stat--sum-item"
+			:class="[item.value === '今天' ? 'uni-stat--sum-item-width' : '']">
+			<!-- #ifdef MP -->
+			<view class="uni-stat--sum-item-title">
+				{{item.title ? item.title : ''}}
+			</view>
+			<!-- #endif -->
+			<!-- #ifndef MP -->
+			<uni-tooltip>
+				<view class="uni-stat--sum-item-title">
+					{{item.title ? item.title : ''}}
+					<uni-icons v-if="item.title" class="ml-s" type="help" color="#666" />
+				</view>
+				<template v-if="item.tooltip" v-slot:content>
+					<view class="uni-stat-tooltip-s">
+						{{item.tooltip}}
+					</view>
+				</template>
+			</uni-tooltip>
+			<!-- #endif -->
+			<view class="uni-stat--sum-item-value">{{item.value ? item.value : 0}}</view>
+			<view v-if="contrast" class="uni-stat--sum-item-contrast">{{item.contrast ? item.contrast : 0}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "uni-stat-panel",
+		data() {
+			return {
+
+			};
+		},
+		props: {
+			items: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+			contrast: {
+				type: Boolean,
+				default: false
+			}
+		}
+
+	}
+</script>
+
+<style lang="scss">
+	.uni-stat-tooltip-s {
+		width: 160px;
+		white-space: normal;
+	}
+
+	.uni-stat--sum {
+		&-x {
+			display: flex;
+			justify-content: space-evenly;
+			flex-wrap: wrap;
+			border-radius: 4px;
+			padding: 15px;
+			box-shadow: -1px -1px 5px 0 rgba(0, 0, 0, 0.1);
+		}
+
+		&-item {
+			white-space: nowrap;
+			text-align: center;
+			margin: 10px 18px;
+
+			&-width {
+				width: 100px
+			}
+		}
+
+		&-item-title {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			min-height: 17px;
+			font-size: 12px;
+			color: #666;
+		}
+
+		&-item-value {
+			font-size: 24px;
+			line-height: 48px;
+			font-weight: 700;
+			color: #333;
+		}
+
+		&-item-contrast {
+			font-size: 14px;
+			color: #666;
+		}
+	}
+
+	/* #ifndef APP-NVUE */
+	@media screen and (max-width: 500px) {
+		.uni-stat--sum-x {
+			padding: 15px 0;
+			justify-content: space-between;
+			flex-wrap: unset;
+			overflow-x: auto !important;
+		}
+
+		::-webkit-scrollbar {
+			display: none;
+		}
+	}
+
+	/* #endif */
+</style>

+ 71 - 0
components/uni-stat-table/uni-stat-table.vue

@@ -0,0 +1,71 @@
+<template>
+	<uni-table :loading="loading" border stripe emptyText="暂无数据">
+		<uni-tr>
+			<template v-for="(mapper, index) in filedsMap">
+				<uni-th v-if="mapper.title" :key="index" align="center">
+					<!-- #ifdef MP -->
+					{{mapper.title}}
+					<!-- #endif -->
+					<!-- #ifndef MP -->
+					<uni-tooltip>
+						{{mapper.title}}
+						<uni-icons v-if="tooltip && mapper.tooltip" type="help" color="#666" />
+						<template v-if="tooltip && mapper.tooltip" v-slot:content>
+							<view class="uni-stat-tooltip-s">
+								{{mapper.tooltip}}
+							</view>
+						</template>
+					</uni-tooltip>
+					<!-- #endif -->
+				</uni-th>
+			</template>
+		</uni-tr>
+		<uni-tr v-for="(item ,i) in data" :key="i">
+			<template v-for="(mapper, index) in filedsMap">
+				<uni-td v-if="mapper.title" :key="index" align="center">
+					{{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
+				</uni-td>
+			</template>
+		</uni-tr>
+	</uni-table>
+</template>
+
+<script>
+	export default {
+		name: "uni-stat-table",
+		data() {
+			return {
+
+			};
+		},
+		props: {
+			data: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+			filedsMap: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+			loading: {
+				type: Boolean,
+				default: false
+			},
+			tooltip: {
+				type: Boolean,
+				default: false
+			}
+		}
+	}
+</script>
+
+<style>
+	.uni-stat-tooltip-s {
+		width: 160px;
+		white-space: normal;
+	}
+</style>

+ 332 - 0
components/uni-stat-tabs/uni-stat-tabs.vue

@@ -0,0 +1,332 @@
+<template>
+	<view class="uni-stat--tab-x">
+		<view v-if="label" class="uni-label-text hide-on-phone">{{label + ':'}}</view>
+		<view class="uni-stat--tab">
+			<view v-if="!renderTabs.length" class="uni-stat--tab-item uni-stat--tab-item-disabled"
+				:class="[`uni-stat--tab-item-${type}`]">
+				{{placeholder}}
+			</view>
+			<view v-else v-for="(item, index) in renderTabs" :key="index" @click="change(item, index)"
+				class="uni-stat--tab-item" :class="[
+					index === currentTab ? `uni-stat--tab-item-${type}-active` : '' , `uni-stat--tab-item-${type}`,
+					item.disabled ? 'uni-stat--tab-item-disabled' : ''
+				]">
+				<!-- #ifdef MP -->
+				{{item.name}}
+				<!-- #endif -->
+				<!-- #ifndef MP -->
+				<uni-tooltip>
+					{{item.name}}
+					<uni-icons v-if="item.tooltip" type="help" color="#666" />
+					<template v-if="item.tooltip" v-slot:content>
+						<view class="uni-stat-tooltip-s">
+							{{item.tooltip}}
+						</view>
+					</template>
+				</uni-tooltip>
+				<!-- #endif -->
+			</view>
+
+		</view>
+	</view>
+
+</template>
+
+<script>
+	export default {
+		name: "uni-stat-tabs",
+		data() {
+			return {
+				currentTab: 0,
+				renderTabs: []
+			};
+		},
+		props: {
+			type: {
+				type: String,
+				default: 'line'
+			},
+			value: {
+				type: [String, Number],
+				default: ''
+			},
+			modelValue: {
+				type: [String, Number],
+				default: ''
+			},
+			current: {
+				type: [String, Number],
+				default: 0
+			},
+			mode: {
+				type: String,
+				default: ''
+			},
+			today: {
+				type: Boolean,
+				default: false
+			},
+			yesterday: {
+				type: Boolean,
+				default: true
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			tooltip: {
+				type: Boolean,
+				default: false
+			},
+			all: {
+				type: Boolean,
+				default: true
+			},
+			label: {
+				type: String,
+				default: ''
+			},
+			placeholder: {
+				type: String,
+				default: '暂无选项'
+			},
+			tabs: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			}
+		},
+		created() {
+			this.last = `${this.mode.replace('-', '_')}_last_data`
+		},
+		mounted() {
+			this.init()
+		},
+		watch: {
+			current(val) {
+				this.currentTab = val
+			},
+
+			// value(val) {
+			// 	this.currentTab = val
+			// },
+
+			tabs: {
+				immediate: false,
+				handler(val) {
+					this.init()
+				}
+			},
+
+			renderTabs(val) {
+				const index = this.current
+				if (this.mode && val.length && index >= 0) {
+					this.$nextTick(function() {
+						const item = this.renderTabs[index]
+						this.change(item, index)
+					})
+				}
+			}
+		},
+		methods: {
+			init() {
+				if (this.mode.indexOf('platform') > -1) {
+					this.renderTabs = uni.getStorageSync(this.last)
+					this.getPlatform()
+				} else if (this.mode === 'date') {
+					const dates = [{
+						_id: 7,
+						name: '最近七天',
+					}, {
+						_id: 30,
+						name: '最近30天',
+					}, {
+						_id: 90,
+						name: '最近90天',
+					}]
+					if (this.yesterday) {
+						dates.unshift({
+							_id: 1,
+							name: '昨天',
+						})
+					}
+					if (this.today) {
+						dates.unshift({
+							_id: 0,
+							name: '今天',
+						})
+					}
+					this.renderTabs = dates
+				} else {
+					this.renderTabs = this.tabs
+				}
+			},
+			change(item, index) {
+				if (item.disabled) return
+				const id = item._id
+				const name = item.name
+				this.currentTab = index
+				this.emit(id, index, name, item)
+			},
+			emit(id, index, name, item) {
+				this.$emit('change', id, index, name, item)
+				this.$emit('input', id, index, name)
+				this.$emit('update:modelValue', id, index, name)
+			},
+			getPlatform() {
+				const db = uniCloud.database()
+				const appList = db.collection('uni-stat-app-platforms')
+					.get()
+					.then(res => {
+						let platforms = res.result.data
+						platforms = platforms.filter(p => p.hasOwnProperty('enable') ? p.enable : true)
+						platforms.sort((a, b) => a.order - b.order)
+						if (this.mode === 'platform-channel') {
+							platforms = platforms.filter(item => /^android|ios$/.test(item.code))
+							let _id = platforms.map(p => `platform_id == "${p._id}"`).join(' || ')
+							_id = `(${_id})`
+							this.setAllItem(platforms, _id)
+						} else if (this.mode === 'platform-scene') {
+							platforms = platforms.filter(item => /mp-/.test(item.code))
+							let _id = platforms.map(p => `platform_id == "${p._id}"`).join(' || ')
+							_id = `(${_id})`
+							this.setAllItem(platforms, _id)
+						} else {
+							this.setAllItem(platforms)
+						}
+						uni.setStorageSync(this.last, platforms)
+						this.renderTabs = platforms
+					})
+			},
+			setAllItem(platforms, _id = '', name = '全部') {
+				this.all && platforms.unshift({
+					name,
+					_id
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.uni-stat-tooltip-s {
+		width: 160px;
+		white-space: normal;
+	}
+
+	.uni-label-text {
+		font-size: 14px;
+		font-weight: bold;
+		color: #555;
+		margin-top: 17px;
+		margin-bottom: 17px;
+		margin-right: 5px;
+		// display: flex;
+		// align-items: center;
+		// justify-content: center;
+	}
+
+	.uni-stat--tab-x {
+		display: flex;
+		margin: 0 15px;
+		white-space: nowrap;
+	}
+
+	.uni-stat--tab {
+		display: flex;
+		flex-wrap: wrap;
+	}
+
+	.uni-stat {
+
+		&--tab {
+
+			&-item {
+				white-space: nowrap;
+				font-size: 14px;
+				color: #666;
+				text-align: center;
+				cursor: pointer;
+				box-sizing: border-box;
+				margin: 15px 0;
+
+				&-disabled {
+					cursor: unset;
+					opacity: 0.4;
+				}
+
+				&-line {
+					margin-right: 30px;
+					padding: 2px 0;
+					border-bottom: 1px solid transparent;
+
+					&:last-child {
+						margin-right: 0;
+					}
+
+					&-active {
+						color: $uni-color-primary;
+						border-bottom: 1px solid $uni-color-primary;
+						// &-disabled {
+						// 	color: #666;
+						// 	border-color: #666;
+						// }
+					}
+				}
+
+				&-boldLine {
+					box-sizing: border-box;
+					margin-right: 30px;
+					padding: 2px 0;
+					border-bottom: 2px solid transparent;
+
+					&:last-child {
+						margin-right: 0;
+					}
+
+					&-active {
+						box-sizing: border-box;
+						color: $uni-color-primary;
+						border-bottom: 2px solid $uni-color-primary;
+					}
+				}
+
+				&-box {
+					padding: 5px 15px;
+					border: 1px solid #dcdfe6;
+					// margin: 0;
+
+					&:not(:last-child) {
+						border-right-color: transparent;
+					}
+
+
+					&-active {
+						box-sizing: border-box;
+						border: 1px solid $uni-color-primary !important;
+					}
+				}
+			}
+
+		}
+	}
+
+	/* #ifndef APP-NVUE */
+	@media screen and (max-width: 500px) {
+		.hide-on-phone {
+			display: none;
+		}
+
+		.uni-stat--tab {
+			flex-wrap: unset;
+			overflow-x: auto !important;
+		}
+
+		::-webkit-scrollbar {
+			display: none;
+		}
+	}
+
+	/* #endif */
+</style>

+ 164 - 0
components/uni-sub-menu/uni-sub-menu.vue

@@ -0,0 +1,164 @@
+<template>
+	<view class="uni-sub-menu">
+		<view class="uni-sub-menu__title"  :class="{'is-disabled':disabled}" :style="{paddingLeft:paddingLeft}" @click="select">
+			<view class="uni-sub-menu__title-sub" :style="{color:disabled?'#999':textColor}">
+				<slot name="title"></slot>
+			</view>
+			<uni-icons class="uni-sub-menu__icon" :class="{transition:isOpen}" type="arrowdown" color="#bbb" size="14"></uni-icons>
+		</view>
+		<view class="uni-sub-menu__content" :class="{'uni-sub-menu--close':!isOpen}" :style="{'background-color':backgroundColor}">
+			<view id="content--hook">
+				<slot></slot>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import rootParent from '../uni-nav-menu/mixins/rootParent.js'
+	export default {
+		name: 'uniSubMenu',
+		mixins: [rootParent],
+		props: {
+			// 唯一标识
+			index: {
+				type: [String,Object],
+				default(){
+					return ''
+				}
+			},
+			// TODO 自定义类名
+			popperClass: {
+				type: String,
+				default: ''
+			},
+			// TODO 是否禁用
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			// 展开菜单的背景色
+			backgroundColor: {
+				type: String,
+				default: '#f5f5f5'
+			},
+		},
+		data() {
+			return {
+				height: 0,
+				oldheight: 0,
+				isOpen: false,
+				textColor:'#303133'
+			};
+		},
+		computed: {
+			paddingLeft() {
+				return 20 + 20 * this.rootMenu.SubMenu.length + 'px'
+			}
+		},
+		created() {
+			this.init()
+		},
+		destroyed() {
+			// 销毁页面后,将当前页面实例从数据中删除
+			if (this.$menuParent) {
+				const menuIndex = this.$menuParent.subChildrens.findIndex(item => item === this)
+				this.$menuParent.subChildrens.splice(menuIndex, 1)
+			}
+		},
+		methods: {
+			init() {
+				// 所有父元素
+				this.rootMenu = {
+					NavMenu: [],
+					SubMenu: []
+				}
+				this.childrens = []
+				this.indexPath = []
+				// 获取直系的所有父元素实例
+				this.getParentAll('SubMenu', this)
+				// 获取最外层父元素实例
+				this.$menuParent = this.getParent('uniNavMenu', this)
+				this.textColor = this.$menuParent.textColor
+				// 直系父元素 SubMenu
+				this.$subMenu = this.rootMenu.SubMenu
+
+				// 将当前插入到menu数组中
+				if(this.$menuParent){
+					this.$menuParent.subChildrens.push(this)
+				}
+			},
+			select() {
+				if(this.disabled) return
+				// 手动开关 sunMenu
+				this.$menuParent.selectMenu(this)
+			},
+			open() {
+				this.isOpen = true
+			},
+			close() {
+				this.isOpen = false
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.uni-sub-menu {
+		position: relative;
+		/* background-color: #FFFFFF; */
+	}
+
+	.uni-sub-menu__title {
+		display: flex;
+		align-items: center;
+		padding: 0 20px;
+		padding-right: 10px;
+		height: 56px;
+		line-height: 56px;
+		color: #303133;
+		cursor: pointer;
+		/* border-bottom: 1px #f5f5f5 solid; */
+	}
+
+	.uni-sub-menu__title:hover {
+		color: #42B983;
+		outline: none;
+		background-color: #EBEBEB;
+	}
+
+	.uni-sub-menu__title-sub {
+		display: flex;
+		align-items: center;
+		flex: 1;
+	}
+
+	.uni-sub-menu--close {
+		height: 0;
+		/* transition: all 0.3s; */
+	}
+
+	.uni-sub-menu__content {
+		overflow: hidden;
+	}
+
+	.uni-sub-menu__icon {
+		max-height: auto;
+		transition: all 0.2s;
+	}
+
+	.transition {
+		transform: rotate(-180deg);
+	}
+
+	.is-disabled {
+		/* background-color: #f5f5f5; */
+		color: red;
+	}
+	.uni-sub-menu__title.is-disabled:hover {
+		background-color: inherit;
+		color: #999;
+		cursor: not-allowed;
+	}
+
+</style>

+ 105 - 0
i18n/en.json

@@ -0,0 +1,105 @@
+{
+	"login": {
+		"text": {
+			"title": "System Login",
+			"prompt": "If there is no administrator account, please create an administrator first..."
+		},
+		"field": {
+			"username": "Account",
+			"password": "Password",
+			"captcha": "Captcha"
+		},
+		"button": {
+			"login": "Log In"
+		}
+	},
+	"topwindow": {
+		"text": {
+			"doc": "Admin doc",
+			"plugin": "More admin plugin",
+			"changeLanguage": "Language",
+			"changePwd": "ChangePwd",
+			"signOut": "Sign out"
+		}
+	},
+	"index": {
+		"text": {
+			"prompt": "Main content, customizable content and style",
+			"vesion": "The current version can be viewed in the console and package.json"
+		}
+	},
+	"updatePwd": {
+		"text": {
+			"title": "Change Password"
+		},
+		"field": {
+			"oldPassword": "Old password",
+			"newPassword": "New password",
+			"passwordConfirmation": "Confirm password"
+		},
+		"button": {
+			"save": "Save",
+			"back": "Back"
+		}
+	},
+	"common": {
+		"placeholder": {
+			"query": "Enter search content"
+		},
+		"button": {
+			"search": "Search",
+			"add": "Add",
+			"edit": "Edit",
+			"delete": "Delete",
+			"batchDelete": "Batch Delete",
+			"exportExcel": "Export Excel",
+			"submit": "Submit",
+			"back": "Back",
+			"tagManager": "Tag Manager",
+			"publish": "Publish page management",
+			"version": "version manager"
+		},
+		"empty": "No more data",
+		"piecePerPage": "piece/page"
+	},
+	"user": {
+		"text": {
+			"userManager": "Users Manager"
+		}
+	},
+	"role": {
+		"text": {
+			"roleManager": "Roles Manager"
+		}
+	},
+	"permission": {
+		"text": {
+			"permissionManager": "Permissions Manager"
+		}
+	},
+	"app": {
+		"text": {
+			"appManager": "App Manager",
+			"describle": "Manage the apps that users can login"
+		}
+	},
+	"menu": {
+		"text": {
+			"menuManager": "Menus Manager",
+			"additiveMenu": "Additive Menu"
+		},
+		"button": {
+			"addFirstLevelMenu": "Add First-level Menu",
+			"addChildMenu": "Submenu"
+		}
+	},
+	"demo": {
+		"icons": {
+			"title": "Icons",
+			"describle": "Click icons to copy the icon code"
+		},
+		"table": {
+			"title": "Table"
+		}
+	}
+}

+ 8 - 0
i18n/index.js

@@ -0,0 +1,8 @@
+import en from './en.json'
+import zhHans from './zh-Hans.json'
+import zhHant from './zh-Hant.json'
+export default {
+  en,
+  'zh-Hans': zhHans,
+  'zh-Hant': zhHant
+}

+ 105 - 0
i18n/zh-Hans.json

@@ -0,0 +1,105 @@
+{
+	"login": {
+		"text": {
+			"title": "系统登录",
+			"prompt": "如无管理员账号,请先创建管理员"
+		},
+		"field": {
+			"username": "账号",
+			"password": "密码",
+			"captcha": "验证码"
+		},
+		"button": {
+			"login": "登录"
+		}
+	},
+	"topwindow": {
+		"text": {
+			"doc": "Admin 框架文档",
+			"plugin": "浏览更多 Admin 插件",
+			"changeLanguage": "切换语言",
+			"changePwd": "修改密码",
+			"signOut": "退出"
+		}
+	},
+	"index": {
+		"text": {
+			"prompt": "内容主体,可自定义内容及样式",
+			"vesion": "可在控制台和 package.json 中查看当前的版本"
+		}
+	},
+	"updatePwd": {
+		"text": {
+			"title": "修改密码"
+		},
+		"field": {
+			"oldPassword": "旧密码",
+			"newPassword": "新密码",
+			"passwordConfirmation": "确认新密码"
+		},
+		"button": {
+			"save": "保存",
+			"back": "返回"
+		}
+	},
+	"common": {
+		"placeholder": {
+			"query": "请输入搜索内容"
+		},
+		"button": {
+			"search": "搜索",
+			"add": "新增",
+			"edit": "修改",
+			"delete": "删除",
+			"batchDelete": "批量删除",
+			"exportExcel": "导出 Excel",
+			"submit": "提交",
+			"back": "返回",
+			"tagManager": "标签管理",
+			"publish": "发布页管理",
+			"version": "版本管理"
+		},
+		"empty": "没有更多数据",
+		"piecePerPage": "条/页"
+	},
+	"user": {
+		"text": {
+			"userManager": "用户管理"
+		}
+	},
+	"role": {
+		"text": {
+			"roleManager": "角色管理"
+		}
+	},
+	"permission": {
+		"text": {
+			"permissionManager": "权限管理"
+		}
+	},
+	"app": {
+		"text": {
+			"appManager": "应用管理",
+			"describle": "管理用户可登录的应用"
+		}
+	},
+	"menu": {
+		"text": {
+			"menuManager": "菜单列表",
+			"additiveMenu": "待添加菜单"
+		},
+		"button": {
+			"addFirstLevelMenu": "新增一级菜单",
+			"addChildMenu": "子菜单"
+		}
+	},
+	"demo": {
+		"icons": {
+			"title": "图标",
+			"describle": "点击图标即可复制图标代码"
+		},
+		"table": {
+			"title": "表格"
+		}
+	}
+}

+ 105 - 0
i18n/zh-Hant.json

@@ -0,0 +1,105 @@
+{
+	"login": {
+		"text": {
+			"title": "系統登錄",
+			"prompt": "如無管理員賬號,請先創建管理員..."
+		},
+		"field": {
+			"username": "賬號",
+			"password": "密碼",
+			"captcha": "驗證碼"
+		},
+		"button": {
+			"login": "登錄"
+		}
+	},
+	"topwindow": {
+		"text": {
+			"doc": "Admin 框架文檔",
+			"plugin": "瀏覽更多 Admin 插件",
+			"changeLanguage": "切换语言",
+			"changePwd": "修改密碼",
+			"signOut": "退出"
+		}
+	},
+	"index": {
+		"text": {
+			"prompt": "內容主體,可自定義內容及樣式",
+			"vesion": "可在控制台和 package.json 中查看當前的版本"
+		}
+	},
+	"updatePwd": {
+		"text": {
+			"title": "修改密碼"
+		},
+		"field": {
+			"oldPassword": "舊密碼",
+			"newPassword": "新密碼",
+			"passwordConfirmation": "確認新密碼"
+		},
+		"button": {
+			"save": "保存",
+			"back": "返回"
+		}
+	},
+	"common": {
+		"placeholder": {
+			"query": "請輸入搜索內容"
+		},
+		"button": {
+			"search": "搜索",
+			"add": "新增",
+			"edit": "修改",
+			"delete": "刪除",
+			"batchDelete": "批量刪除",
+			"exportExcel": "導出 Excel",
+			"submit": "提交",
+			"back": "返回",
+			"tagManager": "標簽管理",
+			"publish": "發布頁管理",
+			"version": "版本管理"
+		},
+		"empty": "沒有更多數據",
+		"piecePerPage": "條/頁"
+	},
+	"user": {
+		"text": {
+			"userManager": "用戶管理"
+		}
+	},
+	"role": {
+		"text": {
+			"roleManager": "角色管理"
+		}
+	},
+	"permission": {
+		"text": {
+			"permissionManager": "權限管理"
+		}
+	},
+	"app": {
+		"text": {
+			"appManager": "應用管理",
+			"describle": "管理用戶可登錄的應用"
+		}
+	},
+	"menu": {
+		"text": {
+			"menuManager": "菜單列表",
+			"additiveMenu": "待添加菜單"
+		},
+		"button": {
+			"addFirstLevelMenu": "新增一級增",
+			"addChildMenu": "子菜單"
+		}
+	},
+	"demo": {
+		"icons": {
+			"title": "圖標",
+			"describle": "點擊圖標即可複製圖標代碼"
+		},
+		"table": {
+			"title": "表格"
+		}
+	}
+}

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

+ 66 - 0
js_sdk/uni-admin/constants.js

@@ -0,0 +1,66 @@
+const uniAdminPrefix = 'UNI_ADMIN'
+
+export const MENU = `${uniAdminPrefix}_MENU`
+
+/** uni-id 云端错误码 */
+export const UNI_ID_ERR_CODE = {
+	/** 登陆状态失效,token已过期 */
+	TOKEN_EXPIRED: 'UNI-ID-TOKEN-EXPIRED',
+	/** token校验未通过 */
+	CHECK_TOKEN_FAILED: 'uni-id-check-token-failed',
+	/** 账户已存在 */
+	ACCOUNT_EXISTS: 'uni-id-account-exists',
+	/** 账户不存在 */
+	ACCOUNT_NOT_EXISTS: 'uni-id-account-not-exists',
+	/** 用户账号冲突,可能会由开发者手动更新数据库导致,正常情况下不应 */
+	ACCOUNT_CONFLICT: 'uni-id-account-conflict',
+	/** 此账号已封禁 */
+	ACCOUNT_BANNED: 'uni-id-account-banned',
+	/** 此账号正在审核中 */
+	ACCOUNT_AUDITING: 'uni-id-account-auditing',
+	/** 此账号审核失败 */
+	ACCOUNT_AUDIT_FAILED: 'uni-id-account-audit-failed',
+	/** 此账号已注销 */
+	ACCOUNT_CLOSED: 'uni-id-account-closed',
+	/** 请输入图形验证码 */
+	CAPTCHA_REQUIRED: 'uni-id-captcha-required',
+	/** 用户名或密码错误 */
+	PASSWORD_ERROR: 'uni-id-password-error',
+	/** 用户名不合法 */
+	INVALID_USERNAME: 'uni-id-invalid-username',
+	/** 密码不合法 */
+	INVALID_PASSWORD: 'uni-id-invalid-password',
+	/** 手机号码不合法 */
+	INVALID_MOBILE: 'uni-id-invalid-mobile',
+	/** 邮箱不合法 */
+	INVALID_EMAIL: 'uni-id-invalid-email',
+	/** 昵称不合法 */
+	INVALID_NICKNAME: 'uni-id-invalid-nickname',
+	/** 参数错误 */
+	INVALID_PARAM: 'uni-id-invalid-param',
+	/** 缺少参数 */
+	PARAM_REQUIRED: 'uni-id-param-required',
+	/** 获取第三方账号失败 */
+	GET_THIRD_PARTY_ACCOUNT_FAILED: 'uni-id-get-third-party-account-failed',
+	/** 获取第三方用户信息失败 */
+	GET_THIRD_PARTY_USER_INFO_FAILED: 'uni-id-get-third-party-user-info-failed',
+	/** 手机验证码错误或已过期 */
+	MOBILE_VERIFY_CODE_ERROR: 'uni-id-mobile-verify-code-error',
+	/** 邮箱验证码错误或已过期 */
+	EMAIL_VERIFY_CODE_ERROR: 'uni-id-email-verify-code-error',
+	/** 超级管理员已存在 */
+	ADMIN_EXISTS: 'uni-id-admin-exists',
+	/** 权限错误 */
+	PERMISSION_ERROR: 'uni-id-permission-error',
+	/** 系统错误 */
+	SYSTEM_ERROR: 'uni-id-system-error',
+	/** 设置邀请码失败 */
+	SET_INVITE_CODE_FAILED: 'uni-id-set-invite-code-failed',
+	/** 邀请码不可用 */
+	INVALID_INVITE_CODE: 'uni-id-invalid-invite-code',
+	/** 禁止修改邀请人 */
+	CHANGE_INVITER_FORBIDDEN: 'uni-id-change-inviter-forbidden',
+	/** 此账号(微信、QQ、手机号等)已被绑定 */
+	BIND_CONFLICT: 'uni-id-bind-conflict',
+
+}

+ 42 - 0
js_sdk/uni-admin/error.js

@@ -0,0 +1,42 @@
+import store from '@/store'
+import config from '@/admin.config.js'
+
+// #ifndef VUE3
+export function initError(Vue) {
+     const debugOptions = config.navBar.debug
+     if (debugOptions && debugOptions.enable === true) {
+        const oldErrorHandler = Vue.config.errorHandler
+        Vue.config.errorHandler = function errorHandler(err, vm, info) {
+			console.error(err)
+             const route = vm.$page && vm.$page.route
+             store.dispatch('error/add', {
+                 err: err.toString(),
+				 info,
+                 route,
+                 time: new Date().toLocaleTimeString()
+             })
+            return oldErrorHandler(err, vm, info)
+         }
+     }
+}
+// #endif
+
+// #ifdef VUE3
+export function initError(app) {
+    const debugOptions = config.navBar.debug
+    if (debugOptions && debugOptions.enable === true) {
+        const oldErrorHandler = app.config.errorHandler
+        app.config.errorHandler = function errorHandler(err, vm, info) {
+			console.error(err)
+            const route = vm.$page && vm.$page.route
+            store.dispatch('error/add', {
+                err: err.toString(),
+                info,
+                route,
+                time: new Date().toLocaleTimeString()
+            })
+            return oldErrorHandler && oldErrorHandler(err, vm, info)
+        }
+    }
+}
+// #endif

+ 23 - 0
js_sdk/uni-admin/fetchMock.js

@@ -0,0 +1,23 @@
+function fetchMock(url) {
+	// return fetch(url)
+	// 	.then(response => response.json())
+	// 	.then(res => {
+	// 		return Promise.resolve(res)
+	// 	}).catch(err => {
+	// 		return Promise.resolve([])
+	// 	})
+
+	return Promise.resolve([])
+}
+
+// #ifndef VUE3
+export function initFetch(Vue) {
+	Vue.prototype.$fetch = fetchMock
+}
+// #endif
+
+// #ifdef VUE3
+export function initFetch(app) {
+	app.config.globalProperties.$fetch = fetchMock
+}
+// #endif

+ 15 - 0
js_sdk/uni-admin/interceptor.js

@@ -0,0 +1,15 @@
+import config from '@/admin.config.js'
+
+export function initInterceptor() {
+    uni.addInterceptor('navigateTo', {
+        fail: ({
+            errMsg
+        }) => {
+            if (errMsg.indexOf('is not found') !== -1) { // 404
+                uni.navigateTo({
+                    url: config.error.url + '?errMsg=' + errMsg
+                })
+            }
+        }
+    })
+}

+ 27 - 0
js_sdk/uni-admin/permission.js

@@ -0,0 +1,27 @@
+// #ifndef VUE3
+export function initPermission(Vue) {
+	Vue.prototype.$hasPermission = function hasPermission(name) {
+		const permission = this.$store.state.user.userInfo.permission || []
+		const role = this.$store.state.user.userInfo.role || []
+		return role.indexOf('admin') > -1 || permission.indexOf(name) > -1
+	}
+	Vue.prototype.$hasRole = function hasRole(name) {
+		const role = this.$store.state.user.userInfo.role || []
+		return role.indexOf(name) > -1
+	}
+}
+// #endif
+
+// #ifdef VUE3
+export function initPermission(app) {
+	app.config.globalProperties.$hasPermission = function hasPermission(name) {
+		const permission = this.$store.state.user.userInfo.permission || []
+		const role = this.$store.state.user.userInfo.role || []
+		return role.indexOf('admin') > -1 || permission.indexOf(name) > -1
+	}
+	app.config.globalProperties.$hasRole = function hasRole(name) {
+		const role = this.$store.state.user.userInfo.role || []
+		return role.indexOf(name) > -1
+	}
+}
+// #endif

+ 28 - 0
js_sdk/uni-admin/plugin.js

@@ -0,0 +1,28 @@
+import {
+    initUtil
+} from './util.js'
+import {
+    initError
+} from './error.js'
+import {
+    initRequest
+} from './request.js'
+import {
+    initFetch
+} from './fetchMock.js'
+import {
+    initPermission
+} from './permission.js'
+import {
+    initInterceptor
+} from './interceptor.js'
+export default {
+    install(Vue) {
+        initUtil(Vue)
+        initError(Vue)
+        initRequest(Vue)
+		initFetch(Vue)
+        initPermission(Vue)
+        initInterceptor()
+    }
+}

+ 76 - 0
js_sdk/uni-admin/request.js

@@ -0,0 +1,76 @@
+import store from '@/store/index.js'
+import config from '@/admin.config.js'
+const debugOptions = config.navBar.debug
+
+const db = uniCloud.database()
+
+export function request (action, params, options) {
+	const {objectName, functionName, showModal, ...objectOptions} = Object.assign({
+		objectName: 'uni-id-co',
+		functionName: '',
+		showModal: false,
+		
+		customUI: true,
+		loadingOptions: {
+			title: 'xxx'
+		},
+	}, options)
+
+	// 兼容 云函数 与 云对象 请求,默认为云对象
+	let call
+	if (functionName) {
+		call = uniCloud.callFunction({
+			name: functionName,
+			data: {
+				action,
+				params
+			}
+		})
+	} else {
+		const uniCloudObject = uniCloud.importObject(objectName, objectOptions)
+		call = uniCloudObject[action](params)
+	}
+
+	return call.then(result => {
+		if (!result) {
+			return Promise.resolve(result)
+		}
+
+		if (result.errCode) {
+			return Promise.reject(result)
+		}
+
+		return Promise.resolve(result)
+
+	}).catch(err => {
+		showModal && uni.showModal({
+			content: err.errMsg || '请求服务失败',
+			showCancel: false
+		})
+		// #ifdef H5
+		const noDebugPages = ['/uni_modules/uni-id-pages/pages/login/login-withpwd', '/uni_modules/uni-id-pages/pages/register/register']
+		const path = location.hash.split('#')[1]
+		if (debugOptions && debugOptions.enable === true && noDebugPages.indexOf(path) === -1) {
+			store.dispatch('error/add', {
+				err: err.toString(),
+				info: '$request("' + action + '")',
+				route: '',
+				time: new Date().toLocaleTimeString()
+			})
+		}
+		// #endif
+		return Promise.reject(err)
+	})
+}
+
+// #ifndef VUE3
+export function initRequest(Vue) {
+	Vue.prototype.$request = request
+}
+// #endif
+
+// #ifdef VUE3
+export function initRequest(app) {
+	app.config.globalProperties.$request = request
+}
+// #endif

+ 38 - 0
js_sdk/uni-admin/util.js

@@ -0,0 +1,38 @@
+import {
+	formatDate
+} from '@/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js'
+
+function formatBytes(bytes) {
+	const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
+	if (bytes == 0) {
+		return 'n/a'
+	}
+	const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
+	if (i == 0) {
+		return bytes + ' ' + sizes[i]
+	}
+	return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]
+}
+
+// #ifndef VUE3
+export function initUtil(Vue) {
+	Vue.prototype.$formatDate = formatDate
+	Vue.prototype.$formatBytes = formatBytes
+}
+// #endif
+
+// #ifdef VUE3
+export function initUtil(app) {
+	app.config.globalProperties.$formatDate = formatDate
+	app.config.globalProperties.$formatBytes = formatBytes
+}
+// #endif
+
+export function getDeviceUUID() {
+	let deviceId = uni.getStorageSync('uni_deviceId') ||
+		uni.getSystemInfoSync().deviceId ||
+		uni.getSystemInfoSync().system + '_' + Math.random().toString(36).substr(2);
+
+	uni.setStorageSync('uni_deviceId', deviceId)
+	return deviceId;
+}

+ 394 - 0
js_sdk/uni-stat/util.js

@@ -0,0 +1,394 @@
+/**
+ *  以下为 uni-stat 的工具方法
+ */
+
+// 将查询条件拼接为字符串
+function stringifyQuery(query, dimension = false, delArrs = []) {
+	const queryArr = []
+	const keys = Object.keys(query)
+	const time = query.start_time
+	keys.forEach(key => {
+		if (key === 'time_range' || delArrs.indexOf(key) !== -1) return
+		let val = query[key]
+		if (val) {
+			if (typeof val === 'string' && val.indexOf(key) > -1) {
+				queryArr.push(val)
+			} else {
+				if (typeof val === 'string') {
+					val = `"${val}"`
+				}
+				if (Array.isArray(val)) {
+					if (val.length === 2 && key.indexOf('time') > -1) {
+						queryArr.push(`${key} >= ${val[0]} && ${key} <= ${val[1]}`)
+					} else {
+						val = val.map(item => `${key} == "${item}"`).join(' || ')
+						val && queryArr.push(`(${val})`)
+					}
+				} else if (dimension && key === 'dimension') {
+					if (maxDeltaDay(time)) {
+						queryArr.push(`dimension == "hour"`)
+					} else {
+						if (val && val !== `"hour"`) {
+							queryArr.push(`${key} == ${val}`)
+						} else {
+							queryArr.push(`dimension == "day"`)
+						}
+					}
+				} else {
+					queryArr.push(`${key} == ${val}`)
+				}
+			}
+		}
+	})
+	const queryStr = queryArr.join(' && ')
+	return queryStr || {}
+}
+
+// 根据页面字段配置 fieldsMap 数据计算、格式化字段
+function mapfields(map, data = {}, goal, prefix = '', prop = 'value') {
+	const goals = [],
+		argsGoal = goal
+	map = JSON.parse(JSON.stringify(map))
+	const origin = JSON.parse(JSON.stringify(data))
+	for (const mapper of map) {
+		let {
+			field,
+			computed,
+			formatter,
+			disable,
+			fix
+		} = mapper
+		if (!disable) {
+			goal = argsGoal || mapper
+			const hasValue = goal.hasOwnProperty(prop)
+			const preField = prefix + field
+			if (data) {
+				const value = data[preField]
+				if (computed) {
+					const computedFields = computed.split('/')
+					let [dividend, divisor] = computedFields
+					dividend = Number(origin[prefix + dividend])
+					divisor = Number(origin[prefix + divisor])
+					const val = format(division(dividend, divisor), formatter, fix)
+					if (hasValue && field === goal.field) {
+						goal[prop] = val
+					} else {
+						goal[field] = val
+					}
+				} else {
+					if (value) {
+						const val = format(value, formatter, fix)
+						if (hasValue) {
+							if (goal.field === field) {
+								goal[prop] = val
+							}
+						} else {
+							goal[field] = val
+						}
+					}
+				}
+			}
+			if (hasValue) {
+				goals.push(goal)
+			}
+		}
+	}
+	return goals
+}
+
+// 将查询条件对象拼接为字符串,给 client db 的 field 属性消费
+function stringifyField(mapping, goal, prop) {
+	if (goal) {
+		mapping = mapping.filter(f => f.field === goal)
+	}
+	if (prop) {
+		mapping = mapping.filter(f => f.field && f.hasOwnProperty(prop))
+	}
+	const fieldString = mapping.map(f => {
+		let fields = []
+		if (f.computed) {
+			fields = f.computed.split('/')
+		} else {
+			fields.push(f.field)
+		}
+		fields = fields.map(field => {
+			if (f.stat === -1) {
+				return field
+			} else {
+				return `${field} as ${ 'temp_' + field}`
+			}
+		})
+		return fields.join()
+	})
+	return fieldString.join()
+}
+
+// 将查询条件对象拼接为字符串,给 client db 的 groupField 属性消费
+function stringifyGroupField(mapping, goal, prop) {
+	if (goal) {
+		mapping = mapping.filter(f => f.field === goal)
+	}
+	if (prop) {
+		mapping = mapping.filter(f => f.field && f.hasOwnProperty(prop))
+	}
+	const groupField = mapping.map(f => {
+			const stat = f.stat
+			let fields = []
+			if (f.computed) {
+				fields = f.computed.split('/')
+			} else {
+				fields.push(f.field)
+			}
+			fields = fields.map(field => {
+				if (stat !== -1) {
+					return `${stat ? stat : 'sum' }(${'temp_' + field}) as ${field}`
+				}
+			})
+			return fields.filter(Boolean).join()
+		})
+		.filter(Boolean)
+		.join()
+
+	return groupField
+}
+
+// 除法函数
+function division(dividend, divisor) {
+	if (divisor) {
+		return dividend / divisor
+	} else {
+		return 0
+	}
+}
+
+// 对数字进行格式化,格式 type 配置在页面 fieldMap.js 中
+function format(num, type = ',', fix) {
+	// if (!type) return num
+	if (typeof num !== 'number') return num
+	if (type === '%') {
+		// 注意浮点数精度
+		num = (num * 100)
+		if (String(num).indexOf('.') > -1) {
+			num = num.toFixed(2)
+		}
+		num = num ? num + type : num
+		return num
+	} else if (type === '%%') {
+		num = Number(num)
+		return num.toFixed(2) + '%'
+	} else if (type === '-') {
+		return formatDate(num, 'day')
+	} else if (type === ':') {
+		num = Math.ceil(num)
+		let h, m, s
+		h = m = s = 0
+		const wunH = 60 * 60,
+			wunM = 60 // 单位秒, wun 通 one
+		if (num >= wunH) {
+			h = Math.floor(num / wunH)
+			const remainder = num % wunH
+			if (remainder >= wunM) {
+				m = Math.floor(remainder / wunM)
+				s = remainder % wunM
+			} else {
+				s = remainder
+			}
+		} else if (wunH >= num && num >= wunM) {
+			m = Math.floor(num / wunM)
+			s = num % wunM
+		} else {
+			s = num
+		}
+		const hms = [h, m, s].map(i => i < 10 ? '0' + i : i)
+		return hms.join(type)
+	} else if (type === ',') {
+		return num.toLocaleString()
+	} else {
+		if (String(num).indexOf('.') > -1) {
+			if (Math.abs(num) > 1) {
+				num = num.toFixed(fix || 0)
+			} else {
+				num = num.toFixed(fix || 2)
+			}
+		}
+		return num
+	}
+}
+
+// 格式化日期,返回其所在的范围
+function formatDate(date, type) {
+	let d = new Date(date)
+	if (type === 'hour') {
+		let h = d.getHours()
+		h = h < 10 ? '0' + h : h
+		return `${h}:00 ~ ${h}:59`
+	} else if (type === 'week') {
+		const first = d.getDate() - d.getDay() + 1; // First day is the day of the month - the day of the week
+		const last = first + 6; // last day is the first day + 6
+		let firstday = new Date(d.setDate(first));
+		firstday = parseDateTime(firstday)
+		let lastday = new Date(d.setDate(last));
+		lastday = parseDateTime(lastday)
+		return `${firstday} ~ ${lastday}`
+	} else if (type === 'month') {
+		let firstday = new Date(d.getFullYear(), d.getMonth(), 1);
+		firstday = parseDateTime(firstday)
+		let lastday = new Date(d.getFullYear(), d.getMonth() + 1, 0);
+		lastday = parseDateTime(lastday)
+		return `${firstday} ~ ${lastday}`
+	} else {
+		return parseDateTime(d)
+	}
+}
+
+// 格式化日期,返回其 yyyy-mm-dd 格式
+function parseDateTime(datetime, type, splitor = '-') {
+	let d = datetime
+	if (typeof d !== 'object') {
+		d = new Date(d)
+	}
+	const year = d.getFullYear()
+	const month = d.getMonth() + 1
+	const day = d.getDate()
+	const hour = d.getHours()
+	const minute = d.getMinutes()
+	const second = d.getSeconds()
+	const date = [year, lessTen(month), lessTen(day)].join(splitor)
+	const time = [lessTen(hour), lessTen(minute), lessTen(second)].join(':')
+	if (type === "dateTime") {
+		return date + ' ' + time
+	}
+	return date
+}
+
+function lessTen(item) {
+	return item < 10 ? '0' + item : item
+}
+
+// 获取指定日期当天或 n 天前零点的时间戳,丢弃时分秒
+function getTimeOfSomeDayAgo(days = 0, date = Date.now()) {
+	const d = new Date(date)
+	const oneDayTime = 24 * 60 * 60 * 1000
+	let ymd = [d.getFullYear(), d.getMonth() + 1, d.getDate()].join('/')
+	ymd = ymd + ' 00:00:00'
+	const someDaysAgoTime = new Date(ymd).getTime() - oneDayTime * days
+	return someDaysAgoTime
+}
+
+// 判断时间差值 delta,单位为天
+function maxDeltaDay(times, delta = 2) {
+	if (!times.length) return true
+	const wunDay = 24 * 60 * 60 * 1000
+	const [start, end] = times
+	const max = end - start < wunDay * delta
+	return max
+}
+
+// 查询 总设备数、总用户数, 通过 field 配置
+function getFieldTotal(query = this.query, field = "total_devices") {
+	let fieldTotal
+	if (typeof query === 'object') {
+		query = stringifyQuery(query, false, ['uni_platform'])
+	}
+	const db = uniCloud.database()
+	return db.collection('uni-stat-result')
+		.where(query)
+		.field(`${field} as temp_${field}, start_time`)
+		.groupBy('start_time')
+		.groupField(`sum(temp_${field}) as ${field}`)
+		.orderBy('start_time', 'desc')
+		.get()
+		.then(cur => {
+			const data = cur.result.data
+			fieldTotal = data.length && data[0][field]
+			fieldTotal = format(fieldTotal)
+			this.panelData && this.panelData.forEach(item => {
+				if (item.field === field) {
+					item.value = fieldTotal
+				}
+			})
+			return Promise.resolve(fieldTotal)
+		})
+}
+
+// 防抖函数
+function debounce(fn, time = 100) {
+	let timer = null
+	return function(...args) {
+		if (timer) clearTimeout(timer)
+		timer = setTimeout(() => {
+			fn.apply(this, args)
+		}, time)
+	}
+}
+
+
+const files = {}
+
+function fileToUrl(file) {
+	for (const key in files) {
+		if (files.hasOwnProperty(key)) {
+			const oldFile = files[key]
+			if (oldFile === file) {
+				return key
+			}
+		}
+	}
+	var url = (window.URL || window.webkitURL).createObjectURL(file)
+	files[url] = file
+	return url
+}
+/**
+ * 获取两个时间戳之间的所有时间
+ * let start = new Date(1642694400000) // 2022-01-21 00:00:00
+ * let end = new Date(1643644800000) // 2022-02-01 00:00:00
+ * dateList = getAllDateCN(date1, date2)
+ * @param {*} startTime
+ * @param {*} endTime
+ */
+function getAllDateCN(startTime, endTime) {
+	let date_all = [];
+	let i = 0;
+	while (endTime.getTime() - startTime.getTime() >= 0) {
+		// 获取日期和时间
+		// let year = startTime.getFullYear()
+		// let month = startTime.getMonth() + 1
+		// let day = startTime.getDate()
+		// let time = startTime.toLocaleTimeString()
+		date_all[i] = startTime.getTime()
+
+		// 获取每天00:00:00的时间戳
+		// date_all[i] = new Date(startTime.toLocaleDateString()).getTime() / 1000;
+
+		// 天数+1
+		startTime.setDate(startTime.getDate() + 1);
+		i += 1;
+	}
+	return date_all;
+}
+
+function createUniStatQuery(object) {
+	return Object.assign({}, object, {
+		type: "native_app",
+		create_env: "uni-stat"
+	})
+}
+
+
+export {
+	stringifyQuery,
+	stringifyField,
+	stringifyGroupField,
+	mapfields,
+	getTimeOfSomeDayAgo,
+	division,
+	format,
+	formatDate,
+	parseDateTime,
+	maxDeltaDay,
+	debounce,
+	fileToUrl,
+	getFieldTotal,
+	getAllDateCN,
+	createUniStatQuery
+}

+ 117 - 0
js_sdk/validator/cyt-logs.js

@@ -0,0 +1,117 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+const validator = {
+  "app_status": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "user_name": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "user_brand": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "user_model": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "api_src": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "request_parameters": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "response_parameters": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "api_status": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "error_info": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  }
+}
+
+const enumConverter = {}
+
+function filterToWhere(filter, command) {
+  let where = {}
+  for (let field in filter) {
+    let { type, value } = filter[field]
+    switch (type) {
+      case "search":
+        if (typeof value === 'string' && value.length) {
+          where[field] = new RegExp(value)
+        }
+        break;
+      case "select":
+        if (value.length) {
+          let selectValue = []
+          for (let s of value) {
+            selectValue.push(command.eq(s))
+          }
+          where[field] = command.or(selectValue)
+        }
+        break;
+      case "range":
+        if (value.length) {
+          let gt = value[0]
+          let lt = value[1]
+          where[field] = command.and([command.gte(gt), command.lte(lt)])
+        }
+        break;
+      case "date":
+        if (value.length) {
+          let [s, e] = value
+          let startDate = new Date(s)
+          let endDate = new Date(e)
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+      case "timestamp":
+        if (value.length) {
+          let [startDate, endDate] = value
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+    }
+  }
+  return where
+}
+
+export { validator, enumConverter, filterToWhere }

+ 38 - 0
js_sdk/validator/opendb-admin-log.js

@@ -0,0 +1,38 @@
+// 校验规则由 schema 生成,请不要直接修改当前文件,如果需要请在uniCloud控制台修改schema
+// uniCloud: https://unicloud.dcloud.net.cn/
+
+
+
+export default {
+  "user_name": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "content": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "ip": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "create_date": {
+    "rules": [
+      {
+        "format": "timestamp"
+      }
+    ]
+  }
+}

+ 69 - 0
js_sdk/validator/opendb-admin-menus.js

@@ -0,0 +1,69 @@
+// 校验规则由 schema 生成,请不要直接修改当前文件,如果需要请在uniCloud控制台修改schema
+// uniCloud: https://unicloud.dcloud.net.cn/
+
+
+
+export default {
+  "menu_id": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "name": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "icon": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "url": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "sort": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "parent_id": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "permission": {
+    "rules": [
+      {
+        "format": "array"
+      }
+    ]
+  },
+  "enable": {
+    "rules": [
+      {
+        "format": "bool"
+      }
+    ]
+  }
+}

+ 123 - 0
js_sdk/validator/opendb-app-list.js

@@ -0,0 +1,123 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+const validator = {
+	"appid": {
+		"rules": [{
+				"required": true
+			},
+			{
+				"format": "string"
+			}
+		],
+		"label": "AppID"
+	},
+	"name": {
+		"rules": [{
+				"required": true
+			},
+			{
+				"format": "string"
+			}
+		],
+		"label": "应用名称"
+	},
+	"icon_url": {
+		"rules": [{
+			"format": "string"
+		}],
+		"label": "应用图标"
+	},
+	"introduction": {
+		"rules": [{
+			"format": "string"
+		}],
+		"label": "应用简介"
+	},
+	"description": {
+		"rules": [{
+			"format": "string"
+		}],
+		"label": "应用描述"
+	},
+	"screenshot": {
+		"rules": [{
+			"format": "array"
+		}],
+		"label": "应用截图"
+	},
+	"create_date": {
+		"rules": [{
+			"format": "timestamp"
+		}],
+		"label": "发行时间"
+	}
+}
+
+function filterToWhere(filter, command) {
+	let where = {}
+	for (let field in filter) {
+		let {
+			type,
+			value
+		} = filter[field]
+		switch (type) {
+			case "search":
+				if (typeof value === 'string' && value.length) {
+					where[field] = new RegExp(value)
+				}
+				break;
+			case "select":
+				if (value.length) {
+					let selectValue = []
+					for (let s of value) {
+						selectValue.push(command.eq(s))
+					}
+					where[field] = command.or(selectValue)
+				}
+				break;
+			case "range":
+				if (value.length) {
+					let gt = value[0]
+					let lt = value[1]
+					where[field] = command.and([command.gte(gt), command.lte(lt)])
+				}
+				break;
+			case "date":
+				if (value.length) {
+					let [s, e] = value
+					let startDate = new Date(s)
+					let endDate = new Date(e)
+					where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+				}
+				break;
+			case "timestamp":
+				if (value.length) {
+					let [startDate, endDate] = value
+					where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+				}
+				break;
+		}
+	}
+	return where
+}
+const enumConverter = {}
+const mpPlatform = {
+	'mp_weixin': '微信小程序',
+	'mp_alipay': '支付宝小程序',
+	'mp_baidu': '百度小程序',
+	'mp_toutiao': '字节小程序',
+	'mp_qq': 'QQ小程序',
+	'mp_dingtalk': '钉钉小程序',
+	'mp_kuaishou': '快手小程序',
+	'mp_lark': '飞书小程序',
+	'mp_jd': '京东小程序',
+	'quickapp': '快应用'
+}
+
+export {
+	enumConverter,
+	validator,
+	filterToWhere,
+	mpPlatform
+}

+ 157 - 0
js_sdk/validator/opendb-app-versions.js

@@ -0,0 +1,157 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+
+const validator = {
+	"appid": {
+		"rules": [{
+				"required": true
+			},
+			{
+				"format": "string"
+			}
+		],
+		"label": "AppID"
+	},
+	"name": {
+		"rules": [{
+			"format": "string"
+		}],
+		"label": "应用名称"
+	},
+	"title": {
+		"rules": [{
+			"format": "string"
+		}],
+		"label": "更新标题"
+	},
+	"contents": {
+		"rules": [{
+				"required": true
+			},
+			{
+				"format": "string"
+			}
+		],
+		"label": "更新内容"
+	},
+	"platform": {
+		"rules": [{
+				"required": true
+			},
+			/* 此处不校验数据类型,因为platform在发布app端是单选,在发布wgt时可能是多选
+			{
+				"format": "array"
+			}, */
+			{
+				"range": [{
+						"value": "Android",
+						"text": "安卓"
+					},
+					{
+						"value": "iOS",
+						"text": "苹果"
+					}
+				]
+			}
+		],
+		"label": "平台"
+	},
+	"type": {
+		"rules": [{
+				"required": true
+			}, {
+				"format": "string"
+			},
+			{
+				"range": [{
+						"value": "native_app",
+						"text": "原生App安装包"
+					},
+					{
+						"value": "wgt",
+						"text": "wgt资源包"
+					}
+				]
+			}
+		],
+		"label": "安装包类型"
+	},
+	"version": {
+		"rules": [{
+				"required": true
+			},
+			{
+				"format": "string"
+			}
+		],
+		"label": "版本号"
+	},
+	"min_uni_version": {
+		"rules": [{
+			"format": "string"
+		}],
+		"label": "原生App最低版本"
+	},
+	"url": {
+		"rules": [{
+			"required": true
+		}, {
+			"format": "string"
+		}],
+		"label": "链接"
+	},
+	"stable_publish": {
+		"rules": [{
+			"format": "bool"
+		}],
+		"label": "上线发行"
+	},
+	"create_date": {
+		"rules": [{
+			"format": "timestamp"
+		}],
+		"label": "上传时间"
+	},
+	"is_silently": {
+		"rules": [{
+			"format": "bool"
+		}],
+		"label": "静默更新",
+		"defaultValue": false
+	},
+	"is_mandatory": {
+		"rules": [{
+			"format": "bool"
+		}],
+		"label": "强制更新",
+		"defaultValue": false
+	},
+	"store_list": {
+		"rules": [{
+			"format": "array"
+		}],
+		"label": "应用市场"
+	},
+}
+
+const enumConverter = {
+	"platform_valuetotext": [{
+			"value": "Android",
+			"text": "安卓"
+		},
+		{
+			"value": "iOS",
+			"text": "苹果"
+		}
+	],
+	"type_valuetotext": {
+		"native_app": "原生App安装包",
+		"wgt": "wgt资源包"
+	}
+}
+
+export {
+	validator,
+	enumConverter
+}

+ 38 - 0
js_sdk/validator/uni-id-log.js

@@ -0,0 +1,38 @@
+// 校验规则由 schema 生成,请不要直接修改当前文件,如果需要请在uniCloud控制台修改schema
+// uniCloud: https://unicloud.dcloud.net.cn/
+
+
+
+export default {
+  "user_name": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "content": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "ip": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "create_date": {
+    "rules": [
+      {
+        "format": "timestamp"
+      }
+    ]
+  }
+}

+ 84 - 0
js_sdk/validator/uni-id-permissions.js

@@ -0,0 +1,84 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+const validator = {
+  "permission_id": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ],
+    "label": "权限标识"
+  },
+  "permission_name": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ],
+    "label": "权限名称"
+  },
+  "comment": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ],
+    "label": "备注"
+  }
+}
+
+const enumConverter = {}
+
+function filterToWhere(filter, command) {
+  let where = {}
+  for (let field in filter) {
+    let { type, value } = filter[field]
+    switch (type) {
+      case "search":
+        if (typeof value === 'string' && value.length) {
+          where[field] = new RegExp(value)
+        }
+        break;
+      case "select":
+        if (value.length) {
+          let selectValue = []
+          for (let s of value) {
+            selectValue.push(command.eq(s))
+          }
+          where[field] = command.or(selectValue)
+        }
+        break;
+      case "range":
+        if (value.length) {
+          let gt = value[0]
+          let lt = value[1]
+          where[field] = command.and([command.gte(gt), command.lte(lt)])
+        }
+        break;
+      case "date":
+        if (value.length) {
+          let [s, e] = value
+          let startDate = new Date(s)
+          let endDate = new Date(e)
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+      case "timestamp":
+        if (value.length) {
+          let [startDate, endDate] = value
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+    }
+  }
+  return where
+}
+
+export { validator, enumConverter, filterToWhere }

+ 99 - 0
js_sdk/validator/uni-id-roles.js

@@ -0,0 +1,99 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+const validator = {
+  "role_id": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ],
+    "label": "唯一ID"
+  },
+  "role_name": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ],
+    "label": "名称"
+  },
+  "permission": {
+    "rules": [
+      {
+        "format": "array"
+      }
+    ],
+    "label": "权限"
+  },
+  "comment": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ],
+    "label": "备注"
+  },
+  "create_date": {
+    "rules": [
+      {
+        "format": "timestamp"
+      }
+    ]
+  }
+}
+
+const enumConverter = {}
+
+function filterToWhere(filter, command) {
+  let where = {}
+  for (let field in filter) {
+    let { type, value } = filter[field]
+    switch (type) {
+      case "search":
+        if (typeof value === 'string' && value.length) {
+          where[field] = new RegExp(value)
+        }
+        break;
+      case "select":
+        if (value.length) {
+          let selectValue = []
+          for (let s of value) {
+            selectValue.push(command.eq(s))
+          }
+          where[field] = command.or(selectValue)
+        }
+        break;
+      case "range":
+        if (value.length) {
+          let gt = value[0]
+          let lt = value[1]
+          where[field] = command.and([command.gte(gt), command.lte(lt)])
+        }
+        break;
+      case "date":
+        if (value.length) {
+          let [s, e] = value
+          let startDate = new Date(s)
+          let endDate = new Date(e)
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+      case "timestamp":
+        if (value.length) {
+          let [startDate, endDate] = value
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+    }
+  }
+  return where
+}
+
+export { validator, enumConverter, filterToWhere }

+ 84 - 0
js_sdk/validator/uni-id-tag.js

@@ -0,0 +1,84 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+const validator = {
+  "tagid": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ],
+    "label": "标签的tagid"
+  },
+  "name": {
+    "rules": [
+      {
+        "required": true
+      },
+      {
+        "format": "string"
+      }
+    ],
+    "label": "标签名称"
+  },
+  "description": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ],
+    "label": "标签描述"
+  }
+}
+
+const enumConverter = {}
+
+function filterToWhere(filter, command) {
+  let where = {}
+  for (let field in filter) {
+    let { type, value } = filter[field]
+    switch (type) {
+      case "search":
+        if (typeof value === 'string' && value.length) {
+          where[field] = new RegExp(value)
+        }
+        break;
+      case "select":
+        if (value.length) {
+          let selectValue = []
+          for (let s of value) {
+            selectValue.push(command.eq(s))
+          }
+          where[field] = command.or(selectValue)
+        }
+        break;
+      case "range":
+        if (value.length) {
+          let gt = value[0]
+          let lt = value[1]
+          where[field] = command.and([command.gte(gt), command.lte(lt)])
+        }
+        break;
+      case "date":
+        if (value.length) {
+          let [s, e] = value
+          let startDate = new Date(s)
+          let endDate = new Date(e)
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+      case "timestamp":
+        if (value.length) {
+          let [startDate, endDate] = value
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+    }
+  }
+  return where
+}
+
+export { validator, enumConverter, filterToWhere }

+ 152 - 0
js_sdk/validator/uni-id-users.js

@@ -0,0 +1,152 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+const validator = {
+	"username": {
+		"rules": [{
+				"required": true,
+			},
+			{
+				"format": "string"
+			},
+			{
+				"minLength": 2
+			}
+		],
+		"label": "用户名"
+	},
+	"password": {
+		"rules": [{
+				"required": true,
+			},
+			{
+				"format": "password"
+			},
+			{
+				"minLength": 6
+			}
+		],
+		"label": "密码"
+	},
+	"mobile": {
+		"rules": [{
+				"format": "string"
+			},
+			{
+				"pattern": "^\\+?[0-9-]{3,20}$"
+			}
+		],
+		"label": "手机号码"
+	},
+	"status": {
+		"rules": [{
+				"format": "int"
+			},
+			{
+				"range": [{
+						"text": "正常",
+						"value": 0
+					},
+					{
+						"text": "禁用",
+						"value": 1
+					},
+					{
+						"text": "审核中",
+						"value": 2
+					},
+					{
+						"text": "审核拒绝",
+						"value": 3
+					}
+				]
+			}
+		],
+		"defaultValue": 0,
+		"label": "用户状态"
+	},
+	"email": {
+		"rules": [{
+				"format": "string"
+			},
+			{
+				"format": "email"
+			}
+		],
+		"label": "邮箱"
+	},
+	"role": {
+		"rules": [{
+			"format": "array"
+		}],
+		"label": "角色"
+	},
+	"last_login_date": {
+		"rules": [{
+			"format": "timestamp"
+		}]
+	}
+}
+
+const enumConverter = {
+	"status_valuetotext": {
+		"0": "正常",
+		"1": "禁用",
+		"2": "审核中",
+		"3": "审核拒绝"
+	}
+}
+
+function filterToWhere(filter, command) {
+	let where = {}
+	for (let field in filter) {
+		let {
+			type,
+			value
+		} = filter[field]
+		switch (type) {
+			case "search":
+				if (typeof value === 'string' && value.length) {
+					where[field] = new RegExp(value)
+				}
+				break;
+			case "select":
+				if (value.length) {
+					let selectValue = []
+					for (let s of value) {
+						selectValue.push(command.eq(s))
+					}
+					where[field] = command.or(selectValue)
+				}
+				break;
+			case "range":
+				if (value.length) {
+					let gt = value[0]
+					let lt = value[1]
+					where[field] = command.and([command.gte(gt), command.lte(lt)])
+				}
+				break;
+			case "date":
+				if (value.length) {
+					let [s, e] = value
+					let startDate = new Date(s)
+					let endDate = new Date(e)
+					where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+				}
+				break;
+			case "timestamp":
+				if (value.length) {
+					let [startDate, endDate] = value
+					where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+				}
+				break;
+		}
+	}
+	return where
+}
+
+export {
+	validator,
+	enumConverter,
+	filterToWhere
+}

+ 264 - 0
js_sdk/validator/uni-stat-app-crash-logs.js

@@ -0,0 +1,264 @@
+// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
+
+
+const validator = {
+  "appid": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "version": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "platform": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "channel": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "sdk_version": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_id": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_net": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_os": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_os_version": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_vendor": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_model": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_is_root": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "device_os_name": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_batt_level": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "device_batt_temp": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "device_memory_use_size": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "device_memory_total_size": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "device_disk_use_size": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "device_disk_total_size": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "device_abis": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "app_count": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "app_use_memory_size": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "app_webview_count": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "app_use_duration": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "app_run_fore": {
+    "rules": [
+      {
+        "format": "int"
+      }
+    ]
+  },
+  "package_name": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "package_version": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "page_url": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "error_msg": {
+    "rules": [
+      {
+        "format": "string"
+      }
+    ]
+  },
+  "create_time": {
+    "rules": [
+      {
+        "format": "timestamp"
+      }
+    ]
+  }
+}
+
+const enumConverter = {}
+
+function filterToWhere(filter, command) {
+  let where = {}
+  for (let field in filter) {
+    let { type, value } = filter[field]
+    switch (type) {
+      case "search":
+        if (typeof value === 'string' && value.length) {
+          where[field] = new RegExp(value)
+        }
+        break;
+      case "select":
+        if (value.length) {
+          let selectValue = []
+          for (let s of value) {
+            selectValue.push(command.eq(s))
+          }
+          where[field] = command.or(selectValue)
+        }
+        break;
+      case "range":
+        if (value.length) {
+          let gt = value[0]
+          let lt = value[1]
+          where[field] = command.and([command.gte(gt), command.lte(lt)])
+        }
+        break;
+      case "date":
+        if (value.length) {
+          let [s, e] = value
+          let startDate = new Date(s)
+          let endDate = new Date(e)
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+      case "timestamp":
+        if (value.length) {
+          let [startDate, endDate] = value
+          where[field] = command.and([command.gte(startDate), command.lte(endDate)])
+        }
+        break;
+    }
+  }
+  return where
+}
+
+export { validator, enumConverter, filterToWhere }

BIN=BIN
log.zip


+ 43 - 0
main.js

@@ -0,0 +1,43 @@
+import App from './App'
+import store from './store'
+import plugin from './js_sdk/uni-admin/plugin'
+import messages from './i18n/index.js'
+
+const lang = uni.getLocale()
+// #ifndef VUE3
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+Vue.config.productionTip = false
+Vue.use(VueI18n)
+// 通过选项创建 VueI18n 实例
+const i18n = new VueI18n({
+  locale: lang, // 设置地区
+  messages, // 设置地区信息
+})
+Vue.use(plugin)
+App.mpType = 'app'
+const app = new Vue({
+  i18n,
+  store,
+  ...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+import { createI18n } from 'vue-i18n'
+export function createApp() {
+  const app = createSSRApp(App)
+  const i18n = createI18n({
+  	locale: lang,
+  	messages
+  })
+  app.use(i18n)
+  app.use(plugin)
+  app.use(store)
+  return {
+    app
+  }
+}
+// #endif

+ 82 - 0
manifest.json

@@ -0,0 +1,82 @@
+{
+    "name" : "log",
+    "appid" : "__UNI__E203941",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "h5" : {
+        "template" : "template.h5.html",
+        "router" : {
+            "mode" : "hash",
+            "base" : "/admin/"
+        }
+    },
+    "networkTimeout" : {
+        "uploadFile" : 1200000 //ms, 如果不配置,上传大文件可能会超时
+    },
+    "vueVersion" : "2"
+}

+ 14 - 0
mock/uni-stat/appOverview.json

@@ -0,0 +1,14 @@
+ {
+   "yesterday": {
+     "num_new_visitor": "394",
+     "num_visitor": "884",
+     "num_page_views": "21,818",
+     "num_total_visitor": "726,161"
+   },
+   "today": {
+     "num_new_visitor": "258",
+     "num_visitor": "564",
+     "num_page_views": "14,795",
+     "num_total_visitor": "781,700"
+   }
+ }

+ 17 - 0
mock/uni-stat/appsDetail.json

@@ -0,0 +1,17 @@
+{
+	"last_page": 1,
+	"current_page": 1,
+	"total": 1,
+	"item": [{
+		"id_app": 3,
+		"today_num_new_visitor": "245",
+		"today_num_visitor": "502",
+		"today_num_page_views": "10,777",
+		"num_total_visitor": "724,620",
+		"appid": "__UNI__HelloUniApp",
+		"name": "Hello uni-app",
+		"yesterday_num_new_visitor": "666",
+		"yesterday_num_visitor": "1,170",
+		"yesterday_num_page_views": "28,699"
+	}]
+}

+ 11 - 0
mock/uni-stat/db.js

@@ -0,0 +1,11 @@
+module.exports = function() {
+  return {
+    appsDetail: require('./appsDetail'),
+    pageRule: require('./pageRule'),
+	appOverview: require('./appOverview'),
+	pageContent: require('./pageContent'),
+	pageRes: require('./pageRes'),
+	pageEnt: require('./pageEnt'),
+	event: require('./event'),
+  }
+}

+ 87 - 0
mock/uni-stat/event.json

@@ -0,0 +1,87 @@
+{
+  "last_page": 1,
+  "current_page": 1,
+  "total": 10,
+  "item": [
+    {
+      "id_event": 3,
+      "event_key": "login",
+      "event_name": "登录事件",
+      "num_visitor": "65",
+      "num_visits": "666",
+      "visitor_avg_hits": "10.25"
+    },
+    {
+      "id_event": 30,
+      "event_key": "share",
+      "event_name": "分享",
+      "num_visitor": "18",
+      "num_visits": "164",
+      "visitor_avg_hits": "9.11"
+    },
+    {
+      "id_event": 82,
+      "event_key": "pay_fail",
+      "event_name": "支付失败",
+      "num_visitor": "11",
+      "num_visits": "145",
+      "visitor_avg_hits": "13.18"
+    },
+    {
+      "id_event": 841,
+      "event_key": "加入购物车",
+      "event_name": "",
+      "num_visitor": "13",
+      "num_visits": "74",
+      "visitor_avg_hits": "5.69"
+    },
+    {
+      "id_event": 840,
+      "event_key": "收藏",
+      "event_name": "",
+      "num_visitor": "13",
+      "num_visits": "57",
+      "visitor_avg_hits": "4.38"
+    },
+    {
+      "id_event": 842,
+      "event_key": "立即购买",
+      "event_name": "",
+      "num_visitor": "12",
+      "num_visits": "54",
+      "visitor_avg_hits": "4.50"
+    },
+    {
+      "id_event": 844,
+      "event_key": "取消收藏",
+      "event_name": "",
+      "num_visitor": "8",
+      "num_visits": "32",
+      "visitor_avg_hits": "4.00"
+    },
+    {
+      "id_event": 161,
+      "event_key": "pay_success",
+      "event_name": "支付成功",
+      "num_visitor": "1",
+      "num_visits": "11",
+      "visitor_avg_hits": "11.00"
+    },
+    {
+      "id_event": 94522,
+      "event_key": "ipa",
+      "event_name": "",
+      "num_visitor": "1",
+      "num_visits": "2",
+      "visitor_avg_hits": "2.00"
+    },
+    {
+      "id_event": 281238,
+      "event_key": "iapstatus",
+      "event_name": "",
+      "num_visitor": "1",
+      "num_visits": "2",
+      "visitor_avg_hits": "2.00"
+    }
+  ]
+}

+ 508 - 0
mock/uni-stat/pageContent.json

@@ -0,0 +1,508 @@
+{
+  "base_url": "",
+  "last_page": 4,
+  "current_page": 1,
+  "total": 199,
+  "item": [
+    {
+      "id_page": 81556056,
+      "url": "pages/forum/detail/detail?id=39355&type=2",
+      "name": "uni-app提供开箱即用的SSR支持",
+      "num_visitor": "54",
+      "num_visits": "60",
+      "visit_avg_time": "00:00:21",
+      "visitor_avg_time": "00:00:23",
+      "num_share": "0"
+    },
+    {
+      "id_page": 82631518,
+      "url": "pages/forum/detail/detail?id=133960&type=1",
+      "name": "前端老手,寻找远程工作机会,外包项目合作,个人随缘接单",
+      "num_visitor": "36",
+      "num_visits": "38",
+      "visit_avg_time": "00:00:23",
+      "visitor_avg_time": "00:00:25",
+      "num_share": "0"
+    },
+    {
+      "id_page": 1241,
+      "url": "pages/forum/detail/detail?id=35050&type=2",
+      "name": "【收费ui ¥200】uni-app前端UI框架 graceUI 持续更新(29个基础组件   14 个界面库   表单验证模块 ),大幅度提高您的开发速度!实测 真实项目30个页面 2天完成布局",
+      "num_visitor": "19",
+      "num_visits": "34",
+      "visit_avg_time": "00:00:09",
+      "visitor_avg_time": "00:00:16",
+      "num_share": "0"
+    },
+    {
+      "id_page": 65803226,
+      "url": "pages/forum/detail/detail?id=100790&type=1",
+      "name": "请问uniapp怎么获取手机内的文件",
+      "num_visitor": "14",
+      "num_visits": "14",
+      "visit_avg_time": "00:00:13",
+      "visitor_avg_time": "00:00:13",
+      "num_share": "0"
+    },
+    {
+      "id_page": 68899690,
+      "url": "pages/forum/detail/detail?id=37834&type=2",
+      "name": " uni-app 项目小程序端支持 vue3 介绍",
+      "num_visitor": "13",
+      "num_visits": "13",
+      "visit_avg_time": "00:00:26",
+      "visitor_avg_time": "00:00:26",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83432568,
+      "url": "pages/forum/detail/detail?id=135769&type=1",
+      "name": "【报Bug】字节跳动小程序使用page-meta设置背景,开发工具有报错,uni.setBackgroundColor is not a function",
+      "num_visitor": "9",
+      "num_visits": "11",
+      "visit_avg_time": "00:00:05",
+      "visitor_avg_time": "00:00:07",
+      "num_share": "0"
+    },
+    {
+      "id_page": 82778852,
+      "url": "pages/forum/detail/detail?id=134332&type=1",
+      "name": "uniapp打包后的通知授权推送",
+      "num_visitor": "11",
+      "num_visits": "11",
+      "visit_avg_time": "00:00:08",
+      "visitor_avg_time": "00:00:08",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83039358,
+      "url": "pages/forum/detail/detail?id=134922&type=1",
+      "name": "google play 上架应用因为Dcloud SDK被暂停",
+      "num_visitor": "9",
+      "num_visits": "10",
+      "visit_avg_time": "00:00:09",
+      "visitor_avg_time": "00:00:10",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83419662,
+      "url": "pages/forum/detail/detail?id=135744&type=1",
+      "name": "一键登录开通应用催审",
+      "num_visitor": "10",
+      "num_visits": "10",
+      "visit_avg_time": "00:00:03",
+      "visitor_avg_time": "00:00:03",
+      "num_share": "0"
+    },
+    {
+      "id_page": 62476061,
+      "url": "pages/forum/detail/detail?id=35915&type=2",
+      "name": "iOS平台:用Native.js来写 如何判断系统功能权限是否开启",
+      "num_visitor": "6",
+      "num_visits": "10",
+      "visit_avg_time": "00:00:15",
+      "visitor_avg_time": "00:00:25",
+      "num_share": "0"
+    },
+    {
+      "id_page": 74506803,
+      "url": "pages/forum/detail/detail?id=36027&type=2",
+      "name": "终于搞定了UniApp开发的微信小程序的支付,分享下有关微信支付踩的坑",
+      "num_visitor": "9",
+      "num_visits": "9",
+      "visit_avg_time": "00:00:30",
+      "visitor_avg_time": "00:00:30",
+      "num_share": "0"
+    },
+    {
+      "id_page": 62529047,
+      "url": "pages/forum/detail/detail?id=85976&type=1",
+      "name": "uni app 打包成 Android 后无法请求服务器",
+      "num_visitor": "9",
+      "num_visits": "9",
+      "visit_avg_time": "00:00:34",
+      "visitor_avg_time": "00:00:34",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83415318,
+      "url": "pages/forum/detail/detail?id=135730&type=1",
+      "name": "uniapp-安卓实现自动拍照",
+      "num_visitor": "8",
+      "num_visits": "8",
+      "visit_avg_time": "00:00:07",
+      "visitor_avg_time": "00:00:07",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83418841,
+      "url": "pages/forum/detail/detail?id=135739&type=1",
+      "name": "",
+      "num_visitor": "6",
+      "num_visits": "7",
+      "visit_avg_time": "00:00:04",
+      "visitor_avg_time": "00:00:04",
+      "num_share": "0"
+    },
+    {
+      "id_page": 66951069,
+      "url": "pages/forum/detail/detail?id=102581&type=1",
+      "name": "uniapp 打包成手机h5 js缓存问题怎么解决,只能清手机缓存吗",
+      "num_visitor": "6",
+      "num_visits": "7",
+      "visit_avg_time": "00:00:10",
+      "visitor_avg_time": "00:00:11",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83408388,
+      "url": "pages/forum/detail/detail?id=135713&type=1",
+      "name": "使用VideoPlayer播放视频,视频有宽度自动收缩",
+      "num_visitor": "7",
+      "num_visits": "7",
+      "visit_avg_time": "00:00:02",
+      "visitor_avg_time": "00:00:02",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83442697,
+      "url": "pages/forum/detail/detail?id=135783&type=1",
+      "name": "有没有不用二维数组的版本",
+      "num_visitor": "7",
+      "num_visits": "7",
+      "visit_avg_time": "00:00:02",
+      "visitor_avg_time": "00:00:02",
+      "num_share": "0"
+    },
+    {
+      "id_page": 63688602,
+      "url": "pages/forum/detail/detail?id=37228&type=2",
+      "name": "5 App和uni-app在App开发上的对比",
+      "num_visitor": "7",
+      "num_visits": "7",
+      "visit_avg_time": "00:00:11",
+      "visitor_avg_time": "00:00:11",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83415397,
+      "url": "pages/forum/detail/detail?id=39503&type=2",
+      "name": "uniapp MIUI全局自由窗口适配,uniapp悬浮小窗和分屏适配",
+      "num_visitor": "2",
+      "num_visits": "7",
+      "visit_avg_time": "00:00:17",
+      "visitor_avg_time": "00:00:59",
+      "num_share": "0"
+    },
+    {
+      "id_page": 62347163,
+      "url": "pages/forum/detail/detail?id=80913&type=1",
+      "name": "uni-app flex布局,position:absolute有问题",
+      "num_visitor": "7",
+      "num_visits": "7",
+      "visit_avg_time": "00:00:28",
+      "visitor_avg_time": "00:00:28",
+      "num_share": "0"
+    },
+    {
+      "id_page": 75994881,
+      "url": "pages/forum/detail/detail?id=122399&type=1",
+      "name": "【报Bug】uni.chooseVideo()  方法选中视频文件 加载时长问题",
+      "num_visitor": "5",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:06",
+      "visitor_avg_time": "00:00:08",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83422277,
+      "url": "pages/forum/detail/detail?id=135749&type=1",
+      "name": "【报Bug】plus.video.LivePusher控件推流出现变形,直播源是压扁的",
+      "num_visitor": "5",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:09",
+      "visitor_avg_time": "00:00:10",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83340805,
+      "url": "pages/forum/detail/detail?id=135644&type=1",
+      "name": "uniapp中的input、text area、editor聚焦后无法唤起输入法",
+      "num_visitor": "3",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:01",
+      "visitor_avg_time": "00:00:03",
+      "num_share": "0"
+    },
+    {
+      "id_page": 82122616,
+      "url": "pages/forum/detail/detail?id=39390&type=2",
+      "name": "公告:阿里云服务空间云存储容量上限调整周知",
+      "num_visitor": "6",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:27",
+      "visitor_avg_time": "00:00:27",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83434138,
+      "url": "pages/forum/detail/detail?id=135773&type=1",
+      "name": "海报 二维码",
+      "num_visitor": "5",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:01",
+      "visitor_avg_time": "00:00:02",
+      "num_share": "0"
+    },
+    {
+      "id_page": 79225826,
+      "url": "pages/forum/detail/detail?id=127577&type=1",
+      "name": "【报Bug】uniapp安卓热更新偶尔出现第一次重启样式错乱,第二次重启正常。",
+      "num_visitor": "6",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:07",
+      "visitor_avg_time": "00:00:07",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83416762,
+      "url": "pages/forum/detail/detail?id=135731&type=1",
+      "name": "【报Bug】uni.chooseVideo方法 拍摄视频 超过一分钟之后返回的路径不对",
+      "num_visitor": "5",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:03",
+      "visitor_avg_time": "00:00:04",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83113722,
+      "url": "pages/forum/detail/detail?id=135136&type=1",
+      "name": "【报Bug】ios nvue 组件中使用富文本rich-text,设置font-size,更新数据后崩溃",
+      "num_visitor": "5",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:05",
+      "visitor_avg_time": "00:00:06",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83437008,
+      "url": "pages/forum/detail/detail?id=135779&type=1",
+      "name": "详情",
+      "num_visitor": "5",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:03",
+      "visitor_avg_time": "00:00:03",
+      "num_share": "0"
+    },
+    {
+      "id_page": 1427,
+      "url": "pages/forum/detail/detail?id=41&type=2",
+      "name": "iOS离线打包",
+      "num_visitor": "5",
+      "num_visits": "6",
+      "visit_avg_time": "00:00:14",
+      "visitor_avg_time": "00:00:17",
+      "num_share": "0"
+    },
+    {
+      "id_page": 63162055,
+      "url": "pages/forum/detail/detail?id=37140&type=2",
+      "name": "解决uni-app的pages.json的模块化及模块热重载的问题",
+      "num_visitor": "4",
+      "num_visits": "5",
+      "visit_avg_time": "00:00:07",
+      "visitor_avg_time": "00:00:09",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83405330,
+      "url": "pages/forum/detail/detail?id=135711&type=1",
+      "name": "【报Bug】将网站套壳打包,wap2app,创建的app会随机出现无响应的情况",
+      "num_visitor": "5",
+      "num_visits": "5",
+      "visit_avg_time": "00:00:01",
+      "visitor_avg_time": "00:00:01",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83444463,
+      "url": "pages/forum/detail/detail?id=135784&type=1",
+      "name": "#插件讨论# 【 区块链货币数字钱包APP uni-app模板 - 8***@qq.com 】 怎么加你",
+      "num_visitor": "3",
+      "num_visits": "5",
+      "visit_avg_time": "00:00:06",
+      "visitor_avg_time": "00:00:10",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83447956,
+      "url": "pages/forum/detail/detail?id=106275&type=1",
+      "name": "外包uni-app项目里的APP两端的“APP原生插件配置”谷歌地图开发",
+      "num_visitor": "4",
+      "num_visits": "5",
+      "visit_avg_time": "00:00:05",
+      "visitor_avg_time": "00:00:07",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83445502,
+      "url": "pages/forum/detail/detail?id=135786&type=1",
+      "name": "【报Bug】savefile 保存doc文件时提示文件没有发现",
+      "num_visitor": "4",
+      "num_visits": "5",
+      "visit_avg_time": "00:00:04",
+      "visitor_avg_time": "00:00:05",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83424862,
+      "url": "pages/forum/detail/detail?id=135755&type=1",
+      "name": "web-view嵌套的h5页面怎么实现video全屏横屏播放",
+      "num_visitor": "4",
+      "num_visits": "5",
+      "visit_avg_time": "00:00:02",
+      "visitor_avg_time": "00:00:03",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83397776,
+      "url": "pages/forum/detail/detail?id=135707&type=1",
+      "name": "插件上传,一直都是提示名称错误",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:05",
+      "visitor_avg_time": "00:00:05",
+      "num_share": "0"
+    },
+    {
+      "id_page": 82995242,
+      "url": "pages/forum/detail/detail?id=134753&type=1",
+      "name": "【报Bug】华为应用市场  收集个人信息因 ‘好的’ ‘我知道了’ 字眼 违规",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:02",
+      "visitor_avg_time": "00:00:02",
+      "num_share": "0"
+    },
+    {
+      "id_page": 68918405,
+      "url": "pages/forum/detail/detail?id=102915&type=1",
+      "name": "uniapp 如何重写音量键动作呢?",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:10",
+      "visitor_avg_time": "00:00:10",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83430380,
+      "url": "pages/forum/detail/detail?id=135768&type=1",
+      "name": "在官网下载的App离线SDK,配置了appid、包名、签名、appkey,打包运行后一直停留在HBuilder的界面,没有错误提示,请问如何解决?",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:04",
+      "visitor_avg_time": "00:00:04",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83424008,
+      "url": "pages/forum/detail/detail?id=135752&type=1",
+      "name": "uni.navigateTo和uni.redirectTo跳转",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:04",
+      "visitor_avg_time": "00:00:04",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83423019,
+      "url": "pages/forum/detail/detail?id=135736&type=1",
+      "name": "使用video组件 模拟器上能正常播放,真机上无法播放",
+      "num_visitor": "3",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:02",
+      "visitor_avg_time": "00:00:02",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83396003,
+      "url": "pages/forum/detail/detail?id=135705&type=1",
+      "name": "【报Bug】HBuilderX3.3.0 引出的“系统定位”模块 问题",
+      "num_visitor": "3",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:39",
+      "visitor_avg_time": "00:00:52",
+      "num_share": "0"
+    },
+    {
+      "id_page": 885,
+      "url": "pages/forum/detail/detail?id=63012&type=1",
+      "name": "用HTML5 新增的蓝牙Bluetooth模块的demo测试,安卓手机可以搜索到附近的蓝牙设备,但是苹果手机搜索不到附近的蓝牙设备,用的ios都是11.1版本以上的,请问一下这是什么原因呢?",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:08",
+      "visitor_avg_time": "00:00:08",
+      "num_share": "0"
+    },
+    {
+      "id_page": 9977925,
+      "url": "pages/forum/detail/detail?id=35907&type=2",
+      "name": "DCloud appid 用途/作用/使用说明",
+      "num_visitor": "3",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:13",
+      "visitor_avg_time": "00:00:17",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83351021,
+      "url": "pages/forum/detail/detail?id=135657&type=1",
+      "name": "【报Bug】  使用了Hbuilder 3.2.13之后的版本 进行usb设备的拔插后(如键盘等输入设备)  页面v-if会失效以及很多视图无法及时更新。",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:04",
+      "visitor_avg_time": "00:00:04",
+      "num_share": "0"
+    },
+    {
+      "id_page": 63151975,
+      "url": "pages/forum/detail/detail?id=81272&type=1",
+      "name": "uni.chooseImage方法使用从相册选择上传就好使  拍照的上传就失败  文件都能获取到",
+      "num_visitor": "3",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:04",
+      "visitor_avg_time": "00:00:06",
+      "num_share": "0"
+    },
+    {
+      "id_page": 74766262,
+      "url": "pages/forum/detail/detail?id=119804&type=1",
+      "name": "求助!scroll-view 自定义下拉刷新多次触发问题,上滑都会多次触发",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:10",
+      "visitor_avg_time": "00:00:10",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83441117,
+      "url": "pages/forum/detail/detail?id=135781&type=1",
+      "name": "【报Bug】onLaunch时 plus.screen.lockOrientation('portrait-primary') 屏幕锁定无效",
+      "num_visitor": "3",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:09",
+      "visitor_avg_time": "00:00:12",
+      "num_share": "0"
+    },
+    {
+      "id_page": 83417459,
+      "url": "pages/forum/detail/detail?id=135738&type=1",
+      "name": "使用unicloud,车辆运行轨迹数据库应该如何设计",
+      "num_visitor": "4",
+      "num_visits": "4",
+      "visit_avg_time": "00:00:03",
+      "visitor_avg_time": "00:00:03",
+      "num_share": "0"
+    }
+  ]
+}

+ 337 - 0
mock/uni-stat/pageEnt.json

@@ -0,0 +1,337 @@
+{
+  "last_page": 6,
+  "current_page": 1,
+  "total": 170,
+  "item": [
+    {
+      "id_page": 8,
+      "url": "pages/tabBar/forum/forum",
+      "name": "社区",
+      "num_visitor": "868",
+      "num_visits": "3,203",
+      "visit_avg_time": "00:01:27",
+      "visitor_avg_time": "00:05:23",
+      "entry_num_visits": "1,231",
+      "bounce_rate": "2.52%"
+    },
+    {
+      "id_page": 9,
+      "url": "pages/forum/detail/detail",
+      "name": "社区详情",
+      "num_visitor": "293",
+      "num_visits": "1,005",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:01",
+      "entry_num_visits": "70",
+      "bounce_rate": "18.57%"
+    },
+    {
+      "id_page": 11,
+      "url": "pages/tabBar/case/case",
+      "name": "实例",
+      "num_visitor": "550",
+      "num_visits": "6,957",
+      "visit_avg_time": "00:00:22",
+      "visitor_avg_time": "00:04:47",
+      "entry_num_visits": "65",
+      "bounce_rate": "23.08%"
+    },
+    {
+      "id_page": 126,
+      "url": "pages/forum/login/login",
+      "name": "登录",
+      "num_visitor": "156",
+      "num_visits": "375",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "25",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 10,
+      "url": "pages/tabBar/center/center",
+      "name": "个人中心",
+      "num_visitor": "365",
+      "num_visits": "1,046",
+      "visit_avg_time": "00:01:53",
+      "visitor_avg_time": "00:05:24",
+      "entry_num_visits": "18",
+      "bounce_rate": "50.00%"
+    },
+    {
+      "id_page": 169,
+      "url": "pages/forum/search/index",
+      "name": "[object Object]",
+      "num_visitor": "84",
+      "num_visits": "266",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "15",
+      "bounce_rate": "40.00%"
+    },
+    {
+      "id_page": 74,
+      "url": "pages/template/list2detail-list/list2detail-list",
+      "name": "列表到详情示例",
+      "num_visitor": "53",
+      "num_visits": "129",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "13",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 33,
+      "url": "pages/component/scroll-view/scroll-view",
+      "name": "scroll-view",
+      "num_visitor": "62",
+      "num_visits": "86",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "10",
+      "bounce_rate": "40.00%"
+    },
+    {
+      "id_page": 141,
+      "url": "pages/API/scan-code/scan-code",
+      "name": "扫码",
+      "num_visitor": "38",
+      "num_visits": "90",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "9",
+      "bounce_rate": "11.11%"
+    },
+    {
+      "id_page": 25,
+      "url": "pages/component/view/view",
+      "name": "view",
+      "num_visitor": "98",
+      "num_visits": "126",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "9",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 60,
+      "url": "pages/template/ucharts/ucharts",
+      "name": "uCharts 图表",
+      "num_visitor": "65",
+      "num_visits": "88",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "9",
+      "bounce_rate": "22.22%"
+    },
+    {
+      "id_page": 78,
+      "url": "pages/component/button/button",
+      "name": "button",
+      "num_visitor": "73",
+      "num_visits": "124",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "9",
+      "bounce_rate": "11.11%"
+    },
+    {
+      "id_page": 123,
+      "url": "pages/template/nav-dot/nav-dot",
+      "name": "[object Object]",
+      "num_visitor": "34",
+      "num_visits": "65",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "8",
+      "bounce_rate": "37.50%"
+    },
+    {
+      "id_page": 144,
+      "url": "pages/template/scheme/scheme",
+      "name": "打开外部应用",
+      "num_visitor": "55",
+      "num_visits": "115",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "7",
+      "bounce_rate": "28.57%"
+    },
+    {
+      "id_page": 2835009,
+      "url": "pages/component/ad/index",
+      "name": "",
+      "num_visitor": "76",
+      "num_visits": "215",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "7",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 313,
+      "url": "platforms/app-plus/push/push",
+      "name": "推送",
+      "num_visitor": "38",
+      "num_visits": "110",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:01",
+      "entry_num_visits": "7",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 34,
+      "url": "pages/component/swiper/swiper",
+      "name": "swiper",
+      "num_visitor": "43",
+      "num_visits": "59",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "6",
+      "bounce_rate": "66.67%"
+    },
+    {
+      "id_page": 40,
+      "url": "pages/about/about",
+      "name": "关于",
+      "num_visitor": "47",
+      "num_visits": "68",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "5",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 315,
+      "url": "pages/template/vant-button/vant-button",
+      "name": "微信自定义组件示例",
+      "num_visitor": "37",
+      "num_visits": "44",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "5",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 339,
+      "url": "pages/component/web-view-local/web-view-local",
+      "name": "button",
+      "num_visitor": "33",
+      "num_visits": "47",
+      "visit_avg_time": "00:00:01",
+      "visitor_avg_time": "00:00:01",
+      "entry_num_visits": "5",
+      "bounce_rate": "20.00%"
+    },
+    {
+      "id_page": 99,
+      "url": "pages/template/nav-button/nav-button",
+      "name": "导航栏带自定义按钮",
+      "num_visitor": "69",
+      "num_visits": "133",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "5",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 32,
+      "url": "pages/component/picker/picker",
+      "name": "picker",
+      "num_visitor": "38",
+      "num_visits": "52",
+      "visit_avg_time": "00:00:05",
+      "visitor_avg_time": "00:00:06",
+      "entry_num_visits": "5",
+      "bounce_rate": "40.00%"
+    },
+    {
+      "id_page": 30,
+      "url": "pages/component/web-view/web-view",
+      "name": "web-view",
+      "num_visitor": "60",
+      "num_visits": "125",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "4",
+      "bounce_rate": "25.00%"
+    },
+    {
+      "id_page": 110,
+      "url": "pages/component/textarea/textarea",
+      "name": "textarea",
+      "num_visitor": "31",
+      "num_visits": "34",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "4",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 342,
+      "url": "pages/template/nav-image/nav-image",
+      "name": "[object Object]",
+      "num_visitor": "37",
+      "num_visits": "51",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "4",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 16,
+      "url": "pages/tabBar/component/component",
+      "name": "组件",
+      "num_visitor": "4",
+      "num_visits": "11",
+      "visit_avg_time": "00:00:02",
+      "visitor_avg_time": "00:00:07",
+      "entry_num_visits": "4",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 44,
+      "url": "pages/API/request/request",
+      "name": "网络请求",
+      "num_visitor": "22",
+      "num_visits": "29",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "4",
+      "bounce_rate": "50.00%"
+    },
+    {
+      "id_page": 128,
+      "url": "pages/API/share/share",
+      "name": "分享",
+      "num_visitor": "24",
+      "num_visits": "77",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "4",
+      "bounce_rate": "75.00%"
+    },
+    {
+      "id_page": 26,
+      "url": "pages/API/set-navigation-bar-title/set-navigation-bar-title",
+      "name": "设置界面标题",
+      "num_visitor": "63",
+      "num_visits": "85",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "4",
+      "bounce_rate": "0.00%"
+    },
+    {
+      "id_page": 35,
+      "url": "pages/component/movable-view/movable-view",
+      "name": "movable-view",
+      "num_visitor": "30",
+      "num_visits": "40",
+      "visit_avg_time": "00:00:00",
+      "visitor_avg_time": "00:00:00",
+      "entry_num_visits": "4",
+      "bounce_rate": "50.00%"
+    }
+  ]
+}

+ 397 - 0
mock/uni-stat/pageRes.json

@@ -0,0 +1,397 @@
+ {
+   "last_page": 6,
+   "current_page": 1,
+   "total": 170,
+   "item": [
+     {
+       "id_page": 11,
+       "url": "pages/tabBar/case/case",
+       "name": "实例",
+       "num_visitor": "550",
+       "num_visits": "6,957",
+       "visit_avg_time": "00:00:38",
+       "visitor_avg_time": "00:08:05",
+       "exit_num_visits": "485",
+       "exit_rate": "6.97%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 8,
+       "url": "pages/tabBar/forum/forum",
+       "name": "社区",
+       "num_visitor": "868", 
+       "num_visits": "3,203",
+       "visit_avg_time": "00:01:47",
+       "visitor_avg_time": "00:06:37",
+       "exit_num_visits": "519",
+       "exit_rate": "16.20%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 10,
+       "url": "pages/tabBar/center/center",
+       "name": "个人中心",
+       "num_visitor": "365",
+       "num_visits": "1,046",
+       "visit_avg_time": "00:02:18",
+       "visitor_avg_time": "00:06:37",
+       "exit_num_visits": "93",
+       "exit_rate": "8.89%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 9,
+       "url": "pages/forum/detail/detail",
+       "name": "社区详情",
+       "num_visitor": "293",
+       "num_visits": "1,005",
+       "visit_avg_time": "00:00:15",
+       "visitor_avg_time": "00:00:53",
+       "exit_num_visits": "122",
+       "exit_rate": "12.14%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 126,
+       "url": "pages/forum/login/login",
+       "name": "登录",
+       "num_visitor": "156",
+       "num_visits": "375",
+       "visit_avg_time": "00:00:06",
+       "visitor_avg_time": "00:00:14",
+       "exit_num_visits": "27",
+       "exit_rate": "7.20%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 169,
+       "url": "pages/forum/search/index",
+       "name": "[object Object]",
+       "num_visitor": "84",
+       "num_visits": "266",
+       "visit_avg_time": "00:00:11",
+       "visitor_avg_time": "00:00:35",
+       "exit_num_visits": "34",
+       "exit_rate": "12.78%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 310,
+       "url": "pages/API/subnvue/subnvue",
+       "name": "SubNvue",
+       "num_visitor": "50",
+       "num_visits": "241",
+       "visit_avg_time": "00:00:12",
+       "visitor_avg_time": "00:00:58",
+       "exit_num_visits": "6",
+       "exit_rate": "2.49%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 2835009,
+       "url": "pages/component/ad/index",
+       "name": "",
+       "num_visitor": "76",
+       "num_visits": "215",
+       "visit_avg_time": "00:00:01",
+       "visitor_avg_time": "00:00:04",
+       "exit_num_visits": "9",
+       "exit_rate": "4.19%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 21,
+       "url": "pages/template/tabbar/tabbar",
+       "name": "可拖动顶部选项卡",
+       "num_visitor": "66",
+       "num_visits": "143",
+       "visit_avg_time": "00:00:30",
+       "visitor_avg_time": "00:01:06",
+       "exit_num_visits": "5",
+       "exit_rate": "3.50%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 99,
+       "url": "pages/template/nav-button/nav-button",
+       "name": "导航栏带自定义按钮",
+       "num_visitor": "69",
+       "num_visits": "133",
+       "visit_avg_time": "00:00:04",
+       "visitor_avg_time": "00:00:09",
+       "exit_num_visits": "5",
+       "exit_rate": "3.76%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 74,
+       "url": "pages/template/list2detail-list/list2detail-list",
+       "name": "列表到详情示例",
+       "num_visitor": "53",
+       "num_visits": "129",
+       "visit_avg_time": "00:00:07",
+       "visitor_avg_time": "00:00:19",
+       "exit_num_visits": "3",
+       "exit_rate": "2.33%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 25,
+       "url": "pages/component/view/view",
+       "name": "view",
+       "num_visitor": "98",
+       "num_visits": "126",
+       "visit_avg_time": "00:00:10",
+       "visitor_avg_time": "00:00:13",
+       "exit_num_visits": "3",
+       "exit_rate": "2.38%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 27,
+       "url": "pages/component/navigator/navigator",
+       "name": "navigator",
+       "num_visitor": "67",
+       "num_visits": "125",
+       "visit_avg_time": "00:00:01",
+       "visitor_avg_time": "00:00:02",
+       "exit_num_visits": "2",
+       "exit_rate": "1.60%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 30,
+       "url": "pages/component/web-view/web-view",
+       "name": "web-view",
+       "num_visitor": "60",
+       "num_visits": "125",
+       "visit_avg_time": "00:00:03",
+       "visitor_avg_time": "00:00:08",
+       "exit_num_visits": "6",
+       "exit_rate": "4.80%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 78,
+       "url": "pages/component/button/button",
+       "name": "button",
+       "num_visitor": "73",
+       "num_visits": "124",
+       "visit_avg_time": "00:00:06",
+       "visitor_avg_time": "00:00:11",
+       "exit_num_visits": "10",
+       "exit_rate": "8.06%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 144,
+       "url": "pages/template/scheme/scheme",
+       "name": "打开外部应用",
+       "num_visitor": "55",
+       "num_visits": "115",
+       "visit_avg_time": "00:00:06",
+       "visitor_avg_time": "00:00:13",
+       "exit_num_visits": "16",
+       "exit_rate": "13.91%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 313,
+       "url": "platforms/app-plus/push/push",
+       "name": "推送",
+       "num_visitor": "38",
+       "num_visits": "110",
+       "visit_avg_time": "00:00:05",
+       "visitor_avg_time": "00:00:15",
+       "exit_num_visits": "4",
+       "exit_rate": "3.64%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 129,
+       "url": "pages/API/login/login",
+       "name": "授权登录",
+       "num_visitor": "44",
+       "num_visits": "109",
+       "visit_avg_time": "00:00:03",
+       "visitor_avg_time": "00:00:08",
+       "exit_num_visits": "5",
+       "exit_rate": "4.59%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 135,
+       "url": "pages/API/request-payment/request-payment",
+       "name": "发起支付",
+       "num_visitor": "43",
+       "num_visits": "101",
+       "visit_avg_time": "00:00:04",
+       "visitor_avg_time": "00:00:10",
+       "exit_num_visits": "5",
+       "exit_rate": "4.95%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 141,
+       "url": "pages/API/scan-code/scan-code",
+       "name": "扫码",
+       "num_visitor": "38",
+       "num_visits": "90",
+       "visit_avg_time": "00:00:20",
+       "visitor_avg_time": "00:00:48",
+       "exit_num_visits": "13",
+       "exit_rate": "14.44%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 42,
+       "url": "pages/API/navigator/navigator",
+       "name": "页面跳转",
+       "num_visitor": "27",
+       "num_visits": "89",
+       "visit_avg_time": "00:00:02",
+       "visitor_avg_time": "00:00:07",
+       "exit_num_visits": "1",
+       "exit_rate": "1.12%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 57,
+       "url": "pages/extUI/list/list",
+       "name": "List 列表",
+       "num_visitor": "36",
+       "num_visits": "88",
+       "visit_avg_time": "00:03:21",
+       "visitor_avg_time": "00:08:11",
+       "exit_num_visits": "8",
+       "exit_rate": "9.09%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 60,
+       "url": "pages/template/ucharts/ucharts",
+       "name": "uCharts 图表",
+       "num_visitor": "65",
+       "num_visits": "88",
+       "visit_avg_time": "00:00:19",
+       "visitor_avg_time": "00:00:26",
+       "exit_num_visits": "12",
+       "exit_rate": "13.64%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 33,
+       "url": "pages/component/scroll-view/scroll-view",
+       "name": "scroll-view",
+       "num_visitor": "62",
+       "num_visits": "86",
+       "visit_avg_time": "00:00:11",
+       "visitor_avg_time": "00:00:16",
+       "exit_num_visits": "11",
+       "exit_rate": "12.79%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 26,
+       "url": "pages/API/set-navigation-bar-title/set-navigation-bar-title",
+       "name": "设置界面标题",
+       "num_visitor": "63",
+       "num_visits": "85",
+       "visit_avg_time": "00:00:06",
+       "visitor_avg_time": "00:00:08",
+       "exit_num_visits": "1",
+       "exit_rate": "1.18%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 3084839,
+       "url": "pages/template/swiper-list-nvue/swiper-list-nvue",
+       "name": "",
+       "num_visitor": "48",
+       "num_visits": "80",
+       "visit_avg_time": "00:01:11",
+       "visitor_avg_time": "00:01:58",
+       "exit_num_visits": "6",
+       "exit_rate": "7.50%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 128,
+       "url": "pages/API/share/share",
+       "name": "分享",
+       "num_visitor": "24",
+       "num_visits": "77",
+       "visit_avg_time": "00:00:06",
+       "visitor_avg_time": "00:00:22",
+       "exit_num_visits": "8",
+       "exit_rate": "10.39%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 360577,
+       "url": "pages/template/component-communication/component-communication",
+       "name": "组件通讯",
+       "num_visitor": "52",
+       "num_visits": "71",
+       "visit_avg_time": "00:00:03",
+       "visitor_avg_time": "00:00:04",
+       "exit_num_visits": "1",
+       "exit_rate": "1.41%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 361612,
+       "url": "pages/API/navigator/new-page/new-vue-page-1",
+       "name": "新VUE页面1",
+       "num_visitor": "18",
+       "num_visits": "71",
+       "visit_avg_time": "00:00:01",
+       "visitor_avg_time": "00:00:06",
+       "exit_num_visits": "0",
+       "exit_rate": "0.00%",
+       "num_share": "0",
+       "hasChildren": true
+     },
+     {
+       "id_page": 314,
+       "url": "platforms/app-plus/feedback/feedback",
+       "name": "问题反馈",
+       "num_visitor": "39",
+       "num_visits": "69",
+       "visit_avg_time": "00:00:10",
+       "visitor_avg_time": "00:00:18",
+       "exit_num_visits": "7",
+       "exit_rate": "10.14%",
+       "num_share": "0",
+       "hasChildren": true
+     }
+   ]
+ }

+ 132 - 0
mock/uni-stat/pageRule.json

@@ -0,0 +1,132 @@
+{
+  "last_page": 16,
+  "current_page": 1,
+  "total": 315,
+  "item": [
+    {
+      "id_page": 8,
+      "url": "pages/tabBar/forum/forum",
+      "name": "社区",
+      "rules": [
+        "id",
+        "type"
+      ]
+    },
+    {
+      "id_page": 9,
+      "url": "pages/forum/detail/detail",
+      "name": "社区详情",
+      "rules": [
+        "id,type"
+      ]
+    },
+    {
+      "id_page": 10,
+      "url": "pages/tabBar/center/center",
+      "name": "个人中心",
+      "rules": null
+    },
+    {
+      "id_page": 11,
+      "url": "pages/tabBar/case/case",
+      "name": "实例",
+      "rules": null
+    },
+    {
+      "id_page": 16,
+      "url": "pages/tabBar/component/component",
+      "name": "组件",
+      "rules": null
+    },
+    {
+      "id_page": 17,
+      "url": "pages/tabBar/API/API",
+      "name": "API",
+      "rules": null
+    },
+    {
+      "id_page": 18,
+      "url": "pages/tabBar/extUI/extUI",
+      "name": "扩展组件",
+      "rules": null
+    },
+    {
+      "id_page": 19,
+      "url": "pages/tabBar/template/template",
+      "name": "模版",
+      "rules": null
+    },
+    {
+      "id_page": 20,
+      "url": "pages/template/mpvue-picker/mpvue-picker",
+      "name": "多列选择picker",
+      "rules": null
+    },
+    {
+      "id_page": 21,
+      "url": "pages/template/tabbar/tabbar",
+      "name": "可拖动顶部选项卡",
+      "rules": null
+    },
+    {
+      "id_page": 22,
+      "url": "pages/template/scrollmsg/scrollmsg",
+      "name": "滚动公告",
+      "rules": null
+    },
+    {
+      "id_page": 23,
+      "url": "pages/template/datachecker/datachecker",
+      "name": "表单校验",
+      "rules": null
+    },
+    {
+      "id_page": 24,
+      "url": "pages/extUI/swipe-action/swipe-action",
+      "name": "SwipeAction 滑动操作",
+      "rules": null
+    },
+    {
+      "id_page": 25,
+      "url": "pages/component/view/view",
+      "name": "view",
+      "rules": null
+    },
+    {
+      "id_page": 26,
+      "url": "pages/API/set-navigation-bar-title/set-navigation-bar-title",
+      "name": "设置界面标题",
+      "rules": null
+    },
+    {
+      "id_page": 27,
+      "url": "pages/component/navigator/navigator",
+      "name": "navigator",
+      "rules": null
+    },
+    {
+      "id_page": 28,
+      "url": "pages/component/navigator/redirect/redirect",
+      "name": "redirectPage",
+      "rules": null
+    },
+    {
+      "id_page": 29,
+      "url": "pages/component/map/map",
+      "name": "map",
+      "rules": null
+    },
+    {
+      "id_page": 30,
+      "url": "pages/component/web-view/web-view",
+      "name": "web-view",
+      "rules": null
+    },
+    {
+      "id_page": 31,
+      "url": "pages/component/form/form",
+      "name": "form",
+      "rules": null
+    }
+  ]
+}

+ 73 - 0
mock/uni-stat/userActivity.json

@@ -0,0 +1,73 @@
+{
+  "categories": [
+    "2021-11-08",
+    "2021-11-09",
+    "2021-11-10",
+    "2021-11-11",
+    "2021-11-12",
+    "2021-11-13",
+    "2021-11-14",
+    "2021-11-15",
+    "2021-11-16",
+    "2021-11-17",
+    "2021-11-18",
+    "2021-11-19",
+    "2021-11-20",
+    "2021-11-21",
+    "2021-11-22",
+    "2021-11-23",
+    "2021-11-24",
+    "2021-11-25",
+    "2021-11-26",
+    "2021-11-27",
+    "2021-11-28",
+    "2021-11-29",
+    "2021-11-30",
+    "2021-12-01",
+    "2021-12-02",
+    "2021-12-03",
+    "2021-12-04",
+    "2021-12-05",
+    "2021-12-06",
+    "2021-12-07",
+    "2021-12-08"
+  ],
+  "series": [
+    {
+      "name": "日活",
+      "data": [
+        1520,
+        1523,
+        1462,
+        1445,
+        1433,
+        972,
+        768,
+        1421,
+        1581,
+        1613,
+        1549,
+        1517,
+        989,
+        839,
+        1579,
+        1539,
+        1574,
+        1518,
+        1584,
+        1043,
+        853,
+        1498,
+        1553,
+        1170,
+        909,
+        866,
+        620,
+        566,
+        884,
+        905,
+        643
+      ]
+    }
+  ]
+}

+ 93 - 0
package.json

@@ -0,0 +1,93 @@
+{
+	"name": "uni-admin 基础框架(原名 uniCloud admin)",
+	"id": "uni-template-admin",
+	"displayName": "uni-admin 基础框架",
+	"version": "2.0.2",
+	"description": "基于uni-app & uniCloud的后台管理项目模板(管理后台开发必备神器)",
+	"main": "main.js",
+	"scripts": {
+		"test": "echo \"Error: no test specified\" && exit 1"
+	},
+	"repository": "https://github.com/dcloudio/uni-admin.git",
+	"keywords": [
+        "admin",
+        "uniCloud",
+        "管理后台",
+        "云后台",
+        "uni-admin"
+    ],
+	"engines": {
+		"HBuilderX": "^3.6.0"
+	},
+	"author": "",
+	"license": "MIT",
+	"bugs": {
+		"url": "https://github.com/dcloudio/uni-admin/issues"
+	},
+	"homepage": "https://github.com/dcloudio/uni-admin#readme",
+    "dcloudext": {
+        "sale": {
+			"regular": {
+				"price": "0.00"
+			},
+			"sourcecode": {
+				"price": "0.00"
+			}
+		},
+		"contact": {
+			"qq": ""
+		},
+		"declaration": {
+			"ads": "无",
+			"data": "无",
+			"permissions": "无"
+		},
+        "npmurl": "",
+        "type": "unicloud-template-project"
+	},
+	"uni_modules": {
+		"dependencies": [],
+		"encrypt": [],
+		"platforms": {
+			"cloud": {
+				"tcb": "y",
+				"aliyun": "y"
+			},
+			"client": {
+				"App": {
+					"app-vue": "y",
+					"app-nvue": "y"
+				},
+				"H5-mobile": {
+					"Safari": "y",
+					"Android Browser": "y",
+					"微信浏览器(Android)": "y",
+					"QQ浏览器(Android)": "y"
+				},
+				"H5-pc": {
+					"Chrome": "y",
+					"IE": "n",
+					"Edge": "y",
+					"Firefox": "y",
+					"Safari": "y"
+				},
+				"小程序": {
+					"微信": "y",
+					"阿里": "y",
+					"百度": "y",
+					"字节跳动": "y",
+                    "QQ": "y",
+                    "京东": "u"
+				},
+				"快应用": {
+					"华为": "u",
+					"联盟": "u"
+                },
+                "Vue": {
+                    "vue2": "y",
+                    "vue3": "y"
+                }
+			}
+		}
+	}
+}

+ 433 - 0
pages.json

@@ -0,0 +1,433 @@
+{
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/index"
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/login/login-withpwd",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "登录"
+			}
+		}, {
+			"path": "pages/error/404",
+			"style": {
+				"navigationBarTitleText": "Not Found"
+			}
+		},
+		{
+			"path": "pages/cyt-logs/add",
+			"style": {
+				"navigationBarTitleText": "新增"
+			}
+		}, {
+			"path": "pages/cyt-logs/edit",
+			"style": {
+				"navigationBarTitleText": "编辑"
+			}
+		}, {
+			"path": "pages/cyt-logs/list",
+			"style": {
+				"navigationBarTitleText": "列表"
+			}
+		},
+		{
+			"path": "uni_modules/uni-id-pages/pages/userinfo/change_pwd/change_pwd",
+			"style": {
+				"navigationBarTitleText": "修改密码"
+			}
+
+		}, {
+			"path": "uni_modules/uni-upgrade-center/pages/version/list",
+			"style": {
+				"navigationBarTitleText": "版本列表"
+			}
+		}, {
+			"path": "uni_modules/uni-upgrade-center/pages/version/add",
+			"style": {
+				"navigationBarTitleText": "新版发布"
+			}
+		}, {
+			"path": "uni_modules/uni-upgrade-center/pages/version/detail",
+			"style": {
+				"navigationBarTitleText": "版本信息查看"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/userinfo/deactivate/deactivate",
+			"style": {
+				"navigationBarTitleText": "注销账号"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/userinfo/userinfo",
+			"style": {
+				"navigationBarTitleText": "个人资料"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/userinfo/bind-mobile/bind-mobile",
+			"style": {
+				"navigationBarTitleText": "绑定手机号码"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/userinfo/cropImage/cropImage",
+			"style": {
+				"navigationBarTitleText": ""
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/login/login-smscode",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "手机验证码登录"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/login/login-withoutpwd",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "免密登录页"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/register/register",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "注册"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/register/register-admin",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "创建超级管理员"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/register/register-by-email",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "邮箱验证码注册"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/retrieve/retrieve",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "重置密码"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/retrieve/retrieve-by-email",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"navigationBarTitleText": "通过邮箱重置密码"
+			}
+		}, {
+			"path": "uni_modules/uni-id-pages/pages/common/webview/webview",
+			"style": {
+				"topWindow": false,
+				"leftWindow": false,
+				"enablePullDownRefresh": false,
+				"navigationBarTitleText": ""
+			}
+		},
+		{
+			"path": "pages/opendb-admin-log/list",
+			"style": {}
+		}
+	],
+	"subPackages": [{
+		"root": "pages/system",
+		"pages": [{
+			"path": "menu/list",
+			"style": {
+				"navigationBarTitleText": "菜单管理"
+			}
+		}, {
+			"path": "menu/add",
+			"style": {
+				"navigationBarTitleText": "新增菜单",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "menu/edit",
+			"style": {
+				"navigationBarTitleText": "修改菜单",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "permission/list",
+			"style": {
+				"navigationBarTitleText": "权限管理"
+			}
+		}, {
+			"path": "permission/add",
+			"style": {
+				"navigationBarTitleText": "新增权限",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "permission/edit",
+			"style": {
+				"navigationBarTitleText": "修改权限",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "role/add",
+			"style": {
+				"navigationBarTitleText": "新增角色",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "role/edit",
+			"style": {
+				"navigationBarTitleText": "修改角色",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "role/list",
+			"style": {
+				"navigationBarTitleText": "角色管理"
+			}
+		}, {
+			"path": "user/add",
+			"style": {
+				"navigationBarTitleText": "新增用户",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "user/edit",
+			"style": {
+				"navigationBarTitleText": "修改用户",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "user/list",
+			"style": {
+				"navigationBarTitleText": "用户管理"
+			}
+		}, {
+			"path": "app/add",
+			"style": {
+				"navigationBarTitleText": "新增应用",
+				"navigationStyle": "default"
+			}
+		}, {
+			"path": "app/list",
+			"style": {
+				"navigationBarTitleText": "应用管理"
+			}
+		}, {
+			"path": "app/uni-portal/uni-portal",
+			"style": {
+				"navigationBarTitleText": "发布页管理",
+				"navigationStyle": "default"
+			}
+
+		}, {
+			"path": "tag/add",
+			"style": {
+				"navigationBarTitleText": "新增标签"
+			}
+		}, {
+			"path": "tag/edit",
+			"style": {
+				"navigationBarTitleText": "修改标签"
+			}
+		}, {
+			"path": "tag/list",
+			"style": {
+				"navigationBarTitleText": "标签管理"
+			}
+		}, {
+			"path": "safety/list",
+			"style": {
+				"navigationBarTitleText": "用户日志"
+			}
+		}]
+	}, {
+		"root": "pages/demo",
+		"pages": [{
+			"path": "icons/icons",
+			"style": {
+				"navigationBarTitleText": "图标"
+			}
+		}, {
+			"path": "table/table",
+			"style": {
+				"navigationBarTitleText": "表格"
+			}
+		}]
+	}, {
+		"root": "pages/uni-stat",
+		"pages": [{
+			"path": "page-res/page-res",
+			"style": {
+				"navigationBarTitleText": "受访页",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "page-ent/page-ent",
+			"style": {
+				"navigationBarTitleText": "入口页",
+				"enablePullDownRefresh": false
+			}
+		}, {
+			"path": "scene/scene",
+			"style": {
+				"navigationBarTitleText": "场景值(小程序)",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "channel/channel",
+			"style": {
+				"navigationBarTitleText": "渠道(app)",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "error/js/js",
+			"style": {
+				"navigationBarTitleText": "js报错统计",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "error/js/detail",
+			"style": {
+				"navigationBarTitleText": "错误信息",
+				"navigationStyle": "default",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "error/app/app",
+			"style": {
+				"navigationBarTitleText": "app原生报错统计",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "event/event",
+			"style": {
+				"navigationBarTitleText": "事件和转化",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "device/overview/overview",
+			"style": {
+				"navigationBarTitleText": "今日概况",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "device/activity/activity",
+			"style": {
+				"navigationBarTitleText": "活跃度",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "device/trend/trend",
+			"style": {
+				"navigationBarTitleText": "趋势分析",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "device/retention/retention",
+			"style": {
+				"navigationBarTitleText": "留存",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "device/comparison/comparison",
+			"style": {
+				"navigationBarTitleText": "平台对比",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "device/stickiness/stickiness",
+			"style": {
+				"navigationBarTitleText": "粘性",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "user/overview/overview",
+			"style": {
+				"navigationBarTitleText": "今日概况",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "user/activity/activity",
+			"style": {
+				"navigationBarTitleText": "活跃度",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "user/trend/trend",
+			"style": {
+				"navigationBarTitleText": "趋势分析",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "user/retention/retention",
+			"style": {
+				"navigationBarTitleText": "留存",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "user/comparison/comparison",
+			"style": {
+				"navigationBarTitleText": "平台对比",
+				"enablePullDownRefresh": false
+			}
+
+		}, {
+			"path": "user/stickiness/stickiness",
+			"style": {
+				"navigationBarTitleText": "粘性",
+				"enablePullDownRefresh": false
+			}
+
+		}]
+	}],
+	"globalStyle": {
+		"navigationStyle": "custom",
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "管理系统",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	},
+	"topWindow": {
+		"path": "windows/topWindow",
+		"style": {
+			"height": "60px"
+		},
+		"matchMedia": {
+			"minWidth": 0
+		}
+	},
+	"leftWindow": {
+		"path": "windows/leftWindow",
+		"style": {
+			"width": "240px"
+		}
+	},
+	"uniIdRouter": {
+		"loginPage": "uni_modules/uni-id-pages/pages/login/login-withpwd",
+		"needLogin": [
+			"^((?!uni-id-pages\/pages\/login|register|retrieve).)*$"
+		],
+		"resToLogin": true
+	}
+}

+ 121 - 0
pages/cyt-logs/add.vue

@@ -0,0 +1,121 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :model="formData" validateTrigger="bind">
+      <uni-forms-item name="app_status" label="">
+        <uni-easyinput placeholder="app客户端" v-model="formData.app_status"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="user_name" label="">
+        <uni-easyinput placeholder="用户名称" v-model="formData.user_name"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="user_brand" label="">
+        <uni-easyinput placeholder="手机品牌" v-model="formData.user_brand"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="user_model" label="">
+        <uni-easyinput placeholder="手机型号" v-model="formData.user_model"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="api_src" label="">
+        <uni-easyinput placeholder="接口请求地址" v-model="formData.api_src"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="request_parameters" label="">
+        <uni-easyinput placeholder="请求参数" v-model="formData.request_parameters"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="response_parameters" label="">
+        <uni-easyinput placeholder="返回参数" v-model="formData.response_parameters"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="api_status" label="">
+        <uni-easyinput placeholder="接口状态" v-model="formData.api_status"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="error_info" label="">
+        <uni-easyinput placeholder="接口状态" v-model="formData.error_info"></uni-easyinput>
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">提交</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+          <button class="uni-button" style="width: 100px;">返回</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '../../js_sdk/validator/cyt-logs.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'cyt-logs';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  
+
+  export default {
+    data() {
+      let formData = {
+        "app_status": "",
+        "user_name": "",
+        "user_brand": "",
+        "user_model": "",
+        "api_src": "",
+        "request_parameters": "",
+        "response_parameters": "",
+        "api_status": "",
+        "error_info": ""
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      
+      /**
+       * 验证表单并提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          return this.submitForm(res)
+        }).catch(() => {
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      },
+
+      /**
+       * 提交表单
+       */
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        return db.collection(dbCollectionName).add(value).then((res) => {
+          uni.showToast({
+            title: '新增成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        })
+      }
+    }
+  }
+</script>

+ 152 - 0
pages/cyt-logs/edit.vue

@@ -0,0 +1,152 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :model="formData" validateTrigger="bind">
+      <uni-forms-item name="app_status" label="">
+        <uni-easyinput placeholder="app客户端" v-model="formData.app_status"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="user_name" label="">
+        <uni-easyinput placeholder="用户名称" v-model="formData.user_name"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="user_brand" label="">
+        <uni-easyinput placeholder="手机品牌" v-model="formData.user_brand"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="user_model" label="">
+        <uni-easyinput placeholder="手机型号" v-model="formData.user_model"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="api_src" label="">
+        <uni-easyinput placeholder="接口请求地址" v-model="formData.api_src"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="request_parameters" label="">
+        <uni-easyinput placeholder="请求参数" v-model="formData.request_parameters"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="response_parameters" label="">
+        <uni-easyinput placeholder="返回参数" v-model="formData.response_parameters"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="api_status" label="">
+        <uni-easyinput placeholder="接口状态" v-model="formData.api_status"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="error_info" label="">
+        <uni-easyinput placeholder="接口状态" v-model="formData.error_info"></uni-easyinput>
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">提交</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+          <button class="uni-button" style="width: 100px;">返回</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '../../js_sdk/validator/cyt-logs.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'cyt-logs';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  
+
+  export default {
+    data() {
+      let formData = {
+        "app_status": "",
+        "user_name": "",
+        "user_brand": "",
+        "user_model": "",
+        "api_src": "",
+        "request_parameters": "",
+        "response_parameters": "",
+        "api_status": "",
+        "error_info": ""
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onLoad(e) {
+      if (e.id) {
+        const id = e.id
+        this.formDataId = id
+        this.getDetail(id)
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      
+      /**
+       * 验证表单并提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          return this.submitForm(res)
+        }).catch(() => {
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      },
+
+      /**
+       * 提交表单
+       */
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        return db.collection(dbCollectionName).doc(this.formDataId).update(value).then((res) => {
+          uni.showToast({
+            title: '修改成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        })
+      },
+
+      /**
+       * 获取表单数据
+       * @param {Object} id
+       */
+      getDetail(id) {
+        uni.showLoading({
+          mask: true
+        })
+        db.collection(dbCollectionName).doc(id).field("app_status,user_name,user_brand,user_model,api_src,request_parameters,response_parameters,api_status,error_info").get().then((res) => {
+          const data = res.result.data[0]
+          if (data) {
+            this.formData = data
+            
+          }
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      }
+    }
+  }
+</script>

+ 240 - 0
pages/cyt-logs/list.vue

@@ -0,0 +1,240 @@
+<template>
+	<view>
+		<view class="uni-header">
+			<view class="uni-group">
+				<view class="uni-title"></view>
+				<view class="uni-sub-title"></view>
+			</view>
+			<view class="uni-group">
+				<input class="uni-search" type="text" v-model="query" @confirm="search" placeholder="请输入搜索内容" />
+				<button class="uni-button" type="default" size="mini" @click="search">搜索</button>
+				<button class="uni-button" type="default" size="mini" @click="navigateTo('./add')">新增</button>
+				<button class="uni-button" type="default" size="mini" :disabled="!selectedIndexs.length"
+					@click="delTable">批量删除</button>
+				<download-excel class="hide-on-phone" :fields="exportExcel.fields" :data="exportExcelData"
+					:type="exportExcel.type" :name="exportExcel.filename">
+					<button class="uni-button" type="primary" size="mini">导出 Excel</button>
+				</download-excel>
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" :collection="collectionList"
+				field="app_status,user_name,user_brand,user_model,api_src,request_parameters,response_parameters,api_status,error_info"
+				:where="where" page-data="replace" :orderby="orderby" :getcount="true" :page-size="options.pageSize"
+				:page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error,options}"
+				:options="options" loadtime="manual" @load="onqueryload">
+				<uni-table ref="table" :loading="loading" :emptyText="error.message || '没有更多数据'" border stripe
+					type="selection" @selection-change="selectionChange">
+					<uni-tr>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'app_status')"
+							sortable @sort-change="sortChange($event, 'app_status')">app_status</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'user_name')"
+							sortable @sort-change="sortChange($event, 'user_name')">user_name</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'user_brand')"
+							sortable @sort-change="sortChange($event, 'user_brand')">user_brand</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'user_model')"
+							sortable @sort-change="sortChange($event, 'user_model')">user_model</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'api_src')"
+							sortable @sort-change="sortChange($event, 'api_src')">api_src</uni-th>
+						<uni-th align="center" filter-type="search"
+							@filter-change="filterChange($event, 'request_parameters')" sortable
+							@sort-change="sortChange($event, 'request_parameters')">request_parameters</uni-th>
+						<uni-th align="center" filter-type="search"
+							@filter-change="filterChange($event, 'response_parameters')" sortable
+							@sort-change="sortChange($event, 'response_parameters')">response_parameters</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'api_status')"
+							sortable @sort-change="sortChange($event, 'api_status')">api_status</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'error_info')"
+							sortable @sort-change="sortChange($event, 'error_info')">error_info</uni-th>
+						<uni-th align="center">操作</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index">
+						<uni-td align="center">{{item.app_status}}</uni-td>
+						<uni-td align="center">{{item.user_name}}</uni-td>
+						<uni-td align="center">{{item.user_brand}}</uni-td>
+						<uni-td align="center">{{item.user_model}}</uni-td>
+						<uni-td align="center">{{item.api_src}}</uni-td>
+						<uni-td align="center">{{item.request_parameters}}</uni-td>
+						<uni-td align="center">{{item.response_parameters}} change()</uni-td>
+						<uni-td align="center">{{item.api_status}}</uni-td>
+						<uni-td align="center">{{item.error_info}}</uni-td>
+						<uni-td align="center">
+							<view class="uni-group">
+								<button @click="navigateTo('./edit?id='+item._id, false)" class="uni-button" size="mini"
+									type="primary">修改</button>
+								<button @click="confirmDelete(item._id)" class="uni-button" size="mini"
+									type="warn">删除</button>
+							</view>
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon :page-size="pagination.size" v-model="pagination.current"
+						:total="pagination.count" @change="onPageChanged" />
+				</view>
+			</unicloud-db>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		enumConverter,
+		filterToWhere
+	} from '../../js_sdk/validator/cyt-logs.js';
+
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbOrderBy = '' // 排序字段
+	const dbSearchFields = [] // 模糊搜索字段,支持模糊搜索的字段列表。联表查询格式: 主表字段名.副表字段名,例如用户表关联角色表 role.role_name
+	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	const orderByMapping = {
+		"ascending": "asc",
+		"descending": "desc"
+	}
+
+	export default {
+		data() {
+			return {
+				collectionList: "cyt-logs",
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				orderByFieldName: "",
+				selectedIndexs: [],
+				options: {
+					pageSize,
+					pageCurrent,
+					filterData: {},
+					...enumConverter
+				},
+				imageStyles: {
+					width: 64,
+					height: 64
+				},
+				exportExcel: {
+					"filename": "cyt-logs.xls",
+					"type": "xls",
+					"fields": {
+						"app_status": "app_status",
+						"user_name": "user_name",
+						"user_brand": "user_brand",
+						"user_model": "user_model",
+						"api_src": "api_src",
+						"request_parameters": "request_parameters",
+						"response_parameters": "response_parameters",
+						"api_status": "api_status",
+						"error_info": "error_info"
+					}
+				},
+				exportExcelData: []
+			}
+		},
+		onLoad() {
+			this._filter = {}
+		},
+		onReady() {
+			this.$refs.udb.loadData()
+		},
+		methods: {
+			onqueryload(data) {
+				this.exportExcelData = data
+			},
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				this.where = newWhere
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.selectedIndexs.length = 0
+				this.$refs.table.clearSelection()
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url, clear) {
+				// clear 表示刷新列表时是否清除页码,true 表示刷新并回到列表第 1 页,默认为 true
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData(clear)
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			// 批量删除
+			delTable() {
+				this.$refs.udb.remove(this.selectedItems(), {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				this.$refs.udb.remove(id, {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			sortChange(e, name) {
+				this.orderByFieldName = name;
+				if (e.order) {
+					this.orderby = name + ' ' + orderByMapping[e.order]
+				} else {
+					this.orderby = ''
+				}
+				this.$refs.table.clearSelection()
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			filterChange(e, name) {
+				this._filter[name] = {
+					type: e.filterType,
+					value: e.filter
+				}
+				let newWhere = filterToWhere(this._filter, db.command)
+				if (Object.keys(newWhere).length) {
+					this.where = newWhere
+				} else {
+					this.where = ''
+				}
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 111 - 0
pages/demo/icons/icons.vue

@@ -0,0 +1,111 @@
+<template>
+	<view>
+		<view class="uni-header">
+			<view class="uni-group">
+				<view class="uni-title">{{$t('demo.icons.title')}}(uni-icons)</view>
+				<view class="uni-sub-title">{{$t('demo.icons.describle')}}</view>
+			</view>
+		</view>
+		<view class="uni-container">
+			<view class="icons">
+				<view v-for="(icon,index) in icons" :key="index" class="icon-item pointer">
+					<view @click="setClipboardData('tag',icon)" :class="'uni-icons-'+icon"></view>
+					<text @click="setClipboardData('class',icon)" class="icon-text">uni-icons-{{icon}}</text>
+				</view>
+			</view>
+		</view>
+		<!-- #ifndef H5 -->
+		<fix-window v-if="fixWindow" />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import icons from './uni-icons.js'
+	export default {
+		data() {
+			return {
+				icons
+			}
+		},
+		props:{
+			tag: {
+				type: Boolean,
+				default: true
+			},
+			fixWindow: {
+				type: Boolean,
+				default: true
+			}
+		},
+		methods: {
+			setClipboardData(type, icon) {
+				let data = 'uni-icons-' + icon
+				if (this.tag && type === 'tag') {
+					data = '<view class="' + data + '"></view>'
+				}
+				uni.setClipboardData({
+					data,
+					success(res) {
+						uni.showToast({
+							icon: 'none',
+							title: '复制 ' + data + ' 成功!'
+						})
+					},
+					fail(res) {
+						uni.showModal({
+							content: '复制 ' + data + ' 失败!',
+							showCancel: false
+						})
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/* #ifndef H5 */
+	page {
+		padding-top: 85px;
+	}
+	/* #endif */
+	.icons {
+		display: flex;
+		flex-direction: row;
+		flex-wrap: wrap;
+	}
+
+	.icon-item {
+		display: flex;
+		width: 16.6%;
+		height: 120px;
+		font-size: 30px;
+		text-align: center;
+		justify-content: center;
+		align-items: center;
+		flex-direction: column;
+	}
+
+	.icon-item:hover,
+	.icon-item:hover .icon-text {
+		color: $uni-color-primary;
+	}
+
+	.icon-text {
+		color: #99a9bf;
+		font-size: 12px;
+		text-align: center;
+		height: 1em;
+		line-height: 1em;
+		margin-top: 15px;
+	}
+
+	/* #ifdef H5 */
+	@media only screen and (max-width: 500px) {
+		.icon-item {
+			width: 33.3%;
+		}
+	}
+	/* #endif */
+</style>

+ 132 - 0
pages/demo/icons/uni-icons.js

@@ -0,0 +1,132 @@
+export default [
+	'pulldown',
+	'refreshempty',
+	'back',
+	'forward',
+	'more',
+	'more-filled',
+	'scan',
+	'qq',
+	'weibo',
+	'weixin',
+	'pengyouquan',
+	'loop',
+	'refresh',
+	'refresh-filled',
+	'arrowthindown',
+	'arrowthinleft',
+	'arrowthinright',
+	'arrowthinup',
+	'undo-filled',
+	'undo',
+	'redo',
+	'redo-filled',
+	'bars',
+	'chatboxes',
+	'camera',
+	'chatboxes-filled',
+	'camera-filled',
+	'cart-filled',
+	'cart',
+	'checkbox-filled',
+	'checkbox',
+	'arrowleft',
+	'arrowdown',
+	'arrowright',
+	'smallcircle-filled',
+	'arrowup',
+	'circle',
+	'eye-filled',
+	'eye-slash-filled',
+	'eye-slash',
+	'eye',
+	'flag-filled',
+	'flag',
+	'gear-filled',
+	'reload',
+	'gear',
+	'hand-thumbsdown-filled',
+	'hand-thumbsdown',
+	'hand-thumbsup-filled',
+	'heart-filled',
+	'hand-thumbsup',
+	'heart',
+	'home',
+	'info',
+	'home-filled',
+	'info-filled',
+	'circle-filled',
+	'chat-filled',
+	'chat',
+	'mail-open-filled',
+	'email-filled',
+	'mail-open',
+	'email',
+	'checkmarkempty',
+	'list',
+	'locked-filled',
+	'locked',
+	'map-filled',
+	'map-pin',
+	'map-pin-ellipse',
+	'map',
+	'minus-filled',
+	'mic-filled',
+	'minus',
+	'micoff',
+	'mic',
+	'clear',
+	'smallcircle',
+	'close',
+	'closeempty',
+	'paperclip',
+	'paperplane',
+	'paperplane-filled',
+	'person-filled',
+	'contact-filled',
+	'person',
+	'contact',
+	'images-filled',
+	'phone',
+	'images',
+	'image',
+	'image-filled',
+	'location-filled',
+	'location',
+	'plus-filled',
+	'plus',
+	'plusempty',
+	'help-filled',
+	'help',
+	'navigate-filled',
+	'navigate',
+	'mic-slash-filled',
+	'search',
+	'settings',
+	'sound',
+	'sound-filled',
+	'spinner-cycle',
+	'download-filled',
+	'personadd-filled',
+	'videocam-filled',
+	'personadd',
+	'upload',
+	'upload-filled',
+	'starhalf',
+	'star-filled',
+	'star',
+	'trash',
+	'phone-filled',
+	'compose',
+	'videocam',
+	'trash-filled',
+	'download',
+	'chatbubble-filled',
+	'chatbubble',
+	'cloud-download',
+	'cloud-upload-filled',
+	'cloud-upload',
+	'cloud-download-filled',
+	'headphones',
+	'shop'
+]

+ 146 - 0
pages/demo/table/table.vue

@@ -0,0 +1,146 @@
+<template>
+    <view>
+        <view class="uni-header">
+            <view class="uni-group hide-on-phone">
+                <view class="uni-title">{{$t('demo.table.title')}}</view>
+            </view>
+            <view class="uni-group">
+				<input class="uni-search" type="text" v-model="searchVal" @confirm="search" :placeholder="$t('common.placeholder.query')" />
+				<button class="uni-button" type="default" size="mini" @click="search">{{$t('common.button.search')}}</button>
+				<button class="uni-button" type="primary" size="mini">{{$t('common.button.add')}}</button>
+				<button class="uni-button" type="warn" size="mini" @click="delTable">{{$t('common.button.batchDelete')}}</button>
+            </view>
+        </view>
+        <view class="uni-container">
+            <uni-table :loading="loading" border stripe type="selection" :emptyText="$t('common.empty')" @selection-change="selectionChange">
+                <uni-tr>
+                    <uni-th width="150" align="center">日期</uni-th>
+                    <uni-th width="150" align="center">姓名</uni-th>
+                    <uni-th align="center">地址</uni-th>
+                    <uni-th width="204" align="center">设置</uni-th>
+                </uni-tr>
+                <uni-tr v-for="(item ,index) in tableData" :key="index">
+                    <uni-td>{{item.date}}</uni-td>
+                    <uni-td>
+                        <view class="name">{{item.name}}</view>
+                    </uni-td>
+                    <uni-td>{{item.address}}</uni-td>
+                    <uni-td>
+                        <view class="uni-group">
+                            <button class="uni-button" size="mini" type="primary">{{$t('common.button.edit')}}</button>
+                            <button class="uni-button" size="mini" type="warn">{{$t('common.button.delete')}}</button>
+                        </view>
+                    </uni-td>
+                </uni-tr>
+            </uni-table>
+            <view class="uni-pagination-box">
+                <uni-pagination show-icon :page-size="pageSize" :current="pageCurrent" :total="total" @change="change" />
+            </view>
+        </view>
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+    </view>
+</template>
+
+<script>
+    import tableData from './tableData.js'
+    export default {
+        data() {
+            return {
+                searchVal: '',
+                tableData: [],
+                // 每页数据量
+                pageSize: 10,
+                // 当前页
+                pageCurrent: 1,
+                // 数据总量
+                total: 0,
+                loading: false
+            }
+        },
+        onLoad() {
+            this.selectedIndexs = []
+            this.getData(1)
+        },
+        methods: {
+            // 多选处理
+            selectedItems() {
+                return this.selectedIndexs.map(i => this.tableData[i])
+            },
+            // 多选
+            selectionChange(e) {
+                console.log(e.detail.index);
+                this.selectedIndexs = e.detail.index
+            },
+            //批量删除
+            delTable() {
+                console.log(this.selectedItems());
+            },
+            // 分页触发
+            change(e) {
+                this.getData(e.current)
+            },
+            // 搜索
+            search() {
+                this.getData(1, this.searchVal)
+            },
+            // 获取数据
+            getData(pageCurrent, value = "") {
+                this.loading = true
+                this.pageCurrent = pageCurrent
+                this.request({
+                    pageSize: this.pageSize,
+                    pageCurrent: pageCurrent,
+                    value: value,
+                    success: (res) => {
+                        // console.log('data', res);
+                        this.tableData = res.data
+                        this.total = res.total
+                        this.loading = false
+                    }
+                })
+            },
+            // 伪request请求
+            request(options) {
+                const {
+                    pageSize,
+                    pageCurrent,
+                    success,
+                    value
+                } = options
+                let total = tableData.length
+                let data = tableData.filter((item, index) => {
+                    const idx = index - (pageCurrent - 1) * pageSize
+                    return idx < pageSize && idx >= 0
+                })
+                if (value) {
+                    data = []
+                    tableData.forEach(item => {
+                        if (item.name.indexOf(value) !== -1) {
+                            data.push(item)
+                        }
+                    })
+                    total = data.length
+                }
+
+                setTimeout(() => {
+                    typeof success === 'function' && success({
+                        data: data,
+                        total: total
+                    })
+                }, 500)
+
+            }
+
+        }
+    }
+</script>
+
+<style>
+	/* #ifndef H5 */
+	page {
+		padding-top: 85px;
+	}
+	/* #endif */
+</style>

+ 193 - 0
pages/demo/table/tableData.js

@@ -0,0 +1,193 @@
+export default [{
+    "date": "2020-09-01",
+    "name": "Dcloud1",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-02",
+    "name": "Dcloud2",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-03",
+    "name": "Dcloud3",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-04",
+    "name": "Dcloud4",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-05",
+    "name": "Dcloud5",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-06",
+    "name": "Dcloud6",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-07",
+    "name": "Dcloud7",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-08",
+    "name": "Dcloud8",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-09",
+    "name": "Dcloud9",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-10",
+    "name": "Dcloud10",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-11",
+    "name": "Dcloud11",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-12",
+    "name": "Dcloud12",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-13",
+    "name": "Dcloud13",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-14",
+    "name": "Dcloud14",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-15",
+    "name": "Dcloud15",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-16",
+    "name": "Dcloud16",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-01",
+    "name": "Dcloud17",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-02",
+    "name": "Dcloud18",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-03",
+    "name": "Dcloud19",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-04",
+    "name": "Dcloud20",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-05",
+    "name": "Dcloud21",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-06",
+    "name": "Dcloud22",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-07",
+    "name": "Dcloud23",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-08",
+    "name": "Dcloud24",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-09",
+    "name": "Dcloud25",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-10",
+    "name": "Dcloud26",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-11",
+    "name": "Dcloud27",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-12",
+    "name": "Dcloud28",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-13",
+    "name": "Dcloud29",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-14",
+    "name": "Dcloud30",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-15",
+    "name": "Dcloud31",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-16",
+    "name": "Dcloud32",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-01",
+    "name": "Dcloud33",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-02",
+    "name": "Dcloud34",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-03",
+    "name": "Dcloud35",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-04",
+    "name": "Dcloud36",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-05",
+    "name": "Dcloud37",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-06",
+    "name": "Dcloud38",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-07",
+    "name": "Dcloud39",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-08",
+    "name": "Dcloud40",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-09",
+    "name": "Dcloud41",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-10",
+    "name": "Dcloud42",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-11",
+    "name": "Dcloud43",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-12",
+    "name": "Dcloud44",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}, {
+    "date": "2020-09-13",
+    "name": "Dcloud45",
+    "address": "上海市普陀区金沙江路 1518 弄"
+}, {
+    "date": "2020-09-14",
+    "name": "Dcloud46",
+    "address": "上海市普陀区金沙江路 1517 弄"
+}, {
+    "date": "2020-09-15",
+    "name": "Dcloud47",
+    "address": "上海市普陀区金沙江路 1519 弄"
+}, {
+    "date": "2020-09-16",
+    "name": "Dcloud48",
+    "address": "上海市普陀区金沙江路 1516 弄"
+}]

+ 41 - 0
pages/error/404.vue

@@ -0,0 +1,41 @@
+<template>
+    <view>
+        <view>
+            <text style="font-size: 25px;color: #333;">
+                404 Page Not Found
+            </text>
+        </view>
+        <view>
+            <text style="font-size: 18px;color: #999;">
+                {{errMsg}}
+            </text>
+        </view>
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+    </view>
+</template>
+
+<script>
+    export default {
+        data() {
+            return {
+
+            }
+        },
+        onLoad(query) {
+            this.errMsg = query.errMsg || ''
+        },
+        methods: {
+
+        }
+    }
+</script>
+
+<style>
+	/* #ifndef H5 */
+	page {
+		padding-top: 85px;
+	}
+	/* #endif */
+</style>

+ 82 - 0
pages/index/fieldsMap.js

@@ -0,0 +1,82 @@
+const deviceFeildsMap = [{
+	value: '今天',
+	contrast: '昨天'
+}, {
+	field: 'appid',
+	title: 'APPID',
+	tooltip: '',
+}, {
+	field: 'name',
+	title: '应用名',
+	tooltip: '',
+}, {
+	field: 'total_devices',
+	title: '总设备数',
+	tooltip: '从添加统计到当前选择时间的总设备数(去重)',
+	value: 0,
+	contrast: 0,
+}, {
+	field: 'new_device_count',
+	title: '新增设备',
+	tooltip: '首次访问应用的设备数(以设备为判断标准,去重)',
+	value: 0,
+	contrast: 0
+}, {
+	field: 'active_device_count',
+	title: '活跃设备',
+	tooltip: '访问过应用内任意页面的总设备数(去重)',
+	value: 0,
+	contrast: 0
+},
+// {
+// 	field: 'page_visit_count',
+// 	title: '访问次数',
+// 	tooltip: '访问过应用内任意页面总次数,多个页面之间跳转、同一页面的重复访问计为多次访问',
+// 	value: 0,
+// 	contrast: 0
+// }
+]
+
+const userFeildsMap = [{
+	value: '今天',
+	contrast: '昨天'
+}, {
+	field: 'appid',
+	title: 'APPID',
+	tooltip: '',
+}, {
+	field: 'name',
+	title: '应用名',
+	tooltip: '',
+}, {
+	field: 'total_users',
+	title: '总用户数',
+	tooltip: '从添加统计到当前选择时间的总用户数(去重)',
+	value: 0,
+	contrast: 0,
+}, {
+	field: 'new_user_count',
+	title: '新增用户',
+	tooltip: '首次访问应用的用户数(以用户为判断标准,去重)',
+	value: 0,
+	contrast: 0
+}, {
+	field: 'active_user_count',
+	title: '活跃用户',
+	tooltip: '访问过应用内任意页面的总用户数(去重)',
+	value: 0,
+	contrast: 0
+},
+// {
+// 	field: 'page_visit_count',
+// 	title: '访问次数',
+// 	tooltip: '访问过应用内任意页面总次数,多个页面之间跳转、同一页面的重复访问计为多次访问',
+// 	value: 0,
+// 	co\rast: 0
+// }
+]
+
+export {
+	deviceFeildsMap,
+	userFeildsMap
+}

+ 296 - 0
pages/index/index.vue

@@ -0,0 +1,296 @@
+<template>
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<view class="uni-sub-title hide-on-phone"></view>
+			</view>
+		</view>
+		<view class="uni-container">
+			<uni-notice-bar v-if="!deviceTableData.length && !userTableData.length && !query.platform_id" showGetMore
+				showIcon class="mb-m pointer" text="暂无数据, 统计相关功能需开通 uni 统计后才能使用, 如未开通, 点击查看具体流程"
+				@click="navTo('https://uniapp.dcloud.io/uni-stat-v2.html')" />
+			<view class="uni-stat--x mb-m">
+				<uni-stat-tabs label="平台选择" type="boldLine" mode="platform" v-model="query.platform_id" />
+			</view>
+			<!-- <uni-stat-panel :items="panelData" :contrast="true" /> -->
+			<view class="uni-stat--x p-m">
+				<view class="uni-stat-card-header">设备概览</view>
+				<uni-table :loading="loading" border stripe emptyText="暂无数据">
+					<uni-tr>
+						<!-- <uni-th align="center">操作</uni-th> -->
+						<template v-for="(mapper, index) in deviceTableFields">
+							<uni-th v-if="mapper.title" :key="index" align="center">
+								{{mapper.title}}
+							</uni-th>
+						</template>
+					</uni-tr>
+					<uni-tr v-for="(item ,i) in deviceTableData" :key="i">
+						<template v-for="(mapper, index) in deviceTableFields">
+							<uni-td v-if="mapper.field === 'appid'" align="center">
+								<view v-if="item.appid" @click="navTo('/pages/uni-stat/device/overview/overview', item.appid)"
+									class="link-btn-color">
+									{{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
+								</view>
+								<view v-else @click="navTo('/pages/system/app/add')" class="link-btn-color">
+									需添加此应用的 appid
+								</view>
+							</uni-td>
+							<uni-td v-else :key="index" align="center">
+								{{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
+							</uni-td>
+						</template>
+					</uni-tr>
+				</uni-table>
+			</view>
+			<view class="uni-stat--x p-m">
+				<view class="uni-stat-card-header">注册用户概览</view>
+				<uni-table :loading="loading" border stripe emptyText="暂无数据">
+					<uni-tr>
+						<template v-for="(mapper, index) in userTableFields">
+							<uni-th v-if="mapper.title" :key="index" align="center">
+								{{mapper.title}}
+							</uni-th>
+						</template>
+					</uni-tr>
+					<uni-tr v-for="(item ,i) in userTableData" :key="i">
+						<template v-for="(mapper, index) in userTableFields">
+							<uni-td v-if="mapper.field === 'appid'" align="center">
+								<view v-if="item.appid" @click="navTo('/pages/uni-stat/user/overview/overview', item.appid)"
+									class="link-btn-color">
+									{{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
+								</view>
+								<view v-else @click="navTo('/pages/system/app/add')" class="link-btn-color">
+									需添加此应用的 appid
+								</view>
+							</uni-td>
+							<uni-td v-else :key="index" align="center">
+								{{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
+							</uni-td>
+						</template>
+					</uni-tr>
+				</uni-table>
+			</view>
+			<!-- <view class="uni-pagination-box">
+				<uni-pagination show-icon :page-size="pageSize" :current="pageCurrent" :total="tableData.length" />
+			</view> -->
+		</view>
+
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import {
+		stringifyQuery,
+		stringifyField,
+		stringifyGroupField,
+		getTimeOfSomeDayAgo,
+		division,
+		format,
+		parseDateTime,
+		getFieldTotal
+	} from '@/js_sdk/uni-stat/util.js'
+
+	import {
+		deviceFeildsMap,
+		userFeildsMap
+	} from './fieldsMap.js'
+
+	export default {
+		data() {
+			return {
+				query: {
+					platform_id: '',
+					start_time: [getTimeOfSomeDayAgo(1), new Date().getTime()]
+				},
+				deviceTableData: [],
+				userTableData: [],
+				// panelData: panelOption,
+				// 每页数据量
+				pageSize: 10,
+				// 当前页
+				pageCurrent: 1,
+				// 数据总量
+				total: 0,
+				loading: false,
+				// fieldsMap,
+			}
+		},
+		onReady() {
+			this.getApps(this.queryStr, deviceFeildsMap, 'device')
+			this.getApps(this.queryStr, userFeildsMap, 'user')
+		},
+		watch: {
+			query: {
+				deep: true,
+				handler(newVal) {
+					this.getApps(this.queryStr, deviceFeildsMap, 'device')
+					this.getApps(this.queryStr, userFeildsMap, 'user')
+				}
+			}
+		},
+		computed: {
+			queryStr() {
+				const defQuery = `(dimension == "hour" || dimension == "day")`
+				return stringifyQuery(this.query) + ' && ' + defQuery
+			},
+
+			deviceTableFields() {
+				return this.tableFieldsMap(deviceFeildsMap)
+			},
+			userTableFields() {
+				return this.tableFieldsMap(userFeildsMap)
+			}
+		},
+		methods: {
+			tableFieldsMap(fieldsMap) {
+				let tableFields = []
+				const today = []
+				const yesterday = []
+				const other = []
+				for (const mapper of fieldsMap) {
+					if (mapper.field) {
+						if (mapper.hasOwnProperty('value')) {
+							const t = JSON.parse(JSON.stringify(mapper))
+							const y = JSON.parse(JSON.stringify(mapper))
+							if (mapper.field !== 'total_users' && mapper.field !== 'total_devices') {
+								t.title = '今日' + mapper.title
+								t.field = mapper.field + '_value'
+								y.title = '昨日' + mapper.title
+								y.field = mapper.field + '_contrast'
+								today.push(t)
+								yesterday.push(y)
+							} else {
+								t.field = mapper.field + '_value'
+								other.push(t)
+							}
+						} else {
+							tableFields.push(mapper)
+						}
+					}
+				}
+				tableFields = [...tableFields, ...today, ...yesterday, ...other]
+				return tableFields
+			},
+
+			getApps(query, fieldsMap, type = "device") {
+				this.loading = true
+				const db = uniCloud.database()
+				const appList = db.collection('opendb-app-list').getTemp()
+				const appDaily = db.collection('uni-stat-result')
+					.where(query)
+					.getTemp()
+
+				db.collection(appDaily, appList)
+					.field(
+						`${stringifyField(fieldsMap, '', 'value')},stat_date,appid,dimension`
+					)
+					.groupBy(`appid,dimension,stat_date`)
+					.groupField(stringifyGroupField(fieldsMap, '', 'value'))
+					.orderBy(`appid`, 'desc')
+					.get()
+					.then((res) => {
+						let {
+							data
+						} = res.result
+						this[`${type}TableData`] = []
+						if (!data.length) return
+						let appids = [],
+							todays = [],
+							yesterdays = [],
+							isToday = parseDateTime(getTimeOfSomeDayAgo(0), '', ''),
+							isYesterday = parseDateTime(getTimeOfSomeDayAgo(1), '', '')
+						for (const item of data) {
+							const {
+								appid,
+								name
+							} = item.appid && item.appid[0] || {}
+							item.appid = appid
+							item.name = name
+
+							if (appids.indexOf(item.appid) < 0) {
+								appids.push(item.appid)
+							}
+							if (item.dimension === 'hour' && item.stat_date === isToday) {
+								todays.push(item)
+							}
+							if (item.dimension === 'day' && item.stat_date === isYesterday) {
+								yesterdays.push(item)
+							}
+						}
+						const keys = fieldsMap.map(f => f.field).filter(Boolean)
+						for (const appid of appids) {
+							const rowData = {}
+							const t = todays.find(item => item.appid === appid)
+							const y = yesterdays.find(item => item.appid === appid)
+							for (const key of keys) {
+								if (key === 'appid' || key === 'name') {
+									rowData[key] = t && t[key]
+								} else {
+									const value = t && t[key]
+									const contrast = y && y[key]
+									rowData[key + '_value'] = format(value)
+									rowData[key + '_contrast'] = format(contrast)
+								}
+							}
+							this[`${type}TableData`].push(rowData)
+
+							if (appid) {
+								// total_users 不准确,置空后由 getFieldTotal 处理, appid 不存在时暂不处理
+								t[`total_${type}s`] = 0
+								const query = JSON.parse(JSON.stringify(this.query))
+								query.start_time = [getTimeOfSomeDayAgo(0), new Date().getTime()]
+								query.appid = appid
+								getFieldTotal.call(this, query, `total_${type}s`).then(total => {
+									this[`${type}TableData`].find(item => item.appid === appid)[
+										`total_${type}s_value`] = total
+								})
+							}
+						}
+					}).catch((err) => {
+						console.error(err)
+						// err.message 错误信息
+						// err.code 错误码
+					}).finally(() => {
+						this.loading = false
+					})
+			},
+
+			navTo(url, id) {
+				if (url.indexOf('http') > -1) {
+					window.open(url)
+				} else {
+					if (id) {
+						url = `${url}?appid=${id}`
+					}
+					uni.navigateTo({
+						url
+					})
+				}
+			}
+		}
+
+	}
+</script>
+
+<style>
+	.uni-stat-card-header {
+		display: flex;
+		justify-content: space-between;
+		color: #555;
+		font-size: 14px;
+		font-weight: 600;
+		padding: 10px 0;
+		margin-bottom: 15px;
+	}
+	.uni-table-scroll {
+		min-height: auto;
+	}
+	.link-btn-color {
+		color: #007AFF;
+		cursor: pointer;
+	}
+</style>

+ 123 - 0
pages/opendb-admin-log/list.vue

@@ -0,0 +1,123 @@
+<template>
+	<view>
+		<view class="uni-header">
+			<view class="uni-group">
+				<view class="uni-title"></view>
+				<view class="uni-sub-title"></view>
+			</view>
+			<view class="uni-group">
+				<input class="uni-search" type="text" v-model="query" placeholder="用户/内容" />
+				<button class="uni-button" type="default" size="mini" @click="search">搜索</button>
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" :collection="collectionName" :options="options" :where="where" page-data="replace" :orderby="orderby"
+			 :getcount="true" :page-size="options.pageSize" :page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error}">
+				<uni-table :loading="loading" :emptyText="error.message || '没有更多数据'" border stripe >
+					<uni-tr>
+						<uni-th align="center">序号</uni-th>
+						<uni-th align="center">用户</uni-th>
+						<uni-th align="center">内容</uni-th>
+						<uni-th align="center">IP</uni-th>
+						<uni-th align="center">时间</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index">
+						<uni-td align="center">{{(pagination.current -1)*pagination.size + (index+1)}}</uni-td>
+						<uni-td align="center">{{item.user_name}}</uni-td>
+						<uni-td align="center">{{item.content}}</uni-td>
+						<uni-td align="center">{{item.ip}}</uni-td>
+						<uni-td align="center">
+							<uni-dateformat :date="item.create_date" :threshold="[0, 0]" />
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon :page-size="pagination.size" v-model="pagination.current" :total="pagination.count"
+					 @change="onPageChanged" />
+				</view>
+			</unicloud-db>
+		</view>
+	</view>
+</template>
+
+<script>
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbCollectionName = 'opendb-admin-log'
+	const dbOrderBy = 'create_date' // 排序字段
+	const dbSearchFields = ["user_name","content"] // 支持模糊搜索的字段列表
+	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				collectionName: dbCollectionName,
+				options: {
+					pageSize,
+					pageCurrent
+				}
+			}
+		},
+		methods: {
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				const isSameWhere = newWhere === this.where
+				this.where = newWhere
+				if (isSameWhere) { // 相同条件时,手动强制刷新
+					this.loadData()
+				}
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url) {
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData()
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			//批量删除
+			delTable() {
+				this.$refs.udb.remove(this.selectedItems())
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				this.$refs.udb.remove(id)
+			}
+		}
+	}
+</script>
+<style>
+</style>

+ 490 - 0
pages/system/app/add.vue

@@ -0,0 +1,490 @@
+<template>
+	<view class="uni-container">
+		<uni-notice-bar style="margin: 0;" showIcon text="本页面信息,在应用发布、app升级模块中,都会关联使用,请认真填写" />
+		<uni-forms ref="form" v-model="formData" validateTrigger="bind" style="max-width: 792px;"
+			:labelWidth="labelWidth" :rules="rules">
+			<uni-card title="基础信息">
+				<uni-forms-item class="forn-item__flex" name="appid" label="AppID" required>
+					<uni-easyinput :disabled="isEdit" placeholder="应用的AppID" v-model="formData.appid" trim="both">
+					</uni-easyinput>
+				</uni-forms-item>
+				<uni-forms-item name="name" label="应用名称" required>
+					<uni-easyinput :disabled="isEdit" placeholder="应用名称" v-model="formData.name" trim="both">
+					</uni-easyinput>
+				</uni-forms-item>
+				<uni-forms-item name="introduction" label="应用简介">
+					<uni-easyinput placeholder="应用简介" v-model="formData.introduction" trim="both"></uni-easyinput>
+				</uni-forms-item>
+				<uni-forms-item name="description" label="应用描述">
+					<textarea :maxlength="-1" auto-height placeholder="应用描述"
+						@input="binddata('description', $event.detail.value)" class="uni-textarea-border"
+						v-model="formData.description"></textarea>
+				</uni-forms-item>
+			</uni-card>
+
+			<uni-card title="图标素材">
+				<uni-forms-item label="应用图标">
+					<uni-file-picker v-model="middleware_img.icon_url" :image-styles="{'width' : '200rpx'}"
+						return-type="object" file-mediatype="image" limit="1" mode="grid"
+						@success="(res) => iconUrlSuccess(res,'icon_url')"
+						@delete="(res) => iconUrlDelete(res,'icon_url')">
+					</uni-file-picker>
+				</uni-forms-item>
+				<uni-forms-item label="应用截图">
+					<uni-file-picker v-model="screenshotList" file-mediatype="image" mode="grid"
+						:image-styles="{'height': '500rpx','width' : '300rpx'}" @delete="iconUrlDelete">
+					</uni-file-picker>
+				</uni-forms-item>
+			</uni-card>
+
+			<uni-card class="app_platform" title="App 信息">
+				<view v-if="isEdit" class="extra-button">
+					<button type="primary" plain size="mini" @click="autoFillApp">自动填充</button>
+					<show-info :left="-10" :top="-35" width="230" content="从App升级中心同步应用安装包信息" />
+				</view>
+				<view v-for="item in appPlatformKeys" :key="item">
+					<checkbox-group @change="({detail:{value}}) => {setPlatformChcekbox(item,!!value.length)}">
+						<label class="title_padding" :class="{'font_bold':getPlatformChcekbox(item)}">
+							<checkbox :value="item" :checked="middleware_checkbox[item]" />
+							<text>{{appPlatformValues[item]}}</text>
+						</label>
+					</checkbox-group>
+					<template v-if="getPlatformChcekbox(item)">
+						<uni-forms-item label="名称">
+							<uni-easyinput v-model="formData[item].name" trim="both"></uni-easyinput>
+						</uni-forms-item>
+						<uni-forms-item class="forn-item__flex" v-if="item === 'app_android'" label="上传apk包">
+							<uni-file-picker v-model="appPackageInfo" file-extname="apk" :disabled="hasPackage"
+								returnType="object" file-mediatype="all" limit="1"
+								@success="(res) => iconUrlSuccess(res, `${item}.url`)"
+								@delete="(res) => iconUrlDelete(res,`${item}.url`)" style="flex:1;">
+								<view class="flex">
+									<button type="primary" size="mini" @click="selectFile"
+										style="margin: 0;">选择文件</button>
+									<text style="padding: 10px;font-size: 12px;color: #666;">
+										上传apk到当前服务空间的云存储中,上传成功后,会自动使用云存储地址填充下载链接
+									</text>
+								</view>
+							</uni-file-picker>
+							<text v-if="hasPackage" style="padding-left: 20px;color: #a8a8a8;">
+								{{appPackageInfo.size && Number(appPackageInfo.size / 1024 / 1024).toFixed(2) + 'M'}}
+							</text>
+						</uni-forms-item>
+						<uni-forms-item :label="item === 'app_ios' ? 'AppStore' : '下载链接'">
+							<uni-easyinput :maxlength="-1" v-model="formData[item].url" trim="both"></uni-easyinput>
+						</uni-forms-item>
+					</template>
+				</view>
+
+				<uni-popup ref="scheme" background-color="#fff">
+					<view class="popup-content">
+						<text style="font-size: 15px;font-weight: bold;">
+							常见的应用商店 scheme 地址
+						</text>
+						<view></view>
+						<text>
+							应用宝:tmast://appdetails?r=XXX&pname=xxx;
+							小米:mimarket://details?id=com.xx.xx;
+							三星:samsungapps://ProductDetail/com.xx.xx;
+							华为:appmarket://details?id=com.xx.xx;
+							oppo:oppomarket://details?packagename=com.xx.xx;
+							vivo:vivomarket://details?id=com.xx.xx;
+						</text>
+					</view>
+				</uni-popup>
+				<uni-forms-item name="store_schemes" label="Android应用市场" labelWidth="120">
+					<view style="height: 100%;">
+						<view class="flex" style="justify-content: end;">
+							<text class="pointer"
+								style="text-decoration: underline;color: #666;font-size: 12px;padding-left: 10rpx;"
+								@click="schemeDemo">常见应用商店schema汇总</text>
+							<button type="primary" size="mini" @click="addStoreScheme"
+								style="margin: 0 0 0 10px;">新增</button>
+						</view>
+
+						<view v-for="(item,index) in formData.store_list" :key="item.id">
+							<uni-card title="" style="margin: 20px 0px 0px 0px;">
+								<view style="display: flex;">
+									<view style="padding-left: 10px;">
+										<button type="warn" size="mini" @click="deleteStore(index, item)">删除</button>
+									</view>
+								</view>
+								<uni-forms-item label="商店名称">
+									<uni-easyinput v-model="item.name" trim="both"></uni-easyinput>
+								</uni-forms-item>
+								<uni-forms-item label="Scheme">
+									<uni-easyinput :maxlength="-1" v-model="item.scheme" trim="both"></uni-easyinput>
+								</uni-forms-item>
+							</uni-card>
+						</view>
+					</view>
+				</uni-forms-item>
+			</uni-card>
+
+			<uni-card class="mp_platform" title="小程序/快应用信息">
+				<!-- <view class="extra-button">
+					<button type="primary" plain size="mini" @click="mpAccordion">{{mpExtra}}</button>
+					<show-info :left="40" :top="-20" content="折叠状态下,即使勾选也不会显示详情" />
+				</view> -->
+				<view v-for="item in mpPlatformKeys" :key="item">
+					<checkbox-group @change="({detail:{value}}) => {setPlatformChcekbox(item,!!value.length)}">
+						<label class="title_padding" :class="{'font_bold':getPlatformChcekbox(item)}">
+							<checkbox :value="item" :checked="middleware_checkbox[item]" />
+							<text>{{mpPlatform[item]}}</text>
+						</label>
+					</checkbox-group>
+					<template v-if="mpAccordionStatus && getPlatformChcekbox(item)">
+						<uni-forms-item label="名称">
+							<uni-easyinput v-model="formData[item].name" trim="both"></uni-easyinput>
+						</uni-forms-item>
+						<uni-forms-item :label="mpPlatform[item].slice(-3) + '码'">
+							<uni-file-picker v-model="middleware_img[item]" :image-styles="{'width' : '200rpx'}"
+								return-type="object" file-mediatype="image" limit="1" mode="grid"
+								@success="(res) => iconUrlSuccess(res, `${item}.qrcode_url`)"
+								@delete="(res) => iconUrlDelete(res, `${item}.qrcode_url`)">
+							</uni-file-picker>
+						</uni-forms-item>
+					</template>
+				</view>
+			</uni-card>
+
+			<uni-card title="web信息">
+				<uni-forms-item label="链接地址">
+					<uni-easyinput :maxlength="-1" v-model="formData.h5.url" trim="both"></uni-easyinput>
+					<span style="font-size: 13px; color: #999;">如需免费的前端网页托管,请开通 <a style="color: inherit;"
+							href="https://unicloud.dcloud.net.cn">uniCloud</a> ,创建服务空间,并在 “前端网页托管”
+						里上传你的网页</span>
+				</uni-forms-item>
+			</uni-card>
+
+			<uni-card :isShadow="false" v-if="isEdit">
+				<text><text style="font-weight: bold;">提示:</text>保存后需重新生成发布页</text>
+			</uni-card>
+
+			<view class="uni-button-group">
+				<button type="primary" class="uni-button" style="width: 100px;" @click="submit">保存</button>
+				<navigator open-type="navigateBack" style="margin-left: 15px;">
+					<button class="uni-button" style="width: 100px;">返回</button>
+				</navigator>
+			</view>
+		</uni-forms>
+	</view>
+</template>
+
+<script>
+	import mixin from './mixin/publish_add_detail_mixin.js';
+
+	const db = uniCloud.database();
+	const dbCmd = db.command;
+	const dbCollectionName = 'opendb-app-list';
+
+	function randomString(len) {
+		//设定要生成的字符串包含的字符
+		var array = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+			'U', 'V', 'W', 'X', 'Y', 'Z'
+		];
+		var result = '';
+		for (var i = 0; i < len; i++) {
+			result += array[Math.floor(Math.random() * 26)];
+		}
+		return result;
+	}
+
+	export default {
+		mixins: [mixin],
+		data() {
+			return {
+				mpExtra: ' ',
+				mpAccordionStatus: 1,
+				labelWidth: '80px'
+			}
+		},
+		onLoad(e) {
+			if (e.id) {
+				this.isEdit = true
+				uni.setNavigationBarTitle({
+					title: '修改应用'
+				})
+				// this.formDataId = e.id
+				this.setFormData('appid', e.id)
+				this.getDetail(e.id)
+			} else {
+				// 填写应用名称后,给各平台设置相同的名称
+				this.$watch('formData.name', (name) => {
+					this.platFormKeys.forEach(key => {
+						this.setFormData(`${key}.name`, name)
+					})
+				})
+			}
+		},
+		onReady() {
+			this.mpExtra = '折叠'
+		},
+		methods: {
+			// 更新线上版本的 store 记录
+			resolvestableVersionStoreList() {
+				const modifiedMap = {}
+				const modifiedKeys = []
+				this.formData.store_list.forEach((item, index) => {
+					modifiedKeys.push(item.id)
+					modifiedMap[item.id] = index
+				})
+
+				return this.fetchAppInfo(this.getFormData('appid'), 'Android')
+					.then(res => {
+						if (!res.success) return
+						if (res.store_list) {
+							const originalMap = {}
+							const originalKeys = []
+							res.store_list.forEach((item, index) => {
+								originalKeys.push(item.id)
+								originalMap[item.id] = index
+							})
+
+							modifiedKeys.forEach((key, index) => {
+								const afterItem = this.formData.store_list[modifiedMap[key]]
+								// 新增
+								if (originalKeys.indexOf(key) === -1) {
+									res.store_list.push(afterItem)
+								} else {
+									// 修改
+									res.store_list[originalMap[key]].name = afterItem.name
+									res.store_list[originalMap[key]].scheme = afterItem.scheme
+								}
+							})
+
+							// 删除
+							for (let i = 0; i < res.store_list.length; i++) {
+								let id = res.store_list[i].id
+								if (this.deletedStore.indexOf(id) !== -1 && modifiedKeys.indexOf(id) === -1) {
+									res.store_list.splice(i, 1)
+									i--
+								}
+							}
+						} else {
+							res.store_list = this.formData.store_list
+						}
+
+						return this.updateAppVersion(res._id, {
+							store_list: res.store_list
+						})
+					})
+			},
+			updateAppVersion(id, value) {
+				return db.collection('opendb-app-versions').doc(id).update(value)
+			},
+			/**
+			 * 验证表单并提交
+			 */
+			submit() {
+				uni.showLoading({
+					mask: true
+				})
+
+				this.formatFormData()
+
+				this.$refs.form.validate(this.keepItems).then((res) => {
+					return this.submitForm(res)
+				}).catch((err) => {
+					console.error(err)
+				}).finally(() => {
+					uni.hideLoading()
+				})
+			},
+			/**
+			 * 提交表单
+			 */
+			submitForm(value) {
+				// 使用 clientDB 提交数据
+				(
+					this.isEdit ?
+					this.requestCloudFunction('setNewAppData', {
+						id: this.formDataId,
+						value
+					}) :
+					db.collection(dbCollectionName).add(value)
+				)
+				.then((res) => {
+						if (this.isEdit) return this.resolvestableVersionStoreList()
+					})
+					.then(() => {
+						uni.showToast({
+							title: `${this.isEdit ? '更新' : '新增'}成功`
+						})
+						this.getOpenerEventChannel().emit('refreshData')
+						setTimeout(() => uni.navigateBack(), 500)
+					})
+					.catch((err) => {
+						uni.showModal({
+							content: err.message || '请求服务失败',
+							showCancel: false
+						})
+					})
+			},
+			/**
+			 * 获取表单数据
+			 * @param {Object} id
+			 */
+			getDetail(id) {
+				uni.showLoading({
+					mask: true
+				})
+				db.collection(dbCollectionName).where({
+					appid: id
+				}).get().then((res) => {
+					const data = res.result.data[0]
+					if (data) {
+						this.formDataId = data._id
+						this.initFormData(data)
+					} else {
+						this.autoFill()
+						this.autoFillApp()
+					}
+				}).catch((err) => {
+					uni.showModal({
+						content: err.message || '请求服务失败',
+						showCancel: false
+					})
+				}).finally(() => {
+					uni.hideLoading()
+				})
+			},
+			mpAccordion() {
+				if (this.mpAccordionStatus) {
+					this.mpExtra = '展开'
+					this.mpAccordionStatus = 0
+				} else {
+					this.mpExtra = '折叠'
+					this.mpAccordionStatus = 1
+				}
+			},
+			addStoreScheme() {
+				this.formData.store_list.push({
+					enable: false,
+					priority: 0,
+					id: randomString(5) + '_' + Date.now()
+				})
+			},
+			deleteStore(index, item) {
+				if (item.scheme && item.scheme.trim().length && this.isEdit) {
+					uni.showModal({
+						content: '是否同步删除线上版本此条商店记录?',
+						success: (res) => {
+							const storeItem = this.formData.store_list.splice(index, 1)[0]
+							if (storeItem && res.confirm) {
+								this.deletedStore.push(storeItem.id)
+							}
+						}
+					})
+				} else {
+					this.formData.store_list.splice(index, 1)[0]
+				}
+			},
+			schemeDemo() {
+				// #ifndef H5
+				$refs.scheme.open('center')
+				// #endif
+				// #ifdef H5
+				window.open("https://ask.dcloud.net.cn/article/39960", '_blank')
+				// #endif
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.title_padding {
+		padding-bottom: 15px;
+		display: block;
+	}
+
+	.font_bold {
+		font-weight: bold;
+	}
+
+	.uni-button-group {
+		& button {
+			margin-left: 15px;
+		}
+
+		& button:first-child {
+			margin-left: 0px;
+		}
+	}
+
+	::v-deep {
+		.forn-item__flex {
+			.uni-forms-item__content {
+				display: flex;
+				align-items: center;
+
+				.custom-button {
+					height: 100%;
+					margin-left: 10rpx;
+					line-height: 36px;
+				}
+			}
+		}
+
+		.uni-card {
+			padding: 0 !important;
+			cursor: auto;
+		}
+
+		.uni-card__header {
+			background-color: #eee;
+		}
+
+		.uni-card__header-title-text {
+			font-weight: bold;
+		}
+	}
+
+	.extra-button {
+		display: flex;
+		align-items: center;
+		margin-bottom: 15px;
+
+		button {
+			margin: 0;
+		}
+	}
+
+	.flex-center-r {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tip {
+		display: flex;
+		flex-direction: column;
+		align-items: flex-start;
+		background-color: #f3f5f7;
+		color: #2c3e50;
+		padding: 10px;
+		font-size: 32rpx;
+
+		border: {
+			color: #e96900;
+			left-width: 8px;
+			left-style: solid;
+		}
+
+		text {
+			margin-right: 15px;
+		}
+
+		.custom-button {
+			margin-left: 0px;
+		}
+	}
+
+	.popup-content {
+		padding: 30rpx;
+	}
+
+	::v-deep .uni-file-picker__files {
+		max-width: 100%;
+	}
+</style>

+ 293 - 0
pages/system/app/list.vue

@@ -0,0 +1,293 @@
+<template>
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<input class="uni-search" type="text" v-model="query" @confirm="search"
+					:placeholder="$t('common.placeholder.query')" />
+				<button class="uni-button hide-on-phone" type="default" size="mini"
+					@click="search">{{$t('common.button.search')}}</button>
+				<button class="uni-button" type="primary" size="mini"
+					@click="navigateTo('./add')">{{$t('common.button.add')}}</button>
+				<button class="uni-button" type="warn" size="mini" :disabled="!selectedIndexs.length"
+					@click="delTable">{{$t('common.button.batchDelete')}}</button>
+				<!-- #ifdef H5 -->
+				<!-- #ifndef VUE3 -->
+				<download-excel class="hide-on-phone" :fields="exportExcel.fields" :data="exportExcelData"
+					:type="exportExcel.type" :name="exportExcel.filename">
+					<button class="uni-button" type="primary" size="mini">{{$t('common.button.exportExcel')}}</button>
+				</download-excel>
+				<!-- #endif -->
+				<!-- #endif -->
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" collection="opendb-app-list" field="appid,name,description,create_date"
+				:where="where" page-data="replace" :orderby="orderby" :getcount="true" :page-size="options.pageSize"
+				:page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error,options}"
+				:options="options" loadtime="manual" @load="onqueryload">
+				<uni-table ref="table" :loading="loading || addAppidLoading"
+					:emptyText="error.message || $t('common.empty')" border stripe type="selection"
+					@selection-change="selectionChange" class="table-pc">
+					<uni-tr>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'appid')"
+							sortable @sort-change="sortChange($event, 'appid')">AppID</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'name')"
+							sortable @sort-change="sortChange($event, 'name')">应用名称</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'description')"
+							sortable @sort-change="sortChange($event, 'description')" :width="descriptionThWidth">应用描述
+						</uni-th>
+						<uni-th align="center" filter-type="timestamp"
+							@filter-change="filterChange($event, 'create_date')" sortable
+							@sort-change="sortChange($event, 'create_date')">创建时间</uni-th>
+						<uni-th align="center" :width="buttonThWidth">操作</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index" :disabled="item.appid === appid">
+						<uni-td align="center">{{item.appid}}</uni-td>
+						<uni-td align="center">{{item.name}}</uni-td>
+						<uni-td align="left">{{item.description}}</uni-td>
+						<uni-td align="center">
+							<uni-dateformat :threshold="[0, 0]" :date="item.create_date"></uni-dateformat>
+						</uni-td>
+						<uni-td align="center">
+							<view v-if="item.appid === appid">
+								-
+							</view>
+							<view v-else class="uni-group">
+								<button @click="publish(item._id)" class="uni-button" size="mini"
+									type="primary">{{$t('common.button.publish')}}</button>
+								<button
+									@click="navigateTo('/uni_modules/uni-upgrade-center/pages/version/list?appid='+item.appid, false)"
+									class="uni-button" size="mini"
+									type="primary">{{$t('common.button.version')}}</button>
+								<button @click="navigateTo('./add?id='+item.appid, false)" class="uni-button"
+									size="mini" type="primary">{{$t('common.button.edit')}}</button>
+								<button @click="confirmDelete(item._id)" class="uni-button" size="mini"
+									type="warn">{{$t('common.button.delete')}}</button>
+							</view>
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon show-page-size :page-size="pagination.size" v-model="pagination.current"
+						:total="pagination.count" @change="onPageChanged" @pageSizeChange="pageSizeChange" />
+				</view>
+			</unicloud-db>
+		</view>
+
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+<script>
+	import {
+		enumConverter,
+		filterToWhere
+	} from '../../../js_sdk/validator/opendb-app-list.js';
+	import {
+		mapState
+	} from 'vuex'
+
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbOrderBy = 'create_date' // 排序字段
+	const dbSearchFields = [] // 模糊搜索字段,支持模糊搜索的字段列表
+	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	const orderByMapping = {
+		"ascending": "asc",
+		"descending": "desc"
+	}
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				orderByFieldName: "",
+				selectedIndexs: [],
+				options: {
+					pageSize,
+					pageCurrent,
+					filterData: {},
+					...enumConverter
+				},
+				imageStyles: {
+					width: 64,
+					height: 64
+				},
+				exportExcel: {
+					"filename": "opendb-app-list.xls",
+					"type": "xls",
+					"fields": {
+						"AppID": "appid",
+						"应用名称": "name",
+						"应用描述": "description",
+						"创建时间": "create_date"
+					}
+				},
+				exportExcelData: [],
+				addAppidLoading: true,
+				descriptionThWidth: 380,
+				buttonThWidth: 400
+			}
+		},
+		onLoad() {
+			this._filter = {}
+		},
+		onReady() {
+			this.$refs.udb.loadData()
+		},
+		computed: {
+			...mapState('app', ['appName', 'appid'])
+		},
+		methods: {
+			pageSizeChange(pageSize) {
+				this.options.pageSize = pageSize
+				this.options.pageCurrent = 1
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			onqueryload(data) {
+				if (!data.find(item => item.appid === this.appid)) {
+					this.addCurrentAppid({
+						appid: this.appid,
+						name: this.appName,
+						description: "admin 管理后台",
+						create_date: Date.now()
+					})
+				} else {
+					this.addAppidLoading = false
+				}
+				this.exportExcelData = data
+			},
+			changeSize(e) {
+				this.pageSizeIndex = e.detail.value
+			},
+			addCurrentAppid(app) {
+				// 使用 clientDB 提交当前 appid
+				db.collection('opendb-app-list').add(app).then((res) => {
+					this.loadData()
+					setTimeout(() => {
+						uni.showModal({
+							content: `检测到数据库中无当前应用, 已自动添加应用: ${this.appName}`,
+							showCancel: false
+						})
+					}, 500)
+				}).catch((err) => {
+
+				}).finally(() => {
+					this.addAppidLoading = false
+				})
+			},
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				this.where = newWhere
+				this.loadData()
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.selectedIndexs.length = 0
+				this.$refs.table.clearSelection()
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url, clear) {
+				// clear 表示刷新列表时是否清除页码,true 表示刷新并回到列表第 1 页,默认为 true
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData(clear)
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			// 批量删除
+			delTable() {
+				console.warn(
+					"删除应用,只能删除应用表 opendb-app-list 中的应用数据记录,不能删除与应用关联的其他数据,例如:使用升级中心 uni-upgrade-center 等插件产生的数据(应用版本数据等)"
+				)
+				this.$refs.udb.remove(this.selectedItems(), {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				console.warn(
+					"删除应用,只能删除应用表 opendb-app-list 中的应用数据记录,不能删除与应用关联的其他数据,例如:使用升级中心 uni-upgrade-center 等插件产生的数据(应用版本数据等)"
+				)
+				this.$refs.udb.remove(id, {
+					confirmContent: '是否删除该应用',
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			sortChange(e, name) {
+				this.orderByFieldName = name;
+				if (e.order) {
+					this.orderby = name + ' ' + orderByMapping[e.order]
+				} else {
+					this.orderby = ''
+				}
+				this.$refs.table.clearSelection()
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			filterChange(e, name) {
+				this._filter[name] = {
+					type: e.filterType,
+					value: e.filter
+				}
+				let newWhere = filterToWhere(this._filter, db.command)
+				if (Object.keys(newWhere).length) {
+					this.where = newWhere
+				} else {
+					this.where = ''
+				}
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			publish(id) {
+				uni.navigateTo({
+					url: '/pages/system/app/uni-portal/uni-portal?id=' + id
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 273 - 0
pages/system/app/mixin/publish_add_detail_mixin.js

@@ -0,0 +1,273 @@
+import {
+	validator,
+	mpPlatform
+} from '@/js_sdk/validator/opendb-app-list.js';
+
+const formatFilePickerValue = (url) => (url ? {
+	"name": "",
+	"extname": "",
+	"url": url,
+} : {})
+
+function getValidator(fields) {
+	let result = {}
+	for (let key in validator) {
+		if (fields.includes(key)) {
+			result[key] = validator[key]
+		}
+	}
+	return result
+}
+
+const schemes = ["mimarket", "samsungapps", "appmarket", "oppomarket", "vivomarket"]
+const schemeBrand = ["xiaomi", "samsung", "huawei", "oppo", "vivo"]
+
+export default {
+	data() {
+		let formData = {
+			"appid": "",
+			"name": "",
+			"icon_url": "",
+			"introduction": "",
+			"alias": "",
+			"description": "",
+			"screenshot": [],
+			"store_list": [],
+			"app_android": {},
+			"app_ios": {},
+			"mp_weixin": {},
+			"mp_alipay": {},
+			"mp_baidu": {},
+			"mp_toutiao": {},
+			"mp_qq": {},
+			"mp_lark": {},
+			"mp_kuaishou": {},
+			"mp_dingtalk": {},
+			"mp_jd": {},
+			"h5": {},
+			"quickapp": {}
+		}
+		const data = {
+			formData,
+			rules: Object.freeze(getValidator(Object.keys(formData))),
+			mpPlatform: Object.freeze(mpPlatform),
+			screenshotList: [],
+			middleware_img: {},
+			middleware_checkbox: {},
+			appPackageInfo: {},
+			appPlatformKeys: Object.freeze(['app_ios', 'app_android']),
+			appPlatformValues: Object.freeze({
+				app_android: 'Android',
+				app_ios: 'iOS'
+			}),
+			keepItems: Object.freeze([]),
+			isEdit: false,
+			deletedStore: []
+		}
+		const mpKeys = Object.keys(mpPlatform);
+		data.mpPlatformKeys = Object.freeze(mpKeys);
+		[].concat(mpKeys, ['icon_url', 'quickapp']).forEach(key => data.middleware_img[key] = {});
+		data.platFormKeys = Object.freeze([].concat(mpKeys, data.appPlatformKeys))
+		data.platFormKeys.forEach(key => data.middleware_checkbox[key] = false)
+		return data
+	},
+	methods: {
+		requestCloudFunction(functionName, params = {}) {
+			return this.$request(functionName, params, {
+				functionName: 'uni-upgrade-center'
+			})
+		},
+		hasValue(value) {
+			if (typeof value !== 'object') return !!value
+			if (value instanceof Array) return !!value.length
+			return !!(value && Object.keys(value).length)
+		},
+		initFormData(obj) {
+			if (!obj || !Object.keys(obj).length) return;
+			// TODO delete
+			for (let key in obj) {
+				const value = obj[key]
+				switch (key) {
+					case 'icon_url':
+						this.middleware_img[key] = formatFilePickerValue(value)
+						break;
+					case 'screenshot':
+						this.screenshotList = value.map(item => formatFilePickerValue(item))
+						break;
+					default:
+						if ((key.indexOf('mp') !== -1 || key.indexOf('app') !== -1) && this.hasValue(value)) {
+							this.setPlatformChcekbox(key, true)
+							if (value.qrcode_url)
+								this.middleware_img[key] = formatFilePickerValue(value.qrcode_url)
+						}
+						break;
+				}
+				this.setFormData(key, value)
+			}
+		},
+		setFormData(key, value) {
+			const keys = key.indexOf('.') !== -1 ? key.split('.') : [key];
+			const lens = keys.length - 1
+			let tempObj = this.formData
+			keys.forEach((key, index) => {
+				const obj = tempObj[key]
+				if (typeof obj === 'object' && index < lens) {
+					tempObj = obj
+				} else {
+					tempObj[key] = value
+				}
+			})
+		},
+		getFormData(key) {
+			const keys = key.indexOf('.') !== -1 ? key.split('.') : [key];
+			const lens = keys.length - 1
+			let tempObj = this.formData
+			for (let i = 0; i < keys.length; i++) {
+				const key = keys[i]
+				tempObj = tempObj[key]
+				if (tempObj == null) {
+					return false
+				}
+			}
+			return tempObj
+		},
+		formatFormData() {
+			this.setFormData('screenshot', this.screenshotList.map(item => item.fileID || item.url))
+
+			for (let i = 0; i < this.formData.store_list.length; i++) {
+				const item = this.formData.store_list[i]
+
+				if (item.scheme.trim().length === 0) {
+					this.formData.store_list.splice(i, 1)
+					i--
+					continue;
+				}
+
+				const index = schemes.indexOf((item.scheme.match(/(.*):\/\//) || [])[1])
+				if (index !== -1) {
+					if (item.id !== schemeBrand[index]) {
+						this.deletedStore.push(item.id)
+					}
+					item.id = schemeBrand[index]
+				}
+				item.priority = parseFloat(item.priority)
+			}
+
+			this.keepItems = this.platFormKeys
+				.filter(key =>
+					this.getPlatformChcekbox(key) &&
+					(this.formData[key].url || this.formData[key].qrcode_url)
+				)
+				.concat(['icon_url', 'screenshot', 'create_date', 'store_list'])
+
+			if (this.formData.h5 && this.formData.h5.url)
+				this.keepItems.push('h5');
+		},
+		// 根据 appid 自动填充
+		autoFill() {
+			const appid = this.getFormData('appid')
+			if (!appid) {
+				return
+			}
+
+			uni.showLoading({
+				mask: true
+			})
+
+			this.requestCloudFunction('getAppInfo', {
+					appid
+				})
+				.then(res => {
+					if (res.success) {
+						this.setFormData('description', res.description)
+						this.setFormData('name', res.name)
+						return
+					}
+				}).catch(e => {
+					console.error(e)
+				}).finally(() => {
+					uni.hideLoading()
+				})
+		},
+		autoFillApp() {
+			const appid = this.getFormData('appid')
+			if (!appid) {
+				return
+			}
+
+			this.appPlatformKeys.forEach(key => {
+				this.fetchAppInfo(appid, this.appPlatformValues[key]).then(res => {
+					if (res && res.success) {
+						this.setPlatformChcekbox(key, true)
+						this.setFormData(key, {
+							name: res.name,
+							url: res.url
+						})
+						return;
+					}
+				})
+			})
+		},
+		fetchAppInfo(appid, platform) {
+			uni.showLoading({
+				mask: true
+			})
+			return this.requestCloudFunction('getAppVersionInfo', {
+				appid,
+				platform
+			}).then(res => {
+				return res
+			}).catch(e => {
+				console.error(e)
+			}).finally(() => {
+				uni.hideLoading()
+			})
+		},
+		iconUrlSuccess(res, key) {
+			uni.showToast({
+				icon: 'success',
+				title: '上传成功',
+				duration: 500
+			})
+			this.setFormData(key, res.tempFilePaths[0])
+		},
+		async iconUrlDelete(res, key) {
+			let deleteRes = await this.requestCloudFunction('deleteFile', {
+				fileList: [res.tempFile.fileID || res.tempFile.url]
+			})
+			deleteRes.fileList ?
+				deleteRes = deleteRes.fileList[0] :
+				deleteRes = deleteRes[0];
+			if (deleteRes.success || deleteRes.code === "SUCCESS") {
+				uni.showToast({
+					icon: 'success',
+					title: '删除成功',
+					duration: 800
+				})
+				if (!key) return;
+				this.setFormData(key, '')
+				this.$refs.form.clearValidate(key)
+			}
+		},
+		getPlatformChcekbox(mp_name) {
+			return this.middleware_checkbox[mp_name]
+		},
+		setPlatformChcekbox(mp_name, value = false) {
+			this.middleware_checkbox[mp_name] = value
+		},
+		selectFile() {
+			if (this.hasPackage) {
+				uni.showToast({
+					icon: 'none',
+					title: '只可上传一个文件,请删除已上传后重试',
+					duration: 1000
+				});
+			}
+		}
+	},
+	computed: {
+		hasPackage() {
+			return this.appPackageInfo && !!Object.keys(this.appPackageInfo).length
+		},
+	}
+}

+ 161 - 0
pages/system/app/uni-portal/uni-portal.vue

@@ -0,0 +1,161 @@
+<template>
+	<view class="uni-container">
+		<!-- <h2 class="text-separated" style="padding-bottom: 40rpx;">统一发布页管理</h2> -->
+
+		<h3 class="text-separated" style="padding: 0 0 20rpx 0;">步骤1:了解“统一发布页”</h3>
+
+		<view style="margin-top: 20rpx;">
+			<view class="text-separated">
+				<text class="strong">uni-portal </text>
+				<text>是 uni-app 提供的一套开箱即用的“统一发布页”。</text>
+			</view>
+			<view class="text-separated">
+				<text class="strong">uni-portal </text>
+				<text>可作为面向用户的统一业务名片,在一个页面集中展现:App下载地址、小程序二维码、H5访问链接等信息。</text>
+			</view>
+
+			<!-- #ifdef H5 -->
+			<view class="text-separated">
+				<text style="font-size: 16px;">uni-app 官方示例的发布页就是基于<text class="strong">uni-portal </text> 制作的,<a
+						href="https://hellouniapp.dcloud.net.cn/portal" target="_blank" class="a-label">点击体验</a>
+				</text>
+			</view>
+			<!-- #endif -->
+		</view>
+
+		<h3 class="text-separated" style="padding: 40rpx 0 20rpx 0;">步骤2:获取“统一发布页”</h3>
+		<view class="flex text-separated" style="margin-top: 20rpx;">
+			<text>
+				<view class="strong">uni-portal </view> 可根据「应用管理」中所填写的应用信息,一键生成发布页:
+			</text>
+			<button class="custom-button" size="mini" type="primary" @click="publish"
+				style="margin: 0;">生成并下载发布页</button>
+		</view>
+
+		<h3 class="text-separated" style="padding: 40rpx 0 20rpx 0;">步骤3:上传“统一发布页”</h3>
+
+		<view style="margin-top: 20rpx;">
+			<view class="text-separated">
+				<text>
+					步骤2下载的“统一发布页”,是一个静态HTML页面,你可以直接在本地浏览器中打开访问。
+				</text>
+			</view>
+
+			<view class="text-separated">
+				<text>
+					为了让用户访问到这个“统一发布页”,你需要将该静态HTML文件上传到你的服务器中;推荐使用<a href="https://uniapp.dcloud.io/uniCloud/hosting"
+						target="_blank" class="a-label" style="padding: 5px;">前端网页托管</a>,因为前端网页托管具备使用更简单、价格更便宜、访问更快等优点。
+				</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	var download = function(content, filename) {
+		var eleLink = document.createElement('a');
+		eleLink.download = filename;
+		eleLink.style.display = 'none';
+		var blob = new Blob([content]);
+		eleLink.href = URL.createObjectURL(blob);
+		document.body.appendChild(eleLink);
+		eleLink.click();
+		document.body.removeChild(eleLink);
+	};
+
+	export default {
+		data() {
+			return {
+				id: ''
+			}
+		},
+		onLoad({
+			id
+		}) {
+			this.id = id
+		},
+		methods: {
+			publish() {
+				if (!this.id) {
+					uni.showModal({
+						content: '页面出错,请返回重进',
+						showCancel: false,
+						success(res) {
+							uni.redirectTo({
+								url: '/pages/system/app/list'
+							})
+						}
+					})
+					return
+				}
+				this.$request('createPublishHtml', {
+					id: this.id
+				}, {
+					functionName: 'uni-portal',
+					showModal: false
+				}).then(res => {
+					// #ifdef H5
+					if ('download' in document.createElement('a')) {
+						download(res.body, 'index.html');
+					} else {
+						uni.showToast({
+							icon: 'error',
+							title: '浏览器不支持',
+							duration: 800
+						})
+					}
+					// #endif
+				}).catch((res) => {
+					uni.showModal({
+						content: res.errMsg,
+						showCancel: false
+					})
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.strong {
+		padding: 10rpx;
+		display: inline-block;
+		color: #c7254e;
+	}
+
+	.a-label {
+		text-decoration: none;
+		color: #0366d6;
+		font-weight: bold;
+		padding: 10rpx;
+	}
+
+	.text-separated {
+		line-height: 2em;
+		color: #2c3e50;
+	}
+
+	.tip {
+		display: flex;
+		flex-direction: column;
+		align-items: flex-start;
+		background-color: #f3f5f7;
+		color: #2c3e50;
+		padding: 10px;
+		font-size: 32rpx;
+
+		border: {
+			color: #409EFF;
+			left-width: 8px;
+			left-style: solid;
+		}
+
+		text {
+			margin-right: 15px;
+		}
+
+		.custom-button {
+			margin-left: 0px;
+		}
+	}
+</style>

+ 164 - 0
pages/system/menu/add.vue

@@ -0,0 +1,164 @@
+<template>
+	<view class="uni-container">
+		<uni-forms labelWidth="80" ref="form" v-model="formData" :rules="rules" validateTrigger="bind" @submit="submit">
+			<uni-forms-item name="menu_id" label="标识" required>
+				<uni-easyinput v-model="formData.menu_id" :clearable="false" placeholder="请输入菜单项的ID,不可重复" />
+			</uni-forms-item>
+			<uni-forms-item name="name" label="显示名称" required>
+				<uni-easyinput v-model="formData.name" :clearable="false" placeholder="请输入菜单名称" />
+			</uni-forms-item>
+			<uni-forms-item name="icon" label="图标class" style="margin-bottom: 10px;">
+				<uni-easyinput v-model="formData.icon" :clearable="false" placeholder="请输入菜单图标css样式类名">
+					<template v-slot:right>
+						<span style="color: #007aff; cursor: pointer;padding-right: 10px;" @click="showIconPopup">内置图标</span>
+					</template>
+				</uni-easyinput>
+				<uni-link font-size="12" href="https://uniapp.dcloud.net.cn/uniCloud/admin?id=icon-%e5%9b%be%e6%a0%87" text="如何使用自定义图标?"
+				 class="uni-form-item-tips"></uni-link>
+			</uni-forms-item>
+			<uni-forms-item name="url" label="页面URL">
+				<uni-easyinput v-model="formData.url" :clearable="false" placeholder="URL为空代表是目录而不是叶子节点" />
+			</uni-forms-item>
+			<uni-forms-item name="sort" label="序号">
+				<uni-easyinput v-model="formData.sort" :clearable="false" placeholder="请输入菜单序号(越大越靠后)" />
+			</uni-forms-item>
+			<uni-forms-item name="parent_id" label="父菜单标识">
+				<uni-easyinput :disabled="true" v-model="formData.parent_id" :clearable="false" placeholder="新增菜单时自动填充, 一级菜单不需要填写" />
+			</uni-forms-item>
+			<uni-forms-item name="permission" label="权限列表" class="flex-center-x">
+				<uni-data-checkbox :multiple="true" v-model="formData.permission" collection="uni-id-permissions" :page-size="500" field="permission_name as text, permission_id as value" />
+				<view class="uni-form-item-tips">
+					当用户拥有以上被选中的权限时,可以访问此菜单。建议仅对子菜单配置权限,父菜单会自动包含。如不选择权限,意味着仅超级管理员可访问本菜单
+				</view>
+			</uni-forms-item>
+			<uni-forms-item name="enable" label="是否启用">
+				<switch @change="binddata('enable', $event.detail.value)" :checked="formData.enable" />
+			</uni-forms-item>
+			<view class="uni-button-group">
+				<button type="primary" class="uni-button" @click="submitForm" style="width: 100px;">{{$t('common.button.submit')}}</button>
+				<navigator open-type="navigateBack" style="margin-left: 15px;"><button class="uni-button" tyle="width: 100px;">{{$t('common.button.back')}}</button></navigator>
+			</view>
+		</uni-forms>
+		<uni-popup class="icon-modal-box" ref="iconPopup" type="center">
+			<view class="icon-modal icon-modal-pc">
+				<Icons :tag="false" :fix-window="false"/>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script>
+	import validator from '@/js_sdk/validator/opendb-admin-menus.js';
+	import Icons from '@/pages/demo/icons/icons.vue'
+
+	const db = uniCloud.database();
+	const dbCmd = db.command;
+	const dbCollectionName = 'opendb-admin-menus';
+
+	function getValidator(fields) {
+		let result = {}
+		for (let key in validator) {
+			if (fields.includes(key)) {
+				result[key] = validator[key]
+			}
+		}
+		return result
+	}
+
+	export default {
+		components: {
+			Icons
+		},
+		data() {
+			return {
+				formData: {
+					"menu_id": "",
+					"name": "",
+					"icon": "",
+					"url": "",
+					"sort": null,
+					"parent_id": "",
+					"permission": [],
+					"enable": true
+				},
+				rules: {
+					...getValidator(["menu_id", "name", "icon", "url", "sort", "parent_id", "permission", "enable"])
+				}
+			}
+		},
+		onLoad(e) {
+			if (e.parent_id) {
+				this.formData.parent_id = e.parent_id
+			}
+		},
+		methods: {
+			/**
+			 * 触发表单提交
+			 */
+			submitForm() {
+				this.$refs.form.submit();
+			},
+
+			/**
+			 * 表单提交
+			 * @param {Object} event 回调参数 Function(callback:{value,errors})
+			 */
+			submit(event) {
+				const {
+					value,
+					errors
+				} = event.detail
+
+				// 表单校验失败页面会提示报错 ,要停止表单提交逻辑
+				if (errors) {
+					return
+				}
+				uni.showLoading({
+					title: '提交中...',
+					mask: true
+				})
+				// 使用 uni-clientDB 提交数据
+				db.collection(dbCollectionName).add(value).then((res) => {
+					uni.showToast({
+						title: '新增成功'
+					})
+					this.getOpenerEventChannel().emit('refreshData')
+					setTimeout(() => uni.navigateBack(), 500)
+				}).catch((err) => {
+					uni.showModal({
+						content: err.message || '请求服务失败',
+						showCancel: false
+					})
+				}).finally(() => {
+					uni.hideLoading()
+				})
+			},
+			showIconPopup() {
+				this.$refs.iconPopup.open()
+			}
+		}
+	}
+</script>
+<style scoped>
+	.icon-modal-box {
+		padding-top: var(--top-window-height);
+	}
+
+	.icon-modal {
+		width: 350px;
+		background-color: #fff;
+		height: 500px;
+		overflow-y: scroll;
+	}
+
+	@media screen and (min-width: 768px) {
+		.icon-modal-pc {
+			width: 600px;
+		}
+	}
+	
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+	
+</style>

+ 188 - 0
pages/system/menu/edit.vue

@@ -0,0 +1,188 @@
+<template>
+	<view class="uni-container">
+		<uni-forms labelWidth="80" ref="form" v-model="formData" :rules="rules" validateTrigger="bind" @submit="submit">
+			<uni-forms-item name="menu_id" label="标识" required>
+				<uni-easyinput v-model="formData.menu_id" :clearable="false" placeholder="请输入菜单项的ID,不可重复" />
+			</uni-forms-item>
+			<uni-forms-item name="name" label="显示名称" required>
+				<uni-easyinput v-model="formData.name" :clearable="false" placeholder="请输入菜单名称" />
+			</uni-forms-item>
+			<uni-forms-item name="icon" label="图标 class" style="margin-bottom: 40px;">
+				<uni-easyinput v-model="formData.icon" :clearable="false" placeholder="请输入菜单图标css样式类名">
+					<span slot="right" style="color: #007aff; cursor: pointer;padding-right: 10px;" @click="showIconPopup">内置图标</span>
+				</uni-easyinput>
+				<uni-link font-size="12" href="https://uniapp.dcloud.net.cn/uniCloud/admin?id=icon-%e5%9b%be%e6%a0%87" text="如何使用自定义图标?"
+				 class="uni-form-item-tips"></uni-link>
+			</uni-forms-item>
+			<uni-forms-item name="url" label="页面URL">
+				<uni-easyinput v-model="formData.url" :clearable="false" placeholder="URL为空代表是目录而不是叶子节点" />
+			</uni-forms-item>
+			<uni-forms-item name="sort" label="序号">
+				<uni-easyinput v-model="formData.sort" :clearable="false" placeholder="请输入菜单序号(越大越靠后)" />
+			</uni-forms-item>
+			<uni-forms-item name="parent_id" label="父菜单标识">
+				<uni-easyinput v-model="formData.parent_id" :clearable="false" placeholder="请输入父级菜单标识, 一级菜单不需要填写" />
+			</uni-forms-item>
+			<uni-forms-item name="permission" label="权限列表" class="flex-center-x">
+				<uni-data-checkbox :multiple="true" v-model="formData.permission" collection="uni-id-permissions" :page-size="500" field="permission_name as text, permission_id as value" />
+				<view class="uni-form-item-tips">
+					当用户拥有以上被选中的权限时,可以访问此菜单。建议仅对子菜单配置权限,父菜单会自动包含。如不选择权限,意味着仅超级管理员可访问本菜单
+				</view>
+			</uni-forms-item>
+			<uni-forms-item name="enable" label="是否启用">
+				<switch @change="binddata('enable', $event.detail.value)" :checked="formData.enable" />
+			</uni-forms-item>
+
+			<view class="uni-button-group">
+				<button type="primary" class="uni-button" @click="submitForm" style="width: 100px;">{{$t('common.button.submit')}}</button>
+				<navigator open-type="navigateBack" style="margin-left: 15px;"><button class="uni-button" style="width: 100px;">{{$t('common.button.back')}}</button></navigator>
+			</view>
+			<uni-popup class="icon-modal-box" ref="iconPopup" type="center">
+				<view class="icon-modal icon-modal-pc">
+					<Icons :tag="false" :fix-window="false"/>
+				</view>
+			</uni-popup>
+		</uni-forms>
+	</view>
+</template>
+
+<script>
+	import validator from '@/js_sdk/validator/opendb-admin-menus.js';
+	import Icons from '@/pages/demo/icons/icons.vue'
+
+	const db = uniCloud.database();
+	const dbCmd = db.command;
+	const dbCollectionName = 'opendb-admin-menus';
+
+	function getValidator(fields) {
+		let result = {}
+		for (let key in validator) {
+			if (fields.includes(key)) {
+				result[key] = validator[key]
+			}
+		}
+		return result
+	}
+
+	export default {
+		components: {
+			Icons
+		},
+		data() {
+			return {
+				formData: {
+					"menu_id": "",
+					"name": "",
+					"icon": "",
+					"url": "",
+					"sort": '',
+					"parent_id": "",
+					"permission": [],
+					"enable": null
+				},
+				rules: {
+					...getValidator(["menu_id", "name", "icon", "url", "sort", "parent_id", "permission", "enable"])
+				}
+			}
+		},
+		onLoad(e) {
+			const id = e.id
+			this.formDataId = id
+			this.getDetail(id)
+		},
+		methods: {
+			/**
+			 * 触发表单提交
+			 */
+			submitForm(form) {
+				this.$refs.form.submit();
+			},
+
+			/**
+			 * 表单提交
+			 * @param {Object} event 回调参数 Function(callback:{value,errors})
+			 */
+			submit(event) {
+				const {
+					value,
+					errors
+				} = event.detail
+
+				// 表单校验失败页面会提示报错 ,要停止表单提交逻辑
+				if (errors) {
+					return
+				}
+
+				uni.showLoading({
+					title: '修改中...',
+					mask: true
+				})
+				// 使用 uni-clientDB 提交数据
+				db.collection(dbCollectionName).doc(this.formDataId).update(value).then((res) => {
+				    uni.showToast({
+				        title: '修改成功'
+				    })
+				    this.getOpenerEventChannel().emit('refreshData')
+				    setTimeout(() => uni.navigateBack(), 500)
+				}).catch((err) => {
+				    uni.showModal({
+				        content: err.message || '请求服务失败',
+				        showCancel: false
+				    })
+				}).finally(() => {
+				    uni.hideLoading()
+				})
+			},
+
+			/**
+			 * 获取表单数据
+			 * @param {Object} id
+			 */
+			getDetail(id) {
+				uni.showLoading({
+					mask: true
+				})
+				db.collection(dbCollectionName).where({
+					_id: id
+				}).get().then((res) => {
+					const data = res.result.data[0]
+					if (data) {
+						this.formData = data
+					}
+				}).catch((err) => {
+					uni.showModal({
+						content: err.message || '请求服务失败',
+						showCancel: false
+					})
+				}).finally(() => {
+					uni.hideLoading()
+				})
+			},
+			showIconPopup() {
+				this.$refs.iconPopup.open()
+			}
+		}
+	}
+</script>
+<style scoped>
+	.icon-modal-box {
+		padding-top: var(--top-window-height);
+	}
+
+	.icon-modal {
+		width: 350px;
+		background-color: #fff;
+		height: 500px;
+		overflow-y: scroll;
+	}
+
+	@media screen and (min-width: 768px) {
+		.icon-modal-pc {
+			width: 600px;
+		}
+	}
+
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 360 - 0
pages/system/menu/list.vue

@@ -0,0 +1,360 @@
+<template>
+	<view>
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+		</view>
+		<view class="uni-tabs__header">
+			<view class="uni-tabs__nav-wrap">
+				<view class="uni-tabs__nav-scroll">
+					<view class="uni-tabs__nav">
+						<view @click="switchTab('menus')" :class="{'is-active':currentTab==='menus'}"
+							class="uni-tabs__item">
+							{{$t('menu.text.menuManager')}}
+						</view>
+						<view @click="switchTab('pluginMenus')" v-if="pluginMenus.length"
+							:class="{'is-active':currentTab==='pluginMenus'}" class="uni-tabs__item">
+							{{$t('menu.text.additiveMenu')}}
+							<uni-badge class="menu-badge" :text="pluginMenus.length" type="error"></uni-badge>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+		<view v-show="currentTab==='menus'">
+			<view class="uni-header" style="border-bottom: 0;margin-bottom: -15px;">
+				<view class="uni-group">
+					<button @click="navigateTo('./add')" size="mini" plain="true"
+						type="primary">{{$t('menu.button.addFirstLevelMenu')}}</button>
+				</view>
+				<view class="uni-group">
+
+				</view>
+			</view>
+			<view class="uni-container">
+				<unicloud-db ref="udb" @load="onqueryload" collection="opendb-admin-menus" :options="options"
+					:where="where" page-data="replace" :orderby="orderby" :getcount="true" :page-size="options.pageSize"
+					:page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error}">
+					<uni-table :loading="loading" class="table-pc" :emptyText="errMsg || $t('common.empty')" border
+						stripe>
+						<uni-tr>
+							<uni-th align="center">排序</uni-th>
+							<uni-th width="200" align="center">名称</uni-th>
+							<uni-th align="center">标识</uni-th>
+							<uni-th align="center">URL</uni-th>
+							<uni-th width="100" align="center">是否启用</uni-th>
+							<uni-th align="center">操作</uni-th>
+						</uni-tr>
+						<uni-tr v-for="(item,index) in data" :key="index">
+							<uni-td align="center">{{item.sort}}</uni-td>
+							<uni-td>{{item.name}}</uni-td>
+							<uni-td>{{item.menu_id}}</uni-td>
+							<uni-td>{{item.url}}</uni-td>
+							<uni-td align="center" :class="{'menu-disable':!item.enable}">{{item.enable?'已启用':'未启用'}}
+							</uni-td>
+							<uni-td align="center">
+								<view class="uni-group" style="justify-content: left;">
+									<button @click="navigateTo('./edit?id='+item._id, false)" class="uni-button"
+										size="mini" type="primary">{{$t('common.button.edit')}}</button>
+									<button
+										v-if="item.menu_id !== 'system_menu' && item.menu_id !== 'system_management'"
+										@click="confirmDelete(item)" class="uni-button" size="mini"
+										type="warn">{{$t('common.button.delete')}}</button>
+									<button v-if="!item.url" @click="navigateTo('./add?parent_id='+item.menu_id, false)"
+										class="uni-button" size="mini"
+										type="primary">{{$t('menu.button.addChildMenu')}}</button>
+								</view>
+							</uni-td>
+						</uni-tr>
+					</uni-table>
+				</unicloud-db>
+			</view>
+		</view>
+		<view v-show="currentTab==='pluginMenus'">
+			<view class="uni-header" style="border-bottom: 0;margin-bottom: -15px;">
+				<view class="uni-group">
+					<button style="width: 130px;" @click="addPluginMenus" size="mini" type="primary">添加选中的菜单</button>
+				</view>
+				<view class="uni-group"></view>
+			</view>
+			<view class="uni-container">
+				<uni-table ref="pluginMenusTable" type="selection" border stripe
+					@selection-change="pluginMenuSelectChange">
+					<uni-tr>
+						<uni-th align="center">名称(标识)</uni-th>
+						<uni-th align="center">URL</uni-th>
+						<uni-th align="center">插件菜单 json 文件</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in pluginMenus" :key="index">
+						<uni-td>{{item.name}}({{item.menu_id}})</uni-td>
+						<uni-td>{{item.url}}</uni-td>
+						<uni-td>{{item.json}}</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-sub-title" style="margin-top: 15px;">
+					以上待添加菜单来自于三方插件,添加后,将显示在菜单管理中,若不希望显示在上述表格中时,可手动删除项目中对应的`插件id-menu.json`文件。
+				</view>
+			</view>
+		</view>
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import {
+		buildMenus
+	} from '../../../components/uni-data-menu/util.js'
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbOrderBy = 'create_date asc'
+	// 分页配置
+	const pageSize = 20000
+	const pageCurrent = 1
+	// 查找插件注册的菜单列表(目前仅在开发模式启用,仅限 admin 角色)
+	const pluginMenuJsons = []
+	// #ifndef VUE3
+	if (process.env.NODE_ENV === 'development') {
+		const rootModules = require.context(
+			'../../../',
+			false,
+			/-menu.json$/
+		)
+		rootModules.keys().forEach(function(key) {
+			const json = key.substr(2)
+			rootModules(key).forEach(item => {
+				item.json = json
+				pluginMenuJsons.push(item)
+			})
+		})
+		const pluginModules = require.context(
+			'../../../uni_modules/',
+			true,
+			/menu.json$/
+		)
+		pluginModules.keys().forEach(function(key) {
+			const json = 'uni_modules' + key.substr(1)
+			pluginModules(key).forEach(item => {
+				item.json = json
+				pluginMenuJsons.push(item)
+			})
+		})
+	}
+	// #endif
+
+	// 获取父的个数
+	function getParents(menus, id, depth = 0) {
+		menus.forEach(menu => {
+			if (menu.menu_id === id && menu.parent_id) {
+				depth = depth + 1 + getParents(menus, menu.parent_id, depth)
+			}
+		})
+		return depth
+	}
+
+	// 获取子的 _id
+	function getChildren(menus, id, childrenIds = []) {
+		if (menus.find(menu => menu.parent_id === id)) {
+			menus.forEach(item => {
+				if (item.parent_id === id) {
+					childrenIds.push(item._id)
+					getChildren(menus, item.menu_id, childrenIds)
+				}
+			})
+		}
+		return childrenIds
+	}
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				options: {
+					pageSize,
+					pageCurrent
+				},
+				selectedIndexs: [], //批量选中的项
+				loading: true,
+				menus: [],
+				errMsg: '',
+				currentTab: 'menus',
+				selectedPluginMenuIndexs: []
+			}
+		},
+		computed: {
+			pluginMenus() {
+				const menus = []
+				if (!this.$hasRole('admin')) {
+					return menus
+				}
+				const dbMenus = this.menus
+				if (!dbMenus.length) {
+					return menus
+				}
+				pluginMenuJsons.forEach(menu => {
+					// 查找尚未被注册到数据库中的菜单
+					if (!dbMenus.find(item => item.menu_id === menu.menu_id)) {
+						menus.push(menu)
+					}
+				})
+				return menus
+			},
+		},
+		watch: {
+			pluginMenus(val) {
+				if (!val.length) {
+					this.currentTab = 'menus'
+				}
+			}
+		},
+		methods: {
+			getSortMenu(menuList) {
+				// 标记叶子节点
+				menuList.map(item => {
+					if (!menuList.some(subMenuItem => subMenuItem.parent_id === item.menu_id)) {
+						item.isLeafNode = true
+					}
+				})
+				return buildMenus(menuList)
+			},
+			onqueryload(data) {
+				for (var i = 0; i < data.length; i++) {
+					let item = data[i]
+					const depth = getParents(data, item.menu_id)
+					item.name = (depth ? ' '.repeat(depth) + '|-' : '') + item.name
+				}
+				const menuTree = this.getSortMenu(data)
+				const sortMenus = []
+				this.patTree(menuTree, sortMenus)
+				data.length = 0;
+				data.push(...sortMenus)
+				this.menus = data //仅导出当前页
+			},
+			patTree(tree, sortMenus) {
+				tree.forEach(item => {
+					sortMenus.push(item)
+					if (item.children.length) {
+						this.patTree(item.children, sortMenus)
+					}
+				})
+				return sortMenus
+			},
+			switchTab(tab) {
+				this.currentTab = tab
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			navigateTo(url, clear) { // clear 表示刷新列表时是否清除当前页码,true 表示刷新并回到列表第 1 页,默认为 true
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData(clear)
+						}
+					}
+				})
+			},
+			confirmDelete(menu) {
+				let ids = menu._id
+				let content = '是否删除该菜单?'
+				// 如有子菜单
+				const children = getChildren(this.menus, menu.menu_id)
+				if (children.length) content = '是否删除该菜单及其子菜单?'
+				ids = [ids, ...children]
+				uni.showModal({
+					title: '提示',
+					content,
+					success: (res) => {
+						if (!res.confirm) {
+							return
+						}
+						this.$refs.udb.remove(ids, {
+							needConfirm: false
+						})
+					}
+				})
+			},
+			pluginMenuSelectChange(e) {
+				this.selectedPluginMenuIndexs = e.detail.index
+			},
+			addPluginMenus(confirmContent) {
+				if (!this.selectedPluginMenuIndexs.length) {
+					return uni.showModal({
+						title: '提示',
+						content: '请选择要添加的菜单!',
+						showCancel: false
+					})
+				}
+				const pluginMenus = this.pluginMenus
+				const menus = []
+				this.selectedPluginMenuIndexs.forEach(i => {
+					const menu = pluginMenus[i]
+					if (menu) {
+						// 拷贝一份,移除 json 字段
+						const dbMenu = JSON.parse(JSON.stringify(menu))
+						delete dbMenu.json
+						menus.push(dbMenu)
+					}
+				})
+				uni.showModal({
+					title: '提示',
+					content: '您确认要添加已选中的菜单吗?',
+					success: (res) => {
+						if (!res.confirm) {
+							return
+						}
+						uni.showLoading({
+							mask: true
+						})
+						const checkAll = menus.length === pluginMenus.length
+						uniCloud.database().collection('opendb-admin-menus').add(menus).then(res => {
+							this.init()
+							uni.showModal({
+								title: '提示',
+								content: '添加菜单成功!',
+								showCancel: false,
+								success: () => {
+									this.$refs.pluginMenusTable.clearSelection()
+									if (checkAll) {
+										this.currentTab = 'menus'
+									}
+									this.loadData()
+								}
+							})
+						}).catch(err => {
+							uni.showModal({
+								title: '提示',
+								content: err.message,
+								showCancel: false
+							})
+						}).finally(() => {
+							uni.hideLoading()
+						})
+					}
+				})
+			}
+		}
+	}
+</script>
+<style>
+	/* #ifndef H5 */
+	page {
+		padding-top: 85px;
+	}
+
+	/* #endif */
+	.menu-disable {
+		color: red;
+	}
+
+	.menu-badge {
+		position: absolute;
+		top: 0;
+		right: 5px;
+	}
+</style>

+ 98 - 0
pages/system/permission/add.vue

@@ -0,0 +1,98 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :value="formData" validateTrigger="bind">
+      <uni-forms-item name="permission_id" label="权限ID" required>
+        <input placeholder="权限唯一标识,不可修改,不允许重复" @input="binddata('permission_id', $event.detail.value)" class="uni-input-border" v-model="formData.permission_id" trim="both" />
+      </uni-forms-item>
+      <uni-forms-item name="permission_name" label="权限名称" required>
+        <input placeholder="权限名称" @input="binddata('permission_name', $event.detail.value)" class="uni-input-border" v-model="formData.permission_name" trim="both" />
+      </uni-forms-item>
+      <uni-forms-item name="comment" label="备注">
+        <textarea placeholder="备注" @input="binddata('comment', $event.detail.value)" class="uni-textarea-border" v-model="formData.comment" trim="both" />
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">{{$t('common.button.submit')}}</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+            <button class="uni-button" style="width: 100px;">{{$t('common.button.back')}}</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '@/js_sdk/validator/uni-id-permissions.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'uni-id-permissions';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  export default {
+    data() {
+      let formData = {
+        "permission_id": "",
+        "permission_name": "",
+        "comment": ""
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      /**
+       * 触发表单提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          this.submitForm(res)
+        }).catch(() => {
+          uni.hideLoading()
+        })
+      },
+
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        db.collection(dbCollectionName).add(value).then((res) => {
+          uni.showToast({
+            title: '新增成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      }
+    }
+  }
+</script>
+
+<style>
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 128 - 0
pages/system/permission/edit.vue

@@ -0,0 +1,128 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :value="formData" validateTrigger="bind">
+      <uni-forms-item name="permission_id" label="权限ID" required>
+        <uni-easyinput placeholder="权限唯一标识,不可修改,不允许重复" @input="binddata('permission_id', $event.detail.value)"  v-model="formData.permission_id" trim="both" disabled></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="permission_name" label="权限名称" required>
+        <input placeholder="权限名称" @input="binddata('permission_name', $event.detail.value)" class="uni-input-border" v-model="formData.permission_name" trim="both" />
+      </uni-forms-item>
+      <uni-forms-item name="comment" label="备注">
+        <textarea placeholder="备注" @input="binddata('comment', $event.detail.value)" class="uni-textarea-border" v-model="formData.comment" trim="both"></textarea>
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">{{$t('common.button.submit')}}</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+            <button class="uni-button" style="width: 100px;">{{$t('common.button.back')}}</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '@/js_sdk/validator/uni-id-permissions.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'uni-id-permissions';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  export default {
+    data() {
+      let formData = {
+        "permission_id": "",
+        "permission_name": "",
+        "comment": ""
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onLoad(e) {
+      if (e.id) {
+        const id = e.id
+        this.formDataId = id
+        this.getDetail(id)
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      /**
+       * 触发表单提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          this.submitForm(res)
+        }).catch(() => {
+          uni.hideLoading()
+        })
+      },
+
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        db.collection(dbCollectionName).doc(this.formDataId).update(value).then((res) => {
+          uni.showToast({
+            title: '修改成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      },
+
+      /**
+       * 获取表单数据
+       * @param {Object} id
+       */
+      getDetail(id) {
+        uni.showLoading({
+          mask: true
+        })
+        db.collection(dbCollectionName).doc(id).field("permission_id,permission_name,comment").get().then((res) => {
+          const data = res.result.data[0]
+          if (data) {
+            this.formData = data
+          }
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      }
+    }
+  }
+</script>
+
+<style>
+::v-deep .uni-forms-item__label {
+	width: 90px !important;
+}
+</style>

+ 234 - 0
pages/system/permission/list.vue

@@ -0,0 +1,234 @@
+<template>
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<input class="uni-search" type="text" v-model="query" @confirm="search"
+					:placeholder="$t('common.placeholder.query')" />
+				<button class="uni-button hide-on-phone" type="default" size="mini"
+					@click="search">{{$t('common.button.search')}}</button>
+				<button class="uni-button" type="primary" size="mini"
+					@click="navigateTo('./add')">{{$t('common.button.add')}}</button>
+				<button class="uni-button" type="warn" size="mini" :disabled="!selectedIndexs.length"
+					@click="delTable">{{$t('common.button.batchDelete')}}</button>
+				<!-- #ifdef H5 -->
+				<download-excel class="hide-on-phone" :fields="exportExcel.fields" :data="exportExcelData"
+					:type="exportExcel.type" :name="exportExcel.filename">
+					<button class="uni-button" type="primary" size="mini">{{$t('common.button.exportExcel')}}</button>
+				</download-excel>
+				<!-- #endif -->
+
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" collection="uni-id-permissions"
+				field="permission_id,permission_name,comment,create_date" :where="where" page-data="replace"
+				:orderby="orderby" :getcount="true" :page-size="options.pageSize" :page-current="options.pageCurrent"
+				v-slot:default="{data,pagination,loading,error,options}" :options="options" loadtime="manual"
+				@load="onqueryload">
+				<uni-table ref="table" :loading="loading" :emptyText="error.message || $t('common.empty')" border stripe
+					type="selection" @selection-change="selectionChange">
+					<uni-tr>
+						<uni-th align="center" filter-type="search"
+							@filter-change="filterChange($event, 'permission_id')" sortable
+							@sort-change="sortChange($event, 'permission_id')">权限标识</uni-th>
+						<uni-th align="center" filter-type="search"
+							@filter-change="filterChange($event, 'permission_name')" sortable
+							@sort-change="sortChange($event, 'permission_name')">权限名称</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'comment')"
+							sortable @sort-change="sortChange($event, 'comment')">备注</uni-th>
+						<uni-th align="center" filter-type="timestamp"
+							@filter-change="filterChange($event, 'create_date')" sortable
+							@sort-change="sortChange($event, 'create_date')">创建时间</uni-th>
+						<uni-th align="center">操作</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index">
+						<uni-td align="center">{{item.permission_id}}</uni-td>
+						<uni-td align="center">{{item.permission_name}}</uni-td>
+						<uni-td align="center">{{item.comment}}</uni-td>
+						<uni-td align="center">
+							<uni-dateformat :threshold="[0, 0]" :date="item.create_date"></uni-dateformat>
+						</uni-td>
+						<uni-td align="center">
+							<view class="uni-group">
+								<button @click="navigateTo('./edit?id='+item._id, false)" class="uni-button" size="mini"
+									type="primary">{{$t('common.button.edit')}}</button>
+								<button @click="confirmDelete(item._id)" class="uni-button" size="mini"
+									type="warn">{{$t('common.button.delete')}}</button>
+							</view>
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon show-page-size :page-size="pagination.size" v-model="pagination.current"
+						:total="pagination.count" @change="onPageChanged" @pageSizeChange="changeSize" />
+				</view>
+			</unicloud-db>
+		</view>
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import {
+		enumConverter,
+		filterToWhere
+	} from '@/js_sdk/validator/uni-id-permissions.js';
+
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbOrderBy = 'create_date desc' // 排序字段
+	const dbSearchFields = ['permission_id', 'permission_name'] // 模糊搜索字段,支持模糊搜索的字段列表
+	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	const orderByMapping = {
+		"ascending": "asc",
+		"descending": "desc"
+	}
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				orderByFieldName: "",
+				selectedIndexs: [],
+				options: {
+					pageSize,
+					pageCurrent,
+					filterData: {},
+					...enumConverter
+				},
+				imageStyles: {
+					width: 64,
+					height: 64
+				},
+				exportExcel: {
+					"filename": "uni-id-permissions.xls",
+					"type": "xls",
+					"fields": {
+						"权限ID": "permission_id",
+						"权限名称": "permission_name",
+						"备注": "comment"
+					}
+				},
+				exportExcelData: []
+			}
+		},
+		onLoad() {
+			this._filter = {}
+		},
+		onReady() {
+			this.$refs.udb.loadData()
+		},
+		methods: {
+			onqueryload(data) {
+				this.exportExcelData = data
+			},
+			changeSize(pageSize) {
+				this.options.pageSize = pageSize
+				this.options.pageCurrent = 1
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				this.where = newWhere
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.selectedIndexs.length = 0
+				this.$refs.table.clearSelection()
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url, clear) {
+				// clear 表示刷新列表时是否清除页码,true 表示刷新并回到列表第 1 页,默认为 true
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData(clear)
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			// 批量删除
+			delTable() {
+				this.$refs.udb.remove(this.selectedItems(), {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				this.$refs.udb.remove(id, {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			sortChange(e, name) {
+				this.orderByFieldName = name;
+				if (e.order) {
+					this.orderby = name + ' ' + orderByMapping[e.order]
+				} else {
+					this.orderby = ''
+				}
+				this.$refs.table.clearSelection()
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			filterChange(e, name) {
+				this._filter[name] = {
+					type: e.filterType,
+					value: e.filter
+				}
+				let newWhere = filterToWhere(this._filter, db.command)
+				if (Object.keys(newWhere).length) {
+					this.where = newWhere
+				} else {
+					this.where = ''
+				}
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 102 - 0
pages/system/role/add.vue

@@ -0,0 +1,102 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :value="formData" validateTrigger="bind">
+      <uni-forms-item name="role_id" label="唯一ID" required>
+        <uni-easyinput placeholder="角色唯一标识,不可修改,不允许重复" v-model="formData.role_id" trim="both"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="role_name" label="名称" required>
+        <uni-easyinput placeholder="角色名称" v-model="formData.role_name" trim="both"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="permission" label="权限" class="flex-center-x">
+        <uni-data-checkbox :multiple="true" v-model="formData.permission" collection="uni-id-permissions" :page-size="500" field="permission_name as text, permission_id as value"></uni-data-checkbox>
+      </uni-forms-item>
+      <uni-forms-item name="comment" label="备注">
+        <uni-easyinput type="textarea" placeholder="备注" v-model="formData.comment" trim="both"></uni-easyinput>
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">{{$t('common.button.submit')}}</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+            <button class="uni-button" style="width: 100px;">{{$t('common.button.back')}}</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '@/js_sdk/validator/uni-id-roles.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'uni-id-roles';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  export default {
+    data() {
+      let formData = {
+        "role_id": "",
+        "role_name": "",
+        "permission": [],
+        "comment": "",
+        "create_date": null
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      /**
+       * 触发表单提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          this.submitForm(res)
+        }).catch(() => {
+          uni.hideLoading()
+        })
+      },
+
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        db.collection(dbCollectionName).add(value).then((res) => {
+          uni.showToast({
+            title: '新增成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      }
+    }
+  }
+</script>
+<style>
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 132 - 0
pages/system/role/edit.vue

@@ -0,0 +1,132 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :value="formData" validateTrigger="bind">
+      <uni-forms-item name="role_id" label="唯一ID" required>
+        <uni-easyinput placeholder="角色唯一标识,不可修改,不允许重复" v-model="formData.role_id" trim="both" disabled></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="role_name" label="名称" required>
+        <uni-easyinput placeholder="角色名称" v-model="formData.role_name" trim="both"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="permission" label="权限" class="flex-center-x">
+        <uni-data-checkbox :multiple="true" v-model="formData.permission" collection="uni-id-permissions" :page-size="500" field="permission_name as text, permission_id as value"></uni-data-checkbox>
+      </uni-forms-item>
+      <uni-forms-item name="comment" label="备注">
+        <uni-easyinput type="textarea" placeholder="备注" v-model="formData.comment" trim="both"></uni-easyinput>
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">{{$t('common.button.submit')}}</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+            <button class="uni-button" style="width: 100px;">{{$t('common.button.back')}}</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '@/js_sdk/validator/uni-id-roles.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'uni-id-roles';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  export default {
+    data() {
+      let formData = {
+        "role_id": "",
+        "role_name": "",
+        "permission": [],
+        "comment": "",
+        "create_date": null
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onLoad(e) {
+      if (e.id) {
+        const id = e.id
+        this.formDataId = id
+        this.getDetail(id)
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      /**
+       * 触发表单提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          this.submitForm(res)
+        }).catch(() => {
+          uni.hideLoading()
+        })
+      },
+
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        db.collection(dbCollectionName).doc(this.formDataId).update(value).then((res) => {
+          uni.showToast({
+            title: '修改成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      },
+
+      /**
+       * 获取表单数据
+       * @param {Object} id
+       */
+      getDetail(id) {
+        uni.showLoading({
+          mask: true
+        })
+        db.collection(dbCollectionName).doc(id).field("role_id,role_name,permission,comment,create_date").get().then((res) => {
+          const data = res.result.data[0]
+          if (data) {
+            this.formData = data
+          }
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      }
+    }
+  }
+</script>
+<style>
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 236 - 0
pages/system/role/list.vue

@@ -0,0 +1,236 @@
+<template>
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<input class="uni-search" type="text" v-model="query" @confirm="search" :placeholder="$t('common.placeholder.query')" />
+				<button class="uni-button hide-on-phone" type="default" size="mini" @click="search">{{$t('common.button.search')}}</button>
+				<button class="uni-button" type="primary" size="mini" @click="navigateTo('./add')">{{$t('common.button.add')}}</button>
+				<button class="uni-button" type="warn" size="mini" :disabled="!selectedIndexs.length"
+					@click="delTable">{{$t('common.button.batchDelete')}}</button>
+				<!-- #ifdef H5 -->
+				<download-excel class="hide-on-phone" :fields="exportExcel.fields" :data="exportExcelData"
+					:type="exportExcel.type" :name="exportExcel.filename">
+					<button class="uni-button" type="primary" size="mini">{{$t('common.button.exportExcel')}}</button>
+				</download-excel>
+				<!-- #endif -->
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" collection="uni-id-roles,uni-id-permissions"
+				field="role_id,role_name,permission{permission_name},comment,create_date" :where="where"
+				page-data="replace" :orderby="orderby" :getcount="true" :page-size="options.pageSize"
+				:page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error,options}"
+				:options="options" loadtime="manual" @load="onqueryload">
+				<uni-table ref="table" :loading="loading" :emptyText="error.message || $t('common.empty')" border stripe
+					type="selection" @selection-change="selectionChange">
+					<uni-tr>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'role_id')"
+							sortable @sort-change="sortChange($event, 'role_id')">唯一ID</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'role_name')"
+							sortable @sort-change="sortChange($event, 'role_name')">名称</uni-th>
+						<uni-th align="center">权限</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'comment')"
+							sortable @sort-change="sortChange($event, 'comment')">备注</uni-th>
+						<uni-th align="center" filter-type="timestamp"
+							@filter-change="filterChange($event, 'create_date')" sortable
+							@sort-change="sortChange($event, 'create_date')">创建时间</uni-th>
+						<uni-th align="center">操作</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index">
+						<uni-td align="center">{{item.role_id}}</uni-td>
+						<uni-td align="center">{{item.role_name}}</uni-td>
+						<uni-td align="center">{{item.permission}}</uni-td>
+						<uni-td align="center">{{item.comment}}</uni-td>
+						<uni-td align="center">
+							<uni-dateformat :threshold="[0, 0]" :date="item.create_date"></uni-dateformat>
+						</uni-td>
+						<uni-td align="center">
+							<view class="uni-group">
+								<button @click="navigateTo('./edit?id='+item._id, false)" class="uni-button" size="mini"
+									type="primary">{{$t('common.button.edit')}}</button>
+								<button @click="confirmDelete(item._id)" class="uni-button" size="mini"
+									type="warn">{{$t('common.button.delete')}}</button>
+							</view>
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon show-page-size :page-size="pagination.size" v-model="pagination.current"
+						:total="pagination.count" @change="onPageChanged" @pageSizeChange="changeSize"/>
+				</view>
+			</unicloud-db>
+		</view>
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import {
+		enumConverter,
+		filterToWhere
+	} from '@/js_sdk/validator/uni-id-roles.js';
+
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbOrderBy = 'create_date desc' // 排序字段
+	const dbSearchFields = ['role_id', 'role_name', 'permission.permission_name'] // 支持模糊搜索的字段列表	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	const orderByMapping = {
+		"ascending": "asc",
+		"descending": "desc"
+	}
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				orderByFieldName: "",
+				selectedIndexs: [],
+				options: {
+					pageSize,
+					pageCurrent,
+					filterData: {},
+					...enumConverter
+				},
+				imageStyles: {
+					width: 64,
+					height: 64
+				},
+				exportExcel: {
+					"filename": "uni-id-roles.xls",
+					"type": "xls",
+					"fields": {
+						"唯一ID": "role_id",
+						"名称": "role_name",
+						"权限": "permission",
+						"备注": "comment",
+						"create_date": "create_date"
+					}
+				},
+				exportExcelData: []
+			}
+		},
+		onLoad() {
+			this._filter = {}
+		},
+		onReady() {
+			this.$refs.udb.loadData()
+		},
+		methods: {
+			onqueryload(data) {
+				for (var i = 0; i < data.length; i++) {
+					let item = data[i]
+					item.permission = item.permission.map(pItem => pItem.permission_name).join('、')
+					item.create_date = this.$formatDate(item.create_date)
+				}
+				this.exportExcelData = data
+			},
+			changeSize(pageSize) {
+				this.options.pageSize = pageSize
+				this.options.pageCurrent = 1
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				this.where = newWhere
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.selectedIndexs.length = 0
+				this.$refs.table.clearSelection()
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url, clear) {
+				// clear 表示刷新列表时是否清除页码,true 表示刷新并回到列表第 1 页,默认为 true
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData(clear)
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			// 批量删除
+			delTable() {
+				this.$refs.udb.remove(this.selectedItems(), {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				this.$refs.udb.remove(id, {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			sortChange(e, name) {
+				this.orderByFieldName = name;
+				if (e.order) {
+					this.orderby = name + ' ' + orderByMapping[e.order]
+				} else {
+					this.orderby = ''
+				}
+				this.$refs.table.clearSelection()
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			filterChange(e, name) {
+				this._filter[name] = {
+					type: e.filterType,
+					value: e.filter
+				}
+				let newWhere = filterToWhere(this._filter, db.command)
+				if (Object.keys(newWhere).length) {
+					this.where = newWhere
+				} else {
+					this.where = ''
+				}
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 122 - 0
pages/system/safety/list.vue

@@ -0,0 +1,122 @@
+<template>
+	<view>
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<!-- <input class="uni-search" type="text" v-model="query" placeholder="用户/内容" />
+				<button class="uni-button" type="default" size="mini" @click="search">搜索</button> -->
+				<input class="uni-search" type="text" v-model="query" @confirm="search" :placeholder="$t('common.placeholder.query')" />
+				<button class="uni-button hide-on-phone" type="default" size="mini" @click="search">{{$t('common.button.search')}}</button>
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" :collection="collectionName" orderby="create_date desc" field="type, ip, create_date, user_id{username}[0].username as username" :options="options" :where="where" page-data="replace" :orderby="orderby"
+			 :getcount="true" :page-size="options.pageSize" :page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error}">
+				<uni-table :loading="loading" :emptyText="error.message || '没有更多数据'" border stripe >
+					<uni-tr>
+						<uni-th align="center">序号</uni-th>
+						<uni-th align="center">用户</uni-th>
+						<uni-th align="center">内容</uni-th>
+						<uni-th align="center">IP</uni-th>
+						<uni-th align="center">时间</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index">
+						<uni-td align="center">{{(pagination.current -1)*pagination.size + (index+1)}}</uni-td>
+						<uni-td align="center">{{item.user_id[0] && item.user_id[0].username || '-'}}</uni-td>
+						<uni-td align="center">{{item.type}}</uni-td>
+						<uni-td align="center">{{item.ip}}</uni-td>
+						<uni-td align="center">
+							<uni-dateformat :date="item.create_date" :threshold="[0, 0]" />
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon :page-size="pagination.size" v-model="pagination.current" :total="pagination.count"
+					 @change="onPageChanged" />
+				</view>
+			</unicloud-db>
+		</view>
+	</view>
+</template>
+
+<script>
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbCollectionName = 'uni-id-log, uni-id-users'
+	const dbOrderBy = 'create_date' // 排序字段
+	const dbSearchFields = ["user_id.username","type", "ip"] // 支持模糊搜索的字段列表
+	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				collectionName: dbCollectionName,
+				options: {
+					pageSize,
+					pageCurrent
+				}
+			}
+		},
+		methods: {
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				const isSameWhere = newWhere === this.where
+				this.where = newWhere
+				if (isSameWhere) { // 相同条件时,手动强制刷新
+					this.loadData()
+				}
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url) {
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData()
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			//批量删除
+			delTable() {
+				this.$refs.udb.remove(this.selectedItems())
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				this.$refs.udb.remove(id)
+			}
+		}
+	}
+</script>
+<style>
+</style>

+ 100 - 0
pages/system/tag/add.vue

@@ -0,0 +1,100 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :value="formData" validateTrigger="bind">
+      <uni-forms-item name="tagid" label="标签tagid" required>
+        <uni-easyinput placeholder="标签的tagid" v-model="formData.tagid"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="name" label="标签名称" required>
+        <uni-easyinput placeholder="标签名称" v-model="formData.name"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="description" label="标签描述">
+        <textarea placeholder="标签描述" @input="binddata('description', $event.detail.value)" class="uni-textarea-border" v-model="formData.description"></textarea>
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">提交</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+          <button class="uni-button" style="width: 100px;">返回</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '@/js_sdk/validator/uni-id-tag.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'uni-id-tag';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  export default {
+    data() {
+      let formData = {
+        "tagid": "",
+        "name": "",
+        "description": ""
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      /**
+       * 验证表单并提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          return this.submitForm(res)
+        }).catch(() => {
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      },
+
+      /**
+       * 提交表单
+       */
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        return db.collection(dbCollectionName).add(value).then((res) => {
+          uni.showToast({
+            title: '新增成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          this.getOpenerEventChannel().emit('refreshCheckboxData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        })
+      }
+    }
+  }
+</script>
+<style>
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 129 - 0
pages/system/tag/edit.vue

@@ -0,0 +1,129 @@
+<template>
+  <view class="uni-container">
+    <uni-forms ref="form" :value="formData" validateTrigger="bind">
+      <uni-forms-item name="tagid" label="标签的tagid" required>
+        <uni-easyinput :disabled="true" placeholder="标签tagid" v-model="formData.tagid"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="name" label="标签名称" required>
+        <uni-easyinput placeholder="标签名称" v-model="formData.name"></uni-easyinput>
+      </uni-forms-item>
+      <uni-forms-item name="description" label="标签描述">
+        <textarea placeholder="标签描述" @input="binddata('description', $event.detail.value)" class="uni-textarea-border" v-model="formData.description"></textarea>
+      </uni-forms-item>
+      <view class="uni-button-group">
+        <button type="primary" class="uni-button" style="width: 100px;" @click="submit">提交</button>
+        <navigator open-type="navigateBack" style="margin-left: 15px;">
+          <button class="uni-button" style="width: 100px;">返回</button>
+        </navigator>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script>
+  import { validator } from '@/js_sdk/validator/uni-id-tag.js';
+
+  const db = uniCloud.database();
+  const dbCmd = db.command;
+  const dbCollectionName = 'uni-id-tag';
+
+  function getValidator(fields) {
+    let result = {}
+    for (let key in validator) {
+      if (fields.includes(key)) {
+        result[key] = validator[key]
+      }
+    }
+    return result
+  }
+
+  export default {
+    data() {
+      let formData = {
+        "tagid": "",
+        "name": "",
+        "description": ""
+      }
+      return {
+        formData,
+        formOptions: {},
+        rules: {
+          ...getValidator(Object.keys(formData))
+        }
+      }
+    },
+    onLoad(e) {
+      if (e.id) {
+        const id = e.id
+        this.formDataId = id
+        this.getDetail(id)
+      }
+    },
+    onReady() {
+      this.$refs.form.setRules(this.rules)
+    },
+    methods: {
+      /**
+       * 验证表单并提交
+       */
+      submit() {
+        uni.showLoading({
+          mask: true
+        })
+        this.$refs.form.validate().then((res) => {
+          return this.submitForm(res)
+        }).catch(() => {
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      },
+
+      /**
+       * 提交表单
+       */
+      submitForm(value) {
+        // 使用 clientDB 提交数据
+        return db.collection(dbCollectionName).doc(this.formDataId).update(value).then((res) => {
+          uni.showToast({
+            title: '修改成功'
+          })
+          this.getOpenerEventChannel().emit('refreshData')
+          setTimeout(() => uni.navigateBack(), 500)
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        })
+      },
+
+      /**
+       * 获取表单数据
+       * @param {Object} id
+       */
+      getDetail(id) {
+        uni.showLoading({
+          mask: true
+        })
+        db.collection(dbCollectionName).doc(id).field("tagid,name,description").get().then((res) => {
+          const data = res.result.data[0]
+          if (data) {
+            this.formData = data
+          }
+        }).catch((err) => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(() => {
+          uni.hideLoading()
+        })
+      }
+    }
+  }
+</script>
+<style>
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 230 - 0
pages/system/tag/list.vue

@@ -0,0 +1,230 @@
+<template>
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<input class="uni-search" type="text" v-model="query" @confirm="search" placeholder="请输入搜索内容" />
+				<button class="uni-button hide-on-phone" type="default" size="mini" @click="search">搜索</button>
+				<button class="uni-button" type="primary" size="mini" @click="navigateTo('./add')">新增</button>
+				<button class="uni-button" type="warn" size="mini" :disabled="!selectedIndexs.length"
+					@click="delTable">批量删除</button>
+				<download-excel class="hide-on-phone" :fields="exportExcel.fields" :data="exportExcelData"
+					:type="exportExcel.type" :name="exportExcel.filename">
+					<button class="uni-button" type="primary" size="mini">导出 Excel</button>
+				</download-excel>
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" collection="uni-id-tag" field="tagid,name,description,create_date" :where="where"
+				page-data="replace" :orderby="orderby" :getcount="true" :page-size="options.pageSize"
+				:page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error,options}"
+				:options="options" loadtime="manual" @load="onqueryload">
+				<uni-table ref="table" :loading="loading" :emptyText="error.message || '没有更多数据'" border stripe
+					type="selection" @selection-change="selectionChange">
+					<uni-tr>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'tagid')"
+							sortable @sort-change="sortChange($event, 'tagid')">标签的tagid</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'name')"
+							sortable @sort-change="sortChange($event, 'name')">标签名称</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'description')"
+							sortable @sort-change="sortChange($event, 'description')">标签描述</uni-th>
+						<uni-th align="center" filter-type="timestamp"
+							@filter-change="filterChange($event, 'create_date')" sortable
+							@sort-change="sortChange($event, 'create_date')">创建时间</uni-th>
+						<uni-th align="center">操作</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index">
+						<uni-td align="center">{{item.tagid}}</uni-td>
+						<uni-td align="center">
+							<uni-tag type="primary" inverted size="small" :text="item.name"></uni-tag>
+						</uni-td>
+						<uni-td align="center">{{item.description}}</uni-td>
+						<uni-td align="center">
+							<uni-dateformat :threshold="[0, 0]" :date="item.create_date"></uni-dateformat>
+						</uni-td>
+						<uni-td align="center">
+							<view class="uni-group">
+								<button @click="navigateTo('../user/list?tagid='+item.tagid, false)" class="uni-button"
+									size="mini" type="primary">成员</button>
+								<button @click="navigateTo('./edit?id='+item._id, false)" class="uni-button" size="mini"
+									type="primary">修改</button>
+								<button @click="confirmDelete(item._id)" class="uni-button" size="mini"
+									type="warn">删除</button>
+							</view>
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-iconn show-page-size :page-size="pagination.size" v-model="pagination.current"
+						:total="pagination.count" @change="onPageChanged" @pageSizeChange="changeSize"/>
+				</view>
+			</unicloud-db>
+		</view>
+
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import {
+		enumConverter,
+		filterToWhere
+	} from '@/js_sdk/validator/uni-id-tag.js';
+
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbOrderBy = '' // 排序字段
+	const dbSearchFields = [] // 模糊搜索字段,支持模糊搜索的字段列表。联表查询格式: 主表字段名.副表字段名,例如用户表关联角色表 role.role_name
+	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	const orderByMapping = {
+		"ascending": "asc",
+		"descending": "desc"
+	}
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				orderByFieldName: "",
+				selectedIndexs: [],
+				options: {
+					pageSize,
+					pageCurrent,
+					filterData: {},
+					...enumConverter
+				},
+				imageStyles: {
+					width: 64,
+					height: 64
+				},
+				exportExcel: {
+					"filename": "uni-id-tag.xls",
+					"type": "xls",
+					"fields": {
+						"标签的tagid": "tagid",
+						"标签名称": "name",
+						"标签描述": "description"
+					}
+				},
+				exportExcelData: []
+			}
+		},
+		onLoad() {
+			this._filter = {}
+		},
+		onReady() {
+			this.$refs.udb.loadData()
+		},
+		methods: {
+			onqueryload(data) {
+				this.exportExcelData = data
+			},
+			changeSize(pageSize) {
+				this.options.pageSize = pageSize
+				this.options.pageCurrent = 1
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				this.where = newWhere
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.selectedIndexs.length = 0
+				this.$refs.table.clearSelection()
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url, clear) {
+				// clear 表示刷新列表时是否清除页码,true 表示刷新并回到列表第 1 页,默认为 true
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadData(clear)
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			// 批量删除
+			delTable() {
+				this.$refs.udb.remove(this.selectedItems(), {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				this.$refs.udb.remove(id, {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			sortChange(e, name) {
+				this.orderByFieldName = name;
+				if (e.order) {
+					this.orderby = name + ' ' + orderByMapping[e.order]
+				} else {
+					this.orderby = ''
+				}
+				this.$refs.table.clearSelection()
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			filterChange(e, name) {
+				this._filter[name] = {
+					type: e.filterType,
+					value: e.filter
+				}
+				let newWhere = filterToWhere(this._filter, db.command)
+				if (Object.keys(newWhere).length) {
+					this.where = newWhere
+				} else {
+					this.where = ''
+				}
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 193 - 0
pages/system/user/add.vue

@@ -0,0 +1,193 @@
+<template>
+	<view class="uni-container">
+		<uni-forms ref="form" v-model="formData" :rules="rules" validateTrigger="bind" @submit="submit">
+			<uni-forms-item name="username" label="用户名" required>
+				<uni-easyinput v-model="formData.username" :clearable="false" placeholder="请输入用户名" />
+			</uni-forms-item>
+			<uni-forms-item name="nickname" label="用户昵称" required>
+				<uni-easyinput v-model="formData.nickname" :clearable="false" placeholder="请输入用户昵称" />
+			</uni-forms-item>
+			<uni-forms-item name="password" label="初始密码" required>
+				<uni-easyinput v-model="formData.password" :clearable="false" placeholder="请输入初始密码" />
+			</uni-forms-item>
+			<uni-forms-item name="role" label="角色列表" class="flex-center-x">
+				<uni-data-checkbox multiple :localdata="roles" v-model="formData.role" />
+			</uni-forms-item>
+			<uni-forms-item name="tags" label="用户标签" labelWidth="100" class="flex-center-x">
+				<uni-data-checkbox ref="checkbox" :multiple="true" v-model="formData.tags" collection="uni-id-tag"
+					field="tagid as value, name as text"></uni-data-checkbox>
+				<span class="link-btn" @click="gotoTagAdd">新增</span>
+				<span class="link-btn" @click="gotoTagList" style="margin-left: 10px;">管理</span>
+			</uni-forms-item>
+			<uni-forms-item name="authorizedApp" label="可登录应用" labelWidth="100" class="flex-center-x">
+				<uni-data-checkbox :multiple="true" v-model="formData.authorizedApp" collection="opendb-app-list"
+					field="appid as value, name as text"></uni-data-checkbox>
+				<span class="link-btn" @click="gotoAppList">管理</span>
+			</uni-forms-item>
+			<uni-forms-item name="mobile" label="手机号">
+				<uni-easyinput v-model="formData.mobile" :clearable="false" placeholder="请输入手机号" />
+			</uni-forms-item>
+			<uni-forms-item name="email" label="邮箱">
+				<uni-easyinput v-model="formData.email" :clearable="false" placeholder="请输入邮箱" />
+			</uni-forms-item>
+			<uni-forms-item name="status" label="是否启用">
+				<switch @change="binddata('status', $event.detail.value)" :checked="formData.status" />
+			</uni-forms-item>
+			<view class="uni-button-group">
+				<button style="width: 100px;" type="primary" class="uni-button"
+					@click="submitForm">{{$t('common.button.submit')}}</button>
+				<navigator open-type="navigateBack" style="margin-left: 15px;"><button style="width: 100px;"
+						class="uni-button">{{$t('common.button.back')}}</button></navigator>
+			</view>
+		</uni-forms>
+	</view>
+</template>
+
+<script>
+	import {
+		validator
+	} from '@/js_sdk/validator/uni-id-users.js';
+
+	const db = uniCloud.database();
+	const dbCmd = db.command;
+	const dbCollectionName = 'uni-id-users';
+
+	function getValidator(fields) {
+		let result = {}
+		for (let key in validator) {
+			if (fields.includes(key)) {
+				result[key] = validator[key]
+			}
+		}
+		return result
+	}
+
+	export default {
+		data() {
+			return {
+				formData: {
+					"username": "",
+					"nickname": "",
+					"password": "",
+					"role": [],
+					"authorizedApp": [],
+					"tags": [],
+					"mobile": "",
+					"email": "",
+					"status": true //默认启用
+				},
+				rules: {
+					...getValidator(["username", "password", "role", "mobile", "email"]),
+					"status": {
+						"rules": [{
+							"format": "bool"
+						}]
+					}
+				},
+				roles: []
+			}
+		},
+		onLoad() {
+			this.loadroles()
+		},
+		methods: {
+			/**
+			 * 跳转应用管理的 list 页
+			 */
+			gotoAppList() {
+				uni.navigateTo({
+					url: '../app/list'
+				})
+			},
+			gotoTagList() {
+				uni.navigateTo({
+					url: '../tag/list'
+				})
+			},
+			gotoTagAdd() {
+				uni.navigateTo({
+					url: '../tag/add',
+					events: {
+						refreshCheckboxData: () => {
+							this.$refs.checkbox.loadData()
+						}
+					}
+				})
+			},
+
+			/**
+			 * 触发表单提交
+			 */
+			submitForm() {
+				this.$refs.form.submit();
+			},
+
+			/**
+			 * 表单提交
+			 * @param {Object} event 回调参数 Function(callback:{value,errors})
+			 */
+			submit(event) {
+				const {
+					value,
+					errors
+				} = event.detail
+
+				// 表单校验失败页面会提示报错 ,要停止表单提交逻辑
+				if (errors) {
+					return
+				}
+				uni.showLoading({
+					title: '提交中...',
+					mask: true
+				})
+				// 是否启用功能的数据类型转换, 0 正常, 1 禁用
+				if (typeof value.status === "boolean") {
+					value.status = Number(!value.status)
+				}
+				this.$request('addUser', value).then(() => {
+					uni.showToast({
+						title: '新增成功'
+					})
+					this.getOpenerEventChannel().emit('refreshData')
+					setTimeout(() => uni.navigateBack(), 500)
+				}).catch(err => {
+					uni.showModal({
+						content: err.message || '请求服务失败',
+						showCancel: false
+					})
+				}).finally(err => {
+					uni.hideLoading()
+				})
+			},
+			loadroles() {
+				db.collection('uni-id-roles').limit(500).get().then(res => {
+					const roleIds = []
+					this.roles = res.result.data.map(item => {
+						roleIds.push(item.role_id)
+						return {
+							value: item.role_id,
+							text: item.role_name
+						}
+					})
+					if (roleIds.indexOf('admin') === -1) {
+						this.roles.unshift({
+							value: 'admin',
+							text: '超级管理员'
+						})
+					}
+				}).catch(err => {
+					uni.showModal({
+						title: '提示',
+						content: err.message,
+						showCancel: false
+					})
+				})
+			}
+		}
+	}
+</script>
+<style>
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 277 - 0
pages/system/user/edit.vue

@@ -0,0 +1,277 @@
+<template>
+	<view class="uni-container">
+		<uni-forms ref="form" v-model="formData" :rules="rules" validateTrigger="bind" @submit="submit">
+			<uni-forms-item name="username" label="用户名" required>
+				<uni-easyinput v-model="formData.username" :clearable="false" placeholder="请输入用户名" />
+			</uni-forms-item>
+			<uni-forms-item name="nickname" label="用户昵称" required>
+				<uni-easyinput v-model="formData.nickname" :clearable="false" placeholder="请输入用户昵称" />
+			</uni-forms-item>
+			<uni-forms-item :name="showPassword ? 'password' : ''" label="重置密码">
+				<span v-show="!showPassword" class="reset-password-btn" @click="trigger">点击重置密码</span>
+				<uni-easyinput v-show="showPassword" v-model="formData.password" :clearable="false" placeholder="请输入重置密码">
+					<view slot="right" class="cancel-reset-password-btn" @click="trigger">取消</view>
+				</uni-easyinput>
+			</uni-forms-item>
+			<uni-forms-item name="role" label="角色列表" class="flex-center-x">
+				<uni-data-checkbox multiple :localdata="roles" v-model="formData.role" />
+			</uni-forms-item>
+			<uni-forms-item name="tags" label="用户标签" labelWidth="100" class="flex-center-x">
+				<uni-data-checkbox :multiple="true" v-model="formData.tags" collection="uni-id-tag"
+					field="tagid as value, name as text"></uni-data-checkbox>
+				<span class="link-btn" @click="gotoTagAdd">新增</span>
+				<span class="link-btn" @click="gotoTagList" style="margin-left: 10px;">管理</span>
+			</uni-forms-item>
+			<uni-forms-item name="authorizedApp" label="可登录应用" class="flex-center-x">
+				<uni-data-checkbox :multiple="true" v-model="formData.authorizedApp" collection="opendb-app-list"
+					field="appid as value, name as text"></uni-data-checkbox>
+				<span class="link-btn" @click="gotoAppList">管理</span>
+			</uni-forms-item>
+			<uni-forms-item name="mobile" label="手机号">
+				<uni-easyinput v-model="formData.mobile" :clearable="false" placeholder="请输入手机号" />
+			</uni-forms-item>
+			<uni-forms-item name="email" label="邮箱">
+				<uni-easyinput v-model="formData.email" :clearable="false" placeholder="请输入邮箱" />
+			</uni-forms-item>
+			<uni-forms-item name="status" label="用户状态">
+				<switch v-if="Number(formData.status) < 2" @change="binddata('status', $event.detail.value)" :checked="formData.status" />
+				<view v-else class="uni-form-item-empty">{{parseUserStatus(formData.status)}}</view>
+			</uni-forms-item>
+			<view class="uni-button-group">
+				<button style="width: 100px;" type="primary" class="uni-button" @click="submitForm">{{$t('common.button.submit')}}</button>
+				<navigator open-type="navigateBack" style="margin-left: 15px;"><button style="width: 100px;" class="uni-button">{{$t('common.button.back')}}</button></navigator>
+			</view>
+		</uni-forms>
+	</view>
+</template>
+
+<script>
+	import { validator } from '@/js_sdk/validator/uni-id-users.js';
+
+	const db = uniCloud.database();
+	const dbCmd = db.command;
+	const dbCollectionName = 'uni-id-users';
+
+	function getValidator(fields) {
+		let result = {}
+		for (let key in validator) {
+			if (fields.includes(key)) {
+				result[key] = validator[key]
+			}
+		}
+		return result
+	}
+
+	export default {
+		data() {
+			return {
+				showPassword: false,
+				formData: {
+					"username": "",
+					"nickname": "",
+					"password": "",
+					"role": [],
+					"authorizedApp": [],
+					"mobile": "",
+					"email": "",
+					"status": false //默认禁用
+				},
+				rules: {
+					...getValidator(["username", "password", "role", "mobile", "email"]),
+					"status": {
+						"rules": [{
+							"format": "bool"
+						}]
+					}
+				},
+				roles: []
+			}
+		},
+		onLoad(e) {
+			const id = e.id
+			this.formDataId = id
+			this.getDetail(id)
+			this.loadroles()
+		},
+		methods: {
+			/**
+			 * 跳转应用管理的 list 页
+			 */
+			gotoAppList() {
+				uni.navigateTo({
+					url: '../app/list'
+				})
+			},
+			gotoTagList() {
+				uni.navigateTo({
+					url: '../tag/list'
+				})
+			},
+			gotoTagAdd() {
+				uni.navigateTo({
+					url: '../tag/add',
+					events: {
+						refreshCheckboxData: () => {
+							this.$refs.checkbox.loadData()
+						}
+					}
+				})
+			},
+			/**
+			 * 切换重置密码框显示或隐藏
+			 */
+			trigger() {
+				this.showPassword = !this.showPassword
+			},
+
+			/**
+			 * 触发表单提交
+			 */
+			submitForm(form) {
+				this.$refs.form.submit();
+			},
+
+			/**
+			 * 表单提交
+			 * @param {Object} event 回调参数 Function(callback:{value,errors})
+			 */
+			submit(event) {
+				const {
+					value,
+					errors
+				} = event.detail
+				// 表单校验失败页面会提示报错 ,要停止表单提交逻辑
+				if (errors) {
+					return
+				}
+				uni.showLoading({
+					title: '修改中...',
+					mask: true
+				})
+
+				// 是否启用功能的数据类型转换, 0 正常, 1 禁用
+				if (typeof value.status === "boolean") {
+					value.status = Number(!value.status)
+				}
+				value.uid = this.formDataId
+
+				this.$request('updateUser', value).then(() => {
+					uni.showToast({
+						title: '修改成功'
+					})
+					this.getOpenerEventChannel().emit('refreshData')
+					setTimeout(() => uni.navigateBack(), 500)
+				}).catch(err => {
+					uni.showModal({
+						content: err.message || '请求服务失败',
+						showCancel: false
+					})
+				}).finally(err => {
+					uni.hideLoading()
+				})
+			},
+
+			resetPWd(resetData) {
+				this.$request('system/user/resetPwd', resetData)
+					.then().catch(err => {
+						uni.showModal({
+							content: err.message || '请求服务失败',
+							showCancel: false
+						})
+					}).finally()
+			},
+			/**
+			 * 获取表单数据
+			 * @param {Object} id
+			 */
+			getDetail(id) {
+				uni.showLoading({
+					mask: true
+				})
+				db.collection(dbCollectionName)
+					.doc(id)
+					.field('username,nickname,role,dcloud_appid as authorizedApp,tags,mobile,email,status')
+					.get()
+					.then((res) => {
+						const data = res.result.data[0]
+						if (data) {
+							if (data.status === undefined) {
+								data.status = true
+							}
+							if (data.status === 0) {
+								data.status = true
+							}
+							if (data.status === 1) {
+								data.status = false
+							}
+							this.formData = Object.assign(this.formData, data)
+						}
+					}).catch((err) => {
+						uni.showModal({
+							content: err.message || '请求服务失败',
+							showCancel: false
+						})
+					}).finally(() => {
+						uni.hideLoading()
+					})
+			},
+			loadroles() {
+				db.collection('uni-id-roles').limit(500).get().then(res => {
+					const roleIds = []
+					this.roles = res.result.data.map(item => {
+						roleIds.push(item.role_id)
+						return {
+							value: item.role_id,
+							text: item.role_name
+						}
+					})
+					if (roleIds.indexOf('admin') === -1) {
+						this.roles.unshift({
+							value: 'admin',
+							text: '超级管理员'
+						})
+					}
+				}).catch(err => {
+					uni.showModal({
+						title: '提示',
+						content: err.message,
+						showCancel: false
+					})
+				})
+			},
+			// status 对应文字显示
+			parseUserStatus(status) {
+				if (status === 0) {
+					return '启用'
+				} else if (status === 1) {
+					return '禁用'
+				} else if (status === 2) {
+					return '审核中'
+				} else if (status === 3) {
+					return '审核拒绝'
+				} else {
+					return '启用'
+				}
+			}
+		}
+	}
+</script>
+
+<style>
+	.reset-password-btn {
+		/* height: 100%; */
+		line-height: 36px;
+		color: #007AFF;
+		text-decoration: underline;
+		cursor: pointer;
+	}
+
+	.cancel-reset-password-btn {
+		color: #007AFF;
+		padding-right: 10px;
+		cursor: pointer;
+	}
+	::v-deep .uni-forms-item__label {
+		width: 90px !important;
+	}
+</style>

+ 406 - 0
pages/system/user/list.vue

@@ -0,0 +1,406 @@
+<template>
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<input class="uni-search" type="text" v-model="query" @confirm="search"
+					:placeholder="$t('common.placeholder.query')" />
+				<button class="uni-button hide-on-phone" type="default" size="mini"
+					@click="search">{{$t('common.button.search')}}</button>
+				<button class="uni-button" type="primary" size="mini"
+					@click="navigateTo('./add')">{{$t('common.button.add')}}</button>
+				<button class="uni-button" type="warn" size="mini" :disabled="!selectedIndexs.length"
+					@click="delTable">{{$t('common.button.batchDelete')}}</button>
+				<button class="uni-button" type="primary" size="mini" :disabled="!selectedIndexs.length"
+					@click="openTagsPopup">标签管理</button>
+				<!-- #ifdef H5 -->
+				<download-excel class="hide-on-phone" :fields="exportExcel.fields" :data="exportExcelData"
+					:type="exportExcel.type" :name="exportExcel.filename">
+					<button class="uni-button" type="primary" size="mini">{{$t('common.button.exportExcel')}}</button>
+				</download-excel>
+				<!-- #endif -->
+			</view>
+		</view>
+		<view class="uni-container">
+			<unicloud-db ref="udb" collection="uni-id-users,uni-id-roles"
+				field="username,nickname,mobile,status,email,role{role_name},dcloud_appid,tags,last_login_date" :where="where"
+				page-data="replace" :orderby="orderby" :getcount="true" :page-size="options.pageSize"
+				:page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error,options}"
+				:options="options" loadtime="manual" @load="onqueryload">
+				<uni-table ref="table" :loading="loading" :emptyText="error.message || $t('common.empty')" border stripe
+					type="selection" @selection-change="selectionChange">
+					<uni-tr>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'username')"
+							sortable @sort-change="sortChange($event, 'username')">用户名</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'nickname')"
+							sortable @sort-change="sortChange($event, 'nickname')">用户昵称</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'mobile')"
+							sortable @sort-change="sortChange($event, 'mobile')">手机号码</uni-th>
+						<uni-th align="center" filter-type="select" :filter-data="options.filterData.status_localdata"
+							@filter-change="filterChange($event, 'status')">用户状态</uni-th>
+						<uni-th align="center" filter-type="search" @filter-change="filterChange($event, 'email')"
+							sortable @sort-change="sortChange($event, 'email')">邮箱</uni-th>
+						<uni-th align="center">角色</uni-th>
+						<uni-th align="center" filter-type="select" :filter-data="tagsData"
+							@filter-change="filterChange($event, 'tags')">用户标签</uni-th>
+						<uni-th align="center">可登录应用</uni-th>
+						<uni-th align="center" filter-type="timestamp"
+							@filter-change="filterChange($event, 'last_login_date')" sortable
+							@sort-change="sortChange($event, 'last_login_date')">最后登录时间</uni-th>
+						<uni-th align="center">操作</uni-th>
+					</uni-tr>
+					<uni-tr v-for="(item,index) in data" :key="index">
+						<uni-td align="center">{{item.username}}</uni-td>
+						<uni-td align="center">{{item.nickname}}</uni-td>
+						<uni-td align="center">{{item.mobile}}</uni-td>
+						<uni-td align="center">{{options.status_valuetotext[item.status]}}</uni-td>
+						<uni-td align="center">
+							<uni-link :href="'mailto:'+item.email" :text="item.email"></uni-link>
+						</uni-td>
+						<uni-td align="center">{{item.role}}</uni-td>
+						<uni-td align="center">
+							<template v-if="item.tags" v-for="tag in item.tags">
+								<uni-tag type="primary" inverted size="small" :text="tag" style="margin: 0 5px;">
+								</uni-tag>
+							</template>
+						</uni-td>
+						<uni-td align="center">
+							<uni-link v-if="item.dcloud_appid === undefined" :href="noAppidWhatShouldIDoLink">
+								未绑定可登录应用<view class="uni-icons-help"></view>
+							</uni-link>
+							{{item.dcloud_appid}}
+						</uni-td>
+						<uni-td align="center">
+							<uni-dateformat :threshold="[0, 0]" :date="item.last_login_date"></uni-dateformat>
+						</uni-td>
+						<uni-td align="center">
+							<view class="uni-group">
+								<button @click="navigateTo('./edit?id='+item._id, false)" class="uni-button" size="mini"
+									type="primary">{{$t('common.button.edit')}}</button>
+								<button @click="confirmDelete(item._id)" class="uni-button" size="mini"
+									type="warn">{{$t('common.button.delete')}}</button>
+							</view>
+						</uni-td>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-iconn show-page-size :page-size="pagination.size" v-model="pagination.current"
+						:total="pagination.count" @change="onPageChanged" @pageSizeChange="changeSize" />
+				</view>
+			</unicloud-db>
+		</view>
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+		<uni-popup ref="tagsPopup" type="center">
+			<view class="tags-manager--x">
+				<view class="tags-manager--header mb">管理标签</view>
+				<uni-data-checkbox ref="checkbox" v-model="managerTags" class="mb ml" :multiple="true"
+					collection="uni-id-tag" field="tagid as value, name as text"></uni-data-checkbox>
+				<view class="uni-group">
+					<button @click="managerMultiTag" class="uni-button" type="primary"
+						style="margin-right: 75px;">保存</button>
+				</view>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script>
+	import {
+		enumConverter,
+		filterToWhere
+	} from '../../../js_sdk/validator/uni-id-users.js';
+
+	const db = uniCloud.database()
+	// 表查询配置
+	const dbOrderBy = 'last_login_date desc' // 排序字段
+	const dbSearchFields = ['username', 'role.role_name', 'mobile', 'email'] // 支持模糊搜索的字段列表
+	// 分页配置
+	const pageSize = 20
+	const pageCurrent = 1
+
+	const orderByMapping = {
+		"ascending": "asc",
+		"descending": "desc"
+	}
+
+	export default {
+		data() {
+			return {
+				query: '',
+				where: '',
+				orderby: dbOrderBy,
+				orderByFieldName: "",
+				selectedIndexs: [],
+				pageSizeIndex: 0,
+				pageSizeOption: [20, 50, 100, 500],
+				tags: {},
+				managerTags: [],
+				queryTagid: '',
+				options: {
+					pageSize,
+					pageCurrent,
+					filterData: {
+						"status_localdata": [{
+								"text": "正常",
+								"value": 0,
+								"checked": true
+							},
+							{
+								"text": "禁用",
+								"value": 1
+							},
+							{
+								"text": "审核中",
+								"value": 2
+							},
+							{
+								"text": "审核拒绝",
+								"value": 3
+							}
+						]
+					},
+					...enumConverter
+				},
+				imageStyles: {
+					width: 64,
+					height: 64
+				},
+				exportExcel: {
+					"filename": "uni-id-users.xls",
+					"type": "xls",
+					"fields": {
+						"用户名": "username",
+						"手机号码": "mobile",
+						"用户状态": "status",
+						"邮箱": "email",
+						"角色": "role",
+						"last_login_date": "last_login_date"
+					}
+				},
+				exportExcelData: [],
+				noAppidWhatShouldIDoLink: 'https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=makeup-dcloud-appid'
+			}
+		},
+		onLoad(e) {
+			this._filter = {}
+			const tagid = e.tagid
+			if (tagid) {
+				this.queryTagid = tagid
+				const options = {
+					filterType: "select",
+					filter: [tagid]
+				}
+				this.filterChange(options, "tags")
+			}
+		},
+		onReady() {
+			this.loadTags()
+			if (!this.queryTagid) {
+				this.$refs.udb.loadData()
+			}
+		},
+		computed: {
+			tagsData() {
+				const dynamic_data = []
+				for (const key in this.tags) {
+					const tag = {
+						value: key,
+						text: this.tags[key]
+					}
+					if (key === this.queryTagid) {
+						tag.checked = true
+					}
+					dynamic_data.push(tag)
+				}
+				return dynamic_data
+			}
+		},
+		methods: {
+			onqueryload(data) {
+				for (var i = 0; i < data.length; i++) {
+					let item = data[i]
+					const roleArr = item.role.map(item => item.role_name)
+					item.role = roleArr.join('、')
+					const tagsArr = item.tags && item.tags.map(item => this.tags[item])
+					item.tags = tagsArr
+					if (Array.isArray(item.dcloud_appid)) {
+						item.dcloud_appid = item.dcloud_appid.join('、')
+					}
+					item.last_login_date = this.$formatDate(item.last_login_date)
+				}
+				this.exportExcelData = data
+			},
+			changeSize(pageSize) {
+				this.options.pageSize = pageSize
+				this.options.pageCurrent = 1
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			openTagsPopup() {
+				this.$refs.tagsPopup.open()
+			},
+			closeTagsPopup() {
+				this.$refs.tagsPopup.close()
+			},
+			getWhere() {
+				const query = this.query.trim()
+				if (!query) {
+					return ''
+				}
+				const queryRe = new RegExp(query, 'i')
+				return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
+			},
+			search() {
+				const newWhere = this.getWhere()
+				this.where = newWhere
+				// 下一帧拿到查询条件
+				this.$nextTick(() => {
+					this.loadData()
+				})
+			},
+			loadData(clear = true) {
+				this.$refs.udb.loadData({
+					clear
+				})
+			},
+			onPageChanged(e) {
+				this.selectedIndexs.length = 0
+				this.$refs.table.clearSelection()
+				this.$refs.udb.loadData({
+					current: e.current
+				})
+			},
+			navigateTo(url, clear) {
+				// clear 表示刷新列表时是否清除页码,true 表示刷新并回到列表第 1 页,默认为 true
+				uni.navigateTo({
+					url,
+					events: {
+						refreshData: () => {
+							this.loadTags()
+							this.loadData(clear)
+						}
+					}
+				})
+			},
+			// 多选处理
+			selectedItems() {
+				var dataList = this.$refs.udb.dataList
+				return this.selectedIndexs.map(i => dataList[i]._id)
+			},
+			// 批量删除
+			delTable() {
+				this.$refs.udb.remove(this.selectedItems(), {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			// 多选
+			selectionChange(e) {
+				this.selectedIndexs = e.detail.index
+			},
+			confirmDelete(id) {
+				this.$refs.udb.remove(id, {
+					success: (res) => {
+						this.$refs.table.clearSelection()
+					}
+				})
+			},
+			sortChange(e, name) {
+				this.orderByFieldName = name;
+				if (e.order) {
+					this.orderby = name + ' ' + orderByMapping[e.order]
+				} else {
+					this.orderby = ''
+				}
+				this.$refs.table.clearSelection()
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			filterChange(e, name) {
+				this._filter[name] = {
+					type: e.filterType,
+					value: e.filter
+				}
+				let newWhere = filterToWhere(this._filter, db.command)
+				if (Object.keys(newWhere).length) {
+					this.where = newWhere
+				} else {
+					this.where = ''
+				}
+				this.$nextTick(() => {
+					this.$refs.udb.loadData()
+				})
+			},
+			loadTags() {
+				db.collection('uni-id-tag').limit(500).get().then(res => {
+					res.result.data.map(item => {
+						this.tags[item.tagid] = item.name
+					})
+				}).catch(err => {
+					uni.showModal({
+						title: '提示',
+						content: err.message,
+						showCancel: false
+					})
+				})
+			},
+			managerMultiTag() {
+				const ids = this.selectedItems()
+
+        db.collection('uni-id-users').where({
+          _id: db.command.in(ids)
+        }).update({
+          tags: this.managerTags
+        }).then(() => {
+          uni.showToast({
+            title: '修改标签成功',
+            duration: 2000
+          })
+          this.$refs.table.clearSelection()
+          this.managerTags = []
+          this.loadData()
+          this.closeTagsPopup()
+        }).catch(err => {
+          uni.showModal({
+            content: err.message || '请求服务失败',
+            showCancel: false
+          })
+        }).finally(err => {
+          uni.hideLoading()
+        })
+
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.tags-manager {
+
+		&--x {
+			width: 400px;
+			padding: 40px 30px;
+			border-radius: 5px;
+			background-color: #fff;
+		}
+
+		&--header {
+			font-size: 22px;
+			color: #333;
+			text-align: center;
+		}
+	}
+
+	.mb {
+		margin-bottom: 80px;
+	}
+
+	.ml {
+		margin-left: 30px;
+	}
+</style>

+ 476 - 0
pages/uni-stat/channel/channel.vue

@@ -0,0 +1,476 @@
+<template>
+	<!-- 对应页面:渠道(app)  -->
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<view class="uni-sub-title hide-on-phone">
+					<uni-link href="https://ask.dcloud.net.cn/article/35974"
+						text="支持Android App多渠道统计。设置App渠道包的方法,请参考 https://ask.dcloud.net.cn/article/35974。"></uni-link>
+				</view>
+			</view>
+		</view>
+		<view class="uni-container">
+			<view class="uni-stat--x flex">
+				<uni-data-select collection="opendb-app-list" field="appid as value, name as text" orderby="text asc"
+					:defItem="1" label="应用选择" v-model="query.appid" :clear="false" />
+				<uni-data-select collection="opendb-app-versions" :storage="false" :where="versionQuery"
+					field="_id as value, version as text" orderby="text asc" label="版本选择" v-model="query.version_id" />
+				<uni-stat-tabs label="平台选择" type="boldLine" mode="platform-channel" :all="false"
+					v-model="query.platform_id" @change="changePlatform" />
+				<view class="flex">
+					<uni-stat-tabs label="日期选择" :current="currentDateTab" mode="date" @change="changeTimeRange" />
+					<uni-datetime-picker type="daterange" :end="new Date().getTime()" v-model="query.start_time"
+						returnType="timestamp" :clearIcon="false" class="uni-stat-datetime-picker"
+						:class="{'uni-stat__actived': currentDateTab < 0 && !!query.start_time.length}"
+						@change="useDatetimePicker" />
+				</view>
+			</view>
+			<view class="uni-stat--x" style="padding: 15px 0;">
+				<uni-stat-panel :items="panelData" class="uni-stat-panel" />
+				<uni-stat-tabs type="box" v-model="chartTab" :tabs="chartTabs" class="mb-l" @change="changeChartTab" />
+				<view class="uni-charts-box">
+					<qiun-data-charts type="area" :chartData="chartData" echartsH5 echartsApp
+						tooltipFormat="tooltipCustom" />
+				</view>
+			</view>
+
+			<view class="uni-stat--x p-m">
+				<view class="mb-m">
+					<uni-link color="" href="https://ask.dcloud.net.cn/article/35974" text="如何自定义渠道包?"></uni-link>
+				</view>
+				<uni-table :loading="loading" border stripe :emptyText="$t('common.empty')">
+					<uni-tr>
+						<template v-for="(mapper, index) in fieldsMap.slice(0, fieldsMap.length-1)">
+							<uni-th v-if="mapper.title" :key="index" align="center">
+								{{mapper.title}}
+							</uni-th>
+						</template>
+					</uni-tr>
+					<uni-tr v-for="(item ,i) in tableData" :key="i">
+						<template v-for="(mapper, index) in fieldsMap.slice(0, fieldsMap.length-1)">
+							<uni-td v-if="mapper.title && index === 1" :key="mapper.field" class="uni-stat-edit--x">
+								{{item[mapper.field] ? item[mapper.field] : '-'}}
+								<uni-icons type="compose" color="#2979ff" size="25" class="uni-stat-edit--btn"
+									@click="inputDialogToggle(item.channel_code, item.channel_name)" />
+							</uni-td>
+							<uni-td v-else="mapper.title" :key="mapper.field" align="center">
+								{{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
+							</uni-td>
+						</template>
+					</uni-tr>
+				</uni-table>
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon show-page-size :page-size="paginationOptions.pageSize"
+						:current="paginationOptions.pageCurrent" :total="paginationOptions.total"
+						@change="changePageCurrent" @pageSizeChange="changePageSize" />
+				</view>
+			</view>
+		</view>
+		<uni-popup ref="inputDialog" type="dialog" :maskClick="true">
+			<uni-popup-dialog ref="inputClose" mode="input" title="请编辑名称" v-model="updateValue" placeholder="请输入内容"
+				@confirm="editName"></uni-popup-dialog>
+		</uni-popup>
+
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import {
+		mapfields,
+		stringifyQuery,
+		stringifyField,
+		stringifyGroupField,
+		maxDeltaDay,
+		getTimeOfSomeDayAgo,
+		division,
+		format,
+		formatDate,
+		getFieldTotal,
+		debounce
+	} from '@/js_sdk/uni-stat/util.js'
+	import fieldsMap from './fieldsMap.js'
+	export default {
+		data() {
+			return {
+				// 字段映射表
+				fieldsMap,
+				// 查询参数
+				query: {
+					// 统计范围 day:按天统计,hour:按小时统计
+					dimension: "day",
+					// 应用id
+					appid: '',
+					// 平台
+					uni_platform: 'android',
+					// 平台id
+					platform_id: '',
+					// 版本号
+					version_id: '',
+					// 开始时间
+					start_time: [],
+				},
+				// 分页数据
+				paginationOptions: {
+					pageSize: 20,
+					pageCurrent: 1, // 当前页
+					total: 0, // 数据总量
+				},
+				// 加载状态
+				loading: false,
+				// 日期选择索引
+				currentDateTab: 1,
+				days: 0,
+				// 表格数据
+				tableData: [],
+				panelData: fieldsMap.filter(f => f.hasOwnProperty('value')),
+				chartData: {},
+				chartTab: 'new_device_count',
+				queryId: '',
+				updateValue: ''
+			}
+		},
+		computed: {
+			chartTabs() {
+				const tabs = []
+				fieldsMap.forEach(item => {
+					const {
+						field: _id,
+						title: name
+					} = item
+					const isTab = item.hasOwnProperty('value')
+					if (_id && name && isTab) {
+						tabs.push({
+							_id,
+							name
+						})
+					}
+				})
+				return tabs
+			},
+			queryStr() {
+				return stringifyQuery(this.query, true)
+			},
+			dimension() {
+				if (maxDeltaDay(this.query.start_time, 1)) {
+					return 'hour'
+				} else {
+					return 'day'
+				}
+			},
+			versionQuery() {
+				const {
+					appid,
+					uni_platform
+				} = this.query
+				const query = stringifyQuery({
+					appid,
+					uni_platform,
+					type: 'native_app'
+				})
+				return query
+			}
+		},
+		created() {
+			this.debounceGet = debounce(() => this.getAllData())
+		},
+		watch: {
+			query: {
+				deep: true,
+				handler(val) {
+					this.paginationOptions.pageCurrent = 1 // 重置分页
+					this.debounceGet()
+				}
+			}
+		},
+		methods: {
+			useDatetimePicker() {
+				this.currentDateTab = -1
+			},
+			changePlatform(id, index, name, item) {
+				this.query.version_id = 0
+				console.log(item.code);
+				this.query.uni_platform = item.code
+			},
+			changeTimeRange(id, index) {
+				this.currentDateTab = index
+				const day = 24 * 60 * 60 * 1000
+				let start, end
+				start = getTimeOfSomeDayAgo(id)
+				if (!id) {
+					end = getTimeOfSomeDayAgo(0) + day - 1
+				} else {
+					end = getTimeOfSomeDayAgo(0) - 1
+				}
+				this.query.start_time = [start, end]
+			},
+
+			changePageCurrent(e) {
+				this.paginationOptions.pageCurrent = e.current
+				this.getTableData()
+			},
+
+			changePageSize(pageSize) {
+				this.paginationOptions.pageSize = pageSize
+				this.paginationOptions.pageCurrent = 1 // 重置分页
+				this.getTableData()
+			},
+
+			changeChartTab(id, index, name) {
+				this.getChartData(id, name)
+			},
+
+			getAllData(query) {
+				this.getPanelData()
+				this.getChartData()
+				this.getTableData()
+			},
+
+			getChartData(field = this.chartTab) {
+				// this.chartData = {}
+				let querystr = stringifyQuery(this.query, false, ['uni_platform'])
+				const {
+					pageCurrent
+				} = this.paginationOptions
+				const db = uniCloud.database()
+				db.collection('uni-stat-result')
+					.where(querystr)
+					.field(`${stringifyField(fieldsMap, field)}, start_time, channel_id`)
+					.groupBy('channel_id,start_time')
+					.groupField(stringifyGroupField(fieldsMap, field))
+					.orderBy('start_time', 'asc')
+					.get({
+						getCount: true
+					})
+					.then(res => {
+						const {
+							count,
+							data
+						} = res.result
+						const options = {
+							categories: [],
+							series: [{
+								name: '暂无数据',
+								data: []
+							}]
+						}
+						const xAxis = options.categories
+						if (this.dimension === 'hour') {
+							for (let i = 0; i < 24; ++i) {
+								const hour = i < 10 ? '0' + i : i
+								const x = `${hour}:00 ~ ${hour}:59`
+								xAxis.push(x)
+							}
+						}
+						const hasChannels = []
+						console.log('data----', data);
+						data.forEach(item => {
+							if (hasChannels.indexOf(item.channel_id) < 0) {
+								hasChannels.push(item.channel_id)
+							}
+						})
+						// 请求所有渠道数据,与 hasChannels 匹配得出 channel_name
+						let allChannels = []
+						this.getChannels().then(res => {
+							allChannels = res.result.data
+						}).finally(() => {
+							hasChannels.forEach((channel, index) => {
+								// TODO 需要做个排序,暂时排序还是有问题的
+								// allChannels = allChannels.sort((a,b)=>{ return a.channel_code.localeCompare(b.channel_code)})
+								const c = allChannels.find(item => item._id === channel)
+								const line = options.series[index] = {
+									name: c && c.channel_name || '未知',
+									data: []
+								}
+								if (this.dimension === 'hour') {
+									for (let i = 0; i < 24; ++i) {
+										line.data[i] = 0
+									}
+								}
+								let mapper = fieldsMap.filter(f => f.field === field)
+								mapper = JSON.parse(JSON.stringify(mapper))
+								delete mapper[0].value
+								mapper[0].formatter = ''
+								for (const item of data) {
+									// 将 item 根据 mapper 计算、格式化
+									mapfields(mapper, item, item)
+									let date = item.start_time
+									const x = formatDate(date, this.dimension)
+									let y = item[field]
+									const dateIndex = xAxis.indexOf(x)
+									if (channel === item.channel_id) {
+										if (dateIndex < 0) {
+											xAxis.push(x)
+											line.data.push(y)
+										} else {
+											line.data[dateIndex] = y
+										}
+									}
+
+								}
+							})
+
+							console.log(options);
+							options.series = options.series.sort((a, b) => {
+								return a.name.localeCompare(b.name)
+							})
+							this.chartData = options
+						})
+					}).catch((err) => {
+						console.error(err)
+						// err.message 错误信息
+						// err.code 错误码
+					}).finally(() => {})
+			},
+
+			getChannels() {
+				const db = uniCloud.database()
+				console.log(this.query);
+				return db.collection('uni-stat-app-channels').where(stringifyQuery({
+					appid: this.query.appid,
+					platform_id: this.query.platform_id
+				})).get()
+			},
+
+			getTableData() {
+				const query = stringifyQuery(this.query, false, ['uni_platform'])
+				const {
+					pageCurrent
+				} = this.paginationOptions
+				this.loading = true
+				const db = uniCloud.database()
+				db.collection('uni-stat-result')
+					.where(query)
+					.field(`${stringifyField(fieldsMap)},appid, channel_id`)
+					.groupBy('appid, channel_id')
+					.groupField(stringifyGroupField(fieldsMap))
+					.orderBy('new_device_count', 'desc')
+					.skip((pageCurrent - 1) * this.paginationOptions.pageSize)
+					.limit(this.paginationOptions.pageSize)
+					.get({
+						getCount: true
+					})
+					.then(res => {
+						const {
+							count,
+							data
+						} = res.result
+
+						this.getChannels().then(res => {
+							const channels = res.result.data
+							for (const item of data) {
+								channels.forEach(channel => {
+									if (item.channel_id === channel._id) {
+										item.channel_code = channel.channel_code
+										item.channel_name = channel.channel_name
+									}
+								})
+							}
+						}).finally(() => {
+							for (const item of data) {
+								mapfields(fieldsMap, item, item, 'total_')
+							}
+							this.tableData = []
+							this.paginationOptions.total = count
+							this.tableData = data
+							this.loading = false
+						})
+
+					}).catch((err) => {
+						console.error(err)
+						// err.message 错误信息
+						// err.code 错误码
+						this.loading = false
+					})
+			},
+
+			createStr(maps, fn, prefix = 'total_') {
+				const strArr = []
+				maps.forEach(mapper => {
+					if (field.hasOwnProperty('value')) {
+						const fieldName = mapper.field
+						strArr.push(`${fn}(${fieldName}) as ${prefix + fieldName}`)
+					}
+				})
+				return strArr.join()
+			},
+
+			getPanelData() {
+				let query = JSON.parse(JSON.stringify(this.query))
+				query.dimension = 'day'
+				// let query = stringifyQuery(cloneQuery)
+				let querystr = stringifyQuery(query, false, ['uni_platform'])
+				console.log('channel --:', querystr);
+				const db = uniCloud.database()
+				const subTable = db.collection('uni-stat-result')
+					.where(querystr)
+					.field(stringifyField(fieldsMap))
+					.groupBy('appid')
+					.groupField(stringifyGroupField(fieldsMap))
+					.orderBy('start_time', 'desc')
+					.get()
+					.then(res => {
+						const item = res.result.data[0]
+						item && (item.total_devices = 0)
+						getFieldTotal.call(this, query)
+						this.panelData = []
+						this.panelData = mapfields(fieldsMap, item)
+					})
+			},
+
+			inputDialogToggle(queryId, updateValue) {
+				this.queryId = queryId
+				this.updateValue = updateValue
+				this.$refs.inputDialog.open()
+			},
+
+			editName(value) {
+				// 使用 clientDB 提交数据
+				const db = uniCloud.database()
+				db.collection('uni-stat-app-channels')
+					.where({
+						channel_code: this.queryId
+					})
+					.update({
+						channel_name: value
+					})
+					.then((res) => {
+						uni.showToast({
+							title: '修改成功'
+						})
+						this.getTableData()
+					}).catch((err) => {
+						uni.showModal({
+							content: err.message || '请求服务失败',
+							showCancel: false
+						})
+					}).finally(() => {
+						uni.hideLoading()
+					})
+			}
+
+		}
+
+	}
+</script>
+
+<style>
+	.uni-stat-panel {
+		box-shadow: unset;
+		border-bottom: 1px solid #eee;
+		padding: 0;
+		margin: 0 15px;
+	}
+
+	.uni-stat-edit--x {
+		display: flex;
+		justify-content: space-between;
+	}
+
+	.uni-stat-edit--btn {
+		cursor: pointer;
+	}
+</style>

+ 74 - 0
pages/uni-stat/channel/fieldsMap.js

@@ -0,0 +1,74 @@
+/**
+ * 页面上的数据都来自数据库,且多处 ui 消费,页面直接使用字段会造成耦合和无谓的重复,固在此抽出来统一配置和处理(计算、格式化等)
+ * title 显示所使用名称
+ * field 字段名
+ * computed 计算表达式配置(需要 mapfield 函数支持)
+ * tooltip 对字段解释的提示文字
+ * formatter 数字格式化的配置,省缺为 ','
+  	* '' 空字符串 则表示不格式化
+	* ',' 数字格式,例:1000 格式为 1,000
+	* '%' 百分比格式 例:0.1 格式为 10%
+	* ':' 时分秒格式 例:90 格式为 00:01:30
+	* '-' 日期格式 例:1655196831390(值需为时间戳) 格式为 2022-06-14
+ * fix 数字保留几位小数,>1 默认不保留小数,<1 默认保留两位小数
+ * value 默认值 (仅用于 uni-stat-panel 组件) todo: 可移除
+ * contrast 对比值 (仅用于 uni-stat-panel 组件) todo: 可移除
+ */
+
+export default [{
+	title: '渠道值',
+	field: 'channel_code',
+	tooltip: '',
+	formatter: '',
+}, {
+	title: '渠道名称',
+	field: 'channel_name',
+	tooltip: '',
+	formatter: '',
+}, {
+	title: '新增设备',
+	field: 'new_device_count',
+	tooltip: '首次访问应用的设备数(以设备为判断标准,去重)',
+	value: 0
+}, {
+	title: '活跃设备',
+	field: 'active_device_count',
+	tooltip: '访问过应用内任意页面的总设备数(去重)',
+	value: 0
+}, {
+	title: '访问次数',
+	field: 'page_visit_count',
+	tooltip: '访问过应用内任意页面总次数,多个页面之间跳转、同一页面的重复访问计为多次访问',
+	value: 0
+}, {
+	title: '启动次数',
+	field: 'app_launch_count',
+	tooltip: '设备从打开应用到主动关闭应用或超时退出计为一次启动',
+	value: 0
+}, {
+	title: '次均停留时长',
+	field: 'avg_device_session_time',
+	formatter: ':',
+	tooltip: '平均每次打开应用停留在应用内的总时长,即应用停留总时长/启动次数',
+	value: 0
+}, {
+	title: '设备平均停留时长 ',
+	field: 'avg_device_time',
+	formatter: ':',
+	tooltip: '平均每个设备停留在应用内的总时长,即应用停留总时长/活跃设备',
+	value: 0
+}, {
+	title: '跳出率',
+	field: 'bounceRate',
+	computed: 'bounce_times/app_launch_count',
+	formatter: '%',
+	tooltip: '只浏览一个页面便离开应用的次数占总启动次数的百分比',
+	value: 0,
+	contrast: 0,
+	fix: 2
+}, {
+	title: '总设备数',
+	field: 'total_devices',
+	tooltip: '从添加统计到当前选择时间的总设备数(去重)',
+	value: 0,
+}]

+ 437 - 0
pages/uni-stat/device/activity/activity.vue

@@ -0,0 +1,437 @@
+<template>
+	<!-- 对应页面:设备统计-活跃度  -->
+	<view class="fix-top-window">
+		<view class="uni-header">
+			<uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
+			<view class="uni-group">
+				<view class="uni-sub-title hide-on-phone">用户活跃度分析</view>
+			</view>
+		</view>
+		<view class="uni-container">
+			<view class="uni-stat--x flex">
+				<uni-data-select collection="opendb-app-list" field="appid as value, name as text" orderby="text asc"
+					:defItem="1" label="应用选择" @change="changeAppid" v-model="query.appid" :clear="false" />
+				<uni-data-select collection="opendb-app-versions" :where="versionQuery"
+					field="_id as value, version as text" orderby="text asc" label="版本选择" v-model="query.version_id" />
+				<view class="flex">
+					<uni-stat-tabs label="日期选择" :current="currentDateTab" mode="date" :yesterday="false"
+						@change="changeTimeRange" />
+					<uni-datetime-picker type="daterange" :end="new Date().getTime()" v-model="query.start_time"
+						returnType="timestamp" :clearIcon="false" class="uni-stat-datetime-picker"
+						:class="{'uni-stat__actived': currentDateTab < 0 && !!query.start_time.length}"
+						@change="useDatetimePicker" />
+				</view>
+			</view>
+			<view class="uni-stat--x">
+				<uni-stat-tabs label="平台选择" type="boldLine" mode="platform" v-model="query.platform_id"
+					@change="changePlatform" />
+				<uni-data-select v-if="query.platform_id && query.platform_id.indexOf('==') === -1"
+					:localdata="channelData" label="渠道/场景值选择" v-model="query.channel_id"></uni-data-select>
+			</view>
+			<view class="uni-stat--x p-m">
+				<view class="label-text mb-l">
+					趋势图
+				</view>
+				<uni-stat-tabs type="box" :tabs="chartTabs" class="mb-l" @change="changeChartTab" />
+				<view class="uni-charts-box">
+					<qiun-data-charts type="area" :chartData="chartData" echartsH5 echartsApp />
+				</view>
+			</view>
+			<view class="uni-stat--x p-m">
+				<uni-stat-table :data="tableData" :filedsMap="fieldsMap" :loading="loading" tooltip />
+				<view class="uni-pagination-box">
+					<uni-pagination show-icon show-page-size :page-size="options.pageSize"
+						:current="options.pageCurrent" :total="options.total" @change="changePageCurrent"
+						@pageSizeChange="changePageSize" />
+				</view>
+			</view>
+		</view>
+
+		<!-- #ifndef H5 -->
+		<fix-window />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import {
+		mapfields,
+		stringifyQuery,
+		stringifyField,
+		stringifyGroupField,
+		getTimeOfSomeDayAgo,
+		division,
+		format,
+		formatDate,
+		maxDeltaDay,
+		debounce,
+	} from '@/js_sdk/uni-stat/util.js'
+	import fieldsMap from './fieldsMap.js'
+	export default {
+		data() {
+			return {
+				tableName: 'uni-stat-result',
+				fieldsMap,
+				query: {
+					dimension: "day",
+					appid: '',
+					platform_id: '',
+					uni_platform: '',
+					version_id: '',
+					channel_id: '',
+					start_time: [],
+				},
+				options: {
+					pageSize: 20,
+					pageCurrent: 1, // 当前页
+					total: 0, // 数据总量
+				},
+				loading: false,
+				currentDateTab: 0,
+				currentChartTab: 'day',
+				tableData: [],
+				chartData: {},
+				channelData: [],
+				tabName: '日活'
+			}
+		},
+		computed: {
+			chartTabs() {
+				const tabs = [{
+					_id: 'day',
+					name: '日活'
+				}, {
+					_id: 'week',
+					name: '周活'
+				}, {
+					_id: 'month',
+					name: '月活'
+				}]
+				if (maxDeltaDay(this.query.start_time, 7)) {
+					tabs.forEach((tab, index) => {
+						if (tab._id === 'month') {
+							tab.disabled = true
+						} else {
+							tab.disabled = false
+						}
+					})
+				}
+				return tabs
+			},
+			channelQuery() {
+				const platform_id = this.query.platform_id
+				return stringifyQuery({
+					platform_id
+				})
+			},
+			versionQuery() {
+				const {
+					appid,
+					uni_platform
+				} = this.query
+				const query = stringifyQuery({
+					appid,
+					uni_platform,
+				})
+				return query
+			}
+		},
+		created() {
+			this.debounceGet = debounce(() => this.getAllData(this.query))
+			this.getChannelData()
+		},
+		watch: {
+			query: {
+				deep: true,
+				handler(val) {
+					this.options.pageCurrent = 1 // 重置分页
+					this.debounceGet()
+				}
+			}
+		},
+		methods: {
+			useDatetimePicker() {
+				this.currentDateTab = -1
+			},
+			changeAppid(id) {
+				this.getChannelData(id, false)
+			},
+			changePlatform(id, index, name, item) {
+				this.getChannelData(null, id)
+				this.query.version_id = 0
+				this.query.uni_platform = item.code
+				console.log('this.query.uni_platform = item.code', item.code);
+			},
+			changeTimeRange(id, index) {
+				this.currentDateTab = index
+				const day = 24 * 60 * 60 * 1000
+				let start, end
+				start = getTimeOfSomeDayAgo(id)
+				if (!id) {
+					end = getTimeOfSomeDayAgo(0) + day - 1
+				} else {
+					end = getTimeOfSomeDayAgo(0) - 1
+				}
+				this.query.start_time = [start, end]
+			},
+			changePageCurrent(e) {
+				this.options.pageCurrent = e.current
+				this.getTabelData(this.query)
+			},
+
+			changePageSize(pageSize) {
+				this.options.pageSize = pageSize
+				this.options.pageCurrent = 1 // 重置分页
+				this.getTabelData(this.query)
+			},
+
+			changeChartTab(type, index, name) {
+				this.currentChartTab = type
+				this.tabName = name
+				this.getChartData(this.query, type, name)
+			},
+
+			getAllData(query) {
+				this.getChartData(query, this.currentChartTab, this.tabName)
+				this.getTabelData(query)
+			},
+
+			getChartData(query, type, name = '日活', field = 'active_device_count') {
+				this.chartData = {}
+				const options = {
+					categories: [],
+					series: [{
+						name,
+						data: []
+					}]
+				}
+				query = stringifyQuery(query, false, ['uni_platform'])
+				const db = uniCloud.database()
+				if (type === 'day') {
+					db.collection(this.tableName)
+						.where(query)
+						.field(`${stringifyField(fieldsMap, field)}, start_time`)
+						.groupBy('start_time')
+						.groupField(stringifyGroupField(fieldsMap, field))
+						.orderBy('start_time', 'asc')
+						.get({
+							getCount: true
+						})
+						.then(res => {
+							const {
+								count,
+								data
+							} = res.result
+							this.chartData = []
+							for (const item of data) {
+								const x = formatDate(item.start_time, 'day')
+								const y = item[field]
+								options.series[0].data.push(y)
+								options.categories.push(x)
+							}
+							this.chartData = options
+						}).catch((err) => {
+							console.error(err)
+						})
+				} else {
+					// 周、月范围的处理
+					this.getRangeCountData(query, type).then(res => {
+						const oldType = type
+						if (type === 'week') type = 'isoWeek'
+						const {
+							count,
+							data
+						} = res.result
+						this.chartData = []
+						const wunWeekTime = 7 * 24 * 60 * 60 * 1000
+						for (const item of data) {
+							const date = +new Date(item.year, 0) + (Number(item[type]) * wunWeekTime - 1)
+							const x = formatDate(date, oldType)
+							const y = item[type + '_' + field]
+							if (y) {
+								options.series[0].data.push(y)
+								options.categories.push(x)
+							}
+						}
+						this.chartData = options
+					})
+				}
+			},
+
+			getTabelData(query, field = 'active_device_count') {
+				const {
+					pageCurrent
+				} = this.options
+				query = stringifyQuery(query)
+				this.loading = true
+				const db = uniCloud.database()
+				db.collection(this.tableName)
+					.where(query)
+					.field(`${stringifyField(fieldsMap, field)}, start_time`)
+					.groupBy('start_time')
+					.groupField(stringifyGroupField(fieldsMap, field))
+					.orderBy('start_time', 'desc')
+					.skip((pageCurrent - 1) * this.options.pageSize)
+					.limit(this.options.pageSize)
+					.get({
+						getCount: true
+					})
+					.then(res => {
+						const {
+							count,
+							data
+						} = res.result
+						let daysData = data,
+							daysCount = count,
+							weeks = [],
+							months = []
+						// 获取周活、月活
+						this.getRangeCountData(query, 'week').then(res => {
+							const {
+								count,
+								data
+							} = res.result
+							weeks = data
+
+							this.getRangeCountData(query, 'month').then(res => {
+								const {
+									count,
+									data
+								} = res.result
+								months = data
+
+								const allData = this.mapWithWeekAndMonth(daysData, weeks, months)
+
+								for (const item of allData) {
+									mapfields(fieldsMap, item, item)
+								}
+								this.tableData = []
+								this.options.total = daysCount
+								this.tableData = allData
+							}).finally(() => {
+								this.loading = false
+							})
+						})
+					}).catch((err) => {
+						console.error(err)
+						// err.message 错误信息
+						// err.code 错误码
+					})
+			},
+
+			getRangeCountData(query, type, field = 'active_device_count') {
+				if (type === 'week') type = 'isoWeek'
+				const {
+					pageCurrent
+				} = this.options
+				const db = uniCloud.database()
+				return db.collection(this.tableName)
+					.where(query)
+					.field(
+						`${field}, start_time, ${type}(add(new Date(0),start_time), "Asia/Shanghai") as ${type},year(add(new Date(0),start_time), "Asia/Shanghai") as year`
+					)
+					.groupBy(`year, ${type}`)
+					.groupField(`sum(${field}) as ${type}_${field}`)
+					.orderBy(`year asc, ${type} asc`)
+					.get({
+						getCount: true
+					})
+			},
+
+			// 匹配数据日期所在的周活、月活
+			mapWithWeekAndMonth(data, weeks, months, field = 'active_device_count') {
+				for (const item of data) {
+					const date = new Date(item.start_time)
+					const year = date.getUTCFullYear()
+					const month = date.getMonth() + 1
+					const week = this.getWeekNumber(date)
+					for (const w of weeks) {
+						if (w.isoWeek === week && w.year === year) {
+							item[`week_${field}`] = w[`isoWeek_${field}`]
+						}
+					}
+					for (const m of months) {
+						if (m.month === month && m.year === year) {
+							item[`month_${field}`] = m[`month_${field}`]
+						}
+					}
+				}
+				return data
+			},
+
+			//日期所在的周(一年中的第几周)
+			getWeekNumber(d) {
+				// Copy date so don't modify original
+				d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
+				// Set to nearest Thursday: current date + 4 - current day number
+				// Make Sunday's day number 7
+				d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
+				// Get first day of year
+				var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+				// Calculate full weeks to nearest Thursday
+				var weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
+				// Return array of year and week number
+				return weekNo;
+			},
+			//获取渠道信息
+			getChannelData(appid, platform_id) {
+				this.query.channel_id = ''
+				const db = uniCloud.database()
+				const condition = {}
+				//对应应用
+				appid = appid ? appid : this.query.appid
+				if (appid) {
+					condition.appid = appid
+				}
+				//对应平台
+				platform_id = platform_id ? platform_id : this.query.platform_id
+				if (platform_id) {
+					condition.platform_id = platform_id
+				}
+
+				let platformTemp = db.collection('uni-stat-app-platforms')
+					.field('_id, name')
+					.getTemp()
+
+				let channelTemp = db.collection('uni-stat-app-channels')
+					.where(condition)
+					.field('_id, channel_name, create_time, platform_id')
+					.getTemp()
+
+				db.collection(channelTemp, platformTemp)
+					.orderBy('platform_id', 'asc')
+					.get()
+					.then(res => {
+						let data = res.result.data
+						let channels = []
+						if (data.length > 0) {
+							let channelName
+							for (let i in data) {
+								channelName = data[i].channel_name ? data[i].channel_name : '默认'
+								if (data[i].platform_id.length > 0) {
+									channelName = data[i].platform_id[0].name + '-' + channelName
+								}
+								channels.push({
+									value: data[i]._id,
+									text: channelName
+								})
+							}
+						}
+						this.channelData = channels
+					})
+					.catch((err) => {
+						console.error(err)
+						// err.message 错误信息
+						// err.code 错误码
+					}).finally(() => {})
+
+			}
+
+		}
+
+	}
+</script>
+
+<style>
+
+</style>

+ 48 - 0
pages/uni-stat/device/activity/fieldsMap.js

@@ -0,0 +1,48 @@
+/**
+ * 页面上的数据都来自数据库,且多处 ui 消费,页面直接使用字段会造成耦合和无谓的重复,固在此抽出来统一配置和处理(计算、格式化等)
+ * title 显示所使用名称
+ * field 字段名
+ * computed 计算表达式配置(需要 mapfield 函数支持)
+ * tooltip 对字段解释的提示文字
+ * formatter 数字格式化的配置,省缺为 ','
+  	* '' 空字符串 则表示不格式化
+	* ',' 数字格式,例:1000 格式为 1,000
+	* '%' 百分比格式 例:0.1 格式为 10%
+	* ':' 时分秒格式 例:90 格式为 00:01:30
+	* '-' 日期格式 例:1655196831390(值需为时间戳) 格式为 2022-06-14
+ * fix 数字保留几位小数,>1 默认不保留小数,<1 默认保留两位小数
+ * value 默认值 (仅用于 uni-stat-panel 组件) todo: 可移除
+ * contrast 对比值 (仅用于 uni-stat-panel 组件) todo: 可移除
+ */
+
+
+export default [{
+	title: '日期',
+	field: 'start_time',
+	tooltip: '',
+	formatter: '-',
+}, {
+	title: '日活',
+	field: 'active_device_count',
+	tooltip: '选中日期当天的访问用户数',
+}, {
+	title: '周活',
+	field: 'week_active_device_count',
+	tooltip: '选中日期所在自然周(包括选中日期在内)的访问用户数',
+}, {
+	title: '日活/周活',
+	field: 'active_device_count/week_active_device_count',
+	computed: 'active_device_count/week_active_device_count',
+	tooltip: '选中日期的访问用户数占周访问用户数的百分比',
+	formatter: '%',
+}, {
+	title: '月活',
+	field: 'month_active_device_count',
+	tooltip: '选中日期所在自然月(包括选中日期在内)的访问用户数',
+}, {
+	title: '日活/月活',
+	field: 'active_device_count/month_active_device_count',
+	computed: 'active_device_count/month_active_device_count',
+	tooltip: '选中日期的访问用户数占月访问用户数的百分比',
+	formatter: '%',
+}]

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio