提交 70141bb2 编写于 作者: M MicroMilo

admin uniapp 创建“

上级 5f5f04d7
{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"h5" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}
<script>
import {
mapActions,
mapMutations
} from 'vuex'
import config from '@/admin.config.js'
import {
version
} from './package.json'
import { uniAdminCacheKey } from './store/constants.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-app官方团队诚邀优秀前端工程师加盟,一起打造更卓越的uni-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')
},
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>
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.
## 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>
## 官方教程
> [bilibili 教程](https://www.bilibili.com/video/BV17p4y1a71x)
\ No newline at end of file
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: {
}
}
## 2.3.6(2023-04-10)
- 优化 支付统计-价值用户排行:只统计已支付的订单金额,且去除退款金额。
## 2.3.5(2023-02-24)
- 修复 升级中心安卓应用商店不显示的Bug
## 2.3.4(2023-02-09)
- 重要 阿里云空间支持上传sourceMap用以分析js错误统计 [详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#sourcemap-parse-error)
## 2.3.3(2023-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.2(2023-01-30)
- 修复 禁用的菜单仍然会在左侧菜单列表中显示的bug
## 2.3.1(2023-01-29)
- 短信群发功能 新增 筛选用户后可以跨分页群发
## 2.3.0(2023-01-16)
- 重要 新增uni-starter需要的相关依赖和初始化数据(方便uni-starter关联uni-admin后可直接运行)
- 升级 uni-id-pages 至 1.0.40
- 修复 非H5环境时,点击跳首页会报错的问题。
- 修复 charts 更新后,vue3模式下无法显示的bug
- 修复 用户管理-编辑时,新增标签后返回报错的问题
- 修复 用户管理-编辑时,若用户拥有的应用未添加到应用管理时,点击保存会导致用户丢失该应用的appid,进而导致下次登录提示未在该应用注册的问题。
- 修复 用户管理-编辑时,无法将已禁用的用户恢复成正常状态的问题。
- 修复 用户管理-编辑时,无法将手机号和邮箱清空的问题。
- 优化 用户管理-编辑时,禁止将当前登录的admin账户禁用(防止误操作导致无法登录admin)
- 优化 统计报表中的版本选择组件显示的内容,以便更好的区分平台和版本号
- 优化 新增用户时的表单验证提示
- 优化 当没有创建任何应用时,首页会友好提示请先创建应用。
## 2.2.3(2022-12-30)
- 修复 uni统计js报错页面无法正常显示数据的问题 [详情](https://ask.dcloud.net.cn/question/160337)
- 修复 一键部署因database目录有多余的db_init.json 导致部署失败的问题。
- 优化 uni统计前端页面,减少不必要的请求次数。
## 2.2.2(2022-12-20)
- 修复 升级中心删除安装包时报错的Bug [详情](https://ask.dcloud.net.cn/question/159918)
## 2.2.1(2022-12-13)
- 修复 因HBX升级3.6.13导致菜单管理加载失败的问题
- 优化 微信小程序报很多警告的问题
## 2.2.0(2022-12-12)
- 新增 uni统计新增支付统计 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-pay.html#pay-stat)
- 优化 uni统计UI排版细节
- 修复 国际化繁体中文 新增一級菜單 文案错误问题
## 2.1.9(2022-12-06)
- 升级 uni-id-pages 至 1.0.35
- 优化 去除非必要的日志打印
- 优化 用户管理、角色管理、日志管理使用getTemp连表,提升查询性能
- 优化 添加用户时手机号、邮箱选填
- 优化 添加用户时,昵称允许是中文
- 修复 运行时提示表不存在的问题
## 2.1.8(2022-12-01)
- 修复 uni-stat-receiver 无法找到 uni-id 模块的bug
## 2.1.7(2022-11-30)
- 新增 换肤功能
## 2.1.6(2022-11-28)
- 优化 群发短信功能的 schema 命名规范
## 2.1.5(2022-11-17)
- 升级 uni-id-pages 至 1.0.31
- 优化 添加用户时手机号、邮箱必填
## 2.1.4(2022-11-11)
- 修复 Vue3微信小程序运行报错的bug
## 2.1.3(2022-11-03)
- 修复 微信小程序上运行时错误 `process is not defined`
## 2.1.2(2022-11-02)
- 修复 Vue3无法导入插件菜单
## 2.1.1(2022-10-17)
- 修复 uni统计 App-Android 平台部分统计数据不准确的Bug [详情](https://ask.dcloud.net.cn/article/40097)
- 修复 uni统计 周/月数据不准确的Bug
## 2.1.0(2022-10-14)
- 新增 群发短信功能 [详情](https://uniapp.dcloud.net.cn//uniCloud/admin.html#batch-sms)
- 修复 无法重置用户密码的bug
## 2.0.5(2022-09-28)
- 修复 导入插件时不显示“待添加菜单”bug
## 2.0.4(2022-09-23)
- 升级 uni-id-pages 至 1.0.22
## 2.0.3(2022-09-21)
- 修复 云函数请求无返回数据的bug
## 2.0.2(2022-09-20)
- 升级 uni-id-pages 至 1.0.18
- 优化 导航登录用户名的显示规则:用户昵称 > 用户名 > 手机号 > 邮箱
## 2.0.1(2022-09-19)
- 升级 uni-id-pages 至 1.0.17
- 修改 导航登录用户名的显示规则:优先显示用户昵称,其次显示用户名
- 增加 用户管理列表展示“用户昵称”字段
- 增加 创建用户支持添加“用户昵称”字段
## 2.0.0(2022-09-16)
- 升级 uni-id-pages 至 1.0.13
- 修复 应用中心修改应用无法修改的bug
## 1.10.1(2022-09-08)
- 修复 使用 uniIdRouter 时导致页面无法打开的Bug
## 1.10.0(2022-09-08)
- 升级 uni-id 至 4.0,移除 uni-id、uni-id-cf 插件,增加 uni-id-pages、uni-id-common 插件。[uni-id详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html)
## 1.9.8(2022-08-15)
- 修复 应用管理修改页面报错
## 1.9.7(2022-08-08)
- 改进 sourceMap 回溯源码功能使用方法,需要在 admin.config.js 中配置相关信息。[详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#upload-sourcemap)
- 修复 js报错统计报错的Bug
## 1.9.6(2022-08-02)
- 修复 vue3 打包报错的Bug
- 修复 升级中心发布 wgt 时原生 App 最低版本没有必填的Bug
- 修复 升级中心发布 wgt 时显示Android应用市场的Bug
## 1.9.5(2022-07-29)
- 修复 运行到微信小程序控制台报错的Bug
## 1.9.4(2022-07-28)
- 新增 uni-admin uni统计支持上传 sourceMap,报错可准确回溯源码 [详情](https://uniapp.dcloud.io/uni-stat-v2.html#sourcemap-parse-error)
## 1.9.3(2022-07-19)
- 优化 uni-admin 应用管理模块可管理App下载地址、小程序二维码等更多应用信息 [详情](https://uniapp.dcloud.io/uniCloud/admin.html#app-manager)
- 调整 uni-admin 内置 统一发布页(uni-portal)插件 [详情](https://uniapp.dcloud.io/uniCloud/admin.html#uni-portal)
- 调整 uni-admin 内置 App升级中心(uni-upgrade-center)插件,并支持多应用商店更新 [详情](https://uniapp.dcloud.io/uniCloud/admin.html#uni-upgrade-center)
- 升级前最好将旧版 uni-portal、uni-upgrade-center 插件备份并移出 uni_modules 目录
## 1.9.2(2022-07-11)
- 修复 留存统计跑批任务获取不到版本号的Bug
## 1.9.1(2022-07-06)
- 新增 opendb-device表,开通 uni-push2.0 与 uni统计2.0 自动上报 push_clientid 到 opendb-device表
## 1.9.0(2022-07-05)
- 【重要】uni-admin 优化 uni统计 版本记录复用uni升级中心的opendb-app-versions表,废弃uni-stat-app-versions表 [详情](https://uniapp.dcloud.net.cn/uni-stat-v2.html#upgrade)
- 新增 uni统计 app崩溃页面,补充崩溃率统计
- 修复 uni统计 js报错页面,错误率计算不准确的Bug
- 修复 uni统计 切换版本或者修改时间等操作后,趋势图状态显示不正确的Bug
- 修复 uni统计 部分页面首次进入时界面闪烁的问题
## 1.8.5(2022-06-29)
- 新增 支持 ios 安全区
## 1.8.4(2022-06-01)
- 新增 uni统计 可通过选择「应用版本」查询数据
- 新增 uni统计 原生 app 崩溃页各项功能
- 修复 uni统计 渠道页 table 表格最后一列空白的 bug
- 修复 uni统计 场景分析页趋势图有数据却显示为 0 的 bug
- 修复 系统设置权限只能加载 20 条的 bug
## 1.8.3(2022-05-19)
- 优化 「首页」逻辑调整,无 appid 时提示添加 app 记录,可跳转 app 管理的新增页
- 优化 移除登录时多余的 init 的逻辑,提升登录速度
- 优化 「页面统计」添加 「入口页」、「登录页」的提示文字
- 修复 从「首页」跳转「概况」时,url 的 query 丢失的 bug
## 1.8.2(2022-05-18)
- 优化 uni 统计的「统计首页」菜单移动到应用「首页」,添加了设备概览、注册用户概览
- 优化 uni 统计的「帮助」菜单移动到「文档与插件」
- 修复 路由改变后面包屑未响应的 bug
## 1.8.1(2022-05-17)
- 修复 去掉多余的 schema
## 1.8.0(2022-05-17)
**重要更新:**
- 新增 用户日志功能
- 新增 内置 uni 统计报表体系,开源、免费、可私有化部署,[了解更多](https://uniapp.dcloud.net.cn/uni-stat-v2.html#uni%E7%BB%9F%E8%AE%A1),具体功能如下
- 统计首页
- 设备统计
- 用户统计
- 页面统计
- 渠道/场景值分析
- 自定义事件
- 错误统计
## 1.7.13(2022-02-15)
- 修复 新增菜单页‘内置图标’在 vue3 平台不显示的 bug
- 修复 ‘新增一级菜单’ 按钮的文字错误
## 1.7.12(2022-01-26)
- 修复 uni-admin 的 'registerUser' 接口,注册用户含有多余字段 uid
## 1.7.11(2022-01-19)
- 修复 多个用户的用户名相同时,后注册的同名用户登录时提示“用户不存在”的 bug
- 修复 偶发的验证码输出正确却提示“验证码错误”的 bug
- 修复 刷新页面后验证码消的 bug
## 1.7.10(2021-12-20)
- 优化 支持 vue3 查找并注册的菜单(包括插件菜单)
## 1.7.9(2021-12-07)
- 新增 标签管理功能,可批量为用户添加或移除标签、通过标签过滤用户
## 1.7.8(2021-11-30)
- 修复 Android 平台切换语言闪退的 bug,该平台暂不支持切换语言
## 1.7.7(2021-11-29)
- 修复 uni-datetime-picker 国际化未默认英文的问题
- 修复 uni-datetime-picker 范围选择在表格列头中渲染相同月份的问题
## 1.7.6(2021-11-11)
- 优化 修改密码功能不再支持查看明文密码
- 修复 某些屏幕上,input 框中下划线 '_' 被隐藏的 bug
## 1.7.5(2021-10-08)
- 修复 用户管理与角色管理模糊搜索时关联的外键无法搜索的 bug
## 1.7.4(2021-09-30)
- 修复 topwindow 非 h5 端,key 使用表达式报错的 bug
- 优化 topwindow 中英文混排不对齐的问题
## 1.7.3(2021-09-27)
- 修复 vue3 上加载 PostCSS 插件失败的 bug
## 1.7.2(2021-09-17)
- 优化 取消菜单管理请求数据条数限制
- 优化 topwindow 菜单文字换行的问题
- 修复 左侧菜单栏刷新失去打开状态的 bug
## 1.7.1(2021-09-14)
- 修复 vue3 下 i18n 未定义的 bug
- 优化 抛出被 error.js 拦截的报错
## 1.7.0(2021-08-31)
- 新增 支持国际化 i18n
- 优化 验证码图片边框样式调整
## 1.6.2(2021-08-26)
- 修复 非 admin 角色的用户无权限访问菜单表,动态菜单不显示的 bug
> 更新后,需上传 opendb-admin-menus.schema.json
- 优化 list 页的表格样式
## 1.6.1(2021-08-16)
- 修复 uni-id-cf 中无用的node_modules造成的报错
- 修复 uni.css 中样式穿透造成的 uni-file-picker 不可见的 bug
## 1.6.0(2021-07-31)
**重要更新:**
- 新增 应用管理功能,管理用户可登录的应用(uni-id@3.3.1+ 支持)
- 新增 升级系统管理 list 页的表格功能,支持数据排序、筛选、搜索等功能
- 新增 同时适配 vue2 和 vue3(HBuilder X 3.2.0+ 支持 vue3)
- 修复 刷新页面时,左侧菜单丢失高亮状态的 bug
- 修复 修改密码失败的 bug
## 1.5.8(2021-07-12)
- 修复 侧边栏菜单查询数据条数一次不超过 20 条的 bug(限制是最大一次 500 条)
## 1.5.7(2021-07-02)
- 修复 菜单管理排序错误的 bug
- 优化 框架设定非 admin 不能创建用户, 用户可自定义
## 1.5.6(2021-06-28)
- 修复 left-window 在小程序上的编译错误
## 1.5.5(2021-06-21)
- 修复 角色管理删除功能失效的 bug
- 修复 权限管理删除功能失效的 bug
## 1.5.4(2021-06-21)
- 优化 云函数 uni-id-cf uni_module 化,更新更方便
## 1.5.3(2021-06-17)
- 优化 opendb-admin-menus.schema 读权限配置默认为 true
> 原因:侧边栏菜单管理功能使用了 clientDB, 默认全部读取,通过用户权限过滤
## 1.4.6(2021-05-27)
- 修复 未连接服务空间时登录页空白的 bug
## 1.4.5(2021-05-18)
- 新增 选择表格分页条数功能
- 修复 切换分页条数当前分页不是1时获取数据出错的 bug
## 1.4.4(2021-05-17)
- 优化 导出 Excel 功能的代码
- 优化 系统管理 list 页面样式
- 优化 文案调整
## 1.4.3(2021-05-14)
- PC 端支持表格导出数据为 Excel
## 1.4.2(2021-04-21)
- 更新 uni-id 3.1.0
- 增加对用户名、邮箱、密码字段的两端去空格
- 默认忽略用户名、邮箱的大小写 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=case-sensitive)
- 修复 customToken导出async方法报错的Bug
## 1.4.1(2021-04-16)
- 更新 uni-tabel 1.0.3
- 新增 根目录下 changelog.md
@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";
}
@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;
}
}
}
}
}
@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";
}
/* 全局公共样式 */
body,
html {
-webkit-user-select: auto;
user-select: auto;
font-size: 16px;
}
/* #ifdef H5 */
.uni-app--showleftwindow uni-main {
position: relative;
background-color: #f5f5f5;
}
.uni-mask + .uni-left-window,
.uni-mask + .uni-right-window {
position: fixed;
}
.uni-app--showleftwindow uni-page-head .uni-page-head {
color: #333 !important;
/* margin-right: 15px; */
}
uni-page-head .uni-btn-icon {
color: #333 !important;
}
.uni-app--showleftwindow
uni-page-head[uni-page-head-type="default"]
~ uni-page-wrapper {
height: auto;
padding-top: 44px;
}
.uni-app--showleftwindow uni-page-head ~ uni-page-wrapper uni-page-body {
/* padding-top: 44px; */
}
.uni-app--showleftwindow uni-page-wrapper {
position: absolute;
width: 100%;
top: 0;
bottom: 0;
padding: 15px;
overflow-y: auto;
box-sizing: border-box;
background-color: #f5f5f5;
}
.uni-app--showleftwindow uni-page-body {
width: 100%;
min-height: 100%;
box-sizing: border-box;
border-radius: 5px;
box-shadow: -1px -1px 5px 0 rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.uni-app--showleftwindow .uni-container .uni-forms {
padding: 15px;
max-width: 650px;
}
/* #endif */
/* #ifndef H5 */
.uni-nav-menu {
height: 100vh;
}
/* #endif */
.pointer {
cursor: pointer;
}
.uni-top-window {
z-index: 999;
overflow: visible;
}
.uni-tips {
font-size: 12px;
color: #666;
}
/* 容器 */
.uni-container {
padding: 15px;
box-sizing: border-box;
}
/* 标题栏 */
.uni-header {
padding: 0 15px;
display: flex;
min-height: 55px;
align-items: center;
justify-content: space-between;
border-bottom: 1px #f5f5f5 solid;
flex-wrap: wrap;
}
.uni-title {
margin-right: 10px;
font-size: 16px;
font-weight: 500;
color: #333;
}
.uni-sub-title {
margin-top: 3px;
font-size: 14px;
color: #999;
}
.uni-link {
color: #3a8ee6;
cursor: pointer;
text-decoration: underline;
}
.uni-group {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
word-break: keep-all;
}
/* 按钮样式 */
.uni-button-group {
margin-top: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.uni-button {
padding: 10px 20px;
font-size: 14px;
border-radius: 4px;
line-height: 1;
margin: 0;
box-sizing: border-box;
overflow: initial;
}
.uni-group .uni-button {
margin: 10px;
}
.uni-group .uni-search {
margin: 10px;
}
.uni-group > .uni-button:first-child {
margin-left: 0;
}
.uni-button:hover,
.uni-button:focus {
opacity: 0.9;
}
.uni-button:active {
opacity: 1;
}
.uni-button-full {
width: 100%;
}
/* 搜索框样式 */
.uni-search {
width: 268px;
height: 28px;
line-height: 28px;
font-size: 12px;
color: #606266;
padding: 0 10px;
border: 1px #dcdfe6 solid;
/* margin-right: 10px; */
border-radius: 3px;
}
/* 分页容器 */
.uni-pagination-box {
margin-top: 20px;
}
.uni-input-border,
.uni-textarea-border {
width: 100%;
font-size: 14px;
color: #666;
border: 1px #e5e5e5 solid;
border-radius: 5px;
box-sizing: border-box;
}
.uni-input-border {
padding: 0 10px;
height: 35px;
}
.uni-textarea-border {
padding: 10px;
height: 80px;
}
.uni-disabled {
background-color: #f5f7fa;
color: #c0c4cc;
}
.uni-icon-password-eye {
position: absolute;
right: 8px;
top: 6px;
font-family: uniicons;
font-size: 20px;
font-weight: normal;
font-style: normal;
width: 24px;
height: 24px;
line-height: 24px;
color: #999999;
}
.uni-eye-active {
color: #007aff;
}
.uni-tabs__header {
position: relative;
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.uni-tabs__nav-wrap {
overflow: hidden;
margin-bottom: -1px;
position: relative;
}
.uni-tabs__nav-scroll {
overflow: hidden;
}
.uni-tabs__nav {
position: relative;
white-space: nowrap;
}
.uni-tabs__item {
position: relative;
padding: 0 20px;
height: 40px;
box-sizing: border-box;
line-height: 40px;
display: inline-block;
list-style: none;
font-size: 14px;
font-weight: 500;
color: #909399;
margin-top: -1px;
margin-left: -1px;
border: 1px solid transparent;
cursor: pointer;
}
.uni-tabs__item.is-active {
color: $uni-color-primary;
background-color: #fff;
border-right-color: #dcdfe6;
border-left-color: #dcdfe6;
}
.uni-form-item-tips {
color: #999;
font-size: 12px;
margin-top: 10px;
/* position: absolute; */
/* top: 40px; */
}
.uni-form-item-empty {
color: #999;
min-height: 36px;
line-height: 36px;
}
::v-deep .uni-forms-item__label .label-text {
color: #606266 !important;
}
::v-deep .flex-center-x .uni-forms-item__content {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.link-btn {
line-height: 26px;
margin-top: 5px;
color: #007aff !important;
text-decoration: underline;
cursor: pointer;
}
/* button 重置样式 */
::v-deep button[size="mini"] {
line-height: 2.4;
font-size: 12px;
border-radius: 3px;
}
button {
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
box-sizing: border-box;
}
button[type="primary"] {
background-color: #409eff;
border-color: #409eff;
border-width: 0;
}
button[type="warn"] {
background-color: #f56c6c;
border-color: #f56c6c;
border-width: 0;
}
button[type="default"] {
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
box-sizing: border-box;
}
button[type="primary"][plain] {
border-color: #409eff;
color: #409eff;
}
button[type="warn"][plain] {
border-color: #f56c6c;
color: #f56c6c;
}
button[type="default"][plain] {
border-color: #dcdfe6;
color: #606266;
}
button[plain] {
border-color: #dcdfe6;
color: #606266;
}
button:after {
border-width: 0;
}
.uni-input-placeholder {
color: #999;
}
.uni-pagination-box {
display: flex;
align-items: center;
justify-content: center;
}
.select-picker {
margin-right: 20px;
}
.select-picker button {
margin-top: 5px;
line-height: 29px;
font-size: 14px;
}
.select-picker button text {
color: #999;
}
.select-picker-icon {
margin-left: 8px;
}
/* stat style start */
.m-m {
margin: 15px !important;
}
.mb-s {
margin-bottom: 5px;
}
.mb-m {
margin-bottom: 15px !important;
}
.mb-l {
margin-bottom: 30px !important;
}
.ml-s {
margin-left: 5px;
}
.ml-m {
margin-left: 15px !important;
}
.ml-l {
margin-left: 30px !important;
}
.p-m {
padding: 15px;
}
.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__actived {
/* outline: 1px solid #2979ff; */
}
.flex {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.label-text {
font-size: 14px;
font-weight: bold;
color: #555;
margin: auto 0;
margin-right: 5px;
}
.uni-stat-edit--x {
display: flex;
justify-content: space-between;
}
.uni-stat-edit--btn {
cursor: pointer;
}
.uni-stat-datetime-picker {
margin: 15px;
}
/* uni-popup modal start */
.modal {
/* width: 100%; */
max-width: calc(100vw - 200px);
min-width: 600px;
margin: 0 auto;
background-color: #ffffff;
}
.modal-header {
padding: 20px 0;
text-align: center;
border-bottom: 1px solid #eee;
}
.modal-footer {
padding: 20px;
display: flex;
justify-content: flex-end;
align-items: center;
/* border-top: 1px solid #eee; */
}
.modal-content {
padding: 15px;
height: 600px;
box-sizing: border-box;
}
/* uni-popup modal end */
.uni-stat-tooltip-s {
width: 160px;
white-space: normal;
}
/* #ifndef APP-NVUE */
@media screen and (max-width: 500px) {
.hide-on-phone {
display: none !important;
}
.uni-charts-box {
width: 100%;
height: 220px;
}
.uni-group .uni-search {
height: 32px;
line-height: 32px;
width: 100%;
margin: 20px 20px 10px 20px;
}
.uni-header {
padding-left: 0px;
padding-right: 0px;
border: unset;
}
.uni-group {
width: 100%;
}
.uni-stat-breadcrumb-on-phone {
padding: 0 20px !important;
border-bottom: 1px #f5f5f5 solid;
}
.flex {
width: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
}
}
@media screen and (min-width: 500px) {
.dispaly-grid {
display: grid;
/* grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); */
grid-template-columns: 1fr 1fr;
/* grid-template-rows: 1fr 1fr; */
column-gap: 15px;
}
.pc-flex-wrap {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.uni-stat-datetime-picker {
max-width: 350px;
}
::v-deep .uni-pagination-picker-show .uni-picker-container .uni-picker-custom {
width: 100px;
margin: 0 86px;
}
::v-deep .uni-pagination-picker-show .uni-picker-container .uni-picker-custom .uni-picker-select + div {
left: 50% !important;
}
}
/* #endif */
/* #ifdef H5 */
/* fix 弹出层被遮盖 */
::v-deep .uni-table-scroll {
min-height: calc(100vh - 237px);
box-sizing: border-box;
}
::v-deep .uni-table .tr-table--border {
border-left: 1px #ebeef5 solid;
}
/* #endif */
/* #ifdef H5 */
/* fix 弹出层被遮盖 */
::v-deep .uni-table-scroll {
min-height: calc(100vh - 237px);
box-sizing: border-box;
}
::v-deep .uni-table .tr-table--border {
border-left: 1px #ebeef5 solid;
}
/* #endif */
/* #ifndef H5 */
.fix-top-window {
margin-top: 85px;
}
/* #endif */
<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;
}
}
//.options {
// display: flex;
// align-content: center;
//}
.field {
// flex: 1;
}
.type {
width: 100px;
flex: none;
}
.value {
// width: 80px;
}
.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>
<template>
<div :id="idName" @click="generate">
<slot> Download {{ name }} </slot>
</div>
</template>
<script>
import download from "./download";
export default {
name: "downloadExcel",
props: {
// mime type [xls, csv]
type: {
type: String,
default: "xls",
},
// Json to download
data: {
type: Array,
required: false,
default: null,
},
// fields inside the Json Object that you want to export
// if no given, all the properties in the Json are exported
fields: {
type: Object,
default: () => null,
},
// this prop is used to fix the problem with other components that use the
// variable fields, like vee-validate. exportFields works exactly like fields
exportFields: {
type: Object,
default: () => null,
},
// Use as fallback when the row has no field values
defaultValue: {
type: String,
required: false,
default: "",
},
// Title(s) for the data, could be a string or an array of strings (multiple titles)
header: {
default: null,
},
// Footer(s) for the data, could be a string or an array of strings (multiple footers)
footer: {
default: null,
},
// filename to export
name: {
type: String,
default: "data.xls",
},
fetch: {
type: Function,
},
meta: {
type: Array,
default: () => [],
},
worksheet: {
type: String,
default: "Sheet1",
},
//event before generate was called
beforeGenerate: {
type: Function,
},
//event before download pops up
beforeFinish: {
type: Function,
},
// Determine if CSV Data should be escaped
escapeCsv: {
type: Boolean,
default: true,
},
// long number stringify
stringifyLongNum: {
type: Boolean,
default: false,
},
},
computed: {
// unique identifier
idName() {
var now = new Date().getTime();
return "export_" + now;
},
downloadFields() {
if (this.fields) return this.fields;
if (this.exportFields) return this.exportFields;
},
},
methods: {
async generate() {
if (typeof this.beforeGenerate === "function") {
await this.beforeGenerate();
}
let data = this.data;
if (typeof this.fetch === "function" || !data) data = await this.fetch();
if (!data || !data.length) {
return;
}
let json = this.getProcessedJson(data, this.downloadFields);
if (this.type === "html") {
// this is mainly for testing
return this.export(
this.jsonToXLS(json),
this.name.replace(".xls", ".html"),
"text/html"
);
} else if (this.type === "csv") {
return this.export(
this.jsonToCSV(json),
this.name.replace(".xls", ".csv"),
"application/csv"
);
}
return this.export(
this.jsonToXLS(json),
this.name,
"application/vnd.ms-excel"
);
},
/*
Use downloadjs to generate the download link
*/
export: async function (data, filename, mime) {
let blob = this.base64ToBlob(data, mime);
if (typeof this.beforeFinish === "function") await this.beforeFinish();
download(blob, filename, mime);
},
/*
jsonToXLS
---------------
Transform json data into an xml document with MS Excel format, sadly
it shows a prompt when it opens, that is a default behavior for
Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
*/
jsonToXLS(data) {
let xlsTemp =
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>';
let xlsData = "<thead>";
const colspan = Object.keys(data[0]).length;
let _self = this;
//Header
const header = this.header || this.$attrs.title;
if (header) {
xlsData += this.parseExtraData(
header,
'<tr><th colspan="' + colspan + '">${data}</th></tr>'
);
}
//Fields
xlsData += "<tr>";
for (let key in data[0]) {
xlsData += "<th>" + key + "</th>";
}
xlsData += "</tr>";
xlsData += "</thead>";
//Data
xlsData += "<tbody>";
data.map(function (item, index) {
xlsData += "<tr>";
for (let key in item) {
xlsData +=
"<td>" +
_self.preprocessLongNum(
_self.valueReformattedForMultilines(item[key])
) +
"</td>";
}
xlsData += "</tr>";
});
xlsData += "</tbody>";
//Footer
if (this.footer != null) {
xlsData += "<tfoot>";
xlsData += this.parseExtraData(
this.footer,
'<tr><td colspan="' + colspan + '">${data}</td></tr>'
);
xlsData += "</tfoot>";
}
return xlsTemp
.replace("${table}", xlsData)
.replace("${worksheet}", this.worksheet);
},
/*
jsonToCSV
---------------
Transform json data into an CSV file.
*/
jsonToCSV(data) {
let _self = this;
var csvData = [];
//Header
const header = this.header || this.$attrs.title;
if (header) {
csvData.push(this.parseExtraData(header, "${data}\r\n"));
}
//Fields
for (let key in data[0]) {
csvData.push(key);
csvData.push(",");
}
csvData.pop();
csvData.push("\r\n");
//Data
data.map(function (item) {
for (let key in item) {
let escapedCSV = item[key] + "";
// Escaped CSV data to string to avoid problems with numbers or other types of values
// this is controlled by the prop escapeCsv
if (_self.escapeCsv) {
escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string
if (escapedCSV.match(/[,"\n]/)) {
escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"';
}
}
csvData.push(escapedCSV);
csvData.push(",");
}
csvData.pop();
csvData.push("\r\n");
});
//Footer
if (this.footer != null) {
csvData.push(this.parseExtraData(this.footer, "${data}\r\n"));
}
return csvData.join("");
},
/*
getProcessedJson
---------------
Get only the data to export, if no fields are set return all the data
*/
getProcessedJson(data, header) {
let keys = this.getKeys(data, header);
let newData = [];
let _self = this;
data.map(function (item, index) {
let newItem = {};
for (let label in keys) {
let property = keys[label];
newItem[label] = _self.getValue(property, item);
}
newData.push(newItem);
});
return newData;
},
getKeys(data, header) {
if (header) {
return header;
}
let keys = {};
for (let key in data[0]) {
keys[key] = key;
}
return keys;
},
/*
parseExtraData
---------------
Parse title and footer attribute to the csv format
*/
parseExtraData(extraData, format) {
let parseData = "";
if (Array.isArray(extraData)) {
for (var i = 0; i < extraData.length; i++) {
if (extraData[i])
parseData += format.replace("${data}", extraData[i]);
}
} else {
parseData += format.replace("${data}", extraData);
}
return parseData;
},
getValue(key, item) {
const field = typeof key !== "object" ? key : key.field;
let indexes = typeof field !== "string" ? [] : field.split(".");
let value = this.defaultValue;
if (!field) value = item;
else if (indexes.length > 1)
value = this.getValueFromNestedItem(item, indexes);
else value = this.parseValue(item[field]);
if (key.hasOwnProperty("callback"))
value = this.getValueFromCallback(value, key.callback);
return value;
},
/*
convert values with newline \n characters into <br/>
*/
valueReformattedForMultilines(value) {
if (typeof value == "string") return value.replace(/\n/gi, "<br/>");
else return value;
},
preprocessLongNum(value) {
if (this.stringifyLongNum) {
if (String(value).startsWith("0x")) {
return value;
}
if (!isNaN(value) && value != "") {
if (value > 99999999999 || value < 0.0000000000001) {
return '="' + value + '"';
}
}
}
return value;
},
getValueFromNestedItem(item, indexes) {
let nestedItem = item;
for (let index of indexes) {
if (nestedItem) nestedItem = nestedItem[index];
}
return this.parseValue(nestedItem);
},
getValueFromCallback(item, callback) {
if (typeof callback !== "function") return this.defaultValue;
const value = callback(item);
return this.parseValue(value);
},
parseValue(value) {
return value || value === 0 || typeof value === "boolean"
? value
: this.defaultValue;
},
base64ToBlob(data, mime) {
let base64 = window.btoa(window.unescape(encodeURIComponent(data)));
let bstr = atob(base64);
let n = bstr.length;
let u8arr = new Uint8ClampedArray(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
},
}, // end methods
};
</script>
//download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
// v4 adds AMD/UMD, commonJS, and plain browser support
// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
// https://github.com/rndme/download
export default function download(data, strFileName, strMimeType) {
var self = window, // this script is only for browsers anyway...
defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
mimeType = strMimeType || defaultMime,
payload = data,
url = !strFileName && !strMimeType && payload,
anchor = document.createElement("a"),
toString = function(a){return String(a);},
myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
fileName = strFileName || "download",
blob,
reader;
myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
payload=[payload, mimeType];
mimeType=payload[0];
payload=payload[1];
}
if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
fileName = url.split("/").pop().split("?")[0];
anchor.href = url; // assign href prop to temp anchor
if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
var ajax=new XMLHttpRequest();
ajax.open( "GET", url, true);
ajax.responseType = 'blob';
ajax.onload= function(e){
download(e.target.response, fileName, defaultMime);
};
setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
return ajax;
} // end if valid url?
} // end if url?
//go ahead and download dataURLs right away
if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){
if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
payload=dataUrlToBlob(payload);
mimeType=payload.type || defaultMime;
}else{
return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
saver(payload) ; // everyone else can save dataURLs un-processed
}
}else{//not data url, is it a string with special needs?
if(/([\x80-\xff])/.test(payload)){
var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length;
for(i;i<mx;++i) tempUiArr[i]= payload.charCodeAt(i);
payload=new myBlob([tempUiArr], {type: mimeType});
}
}
blob = payload instanceof myBlob ?
payload :
new myBlob([payload], {type: mimeType}) ;
function dataUrlToBlob(strUrl) {
var parts= strUrl.split(/[:;,]/),
type= parts[1],
decoder= parts[2] == "base64" ? atob : decodeURIComponent,
binData= decoder( parts.pop() ),
mx= binData.length,
i= 0,
uiArr= new Uint8Array(mx);
for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);
return new myBlob([uiArr], {type: type});
}
function saver(url, winMode){
if ('download' in anchor) { //html5 A[download]
anchor.href = url;
anchor.setAttribute("download", fileName);
anchor.className = "download-js-link";
anchor.innerHTML = "downloading...";
anchor.style.display = "none";
document.body.appendChild(anchor);
setTimeout(function() {
anchor.click();
document.body.removeChild(anchor);
if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
}, 66);
return true;
}
// handle non-a[download] safari as best we can:
if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
if(/^data:/.test(url)) url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
if(!window.open(url)){ // popup blocked, offer direct download:
if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
}
return true;
}
//do iframe dataURL download (old ch+FF):
var f = document.createElement("iframe");
document.body.appendChild(f);
if(!winMode && /^data:/.test(url)){ // force a mime that will download:
url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
}
f.src=url;
setTimeout(function(){ document.body.removeChild(f); }, 333);
}//end saver
if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
return navigator.msSaveBlob(blob, fileName);
}
if(self.URL){ // simple fast and modern way using Blob and URL:
saver(self.URL.createObjectURL(blob), true);
}else{
// handle non-Blob()+non-URL browsers:
if(typeof blob === "string" || blob.constructor===toString ){
try{
return saver( "data:" + mimeType + ";base64," + self.btoa(blob) );
}catch(y){
return saver( "data:" + mimeType + "," + encodeURIComponent(blob) );
}
}
// Blob but not URL support:
reader=new FileReader();
reader.onload=function(e){
saver(this.result);
};
reader.readAsDataURL(blob);
}
return true;
}; /* end download() */
\ No newline at end of file
<template>
<view class="fix-window">
<top-window class="fix-window-top"/>
<view class="fix-window-button" @click="tiggerWindow"></view>
<view v-show="visible" class="fix-window--mask" @click="tiggerWindow"></view>
<left-window v-show="visible" class="fix-window--popup" />
</view>
</template>
<script>
import topWindow from '../../windows/topWindow.vue'
import leftWindow from '../../windows/leftWindow.vue'
export default {
components:{
topWindow,
leftWindow
},
data() {
return {
visible: false
};
},
methods: {
tiggerWindow() {
this.visible = !this.visible
}
}
}
</script>
<style>
.fix-window {
}
.fix-window-button {
width: 30px;
height: 30px;
opacity: 0.5;
position: fixed;
top: 40px;
left: 20px;
z-index: 999;
}
.fix-window-top {
width: 100%;
position: fixed;
top: 25px;
left: 0;
z-index: 999;
}
.fix-window--mask {
position: fixed;
bottom: 0px;
top: 25px;
left: 0px;
right: 0px;
background-color: rgba(0, 0, 0, 0.4);
transition-duration: 0.3s;
z-index: 997;
}
.fix-window--popup {
position: fixed;
top: 85px;
left: 0;
/* transform: translate(-50%, -50%); */
transition-duration: 0.3s;
z-index: 998;
}
</style>
<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>
<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>
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
}
<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>
<template>
<view class="uni-menu-item"
:class="{
'is-active':active,
'is-disabled':disabled
}"
:style="{
paddingLeft:paddingLeft,
'background-color':active?activeBackgroundColor:''
}"
@click="onClickItem">
<slot></slot>
</view>
</template>
<script>
import rootParent from '../uni-nav-menu/mixins/rootParent.js'
export default {
name: 'uniMenuItem',
mixins: [rootParent],
props: {
// 唯一标识
index: {
type: [String,Object],
default(){
return ''
}
},
// TODO 是否禁用
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
active: false,
activeTextColor: '#42B983',
textColor: '#303133',
activeBackgroundColor: ''
};
},
computed: {
paddingLeft() {
return 20 + 20 * this.rootMenu.SubMenu.length + 'px'
}
},
created() {
this.init()
},
destroyed() {
if (this.$menuParent) {
const menuIndex = this.$menuParent.itemChildrens.findIndex(item => item === this)
this.$menuParent.itemChildrens.splice(menuIndex, 1)
}
},
methods: {
init() {
this.rootMenu = {
NavMenu: [],
SubMenu: []
}
this.indexPath = []
// 获取直系的所有父元素实例
this.getParentAll('SubMenu', this)
// 获取最外层父元素实例
this.$menuParent = this.getParent('uniNavMenu', this)
this.$subMenu = this.rootMenu.SubMenu
this.activeTextColor = this.$menuParent.activeTextColor
this.textColor = this.$menuParent.textColor
this.activeBackgroundColor = this.$menuParent.activeBackgroundColor
// 将当前插入到menu数组中
if (this.$menuParent) {
this.$menuParent.itemChildrens.push(this)
this.$menuParent.isActive(this)
}
},
// 点击 menuItem
onClickItem(e) {
if (this.disabled) return
// 关闭其他已经选中的 itemMenu
this.$menuParent.closeOtherActive(this)
this.active = true
this.indexPath.unshift(this.index)
this.indexPath.reverse()
if(e !== 'init'){
// this.$menuParent.activeIndex=this.index
this.$menuParent.select(this.index, this.indexPath)
}
}
}
}
</script>
<style lang="scss">
.uni-menu-item {
display: flex;
align-items: center;
padding: 0 20px;
height: 56px;
line-height: 56px;
color: #303133;
transition: all 0.3s;
cursor: pointer;
// border-bottom: 1px #f5f5f5 solid;
}
.uni-menu-item:hover {
outline: none;
background-color: #EBEBEB;
transition: all 0.3s;
}
.is-active {
color: $uni-color-primary;
// background-color: #ecf8f3;
}
.is-disabled {
// background-color: #f5f5f5;
color: #999;
}
.uni-menu-item.is-disabled:hover {
background-color: inherit;
color: #999;
cursor: not-allowed;
}
</style>
<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>
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;
}
}
}
\ No newline at end of file
<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>
<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>
<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>
<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>
<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>
<template>
<view class="uni-sub-menu">
<view class="uni-sub-menu__title" :class="{'is-disabled':disabled}" :style="{paddingLeft:paddingLeft}" @click="select">
<view class="uni-sub-menu__title-sub" :style="{color:disabled?'#999':textColor}">
<slot name="title"></slot>
</view>
<uni-icons class="uni-sub-menu__icon" :class="{transition:isOpen}" type="arrowdown" color="#bbb" size="14"></uni-icons>
</view>
<view class="uni-sub-menu__content" :class="{'uni-sub-menu--close':!isOpen}" :style="{'background-color':backgroundColor}">
<view id="content--hook">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
import rootParent from '../uni-nav-menu/mixins/rootParent.js'
export default {
name: 'uniSubMenu',
mixins: [rootParent],
props: {
// 唯一标识
index: {
type: [String,Object],
default(){
return ''
}
},
// TODO 自定义类名
popperClass: {
type: String,
default: ''
},
// TODO 是否禁用
disabled: {
type: Boolean,
default: false
},
// 展开菜单的背景色
backgroundColor: {
type: String,
default: '#f5f5f5'
},
},
data() {
return {
height: 0,
oldheight: 0,
isOpen: false,
textColor:'#303133'
};
},
computed: {
paddingLeft() {
return 20 + 20 * this.rootMenu.SubMenu.length + 'px'
}
},
created() {
this.init()
},
destroyed() {
// 销毁页面后,将当前页面实例从数据中删除
if (this.$menuParent) {
const menuIndex = this.$menuParent.subChildrens.findIndex(item => item === this)
this.$menuParent.subChildrens.splice(menuIndex, 1)
}
},
methods: {
init() {
// 所有父元素
this.rootMenu = {
NavMenu: [],
SubMenu: []
}
this.childrens = []
this.indexPath = []
// 获取直系的所有父元素实例
this.getParentAll('SubMenu', this)
// 获取最外层父元素实例
this.$menuParent = this.getParent('uniNavMenu', this)
this.textColor = this.$menuParent.textColor
// 直系父元素 SubMenu
this.$subMenu = this.rootMenu.SubMenu
// 将当前插入到menu数组中
if(this.$menuParent){
this.$menuParent.subChildrens.push(this)
}
},
select() {
if(this.disabled) return
// 手动开关 sunMenu
this.$menuParent.selectMenu(this)
},
open() {
this.isOpen = true
},
close() {
this.isOpen = false
}
}
}
</script>
<style lang="scss">
.uni-sub-menu {
position: relative;
/* background-color: #FFFFFF; */
}
.uni-sub-menu__title {
display: flex;
align-items: center;
padding: 0 20px;
padding-right: 10px;
height: 56px;
line-height: 56px;
color: #303133;
cursor: pointer;
/* border-bottom: 1px #f5f5f5 solid; */
}
.uni-sub-menu__title:hover {
color: #42B983;
outline: none;
background-color: #EBEBEB;
}
.uni-sub-menu__title-sub {
display: flex;
align-items: center;
flex: 1;
}
.uni-sub-menu--close {
height: 0;
/* transition: all 0.3s; */
}
.uni-sub-menu__content {
overflow: hidden;
}
.uni-sub-menu__icon {
max-height: auto;
transition: all 0.2s;
}
.transition {
transform: rotate(-180deg);
}
.is-disabled {
/* background-color: #f5f5f5; */
color: red;
}
.uni-sub-menu__title.is-disabled:hover {
background-color: inherit;
color: #999;
cursor: not-allowed;
}
</style>
{
"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"
}
}
}
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
}
{
"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": "表格"
}
}
}
此差异已折叠。
<!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>
此差异已折叠。
此差异已折叠。
此差异已折叠。
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
})
}
}
})
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册