ngtools 管理端

This commit is contained in:
ldeyun 2024-09-13 16:39:31 +08:00
commit 08a0a7c920
1155 changed files with 130122 additions and 0 deletions

15
.hbuilderx/launch.json Normal file
View File

@ -0,0 +1,15 @@
{
"version" : "1.0",
"configurations" : [
{
"default" : {
"launchtype" : "local"
},
"h5" : {
"launchtype" : "remote"
},
"provider" : "aliyun",
"type" : "uniCloud"
}
]
}

92
App.vue Normal file
View File

@ -0,0 +1,92 @@
<script>
import {
mapActions,
mapMutations
} from 'vuex'
import config from '@/admin.config.js'
import {
version
} from './package.json'
import { uniAdminCacheKey } from './store/constants.js'
import uploadFileForExtStorage from "@/js_sdk/ext-storage/uploadFileForExtStorage.js"
export default {
created() {
this.clear = undefined
},
methods: {
...mapMutations('app', ['SET_THEME']),
...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
// #ifdef H5
// 访访
// let uni_id_token_expired = uni.getStorageSync("uni_id_token_expired");
// if (!uni_id_token_expired || uni_id_token_expired < Date.now()) {
// uni.reLaunch({
// url: config.login.url
// })
// }
// #endif
// 线使
// console.log('%c uni-appuni-app & uniCloud hr2013@dcloud.io', 'color: red');
console.log('App Launch')
if (this.$uniIdPagesStore.store.hasLogin) {
this.init()
}
//
uni.$on('uni-id-pages-login-success', () => {
this.init()
})
// theme
this.SET_THEME(uni.getStorageSync(uniAdminCacheKey.theme) || 'default')
// uniCloud.uploadFile
uploadFileForExtStorage.init({
provider: "unicloud", // provider "unicloud" ; "extStorage" ;
domain: "cdn.example.com", //
fileID2fileURL: true, // fileIDfileURL便
//
uploadFileOptions: async (event) => {
// ext-storage-co https://doc.dcloud.net.cn/uniCloud/ext-storage/dev.html#getuploadfileoptions
const uniCloudStorageExtCo = uniCloud.importObject("ext-storage-co");
return await uniCloudStorageExtCo.getUploadFileOptions(event);
}
});
},
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
LICENSE Normal file
View File

@ -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
README.md Normal file
View File

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

85
admin.config.js Normal file
View File

@ -0,0 +1,85 @@
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'
}],
themes: [{
text: '默认',
value: 'default'
}, {
text: '绿柔',
value: 'green'
}],
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: {
}
}

300
changelog.md Normal file
View File

@ -0,0 +1,300 @@
## 2.4.142024-06-03
- 修复 uni统计-趋势分析-次均停留和设备平均停留时长异常问题
- 修复 左侧父级菜单右侧箭头不显示的问题
## 2.4.132024-05-24
- 修复 2.4.9 更新引出的设备统计-概况默认不是按天查询等问题
## 2.4.122024-05-22
- 优化 非debug模式下去除支付统计的日志打印
## 2.4.112024-05-21
- 修复 uni统计支付宝云批量添加可能会报错的问题
## 2.4.102024-05-20
- 更新 uni-id-pages 至 1.1.20
- 更新 uni_modules 依赖
## 2.4.92024-05-16
优化 uni统计维度选择支持按时查询
## 2.4.82024-05-10
- 优化 uni统计维度选择支持按时查询调整应用选择的选择器宽度样式
- 更新 uni统计表的索引对齐opendb仓库内表的索引
## 2.4.72024-04-23
- 优化 uni统计未选择appid时不进行查询
## 2.4.62024-04-23
- 还原 uni-tooltip 组件的更新新版uni-tooltip组件有问题
## 2.4.52024-04-19
- 更新 uni_modules依赖
- 新增 数据库索引初始化文件
- 优化 抖音小程序兼容性
## 2.4.42024-04-10
- 更新 菜单表初始化数据指定_id(防止重复初始化时数据重复)
- 更新 update: 优化vue3模块下菜单管理无法显示待添加的插件菜单的问题
- 更新 应用管理支持填写 iOS ABM 包登录获取链接,发布页支持 iOS ABM 包获取,[hello-uniapp x](https://hellouniappx.dcloud.net.cn/)
## 2.4.32024-01-25
- 优化 修改数据库初始化提示语使用新版初始化方案
- 优化 uni统计-内容统计-页面规则 支持根据应用筛选
- 优化 代码通过 SonarLint 规范检测
## 2.4.22024-01-15
- 优化 APP升级中心支持上传到扩展存储 [简介](https://doc.dcloud.net.cn/uniCloud/ext-storage/intro.html)
## 2.4.12023-12-27
- 调整 db_init.json 新增内容统计相关表 [详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#upgrade2)
## 2.4.02023-12-27
- 重要 uni统计-内容统计 [详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#upgrade2)
## 2.3.132023-12-15
- 修复 opendb-news-comments.schema.json permission 配置少一个s的问题
## 2.3.122023-11-07
- 修复 uni统计兼容 skd 未上报 ut 参数的Bug
- 优化 uni统计配置增加cronMinTips用于对cronMin参数进行解释
- 更新 uni-captcha模块至0.7.0
- 更新 升级中心升级至 0.6.1
- 新增 升级中心 uni-upgrade-center 云函数 checkVersion.js 支持传递 is_uniapp_x 参数uni-app x 项目安卓端无 wgt 更新)
## 2.3.112023-08-07
- 优化 当菜单表无数据时,提示请先初始化云数据库
## 2.3.102023-08-04
- 调整 db_init.json新增opendb-poi表
## 2.3.92023-08-04
- 优化 打开选择地图时确认按钮被top-window窗口覆盖的问题
## 2.3.82023-08-03
- 修复 uni统计页面统计修改页面名称不生效的问题
- 调整 uni统计支付订单明细查询改用全等匹配
- 调整 opendb-news-articles.schema.json read默认权限
- 优化 合并uni_modules插件的菜单时菜单默认启用不再需要一个一个点修改了
## 2.3.72023-05-29
- 升级 uni-id-pages 至 1.1.14
- 修复 uni统计自定义事件的查询bug
- 优化 uni统计日期选择支持时分秒
- 优化 uni统计优化自定义事件查询支持事件ID和设备标识查询
- 优化 去除注册admin时的验证码组件admin只能注册一次无需验证码
## 2.3.62023-04-10
- 优化 支付统计-价值用户排行:只统计已支付的订单金额,且去除退款金额。
## 2.3.52023-02-24
- 修复 升级中心安卓应用商店不显示的Bug
## 2.3.42023-02-09
- 重要 阿里云空间支持上传sourceMap用以分析js错误统计 [详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#sourcemap-parse-error)
## 2.3.32023-02-02
- 新增 菜单管理新增【更新内置菜单】功能方便旧版本uni-admin升级至新版本uni-admin后一键同步内置菜单
- 升级 uni-id-pages 至 1.1.0
- 优化 uni-admin的storage键名命名规范 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-storage-format.html)
- 优化 安全审计-用户日志 排序规则调整为降序
- 优化 uni统计-版本选择组件的查询条件短期内多次变更只查询最后一次变更后的结果
## 2.3.22023-01-30
- 修复 禁用的菜单仍然会在左侧菜单列表中显示的bug
## 2.3.12023-01-29
- 短信群发功能 新增 筛选用户后可以跨分页群发
## 2.3.02023-01-16
- 重要 新增uni-starter需要的相关依赖和初始化数据方便uni-starter关联uni-admin后可直接运行
- 升级 uni-id-pages 至 1.0.40
- 修复 非H5环境时点击跳首页会报错的问题。
- 修复 charts 更新后vue3模式下无法显示的bug
- 修复 用户管理-编辑时,新增标签后返回报错的问题
- 修复 用户管理-编辑时若用户拥有的应用未添加到应用管理时点击保存会导致用户丢失该应用的appid进而导致下次登录提示未在该应用注册的问题。
- 修复 用户管理-编辑时,无法将已禁用的用户恢复成正常状态的问题。
- 修复 用户管理-编辑时,无法将手机号和邮箱清空的问题。
- 优化 用户管理-编辑时禁止将当前登录的admin账户禁用防止误操作导致无法登录admin
- 优化 统计报表中的版本选择组件显示的内容,以便更好的区分平台和版本号
- 优化 新增用户时的表单验证提示
- 优化 当没有创建任何应用时,首页会友好提示请先创建应用。
## 2.2.32022-12-30
- 修复 uni统计js报错页面无法正常显示数据的问题 [详情](https://ask.dcloud.net.cn/question/160337)
- 修复 一键部署因database目录有多余的db_init.json 导致部署失败的问题。
- 优化 uni统计前端页面减少不必要的请求次数。
## 2.2.22022-12-20
- 修复 升级中心删除安装包时报错的Bug [详情](https://ask.dcloud.net.cn/question/159918)
## 2.2.12022-12-13
- 修复 因HBX升级3.6.13导致菜单管理加载失败的问题
- 优化 微信小程序报很多警告的问题
## 2.2.02022-12-12
- 新增 uni统计新增支付统计 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-pay.html#pay-stat)
- 优化 uni统计UI排版细节
- 修复 国际化繁体中文 新增一級菜單 文案错误问题
## 2.1.92022-12-06
- 升级 uni-id-pages 至 1.0.35
- 优化 去除非必要的日志打印
- 优化 用户管理、角色管理、日志管理使用getTemp连表提升查询性能
- 优化 添加用户时手机号、邮箱选填
- 优化 添加用户时,昵称允许是中文
- 修复 运行时提示表不存在的问题
## 2.1.82022-12-01
- 修复 uni-stat-receiver 无法找到 uni-id 模块的bug
## 2.1.72022-11-30
- 新增 换肤功能
## 2.1.62022-11-28
- 优化 群发短信功能的 schema 命名规范
## 2.1.52022-11-17
- 升级 uni-id-pages 至 1.0.31
- 优化 添加用户时手机号、邮箱必填
## 2.1.42022-11-11
- 修复 Vue3微信小程序运行报错的bug
## 2.1.32022-11-03
- 修复 微信小程序上运行时错误 `process is not defined`
## 2.1.22022-11-02
- 修复 Vue3无法导入插件菜单
## 2.1.12022-10-17
- 修复 uni统计 App-Android 平台部分统计数据不准确的Bug [详情](https://ask.dcloud.net.cn/article/40097)
- 修复 uni统计 周/月数据不准确的Bug
## 2.1.02022-10-14
- 新增 群发短信功能 [详情](https://uniapp.dcloud.net.cn//uniCloud/admin.html#batch-sms)
- 修复 无法重置用户密码的bug
## 2.0.52022-09-28
- 修复 导入插件时不显示“待添加菜单”bug
## 2.0.42022-09-23
- 升级 uni-id-pages 至 1.0.22
## 2.0.32022-09-21
- 修复 云函数请求无返回数据的bug
## 2.0.22022-09-20
- 升级 uni-id-pages 至 1.0.18
- 优化 导航登录用户名的显示规则:用户昵称 > 用户名 > 手机号 > 邮箱
## 2.0.12022-09-19
- 升级 uni-id-pages 至 1.0.17
- 修改 导航登录用户名的显示规则:优先显示用户昵称,其次显示用户名
- 增加 用户管理列表展示“用户昵称”字段
- 增加 创建用户支持添加“用户昵称”字段
## 2.0.02022-09-16
- 升级 uni-id-pages 至 1.0.13
- 修复 应用中心修改应用无法修改的bug
## 1.10.12022-09-08
- 修复 使用 uniIdRouter 时导致页面无法打开的Bug
## 1.10.02022-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.82022-08-15
- 修复 应用管理修改页面报错
## 1.9.72022-08-08
- 改进 sourceMap 回溯源码功能使用方法,需要在 admin.config.js 中配置相关信息。[详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#upload-sourcemap)
- 修复 js报错统计报错的Bug
## 1.9.62022-08-02
- 修复 vue3 打包报错的Bug
- 修复 升级中心发布 wgt 时原生 App 最低版本没有必填的Bug
- 修复 升级中心发布 wgt 时显示Android应用市场的Bug
## 1.9.52022-07-29
- 修复 运行到微信小程序控制台报错的Bug
## 1.9.42022-07-28
- 新增 uni-admin uni统计支持上传 sourceMap报错可准确回溯源码 [详情](https://uniapp.dcloud.io/uni-stat-v2.html#sourcemap-parse-error)
## 1.9.32022-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.22022-07-11
- 修复 留存统计跑批任务获取不到版本号的Bug
## 1.9.12022-07-06
- 新增 opendb-device表开通 uni-push2.0 与 uni统计2.0 自动上报 push_clientid 到 opendb-device表
## 1.9.02022-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.52022-06-29
- 新增 支持 ios 安全区
## 1.8.42022-06-01
- 新增 uni统计 可通过选择「应用版本」查询数据
- 新增 uni统计 原生 app 崩溃页各项功能
- 修复 uni统计 渠道页 table 表格最后一列空白的 bug
- 修复 uni统计 场景分析页趋势图有数据却显示为 0 的 bug
- 修复 系统设置权限只能加载 20 条的 bug
## 1.8.32022-05-19
- 优化 「首页」逻辑调整,无 appid 时提示添加 app 记录,可跳转 app 管理的新增页
- 优化 移除登录时多余的 init 的逻辑,提升登录速度
- 优化 「页面统计」添加 「入口页」、「登录页」的提示文字
- 修复 从「首页」跳转「概况」时url 的 query 丢失的 bug
## 1.8.22022-05-18
- 优化 uni 统计的「统计首页」菜单移动到应用「首页」,添加了设备概览、注册用户概览
- 优化 uni 统计的「帮助」菜单移动到「文档与插件」
- 修复 路由改变后面包屑未响应的 bug
## 1.8.12022-05-17
- 修复 去掉多余的 schema
## 1.8.02022-05-17
**重要更新:**
- 新增 用户日志功能
- 新增 内置 uni 统计报表体系,开源、免费、可私有化部署,[了解更多](https://uniapp.dcloud.net.cn/uni-stat-v2.html#uni%E7%BB%9F%E8%AE%A1),具体功能如下
- 统计首页
- 设备统计
- 用户统计
- 页面统计
- 渠道/场景值分析
- 自定义事件
- 错误统计
## 1.7.132022-02-15
- 修复 新增菜单页‘内置图标’在 vue3 平台不显示的 bug
- 修复 ‘新增一级菜单’ 按钮的文字错误
## 1.7.122022-01-26
- 修复 uni-admin 的 'registerUser' 接口,注册用户含有多余字段 uid
## 1.7.112022-01-19
- 修复 多个用户的用户名相同时,后注册的同名用户登录时提示“用户不存在”的 bug
- 修复 偶发的验证码输出正确却提示“验证码错误”的 bug
- 修复 刷新页面后验证码消的 bug
## 1.7.102021-12-20
- 优化 支持 vue3 查找并注册的菜单(包括插件菜单)
## 1.7.92021-12-07
- 新增 标签管理功能,可批量为用户添加或移除标签、通过标签过滤用户
## 1.7.82021-11-30
- 修复 Android 平台切换语言闪退的 bug该平台暂不支持切换语言
## 1.7.72021-11-29
- 修复 uni-datetime-picker 国际化未默认英文的问题
- 修复 uni-datetime-picker 范围选择在表格列头中渲染相同月份的问题
## 1.7.62021-11-11
- 优化 修改密码功能不再支持查看明文密码
- 修复 某些屏幕上input 框中下划线 '_' 被隐藏的 bug
## 1.7.52021-10-08
- 修复 用户管理与角色管理模糊搜索时关联的外键无法搜索的 bug
## 1.7.42021-09-30
- 修复 topwindow 非 h5 端key 使用表达式报错的 bug
- 优化 topwindow 中英文混排不对齐的问题
## 1.7.32021-09-27
- 修复 vue3 上加载 PostCSS 插件失败的 bug
## 1.7.22021-09-17
- 优化 取消菜单管理请求数据条数限制
- 优化 topwindow 菜单文字换行的问题
- 修复 左侧菜单栏刷新失去打开状态的 bug
## 1.7.12021-09-14
- 修复 vue3 下 i18n 未定义的 bug
- 优化 抛出被 error.js 拦截的报错
## 1.7.02021-08-31
- 新增 支持国际化 i18n
- 优化 验证码图片边框样式调整
## 1.6.22021-08-26
- 修复 非 admin 角色的用户无权限访问菜单表,动态菜单不显示的 bug
> 更新后,需上传 opendb-admin-menus.schema.json
- 优化 list 页的表格样式
## 1.6.12021-08-16
- 修复 uni-id-cf 中无用的node_modules造成的报错
- 修复 uni.css 中样式穿透造成的 uni-file-picker 不可见的 bug
## 1.6.02021-07-31
**重要更新:**
- 新增 应用管理功能管理用户可登录的应用uni-id@3.3.1+ 支持)
- 新增 升级系统管理 list 页的表格功能,支持数据排序、筛选、搜索等功能
- 新增 同时适配 vue2 和 vue3HBuilder X 3.2.0+ 支持 vue3
- 修复 刷新页面时,左侧菜单丢失高亮状态的 bug
- 修复 修改密码失败的 bug
## 1.5.82021-07-12
- 修复 侧边栏菜单查询数据条数一次不超过 20 条的 bug限制是最大一次 500 条)
## 1.5.72021-07-02
- 修复 菜单管理排序错误的 bug
- 优化 框架设定非 admin 不能创建用户, 用户可自定义
## 1.5.62021-06-28
- 修复 left-window 在小程序上的编译错误
## 1.5.52021-06-21
- 修复 角色管理删除功能失效的 bug
- 修复 权限管理删除功能失效的 bug
## 1.5.42021-06-21
- 优化 云函数 uni-id-cf uni_module 化,更新更方便
## 1.5.32021-06-17
- 优化 opendb-admin-menus.schema 读权限配置默认为 true
> 原因:侧边栏菜单管理功能使用了 clientDB, 默认全部读取,通过用户权限过滤
## 1.4.62021-05-27
- 修复 未连接服务空间时登录页空白的 bug
## 1.4.52021-05-18
- 新增 选择表格分页条数功能
- 修复 切换分页条数当前分页不是1时获取数据出错的 bug
## 1.4.42021-05-17
- 优化 导出 Excel 功能的代码
- 优化 系统管理 list 页面样式
- 优化 文案调整
## 1.4.32021-05-14
- PC 端支持表格导出数据为 Excel
## 1.4.22021-04-21
- 更新 uni-id 3.1.0
- 增加对用户名、邮箱、密码字段的两端去空格
- 默认忽略用户名、邮箱的大小写 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=case-sensitive)
- 修复 customToken导出async方法报错的Bug
## 1.4.12021-04-16
- 更新 uni-tabel 1.0.3
- 新增 根目录下 changelog.md

199
common/admin-icons.css Normal file
View File

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

293
common/theme.scss Normal file
View File

@ -0,0 +1,293 @@
@import '@/uni.scss';
$theme-map: ();
$primary-key: 'primary';
$success-key: 'success';
$warn-key: 'warn';
$warning-key: 'warning';
$error-key: 'error';
@mixin themeify {
@each $theme-name, $theme-map in $themes {
$theme-map: $theme-map !global;
[data-theme='#{inspect($theme-name)}'] {
@content;
}
}
}
@function getTheme($key) {
@return map-get($theme-map, $key);
}
@mixin uni-button($button-type) {
$button-type-color: getTheme(#{$button-type + '-color'});
uni-button,
button {
&[type='#{$button-type}'] {
background-color: $button-type-color;
&[disabled] {
background-color: opacify($button-type-color, 0.6);
}
&[plain] {
color: $button-type-color;
border-color: $button-type-color;
background-color: transparent;
}
&[loading] {
background-color: $button-type-color;
&[plain] {
color: $button-type-color;
}
}
&.button-hover {
$hover-color: darken(
$color: $button-type-color,
$amount: 10%
);
background-color: $hover-color;
&[plain] {
color: $hover-color;
border-color: $hover-color;
background-color: transparent;
}
}
}
}
}
@mixin uni-switch {
$primary-color: getTheme(#{$primary-key + '-color'});
.uni-switch-input.uni-switch-input-checked {
background-color: $primary-color !important;
border-color: $primary-color !important;
}
}
@mixin uni-ui-checkbox {
$primary-color: getTheme(#{$primary-key + '-color'});
.checklist-box {
&.is-checked {
.checkbox__inner {
border-color: $primary-color !important;
background-color: $primary-color !important;
}
.radio__inner {
border-color: $primary-color !important;
.radio__inner-icon {
background-color: $primary-color !important;
}
}
.checklist-text {
color: $primary-color !important;
}
}
.checkbox__inner:hover {
border-color: $primary-color !important;
}
}
}
@mixin uni-ui-easyinput {
$primary-color: getTheme(#{$primary-key + '-color'});
$error-color: getTheme(#{$error-key + '-color'});
.uni-easyinput {
&.uni-easyinput-error {
color: $error-color !important;
}
.uni-easyinput__content {
&.is-focused {
&.is-input-border {
border-color: $primary-color !important;
}
.uni-icons {
color: $primary-color !important;
}
}
}
}
}
@mixin uni-menu {
$primary-color: getTheme(#{$primary-key + '-color'});
// 左侧菜单
.uni-nav-menu {
.uni-menu-item.is-active {
color: $primary-color;
}
}
// 修改密码
.navbar-menu {
.menu-item.hover-highlight:hover {
color: $primary-color;
}
}
}
@mixin uni-table {
$primary-color: getTheme(#{$primary-key + '-color'});
.uni-table {
.link-btn-color {
color: $primary-color;
}
.uni-table-checkbox {
.checkbox__inner {
&.checkbox--indeterminate,
&.is-checked {
border-color: $primary-color;
background-color: $primary-color;
}
}
.checkbox__inner:hover {
border-color: $primary-color;
}
}
.uni-table-th-content {
.arrow-box {
.arrow.active ::after {
background-color: $primary-color;
}
}
}
// 表格头部搜索按钮
.opera-area {
.btn.btn-submit {
background-color: $primary-color;
}
}
.dropdown-btn {
.icon-search.active {
.icon-search-0 {
border-color: $primary-color;
}
.icon-search-1 {
background-color: $primary-color;
}
}
.icon-calendar.active {
.icon-calendar-0 {
border-color: $primary-color;
}
.icon-calendar-1 {
background-color: $primary-color;
}
}
}
.uni-icons.uni-stat-edit--btn {
color: $primary-color !important;
}
}
.uni-pagination {
.uni-pagination__num-current .page--active {
background-color: $primary-color !important;
}
}
}
@mixin uni-picker {
$primary-color: getTheme(#{$primary-key + '-color'});
.uni-picker-select {
.uni-picker-item.selected {
color: $primary-color;
}
}
}
@mixin uni-calendar {
$primary-color: getTheme(#{$primary-key + '-color'});
.uni-calendar__button-text {
color: $primary-color;
}
.uni-datetime-picker--btn {
background-color: $primary-color;
}
.uni-calendar-item--multiple {
.uni-calendar-item--before-checked,
.uni-calendar-item--after-checked {
background-color: $primary-color;
}
}
.uni-calendar-item__weeks-box {
.uni-calendar-item--checked {
background-color: $primary-color;
}
&-text {
color: darken($color: $primary-color, $amount: 40%);
}
}
}
@mixin uni-popup {
$primary-color: getTheme(#{$primary-key + '-color'});
.uni-popup-dialog {
.uni-button-color {
color: $primary-color;
}
}
}
@mixin uni-tag($tag-type) {
$tag-type-color: getTheme(#{$tag-type + '-color'});
.uni-tag {
&--#{$tag-type} {
&--inverted {
background-color: #fff !important;
color: $tag-type-color !important;
}
background-color: $tag-type-color !important;
border-color: $tag-type-color !important;
}
}
}
body {
@at-root {
@include themeify {
$primary-color: getTheme(#{$primary-key + '-color'});
// 组件
@include uni-button($primary-key);
@include uni-button($warn-key);
@include uni-tag($primary-key);
@include uni-tag($success-key);
@include uni-tag($warning-key);
@include uni-tag($error-key);
@include uni-ui-checkbox;
@include uni-switch;
@include uni-ui-easyinput;
@include uni-menu;
@include uni-table;
@include uni-picker;
@include uni-calendar;
@include uni-popup;
// 页面
.link-btn {
color: $primary-color !important;
}
.uni-stat--tab-item {
&.uni-stat--tab-item-line-active,
&.uni-stat--tab-item-boldLine-active {
color: $primary-color;
border-color: $primary-color;
}
&.uni-stat--tab-item-box-active {
border-color: $primary-color;
}
}
.uni-title.app-list {
color: $primary-color;
border-color: $primary-color;
}
.uni-link {
color: $primary-color;
}
.uni-selector-select .uni-picker-item.selected {
color: $primary-color;
}
.uni-tabs__item.is-active {
color: $primary-color;
}
.uni-modal__btn_primary {
color: $primary-color !important;
}
.uni-radio-input-checked {
background-color: $primary-color !important;
border-color: $primary-color !important;
}
.uni-container {
.icon-item:hover,
.icon-item:hover .icon-text {
color: $primary-color;
}
}
}
}
}

542
common/uni-icons.css Normal file
View File

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

605
common/uni.css Normal file
View File

@ -0,0 +1,605 @@
/* 全局公共样式 */
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;
}
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-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;
border-radius: 3px;
}
/* 分页容器 */
.uni-pagination-box {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.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-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 {
background-color: #fff;
border-right-color: #dcdfe6;
border-left-color: #dcdfe6;
}
.uni-form-item-tips {
color: #999;
font-size: 12px;
margin-top: 10px;
}
.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 {
/* #ifndef MP-TOUTIAO */
background: #fff;
/* #endif */
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;
}
.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;
}
.p-channel {
padding: 0 15px 15px 15px;
}
.p-1015 {
padding: 10px 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--app-select{
display: flex;
flex-wrap: wrap;
align-items: center;
width: 900px;
max-width: 100%;
}
.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 {
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;
}
.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: 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 */
/* #ifndef H5 */
.fix-top-window {
margin-top: 85px;
}
/* #endif */
/* 地图选择top需要大于topWindow的高度 */
.uni-system-choose-location{
display: block;
position: fixed;
left: 0;
top: 60px;
width: 100%;
height: calc(100% - 60px);
background: #f8f8f8;
}

View File

@ -0,0 +1,507 @@
<template>
<view>
<uni-popup ref="smsPopup" type="center" @change="popupChange" :is-mask-click="false">
<view class="sms-manager">
<view class="sms-manager--header mb">群发短信</view>
<uni-forms :label-width="100" :modelValue="smsDataModel" ref="smsForm">
<uni-forms-item v-if="toType === 'user' && !isSelectedReceiver" label="目标对象" name="smsPreset" :rules="[{ required: true, errorMessage: '请选择目标对象' }]" required>
<uni-data-select class="type m" placeholder="预设条件" size="mini" :clear="false"
:localdata="smsPresetList" v-model="smsDataModel.smsPreset">
</uni-data-select>
<view class="sms-data-tip">如需给指定用户发送请在列表选择要发送的用户</view>
</uni-forms-item>
<uni-forms-item label="目标对象" v-else-if="toType === 'user' && isSelectedReceiver">
<view>当前已选择{{ receiver.length }}</view>
</uni-forms-item>
<uni-forms-item label="目标对象" v-else-if="toType === 'userTags' ">
<view>当前已选择{{ receiver.length }}个标签</view>
<view class="sms-data-tip">如标签关联的用户没有绑定手机号将不会发送短信</view>
</uni-forms-item>
<uni-forms-item label="跨分页选择" v-if="isSelectedReceiver && hasCondition">
<checkbox-group @change="smsFilteredChange">
<checkbox style="transform: scale(.9)" :checked="smsDataModel.filtered"></checkbox>
</checkbox-group>
<view class="sms-data-tip">对用户进行了筛选后可能存在分页无法全部选中时请勾选跨分页选中</view>
</uni-forms-item>
<uni-forms-item label="任务名称" name="name" required
:rules="[{ required: true, errorMessage: '请输入任务名称' }]">
<uni-easyinput v-model="smsDataModel.name" placeholder="请输入任务名称,例如 “7日内未登录用户召回”"/>
</uni-forms-item>
<uni-forms-item required label="短信模板" name="templateId"
:rules="[{ required: true, errorMessage: '请选择短信模板' }]">
<template v-if="!smsTemplateLoading">
<view v-if="smsTemplate.length">
<uni-data-select class="type m" placeholder="请选择短信模板" size="mini" :clear="false"
:localdata="smsTemplate" v-model="smsDataModel.templateId"
@change="onSmsTemplateSelected">
</uni-data-select>
<view class="sms-data-tip">
导入短信模版参考<a class="a-link" href="https://uniapp.dcloud.net.cn/uniCloud/admin.html#群发短信"
target="_blank">教程</a>若有新的短信模版
<text @click="chooseFile"
class="a-link">点此导入
</text>
</view>
</view>
<view v-else>
<button @click="chooseFile" type="primary" style="width: 120px;"
size="mini">上传短信模板
</button>
<view class="sms-data-tip">当前未导入短信模板请从dev.dcloud.net.cn的短信-<a
href="https://dev.dcloud.net.cn/pages/sms/template" target="_blank">模板配置</a>中导出短信模版并在此导入教程<a
href="https://uniapp.dcloud.net.cn/uniCloud/admin.html#batch-sms" target="_blank">详见</a></view>
</view>
</template>
<template v-else>
模板加载中...
</template>
</uni-forms-item>
<uni-forms-item label="短信内容" v-if="smsTemplateContent">
<view class="form-item-flex-center">{{ smsTemplateContent }}</view>
</uni-forms-item>
<uni-forms-item label="模板变量配置" :error-message="smsTemplateDataErrorMessage"
v-if="smsDataModel.templateData.length">
<view class="sms-data-item" :key="template.field"
v-for="(template, index) in smsDataModel.templateData">
<uni-easyinput class="field m" v-model="template.field" placeholder="字段" :clearable="false"
:disabled="true" style="width: 120px;flex:none;"/>
<uni-easyinput class="value m" v-model="template.value"
placeholder="例 {uni-id-users.username}" :clearable="false"/>
</view>
<view class="sms-data-tip">
短信变量支持固定值和数据表查询两种方式固定值如各位同事数据表查询如{uni-id-users.username}请注意若使用数据表查询方式目前仅支持查询
uni-id-users 并注意确保数据库中查询字段值不为空否则短信将发送失败
</view>
</uni-forms-item>
</uni-forms>
<view class="uni-group">
<button @click="sendSms(true)" class="uni-button">预览</button>
<button @click="sendSms()" class="uni-button" type="primary">提交</button>
</view>
</view>
<uni-icons type="closeempty" size="24" class="close" @click="close"></uni-icons>
</uni-popup>
<uni-popup ref="previewPopup" type="center" :is-mask-click="false">
<view class="sms-manager preview">
<view class="sms-manager--header mb">
<view>短信预览</view>
<view class="sub-title">仅预览第一条短信内容</view>
<view class="sub-title">预计送达 <text style="color: red">{{smsSendUserCount}}</text> 位用户</view>
</view>
<view class="content">
<view v-for="(content,index) of smsPreviewContent" :key="index">{{ content }}</view>
<view class="length">短信字数
<text class="num">{{ smsPreviewContent.length ? smsPreviewContent[0].length : 0 }}</text>
</view>
</view>
<view class="tip">
<view>说明</view>
<view>若从数据表中查询字段内容长度会影响总字数短信字数短信签名字数+短信内容字数</view>
<view>短信长度不超过70个字按照一条短信计费超过70个字按照67字/条拆分成多条计费</view>
</view>
<view class="uni-group">
<button @click="$refs.previewPopup.close()" class="uni-button">关闭</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
const uniSmsCo = uniCloud.importObject('uni-sms-co')
export default {
name: 'batchSms',
props: {
// user|userTags
toType: String,
// user=user._id, userTags=tag.id
receiver: {
type: Array,
default() {
return []
}
},
//
condition: {
type: Object,
default () {
return {}
}
}
},
data() {
return {
smsTemplateLoading: false,
smsPresetList: [{
value: 'all',
text: '全部用户',
},{
value: '7-day-offline-users',
text: '7天内未登录用户',
},{
value: '15-day-offline-users',
text: '15天内未登录用户',
},{
value: '30-day-offline-users',
text: '30天内未登录用户',
}],
smsTemplate: [],
smsTemplateDataErrorMessage: '',
smsDataModel: {
name: '',
templateId: '',
templateData: [],
smsPreset: '',
filtered: false
},
smsTemplateContent: '',
smsPreviewContent: [],
smsSendUserCount: 0
}
},
computed: {
isSelectedReceiver() {
return !!this.receiver.length
},
sendAll() {
return this.smsDataModel.smsPreset === 'all' || this.toType === 'userTags'
},
hasCondition () {
return !!Object.keys(this.condition).length
}
},
watch: {
smsDataModel: {
handler(smsDataModel) {
if (!smsDataModel.templateId) return ''
const template = this.smsTemplate.find(template => template.value === smsDataModel.templateId)
let content = smsDataModel.templateData.reduce((res, param) => {
const reg = new RegExp(`\\$\\{${param.field}\\}`)
return res.replace(reg, ($1) => param.value || $1)
}, template.content)
this.smsTemplateContent = `${template.sign}${content}`
},
deep: true
}
},
methods: {
smsFilteredChange () {
this.smsDataModel.filtered = !this.smsDataModel.filtered
},
popupChange(e) {
if (!e.show) this.reset()
},
open() {
this.$refs.smsPopup.open()
this.loadSmsTemplate()
},
close() {
this.reset()
this.$refs.smsPopup.close()
},
async loadSmsTemplate() {
if (this.smsTemplate.length > 0 || this.smsTemplateLoading) return
this.smsTemplateLoading = true
try {
const uniSmsCo = uniCloud.importObject('uni-sms-co', {customUI: true})
const res = await uniSmsCo.template()
this.smsTemplate = res.map(item => ({
...item,
value: item._id,
text: item.name,
}))
} finally {
this.smsTemplateLoading = false
}
},
onSmsTemplateSelected(templateId) {
const current = this.smsTemplate.find(template => template.value === templateId)
if (!current) return
const reg = new RegExp(/\$\{(.*?)\}/g)
let templateVars = []
let _execResult
while (_execResult = reg.exec(current.content)) {
const param = _execResult[1]
if (param) {
templateVars.push({
field: param,
value: ''
})
}
}
this.smsDataModel.templateData = templateVars
},
async sendSms(isPreview = false) {
const values = await this.$refs.smsForm.validate()
const receiver = this.receiver
for (const template of this.smsDataModel.templateData) {
if (!template.value) {
this.smsTemplateDataErrorMessage = '字段/值不可为空'
return
}
}
this.smsTemplateDataErrorMessage = ''
const to = {
type: this.toType,
receiver,
}
if (this.smsDataModel.filtered || this.smsDataModel.smsPreset) {
to.condition = this.smsDataModel.smsPreset || this.condition
}
if (isPreview) {
const res = await uniSmsCo.preview(
to,
values.templateId,
this.smsDataModel.templateData
)
if (res.errCode === 0) {
this.smsPreviewContent = res.list
this.$refs.previewPopup.open()
this.smsSendUserCount = res.total
return
}
}
uni.showModal({
title: '发送确认',
content: `短信${this.sendAll ? '将发送给所有用户' : this.smsSendUserCount ? `预计发送${this.smsSendUserCount}`: `将发送给符合条件的用户`},确定发送?`,
success: async (e) => {
this.smsSendUserCount = 0
if (e.cancel) return
const res = await uniSmsCo.createSmsTask(
to,
values.templateId,
this.smsDataModel.templateData, {
taskName: values.name
}
)
if (res.taskId) {
uni.showModal({
content: '短信任务已提交您可在DCloud开发者后台查看短信发送记录',
confirmText: '立即查看',
cancelText: '关闭',
success: (e) => {
if (e.cancel) {
this.reset()
this.$refs.smsPopup.close()
} else {
// #ifdef H5
window.open('https://dev.dcloud.net.cn/#/pages/sms/sendLog', '_blank')
// #endif
// ifndef H5
this.reset()
this.$refs.smsPopup.close()
// endif
}
}
})
}
}
})
},
chooseFile() {
if (typeof window.FileReader === 'undefined') {
uni.showModal({
content: '当前浏览器不支持文件上传,请升级浏览器重试',
showCancel: false
})
return
}
uni.chooseFile({
count: 1,
extension: ['.json'],
success: ({tempFiles}) => {
if (tempFiles.length <= 0) return
const [file] = tempFiles
const reader = new FileReader()
reader.readAsText(file)
reader.onload = () => this.parserFileJson(null, reader.result)
reader.onerror = () => this.parserFileJson(reader.error)
},
fail: () => {
uni.showModal({
content: '打开选择文件框失败',
showCancel: false
})
}
})
},
async parserFileJson(error, fileContent) {
if (error) {
console.error(error)
uni.showModal({
content: '文件读取失败,请重新上传文件',
showCancel: false
})
return
}
let templates = []
try {
templates = JSON.parse(fileContent)
} catch (e) {
console.error(e)
uni.showModal({
content: '短信模板解析失败,请检查模板格式',
showCancel: false
})
return
}
const res = await uniSmsCo.updateTemplates(templates)
if (res.errCode === 0) {
uni.showModal({
content: '短信模板更新成功',
showCancel: false,
success: () => {
this.loadSmsTemplate()
}
})
}
},
reset() {
this.smsDataModel.name = ''
this.smsDataModel.smsPreset = ''
this.smsDataModel.templateId = ''
this.smsDataModel.templateData = []
this.smsPreviewContent = []
this.smsTemplateContent = ''
this.smsSendUserCount = 0
}
}
}
</script>
<style lang="scss">
@import '@/uni_modules/uni-scss/variables.scss';
.a-link {
cursor: pointer;
color: $uni-primary;
text-decoration: none;
}
.close {
position: absolute;
right: 20px;
top: 20px;
cursor: pointer;
}
.sms-manager {
width: 570px;
background: #fff;
padding: 30px;
border-radius: 5px;
&.preview {
width: 550px;
}
&--header {
text-align: center;
font-size: 22px;
&.mb {
margin-bottom: 50px;
}
.sub-title {
margin-top: 5px;
font-size: 16px;
color: #999;
}
}
.content {
margin-top: 20px;
font-size: 16px;
line-height: 1.5;
.length {
text-align: right;
font-size: 13px;
margin-top: 20px;
.num {
color: red;
}
}
}
.tip {
border-top: #ccc solid 1px;
padding-top: 20px;
margin-top: 20px;
line-height: 1.7;
font-size: 13px;
color: #999;
}
}
.sms-data-item {
display: flex;
align-items: center;
margin-top: 10px;
&:first-child {
margin-top: 0;
}
.m {
margin: 0 5px;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
.type {
width: 100px;
flex: none;
}
.add,
.minus {
cursor: pointer;
}
}
.sms-data-tip {
color: $uni-info;
font-size: 12px;
margin-top: 5px;
}
.form-item-flex-center {
height: 100%;
display: flex;
align-items: center;
}
</style>

View File

@ -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() {
let 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;
let 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 (let 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>

View File

@ -0,0 +1,153 @@
//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) {
let 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:
let 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)) {
let 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) {
let 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):
let 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() */

View File

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

View File

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

View File

@ -0,0 +1,186 @@
<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
}
},
immediate: true
},
// TODO
// #ifdef H5
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)
}
}
},
// #endif
$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
// #ifndef H5
this.load()
// #endif
},
// 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)
}).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>

View File

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

View File

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

View File

@ -0,0 +1,134 @@
<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() {
let subMenu = this.rootMenu && this.rootMenu.SubMenu && this.rootMenu.SubMenu.length || 0;
return 20 + 20 * subMenu + '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>

View File

@ -0,0 +1,48 @@
<template>
<view class="pointer">
<block v-for="(item,index) in data" :key="index">
<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>
</block>
</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>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,71 @@
<template>
<uni-table :loading="loading" border stripe emptyText="暂无数据">
<uni-tr>
<block v-for="(mapper, index) in filedsMap" :key="index">
<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>
</block>
</uni-tr>
<uni-tr v-for="(item ,i) in data" :key="i">
<block v-for="(mapper, index) in filedsMap" :key="index">
<uni-td v-if="mapper.title" :key="index" align="center">
{{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
</uni-td>
</block>
</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>

View File

@ -0,0 +1,361 @@
<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: [],
cacheKey: "uni-admin-statTabsData"
};
},
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()
},
computed:{
},
watch: {
current: {
immediate: true,
handler(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 = this.getCache() || [];
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)
}
this.setCache(platforms);
this.renderTabs = platforms
})
},
setAllItem(platforms, _id = '', name = '全部') {
this.all && platforms.unshift({
name,
_id
})
},
// key
getCurrentCacheKey(){
return this.mode;
},
//
getCache(name=this.getCurrentCacheKey()){
let cacheData = uni.getStorageSync(this.cacheKey) || {};
return cacheData[name];
},
//
setCache(value, name=this.getCurrentCacheKey()){
let cacheData = uni.getStorageSync(this.cacheKey) || {};
cacheData[name] = value;
uni.setStorageSync(this.cacheKey, cacheData);
},
//
removeCache(name=this.getCurrentCacheKey()){
let cacheData = uni.getStorageSync(this.cacheKey) || {};
delete cacheData[name];
uni.setStorageSync(this.cacheKey, cacheData);
},
}
}
</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;
}
/* #ifdef H5 */
::-webkit-scrollbar {
display: none;
}
/* #endif */
}
/* #endif */
</style>

View File

@ -0,0 +1,165 @@
<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="down" 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() {
let subMenu = this.rootMenu && this.rootMenu.SubMenu && this.rootMenu.SubMenu.length || 0;
return 20 + 20 * subMenu + '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>

107
i18n/en.json Normal file
View File

@ -0,0 +1,107 @@
{
"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",
"sendSMS": "Send SMS"
},
"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",
"updateBuiltInMenu": "Update built-in Menu"
}
},
"demo": {
"icons": {
"title": "Icons",
"describle": "Click icons to copy the icon code"
},
"table": {
"title": "Table"
}
}
}

8
i18n/index.js Normal file
View File

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

107
i18n/zh-Hans.json Normal file
View File

@ -0,0 +1,107 @@
{
"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": "版本管理",
"sendSMS": "群发短信"
},
"empty": "没有更多数据",
"piecePerPage": "条/页"
},
"user": {
"text": {
"userManager": "用户管理"
}
},
"role": {
"text": {
"roleManager": "角色管理"
}
},
"permission": {
"text": {
"permissionManager": "权限管理"
}
},
"app": {
"text": {
"appManager": "应用管理",
"describle": "管理用户可登录的应用"
}
},
"menu": {
"text": {
"menuManager": "菜单列表",
"additiveMenu": "待添加菜单"
},
"button": {
"addFirstLevelMenu": "新增一级菜单",
"addChildMenu": "子菜单",
"updateBuiltInMenu": "更新内置菜单"
}
},
"demo": {
"icons": {
"title": "图标",
"describle": "点击图标即可复制图标代码"
},
"table": {
"title": "表格"
}
}
}

107
i18n/zh-Hant.json Normal file
View File

@ -0,0 +1,107 @@
{
"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": "版本管理",
"sendSMS": "群發短信"
},
"empty": "沒有更多數據",
"piecePerPage": "條/頁"
},
"user": {
"text": {
"userManager": "用戶管理"
}
},
"role": {
"text": {
"roleManager": "角色管理"
}
},
"permission": {
"text": {
"permissionManager": "權限管理"
}
},
"app": {
"text": {
"appManager": "應用管理",
"describle": "管理用戶可登錄的應用"
}
},
"menu": {
"text": {
"menuManager": "菜單列表",
"additiveMenu": "待添加菜單"
},
"button": {
"addFirstLevelMenu": "新增一級菜單",
"addChildMenu": "子菜單",
"updateBuiltInMenu": "更新內寘選單"
}
},
"demo": {
"icons": {
"title": "圖標",
"describle": "點擊圖標即可複製圖標代碼"
},
"table": {
"title": "表格"
}
}
}

20
index.html Normal file
View File

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

View File

@ -0,0 +1,204 @@
/**
* 设置 uniCloud.uploadFile 默认上传到扩展存储
* @param {String} provider 云储存供应商
* @value unicloud 内置存储
* @value extStorage 扩展存储
* @param {String} domain 自定义域名仅扩展存储有效
* @param {Boolean} fileID2fileURL 是否将fileID转为fileURL
* @param {Function} uploadFileOptions 获取上传参数的函数仅扩展存储有效
*/
function init(options = {}) {
let {
provider: defaultProvider,
} = options;
let originalDefaultProvider = defaultProvider;
let extStorage = new ExtStorage(options);
const uploadFile = uniCloud.uploadFile;
uniCloud.uploadFile = (...e) => {
let options = e[0] || {};
let {
provider = defaultProvider
} = options;
if (provider === "extStorage") {
return extStorage.uploadFile(...e);
} else {
return uploadFile(...e);
}
}
const getTempFileURL = uniCloud.getTempFileURL;
uniCloud.getTempFileURL = (...e) => {
let options = e[0] || {};
let {
provider = defaultProvider
} = options;
if (provider === "extStorage") {
return extStorage.getTempFileURL(...e);
} else {
return getTempFileURL(...e);
}
}
const deleteFile = uniCloud.deleteFile;
uniCloud.deleteFile = (...e) => {
let options = e[0] || {};
let {
provider = defaultProvider
} = options;
if (provider === "extStorage") {
return extStorage.deleteFile(...e);
} else {
return deleteFile(...e);
}
}
uniCloud.setCloudStorage = (data={}) => {
let {
provider,
domain,
fileID2fileURL,
} = data;
if (provider === null) {
defaultProvider = originalDefaultProvider;
} else if (provider) {
defaultProvider = provider;
}
if (domain) extStorage.domain = domain;
if (fileID2fileURL) extStorage.fileID2fileURL = fileID2fileURL;
}
}
export default {
init
}
class ExtStorage {
constructor(data = {}) {
let {
uploadFileOptions,
domain,
fileID2fileURL
} = data;
this.uploadFileOptions = uploadFileOptions;
this.domain = domain;
this.fileID2fileURL = fileID2fileURL;
}
// 上传文件
uploadFile(options) {
let {
filePath,
cloudPath,
} = options;
const promiseRes = new Promise(async (resolve, reject) => {
try {
const uploadFileOptionsRes = await this.uploadFileOptions({
cloudPath,
domain: this.domain
});
const uploadTask = uni.uploadFile({
...uploadFileOptionsRes.uploadFileOptions, // 上传文件所需参数
filePath, // 本地文件路径
success: (uploadFileRes) => {
if (uploadFileRes.statusCode !== 200) {
const err = uploadFileRes;
if (typeof options.fail === "function") options.fail(err);
reject(err);
} else {
const res = {
cloudPath: uploadFileOptionsRes.cloudPath, // 文件云端路径
fileID: uploadFileOptionsRes.fileID, // 文件ID
fileURL: uploadFileOptionsRes.fileURL, // 文件URL如果是私有权限则此URL是无法直接访问的
};
if (this.fileID2fileURL) {
res.fileID = `https://${this.domain}/${res.cloudPath}`;
}
if (typeof options.success === "function") options.success(res);
resolve(res);
}
},
fail: (err) => {
if (typeof options.fail === "function") options.fail(err);
reject(err);
},
complete: () => {
if (typeof options.complete === "function") options.complete();
}
});
// 监听上传进度
uploadTask.onProgressUpdate((progressEvent) => {
if (typeof options.onUploadProgress === "function") {
const total = progressEvent.totalBytesExpectedToSend;
const loaded = progressEvent.totalBytesSent;
const progress = Math.round(loaded * 100 / total);
options.onUploadProgress({
total,
loaded,
progress
});
}
});
} catch (err) {
if (typeof options.fail === "function") options.fail(err);
reject(err);
if (typeof options.complete === "function") options.complete();
}
});
promiseRes.catch(() => {
});
return promiseRes;
}
// 获取临时文件下载地址
getTempFileURL(options = {}) {
let {
fileList
} = options;
return new Promise((resolve, reject) => {
let res = {
fileList: fileList.map((item, index) => {
let cloudPath = getCloudPath(item);
return {
fileID: item,
tempFileURL: `https://${this.domain}/${cloudPath}`
}
})
}
if (typeof options.success === "function") options.success(res);
resolve(res);
if (typeof options.complete === "function") options.complete();
});
}
// 删除文件
deleteFile(options = {}) {
// 扩展存储不允许前端删除文件(故此处直接返回)
return new Promise((resolve, reject) => {
let res = {
fileList: []
};
if (typeof options.success === "function") options.success(res);
resolve(res);
if (typeof options.complete === "function") options.complete();
});
}
}
function getCloudPath(cloudPath) {
const qiniuPrefix = 'qiniu://';
if (cloudPath.indexOf(qiniuPrefix) === 0) {
cloudPath = cloudPath.substring(qiniuPrefix.length);
} else if (cloudPath.indexOf('http://') === 0 || cloudPath.indexOf('https://') === 0) {
let startIndex = cloudPath.indexOf('://') + 3;
startIndex = cloudPath.indexOf('/', startIndex);
let endIndex = cloudPath.indexOf('?') === -1 ? cloudPath.length : cloudPath.indexOf('?');
endIndex = cloudPath.indexOf('#') !== -1 && cloudPath.indexOf('#') < endIndex ? cloudPath.indexOf('#') : endIndex;
cloudPath = cloudPath.substring(startIndex + 1, endIndex);
}
return cloudPath
}

View File

@ -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
js_sdk/uni-admin/error.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
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'
import {
initUniIdPageStore
} from "../uni-id-pages/store"
export default {
install(Vue) {
initUtil(Vue)
initError(Vue)
initUniIdPageStore(Vue)
initRequest(Vue)
initFetch(Vue)
initPermission(Vue)
initInterceptor()
}
}

View File

@ -0,0 +1,77 @@
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 => {
result = functionName ? result.result: 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

29
js_sdk/uni-admin/util.js Normal file
View File

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

View File

@ -0,0 +1,13 @@
import * as uniIdPagesStore from '@/uni_modules/uni-id-pages/common/store'
// #ifndef VUE3
export function initUniIdPageStore(Vue) {
Vue.prototype.$uniIdPagesStore = uniIdPagesStore
}
// #endif
// #ifdef VUE3
export function initUniIdPageStore(app) {
app.config.globalProperties.$uniIdPagesStore = uniIdPagesStore
}
// #endif

186
js_sdk/uni-stat/timeUtil.js Normal file
View File

@ -0,0 +1,186 @@
/**
* 时间工具类
*/
let timeUtil = {};
// 尽可能的将参数转成正确的时间对象
timeUtil.getDateObject = function(date) {
if (!date) return "";
let nowDate;
// 如果是字符串,且纯数字,则强制转数值
if (typeof date === "string" && !isNaN(date)) date = Number(date);
if (typeof date === "number") {
if (date.toString().length === 10) date *= 1000;
nowDate = new Date(date); // 转时间对象
} else if (typeof date === "object") {
nowDate = new Date(date.getTime()); // 新建一个时间对象
}
return nowDate;
};
/**
* 日期格式化
* @param {Date || Number} date 需要格式化的时间
* timeUtil.timeFormat(new Date(),"yyyy-MM-dd hh:mm:ss");
*/
timeUtil.timeFormat = function(date, fmt = 'yyyy-MM-dd hh:mm:ss') {
try {
if (!date) return "";
let nowDate = timeUtil.getDateObject(date);
let opt = {
"M+": nowDate.getMonth() + 1, //月份
"d+": nowDate.getDate(), //日
"h+": nowDate.getHours(), //小时
"m+": nowDate.getMinutes(), //分
"s+": nowDate.getSeconds(), //秒
//"w+": nowDate.getDay(), //周
"q+": Math.floor((nowDate.getMonth() + 3) / 3), //季度
"S": nowDate.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (nowDate.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in opt) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (opt[k]) : (("00" + opt[k]).substr(("" + opt[k]).length)));
}
}
return fmt;
} catch (err) {
// 若格式错误,则原值显示
return time;
}
};
/**
* 解析日期对象属性
* @param {Date || Number} date 需要转换的时间
* timeUtil.getDateInfo(new Date());
*/
timeUtil.getDateInfo = function(date = new Date()) {
let nowDate = timeUtil.getDateObject(date);
let year = nowDate.getFullYear() + '';
let month = (nowDate.getMonth() + 1 < 10 ? '0' + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1);
let day = (nowDate.getDate() < 10 ? '0' + (nowDate.getDate()) : nowDate.getDate());
let hour = (nowDate.getHours() < 10 ? '0' + (nowDate.getHours()) : nowDate.getHours());
let minute = (nowDate.getMinutes() < 10 ? '0' + (nowDate.getMinutes()) : nowDate.getMinutes());
let second = (nowDate.getSeconds() < 10 ? '0' + (nowDate.getSeconds()) : nowDate.getSeconds());
let millisecond = nowDate.getMilliseconds(); //毫秒
let week = nowDate.getDay(); // 周
let quarter = Math.floor((nowDate.getMonth() + 3) / 3); //季度
return {
year: Number(year),
month: Number(month),
day: Number(day),
hour: Number(hour),
minute: Number(minute),
second: Number(second),
millisecond: Number(millisecond),
week: Number(week),
quarter: Number(quarter),
};
};
/**
* 获得相对当前时间的偏移 count 小时季度年的起止日期开始和结束时间戳
* @param {Number} count 偏移量
* @param {Date || Number} date 指定从哪个时间节点开始计算
* timeUtil.getOffsetStartAndEnd("hour", 0);
* timeUtil.getOffsetStartAndEnd("day", 0);
* timeUtil.getOffsetStartAndEnd("week", 0);
* timeUtil.getOffsetStartAndEnd("month", 0);
* timeUtil.getOffsetStartAndEnd("quarter", 0);
* timeUtil.getOffsetStartAndEnd("year", 0);
*/
timeUtil.getOffsetStartAndEnd = function(type="day", count = 0, date = new Date()) {
let startTime, endTime;
let nowDate = timeUtil.getDateObject(date);
if (type === "hour") {
// 小时
// 一小时毫秒数
let offsetMillisecond = 1000 * 60 * 60;
// 相对于当前日期count个天的日期
let dateInfo = timeUtil.getDateInfo(new Date(nowDate.getTime() + (offsetMillisecond * 1 * count)));
// 获得当天的起始时间
startTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day} ${dateInfo.hour}:00:00`).getTime();
// 获得当天的结束时间
endTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day} ${dateInfo.hour}:00:00`).getTime() + (offsetMillisecond -1);
} else if (type === "day") {
// 天
// 一天的毫秒数
let offsetMillisecond = 1000 * 60 * 60 * 24;
// 相对于当前日期count个天的日期
let dateInfo = timeUtil.getDateInfo(new Date(nowDate.getTime() + (offsetMillisecond * 1 * count)));
// 获得当天的起始时间
startTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day}`).getTime();
// 获得当天的结束时间
endTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day}`).getTime() + (offsetMillisecond - 1);
} else if (type === "week") {
// 周
nowDate.setDate(nowDate.getDate() - nowDate.getDay() + 1 + count * 7);
let dateInfo1 = timeUtil.getDateInfo(nowDate);
nowDate.setDate(nowDate.getDate() + 7);
let dateInfo2 = timeUtil.getDateInfo(nowDate);
// 开始时间
startTime = new Date(`${dateInfo1.year}/${dateInfo1.month}/${dateInfo1.day}`).getTime();
// 结束时间
endTime = new Date(`${dateInfo2.year}/${dateInfo2.month}/${dateInfo2.day}`).getTime() - 1;
} else if (type === "month") {
// 月
let dateInfo = timeUtil.getDateInfo(nowDate);
let month = dateInfo.month + count;
let year = dateInfo.year;
if (month > 12) {
year = year + Math.floor(month / 12);
month = Math.abs(month) % 12;
} else if (month <= 0) {
year = year - 1 - Math.floor(Math.abs(month) / 12);
month = 12 - Math.abs(month) % 12;
}
let month_last_day = new Date(year, month, 0).getDate();
// 开始时间
startTime = new Date(`${year}/${month}/1`).getTime();
// 结束时间
endTime = new Date(`${year}/${month}/${month_last_day}`).getTime() + (24 * 60 * 60 * 1000 - 1);
} else if (type === "quarter") {
// 季度
nowDate.setMonth(nowDate.getMonth() + count * 3);
let dateInfo = timeUtil.getDateInfo(nowDate);
let month = dateInfo.month;
if ([1, 2, 3].indexOf(month) > -1) {
// 第1季度
month = 1;
} else if ([4, 5, 6].indexOf(month) > -1) {
// 第2季度
month = 4;
} else if ([7, 8, 9].indexOf(month) > -1) {
// 第3季度
month = 7;
} else if ([10, 11, 12].indexOf(month) > -1) {
// 第4季度
month = 10;
}
nowDate.setMonth(month - 1); // 因为0代表1月所以这里要减1
let dateInfo1 = timeUtil.getDateInfo(nowDate);
nowDate.setMonth(nowDate.getMonth() + 3);
let dateInfo2 = timeUtil.getDateInfo(nowDate);
// 开始时间
startTime = new Date(`${dateInfo1.year}/${dateInfo1.month}/1`).getTime();
// 结束时间
endTime = new Date(`${dateInfo2.year}/${dateInfo2.month}/1`).getTime() - 1;
} else if (type === "year") {
// 年
let dateInfo = timeUtil.getDateInfo(nowDate);
let year = dateInfo.year + count;
// 开始时间
startTime = new Date(`${year}/1/1`).getTime();
// 结束时间
endTime = new Date(`${year}/12/31`).getTime() + (24 * 60 * 60 * 1000 - 1);
}
return {
startTime,
endTime
};
};
export default timeUtil;

497
js_sdk/uni-stat/util.js Normal file
View File

@ -0,0 +1,497 @@
/**
* 以下为 uni-stat 的工具方法
*/
// 千分位
function regexHandleNum(num) {
return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ','); // 3是千分位4是万分位
}
// 新版格式化字段数据函数
function formatterData(object) {
let {
fieldsMap,
data,
formatter = true
} = object;
let rows = JSON.parse(JSON.stringify(data));
rows.map((row) => {
for (let key in row) {
let fieldItem = fieldsMap.find((item) => {
return item.field == key;
});
if (typeof fieldItem === "object") {
let {
fix = 0,
} = fieldItem;
if (typeof fieldItem.multiple === "number" && typeof row[key] === "number") {
row[key] = Number((row[key] * fieldItem.multiple).toFixed(fix));
}
if (formatter && fieldItem.formatter && typeof row[key] === "number") {
if (fieldItem.formatter === ",") {
row[key] = regexHandleNum(row[key]);
} else if (fieldItem.formatter === "%") {
row[key] = `${(row[key] * 100).toFixed(fix)}%`
} else if (fieldItem.formatter === "-") {
// 时分秒格式
row[key] = parseDateTime(row[key]);
}
}
}
}
});
return rows;
}
// 补全趋势图的数据
function fillTrendChartData(data, query, fieldsMap) {
let { start_time, dimension } = query;
if (["hour","day"].indexOf(dimension)>-1){
let timeArr = [];
let oneTime;
if (dimension === "hour"){
oneTime = 1000*3600;
} else if (dimension === "day"){
oneTime = 1000*3600*24;
}
let start = start_time[0];
let end = start_time[1];
let nowTime = start;
timeArr = [start];
while ((nowTime+oneTime)<=end){
nowTime += oneTime;
timeArr.push(nowTime);
}
let newData = [];
for (let i = 0; i < timeArr.length; i++) {
let time = timeArr[i];
let dataItem = data.find((item, index) => {
return item.start_time === time;
});
if (dataItem) {
newData.push(dataItem);
} else {
let obj = {
start_time: time
};
fieldsMap.map((item, index) => {
obj[item.field] = 0;
});
newData.push(obj);
}
}
return newData
} else {
return data;
}
}
// 将查询条件拼接为字符串
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 {
// 判断页面仅在pages/uni-stat/device/trend/trend和pages/uni-stat/user/trend/trend页面下放开按小时查询的时间限制
let pages = getCurrentPages();
let page = pages[pages.length-1];
if (["pages/uni-stat/device/trend/trend","pages/uni-stat/user/trend/trend"].indexOf(page.route) > -1) {
// 放开按小时查询的时间限制
queryArr.push(`${key} == ${val}`)
} 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
let str = `${h}:00 ~ ${h}:59`
if (h === "00") {
// 0 点的时候,显示为 yyyy-mm-dd00:00 ~ 00:59
let firstday = parseDateTime(d)
str = firstday + "00:00 ~ 00:59";
}
return str
} 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
}
}
}
let 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 {
formatterData,
fillTrendChartData,
stringifyQuery,
stringifyField,
stringifyGroupField,
mapfields,
getTimeOfSomeDayAgo,
division,
format,
formatDate,
parseDateTime,
maxDeltaDay,
debounce,
fileToUrl,
getFieldTotal,
getAllDateCN,
createUniStatQuery
}

View File

@ -0,0 +1,77 @@
// 校验规则由 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"
},
{
validateFunction:function(rule,value,data,callback){
if (value !== "" && value.indexOf("http") === -1 && value.indexOf("/") !==0){
callback('URL必须以/开头,如/pages/index/index')
}
return true
}
}
]
},
"sort": {
"rules": [
{
"format": "int"
}
]
},
"parent_id": {
"rules": [
{
"format": "string"
}
]
},
"permission": {
"rules": [
{
"format": "array"
}
]
},
"enable": {
"rules": [
{
"format": "bool"
}
]
}
}

View File

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

View File

@ -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
},
/* platformappwgt
{
"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
}

View File

@ -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"
}
]
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,191 @@
// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
const validator = {
"username": {
"rules": [{
"required": true,
"errorMessage": '请输入用户名',
},
{
"minLength": 3,
"maxLength": 32,
"errorMessage": '用户名长度在 {minLength} 到 {maxLength} 个字符',
},
{
validateFunction: function(rule, value, data, callback) {
// console.log(value);
if (/^1\d{10}$/.test(value) || /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(value)) {
callback('用户名不能是:手机号或邮箱')
};
if (/^\d+$/.test(value)) {
callback('用户名不能为纯数字')
};
if(/[\u4E00-\u9FA5\uF900-\uFA2D]{1,}/.test(value)){
callback('用户名不能包含中文')
}
return true
}
}
],
"label": "用户名"
},
"nickname": {
"rules": [{
minLength: 3,
maxLength: 32,
errorMessage: '昵称长度在 {minLength} 到 {maxLength} 个字符',
},
{
validateFunction: function(rule, value, data, callback) {
// console.log(value);
if (/^1\d{10}$/.test(value) || /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(value)) {
callback('昵称不能是:手机号或邮箱')
};
if (/^\d+$/.test(value)) {
callback('昵称不能为纯数字')
};
// if(/[\u4E00-\u9FA5\uF900-\uFA2D]{1,}/.test(value)){
// callback('昵称不能包含中文')
// }
return true
}
}
],
"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
}

View File

@ -0,0 +1,309 @@
// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
const validator = {
"user_id": {
"rules": [
{
"format": "string"
}
],
"label": "用户ID"
},
"provider": {
"rules": [
{
"format": "string"
},
{
"range": [
{
"text": "微信支付",
"value": "wxpay"
},
{
"text": "支付宝",
"value": "alipay"
},
{
"text": "苹果应用内支付",
"value": "appleiap"
}
]
}
],
"label": "支付供应商"
},
"provider_pay_type": {
"rules": [
{
"format": "string"
}
],
"label": "支付方式"
},
"uni_platform": {
"rules": [
{
"format": "string"
}
],
"label": "应用平台"
},
"status": {
"rules": [
{
"format": "int"
},
{
"range": [
{
"text": "已关闭",
"value": -1
},
{
"text": "未支付",
"value": 0
},
{
"text": "已支付",
"value": 1
},
{
"text": "已部分退款",
"value": 2
},
{
"text": "已全额退款",
"value": 3
}
]
}
],
"defaultValue": 0,
"label": "订单状态"
},
"type": {
"rules": [
{
"format": "string"
}
],
"label": "订单类型"
},
"order_no": {
"rules": [
{
"format": "string"
},
{
"minLength": 20,
"maxLength": 28
}
],
"label": "业务系统订单号"
},
"out_trade_no": {
"rules": [
{
"format": "string"
}
],
"label": "支付插件订单号"
},
"transaction_id": {
"rules": [
{
"format": "string"
}
],
"label": "交易单号"
},
"device_id": {
"rules": [
{
"format": "string"
}
],
"label": "设备ID"
},
"client_ip": {
"rules": [
{
"format": "string"
}
],
"label": "客户端IP"
},
"openid": {
"rules": [
{
"format": "string"
}
],
"label": "openid"
},
"description": {
"rules": [
{
"format": "string"
}
],
"label": "支付描述"
},
"err_msg ": {
"rules": [
{
"format": "string"
}
],
"label": "支付失败原因"
},
"total_fee": {
"rules": [
{
"format": "int"
}
],
"label": "订单总金额"
},
"refund_fee": {
"rules": [
{
"format": "int"
}
],
"label": "订单总退款金额"
},
"refund_count": {
"rules": [
{
"format": "int"
}
],
"label": "当前退款笔数"
},
"refund_list": {
"rules": [
{
"format": "array"
}
],
"label": "退款详情"
},
"provider_appid": {
"rules": [
{
"format": "string"
}
],
"label": "开放平台appid"
},
"appid": {
"rules": [
{
"format": "string"
}
],
"label": "DCloud AppId"
},
"user_order_success": {
"rules": [
{
"format": "bool"
}
],
"label": "回调状态"
},
"pay_date": {
"rules": [
{
"format": "timestamp"
}
],
"label": "支付时间"
},
"notify_date": {
"rules": [
{
"format": "timestamp"
}
],
"label": "异步通知时间"
},
"cancel_date": {
"rules": [
{
"format": "timestamp"
}
],
"label": "取消时间"
}
}
const enumConverter = {
"provider_valuetotext": {
"wxpay": "微信支付",
"alipay": "支付宝",
"appleiap": "苹果应用内支付"
},
"status_valuetotext": {
"0": "未支付",
"1": "已支付",
"2": "已部分退款",
"3": "已全额退款",
"-1": "已关闭"
}
}
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)
where[field] = value
}
// if (typeof value === 'string' && value.length) {
// str += `(${field} == '${value}' || /${value}/.test(${field}))`
// 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 }

View File

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

View File

@ -0,0 +1,68 @@
// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
const validator = {
"title": {
"rules": [
{
"format": "string"
}
]
},
"path": {
"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 }

43
main.js Normal file
View File

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

92
manifest.json Normal file
View File

@ -0,0 +1,92 @@
{
"name" : "NGToolsAdmin",
"appid" : "__UNI__C7E7789",
"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,
"minified" : true,
"postcss" : true
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true,
"scopedSlotsCompiler" : "augmented",
"setting" : {
"urlCheck" : false,
"postcss" : true,
"minified" : true
}
},
"uniStatistics" : {
"enable" : false
},
"h5" : {
"template" : "template.h5.html",
"router" : {
"mode" : "hash",
"base" : "/admin/"
}
},
"vueVersion" : "2",
"networkTimeout" : {
"uploadFile" : 1200000 //ms
},
"fallbackLocale" : "zh-Hans",
"locale" : "zh-Hans"
}

View File

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

View File

@ -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
mock/uni-stat/db.js Normal file
View File

@ -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
mock/uni-stat/event.json Normal file
View File

@ -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"
}
]
}

View File

@ -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
mock/uni-stat/pageEnt.json Normal file
View File

@ -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
mock/uni-stat/pageRes.json Normal file
View File

@ -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
mock/uni-stat/pageRule.json Normal file
View File

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

View File

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

95
package.json Normal file
View File

@ -0,0 +1,95 @@
{
"name": "uni-admin 基础框架(原名 uniCloud admin",
"id": "uni-template-admin",
"displayName": "uni-admin 基础框架",
"version": "2.4.14",
"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",
"alipay": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "n"
},
"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"
}
}
}
}
}

508
pages.json Normal file
View File

@ -0,0 +1,508 @@
{
"pages": [ //pageshttps://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index"
},
{
"path": "pages/demo/icons/icons",
"style": {
"navigationBarTitleText": "图标"
}
},
{
"path": "pages/demo/table/table",
"style": {
"navigationBarTitleText": "表格"
}
},
{
"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": "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": "uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd",
"style": {
"enablePullDownRefresh": false,
"navigationBarTitleText": "设置密码"
}
}
,{
"path": "uni_modules/uni-id-pages/pages/userinfo/realname-verify/realname-verify",
"style": {
"enablePullDownRefresh": false,
"navigationBarTitleText": "实名认证"
}
}
],
"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/uni-stat",
"pages": [{
"path": "page-res/page-res",
"style": {
"navigationBarTitleText": "受访页",
"enablePullDownRefresh": false
}
},
{
"path": "page-ent/page-ent",
"style": {
"navigationBarTitleText": "入口页",
"enablePullDownRefresh": false
}
},
{
"path": "page-content/page-content",
"style": {
"navigationBarTitleText": "内容统计",
"enablePullDownRefresh": false
}
},
{
"path": "page-rule/page-rule",
"style": {
"navigationBarTitleText": "页面规则",
"enablePullDownRefresh": false
}
},
{
"path": "scene/scene",
"style": {
"navigationBarTitleText": "场景值(小程序)",
"enablePullDownRefresh": false
}
},
{
"path": "channel/channel",
"style": {
"navigationBarTitleText": "渠道app",
"enablePullDownRefresh": false
}
},
// #ifndef MP
{
"path": "error/js/js",
"style": {
"navigationBarTitleText": "js报错统计",
"enablePullDownRefresh": false
}
},
// #endif
{
"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
}
},
{
"path": "pay-order/overview/overview",
"style": {
"navigationBarTitleText": "订单概况",
"enablePullDownRefresh": false
}
},
{
"path": "pay-order/list/list",
"style": {
"navigationBarTitleText": "订单明细",
"enablePullDownRefresh": false
}
},
{
"path": "pay-order/funnel/funnel",
"style": {
"navigationBarTitleText": "漏斗分析",
"enablePullDownRefresh": false
}
},
{
"path": "pay-order/ranking/ranking",
"style": {
"navigationBarTitleText": "用户价值排行",
"enablePullDownRefresh": false
}
}
]
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "管理系统",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"h5": {
"titleNView": false
}
},
"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
}
}

149
pages/demo/icons/icons.vue Normal file
View File

@ -0,0 +1,149 @@
<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>
<!-- 在非H5环境下显示fix-window组件 -->
<!-- #ifndef H5 -->
<fix-window v-if="fixWindow" />
<!-- #endif -->
</view>
</template>
<script>
// "icons" './uni-icons.js'
import icons from './uni-icons.js'
//
export default {
//
data() {
return {
// icons
icons
}
},
//
props:{
// tag
tag: {
//
type: Boolean,
// true
default: true
},
// fixWindow
fixWindow: {
//
type: Boolean,
// true
default: true
}
},
//
methods: {
// setClipboardData type icon
setClipboardData(type, icon) {
// data 'uni-icons-' icon
let data = 'uni-icons-' + icon
// this.tag type 'tag'
if (this.tag && type === 'tag') {
// data class
data = '<view class="' + data + '"></view>'
}
// uni.setClipboardData
uni.setClipboardData({
// data
data,
//
success(res) {
// uni.showToast
uni.showToast({
// 'none'
icon: 'none',
// ' ' data ' '
title: '复制 ' + data + ' 成功!'
})
},
//
fail(res) {
// uni.showModal
uni.showModal({
// ' ' data ' '
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>

View File

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

177
pages/demo/table/table.vue Normal file
View File

@ -0,0 +1,177 @@
<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>
// "tableData" './tableData.js'
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) {
this.selectedIndexs = e.detail.index
},
//
delTable() {
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) => {
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>

View File

@ -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 弄"
}]

45
pages/error/404.vue Normal file
View File

@ -0,0 +1,45 @@
<template>
<view>
<view>
<!-- 显示404错误页面标题 -->
<text style="font-size: 25px;color: #333;">
404 页面未找到
</text>
</view>
<view>
<!-- 显示错误消息 -->
<text style="font-size: 18px;color: #999;">
{{errMsg}}
</text>
</view>
<!-- 在非H5环境下显示fix-window组件 -->
<!-- #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
pages/index/fieldsMap.js Normal file
View File

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

424
pages/index/index.vue Normal file
View File

@ -0,0 +1,424 @@
<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">
<!-- 提示条1初始化db_init.json -->
<uni-notice-bar v-if="showdbInit" showGetMore showIcon class="mb-m pointer" text="检测到您未初始化db_init.json请先右键uniCloud/database/db_init.json文件执行初始化云数据库否则左侧无法显示菜单等数据" background-color="#fef0f0" color="#f56c6c" @click="toAddAppId" />
<!-- 提示条2添加应用 -->
<uni-notice-bar v-if="showAddAppId" showGetMore showIcon class="mb-m pointer" text="检测到您还未添加应用,点击前往应用管理添加" @click="toAddAppId" />
<!-- 提示条3暂无数据需开通统计功能 -->
<uni-notice-bar v-if="!deviceTableData.length && !userTableData.length && !query.platform_id && complete" 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>
<view class="uni-stat--x p-m">
<view class="uni-stat-card-header">设备概览</view>
<!-- 设备概览表格 -->
<uni-table :loading="loading" border stripe emptyText="暂无数据">
<uni-tr>
<block v-for="(mapper, index) in deviceTableFields" :key="index">
<!-- 表头列 -->
<uni-th v-if="mapper.title" :key="index" align="center">
{{mapper.title}}
</uni-th>
</block>
</uni-tr>
<uni-tr v-for="(item ,i) in deviceTableData" :key="i">
<block v-for="(mapper, index) in deviceTableFields" :key="index">
<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>
</block>
</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>
<block v-for="(mapper, index) in userTableFields" :key="index">
<uni-th v-if="mapper.title" :key="index" align="center">
{{mapper.title}}
</uni-th>
</block>
</uni-tr>
<uni-tr v-for="(item ,i) in userTableData" :key="i">
<block v-for="(mapper, index) in userTableFields" :key="index">
<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>
</block>
</uni-tr>
</uni-table>
</view>
</view>
<!-- #ifndef H5 -->
<fix-window />
<!-- #endif -->
</view>
</template>
<script>
import {
stringifyQuery,
stringifyField,
stringifyGroupField,
getTimeOfSomeDayAgo,
division,
format,
parseDateTime,
getFieldTotal,
debounce
} 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,
complete: false,
statSetting: {
mode: "",
day: 7
},
statModeList: [
{ "value": "open", "text": "开启" },
{ "value": "close", "text": "关闭" },
{ "value": "auto", "text": "节能" },
],
showAddAppId: false,
showdbInit: false
}
},
onReady() {
// getAllData
this.debounceGet = debounce(() => {
this.getAllData(this.queryStr);
}, 300);
//
this.debounceGet();
// appId
this.checkAppId();
this.checkdbInit();
},
watch: {
query: {
deep: true,
handler(newVal) {
// query
this.debounceGet(this.queryStr);
}
}
},
computed: {
queryStr() {
//
const defQuery = `(dimension == "hour" || dimension == "day")`;
// query
return stringifyQuery(this.query) + ' && ' + defQuery;
},
deviceTableFields() {
//
return this.tableFieldsMap(deviceFeildsMap);
},
userTableFields() {
//
return this.tableFieldsMap(userFeildsMap);
}
},
methods: {
getAllData(queryStr) {
//
this.getApps(this.queryStr, deviceFeildsMap, 'device');
//
this.getApps(this.queryStr, userFeildsMap, 'user');
},
tableFieldsMap(fieldsMap) {
let tableFields = [];
const today = [];
const yesterday = [];
const other = [];
for (const mapper of fieldsMap) {
if (mapper.field) {
if (mapper.hasOwnProperty('value')) {
// '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
tableFields.push(mapper);
}
}
}
//
tableFields = [...tableFields, ...today, ...yesterday, ...other];
return tableFields;
},
getApps(query, fieldsMap, type = "device") {
this.loading = true
const db = uniCloud.database()
const appDaily = db.collection('uni-stat-result').where(query).getTemp();
const appList = db.collection('opendb-app-list').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
//console.log('data: ', data)
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)
}
}
if (appid) {
rowData[`total_${type}s_value`] = "获取中...";
}
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;
this.complete = true;
})
},
navTo(url, id) {
if (url.indexOf('http') > -1) {
// url'http'
window.open(url);
} else {
if (id) {
// idurl
url = `${url}?appid=${id}`;
}
// 使uni.navigateTo
uni.navigateTo({
url
});
}
},
toUrl(url) {
// #ifdef H5
// urlH5
window.open(url, "_blank");
// #endif
},
toAddAppId() {
// App ID
this.showAddAppId = false;
// 使uni.navigateTo
uni.navigateTo({
url: "/pages/system/app/list",
events: {
//
refreshData: () => {
this.checkAppId();
}
}
});
},
async checkAppId() {
// uniCloud
const db = uniCloud.database();
// 'opendb-app-list'
let res = await db.collection('opendb-app-list').count();
// total0App ID
this.showAddAppId = (!res.result || res.result.total === 0) ? true : false;
},
async checkdbInit(){
// uniCloud
const db = uniCloud.database();
// 'opendb-app-list'
let res = await db.collection('opendb-admin-menus').count();
// total0App ID
this.showdbInit = (!res.result || res.result.total === 0) ? true : false;
if (this.showdbInit) {
uni.showModal({
title: "重要提示",
content: `检测到您未初始化数据库请先右键uni-admin项目根目下的 uniCloud/database 目录,执行初始化云数据库,否则左侧无法显示菜单等数据`,
showCancel: false,
confirmText: "我知道了"
});
}
}
}
}
</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;
}
.uni-stat-text {
color: #606266;
}
.mt10 {
margin-top: 10px;
}
.uni-radio-cell {
margin: 0 10px;
}
.uni-stat-tooltip-s {
width: 400px;
white-space: normal;
}
.uni-a {
cursor: pointer;
text-decoration: underline;
color: #555;
font-size: 14px;
}
</style>

1
pages/index/test.ts Normal file
View File

@ -0,0 +1 @@
var testValue = 3 > 2 ? true : false;

522
pages/system/app/add.vue Normal file
View File

@ -0,0 +1,522 @@
<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" :provider="uniFilePickerProvider"
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">
<radio-group @change="e => this.uniFilePickerProvider = e.detail.value">
<view class="flex" style="flex-wrap: nowrap;">
上传至
<label>
<radio value="unicloud" checked/><text>内置存储</text>
</label>
<label style="margin-left: 20rpx;">
<radio value="extStorage" /><text>扩展存储</text>
</label>
</view>
</radio-group>
<button type="primary" size="mini" @click="selectFile" style="margin: 0 0 0 20rpx;">选择文件</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>
<uni-forms-item v-if="item === 'app_ios'" label="获取 ABM 应用登录链接">
<uni-easyinput :maxlength="-1" v-model="formData[item].abm_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
oppooppomarket://details?packagename=com.xx.xx
vivovivomarket://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 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>
// mixin
import mixin from './mixin/publish_add_detail_mixin.js';
//
const db = uniCloud.database();
//
const dbCmd = db.command;
//
const dbCollectionName = 'opendb-app-list';
/**
* 生成指定长度的随机字符串
* @param {number} len - 随机字符串的长度
* @returns {string} - 生成的随机字符串
*/
function randomString(len) {
//
let 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'
];
let result = '';
for (let i = 0; i < len; i++) {
result += array[Math.floor(Math.random() * 26)];
}
return result;
}
export default {
// mixins mixin
mixins: [mixin],
//
data() {
return {
//
mpExtra: ' ',
// 1
mpAccordionStatus: 1,
// '80px'
labelWidth: '80px',
uniFilePickerProvider: 'unicloud'
}
},
/**
* 页面加载时的处理函数
* @param {object} e - 传入的参数对象
*/
onLoad(e) {
if (e.id) {
//
this.isEdit = true;
// ''
uni.setNavigationBarTitle({
title: '修改应用'
});
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) {
(
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>

292
pages/system/app/list.vue Normal file
View File

@ -0,0 +1,292 @@
<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 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 管理后台"
})
} 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() {
let 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>

View File

@ -0,0 +1,268 @@
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].abm_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]
})
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
},
}
}

View File

@ -0,0 +1,160 @@
<template>
<view class="uni-container">
<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>
const download = function(content, filename) {
let eleLink = document.createElement('a');
eleLink.download = filename;
eleLink.style.display = 'none';
let 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
pages/system/menu/add.vue Normal file
View File

@ -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必须是/开头若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
pages/system/menu/edit.vue Normal file
View File

@ -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必须是/开头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>

495
pages/system/menu/list.vue Normal file
View File

@ -0,0 +1,495 @@
<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>
<button @click="updateBuiltInMenu" size="mini" plain="true" style="margin-left: 10px;"
type="warn">{{ $t('menu.button.updateBuiltInMenu') }}
</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}">
<switch :checked="item.enable" @change="enableChange(item)" />
<!-- {{ 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'
import originalMenuList from './originalMenuList.json'
const db = uniCloud.database()
//
const dbOrderBy = 'create_date asc'
//
const pageSize = 20000
const pageCurrent = 1
// admin
const pluginMenuJsons = []
if (process.env.NODE_ENV === 'development') {
// #ifdef VUE2
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
// #ifdef VUE3
const rootModules = import.meta.glob('../../../uni_modules/*/*-menu.json', {eager: true});
for (const modulePath in rootModules) {
const json = modulePath.replace(/^..\/..\/..\//, '');
let moduleItem = rootModules[modulePath];
if (typeof moduleItem === "function") {
// HBX3.6.5
moduleItem().then(module => {
module = module.default ? module.default : module
module.forEach(item => {
item.json = json
pluginMenuJsons.push(item)
});
})
} else {
// HBX3.6.13
let module = moduleItem.default ? moduleItem.default : moduleItem;
module.forEach(item => {
item.json = json
pluginMenuJsons.push(item)
});
}
}
const pluginModules = import.meta.glob('../../../uni_modules/**/menu.json', {eager: true});
for (const modulePath in pluginModules) {
const json = modulePath.replace(/^..\/..\/..\//, '');
let moduleItem = pluginModules[modulePath];
if (typeof moduleItem === "function") {
// HBX3.6.5
moduleItem().then(module => {
module = module.default ? module.default : module
module.forEach(item => {
item.json = json
pluginMenuJsons.push(item)
})
})
} else {
// HBX3.6.13
let module = moduleItem.default ? moduleItem.default : moduleItem;
module.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: {
enableChange(item){
item.enable = item.enable ? false : true;
db.collection("opendb-admin-menus").doc(item._id).update({
enable: item.enable
});
},
getSortMenu(menuList) {
//
menuList.map(item => {
if (!menuList.some(subMenuItem => subMenuItem.parent_id === item.menu_id)) {
item.isLeafNode = true
}
})
return buildMenus(menuList)
},
onqueryload(data) {
for (let 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))
dbMenu.enable = true;
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()
})
}
})
},
//
async updateBuiltInMenu(){
uni.showModal({
title: '提示',
content: '确定更新内置菜单吗?\n该操作不会影响现有的菜单',
success: async (res) => {
if (res.confirm) {
const db = uniCloud.database();
const _ = db.command;
let menu_ids = originalMenuList.map((item, index) => {
return item.menu_id;
});
uni.showLoading({
title:"更新中...",
mask:true
});
try {
let addMenuList = [];
//
let oldMenuListRes = await db.collection("opendb-admin-menus").where({
menu_id: _.in[menu_ids]
}).limit(500).get();
let oldMenuList = oldMenuListRes.result.data;
originalMenuList.map((item, index) => {
let oldMenuItem = oldMenuList.find((item2, index2, arr2) => {
return item2.menu_id === item.menu_id;
});
if (!oldMenuItem) {
addMenuList.push({
...item,
create_date: undefined
});
}
});
if (addMenuList && addMenuList.length > 0) {
//
let addRes = await db.collection("opendb-admin-menus").add(addMenuList);
uni.showToast({
title:`新增了${addRes.result.inserted}个菜单,即将刷新`,
icon:"none"
})
setTimeout(() => {
// #ifdef H5
window.location.reload();
// #endif
// #ifndef H5
this.loadData(true);
// #endif
}, 300);
} else {
uni.showToast({
title:"菜单无变动",
icon:"none"
})
}
} catch(err) {
console.error(err)
} 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>

View File

@ -0,0 +1,446 @@
[{
"menu_id": "index",
"name": "首页",
"icon": "uni-icons-home",
"url": "/",
"sort": 100,
"parent_id": "",
"permission": [],
"enable": true,
"create_date": 1602662469396
}, {
"menu_id": "system_management",
"name": "系统管理",
"icon": "admin-icons-fl-xitong",
"url": "",
"sort": 1000,
"parent_id": "",
"permission": [],
"enable": true,
"create_date": 1602662469396
}, {
"menu_id": "system_user",
"name": "用户管理",
"icon": "admin-icons-manager-user",
"url": "/pages/system/user/list",
"sort": 1010,
"parent_id": "system_management",
"permission": [],
"enable": true,
"create_date": 1602662469398
}, {
"menu_id": "system_role",
"name": "角色管理",
"icon": "admin-icons-manager-role",
"url": "/pages/system/role/list",
"sort": 1020,
"parent_id": "system_management",
"permission": [],
"enable": true,
"create_date": 1602662469397
}, {
"menu_id": "system_permission",
"name": "权限管理",
"icon": "admin-icons-manager-permission",
"url": "/pages/system/permission/list",
"sort": 1030,
"parent_id": "system_management",
"permission": [],
"enable": true,
"create_date": 1602662469396
}, {
"menu_id": "system_menu",
"name": "菜单管理",
"icon": "admin-icons-manager-menu",
"url": "/pages/system/menu/list",
"sort": 1040,
"parent_id": "system_management",
"permission": [],
"enable": true,
"create_date": 1602662469396
}, {
"menu_id": "system_app",
"name": "应用管理",
"icon": "admin-icons-manager-app",
"url": "/pages/system/app/list",
"sort": 1035,
"parent_id": "system_management",
"permission": [],
"enable": true,
"create_date": 1602662469399
}, {
"menu_id": "system_update",
"name": "App升级中心",
"icon": "uni-icons-cloud-upload",
"url": "/uni_modules/uni-upgrade-center/pages/version/list",
"sort": 1036,
"parent_id": "system_management",
"permission": [],
"enable": true,
"create_date": 1656491532434
}, {
"menu_id": "system_tag",
"name": "标签管理",
"icon": "admin-icons-manager-tag",
"url": "/pages/system/tag/list",
"sort": 1037,
"parent_id": "system_management",
"permission": [],
"enable": true,
"create_date": 1602662479389
}, {
"permission": [],
"enable": true,
"menu_id": "safety_statistics",
"name": "安全审计",
"icon": "admin-icons-safety",
"url": "",
"sort": 3100,
"parent_id": "",
"create_date": 1638356430871
}, {
"permission": [],
"enable": true,
"menu_id": "safety_statistics_user_log",
"name": "用户日志",
"icon": "",
"url": "/pages/system/safety/list",
"sort": 3101,
"parent_id": "safety_statistics",
"create_date": 1638356430871
}, {
"permission": [],
"enable": true,
"menu_id": "uni-stat",
"name": "uni 统计",
"icon": "admin-icons-tongji",
"url": "",
"sort": 2100,
"parent_id": "",
"create_date": 1638356430871
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device",
"name": "设备统计",
"icon": "admin-icons-shebeitongji",
"url": "",
"sort": 2120,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-overview",
"name": "概况",
"icon": "",
"url": "/pages/uni-stat/device/overview/overview",
"sort": 2121,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-activity",
"name": "活跃度",
"icon": "",
"url": "/pages/uni-stat/device/activity/activity",
"sort": 2122,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-trend",
"name": "趋势分析",
"icon": "",
"url": "/pages/uni-stat/device/trend/trend",
"sort": 2123,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-retention",
"name": "留存",
"icon": "",
"url": "/pages/uni-stat/device/retention/retention",
"sort": 2124,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-comparison",
"name": "平台对比",
"icon": "",
"url": "/pages/uni-stat/device/comparison/comparison",
"sort": 2125,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-stickiness",
"name": "粘性",
"icon": "",
"url": "/pages/uni-stat/device/stickiness/stickiness",
"sort": 2126,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user",
"name": "注册用户统计",
"icon": "admin-icons-yonghutongji",
"url": "",
"sort": 2122,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-overview",
"name": "概况",
"icon": "",
"url": "/pages/uni-stat/user/overview/overview",
"sort": 2121,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-activity",
"name": "活跃度",
"icon": "",
"url": "/pages/uni-stat/user/activity/activity",
"sort": 2122,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"icon": "",
"menu_id": "uni-stat-user-trend",
"name": "趋势分析",
"url": "/pages/uni-stat/user/trend/trend",
"sort": 2123,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-retention",
"name": "留存",
"icon": "",
"url": "/pages/uni-stat/user/retention/retention",
"sort": 2124,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-comparison",
"name": "平台对比",
"icon": "",
"url": "/pages/uni-stat/user/comparison/comparison",
"sort": 2125,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-stickiness",
"name": "粘性",
"icon": "",
"url": "/pages/uni-stat/user/stickiness/stickiness",
"sort": 2126,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-analysis",
"name": "页面统计",
"icon": "admin-icons-page-ent",
"url": "",
"sort": 2123,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-page-analysis",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-res",
"name": "受访页",
"icon": "",
"url": "/pages/uni-stat/page-res/page-res",
"sort": 2131,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-page-analysis",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-ent",
"name": "入口页",
"icon": "",
"url": "/pages/uni-stat/page-ent/page-ent",
"sort": 2132,
"create_date": 1638356902516
},{
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-content-analysis",
"name": "内容统计",
"icon": "admin-icons-doc",
"url": "",
"sort": 2140,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-page-content-analysis",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-content",
"name": "内容统计",
"icon": "",
"url": "/pages/uni-stat/page-content/page-content",
"sort": 2141,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-page-content-analysis",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-rule",
"name": "页面规则",
"icon": "",
"url": "/pages/uni-stat/page-rule/page-rule",
"sort": 2142,
"create_date": 1638356902516
},{
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-senceChannel",
"name": "渠道/场景值分析",
"icon": "admin-icons-qudaofenxi",
"url": "",
"sort": 2150,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-senceChannel",
"permission": [],
"enable": true,
"menu_id": "uni-stat-senceChannel-scene",
"name": "场景值(小程序)",
"icon": "",
"url": "/pages/uni-stat/scene/scene",
"sort": 2151,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-senceChannel",
"permission": [],
"enable": true,
"menu_id": "uni-stat-senceChannel-channel",
"name": "渠道app",
"icon": "",
"url": "/pages/uni-stat/channel/channel",
"sort": 2152,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-event-event",
"name": "自定义事件",
"icon": "admin-icons-shijianfenxi",
"url": "/pages/uni-stat/event/event",
"sort": 2160,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-error",
"name": "错误统计",
"icon": "admin-icons-cuowutongji",
"url": "",
"sort": 2170,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-error",
"permission": [],
"enable": true,
"menu_id": "uni-stat-error-js",
"name": "js报错",
"icon": "",
"url": "/pages/uni-stat/error/js/js",
"sort": 2171,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-error",
"permission": [],
"enable": true,
"menu_id": "uni-stat-error-app",
"name": "app崩溃",
"icon": "",
"url": "/pages/uni-stat/error/app/app",
"sort": 2172,
"create_date": 1638356902516
},
{
"menu_id": "uni-stat-pay",
"name": "支付统计",
"icon": "uni-icons-circle",
"url": "",
"sort": 2122,
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"create_date": 1667386977981
}, {
"menu_id": "uni-stat-pay-overview",
"name": "概况",
"icon": "",
"url": "/pages/uni-stat/pay-order/overview/overview",
"sort": 21221,
"parent_id": "uni-stat-pay",
"permission": [],
"enable": true,
"create_date": 1667387038602
},
{
"menu_id": "uni-stat-pay-funnel",
"name": "转换漏斗分析",
"icon": "",
"url": "/pages/uni-stat/pay-order/funnel/funnel",
"sort": 21222,
"parent_id": "uni-stat-pay",
"permission": [],
"enable": true,
"create_date": 1668430092890
},
{
"menu_id": "uni-stat-pay-ranking",
"name": "价值用户排行",
"icon": "",
"url": "/pages/uni-stat/pay-order/ranking/ranking",
"sort": 21223,
"parent_id": "uni-stat-pay",
"permission": [],
"enable": true,
"create_date": 1668430128302
},
{
"menu_id": "uni-stat-pay-order-list",
"name": "订单明细",
"icon": "",
"url": "/pages/uni-stat/pay-order/list/list",
"sort": 21224,
"parent_id": "uni-stat-pay",
"permission": [],
"enable": true,
"create_date": 1667387078947
}
]

View File

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

View File

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

View File

@ -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() {
let 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
pages/system/role/add.vue Normal file
View File

@ -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
pages/system/role/edit.vue Normal file
View File

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

237
pages/system/role/list.vue Normal file
View File

@ -0,0 +1,237 @@
<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="collectionList"
: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 {
collectionList: [ db.collection('uni-id-roles').field('comment,permission,role_id,role_name,create_date').getTemp(),db.collection('uni-id-permissions').field('permission_name, permission_id').getTemp() ],
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 (let 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() {
let 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>

View File

@ -0,0 +1,130 @@
<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" @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="collectionList" :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">内容</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.user_id[0] && item.user_id[0].nickname || '-'}}</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 dbOrderBy = 'create_date desc' //
const dbSearchFields = ["user_id.username","user_id.nickname","type", "ip"] //
//
const pageSize = 20
const pageCurrent = 1
export default {
data() {
return {
collectionList: [ db.collection('uni-id-log').field('type, ip, create_date, user_id').getTemp(),db.collection('uni-id-users').field('_id, username,nickname').getTemp() ],
query: '',
where: '',
orderby: dbOrderBy,
options: {
pageSize,
pageCurrent
}
}
},
methods: {
getWhere() {
const query = this.query.trim()
if (!query) {
return ''
}
let queryRe;
try {
queryRe = new RegExp(query, 'i')
} catch(err){
uni.showToast({
title:'请勿输入\等不满足正则格式的符号',
icon:"none"
})
return;
}
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() {
let 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
pages/system/tag/add.vue Normal file
View File

@ -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
pages/system/tag/edit.vue Normal file
View File

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

241
pages/system/tag/list.vue Normal file
View File

@ -0,0 +1,241 @@
<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>
<button class="uni-button" type="primary" size="mini" @click="$refs.batchSms.open()">群发短信</button>
</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 -->
<!-- #ifdef H5 -->
<batch-sms ref="batchSms" toType="userTags" :receiver="smsReceiver"></batch-sms>
<!-- #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()
},
computed: {
smsReceiver() {
if (this.selectedIndexs.length) {
let dataList = this.$refs.udb.dataList
return this.selectedIndexs.map(i => dataList[i].tagid)
}
}
},
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() {
let 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>

193
pages/system/user/add.vue Normal file
View File

@ -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": undefined,
"email": undefined,
"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>

342
pages/system/user/edit.vue Normal file
View File

@ -0,0 +1,342 @@
<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 v-if="showPassword" name="password" label="重置密码" key="password">
<uni-easyinput 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 v-else label="重置密码">
<span class="reset-password-btn" @click="trigger">点击重置密码</span>
</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="checkboxTags" :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="可登录应用">
<view class="uni-forms-item-flex-center-x">
<uni-data-checkbox :multiple="true" v-model="formData.authorizedApp" :localdata="appList"></uni-data-checkbox>
<span class="link-btn" @click="gotoAppList">管理</span>
</view>
<view v-if="formDataId === userId" class="uni-form-item-tips">当前有未添加的应用{{ unknownAppidsCom }}建议点击右侧管理进行添加</view>
</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" :disabled="formDataId === userId" />
<view v-else class="uni-form-item-empty">{{parseUserStatus(formData.status)}}</view>
<view v-if="formDataId === userId" class="uni-form-item-tips">请勿禁用当前登录的账号</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": undefined,
"role": [],
"tags": [],
"authorizedApp": [],
"mobile": undefined,
"email": undefined,
"status": false //
},
rules: {
...getValidator(["username", "password", "role", "mobile", "email"]),
"status": {
"rules": [{
"format": "bool"
}]
}
},
roles: [],
userId:"",
appList:[],
unknownAppids:[]
}
},
onLoad(e) {
const id = e.id
this.formDataId = id
let userInfo = uni.getStorageSync("uni-id-pages-userInfo") || {};
this.userId = userInfo._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.checkboxTags.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: '修改成功'
})
const eventChannel = this.getOpenerEventChannel();
if (eventChannel.emit) eventChannel.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);
this.loadAppList(this.formData.authorizedApp);
}
}).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
})
})
},
loadAppList(authorizedApp){
db.collection('opendb-app-list').limit(500).get().then(res => {
let list = res.result.data.map((item, index) => {
return {
value: item.appid,
text: item.name
}
});
if (!list) list = [];
authorizedApp.map((appid) => {
let info = list.find((item) => {
return item.value === appid;
});
if (!info) {
this.unknownAppids.push(appid);
list.push({
value: appid,
text: `未知应用${appid}`
});
}
});
this.appList = list;
}).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 if (status === 4) {
return '已注销'
} else if (typeof status !== "undefined") {
return '未知'
} else {
return '启用'
}
}
},
computed:{
unknownAppidsCom(){
let str = "";
this.unknownAppids.map((item, index) => {
str += item;
if (index !== this.unknownAppids.length-1){
str += "、";
}
});
return str;
}
}
}
</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;
}
.uni-forms-item-flex-center-x {
display: flex;
align-items: center;
flex-wrap: wrap;
}
</style>

462
pages/system/user/list.vue Normal file
View File

@ -0,0 +1,462 @@
<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">{{$t('common.button.tagManager')}}</button>
<!-- #ifdef H5 -->
<button class="uni-button" type="primary" size="mini" @click="$refs.batchSms.open()">{{$t('common.button.sendSMS')}}</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">{{$t('common.button.exportExcel')}}</button>
</download-excel>
<!-- #endif -->
</view>
</view>
<view class="uni-container">
<unicloud-db ref="udb" :collection="collectionList" :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">
<block v-for="(tag,tagIndex) in item.tags" :key="tagIndex">
<uni-tag type="primary" inverted size="small" :text="tag" v-if="item.tags" style="margin: 0 5px;"></uni-tag>
</block>
</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>
<!-- #ifdef H5 -->
<batch-sms ref="batchSms" toType="user" :receiver="smsReceiver" :condition="smsCondition"></batch-sms>
<!-- #endif -->
</view>
</template>
<script>
import {
enumConverter,
filterToWhere
} from '../../../js_sdk/validator/uni-id-users.js';
import UniForms from "@/uni_modules/uni-forms/components/uni-forms/uni-forms";
import UniFormsItem from "@/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item";
import UniEasyinput from "@/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput";
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 {
collectionList: [ db.collection('uni-id-users').field('ali_openid,apple_openid,avatar,avatar_file,comment,dcloud_appid,department_id,email,email_confirmed,gender,invite_time,inviter_uid,last_login_date,last_login_ip,mobile,mobile_confirmed,my_invite_code,nickname,role,score,status,username,wx_unionid,qq_unionid,tags').getTemp(),db.collection('uni-id-roles').field('role_id, role_name').getTemp() ],
query: '',
where: '',
orderby: dbOrderBy,
orderByFieldName: "",
selectedIndexs: [],
pageSizeIndex: 0,
pageSizeOption: [20, 50, 100, 500],
tags: {},
managerTags: [],
queryTagid: '',
queryUserId: '',
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',
smsCondition: {}
}
},
onLoad(e) {
this._filter = {}
const tagid = e.tagid
const userId = e.id
if (tagid) {
this.queryTagid = tagid
const options = {
filterType: "select",
filter: [tagid]
}
this.filterChange(options, "tags")
}
if (userId) {
this.queryUserId = userId
const options = {
filterType: "select",
filter: [userId]
}
this.filterChange(options, "_id")
}
},
onReady() {
this.loadTags()
if (!this.queryTagid && !this.queryUserId) {
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
},
smsReceiver() {
if (this.selectedIndexs.length) {
let dataList = this.$refs.udb.dataList
return this.selectedIndexs.map(i => dataList[i]._id)
} else {
return undefined;
}
}
},
methods: {
onqueryload(data) {
for (let 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')
console.log(
JSON.stringify(
db.command.or(
dbSearchFields.map(name => {
return {
[name]: queryRe
}
})
)
)
)
return db.command.or(
dbSearchFields.map(name => {
return {
[name]: queryRe
}
})
)
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() {
let 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 = ''
}
// uni-sms-co
if (Object.keys(this._filter).length) {
this.smsCondition = this._filter
} else {
this.smsCondition = {}
}
this.$nextTick(() => {
this.$refs.udb.loadData()
})
},
loadTags() {
db.collection('uni-id-tag').limit(500).get().then(res => {
res.result.data.map(item => {
this.$set(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>

View File

@ -0,0 +1,478 @@
<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 p-1015">
<view class="uni-stat--app-select">
<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" class="ml-m" field="_id as value, version as text, uni_platform as label, create_date as date" format="{label} - {text}" orderby="date desc" label="版本选择" v-model="query.version_id" />
</view>
<uni-stat-tabs label="平台选择" type="boldLine" mode="platform-channel" :all="false" v-model="query.platform_id" @change="changePlatform" />
</view>
<view class="uni-stat--x flex">
<uni-stat-tabs label="日期选择" :current="currentDateTab" mode="date" @change="changeTimeRange" />
<uni-datetime-picker type="datetimerange" :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 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" :errorMessage="errorMessage"/>
</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="errorMessage || $t('common.empty')">
<uni-tr>
<block v-for="(mapper, index) in fieldsMap.slice(0, fieldsMap.length-1)" :key="index">
<uni-th v-if="mapper.title" :key="index" align="center">
{{mapper.title}}
</uni-th>
</block>
</uni-tr>
<uni-tr v-for="(item ,i) in tableData" :key="i">
<block v-for="(mapper, index) in fieldsMap.slice(0, fieldsMap.length-1)" :key="index">
<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>
</block>
</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: '',
errorMessage: ""
}
},
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(this.queryStr);
}, 300);
},
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
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) {
if (!this.query.appid){
this.errorMessage = "请先选择应用";
return;
}
this.errorMessage = "";
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 = []
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
}
}
}
})
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()
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'])
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>

View File

@ -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,
}]

View File

@ -0,0 +1,442 @@
<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 p-1015">
<view class="uni-stat--app-select">
<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" class="ml-m" field="_id as value, version as text, uni_platform as label, create_date as date" format="{label} - {text}" orderby="date desc" label="版本选择" v-model="query.version_id" />
</view>
</view>
<view class="uni-stat--x flex">
<uni-stat-tabs label="日期选择" :current="currentDateTab" mode="date" :yesterday="false" @change="changeTimeRange" />
<uni-datetime-picker type="datetimerange" :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 class="uni-stat--x">
<uni-stat-tabs label="平台选择" type="boldLine" mode="platform" v-model="query.platform_id" @change="changePlatform" />
<uni-data-select ref="version-select" v-if="query.platform_id && query.platform_id.indexOf('==') === -1" collection="uni-stat-app-channels" :where="channelQuery" class="p-channel" field="_id as value, channel_name as text" orderby="text asc" label="渠道/场景值选择" v-model="query.channel_id" />
</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 :errorMessage="errorMessage"/>
</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: '日活',
errorMessage: "",
}
},
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);
}, 300);
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
},
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) {
if (!query.appid) {
this.errorMessage = "请先选择应用";
return; // appid
}
this.errorMessage = "";
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'])
console.log('query: ', query)
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(queryData, field = 'active_device_count') {
const {
pageCurrent
} = this.options
let query = stringifyQuery(queryData)
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 = []
//
// stringifyQuery(queryData)
let query = JSON.parse(JSON.stringify(queryData))
query.dimension = 'week'
this.getRangeCountData(stringifyQuery(query), 'week').then(res => {
const {
count,
data
} = res.result
weeks = data
// queryData.dimension = 'month'
let query = JSON.parse(JSON.stringify(queryData))
query.dimension = 'month'
this.getRangeCountData(stringifyQuery(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
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
// Calculate full weeks to nearest Thursday
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
},
//
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>

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