提交 76c9b054 编写于 作者: DCloud_JSON's avatar DCloud_JSON 提交者: study夏羽

修复已知问题

上级 5f20f8e0
......@@ -31,7 +31,7 @@
// #ifdef APP-PLUS
//idfa有需要的用户在应用首次启动时自己获取存储到storage中
var idfa = '';
/*var idfa = '';
var manager = plus.ios.invoke('ASIdentifierManager', 'sharedManager');
if(plus.ios.invoke(manager, 'isAdvertisingTrackingEnabled')){
var identifier = plus.ios.invoke(manager, 'advertisingIdentifier');
......@@ -39,19 +39,7 @@
plus.ios.deleteObject(identifier);
}
plus.ios.deleteObject(manager);
console.log('idfa = '+idfa);
//https://ask.dcloud.net.cn/article/36107
/*if(~plus.storage.getItem('idfa')){
plus.device.getInfo({//需要勾选IDFA
success:function(e){
console.log('idfa = '+JSON.stringify(e.idfa));
},
fail:function(e){
console.log('getDeviceInfo failed: '+JSON.stringify(e));
}
});
}*/
console.log('idfa = '+idfa);*/
// #endif
},
onShow: function() {
......
<h2>
文档已移至 <a href="https://uniapp.dcloud.io/uniCloud/uni-starter.html" target="_blank">uni-starter文档</a>
</h2>
> 以下为uni-starter v2的文档,新项目推荐直接使用。如果你的老项目使用的是uni-starter v1版,不想升级请查看:[uni-starter v1 文档](https://gitcode.net/dcloud/uni-starter/-/blob/v1/README.md)
## 简介
uni-starter是集成商用项目常见功能的、云端一体应用快速开发项目模版。
一个应用有很多通用的功能,比如登录注册、个人中心、设置、权限管理、拦截器、banner... uni-starter将这些功能都已经集成好。
直接在`HBuilderx`新建项目选择`uni-starter`模板,即可在此基础上快速开发自己的特色业务。
有了`uni-starter`,再加上`schema2code`生成前端页面,一个简单应用就可以快速完成。
如果说[uni-admin](https://uniapp.dcloud.io/uniCloud/admin)是管理端项目的基本项目模版,那么uni-starter则是用户端、尤其是移动端的基础项目模板。
`uni-starter` + `uni-admin` 提供了用户端和管理端的全套模版,开箱即用,应用开发从未如此简单快捷!
演示项目:[https://uni-starter.dcloud.net.cn](https://uni-starter.dcloud.net.cn)
扫码体验:<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/0625ae17-1262-45cb-a713-cdbf02df5c0a.png" style="width: 120px;">
下载地址:[https://ext.dcloud.net.cn/plugin?id=5057](https://ext.dcloud.net.cn/plugin?id=5057)
GitCode 仓库:[https://gitcode.net/dcloud/uni-starter](https://gitcode.net/dcloud/uni-starter)
## uni-starter集成包括:
1. 用户管理:
uni-starter的用户管理本质是集成了[uni-id-pages](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html),它包括众多功能:
- 登录注册(用户名密码登录、手机号验证码登录、APP一键登录、微信登录、Apple登录、微信小程序登录、手机微信扫码登录、微信公众号内登录)
- 修改密码、忘记密码、头像更换(集成图片裁剪)、昵称修改、积分查看、退出登录、账号注销
2. 系统设置:
- 内置[App升级中心](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html)(整包升级、wgt升级、强制升级,后台搭配uni-admin的升级中心插件管理)
- 推送开关(app)、清除缓存(app)
- 指纹解锁(app)、人脸解锁(app)
- 多语言切换
3. 隐私权限:内置Android先弹出隐私协议对话框,然后再向用户申请设备权限
4. 权限引导:当应用拒绝授权某些权限,但在后续使用中又需要这个权限;此时实现:引导用户可“一键跳转至系统设置”中开启。
- 而不是报错让用户自己去找解决方案(更好的用户体验)。
- 采用高内聚低耦合的设计结构,直接在应用启动时,应用拦截器中实现。免去在每个业务代码中处理这类问题,更优雅更方便。
- 已实现项目:摄像头、相册、获取GPS定位、网络2/3/4/5G和Wi-Fi。你可以参考这些实现,处理更多该类场景的处理。uni-starter也会持续更新完善。
5. 实用功能
- 问题与反馈、关于、隐私政策、用户服务协议
- banner(后台搭配uni-admin的banner插件管理)
- 新闻的搜索、列表、详情、分享。通过clientDB实现,开发者直接修改定义的表名等参数,即可轻松改为自己的业务
- 可覆盖原生层的分享菜单
- h5版在页面顶部(全局悬浮)引导用户点击下载App
- 营销裂变:点击“分销推荐”,生成带用户inviteCode参数的应用下载页(H5),一键分享到微信或微信朋友圈等。被邀请人打开下载页面点击下载,设备剪贴板的内容会被自动设置为邀请者的inviteCode。被邀请人下载app之后通过任何方式登录(含:注册并登录),uni-starter框架会自动获取设备剪切板中的inviteCode提交到服务端绑定关联关系。
6. 更好的性能:首页采用nvue,fast编译模式,加快App端启动速度
7. 内置拦截器:
- 页面路由拦截,配置需强制登录的页面;打开时自动检测`token`若无效就自动跳转到登录页
- 调用云函数(callFunction)拦截器,自动携带必要参数、自动处理响应体。详见8.自动完成1-2
8. 自动完成:
- 分析uniCloud.callFunction和clientDB操作的响应体,判断code执行对应的操作如跳转到登录页,自动续期token
- 操作注册/登录操作自动获取客户端设备:push_clientid、imei、oaid、idfa新增/更新到数据表uni-id-device
- 异常恢复处理:断网恢复后自动重连“因网络错误导致的”网络请求
- 为迎合苹果App Store的规则,登录与分享功能项显示之前自动检测是否安装了对应客户端。比如:设备未安装微信则不显示微信快捷登录和微信分享选项
## 快速体验部署流程
#### 1. 开通uniCloud
- 开通`uniCloud`:本项目是云端一体的,它的云端代码需要部署在uniCloud云服务空间里,需要开通uniCloud。在[https://unicloud.dcloud.net.cn/](https://unicloud.dcloud.net.cn/)登录,按云厂商要求进行实名认证。
- 在uniCloud认证通过后,创建一个服务空间给本项目使用。选择阿里云或腾讯云均可,两种服务空间差异[详情](https://uniapp.dcloud.net.cn/uniCloud/price)
#### 2. 运行云服务空间初始化向导
<img style="width:61.8%" src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/472d8525-4e64-4a86-a77a-8c37c4379610.jpg" />
<img style="width:61.8%" src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/78b3f17c-cf70-4cdf-9ada-1796753ffeac.jpg" />
<img style="width:61.8%" src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/09916d79-4753-4baa-9b95-baa831f1a873.jpg" />
## 功能模块介绍
### 1.账户管理
uni-starter 使用 `uni-id-pages`实现:登录注册账户管理相关功能 [详情查看](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html)
### 2.路由管理
大多数应用,都会指定某些页面需要登录才能访问。以往开发者需要写不少代码。现在,只需在项目的pages.json内配置登录页路径、需要登录才能访问的页面等信息,uni-app框架的路由跳转,会自动在需要登录且客户端登录状态过期或未登录时跳转到登录页面。详情查看:[uniIdRouter文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#uni-id-router)
### 3.h5版在页面顶部引导用户`点击下载App`
把h5端用户引流到APP端,是一个非常实用的功能。相对于h5,APP端有更高的用户留存和更好的产品体验。
uni-starter集成了这个功能,你只需直接在`项目根目录/uni-starter.config.js`的"h5"->"openApp"中配置相关内容,即可开启全局悬浮的下载引导。
这也是一个演示开发者如何在h5端做全局悬浮块的例子。你也可以在`/common/openApp.js`中修改他的样式等代码等,注意它只支持原生js语法。
### 4.分享模块
一个可覆盖原生层分享模块
- 应用配置:`manifest.json` App模块配置 --> Share --> 勾选并配置你所需要的模块
- 分享功能配置参数,随着应用的业务场景决定,在各场景调用的时候配置。参考uni-starter的`/pages/list/detail.vue``methods -> shareClick`
- 更多`uni-share`的介绍 [详情](https://ext.dcloud.net.cn/plugin?id=4860)
### 5.升级中心相关
为了解决开发者维护多个 App 升级繁琐,重复逻辑过多,管理不便的问题,升级中心`uni-upgrade-center`应运而生。
提供了简单、易用、统一的 App 管理、App 版本管理、安装包发布管理,升级检测更新管理。
- 升级中心分为两个部分:`uni-upgrade-center` 前台检测更新和`uni-upgrade-center-Admin`后台管理系统。
- `uni-upgrade-center`的介绍 [详情](https://ext.dcloud.net.cn/plugin?id=4542)
- `uni-upgrade-center-Admin`的介绍 [详情](https://ext.dcloud.net.cn/plugin?id=4470)
### 6.意见反馈
- 客户端[详情](https://ext.dcloud.net.cn/plugin?id=50)
- admin端[详情](https://ext.dcloud.net.cn/plugin?id=4992)
### 7.指纹识别模块
- `manifest.json` App模块配置 --> `Fingerprint`指纹识别
### 8.消息推送模块
- `manifest.json` App模块配置 --> `push`消息推送
### 9.隐私政策弹框
根据工业和信息化部关于开展APP侵害用户权益专项整治要求,App提交到应用市场必须满足以下条件:
- 应用启动运行时需弹出隐私政策协议,说明应用采集用户数据
- 应用不能强制要求用户授予权限,即不能“不给权限不让用”
+ 如不希望应用启动时申请“读写手机存储”和“访问设备信息”权限,请参考:[https://ask.dcloud.net.cn/article/36549](https://ask.dcloud.net.cn/article/36549)
配置弹出“隐私协议和政策”打开项目的manifest.json文件,切换到“源码视图”项
`manifest.json` -> `app-plus` -> `privacy` 节点下添加 prompt节点
```js
"privacy" : {
"prompt" : "template",
"template" : {
"title" : "服务协议和隐私政策",
"message" : "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"https://ask.dcloud.net.cn/protocol.html\">《服务协议》</a>和<a href=\"https://ask.dcloud.net.cn/protocol.html\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept" : "同意",
"buttonRefuse" : "暂不同意"
}
}
```
- prompt
字符串类型,必填,隐私政策提示框配置策略,可取值template、custom、none,默认值为none
+ template
使用原生提示框模板,可自定义标题、内容已经按钮上的文本
+ custom
自定义隐私政策提示框,uni-app项目中推荐使用nvue页面进行自定义,5+ APP使用html页面进行自定义
+ none
不弹出隐私政策提示框
- template
json格式,可选,模板提示框上显示的内容
+ title
模板提示框上的标题,默认为“服务协议和隐私政策”
+ message
模板提示框上的内容,richtext类型字符串,支持a/font/br等节点,点击a链接会调用内置页面打开其href属性中链接地址。
**注意:务必配置此提示内容,或参考上面示例内容并修改《服务协议》和《隐私政策》链接地址**
+ buttonAccept
模板提示框上接受按钮的文本,默认值为“我知道了”
+ buttonRefuse
模板提示框上拒绝按钮的文本,默认不显示此按钮
+ second
HBuilderX3.1.12+版本新增支持隐私提示框二次确认提示,用于配置二次确认提示框显示内容,message属性值不为空时弹出二次确认提示框
+ title 二次确认提示框上的标题
+ message 二次确认提示框上的内容,支持richtext类型字符串
+ buttonAccept 二次确认提示框上接受按钮的文本
+ buttonRefuse 二次确认提示框上拒绝按钮的文本
> 更多Android平台隐私与政策提示框配置方法,[详情](https://ask.dcloud.net.cn/article/36937)
##### 注意:
1. 最新的华为应用市场要求,隐私政策提示框上接受按钮的文本,必须为“同意”而不能是其他有歧义的文字。
2. 配置后提交云端打包后生效。理论上绝大部分和`manifest.json`生效相关的配置均需要提交云打包后生效
### 10.云对象拦截器应用
云对象拦截器文档[详情查看](https://uniapp.dcloud.net.cn/uniCloud/client-sdk.html#add-interceptor)
1. 控制调试模式
配置路径:`uni-starter/uni-starter.config.js`
云对象请求fail时,开启调试模式将以`showModal`的模式弹出真实错误信息。关闭调试模式,则以`showToast`的模式模糊提示(弹出系统错误请稍后再试!)
2. 裂变营销功能原理
当用户请求云对象`uni-id-co`的任何方式登录(含:注册并登录)功能时,
云对象拦截器逻辑内部:判断用户的剪切板是否包含`uniInvitationCode:`开头的邀请码,如果存在则在请求时带上此邀请码;实现裂变营销功能的用户关系绑定。
### 12.关于升级
- 项目升级
uni-starter遵循uni-app的插件模块化规范,即:[uni_modules](https://uniapp.dcloud.io/uni_modules) 。它是个项目类型的插件。在项目的根目录下有一个符合uni_modules规范的package.json文件,在这个文件右键-从插件市场更新即可更新该插件。
- 插件升级
非项目类型的`uni_modules`插件在项目根目录下的`uni_modules`目录下。以插件ID为插件文件夹命名,在该目录右键也会看到“从插件市场更新”选项,点击即可更新该插件。
uni-starter内集成的uni-id-pages、uni-upgrade-center等插件都可以独立升级。
### 13.多语言国际化
uni-starter支持多语言国际化,默认关闭,可以在`uni-starter.config.js`->`i18n`->`enable`中配置。
如果你启用了多语言国际化需要先阅读:[uni-app多语言国际化](https://uniapp.dcloud.io/collocation/i18n?id=%e6%a1%86%e6%9e%b6%e5%86%85%e7%bd%ae%e7%bb%84%e4%bb%b6%e5%92%8capi%e5%9b%bd%e9%99%85%e5%8c%96)
### 14.微信登录自动获取头像
当用户首次在微信小程序中通过微信登录应用。uni-starter将获取用户的微信头像,设置为当前账号头像。
**注意:** 保存头像的过程是:先将微信头像的图片下载,再上传到uniCloud云存储。而小程序平台要求在管理后台配置小程序应用的联网服务器域名,否则无法联网。请确认已正确配置download、uploadFile合法域名[详情查看](https://uniapp.dcloud.io/uniCloud/publish.html#useinmp)
## initApp()做了什么
1. 读取uni-starter.config并挂载到globalData的config下
2. 读取应用版本号,并存到globalData下
3. 检查是否有可更新的应用版本,决定是否启动在线更新版本
4. 监听设备的网络变化并以uni.showToast APi的方式提醒用户
5. 使用[拦截器](https://uniapp.dcloud.io/api/interceptor?id=addinterceptor) 实现:自动引导打开`选择图片`所需要的权限。当调用`uni.chooseImage`时检测到无权限则自动开启引导。并不是在每次调用接口时处理这类问题,你可以参考该例子做更多该类场景的处理。uni-starter也会持续完善。
## 配置文件
uni-starter提供了`uni-starter.config.js`,可配置选择登录注册方式及优先级等,可指定该应用是否强制登录才能进入某个页面。配置项内容如下:
```js
module.exports = {
"h5": {
"url": "https://static-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e.bspapp.com", // 前端网页托管的域名
// 在h5端全局悬浮引导用户下载app的功能 更多自定义要求在/common/openApp.js中修改
"openApp": {
//点击悬浮下载栏后打开的网页链接
"openUrl": 'https://sj.qq.com/myapp/detail.htm?apkName=com.tencent.android.qqdownloader&info=6646FD239A6EBA9E2DEE5DFC7E18D867',
//左侧显示的应用名称
"appname": 'uni-starter',
//应用的图标
"logo": './static/logo.png',
}
},
"mp": {
"weixin": {
//微信小程序原始id,微信小程序分享时
"id": "gh_132465798"
}
},
//关于应用
"about": {
//应用名称
"appName": "uni-starter",
//应用logo
"logo": "/static/logo.png",
//公司名称
"company": "数字天堂(北京)网络技术有限公司",
//口号
"slogan": "为开发而生",
//应用的链接,用于分享到第三方平台和生成关于我们页的二维码
"download": "https://m3w.cn/uniapp",
//应用版本号,用于非app端显示,app端自动获取
"version":"1.0.0"
},
//用于打开应用市场评分界面
"marketId":{
"ios":"",
"android":""
},
//配置多语言国际化。i18n为英文单词 internationalization的首末字符i和n,18为中间的字符数 是“国际化”的简称
"i18n":{
"enable":false //默认启用,国际化。如果你不想使用国际化相关功能,请改为false
}
}
```
## 目录结构@catalogue
<pre v-pre="" data-lang="">
<code class="lang-" style="padding:0">
uni-starter
├─uniCloud-aliyun
│ ├─cloudfunctions 云函数目录
│ | ├─common 公共模块
│ │ | ├─uni-config-center uni-starter的服务端配置中心,项目所有云函数的配置在这里填写 <a target="_blank" href="https://ext.dcloud.net.cn/plugin?id=4425">详情</a>
│ │ | | ├─index.js config-center入口文件
│ │ | | └─uni-id uni-id模块配置目录
│ │ | | ├─config.json uni-id对应的配置数据:微信登录、一键登录、短信验证码登录等key都在这里填写<a target="_blank" href="https://ext.dcloud.net.cn/plugin?id=4425">详情</a>
│ │ | | └─file.cert uni-id依赖的配置文件,假如你使用微信发红包功能,需要的证书文件就是放到这里
│ | | └───uni-id uni-id用户体系 <a target="_blank" href="https://uniapp.dcloud.io/uniCloud/uni-id-summary">详情</a>
│ | ├─check-version 检查更新云函数 <a target="_blank" href="https://ext.dcloud.net.cn/plugin?name=uni-upgrade-center-app">详情</a>
│ | ├─rewarded-video-ad-notify-url 签到插件广告回调 <a target="_blank" href="https://ext.dcloud.net.cn/plugin?name=uni-sign-in">详情</a>
│ | ├─uni-analyse-searchhot 云端一体搜索模板依赖的云函数 <a target="_blank" href="https://ext.dcloud.net.cn/plugin?id=3851">详情</a>
│ | ├─uni-captcha-co 云端一体图形验证码组件云对象 <a target="_blank" href="https://ext.dcloud.net.cn/plugin?name=uni-captcha">详情</a>
│ | ├─uni-clientDB-actions 客户端直接操作数据库拦截逻辑 <a target="_blank" href="https://uniapp.dcloud.net.cn/uniCloud/jql.html#action">详情</a>
│ | ├─uni-open-bridge 统一接管微信等三方平台认证的开源库 <a target="_blank" href="https://uniapp.dcloud.net.cn/uniCloud/jql.html#action">详情</a>
│ | ├─upgrade-center 云端一体uni升级中心的云函数 <a target="_blank" href="https://uniapp.dcloud.net.cn/uniCloud/jql.html#action">详情</a>
│ | └─uni-id-co 用户中心云函数,实现用户注册、修改密码、发送验证码、快捷登录(微信、短信、账户、一键登录) <a target="_blank" href="https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html">详情</a>
│ └──database 云数据目录
│ ├─db_init.json db_init.json初始化数据库文件,其中不再包含schema <a target="_blank" href="https://uniapp.dcloud.net.cn/uniCloud/hellodb?id=db-init">详情</a>
│ ├─opendb-app-versions.schema.json 应用版本,表结构文件
│ ├─opendb-banner.schema.json 横幅数据表,表结构文件
│ ├─opendb-feedback.schema.json 意见反馈表,表结构文件
│ ├─opendb-news-articles.schema.json 新闻文章表,表结构文件
│ ├─opendb-news-categories.schema.json 新闻分类表,表结构文件
│ ├─opendb-news-comments.schema.json 新闻评论表,表结构文件
│ ├─opendb-news-favorite.schema.json 新闻收藏表,表结构文件
│ ├─opendb-search-hot.schema.json 热门搜索表,表结构文件
│ ├─opendb-search-log.schema.json 搜索记录表,表结构文件
│ ├─opendb-verify-codes.schema.json 验证码表,表结构文件
│ ├─uni-id-log.schema.json 登录日志表,表结构文件
│ ├─uni-id-scores.schema.json 用户积分表,表结构文件
│ └─uni-id-users.schema.json 用户表,表结构文件
├─pages 业务页面文件存放的目录
│ ├─common
│ │ └─webview webview目录
│ │ └─webview.vue webview页面 用于实现跨端的web页面浏览
│ ├─grid
│ │ └─grid.vue 带宫格和banner的示例页面
│ ├─list
│ │ ├─list.vue 新闻列表
│ │ ├─search
│ │ │ └─search 云端一体搜索插件
│ │ └─detail.vue 新闻详情
│ ├─ucenter
│ │ ├─about 关于我们
│ │ │ └─about
│ │ ├─login-page 登录模块<a target="_blank" href="https://ext.dcloud.net.cn/plugin?id=8577">详情参考</a>
│ │ ├─read-news-log 新闻阅读记录
│ │ │ └─read-news-log
│ │ ├─invite 带用户inviteCode参数的应用下载页
│ │ │ └─invite
│ │ ├─settings
│ │ │ ├─dc-push
│ │ │ │ └─push.js push权限操作SDK
│ │ │ └─settings.vue app设置
│ │ ├─userinfo 用户个人信息
│ │ │ ├─bind-mobile
│ │ │ │ └─bind-mobile.vue 绑定手机号码
│ │ │ ├─limeClipper 图片裁剪插件,来源<a target="_blank" href="https://ext.dcloud.net.cn/plugin?id=3594">limeClipper @作者: 陌上华年</a>
│ │ │ │ ├─images
│ │ │ │ │ ├─photo.svg
│ │ │ │ │ └─rotate.svg
│ │ │ │ ├─index.css
│ │ │ │ ├─limeClipper.vue
│ │ │ │ ├─README.md
│ │ │ │ └─utils.js
│ │ │ ├─main.js
│ │ │ ├─cropImage.vue 引用limeClipper的图片裁剪模块,为了方便二开可能会出现兼容`vue``nvue`,所以做成了`页面`而不是`组件`
│ │ │ └─userinfo.vue
│ | └─ucenter.vue 用户中心
│ |
├─static 存放应用引用的本地静态资源(如图片、视频等)的目录,<b>注意:</b>静态资源只能存放于此
├─uni_modules 存放<a target="_blank" href="https://uniapp.dcloud.net.cn/plugin/uni_modules.html">uni_modules</a>规范的插件。
├─uni_modules_tools uni_modules插件上传辅助脚本 <a href="https://ext.dcloud.net.cn/plugin?id=5256">详情</a>
├─main.js Vue初始化入口文件
├─App.vue 应用配置,用来配置App全局样式以及监听 <a href="/collocation/frame/lifecycle?id=应用生命周期">应用生命周期</a>
├─uni-starter.config uni-starter的前端的配置文件,项目所有模块的配置在这里填写。详见该文件的代码注释。
├─manifest.json 配置应用名称、appid、logo、版本等打包信息,<a href="/collocation/manifest">详见</a>
└─pages.json 配置页面路由、导航条、选项卡等页面类信息,<a href="/collocation/pages">详见</a>
</code>
</pre>
完整的uni-app目录结构[详情](https://uniapp.dcloud.io/frame?id=%e7%9b%ae%e5%bd%95%e7%bb%93%e6%9e%84)
## 常见API示范
1. 判断当前用户是否拥有某角色`uniIDHasRole` 演示页面:`/pages/grid/grid` [API文档详情](https://uniapp.dcloud.io/api/global?id=uniidhasrole)
2. 指纹解锁、人脸解锁 演示页面:`/pages/ucenter/settings/settings` [API文档详情](https://uniapp.dcloud.io/api/system/authentication)
## 注意事项
1. 真机运行需要制作自定义基座,制作后选择运行到自定义基座
2. 苹果登录的图标,需要满足苹果应用市场的审核规范请勿随便修改;如需修改请先阅读:[Sign in with Apple Button](https://appleid.apple.com/signinwithapple/button)
3. 应用登录功能,默认不勾选同意隐私权限是响应安卓应用市场的规范;请勿修改该逻辑。
## FAQ:常见问题
1. 提示“公共模块uni-id缺少配置信息”解决方案:在cloudfunctions右键‘上传所有云函数、公共模块及actions’之后,需要在cloudfunctions -> common -> uni-config-center 目录单独上传一次,右键‘上传公共模块’。
2. 本项目代码可以商用,无需为DCloud付费。但不能把本项目的代码改造用于非uni-app和uniCloud的技术体系。即,不能将后台改成php、java等其他后台,这将违反使用许可协议。
## 相关案例
[
![](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/dd4c366f-6165-46c0-8500-5a679d7e5463.jpg)
](https://ext.dcloud.net.cn/search?q=uni-starter)
(点击跳转到案例列表)
## 第三方插件(感谢插件作者,排名不分前后):
1. 图片裁剪 [limeClipper](https://ext.dcloud.net.cn/plugin?id=3594) @作者: 陌上华年
2. 二维码生成 [Sansnn-uQRCode](https://ext.dcloud.net.cn/plugin?id=1287) @作者: 3snn
\ No newline at end of file
## 2.0.0(2022-08-10)
应用`uni-id-pages``uniIdRouter`
- 重要:应用`uni-id-pages``uniIdRouter`
## 1.2.7(2022-08-10)
- 修复微信小程序绑定手机号失败的问题
## 1.2.6(2022-06-29)
......
......@@ -4,7 +4,7 @@ import uniStarterConfig from '@/uni-starter.config.js';
import checkUpdate from '@/uni_modules/uni-upgrade-center-app/utils/check-update';
import callCheckVersion from '@/uni_modules/uni-upgrade-center-app/utils/call-check-version';
// 实现,路由拦截。当应用无访问摄像头/相册权限,引导跳到设置界面
// 实现,路由拦截。当应用无访问摄像头/相册权限,引导跳到设置界面 https://ext.dcloud.net.cn/plugin?id=5095
import interceptorChooseImage from '@/uni_modules/json-interceptor-chooseImage/js_sdk/main.js';
interceptorChooseImage()
......@@ -82,7 +82,7 @@ export default async function() {
}
});
})
console.log(7897897897899,params);
// console.log(params);
}
console.log(params);
},
......@@ -93,12 +93,17 @@ export default async function() {
},
fail(e){
console.error(e);
if (debug) {
uni.showModal({
content: JSON.stringify(e),
showCancel: false
});
console.error(e);
}else{
uni.showToast({
title: '系统错误请稍后再试',
icon:'error'
});
}
}
})
......@@ -156,77 +161,3 @@ function initAppVersion() {
// 检查更新
// #endif
}
\ No newline at end of file
async function getDeviceInfo() {
let deviceInfo = {
"uuid": '',
"vendor": '',
"push_clientid": '',
"imei": '',
"oaid": '',
"idfa": '',
"model": '',
"platform": '',
}
const {
model,
platform,
} = uni.getSystemInfoSync();
Object.assign(deviceInfo, {
model,
platform
});
// #ifdef APP-PLUS
const oaid = await new Promise((callBack, fail) => {
if (deviceInfo.platform == "android") {
plus.device.getOAID({
success: function(e) {
callBack(e.oaid)
// console.log('getOAID success: '+JSON.stringify(e));
},
fail: function(e) {
callBack()
console.log('getOAID failed: ' + JSON.stringify(e));
}
});
} else {
callBack()
}
}),
{
imei,
uuid
} = await new Promise((callBack, fail) => {
plus.device.getInfo({
success: function(e) {
callBack(e)
// console.log('getOAID success: '+JSON.stringify(e));
},
fail: function(e) {
callBack()
console.log('getOAID failed: ' + JSON.stringify(e));
}
});
}),
idfa = plus.storage.getItem('idfa') || '', //idfa有需要的用户在应用首次启动时自己获取存储到storage中
vendor = plus.device.vendor;
try {
deviceInfo.push_clientid = plus.push.getClientInfo().clientid
} catch (e) {
uni.showModal({
content: '获取推送标识失败。如果你的应用不需要推送功能,请注释掉本代码块',
showCancel: false,
confirmText: "好的"
});
console.log(e)
}
Object.assign(deviceInfo, {
imei,
uuid,
idfa,
vendor
});
// #endif
return deviceInfo
}
{
"id": "uni-starter",
"displayName": "uni-starter",
"version": "1.2.7",
"version": "2.0.0",
"description": "云端一体应用快速开发基本项目模版",
"keywords": [
"login",
......
......@@ -176,7 +176,26 @@
"navigationBarTitleText": "修改密码"
}
}
],
,{
"path": "uni_modules/uni-id-pages/pages/register/register-by-email",
"style": {
"navigationBarTitleText": "邮箱验证码注册"
}
}
,{
"path": "uni_modules/uni-id-pages/pages/retrieve/retrieve-by-email",
"style": {
"navigationBarTitleText": "通过邮箱重置密码"
}
}
,{
"path": "uni_modules/uni-id-pages/pages/register/register-admin",
"style": {
"enablePullDownRefresh": false,
"navigationBarTitleText": "注册管理员账号"
}
}
],
"globalStyle": {
// #ifdef H5
"h5": {
......
......@@ -21,7 +21,6 @@
</uni-swiper-dot>
</unicloud-db>
<!-- 宫格 -->
<uni-section :title="$t('grid.grid')" style="margin: 0;" type="line"></uni-section>
<view class="example-body">
......
......@@ -41,8 +41,8 @@ export default {
},
//用于打开应用市场评分界面
"marketId":{
"ios":"id1417078253",
"android":"123456"
"ios":"",
"android":""
},
//配置多语言国际化。i18n为英文单词 internationalization的首末字符i和n,18为中间的字符数 是“国际化”的简称
"i18n":{
......
## 1.2.1(2022-09-05)
- 修复 当 text 超过 max-num 时,badge 的宽度计算是根据 text 的长度计算,更改为 css 计算实际展示宽度,详见:[https://ask.dcloud.net.cn/question/150473](https://ask.dcloud.net.cn/question/150473)
## 1.2.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-badge](https://uniapp.dcloud.io/component/uniui/uni-badge)
......
<template>
<view class="uni-badge--x">
<slot />
<text v-if="text" :class="classNames" :style="[badgeWidth, positionStyle, customStyle, dotStyle]"
<text v-if="text" :class="classNames" :style="[positionStyle, customStyle, dotStyle]"
class="uni-badge" @click="onClick()">{{displayValue}}</text>
</view>
</template>
......@@ -130,16 +130,13 @@
const match = whiteList[this.absolute]
return match ? match : whiteList['rightTop']
},
badgeWidth() {
return {
width: `${this.width}px`
}
},
dotStyle() {
if (!this.isDot) return {}
return {
width: '10px',
minWidth: '0',
height: '10px',
padding: '0',
borderRadius: '10px'
}
},
......@@ -160,7 +157,7 @@
};
</script>
<style lang="scss" scoped>
<style lang="scss" >
$uni-primary: #2979ff !default;
$uni-success: #4cd964 !default;
$uni-warning: #f0ad4e !default;
......@@ -199,6 +196,8 @@
justify-content: center;
flex-direction: row;
height: 20px;
min-width: 20px;
padding: 0 4px;
line-height: 18px;
color: #fff;
border-radius: 100px;
......@@ -207,6 +206,7 @@
border: 1px solid #fff;
text-align: center;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-feature-settings: "tnum";
font-size: $bage-size;
/* #ifdef H5 */
z-index: 999;
......
{
"id": "uni-badge",
"displayName": "uni-badge 数字角标",
"version": "1.2.0",
"version": "1.2.1",
"description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。",
"keywords": [
"",
......@@ -18,11 +18,7 @@
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
......@@ -39,10 +35,11 @@
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
......
## 1.0.3(2022-09-16)
- 可以使用 uni-scss 控制主题色
## 1.0.2(2022-06-30)
- 优化 在 uni-forms 中的依赖注入方式
## 1.0.1(2022-02-07)
- 修复 multiple 为 true 时,v-model 的值为 null 报错的 bug
## 1.0.0(2021-11-19)
......
......@@ -155,17 +155,17 @@
value(newVal) {
this.dataList = this.getDataList(newVal)
// fix by mehaotian is_reset 在 uni-forms 中定义
if(!this.is_reset){
this.is_reset = false
this.formItem && this.formItem.setValue(newVal)
}
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
},
modelValue(newVal) {
this.dataList = this.getDataList(newVal);
if(!this.is_reset){
this.is_reset = false
this.formItem && this.formItem.setValue(newVal)
}
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
}
},
data() {
......@@ -193,22 +193,22 @@
}
},
created() {
this.form = this.getForm('uniForms')
this.formItem = this.getForm('uniFormsItem')
// this.form = this.getForm('uniForms')
// this.formItem = this.getForm('uniFormsItem')
// this.formItem && this.formItem.setValue(this.value)
if (this.formItem) {
this.isTop = 6
if (this.formItem.name) {
// 如果存在name添加默认值,否则formData 中不存在这个字段不校验
if(!this.is_reset){
this.is_reset = false
this.formItem.setValue(this.dataValue)
}
this.rename = this.formItem.name
this.form.inputChildrens.push(this)
}
}
// if (this.formItem) {
// this.isTop = 6
// if (this.formItem.name) {
// // 如果存在name添加默认值,否则formData 中不存在这个字段不校验
// if(!this.is_reset){
// this.is_reset = false
// this.formItem.setValue(this.dataValue)
// }
// this.rename = this.formItem.name
// this.form.inputChildrens.push(this)
// }
// }
if (this.localdata && this.localdata.length !== 0) {
this.isLocal = true
......@@ -273,7 +273,7 @@
}
}
}
this.formItem && this.formItem.setValue(detail.value)
// this.formItem && this.formItem.setValue(detail.value)
// TODO 兼容 vue2
this.$emit('input', detail.value);
// // TOTO 兼容 vue3
......@@ -375,7 +375,7 @@
selectedArr.push(item[this.map.value])
}
})
return this.dataValue && this.dataValue.length > 0 ? this.dataValue : selectedArr
return this.dataValue.length > 0 ? this.dataValue : selectedArr
},
/**
......@@ -384,12 +384,14 @@
setStyleBackgroud(item) {
let styles = {}
let selectedColor = this.selectedColor?this.selectedColor:'#2979ff'
if (this.selectedColor) {
if (this.mode !== 'list') {
styles['border-color'] = item.selected?selectedColor:'#DCDFE6'
}
if (this.mode === 'tag') {
styles['background-color'] = item.selected? selectedColor:'#f5f5f5'
}
}
let classles = ''
for (let i in styles) {
classles += `${i}:${styles[i]};`
......@@ -399,6 +401,7 @@
setStyleIcon(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor?this.selectedColor:'#2979ff'
styles['background-color'] = item.selected?selectedColor:'#fff'
styles['border-color'] = item.selected?selectedColor:'#DCDFE6'
......@@ -407,7 +410,7 @@
styles['background-color'] = '#F2F6FC'
styles['border-color'] = item.selected?selectedColor:'#DCDFE6'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
......@@ -416,6 +419,7 @@
setStyleIconText(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor?this.selectedColor:'#2979ff'
if (this.mode === 'tag') {
styles.color = item.selected?(this.selectedTextColor?this.selectedTextColor:'#fff'):'#666'
......@@ -425,7 +429,7 @@
if(!item.selected && item.disabled){
styles.color = '#999'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
......@@ -448,7 +452,7 @@
</script>
<style lang="scss">
$checked-color: #2979ff;
$uni-primary: #2979ff !default;
$border-color: #DCDFE6;
$disable:0.4;
......@@ -614,8 +618,8 @@
// 选中
&.is-checked {
.checkbox__inner {
border-color: $checked-color;
background-color: $checked-color;
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
......@@ -623,14 +627,14 @@
}
}
.radio__inner {
border-color: $checked-color;
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $checked-color;
background-color: $uni-primary;
}
}
.checklist-text {
color: $checked-color;
color: $uni-primary;
}
// 选中禁用
&.is-disable {
......@@ -683,10 +687,10 @@
}
&.is-checked {
border-color: $checked-color;
border-color: $uni-primary;
.checkbox__inner {
border-color: $checked-color;
background-color: $checked-color;
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
......@@ -694,16 +698,16 @@
}
.radio__inner {
border-color: $checked-color;
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $checked-color;
background-color: $uni-primary;
}
}
.checklist-text {
color: $checked-color;
color: $uni-primary;
}
// 选中禁用
......@@ -735,8 +739,8 @@
}
&.is-checked {
background-color: $checked-color;
border-color: $checked-color;
background-color: $uni-primary;
border-color: $uni-primary;
.checklist-text {
color: #fff;
......@@ -775,8 +779,8 @@
&.is-checked {
.checkbox__inner {
border-color: $checked-color;
background-color: $checked-color;
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
......@@ -789,13 +793,13 @@
}
}
.checklist-text {
color: $checked-color;
color: $uni-primary;
}
.checklist-content {
.checkobx__list {
opacity: 1;
border-color: $checked-color;
border-color: $uni-primary;
}
}
......
{
"id": "uni-data-checkbox",
"displayName": "uni-data-checkbox 数据选择器",
"version": "1.0.1",
"version": "1.0.3",
"description": "通过数据驱动的单选框和复选框",
"keywords": [
"uni-ui",
......@@ -17,11 +17,7 @@
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
......@@ -38,7 +34,8 @@
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-load-more","uni-scss"],
......
## 1.1.0(2022-06-30)
- 新增 在 uni-forms 1.4.0 中使用可以在 blur 时校验内容
- 新增 clear 事件,点击右侧叉号图标触发
- 新增 change 事件 ,仅在输入框失去焦点或用户按下回车时触发
- 优化 组件样式,组件获取焦点时高亮显示,图标颜色调整等
-
## 1.0.5(2022-06-07)
- 优化 clearable 显示策略
## 1.0.4(2022-06-07)
- 优化 clearable 显示策略
## 1.0.3(2022-05-20)
- 修复 关闭图标某些情况下无法取消的bug
## 1.0.2(2022-04-12)
......
<template>
<view class="uni-easyinput" :class="{'uni-easyinput-error':msg}" :style="{color:inputBorder && msg?'#e43d33':styles.color}">
<view class="uni-easyinput__content" :class="{'is-input-border':inputBorder ,'is-input-error-border':inputBorder && msg,'is-textarea':type==='textarea','is-disabled':disabled}"
:style="{'border-color':inputBorder && msg?'#dd524d':styles.borderColor,'background-color':disabled?styles.disableColor:''}">
<uni-icons v-if="prefixIcon" class="content-clear-icon" :type="prefixIcon" color="#c0c4cc" @click="onClickIcon('prefix')"></uni-icons>
<textarea v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{'input-padding':inputBorder}"
:name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" placeholder-class="uni-easyinput__placeholder-class"
:maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" @input="onInput" @blur="onBlur" @focus="onFocus"
@confirm="onConfirm"></textarea>
<input v-else :type="type === 'password'?'text':type" class="uni-easyinput__content-input" :style="{
'padding-right':type === 'password' ||clearable || prefixIcon?'':'10px',
'padding-left':prefixIcon?'':'10px'
}"
:name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder"
:placeholderStyle="placeholderStyle" placeholder-class="uni-easyinput__placeholder-class" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" :confirmType="confirmType" @focus="onFocus"
@blur="onBlur" @input="onInput" @confirm="onConfirm" />
<template v-if="type === 'password' && passwordIcon" >
<uni-icons v-if="val" class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" :type="showPassword?'eye-slash-filled':'eye-filled'"
:size="18" color="#c0c4cc" @click="onEyes"></uni-icons>
<view class="uni-easyinput" :class="{'uni-easyinput-error':msg}" :style="boxStyle">
<view class="uni-easyinput__content" :class="inputContentClass" :style="inputContentStyle">
<uni-icons v-if="prefixIcon" class="content-clear-icon" :type="prefixIcon" color="#c0c4cc"
@click="onClickIcon('prefix')" size="22"></uni-icons>
<textarea v-if="type === 'textarea'" class="uni-easyinput__content-textarea"
:class="{'input-padding':inputBorder}" :name="name" :value="val" :placeholder="placeholder"
:placeholderStyle="placeholderStyle" :disabled="disabled"
placeholder-class="uni-easyinput__placeholder-class" :maxlength="inputMaxlength" :focus="focused"
:autoHeight="autoHeight" @input="onInput" @blur="_Blur" @focus="_Focus" @confirm="onConfirm"></textarea>
<input v-else :type="type === 'password'?'text':type" class="uni-easyinput__content-input"
:style="inputStyle" :name="name" :value="val" :password="!showPassword && type === 'password'"
:placeholder="placeholder" :placeholderStyle="placeholderStyle"
placeholder-class="uni-easyinput__placeholder-class" :disabled="disabled" :maxlength="inputMaxlength"
:focus="focused" :confirmType="confirmType" @focus="_Focus" @blur="_Blur" @input="onInput"
@confirm="onConfirm" />
<template v-if="type === 'password' && passwordIcon">
<!-- 开启密码时显示小眼睛 -->
<uni-icons v-if="isVal" class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}"
:type="showPassword?'eye-slash-filled':'eye-filled'" :size="22"
:color="focusShow?'#2979ff':'#c0c4cc'" @click="onEyes">
</uni-icons>
</template>
<template v-else-if="suffixIcon">
<uni-icons v-if="suffixIcon" class="content-clear-icon" :type="suffixIcon" color="#c0c4cc" @click="onClickIcon('suffix')"></uni-icons>
<uni-icons v-if="suffixIcon" class="content-clear-icon" :type="suffixIcon" color="#c0c4cc"
@click="onClickIcon('suffix')" size="22"></uni-icons>
</template>
<template v-else>
<uni-icons class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" type="clear" :size="clearSize"
v-if="clearable && val && !disabled" color="#c0c4cc" @click="onClear"></uni-icons>
<uni-icons v-if="clearable && isVal && !disabled && type !== 'textarea'" class="content-clear-icon"
:class="{'is-textarea-icon':type==='textarea'}" type="clear" :size="clearSize"
:color="msg?'#dd524d':(focusShow?'#2979ff':'#c0c4cc')" @click="onClear"></uni-icons>
</template>
<slot name="right"></slot>
</view>
......@@ -31,10 +36,6 @@
</template>
<script>
// import {
// debounce,
// throttle
// } from './common.js'
/**
* Easyinput 输入框
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。
......@@ -48,7 +49,7 @@
* @value idcard 身份证输入键盘,信、支付宝、百度、QQ小程序
* @value digit 带小数点的数字键盘 ,App的nvue页面、微信、支付宝、百度、头条、QQ小程序支持
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件,点击可清空输入框内容(默认true)
* @property {Boolean} autoHeight 是否自动增高输入区域,type为textarea时有效(默认false)
* @property {Boolean} autoHeight 是否自动增高输入区域,type为textarea时有效(默认true)
* @property {String } placeholder 输入框的提示文字
* @property {String } placeholderStyle placeholder的样式(内联样式,字符串),如"color: #ddd"
* @property {Boolean} focus 是否自动获得焦点(默认false)
......@@ -76,13 +77,44 @@
* @event {Function} iconClick 点击图标时触发
* @example <uni-easyinput v-model="mobile"></uni-easyinput>
*/
function obj2strClass(obj) {
let classess = ''
for (let key in obj) {
const val = obj[key]
if (val) {
classess += `${key} `
}
}
return classess
}
function obj2strStyle(obj) {
let style = ''
for (let key in obj) {
const val = obj[key]
style += `${key}:${val};`
}
return style
}
export default {
name: 'uni-easyinput',
emits:['click','iconClick','update:modelValue','input','focus','blur','confirm'],
model:{
prop:'modelValue',
event:'update:modelValue'
emits: ['click', 'iconClick', 'update:modelValue', 'input', 'focus', 'blur', 'confirm', 'clear', 'eyes', 'change'],
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
options: {
virtualHost: true
},
inject: {
form: {
from: 'uniForm',
default: null
},
formItem: {
from: 'uniFormItem',
default: null
},
},
props: {
name: String,
......@@ -100,7 +132,10 @@
type: Boolean,
default: false
},
placeholder: String,
placeholder: {
type: String,
default: ' '
},
placeholderStyle: String,
focus: {
type: Boolean,
......@@ -120,7 +155,7 @@
},
clearSize: {
type: [Number, String],
default: 15
default: 24
},
inputBorder: {
type: Boolean,
......@@ -138,7 +173,7 @@
type: [Boolean, String],
default: true
},
passwordIcon:{
passwordIcon: {
type: Boolean,
default: true
},
......@@ -152,79 +187,105 @@
}
}
},
errorMessage:{
type:[String,Boolean],
default:''
errorMessage: {
type: [String, Boolean],
default: ''
}
},
data() {
return {
focused: false,
errMsg: '',
val: '',
showMsg: '',
border: false,
isFirstBorder: false,
showClearIcon: false,
showPassword: false
showPassword: false,
focusShow: false,
localMsg: ''
};
},
computed: {
// 输入框内是否有值
isVal() {
const val = this.val
// fixed by mehaotian 处理值为0的情况,字符串0不在处理范围
if (val || val === 0) {
return true
}
return false
},
msg() {
return this.errorMessage || this.errMsg;
// console.log('computed', this.form, this.formItem);
// if (this.form) {
// return this.errorMessage || this.formItem.errMsg;
// }
// TODO 处理头条 formItem 中 errMsg 不更新的问题
return this.localMsg || this.errorMessage
},
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,用户可以传入字符串数值
inputMaxlength() {
return Number(this.maxlength);
},
// 处理外层样式的style
boxStyle() {
return `color:${this.inputBorder && this.msg?'#e43d33':this.styles.color};`
},
// input 内容的类和样式处理
inputContentClass() {
return obj2strClass({
'is-input-border': this.inputBorder,
'is-input-error-border': this.inputBorder && this.msg,
'is-textarea': this.type === 'textarea',
'is-disabled': this.disabled
})
},
inputContentStyle() {
const focusColor = this.focusShow ? '#2979ff' : this.styles.borderColor
const borderColor = this.inputBorder && this.msg ? '#dd524d' : focusColor
return obj2strStyle({
'border-color': borderColor || '#e5e5e5',
'background-color': this.disabled ? this.styles.disableColor : '#fff'
})
},
// input右侧样式
inputStyle() {
const paddingRight = this.type === 'password' || this.clearable || this.prefixIcon ? '' : '10px'
return obj2strStyle({
'padding-right': paddingRight,
'padding-left': this.prefixIcon ? '' : '10px'
})
}
},
watch: {
value(newVal) {
if (this.errMsg) this.errMsg = ''
this.val = newVal
// fix by mehaotian is_reset 在 uni-forms 中定义
if (this.form && this.formItem &&!this.is_reset) {
this.is_reset = false
this.formItem.setValue(newVal)
}
},
modelValue(newVal) {
if (this.errMsg) this.errMsg = ''
this.val = newVal
if (this.form && this.formItem &&!this.is_reset) {
this.is_reset = false
this.formItem.setValue(newVal)
}
},
focus(newVal) {
this.$nextTick(() => {
this.focused = this.focus
this.focusShow = this.focus
})
}
},
created() {
if(!this.value && this.value !== 0){
this.val = this.modelValue
}
if(!this.modelValue && this.modelValue !== 0){
this.val = this.value
}
this.form = this.getForm('uniForms')
this.formItem = this.getForm('uniFormsItem')
this.init()
// TODO 处理头条vue3 computed 不监听 inject 更改的问题(formItem.errMsg)
if (this.form && this.formItem) {
if (this.formItem.name) {
if(!this.is_reset){
this.is_reset = false
this.formItem.setValue(this.val)
}
this.rename = this.formItem.name
this.form.inputChildrens.push(this)
}
this.$watch('formItem.errMsg', (newVal) => {
this.localMsg = newVal
})
}
},
mounted() {
this.$nextTick(() => {
this.focused = this.focus
this.focusShow = this.focus
})
},
methods: {
......@@ -232,28 +293,35 @@
* 初始化变量值
*/
init() {
if (this.value || this.value === 0) {
this.val = this.value
} else if (this.modelValue || this.modelValue === 0) {
this.val = this.modelValue
} else {
this.val = null
}
},
/**
* 点击图标时触发
* @param {Object} type
*/
onClickIcon(type) {
this.$emit('iconClick', type)
},
/**
* 获取父元素实例
* 显示隐藏内容,密码框时生效
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false;
parentName = parent.$options.name;
}
return parent;
},
onEyes() {
this.showPassword = !this.showPassword
this.$emit('eyes', this.showPassword)
},
/**
* 输入时触发
* @param {Object} event
*/
onInput(event) {
let value = event.detail.value;
// 判断是否去除空格
......@@ -270,30 +338,79 @@
// TODO 兼容 vue2
this.$emit('input', value);
// TODO 兼容 vue3
this.$emit('update:modelValue',value)
this.$emit('update:modelValue', value)
},
/**
* 外部调用方法
* 获取焦点时触发
* @param {Object} event
*/
onFocus() {
this.$nextTick(() => {
this.focused = true
})
this.$emit('focus', null);
},
onFocus(event) {
_Focus(event) {
this.focusShow = true
this.$emit('focus', event);
},
onBlur(event) {
/**
* 外部调用方法
* 失去焦点时触发
* @param {Object} event
*/
onBlur() {
this.focused = false
this.$emit('focus', null);
},
_Blur(event) {
let value = event.detail.value;
this.focusShow = false
this.$emit('blur', event);
// 根据类型返回值,在event中获取的值理论上讲都是string
this.$emit('change', this.val)
// 失去焦点时参与表单校验
if (this.form && this.formItem) {
const {
validateTrigger
} = this.form
if (validateTrigger === 'blur') {
this.formItem.onFieldChange()
}
}
},
/**
* 按下键盘的发送键
* @param {Object} e
*/
onConfirm(e) {
this.$emit('confirm', e.detail.value);
this.$emit('confirm', this.val);
this.$emit('change', this.val)
},
/**
* 清理内容
* @param {Object} event
*/
onClear(event) {
this.val = '';
// TODO 兼容 vue2
this.$emit('input', '');
// TODO 兼容 vue2
// TODO 兼容 vue3
this.$emit('update:modelValue','')
},
fieldClick() {
this.$emit('click');
this.$emit('update:modelValue', '')
// 点击叉号触发
this.$emit('clear')
},
/**
* 去除空格
*/
trimStr(str, pos = 'both') {
if (pos === 'both') {
return str.trim();
......@@ -316,9 +433,10 @@
};
</script>
<style lang="scss" >
<style lang="scss">
$uni-error: #e43d33;
$uni-border-1: #DCDFE6 !default;
.uni-easyinput {
/* #ifndef APP-NVUE */
width: 100%;
......@@ -336,10 +454,14 @@
width: 100%;
display: flex;
box-sizing: border-box;
min-height: 36px;
// min-height: 36px;
/* #endif */
flex-direction: row;
align-items: center;
// 处理border动画刚开始显示黑色的问题
border-color: #fff;
transition-property: border-color;
transition-duration: 0.3s;
}
.uni-easyinput__content-input {
......@@ -351,12 +473,16 @@
flex: 1;
line-height: 1;
font-size: 14px;
height: 35px;
// min-height: 36px;
}
.uni-easyinput__placeholder-class {
color: #999;
font-size: 12px;
font-weight: 200;
// font-weight: 200;
}
.is-textarea {
align-items: flex-start;
}
......@@ -371,9 +497,10 @@
flex: 1;
line-height: 1.5;
font-size: 14px;
padding-top: 6px;
padding-bottom: 10px;
margin: 6px;
margin-left: 0;
height: 80px;
min-height: 80px;
/* #ifndef APP-NVUE */
min-height: 80px;
width: auto;
......@@ -403,6 +530,9 @@
align-items: center;
border: 1px solid $uni-border-1;
border-radius: 4px;
/* #ifdef MP-ALIPAY */
overflow: hidden;
/* #endif */
}
.uni-error-message {
......@@ -423,8 +553,10 @@
.is-input-error-border {
border-color: $uni-error;
.uni-easyinput__placeholder-class {
color: mix(#fff, $uni-error, 50%);;
color: mix(#fff, $uni-error, 50%);
;
}
}
......@@ -450,9 +582,9 @@
}
.is-disabled {
border-color: red;
background-color: #F7F6F6;
color: #D5D5D5;
.uni-easyinput__placeholder-class {
color: #D5D5D5;
font-size: 12px;
......
{
"id": "uni-easyinput",
"displayName": "uni-easyinput 增强输入框",
"version": "1.0.3",
"version": "1.1.0",
"description": "Easyinput 组件是对原生input组件的增强",
"keywords": [
"uni-ui",
......
## 1.4.8(2022-08-23)
- 优化 根据 rules 自动添加 required 的问题
## 1.4.7(2022-08-22)
- 修复 item 未设置 require 属性,rules 设置 require 后,星号也显示的 bug,详见:[https://ask.dcloud.net.cn/question/151540](https://ask.dcloud.net.cn/question/151540)
## 1.4.6(2022-07-13)
- 修复 model 需要校验的值没有声明对应字段时,导致第一次不触发校验的bug
## 1.4.5(2022-07-05)
- 新增 更多表单示例
- 优化 子表单组件过期提示的问题
- 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式
## 1.4.4(2022-07-04)
- 更新 删除组件日志
## 1.4.3(2022-07-04)
- 修复 由 1.4.0 引发的 label 插槽不生效的bug
## 1.4.2(2022-07-04)
- 修复 子组件找不到 setValue 报错的bug
## 1.4.1(2022-07-04)
- 修复 uni-data-picker 在 uni-forms-item 中报错的bug
- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug
## 1.4.0(2022-06-30)
- 【重要】组件逻辑重构,部分用法用旧版本不兼容,请注意兼容问题
- 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力
- 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃
- 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效
- 新增 onFieldChange 方法,可以对子表单进行校验,可替代binddata方法
- 新增 子表单的 setRules 方法,配合自定义校验函数使用
- 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则
- 优化 动态表单校验方式,废弃拼接name的方式
## 1.3.3(2022-06-22)
- 修复 表单校验顺序无序问题
## 1.3.2(2021-12-09)
-
## 1.3.1(2021-11-19)
......
<template>
<view class="uni-forms-item" :class="{ 'uni-forms-item--border': border, 'is-first-border': border && isFirstBorder, 'uni-forms-item-error': msg }">
<view class="uni-forms-item__box">
<view class="uni-forms-item__inner" :class="['is-direction-' + labelPos]">
<view class="uni-forms-item__label" :style="{ width: labelWid , justifyContent: justifyContent }">
<view class="uni-forms-item"
:class="['is-direction-' + localLabelPos ,border?'uni-forms-item--border':'' ,border && isFirstBorder?'is-first-border':'']">
<slot name="label">
<text v-if="required" class="is-required">*</text>
<uni-icons v-if="leftIcon" class="label-icon" size="16" :type="leftIcon" :color="iconColor" />
<text class="label-text">{{ label }}</text>
<view v-if="label" class="label-seat"></view>
<view class="uni-forms-item__label" :class="{'no-label':!label && !isRequired}"
:style="{width:localLabelWidth,justifyContent: localLabelAlign}">
<text v-if="isRequired" class="is-required">*</text>
<text>{{label}}</text>
</view>
</slot>
<!-- #ifndef APP-NVUE -->
<view class="uni-forms-item__content">
<slot></slot>
<view class="uni-forms-item__error" :class="{'msg--active':msg}">
<text>{{msg}}</text>
</view>
<view class="uni-forms-item__content" :class="{ 'is-input-error-border': msg }"><slot></slot></view>
</view>
<view
v-if="msg"
class="uni-error-message"
:class="{ 'uni-error-msg--boeder': border }"
:style="{
paddingLeft: labelLeft
}"
>
<text class="uni-error-message-text">{{ showMsg === 'undertext' ? msg : '' }}</text>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view class="uni-forms-item__nuve-content">
<view class="uni-forms-item__content">
<slot></slot>
</view>
<view class="uni-forms-item__error" :class="{'msg--active':msg}">
<text class="error-text">{{msg}}</text>
</view>
</view>
<!-- #endif -->
</view>
</template>
<script>
/**
* Field 输入框
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。
* @tutorial https://ext.dcloud.net.cn/plugin?id=21001
* @property {Boolean} required 是否必填,左边显示红色"*"号(默认false)
* @property {String} validateTrigger = [bind|submit] 校验触发器方式 默认 submit 可选
* @value bind 发生变化时触发
* @value submit 提交时触发
* @property {String } leftIcon label左边的图标,限 uni-ui 的图标名称
* @property {String } iconColor 左边通过icon配置的图标的颜色(默认#606266)
/**
* uni-fomrs-item 表单子组件
* @description uni-fomrs-item 表单子组件,提供了基础布局已经校验能力
* @tutorial https://ext.dcloud.net.cn/plugin?id=2773
* @property {Boolean} required 是否必填,左边显示红色"*"号
* @property {String } label 输入框左边的文字提示
* @property {Number } labelWidth label的宽度,单位px(默认65)
* @property {String } labelAlign = [left|center|right] label的文字对齐方式(默认left)
* @value left label 左侧显示
* @value center label 居中
* @value right label 右侧对齐
* @property {String } labelPosition = [top|left] label的文字的位置(默认left)
* @value top 顶部显示 label
* @value left 左侧显示 label
* @property {String } errorMessage 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
* @property {String } name 表单域的属性名,在使用校验规则时必填
* @property {String } leftIcon 【1.4.0废弃】label左边的图标,限 uni-ui 的图标名称
* @property {String } iconColor 【1.4.0废弃】左边通过icon配置的图标的颜色(默认#606266)
* @property {String} validateTrigger = [bind|submit|blur] 【1.4.0废弃】校验触发器方式 默认 submit
* @value bind 发生变化时触发
* @value submit 提交时触发
* @value blur 失去焦点触发
* @property {String } labelPosition = [top|left] 【1.4.0废弃】label的文字的位置(默认left)
* @value top 顶部显示 label
* @value left 左侧显示 label
*/
export default {
export default {
name: 'uniFormsItem',
options: {
virtualHost: true
},
provide() {
return {
uniFormItem: this
}
},
inject: {
form: {
from: 'uniForm',
default: null
},
},
props: {
// 自定义内容
custom: {
type: Boolean,
default: false
// 表单校验规则
rules: {
type: Array,
default () {
return null;
}
},
// 是否显示报错信息
showMessage: {
// 表单域的属性名,在使用校验规则时必填
name: {
type: [String, Array],
default: ''
},
required: {
type: Boolean,
default: true
default: false
},
name: String,
required: Boolean,
validateTrigger: {
label: {
type: String,
default: ''
},
leftIcon: String,
iconColor: {
type: String,
default: '#606266'
},
label: String,
// 左边标题的宽度单位px
// label的宽度 ,默认 80
labelWidth: {
type: [Number, String],
type: [String, Number],
default: ''
},
// 对齐方式,left|center|right
// label 居中方式,默认 left 取值 left/center/right
labelAlign: {
type: String,
default: ''
},
// lable的位置,可选为 left-左边,top-上边
labelPosition: {
type: String,
default: ''
},
// 强制显示错误信息
errorMessage: {
type: [String, Boolean],
default: ''
},
// 表单校验规则
rules: {
type: Array,
default() {
return [];
}
}
// 1.4.0 弃用,统一使用 form 的校验时机
// validateTrigger: {
// type: String,
// default: ''
// },
// 1.4.0 弃用,统一使用 form 的label 位置
// labelPosition: {
// type: String,
// default: ''
// },
// 1.4.0 以下属性已经废弃,请使用 #label 插槽代替
leftIcon: String,
iconColor: {
type: String,
default: '#606266'
},
},
data() {
return {
errorTop: false,
errorBottom: false,
labelMarginBottom: '',
errorWidth: '',
errMsg: '',
val: '',
labelPos: '',
labelWid: '',
labelAli: '',
showMsg: 'undertext',
isRequired: false,
userRules: null,
localLabelAlign: 'left',
localLabelWidth: '65px',
localLabelPos: 'left',
border: false,
isFirstBorder: false,
isArray: false,
arrayField: ''
};
},
computed: {
// 处理错误信息
msg() {
return this.errorMessage || this.errMsg;
},
fieldStyle() {
let style = {};
if (this.labelPos == 'top') {
style.padding = '0 0';
this.labelMarginBottom = '6px';
}
if (this.labelPos == 'left' && this.msg !== false && this.msg != '') {
style.paddingBottom = '0px';
this.errorBottom = true;
this.errorTop = false;
} else if (this.labelPos == 'top' && this.msg !== false && this.msg != '') {
this.errorBottom = false;
this.errorTop = true;
} else {
// style.paddingBottom = ''
this.errorTop = false;
this.errorBottom = false;
}
return style;
},
watch: {
// 规则发生变化通知子组件更新
'form.formRules'(val) {
// TODO 处理头条vue3 watch不生效的问题
// #ifndef MP-TOUTIAO
this.init()
// #endif
},
'form.labelWidth'(val) {
// 宽度
this.localLabelWidth = this._labelWidthUnit(val)
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
justifyContent() {
if (this.labelAli === 'left') return 'flex-start';
if (this.labelAli === 'center') return 'center';
if (this.labelAli === 'right') return 'flex-end';
},
labelLeft(){
return (this.labelPos === 'left' ? parseInt(this.labelWid) : 0) + 'px'
}
'form.labelPosition'(val) {
// 标签位置
this.localLabelPos = this._labelPosition()
},
watch: {
validateTrigger(trigger) {
this.formTrigger = trigger;
'form.labelAlign'(val) {
}
},
created() {
this.form = this.getForm();
this.group = this.getForm('uniGroup');
this.formRules = [];
this.formTrigger = this.validateTrigger;
// 处理 name,是否数组
if (this.name && this.name.indexOf('[') !== -1 && this.name.indexOf(']') !== -1) {
this.isArray = true;
this.arrayField = this.name
// fix by mehaotian 修改不修改的情况,动态值不检验的问题
this.form.formData[this.name] = this.form._getValue(this.name, '')
}
},
mounted() {
if (this.form) {
this.form.childrens.push(this);
this.init(true)
if (this.name && this.form) {
// TODO 处理头条vue3 watch不生效的问题
// #ifdef MP-TOUTIAO
this.$watch('form.formRules', () => {
this.init()
})
// #endif
// 监听变化
this.$watch(
() => {
const val = this.form._getDataValue(this.name, this.form.localData)
return val
},
(value, oldVal) => {
const isEqual = this.form._isEqual(value, oldVal)
// 简单判断前后值的变化,只有发生变化才会发生校验
// TODO 如果 oldVal = undefined ,那么大概率是源数据里没有值导致 ,这个情况不哦校验 ,可能不严谨 ,需要在做观察
// fix by mehaotian 暂时取消 && oldVal !== undefined ,如果formData 中不存在,可能会不校验
if (!isEqual) {
const val = this.itemSetValue(value)
this.onFieldChange(val, false)
}
}, {
immediate: false
}
this.init();
);
}
},
// #ifndef VUE3
destroyed() {
if(this.__isUnmounted) return
if (this.__isUnmounted) return
this.unInit()
},
// #endif
// #ifdef VUE3
unmounted(){
unmounted() {
this.__isUnmounted = true
this.unInit()
},
// #endif
methods: {
init() {
if (this.form) {
let { formRules, validator, formData, value, labelPosition, labelWidth, labelAlign, errShowType } = this.form;
this.labelPos = this.labelPosition ? this.labelPosition : labelPosition;
/**
* 外部调用方法
* 设置规则 ,主要用于小程序自定义检验规则
* @param {Array} rules 规则源数据
*/
setRules(rules = null) {
this.userRules = rules
this.init(false)
},
// 兼容老版本表单组件
setValue() {
// console.log('setValue 方法已经弃用,请使用最新版本的 uni-forms 表单组件以及其他关联组件。');
},
/**
* 外部调用方法
* 校验数据
* @param {any} value 需要校验的数据
* @param {boolean} 是否立即校验
* @return {Array|null} 校验内容
*/
async onFieldChange(value, formtrigger = true) {
const {
formData,
localData,
errShowType,
validateCheck,
validateTrigger,
_isRequiredField,
_realName
} = this.form
const name = _realName(this.name)
if (!value) {
value = this.form.formData[name]
}
// fixd by mehaotian 不在校验前清空信息,解决闪屏的问题
// this.errMsg = '';
// fix by mehaotian 解决没有检验规则的情况下,抛出错误的问题
const ruleLen = this.itemRules.rules && this.itemRules.rules.length
if (!this.validator || !ruleLen || ruleLen === 0) return;
// 检验时机
// let trigger = this.isTrigger(this.itemRules.validateTrigger, this.validateTrigger, validateTrigger);
const isRequiredField = _isRequiredField(this.itemRules.rules || []);
let result = null;
// 只有等于 bind 时 ,才能开启时实校验
if (validateTrigger === 'bind' || formtrigger) {
// 校验当前表单项
result = await this.validator.validateUpdate({
[name]: value
},
formData
);
if(this.label){
this.labelWid = (this.labelWidth ? this.labelWidth : (labelWidth||70))
}else{
this.labelWid =( this.labelWidth ? this.labelWidth : (labelWidth||'auto'))
// 判断是否必填,非必填,不填不校验,填写才校验 ,暂时只处理 undefined 和空的情况
if (!isRequiredField && (value === undefined || value === '')) {
result = null;
}
if(this.labelWid && this.labelWid !=='auto') {
this.labelWid +='px'
// 判断错误信息显示类型
if (result && result.errorMessage) {
if (errShowType === 'undertext') {
// 获取错误信息
this.errMsg = !result ? '' : result.errorMessage;
}
if (errShowType === 'toast') {
uni.showToast({
title: result.errorMessage || '校验错误',
icon: 'none'
});
}
if (errShowType === 'modal') {
uni.showModal({
title: '提示',
content: result.errorMessage || '校验错误'
});
}
} else {
this.errMsg = ''
}
// 通知 form 组件更新事件
validateCheck(result ? result : null)
} else {
this.errMsg = ''
}
this.labelAli = this.labelAlign ? this.labelAlign : labelAlign;
return result ? result : null;
},
/**
* 初始组件数据
*/
init(type = false) {
const {
validator,
formRules,
childrens,
formData,
localData,
_realName,
labelWidth,
_getDataValue,
_setDataValue
} = this.form || {}
// 对齐方式
this.localLabelAlign = this._justifyContent()
// 宽度
this.localLabelWidth = this._labelWidthUnit(labelWidth)
// 标签位置
this.localLabelPos = this._labelPosition()
this.isRequired = this.required
// 将需要校验的子组件加入form 队列
this.form && type && childrens.push(this)
if (!validator || !formRules) return
// 判断第一个 item
if (!this.form.isFirstBorder) {
this.form.isFirstBorder = true;
......@@ -221,49 +333,49 @@ export default {
this.isFirstBorder = true;
}
}
this.border = this.form.border;
this.showMsg = errShowType;
let name = this.isArray ? this.arrayField : this.name;
if(!name) return
if (formRules && this.rules.length > 0) {
if (!formRules[name]) {
// 获取子域的真实名称
const name = _realName(this.name)
const itemRule = this.userRules || this.rules
if (typeof formRules === 'object' && itemRule) {
// 子规则替换父规则
formRules[name] = {
rules: this.rules
}
rules: itemRule
}
validator.updateSchema(formRules);
}
this.formRules = formRules[name] || {};
this.validator = validator;
} else {
this.labelPos = this.labelPosition || 'left';
this.labelWid = this.labelWidth || 65;
this.labelAli = this.labelAlign || 'left';
}
// 注册校验规则
const itemRules = formRules[name] || {}
this.itemRules = itemRules
// 注册校验函数
this.validator = validator
// 默认值赋予
this.itemSetValue(_getDataValue(this.name, localData))
this.isRequired = this._isRequired()
},
unInit(){
unInit() {
if (this.form) {
this.form.childrens.forEach((item, index) => {
const {
childrens,
formData,
_realName
} = this.form
childrens.forEach((item, index) => {
if (item === this) {
this.form.childrens.splice(index, 1)
delete this.form.formData[item.name]
delete formData[_realName(item.name)]
}
})
}
},
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false;
parentName = parent.$options.name;
}
return parent;
// 设置item 的值
itemSetValue(value) {
const name = this.form._realName(this.name)
const rules = this.itemRules.rules || []
const val = this.form._getValue(name, value, rules)
this.form._setDataValue(name, this.form.formData, val)
return val
},
/**
......@@ -272,238 +384,248 @@ export default {
clearValidate() {
this.errMsg = '';
},
/**
* 子组件调用,如 easyinput
* @param {Object} value
*/
setValue(value) {
let name = this.isArray ? this.arrayField : this.name;
if (name) {
if (this.errMsg) this.errMsg = '';
// 给组件赋值
this.form.formData[name] = this.form._getValue(name, value);
if (!this.formRules || (typeof this.formRules && JSON.stringify(this.formRules) === '{}')) return;
this.triggerCheck(this.form._getValue(this.name, value));
}
// 是否显示星号
_isRequired() {
// TODO 不根据规则显示 星号,考虑后续兼容
// if (this.form) {
// if (this.form._isRequiredField(this.itemRules.rules || []) && this.required) {
// return true
// }
// return false
// }
return this.required
},
/**
* 校验规则
* @param {Object} value
*/
async triggerCheck(value,formTrigger) {
let promise = null;
this.errMsg = '';
// fix by mehaotian 解决没有检验规则的情况下,抛出错误的问题
if (!this.validator || Object.keys(this.formRules).length === 0) return;
const isNoField = this.isRequired(this.formRules.rules || []);
let isTrigger = this.isTrigger(this.formRules.validateTrigger, this.validateTrigger, this.form.validateTrigger);
let result = null;
if (!!isTrigger || formTrigger) {
let name = this.isArray ? this.arrayField : this.name;
result = await this.validator.validateUpdate(
{
[name]: value
// 处理对齐方式
_justifyContent() {
if (this.form) {
const {
labelAlign
} = this.form
let labelAli = this.labelAlign ? this.labelAlign : labelAlign;
if (labelAli === 'left') return 'flex-start';
if (labelAli === 'center') return 'center';
if (labelAli === 'right') return 'flex-end';
}
return 'flex-start';
},
this.form.formData
);
}
// 判断是否必填,非必填,不填不校验,填写才校验
if (!isNoField && (value === undefined || value === '')) {
result = null;
}
const inputComp = this.form.inputChildrens.find(child => child.rename === this.name);
if ((isTrigger || formTrigger) && result && result.errorMessage) {
if (inputComp) {
inputComp.errMsg = result.errorMessage;
}
if (this.form.errShowType === 'toast') {
uni.showToast({
title: result.errorMessage || '校验错误',
icon: 'none'
});
}
if (this.form.errShowType === 'modal') {
uni.showModal({
title: '提示',
content: result.errorMessage || '校验错误'
});
}
} else {
if (inputComp) {
inputComp.errMsg = '';
}
}
// 处理 label宽度单位 ,继承父元素的值
_labelWidthUnit(labelWidth) {
// if (this.form) {
// const {
// labelWidth
// } = this.form
return this.num2px(this.labelWidth ? this.labelWidth : (labelWidth || (this.label ? 65 : 'auto')))
// }
// return '65px'
},
// 处理 label 位置
_labelPosition() {
if (this.form) return this.form.labelPosition || 'left'
return 'left'
this.errMsg = !result ? '' : result.errorMessage;
// 触发validate事件
this.form.validateCheck(result ? result : null);
// typeof callback === 'function' && callback(result ? result : null);
// if (promise) return promise
return result ? result : null;
},
/**
* 触发时机
* @param {Object} event
* @param {Object} rule 当前规则内时机
* @param {Object} itemRlue 当前组件时机
* @param {Object} parentRule 父组件时机
*/
isTrigger(rule, itemRlue, parentRule) {
let rl = true;
// bind submit
if (rule === 'submit' || !rule) {
if (rule === undefined) {
if (itemRlue !== 'bind') {
if (!itemRlue) {
return parentRule === 'bind' ? true : false;
return parentRule === '' ? 'bind' : 'submit';
}
return false;
return 'submit';
}
return true;
return 'bind';
}
return false;
return 'submit';
}
return true;
return 'bind';
},
// 是否有必填字段
isRequired(rules) {
let isNoField = false;
for (let i = 0; i < rules.length; i++) {
const ruleData = rules[i];
if (ruleData.required) {
isNoField = true;
break;
num2px(num) {
if (typeof num === 'number') {
return `${num}px`
}
return num
}
return isNoField;
}
}
};
};
</script>
<style lang="scss" >
.uni-forms-item {
position: relative;
padding: 0px;
text-align: left;
color: #333;
font-size: 14px;
// margin-bottom: 22px;
}
.uni-forms-item__box {
<style lang="scss">
.uni-forms-item {
position: relative;
}
.uni-forms-item__inner {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
// flex-direction: row;
// align-items: center;
/* #ifdef APP-NVUE */
// 在 nvue 中,使用 margin-bottom error 信息会被隐藏
padding-bottom: 22px;
// margin-bottom: 22px;
}
.is-direction-left {
/* #endif */
/* #ifndef APP-NVUE */
margin-bottom: 22px;
/* #endif */
flex-direction: row;
}
.is-direction-top {
flex-direction: column;
}
.uni-forms-item__label {
/* #ifndef APP-NVUE */
&__label {
display: flex;
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
flex-direction: row;
align-items: center;
width: 65px;
// line-height: 2;
// margin-top: 3px;
padding: 5px 0;
text-align: left;
font-size: 14px;
color: #606266;
height: 36px;
// margin-right: 5px;
padding: 0 12px 0 0;
/* #ifndef APP-NVUE */
vertical-align: middle;
flex-shrink: 0;
/* #endif */
.label-text {
font-size: 13px;
color: #666666;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
&.no-label {
padding: 0;
}
.label-seat {
margin-right: 5px;
}
}
.uni-forms-item__content {
&__content {
/* #ifndef MP-TOUTIAO */
// display: flex;
// align-items: center;
/* #endif */
position: relative;
font-size: 14px;
flex: 1;
/* #ifndef APP-NVUE */
width: 100%;
box-sizing: border-box;
min-height: 36px;
/* #endif */
flex: 1;
}
flex-direction: row;
.label-icon {
margin-right: 5px;
margin-top: -1px;
}
/* #ifndef APP || H5 || MP-WEIXIN || APP-NVUE */
// TODO 因为小程序平台会多一层标签节点 ,所以需要在多余节点继承当前样式
&>uni-easyinput,
&>uni-data-picker {
width: 100%;
}
// 必填
.is-required {
// color: $uni-color-error;
color: #dd524d;
font-weight: bold;
}
/* #endif */
.uni-error-message {
}
& .uni-forms-item__nuve-content {
display: flex;
flex-direction: column;
flex: 1;
}
&__error {
color: #f56c6c;
font-size: 12px;
line-height: 1;
padding-top: 4px;
position: absolute;
bottom: 0px;
/* #ifndef APP-NVUE */
top: 100%;
left: 0;
text-align: left;
}
transition: transform 0.3s;
transform: translateY(-100%);
/* #endif */
/* #ifdef APP-NVUE */
bottom: 5px;
/* #endif */
.uni-error-message-text {
line-height: 22px;
color: #dd524d;
opacity: 0;
.error-text {
// 只有 nvue 下这个样式才生效
color: #f56c6c;
font-size: 12px;
}
}
.uni-error-msg--boeder {
position: relative;
bottom: 0;
line-height: 22px;
}
&.msg--active {
opacity: 1;
transform: translateY(0%);
}
}
// 位置修饰样式
&.is-direction-left {
flex-direction: row;
}
&.is-direction-top {
flex-direction: column;
.uni-forms-item__label {
padding: 0 0 8px;
line-height: 1.5715;
text-align: left;
/* #ifndef APP-NVUE */
white-space: initial;
/* #endif */
}
}
.is-required {
// color: $uni-color-error;
color: #dd524d;
font-weight: bold;
}
}
.is-input-error-border {
border-color: #dd524d;
}
.uni-forms-item--border {
.uni-forms-item--border {
margin-bottom: 0;
padding: 10px 0;
// padding-bottom: 0;
border-top: 1px #eee solid;
.uni-forms-item__inner {
padding: 0;
/* #ifndef APP-NVUE */
.uni-forms-item__content {
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.uni-forms-item__error {
position: relative;
top: 5px;
left: 0;
padding-top: 0;
}
}
}
.uni-forms-item-error {
// padding-bottom: 0;
}
/* #endif */
.is-first-border {
/* #ifdef APP-NVUE */
display: flex;
flex-direction: column;
.uni-forms-item__error {
position: relative;
top: 0px;
left: 0;
padding-top: 0;
margin-top: 5px;
}
/* #endif */
}
.is-first-border {
/* #ifndef APP-NVUE */
border: none;
/* #endif */
/* #ifdef APP-NVUE */
border-width: 0;
/* #endif */
}
.uni-forms--no-padding {
padding: 0;
}
}
</style>
<template>
<view class="uni-forms" :class="{ 'uni-forms--top': !border }">
<form @submit.stop="submitForm" @reset="resetForm">
<view class="uni-forms">
<form>
<slot></slot>
</form>
</view>
</template>
<script>
import Validator from './validate.js';
import {
deepCopy,
getValue,
isRequiredField,
setDataValue,
getDataValue,
realName,
isRealName,
rawData,
isEqual
} from './utils.js'
// #ifndef VUE3
// 后续会慢慢废弃这个方法
import Vue from 'vue';
Vue.prototype.binddata = function(name, value, formName) {
if (formName) {
......@@ -26,18 +40,15 @@
}
};
// #endif
import Validator from './validate.js';
/**
* Forms 表单
* @description 由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据
* @tutorial https://ext.dcloud.net.cn/plugin?id=2773
* @property {Object} rules 表单校验规则
* @property {String} validateTrigger = [bind|submit] 校验触发器方式 默认 submit
* @property {String} validateTrigger = [bind|submit|blur] 校验触发器方式 默认 submit
* @value bind 发生变化时触发
* @value submit 提交时触发
* @value blur 失去焦点时触发
* @property {String} labelPosition = [top|left] label 位置 默认 left
* @value top 顶部显示 label
* @value left 左侧显示 label
......@@ -51,25 +62,34 @@
* @value toast 错误信息toast显示
* @value modal 错误信息modal显示
* @event {Function} submit 提交时触发
* @event {Function} validate 校验结果发生变化触发
*/
export default {
name: 'uniForms',
components: {},
emits:['input','reset','validate','submit'],
emits: ['validate', 'submit'],
options: {
virtualHost: true
},
props: {
// 即将弃用
value: {
type: Object,
default () {
return {};
return null;
}
},
// 替换 value 属性
// vue3 替换 value 属性
modelValue: {
type: Object,
default () {
return {};
return null;
}
},
// 1.4.0 开始将不支持 v-model ,且废弃 value 和 modelValue
model: {
type: Object,
default () {
return null;
}
},
// 表单校验规则
......@@ -79,58 +99,68 @@
return {};
}
},
// 校验触发器方式,默认 关闭
//校验错误信息提示方式 默认 undertext 取值 [undertext|toast|modal]
errShowType: {
type: String,
default: 'undertext'
},
// 校验触发器方式 默认 bind 取值 [bind|submit]
validateTrigger: {
type: String,
default: ''
default: 'submit'
},
// label 位置,可选值 top/left
// label 位置,默认 left 取值 top/left
labelPosition: {
type: String,
default: 'left'
},
// label 宽度,单位 px
// label 宽度
labelWidth: {
type: [String, Number],
default: ''
},
// label 居中方式,可选值 left/center/right
// label 居中方式,默认 left 取值 left/center/right
labelAlign: {
type: String,
default: 'left'
},
errShowType: {
type: String,
default: 'undertext'
},
border: {
type: Boolean,
default: false
}
},
provide() {
return {
uniForm: this
}
},
data() {
return {
formData: {}
// 表单本地值的记录,不应该与传如的值进行关联
formData: {},
formRules: {}
};
},
computed: {
dataValue() {
if (JSON.stringify(this.modelValue) === '{}') {
return this.value
} else {
return this.modelValue
// 计算数据源变化的
localData() {
const localVal = this.model || this.modelValue || this.value
if (localVal) {
return deepCopy(localVal)
}
return {}
}
},
watch: {
rules(newVal) {
// 如果规则发生变化,要初始化组件
this.init(newVal);
// 监听数据变化 ,暂时不使用,需要单独赋值
// localData: {},
// 监听规则变化
rules: {
handler: function(val, oldVal) {
this.setRules(val)
},
labelPosition() {
this.childrens.forEach(vm => {
vm.init()
})
deep: true,
immediate: true
}
},
created() {
......@@ -156,137 +186,132 @@
}
// #endif
// 存放watch 监听数组
this.unwatchs = [];
// 存放子组件数组
this.childrens = [];
// 存放 easyInput 组件
this.inputChildrens = [];
// 存放 dataCheckbox 组件
this.checkboxChildrens = [];
// 存放规则
this.formRules = [];
this.init(this.rules);
// 子组件实例数组
this.childrens = []
// TODO 兼容旧版 uni-data-picker ,新版本中无效,只是避免报错
this.inputChildrens = []
this.setRules(this.rules)
},
// mounted() {
// this.init(this.rules)
// },
methods: {
init(formRules) {
// 判断是否有规则
if (Object.keys(formRules).length === 0) {
this.formData = this.dataValue
return
};
this.formRules = formRules;
this.validator = new Validator(formRules);
this.registerWatch();
},
// 监听 watch
registerWatch() {
// 取消监听,避免多次调用 init 重复执行 $watch
this.unwatchs.forEach(v => v());
this.childrens.forEach((v) => {
v.init()
})
// watch 每个属性 ,需要知道具体那个属性发变化
Object.keys(this.dataValue).forEach(key => {
let watch = this.$watch(
'dataValue.' + key,
value => {
if (!value) return
// 如果是对象 ,则平铺内容
if (value.toString() === '[object Object]') {
for (let i in value) {
let name = `${key}[${i}]`;
this.formData[name] = this._getValue(name, value[i]);
}
} else {
this.formData[key] = this._getValue(key, value);
}
},
{
deep: true,
immediate: true
}
);
this.unwatchs.push(watch);
});
},
/**
* 公开给用户使用
* 设置验规则
* @param {Object} formRules
* 外部调用方法
* 设置规则 ,主要用于小程序自定义检验规则
* @param {Array} rules 规则源数据
*/
setRules(formRules) {
this.init(formRules);
setRules(rules) {
// TODO 有可能子组件合并规则的时机比这个要早,所以需要合并对象 ,而不是直接赋值,可能会被覆盖
this.formRules = Object.assign({}, this.formRules, rules)
// 初始化校验函数
this.validator = new Validator(rules);
},
/**
* 公开给用户使用
* 设置自定义表单组件 value 值
* @param {String} name 字段名称
* @param {String} value 字段值
* 外部调用方法
* 设置数据,用于设置表单数据,公开给用户使用 , 不支持在动态表单中使用
* @param {Object} key
* @param {Object} value
*/
setValue(name, value, callback) {
let example = this.childrens.find(child => child.name === name);
setValue(key, value) {
let example = this.childrens.find(child => child.name === key);
if (!example) return null;
value = this._getValue(example.name, value);
this.formData[name] = value;
example.val = value;
return example.triggerCheck(value, callback);
this.formData[key] = getValue(key, value, (this.formRules[key] && this.formRules[key].rules) || [])
return example.onFieldChange(this.formData[key]);
},
/**
* 表单重置
* @param {Object} event
* 外部调用方法
* 手动提交校验表单
* 对整个表单进行校验的方法,参数为一个回调函数。
* @param {Array} keepitem 保留不参与校验的字段
* @param {type} callback 方法回调
*/
resetForm(event) {
validate(keepitem, callback) {
return this.checkAll(this.formData, keepitem, callback);
},
/**
* 外部调用方法
* 部分表单校验
* @param {Array|String} props 需要校验的字段
* @param {Function} 回调函数
*/
validateField(props = [], callback) {
props = [].concat(props);
let invalidFields = {};
this.childrens.forEach(item => {
item.errMsg = '';
const inputComp = this.inputChildrens.find(child => child.rename === item.name);
if (inputComp) {
inputComp.errMsg = '';
// fix by mehaotian 不触发其他组件的 setValue
inputComp.is_reset = true
inputComp.$emit('input', inputComp.multiple ? [] : '');
inputComp.$emit('update:modelValue', inputComp.multiple ? [] : '');
const name = realName(item.name)
if (props.indexOf(name) !== -1) {
invalidFields = Object.assign({}, invalidFields, {
[name]: this.formData[name]
});
}
});
return this.checkAll(invalidFields, [], callback);
},
/**
* 外部调用方法
* 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果
* @param {Array|String} props 需要移除校验的字段 ,不填为所有
*/
clearValidate(props = []) {
props = [].concat(props);
this.childrens.forEach(item => {
if (item.name) {
this.formData[item.name] = this._getValue(item.name, '');
if (props.length === 0) {
item.errMsg = '';
} else {
const name = realName(item.name)
if (props.indexOf(name) !== -1) {
item.errMsg = '';
}
}
});
this.$emit('reset', event);
},
/**
* 触发表单校验,通过 @validate 获取
* @param {Object} validate
* 外部调用方法 ,即将废弃
* 手动提交校验表单
* 对整个表单进行校验的方法,参数为一个回调函数。
* @param {Array} keepitem 保留不参与校验的字段
* @param {type} callback 方法回调
*/
validateCheck(validate) {
if (validate === null) validate = null;
this.$emit('validate', validate);
submit(keepitem, callback, type) {
for (let i in this.dataValue) {
const itemData = this.childrens.find(v => v.name === i);
if (itemData) {
if (this.formData[i] === undefined) {
this.formData[i] = this._getValue(i, this.dataValue[i]);
}
}
}
if (!type) {
console.warn('submit 方法即将废弃,请使用validate方法代替!');
}
return this.checkAll(this.formData, keepitem, callback, 'submit');
},
/**
* 校验所有或者部分表单
*/
async validateAll(invalidFields, type, keepitem, callback) {
// 校验所有
async checkAll(invalidFields, keepitem, callback, type) {
// 不存在校验规则 ,则停止校验流程
if (!this.validator) return
let childrens = []
// 处理参与校验的item实例
for (let i in invalidFields) {
const item = this.childrens.find(v => v.name === i)
const item = this.childrens.find(v => realName(v.name) === i)
if (item) {
childrens.push(item)
}
}
// 如果validate第一个参数是funciont ,那就走回调
if (!callback && typeof keepitem === 'function') {
callback = keepitem;
}
let promise;
// 如果不存在回调,那么使用 Promise 方式返回
if (!callback && typeof callback !== 'function' && Promise) {
promise = new Promise((resolve, reject) => {
callback = function(valid, invalidFields) {
......@@ -296,47 +321,39 @@
}
let results = [];
let newFormData = {};
if (this.validator) {
for (let key in childrens) {
const child = childrens[key];
let name = child.isArray ? child.arrayField : child.name;
if (child.isArray) {
if (child.name.indexOf('[') !== -1 && child.name.indexOf(']') !== -1) {
const fieldData = child.name.split('[');
const fieldName = fieldData[0];
const fieldValue = fieldData[1].replace(']', '');
if (!newFormData[fieldName]) {
newFormData[fieldName] = {};
}
newFormData[fieldName][fieldValue] = this._getValue(name, invalidFields[name]);
}
} else {
newFormData[name] = this._getValue(name, invalidFields[name]);
}
const result = await child.triggerCheck(invalidFields[name], true);
// 避免引用错乱 ,建议拷贝对象处理
let tempFormData = JSON.parse(JSON.stringify(invalidFields))
// 所有子组件参与校验,使用 for 可以使用 awiat
for (let i in childrens) {
const child = childrens[i]
let name = realName(child.name);
const result = await child.onFieldChange(tempFormData[name]);
if (result) {
results.push(result);
// toast ,modal 只需要执行第一次就可以
if (this.errShowType === 'toast' || this.errShowType === 'modal') break;
}
}
} else {
newFormData = invalidFields
}
if (Array.isArray(results)) {
if (results.length === 0) results = null;
}
if (Array.isArray(keepitem)) {
keepitem.forEach(v => {
newFormData[v] = this.dataValue[v];
let vName = realName(v);
let value = getDataValue(v, this.localData)
if (value !== undefined) {
tempFormData[vName] = value
}
});
}
// TODO submit 即将废弃
if (type === 'submit') {
this.$emit('submit', {
detail: {
value: newFormData,
value: tempFormData,
errors: results
}
});
......@@ -344,129 +361,37 @@
this.$emit('validate', results);
}
callback && typeof callback === 'function' && callback(results, newFormData);
// const resetFormData = rawData(tempFormData, this.localData, this.name)
let resetFormData = {}
resetFormData = rawData(tempFormData, this.name)
callback && typeof callback === 'function' && callback(results, resetFormData);
if (promise && callback) {
return promise;
} else {
return null;
}
},
submitForm() {},
/**
* 外部调用方法
* 手动提交校验表单
* 对整个表单进行校验的方法,参数为一个回调函数。
*/
submit(keepitem, callback, type) {
for (let i in this.dataValue) {
const itemData = this.childrens.find(v => v.name === i);
if (itemData) {
if (this.formData[i] === undefined) {
this.formData[i] = this._getValue(i, this.dataValue[i]);
}
}
}
if (!type) {
console.warn('submit 方法即将废弃,请使用validate方法代替!');
}
return this.validateAll(this.formData, 'submit', keepitem, callback);
},
/**
* 外部调用方法
* 校验表单
* 对整个表单进行校验的方法,参数为一个回调函数。
*/
validate(keepitem, callback) {
return this.submit(keepitem, callback, true);
},
/**
* 部分表单校验
* @param {Object} props
* @param {Object} cb
* 返回validate事件
* @param {Object} result
*/
validateField(props, callback) {
props = [].concat(props);
let invalidFields = {};
this.childrens.forEach(item => {
if (props.indexOf(item.name) !== -1) {
invalidFields = Object.assign({}, invalidFields, {
[item.name]: this.formData[item.name]
});
}
});
return this.validateAll(invalidFields, 'submit', [], callback);
validateCheck(result) {
this.$emit('validate', result);
},
/**
* 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
*/
resetFields() {
this.resetForm();
},
/**
* 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果
*/
clearValidate(props) {
props = [].concat(props);
this.childrens.forEach(item => {
const inputComp = this.inputChildrens.find(child => child.rename === item.name);
if (props.length === 0) {
item.errMsg = '';
if (inputComp) {
inputComp.errMsg = '';
}
} else {
if (props.indexOf(item.name) !== -1) {
item.errMsg = '';
if (inputComp) {
inputComp.errMsg = '';
}
}
}
});
},
/**
* 把 value 转换成指定的类型
* @param {Object} key
* @param {Object} value
*/
_getValue(key, value) {
const rules = (this.formRules[key] && this.formRules[key].rules) || [];
const isRuleNum = rules.find(val => val.format && this.type_filter(val.format));
const isRuleBool = rules.find(val => (val.format && val.format === 'boolean') || val.format === 'bool');
// 输入值为 number
if (isRuleNum) {
value = isNaN(value) ? value : value === '' || value === null ? null : Number(value);
}
// 简单判断真假值
if (isRuleBool) {
value = !value ? false : true;
}
return value;
},
/**
* 过滤数字类型
* @param {Object} format
*/
type_filter(format) {
return format === 'int' || format === 'double' || format === 'number' || format === 'timestamp';
}
_getValue: getValue,
_isRequiredField: isRequiredField,
_setDataValue: setDataValue,
_getDataValue: getDataValue,
_realName: realName,
_isRealName: isRealName,
_isEqual: isEqual
}
};
</script>
<style lang="scss" >
.uni-forms {
// overflow: hidden;
// padding: 10px 15px;
}
.uni-forms--top {
// padding: 10px 15px;
// padding-top: 22px;
}
<style lang="scss">
.uni-forms {}
</style>
{
"id": "uni-forms",
"displayName": "uni-forms 表单",
"version": "1.3.2",
"version": "1.4.8",
"description": "由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据",
"keywords": [
"uni-ui",
......@@ -17,11 +17,7 @@
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
......@@ -38,7 +34,8 @@
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [
......@@ -74,7 +71,8 @@
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
"QQ": "y",
"京东": "u"
},
"快应用": {
"华为": "u",
......
## 1.0.14(2022-09-16)
- 修改 配置项`isAdmin`默认值为`false`
## 1.0.13(2022-09-16)
- 新增 管理员注册页面
- 新增 配置项`isAdmin`区分是否为管理端
- 新增 登录成功后自动跳转;跳转优先级:路由携带(`uniIdRedirectUrl`参数) > 返回上一路由 > 跳转首页
- uni-id-co 优化 注册管理员时管理员存在提示文案
## 1.0.12(2022-09-07)
- 修复 getSupportedLoginType判断是否支持微信公众号、PC网页微信扫码登录方式报错的Bug
- 优化 适配pc端样式
......
import config from '../config'
const uniIdCo = uniCloud.importObject("uni-id-co")
export default {
async logout() {
......@@ -5,7 +6,7 @@ export default {
uni.removeStorageSync('uni_id_token');
uni.setStorageSync('uni_id_token_expired', 0)
uni.redirectTo({
url: '/uni_modules/uni-id-pages/pages/login/login-withoutpwd',
url: config.isAdmin ? '/uni_modules/uni-id-pages/pages/login/login-withpwd': '/uni_modules/uni-id-pages/pages/login/login-withoutpwd',
});
uni.$emit('uni-id-pages-logout')
},
......
......@@ -4,6 +4,7 @@ let mixin = {
data() {
return {
config,
uniIdRedirectUrl: '',
isMounted: false
}
},
......@@ -38,6 +39,10 @@ let mixin = {
}, 'weixin')
})
}
if (e.uniIdRedirectUrl) {
this.uniIdRedirectUrl = decodeURIComponent(e.uniIdRedirectUrl)
}
},
computed: {
needAgreements() {
......@@ -70,7 +75,10 @@ let mixin = {
},
methods: {
loginSuccess(e) {
loginSuccess(e)
loginSuccess({
...e,
uniIdRedirectUrl: this.uniIdRedirectUrl
})
}
}
}
......
import pagesJson from '@/pages.json'
export default function(e = {}) {
const {
showToast = true, toastText = '登录成功', autoBack = true
showToast = true, toastText = '登录成功', autoBack = true, uniIdRedirectUrl = ''
} = e
console.log({
toastText,
......@@ -23,6 +25,11 @@ export default function(e = {}) {
}
})
console.log('判断需要返回几层:',pages, delta);
if (uniIdRedirectUrl) {
return uni.reLaunch({
url: uniIdRedirectUrl
})
}
// #ifdef H5
if(e.loginType == 'weixin'){
console.log('window.history',window.history);
......@@ -30,6 +37,13 @@ export default function(e = {}) {
}
// #endif
if (delta) {
const page = pagesJson.pages[0]
return uni.reLaunch({
url: `/${page.path}`
})
}
uni.navigateBack({
delta
})
......
......@@ -46,8 +46,8 @@
watch: {
src:{
handler(src) {
console.log(src);
console.log(src.substring(0, 8));
// console.log(src);
// console.log(src.substring(0, 8));
if (src&&src.substring(0, 8) == "cloud://") {
uniCloud.getTempFileURL({
fileList: [src]
......
......@@ -5,6 +5,7 @@ export default {
登录类型 未列举到的或运行环境不支持的,将被自动隐藏。
如果需要在不同平台有不同的配置,直接用条件编译即可
*/
"isAdmin": false, // 区分管理端与用户端
"loginTypes": [
// "qq",
// "xiaomi",
......
{
"id": "uni-id-pages",
"displayName": "uni-id-pages",
"version": "1.0.10",
"version": "1.0.14",
"description": "云端一体简单、统一、可扩展的用户中心页面模版",
"keywords": [
"用户管理",
......
......@@ -21,11 +21,12 @@
<button class="uni-btn" type="primary" @click="pwdLogin">登录</button>
<!-- 忘记密码 -->
<view class="link-box">
<view>
<view v-if="!config.isAdmin">
<text class="forget">忘记了?</text>
<text class="link" @click="toRetrievePwd">找回密码</text>
</view>
<text class="link" @click="toRegister">注册账号</text>
<text class="link" @click="toRegister">{{config.isAdmin ? '注册管理员账号': '注册账号'}}</text>
<!-- <text class="link" @click="toRegister" v-if="!config.isAdmin">注册账号</text> -->
</view>
<!-- 悬浮登录方式组件 -->
<uni-id-pages-fab-login ref="uniFabLogin"></uni-id-pages-fab-login>
......@@ -33,13 +34,13 @@
</template>
<script>
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
const uniIdCo = uniCloud.importObject("uni-id-co",{
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
const uniIdCo = uniCloud.importObject("uni-id-co",{
errorOptions:{
type:'toast'
}
})
export default {
})
export default {
mixins: [mixin],
data() {
return {
......@@ -129,36 +130,38 @@
})
},
/* 前往注册 */
toRegister(e) {
console.log(e);
toRegister() {
uni.navigateTo({
url: '/uni_modules/uni-id-pages/pages/register/register'
})
url: this.config.isAdmin ? '/uni_modules/uni-id-pages/pages/register/register-admin': '/uni_modules/uni-id-pages/pages/register/register',
fail(e) {
console.error(e);
}
})
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
@media screen and (min-width: 690px) {
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
@media screen and (min-width: 690px) {
}
.forget{
}
.forget{
font-size: 12px;
color: #8a8f8b;
}
}
.link-box {
.link-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
margin-top: 20px;
}
}
.link {
.link {
font-size: 12px;
}
}
</style>
<!-- 创建超级管理员 -->
<template>
<view class="uni-content">
<match-media :min-width="690">
<view class="login-logo">
<image :src="logo"></image>
</view>
<!-- 顶部文字 -->
<text class="title title-box">创建超级管理员</text>
</match-media>
<uni-forms ref="form" :value="formData" :rules="rules" validate-trigger="submit" err-show-type="toast">
<uni-forms-item name="username" required>
<uni-easyinput :inputBorder="false" :focus="focusUsername" @blur="focusUsername = false"
class="input-box" placeholder="请输入用户名" v-model="formData.username" trim="both" />
</uni-forms-item>
<uni-forms-item name="nickname">
<uni-easyinput :inputBorder="false" :focus="focusNickname" @blur="focusNickname = false" class="input-box" placeholder="请输入用户昵称" v-model="formData.nickname"
trim="both" />
</uni-forms-item>
<uni-forms-item name="password" v-model="formData.password" required>
<uni-easyinput :inputBorder="false" :focus="focusPassword" @blur="focusPassword = false"
class="input-box" maxlength="20" :placeholder="'请输入' + (config.passwordStrength == 'weak'?'6':'8') + '-16位密码'" type="password"
v-model="formData.password" trim="both" />
</uni-forms-item>
<uni-forms-item name="password2" v-model="formData.password2" required>
<uni-easyinput :inputBorder="false" :focus="focusPassword2" @blur="focusPassword2 =false"
class="input-box" placeholder="再次输入密码" maxlength="20" type="password" v-model="formData.password2"
trim="both" />
</uni-forms-item>
<uni-forms-item>
<uni-captcha ref="captcha" scene="register" v-model="formData.captcha" />
</uni-forms-item>
<uni-id-pages-agreements scope="register" ref="agreements" ></uni-id-pages-agreements>
<button class="uni-btn" type="primary" @click="submit">注册</button>
<button @click="navigateBack" class="register-back">返回</button>
<match-media :min-width="690">
<view class="link-box">
<text class="link" @click="toLogin">已有账号?点此登录</text>
</view>
</match-media>
</uni-forms>
</view>
</template>
<script>
import rules from './validator.js';
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
import config from '@/uni_modules/uni-id-pages/config.js'
const uniIdCo = uniCloud.importObject("uni-id-co", {customUI: true})
export default {
mixins: [mixin],
data() {
return {
formData: {
username: "",
nickname: "",
password: "",
password2: "",
captcha: ""
},
rules,
focusUsername:false,
focusNickname:false,
focusPassword:false,
focusPassword2:false,
logo: "/static/logo.png"
}
},
onReady() {
this.$refs.form.setRules(this.rules)
},
onShow() {
// #ifdef H5
document.onkeydown = event => {
var e = event || window.event;
if (e && e.keyCode == 13) { //回车键的键值为13
this.submit()
}
};
// #endif
},
methods: {
/**
* 触发表单提交
*/
submit() {
this.$refs.form.validate().then((res) => {
if(this.formData.captcha.length != 4){
this.$refs.captcha.focusCaptchaInput = true
return uni.showToast({
title: '请输入验证码',
icon: 'none'
});
}
if (this.needAgreements && !this.agree) {
return this.$refs.agreements.popup(()=>{
this.submitForm(res)
})
}
this.submitForm(res)
}).catch((errors) => {
let key = errors[0].key
key = key.replace(key[0], key[0].toUpperCase())
console.log(key);
this['focus'+key] = true
})
},
submitForm(params) {
uniIdCo.registerAdmin(this.formData).then(e => {
console.log(e);
uni.navigateBack()
})
.catch(e => {
console.log(e);
console.log(e.message);
//更好的体验:登录错误,直接刷新验证码
this.$refs.captcha.getImageCaptcha()
uni.showModal({
title: '提示',
content: e.errMsg || `创建失败: ${e.errCode}`,
showCancel: false
})
})
},
navigateBack() {
uni.navigateBack()
},
toLogin() {
uni.navigateTo({
url: '/uni_modules/uni-id-pages/pages/login/login-withpwd'
})
},
registerByEmail() {
uni.navigateTo({
url: '/uni_modules/uni-id-pages/pages/register/register-by-email'
})
}
}
}
</script>
<style lang="scss">
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
@media screen and (max-width: 690px) {
.uni-content{
margin-top: 15px;
height: 100%;
background-color: #fff;
}
}
@media screen and (min-width: 690px) {
.uni-content{
padding: 30px 40px 60px;
}
.link-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
margin-top: 10px;
}
.link {
font-size: 12px;
}
}
.uni-content ::v-deep .uni-forms-item__label {
position: absolute;
left: -15px;
}
button {
margin-top: 15px;
}
</style>
......@@ -9,6 +9,8 @@
</uni-list-item>
<uni-list-item class="item" @click="bindMobile" title="手机号" :rightText="userInfo.mobile||'未绑定'" link>
</uni-list-item>
<uni-list-item v-if="userInfo.email" class="item" title="电子邮箱" :rightText="userInfo.email">
</uni-list-item>
<uni-list-item v-if="hasPwd" class="item" @click="changePassword" title="修改密码" link>
</uni-list-item>
</uni-list>
......@@ -87,7 +89,7 @@
uni.showLoading({
mask: true
});
usersTable.where("'_id' == $cloudEnv_uid").field('mobile,nickname').get().then(res => {
usersTable.where("'_id' == $cloudEnv_uid").field('mobile,nickname,email').get().then(res => {
console.log({res});
this.userInfo = res.result.data[0]
console.log('this.userInfo', this.userInfo);
......
......@@ -36,7 +36,9 @@ const CAPTCHA_SCENE = {
LOGIN_BY_PWD: 'login-by-pwd',
LOGIN_BY_SMS: 'login-by-sms',
RESET_PWD_BY_SMS: 'reset-pwd-by-sms',
RESET_PWD_BY_EMAIL: 'reset-pwd-by-email',
SEND_SMS_CODE: 'send-sms-code',
SEND_EMAIL_CODE: 'send-email-code',
BIND_MOBILE_BY_SMS: 'bind-mobile-by-sms'
}
......@@ -45,6 +47,7 @@ const LOG_TYPE = {
LOGIN: 'login',
REGISTER: 'register',
RESET_PWD_BY_SMS: 'reset-pwd',
RESET_PWD_BY_EMAIL: 'reset-pwd',
BIND_MOBILE: 'bind-mobile',
BIND_WEIXIN: 'bind-weixin',
BIND_QQ: 'bind-qq',
......@@ -58,6 +61,13 @@ const SMS_SCENE = {
BIND_MOBILE_BY_SMS: 'bind-mobile-by-sms'
}
const EMAIL_SCENE = {
REGISTER: 'register',
LOGIN_BY_EMAIL: 'login-by-email',
RESET_PWD_BY_EMAIL: 'reset-pwd-by-email',
BIND_EMAIL: 'bind-email'
}
module.exports = {
db,
dbCmd,
......@@ -68,5 +78,6 @@ module.exports = {
USER_STATUS,
CAPTCHA_SCENE,
LOG_TYPE,
SMS_SCENE
SMS_SCENE,
EMAIL_SCENE
}
......@@ -15,7 +15,8 @@ const middleware = require('./middleware/index')
const {
registerAdmin,
registerUser
registerUser,
registerUserByEmail
} = require('./module/register/index')
const {
addUser,
......@@ -45,13 +46,15 @@ const {
const {
updatePwd,
resetPwdBySms,
resetPwdByEmail,
closeAccount,
getAccountInfo
} = require('./module/account/index')
const {
createCaptcha,
refreshCaptcha,
sendSmsCode
sendSmsCode,
sendEmailCode
} = require('./module/verify/index')
const {
refreshToken,
......@@ -273,6 +276,17 @@ module.exports = {
* @returns
*/
registerUser,
/**
* 通过邮箱+验证码注册用户
* @param {Object} params
* @param {String} params.email 邮箱
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @param {String} params.code 邮箱验证码
* @param {String} params.inviteCode 邀请码
* @returns
*/
registerUserByEmail,
/**
* 用户名密码登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login
......@@ -433,6 +447,16 @@ module.exports = {
* @returns {object}
*/
resetPwdBySms,
/**
* 通过邮箱验证码重置密码
* @param {object} params
* @param {string} params.email 邮箱
* @param {string} params.code 邮箱验证码
* @param {string} params.password 密码
* @param {string} params.captcha 图形验证码
* @returns {object}
*/
resetPwdByEmail,
/**
* 注销账户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#close-account
......@@ -470,6 +494,16 @@ module.exports = {
* @returns
*/
sendSmsCode,
/**
* 发送邮箱验证码
* @tutorial 需自行实现功能
* @param {Object} params
* @param {String} params.email 邮箱
* @param {String} params.captcha 图形验证码
* @param {String} params.scene 短信验证码使用场景
* @returns
*/
sendEmailCode,
/**
* 刷新token
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-token
......
module.exports = {
updatePwd: require('./update-pwd'),
resetPwdBySms: require('./reset-pwd-by-sms'),
resetPwdByEmail: require('./reset-pwd-by-email'),
closeAccount: require('./close-account'),
getAccountInfo: require('./get-account-info')
}
const {
ERROR
} = require('../../common/error')
const {
getNeedCaptcha,
verifyCaptcha
} = require('../../lib/utils/captcha')
const {
verifyEmailCode
} = require('../../lib/utils/verify-code')
const {
userCollection,
EMAIL_SCENE,
CAPTCHA_SCENE,
LOG_TYPE
} = require('../../common/constants')
const {
findUser
} = require('../../lib/utils/account')
const PasswordUtils = require('../../lib/utils/password')
/**
* 通过邮箱验证码重置密码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-email
* @param {object} params
* @param {string} params.email 邮箱
* @param {string} params.code 邮箱验证码
* @param {string} params.password 密码
* @param {string} params.captcha 图形验证码
* @returns {object}
*/
module.exports = async function (params = {}) {
const schema = {
email: 'email',
code: 'string',
password: 'password',
captcha: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
email,
code,
password,
captcha
} = params
const needCaptcha = await getNeedCaptcha.call(this, {
email,
type: LOG_TYPE.RESET_PWD_BY_EMAIL
})
if (needCaptcha) {
await verifyCaptcha.call(this, {
captcha,
scene: CAPTCHA_SCENE.RESET_PWD_BY_EMAIL
})
}
try {
// 验证手机号验证码,验证不通过时写入失败日志
await verifyEmailCode({
email,
code,
scene: EMAIL_SCENE.RESET_PWD_BY_EMAIL
})
} catch (error) {
await this.middleware.uniIdLog({
data: {
email
},
type: LOG_TYPE.RESET_PWD_BY_EMAIL,
success: false
})
throw error
}
// 根据手机号查找匹配的用户
const userMatched = await findUser.call(this, {
userQuery: {
email
},
authorizedApp: [this.getClientInfo().appId]
})
if (userMatched.length === 0) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
} else if (userMatched.length > 1) {
throw {
errCode: ERROR.ACCOUNT_CONFLICT
}
}
const { _id: uid } = userMatched[0]
const {
passwordHash,
version
} = new PasswordUtils({
passwordSecret: this.config.passwordSecret
}).generatePasswordHash({
password
})
// 更新用户密码
await userCollection.doc(uid).update({
password: passwordHash,
password_secret_version: version,
valid_token_date: Date.now()
})
// 写入成功日志
await this.middleware.uniIdLog({
data: {
email
},
type: LOG_TYPE.RESET_PWD_BY_SMS
})
return {
errCode: 0
}
}
module.exports = {
registerUser: require('./register-user'),
registerAdmin: require('./register-admin')
registerAdmin: require('./register-admin'),
registerUserByEmail: require('./register-user-by-email')
}
......@@ -38,7 +38,8 @@ module.exports = async function (params = {}) {
}).limit(1).get()
if (getAdminRes.data.length > 0) {
return {
errCode: ERROR.ADMIN_EXISTS
errCode: ERROR.ADMIN_EXISTS,
errMsg: this.t('uni-id-admin-exists')
}
}
const {
......
const {
postRegister,
preRegisterWithPassword
} = require('../../lib/utils/register')
const {
verifyCaptcha
} = require('../../lib/utils/captcha')
const {
CAPTCHA_SCENE,
EMAIL_SCENE,
LOG_TYPE
} = require('../../common/constants')
const {
verifyEmailCode
} = require('../../lib/utils/verify-code')
/**
* 通过邮箱+验证码注册普通用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-user-by-email
* @param {Object} params
* @param {String} params.email 邮箱
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @param {String} params.code 邮箱验证码
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
email: 'email',
password: 'password',
nickname: {
required: false,
type: 'nickname'
},
code: 'string',
inviteCode: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
email,
password,
nickname,
code,
inviteCode
} = params
try {
// 验证邮箱验证码,验证不通过时写入失败日志
await verifyEmailCode({
email,
code,
scene: EMAIL_SCENE.REGISTER
})
} catch (error) {
await this.middleware.uniIdLog({
data: {
email
},
type: LOG_TYPE.REGISTER,
success: false
})
throw error
}
const {
user,
extraData
} = await preRegisterWithPassword.call(this, {
user: {
email
},
password
})
return postRegister.call(this, {
user,
extraData: {
...extraData,
nickname,
email_confirmed: 1
},
inviteCode
})
}
const {
verifyCaptcha
} = require('../../lib/utils/captcha')
const {
EMAIL_SCENE
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
/**
* 发送邮箱验证码,可用于登录、注册、绑定邮箱、修改密码等操作
* @tutorial
* @param {Object} params
* @param {String} params.email 邮箱
* @param {String} params.captcha 图形验证码
* @param {String} params.scene 使用场景
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现,欢迎向我们提交pr
throw new Error('api[sendEmailCode] is not yet implemented')
const schema = {
email: 'email',
captcha: 'string',
scene: 'string'
}
this.middleware.validate(params, schema)
const {
email,
captcha,
scene
} = params
if (!(Object.values(EMAIL_SCENE).includes(scene))) {
throw {
errCode: ERROR.INVALID_PARAM
}
}
await verifyCaptcha.call(this, {
scene: 'send-email-code',
captcha
})
// -- 测试代码
require('../../lib/utils/verify-code')
.setEmailVerifyCode.call(this, {
email,
code: '123456',
expiresIn: 180,
scene
})
return {
errCode: 'uni-id-invalid-mail-template',
errMsg: `已启动测试模式,直接使用:123456作为邮箱验证码即可。\n如果是正式项目,需自行实现发送邮件的相关功能`
}
// -- 测试代码
//发送邮件--需自行实现
}
{
"name": "uni-id-co",
"version": "1.0.12",
"version": "1.0.13",
"description": "",
"main": "index.js",
"keywords": [],
......
......@@ -77,7 +77,7 @@
},
"network_model": {
"bsonType": "string",
"description": "设备网络型号wifi/3G/4G/"
"description": "设备网络型号wifi\/3G\/4G\/"
},
"window_width": {
"bsonType": "string",
......
......@@ -2,7 +2,7 @@
"bsonType": "object",
"permission": {
"update": "doc._id == auth.uid",
"read": true
"read": "doc._id == auth.uid"
},
"properties": {
"_id": {
......
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## 1.0.3(2022-09-06)
- 修复 过期时间问题,容错 AccessToken 默认 fallback 逻辑,当微信服务器没有返回过期时间时设置为2小时后过期
## 1.0.2(2022-09-02)
- 新增 依赖数据表schema opendb-open-data
## 1.0.0(2022-08-22)
- 首次发布
{
"id": "uni-open-bridge-common",
"displayName": "uni-open-bridge-common",
"version": "1.0.0",
"version": "1.0.3",
"description": "统一接管微信等三方平台认证凭据",
"keywords": [
"uni-open-bridge-common",
......
......@@ -43,7 +43,7 @@ class AccessToken extends Storage {
const responseData = await WeixinServer[methodName](oauthConfig)
const duration = responseData.expires_in
const duration = responseData.expires_in || (60 * 60 * 2)
delete responseData.expires_in
return {
......
......@@ -61,7 +61,7 @@ class DatabaseCache {
value = this._serializeValue(value)
await this.collection.doc(key).set({
value,
expired: duration && duration !== -1 ? Date.now() + duration : -1
expired: duration && duration !== -1 ? Date.now() + (duration * 1000) : -1
})
}
......
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": ["_id", "value"],
"properties": {
"_id": {
"bsonType": "string",
"description": "key,格式:uni-id:[dcloudAppid]:[platform]:[openid]:[access-token|user-access-token|session-key|encrypt-key-version|ticket]"
},
"value": {
"bsonType": "object",
"description": "字段_id对应的值"
},
"expired": {
"bsonType": "date",
"description": "过期时间"
}
}
}
## 0.5.1(2022-07-06)
- 修复 上版带出云函数不存在的Bug
- 升级 uni-admin 大于等于 1.9.0 务必更新至此版本。uni-admin 版本小于 1.9.0 请不要更新,历史版本在 Gitee 有发行版。后续 uni-admin 会集成升级中心
## 0.5.0(2022-07-05)
- 修复 版本列表默认显示全部版本的Bug
- 升级 uni-admin 1.9.0 务必更新至此版本。uni-admin 版本小于 1.9.0 请不要更新,后续 uni-admin 会集成升级中心
## 0.4.2(2021-12-07)
- 更新 优化 list 页面显示,修复 list 页面报错
## 0.4.1(2021-12-01)
- 修复 0.4.0版本带出来,发布新版时 appid、name 不会自动填充的Bug
## 0.4.0(2021-11-26)
- 更新 升级中心移除应用管理,现在由uni-admin接管。旧版本若没有应用管理请做升级处理
## 0.3.0(2021-11-18)
- 兼容 uni-admin 新版内置 $request 函数改动
## 0.2.2(2021-09-06)
- 解决 opendb-app-list表对应的schema名称冲突的问题
## 0.2.1(2021-09-03)
- 修复 一个在添加菜单时报错,createdate不与默认值匹配 的Bug
## 0.2.0(2021-08-25)
- 兼容vue3.0
## 0.1.9(2021-08-13)
- 更新 uni-forms使用validate校验字段
- 修复 报错dirty_data、create_date在数据库中并不存在
## 0.1.8(2021-08-09)
- 修复 默认配置项配置错误
## 0.1.7(2021-08-09)
- 移除测试时配置项
## 0.1.6(2021-08-09)
- 修复 修改版本信息时,上传时间丢失问题
## 0.1.5(2021-07-21)
- 更新 :value.sync 改为 :value 和 @update:value
## 0.1.4(2021-07-13)
- 修复 uni-easyinput去除输入字符长度限制
- 更新文档 关于 uni-id缺少配置信息 错误。请查看安装指引第13条
## 0.1.3(2021-06-15)
- 修复 wgt更新某些情况下获取数据错误
## 0.1.2(2021-06-04)
- 修复 上传包时根据平台筛选文件
- 更新 文档
## 0.1.1(2021-05-18)
- 更新uni-table中uni-tr组件的selectable属性为disabled
## 0.1.0(2021-04-07)
- 更新版本对比函数 compare
## 0.0.6(2021-04-01)
- 调整db_init.json
## 0.0.5(2021-03-25)
- 调整为uni_modules目录
- 升级中心后台管理系统拆分为 Admin 后台管理 和 前台检查更新(uni-upgrade-center-app)
// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
const validator = {
"appid": {
"rules": [
{
"required": true
},
{
"format": "string"
}
],
"label": "AppID"
},
"name": {
"rules": [
{
"required": true
},
{
"format": "string"
}
],
"label": "应用名称"
},
"description": {
"rules": [
{
"required": true
},
{
"format": "string"
}
],
"label": "应用描述"
}
}
const enumConverter = {}
export { validator, enumConverter }
// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
const validator = {
"appid": {
"rules": [{
"required": true
},
{
"format": "string"
}
],
"label": "AppID"
},
"name": {
"rules": [{
"format": "string"
}],
"label": "应用名称"
},
"title": {
"rules": [{
"format": "string"
}],
"label": "更新标题"
},
"contents": {
"rules": [{
"required": true
},
{
"format": "string"
}
],
"label": "更新内容"
},
"platform": {
"rules": [{
"required": true
},
/* 此处不校验数据类型,因为platform在发布app端是单选,在发布wgt时可能是多选
{
"format": "array"
}, */
{
"range": [{
"value": "Android",
"text": "安卓"
},
{
"value": "iOS",
"text": "苹果"
}
]
}
],
"label": "平台"
},
"type": {
"rules": [{
"required": true
}, {
"format": "string"
},
{
"range": [{
"value": "native_app",
"text": "原生App安装包"
},
{
"value": "wgt",
"text": "wgt资源包"
}
]
}
],
"label": "安装包类型"
},
"version": {
"rules": [{
"required": true
},
{
"format": "string"
}
],
"label": "版本号"
},
"min_uni_version": {
"rules": [{
"format": "string"
}],
"label": "原生App最低版本"
},
"url": {
"rules": [{
"required": true
}, {
"format": "string"
}],
"label": "包地址"
},
"stable_publish": {
"rules": [{
"format": "bool"
}],
"label": "上线发行"
},
"create_date": {
"rules": [{
"format": "timestamp"
}],
"label": "上传时间"
},
"is_silently": {
"rules": [{
"format": "bool"
}],
"label": "静默更新",
"defaultValue": false
},
"is_mandatory": {
"rules": [{
"format": "bool"
}],
"label": "强制更新",
"defaultValue": false
}
}
const enumConverter = {
"platform_valuetotext": [{
"value": "Android",
"text": "安卓"
},
{
"value": "iOS",
"text": "苹果"
}
],
"type_valuetotext": {
"native_app": "原生App安装包",
"wgt": "wgt资源包"
}
}
export {
validator,
enumConverter
}
[{
"menu_id": "system_update",
"name": "升级中心",
"icon": "uni-icons-cloud-upload",
"url": "uni_modules/uni-upgrade-center/pages/version/list",
"sort": 1050,
"parent_id": "system_management",
"permission": [],
"enable": true
}]
{
"id": "uni-upgrade-center",
"displayName": "升级中心 uni-upgrade-center - Admin",
"version": "0.5.1",
"description": "uni升级中心 - 后台管理系统",
"keywords": [
"uniCloud",
"admin",
"update",
"升级",
"wgt"
],
"repository": "https://gitee.com/dcloud/uni-upgrade-center/tree/master/uni_modules/uni-upgrade-center",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"category": [
"uniCloud",
"Admin插件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uni-data-checkbox",
"uni-data-picker",
"uni-dateformat",
"uni-easyinput",
"uni-file-picker",
"uni-forms",
"uni-icons",
"uni-pagination",
"uni-table"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}
<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 {
width: 200px;
position: absolute;
padding: 5px 10px;
background-color: $main_back_color;
color: $main_color;
border-radius: 4px;
border: 1px solid #e9e9eb;
z-index: 99999;
}
</style>
import {
validator,
enumConverter
} from '@/uni_modules/uni-upgrade-center/js_sdk/validator/opendb-app-versions.js';
const platform_iOS = 'iOS';
const platform_Android = 'Android';
function getValidator(fields) {
let reuslt = {}
for (let key in validator) {
if (fields.includes(key)) {
reuslt[key] = validator[key]
}
}
return reuslt
}
export const fields =
'appid,name,title,contents,platform,type,version,min_uni_version,url,stable_publish,is_silently,is_mandatory,create_date'
export default {
data() {
return {
labelWidth: '80px',
enableiOSWgt: true, // 是否开启iOS的wgt更新
silentlyContent: '静默更新:App升级时会在后台下载wgt包并自行安装。新功能在下次启动App时生效',
mandatoryContent: '强制更新:App升级弹出框不可取消',
stablePublishContent: '同时只可有一个线上发行版,线上发行不可更设为下线。\n未上线可以设为上线发行并自动替换当前线上发行版',
stablePublishContent2: '使用本包替换当前线上发行版',
uploadFileContent: '可下载安装包地址。上传文件到云存储自动填写,也可以手动填写',
minUniVersionContent: '上次使用新Api或打包新模块的App版本',
priorityContent: '检查更新时,按照优先级从大到小依次尝试跳转商店。如果都跳转失败,则会打开浏览器使用下载链接下载apk安装包',
latestStableData: [], // 库中最新已上线版
appFileList: null, // 上传包
type_valuetotext: enumConverter.type_valuetotext,
preUrl: '',
formData: {
"appid": "",
"name": "",
"title": "",
"contents": "",
"platform": [],
"type": "",
"version": "",
"min_uni_version": "",
"url": "",
"stable_publish": false,
"create_date": null
},
formOptions: {
"platform_localdata": [{
"value": "Android",
"text": "安卓"
},
{
"value": "iOS",
"text": "苹果"
}
],
"type_localdata": [{
"value": "native_app",
"text": "原生App安装包"
},
{
"value": "wgt",
"text": "App资源包"
}
]
},
rules: {
...getValidator(["appid", "contents", "platform", "type", "version", "min_uni_version", "url",
"stable_publish",
"title", "name", "is_silently", "is_mandatory"
])
}
}
},
onReady() {
this.$refs.form.setRules(this.rules)
},
computed: {
isWGT() {
return this.formData.type === 'wgt'
},
isiOS() {
return !this.isWGT ? this.formData.platform.includes(platform_iOS) : false;
},
hasPackage() {
return this.appFileList && !!Object.keys(this.appFileList).length
},
fileExtname() {
return this.isWGT ? ['wgt'] : ['apk']
},
platformLocaldata() {
return !this.isWGT ? this.formOptions.platform_localdata : this.enableiOSWgt ? this.formOptions
.platform_localdata : [this.formOptions.platform_localdata[0]]
},
uni_platform() {
return (this.isiOS ? platform_iOS : platform_Android).toLocaleLowerCase()
}
},
methods: {
packageUploadSuccess(res) {
uni.showToast({
icon: 'success',
title: '上传成功',
duration: 800
})
this.preUrl = this.formData.url
this.formData.url = res.tempFilePaths[0]
},
async packageDelete(res) {
if (!this.hasPackage) return;
let [deleteRes] = await this.$request('deleteFile', {
fileList: [res.tempFilePath]
}, {
functionName: 'upgrade-center'
})
if (deleteRes.success) {
uni.showToast({
icon: 'success',
title: '删除成功',
duration: 800
})
this.formData.url = this.preUrl
this.$refs.form.clearValidate('url')
}
},
selectFile() {
if (this.hasPackage) {
uni.showToast({
icon: 'none',
title: '只可上传一个文件,请删除已上传后重试',
duration: 1000
});
}
},
createCenterRecord(value) {
return {
...value,
uni_platform: this.uni_platform,
create_env: 'upgrade-center'
}
},
createCenterQuery({
appid
}) {
return {
appid,
create_env: 'upgrade-center'
}
},
createStatQuery({
appid,
type,
version,
uni_platform
}) {
return {
appid,
type,
version,
uni_platform: uni_platform ? uni_platform : this.uni_platform,
create_env: 'uni-stat',
stable_publish: false
}
}
}
}
// 判断arr是否为一个数组,返回一个bool值
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
// 深度克隆
export function deepClone(obj) {
// 对常见的“非”值,直接返回原来值
if ([null, undefined, NaN, false].includes(obj)) return obj;
if (typeof obj !== "object" && typeof obj !== 'function') {
//原始类型直接返回
return obj;
}
var o = isArray(obj) ? [] : {};
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
}
}
return o;
}
export const appListDbName = 'opendb-app-list'
export const appVersionListDbName = 'opendb-app-versions'
// 版本列表默认显示应用Appid
export const defaultDisplayApp = ''
<template>
<view class="uni-container">
<view class="uni-header">
<view class="uni-group">
<view class="uni-title">包类型</view>
<view class="uni-sub-title">{{type_valuetotext[formData.type]}}</view>
</view>
</view>
<uni-forms ref="form" :value="formData" validateTrigger="bind" :labelWidth="labelWidth">
<uni-forms-item name="appid" label="AppID" required>
<uni-easyinput :disabled="true" v-model="formData.appid" trim="both" />
</uni-forms-item>
<uni-forms-item name="name" label="应用名称">
<uni-easyinput :disabled="true" v-model="formData.name" trim="both" />
</uni-forms-item>
<uni-forms-item name="title" label="更新标题">
<uni-easyinput placeholder="更新标题" v-model="formData.title" />
</uni-forms-item>
<uni-forms-item name="contents" label="更新内容" required>
<textarea auto-height style="box-sizing: content-box;"
@input="binddata('contents', $event.detail.value)" class="uni-textarea-border"
:value="formData.contents" @update:value="val => formData.contents = val"></textarea>
</uni-forms-item>
<uni-forms-item name="platform" label="平台" required>
<uni-data-checkbox :multiple="isWGT" v-model="formData.platform" :localdata="platformLocaldata" />
</uni-forms-item>
<uni-forms-item name="version" label="版本号" required>
<uni-easyinput v-model="formData.version" placeholder="当前包版本号,必须大于当前线上发行版本号" />
</uni-forms-item>
<uni-forms-item v-if="isWGT" name="min_uni_version" label="原生App最低版本" :required="isWGT">
<uni-easyinput placeholder="原生App最低版本" v-model="formData.min_uni_version" />
<show-info :content="minUniVersionContent"></show-info>
</uni-forms-item>
<uni-forms-item v-if="!isiOS" label="上传apk包">
<uni-file-picker v-model="appFileList" :file-extname="fileExtname" :disabled="hasPackage"
returnType="object" file-mediatype="all" limit="1" @success="packageUploadSuccess"
@delete="packageDelete">
<view class="flex">
<button type="primary" size="mini" @click="selectFile" style="margin: 0;">选择文件</button>
<text
style="padding: 10px;font-size: 12px;color: #666;">上传apk到当前服务空间的云存储中,上传成功后,会自动使用云存储地址填充下载链接</text>
</view>
</uni-file-picker>
<text v-if="hasPackage"
style="padding-left: 20px;color: #a8a8a8;">{{Number(appFileList.size / 1024 / 1024).toFixed(2)}}M</text>
</uni-forms-item>
<uni-forms-item name="url" :label="isiOS ? 'AppStore' : '包地址'" required>
<uni-easyinput placeholder="可下载安装包地址" v-model="formData.url" :maxlength="-1" />
<show-info :top="-80" :content="uploadFileContent"></show-info>
</uni-forms-item>
<!-- <uni-forms-item v-if="formData.store_list" name="store_list" style="height: 0px;"></uni-forms-item> -->
<uni-forms-item v-if="isWGT" name="is_silently" label="静默更新">
<switch @change="binddata('is_silently', $event.detail.value)" :checked="formData.is_silently" />
<show-info :top="-80" :content="silentlyContent"></show-info>
</uni-forms-item>
<uni-forms-item v-if="!isiOS" name="is_mandatory" label="强制更新">
<switch @change="binddata('is_mandatory', $event.detail.value)" :checked="formData.is_mandatory" />
<show-info :content="mandatoryContent"></show-info>
</uni-forms-item>
<uni-forms-item name="stable_publish" label="上线发行">
<switch @change="binddata('stable_publish', $event.detail.value)" :checked="formData.stable_publish" />
<show-info :top="-40" :content="stablePublishContent2"></show-info>
</uni-forms-item>
<uni-forms-item v-show="false" name="type" label="安装包类型">
<uni-data-checkbox v-model="formData.type" :localdata="formOptions.type_localdata" />
</uni-forms-item>
<view class="uni-button-group">
<button type="primary" class="uni-button" style="width: 100px;" @click="submit">发布</button>
<button type="warn" class="uni-button" style="width: 100px;margin-left: 15px;" @click="back">取消</button>
</view>
</uni-forms>
</view>
</template>
<script>
import {
validator,
enumConverter
} from '@/uni_modules/uni-upgrade-center/js_sdk/validator/opendb-app-versions.js';
import addAndDetail, {
fields
} from '../mixin/version_add_detail_mixin.js';
import {
appVersionListDbName
} from '../utils.js';
import showInfo from '../components/show-info.vue'
const db = uniCloud.database();
const dbCmd = db.command;
const dbCollectionName = appVersionListDbName;
const platform_iOS = 'iOS';
const platform_Android = 'Android';
/**
* 对比版本号,如需要,请自行修改判断规则
* 支持比对 ("3.0.0.0.0.1.0.1", "3.0.0.0.0.1") ("3.0.0.1", "3.0") ("3.1.1", "3.1.1.1") 之类的
* @param {Object} v1
* @param {Object} v2
* v1 > v2 return 1
* v1 < v2 return -1
* v1 == v2 return 0
*/
function compare(v1 = '0', v2 = '0') {
v1 = String(v1).split('.')
v2 = String(v2).split('.')
const minVersionLens = Math.min(v1.length, v2.length);
let result = 0;
for (let i = 0; i < minVersionLens; i++) {
const curV1 = Number(v1[i])
const curV2 = Number(v2[i])
if (curV1 > curV2) {
result = 1
break;
} else if (curV1 < curV2) {
result = -1
break;
}
}
if (result === 0 && (v1.length !== v2.length)) {
const v1BiggerThenv2 = v1.length > v2.length;
const maxLensVersion = v1BiggerThenv2 ? v1 : v2;
for (let i = minVersionLens; i < maxLensVersion.length; i++) {
const curVersion = Number(maxLensVersion[i])
if (curVersion > 0) {
v1BiggerThenv2 ? result = 1 : result = -1
break;
}
}
}
return result;
}
export default {
components: {
showInfo
},
mixins: [addAndDetail],
data() {
return {
latestVersion: '0.0.0',
lastVersionId: ''
}
},
async onLoad({
appid,
name,
type
}) {
if (appid && type && name) {
// const store_list = await this.getStoreList(appid)
this.formData = {
...this.formData,
...{
appid,
name,
type,
// store_list
}
}
this.latestStableData = await this.getDetail(appid, type)
// 如果有数据,否则为发布第一版,默认为Android
if (!this.isWGT && this.latestStableData.length) {
this.setFormData(platform_Android)
}
// 如果是wgt ,则需要将 min_uni_version 设为必填
if (this.isWGT) {
this.rules.min_uni_version.rules.push({
"required": true
})
}
}
},
watch: {
isiOS(val) {
if (val) {
this.setFormData(platform_iOS)
return;
} else if (this.hasPackage) {
this.formData.url = this.appFileList.url
return;
}
this.formData.url = ''
},
"formData.platform"(val) {
// wgt热更新数据渲染
if (this.isWGT) {
this.setFormData(val)
}
}
},
methods: {
setFormData(os) {
uni.showLoading({
mask: true
})
// 每次需初始化 版本 与 id ,因为可能是新增第一版
this.latestVersion = '0.0.0';
this.lastVersionId = ''
const data = this.getData(this.latestStableData, os)[0]
if (data) {
const {
_id,
version,
name,
platform,
min_uni_version,
url
} = data
this.lastVersionId = _id
this.latestVersion = version;
this.formData.name = name
// 如果不是wgt,则需要删除 min_uni_version 字段
if (!this.isWGT) {
delete this.formData.min_uni_version;
this.formData.platform = platform[0]
// iOS需要带出上一版本的AppStore链接
if (this.isiOS) {
this.formData.url = url;
}
} else {
this.formData.min_uni_version = min_uni_version
// this.formData.platform = [os]
}
} else if (this.isWGT) {
this.formData.min_uni_version = ''
}
uni.hideLoading()
},
/**
* 触发表单提交
*/
submit() {
if (!this.formData.url && this.isiOS) {
uni.showToast({
icon: 'error',
title: 'AppStore 链接必填'
})
return
}
uni.showLoading({
mask: true
})
this.$refs.form.validate().then((res) => {
// TODO 兼容 uni-forms 的bug
res.url = this.formData.url
if (compare(this.latestVersion, res.version) >= 0) {
uni.showModal({
content: `版本号必须大于当前已上线版本(${this.latestVersion})`,
showCancel: false
})
throw new Error('版本号必须大于已上线版本(${this.latestVersion})');
return;
}
// 如果不是 wgt 更新,则需将 platform 字段还原为 array
if (!this.isWGT) {
res.platform = [res.platform]
}
if (this.isiOS || this.isWGT) delete res.store_list;
if (res.store_list) {
res.store_list.forEach(item => {
item.priority = parseFloat(item.priority)
})
}
this.submitForm(res)
}).catch((errors) => {
uni.hideLoading()
})
},
async submitForm(value) {
value = this.createCenterRecord(value)
const collectionDB = db.collection(dbCollectionName)
// uni-stat 会创建这些字段 appid
let recordCreateByUniStat = []
if (!this.isWGT) {
recordCreateByUniStat = await this.getDetail(value.appid, value.type, this.createStatQuery(value))
}
let dbOperate
if (!recordCreateByUniStat.length) {
dbOperate = collectionDB.add(value)
} else {
value.create_date = Date.now()
dbOperate = collectionDB.doc(recordCreateByUniStat[0]._id).update(value)
}
// 使用 clientDB 提交数据
dbOperate.then(async (res) => {
// 如果新增版本为上线发行,且之前有该平台的上线发行,则自动将上一版设为下线
if (value.stable_publish && this.lastVersionId) {
await collectionDB.doc(this.lastVersionId).update({
stable_publish: false
})
}
uni.showToast({
title: '新增成功'
})
this.getOpenerEventChannel().emit('refreshData')
setTimeout(() => uni.navigateBack(), 500)
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
/**
* 获取表单数据
* @param {Object} id
*/
getDetail(appid, type, args = {}) {
uni.showLoading({
mask: true
})
return db.collection(dbCollectionName)
.where(
Object.assign({
appid,
type,
stable_publish: true
}, args)
)
.field(fields)
.get()
.then((res) => res.result.data)
.catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
getStoreList(appid) {
return db.collection('opendb-app-list')
.where({
appid
})
.get()
.then(res => {
const data = res.result.data[0]
return data.store_list || []
})
},
getData(data = [], platform) {
if (typeof platform === 'string') {
return data.filter(item => item.platform.includes(platform))
} else {
return data.filter(item => item.platform.toString() === platform.toString())
}
},
back() {
uni.showModal({
title: '取消发布',
content: this.hasPackage ? '将会删除已上传的包' : undefined,
success: res => {
if (res.confirm) {
// 若已上传包但取消发布,则自动将包删除
if (this.hasPackage) {
let fileList = [];
fileList.push(this.appFileList.url)
this.$request('deleteFile', {
fileList
}, {
functionName: 'upgrade-center'
})
}
uni.navigateBack()
}
}
});
}
}
}
</script>
<style lang="scss">
::v-deep .uni-forms-item__content {
display: flex;
align-items: center;
}
.uni-button-group {
& button {
margin-left: 15px;
}
& button:first-child {
margin-left: 0px;
}
}
.title_padding {
padding-bottom: 15px;
display: block;
}
::v-deep .uni-file-picker__files {
max-width: 100%;
}
</style>
<template>
<view class="uni-container">
<view class="uni-header">
<view class="uni-group">
<view class="uni-title">包类型</view>
<view class="uni-sub-title" style="display: flex;justify-content: center;align-items: center;">
{{type_valuetotext[formData.type]}}
</view>
</view>
<view v-if="!isStable" class="uni-group">
<button class="uni-button" type="warn" size="mini" @click="deletePackage">删除</button>
</view>
</view>
<uni-forms ref="form" :value="formData" validateTrigger="bind" :labelWidth="labelWidth">
<uni-forms-item name="appid" label="AppID" required>
<uni-easyinput :disabled="true" v-model="formData.appid" trim="both" />
</uni-forms-item>
<uni-forms-item name="name" label="应用名称">
<uni-easyinput :disabled="true" v-model="formData.name" trim="both" />
</uni-forms-item>
<uni-forms-item name="title" label="更新标题">
<uni-easyinput :disabled="detailsState" placeholder="更新标题" v-model="formData.title" />
</uni-forms-item>
<uni-forms-item name="contents" label="更新内容" required>
<textarea auto-height style="box-sizing: content-box;" :disabled="detailsState"
@input="binddata('contents', $event.detail.value)" class="uni-textarea-border"
:value="formData.contents" @update:value="val => formData.contents = val"></textarea>
</uni-forms-item>
<uni-forms-item name="platform" label="平台" required>
<uni-data-checkbox :disabled="true" :multiple="true" v-model="formData.platform"
:localdata="platformLocaldata" />
</uni-forms-item>
<uni-forms-item name="version" label="版本号" required>
<uni-easyinput :disabled="true" v-model="formData.version" placeholder="当前包版本号,必须大于当前已上线版本号" />
</uni-forms-item>
<uni-forms-item v-if="isWGT" name="min_uni_version" label="原生App最低版本" :required="isWGT">
<uni-easyinput :disabled="detailsState" placeholder="原生App最低版本" v-model="formData.min_uni_version" />
<show-info :content="minUniVersionContent"></show-info>
</uni-forms-item>
<uni-forms-item v-if="!isiOS && !detailsState" label="上传apk包">
<uni-file-picker v-model="appFileList" :file-extname="fileExtname" :disabled="hasPackage"
returnType="object" file-mediatype="all" limit="1" @success="packageUploadSuccess"
@delete="packageDelete">
<button type="primary" size="mini" @click="selectFile">选择文件</button>
</uni-file-picker>
<text v-if="hasPackage"
style="padding-left: 20px;color: #a8a8a8;">{{Number(appFileList.size / 1024 / 1024).toFixed(2)}}M</text>
</uni-forms-item>
<uni-forms-item name="url" :label="isiOS ? 'AppStore' : '下载链接'" required>
<uni-easyinput :disabled="detailsState" placeholder="下载链接" v-model="formData.url" :maxlength="-1" />
<!-- <show-info :top="-80" :content="uploadFileContent"></show-info> -->
</uni-forms-item>
<uni-forms-item v-if="isWGT" name="is_silently" label="静默更新">
<switch :disabled="detailsState"
@change="binddata('is_silently', $event.detail.value),formData.is_silently=$event.detail.value"
:checked="formData.is_silently" />
<show-info :top="-80" :content="silentlyContent"></show-info>
</uni-forms-item>
<uni-forms-item v-if="!isiOS" name="is_mandatory" label="强制更新">
<switch :disabled="detailsState"
@change="binddata('is_mandatory', $event.detail.value),formData.is_mandatory=$event.detail.value"
:checked="formData.is_mandatory" />
<show-info width="230" :top="-30" :content="mandatoryContent"></show-info>
</uni-forms-item>
<uni-forms-item name="stable_publish" label="上线发行">
<switch :disabled="detailsState || isStable"
@change="binddata('stable_publish', $event.detail.value),formData.stable_publish=$event.detail.value"
:checked="formData.stable_publish" />
<show-info v-if="isStable" :top="-50" width="350" :content="stablePublishContent"></show-info>
<show-info v-else :top="-40" :content="stablePublishContent2"></show-info>
</uni-forms-item>
<uni-forms-item name="create_date" label="上传时间">
<uni-dateformat format="yyyy-MM-dd hh:mm:ss" :date="formData.create_date" :threshold="[0, 0]" />
</uni-forms-item>
<uni-forms-item v-show="false" name="type" label="安装包类型">
<uni-data-checkbox v-model="formData.type" :localdata="formOptions.type_localdata" />
</uni-forms-item>
<view class="uni-button-group">
<button type="primary" class="uni-button" style="width: 100px;" @click="detailsState = false"
v-if="detailsState">修改</button>
<button type="primary" class="uni-button" style="width: 100px;" @click="submit"
v-if="!detailsState">提交</button>
<button type="warn" class="uni-button" style="width: 100px;" @click="cancelEdit"
v-if="!detailsState">取消</button>
<navigator open-type="navigateBack" style="margin-left: 15px;">
<button class="uni-button" style="width: 100px;">返回</button>
</navigator>
</view>
</uni-forms>
</view>
</template>
<script>
import {
validator,
enumConverter
} from '@/uni_modules/uni-upgrade-center/js_sdk/validator/opendb-app-versions.js';
import addAndDetail, {
fields
} from '../mixin/version_add_detail_mixin.js'
import {
deepClone,
appVersionListDbName
} from '../utils.js'
import showInfo from '../components/show-info.vue'
const db = uniCloud.database();
const dbCmd = db.command;
const dbCollectionName = appVersionListDbName;
const platform_iOS = 'iOS';
const platform_Android = 'Android';
function getValidator(fields) {
let reuslt = {}
for (let key in validator) {
if (fields.includes(key)) {
reuslt[key] = validator[key]
}
}
return reuslt
}
export default {
components: {
showInfo
},
mixins: [addAndDetail],
data() {
return {
showStableInfo: false,
isStable: true, // 是否是线上发行版
originalData: {}, // 原始数据,用于恢复状态
detailsState: true // 查看状态
}
},
async onLoad(e) {
const id = e.id
this.formDataId = id
await this.getDetail(id)
this.isStable = this.formData.stable_publish;
this.latestStableData = await this.getLatestVersion();
if (this.isWGT) {
this.rules.min_uni_version.rules.push({
"required": true
})
}
},
methods: {
/**
* 触发表单提交
*/
submit() {
uni.showLoading({
mask: true
})
this.$refs.form.validate().then((res) => {
res.store_list = this.formData.store_list
if (res.store_list) {
res.store_list.forEach(item => {
item.priority = parseFloat(item.priority)
})
}
this.submitForm(res)
}).catch((errors) => {
uni.hideLoading()
})
},
async submitForm(value) {
const collectionDB = db.collection(dbCollectionName)
// 使用 clientDB 提交数据
collectionDB.doc(this.formDataId).update(value).then(async (res) => {
// 如果不是线上发行版,则在设置为上线发行时,需将之前的已上线版设为下线
if (!this.isStable && value.stable_publish === true && this.latestStableData) {
await collectionDB.doc(this.latestStableData._id).update({
stable_publish: false
})
}
uni.showToast({
title: '修改成功'
})
this.getOpenerEventChannel().emit('refreshData')
setTimeout(() => uni.navigateBack(), 500)
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
/**
* 获取表单数据
* @param {Object} id
*/
getDetail(id) {
uni.showLoading({
mask: true
})
return db.collection(dbCollectionName)
.doc(id)
.field(fields)
.get()
.then((res) => {
const data = res.result.data[0]
if (data) {
this.formData = data
this.originalData = deepClone(this.formData)
}
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
deletePackage() {
uni.showModal({
title: '提示',
content: '是否删除该版本',
success: res => {
if (res.confirm) {
uni.showLoading({
mask: true
})
db.collection(dbCollectionName).doc(this.formDataId).remove()
.then(() => {
uni.showToast({
title: '删除成功'
})
this.getOpenerEventChannel().emit('refreshData')
setTimeout(() => uni.navigateBack(), 500)
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
}
}
});
},
async getLatestVersion() {
const where = {
appid: this.formData.appid,
type: this.formData.type,
stable_publish: true
};
if (!this.isWGT) {
where.platform = this.formData.platform[0]
}
const latestStableData = await db.collection(dbCollectionName).where(where).get()
return latestStableData.result.data.find(item => item.platform.toString() === this.formData.platform
.toString());
},
cancelEdit() {
let content = '';
!this.isiOS && this.hasPackage ? content += '\n将会删除已上传的包' : '';
uni.showModal({
title: '取消修改',
content,
success: res => {
if (res.confirm) {
this.formData = deepClone(this.originalData)
this.detailsState = true
if (this.hasPackage) {
let fileList = [];
fileList.push(this.appFileList.url)
this.$request('deleteFile', {
fileList
}, {
functionName: 'upgrade-center'
})
}
}
}
});
}
}
}
</script>
<style lang="scss">
.show-stable-info {
position: absolute;
left: 165px;
padding: 5px 10px;
background-color: #f4f4f5;
color: #909399;
border-radius: 4px;
border: 1px solid #e9e9eb;
}
::v-deep .uni-forms-item__content {
display: flex;
align-items: center;
}
.uni-button-group {
& button {
margin-left: 15px;
}
& button:first-child {
margin-left: 0px;
}
}
</style>
<template>
<view class="main">
<view v-if="loaded">
<view class="uni-header">
<view class="uni-group">
<view class="uni-sub-title">当前应用:</view>
<view class="uni-title app-list">
<picker @change="(e) => showAppIndex = e.detail.value" :value="showAppIndex"
:range="appNameList">
<view class="uni-input">
{{appNameList[showAppIndex]}}
<uni-icons type="bottom"></uni-icons>
</view>
</picker>
</view>
</view>
<view class="uni-group">
<input class="uni-search" type="text" v-model="query" @confirm="search" placeholder="请输入搜索内容" />
<button class="uni-button" type="default" size="mini" @click="search">搜索</button>
<button class="uni-button publish" type="primary" size="mini" @click="publish">发布新版</button>
<button class="uni-button" type="warn" size="mini" :disabled="!selectedIndexs.length"
@click="delTable">批量删除</button>
</view>
</view>
<view class="uni-container">
<unicloud-db ref="udb" :collection="appVersionListDbName"
field="appid,contents,platform,type,version,min_uni_version,url,stable_publish,create_date,title,name"
:where="where" page-data="replace" :orderby="orderby" :getcount="true" :page-size="options.pageSize"
:page-current="options.pageCurrent" v-slot:default="{data,pagination,loading,error,options}"
:options="options">
<uni-table style="overflow-y: hidden;" :loading="loading" :emptyText="error.message || '没有更多数据'"
border stripe type="selection" @selection-change="selectionChange">
<uni-tr>
<uni-th align="center">AppID</uni-th>
<uni-th align="center">更新标题</uni-th>
<uni-th align="center">安装包类型</uni-th>
<uni-th align="center">平台</uni-th>
<!-- <uni-th align="center">已上架应用市场</uni-th> -->
<uni-th align="center">版本号</uni-th>
<uni-th align="center">安装包状态</uni-th>
<uni-th align="center">上传时间</uni-th>
<uni-th align="center">操作</uni-th>
</uni-tr>
<uni-tr v-for="(item,index) in data" :key="index" :disabled="item.stable_publish">
<uni-td align="center"> {{item.appid}} </uni-td>
<uni-td align="center"> {{item.title || '-'}} </uni-td>
<uni-td align="center">
<text :style="{
padding: '5px 8px',
backgroundColor: item.type === 'wgt' ? '#f0f9eb' : '#ecf5ff',
color: item.type === 'wgt' ? '#67c23a' : '#409eff',
border: `1px solid ${item.type === 'wgt' ? '#e1f3d8' : '#d9ecff'}`,
borderRadius: '4px'
}">{{options.type_valuetotext[item.type]}}</text>
</uni-td>
<uni-td align="center">
<uni-data-picker :localdata="options.platform_valuetotext" :value="item.platform"
:border="false" :readonly="true" split="," />
</uni-td>
<!-- <uni-td align="center">
<text>{{store_list_key(item.store_list)}}</text>
</uni-td> -->
<uni-td align="center"> {{item.version}} </uni-td>
<uni-td align="center"> {{item.stable_publish == true ? '已上线' : '已下线'}} </uni-td>
<uni-td align="center">
<uni-dateformat format="yyyy-MM-dd hh:mm:ss" :date="item.create_date"
:threshold="[0, 0]" />
</uni-td>
<uni-td align="center">
<!-- <view class="uni-group"> -->
<button @click="navigateTo('./detail?id='+item._id, false)" class="uni-button"
size="mini" type="primary">详情</button>
<!-- <button @click="confirmDelete(item._id)" class="uni-button" size="mini" type="warn">删除</button> -->
<!-- </view> -->
</uni-td>
</uni-tr>
</uni-table>
<view class="uni-pagination-box">
<uni-pagination show-icon :page-size="pagination.size" v-model="pagination.current"
:total="pagination.count" @change="onPageChanged" />
</view>
</unicloud-db>
</view>
</view>
<view v-else class="page-loading" :style="containerTop">
<i class="uni-icon_toast uni-loading"></i>
</view>
</view>
</template>
<script>
import {
enumConverter
} from '@/uni_modules/uni-upgrade-center/js_sdk/validator/opendb-app-versions.js';
import {
appListDbName,
appVersionListDbName,
defaultDisplayApp
} from '../utils.js'
import {
mapState
} from 'vuex'
const db = uniCloud.database()
const dbCmd = db.command
// 表查询配置
const dbOrderBy = 'stable_publish desc,create_date desc' // 排序字段
const dbSearchFields = ['name', 'title', 'stable_publish', 'type'] // 模糊搜索字段,支持模糊搜索的字段列表
// 分页配置
const pageSize = 20
const pageCurrent = 1
const appidKey = '__app_version_appid'
const nameKey = '__app_version_name'
function getScreenHeight() {
return document.documentElement ? document.documentElement.clientHeight : window.innerHeight;
}
function createListQuery(condition = {}) {
return {
create_env: dbCmd.neq("uni-stat"),
...condition
}
}
export default {
data() {
return {
backButtonHover: false,
appVersionListDbName,
currentAppid: '',
currentAppName: '',
query: '',
where: '',
orderby: dbOrderBy,
selectedIndexs: [],
options: {
pageSize,
pageCurrent,
...enumConverter
},
imageStyles: {
width: 64,
height: 64
},
loaded: false,
containerTop: {},
appList: [],
showAppIndex: 0
}
},
async onLoad({
appid
}) {
await this.getAppList()
if (!this.appList.length) return
this.loaded = true
this.appList.forEach((item, index) => {
if (item.appid === appid || defaultDisplayApp) {
this.showAppIndex = index
}
})
this.setAppInfo(this.showAppIndex)
this.where = createListQuery({
appid: this.currentAppid
})
},
computed: {
...mapState('app', ['appid']),
appNameList() {
return this.appList.map(item => item.name)
}
},
watch: {
showAppIndex(val) {
this.setAppInfo(val)
this.where = createListQuery({
appid: this.currentAppid
})
}
},
onReady() {
this.containerTop.height = `${getScreenHeight()}px`
},
methods: {
setAppInfo(index) {
this.currentAppid = this.appList[index].appid
this.currentAppName = this.appList[index].name
},
navigateBack() {
uni.navigateBack()
},
getWhere() {
const query = this.query.trim()
if (!query) {
return ''
}
const queryRe = new RegExp(query, 'i')
return dbSearchFields.map(name => queryRe + '.test(' + name + ')').join(' || ')
},
search() {
const newWhere = this.getWhere()
const isSameWhere = newWhere === this.where
this.where = newWhere
if (this.where) {
this.where = `(${this.where}) && `
}
this.where += `${new RegExp(this.currentAppid, 'i')}.test(appid)`
if (isSameWhere) { // 相同条件时,手动强制刷新
this.loadData()
}
},
loadData(clear = true) {
this.$refs.udb.loadData({
clear
})
},
onPageChanged(e) {
this.$refs.udb.loadData({
current: e.current
})
},
navigateTo(url, clear) {
// clear 表示刷新列表时是否清除页码,true 表示刷新并回到列表第 1 页,默认为 true
uni.navigateTo({
url,
events: {
refreshData: () => {
this.loadData(clear)
}
}
})
},
// 多选处理
selectedItems() {
var dataList = this.$refs.udb.dataList
return this.selectedIndexs.map(i => dataList[i]._id)
},
// 批量删除
delTable() {
this.$refs.udb.remove(this.selectedItems())
},
// 多选
selectionChange(e) {
this.selectedIndexs = e.detail.index
},
confirmDelete(id) {
this.$refs.udb.remove(id)
},
publish(e) {
// #ifdef H5
const {
top,
left,
width,
height
} = document.querySelector('.uni-button.publish').getBoundingClientRect()
// #endif
const platforms = Object.keys(this.options.type_valuetotext)
uni.showActionSheet({
itemList: Object.values(this.options.type_valuetotext),
// #ifdef H5
popover: {
top: top + height,
left,
width
},
// #endif
success: async (res) => {
this.navigateTo(
`./add?appid=${this.currentAppid}&name=${this.currentAppName}&type=${platforms[res.tapIndex]}`
)
}
});
},
async getAppList() {
try {
const {
result
} = await db.collection(appListDbName).get()
if (result && result.data && result.data.length > 0) {
this.appList = result.data.filter(item => item.appid !== this.appid)
} else {
this.showModalToAppManager()
}
} catch (e) {
const arr = ['TOKEN_INVALID_TOKEN_EXPIRED', 'TOKEN_INVALID_ANONYMOUS_USER']
if (arr.indexOf(e.code) === -1)
this.showModalToAppManager()
}
},
showModalToAppManager() {
let timer = null
let second = 3
function jump() {
uni.navigateTo({
url: '/pages/system/app/list'
})
clearInterval(timer)
}
timer = setInterval(() => {
if (--second <= 0) {
jump()
}
}, 1000)
uni.showModal({
title: '请先添加应用',
content: '即将跳转至应用管理……',
showCancel: false,
confirmText: '立即跳转',
success: (res) => jump()
})
},
store_list_key(store_list) {
const arr = store_list ? store_list.filter(item => item.enable) : []
return arr.length ?
arr.sort((a, b) => b.priority - a.priority)
.map(item => item.name).join(',') :
'-'
}
}
}
</script>
<style lang="scss">
.page-loading {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex: 1;
i {
$icon-size: 80rpx;
width: $icon-size;
height: $icon-size;
}
}
page,
page .main,
.page-loading {
height: 100%;
}
.app-list {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
border-radius: 4px;
border: 1px solid #2e76ba;
color: #3A8EE6;
uni-text {
margin-left: 10px;
}
}
</style>
# uni-upgrade-center - Admin
### 概述
> 统一管理App及App在`Android`、`iOS`平台上`App安装包`和`wgt资源包`的发布升级
> 本插件为升级中心Admin后台管理系统,前台检查更新函数请点击查看 [uni-upgrade-center-app](https://ext.dcloud.net.cn/plugin?id=4542)
### 基于uniCloud的App升级中心,本插件具有如下特征:
- 云端基于uniCloud云函数实现
- 数据库遵循opendb规范
- 遵循uniCloud Admin框架规范,可直接导入Admin项目中
- 支持App整包升级及wgt资源包升级
## 什么是 uniCloud
uniCloud 是 DCloud 联合阿里云、腾讯云,为开发者提供的基于 serverless 模式和 js 编程的云开发平台,更多请参考[uniCloud 文档](https://uniapp.dcloud.io/uniCloud)
## 升级中心解决了什么问题?
升级中心是一款uniCloud admin插件,负责App版本更新业务。包含后台管理界面、更新检查逻辑,App内只要调用弹出提示即可。
升级中心有以下功能点:
- 应用管理,对App的信息记录和应用版本管理
- 版本管理,可以发布新版,也可方便直观的对当前App历史版本以及线上发行版本进行查看、编辑和删除操作
- 版本发布信息管理,包括 更新标题,更新内容,版本号,静默更新,强制更新,灵活上线发行 的设置和修改
- 原生App安装包,发布Apk更新,用于App的整包更新,可设置是否强制更新
- wgt资源包,发布wgt更新,用于App的热更新,可设置是否强制更新,静默更新
- App管理列表及App版本记录列表搜索
只需导入插件,初始化数据库即可拥有上述功能。
您也可以自己修改逻辑自定义数据库字段,和随意定制 UI 样式。
## 安装指引
1. 使用`HBuilderX 3.1.0+`,因为要使用到`uni_modules`
2. 使用已有`uniCloud-admin`项目或新建项目:`打开HBuilderX` -> `文件` -> `新建` -> `项目` -> `uni-app` 选择 `uniCloud admin`模板,键入一个名字,确定
3. 鼠标右键选择`关联云服务空间``运行云服务空间初始化向导`
3. 在插件市场打开本插件页面,在右侧点击`使用 HBuilderX 导入插件`,选择 `uniCloud admin` 项目点击确定
4. 等待下载安装完毕。由于本插件依赖一些uni-ui插件,下载完成后会显示合并插件页面,自行选择即可
5. 找到`/uni_modules/uni-upgrade-center/uniCloud/cloudfunctions/upgrade-center`,右键上传部署
6. 找到`/uni_modules/uni-upgrade-center/uniCloud/database/db_init.json`,右键初始化数据库
7.`pages.json`中添加页面路径
```json
//此结构与uniCloud admin中的pages.json结构一致
{
"pages": [
// ……其他页面配置
{
"path": "uni_modules/uni-upgrade-center/pages/version/list",
"style": {
"navigationBarTitleText": "版本列表"
}
}, {
"path": "uni_modules/uni-upgrade-center/pages/version/add",
"style": {
"navigationBarTitleText": "新版发布"
}
}, {
"path": "uni_modules/uni-upgrade-center/pages/version/detail",
"style": {
"navigationBarTitleText": "版本信息查看"
}
}
]
}
```
8.`manifest.json -> 源码视图`中添加以下配置:
```js
"networkTimeout":{
"uploadFile":1200000 //ms, 如果不配置,上传大文件可能会超时
}
```
9. 运行项目到`Chrome`
10. 运行起来uniCloud admin,菜单管理模块会自动读取`/uni_modules/uni-upgrade-center/menu.json`文件中的菜单配置,生成【待添加菜单】,选中升级中心,点击`添加选中的菜单`即可
<div align="center">
<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/16dc338e-7d5b-4290-98a9-adb7f0c23754.png" width="800"></img>
</div>
11. 添加成功后,就可以在左侧的菜单栏中找到`升级中心`菜单
<div align="center">
<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/fcf04804-0c4c-4342-9a8b-dfc273dfc83c.png" width="300"></img>
</div>
12. 在进入`升级中心`之前:
1. 需要到`uni-admin``应用管理`中添加一个应用,才可以在`升级中心`中发布对应应用的版本。
2. 当你有多个应用时,可以在`/uni_modules/uni-upgrade-center/pages/utils.js`中修改`defaultDisplayApp`字段来设置默认显示应用的`appid`
3. 如果不设置或设置应用不存在则默认从数据库中查出来的第一个应用。
13. 由于插件依赖的uni-ui的一些组件,建议右键`/uni_modules/uni-upgrade-center`安装一下第三方依赖,否则可能会出现一些问题
14. 运行在`uniCloud`,由于本插件使用了`clientDB`,因此可能需要配置一下`uni-config-center插件`关于`uni-id`的配置信息。如提示`公用模块uni-id缺少配置信息`请这样做:
1. 点击[uni-config-center](https://ext.dcloud.net.cn/plugin?id=4425)导入插件
2.`/uniCloud/cloudfunctions/common/uni-config-center/`下创建`uni-id`文件夹,文件夹内创建`config.json`文件。
3. 点击[config.json默认配置](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=start)。将内容拷贝至`config.json`中。**注:一定要把注释去除!**
## 使用指南
### 升级中心
#### 应用列表
1. 点击菜单 `应用管理`,这里展示你所添加的 App,点击右上角 `新增` 可以新增一个 App
<div align="center">
<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/7f85aa6a-eff3-4cc6-bb32-9feaaeaf97d0.png" width="400"></img>
</div>
2. 将App的信息都填写完善后,你可以在列表的操作列进行`修改`应用信息或者`删除`该应用。
**Tips**
- 删除应用会把该应用的所有版本记录同时删除
#### 版本管理
1. 在版本管理list的右上角点击`发布新版`,可以发布`原生App安装包``wgt资源包`。在左上角点击`下拉列表`,可以切换展示应用。
<div align="center">
<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/442e84e7-e7f3-4d27-9c98-45568e5db835.png" width="800"></img>
</div>
- #### 发布原生App安装包
1. 在上传安装包界面填写此次发版信息
<div align="center" >
<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/67932ae3-1a7a-4f21-9849-ba3bcc500c36.png" width="400"></img>
</div>
2. `包地址`
- 可以选择手动上传一个文件到 `云存储`,会自动将地址填入该项
- 也可以手动填写一个地址,就可以不用再上传文件
- 如果是发布`苹果`版本,包地址则为 应用在`AppStore的链接`
3. `强制更新`
- 如果使用强制更新,App端接收到该字段后,App升级弹出框不可取消
4. `上线发行`
- 可设置当前包是否上线发行,只有已上线才会进行更新检测
- 同时只可有一个线上发行版,线上发行不可更设为下线。未上线可以设为上线发行并自动替换当前线上发行版
- 修改当前包为上线发行,自动替换当前线上发行版
- #### 发布wgt资源包
1. 大部分配置与发布 `原生App安装包` 一致
<div align="center">
<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/ec916cde-0d0e-4bf3-a735-643ea2a45b74.png" width="400"></img>
</div>
2. `原生App最低版本`
- 上次使用新Api或打包新模块的App版本
- 如果此次打包wgt使用了`新的api`或者打包了`新的模块`,则在发布 `wgt资源包` 的时候,将此版本更新为本次版本
- 如果已有正式版`wgt资源包`,则本次新增会自动带出
2. `静默更新`
- App升级时会在后台下载wgt包并自行安装。新功能在下次启动App时生效
- #### 发布完成页面
<div align="center">
<img src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/c5470d8c-cc37-4b41-8d56-6d50f8daac62.png" width="800"></img>
</div>
**Tips**
1. `pages/system/upgradecenter/version/add.vue`中有版本对比函数(compare)。
- 使用多段式版本格式(如:"3.0.0.0.0.1.0.1", "3.0.0.0.0.1")。如果不满足对比规则,请自行修改。
## 项目代码说明
### uniCloud 数据表
数据表基于 [openDB](https://gitee.com/dcloud/opendb/tree/master) 规范,它约定了一个标准用户表的表名和字段定义,并且基于 nosql 的特性,可以由开发者自行扩展字段。
本项目用到了 2 个表:
- opendb-app-list:app管理列表。记录应用的 appid、name、description 用于展示。[详见](https://gitee.com/dcloud/opendb/tree/master/collection/opendb-app-list)
- opendb-app-versions:应用版本管理表。记录管理应用的版本信息。[详见](https://gitee.com/dcloud/opendb/tree/master/collection/opendb-app-versions)
### 前端页面
点击`升级中心`,会进入应用管理列表,在这里你可以新增应用,或者在`应用详情`中查看、修改或删除一个已经录入的应用。
在应用管理列表中点击某个应用的`版本管理`,进入该应用的所有版本记录。列表排序为:先排序已上线版本,剩下已下线版本根据创建时间排列。
在应用版本列表中点击`详情`,即可进入该版本的信息详情中查看、修改或删除该记录。
**Tips**
- 升级中心设计之初就支持iOS的wgt更新
- iOS的wgt更新肯定是违反apple政策的,注意事项:
- 审核期间请不要弹窗升级
- 升级完后尽量不要自行重启
- 尽量使用静默更新
- 可以通过以下修改支持iOS的wgt更新:
> \uni_modules\uni-upgrade-center\pages\mixin\version_add_detail_mixin.js
>
> 将 `data` 中的 `enableiOSWgt: false` 中 改为 `enableiOSWgt: true`
**常见问题**
- 以下问题可以通过升级插件版本解决:
- createdate不与默认值匹配
- ["create_date"]在数据库中并不存在
- 提交的字段["dirty_data"]在数据库中并不存在
- 集合[opendb-app-list]对应的schema内存在错误,详细信息:opendb-app-list表对应的schema名称冲突,这是什么意思呢
- 没有/找不到 [opendb-app-list] 集合/表。**解决方案:**升级 admin 至 1.6.0+ 即可
- 测试时发布了高版本的包,测试完了发布包提示需要大于版本号 (x.x.x)。**解决方案:**直接在控制台修改数据库
\ No newline at end of file
'use strict';
exports.main = async (event, context) => {
//event为客户端上传的参数
console.log('event : ', event)
let res = {};
let params = event.data || event.params;
switch (event.action) {
case 'deleteFile':
res = await uniCloud.deleteFile({
fileList: params.fileList
})
break;
}
//返回数据给客户端
return res
};
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册