移动app
168
uni-im示例/uni_modules/uni-id-pages/changelog.md
Normal file
@@ -0,0 +1,168 @@
|
||||
## 1.1.14(2023-05-19)
|
||||
- 修复 退出登录不会跳转至登录页的问题
|
||||
## 1.1.13(2023-05-10)
|
||||
- 修复 启用摇树优化 报错的问题
|
||||
## 1.1.12(2023-05-05)
|
||||
- uni-id-co 新增 调用 add-user 接口创建用户时允许触发 beforeRegister 钩子方法,beforeRegister 钩子[详见](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#before-register)
|
||||
- uni-id-co 新增 自无 unionid 到有 unionid 状态进行登录时为用户补充 unionid 字段
|
||||
- uni-id-co 修复 i18n 在特定场景下报错的 bug
|
||||
- uni-id-co 修复 跨平台解绑微信/QQ时无法解绑的 bug
|
||||
- uni-id-co 修复 微信小程序等平台创建验证码时无法展示的 bug
|
||||
- uni-id-co 修复 更新 push_clientid 时因 device_id 没有变化导致无法更新
|
||||
## 1.1.11(2023-03-24)
|
||||
- 修复 tabbar页面因为token无效而强制跳转至登录页面(url参数包含`uniIdRedirectUrl`)后无法返回的问题
|
||||
## 1.1.10(2023-03-24)
|
||||
- 修复 PC微信扫码登录跳转地址错误
|
||||
- uni-id-co 新增 请求鉴权支持 uni-cloud-s2s 模块验证签名 [uni-cloud-s2s文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-s2s.html)
|
||||
## 1.1.9(2023-03-24)
|
||||
- 修复 跳转至登录页面的url参数包含`uniIdRedirectUrl`后无法返回的问题
|
||||
## 1.1.8(2023-03-02)
|
||||
- 修复 调试模式下没有对微信授权手机号登录方式进行配置检测
|
||||
## 1.1.7(2023-02-27)
|
||||
- 【重要】新增 实名认证功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#frv)
|
||||
## 1.1.6(2023-02-24)
|
||||
- uni-id-co 新增 注册用户时允许配置默认角色 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#config-defult-role)
|
||||
- uni-id-co 优化 `updateUserInfoByExternal`接口,允许修改头像、性别
|
||||
- uni-id-co 修复 请求签名密钥字段 `requestAuthSecret` 缺少为空判断
|
||||
- uni-id-co 修复 `externalRegister`接口头像未使用`avatar_file`字段保存
|
||||
- 修复 web微信登录回调地址不正确
|
||||
## 1.1.5(2023-02-23)
|
||||
- 更新 微信小程序端 更新头像信息,如果是使用微信的头像则不再调用裁剪接口
|
||||
## 1.1.4(2023-02-21)
|
||||
- 修复 部分情况下 `uniIdRedirectUrl` 参数无效的问题
|
||||
## 1.1.3(2023-02-20)
|
||||
- 修复 非微信小程序端报`TypeError: uni.hideHomeButton is not a function`的问题
|
||||
## 1.1.2(2023-02-10)
|
||||
- 新增 微信小程序端 首页需强制登录时,隐藏返回首页按钮
|
||||
- uni-id-co 新增 外部联登后修改用户信息接口(updateUserInfoByExternal) [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo)
|
||||
- uni-id-co 优化外部联登接口(登录、注册)逻辑
|
||||
## 1.1.1(2023-02-02)
|
||||
- 新增 微信小程序端 支持选择使用微信资料的“头像”和“昵称” 设置用户资料 [详情参考](https://wdoc-76491.picgzc.qpic.cn/MTY4ODg1MDUyNzQyMDUxNw_21263_rTNhg68FTngQGdvQ_1647431233?w=1280&h=695.7176470588236)
|
||||
## 1.1.0(2023-01-31)
|
||||
- 【重要】优化 小程序端资源包大小(运行时大小为:731KB,发行后为:583KB;注:可以直接将本插件作为分包使用)
|
||||
- 更新 微信小程序端 上传头像功能 用`wx.cropImage`实现图片裁剪
|
||||
- 修复 选择一键登录时会先显示 非密码登录页面的问题
|
||||
- 修复 一键登录 点击右上角的关闭按钮没有返回上一页的问题
|
||||
## 1.0.41(2023-01-16)
|
||||
- 优化 压缩依赖的文件资源大小
|
||||
## 1.0.40(2023-01-16)
|
||||
- 更新依赖的 验证码插件`uni-captcha`版本的版本为 0.6.4 修复 部分情况下APP端无法获取验证码的问题 [详情参考](https://ext.dcloud.net.cn/plugin?id=4048)
|
||||
- 修复 客户端token过期后,点击退出登录按钮报错的问题
|
||||
- uni-id-co 修复 updateUser 接口`手机号`和`邮箱`参数值为空字符串时,修改无效的问题
|
||||
## 1.0.39(2022-12-28)
|
||||
- uni-id-co 修复 URL化时第三方登录无法获取 uniPlatform 参数
|
||||
- uni-id-co 修复 validator error
|
||||
## 1.0.38(2022-12-26)
|
||||
- uni-id-co 优化 手机号与邮箱验证规则为空字符串时不校验
|
||||
## 1.0.37(2022-12-09)
|
||||
- 优化admin端样式
|
||||
## 1.0.36(2022-12-08)
|
||||
- uni-id-co 修复 `updateUser` 接口部分参数为空时数据修改异常
|
||||
## 1.0.35(2022-11-30)
|
||||
- uni-id-co 新增 匹配到的用户不可在当前应用登录时的错误码 `uni-id-account-not-exists-in-current-app` [错误码说明](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#errcode)
|
||||
## 1.0.34(2022-11-29)
|
||||
- 优化 toast 错误提示时间为3秒
|
||||
- uni-id-co 修复 无法从 clientInfo 中获取 uniIdToken
|
||||
## 1.0.33(2022-11-25)
|
||||
- uni-id-co 新增 外部系统联登接口,可为外部系统创建与uni-id相对应的账号,使该账号可以使用依赖uniId的系统及功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external)
|
||||
- uni-id-co 新增 URL化请求时鉴权签名验证 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#http-reqeust-auth)
|
||||
- uni-id-co 修复 微信登录时用户未设置头像的报错问题
|
||||
## 1.0.32(2022-11-21)
|
||||
- 新增 设置密码页面
|
||||
- 新增 登录后跳转设置密码页面配置项`setPasswordAfterLogin` [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd-after-login)
|
||||
- uni-id-co 新增 设置密码接口 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd)
|
||||
## 1.0.31(2022-11-16)
|
||||
- uni-id-co 修复 验证码可能无法收到的bug
|
||||
## 1.0.30(2022-11-11)
|
||||
- uni-id-co 修复 用户只有openid时绑定微信/QQ报错
|
||||
## 1.0.29(2022-11-10)
|
||||
- uni-id-co 支持URL化方式请求 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#adapter-http)
|
||||
## 1.0.28(2022-11-09)
|
||||
- uni-id-co 升级密码加密算法,支持hmac-sha256加密 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#password-safe)
|
||||
- uni-id-co 新增 开发者可以自定义密码加密规则 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#custom-password-encrypt)
|
||||
- uni-id-co 新增 支持将其他系统用户迁移至uni-id [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#move-users-to-uni-id)
|
||||
## 1.0.27(2022-10-26)
|
||||
- uni-id-co 新增 secureNetworkHandshakeByWeixin 接口,用于建立和微信小程序的安全网络连接
|
||||
## 1.0.26(2022-10-18)
|
||||
- 修复 uni-id-pages 导入时异常的Bug
|
||||
## 1.0.25(2022-10-14)
|
||||
- uni-id-co 增加 微信授权手机号登录方式 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin-mobile)
|
||||
- uni-id-co 增加 解绑第三方平台账号 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-third-account)
|
||||
- uni-id-co 微信绑定手机号支持通过`getPhoneNumber`事件回调的`code`绑定 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin)
|
||||
- 修复 sendSmsCode 接口未在参数内传递 templateId 时 未能从配置文件读取 templateId 的Bug
|
||||
## 1.0.24(2022-10-08)
|
||||
- 修复 报uni-id-users表schema内错误的bug
|
||||
## 1.0.23(2022-10-08)
|
||||
- 修复 vue3下vite编译发行打包失败
|
||||
- 修复 某些情况下注册账号,报TypeErroe:Cannot read properties of undefined (reading ’showToast‘)的错误
|
||||
## 1.0.22(2022-09-23)
|
||||
- 修复 某些情况下,修改密码报“两次输入密码不一致”的bug
|
||||
## 1.0.21(2022-09-21)
|
||||
- 修复 store.hasLogin的值在某些情况下会出错的bug
|
||||
## 1.0.20(2022-09-21)
|
||||
- 新增 store 账号信息状态管理,详情:用户中心页面 路径:`/uni_modules/uni-id-pages/pages/userinfo/userinfo`
|
||||
## 1.0.19(2022-09-20)
|
||||
- 修复 小程序端,使用将自定义节点设置成[虚拟节点](https://uniapp.dcloud.net.cn/tutorial/vue-api.html#%E5%85%B6%E4%BB%96%E9%85%8D%E7%BD%AE)的uni-ui组件,样式错乱的问题
|
||||
## 1.0.18(2022-09-20)
|
||||
- 修复 微信小程序端 WXSS 编译报错的bug
|
||||
## 1.0.17(2022.09-19)
|
||||
- 修复 无法退出登录的bug
|
||||
## 1.0.16(2022-09-19)
|
||||
- 修复 在 Edge 浏览器下 input[type="password"] 会出现浏览器自带的密码查看按钮
|
||||
- 优化 退出登录重定向页面为 uniIdRouter.loginPage
|
||||
- 新增 注册账号页面支持返回登录页面
|
||||
## 1.0.15(2022-09-19)
|
||||
- 更新表结构,解决在uni-admin中部分clientDB操作没有权限的问题
|
||||
## 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端样式
|
||||
- 新增 邮箱验证码注册
|
||||
- 新增 邮箱验证码找回密码
|
||||
- 新增 退出登录(全局)回调事件:`uni-id-pages-logout`,支持通过[uni.$on](https://uniapp.dcloud.net.cn/api/window/communication.html#on)监听;
|
||||
- 调整 抽离退出登录方法至`/uni_modules/uni-id-pages/common/common.js`中,方便在项目其他页面中调用
|
||||
- 调整 用户中心(路径:`/uni_modules/uni-id-pages/pages/userinfo/userinfo`)默认不再显示退出登录按钮。支持页面传参数`showLoginManage=true`恢复显示
|
||||
## 1.0.11(2022-09-01)
|
||||
- 修复 iOS端,一键登录功能卡在showLoading的问题
|
||||
- 更新 合并密码强度与长度配置 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#config)
|
||||
- uni-id-co 修复 调用 removeAuthorizedApp 接口报错的Bug
|
||||
- uni-id-co 新增 管理端接口 updateUser [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user)
|
||||
- uni-id-co 调整 为兼容旧版本,未配置密码强度时提供最简单的密码规则校验(长度大于6即可)
|
||||
- uni-id-co 调整 注册、登录时如果携带了token则尝试对此token进行登出操作
|
||||
- uni-id-co 调整 管理端接口 addUser 增加 mobile、email等参数 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user)
|
||||
## 1.0.10(2022-08-25)
|
||||
- 修复 导入uni-id-pages插件时未自动导入uni-open-bridge-common的Bug
|
||||
## 1.0.9(2022-08-23)
|
||||
- 修复 uni-id-co 缺失uni-open-bridge-common依赖的Bug
|
||||
## 1.0.8(2022-08-23)
|
||||
- 新增 H5端支持微信登录(含微信公众号内的网页授权登录 和 普通浏览器内网页生成二维码,实现手机微信扫码登录)[详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#weixinlogin)
|
||||
- 新增 登录成功(全局)回调事件:`uni-id-pages-login-success`,支持通过[uni.$on](https://uniapp.dcloud.net.cn/api/window/communication.html#on)监听;
|
||||
- 新增 密码强度(是否必须包含大小写字母、数字和特殊符号以及长度)配置 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#config)
|
||||
- 调整 uni-id-co 密码规则调整,废除之前的简单校验,允许配置密码强度 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#password-strength)
|
||||
- 调整 uni-id-co 存储用户 openid 时同时以客户端 AppId 为 Key 的副本,参考:[微信登录](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin)、[QQ登录](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq)
|
||||
- 调整 uni-id-co 依赖 uni-open-bridge-common 存储用户 session_key、access_token 等信息 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#save-user-token)
|
||||
- 新增 uni-id-co 增加 beforeRegister 钩子用户在注册前向用户记录内添加一些数据 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#before-register)
|
||||
## 1.0.7(2022-07-19)
|
||||
- 修复 uni-id-co接口 logout时没有删除token的Bug
|
||||
## 1.0.6(2022-07-13)
|
||||
- 新增 允许覆盖内置校验规则 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#custom-validator)
|
||||
- 修复 app端clientInfo.appVersionCode为数字导致校验无法通过的Bug
|
||||
## 1.0.5(2022-07-11)
|
||||
修复 微信小程序调用uni-id-co接口报错的Bug [详情](https://ask.dcloud.net.cn/question/148877)
|
||||
## 1.0.4(2022-07-06)
|
||||
- uni-id-co增加clientInfo字段类型校验
|
||||
- 监听token更新时机,同步客户端push_clientid至uni-id-device表,改为:同步客户端push_clientid至uni-id-device表和opendb-device表
|
||||
## 1.0.3(2022-07-05)
|
||||
新增监听token更新时机,同步客户端push_clientid至uni-id-device表
|
||||
## 1.0.2(2022-07-04)
|
||||
修复微信小程序登录时无unionid报错的Bug [详情](https://ask.dcloud.net.cn/question/148016)
|
||||
## 1.0.1(2022-06-28)
|
||||
添加相关uni-id表
|
||||
## 1.0.0(2022-06-23)
|
||||
正式版
|
||||
16
uni-im示例/uni_modules/uni-id-pages/common/check-id-card.js
Normal file
@@ -0,0 +1,16 @@
|
||||
function checkIdCard (idCardNumber) {
|
||||
if (!idCardNumber || typeof idCardNumber !== 'string' || idCardNumber.length !== 18) return false
|
||||
|
||||
const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
const checkCode = [1, 0, 'x', 9, 8, 7, 6, 5, 4, 3, 2]
|
||||
const code = idCardNumber.substring(17)
|
||||
|
||||
let sum = 0
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += Number(idCardNumber.charAt(i)) * coefficient[i]
|
||||
}
|
||||
|
||||
return checkCode[sum % 11].toString() === code.toLowerCase()
|
||||
}
|
||||
|
||||
export default checkIdCard
|
||||
95
uni-im示例/uni_modules/uni-id-pages/common/login-page.mixin.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
mutations
|
||||
} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
const mixin = {
|
||||
data() {
|
||||
return {
|
||||
config,
|
||||
uniIdRedirectUrl: '',
|
||||
isMounted: false
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
// #ifdef H5
|
||||
document.onkeydown = false
|
||||
// #endif
|
||||
},
|
||||
mounted() {
|
||||
this.isMounted = true
|
||||
},
|
||||
onLoad(e) {
|
||||
if (e.is_weixin_redirect) {
|
||||
uni.showLoading({
|
||||
mask: true
|
||||
})
|
||||
|
||||
if (window.location.href.includes('#')) {
|
||||
// 将url通过 ? 分割获取后面的参数字符串 再通过 & 将每一个参数单独分割出来
|
||||
const paramsArr = window.location.href.split('?')[1].split('&')
|
||||
paramsArr.forEach(item => {
|
||||
const arr = item.split('=')
|
||||
if (arr[0] == 'code') {
|
||||
e.code = arr[1]
|
||||
}
|
||||
})
|
||||
}
|
||||
this.$nextTick(n => {
|
||||
// console.log(this.$refs.uniFabLogin);
|
||||
this.$refs.uniFabLogin.login({
|
||||
code: e.code
|
||||
}, 'weixin')
|
||||
})
|
||||
}
|
||||
|
||||
if (e.uniIdRedirectUrl) {
|
||||
this.uniIdRedirectUrl = decodeURIComponent(e.uniIdRedirectUrl)
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if (getCurrentPages().length === 1) {
|
||||
uni.hideHomeButton()
|
||||
console.log('已隐藏:返回首页按钮');
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
computed: {
|
||||
needAgreements() {
|
||||
if (this.isMounted) {
|
||||
if (this.$refs.agreements) {
|
||||
return this.$refs.agreements.needAgreements
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
agree: {
|
||||
get() {
|
||||
if (this.isMounted) {
|
||||
if (this.$refs.agreements) {
|
||||
return this.$refs.agreements.isAgree
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
},
|
||||
set(agree) {
|
||||
if (this.$refs.agreements) {
|
||||
this.$refs.agreements.isAgree = agree
|
||||
} else {
|
||||
console.log('不存在 隐私政策协议组件');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loginSuccess(e) {
|
||||
mutations.loginSuccess({
|
||||
...e,
|
||||
uniIdRedirectUrl: this.uniIdRedirectUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default mixin
|
||||
126
uni-im示例/uni_modules/uni-id-pages/common/login-page.scss
Normal file
@@ -0,0 +1,126 @@
|
||||
// 隐藏 edge 浏览器的密码查看按钮
|
||||
|
||||
/* #ifdef H5 */
|
||||
.input-box ::v-deep{
|
||||
.uni-input-input[type="password"] {
|
||||
&::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.uni-content {
|
||||
padding: 0 60rpx;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content {
|
||||
/* #ifndef H5 */
|
||||
padding: 0;
|
||||
max-width: 300px;
|
||||
margin-left: calc(50% - 200px);
|
||||
/* #endif */
|
||||
/* #ifdef H5 */
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
top: 100px;
|
||||
padding: 30px 40px 80px 40px;
|
||||
max-width: 450px;
|
||||
max-height: 450px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px #efefef;
|
||||
background-color: #FFF;
|
||||
/* #endif */
|
||||
}
|
||||
/* #ifdef H5 */
|
||||
.login-logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-logo image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.register-back{
|
||||
display: none;
|
||||
}
|
||||
|
||||
uni-button{
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-content view {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
padding: 18px 0;
|
||||
font-weight: 800;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tip {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
color: #BDBDC0;
|
||||
font-size: 11px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
// 解决小程序端开启虚拟节点virtualHost引起的 class = input-box丢失的问题 [详情参考](https://uniapp.dcloud.net.cn/matter.html#%E5%90%84%E5%AE%B6%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6%E4%B8%8D%E5%90%8C-%E5%8F%AF%E8%83%BD%E5%AD%98%E5%9C%A8%E7%9A%84%E5%B9%B3%E5%8F%B0%E5%85%BC%E5%AE%B9%E9%97%AE%E9%A2%98)
|
||||
.uni-content ::v-deep .uni-easyinput__content,
|
||||
/* #endif */
|
||||
|
||||
.input-box {
|
||||
height: 44px;
|
||||
background-color: #F8F8F8 !important;
|
||||
border-radius: 0;
|
||||
font-size: 14px;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #04498c;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.uni-content ::v-deep .uni-forms-item__inner {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.uni-btn {
|
||||
text-align: center;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 15px 0 10px 0;
|
||||
color: #FFF !important;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uni-body.uni_modules-uni-id-pages-pages-login-login-withoutpwd{
|
||||
height: auto !important;
|
||||
}
|
||||
85
uni-im示例/uni_modules/uni-id-pages/common/password.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// 导入配置
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
|
||||
const {passwordStrength} = config
|
||||
|
||||
// 密码强度表达式
|
||||
const passwordRules = {
|
||||
// 密码必须包含大小写字母、数字和特殊符号
|
||||
super: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||||
// 密码必须包含字母、数字和特殊符号
|
||||
strong: /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||||
// 密码必须为字母、数字和特殊符号任意两种的组合
|
||||
medium: /^(?![0-9]+$)(?![a-zA-Z]+$)(?![~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]+$)[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||||
// 密码必须包含字母和数字
|
||||
weak: /^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{6,16}$/
|
||||
}
|
||||
|
||||
const ERROR = {
|
||||
normal: {
|
||||
noPwd: '请输入密码',
|
||||
noRePwd: '再次输入密码',
|
||||
rePwdErr: '两次输入密码不一致'
|
||||
},
|
||||
passwordStrengthError: {
|
||||
super: '密码必须包含大小写字母、数字和特殊符号,密码长度必须在8-16位之间',
|
||||
strong: '密码必须包含字母、数字和特殊符号,密码长度必须在8-16位之间',
|
||||
medium: '密码必须为字母、数字和特殊符号任意两种的组合,密码长度必须在8-16位之间',
|
||||
weak: '密码必须包含字母,密码长度必须在6-16位之间'
|
||||
}
|
||||
}
|
||||
|
||||
function validPwd(password) {
|
||||
//强度校验
|
||||
if (passwordStrength && passwordRules[passwordStrength]) {
|
||||
if (!new RegExp(passwordRules[passwordStrength]).test(password)) {
|
||||
return ERROR.passwordStrengthError[passwordStrength]
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function getPwdRules(pwdName = 'password', rePwdName = 'password2') {
|
||||
const rules = {}
|
||||
rules[pwdName] = {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: ERROR.normal.noPwd,
|
||||
},
|
||||
{
|
||||
validateFunction: function(rule, value, data, callback) {
|
||||
const checkRes = validPwd(value)
|
||||
if (checkRes !== true) {
|
||||
callback(checkRes)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (rePwdName) {
|
||||
rules[rePwdName] = {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: ERROR.normal.noRePwd,
|
||||
},
|
||||
{
|
||||
validateFunction: function(rule, value, data, callback) {
|
||||
if (value != data[pwdName]) {
|
||||
callback(ERROR.normal.rePwdErr)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
export default {
|
||||
ERROR,
|
||||
validPwd,
|
||||
getPwdRules
|
||||
}
|
||||
174
uni-im示例/uni_modules/uni-id-pages/common/store.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import pagesJson from '@/pages.json'
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co")
|
||||
const db = uniCloud.database();
|
||||
const usersTable = db.collection('uni-id-users')
|
||||
|
||||
let hostUserInfo = uni.getStorageSync('uni-id-pages-userInfo')||{}
|
||||
// console.log( hostUserInfo);
|
||||
const data = {
|
||||
userInfo: hostUserInfo,
|
||||
hasLogin: Object.keys(hostUserInfo).length != 0
|
||||
}
|
||||
|
||||
// console.log('data', data);
|
||||
// 定义 mutations, 修改属性
|
||||
export const mutations = {
|
||||
// data不为空,表示传递要更新的值(注意不是覆盖是合并),什么也不传时,直接查库获取更新
|
||||
async updateUserInfo(data = false) {
|
||||
if (data) {
|
||||
usersTable.where('_id==$env.uid').update(data).then(e => {
|
||||
// console.log(e);
|
||||
if (e.result.updated) {
|
||||
uni.showToast({
|
||||
title: "更新成功",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
this.setUserInfo(data)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "没有改变",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co", {
|
||||
customUI: true
|
||||
})
|
||||
try {
|
||||
let res = await usersTable.where("'_id' == $cloudEnv_uid")
|
||||
.field('mobile,nickname,username,email,avatar_file')
|
||||
.get()
|
||||
|
||||
const realNameRes = await uniIdCo.getRealNameInfo()
|
||||
|
||||
// console.log('fromDbData',res.result.data);
|
||||
this.setUserInfo({
|
||||
...res.result.data[0],
|
||||
realNameAuth: realNameRes
|
||||
})
|
||||
} catch (e) {
|
||||
this.setUserInfo({},{cover:true})
|
||||
console.error(e.message, e.errCode);
|
||||
}
|
||||
}
|
||||
},
|
||||
async setUserInfo(data, {cover}={cover:false}) {
|
||||
// console.log('set-userInfo', data);
|
||||
let userInfo = cover?data:Object.assign(store.userInfo,data)
|
||||
store.userInfo = Object.assign({},userInfo)
|
||||
store.hasLogin = Object.keys(store.userInfo).length != 0
|
||||
// console.log('store.userInfo', store.userInfo);
|
||||
uni.setStorageSync('uni-id-pages-userInfo', store.userInfo)
|
||||
return data
|
||||
},
|
||||
async logout() {
|
||||
// 1. 已经过期就不需要调用服务端的注销接口 2.即使调用注销接口失败,不能阻塞客户端
|
||||
if(uniCloud.getCurrentUserInfo().tokenExpired > Date.now()){
|
||||
try{
|
||||
await uniIdCo.logout()
|
||||
}catch(e){
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
uni.removeStorageSync('uni_id_token');
|
||||
uni.setStorageSync('uni_id_token_expired', 0)
|
||||
uni.redirectTo({
|
||||
url: `/${pagesJson.uniIdRouter && pagesJson.uniIdRouter.loginPage ? pagesJson.uniIdRouter.loginPage: 'uni_modules/uni-id-pages/pages/login/login-withoutpwd'}`,
|
||||
});
|
||||
uni.$emit('uni-id-pages-logout')
|
||||
this.setUserInfo({},{cover:true})
|
||||
},
|
||||
|
||||
loginBack (e = {}) {
|
||||
const {uniIdRedirectUrl = ''} = e
|
||||
let delta = 0; //判断需要返回几层
|
||||
let pages = getCurrentPages();
|
||||
// console.log(pages);
|
||||
pages.forEach((page, index) => {
|
||||
if (pages[pages.length - index - 1].route.split('/')[3] == 'login') {
|
||||
delta++
|
||||
}
|
||||
})
|
||||
// console.log('判断需要返回几层:', delta);
|
||||
if (uniIdRedirectUrl) {
|
||||
return uni.redirectTo({
|
||||
url: uniIdRedirectUrl,
|
||||
fail: (err1) => {
|
||||
uni.switchTab({
|
||||
url:uniIdRedirectUrl,
|
||||
fail: (err2) => {
|
||||
console.log(err1,err2)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// #ifdef H5
|
||||
if (e.loginType == 'weixin') {
|
||||
// console.log('window.history', window.history);
|
||||
return window.history.go(-3)
|
||||
}
|
||||
// #endif
|
||||
|
||||
if (delta) {
|
||||
const page = pagesJson.pages[0]
|
||||
return uni.reLaunch({
|
||||
url: `/${page.path}`
|
||||
})
|
||||
}
|
||||
|
||||
uni.navigateBack({
|
||||
delta
|
||||
})
|
||||
},
|
||||
loginSuccess(e = {}){
|
||||
const {
|
||||
showToast = true, toastText = '登录成功', autoBack = true, uniIdRedirectUrl = '', passwordConfirmed
|
||||
} = e
|
||||
// console.log({toastText,autoBack});
|
||||
if (showToast) {
|
||||
uni.showToast({
|
||||
title: toastText,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
this.updateUserInfo()
|
||||
|
||||
uni.$emit('uni-id-pages-login-success')
|
||||
|
||||
if (config.setPasswordAfterLogin && !passwordConfirmed) {
|
||||
return uni.redirectTo({
|
||||
url: uniIdRedirectUrl ? `/uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd?uniIdRedirectUrl=${uniIdRedirectUrl}&loginType=${e.loginType}`: `/uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd?loginType=${e.loginType}`,
|
||||
fail: (err) => {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (autoBack) {
|
||||
this.loginBack({uniIdRedirectUrl})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// #ifdef VUE2
|
||||
import Vue from 'vue'
|
||||
// 通过Vue.observable创建一个可响应的对象
|
||||
export const store = Vue.observable(data)
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import {
|
||||
reactive
|
||||
} from 'vue'
|
||||
// 通过Vue.observable创建一个可响应的对象
|
||||
export const store = reactive(data)
|
||||
// #endif
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<view @click="onClick" :style="{width,height}" style="justify-content: center;">
|
||||
<image v-if="cSrc" :style="{width,height}" :src="cSrc" :mode="mode"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* cloud-image
|
||||
* @description 兼容普通资源和unicloud图片资源渲染的组件
|
||||
* @property {String} mode 图片裁剪、缩放的模式。默认为widthFix,支持所有image组件的mode值
|
||||
* @property {String} src 资源完了链接或uniCloud云存储资源的fileid
|
||||
* @property {String} width 图片的宽,默认为:100rpx
|
||||
* @property {String} height 图片的高,默认为:100rpx
|
||||
* @event {Function} click 点击 cloud-image 触发事件
|
||||
*/
|
||||
export default {
|
||||
name: "cloud-image",
|
||||
emits:['click'],
|
||||
props: {
|
||||
mode: {
|
||||
type:String,
|
||||
default () {
|
||||
return 'widthFix'
|
||||
}
|
||||
},
|
||||
src: {
|
||||
// type:String,
|
||||
default () {
|
||||
return ""
|
||||
}
|
||||
},
|
||||
width: {
|
||||
type:String,
|
||||
default () {
|
||||
return '100rpx'
|
||||
}
|
||||
},
|
||||
height: {
|
||||
type:String,
|
||||
default () {
|
||||
return '100rpx'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
src:{
|
||||
handler(src) {
|
||||
if (src&&src.substring(0, 8) == "cloud://") {
|
||||
uniCloud.getTempFileURL({
|
||||
fileList: [src]
|
||||
}).then(res=>{
|
||||
this.cSrc = res.fileList[0].tempFileURL
|
||||
})
|
||||
}else{
|
||||
this.cSrc = src
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
onClick(){
|
||||
this.$emit('click')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cSrc:false
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<view class="root" v-if="agreements.length">
|
||||
<template v-if="needAgreements">
|
||||
<checkbox-group @change="setAgree">
|
||||
<label class="checkbox-box">
|
||||
<checkbox :checked="isAgree" style="transform: scale(0.5);margin-right: -6px;" />
|
||||
<text class="text">同意</text>
|
||||
</label>
|
||||
</checkbox-group>
|
||||
<view class="content">
|
||||
<view class="item" v-for="(agreement,index) in agreements" :key="index">
|
||||
<text class="agreement text" @click="navigateTo(agreement)">{{agreement.title}}</text>
|
||||
<text class="text and" v-if="hasAnd(agreements,index)" space="nbsp"> 和 </text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 弹出式 -->
|
||||
<uni-popup v-if="needAgreements||needPopupAgreements" ref="popupAgreement" type="center">
|
||||
<uni-popup-dialog confirmText="同意" @confirm="popupConfirm">
|
||||
<view class="content">
|
||||
<text class="text">请先阅读并同意</text>
|
||||
<view class="item" v-for="(agreement,index) in agreements" :key="index">
|
||||
<text class="agreement text" @click="navigateTo(agreement)">{{agreement.title}}</text>
|
||||
<text class="text and" v-if="hasAnd(agreements,index)" space="nbsp"> 和 </text>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup-dialog>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
let retryFun = ()=>console.log('为定义')
|
||||
/**
|
||||
* uni-id-pages-agreements
|
||||
* @description 用户服务协议和隐私政策条款组件
|
||||
* @property {String,Boolean} scope = [register|login] 作用于哪种场景如:register 注册(包括登录并注册,如:微信登录、苹果登录、短信验证码登录)、login 登录。默认值为:register
|
||||
*/
|
||||
export default {
|
||||
name: "uni-agreements",
|
||||
computed: {
|
||||
agreements() {
|
||||
if(!config.agreements){
|
||||
return []
|
||||
}
|
||||
let {serviceUrl,privacyUrl} = config.agreements
|
||||
return [
|
||||
{
|
||||
url:serviceUrl,
|
||||
title:"用户服务协议"
|
||||
},
|
||||
{
|
||||
url:privacyUrl,
|
||||
title:"隐私政策条款"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
props: {
|
||||
scope: {
|
||||
type: String,
|
||||
default(){
|
||||
return 'register'
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
popupConfirm(){
|
||||
this.isAgree = true
|
||||
retryFun()
|
||||
// this.$emit('popupConfirm')
|
||||
},
|
||||
popup(Fun){
|
||||
this.needPopupAgreements = true
|
||||
// this.needAgreements = true
|
||||
this.$nextTick(()=>{
|
||||
if(Fun){
|
||||
retryFun = Fun
|
||||
}
|
||||
this.$refs.popupAgreement.open()
|
||||
})
|
||||
},
|
||||
navigateTo({
|
||||
url,
|
||||
title
|
||||
}) {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/common/webview/webview?url=' + url + '&title=' + title,
|
||||
success: res => {},
|
||||
fail: () => {},
|
||||
complete: () => {}
|
||||
});
|
||||
},
|
||||
hasAnd(agreements, index) {
|
||||
return agreements.length - 1 > index
|
||||
},
|
||||
setAgree(e) {
|
||||
this.isAgree = !this.isAgree
|
||||
this.$emit('setAgree', this.isAgree)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.needAgreements = (config?.agreements?.scope || []).includes(this.scope)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isAgree: false,
|
||||
needAgreements:true,
|
||||
needPopupAgreements:false
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* #ifndef APP-NVUE */
|
||||
view {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
.root {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #8a8f8b;
|
||||
}
|
||||
|
||||
.checkbox-box ,.uni-label-pointer{
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex-direction: row;
|
||||
}
|
||||
.text{
|
||||
line-height: 26px;
|
||||
}
|
||||
.agreement {
|
||||
color: #04498c;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-box ::v-deep .uni-checkbox-input{
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.checkbox-box ::v-deep .uni-checkbox-input.uni-checkbox-input-checked{
|
||||
border-color: $uni-color-primary;
|
||||
color: #FFFFFF !important;
|
||||
background-color: $uni-color-primary;
|
||||
}
|
||||
|
||||
.content{
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.root ::v-deep .uni-popup__error{
|
||||
color: #333333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<button open-type="chooseAvatar" @chooseavatar="bindchooseavatar" @click="uploadAvatarImg" class="box" :class="{'showBorder':border}" :style="{width,height,lineHeight:height}">
|
||||
<cloud-image v-if="avatar_file" :src="avatar_file.url" :width="width" :height="height"></cloud-image>
|
||||
<uni-icons v-else :style="{width,height,lineHeight:height}" class="chooseAvatar" type="plusempty" size="30"
|
||||
color="#dddddd"></uni-icons>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
store,
|
||||
mutations
|
||||
} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
/**
|
||||
* uni-id-pages-avatar
|
||||
* @description 用户头像组件
|
||||
* @property {String} width 图片的宽,默认为:50px
|
||||
* @property {String} height 图片的高,默认为:50px
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isPC: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
//头像图片宽
|
||||
width: {
|
||||
type: String,
|
||||
default () {
|
||||
return "50px"
|
||||
}
|
||||
},
|
||||
//头像图片高
|
||||
height: {
|
||||
type: String,
|
||||
default () {
|
||||
return "50px"
|
||||
}
|
||||
},
|
||||
border:{
|
||||
type: Boolean,
|
||||
default () {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// #ifdef H5
|
||||
this.isPC = !['ios', 'android'].includes(uni.getSystemInfoSync().platform);
|
||||
// #endif
|
||||
},
|
||||
computed: {
|
||||
hasLogin() {
|
||||
return store.hasLogin
|
||||
},
|
||||
userInfo() {
|
||||
return store.userInfo
|
||||
},
|
||||
avatar_file() {
|
||||
return store.userInfo.avatar_file
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setAvatarFile(avatar_file) {
|
||||
// 使用 clientDB 提交数据
|
||||
mutations.updateUserInfo({avatar_file})
|
||||
},
|
||||
async bindchooseavatar(res){
|
||||
let avatarUrl = res.detail.avatarUrl
|
||||
let avatar_file = {
|
||||
extname: avatarUrl.split('.')[avatarUrl.split('.').length - 1],
|
||||
name:'',
|
||||
url:''
|
||||
}
|
||||
//上传到服务器
|
||||
let cloudPath = this.userInfo._id + '' + Date.now()
|
||||
avatar_file.name = cloudPath
|
||||
try{
|
||||
uni.showLoading({
|
||||
title: "更新中",
|
||||
mask: true
|
||||
});
|
||||
let {
|
||||
fileID
|
||||
} = await uniCloud.uploadFile({
|
||||
filePath:avatarUrl,
|
||||
cloudPath,
|
||||
fileType: "image"
|
||||
});
|
||||
avatar_file.url = fileID
|
||||
uni.hideLoading()
|
||||
}catch(e){
|
||||
console.error(e);
|
||||
}
|
||||
console.log('avatar_file',avatar_file);
|
||||
this.setAvatarFile(avatar_file)
|
||||
},
|
||||
uploadAvatarImg(res) {
|
||||
// #ifdef MP-WEIXIN
|
||||
return false // 微信小程序走 bindchooseavatar方法
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
if(!this.hasLogin){
|
||||
return uni.navigateTo({
|
||||
url:'/uni_modules/uni-id-pages/pages/login/login-withoutpwd'
|
||||
})
|
||||
}
|
||||
const crop = {
|
||||
quality: 100,
|
||||
width: 600,
|
||||
height: 600,
|
||||
resize: true
|
||||
};
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
crop,
|
||||
success: async (res) => {
|
||||
let tempFile = res.tempFiles[0],
|
||||
avatar_file = {
|
||||
// #ifdef H5
|
||||
extname: tempFile.name.split('.')[tempFile.name.split('.').length - 1],
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
extname: tempFile.path.split('.')[tempFile.path.split('.').length - 1]
|
||||
// #endif
|
||||
},
|
||||
filePath = res.tempFilePaths[0]
|
||||
|
||||
//非app端剪裁头像,app端用内置的原生裁剪
|
||||
// #ifndef APP-PLUS
|
||||
filePath = await new Promise((callback) => {
|
||||
// #ifdef H5
|
||||
if (!this.isPC) {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/userinfo/cropImage/cropImage?path=' +
|
||||
filePath + `&options=${JSON.stringify(crop)}`,
|
||||
animationType: "fade-in",
|
||||
events: {
|
||||
success: url => {
|
||||
callback(url)
|
||||
}
|
||||
},
|
||||
complete(e) {
|
||||
// console.log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
// #endif
|
||||
|
||||
let cloudPath = this.userInfo._id + '' + Date.now()
|
||||
avatar_file.name = cloudPath
|
||||
uni.showLoading({
|
||||
title: "更新中",
|
||||
mask: true
|
||||
});
|
||||
let {
|
||||
fileID
|
||||
} = await uniCloud.uploadFile({
|
||||
filePath,
|
||||
cloudPath,
|
||||
fileType: "image"
|
||||
});
|
||||
avatar_file.url = fileID
|
||||
uni.hideLoading()
|
||||
this.setAvatarFile(avatar_file)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* #ifndef APP-NVUE */
|
||||
.box{
|
||||
overflow: hidden;
|
||||
}
|
||||
/* #endif */
|
||||
.box{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chooseAvatar {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
padding: 1px;
|
||||
}
|
||||
.showBorder{
|
||||
border: solid 1px #ddd;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom">
|
||||
<view class="box">
|
||||
<text class="headBox">绑定资料</text>
|
||||
<text class="tip">将一键获取你的手机号码绑定你的个人资料</text>
|
||||
<view class="btnBox">
|
||||
<text @click="closeMe" class="close">关闭</text>
|
||||
<button class="agree uni-btn" type="primary" open-type="getPhoneNumber"
|
||||
@getphonenumber="bindMobileByMpWeixin">获取</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const db = uniCloud.database();
|
||||
const usersTable = db.collection('uni-id-users')
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co")
|
||||
export default {
|
||||
emits: ['success'],
|
||||
computed: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
async beforeGetphonenumber() {
|
||||
return await new Promise((resolve,reject)=>{
|
||||
uni.showLoading({ mask: true })
|
||||
wx.checkSession({
|
||||
success() {
|
||||
// console.log('session_key 未过期');
|
||||
resolve()
|
||||
uni.hideLoading()
|
||||
},
|
||||
fail() {
|
||||
// console.log('session_key 已经失效,正在执行更新');
|
||||
wx.login({
|
||||
success({
|
||||
code
|
||||
}) {
|
||||
uniCloud.importObject("uni-id-co",{
|
||||
customUI:true
|
||||
}).loginByWeixin({code}).then(e=>{
|
||||
resolve()
|
||||
}).catch(e=>{
|
||||
console.log(e);
|
||||
reject()
|
||||
}).finally(e=>{
|
||||
uni.hideLoading()
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error(err);
|
||||
reject()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
async bindMobileByMpWeixin(e) {
|
||||
if (e.detail.errMsg == "getPhoneNumber:ok") {
|
||||
//检查登录信息是否过期,否则通过重新登录刷新session_key
|
||||
await this.beforeGetphonenumber()
|
||||
uniIdCo.bindMobileByMpWeixin(e.detail).then(e => {
|
||||
this.$emit('success')
|
||||
}).finally(e => {
|
||||
this.closeMe()
|
||||
})
|
||||
} else {
|
||||
this.closeMe()
|
||||
}
|
||||
},
|
||||
async open() {
|
||||
this.$refs.popup.open()
|
||||
},
|
||||
closeMe(e) {
|
||||
this.$refs.popup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
|
||||
view {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: #FFFFFF;
|
||||
height: 200px;
|
||||
width: 750rpx;
|
||||
flex-direction: column;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
}
|
||||
|
||||
.headBox {
|
||||
padding: 20rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
margin-left: 15rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: #666666;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
margin: 10rpx 30rpx;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btnBox {
|
||||
margin-top: 45rpx;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.close,
|
||||
.agree {
|
||||
text-align: center;
|
||||
width: 200rpx;
|
||||
height: 80upx;
|
||||
line-height: 80upx;
|
||||
border-radius: 5px;
|
||||
margin: 0 20rpx;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #999999;
|
||||
border-color: #EEEEEE;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.close:active {
|
||||
color: #989898;
|
||||
background-color: #E2E2E2;
|
||||
}
|
||||
|
||||
.agree {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* #ifdef MP */
|
||||
.agree::after {
|
||||
border: none;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.agree:active {
|
||||
background-color: #F5F5F6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<view>
|
||||
<uni-captcha :focus="focusCaptchaInput" ref="captcha" scene="send-email-code" v-model="captcha" />
|
||||
<view class="box">
|
||||
<uni-easyinput :focus="focusEmailCodeInput" @blur="focusEmailCodeInput = false" type="number" class="input-box" :inputBorder="false" v-model="modelValue" maxlength="6"
|
||||
placeholder="请输入邮箱验证码">
|
||||
</uni-easyinput>
|
||||
<view class="short-code-btn" hover-class="hover" @click="start">
|
||||
<text class="inner-text" :class="reverseNumber==0?'inner-text-active':''">{{innerText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function debounce(func, wait) {
|
||||
let timer;
|
||||
wait = wait || 500;
|
||||
return function() {
|
||||
let context = this;
|
||||
let args = arguments;
|
||||
if (timer) clearTimeout(timer);
|
||||
let callNow = !timer;
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
}, wait)
|
||||
if (callNow) func.apply(context, args);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* email-code-form
|
||||
* @description 获取邮箱验证码组件
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=
|
||||
* @property {Number} count 倒计时时长 s
|
||||
* @property {String} email 邮箱
|
||||
* @property {String} type = [login-by-email-code|reset-pwd-by-email-code|bind-email] 验证码类型,用于防止不同功能的验证码混用,目前支持的类型login登录、register注册、bind绑定邮箱、unbind解绑邮箱
|
||||
* @property {false} focusCaptchaInput = [true|false] 验证码输入框是否默认获取焦点
|
||||
*/
|
||||
export default {
|
||||
name: "uni-email-code-form",
|
||||
model: {
|
||||
prop: 'modelValue',
|
||||
event: 'update:modelValue'
|
||||
},
|
||||
props: {
|
||||
event: ['update:modelValue'],
|
||||
/**
|
||||
* 倒计时时长 s
|
||||
*/
|
||||
count: {
|
||||
type: [String, Number],
|
||||
default: 60
|
||||
},
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email: {
|
||||
type: [String],
|
||||
default: ''
|
||||
},
|
||||
/*
|
||||
验证码类型,用于防止不同功能的验证码混用,目前支持的类型login登录、register注册、bind绑定邮箱、unbind解绑邮箱
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
default () {
|
||||
return 'register'
|
||||
}
|
||||
},
|
||||
/*
|
||||
验证码输入框是否默认获取焦点
|
||||
*/
|
||||
focusCaptchaInput: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
captcha: "",
|
||||
reverseNumber: 0,
|
||||
reverseTimer: null,
|
||||
modelValue: "",
|
||||
focusEmailCodeInput:false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
captcha(value, oldValue) {
|
||||
if (value.length == 4 && oldValue.length != 4) {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
modelValue(value) {
|
||||
// TODO 兼容 vue2
|
||||
this.$emit('input', value);
|
||||
// TODO 兼容 vue3
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
innerText() {
|
||||
if (this.reverseNumber == 0) return "获取邮箱验证码";
|
||||
return "重新发送" + '(' + this.reverseNumber + 's)';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initClick();
|
||||
},
|
||||
methods: {
|
||||
getImageCaptcha(focus) {
|
||||
this.$refs.captcha.getImageCaptcha(focus)
|
||||
},
|
||||
initClick() {
|
||||
this.start = debounce(() => {
|
||||
if (this.reverseNumber != 0) return;
|
||||
this.sendMsg();
|
||||
})
|
||||
},
|
||||
sendMsg() {
|
||||
if (this.captcha.length != 4) {
|
||||
this.$refs.captcha.focusCaptchaInput = true
|
||||
return uni.showToast({
|
||||
title: '请先输入图形验证码',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
if(!this.email) return uni.showToast({
|
||||
title: "请输入邮箱",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
let reg_email = /@/;
|
||||
if (!reg_email.test(this.email)) return uni.showToast({
|
||||
title: "邮箱格式错误",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co", {
|
||||
customUI: true
|
||||
})
|
||||
console.log('sendEmailCode',{
|
||||
"email": this.email,
|
||||
"scene": this.type,
|
||||
"captcha": this.captcha
|
||||
});
|
||||
uniIdCo.sendEmailCode({
|
||||
"email": this.email,
|
||||
"scene": this.type,
|
||||
"captcha": this.captcha
|
||||
}).then(result => {
|
||||
uni.showToast({
|
||||
title: "邮箱验证码发送成功",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
this.reverseNumber = Number(this.count);
|
||||
this.getCode();
|
||||
}).catch(e => {
|
||||
if (e.code == "uni-id-invalid-mail-template") {
|
||||
this.modelValue = "123456"
|
||||
uni.showToast({
|
||||
title: '已启动测试模式,详情【控制台信息】',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
console.warn(e.message);
|
||||
} else {
|
||||
this.getImageCaptcha()
|
||||
this.captcha = ""
|
||||
uni.showToast({
|
||||
title: e.message,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
getCode() {
|
||||
if (this.reverseNumber == 0) {
|
||||
clearTimeout(this.reverseTimer);
|
||||
this.reverseTimer = null;
|
||||
return;
|
||||
}
|
||||
this.reverseNumber--;
|
||||
this.reverseTimer = setTimeout(() => {
|
||||
this.getCode();
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.short-code-btn {
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8px;
|
||||
width: 260rpx;
|
||||
max-width: 130px;
|
||||
height: 44px;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.inner-text {
|
||||
font-size: 14px;
|
||||
color: #AAAAAA;
|
||||
}
|
||||
|
||||
.inner-text-active {
|
||||
color: #04498c;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
width: 350rpx;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
background-color: #F8F8F8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.box ::v-deep .content-clear-icon {
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
.box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="fab-login-box">
|
||||
<view class="item" v-for="(item,index) in servicesList" :key="index"
|
||||
@click="item.path?toPage(item.path):login_before(item.id,false)">
|
||||
<image class="logo" :src="item.logo" mode="scaleToFill"></image>
|
||||
<text class="login-title">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
//前一个窗口的页面地址。控制点击切换快捷登录方式是创建还是返回
|
||||
import {store,mutations} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
let allServicesList = []
|
||||
export default {
|
||||
computed: {
|
||||
agreements() {
|
||||
if (!config.agreements) {
|
||||
return []
|
||||
}
|
||||
let {
|
||||
serviceUrl,
|
||||
privacyUrl
|
||||
} = config.agreements
|
||||
return [{
|
||||
url: serviceUrl,
|
||||
title: "用户服务协议"
|
||||
},
|
||||
{
|
||||
url: privacyUrl,
|
||||
title: "隐私政策条款"
|
||||
}
|
||||
]
|
||||
},
|
||||
agree: {
|
||||
get() {
|
||||
return this.getParentComponent().agree
|
||||
},
|
||||
set(agree) {
|
||||
return this.getParentComponent().agree = agree
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
servicesList: [{
|
||||
"id": "username",
|
||||
"text": "账号登录",
|
||||
"logo": "/uni_modules/uni-id-pages/static/login/uni-fab-login/user.png",
|
||||
"path": "/uni_modules/uni-id-pages/pages/login/login-withpwd"
|
||||
},
|
||||
{
|
||||
"id": "smsCode",
|
||||
"text": "短信验证码",
|
||||
"logo": "/uni_modules/uni-id-pages/static/login/uni-fab-login/sms.png",
|
||||
"path": "/uni_modules/uni-id-pages/pages/login/login-withoutpwd?type=smsCode"
|
||||
},
|
||||
{
|
||||
"id": "weixin",
|
||||
"text": "微信登录",
|
||||
"logo": "/uni_modules/uni-id-pages/static/login/uni-fab-login/weixin.png",
|
||||
},
|
||||
// #ifndef MP-WEIXIN
|
||||
{
|
||||
"id": "apple",
|
||||
"text": "苹果登录",
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/apple.png",
|
||||
},
|
||||
{
|
||||
"id": "univerify",
|
||||
"text": "一键登录",
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/univerify.png",
|
||||
},
|
||||
{
|
||||
"id": "taobao",
|
||||
"text": "淘宝登录", //暂未提供该登录方式的接口示例
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/taobao.png",
|
||||
},
|
||||
{
|
||||
"id": "facebook",
|
||||
"text": "脸书登录", //暂未提供该登录方式的接口示例
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/facebook.png",
|
||||
},
|
||||
{
|
||||
"id": "alipay",
|
||||
"text": "支付宝登录", //暂未提供该登录方式的接口示例
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/alipay.png",
|
||||
},
|
||||
{
|
||||
"id": "qq",
|
||||
"text": "QQ登录", //暂未提供该登录方式的接口示例
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/qq.png",
|
||||
},
|
||||
{
|
||||
"id": "google",
|
||||
"text": "谷歌登录", //暂未提供该登录方式的接口示例
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/google.png",
|
||||
},
|
||||
{
|
||||
"id": "douyin",
|
||||
"text": "抖音登录", //暂未提供该登录方式的接口示例
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/douyin.png",
|
||||
},
|
||||
{
|
||||
"id": "sinaweibo",
|
||||
"text": "新浪微博", //暂未提供该登录方式的接口示例
|
||||
"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/sinaweibo.png",
|
||||
}
|
||||
// #endif
|
||||
],
|
||||
univerifyStyle: { //一键登录弹出窗的样式配置参数
|
||||
"fullScreen": true, // 是否全屏显示,true表示全屏模式,false表示非全屏模式,默认值为false。
|
||||
"backgroundColor": "#ffffff", // 授权页面背景颜色,默认值:#ffffff
|
||||
"buttons": { // 自定义登录按钮
|
||||
"iconWidth": "45px", // 图标宽度(高度等比例缩放) 默认值:45px
|
||||
"list": []
|
||||
},
|
||||
"privacyTerms": {
|
||||
"defaultCheckBoxState": false, // 条款勾选框初始状态 默认值: true
|
||||
"textColor": "#BBBBBB", // 文字颜色 默认值:#BBBBBB
|
||||
"termsColor": "#5496E3", // 协议文字颜色 默认值: #5496E3
|
||||
"prefix": "我已阅读并同意", // 条款前的文案 默认值:“我已阅读并同意”
|
||||
"suffix": "并使用本机号码登录", // 条款后的文案 默认值:“并使用本机号码登录”
|
||||
"privacyItems": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
agree(agree) {
|
||||
this.univerifyStyle.privacyTerms.defaultCheckBoxState = agree
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
let servicesList = this.servicesList
|
||||
let loginTypes = config.loginTypes
|
||||
|
||||
servicesList = servicesList.filter(item => {
|
||||
|
||||
// #ifndef APP
|
||||
//非app端去掉apple登录
|
||||
if (item.id == 'apple') {
|
||||
return false
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP
|
||||
//去掉非ios系统上的apple登录
|
||||
if (item.id == 'apple' && uni.getSystemInfoSync().osName != 'ios') {
|
||||
return false
|
||||
}
|
||||
// #endif
|
||||
|
||||
return loginTypes.includes(item.id)
|
||||
})
|
||||
//处理一键登录
|
||||
if (loginTypes.includes('univerify')) {
|
||||
this.univerifyStyle.privacyTerms.privacyItems = this.agreements
|
||||
//设置一键登录功能底下的快捷登录按钮
|
||||
servicesList.forEach(({
|
||||
id,
|
||||
logo,
|
||||
path
|
||||
}) => {
|
||||
if (id != 'univerify') {
|
||||
this.univerifyStyle.buttons.list.push({
|
||||
"iconPath": logo,
|
||||
"provider": id,
|
||||
path //路径用于点击快捷按钮时判断是跳转页面
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// console.log(servicesList);
|
||||
|
||||
//去掉当前页面对应的登录选项
|
||||
this.servicesList = servicesList.filter(item => {
|
||||
let path = item.path ? item.path.split('?')[0] : '';
|
||||
return path != this.getRoute(1)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
getParentComponent(){
|
||||
// #ifndef H5
|
||||
return this.$parent;
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
return this.$parent.$parent;
|
||||
// #endif
|
||||
},
|
||||
setUserInfo(e) {
|
||||
console.log('setUserInfo', e);
|
||||
},
|
||||
getRoute(n = 0) {
|
||||
let pages = getCurrentPages();
|
||||
if (n > pages.length) {
|
||||
return ''
|
||||
}
|
||||
return '/' + pages[pages.length - n].route
|
||||
},
|
||||
toPage(path,index = 0) {
|
||||
//console.log('比较', this.getRoute(1),this.getRoute(2), path)
|
||||
if (this.getRoute(1) == path.split('?')[0] && this.getRoute(1) ==
|
||||
'/uni_modules/uni-id-pages/pages/login/login-withoutpwd') {
|
||||
//如果要被打开的页面已经打开,且这个页面是 /uni_modules/uni-id-pages/pages/index/index 则把类型参数传给他
|
||||
let loginType = path.split('?')[1].split('=')[1]
|
||||
uni.$emit('uni-id-pages-setLoginType', loginType)
|
||||
} else if (this.getRoute(2) == path) { // 如果上一个页面就是,马上要打开的页面,直接返回。防止重复开启
|
||||
uni.navigateBack();
|
||||
} else if (this.getRoute(1) != path) {
|
||||
if(index === 0){
|
||||
uni.navigateTo({
|
||||
url: path,
|
||||
animationType: 'slide-in-left',
|
||||
complete(e) {
|
||||
// console.log(e);
|
||||
}
|
||||
})
|
||||
}else{
|
||||
uni.redirectTo({
|
||||
url: path,
|
||||
animationType: 'slide-in-left',
|
||||
complete(e) {
|
||||
// console.log(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('出乎意料的情况,path:' + path);
|
||||
}
|
||||
},
|
||||
async login_before(type, navigateBack = true, options = {}) {
|
||||
console.log(type);
|
||||
//提示空实现
|
||||
if (["qq",
|
||||
"xiaomi",
|
||||
"sinaweibo",
|
||||
"taobao",
|
||||
"facebook",
|
||||
"google",
|
||||
"alipay",
|
||||
"douyin",
|
||||
].includes(type)) {
|
||||
return uni.showToast({
|
||||
title: '该登录方式暂未实现,欢迎提交pr',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
//检查当前环境是否支持这种登录方式
|
||||
// #ifdef APP
|
||||
let isAppExist = true
|
||||
await new Promise((callback) => {
|
||||
plus.oauth.getServices(oauthServices => {
|
||||
let index = oauthServices.findIndex(e => e.id == type)
|
||||
if(index != -1){
|
||||
isAppExist = oauthServices[index].nativeClient
|
||||
callback()
|
||||
}else{
|
||||
return uni.showToast({
|
||||
title: '当前设备不支持此登录,请选择其他登录方式',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
}, err => {
|
||||
throw new Error('获取服务供应商失败:' + JSON.stringify(err))
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
|
||||
if (
|
||||
// #ifdef APP
|
||||
!isAppExist
|
||||
// #endif
|
||||
|
||||
//非app端使用了,app特有登录方式
|
||||
// #ifndef APP
|
||||
["univerify","apple"].includes(type)
|
||||
// #endif
|
||||
|
||||
) {
|
||||
return uni.showToast({
|
||||
title: '当前设备不支持此登录,请选择其他登录方式',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
//判断是否需要弹出隐私协议授权框
|
||||
let needAgreements = (config?.agreements?.scope || []).includes('register')
|
||||
if (type != 'univerify' && needAgreements && !this.agree) {
|
||||
let agreementsRef = this.getParentComponent().$refs.agreements
|
||||
return agreementsRef.popup(() => {
|
||||
this.login_before(type, navigateBack, options)
|
||||
})
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
if(type == 'weixin'){
|
||||
// console.log('开始微信网页登录');
|
||||
// let redirectUrl = location.protocol +'//'+
|
||||
// document.domain +
|
||||
// (window.location.href.includes('#')?'/#':'') +
|
||||
// '/uni_modules/uni-id-pages/pages/login/login-withoutpwd?is_weixin_redirect=true&type=weixin'
|
||||
// #ifdef VUE2
|
||||
const baseUrl = process.env.BASE_URL
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
const baseUrl = import.meta.env.BASE_URL
|
||||
// #endif
|
||||
|
||||
let redirectUrl = location.protocol +
|
||||
'//' +
|
||||
location.host +
|
||||
baseUrl.replace(/\/$/, '') +
|
||||
(window.location.href.includes('#')?'/#':'') +
|
||||
'/uni_modules/uni-id-pages/pages/login/login-withoutpwd?is_weixin_redirect=true&type=weixin'
|
||||
|
||||
// console.log('redirectUrl----',redirectUrl);
|
||||
let ua = window.navigator.userAgent.toLowerCase();
|
||||
if (ua.match(/MicroMessenger/i) == 'micromessenger'){
|
||||
// console.log('在微信公众号内');
|
||||
return window.open(`https://open.weixin.qq.com/connect/oauth2/authorize?
|
||||
appid=${config.appid.weixin.h5}
|
||||
&redirect_uri=${encodeURIComponent(redirectUrl)}
|
||||
&response_type=code
|
||||
&scope=snsapi_userinfo
|
||||
&state=STATE&connect_redirect=1#wechat_redirect`);
|
||||
|
||||
}else{
|
||||
// console.log('非微信公众号内');
|
||||
return location.href = `https://open.weixin.qq.com/connect/qrconnect?appid=${config.appid.weixin.web}
|
||||
&redirect_uri=${encodeURIComponent(redirectUrl)}
|
||||
&response_type=code&scope=snsapi_login&state=STATE#wechat_redirect`
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
uni.showLoading({
|
||||
mask: true
|
||||
})
|
||||
|
||||
if (type == 'univerify') {
|
||||
let univerifyManager = uni.getUniverifyManager()
|
||||
let clickAnotherButtons = false
|
||||
let onButtonsClickFn = async res => {
|
||||
console.log('点击了第三方登录,provider:', res, res.provider, this.univerifyStyle.buttons.list);
|
||||
clickAnotherButtons = true
|
||||
let checkBoxState = await uni.getCheckBoxState();
|
||||
// 同步一键登录弹出层隐私协议框是否打勾
|
||||
// #ifdef VUE2
|
||||
this.agree = checkBoxState[1].state
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.agree = checkBoxState.state
|
||||
// #endif
|
||||
let {
|
||||
path
|
||||
} = this.univerifyStyle.buttons.list[res.index]
|
||||
if (path) {
|
||||
if( this.getRoute(1).includes('login-withoutpwd') && path.includes('login-withoutpwd') ){
|
||||
this.getParentComponent().showCurrentWebview()
|
||||
}
|
||||
this.toPage(path,1)
|
||||
closeUniverify()
|
||||
} else {
|
||||
if (this.agree) {
|
||||
closeUniverify()
|
||||
setTimeout(() => {
|
||||
this.login_before(res.provider)
|
||||
}, 500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "你未同意隐私政策协议",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeUniverify() {
|
||||
uni.hideLoading()
|
||||
univerifyManager.close()
|
||||
// 取消订阅自定义按钮点击事件
|
||||
univerifyManager.offButtonsClick(onButtonsClickFn)
|
||||
}
|
||||
// 订阅自定义按钮点击事件
|
||||
univerifyManager.onButtonsClick(onButtonsClickFn)
|
||||
// 调用一键登录弹框
|
||||
return univerifyManager.login({
|
||||
"univerifyStyle": this.univerifyStyle,
|
||||
success: res => {
|
||||
this.login(res.authResult, 'univerify')
|
||||
},
|
||||
fail(err) {
|
||||
console.log(err)
|
||||
if(!clickAnotherButtons){
|
||||
uni.navigateBack()
|
||||
}
|
||||
// uni.showToast({
|
||||
// title: JSON.stringify(err),
|
||||
// icon: 'none',
|
||||
// duration: 3000
|
||||
// });
|
||||
},
|
||||
complete: async e => {
|
||||
uni.hideLoading()
|
||||
//同步一键登录弹出层隐私协议框是否打勾
|
||||
// this.agree = (await uni.getCheckBoxState())[1].state
|
||||
// 取消订阅自定义按钮点击事件
|
||||
univerifyManager.offButtonsClick(onButtonsClickFn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'weixinMobile') {
|
||||
return this.login({
|
||||
phoneCode: options.phoneNumberCode
|
||||
}, type)
|
||||
}
|
||||
|
||||
uni.login({
|
||||
"provider": type,
|
||||
"onlyAuthorize": true,
|
||||
// #ifdef APP
|
||||
"univerifyStyle": this.univerifyStyle,
|
||||
// #endif
|
||||
success: async e => {
|
||||
if (type == 'apple') {
|
||||
let res = await this.getUserInfo({
|
||||
provider: "apple"
|
||||
})
|
||||
Object.assign(e.authResult, res.userInfo)
|
||||
uni.hideLoading()
|
||||
}
|
||||
this.login(type == 'weixin' ? {
|
||||
code: e.code
|
||||
} : e.authResult, type)
|
||||
},
|
||||
fail: async (err) => {
|
||||
console.log(err);
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
},
|
||||
login(params, type) { //联网验证登录
|
||||
// console.log('执行登录开始----');
|
||||
console.log({params,type});
|
||||
//toLowerCase
|
||||
let action = 'loginBy' + type.trim().replace(type[0], type[0].toUpperCase())
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co",{
|
||||
customUI:true
|
||||
})
|
||||
uniIdCo[action](params).then(result => {
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
// #ifdef H5
|
||||
result.loginType = type
|
||||
// #endif
|
||||
mutations.loginSuccess(result)
|
||||
})
|
||||
.catch(e=>{
|
||||
uni.showModal({
|
||||
content: e.message,
|
||||
confirmText:"知道了",
|
||||
showCancel: false
|
||||
});
|
||||
})
|
||||
.finally(e => {
|
||||
if (type == 'univerify') {
|
||||
uni.closeAuthView()
|
||||
}
|
||||
uni.hideLoading()
|
||||
})
|
||||
},
|
||||
async getUserInfo(e) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getUserInfo({
|
||||
...e,
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.showModal({
|
||||
content: JSON.stringify(err),
|
||||
showCancel: false
|
||||
});
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* #ifndef APP-NVUE */
|
||||
.fab-login-box,
|
||||
.item {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.fab-login-box {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 750rpx;
|
||||
justify-content: space-around;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200rpx;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
@media screen and (min-width: 690px) {
|
||||
.fab-login-box {
|
||||
max-width: 500px;
|
||||
margin-left: calc(50% - 250px);
|
||||
}
|
||||
.item {
|
||||
height: 160rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 690px) {
|
||||
.fab-login-box {
|
||||
bottom: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
.logo {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
border-radius: 100%;
|
||||
border: solid 1px #F6F6F6;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
width: 70px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<view>
|
||||
<uni-captcha :focus="focusCaptchaInput" ref="captcha" scene="send-sms-code" v-model="captcha" />
|
||||
<view class="box">
|
||||
<uni-easyinput :focus="focusSmsCodeInput" @blur="focusSmsCodeInput = false" type="number" class="input-box" :inputBorder="false" v-model="modelValue" maxlength="6" :clearable="false"
|
||||
placeholder="请输入短信验证码">
|
||||
</uni-easyinput>
|
||||
<view class="short-code-btn" hover-class="hover" @click="start">
|
||||
<text class="inner-text" :class="reverseNumber==0?'inner-text-active':''">{{innerText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function debounce(func, wait) {
|
||||
let timer;
|
||||
wait = wait || 500;
|
||||
return function() {
|
||||
let context = this;
|
||||
let args = arguments;
|
||||
if (timer) clearTimeout(timer);
|
||||
let callNow = !timer;
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
}, wait)
|
||||
if (callNow) func.apply(context, args);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* sms-form
|
||||
* @description 获取短信验证码组件
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=
|
||||
* @property {Number} count 倒计时时长 s
|
||||
* @property {String} phone 手机号码
|
||||
* @property {String} type = [login-by-sms|reset-pwd-by-sms|bind-mobile] 验证码类型,用于防止不同功能的验证码混用,目前支持的类型login登录、register注册、bind绑定手机、unbind解绑手机
|
||||
* @property {false} focusCaptchaInput = [true|false] 验证码输入框是否默认获取焦点
|
||||
*/
|
||||
export default {
|
||||
name: "uni-sms-form",
|
||||
model: {
|
||||
prop: 'modelValue',
|
||||
event: 'update:modelValue'
|
||||
},
|
||||
props: {
|
||||
event: ['update:modelValue'],
|
||||
/**
|
||||
* 倒计时时长 s
|
||||
*/
|
||||
count: {
|
||||
type: [String, Number],
|
||||
default: 60
|
||||
},
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
phone: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
/*
|
||||
验证码类型,用于防止不同功能的验证码混用,目前支持的类型login登录、register注册、bind绑定手机、unbind解绑手机
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
default () {
|
||||
return 'login'
|
||||
}
|
||||
},
|
||||
/*
|
||||
验证码输入框是否默认获取焦点
|
||||
*/
|
||||
focusCaptchaInput: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
captcha: "",
|
||||
reverseNumber: 0,
|
||||
reverseTimer: null,
|
||||
modelValue: "",
|
||||
focusSmsCodeInput:false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
captcha(value, oldValue) {
|
||||
if (value.length == 4 && oldValue.length != 4) {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
modelValue(value) {
|
||||
// TODO 兼容 vue2
|
||||
this.$emit('input', value);
|
||||
// TODO 兼容 vue3
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
innerText() {
|
||||
if (this.reverseNumber == 0) return "获取短信验证码";
|
||||
return "重新发送" + '(' + this.reverseNumber + 's)';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initClick();
|
||||
},
|
||||
methods: {
|
||||
getImageCaptcha(focus) {
|
||||
this.$refs.captcha.getImageCaptcha(focus)
|
||||
},
|
||||
initClick() {
|
||||
this.start = debounce(() => {
|
||||
if (this.reverseNumber != 0) return;
|
||||
this.sendMsg();
|
||||
})
|
||||
},
|
||||
sendMsg() {
|
||||
if (this.captcha.length != 4) {
|
||||
this.$refs.captcha.focusCaptchaInput = true
|
||||
return uni.showToast({
|
||||
title: '请先输入图形验证码',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
let reg_phone = /^1\d{10}$/;
|
||||
if (!reg_phone.test(this.phone)) return uni.showToast({
|
||||
title: "手机号格式错误",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co", {
|
||||
customUI: true
|
||||
})
|
||||
console.log('sendSmsCode',{
|
||||
"mobile": this.phone,
|
||||
"scene": this.type,
|
||||
"captcha": this.captcha
|
||||
});
|
||||
uniIdCo.sendSmsCode({
|
||||
"mobile": this.phone,
|
||||
"scene": this.type,
|
||||
"captcha": this.captcha
|
||||
}).then(result => {
|
||||
uni.showToast({
|
||||
title: "短信验证码发送成功",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
this.reverseNumber = Number(this.count);
|
||||
this.getCode();
|
||||
}).catch(e => {
|
||||
if (e.code == "uni-id-invalid-sms-template-id") {
|
||||
this.modelValue = "123456"
|
||||
uni.showToast({
|
||||
title: '已启动测试模式,详情【控制台信息】',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
console.warn(e.message);
|
||||
} else {
|
||||
this.getImageCaptcha()
|
||||
this.captcha = ""
|
||||
uni.showToast({
|
||||
title: e.message,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
getCode() {
|
||||
if (this.reverseNumber == 0) {
|
||||
clearTimeout(this.reverseTimer);
|
||||
this.reverseTimer = null;
|
||||
return;
|
||||
}
|
||||
this.reverseNumber--;
|
||||
this.reverseTimer = setTimeout(() => {
|
||||
this.getCode();
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.short-code-btn {
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8px;
|
||||
width: 260rpx;
|
||||
max-width: 100px;
|
||||
height: 44px;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inner-text {
|
||||
font-size: 14px;
|
||||
color: #AAAAAA;
|
||||
}
|
||||
|
||||
.inner-text-active {
|
||||
color: #04498c;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
width: 350rpx;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
background-color: #F8F8F8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.box ::v-deep .content-clear-icon {
|
||||
margin-right: 110px;
|
||||
}
|
||||
|
||||
.box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
67
uni-im示例/uni_modules/uni-id-pages/config.js
Normal file
@@ -0,0 +1,67 @@
|
||||
export default {
|
||||
// 调试模式
|
||||
debug: false,
|
||||
/*
|
||||
登录类型 未列举到的或运行环境不支持的,将被自动隐藏。
|
||||
如果需要在不同平台有不同的配置,直接用条件编译即可
|
||||
*/
|
||||
isAdmin: false, // 区分管理端与用户端
|
||||
loginTypes: [
|
||||
// "qq",
|
||||
// "xiaomi",
|
||||
// "sinaweibo",
|
||||
// "taobao",
|
||||
// "facebook",
|
||||
// "google",
|
||||
// "alipay",
|
||||
// "douyin",
|
||||
|
||||
// #ifdef APP
|
||||
'univerify',
|
||||
// #endif
|
||||
'weixin',
|
||||
'username',
|
||||
// #ifdef APP
|
||||
'apple',
|
||||
// #endif
|
||||
'smsCode'
|
||||
],
|
||||
// 政策协议
|
||||
agreements: {
|
||||
serviceUrl: 'https://xxx', // 用户服务协议链接
|
||||
privacyUrl: 'https://xxx', // 隐私政策条款链接
|
||||
// 哪些场景下显示,1.注册(包括登录并注册,如:微信登录、苹果登录、短信验证码登录)、2.登录(如:用户名密码登录)
|
||||
scope: [
|
||||
'register', 'login', 'realNameVerify'
|
||||
]
|
||||
},
|
||||
// 提供各类服务接入(如微信登录服务)的应用id
|
||||
appid: {
|
||||
weixin: {
|
||||
// 微信公众号的appid,来源:登录微信公众号(https://mp.weixin.qq.com)-> 设置与开发 -> 基本配置 -> 公众号开发信息 -> AppID
|
||||
h5: 'xxxxxx',
|
||||
// 微信开放平台的appid,来源:登录微信开放平台(https://open.weixin.qq.com) -> 管理中心 -> 网站应用 -> 选择对应的应用名称,点击查看 -> AppID
|
||||
web: 'xxxxxx'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 密码强度
|
||||
* super(超强:密码必须包含大小写字母、数字和特殊符号,长度范围:8-16位之间)
|
||||
* strong(强: 密密码必须包含字母、数字和特殊符号,长度范围:8-16位之间)
|
||||
* medium (中:密码必须为字母、数字和特殊符号任意两种的组合,长度范围:8-16位之间)
|
||||
* weak(弱:密码必须包含字母和数字,长度范围:6-16位之间)
|
||||
* 为空或false则不验证密码强度
|
||||
*/
|
||||
passwordStrength: 'medium',
|
||||
/**
|
||||
* 登录后允许用户设置密码(只针对未设置密码得用户)
|
||||
* 开启此功能将 setPasswordAfterLogin 设置为 true 即可
|
||||
* "setPasswordAfterLogin": false
|
||||
*
|
||||
* 如果允许用户跳过设置密码 将 allowSkip 设置为 true
|
||||
* "setPasswordAfterLogin": {
|
||||
* "allowSkip": true
|
||||
* }
|
||||
* */
|
||||
setPasswordAfterLogin: false
|
||||
}
|
||||
95
uni-im示例/uni_modules/uni-id-pages/init.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// 导入配置
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
// uni-id的云对象
|
||||
const uniIdCo = uniCloud.importObject('uni-id-co', {
|
||||
customUI: true
|
||||
})
|
||||
// 用户配置的登录方式、是否打开调试模式
|
||||
const {
|
||||
loginTypes,
|
||||
debug
|
||||
} = config
|
||||
|
||||
export default async function () {
|
||||
// 有打开调试模式的情况下
|
||||
if (debug) {
|
||||
// 1. 检查本地uni-id-pages中配置的登录方式,服务器端是否已经配置正确。否则提醒并引导去配置
|
||||
// 调用云对象,获取服务端已正确配置的登录方式
|
||||
const {
|
||||
supportedLoginType
|
||||
} = await uniIdCo.getSupportedLoginType()
|
||||
console.log('supportedLoginType: ' + JSON.stringify(supportedLoginType))
|
||||
// 登录方式,服务端和客户端的映射关系
|
||||
const data = {
|
||||
smsCode: 'mobile-code',
|
||||
univerify: 'univerify',
|
||||
username: 'username-password',
|
||||
weixin: 'weixin',
|
||||
qq: 'qq',
|
||||
xiaomi: 'xiaomi',
|
||||
sinaweibo: 'sinaweibo',
|
||||
taobao: 'taobao',
|
||||
facebook: 'facebook',
|
||||
google: 'google',
|
||||
alipay: 'alipay',
|
||||
apple: 'apple',
|
||||
weixinMobile: 'weixin'
|
||||
}
|
||||
// 遍历客户端配置的登录方式,与服务端比对。并在错误时抛出错误提示
|
||||
const list = loginTypes.filter(type => !supportedLoginType.includes(data[type]))
|
||||
if (list.length) {
|
||||
console.error(
|
||||
`错误:前端启用的登录方式:${list.join(',')};没有在服务端完成配置。配置文件路径:"/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json"`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// 如果uni-id-pages配置的登录功能有一键登录,有则执行预登录(异步)
|
||||
if (loginTypes.includes('univerify')) {
|
||||
uni.preLogin({
|
||||
provider: 'univerify',
|
||||
complete: e => {
|
||||
// console.log(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 3. 绑定clientDB错误事件
|
||||
// clientDB对象
|
||||
const db = uniCloud.database()
|
||||
db.on('error', onDBError)
|
||||
// clientDB的错误提示
|
||||
function onDBError ({
|
||||
code, // 错误码详见https://uniapp.dcloud.net.cn/uniCloud/clientdb?id=returnvalue
|
||||
message
|
||||
}) {
|
||||
// console.error('onDBError', {code,message});
|
||||
}
|
||||
// 解绑clientDB错误事件
|
||||
// db.off('error', onDBError)
|
||||
|
||||
// 4. 同步客户端push_clientid至device表
|
||||
if (uniCloud.onRefreshToken) {
|
||||
uniCloud.onRefreshToken(() => {
|
||||
// console.log('onRefreshToken');
|
||||
if (uni.getPushClientId) {
|
||||
uni.getPushClientId({
|
||||
success: async function (e) {
|
||||
// console.log(e)
|
||||
const pushClientId = e.cid
|
||||
// console.log(pushClientId);
|
||||
const res = await uniIdCo.setPushCid({
|
||||
pushClientId
|
||||
})
|
||||
// console.log('getPushClientId', res);
|
||||
},
|
||||
fail (e) {
|
||||
// console.log(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
102
uni-im示例/uni_modules/uni-id-pages/package.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"id": "uni-id-pages",
|
||||
"displayName": "uni-id-pages",
|
||||
"version": "1.1.14",
|
||||
"description": "云端一体简单、统一、可扩展的用户中心页面模版",
|
||||
"keywords": [
|
||||
"用户管理",
|
||||
"用户中心",
|
||||
"短信验证码",
|
||||
"login",
|
||||
"登录"
|
||||
],
|
||||
"repository": "https://gitcode.net/dcloud/hello_uni-id-pages",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.4.17"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"type": "unicloud-template-page"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"uni-captcha",
|
||||
"uni-config-center",
|
||||
"uni-data-checkbox",
|
||||
"uni-easyinput",
|
||||
"uni-forms",
|
||||
"uni-icons",
|
||||
"uni-id-common",
|
||||
"uni-list",
|
||||
"uni-load-more",
|
||||
"uni-popup",
|
||||
"uni-scss",
|
||||
"uni-transition",
|
||||
"uni-open-bridge-common",
|
||||
"uni-cloud-s2s"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "u",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- 网络链接内容展示页(uni-id-pages中用于展示隐私政策协议内容) -->
|
||||
<template>
|
||||
<view>
|
||||
<web-view v-if="url" :src="url"></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
onLoad({url,title}) {
|
||||
if(url.substring(0, 4) != 'http'){
|
||||
uni.showModal({
|
||||
title:"错误",
|
||||
content: '不是一个有效的网站链接,'+'"'+url+'"',
|
||||
showCancel: false,
|
||||
confirmText:"知道了",
|
||||
complete: () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
});
|
||||
title = "页面路径错误"
|
||||
}else{
|
||||
this.url = url;
|
||||
}
|
||||
if(title){
|
||||
uni.setNavigationBarTitle({title});
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
url:null
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
120
uni-im示例/uni_modules/uni-id-pages/pages/login/login-smscode.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<!-- 短信验证码登录页 -->
|
||||
<template>
|
||||
<view class="uni-content">
|
||||
<view class="login-logo">
|
||||
<image :src="logo"></image>
|
||||
</view>
|
||||
<!-- 顶部文字 -->
|
||||
<text class="title">请输入验证码</text>
|
||||
<text class="tip">先输入图形验证码,再获取短信验证码</text>
|
||||
<uni-forms>
|
||||
<uni-id-pages-sms-form focusCaptchaInput v-model="code" type="login-by-sms" ref="smsCode" :phone="phone">
|
||||
</uni-id-pages-sms-form>
|
||||
<button class="uni-btn send-btn" type="primary" @click="submit">登录</button>
|
||||
</uni-forms>
|
||||
<uni-popup-captcha @confirm="submit" v-model="captcha" scene="login-by-sms" ref="popup"></uni-popup-captcha>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
|
||||
export default {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
"code": "",
|
||||
"phone": "",
|
||||
"captcha": "",
|
||||
"logo": "/static/logo.png"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tipText() {
|
||||
return '验证码已通过短信发送至' + this.phone;
|
||||
},
|
||||
},
|
||||
onLoad({
|
||||
phoneNumber
|
||||
}) {
|
||||
this.phone = phoneNumber;
|
||||
},
|
||||
onShow() {
|
||||
// #ifdef H5
|
||||
document.onkeydown = event => {
|
||||
var e = event || window.event;
|
||||
if (e && e.keyCode == 13) { //回车键的键值为13
|
||||
this.submit()
|
||||
}
|
||||
};
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
submit() { //完成并提交
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co", {
|
||||
errorOptions: {
|
||||
type: 'toast'
|
||||
}
|
||||
})
|
||||
if (this.code.length != 6) {
|
||||
this.$refs.smsCode.focusSmsCodeInput = true
|
||||
return uni.showToast({
|
||||
title: '验证码不能为空',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
uniIdCo.loginBySms({
|
||||
"mobile": this.phone,
|
||||
"code": this.code,
|
||||
"captcha": this.captcha
|
||||
}).then(e => {
|
||||
this.loginSuccess(e)
|
||||
}).catch(e => {
|
||||
if (e.errCode == 'uni-id-captcha-required') {
|
||||
this.$refs.popup.open()
|
||||
} else {
|
||||
console.log(e.errMsg);
|
||||
}
|
||||
}).finally(e => {
|
||||
this.captcha = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
|
||||
|
||||
.tip {
|
||||
margin-top: -15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.popup-captcha {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
padding: 20rpx;
|
||||
background-color: #FFF;
|
||||
border-radius: 2px;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup-captcha .title {
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
padding-bottom: 15px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.popup-captcha .close {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
margin-left: -13px;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.popup-captcha .uni-btn {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<!-- 免密登录页 -->
|
||||
<template>
|
||||
<view class="uni-content">
|
||||
<view class="login-logo">
|
||||
<image :src="logo"></image>
|
||||
</view>
|
||||
<!-- 顶部文字 -->
|
||||
<text class="title">请选择登录方式</text>
|
||||
<!-- 快捷登录框 当url带参数时有效 -->
|
||||
<template v-if="['apple','weixin', 'weixinMobile'].includes(type)">
|
||||
<text class="tip">将根据第三方账号服务平台的授权范围获取你的信息</text>
|
||||
<view class="quickLogin">
|
||||
<image v-if="type !== 'weixinMobile'" @click="quickLogin" :src="imgSrc" mode="widthFix"
|
||||
class="quickLoginBtn"></image>
|
||||
<button v-else type="primary" open-type="getPhoneNumber" @getphonenumber="quickLogin"
|
||||
class="uni-btn">微信授权手机号登录</button>
|
||||
<uni-id-pages-agreements scope="register" ref="agreements"></uni-id-pages-agreements>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="tip">未注册的账号验证通过后将自动注册</text>
|
||||
<view class="phone-box">
|
||||
<view @click="chooseArea" class="area">+86</view>
|
||||
<uni-easyinput :focus="focusPhone" @blur="focusPhone = false" class="input-box" type="number"
|
||||
:inputBorder="false" v-model="phone" maxlength="11" placeholder="请输入手机号" />
|
||||
</view>
|
||||
<uni-id-pages-agreements scope="register" ref="agreements"></uni-id-pages-agreements>
|
||||
<button class="uni-btn" type="primary" @click="toSmsPage">获取验证码</button>
|
||||
</template>
|
||||
<!-- 固定定位的快捷登录按钮 -->
|
||||
<uni-id-pages-fab-login ref="uniFabLogin"></uni-id-pages-fab-login>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let currentWebview; //当前窗口对象
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
|
||||
export default {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
type: "", //快捷登录方式
|
||||
phone: "", //手机号码
|
||||
focusPhone: false,
|
||||
logo: "/static/logo.png"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
async loginTypes() { //读取配置的登录优先级
|
||||
return config.loginTypes
|
||||
},
|
||||
isPhone() { //手机号码校验正则
|
||||
return /^1\d{10}$/.test(this.phone);
|
||||
},
|
||||
imgSrc() { //大快捷登录按钮图
|
||||
return this.type == 'weixin' ? '/uni_modules/uni-id-pages/static/login/weixin.png' :
|
||||
'/uni_modules/uni-id-pages/static/app-plus/apple.png'
|
||||
}
|
||||
},
|
||||
async onLoad(e) {
|
||||
//获取通过url传递的参数type设置当前登录方式,如果没传递直接默认以配置的登录
|
||||
let type = e.type || config.loginTypes[0]
|
||||
this.type = type
|
||||
|
||||
// console.log("this.type: -----------",this.type);
|
||||
if (type != 'univerify') {
|
||||
this.focusPhone = true
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
//关闭重复显示的登录快捷方式
|
||||
if (['weixin', 'apple'].includes(type)) {
|
||||
this.$refs.uniFabLogin.servicesList = this.$refs.uniFabLogin.servicesList.filter(item =>
|
||||
item.id != type)
|
||||
}
|
||||
})
|
||||
uni.$on('uni-id-pages-setLoginType', type => {
|
||||
this.type = type
|
||||
})
|
||||
},
|
||||
onShow() {
|
||||
// #ifdef H5
|
||||
document.onkeydown = event => {
|
||||
var e = event || window.event;
|
||||
if (e && e.keyCode == 13) { //回车键的键值为13
|
||||
this.toSmsPage()
|
||||
}
|
||||
};
|
||||
// #endif
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('uni-id-pages-setLoginType')
|
||||
},
|
||||
onReady() {
|
||||
// 是否优先启动一键登录。即:页面一加载就启动一键登录
|
||||
//#ifdef APP-PLUS
|
||||
if (this.type == "univerify") {
|
||||
const pages = getCurrentPages();
|
||||
currentWebview = pages[pages.length - 1].$getAppWebview();
|
||||
currentWebview.setStyle({
|
||||
"top": "2000px" // 隐藏当前页面窗体
|
||||
})
|
||||
this.type == this.loginTypes[1]
|
||||
// console.log('开始一键登录');
|
||||
this.$refs.uniFabLogin.login_before('univerify')
|
||||
}
|
||||
//#endif
|
||||
},
|
||||
methods: {
|
||||
showCurrentWebview(){
|
||||
// 恢复当前页面窗体的显示 一键登录,默认不显示当前窗口
|
||||
currentWebview.setStyle({
|
||||
"top": 0
|
||||
})
|
||||
},
|
||||
quickLogin(e) {
|
||||
let options = {}
|
||||
|
||||
if (e.detail?.code) {
|
||||
options.phoneNumberCode = e.detail.code
|
||||
}
|
||||
|
||||
if (this.type === 'weixinMobile' && !e.detail?.code) return
|
||||
|
||||
this.$refs.uniFabLogin.login_before(this.type, true, options)
|
||||
},
|
||||
toSmsPage() {
|
||||
if (!this.isPhone) {
|
||||
this.focusPhone = true
|
||||
return uni.showToast({
|
||||
title: "手机号码格式不正确",
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
if (this.needAgreements && !this.agree) {
|
||||
return this.$refs.agreements.popup(this.toSmsPage)
|
||||
}
|
||||
// 发送验证吗
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/login/login-smscode?phoneNumber=' + this.phone
|
||||
});
|
||||
},
|
||||
//去密码登录页
|
||||
toPwdLogin() {
|
||||
uni.navigateTo({
|
||||
url: '../login/password'
|
||||
})
|
||||
},
|
||||
chooseArea() {
|
||||
uni.showToast({
|
||||
title: '暂不支持其他国家',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
|
||||
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content {
|
||||
height: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.uni-content,
|
||||
.quickLogin {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.phone-box {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.area {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
z-index: 9;
|
||||
top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.area::after {
|
||||
content: "";
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #000;
|
||||
top: 12px;
|
||||
left: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* #ifdef MP */
|
||||
// 解决小程序端开启虚拟节点virtualHost引起的 class = input-box丢失的问题 [详情参考](https://uniapp.dcloud.net.cn/matter.html#%E5%90%84%E5%AE%B6%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6%E4%B8%8D%E5%90%8C-%E5%8F%AF%E8%83%BD%E5%AD%98%E5%9C%A8%E7%9A%84%E5%B9%B3%E5%8F%B0%E5%85%BC%E5%AE%B9%E9%97%AE%E9%A2%98)
|
||||
.phone-box ::v-deep .uni-easyinput__content,
|
||||
/* #endif */
|
||||
.input-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
padding-left: 45px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.quickLogin {
|
||||
height: 350px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quickLoginBtn {
|
||||
margin: 20px 0;
|
||||
width: 450rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
max-width: 230px;
|
||||
/* #endif */
|
||||
height: 82rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: -15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 690px) {
|
||||
.quickLogin {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
176
uni-im示例/uni_modules/uni-id-pages/pages/login/login-withpwd.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<!-- 账号密码登录页 -->
|
||||
<template>
|
||||
<view class="uni-content">
|
||||
<view class="login-logo">
|
||||
<image :src="logo"></image>
|
||||
</view>
|
||||
<!-- 顶部文字 -->
|
||||
<text class="title title-box">账号密码登录</text>
|
||||
<uni-forms>
|
||||
<uni-forms-item name="username">
|
||||
<uni-easyinput :focus="focusUsername" @blur="focusUsername = false" class="input-box"
|
||||
:inputBorder="false" v-model="username" placeholder="请输入手机号/用户名/邮箱" />
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="password">
|
||||
<uni-easyinput :focus="focusPassword" @blur="focusPassword = false" class="input-box" clearable
|
||||
type="password" :inputBorder="false" v-model="password" placeholder="请输入密码" />
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
<uni-captcha v-if="needCaptcha" focus ref="captcha" scene="login-by-pwd" v-model="captcha" />
|
||||
<!-- 带选择框的隐私政策协议组件 -->
|
||||
<uni-id-pages-agreements scope="login" ref="agreements"></uni-id-pages-agreements>
|
||||
<button class="uni-btn" type="primary" @click="pwdLogin">登录</button>
|
||||
<!-- 忘记密码 -->
|
||||
<view class="link-box">
|
||||
<view v-if="!config.isAdmin">
|
||||
<text class="forget">忘记了?</text>
|
||||
<text class="link" @click="toRetrievePwd">找回密码</text>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
"password": "",
|
||||
"username": "",
|
||||
"captcha": "",
|
||||
"needCaptcha": false,
|
||||
"focusUsername": false,
|
||||
"focusPassword": false,
|
||||
"logo": "/static/logo.png"
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// #ifdef H5
|
||||
document.onkeydown = event => {
|
||||
var e = event || window.event;
|
||||
if (e && e.keyCode == 13) { //回车键的键值为13
|
||||
this.pwdLogin()
|
||||
}
|
||||
};
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// 页面跳转,找回密码
|
||||
toRetrievePwd() {
|
||||
let url = '/uni_modules/uni-id-pages/pages/retrieve/retrieve'
|
||||
//如果刚好用户名输入框的值为手机号码,就把它传到retrieve页面,根据该手机号找回密码
|
||||
if (/^1\d{10}$/.test(this.username)) {
|
||||
url += `?phoneNumber=${this.username}`
|
||||
}
|
||||
uni.navigateTo({
|
||||
url
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 密码登录
|
||||
*/
|
||||
pwdLogin() {
|
||||
if (!this.password.length) {
|
||||
this.focusPassword = true
|
||||
return uni.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
if (!this.username.length) {
|
||||
this.focusUsername = true
|
||||
return uni.showToast({
|
||||
title: '请输入手机号/用户名/邮箱',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
if (this.needCaptcha && this.captcha.length != 4) {
|
||||
this.$refs.captcha.getImageCaptcha()
|
||||
return uni.showToast({
|
||||
title: '请输入验证码',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
if (this.needAgreements && !this.agree) {
|
||||
return this.$refs.agreements.popup(this.pwdLogin)
|
||||
}
|
||||
|
||||
let data = {
|
||||
"password": this.password,
|
||||
"captcha": this.captcha
|
||||
}
|
||||
|
||||
if (/^1\d{10}$/.test(this.username)) {
|
||||
data.mobile = this.username
|
||||
} else if (/@/.test(this.username)) {
|
||||
data.email = this.username
|
||||
} else {
|
||||
data.username = this.username
|
||||
}
|
||||
|
||||
uniIdCo.login(data).then(e => {
|
||||
this.loginSuccess(e)
|
||||
}).catch(e => {
|
||||
if (e.errCode == 'uni-id-captcha-required') {
|
||||
this.needCaptcha = true
|
||||
} else if (this.needCaptcha) {
|
||||
//登录失败,自动重新获取验证码
|
||||
this.$refs.captcha.getImageCaptcha()
|
||||
}
|
||||
})
|
||||
},
|
||||
/* 前往注册 */
|
||||
toRegister() {
|
||||
uni.navigateTo({
|
||||
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) {
|
||||
.uni-content {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.forget {
|
||||
font-size: 12px;
|
||||
color: #8a8f8b;
|
||||
}
|
||||
|
||||
.link-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,178 @@
|
||||
<!-- 创建超级管理员 -->
|
||||
<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',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
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 => {
|
||||
uni.navigateBack()
|
||||
})
|
||||
.catch(e => {
|
||||
//更好的体验:登录错误,直接刷新验证码
|
||||
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;
|
||||
max-height: 520px;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,216 @@
|
||||
<!-- 邮箱验证码注册 -->
|
||||
<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="email" required>
|
||||
<uni-easyinput :inputBorder="false" :focus="focusEmail" @blur="focusEmail = false"
|
||||
class="input-box" placeholder="请输入邮箱" v-model="formData.email" 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 name="code" >
|
||||
<uni-id-pages-email-form ref="shortCode" :email="formData.email" type="register" v-model="formData.code">
|
||||
</uni-id-pages-email-form>
|
||||
</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="registerByUserName">用户名密码注册</text>
|
||||
<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'
|
||||
import passwordMod from '@/uni_modules/uni-id-pages/common/password.js'
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co")
|
||||
export default {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
email: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
password2: "",
|
||||
code: ""
|
||||
},
|
||||
rules: {
|
||||
email: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入邮箱',
|
||||
},{
|
||||
format:'email',
|
||||
errorMessage: '邮箱格式不正确',
|
||||
}
|
||||
]
|
||||
},
|
||||
nickname: {
|
||||
rules: [{
|
||||
minLength: 3,
|
||||
maxLength: 32,
|
||||
errorMessage: '昵称长度在 {minLength} 到 {maxLength} 个字符',
|
||||
},
|
||||
{
|
||||
validateFunction: function(rule, value, data, callback) {
|
||||
// console.log(value);
|
||||
if (/^1\d{10}$/.test(value) || /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(value)) {
|
||||
callback('昵称不能是:手机号或邮箱')
|
||||
};
|
||||
if (/^\d+$/.test(value)) {
|
||||
callback('昵称不能为纯数字')
|
||||
};
|
||||
if(/[\u4E00-\u9FA5\uF900-\uFA2D]{1,}/.test(value)){
|
||||
callback('昵称不能包含中文')
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
],
|
||||
label: "昵称"
|
||||
},
|
||||
...passwordMod.getPwdRules(),
|
||||
code: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入邮箱验证码',
|
||||
},
|
||||
{
|
||||
pattern: /^.{6}$/,
|
||||
errorMessage: '邮箱验证码不正确',
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
focusEmail: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.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.registerUserByEmail(this.formData).then(e => {
|
||||
// console.log(e);
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/login/login-withpwd',
|
||||
complete: (e) => {
|
||||
// console.log(e);
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
// console.log(e);
|
||||
console.log(e.message);
|
||||
})
|
||||
},
|
||||
navigateBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
toLogin() {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/login/login-withpwd'
|
||||
})
|
||||
},
|
||||
registerByUserName() {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/register/register'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content{
|
||||
padding: 30px 40px;
|
||||
max-height: 650px;
|
||||
}
|
||||
.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>
|
||||
181
uni-im示例/uni_modules/uni-id-pages/pages/register/register.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<!-- 账号注册页 -->
|
||||
<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="registerByEmail">邮箱验证码注册</text>
|
||||
<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'
|
||||
import {
|
||||
store,
|
||||
mutations
|
||||
} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co")
|
||||
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',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
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())
|
||||
this['focus' + key] = true
|
||||
})
|
||||
},
|
||||
submitForm(params) {
|
||||
uniIdCo.registerUser(this.formData).then(e => {
|
||||
this.loginSuccess(e)
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e.message);
|
||||
//更好的体验:登录错误,直接刷新验证码
|
||||
this.$refs.captcha.getImageCaptcha()
|
||||
})
|
||||
},
|
||||
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;
|
||||
max-height: 530px;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,56 @@
|
||||
import passwordMod from '@/uni_modules/uni-id-pages/common/password.js'
|
||||
export default {
|
||||
"username": {
|
||||
"rules": [{
|
||||
required: true,
|
||||
errorMessage: '请输入用户名',
|
||||
},
|
||||
{
|
||||
minLength: 3,
|
||||
maxLength: 32,
|
||||
errorMessage: '用户名长度在 {minLength} 到 {maxLength} 个字符',
|
||||
},
|
||||
{
|
||||
validateFunction: function(rule, value, data, callback) {
|
||||
// console.log(value);
|
||||
if (/^1\d{10}$/.test(value) || /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(value)) {
|
||||
callback('用户名不能是:手机号或邮箱')
|
||||
};
|
||||
if (/^\d+$/.test(value)) {
|
||||
callback('用户名不能为纯数字')
|
||||
};
|
||||
if(/[\u4E00-\u9FA5\uF900-\uFA2D]{1,}/.test(value)){
|
||||
callback('用户名不能包含中文')
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
],
|
||||
"label": "用户名"
|
||||
},
|
||||
"nickname": {
|
||||
"rules": [{
|
||||
minLength: 3,
|
||||
maxLength: 32,
|
||||
errorMessage: '昵称长度在 {minLength} 到 {maxLength} 个字符',
|
||||
},
|
||||
{
|
||||
validateFunction: function(rule, value, data, callback) {
|
||||
// console.log(value);
|
||||
if (/^1\d{10}$/.test(value) || /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(value)) {
|
||||
callback('昵称不能是:手机号或邮箱')
|
||||
};
|
||||
if (/^\d+$/.test(value)) {
|
||||
callback('昵称不能为纯数字')
|
||||
};
|
||||
if(/[\u4E00-\u9FA5\uF900-\uFA2D]{1,}/.test(value)){
|
||||
callback('昵称不能包含中文')
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
],
|
||||
"label": "昵称"
|
||||
},
|
||||
...passwordMod.getPwdRules()
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<!-- 找回密码页 -->
|
||||
<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" err-show-type="toast">
|
||||
<uni-forms-item name="email">
|
||||
<uni-easyinput :focus="focusEmail" @blur="focusEmail = false" class="input-box" :disabled="lock" :inputBorder="false"
|
||||
v-model="formData.email" placeholder="请输入邮箱">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="code">
|
||||
<uni-id-pages-email-form ref="shortCode" :email="formData.email" type="reset-pwd-by-email" v-model="formData.code">
|
||||
</uni-id-pages-email-form>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="password">
|
||||
<uni-easyinput :focus="focusPassword" @blur="focusPassword = false" class="input-box" type="password" :inputBorder="false" v-model="formData.password"
|
||||
placeholder="请输入新密码"></uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="password2">
|
||||
<uni-easyinput :focus="focusPassword2" @blur="focusPassword2 = false" class="input-box" type="password" :inputBorder="false" v-model="formData.password2"
|
||||
placeholder="请再次输入新密码"></uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<button class="uni-btn send-btn-box" type="primary" @click="submit">提交</button>
|
||||
<match-media :min-width="690">
|
||||
<view class="link-box">
|
||||
<text class="link" @click="retrieveByPhone">通过手机验证码找回密码</text>
|
||||
<view></view>
|
||||
<text class="link" @click="backLogin">返回登录</text>
|
||||
</view>
|
||||
</match-media>
|
||||
</uni-forms>
|
||||
<uni-popup-captcha @confirm="submit" v-model="formData.captcha" scene="reset-pwd-by-sms" ref="popup"></uni-popup-captcha>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
|
||||
import passwordMod from '@/uni_modules/uni-id-pages/common/password.js'
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co",{
|
||||
errorOptions:{
|
||||
type:'toast'
|
||||
}
|
||||
})
|
||||
export default {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
lock: false,
|
||||
focusEmail:true,
|
||||
focusPassword:false,
|
||||
focusPassword2:false,
|
||||
formData: {
|
||||
"email": "",
|
||||
"code": "",
|
||||
'password': '',
|
||||
'password2': '',
|
||||
"captcha": ""
|
||||
},
|
||||
rules: {
|
||||
email: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入邮箱',
|
||||
},
|
||||
{
|
||||
format:'email',
|
||||
errorMessage: '邮箱格式不正确',
|
||||
}
|
||||
]
|
||||
},
|
||||
code: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入邮箱验证码',
|
||||
},
|
||||
{
|
||||
pattern: /^.{6}$/,
|
||||
errorMessage: '请输入6位验证码',
|
||||
}
|
||||
]
|
||||
},
|
||||
...passwordMod.getPwdRules()
|
||||
},
|
||||
logo: "/static/logo.png"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEmail() {
|
||||
let reg_email = /@/;
|
||||
let isEmail = reg_email.test(this.formData.email);
|
||||
return isEmail;
|
||||
},
|
||||
isPwd() {
|
||||
let reg_pwd = /^.{6,20}$/;
|
||||
let isPwd = reg_pwd.test(this.formData.password);
|
||||
return isPwd;
|
||||
},
|
||||
isCode() {
|
||||
let reg_code = /^\d{6}$/;
|
||||
let isCode = reg_code.test(this.formData.code);
|
||||
return isCode;
|
||||
}
|
||||
},
|
||||
onLoad(event) {
|
||||
if (event && event.emailNumber) {
|
||||
this.formData.email = event.emailNumber;
|
||||
if(event.lock){
|
||||
this.lock = event.lock //如果是已经登录的账号,点击找回密码就锁定指定的账号绑定的邮箱码
|
||||
this.focusEmail = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
if (this.formData.email) {
|
||||
this.$refs.shortCode.start();
|
||||
}
|
||||
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 => {
|
||||
let {
|
||||
email,
|
||||
password: password,
|
||||
captcha,
|
||||
code
|
||||
} = this.formData
|
||||
uniIdCo.resetPwdByEmail({
|
||||
email,
|
||||
code,
|
||||
password,
|
||||
captcha
|
||||
}).then(e => {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/login/login-withpwd',
|
||||
complete: (e) => {
|
||||
// console.log(e);
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.errCode == 'uni-id-captcha-required') {
|
||||
this.$refs.popup.open()
|
||||
}
|
||||
}).finally(e => {
|
||||
this.formData.captcha = ""
|
||||
})
|
||||
}).catch(errors=>{
|
||||
let key = errors[0].key
|
||||
if(key == 'code'){
|
||||
return this.$refs.shortCode.focusSmsCodeInput = true
|
||||
}
|
||||
key = key.replace(key[0], key[0].toUpperCase())
|
||||
this['focus'+key] = true
|
||||
})
|
||||
},
|
||||
retrieveByPhone() {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/retrieve/retrieve'
|
||||
})
|
||||
},
|
||||
backLogin () {
|
||||
uni.redirectTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/login/login-withpwd'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content{
|
||||
padding: 30px 40px 40px;
|
||||
max-height: 650px;
|
||||
}
|
||||
|
||||
.link-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
241
uni-im示例/uni_modules/uni-id-pages/pages/retrieve/retrieve.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<!-- 找回密码页 -->
|
||||
<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" err-show-type="toast">
|
||||
<uni-forms-item name="phone">
|
||||
<uni-easyinput :focus="focusPhone" @blur="focusPhone = false" class="input-box" :disabled="lock" type="number" :inputBorder="false"
|
||||
v-model="formData.phone" maxlength="11" placeholder="请输入手机号">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="code">
|
||||
<uni-id-pages-sms-form ref="shortCode" :phone="formData.phone" type="reset-pwd-by-sms" v-model="formData.code">
|
||||
</uni-id-pages-sms-form>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="password">
|
||||
<uni-easyinput :focus="focusPassword" @blur="focusPassword = false" class="input-box" type="password" :inputBorder="false" v-model="formData.password"
|
||||
placeholder="请输入新密码"></uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="password2">
|
||||
<uni-easyinput :focus="focusPassword2" @blur="focusPassword2 = false" class="input-box" type="password" :inputBorder="false" v-model="formData.password2"
|
||||
placeholder="请再次输入新密码"></uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<button class="uni-btn send-btn-box" type="primary" @click="submit">提交</button>
|
||||
<match-media :min-width="690">
|
||||
<view class="link-box">
|
||||
<text class="link" @click="retrieveByEmail">通过邮箱验证码找回密码</text>
|
||||
<view></view>
|
||||
<text class="link" @click="backLogin">返回登录</text>
|
||||
</view>
|
||||
</match-media>
|
||||
</uni-forms>
|
||||
<uni-popup-captcha @confirm="submit" v-model="formData.captcha" scene="reset-pwd-by-sms" ref="popup"></uni-popup-captcha>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
lock: false,
|
||||
focusPhone:true,
|
||||
focusPassword:false,
|
||||
focusPassword2:false,
|
||||
formData: {
|
||||
"phone": "",
|
||||
"code": "",
|
||||
'password': '',
|
||||
'password2': '',
|
||||
"captcha": ""
|
||||
},
|
||||
rules: {
|
||||
phone: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入手机号',
|
||||
},
|
||||
{
|
||||
pattern: /^1\d{10}$/,
|
||||
errorMessage: '手机号码格式不正确',
|
||||
}
|
||||
]
|
||||
},
|
||||
code: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入短信验证码',
|
||||
},
|
||||
{
|
||||
pattern: /^.{6}$/,
|
||||
errorMessage: '请输入6位验证码',
|
||||
}
|
||||
]
|
||||
},
|
||||
password: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入新密码',
|
||||
},
|
||||
{
|
||||
pattern: /^.{6,20}$/,
|
||||
errorMessage: '密码为6 - 20位',
|
||||
}
|
||||
]
|
||||
},
|
||||
password2: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请确认密码',
|
||||
},
|
||||
{
|
||||
pattern: /^.{6,20}$/,
|
||||
errorMessage: '密码为6 - 20位',
|
||||
},
|
||||
{
|
||||
validateFunction: function(rule, value, data, callback) {
|
||||
// console.log(value);
|
||||
if (value != data.password) {
|
||||
callback('两次输入密码不一致')
|
||||
};
|
||||
return true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
logo: "/static/logo.png"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPhone() {
|
||||
let reg_phone = /^1\d{10}$/;
|
||||
let isPhone = reg_phone.test(this.formData.phone);
|
||||
return isPhone;
|
||||
},
|
||||
isPwd() {
|
||||
let reg_pwd = /^.{6,20}$/;
|
||||
let isPwd = reg_pwd.test(this.formData.password);
|
||||
return isPwd;
|
||||
},
|
||||
isCode() {
|
||||
let reg_code = /^\d{6}$/;
|
||||
let isCode = reg_code.test(this.formData.code);
|
||||
return isCode;
|
||||
}
|
||||
},
|
||||
onLoad(event) {
|
||||
if (event && event.phoneNumber) {
|
||||
this.formData.phone = event.phoneNumber;
|
||||
if(event.lock){
|
||||
this.lock = event.lock //如果是已经登录的账号,点击找回密码就锁定指定的账号绑定的手机号码
|
||||
this.focusPhone = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
if (this.formData.phone) {
|
||||
this.$refs.shortCode.start();
|
||||
}
|
||||
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 => {
|
||||
let {
|
||||
"phone": mobile,
|
||||
"password": password,
|
||||
captcha,
|
||||
code
|
||||
} = this.formData
|
||||
uniIdCo.resetPwdBySms({
|
||||
mobile,
|
||||
code,
|
||||
password,
|
||||
captcha
|
||||
}).then(e => {
|
||||
uni.navigateBack()
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.errCode == 'uni-id-captcha-required') {
|
||||
this.$refs.popup.open()
|
||||
}
|
||||
}).finally(e => {
|
||||
this.formData.captcha = ""
|
||||
})
|
||||
}).catch(errors=>{
|
||||
let key = errors[0].key
|
||||
if(key == 'code'){
|
||||
return this.$refs.shortCode.focusSmsCodeInput = true
|
||||
}
|
||||
key = key.replace(key[0], key[0].toUpperCase())
|
||||
this['focus'+key] = true
|
||||
})
|
||||
},
|
||||
retrieveByEmail() {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/retrieve/retrieve-by-email'
|
||||
})
|
||||
},
|
||||
backLogin () {
|
||||
uni.redirectTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/login/login-withpwd'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content{
|
||||
padding: 30px 40px 40px;
|
||||
max-height: 650px;
|
||||
}
|
||||
.link-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<!-- 绑定手机号码页 -->
|
||||
<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-easyinput clearable :focus="focusMobile" @blur="focusMobile = false" type="number" class="input-box" :inputBorder="false" v-model="formData.mobile"
|
||||
maxlength="11" placeholder="请输入手机号"></uni-easyinput>
|
||||
<uni-id-pages-sms-form ref="smsForm" type="bind-mobile-by-sms" v-model="formData.code" :phone="formData.mobile">
|
||||
</uni-id-pages-sms-form>
|
||||
<button class="uni-btn send-btn-box" type="primary" @click="submit">提交</button>
|
||||
<uni-popup-captcha @confirm="submit" v-model="formData.captcha" scene="bind-mobile-by-sms" ref="popup">
|
||||
</uni-popup-captcha>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
store,
|
||||
mutations
|
||||
} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
mobile: "",
|
||||
code: "",
|
||||
captcha: ""
|
||||
},
|
||||
focusMobile:true,
|
||||
logo: "/static/logo.png"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tipText() {
|
||||
return `验证码已通过短信发送至 ${this.formData.mobile}。密码为6 - 20位`
|
||||
}
|
||||
},
|
||||
onLoad(event) {},
|
||||
onReady() {},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 完成并提交
|
||||
*/
|
||||
submit() {
|
||||
if(! /^1\d{10}$/.test(this.formData.mobile)){
|
||||
this.focusMobile = true
|
||||
return uni.showToast({
|
||||
title: '手机号码格式不正确',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
if(! /^\d{6}$/.test(this.formData.code)){
|
||||
this.$refs.smsForm.focusSmsCodeInput = true
|
||||
return uni.showToast({
|
||||
title: '验证码格式不正确',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co")
|
||||
uniIdCo.bindMobileBySms(this.formData).then(e => {
|
||||
uni.showToast({
|
||||
title: e.errMsg,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
// #ifdef APP-NVUE
|
||||
const eventChannel = this.$scope.eventChannel; // 兼容APP-NVUE
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
const eventChannel = this.getOpenerEventChannel();
|
||||
// #endif
|
||||
mutations.setUserInfo(this.formData)
|
||||
uni.navigateBack()
|
||||
}).catch(e => {
|
||||
console.log(e);
|
||||
if (e.errCode == 'uni-id-captcha-required') {
|
||||
this.$refs.popup.open()
|
||||
}
|
||||
}).finally(e => {
|
||||
this.formData.captcha = ""
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
|
||||
|
||||
.uni-content {
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 50rpx;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content{
|
||||
padding: 30px 40px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE || VUE3 */
|
||||
.uni-content ::v-deep .uni-easyinput__content {}
|
||||
|
||||
/* #endif */
|
||||
.input-box {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 6rpx;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.send-btn-box {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<!-- 修改密码 -->
|
||||
<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" err-show-type="toast">
|
||||
<uni-forms-item name="oldPassword">
|
||||
<uni-easyinput :focus="focusOldPassword" @blur="focusOldPassword = false" class="input-box"
|
||||
type="password" :inputBorder="false" v-model="formData.oldPassword" placeholder="请输入旧密码">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="newPassword">
|
||||
<uni-easyinput :focus="focusNewPassword" @blur="focusNewPassword = false" class="input-box"
|
||||
type="password" :inputBorder="false" v-model="formData.newPassword" placeholder="请输入新密码">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="newPassword2">
|
||||
<uni-easyinput :focus="focusNewPassword2" @blur="focusNewPassword2 = false" class="input-box"
|
||||
type="password" :inputBorder="false" v-model="formData.newPassword2" placeholder="请再次输入新密码">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<button class="uni-btn send-btn-box" type="primary" @click="submit">提交</button>
|
||||
</uni-forms>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
|
||||
import passwordMod from '@/uni_modules/uni-id-pages/common/password.js'
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co", {
|
||||
customUI:true
|
||||
})
|
||||
export default {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
focusOldPassword: false,
|
||||
focusNewPassword: false,
|
||||
focusNewPassword2: false,
|
||||
formData: {
|
||||
'oldPassword': '',
|
||||
'newPassword': '',
|
||||
'newPassword2': '',
|
||||
},
|
||||
rules: {
|
||||
oldPassword: {
|
||||
rules: [{
|
||||
required: true,
|
||||
errorMessage: '请输入新密码',
|
||||
},
|
||||
{
|
||||
pattern: /^.{6,20}$/,
|
||||
errorMessage: '密码为6 - 20位',
|
||||
}
|
||||
]
|
||||
},
|
||||
...passwordMod.getPwdRules('newPassword', 'newPassword2')
|
||||
},
|
||||
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 => {
|
||||
let {
|
||||
oldPassword,
|
||||
newPassword
|
||||
} = this.formData
|
||||
uniIdCo.updatePwd({
|
||||
oldPassword,
|
||||
newPassword
|
||||
}).then(e => {
|
||||
uni.removeStorageSync('uni_id_token');
|
||||
uni.setStorageSync('uni_id_token_expired', 0)
|
||||
uni.redirectTo({
|
||||
url:'/uni_modules/uni-id-pages/pages/login/login-withpwd'
|
||||
})
|
||||
}).catch(e => {
|
||||
uni.showModal({
|
||||
content: e.message,
|
||||
showCancel: false
|
||||
});
|
||||
})
|
||||
}).catch(errors => {
|
||||
let key = errors[0].key
|
||||
key = key.replace(key[0], key[0].toUpperCase())
|
||||
this['focus' + key] = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content{
|
||||
padding: 30px 40px 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- 图片裁剪页 -->
|
||||
<template>
|
||||
<view class="content" >
|
||||
<limeClipper :width="options.width" :scale-ratio="2" :is-lock-width="false" :is-lock-height="false" :height="options.height" :image-url="path"
|
||||
@success="successFn" @cancel="cancel" />
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import limeClipper from './limeClipper/limeClipper.vue';
|
||||
export default {
|
||||
components: {limeClipper},
|
||||
data() {return {path: '',options:{"width":600,"height":600}}},
|
||||
onLoad({path,options}) {
|
||||
this.path = path
|
||||
// console.log('path-path-path-path',path);
|
||||
if(options){
|
||||
this.options = JSON.parse(options)
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
successFn(e){
|
||||
this.getOpenerEventChannel().emit('success',e.url)
|
||||
uni.navigateBack()
|
||||
},
|
||||
cancel(){
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.box{
|
||||
width: 400rpx;
|
||||
}
|
||||
.mt{
|
||||
margin-top: -10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,227 @@
|
||||
> 插件来源:[https://ext.dcloud.net.cn/plugin?id=3594](https://ext.dcloud.net.cn/plugin?id=3594)
|
||||
##### 以下是作者写的插件介绍:
|
||||
|
||||
# Clipper 图片裁剪
|
||||
> uniapp 图片裁剪,可用于图片头像等裁剪处理
|
||||
> [查看更多](http://liangei.gitee.io/limeui/#/clipper) <br>
|
||||
> Q群:458377637
|
||||
|
||||
|
||||
## 平台兼容
|
||||
|
||||
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
|
||||
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
|
||||
| √ | √ | √ | 未测 | √ | √ | √ |
|
||||
|
||||
|
||||
## 代码演示
|
||||
### 基本用法
|
||||
`@success` 事件点击 👉 **确定** 后会返回生成的图片信息,包含 `url`、`width`、`height`
|
||||
|
||||
```html
|
||||
<image :src="url" v-if="url" mode="widthFix"></image>
|
||||
<l-clipper v-if="show" @success="url = $event.url; show = false" @cancel="show = false" ></l-clipper>
|
||||
<button @tap="show = true">裁剪</button>
|
||||
```
|
||||
|
||||
```js
|
||||
// 非uni_modules引入
|
||||
import lClipper from '@/components/lime-clipper/'
|
||||
// uni_modules引入
|
||||
import lClipper from '@/uni_modules/lime-clipper/components/lime-clipper/'
|
||||
export default {
|
||||
components: {lClipper},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 传入图片
|
||||
`image-url`可传入**相对路径**、**临时路径**、**本地路径**、**网络图片**<br>
|
||||
|
||||
* **当为网络地址时**
|
||||
* H5:👉 需要解决跨域问题。 <br>
|
||||
* 小程序:👉 需要配置 downloadFile 域名 <br>
|
||||
|
||||
|
||||
```html
|
||||
<image :src="url" v-if="url" mode="widthFix"></image>
|
||||
<l-clipper v-if="show" :image-url="imageUrl" @success="url = $event.url; show = false" @cancel="show = false" ></l-clipper>
|
||||
<button @tap="show = true">裁剪</button>
|
||||
```
|
||||
|
||||
```js
|
||||
export default {
|
||||
components: {lClipper},
|
||||
data() {
|
||||
return {
|
||||
imageUrl: 'https://img12.360buyimg.com/pop/s1180x940_jfs/t1/97205/26/1142/87801/5dbac55aEf795d962/48a4d7a63ff80b8b.jpg',
|
||||
show: false,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 确定按钮颜色
|
||||
样式变量名:`--l-clipper-confirm-color`
|
||||
可放到全局样式的 `page` 里或节点的 `style`
|
||||
```html
|
||||
<l-clipper class="clipper" style="--l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24)" ></l-clipper>
|
||||
```
|
||||
```css
|
||||
// css 中为组件设置 CSS 变量
|
||||
.clipper {
|
||||
--l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24)
|
||||
}
|
||||
// 全局
|
||||
page {
|
||||
--l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 使用插槽
|
||||
共五个插槽 `cancel` 取消按钮、 `photo` 选择图片按钮、 `rotate` 旋转按钮、 `confirm` 确定按钮和默认插槽。
|
||||
|
||||
```html
|
||||
<image :src="url" v-if="url" mode="widthFix"></image>
|
||||
<l-clipper
|
||||
v-if="show"
|
||||
:isLockWidth="isLockWidth"
|
||||
:isLockHeight="isLockHeight"
|
||||
:isLockRatio="isLockRatio"
|
||||
:isLimitMove="isLimitMove"
|
||||
:isDisableScale="isDisableScale"
|
||||
:isDisableRotate="isDisableRotate"
|
||||
:isShowCancelBtn="isShowCancelBtn"
|
||||
:isShowPhotoBtn="isShowPhotoBtn"
|
||||
:isShowRotateBtn="isShowRotateBtn"
|
||||
:isShowConfirmBtn="isShowConfirmBtn"
|
||||
@success="url = $event.url; show = false"
|
||||
@cancel="show = false" >
|
||||
<!-- 四个基本按钮插槽 -->
|
||||
<view slot="cancel">取消</view>
|
||||
<view slot="photo">选择图片</view>
|
||||
<view slot="rotate">旋转</view>
|
||||
<view slot="confirm">确定</view>
|
||||
<!-- 默认插槽 -->
|
||||
<view class="tools">
|
||||
<view>显示取消按钮
|
||||
<switch :checked="isShowCancelBtn" @change="isShowCancelBtn = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>显示选择图片按钮
|
||||
<switch :checked="isShowPhotoBtn" @change="isShowPhotoBtn = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>显示旋转按钮
|
||||
<switch :checked="isShowRotateBtn" @change="isShowRotateBtn = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>显示确定按钮
|
||||
<switch :checked="isShowConfirmBtn" @change="isShowConfirmBtn = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>锁定裁剪框宽度
|
||||
<switch :checked="isLockWidth" @change="isLockWidth = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>锁定裁剪框高度
|
||||
<switch :checked="isLockHeight" @change="isLockHeight = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>锁定裁剪框比例
|
||||
<switch :checked="isLockRatio" @change="isLockRatio = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>限制移动范围
|
||||
<switch :checked="isLimitMove" @change="isLimitMove = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>禁止缩放
|
||||
<switch :checked="isDisableScale" @change="isDisableScale = $event.target.value" ></switch>
|
||||
</view>
|
||||
<view>禁止旋转
|
||||
<switch :checked="isDisableRotate" @change="isDisableRotate = $event.target.value" ></switch>
|
||||
</view>
|
||||
</view>
|
||||
</l-clipper>
|
||||
<button @tap="show = true">裁剪</button>
|
||||
```
|
||||
|
||||
```js
|
||||
export default {
|
||||
components: {lClipper},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
url: '',
|
||||
isLockWidth: false,
|
||||
isLockHeight: false,
|
||||
isLockRatio: true,
|
||||
isLimitMove: false,
|
||||
isDisableScale: false,
|
||||
isDisableRotate: false,
|
||||
isShowCancelBtn: true,
|
||||
isShowPhotoBtn: true,
|
||||
isShowRotateBtn: true,
|
||||
isShowConfirmBtn: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------------- | ------------ | ---------------- | ------------ |
|
||||
| image-url | 图片路径 | <em>string</em> | |
|
||||
| quality | 图片的质量,取值范围为 [0, 1],不在范围内时当作1处理 | <em>number</em> | `1` |
|
||||
| source | `{album: '从相册中选择'}`key为图片来源类型,value为选项说明 | <em>Object</em> | |
|
||||
| width | 裁剪框宽度,单位为 `rpx` | <em>number</em> | `400` |
|
||||
| height | 裁剪框高度 | <em>number</em> | `400` |
|
||||
| min-width | 裁剪框最小宽度 | <em>number</em> | `200` |
|
||||
| min-height |裁剪框最小高度 | <em>number</em> | `200` |
|
||||
| max-width | 裁剪框最大宽度 | <em>number</em> | `600` |
|
||||
| max-height | 裁剪框最大宽度 | <em>number</em> | `600` |
|
||||
| min-ratio | 图片最小缩放比 | <em>number</em> | `0.5` |
|
||||
| max-ratio | 图片最大缩放比 | <em>number</em> | `2` |
|
||||
| rotate-angle | 旋转按钮每次旋转的角度 | <em>number</em> | `90` |
|
||||
| scale-ratio | 生成图片相对于裁剪框的比例, **比例越高生成图片越清晰** | <em>number</em> | `1` |
|
||||
| is-lock-width | 是否锁定裁剪框宽度 | <em>boolean</em> | `false` |
|
||||
| is-lock-height | 是否锁定裁剪框高度上 | <em>boolean</em> | `false` |
|
||||
| is-lock-ratio | 是否锁定裁剪框比例 | <em>boolean</em> | `true` |
|
||||
| is-disable-scale | 是否禁止缩放 | <em>boolean</em> | `false` |
|
||||
| is-disable-rotate | 是否禁止旋转 | <em>boolean</em> | `false` |
|
||||
| is-limit-move | 是否限制移动范围 | <em>boolean</em> | `false` |
|
||||
| is-show-photo-btn | 是否显示选择图片按钮 | <em>boolean</em> | `true` |
|
||||
| is-show-rotate-btn | 是否显示转按钮 | <em>boolean</em> | `true` |
|
||||
| is-show-confirm-btn | 是否显示确定按钮 | <em>boolean</em> | `true` |
|
||||
| is-show-cancel-btn | 是否显示关闭按钮 | <em>boolean</em> | `true` |
|
||||
|
||||
|
||||
|
||||
### 事件 Events
|
||||
|
||||
| 事件名 | 说明 | 回调 |
|
||||
| ------- | ------------ | -------------- |
|
||||
| success | 生成图片成功 | {`width`, `height`, `url`} |
|
||||
| fail | 生成图片失败 | `error` |
|
||||
| cancel | 关闭 | `false` |
|
||||
| ready | 图片加载完成 | {`width`, `height`, `path`, `orientation`, `type`} |
|
||||
| change | 图片大小改变时触发 | {`width`, `height`} |
|
||||
| rotate | 图片旋转时触发 | `angle` |
|
||||
|
||||
## 常见问题
|
||||
> 1、H5端使用网络图片需要解决跨域问题。<br>
|
||||
> 2、小程序使用网络图片需要去公众平台增加下载白名单!二级域名也需要配!<br>
|
||||
> 3、H5端生成图片是base64,有时显示只有一半可以使用原生标签`<IMG/>`<br>
|
||||
> 4、IOS APP 请勿使用HBX2.9.3.20201014的版本!这个版本无法生成图片。<br>
|
||||
> 5、APP端无成功反馈、也无失败反馈时,请更新基座和HBX。<br>
|
||||
|
||||
|
||||
## 打赏
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。<br>
|
||||

|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#606060;}
|
||||
.st1{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st2" d="M11.6,11c0.4,0.4,0.6,0.9,0.6,1.5c0,0.6-0.2,1.1-0.6,1.4c-0.4,0.4-0.9,0.6-1.5,0.6c-0.6,0-1.1-0.2-1.5-0.6
|
||||
c-0.4-0.4-0.6-0.9-0.6-1.4s0.2-1.1,0.6-1.5c0.4-0.4,0.9-0.6,1.5-0.6C10.8,10.4,11.2,10.6,11.6,11z M24.6,18.4V6.7H5.4v12l1.8-1.8
|
||||
c0.3-0.3,0.6-0.4,1-0.4c0.4,0,0.7,0.1,1,0.4l1.8,1.8l5.8-7c0.3-0.3,0.6-0.5,1.1-0.5c0.4,0,0.8,0.2,1.1,0.5
|
||||
C18.8,11.6,24.6,18.4,24.6,18.4z M25.6,5.7C25.9,6,26,6.3,26,6.7v16.1c0,0.4-0.1,0.7-0.4,1c-0.3,0.3-0.6,0.4-1,0.4H5.4
|
||||
c-0.4,0-0.7-0.1-1-0.4c-0.3-0.3-0.4-0.6-0.4-1V6.7c0-0.4,0.1-0.7,0.4-1c0.3-0.3,0.6-0.4,1-0.4h19.3C25,5.3,25.3,5.4,25.6,5.7z"/>
|
||||
<path class="st1" d="M24.3,21.5H5.7c-0.2,0-0.3-0.2-0.3-0.3V7c0-0.2,0.2-0.3,0.3-0.3h18.6c0.2,0,0.3,0.2,0.3,0.3v14.2
|
||||
C24.6,21.3,24.5,21.5,24.3,21.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="30px" height="30px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M17.1,24.2h-12c-0.2,0-0.3-0.2-0.3-0.3v-9.3c0-0.2,0.2-0.3,0.3-0.3h12c0.2,0,0.3,0.2,0.3,0.3v9.3
|
||||
C17.5,24.1,17.3,24.2,17.1,24.2z"/>
|
||||
<path class="st0" d="M16.6,5.4c4.8,0,8.7,3.9,8.7,8.7"/>
|
||||
<polyline class="st0" points="19.3,10.1 14.9,5.6 19.3,1.2 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 791 B |
@@ -0,0 +1,160 @@
|
||||
.flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
.bg-transparent {
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
transition-duration: 0.35s;
|
||||
}
|
||||
.l-clipper {
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--window-top));
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
position: fixed;
|
||||
top: var(--window-top);
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.l-clipper-mask {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
.l-clipper__content {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border: 1rpx solid rgba(255,255,255,0.3);
|
||||
box-sizing: border-box;
|
||||
box-shadow: rgba(0,0,0,0.5) 0 0 0 80vh;
|
||||
background: transparent;
|
||||
}
|
||||
.l-clipper__content::before,
|
||||
.l-clipper__content::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 1rpx dashed rgba(255,255,255,0.3);
|
||||
}
|
||||
.l-clipper__content::before {
|
||||
width: 100%;
|
||||
top: 33.33%;
|
||||
height: 33.33%;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
.l-clipper__content::after {
|
||||
width: 33.33%;
|
||||
left: 33.33%;
|
||||
height: 100%;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
.l-clipper__edge {
|
||||
position: absolute;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border: 6rpx solid #fff;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.l-clipper__edge::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: transparent;
|
||||
}
|
||||
.l-clipper__edge:nth-child(1) {
|
||||
left: -6rpx;
|
||||
top: -6rpx;
|
||||
border-bottom-width: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
.l-clipper__edge:nth-child(1):before {
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
}
|
||||
.l-clipper__edge:nth-child(2) {
|
||||
right: -6rpx;
|
||||
top: -6rpx;
|
||||
border-bottom-width: 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
}
|
||||
.l-clipper__edge:nth-child(2):before {
|
||||
top: -50%;
|
||||
left: 50%;
|
||||
}
|
||||
.l-clipper__edge:nth-child(3) {
|
||||
left: -6rpx;
|
||||
bottom: -6rpx;
|
||||
border-top-width: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
.l-clipper__edge:nth-child(3):before {
|
||||
bottom: -50%;
|
||||
left: -50%;
|
||||
}
|
||||
.l-clipper__edge:nth-child(4) {
|
||||
right: -6rpx;
|
||||
bottom: -6rpx;
|
||||
border-top-width: 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
}
|
||||
.l-clipper__edge:nth-child(4):before {
|
||||
bottom: -50%;
|
||||
left: 50%;
|
||||
}
|
||||
.l-clipper-image {
|
||||
width: 100%;
|
||||
border-style: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: center;
|
||||
}
|
||||
.l-clipper-canvas {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
left: -200vw;
|
||||
top: -200vw;
|
||||
pointer-events: none;
|
||||
}
|
||||
.l-clipper-tools {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 10px;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
color: #fff;
|
||||
}
|
||||
.l-clipper-tools__btns {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 20rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.l-clipper-tools__btns .cancel {
|
||||
width: 112rpx;
|
||||
height: 60rpx;
|
||||
text-align: center;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
.l-clipper-tools__btns .confirm {
|
||||
width: 112rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
background-color: #07c160;
|
||||
border-radius: 6rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.l-clipper-tools__btns image {
|
||||
display: block;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
.l-clipper-tools__btns {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -0,0 +1,820 @@
|
||||
<template>
|
||||
<view class="l-clipper" :class="{open: value}" disable-scroll :style="'z-index: ' + zIndex + ';' + customStyle">
|
||||
<view class="l-clipper-mask" @touchstart.stop.prevent="clipTouchStart" @touchmove.stop.prevent="clipTouchMove" @touchend.stop.prevent="clipTouchEnd">
|
||||
<view class="l-clipper__content" :style="clipStyle"><view class="l-clipper__edge" v-for="(item, index) in [0, 0, 0, 0]" :key="index"></view></view>
|
||||
</view>
|
||||
<image
|
||||
class="l-clipper-image"
|
||||
@error="imageLoad"
|
||||
@load="imageLoad"
|
||||
@touchstart.stop.prevent="imageTouchStart"
|
||||
@touchmove.stop.prevent="imageTouchMove"
|
||||
@touchend.stop.prevent="imageTouchEnd"
|
||||
:src="image"
|
||||
:mode="imageWidth == 'auto' ? 'widthFix' : ''"
|
||||
v-if="image"
|
||||
:style="imageStyle"
|
||||
/>
|
||||
<canvas
|
||||
:canvas-id="canvasId"
|
||||
id="l-clipper"
|
||||
disable-scroll
|
||||
:style="'width: ' + canvasWidth * scaleRatio + 'px; height:' + canvasHeight * scaleRatio + 'px;'"
|
||||
class="l-clipper-canvas"
|
||||
></canvas>
|
||||
<view class="l-clipper-tools">
|
||||
<view class="l-clipper-tools__btns">
|
||||
<view v-if="isShowCancelBtn" @tap="cancel">
|
||||
<slot name="cancel" v-if="$slots.cancel" />
|
||||
<view v-else class="cancel">取消</view>
|
||||
</view>
|
||||
<view v-if="isShowPhotoBtn" @tap="uploadImage">
|
||||
<slot name="photo" v-if="$slots.photo" />
|
||||
<image v-else src="./images/photo.svg" />
|
||||
</view>
|
||||
<view v-if="isShowRotateBtn" @tap="rotate">
|
||||
<slot name="rotate" v-if="$slots.rotate" />
|
||||
<image v-else src="./images/rotate.svg" data-type="inverse" />
|
||||
</view>
|
||||
<view v-if="isShowConfirmBtn" @tap="confirm">
|
||||
<slot name="confirm" v-if="$slots.confirm" />
|
||||
<view v-else class="confirm">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { determineDirection, calcImageOffset, calcImageScale, calcImageSize, calcPythagoreanTheorem, clipTouchMoveOfCalculate, imageTouchMoveOfCalcOffset } from './utils';
|
||||
const cache = {}
|
||||
export default {
|
||||
// version: '0.6.3',
|
||||
name: 'l-clipper',
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// #ifdef MP-WEIXIN
|
||||
type: {
|
||||
type: String,
|
||||
default: '2d'
|
||||
},
|
||||
// #endif
|
||||
customStyle: {
|
||||
type: String,
|
||||
},
|
||||
canvasId: {
|
||||
type: String,
|
||||
default: 'l-clipper'
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 99
|
||||
},
|
||||
imageUrl: {
|
||||
type: String
|
||||
},
|
||||
fileType: {
|
||||
type: String,
|
||||
default: 'png'
|
||||
},
|
||||
quality: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: 600
|
||||
},
|
||||
minHeight: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
default: 600
|
||||
},
|
||||
isLockWidth: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isLockHeight: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isLockRatio: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
scaleRatio: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
minRatio: {
|
||||
type: Number,
|
||||
default: 0.5
|
||||
},
|
||||
maxRatio: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
isDisableScale: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDisableRotate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isLimitMove: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isShowPhotoBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isShowRotateBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isShowConfirmBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isShowCancelBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
rotateAngle: {
|
||||
type: Number,
|
||||
default: 90
|
||||
},
|
||||
source: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
album: '从相册中选择',
|
||||
camera: '拍照',
|
||||
// #ifdef MP-WEIXIN
|
||||
message: '从微信中选择'
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
clipX: 0,
|
||||
clipY: 0,
|
||||
clipWidth: 0,
|
||||
clipHeight: 0,
|
||||
animation: false,
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
imageTop: 0,
|
||||
imageLeft: 0,
|
||||
scale: 1,
|
||||
angle: 0,
|
||||
image: this.imageUrl,
|
||||
sysinfo: {},
|
||||
throttleTimer: null,
|
||||
throttleFlag: true,
|
||||
timeClipCenter: null,
|
||||
flagClipTouch: false,
|
||||
flagEndTouch: false,
|
||||
clipStart: {},
|
||||
animationTimer: null,
|
||||
touchRelative: [{x: 0,y: 0}],
|
||||
hypotenuseLength: 0,
|
||||
ctx: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
clipStyle() {
|
||||
const {clipWidth, clipHeight, clipY, clipX, animation} = this
|
||||
return `
|
||||
width: ${clipWidth}px;
|
||||
height:${clipHeight}px;
|
||||
transition-property: ${animation ? '' : 'background'};
|
||||
left: ${clipX}px;
|
||||
top: ${clipY}px
|
||||
`
|
||||
},
|
||||
imageStyle() {
|
||||
const {imageWidth, imageHeight, imageLeft, imageTop, animation, scale, angle} = this
|
||||
return `
|
||||
width: ${imageWidth ? imageWidth + 'px' : 'auto'};
|
||||
height: ${imageHeight ? imageHeight + 'px' : 'auto'};
|
||||
transform: translate3d(${imageLeft - imageWidth / 2}px, ${imageTop - imageHeight / 2}px, 0) scale(${scale}) rotate(${angle}deg);
|
||||
transition-duration: ${animation ? 0.35 : 0}s
|
||||
`
|
||||
},
|
||||
clipSize() {
|
||||
const { clipWidth, clipHeight } = this;
|
||||
return { clipWidth, clipHeight };
|
||||
},
|
||||
clipPoint() {
|
||||
const { clipY, clipX } = this;
|
||||
return { clipY, clipX };
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
if(!val) {
|
||||
this.animation = 0
|
||||
this.angle = 0
|
||||
} else {
|
||||
if(this.imageUrl) {
|
||||
const {imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight, path} = cache?.[this.imageUrl] || {}
|
||||
if(path != this.image) {
|
||||
this.image = this.imageUrl;
|
||||
} else {
|
||||
this.setDiffData({imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
imageUrl(url) {
|
||||
this.image = url
|
||||
},
|
||||
image:{
|
||||
handler: async function(url) {
|
||||
this.getImageInfo(url)
|
||||
},
|
||||
// immediate: true,
|
||||
},
|
||||
clipSize({ widthVal, heightVal }) {
|
||||
let { minWidth, minHeight } = this;
|
||||
minWidth = minWidth / 2;
|
||||
minHeight = minHeight / 2;
|
||||
if (widthVal < minWidth) {
|
||||
this.setDiffData({clipWidth: minWidth})
|
||||
}
|
||||
if (heightVal < minHeight) {
|
||||
this.setDiffData({clipHeight: minHeight})
|
||||
}
|
||||
this.calcClipSize();
|
||||
},
|
||||
angle(val) {
|
||||
this.animation = true;
|
||||
this.moveStop();
|
||||
const { isLimitMove } = this;
|
||||
if (isLimitMove && val % 90) {
|
||||
this.setDiffData({
|
||||
angle: Math.round(val / 90) * 90
|
||||
})
|
||||
}
|
||||
this.imgMarginDetectionScale();
|
||||
},
|
||||
animation(val) {
|
||||
clearTimeout(this.animationTimer);
|
||||
if (val) {
|
||||
let animationTimer = setTimeout(() => {
|
||||
this.setDiffData({
|
||||
animation: false
|
||||
})
|
||||
}, 260);
|
||||
this.setDiffData({animationTimer})
|
||||
this.animationTimer = animationTimer;
|
||||
}
|
||||
},
|
||||
isLimitMove(val) {
|
||||
if (val) {
|
||||
if (this.angle % 90) {
|
||||
this.setDiffData({
|
||||
angle : Math.round(this.angle / 90) * 90
|
||||
})
|
||||
}
|
||||
this.imgMarginDetectionScale();
|
||||
}
|
||||
},
|
||||
clipPoint() {
|
||||
this.cutDetectionPosition();
|
||||
},
|
||||
width(width, oWidth) {
|
||||
if (width !== oWidth) {
|
||||
this.setDiffData({
|
||||
clipWidth: width / 2
|
||||
})
|
||||
}
|
||||
},
|
||||
height(height, oHeight) {
|
||||
if (height !== oHeight) {
|
||||
this.setDiffData({
|
||||
clipHeight: height / 2
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const sysinfo = uni.getSystemInfoSync();
|
||||
this.sysinfo = sysinfo;
|
||||
this.setClipInfo();
|
||||
if(this.image) {
|
||||
this.getImageInfo(this.image)
|
||||
}
|
||||
this.setClipCenter();
|
||||
this.calcClipSize();
|
||||
this.cutDetectionPosition();
|
||||
},
|
||||
methods: {
|
||||
setDiffData(data) {
|
||||
Object.keys(data).forEach(key => {
|
||||
if (this[key] !== data[key]) {
|
||||
this[key] = data[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
getImageInfo(url) {
|
||||
if (!url) return;
|
||||
if(this.value) {
|
||||
uni.showLoading({
|
||||
title: '请稍候...',
|
||||
mask: true
|
||||
});
|
||||
}
|
||||
uni.getImageInfo({
|
||||
src: url,
|
||||
success: res => {
|
||||
this.imgComputeSize(res.width, res.height);
|
||||
this.image = res.path;
|
||||
if (this.isLimitMove) {
|
||||
this.imgMarginDetectionScale();
|
||||
this.$emit('ready', res);
|
||||
}
|
||||
const {imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight} = this
|
||||
cache[url] = Object.assign(res, {imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight});
|
||||
},
|
||||
fail: (err) => {
|
||||
this.imgComputeSize();
|
||||
if (this.isLimitMove) {
|
||||
this.imgMarginDetectionScale();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
setClipInfo() {
|
||||
const { width, height, sysinfo, canvasId } = this;
|
||||
const clipWidth = width / 2;
|
||||
const clipHeight = height / 2;
|
||||
const clipY = (sysinfo.windowHeight - clipHeight) / 2;
|
||||
const clipX = (sysinfo.windowWidth - clipWidth) / 2;
|
||||
const imageLeft = sysinfo.windowWidth / 2;
|
||||
const imageTop = sysinfo.windowHeight / 2;
|
||||
this.ctx = uni.createCanvasContext(canvasId, this);
|
||||
this.clipWidth = clipWidth;
|
||||
this.clipHeight = clipHeight;
|
||||
this.clipX = clipX;
|
||||
this.clipY = clipY;
|
||||
this.canvasHeight = clipHeight;
|
||||
this.canvasWidth = clipWidth;
|
||||
this.imageLeft = imageLeft;
|
||||
this.imageTop = imageTop;
|
||||
},
|
||||
setClipCenter() {
|
||||
const { sysInfo, clipHeight, clipWidth, imageTop, imageLeft } = this;
|
||||
let sys = sysInfo || uni.getSystemInfoSync();
|
||||
let clipY = (sys.windowHeight - clipHeight) * 0.5;
|
||||
let clipX = (sys.windowWidth - clipWidth) * 0.5;
|
||||
this.imageTop = imageTop - this.clipY + clipY;
|
||||
this.imageLeft = imageLeft - this.clipX + clipX;
|
||||
this.clipY = clipY;
|
||||
this.clipX = clipX;
|
||||
},
|
||||
calcClipSize() {
|
||||
const { clipHeight, clipWidth, sysinfo, clipX, clipY } = this;
|
||||
if (clipWidth > sysinfo.windowWidth) {
|
||||
this.setDiffData({
|
||||
clipWidth: sysinfo.windowWidth
|
||||
})
|
||||
} else if (clipWidth + clipX > sysinfo.windowWidth) {
|
||||
this.setDiffData({
|
||||
clipX: sysinfo.windowWidth - clipX
|
||||
})
|
||||
}
|
||||
if (clipHeight > sysinfo.windowHeight) {
|
||||
this.setDiffData({
|
||||
clipHeight: sysinfo.windowHeight
|
||||
})
|
||||
} else if (clipHeight + clipY > sysinfo.windowHeight) {
|
||||
this.clipY = sysinfo.windowHeight - clipY;
|
||||
this.setDiffData({
|
||||
clipY: sysinfo.windowHeight - clipY
|
||||
})
|
||||
}
|
||||
},
|
||||
cutDetectionPosition() {
|
||||
const { clipX, clipY, sysinfo, clipHeight, clipWidth } = this;
|
||||
let cutDetectionPositionTop = () => {
|
||||
if (clipY < 0) {
|
||||
this.setDiffData({clipY: 0})
|
||||
}
|
||||
if (clipY > sysinfo.windowHeight - clipHeight) {
|
||||
this.setDiffData({clipY: sysinfo.windowHeight - clipHeight})
|
||||
}
|
||||
},
|
||||
cutDetectionPositionLeft = () => {
|
||||
if (clipX < 0) {
|
||||
this.setDiffData({clipX: 0})
|
||||
}
|
||||
if (clipX > sysinfo.windowWidth - clipWidth) {
|
||||
this.setDiffData({clipX: sysinfo.windowWidth - clipWidth})
|
||||
}
|
||||
};
|
||||
if (clipY === null && clipX === null) {
|
||||
let newClipY = (sysinfo.windowHeight - clipHeight) * 0.5;
|
||||
let newClipX = (sysinfo.windowWidth - clipWidth) * 0.5;
|
||||
this.setDiffData({
|
||||
clipX: newClipX,
|
||||
clipY: newClipY
|
||||
})
|
||||
} else if (clipY !== null && clipX !== null) {
|
||||
cutDetectionPositionTop();
|
||||
cutDetectionPositionLeft();
|
||||
} else if (clipY !== null && clipX === null) {
|
||||
cutDetectionPositionTop();
|
||||
this.setDiffData({
|
||||
clipX: (sysinfo.windowWidth - clipWidth) / 2
|
||||
})
|
||||
} else if (clipY === null && clipX !== null) {
|
||||
cutDetectionPositionLeft();
|
||||
this.setDiffData({
|
||||
clipY: (sysinfo.windowHeight - clipHeight) / 2
|
||||
})
|
||||
}
|
||||
},
|
||||
imgComputeSize(width, height) {
|
||||
const { imageWidth, imageHeight } = calcImageSize(width, height, this);
|
||||
this.imageWidth = imageWidth;
|
||||
this.imageHeight = imageHeight;
|
||||
},
|
||||
imgMarginDetectionScale(scale) {
|
||||
if (!this.isLimitMove) return;
|
||||
const currentScale = calcImageScale(this, scale);
|
||||
this.imgMarginDetectionPosition(currentScale);
|
||||
},
|
||||
imgMarginDetectionPosition(scale) {
|
||||
if (!this.isLimitMove) return;
|
||||
const { scale: currentScale, left, top } = calcImageOffset(this, scale);
|
||||
this.setDiffData({
|
||||
imageLeft: left,
|
||||
imageTop: top,
|
||||
scale: currentScale
|
||||
})
|
||||
},
|
||||
throttle() {
|
||||
this.setDiffData({
|
||||
throttleFlag: true
|
||||
})
|
||||
},
|
||||
moveDuring() {
|
||||
clearTimeout(this.timeClipCenter);
|
||||
},
|
||||
moveStop() {
|
||||
clearTimeout(this.timeClipCenter);
|
||||
const timeClipCenter = setTimeout(() => {
|
||||
if (!this.animation) {
|
||||
this.setDiffData({animation: true})
|
||||
}
|
||||
this.setClipCenter();
|
||||
}, 800);
|
||||
this.setDiffData({timeClipCenter})
|
||||
},
|
||||
clipTouchStart(event) {
|
||||
// #ifdef H5
|
||||
event.preventDefault()
|
||||
// #endif
|
||||
if (!this.image) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
const currentX = event.touches[0].clientX;
|
||||
const currentY = event.touches[0].clientY;
|
||||
const { clipX, clipY, clipWidth, clipHeight } = this;
|
||||
const corner = determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY);
|
||||
this.moveDuring();
|
||||
if(!corner) {return}
|
||||
this.clipStart = {
|
||||
width: clipWidth,
|
||||
height: clipHeight,
|
||||
x: currentX,
|
||||
y: currentY,
|
||||
clipY,
|
||||
clipX,
|
||||
corner
|
||||
};
|
||||
this.flagClipTouch = true;
|
||||
this.flagEndTouch = true;
|
||||
},
|
||||
clipTouchMove(event) {
|
||||
// #ifdef H5
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
// #endif
|
||||
if (!this.image) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 只针对单指点击做处理
|
||||
if (event.touches.length !== 1) {
|
||||
return;
|
||||
|
||||
}
|
||||
const { flagClipTouch, throttleFlag } = this;
|
||||
if (flagClipTouch && throttleFlag) {
|
||||
const { isLockRatio, isLockHeight, isLockWidth } = this;
|
||||
if (isLockRatio && (isLockWidth || isLockHeight)) return;
|
||||
this.setDiffData({
|
||||
throttleFlag: false
|
||||
})
|
||||
this.throttle();
|
||||
const clipData = clipTouchMoveOfCalculate(this, event);
|
||||
if(clipData) {
|
||||
const { width, height, clipX, clipY } = clipData;
|
||||
if (!isLockWidth && !isLockHeight) {
|
||||
this.setDiffData({
|
||||
clipWidth: width,
|
||||
clipHeight: height,
|
||||
clipX,
|
||||
clipY
|
||||
})
|
||||
} else if (!isLockWidth) {
|
||||
this.setDiffData({
|
||||
clipWidth: width,
|
||||
clipX
|
||||
})
|
||||
} else if (!isLockHeight) {
|
||||
this.setDiffData({
|
||||
clipHeight: height,
|
||||
clipY
|
||||
})
|
||||
}
|
||||
this.imgMarginDetectionScale();
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
clipTouchEnd() {
|
||||
this.moveStop();
|
||||
this.flagClipTouch = false;
|
||||
},
|
||||
imageTouchStart(e) {
|
||||
// #ifdef H5
|
||||
event.preventDefault()
|
||||
// #endif
|
||||
this.flagEndTouch = false;
|
||||
const { imageLeft, imageTop } = this;
|
||||
const clientXForLeft = e.touches[0].clientX;
|
||||
const clientYForLeft = e.touches[0].clientY;
|
||||
|
||||
let touchRelative = [];
|
||||
if (e.touches.length === 1) {
|
||||
touchRelative[0] = {
|
||||
x: clientXForLeft - imageLeft,
|
||||
y: clientYForLeft - imageTop
|
||||
};
|
||||
this.touchRelative = touchRelative;
|
||||
} else {
|
||||
const clientXForRight = e.touches[1].clientX;
|
||||
const clientYForRight = e.touches[1].clientY;
|
||||
let width = Math.abs(clientXForLeft - clientXForRight);
|
||||
let height = Math.abs(clientYForLeft - clientYForRight);
|
||||
const hypotenuseLength = calcPythagoreanTheorem(width, height);
|
||||
|
||||
touchRelative = [
|
||||
{
|
||||
x: clientXForLeft - imageLeft,
|
||||
y: clientYForLeft - imageTop
|
||||
},
|
||||
{
|
||||
x: clientXForRight - imageLeft,
|
||||
y: clientYForRight - imageTop
|
||||
}
|
||||
];
|
||||
this.touchRelative = touchRelative;
|
||||
this.hypotenuseLength = hypotenuseLength;
|
||||
}
|
||||
},
|
||||
imageTouchMove(e) {
|
||||
// #ifdef H5
|
||||
event.preventDefault()
|
||||
// #endif
|
||||
const { flagEndTouch, throttleFlag } = this;
|
||||
if (flagEndTouch || !throttleFlag) return;
|
||||
const clientXForLeft = e.touches[0].clientX;
|
||||
const clientYForLeft = e.touches[0].clientY;
|
||||
this.setDiffData({throttleFlag: false})
|
||||
this.throttle();
|
||||
this.moveDuring();
|
||||
if (e.touches.length === 1) {
|
||||
const { left: imageLeft, top: imageTop} = imageTouchMoveOfCalcOffset(this, clientXForLeft, clientYForLeft);
|
||||
this.setDiffData({
|
||||
imageLeft,
|
||||
imageTop
|
||||
})
|
||||
this.imgMarginDetectionPosition();
|
||||
} else {
|
||||
const clientXForRight = e.touches[1].clientX;
|
||||
const clientYForRight = e.touches[1].clientY;
|
||||
let width = Math.abs(clientXForLeft - clientXForRight),
|
||||
height = Math.abs(clientYForLeft - clientYForRight),
|
||||
hypotenuse = calcPythagoreanTheorem(width, height),
|
||||
scale = this.scale * (hypotenuse / this.hypotenuseLength);
|
||||
if (this.isDisableScale) {
|
||||
|
||||
scale = 1;
|
||||
} else {
|
||||
scale = scale <= this.minRatio ? this.minRatio : scale;
|
||||
scale = scale >= this.maxRatio ? this.maxRatio : scale;
|
||||
this.$emit('change', {
|
||||
width: this.imageWidth * scale,
|
||||
height: this.imageHeight * scale
|
||||
});
|
||||
}
|
||||
|
||||
this.imgMarginDetectionScale(scale);
|
||||
this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
|
||||
this.scale = scale;
|
||||
}
|
||||
},
|
||||
imageTouchEnd() {
|
||||
this.setDiffData({
|
||||
flagEndTouch: true
|
||||
})
|
||||
this.moveStop();
|
||||
},
|
||||
uploadImage() {
|
||||
const itemList = Object.entries(this.source)
|
||||
const sizeType = ['original', 'compressed']
|
||||
const success = ({tempFilePaths:a, tempFiles: b}) => {
|
||||
this.image = a ? a[0] : b[0].path
|
||||
};
|
||||
const _uploadImage = (type) => {
|
||||
if(type !== 'message') {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType,
|
||||
sourceType: [type],
|
||||
success
|
||||
});
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
if(type == 'message') {
|
||||
wx.chooseMessageFile({
|
||||
count: 1,
|
||||
type: 'image',
|
||||
success
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
if(itemList.length > 1) {
|
||||
uni.showActionSheet({
|
||||
itemList: itemList.map(v => v[1]),
|
||||
success: ({tapIndex: i}) => {
|
||||
_uploadImage(itemList[i][0])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
_uploadImage(itemList[0][0])
|
||||
}
|
||||
},
|
||||
imageReset() {
|
||||
const sys = this.sysinfo || uni.getSystemInfoSync();
|
||||
this.scale = 1;
|
||||
this.angle = 0;
|
||||
this.imageTop = sys.windowHeight / 2;
|
||||
this.imageLeft = sys.windowWidth / 2;
|
||||
},
|
||||
imageLoad(e) {
|
||||
this.imageReset();
|
||||
uni.hideLoading();
|
||||
this.$emit('ready', e.detail);
|
||||
},
|
||||
rotate(event) {
|
||||
if (this.isDisableRotate) return;
|
||||
if (!this.image) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { rotateAngle } = this;
|
||||
const originAngle = this.angle
|
||||
const type = event.currentTarget.dataset.type;
|
||||
if (type === 'along') {
|
||||
this.angle = originAngle + rotateAngle
|
||||
} else {
|
||||
this.angle = originAngle - rotateAngle
|
||||
}
|
||||
this.$emit('rotate', this.angle);
|
||||
},
|
||||
confirm() {
|
||||
if (!this.image) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showLoading({
|
||||
title: '加载中'
|
||||
});
|
||||
const { canvasHeight, canvasWidth, clipHeight, clipWidth, ctx, scale, imageLeft, imageTop, clipX, clipY, angle, scaleRatio: dpr, image, quality, fileType, type: imageType, canvasId } = this;
|
||||
const draw = () => {
|
||||
const imageWidth = this.imageWidth * scale * dpr;
|
||||
const imageHeight = this.imageHeight * scale * dpr;
|
||||
const xpos = imageLeft - clipX;
|
||||
const ypos = imageTop - clipY;
|
||||
ctx.translate(xpos * dpr, ypos * dpr);
|
||||
ctx.rotate((angle * Math.PI) / 180);
|
||||
ctx.drawImage(image, -imageWidth / 2, -imageHeight / 2, imageWidth, imageHeight);
|
||||
ctx.draw(false, () => {
|
||||
const width = clipWidth * dpr
|
||||
const height = clipHeight * dpr
|
||||
let params = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
destWidth: width,
|
||||
destHeight: height,
|
||||
canvasId: canvasId,
|
||||
fileType,
|
||||
quality,
|
||||
success: (res) => {
|
||||
data.url = res.tempFilePath;
|
||||
uni.hideLoading();
|
||||
this.$emit('success', data);
|
||||
this.$emit('input', false)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('error', error)
|
||||
this.$emit('fail', error);
|
||||
this.$emit('input', false)
|
||||
}
|
||||
};
|
||||
|
||||
let data = {
|
||||
url: '',
|
||||
width,
|
||||
height
|
||||
};
|
||||
uni.canvasToTempFilePath(params, this)
|
||||
});
|
||||
};
|
||||
|
||||
if (canvasWidth !== clipWidth || canvasHeight !== clipHeight) {
|
||||
this.canvasWidth = clipWidth;
|
||||
this.canvasHeight = clipHeight;
|
||||
ctx.draw();
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
draw();
|
||||
}, 100);
|
||||
})
|
||||
} else {
|
||||
draw();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('cancel', false)
|
||||
this.$emit('input', false)
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './index'
|
||||
</style>
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* 判断手指触摸位置
|
||||
*/
|
||||
export function determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY) {
|
||||
/*
|
||||
* (右下>>1 右上>>2 左上>>3 左下>>4)
|
||||
*/
|
||||
let corner;
|
||||
/**
|
||||
* 思路:(利用直角坐标系)
|
||||
* 1.找出裁剪框中心点
|
||||
* 2.如点击坐标在上方点与左方点区域内,则点击为左上角
|
||||
* 3.如点击坐标在下方点与右方点区域内,则点击为右下角
|
||||
* 4.其他角同理
|
||||
*/
|
||||
const mainPoint = [clipX + clipWidth / 2, clipY + clipHeight / 2]; // 中心点
|
||||
const currentPoint = [currentX, currentY]; // 触摸点
|
||||
|
||||
if (currentPoint[0] <= mainPoint[0] && currentPoint[1] <= mainPoint[1]) {
|
||||
corner = 3; // 左上
|
||||
} else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] <= mainPoint[1]) {
|
||||
corner = 2; // 右上
|
||||
} else if (currentPoint[0] <= mainPoint[0] && currentPoint[1] >= mainPoint[1]) {
|
||||
corner = 4; // 左下
|
||||
} else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] >= mainPoint[1]) {
|
||||
corner = 1; // 右下
|
||||
}
|
||||
|
||||
return corner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片边缘检测检测时,计算图片偏移量
|
||||
*/
|
||||
export function calcImageOffset(data, scale) {
|
||||
let left = data.imageLeft;
|
||||
let top = data.imageTop;
|
||||
scale = scale || data.scale;
|
||||
|
||||
let imageWidth = data.imageWidth;
|
||||
let imageHeight = data.imageHeight;
|
||||
if ((data.angle / 90) % 2) {
|
||||
imageWidth = data.imageHeight;
|
||||
imageHeight = data.imageWidth;
|
||||
}
|
||||
const {
|
||||
clipX,
|
||||
clipWidth,
|
||||
clipY,
|
||||
clipHeight
|
||||
} = data;
|
||||
|
||||
// 当前图片宽度/高度
|
||||
const currentImageSize = (size) => (size * scale) / 2;
|
||||
const currentImageWidth = currentImageSize(imageWidth);
|
||||
const currentImageHeight = currentImageSize(imageHeight);
|
||||
|
||||
left = clipX + currentImageWidth >= left ? left : clipX + currentImageWidth;
|
||||
left = clipX + clipWidth - currentImageWidth <= left ? left : clipX + clipWidth - currentImageWidth;
|
||||
top = clipY + currentImageHeight >= top ? top : clipY + currentImageHeight;
|
||||
top = clipY + clipHeight - currentImageHeight <= top ? top : clipY + clipHeight - currentImageHeight;
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
scale
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片边缘检测时,计算图片缩放比例
|
||||
*/
|
||||
export function calcImageScale(data, scale) {
|
||||
scale = scale || data.scale;
|
||||
let {
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
clipWidth,
|
||||
clipHeight,
|
||||
angle
|
||||
} = data
|
||||
if ((angle / 90) % 2) {
|
||||
imageWidth = imageHeight;
|
||||
imageHeight = imageWidth;
|
||||
}
|
||||
if (imageWidth * scale < clipWidth) {
|
||||
scale = clipWidth / imageWidth;
|
||||
}
|
||||
if (imageHeight * scale < clipHeight) {
|
||||
scale = Math.max(scale, clipHeight / imageHeight);
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算图片尺寸
|
||||
*/
|
||||
export function calcImageSize(width, height, data) {
|
||||
let imageWidth = width,
|
||||
imageHeight = height;
|
||||
let {
|
||||
clipWidth,
|
||||
clipHeight,
|
||||
sysinfo,
|
||||
width: originWidth,
|
||||
height: originHeight
|
||||
} = data
|
||||
if (imageWidth && imageHeight) {
|
||||
if (imageWidth / imageHeight > (clipWidth || originWidth) / (clipWidth || originHeight)) {
|
||||
imageHeight = clipHeight || originHeight;
|
||||
imageWidth = (width / height) * imageHeight;
|
||||
} else {
|
||||
imageWidth = clipWidth || originWidth;
|
||||
imageHeight = (height / width) * imageWidth;
|
||||
}
|
||||
} else {
|
||||
let sys = sysinfo || uni.getSystemInfoSync();
|
||||
imageWidth = sys.windowWidth;
|
||||
imageHeight = 0;
|
||||
}
|
||||
return {
|
||||
imageWidth,
|
||||
imageHeight
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 勾股定理求斜边
|
||||
*/
|
||||
export function calcPythagoreanTheorem(width, height) {
|
||||
return Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖动裁剪框时计算
|
||||
*/
|
||||
export function clipTouchMoveOfCalculate(data, event) {
|
||||
const clientX = event.touches[0].clientX;
|
||||
const clientY = event.touches[0].clientY;
|
||||
|
||||
let {
|
||||
clipWidth,
|
||||
clipHeight,
|
||||
clipY: oldClipY,
|
||||
clipX: oldClipX,
|
||||
clipStart,
|
||||
isLockRatio,
|
||||
maxWidth,
|
||||
minWidth,
|
||||
maxHeight,
|
||||
minHeight
|
||||
} = data;
|
||||
maxWidth = maxWidth / 2;
|
||||
minWidth = minWidth / 2;
|
||||
minHeight = minHeight / 2;
|
||||
maxHeight = maxHeight / 2;
|
||||
|
||||
let width = clipWidth,
|
||||
height = clipHeight,
|
||||
clipY = oldClipY,
|
||||
clipX = oldClipX,
|
||||
// 获取裁剪框实际宽度/高度
|
||||
// 如果大于最大值则使用最大值
|
||||
// 如果小于最小值则使用最小值
|
||||
sizecorrect = () => {
|
||||
width = width <= maxWidth ? (width >= minWidth ? width : minWidth) : maxWidth;
|
||||
height = height <= maxHeight ? (height >= minHeight ? height : minHeight) : maxHeight;
|
||||
},
|
||||
sizeinspect = () => {
|
||||
sizecorrect();
|
||||
if ((width > maxWidth || width < minWidth || height > maxHeight || height < minHeight) && isLockRatio) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
//if (clipStart.corner) {
|
||||
height = clipStart.height + (clipStart.corner > 1 && clipStart.corner < 4 ? 1 : -1) * (clipStart.y - clientY);
|
||||
//}
|
||||
switch (clipStart.corner) {
|
||||
case 1:
|
||||
width = clipStart.width - clipStart.x + clientX;
|
||||
if (isLockRatio) {
|
||||
height = width / (clipWidth / clipHeight);
|
||||
}
|
||||
if (!sizeinspect()) return;
|
||||
break;
|
||||
case 2:
|
||||
width = clipStart.width - clipStart.x + clientX;
|
||||
if (isLockRatio) {
|
||||
height = width / (clipWidth / clipHeight);
|
||||
}
|
||||
if (!sizeinspect()) {
|
||||
return;
|
||||
} else {
|
||||
clipY = clipStart.clipY - (height - clipStart.height);
|
||||
}
|
||||
|
||||
break;
|
||||
case 3:
|
||||
width = clipStart.width + clipStart.x - clientX;
|
||||
if (isLockRatio) {
|
||||
height = width / (clipWidth / clipHeight);
|
||||
}
|
||||
if (!sizeinspect()) {
|
||||
return;
|
||||
} else {
|
||||
clipY = clipStart.clipY - (height - clipStart.height);
|
||||
clipX = clipStart.clipX - (width - clipStart.width);
|
||||
}
|
||||
|
||||
break;
|
||||
case 4:
|
||||
width = clipStart.width + clipStart.x - clientX;
|
||||
if (isLockRatio) {
|
||||
height = width / (clipWidth / clipHeight);
|
||||
}
|
||||
if (!sizeinspect()) {
|
||||
return;
|
||||
} else {
|
||||
clipX = clipStart.clipX - (width - clipStart.width);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
clipX,
|
||||
clipY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 单指拖动图片计算偏移
|
||||
*/
|
||||
export function imageTouchMoveOfCalcOffset(data, clientXForLeft, clientYForLeft) {
|
||||
let left = clientXForLeft - data.touchRelative[0].x,
|
||||
top = clientYForLeft - data.touchRelative[0].y;
|
||||
return {
|
||||
left,
|
||||
top
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- 注销(销毁)账号 -->
|
||||
<template>
|
||||
<view class="uni-content">
|
||||
<text class="words" space="emsp">
|
||||
一、注销是不可逆操作,注销后:\n
|
||||
1.帐号将无法登录、无法找回。\n
|
||||
2.帐号所有信息都会清除(个人身份信息、粉丝数等;发布的作品、评论、点赞等;交易信息等),你
|
||||
的朋友将无法通过本应用帐号联系你,请自行备份相关
|
||||
信息和数据。\n
|
||||
|
||||
二、重要提示\n
|
||||
1.封禁帐号(永久封禁、社交封禁、直播权限封禁)不能申请注销。\n
|
||||
2.注销后,你的身份证、三方帐号(微信、QQ、微博、支付宝)、手机号等绑定关系将解除,解除后可以绑定到其他帐号。\n
|
||||
3.注销后,手机号可以注册新的帐号,新帐号不会存在之前帐号的任何信息(作品、粉丝、评论、个人信息等)。\n
|
||||
4.注销本应用帐号前,需尽快处理帐号下的资金问题。\n
|
||||
5.视具体帐号情况而定,注销最多需要7天。\n
|
||||
</text>
|
||||
<view class="button-group">
|
||||
<button @click="nextStep" class="next" type="default">下一步</button>
|
||||
<button @click="cancel" type="warn">取消</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
onLoad() {},
|
||||
methods: {
|
||||
cancel() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
nextStep() {
|
||||
uni.showModal({
|
||||
content: '已经仔细阅读注销提示,知晓可能带来的后果,并确认要注销',
|
||||
complete: (e) => {
|
||||
if (e.confirm) {
|
||||
const uniIdco = uniCloud.importObject("uni-id-co");
|
||||
uniIdco.closeAccount().then((e) => {
|
||||
uni.showToast({
|
||||
title: '注销成功',
|
||||
duration: 3000
|
||||
});
|
||||
uni.removeStorageSync('uni_id_token');
|
||||
uni.setStorageSync('uni_id_token_expired', 0)
|
||||
uni.navigateTo({
|
||||
url:"/uni_modules/uni-id-pages/pages/login/login-withoutpwd"
|
||||
})
|
||||
})
|
||||
} else {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.words {
|
||||
padding: 0 26rpx;
|
||||
line-height: 46rpx;
|
||||
margin-top: 20rpx;
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
border-radius: 100px;
|
||||
border: none;
|
||||
width: 300rpx;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.button-group button:after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button-group button.next {
|
||||
color: #e64340;
|
||||
border: solid 1px #e64340;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: fixed;
|
||||
height: 50px;
|
||||
bottom: 10px;
|
||||
width: 750rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: solid 1px #e4e6ec;
|
||||
padding-top: 10px;
|
||||
background-color: #FFFFFF;
|
||||
max-width: 690px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content{
|
||||
max-width: 690px;
|
||||
margin-left: calc(50% - 345px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675667510055" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4003" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M807.936 106.656h-76a24.32 24.32 0 0 0-17.92 7.936 27.744 27.744 0 0 0-7.424 19.104c0 6.944 2.464 13.792 7.424 19.104a24.32 24.32 0 0 0 17.92 7.904h76v81.088c0 6.944 2.432 13.76 7.424 19.104a24.32 24.32 0 0 0 35.808 0 27.744 27.744 0 0 0 7.424-19.104V160.704c0-29.824-22.72-54.048-50.656-54.048zM833.248 512a25.12 25.12 0 0 0-17.92 7.392 25.12 25.12 0 0 0-7.392 17.92v76h-76a25.12 25.12 0 0 0-17.92 7.424c-1.344 1.344-2.08 3.072-3.072 4.704-28.576-27.52-60.704-50.112-96.256-65.152 72.192-43.136 117.888-126.08 103.872-219.296-13.216-87.456-81.056-160.576-167.648-178.656a228.16 228.16 0 0 0-46.944-4.896 217.056 217.056 0 0 0-217.12 217.12c0 79.264 42.976 147.936 106.368 185.824-35.456 15.04-67.648 37.632-96.256 65.152-0.96-1.632-1.696-3.36-3.072-4.704a25.12 25.12 0 0 0-17.92-7.424H200v-76a25.12 25.12 0 0 0-7.424-17.92 25.12 25.12 0 0 0-17.92-7.424 25.12 25.12 0 0 0-17.92 7.424 25.12 25.12 0 0 0-7.392 17.92v76c0 27.936 22.72 50.656 50.656 50.656H262.4c-42.336 54.816-71.712 123.488-80.96 200.192-3.424 28.224 19.104 53.12 47.488 53.12h550.048c28.416 0 50.848-24.96 47.488-53.12-9.216-76.8-38.624-145.472-80.96-200.288h62.4c27.968 0 50.688-22.72 50.688-50.656V537.28a25.12 25.12 0 0 0-7.424-17.92 25.12 25.12 0 0 0-17.92-7.392zM174.72 268.8a24.32 24.32 0 0 0 17.888-7.904 27.744 27.744 0 0 0 7.424-19.104V160.704h76a24.32 24.32 0 0 0 17.92-7.904 27.744 27.744 0 0 0 7.392-19.104 27.744 27.744 0 0 0-7.424-19.104 24.32 24.32 0 0 0-17.92-7.936H200c-27.968 0-50.656 24.224-50.656 54.08v81.056c0 6.944 2.432 13.76 7.392 19.104a24.32 24.32 0 0 0 17.92 7.904z" fill="#72a7ff" p-id="4004"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<view>
|
||||
<template v-if="isCertify">
|
||||
<uni-list>
|
||||
<uni-list-item class="item" title="姓名" :rightText="userInfo.realNameAuth.realName"></uni-list-item>
|
||||
<uni-list-item class="item" title="身份证号码" :rightText="userInfo.realNameAuth.identity"></uni-list-item>
|
||||
</uni-list>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="uni-content">
|
||||
<template v-if="verifyFail">
|
||||
<view class="face-icon">
|
||||
<image src="./face-verify-icon.svg" class="face-icon-image" />
|
||||
</view>
|
||||
<view class="error-title">{{verifyFailTitle}}</view>
|
||||
<view class="error-description">{{verifyFailContent}}</view>
|
||||
<button type="primary" @click="retry" v-if="verifyFailCode !== 10013">重新开始验证</button>
|
||||
<button type="primary" @click="retry" v-else>返回</button>
|
||||
<view class="dev-tip" v-if="isDev">请在控制台查看详细错误(此提示仅在开发环境展示)</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="title">实名认证</text>
|
||||
<uni-forms>
|
||||
<uni-forms-item name="realName">
|
||||
<uni-easyinput placeholder="姓名" class="input-box" v-model="realName" :clearable="false">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="idCard">
|
||||
<uni-easyinput placeholder="身份证号码" class="input-box" v-model="idCard" :clearable="false">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
<uni-id-pages-agreements scope="realNameVerify" ref="agreements" style="margin-bottom: 20px;">
|
||||
</uni-id-pages-agreements>
|
||||
<button type="primary" :disabled="!certifyIdNext" @click="getCertifyId">确定</button>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import checkIdCard from '@/uni_modules/uni-id-pages/common/check-id-card.js';
|
||||
import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
|
||||
|
||||
import {
|
||||
store,
|
||||
mutations
|
||||
} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
|
||||
const uniIdCo = uniCloud.importObject('uni-id-co')
|
||||
const tempFrvInfoKey = 'uni-id-pages-temp-frv'
|
||||
export default {
|
||||
mixins: [mixin],
|
||||
data() {
|
||||
return {
|
||||
realName: '',
|
||||
idCard: '',
|
||||
certifyId: '',
|
||||
verifyFail: false,
|
||||
verifyFailCode: 0,
|
||||
verifyFailTitle: '',
|
||||
verifyFailContent: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userInfo() {
|
||||
return store.userInfo
|
||||
},
|
||||
certifyIdNext() {
|
||||
return Boolean(this.realName) && Boolean(this.idCard) && (this.needAgreements && this.agree)
|
||||
},
|
||||
isCertify() {
|
||||
return this.userInfo.realNameAuth && this.userInfo.realNameAuth.authStatus === 2
|
||||
},
|
||||
isDev() {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
const tempFrvInfo = uni.getStorageSync(tempFrvInfoKey);
|
||||
if (tempFrvInfo) {
|
||||
this.realName = tempFrvInfo.realName
|
||||
this.idCard = tempFrvInfo.idCard
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getCertifyId() {
|
||||
if (!this.certifyIdNext) return
|
||||
|
||||
// #ifndef APP
|
||||
return uni.showModal({
|
||||
content: "暂不支持实名认证",
|
||||
showCancel: false
|
||||
})
|
||||
// #endif
|
||||
|
||||
if (!checkIdCard(this.idCard)) {
|
||||
uni.showToast({
|
||||
title: "身份证不合法",
|
||||
icon: "none"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
typeof this.realName !== 'string' ||
|
||||
this.realName.length < 2 ||
|
||||
!/^[\u4e00-\u9fa5]{1,10}(·?[\u4e00-\u9fa5]{1,10}){0,5}$/.test(this.realName)
|
||||
) {
|
||||
uni.showToast({
|
||||
title: "姓名只能是汉字",
|
||||
icon: "none"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.setStorage({
|
||||
key: tempFrvInfoKey,
|
||||
data: {
|
||||
realName: this.realName,
|
||||
idCard: this.idCard
|
||||
}
|
||||
});
|
||||
|
||||
const metaInfo = uni.getFacialRecognitionMetaInfo()
|
||||
|
||||
const res = await uniIdCo.getFrvCertifyId({
|
||||
realName: this.realName,
|
||||
idCard: this.idCard,
|
||||
metaInfo
|
||||
})
|
||||
|
||||
this.certifyId = res.certifyId
|
||||
|
||||
this.startFacialRecognitionVerify()
|
||||
},
|
||||
startFacialRecognitionVerify() {
|
||||
|
||||
// #ifdef APP
|
||||
uni.startFacialRecognitionVerify({
|
||||
certifyId: this.certifyId,
|
||||
progressBarColor: "#2979ff",
|
||||
success: () => {
|
||||
this.verifyFail = false
|
||||
this.getFrvAuthResult()
|
||||
},
|
||||
fail: (e) => {
|
||||
let title = "验证失败"
|
||||
let content
|
||||
|
||||
console.log(
|
||||
`[frv-debug] certifyId auth error: certifyId -> ${this.certifyId}, error -> ${JSON.stringify(e, null, 4)}`
|
||||
)
|
||||
|
||||
switch (e.errCode) {
|
||||
case 10001:
|
||||
content = '认证ID为空'
|
||||
break
|
||||
case 10010:
|
||||
title = '刷脸异常'
|
||||
content = e.cause.message || '错误代码: 10010'
|
||||
break
|
||||
case 10011:
|
||||
title = '验证中断'
|
||||
content = e.cause.message || '错误代码: 10011'
|
||||
break
|
||||
case 10012:
|
||||
content = '网络异常'
|
||||
break
|
||||
case 10013:
|
||||
this.verifyFailCode = e.errCode
|
||||
this.verifyFailContent = e.cause.message || '错误代码: 10013'
|
||||
this.getFrvAuthResult()
|
||||
|
||||
console.log(
|
||||
`[frv-debug] 刷脸失败, certifyId -> ${this.certifyId}, 如在开发环境请检查用户的姓名、身份证号与刷脸用户是否为同一用户。如遇到认证ID已使用请检查opendb-frv-logs表中certifyId状态`
|
||||
)
|
||||
return
|
||||
case 10020:
|
||||
content = '设备设置时间异常'
|
||||
break
|
||||
default:
|
||||
title = ''
|
||||
content = `验证未知错误 (${e.errCode})`
|
||||
break
|
||||
}
|
||||
|
||||
this.verifyFail = true
|
||||
this.verifyFailCode = e.errCode
|
||||
this.verifyFailTitle = title
|
||||
this.verifyFailContent = content
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
async getFrvAuthResult() {
|
||||
const uniIdCo = uniCloud.importObject('uni-id-co', {
|
||||
customUI: true
|
||||
})
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: "验证中...",
|
||||
mask: false
|
||||
})
|
||||
const res = await uniIdCo.getFrvAuthResult({
|
||||
certifyId: this.certifyId
|
||||
})
|
||||
|
||||
const {
|
||||
errCode,
|
||||
...rest
|
||||
} = res
|
||||
|
||||
if (this.verifyFailContent) {
|
||||
console.log(`[frv-debug] 客户端刷脸失败,由实人认证服务查询具体原因,原因:${this.verifyFailContent}`)
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
content: "实名认证成功",
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
mutations.setUserInfo({
|
||||
realNameAuth: rest
|
||||
})
|
||||
this.verifyFail = false
|
||||
}
|
||||
})
|
||||
|
||||
uni.removeStorage({
|
||||
key: tempFrvInfoKey
|
||||
})
|
||||
} catch (e) {
|
||||
this.verifyFail = true
|
||||
this.verifyFailTitle = e.errMsg
|
||||
console.error(JSON.stringify(e));
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
retry() {
|
||||
if (this.verifyFailCode !== 10013) {
|
||||
this.getCertifyId()
|
||||
} else {
|
||||
this.verifyFail = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
|
||||
|
||||
.checkbox-box,
|
||||
.uni-label-pointer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.text {
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.checkbox-box ::v-deep .uni-checkbox-input {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.checkbox-box ::v-deep .uni-checkbox-input.uni-checkbox-input-checked {
|
||||
border-color: $uni-color-primary;
|
||||
color: #FFFFFF !important;
|
||||
background-color: $uni-color-primary;
|
||||
}
|
||||
|
||||
.agreements {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.face-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 50px auto 30px;
|
||||
}
|
||||
|
||||
.face-icon-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
margin: 10px 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dev-tip {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<!-- 设置密码 -->
|
||||
<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 class="set-password-form" ref="form" :value="formData" err-show-type="toast">
|
||||
<text class="tip">输入密码</text>
|
||||
<uni-forms-item name="newPassword">
|
||||
<uni-easyinput :focus="focusNewPassword" @blur="focusNewPassword = false" class="input-box"
|
||||
type="password" :inputBorder="false" v-model="formData.newPassword" placeholder="请输入密码">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<text class="tip">再次输入密码</text>
|
||||
<uni-forms-item name="newPassword2">
|
||||
<uni-easyinput :focus="focusNewPassword2" @blur="focusNewPassword2 = false" class="input-box"
|
||||
type="password" :inputBorder="false" v-model="formData.newPassword2" placeholder="请再次输入新密码">
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
<uni-id-pages-sms-form v-model="formData.code" type="set-pwd-by-sms" ref="smsCode" :phone="userInfo.mobile">
|
||||
</uni-id-pages-sms-form>
|
||||
<view class="link-box">
|
||||
<button class="uni-btn send-btn" type="primary" @click="submit">确认</button>
|
||||
<button v-if="allowSkip" class="uni-btn send-btn" type="default" @click="skip">跳过</button>
|
||||
</view>
|
||||
|
||||
</uni-forms>
|
||||
<uni-popup-captcha @confirm="submit" v-model="formData.captcha" scene="set-pwd-by-sms" ref="popup"></uni-popup-captcha>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import passwordMod from '@/uni_modules/uni-id-pages/common/password.js'
|
||||
import {store, mutations} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
import config from '@/uni_modules/uni-id-pages/config.js'
|
||||
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co", {
|
||||
customUI:true
|
||||
})
|
||||
export default {
|
||||
name: "set-pwd.vue",
|
||||
data () {
|
||||
return {
|
||||
uniIdRedirectUrl: '',
|
||||
loginType: '',
|
||||
logo: '/static/logo.png',
|
||||
focusNewPassword: false,
|
||||
focusNewPassword2: false,
|
||||
allowSkip: false,
|
||||
formData: {
|
||||
code: "",
|
||||
captcha: "",
|
||||
newPassword: "",
|
||||
newPassword2: ""
|
||||
},
|
||||
rules: passwordMod.getPwdRules('newPassword', 'newPassword2')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userInfo () {
|
||||
return store.userInfo
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
this.$refs.form.setRules(this.rules)
|
||||
},
|
||||
onLoad (e) {
|
||||
this.uniIdRedirectUrl = e.uniIdRedirectUrl
|
||||
this.loginType = e.loginType
|
||||
|
||||
if (config.setPasswordAfterLogin && config.setPasswordAfterLogin?.allowSkip) {
|
||||
this.allowSkip = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit () {
|
||||
if(! /^\d{6}$/.test(this.formData.code)){
|
||||
this.$refs.smsCode.focusSmsCodeInput = true
|
||||
return uni.showToast({
|
||||
title: '验证码格式不正确',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
this.$refs.form.validate()
|
||||
.then(res => {
|
||||
uniIdCo.setPwd({
|
||||
password: this.formData.newPassword,
|
||||
code: this.formData.code,
|
||||
captcha: this.formData.captcha
|
||||
}).then(e => {
|
||||
uni.showModal({
|
||||
content: '密码设置成功',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
mutations.loginBack({
|
||||
uniIdRedirectUrl: this.uniIdRedirectUrl,
|
||||
loginType: this.loginType
|
||||
})
|
||||
}
|
||||
});
|
||||
}).catch(e => {
|
||||
uni.showModal({
|
||||
content: e.message,
|
||||
showCancel: false
|
||||
});
|
||||
})
|
||||
}).catch(e => {
|
||||
if (e.errCode == 'uni-id-captcha-required') {
|
||||
this.$refs.popup.open()
|
||||
} else {
|
||||
console.log(e.errMsg);
|
||||
}
|
||||
}).finally(e => {
|
||||
this.formData.captcha = ''
|
||||
})
|
||||
},
|
||||
skip () {
|
||||
mutations.loginBack({
|
||||
uniIdRedirectUrl: this.uniIdRedirectUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
|
||||
|
||||
.uni-btn[type="default"] {
|
||||
color: inherit!important;
|
||||
}
|
||||
|
||||
.uni-content ::v-deep .uni-forms-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.popup-captcha {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
padding: 20rpx;
|
||||
background-color: #FFF;
|
||||
border-radius: 2px;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup-captcha .title {
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
padding-bottom: 15px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.popup-captcha .close {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
margin-left: -13px;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.popup-captcha .uni-btn {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
272
uni-im示例/uni_modules/uni-id-pages/pages/userinfo/userinfo.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<!-- 用户资料页 -->
|
||||
<template>
|
||||
<view class="uni-content">
|
||||
<view class="avatar">
|
||||
<uni-id-pages-avatar width="260rpx" height="260rpx"></uni-id-pages-avatar>
|
||||
</view>
|
||||
<uni-list>
|
||||
<uni-list-item class="item" @click="setNickname('')" title="昵称" :rightText="userInfo.nickname||'未设置'" link>
|
||||
</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>
|
||||
<!-- #ifdef APP -->
|
||||
<!-- 如未开通实人认证服务,可以将实名认证入口注释 -->
|
||||
<uni-list-item class="item" @click="realNameVerify" title="实名认证" :rightText="realNameStatus !== 2 ? '未认证': '已认证'" link>
|
||||
</uni-list-item>
|
||||
<!-- #endif -->
|
||||
<uni-list-item v-if="hasPwd" class="item" @click="changePassword" title="修改密码" link>
|
||||
</uni-list-item>
|
||||
</uni-list>
|
||||
<!-- #ifndef MP -->
|
||||
<uni-list class="mt10">
|
||||
<uni-list-item @click="deactivate" title="注销账号" link="navigateTo"></uni-list-item>
|
||||
</uni-list>
|
||||
<!-- #endif -->
|
||||
<uni-popup ref="dialog" type="dialog">
|
||||
<uni-popup-dialog mode="input" :value="userInfo.nickname" @confirm="setNickname" :inputType="setNicknameIng?'nickname':'text'"
|
||||
title="设置昵称" placeholder="请输入要设置的昵称">
|
||||
</uni-popup-dialog>
|
||||
</uni-popup>
|
||||
<uni-id-pages-bind-mobile ref="bind-mobile-by-sms" @success="bindMobileSuccess"></uni-id-pages-bind-mobile>
|
||||
<template v-if="showLoginManage">
|
||||
<button v-if="userInfo._id" @click="logout">退出登录</button>
|
||||
<button v-else @click="login">去登录</button>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co")
|
||||
import {
|
||||
store,
|
||||
mutations
|
||||
} from '@/uni_modules/uni-id-pages/common/store.js'
|
||||
export default {
|
||||
computed: {
|
||||
userInfo() {
|
||||
return store.userInfo
|
||||
},
|
||||
realNameStatus () {
|
||||
if (!this.userInfo.realNameAuth) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return this.userInfo.realNameAuth.authStatus
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
univerifyStyle: {
|
||||
authButton: {
|
||||
"title": "本机号码一键绑定", // 授权按钮文案
|
||||
},
|
||||
otherLoginButton: {
|
||||
"title": "其他号码绑定",
|
||||
}
|
||||
},
|
||||
// userInfo: {
|
||||
// mobile:'',
|
||||
// nickname:''
|
||||
// },
|
||||
hasPwd: false,
|
||||
showLoginManage: false ,//通过页面传参隐藏登录&退出登录按钮
|
||||
setNicknameIng:false
|
||||
}
|
||||
},
|
||||
async onShow() {
|
||||
this.univerifyStyle.authButton.title = "本机号码一键绑定"
|
||||
this.univerifyStyle.otherLoginButton.title = "其他号码绑定"
|
||||
},
|
||||
async onLoad(e) {
|
||||
if (e.showLoginManage) {
|
||||
this.showLoginManage = true //通过页面传参隐藏登录&退出登录按钮
|
||||
}
|
||||
//判断当前用户是否有密码,否则就不显示密码修改功能
|
||||
let res = await uniIdCo.getAccountInfo()
|
||||
this.hasPwd = res.isPasswordSet
|
||||
},
|
||||
methods: {
|
||||
login() {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/login/login-withoutpwd',
|
||||
complete: (e) => {
|
||||
// console.log(e);
|
||||
}
|
||||
})
|
||||
},
|
||||
logout() {
|
||||
mutations.logout()
|
||||
},
|
||||
bindMobileSuccess() {
|
||||
mutations.updateUserInfo()
|
||||
},
|
||||
changePassword() {
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-id-pages/pages/userinfo/change_pwd/change_pwd',
|
||||
complete: (e) => {
|
||||
// console.log(e);
|
||||
}
|
||||
})
|
||||
},
|
||||
bindMobile() {
|
||||
// #ifdef APP-PLUS
|
||||
uni.preLogin({
|
||||
provider: 'univerify',
|
||||
success: this.univerify(), //预登录成功
|
||||
fail: (res) => { // 预登录失败
|
||||
// 不显示一键登录选项(或置灰)
|
||||
console.log(res)
|
||||
this.bindMobileBySmsCode()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
this.$refs['bind-mobile-by-sms'].open()
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
//...去用验证码绑定
|
||||
this.bindMobileBySmsCode()
|
||||
// #endif
|
||||
},
|
||||
univerify() {
|
||||
uni.login({
|
||||
"provider": 'univerify',
|
||||
"univerifyStyle": this.univerifyStyle,
|
||||
success: async e => {
|
||||
uniIdCo.bindMobileByUniverify(e.authResult).then(res => {
|
||||
mutations.updateUserInfo()
|
||||
}).catch(e => {
|
||||
console.log(e);
|
||||
}).finally(e => {
|
||||
// console.log(e);
|
||||
uni.closeAuthView()
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err);
|
||||
if (err.code == '30002' || err.code == '30001') {
|
||||
this.bindMobileBySmsCode()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
bindMobileBySmsCode() {
|
||||
uni.navigateTo({
|
||||
url: './bind-mobile/bind-mobile'
|
||||
})
|
||||
},
|
||||
setNickname(nickname) {
|
||||
if (nickname) {
|
||||
mutations.updateUserInfo({
|
||||
nickname
|
||||
})
|
||||
this.setNicknameIng = false
|
||||
this.$refs.dialog.close()
|
||||
} else {
|
||||
this.$refs.dialog.open()
|
||||
}
|
||||
},
|
||||
deactivate(){
|
||||
uni.navigateTo({
|
||||
url:"/uni_modules/uni-id-pages/pages/userinfo/deactivate/deactivate"
|
||||
})
|
||||
},
|
||||
async bindThirdAccount(provider) {
|
||||
const uniIdCo = uniCloud.importObject("uni-id-co")
|
||||
const bindField = {
|
||||
weixin: 'wx_openid',
|
||||
alipay: 'ali_openid',
|
||||
apple: 'apple_openid',
|
||||
qq: 'qq_openid'
|
||||
}[provider.toLowerCase()]
|
||||
|
||||
if (this.userInfo[bindField]) {
|
||||
await uniIdCo['unbind' + provider]()
|
||||
await mutations.updateUserInfo()
|
||||
} else {
|
||||
uni.login({
|
||||
provider: provider.toLowerCase(),
|
||||
onlyAuthorize: true,
|
||||
success: async e => {
|
||||
const res = await uniIdCo['bind' + provider]({
|
||||
code: e.code
|
||||
})
|
||||
if (res.errCode) {
|
||||
uni.showToast({
|
||||
title: res.errMsg || '绑定失败',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
await mutations.updateUserInfo()
|
||||
},
|
||||
fail: async (err) => {
|
||||
console.log(err);
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
realNameVerify () {
|
||||
uni.navigateTo({
|
||||
url: "/uni_modules/uni-id-pages/pages/userinfo/realname-verify/realname-verify"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/uni_modules/uni-id-pages/common/login-page.scss";
|
||||
|
||||
.uni-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
view {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 690px) {
|
||||
.uni-content {
|
||||
padding: 0;
|
||||
max-width: 690px;
|
||||
margin-left: calc(50% - 345px);
|
||||
border: none;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
.avatar {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 22px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 10%;
|
||||
margin-top: 40px;
|
||||
border-radius: 0;
|
||||
background-color: #FFFFFF;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.mt10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
15
uni-im示例/uni_modules/uni-id-pages/readme.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 文档已移至uni-id-pages文档[https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html)
|
||||
|
||||
|
||||
|
||||
关于插件更新的说明:
|
||||
|
||||
所有uni_modules,在HBuilderX里点右键都可以直接升级。或者在插件市场导入覆盖。
|
||||
|
||||
覆盖时HBuilderX会弹出代码差异比对,可以决定接受哪些更改、拒绝哪些更改。
|
||||
|
||||
当拒绝局部修改时,注意可能产生兼容性问题。
|
||||
|
||||
你需要二次开发uni-id-pages的前端页面,
|
||||
- 如果改动不大,那么每次更新uni-id-pages时,在HBuilderX的对比界面对比一下就好
|
||||
- 如果改动较大,建议复制一套前端页面到自己工程的pages目录下,pages.json里只引用根目录pages下的页面,不引用uni_modules下的页面。然后每次uni-id-pages更新,你对比下比上一版uni-id-pages改了什么,看你是否需要再合并到你自己的pages里。pages.json里不引用uni_modules里的页面的话,打包时不会把这些页面打包进去,不影响发行后的包体积
|
||||
BIN
uni-im示例/uni_modules/uni-id-pages/static/app-plus/apple.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#606060;}
|
||||
.st1{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st2" d="M11.6,11c0.4,0.4,0.6,0.9,0.6,1.5c0,0.6-0.2,1.1-0.6,1.4c-0.4,0.4-0.9,0.6-1.5,0.6c-0.6,0-1.1-0.2-1.5-0.6
|
||||
c-0.4-0.4-0.6-0.9-0.6-1.4s0.2-1.1,0.6-1.5c0.4-0.4,0.9-0.6,1.5-0.6C10.8,10.4,11.2,10.6,11.6,11z M24.6,18.4V6.7H5.4v12l1.8-1.8
|
||||
c0.3-0.3,0.6-0.4,1-0.4c0.4,0,0.7,0.1,1,0.4l1.8,1.8l5.8-7c0.3-0.3,0.6-0.5,1.1-0.5c0.4,0,0.8,0.2,1.1,0.5
|
||||
C18.8,11.6,24.6,18.4,24.6,18.4z M25.6,5.7C25.9,6,26,6.3,26,6.7v16.1c0,0.4-0.1,0.7-0.4,1c-0.3,0.3-0.6,0.4-1,0.4H5.4
|
||||
c-0.4,0-0.7-0.1-1-0.4c-0.3-0.3-0.4-0.6-0.4-1V6.7c0-0.4,0.1-0.7,0.4-1c0.3-0.3,0.6-0.4,1-0.4h19.3C25,5.3,25.3,5.4,25.6,5.7z"/>
|
||||
<path class="st1" d="M24.3,21.5H5.7c-0.2,0-0.3-0.2-0.3-0.3V7c0-0.2,0.2-0.3,0.3-0.3h18.6c0.2,0,0.3,0.2,0.3,0.3v14.2
|
||||
C24.6,21.3,24.5,21.5,24.3,21.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="30px" height="30px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M17.1,24.2h-12c-0.2,0-0.3-0.2-0.3-0.3v-9.3c0-0.2,0.2-0.3,0.3-0.3h12c0.2,0,0.3,0.2,0.3,0.3v9.3
|
||||
C17.5,24.1,17.3,24.2,17.1,24.2z"/>
|
||||
<path class="st0" d="M16.6,5.4c4.8,0,8.7,3.9,8.7,8.7"/>
|
||||
<polyline class="st0" points="19.3,10.1 14.9,5.6 19.3,1.2 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 791 B |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
BIN
uni-im示例/uni_modules/uni-id-pages/static/login/weixin.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,108 @@
|
||||
const db = uniCloud.database()
|
||||
const dbCmd = db.command
|
||||
const userCollectionName = 'uni-id-users'
|
||||
const userCollection = db.collection(userCollectionName)
|
||||
const verifyCollectionName = 'opendb-verify-codes'
|
||||
const verifyCollection = db.collection(verifyCollectionName)
|
||||
const deviceCollectionName = 'uni-id-device'
|
||||
const deviceCollection = db.collection(deviceCollectionName)
|
||||
const openDataCollectionName = 'opendb-open-data'
|
||||
const openDataCollection = db.collection(openDataCollectionName)
|
||||
const frvLogsCollectionName = 'opendb-frv-logs'
|
||||
const frvLogsCollection = db.collection(frvLogsCollectionName)
|
||||
|
||||
const USER_IDENTIFIER = {
|
||||
_id: 'uid',
|
||||
username: 'username',
|
||||
mobile: 'mobile',
|
||||
email: 'email',
|
||||
wx_unionid: 'wechat-account',
|
||||
'wx_openid.app': 'wechat-account',
|
||||
'wx_openid.mp': 'wechat-account',
|
||||
'wx_openid.h5': 'wechat-account',
|
||||
'wx_openid.web': 'wechat-account',
|
||||
qq_unionid: 'qq-account',
|
||||
'qq_openid.app': 'qq-account',
|
||||
'qq_openid.mp': 'qq-account',
|
||||
ali_openid: 'alipay-account',
|
||||
apple_openid: 'alipay-account',
|
||||
identities: 'idp'
|
||||
}
|
||||
|
||||
const USER_STATUS = {
|
||||
NORMAL: 0,
|
||||
BANNED: 1,
|
||||
AUDITING: 2,
|
||||
AUDIT_FAILED: 3,
|
||||
CLOSED: 4
|
||||
}
|
||||
|
||||
const CAPTCHA_SCENE = {
|
||||
REGISTER: 'register',
|
||||
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',
|
||||
SET_PWD_BY_SMS: 'set-pwd-by-sms'
|
||||
}
|
||||
|
||||
const LOG_TYPE = {
|
||||
LOGOUT: 'logout',
|
||||
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',
|
||||
BIND_APPLE: 'bind-apple',
|
||||
BIND_ALIPAY: 'bind-alipay',
|
||||
UNBIND_WEIXIN: 'unbind-weixin',
|
||||
UNBIND_QQ: 'unbind-qq',
|
||||
UNBIND_ALIPAY: 'unbind-alipay',
|
||||
UNBIND_APPLE: 'unbind-apple'
|
||||
}
|
||||
|
||||
const SMS_SCENE = {
|
||||
LOGIN_BY_SMS: 'login-by-sms',
|
||||
RESET_PWD_BY_SMS: 'reset-pwd-by-sms',
|
||||
BIND_MOBILE_BY_SMS: 'bind-mobile-by-sms',
|
||||
SET_PWD_BY_SMS: 'set-pwd-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'
|
||||
}
|
||||
|
||||
const REAL_NAME_STATUS = {
|
||||
NOT_CERTIFIED: 0,
|
||||
WAITING_CERTIFIED: 1,
|
||||
CERTIFIED: 2,
|
||||
CERTIFY_FAILED: 3
|
||||
}
|
||||
|
||||
const EXTERNAL_DIRECT_CONNECT_PROVIDER = 'externalDirectConnect'
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
dbCmd,
|
||||
userCollection,
|
||||
verifyCollection,
|
||||
deviceCollection,
|
||||
openDataCollection,
|
||||
frvLogsCollection,
|
||||
USER_IDENTIFIER,
|
||||
USER_STATUS,
|
||||
CAPTCHA_SCENE,
|
||||
LOG_TYPE,
|
||||
SMS_SCENE,
|
||||
EMAIL_SCENE,
|
||||
REAL_NAME_STATUS,
|
||||
EXTERNAL_DIRECT_CONNECT_PROVIDER
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
const ERROR = {
|
||||
ACCOUNT_EXISTS: 'uni-id-account-exists',
|
||||
ACCOUNT_NOT_EXISTS: 'uni-id-account-not-exists',
|
||||
ACCOUNT_NOT_EXISTS_IN_CURRENT_APP: 'uni-id-account-not-exists-in-current-app',
|
||||
ACCOUNT_CONFLICT: 'uni-id-account-conflict',
|
||||
ACCOUNT_BANNED: 'uni-id-account-banned',
|
||||
ACCOUNT_AUDITING: 'uni-id-account-auditing',
|
||||
ACCOUNT_AUDIT_FAILED: 'uni-id-account-audit-failed',
|
||||
ACCOUNT_CLOSED: 'uni-id-account-closed',
|
||||
CAPTCHA_REQUIRED: 'uni-id-captcha-required',
|
||||
PASSWORD_ERROR: 'uni-id-password-error',
|
||||
PASSWORD_ERROR_EXCEED_LIMIT: 'uni-id-password-error-exceed-limit',
|
||||
INVALID_USERNAME: 'uni-id-invalid-username',
|
||||
INVALID_PASSWORD: 'uni-id-invalid-password',
|
||||
INVALID_PASSWORD_SUPER: 'uni-id-invalid-password-super',
|
||||
INVALID_PASSWORD_STRONG: 'uni-id-invalid-password-strong',
|
||||
INVALID_PASSWORD_MEDIUM: 'uni-id-invalid-password-medium',
|
||||
INVALID_PASSWORD_WEAK: 'uni-id-invalid-password-weak',
|
||||
INVALID_MOBILE: 'uni-id-invalid-mobile',
|
||||
INVALID_EMAIL: 'uni-id-invalid-email',
|
||||
INVALID_NICKNAME: 'uni-id-invalid-nickname',
|
||||
INVALID_PARAM: 'uni-id-invalid-param',
|
||||
PARAM_REQUIRED: 'uni-id-param-required',
|
||||
GET_THIRD_PARTY_ACCOUNT_FAILED: 'uni-id-get-third-party-account-failed',
|
||||
GET_THIRD_PARTY_USER_INFO_FAILED: 'uni-id-get-third-party-user-info-failed',
|
||||
MOBILE_VERIFY_CODE_ERROR: 'uni-id-mobile-verify-code-error',
|
||||
EMAIL_VERIFY_CODE_ERROR: 'uni-id-email-verify-code-error',
|
||||
ADMIN_EXISTS: 'uni-id-admin-exists',
|
||||
PERMISSION_ERROR: 'uni-id-permission-error',
|
||||
SYSTEM_ERROR: 'uni-id-system-error',
|
||||
SET_INVITE_CODE_FAILED: 'uni-id-set-invite-code-failed',
|
||||
INVALID_INVITE_CODE: 'uni-id-invalid-invite-code',
|
||||
CHANGE_INVITER_FORBIDDEN: 'uni-id-change-inviter-forbidden',
|
||||
BIND_CONFLICT: 'uni-id-bind-conflict',
|
||||
UNBIND_FAIL: 'uni-id-unbind-failed',
|
||||
UNBIND_NOT_SUPPORTED: 'uni-id-unbind-not-supported',
|
||||
UNBIND_UNIQUE_LOGIN: 'uni-id-unbind-unique-login',
|
||||
UNBIND_PASSWORD_NOT_EXISTS: 'uni-id-unbind-password-not-exists',
|
||||
UNBIND_MOBILE_NOT_EXISTS: 'uni-id-unbind-mobile-not-exists',
|
||||
UNSUPPORTED_REQUEST: 'uni-id-unsupported-request',
|
||||
ILLEGAL_REQUEST: 'uni-id-illegal-request',
|
||||
CONFIG_FIELD_REQUIRED: 'uni-id-config-field-required',
|
||||
CONFIG_FIELD_INVALID: 'uni-id-config-field-invalid',
|
||||
FRV_FAIL: 'uni-id-frv-fail',
|
||||
FRV_PROCESSING: 'uni-id-frv-processing',
|
||||
REAL_NAME_VERIFIED: 'uni-id-realname-verified',
|
||||
ID_CARD_EXISTS: 'uni-id-idcard-exists',
|
||||
INVALID_ID_CARD: 'uni-id-invalid-idcard',
|
||||
INVALID_REAL_NAME: 'uni-id-invalid-realname',
|
||||
UNKNOWN_ERROR: 'uni-id-unknown-error',
|
||||
REAL_NAME_VERIFY_UPPER_LIMIT: 'uni-id-realname-verify-upper-limit'
|
||||
}
|
||||
|
||||
function isUniIdError (errCode) {
|
||||
return Object.values(ERROR).includes(errCode)
|
||||
}
|
||||
|
||||
class UniCloudError extends Error {
|
||||
constructor (options) {
|
||||
super(options.message)
|
||||
this.errMsg = options.message || ''
|
||||
this.errCode = options.code
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ERROR,
|
||||
isUniIdError,
|
||||
UniCloudError
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
const crypto = require('crypto')
|
||||
const { ERROR } = require('./error')
|
||||
|
||||
function checkSecret (secret) {
|
||||
if (!secret) {
|
||||
throw {
|
||||
errCode: ERROR.CONFIG_FIELD_REQUIRED,
|
||||
errMsgValue: {
|
||||
field: 'sensitiveInfoEncryptSecret'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (secret.length !== 32) {
|
||||
throw {
|
||||
errCode: ERROR.CONFIG_FIELD_INVALID,
|
||||
errMsgValue: {
|
||||
field: 'sensitiveInfoEncryptSecret'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function encryptData (text = '') {
|
||||
if (!text) return text
|
||||
|
||||
const encryptSecret = this.config.sensitiveInfoEncryptSecret
|
||||
|
||||
checkSecret(encryptSecret)
|
||||
|
||||
const iv = encryptSecret.slice(-16)
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', encryptSecret, iv)
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(Buffer.from(text, 'utf-8')),
|
||||
cipher.final()
|
||||
])
|
||||
|
||||
return encrypted.toString('base64')
|
||||
}
|
||||
|
||||
function decryptData (text = '') {
|
||||
if (!text) return text
|
||||
|
||||
const encryptSecret = this.config.sensitiveInfoEncryptSecret
|
||||
|
||||
checkSecret(encryptSecret)
|
||||
|
||||
const iv = encryptSecret.slice(-16)
|
||||
|
||||
const cipher = crypto.createDecipheriv('aes-256-cbc', encryptSecret, iv)
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
cipher.update(Buffer.from(text, 'base64')),
|
||||
cipher.final()
|
||||
])
|
||||
|
||||
return decrypted.toString('utf-8')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encryptData,
|
||||
decryptData
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
const { ERROR } = require('./error')
|
||||
|
||||
function getHttpClientInfo () {
|
||||
const requestId = this.getUniCloudRequestId()
|
||||
const { clientIP, userAgent, source, secretType = 'none' } = this.getClientInfo()
|
||||
const { clientInfo = {} } = JSON.parse(this.getHttpInfo().body)
|
||||
|
||||
return {
|
||||
...clientInfo,
|
||||
clientIP,
|
||||
userAgent,
|
||||
source,
|
||||
secretType,
|
||||
requestId
|
||||
}
|
||||
}
|
||||
|
||||
function getHttpUniIdToken () {
|
||||
const { uniIdToken = '' } = JSON.parse(this.getHttpInfo().body)
|
||||
|
||||
return uniIdToken
|
||||
}
|
||||
|
||||
function verifyHttpMethod () {
|
||||
const { headers, httpMethod } = this.getHttpInfo()
|
||||
|
||||
if (!/^application\/json/.test(headers['content-type']) || httpMethod.toUpperCase() !== 'POST') {
|
||||
throw {
|
||||
errCode: ERROR.UNSUPPORTED_REQUEST,
|
||||
errMsg: 'unsupported request'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function universal () {
|
||||
if (this.getClientInfo().source === 'http') {
|
||||
verifyHttpMethod.call(this)
|
||||
this.getParams()[0] = JSON.parse(this.getHttpInfo().body).params
|
||||
this.getUniversalClientInfo = getHttpClientInfo.bind(this)
|
||||
this.getUniversalUniIdToken = getHttpUniIdToken.bind(this)
|
||||
} else {
|
||||
this.getUniversalClientInfo = this.getClientInfo
|
||||
this.getUniversalUniIdToken = this.getUniIdToken
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = universal
|
||||
@@ -0,0 +1,270 @@
|
||||
function batchFindObjctValue (obj = {}, keys = []) {
|
||||
const values = {}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
const keyPath = key.split('.')
|
||||
let currentKey = keyPath.shift()
|
||||
let result = obj
|
||||
while (currentKey) {
|
||||
if (!result) {
|
||||
break
|
||||
}
|
||||
result = result[currentKey]
|
||||
currentKey = keyPath.shift()
|
||||
}
|
||||
values[key] = result
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
function getType (val) {
|
||||
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase()
|
||||
}
|
||||
|
||||
function hasOwn (obj, key) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, key)
|
||||
}
|
||||
|
||||
function isValidString (val) {
|
||||
return val && getType(val) === 'string'
|
||||
}
|
||||
|
||||
function isPlainObject (obj) {
|
||||
return getType(obj) === 'object'
|
||||
}
|
||||
|
||||
function isFn (fn) {
|
||||
// 务必注意AsyncFunction
|
||||
return typeof fn === 'function'
|
||||
}
|
||||
|
||||
// 获取文件后缀,只添加几种图片类型供客服消息接口使用
|
||||
const mime2ext = {
|
||||
'image/png': 'png',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/gif': 'gif',
|
||||
'image/svg+xml': 'svg',
|
||||
'image/bmp': 'bmp',
|
||||
'image/webp': 'webp'
|
||||
}
|
||||
|
||||
function getExtension (contentType) {
|
||||
return mime2ext[contentType]
|
||||
}
|
||||
|
||||
const isSnakeCase = /_(\w)/g
|
||||
const isCamelCase = /[A-Z]/g
|
||||
|
||||
function snake2camel (value) {
|
||||
return value.replace(isSnakeCase, (_, c) => (c ? c.toUpperCase() : ''))
|
||||
}
|
||||
|
||||
function camel2snake (value) {
|
||||
return value.replace(isCamelCase, str => '_' + str.toLowerCase())
|
||||
}
|
||||
|
||||
function parseObjectKeys (obj, type) {
|
||||
let parserReg, parser
|
||||
switch (type) {
|
||||
case 'snake2camel':
|
||||
parser = snake2camel
|
||||
parserReg = isSnakeCase
|
||||
break
|
||||
case 'camel2snake':
|
||||
parser = camel2snake
|
||||
parserReg = isCamelCase
|
||||
break
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (hasOwn(obj, key)) {
|
||||
if (parserReg.test(key)) {
|
||||
const keyCopy = parser(key)
|
||||
obj[keyCopy] = obj[key]
|
||||
delete obj[key]
|
||||
if (isPlainObject(obj[keyCopy])) {
|
||||
obj[keyCopy] = parseObjectKeys(obj[keyCopy], type)
|
||||
} else if (Array.isArray(obj[keyCopy])) {
|
||||
obj[keyCopy] = obj[keyCopy].map((item) => {
|
||||
return parseObjectKeys(item, type)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function snake2camelJson (obj) {
|
||||
return parseObjectKeys(obj, 'snake2camel')
|
||||
}
|
||||
|
||||
function camel2snakeJson (obj) {
|
||||
return parseObjectKeys(obj, 'camel2snake')
|
||||
}
|
||||
|
||||
function getOffsetDate (offset) {
|
||||
return new Date(
|
||||
Date.now() + (new Date().getTimezoneOffset() + (offset || 0) * 60) * 60000
|
||||
)
|
||||
}
|
||||
|
||||
function getDateStr (date, separator = '-') {
|
||||
date = date || new Date()
|
||||
const dateArr = []
|
||||
dateArr.push(date.getFullYear())
|
||||
dateArr.push(('00' + (date.getMonth() + 1)).substr(-2))
|
||||
dateArr.push(('00' + date.getDate()).substr(-2))
|
||||
return dateArr.join(separator)
|
||||
}
|
||||
|
||||
function getTimeStr (date, separator = ':') {
|
||||
date = date || new Date()
|
||||
const timeArr = []
|
||||
timeArr.push(('00' + date.getHours()).substr(-2))
|
||||
timeArr.push(('00' + date.getMinutes()).substr(-2))
|
||||
timeArr.push(('00' + date.getSeconds()).substr(-2))
|
||||
return timeArr.join(separator)
|
||||
}
|
||||
|
||||
function getFullTimeStr (date) {
|
||||
date = date || new Date()
|
||||
return getDateStr(date) + ' ' + getTimeStr(date)
|
||||
}
|
||||
|
||||
function getDistinctArray (arr) {
|
||||
return Array.from(new Set(arr))
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接url
|
||||
* @param {string} base 基础路径
|
||||
* @param {string} path 在基础路径上拼接的路径
|
||||
* @returns
|
||||
*/
|
||||
function resolveUrl (base, path) {
|
||||
if (/^https?:/.test(path)) {
|
||||
return path
|
||||
}
|
||||
return base + path
|
||||
}
|
||||
|
||||
function getVerifyCode (len = 6) {
|
||||
let code = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
code += Math.floor(Math.random() * 10)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
function coverMobile (mobile) {
|
||||
if (typeof mobile !== 'string') {
|
||||
return mobile
|
||||
}
|
||||
return mobile.slice(0, 3) + '****' + mobile.slice(7)
|
||||
}
|
||||
|
||||
function getNonceStr (length = 16) {
|
||||
let str = ''
|
||||
while (str.length < length) {
|
||||
str += Math.random().toString(32).substring(2)
|
||||
}
|
||||
return str.substring(0, length)
|
||||
}
|
||||
|
||||
try {
|
||||
require('lodash.merge')
|
||||
} catch (error) {
|
||||
console.error('uni-id-co缺少依赖,请在uniCloud/cloudfunctions/uni-id-co目录执行 npm install 安装依赖')
|
||||
throw error
|
||||
}
|
||||
|
||||
function isMatchUserApp (userAppList, matchAppList) {
|
||||
if (userAppList === undefined || userAppList === null) {
|
||||
return true
|
||||
}
|
||||
if (getType(userAppList) !== 'array') {
|
||||
return false
|
||||
}
|
||||
if (userAppList.includes('*')) {
|
||||
return true
|
||||
}
|
||||
if (getType(matchAppList) === 'string') {
|
||||
matchAppList = [matchAppList]
|
||||
}
|
||||
return userAppList.some(item => matchAppList.includes(item))
|
||||
}
|
||||
|
||||
function checkIdCard (idCardNumber) {
|
||||
if (!idCardNumber || typeof idCardNumber !== 'string' || idCardNumber.length !== 18) return false
|
||||
|
||||
const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
const checkCode = [1, 0, 'x', 9, 8, 7, 6, 5, 4, 3, 2]
|
||||
const code = idCardNumber.substring(17)
|
||||
|
||||
let sum = 0
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += Number(idCardNumber.charAt(i)) * coefficient[i]
|
||||
}
|
||||
|
||||
return checkCode[sum % 11].toString() === code.toLowerCase()
|
||||
}
|
||||
|
||||
function catchAwait (fn, finallyFn) {
|
||||
if (!fn) return [new Error('no function')]
|
||||
|
||||
if (Promise.prototype.finally === undefined) {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Promise.prototype.finally = function (finallyFn) {
|
||||
return this.then(
|
||||
res => Promise.resolve(finallyFn()).then(() => res),
|
||||
error => Promise.resolve(finallyFn()).then(() => { throw error })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return fn
|
||||
.then((data) => [undefined, data])
|
||||
.catch((error) => [error])
|
||||
.finally(() => typeof finallyFn === 'function' && finallyFn())
|
||||
}
|
||||
|
||||
function dataDesensitization (value = '', options = {}) {
|
||||
const { onlyLast = false } = options
|
||||
const [firstIndex, middleIndex, lastIndex] = onlyLast ? [0, 0, -1] : [0, 1, -1]
|
||||
|
||||
if (!value) return value
|
||||
const first = value.slice(firstIndex, middleIndex)
|
||||
const middle = value.slice(middleIndex, lastIndex)
|
||||
const last = value.slice(lastIndex)
|
||||
const star = Array.from(new Array(middle.length), (v) => '*').join('')
|
||||
|
||||
return first + star + last
|
||||
}
|
||||
|
||||
function getCurrentDateTimestamp (date = Date.now(), targetTimezone = 8) {
|
||||
const oneHour = 60 * 60 * 1000
|
||||
return parseInt((date + targetTimezone * oneHour) / (24 * oneHour)) * (24 * oneHour) - targetTimezone * oneHour
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getType,
|
||||
isValidString,
|
||||
batchFindObjctValue,
|
||||
isPlainObject,
|
||||
isFn,
|
||||
getDistinctArray,
|
||||
getFullTimeStr,
|
||||
resolveUrl,
|
||||
getOffsetDate,
|
||||
camel2snakeJson,
|
||||
snake2camelJson,
|
||||
getExtension,
|
||||
getVerifyCode,
|
||||
coverMobile,
|
||||
getNonceStr,
|
||||
isMatchUserApp,
|
||||
checkIdCard,
|
||||
catchAwait,
|
||||
dataDesensitization,
|
||||
getCurrentDateTimestamp
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
const {
|
||||
isValidString,
|
||||
getType
|
||||
} = require('./utils.js')
|
||||
const {
|
||||
ERROR
|
||||
} = require('./error')
|
||||
|
||||
const baseValidator = Object.create(null)
|
||||
|
||||
baseValidator.username = function (username) {
|
||||
const errCode = ERROR.INVALID_USERNAME
|
||||
if (!isValidString(username)) {
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
if (/^\d+$/.test(username)) {
|
||||
// 用户名不能为纯数字
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
};
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
|
||||
// 用户名仅能使用数字、字母、“_”及“-”
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseValidator.password = function (password) {
|
||||
const errCode = ERROR.INVALID_PASSWORD
|
||||
if (!isValidString(password)) {
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
if (password.length < 6) {
|
||||
// 密码长度不能小于6
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseValidator.mobile = function (mobile) {
|
||||
const errCode = ERROR.INVALID_MOBILE
|
||||
if (getType(mobile) !== 'string') {
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
if (mobile && !/^1\d{10}$/.test(mobile)) {
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseValidator.email = function (email) {
|
||||
const errCode = ERROR.INVALID_EMAIL
|
||||
if (getType(email) !== 'string') {
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
if (email && !/@/.test(email)) {
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseValidator.nickname = function (nickname) {
|
||||
const errCode = ERROR.INVALID_NICKNAME
|
||||
if (nickname.indexOf('@') !== -1) {
|
||||
// 昵称不允许含@
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
};
|
||||
if (/^\d+$/.test(nickname)) {
|
||||
// 昵称不能为纯数字
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
};
|
||||
if (nickname.length > 100) {
|
||||
// 昵称不可超过100字符
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const baseType = ['string', 'boolean', 'number', 'null'] // undefined不会由客户端提交上来
|
||||
|
||||
baseType.forEach((type) => {
|
||||
baseValidator[type] = function (val) {
|
||||
if (getType(val) === type) {
|
||||
return
|
||||
}
|
||||
return {
|
||||
errCode: ERROR.INVALID_PARAM
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function tokenize(name) {
|
||||
let i = 0
|
||||
const result = []
|
||||
let token = ''
|
||||
while (i < name.length) {
|
||||
const char = name[i]
|
||||
switch (char) {
|
||||
case '|':
|
||||
case '<':
|
||||
case '>':
|
||||
token && result.push(token)
|
||||
result.push(char)
|
||||
token = ''
|
||||
break
|
||||
default:
|
||||
token += char
|
||||
break
|
||||
}
|
||||
i++
|
||||
if (i === name.length && token) {
|
||||
result.push(token)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理validator名
|
||||
* @param {string} name
|
||||
*/
|
||||
function parseValidatorName(name) {
|
||||
const tokenList = tokenize(name)
|
||||
let i = 0
|
||||
let currentToken = tokenList[i]
|
||||
const result = {
|
||||
type: 'root',
|
||||
children: [],
|
||||
parent: null
|
||||
}
|
||||
let lastRealm = result
|
||||
while (currentToken) {
|
||||
switch (currentToken) {
|
||||
case 'array': {
|
||||
const currentRealm = {
|
||||
type: 'array',
|
||||
children: [],
|
||||
parent: lastRealm
|
||||
}
|
||||
lastRealm.children.push(currentRealm)
|
||||
lastRealm = currentRealm
|
||||
break
|
||||
}
|
||||
case '<':
|
||||
if (lastRealm.type !== 'array') {
|
||||
throw new Error('Invalid validator token "<"')
|
||||
}
|
||||
break
|
||||
case '>':
|
||||
if (lastRealm.type !== 'array') {
|
||||
throw new Error('Invalid validator token ">"')
|
||||
}
|
||||
lastRealm = lastRealm.parent
|
||||
break
|
||||
case '|':
|
||||
if (lastRealm.type !== 'array' && lastRealm.type !== 'root') {
|
||||
throw new Error('Invalid validator token "|"')
|
||||
}
|
||||
break
|
||||
default:
|
||||
lastRealm.children.push({
|
||||
type: currentToken
|
||||
})
|
||||
break
|
||||
}
|
||||
i++
|
||||
currentToken = tokenList[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getRuleCategory(rule) {
|
||||
switch (rule.type) {
|
||||
case 'array':
|
||||
return 'array'
|
||||
case 'root':
|
||||
return 'root'
|
||||
default:
|
||||
return 'base'
|
||||
}
|
||||
}
|
||||
|
||||
function isMatchUnionType(val, rule) {
|
||||
if (!rule.children || rule.children.length === 0) {
|
||||
return true
|
||||
}
|
||||
const children = rule.children
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
const category = getRuleCategory(child)
|
||||
let pass = false
|
||||
switch (category) {
|
||||
case 'base':
|
||||
pass = isMatchBaseType(val, child)
|
||||
break
|
||||
case 'array':
|
||||
pass = isMatchArrayType(val, child)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
if (pass) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isMatchBaseType(val, rule) {
|
||||
if (typeof baseValidator[rule.type] !== 'function') {
|
||||
throw new Error(`invalid schema type: ${rule.type}`)
|
||||
}
|
||||
const validateRes = baseValidator[rule.type](val)
|
||||
if (validateRes && validateRes.errCode) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isMatchArrayType(arr, rule) {
|
||||
if (getType(arr) !== 'array') {
|
||||
return false
|
||||
}
|
||||
if (rule.children && rule.children.length && arr.some(item => !isMatchUnionType(item, rule))) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 特殊符号 https://www.ibm.com/support/pages/password-strength-rules ~!@#$%^&*_-+=`|\(){}[]:;"'<>,.?/
|
||||
// const specialChar = '~!@#$%^&*_-+=`|\(){}[]:;"\'<>,.?/'
|
||||
// const specialCharRegExp = /^[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]$/
|
||||
// for (let i = 0, arr = specialChar.split(''); i < arr.length; i++) {
|
||||
// const char = arr[i]
|
||||
// if (!specialCharRegExp.test(char)) {
|
||||
// throw new Error('check special character error: ' + char)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 密码强度表达式
|
||||
const passwordRules = {
|
||||
// 密码必须包含大小写字母、数字和特殊符号
|
||||
super: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||||
// 密码必须包含字母、数字和特殊符号
|
||||
strong: /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||||
// 密码必须为字母、数字和特殊符号任意两种的组合
|
||||
medium: /^(?![0-9]+$)(?![a-zA-Z]+$)(?![~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]+$)[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||||
// 密码必须包含字母和数字
|
||||
weak: /^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{6,16}$/,
|
||||
|
||||
}
|
||||
|
||||
function createPasswordVerifier({
|
||||
passwordStrength = ''
|
||||
} = {}) {
|
||||
return function (password) {
|
||||
const passwordRegExp = passwordRules[passwordStrength]
|
||||
if (!passwordRegExp) {
|
||||
throw new Error('Invalid password strength config: ' + passwordStrength)
|
||||
}
|
||||
const errCode = ERROR.INVALID_PASSWORD
|
||||
if (!isValidString(password)) {
|
||||
return {
|
||||
errCode
|
||||
}
|
||||
}
|
||||
if (!passwordRegExp.test(password)) {
|
||||
return {
|
||||
errCode: errCode + '-' + passwordStrength
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(value) {
|
||||
return value === undefined ||
|
||||
value === null ||
|
||||
(typeof value === 'string' && value.trim() === '')
|
||||
}
|
||||
|
||||
class Validator {
|
||||
constructor({
|
||||
passwordStrength = ''
|
||||
} = {}) {
|
||||
this.baseValidator = baseValidator
|
||||
this.customValidator = Object.create(null)
|
||||
if (passwordStrength) {
|
||||
this.mixin(
|
||||
'password',
|
||||
createPasswordVerifier({
|
||||
passwordStrength
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mixin(type, handler) {
|
||||
this.customValidator[type] = handler
|
||||
}
|
||||
|
||||
getRealBaseValidator(type) {
|
||||
return this.customValidator[type] || this.baseValidator[type]
|
||||
}
|
||||
|
||||
get validator() {
|
||||
return new Proxy({}, {
|
||||
get: (_, prop) => {
|
||||
if (typeof prop !== 'string') {
|
||||
return
|
||||
}
|
||||
const realBaseValidator = this.getRealBaseValidator(prop)
|
||||
if (realBaseValidator) {
|
||||
return realBaseValidator
|
||||
}
|
||||
const rule = parseValidatorName(prop)
|
||||
return function (val) {
|
||||
if (!isMatchUnionType(val, rule)) {
|
||||
return {
|
||||
errCode: ERROR.INVALID_PARAM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
validate(value = {}, schema = {}) {
|
||||
for (const schemaKey in schema) {
|
||||
let schemaValue = schema[schemaKey]
|
||||
if (getType(schemaValue) === 'string') {
|
||||
schemaValue = {
|
||||
required: true,
|
||||
type: schemaValue
|
||||
}
|
||||
}
|
||||
const {
|
||||
required,
|
||||
type
|
||||
} = schemaValue
|
||||
// value内未传入了schemaKey或对应值为undefined
|
||||
if (isEmpty(value[schemaKey])) {
|
||||
if (required) {
|
||||
return {
|
||||
errCode: ERROR.PARAM_REQUIRED,
|
||||
errMsgValue: {
|
||||
param: schemaKey
|
||||
},
|
||||
schemaKey
|
||||
}
|
||||
} else {
|
||||
//delete value[schemaKey]
|
||||
continue
|
||||
}
|
||||
}
|
||||
const validateMethod = this.validator[type]
|
||||
if (!validateMethod) {
|
||||
throw new Error(`invalid schema type: ${type}`)
|
||||
}
|
||||
const validateRes = validateMethod(value[schemaKey])
|
||||
if (validateRes) {
|
||||
validateRes.schemaKey = schemaKey
|
||||
return validateRes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkClientInfo(clientInfo) {
|
||||
const stringNotRequired = {
|
||||
required: false,
|
||||
type: 'string'
|
||||
}
|
||||
const numberNotRequired = {
|
||||
required: false,
|
||||
type: 'number'
|
||||
}
|
||||
const numberOrStringNotRequired = {
|
||||
required: false,
|
||||
type: 'number|string'
|
||||
}
|
||||
const schema = {
|
||||
uniPlatform: 'string',
|
||||
appId: 'string',
|
||||
deviceId: stringNotRequired,
|
||||
osName: stringNotRequired,
|
||||
locale: stringNotRequired,
|
||||
clientIP: stringNotRequired,
|
||||
appName: stringNotRequired,
|
||||
appVersion: stringNotRequired,
|
||||
appVersionCode: numberOrStringNotRequired,
|
||||
channel: numberOrStringNotRequired,
|
||||
userAgent: stringNotRequired,
|
||||
uniIdToken: stringNotRequired,
|
||||
deviceBrand: stringNotRequired,
|
||||
deviceModel: stringNotRequired,
|
||||
osVersion: stringNotRequired,
|
||||
osLanguage: stringNotRequired,
|
||||
osTheme: stringNotRequired,
|
||||
romName: stringNotRequired,
|
||||
romVersion: stringNotRequired,
|
||||
devicePixelRatio: numberNotRequired,
|
||||
windowWidth: numberNotRequired,
|
||||
windowHeight: numberNotRequired,
|
||||
screenWidth: numberNotRequired,
|
||||
screenHeight: numberNotRequired
|
||||
}
|
||||
const validateRes = new Validator().validate(clientInfo, schema)
|
||||
if (validateRes) {
|
||||
if (validateRes.errCode === ERROR.PARAM_REQUIRED) {
|
||||
console.warn('- 如果使用HBuilderX运行本地云函数/云对象功能时出现此提示,请改为使用客户端调用本地云函数方式调试,或更新HBuilderX到3.4.12及以上版本。\n- 如果是缺少clientInfo.appId,请检查项目manifest.json内是否配置了DCloud AppId')
|
||||
throw new Error(`"clientInfo.${validateRes.schemaKey}" is required.`)
|
||||
} else {
|
||||
throw new Error(`Invalid client info: clienInfo.${validateRes.schemaKey}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Validator,
|
||||
checkClientInfo
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 各接口权限配置,未配置接口表示允许任何用户访问(包括未登录用户)
|
||||
module.exports = {
|
||||
// 管理接口
|
||||
addUser: {
|
||||
// auth: true // 已登录用户方可操作,配置角色或权限时此项可不写
|
||||
role: ['admin'] // 允许进行此操作的角色,包含任一角色均可操作。
|
||||
// permission: [] // 允许进行此操作的权限,包含任一权限均可操作。
|
||||
// 权限角色均配置时,用户拥有任一权限或任一角色均可操作
|
||||
},
|
||||
updateUser: {
|
||||
role: ['admin']
|
||||
},
|
||||
authorizeAppLogin: {
|
||||
role: ['admin']
|
||||
},
|
||||
removeAuthorizedApp: {
|
||||
role: ['admin']
|
||||
},
|
||||
setAuthorizedApp: {
|
||||
role: ['admin']
|
||||
},
|
||||
|
||||
// 用户接口
|
||||
closeAccount: {
|
||||
auth: true
|
||||
},
|
||||
updatePwd: {
|
||||
auth: true
|
||||
},
|
||||
logout: {
|
||||
auth: true
|
||||
},
|
||||
bindMobileBySms: {
|
||||
auth: true
|
||||
},
|
||||
bindMobileByUniverify: {
|
||||
auth: true
|
||||
},
|
||||
bindMobileByMpWeixin: {
|
||||
auth: true
|
||||
},
|
||||
bindAlipay: {
|
||||
auth: true
|
||||
},
|
||||
bindApple: {
|
||||
auth: true
|
||||
},
|
||||
bindQQ: {
|
||||
auth: true
|
||||
},
|
||||
bindWeixin: {
|
||||
auth: true
|
||||
},
|
||||
acceptInvite: {
|
||||
auth: true
|
||||
},
|
||||
getInvitedUser: {
|
||||
auth: true
|
||||
},
|
||||
setPushCid: {
|
||||
auth: true
|
||||
},
|
||||
getAccountInfo: {
|
||||
auth: true
|
||||
},
|
||||
unbindWeixin: {
|
||||
auth: true
|
||||
},
|
||||
unbindAlipay: {
|
||||
auth: true
|
||||
},
|
||||
unbindQQ: {
|
||||
auth: true
|
||||
},
|
||||
unbindApple: {
|
||||
auth: true
|
||||
},
|
||||
setPwd: {
|
||||
auth: true
|
||||
},
|
||||
getFrvCertifyId: {
|
||||
auth: true
|
||||
},
|
||||
getFrvAuthResult: {
|
||||
auth: true
|
||||
},
|
||||
getRealNameInfo: {
|
||||
auth: true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
const uniIdCommon = require('uni-id-common')
|
||||
const uniCaptcha = require('uni-captcha')
|
||||
const {
|
||||
getType,
|
||||
checkIdCard
|
||||
} = require('./common/utils')
|
||||
const {
|
||||
checkClientInfo,
|
||||
Validator
|
||||
} = require('./common/validator')
|
||||
const ConfigUtils = require('./lib/utils/config')
|
||||
const {
|
||||
isUniIdError,
|
||||
ERROR
|
||||
} = require('./common/error')
|
||||
const middleware = require('./middleware/index')
|
||||
const universal = require('./common/universal')
|
||||
|
||||
const {
|
||||
registerAdmin,
|
||||
registerUser,
|
||||
registerUserByEmail
|
||||
} = require('./module/register/index')
|
||||
const {
|
||||
addUser,
|
||||
updateUser
|
||||
} = require('./module/admin/index')
|
||||
const {
|
||||
login,
|
||||
loginBySms,
|
||||
loginByUniverify,
|
||||
loginByWeixin,
|
||||
loginByAlipay,
|
||||
loginByQQ,
|
||||
loginByApple,
|
||||
loginByWeixinMobile
|
||||
} = require('./module/login/index')
|
||||
const {
|
||||
logout
|
||||
} = require('./module/logout/index')
|
||||
const {
|
||||
bindMobileBySms,
|
||||
bindMobileByUniverify,
|
||||
bindMobileByMpWeixin,
|
||||
bindAlipay,
|
||||
bindApple,
|
||||
bindQQ,
|
||||
bindWeixin,
|
||||
unbindWeixin,
|
||||
unbindAlipay,
|
||||
unbindQQ,
|
||||
unbindApple
|
||||
} = require('./module/relate/index')
|
||||
const {
|
||||
setPwd,
|
||||
updatePwd,
|
||||
resetPwdBySms,
|
||||
resetPwdByEmail,
|
||||
closeAccount,
|
||||
getAccountInfo,
|
||||
getRealNameInfo
|
||||
} = require('./module/account/index')
|
||||
const {
|
||||
createCaptcha,
|
||||
refreshCaptcha,
|
||||
sendSmsCode,
|
||||
sendEmailCode
|
||||
} = require('./module/verify/index')
|
||||
const {
|
||||
refreshToken,
|
||||
setPushCid,
|
||||
secureNetworkHandshakeByWeixin
|
||||
} = require('./module/utils/index')
|
||||
const {
|
||||
getInvitedUser,
|
||||
acceptInvite
|
||||
} = require('./module/fission')
|
||||
const {
|
||||
authorizeAppLogin,
|
||||
removeAuthorizedApp,
|
||||
setAuthorizedApp
|
||||
} = require('./module/multi-end')
|
||||
const {
|
||||
getSupportedLoginType
|
||||
} = require('./module/dev/index')
|
||||
const {
|
||||
externalRegister,
|
||||
externalLogin,
|
||||
updateUserInfoByExternal
|
||||
} = require('./module/external')
|
||||
const {
|
||||
getFrvCertifyId,
|
||||
getFrvAuthResult
|
||||
} = require('./module/facial-recognition-verify')
|
||||
|
||||
module.exports = {
|
||||
async _before () {
|
||||
// 支持 callFunction 与 URL化
|
||||
universal.call(this)
|
||||
|
||||
const clientInfo = this.getUniversalClientInfo()
|
||||
/**
|
||||
* 检查clientInfo,无appId和uniPlatform时本云对象无法正常运行
|
||||
* 此外需要保证用到的clientInfo字段均经过类型检查
|
||||
* clientInfo由客户端上传并非完全可信,clientInfo内除clientIP、userAgent、source外均为客户端上传参数
|
||||
* 否则可能会出现一些意料外的情况
|
||||
*/
|
||||
checkClientInfo(clientInfo)
|
||||
let clientPlatform = clientInfo.uniPlatform
|
||||
// 统一platform名称
|
||||
switch (clientPlatform) {
|
||||
case 'app':
|
||||
case 'app-plus':
|
||||
clientPlatform = 'app'
|
||||
break
|
||||
case 'web':
|
||||
case 'h5':
|
||||
clientPlatform = 'web'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
this.clientPlatform = clientPlatform
|
||||
|
||||
// 挂载uni-id实例到this上,方便后续调用
|
||||
this.uniIdCommon = uniIdCommon.createInstance({
|
||||
clientInfo
|
||||
})
|
||||
|
||||
// 包含uni-id配置合并等功能的工具集
|
||||
this.configUtils = new ConfigUtils({
|
||||
context: this
|
||||
})
|
||||
this.config = this.configUtils.getPlatformConfig()
|
||||
this.hooks = this.configUtils.getHooks()
|
||||
|
||||
this.validator = new Validator({
|
||||
passwordStrength: this.config.passwordStrength
|
||||
})
|
||||
|
||||
// 扩展 validator 增加 验证身份证号码合法性
|
||||
this.validator.mixin('idCard', function (idCard) {
|
||||
if (!checkIdCard(idCard)) {
|
||||
return {
|
||||
errCode: ERROR.INVALID_ID_CARD
|
||||
}
|
||||
}
|
||||
})
|
||||
this.validator.mixin('realName', function (realName) {
|
||||
if (
|
||||
typeof realName !== 'string' ||
|
||||
realName.length < 2 ||
|
||||
!/^[\u4e00-\u9fa5]{1,10}(·?[\u4e00-\u9fa5]{1,10}){0,5}$/.test(realName)
|
||||
) {
|
||||
return {
|
||||
errCode: ERROR.INVALID_REAL_NAME
|
||||
}
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 示例:覆盖密码验证规则
|
||||
*/
|
||||
// this.validator.mixin('password', function (password) {
|
||||
// if (typeof password !== 'string' || password.length < 10) {
|
||||
// // 调整为密码长度不能小于10
|
||||
// return {
|
||||
// errCode: ERROR.INVALID_PASSWORD
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
/**
|
||||
* 示例:新增验证规则
|
||||
*/
|
||||
// this.validator.mixin('timestamp', function (timestamp) {
|
||||
// if (typeof timestamp !== 'number' || timestamp > Date.now()) {
|
||||
// return {
|
||||
// errCode: ERROR.INVALID_PARAM
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// // 新增规则同样可以在数组验证规则中使用
|
||||
// this.validator.valdate({
|
||||
// timestamp: 123456789
|
||||
// }, {
|
||||
// timestamp: 'timestamp'
|
||||
// })
|
||||
// this.validator.valdate({
|
||||
// timestampList: [123456789, 123123123123]
|
||||
// }, {
|
||||
// timestampList: 'array<timestamp>'
|
||||
// })
|
||||
// // 甚至更复杂的写法
|
||||
// this.validator.valdate({
|
||||
// timestamp: [123456789, 123123123123]
|
||||
// }, {
|
||||
// timestamp: 'timestamp|array<timestamp>'
|
||||
// })
|
||||
|
||||
// 挂载uni-captcha到this上,方便后续调用
|
||||
this.uniCaptcha = uniCaptcha
|
||||
Object.defineProperty(this, 'uniOpenBridge', {
|
||||
get () {
|
||||
return require('uni-open-bridge-common')
|
||||
}
|
||||
})
|
||||
|
||||
// 挂载中间件
|
||||
this.middleware = {}
|
||||
for (const mwName in middleware) {
|
||||
this.middleware[mwName] = middleware[mwName].bind(this)
|
||||
}
|
||||
|
||||
// 国际化
|
||||
const messages = require('./lang/index')
|
||||
const fallbackLocale = 'zh-Hans'
|
||||
const i18n = uniCloud.initI18n({
|
||||
locale: clientInfo.locale,
|
||||
fallbackLocale,
|
||||
messages: JSON.parse(JSON.stringify(messages))
|
||||
})
|
||||
if (!messages[i18n.locale]) {
|
||||
i18n.setLocale(fallbackLocale)
|
||||
}
|
||||
this.t = i18n.t.bind(i18n)
|
||||
|
||||
this.response = {}
|
||||
|
||||
// 请求鉴权验证
|
||||
await this.middleware.verifyRequestSign()
|
||||
|
||||
// 通用权限校验模块
|
||||
await this.middleware.accessControl()
|
||||
},
|
||||
_after (error, result) {
|
||||
if (error) {
|
||||
// 处理中间件内抛出的标准响应对象
|
||||
if (error.errCode && getType(error) === 'object') {
|
||||
const errCode = error.errCode
|
||||
if (!isUniIdError(errCode)) {
|
||||
return error
|
||||
}
|
||||
return {
|
||||
errCode,
|
||||
errMsg: error.errMsg || this.t(errCode, error.errMsgValue)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return Object.assign(this.response, result)
|
||||
},
|
||||
/**
|
||||
* 注册管理员
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-admin
|
||||
* @param {Object} params
|
||||
* @param {String} params.username 用户名
|
||||
* @param {String} params.password 密码
|
||||
* @param {String} params.nickname 昵称
|
||||
* @returns
|
||||
*/
|
||||
registerAdmin,
|
||||
/**
|
||||
* 新增用户
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user
|
||||
* @param {Object} params
|
||||
* @param {String} params.username 用户名
|
||||
* @param {String} params.password 密码
|
||||
* @param {String} params.nickname 昵称
|
||||
* @param {Array} params.authorizedApp 允许登录的AppID列表
|
||||
* @param {Array} params.role 用户角色列表
|
||||
* @param {String} params.mobile 手机号
|
||||
* @param {String} params.email 邮箱
|
||||
* @param {Array} params.tags 用户标签
|
||||
* @param {Number} params.status 用户状态
|
||||
* @returns
|
||||
*/
|
||||
addUser,
|
||||
/**
|
||||
* 修改用户
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user
|
||||
* @param {Object} params
|
||||
* @param {String} params.id 要更新的用户id
|
||||
* @param {String} params.username 用户名
|
||||
* @param {String} params.password 密码
|
||||
* @param {String} params.nickname 昵称
|
||||
* @param {Array} params.authorizedApp 允许登录的AppID列表
|
||||
* @param {Array} params.role 用户角色列表
|
||||
* @param {String} params.mobile 手机号
|
||||
* @param {String} params.email 邮箱
|
||||
* @param {Array} params.tags 用户标签
|
||||
* @param {Number} params.status 用户状态
|
||||
* @returns
|
||||
*/
|
||||
updateUser,
|
||||
/**
|
||||
* 授权用户登录应用
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#authorize-app-login
|
||||
* @param {Object} params
|
||||
* @param {String} params.uid 用户id
|
||||
* @param {String} params.appId 授权的应用的AppId
|
||||
* @returns
|
||||
*/
|
||||
authorizeAppLogin,
|
||||
/**
|
||||
* 移除用户登录授权
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#remove-authorized-app
|
||||
* @param {Object} params
|
||||
* @param {String} params.uid 用户id
|
||||
* @param {String} params.appId 取消授权的应用的AppId
|
||||
* @returns
|
||||
*/
|
||||
removeAuthorizedApp,
|
||||
/**
|
||||
* 设置用户允许登录的应用列表
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-authorized-app
|
||||
* @param {Object} params
|
||||
* @param {String} params.uid 用户id
|
||||
* @param {Array} params.appIdList 允许登录的应用AppId列表
|
||||
* @returns
|
||||
*/
|
||||
setAuthorizedApp,
|
||||
/**
|
||||
* 注册普通用户
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-user
|
||||
* @param {Object} params
|
||||
* @param {String} params.username 用户名
|
||||
* @param {String} params.password 密码
|
||||
* @param {String} params.captcha 图形验证码
|
||||
* @param {String} params.nickname 昵称
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @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
|
||||
* @param {Object} params
|
||||
* @param {String} params.username 用户名
|
||||
* @param {String} params.mobile 手机号
|
||||
* @param {String} params.email 邮箱
|
||||
* @param {String} params.password 密码
|
||||
* @param {String} params.captcha 图形验证码
|
||||
* @returns
|
||||
*/
|
||||
login,
|
||||
/**
|
||||
* 短信验证码登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-sms
|
||||
* @param {Object} params
|
||||
* @param {String} params.mobile 手机号
|
||||
* @param {String} params.code 短信验证码
|
||||
* @param {String} params.captcha 图形验证码
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @returns
|
||||
*/
|
||||
loginBySms,
|
||||
/**
|
||||
* App端一键登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify
|
||||
* @param {Object} params
|
||||
* @param {String} params.access_token APP端一键登录返回的access_token
|
||||
* @param {String} params.openid APP端一键登录返回的openid
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @returns
|
||||
*/
|
||||
loginByUniverify,
|
||||
/**
|
||||
* 微信登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin
|
||||
* @param {Object} params
|
||||
* @param {String} params.code 微信登录返回的code
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @returns
|
||||
*/
|
||||
loginByWeixin,
|
||||
/**
|
||||
* 支付宝登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-alipay
|
||||
* @param {Object} params
|
||||
* @param {String} params.code 支付宝小程序客户端登录返回的code
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @returns
|
||||
*/
|
||||
loginByAlipay,
|
||||
/**
|
||||
* QQ登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq
|
||||
* @param {Object} params
|
||||
* @param {String} params.code QQ小程序登录返回的code参数
|
||||
* @param {String} params.accessToken App端QQ登录返回的accessToken参数
|
||||
* @param {String} params.accessTokenExpired accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @returns
|
||||
*/
|
||||
loginByQQ,
|
||||
/**
|
||||
* 苹果登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-apple
|
||||
* @param {Object} params
|
||||
* @param {String} params.identityToken 苹果登录返回的identityToken
|
||||
* @param {String} params.nickname 用户昵称
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @returns
|
||||
*/
|
||||
loginByApple,
|
||||
loginByWeixinMobile,
|
||||
/**
|
||||
* 用户退出登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#logout
|
||||
* @returns
|
||||
*/
|
||||
logout,
|
||||
/**
|
||||
* 通过短信验证码绑定手机号
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-sms
|
||||
* @param {Object} params
|
||||
* @param {String} params.mobile 手机号
|
||||
* @param {String} params.code 短信验证码
|
||||
* @param {String} params.captcha 图形验证码
|
||||
* @returns
|
||||
*/
|
||||
bindMobileBySms,
|
||||
/**
|
||||
* 通过一键登录绑定手机号
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-univerify
|
||||
* @param {Object} params
|
||||
* @param {String} params.openid APP端一键登录返回的openid
|
||||
* @param {String} params.access_token APP端一键登录返回的access_token
|
||||
* @returns
|
||||
*/
|
||||
bindMobileByUniverify,
|
||||
/**
|
||||
* 通过微信绑定手机号
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin
|
||||
* @param {Object} params
|
||||
* @param {String} params.encryptedData 微信获取手机号返回的加密信息
|
||||
* @param {String} params.iv 微信获取手机号返回的初始向量
|
||||
* @returns
|
||||
*/
|
||||
bindMobileByMpWeixin,
|
||||
/**
|
||||
* 绑定微信
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-weixin
|
||||
* @param {Object} params
|
||||
* @param {String} params.code 微信登录返回的code
|
||||
* @returns
|
||||
*/
|
||||
bindWeixin,
|
||||
/**
|
||||
* 绑定QQ
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-qq
|
||||
* @param {Object} params
|
||||
* @param {String} params.code 小程序端QQ登录返回的code
|
||||
* @param {String} params.accessToken APP端QQ登录返回的accessToken
|
||||
* @param {String} params.accessTokenExpired accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒
|
||||
* @returns
|
||||
*/
|
||||
bindQQ,
|
||||
/**
|
||||
* 绑定支付宝账号
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-alipay
|
||||
* @param {Object} params
|
||||
* @param {String} params.code 支付宝小程序登录返回的code参数
|
||||
* @returns
|
||||
*/
|
||||
bindAlipay,
|
||||
/**
|
||||
* 绑定苹果账号
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-apple
|
||||
* @param {Object} params
|
||||
* @param {String} params.identityToken 苹果登录返回identityToken
|
||||
* @returns
|
||||
*/
|
||||
bindApple,
|
||||
/**
|
||||
* 更新密码
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-pwd
|
||||
* @param {object} params
|
||||
* @param {string} params.oldPassword 旧密码
|
||||
* @param {string} params.newPassword 新密码
|
||||
* @returns {object}
|
||||
*/
|
||||
updatePwd,
|
||||
/**
|
||||
* 通过短信验证码重置密码
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-sms
|
||||
* @param {object} params
|
||||
* @param {string} params.mobile 手机号
|
||||
* @param {string} params.mobile 短信验证码
|
||||
* @param {string} params.password 密码
|
||||
* @param {string} params.captcha 图形验证码
|
||||
* @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
|
||||
* @returns
|
||||
*/
|
||||
closeAccount,
|
||||
/**
|
||||
* 获取账户账户简略信息
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-account-info
|
||||
*/
|
||||
getAccountInfo,
|
||||
/**
|
||||
* 创建图形验证码
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#create-captcha
|
||||
* @param {Object} params
|
||||
* @param {String} params.scene 图形验证码使用场景
|
||||
* @returns
|
||||
*/
|
||||
createCaptcha,
|
||||
/**
|
||||
* 刷新图形验证码
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-captcha
|
||||
* @param {Object} params
|
||||
* @param {String} params.scene 图形验证码使用场景
|
||||
* @returns
|
||||
*/
|
||||
refreshCaptcha,
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#send-sms-code
|
||||
* @param {Object} params
|
||||
* @param {String} params.mobile 手机号
|
||||
* @param {String} params.captcha 图形验证码
|
||||
* @param {String} params.scene 短信验证码使用场景
|
||||
* @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
|
||||
*/
|
||||
refreshToken,
|
||||
/**
|
||||
* 接受邀请
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#accept-invite
|
||||
* @param {Object} params
|
||||
* @param {String} params.inviteCode 邀请码
|
||||
* @returns
|
||||
*/
|
||||
acceptInvite,
|
||||
/**
|
||||
* 获取受邀用户
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-invited-user
|
||||
* @param {Object} params
|
||||
* @param {Number} params.level 获取受邀用户的级数,1表示直接邀请的用户
|
||||
* @param {Number} params.limit 返回数据大小
|
||||
* @param {Number} params.offset 返回数据偏移
|
||||
* @param {Boolean} params.needTotal 是否需要返回总数
|
||||
* @returns
|
||||
*/
|
||||
getInvitedUser,
|
||||
/**
|
||||
* 更新device表的push_clien_id
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-push-cid
|
||||
* @param {object} params
|
||||
* @param {string} params.pushClientId 客户端pushClientId
|
||||
* @returns
|
||||
*/
|
||||
setPushCid,
|
||||
/**
|
||||
* 获取支持的登录方式
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-supported-login-type
|
||||
* @returns
|
||||
*/
|
||||
getSupportedLoginType,
|
||||
|
||||
/**
|
||||
* 解绑微信
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-weixin
|
||||
* @returns
|
||||
*/
|
||||
unbindWeixin,
|
||||
/**
|
||||
* 解绑支付宝
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-alipay
|
||||
* @returns
|
||||
*/
|
||||
unbindAlipay,
|
||||
/**
|
||||
* 解绑QQ
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-qq
|
||||
* @returns
|
||||
*/
|
||||
unbindQQ,
|
||||
/**
|
||||
* 解绑Apple
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-apple
|
||||
* @returns
|
||||
*/
|
||||
unbindApple,
|
||||
/**
|
||||
* 安全网络握手,目前仅处理微信小程序安全网络握手
|
||||
*/
|
||||
secureNetworkHandshakeByWeixin,
|
||||
/**
|
||||
* 设置密码
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd
|
||||
* @returns
|
||||
*/
|
||||
setPwd,
|
||||
/**
|
||||
* 外部注册用户
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-register
|
||||
* @param {object} params
|
||||
* @param {string} params.externalUid 业务系统的用户id
|
||||
* @param {string} params.nickname 昵称
|
||||
* @param {string} params.gender 性别
|
||||
* @param {string} params.avatar 头像
|
||||
* @returns {object}
|
||||
*/
|
||||
externalRegister,
|
||||
/**
|
||||
* 外部用户登录
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-login
|
||||
* @param {object} params
|
||||
* @param {string} params.userId uni-id体系用户id
|
||||
* @param {string} params.externalUid 业务系统的用户id
|
||||
* @returns {object}
|
||||
*/
|
||||
externalLogin,
|
||||
/**
|
||||
* 使用 userId 或 externalUid 获取用户信息
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo
|
||||
* @param {object} params
|
||||
* @param {string} params.userId uni-id体系的用户id
|
||||
* @param {string} params.externalUid 业务系统的用户id
|
||||
* @param {string} params.nickname 昵称
|
||||
* @param {string} params.gender 性别
|
||||
* @param {string} params.avatar 头像
|
||||
* @returns {object}
|
||||
*/
|
||||
updateUserInfoByExternal,
|
||||
/**
|
||||
* 获取认证ID
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-certify-id
|
||||
* @param {Object} params
|
||||
* @param {String} params.realName 真实姓名
|
||||
* @param {String} params.idCard 身份证号码
|
||||
* @returns
|
||||
*/
|
||||
getFrvCertifyId,
|
||||
/**
|
||||
* 查询认证结果
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-auth-result
|
||||
* @param {Object} params
|
||||
* @param {String} params.certifyId 认证ID
|
||||
* @param {String} params.needAlivePhoto 是否获取认证照片,Y_O (原始图片)、Y_M(虚化,背景马赛克)、N(不返图)
|
||||
* @returns
|
||||
*/
|
||||
getFrvAuthResult,
|
||||
/**
|
||||
* 获取实名信息
|
||||
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-realname-info
|
||||
* @param {Object} params
|
||||
* @param {Boolean} params.decryptData 是否解密数据
|
||||
* @returns
|
||||
*/
|
||||
getRealNameInfo
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
const word = {
|
||||
login: 'login',
|
||||
'verify-mobile': 'verify phone number'
|
||||
}
|
||||
|
||||
const sentence = {
|
||||
'uni-id-account-exists': 'Account exists',
|
||||
'uni-id-account-not-exists': 'Account does not exists',
|
||||
'uni-id-account-not-exists-in-current-app': 'Account does not exists in current app',
|
||||
'uni-id-account-conflict': 'User account conflict',
|
||||
'uni-id-account-banned': 'Account has been banned',
|
||||
'uni-id-account-auditing': 'Account audit in progress',
|
||||
'uni-id-account-audit-failed': 'Account audit failed',
|
||||
'uni-id-account-closed': 'Account has been closed',
|
||||
'uni-id-captcha-required': 'Captcha required',
|
||||
'uni-id-password-error': 'Password error',
|
||||
'uni-id-password-error-exceed-limit': 'The number of password errors is excessive',
|
||||
'uni-id-invalid-username': 'Invalid username',
|
||||
'uni-id-invalid-password': 'invalid password',
|
||||
'uni-id-invalid-password-super': 'Passwords must have 8-16 characters and contain uppercase letters, lowercase letters, numbers, and symbols.',
|
||||
'uni-id-invalid-password-strong': 'Passwords must have 8-16 characters and contain letters, numbers and symbols.',
|
||||
'uni-id-invalid-password-medium': 'Passwords must have 8-16 characters and contain at least two of the following: letters, numbers, and symbols.',
|
||||
'uni-id-invalid-password-weak': 'Passwords must have 6-16 characters and contain letters and numbers.',
|
||||
'uni-id-invalid-mobile': 'Invalid mobile phone number',
|
||||
'uni-id-invalid-email': 'Invalid email address',
|
||||
'uni-id-invalid-nickname': 'Invalid nickname',
|
||||
'uni-id-invalid-param': 'Invalid parameter',
|
||||
'uni-id-param-required': 'Parameter required: {param}',
|
||||
'uni-id-get-third-party-account-failed': 'Get third party account failed',
|
||||
'uni-id-get-third-party-user-info-failed': 'Get third party user info failed',
|
||||
'uni-id-mobile-verify-code-error': 'Verify code error or expired',
|
||||
'uni-id-email-verify-code-error': 'Verify code error or expired',
|
||||
'uni-id-admin-exists': 'Administrator exists',
|
||||
'uni-id-permission-error': 'Permission denied',
|
||||
'uni-id-system-error': 'System error',
|
||||
'uni-id-set-invite-code-failed': 'Set invite code failed',
|
||||
'uni-id-invalid-invite-code': 'Invalid invite code',
|
||||
'uni-id-change-inviter-forbidden': 'Change inviter is not allowed',
|
||||
'uni-id-bind-conflict': 'This account has been bound',
|
||||
'uni-id-admin-exist-in-other-apps': 'Administrator is registered in other consoles',
|
||||
'uni-id-unbind-failed': 'Please bind first and then unbind',
|
||||
'uni-id-unbind-not-supported': 'Unbinding is not supported',
|
||||
'uni-id-unbind-mobile-not-exists': 'This is the only way to login at the moment, please bind your phone number and then try to unbind',
|
||||
'uni-id-unbind-password-not-exists': 'Please set a password first',
|
||||
'uni-id-unsupported-request': 'Unsupported request',
|
||||
'uni-id-illegal-request': 'Illegal request',
|
||||
'uni-id-config-field-required': 'Config field required: {field}',
|
||||
'uni-id-config-field-invalid': 'Config field: {field} is invalid',
|
||||
'uni-id-frv-fail': 'Real name certify failed',
|
||||
'uni-id-frv-processing': 'Waiting for face recognition',
|
||||
'uni-id-realname-verified': 'This account has been verified',
|
||||
'uni-id-idcard-exists': 'The ID number has been bound to the account',
|
||||
'uni-id-invalid-idcard': 'ID number is invalid',
|
||||
'uni-id-invalid-realname': 'The name can only be Chinese characters',
|
||||
'uni-id-unknown-error': 'unknown error',
|
||||
'uni-id-realname-verify-upper-limit': 'The number of real-name certify on the day has reached the upper limit'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
...word,
|
||||
...sentence
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
let lang = {
|
||||
'zh-Hans': require('./zh-hans'),
|
||||
en: require('./en')
|
||||
}
|
||||
|
||||
function mergeLanguage (lang1, lang2) {
|
||||
const localeList = Object.keys(lang1)
|
||||
localeList.push(...Object.keys(lang2))
|
||||
const result = {}
|
||||
for (let i = 0; i < localeList.length; i++) {
|
||||
const locale = localeList[i]
|
||||
result[locale] = Object.assign({}, lang1[locale], lang2[locale])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
const langPath = require.resolve('uni-config-center/uni-id/lang/index.js')
|
||||
lang = mergeLanguage(lang, require(langPath))
|
||||
} catch (error) { }
|
||||
|
||||
module.exports = lang
|
||||
@@ -0,0 +1,64 @@
|
||||
const word = {
|
||||
login: '登录',
|
||||
'verify-mobile': '验证手机号'
|
||||
}
|
||||
|
||||
const sentence = {
|
||||
'uni-id-token-expired': '登录状态失效,token已过期',
|
||||
'uni-id-check-token-failed': 'token校验未通过',
|
||||
'uni-id-account-exists': '此账号已注册',
|
||||
'uni-id-account-not-exists': '此账号未注册',
|
||||
'uni-id-account-not-exists-in-current-app': '此账号未在该应用注册',
|
||||
'uni-id-account-conflict': '用户账号冲突',
|
||||
'uni-id-account-banned': '此账号已封禁',
|
||||
'uni-id-account-auditing': '此账号正在审核中',
|
||||
'uni-id-account-audit-failed': '此账号审核失败',
|
||||
'uni-id-account-closed': '此账号已注销',
|
||||
'uni-id-captcha-required': '请输入图形验证码',
|
||||
'uni-id-password-error': '密码错误',
|
||||
'uni-id-password-error-exceed-limit': '密码错误次数过多,请稍后再试',
|
||||
'uni-id-invalid-username': '用户名不合法',
|
||||
'uni-id-invalid-password': '密码不合法',
|
||||
'uni-id-invalid-password-super': '密码必须包含大小写字母、数字和特殊符号,长度8-16位',
|
||||
'uni-id-invalid-password-strong': '密码必须包含字母、数字和特殊符号,长度8-16位不合法',
|
||||
'uni-id-invalid-password-medium': '密码必须为字母、数字和特殊符号任意两种的组合,长度8-16位',
|
||||
'uni-id-invalid-password-weak': '密码必须包含字母和数字,长度6-16位',
|
||||
'uni-id-invalid-mobile': '手机号码不合法',
|
||||
'uni-id-invalid-email': '邮箱不合法',
|
||||
'uni-id-invalid-nickname': '昵称不合法',
|
||||
'uni-id-invalid-param': '参数错误',
|
||||
'uni-id-param-required': '缺少参数: {param}',
|
||||
'uni-id-get-third-party-account-failed': '获取第三方账号失败',
|
||||
'uni-id-get-third-party-user-info-failed': '获取用户信息失败',
|
||||
'uni-id-mobile-verify-code-error': '手机验证码错误或已过期',
|
||||
'uni-id-email-verify-code-error': '邮箱验证码错误或已过期',
|
||||
'uni-id-admin-exists': '超级管理员已存在',
|
||||
'uni-id-permission-error': '权限错误',
|
||||
'uni-id-system-error': '系统错误',
|
||||
'uni-id-set-invite-code-failed': '设置邀请码失败',
|
||||
'uni-id-invalid-invite-code': '邀请码不可用',
|
||||
'uni-id-change-inviter-forbidden': '禁止修改邀请人',
|
||||
'uni-id-bind-conflict': '此账号已被绑定',
|
||||
'uni-id-admin-exist-in-other-apps': '超级管理员已在其他控制台注册',
|
||||
'uni-id-unbind-failed': '请先绑定后再解绑',
|
||||
'uni-id-unbind-not-supported': '不支持解绑',
|
||||
'uni-id-unbind-mobile-not-exists': '这是当前唯一登录方式,请绑定手机号后再尝试解绑',
|
||||
'uni-id-unbind-password-not-exists': '请先设置密码在尝试解绑',
|
||||
'uni-id-unsupported-request': '不支持的请求方式',
|
||||
'uni-id-illegal-request': '非法请求',
|
||||
'uni-id-frv-fail': '实名认证失败',
|
||||
'uni-id-frv-processing': '等待人脸识别',
|
||||
'uni-id-realname-verified': '该账号已实名认证',
|
||||
'uni-id-idcard-exists': '该证件号码已绑定账号',
|
||||
'uni-id-invalid-idcard': '身份证号码不合法',
|
||||
'uni-id-invalid-realname': '姓名只能是汉字',
|
||||
'uni-id-unknown-error': '未知错误',
|
||||
'uni-id-realname-verify-upper-limit': '当日实名认证次数已达上限',
|
||||
'uni-id-config-field-required': '缺少配置项: {field}',
|
||||
'uni-id-config-field-invalid': '配置项: {field}无效'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
...word,
|
||||
...sentence
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# 说明
|
||||
|
||||
此目录内为uni-id-co基础能力,不建议直接修改。如果你发现有些逻辑加入会更好,或者此部分代码有Bug可以向我们提交PR,仓库地址:[]()。如果有特殊的需求也可以在[论坛](https://ask.dcloud.net.cn/)提出,我们可以讨论下如何实现。
|
||||
@@ -0,0 +1,16 @@
|
||||
const AlipayBase = require('../alipayBase')
|
||||
const protocols = require('./protocols')
|
||||
module.exports = class Auth extends AlipayBase {
|
||||
constructor (options) {
|
||||
super(options)
|
||||
this._protocols = protocols
|
||||
}
|
||||
|
||||
async code2Session (code) {
|
||||
const result = await this._exec('alipay.system.oauth.token', {
|
||||
grantType: 'authorization_code',
|
||||
code
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
code2Session: {
|
||||
// args (fromArgs) {
|
||||
// return fromArgs
|
||||
// },
|
||||
returnValue: {
|
||||
openid: 'userId'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
const {
|
||||
camel2snakeJson,
|
||||
snake2camelJson,
|
||||
getOffsetDate,
|
||||
getFullTimeStr
|
||||
} = require('../../../common/utils')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const ALIPAY_ALGORITHM_MAPPING = {
|
||||
RSA: 'RSA-SHA1',
|
||||
RSA2: 'RSA-SHA256'
|
||||
}
|
||||
|
||||
module.exports = class AlipayBase {
|
||||
constructor (options = {}) {
|
||||
if (!options.appId) throw new Error('appId required')
|
||||
if (!options.privateKey) throw new Error('privateKey required')
|
||||
const defaultOptions = {
|
||||
gateway: 'https://openapi.alipay.com/gateway.do',
|
||||
timeout: 5000,
|
||||
charset: 'utf-8',
|
||||
version: '1.0',
|
||||
signType: 'RSA2',
|
||||
timeOffset: -new Date().getTimezoneOffset() / 60,
|
||||
keyType: 'PKCS8'
|
||||
}
|
||||
|
||||
if (options.sandbox) {
|
||||
options.gateway = 'https://openapi.alipaydev.com/gateway.do'
|
||||
}
|
||||
|
||||
this.options = Object.assign({}, defaultOptions, options)
|
||||
|
||||
const privateKeyType =
|
||||
this.options.keyType === 'PKCS8' ? 'PRIVATE KEY' : 'RSA PRIVATE KEY'
|
||||
|
||||
this.options.privateKey = this._formatKey(
|
||||
this.options.privateKey,
|
||||
privateKeyType
|
||||
)
|
||||
if (this.options.alipayPublicKey) {
|
||||
this.options.alipayPublicKey = this._formatKey(
|
||||
this.options.alipayPublicKey,
|
||||
'PUBLIC KEY'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_formatKey (key, type) {
|
||||
return `-----BEGIN ${type}-----\n${key}\n-----END ${type}-----`
|
||||
}
|
||||
|
||||
_formatUrl (url, params) {
|
||||
let requestUrl = url
|
||||
// 需要放在 url 中的参数列表
|
||||
const urlArgs = [
|
||||
'app_id',
|
||||
'method',
|
||||
'format',
|
||||
'charset',
|
||||
'sign_type',
|
||||
'sign',
|
||||
'timestamp',
|
||||
'version',
|
||||
'notify_url',
|
||||
'return_url',
|
||||
'auth_token',
|
||||
'app_auth_token'
|
||||
]
|
||||
|
||||
for (const key in params) {
|
||||
if (urlArgs.indexOf(key) > -1) {
|
||||
const val = encodeURIComponent(params[key])
|
||||
requestUrl = `${requestUrl}${requestUrl.includes('?') ? '&' : '?'
|
||||
}${key}=${val}`
|
||||
// 删除 postData 中对应的数据
|
||||
delete params[key]
|
||||
}
|
||||
}
|
||||
|
||||
return { execParams: params, url: requestUrl }
|
||||
}
|
||||
|
||||
_getSign (method, params) {
|
||||
const bizContent = params.bizContent || null
|
||||
delete params.bizContent
|
||||
|
||||
const signParams = Object.assign({
|
||||
method,
|
||||
appId: this.options.appId,
|
||||
charset: this.options.charset,
|
||||
version: this.options.version,
|
||||
signType: this.options.signType,
|
||||
timestamp: getFullTimeStr(getOffsetDate(this.options.timeOffset))
|
||||
}, params)
|
||||
|
||||
if (bizContent) {
|
||||
signParams.bizContent = JSON.stringify(camel2snakeJson(bizContent))
|
||||
}
|
||||
|
||||
// params key 驼峰转下划线
|
||||
const decamelizeParams = camel2snakeJson(signParams)
|
||||
|
||||
// 排序
|
||||
const signStr = Object.keys(decamelizeParams)
|
||||
.sort()
|
||||
.map((key) => {
|
||||
let data = decamelizeParams[key]
|
||||
if (Array.prototype.toString.call(data) !== '[object String]') {
|
||||
data = JSON.stringify(data)
|
||||
}
|
||||
return `${key}=${data}`
|
||||
})
|
||||
.join('&')
|
||||
|
||||
// 计算签名
|
||||
const sign = crypto
|
||||
.createSign(ALIPAY_ALGORITHM_MAPPING[this.options.signType])
|
||||
.update(signStr, 'utf8')
|
||||
.sign(this.options.privateKey, 'base64')
|
||||
|
||||
return Object.assign(decamelizeParams, { sign })
|
||||
}
|
||||
|
||||
async _exec (method, params = {}, option = {}) {
|
||||
// 计算签名
|
||||
const signData = this._getSign(method, params)
|
||||
const { url, execParams } = this._formatUrl(this.options.gateway, signData)
|
||||
const { status, data } = await uniCloud.httpclient.request(url, {
|
||||
method: 'POST',
|
||||
data: execParams,
|
||||
// 按 text 返回(为了验签)
|
||||
dataType: 'text',
|
||||
timeout: this.options.timeout
|
||||
})
|
||||
if (status !== 200) throw new Error('request fail')
|
||||
/**
|
||||
* 示例响应格式
|
||||
* {"alipay_trade_precreate_response":
|
||||
* {"code": "10000","msg": "Success","out_trade_no": "111111","qr_code": "https:\/\/"},
|
||||
* "sign": "abcde="
|
||||
* }
|
||||
* 或者
|
||||
* {"error_response":
|
||||
* {"code":"40002","msg":"Invalid Arguments","sub_code":"isv.code-invalid","sub_msg":"授权码code无效"},
|
||||
* }
|
||||
*/
|
||||
const result = JSON.parse(data)
|
||||
const responseKey = `${method.replace(/\./g, '_')}_response`
|
||||
const response = result[responseKey]
|
||||
const errorResponse = result.error_response
|
||||
if (response) {
|
||||
// 按字符串验签
|
||||
const validateSuccess = option.validateSign ? this._checkResponseSign(data, responseKey) : true
|
||||
if (validateSuccess) {
|
||||
if (!response.code || response.code === '10000') {
|
||||
const errCode = 0
|
||||
const errMsg = response.msg || ''
|
||||
return {
|
||||
errCode,
|
||||
errMsg,
|
||||
...snake2camelJson(response)
|
||||
}
|
||||
}
|
||||
const msg = response.sub_code ? `${response.sub_code} ${response.sub_msg}` : `${response.msg || 'unkonwn error'}`
|
||||
throw new Error(msg)
|
||||
} else {
|
||||
throw new Error('check sign error')
|
||||
}
|
||||
} else if (errorResponse) {
|
||||
throw new Error(errorResponse.sub_msg || errorResponse.msg || 'request fail')
|
||||
}
|
||||
|
||||
throw new Error('request fail')
|
||||
}
|
||||
|
||||
_checkResponseSign (signStr, responseKey) {
|
||||
if (!this.options.alipayPublicKey || this.options.alipayPublicKey === '') {
|
||||
console.warn('options.alipayPublicKey is empty')
|
||||
// 支付宝公钥不存在时不做验签
|
||||
return true
|
||||
}
|
||||
|
||||
// 带验签的参数不存在时返回失败
|
||||
if (!signStr) { return false }
|
||||
|
||||
// 根据服务端返回的结果截取需要验签的目标字符串
|
||||
const validateStr = this._getSignStr(signStr, responseKey)
|
||||
// 服务端返回的签名
|
||||
const serverSign = JSON.parse(signStr).sign
|
||||
|
||||
// 参数存在,并且是正常的结果(不包含 sub_code)时才验签
|
||||
const verifier = crypto.createVerify(ALIPAY_ALGORITHM_MAPPING[this.options.signType])
|
||||
verifier.update(validateStr, 'utf8')
|
||||
return verifier.verify(this.options.alipayPublicKey, serverSign, 'base64')
|
||||
}
|
||||
|
||||
_getSignStr (originStr, responseKey) {
|
||||
// 待签名的字符串
|
||||
let validateStr = originStr.trim()
|
||||
// 找到 xxx_response 开始的位置
|
||||
const startIndex = originStr.indexOf(`${responseKey}"`)
|
||||
// 找到最后一个 “"sign"” 字符串的位置(避免)
|
||||
const lastIndex = originStr.lastIndexOf('"sign"')
|
||||
|
||||
/**
|
||||
* 删除 xxx_response 及之前的字符串
|
||||
* 假设原始字符串为
|
||||
* {"xxx_response":{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"}
|
||||
* 删除后变为
|
||||
* :{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"}
|
||||
*/
|
||||
validateStr = validateStr.substr(startIndex + responseKey.length + 1)
|
||||
|
||||
/**
|
||||
* 删除最后一个 "sign" 及之后的字符串
|
||||
* 删除后变为
|
||||
* :{"code":"10000"},
|
||||
* {} 之间就是待验签的字符串
|
||||
*/
|
||||
validateStr = validateStr.substr(0, lastIndex)
|
||||
|
||||
// 删除第一个 { 之前的任何字符
|
||||
validateStr = validateStr.replace(/^[^{]*{/g, '{')
|
||||
|
||||
// 删除最后一个 } 之后的任何字符
|
||||
validateStr = validateStr.replace(/\}([^}]*)$/g, '}')
|
||||
|
||||
return validateStr
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
const rsaPublicKeyPem = require('../rsa-public-key-pem')
|
||||
let authKeysCache = null
|
||||
|
||||
module.exports = class Auth {
|
||||
constructor (options) {
|
||||
this.options = Object.assign({
|
||||
baseUrl: 'https://appleid.apple.com',
|
||||
timeout: 10000
|
||||
}, options)
|
||||
}
|
||||
|
||||
async _fetch (url, options) {
|
||||
const { baseUrl } = this.options
|
||||
return uniCloud.httpclient.request(baseUrl + url, options)
|
||||
}
|
||||
|
||||
async verifyIdentityToken (identityToken) {
|
||||
// 解密出kid,拿取key
|
||||
const jwtHeader = identityToken.split('.')[0]
|
||||
const { kid } = JSON.parse(Buffer.from(jwtHeader, 'base64').toString())
|
||||
let authKeys
|
||||
if (authKeysCache) {
|
||||
authKeys = authKeysCache
|
||||
} else {
|
||||
authKeys = await this.getAuthKeys()
|
||||
authKeysCache = authKeys
|
||||
}
|
||||
const usedKey = authKeys.find(item => item.kid === kid)
|
||||
|
||||
/**
|
||||
* identityToken 格式
|
||||
*
|
||||
* {
|
||||
* iss: 'https://appleid.apple.com',
|
||||
* aud: 'io.dcloud.hellouniapp',
|
||||
* exp: 1610626724,
|
||||
* iat: 1610540324,
|
||||
* sub: '000628.30119d332d9b45a3be4a297f9391fd5c.0403',
|
||||
* c_hash: 'oFfgewoG36cJX00KUbj45A',
|
||||
* email: 'x2awmap99s@privaterelay.appleid.com',
|
||||
* email_verified: 'true',
|
||||
* is_private_email: 'true',
|
||||
* auth_time: 1610540324,
|
||||
* nonce_supported: true
|
||||
* }
|
||||
*/
|
||||
const payload = require('jsonwebtoken').verify(
|
||||
identityToken,
|
||||
rsaPublicKeyPem(usedKey.n, usedKey.e),
|
||||
{
|
||||
algorithms: usedKey.alg
|
||||
}
|
||||
)
|
||||
|
||||
if (payload.iss !== 'https://appleid.apple.com' || payload.aud !== this.options.bundleId) {
|
||||
throw new Error('Invalid identity token')
|
||||
}
|
||||
|
||||
return {
|
||||
openid: payload.sub,
|
||||
email: payload.email,
|
||||
emailVerified: payload.email_verified === 'true',
|
||||
isPrivateEmail: payload.is_private_email === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthKeys () {
|
||||
const { status, data } = await this._fetch('/auth/keys', {
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
timeout: this.options.timeout
|
||||
})
|
||||
if (status !== 200) throw new Error('request https://appleid.apple.com/auth/keys fail')
|
||||
return data.keys
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// http://stackoverflow.com/questions/18835132/xml-to-pem-in-node-js
|
||||
/* eslint-disable camelcase */
|
||||
function rsaPublicKeyPem (modulus_b64, exponent_b64) {
|
||||
const modulus = Buffer.from(modulus_b64, 'base64')
|
||||
const exponent = Buffer.from(exponent_b64, 'base64')
|
||||
|
||||
let modulus_hex = modulus.toString('hex')
|
||||
let exponent_hex = exponent.toString('hex')
|
||||
|
||||
modulus_hex = prepadSigned(modulus_hex)
|
||||
exponent_hex = prepadSigned(exponent_hex)
|
||||
|
||||
const modlen = modulus_hex.length / 2
|
||||
const explen = exponent_hex.length / 2
|
||||
|
||||
const encoded_modlen = encodeLengthHex(modlen)
|
||||
const encoded_explen = encodeLengthHex(explen)
|
||||
const encoded_pubkey = '30' +
|
||||
encodeLengthHex(
|
||||
modlen +
|
||||
explen +
|
||||
encoded_modlen.length / 2 +
|
||||
encoded_explen.length / 2 + 2
|
||||
) +
|
||||
'02' + encoded_modlen + modulus_hex +
|
||||
'02' + encoded_explen + exponent_hex
|
||||
|
||||
const der_b64 = Buffer.from(encoded_pubkey, 'hex').toString('base64')
|
||||
|
||||
const pem = '-----BEGIN RSA PUBLIC KEY-----\n' +
|
||||
der_b64.match(/.{1,64}/g).join('\n') +
|
||||
'\n-----END RSA PUBLIC KEY-----\n'
|
||||
|
||||
return pem
|
||||
}
|
||||
|
||||
function prepadSigned (hexStr) {
|
||||
const msb = hexStr[0]
|
||||
if (msb < '0' || msb > '7') {
|
||||
return '00' + hexStr
|
||||
} else {
|
||||
return hexStr
|
||||
}
|
||||
}
|
||||
|
||||
function toHex (number) {
|
||||
const nstr = number.toString(16)
|
||||
if (nstr.length % 2) return '0' + nstr
|
||||
return nstr
|
||||
}
|
||||
|
||||
// encode ASN.1 DER length field
|
||||
// if <=127, short form
|
||||
// if >=128, long form
|
||||
function encodeLengthHex (n) {
|
||||
if (n <= 127) return toHex(n)
|
||||
else {
|
||||
const n_hex = toHex(n)
|
||||
const length_of_length_byte = 128 + n_hex.length / 2 // 0x80+numbytes
|
||||
return toHex(length_of_length_byte) + n_hex
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = rsaPublicKeyPem
|
||||
36
uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/index.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
const WxAccount = require('./weixin/account/index')
|
||||
const QQAccount = require('./qq/account/index')
|
||||
const AliAccount = require('./alipay/account/index')
|
||||
const AppleAccount = require('./apple/account/index')
|
||||
|
||||
const createApi = require('./share/create-api')
|
||||
|
||||
module.exports = {
|
||||
initWeixin: function () {
|
||||
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'weixin' })
|
||||
return createApi(WxAccount, {
|
||||
appId: oauthConfig.appid,
|
||||
secret: oauthConfig.appsecret
|
||||
})
|
||||
},
|
||||
initQQ: function () {
|
||||
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'qq' })
|
||||
return createApi(QQAccount, {
|
||||
appId: oauthConfig.appid,
|
||||
secret: oauthConfig.appsecret
|
||||
})
|
||||
},
|
||||
initAlipay: function () {
|
||||
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'alipay' })
|
||||
return createApi(AliAccount, {
|
||||
appId: oauthConfig.appid,
|
||||
privateKey: oauthConfig.privateKey
|
||||
})
|
||||
},
|
||||
initApple: function () {
|
||||
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'apple' })
|
||||
return createApi(AppleAccount, {
|
||||
bundleId: oauthConfig.bundleId
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
const {
|
||||
UniCloudError
|
||||
} = require('../../../../common/error')
|
||||
const {
|
||||
resolveUrl
|
||||
} = require('../../../../common/utils')
|
||||
const {
|
||||
callQQOpenApi
|
||||
} = require('../normalize')
|
||||
|
||||
module.exports = class Auth {
|
||||
constructor (options) {
|
||||
this.options = Object.assign({
|
||||
baseUrl: 'https://graph.qq.com',
|
||||
timeout: 5000
|
||||
}, options)
|
||||
}
|
||||
|
||||
async _requestQQOpenapi ({ name, url, data, options }) {
|
||||
const defaultOptions = {
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
dataAsQueryString: true,
|
||||
timeout: this.options.timeout
|
||||
}
|
||||
const result = await callQQOpenApi({
|
||||
name: `auth.${name}`,
|
||||
url: resolveUrl(this.options.baseUrl, url),
|
||||
data,
|
||||
options,
|
||||
defaultOptions
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
async getUserInfo ({
|
||||
accessToken,
|
||||
openid
|
||||
} = {}) {
|
||||
const url = '/user/get_user_info'
|
||||
const result = await this._requestQQOpenapi({
|
||||
name: 'getUserInfo',
|
||||
url,
|
||||
data: {
|
||||
oauthConsumerKey: this.options.appId,
|
||||
accessToken,
|
||||
openid
|
||||
}
|
||||
})
|
||||
return {
|
||||
nickname: result.nickname,
|
||||
avatar: result.figureurl_qq_1
|
||||
}
|
||||
}
|
||||
|
||||
async getOpenidByToken ({
|
||||
accessToken
|
||||
} = {}) {
|
||||
const url = '/oauth2.0/me'
|
||||
const result = await this._requestQQOpenapi({
|
||||
name: 'getOpenidByToken',
|
||||
url,
|
||||
data: {
|
||||
accessToken,
|
||||
unionid: 1,
|
||||
fmt: 'json'
|
||||
}
|
||||
})
|
||||
if (result.clientId !== this.options.appId) {
|
||||
throw new UniCloudError({
|
||||
code: 'APPID_NOT_MATCH',
|
||||
message: 'appid not match'
|
||||
})
|
||||
}
|
||||
return {
|
||||
openid: result.openid,
|
||||
unionid: result.unionid
|
||||
}
|
||||
}
|
||||
|
||||
async code2Session ({
|
||||
code
|
||||
} = {}) {
|
||||
const url = 'https://api.q.qq.com/sns/jscode2session'
|
||||
const result = await this._requestQQOpenapi({
|
||||
name: 'getOpenidByToken',
|
||||
url,
|
||||
data: {
|
||||
grant_type: 'authorization_code',
|
||||
appid: this.options.appId,
|
||||
secret: this.options.secret,
|
||||
js_code: code
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
const {
|
||||
UniCloudError
|
||||
} = require('../../../common/error')
|
||||
const {
|
||||
camel2snakeJson,
|
||||
snake2camelJson
|
||||
} = require('../../../common/utils')
|
||||
|
||||
function generateApiResult (apiName, data) {
|
||||
if (data.ret || data.error) {
|
||||
// 这三种都是qq的错误码规范
|
||||
const code = data.ret || data.error || data.errcode || -2
|
||||
const message = data.msg || data.error_description || data.errmsg || `${apiName} fail`
|
||||
throw new UniCloudError({
|
||||
code,
|
||||
message
|
||||
})
|
||||
} else {
|
||||
delete data.ret
|
||||
delete data.msg
|
||||
delete data.error
|
||||
delete data.error_description
|
||||
delete data.errcode
|
||||
delete data.errmsg
|
||||
return {
|
||||
...data,
|
||||
errMsg: `${apiName} ok`,
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nomalizeError (apiName, error) {
|
||||
throw new UniCloudError({
|
||||
code: error.code || -2,
|
||||
message: error.message || `${apiName} fail`
|
||||
})
|
||||
}
|
||||
|
||||
async function callQQOpenApi ({
|
||||
name,
|
||||
url,
|
||||
data,
|
||||
options,
|
||||
defaultOptions
|
||||
}) {
|
||||
options = Object.assign({}, defaultOptions, options, { data: camel2snakeJson(Object.assign({}, data)) })
|
||||
let result
|
||||
try {
|
||||
result = await uniCloud.httpclient.request(url, options)
|
||||
} catch (e) {
|
||||
return nomalizeError(name, e)
|
||||
}
|
||||
let resData = result.data
|
||||
const contentType = result.headers['content-type']
|
||||
if (
|
||||
Buffer.isBuffer(resData) &&
|
||||
(contentType.indexOf('text/plain') === 0 ||
|
||||
contentType.indexOf('application/json') === 0)
|
||||
) {
|
||||
try {
|
||||
resData = JSON.parse(resData.toString())
|
||||
} catch (e) {
|
||||
resData = resData.toString()
|
||||
}
|
||||
} else if (Buffer.isBuffer(resData)) {
|
||||
resData = {
|
||||
buffer: resData,
|
||||
contentType
|
||||
}
|
||||
}
|
||||
return snake2camelJson(
|
||||
generateApiResult(
|
||||
name,
|
||||
resData || {
|
||||
errCode: -2,
|
||||
errMsg: 'Request failed'
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
callQQOpenApi
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
const {
|
||||
isFn,
|
||||
isPlainObject
|
||||
} = require('../../../common/utils')
|
||||
|
||||
// 注意:不进行递归处理
|
||||
function parseParams (params = {}, rule) {
|
||||
if (!rule || !params) {
|
||||
return params
|
||||
}
|
||||
const internalKeys = ['_pre', '_purify', '_post']
|
||||
// 转换之前的处理
|
||||
if (rule._pre) {
|
||||
params = rule._pre(params)
|
||||
}
|
||||
// 净化参数
|
||||
let purify = { shouldDelete: new Set([]) }
|
||||
if (rule._purify) {
|
||||
const _purify = rule._purify
|
||||
for (const purifyKey in _purify) {
|
||||
_purify[purifyKey] = new Set(_purify[purifyKey])
|
||||
}
|
||||
purify = Object.assign(purify, _purify)
|
||||
}
|
||||
if (isPlainObject(rule)) {
|
||||
for (const key in rule) {
|
||||
const parser = rule[key]
|
||||
if (isFn(parser) && internalKeys.indexOf(key) === -1) {
|
||||
params[key] = parser(params)
|
||||
} else if (typeof parser === 'string' && internalKeys.indexOf(key) === -1) {
|
||||
// 直接转换属性名称的删除旧属性名
|
||||
params[key] = params[parser]
|
||||
purify.shouldDelete.add(parser)
|
||||
}
|
||||
}
|
||||
} else if (isFn(rule)) {
|
||||
params = rule(params)
|
||||
}
|
||||
|
||||
if (purify.shouldDelete) {
|
||||
for (const item of purify.shouldDelete) {
|
||||
delete params[item]
|
||||
}
|
||||
}
|
||||
|
||||
// 转换之后的处理
|
||||
if (rule._post) {
|
||||
params = rule._post(params)
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
function createApi (ApiClass, options) {
|
||||
const apiInstance = new ApiClass(options)
|
||||
return new Proxy(apiInstance, {
|
||||
get: function (obj, prop) {
|
||||
if (typeof obj[prop] === 'function' && prop.indexOf('_') !== 0 && obj._protocols && obj._protocols[prop]) {
|
||||
const protocol = obj._protocols[prop]
|
||||
return async function (params) {
|
||||
params = parseParams(params, protocol.args)
|
||||
let result = await obj[prop](params)
|
||||
result = parseParams(result, protocol.returnValue)
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
return obj[prop]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = createApi
|
||||
@@ -0,0 +1,111 @@
|
||||
const {
|
||||
callWxOpenApi,
|
||||
buildUrl
|
||||
} = require('../normalize')
|
||||
|
||||
module.exports = class Auth {
|
||||
constructor (options) {
|
||||
this.options = Object.assign({
|
||||
baseUrl: 'https://api.weixin.qq.com',
|
||||
timeout: 5000
|
||||
}, options)
|
||||
}
|
||||
|
||||
async _requestWxOpenapi ({ name, url, data, options }) {
|
||||
const defaultOptions = {
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
dataAsQueryString: true,
|
||||
timeout: this.options.timeout
|
||||
}
|
||||
const result = await callWxOpenApi({
|
||||
name: `auth.${name}`,
|
||||
url: `${this.options.baseUrl}${buildUrl(url, data)}`,
|
||||
data,
|
||||
options,
|
||||
defaultOptions
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
async code2Session (code) {
|
||||
const url = '/sns/jscode2session'
|
||||
const result = await this._requestWxOpenapi({
|
||||
name: 'code2Session',
|
||||
url,
|
||||
data: {
|
||||
grant_type: 'authorization_code',
|
||||
appid: this.options.appId,
|
||||
secret: this.options.secret,
|
||||
js_code: code
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
async getOauthAccessToken (code) {
|
||||
const url = '/sns/oauth2/access_token'
|
||||
const result = await this._requestWxOpenapi({
|
||||
name: 'getOauthAccessToken',
|
||||
url,
|
||||
data: {
|
||||
grant_type: 'authorization_code',
|
||||
appid: this.options.appId,
|
||||
secret: this.options.secret,
|
||||
code
|
||||
}
|
||||
})
|
||||
if (result.expiresIn) {
|
||||
result.expired = Date.now() + result.expiresIn * 1000
|
||||
// delete result.expiresIn
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async getUserInfo ({
|
||||
accessToken,
|
||||
openid
|
||||
} = {}) {
|
||||
const url = '/sns/userinfo'
|
||||
const {
|
||||
nickname,
|
||||
headimgurl: avatar
|
||||
} = await this._requestWxOpenapi({
|
||||
name: 'getUserInfo',
|
||||
url,
|
||||
data: {
|
||||
accessToken,
|
||||
openid,
|
||||
appid: this.options.appId,
|
||||
secret: this.options.secret,
|
||||
scope: 'snsapi_userinfo'
|
||||
}
|
||||
})
|
||||
return {
|
||||
nickname,
|
||||
avatar
|
||||
}
|
||||
}
|
||||
|
||||
async getPhoneNumber (accessToken, code) {
|
||||
const url = `/wxa/business/getuserphonenumber?access_token=${accessToken}`
|
||||
const { phoneInfo } = await this._requestWxOpenapi({
|
||||
name: 'getPhoneNumber',
|
||||
url,
|
||||
data: {
|
||||
code
|
||||
},
|
||||
options: {
|
||||
method: 'POST',
|
||||
dataAsQueryString: false,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
purePhoneNumber: phoneInfo.purePhoneNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
const {
|
||||
UniCloudError
|
||||
} = require('../../../common/error')
|
||||
const {
|
||||
camel2snakeJson, snake2camelJson
|
||||
} = require('../../../common/utils')
|
||||
|
||||
function generateApiResult (apiName, data) {
|
||||
if (data.errcode) {
|
||||
throw new UniCloudError({
|
||||
code: data.errcode || -2,
|
||||
message: data.errmsg || `${apiName} fail`
|
||||
})
|
||||
} else {
|
||||
delete data.errcode
|
||||
delete data.errmsg
|
||||
return {
|
||||
...data,
|
||||
errMsg: `${apiName} ok`,
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nomalizeError (apiName, error) {
|
||||
throw new UniCloudError({
|
||||
code: error.code || -2,
|
||||
message: error.message || `${apiName} fail`
|
||||
})
|
||||
}
|
||||
|
||||
// 微信openapi接口接收蛇形(snake case)参数返回蛇形参数,这里进行转化,如果是formdata里面的参数需要在对应api实现时就转为蛇形
|
||||
async function callWxOpenApi ({
|
||||
name,
|
||||
url,
|
||||
data,
|
||||
options,
|
||||
defaultOptions
|
||||
}) {
|
||||
let result = {}
|
||||
// 获取二维码的接口wxacode.get和wxacode.getUnlimited不可以传入access_token(可能有其他接口也不可以),否则会返回data format error
|
||||
const dataCopy = camel2snakeJson(Object.assign({}, data))
|
||||
if (dataCopy && dataCopy.access_token) {
|
||||
delete dataCopy.access_token
|
||||
}
|
||||
try {
|
||||
options = Object.assign({}, defaultOptions, options, { data: dataCopy })
|
||||
result = await uniCloud.httpclient.request(url, options)
|
||||
} catch (e) {
|
||||
return nomalizeError(name, e)
|
||||
}
|
||||
|
||||
// 有几个接口成功返回buffer失败返回json,对这些接口统一成返回buffer,然后分别解析
|
||||
let resData = result.data
|
||||
const contentType = result.headers['content-type']
|
||||
if (
|
||||
Buffer.isBuffer(resData) &&
|
||||
(contentType.indexOf('text/plain') === 0 ||
|
||||
contentType.indexOf('application/json') === 0)
|
||||
) {
|
||||
try {
|
||||
resData = JSON.parse(resData.toString())
|
||||
} catch (e) {
|
||||
resData = resData.toString()
|
||||
}
|
||||
} else if (Buffer.isBuffer(resData)) {
|
||||
resData = {
|
||||
buffer: resData,
|
||||
contentType
|
||||
}
|
||||
}
|
||||
return snake2camelJson(
|
||||
generateApiResult(
|
||||
name,
|
||||
resData || {
|
||||
errCode: -2,
|
||||
errMsg: 'Request failed'
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function buildUrl (url, data) {
|
||||
let query = ''
|
||||
if (data && data.accessToken) {
|
||||
const divider = url.indexOf('?') > -1 ? '&' : '?'
|
||||
query = `${divider}access_token=${data.accessToken}`
|
||||
}
|
||||
return `${url}${query}`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
callWxOpenApi,
|
||||
buildUrl
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
const crypto = require('crypto')
|
||||
const {
|
||||
isPlainObject
|
||||
} = require('../../../common/utils')
|
||||
|
||||
// 退款通知解密key=md5(key)
|
||||
function decryptData (encryptedData, key, iv = '') {
|
||||
// 解密
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
|
||||
// 设置自动 padding 为 true,删除填充补位
|
||||
decipher.setAutoPadding(true)
|
||||
let decoded = decipher.update(encryptedData, 'base64', 'utf8')
|
||||
decoded += decipher.final('utf8')
|
||||
return decoded
|
||||
}
|
||||
|
||||
function md5 (str, encoding = 'utf8') {
|
||||
return crypto
|
||||
.createHash('md5')
|
||||
.update(str, encoding)
|
||||
.digest('hex')
|
||||
}
|
||||
|
||||
function sha256 (str, key, encoding = 'utf8') {
|
||||
return crypto
|
||||
.createHmac('sha256', key)
|
||||
.update(str, encoding)
|
||||
.digest('hex')
|
||||
}
|
||||
|
||||
function getSignStr (obj) {
|
||||
return Object.keys(obj)
|
||||
.filter(key => key !== 'sign' && obj[key] !== undefined && obj[key] !== '')
|
||||
.sort()
|
||||
.map(key => key + '=' + obj[key])
|
||||
.join('&')
|
||||
}
|
||||
|
||||
function getNonceStr (length = 16) {
|
||||
let str = ''
|
||||
while (str.length < length) {
|
||||
str += Math.random().toString(32).substring(2)
|
||||
}
|
||||
return str.substring(0, length)
|
||||
}
|
||||
|
||||
// 简易版Object转XML,只可在微信支付时使用,不支持嵌套
|
||||
function buildXML (obj, rootName = 'xml') {
|
||||
const content = Object.keys(obj).map(item => {
|
||||
if (isPlainObject(obj[item])) {
|
||||
return `<${item}><![CDATA[${JSON.stringify(obj[item])}]]></${item}>`
|
||||
} else {
|
||||
return `<${item}><![CDATA[${obj[item]}]]></${item}>`
|
||||
}
|
||||
})
|
||||
return `<${rootName}>${content.join('')}</${rootName}>`
|
||||
}
|
||||
|
||||
function isXML (str) {
|
||||
const reg = /^(<\?xml.*\?>)?(\r?\n)*<xml>(.|\r?\n)*<\/xml>$/i
|
||||
return reg.test(str.trim())
|
||||
};
|
||||
|
||||
// 简易版XML转Object,只可在微信支付时使用,不支持嵌套
|
||||
function parseXML (xml) {
|
||||
const xmlReg = /<(?:xml|root).*?>([\s|\S]*)<\/(?:xml|root)>/
|
||||
const str = xmlReg.exec(xml)[1]
|
||||
const obj = {}
|
||||
const nodeReg = /<(.*?)>(?:<!\[CDATA\[){0,1}(.*?)(?:\]\]>){0,1}<\/.*?>/g
|
||||
let matches = null
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((matches = nodeReg.exec(str))) {
|
||||
obj[matches[1]] = matches[2]
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
decryptData,
|
||||
md5,
|
||||
sha256,
|
||||
getSignStr,
|
||||
getNonceStr,
|
||||
buildXML,
|
||||
parseXML,
|
||||
isXML
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
const {
|
||||
dbCmd,
|
||||
userCollection
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
USER_IDENTIFIER
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
batchFindObjctValue,
|
||||
getType,
|
||||
isMatchUserApp
|
||||
} = require('../../common/utils')
|
||||
|
||||
/**
|
||||
* 查询满足条件的用户
|
||||
* @param {Object} params
|
||||
* @param {Object} params.userQuery 用户唯一标识组成的查询条件
|
||||
* @param {Object} params.authorizedApp 用户允许登录的应用
|
||||
* @returns userMatched 满足条件的用户列表
|
||||
*/
|
||||
async function findUser (params = {}) {
|
||||
const {
|
||||
userQuery,
|
||||
authorizedApp = []
|
||||
} = params
|
||||
const condition = getUserQueryCondition(userQuery)
|
||||
if (condition.length === 0) {
|
||||
throw new Error('Invalid user query')
|
||||
}
|
||||
const authorizedAppType = getType(authorizedApp)
|
||||
if (authorizedAppType !== 'string' && authorizedAppType !== 'array') {
|
||||
throw new Error('Invalid authorized app')
|
||||
}
|
||||
|
||||
let finalQuery
|
||||
|
||||
if (condition.length === 1) {
|
||||
finalQuery = condition[0]
|
||||
} else {
|
||||
finalQuery = dbCmd.or(condition)
|
||||
}
|
||||
const userQueryRes = await userCollection.where(finalQuery).get()
|
||||
return {
|
||||
total: userQueryRes.data.length,
|
||||
userMatched: userQueryRes.data.filter(item => {
|
||||
return isMatchUserApp(item.dcloud_appid, authorizedApp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getUserIdentifier (userRecord = {}) {
|
||||
const keys = Object.keys(USER_IDENTIFIER)
|
||||
return batchFindObjctValue(userRecord, keys)
|
||||
}
|
||||
|
||||
function getUserQueryCondition (userRecord = {}) {
|
||||
const userIdentifier = getUserIdentifier(userRecord)
|
||||
const condition = []
|
||||
for (const key in userIdentifier) {
|
||||
const value = userIdentifier[key]
|
||||
if (!value) {
|
||||
// 过滤所有value为假值的条件,在查询用户时没有意义
|
||||
continue
|
||||
}
|
||||
const queryItem = {
|
||||
[key]: value
|
||||
}
|
||||
// 为兼容用户老数据用户名及邮箱需要同时查小写及原始大小写数据
|
||||
if (key === 'mobile') {
|
||||
queryItem.mobile_confirmed = 1
|
||||
} else if (key === 'email') {
|
||||
queryItem.email_confirmed = 1
|
||||
const email = userIdentifier.email
|
||||
if (email.toLowerCase() !== email) {
|
||||
condition.push({
|
||||
email: email.toLowerCase(),
|
||||
email_confirmed: 1
|
||||
})
|
||||
}
|
||||
} else if (key === 'username') {
|
||||
const username = userIdentifier.username
|
||||
if (username.toLowerCase() !== username) {
|
||||
condition.push({
|
||||
username: username.toLowerCase()
|
||||
})
|
||||
}
|
||||
} else if (key === 'identities') {
|
||||
queryItem.identities = dbCmd.elemMatch(value)
|
||||
}
|
||||
condition.push(queryItem)
|
||||
}
|
||||
return condition
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findUser,
|
||||
getUserIdentifier
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
|
||||
async function getNeedCaptcha ({
|
||||
uid,
|
||||
username,
|
||||
mobile,
|
||||
email,
|
||||
type = 'login',
|
||||
limitDuration = 7200000, // 两小时
|
||||
limitTimes = 3 // 记录次数
|
||||
} = {}) {
|
||||
const db = uniCloud.database()
|
||||
const dbCmd = db.command
|
||||
// 当用户最近“2小时内(limitDuration)”登录失败达到3次(limitTimes)时。要求用户提交验证码
|
||||
const now = Date.now()
|
||||
const uniIdLogCollection = db.collection('uni-id-log')
|
||||
const userIdentifier = {
|
||||
user_id: uid,
|
||||
username,
|
||||
mobile,
|
||||
email
|
||||
}
|
||||
|
||||
let totalKey = 0; let deleteKey = 0
|
||||
for (const key in userIdentifier) {
|
||||
totalKey++
|
||||
if (!userIdentifier[key] || typeof userIdentifier[key] !== 'string') {
|
||||
deleteKey++
|
||||
delete userIdentifier[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteKey === totalKey) {
|
||||
throw new Error('System error') // 正常情况下不会进入此条件,但是考虑到后续会有其他开发者修改此云对象,在此处做一个判断
|
||||
}
|
||||
|
||||
const {
|
||||
data: recentRecord
|
||||
} = await uniIdLogCollection.where({
|
||||
ip: this.getUniversalClientInfo().clientIP,
|
||||
...userIdentifier,
|
||||
type,
|
||||
create_date: dbCmd.gt(now - limitDuration)
|
||||
})
|
||||
.orderBy('create_date', 'desc')
|
||||
.limit(limitTimes)
|
||||
.get()
|
||||
return recentRecord.length === limitTimes && recentRecord.every(item => item.state === 0)
|
||||
}
|
||||
|
||||
async function verifyCaptcha (params = {}) {
|
||||
const {
|
||||
captcha,
|
||||
scene
|
||||
} = params
|
||||
if (!captcha) {
|
||||
throw {
|
||||
errCode: ERROR.CAPTCHA_REQUIRED
|
||||
}
|
||||
}
|
||||
const payload = await this.uniCaptcha.verify({
|
||||
deviceId: this.getUniversalClientInfo().deviceId,
|
||||
captcha,
|
||||
scene
|
||||
})
|
||||
if (payload.errCode) {
|
||||
throw payload
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNeedCaptcha,
|
||||
verifyCaptcha
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
const {
|
||||
getWeixinPlatform
|
||||
} = require('./weixin')
|
||||
const createConfig = require('uni-config-center')
|
||||
|
||||
const requiredConfig = {
|
||||
'web.weixin-h5': ['appid', 'appsecret'],
|
||||
'web.weixin-web': ['appid', 'appsecret'],
|
||||
'app.weixin': ['appid', 'appsecret'],
|
||||
'mp-weixin.weixin': ['appid', 'appsecret'],
|
||||
'app.qq': ['appid', 'appsecret'],
|
||||
'mp-alipay.alipay': ['appid', 'privateKey'],
|
||||
'app.apple': ['bundleId']
|
||||
}
|
||||
|
||||
const uniIdConfig = createConfig({
|
||||
pluginId: 'uni-id'
|
||||
})
|
||||
|
||||
class ConfigUtils {
|
||||
constructor ({
|
||||
context
|
||||
} = {}) {
|
||||
this.context = context
|
||||
this.clientInfo = context.getUniversalClientInfo()
|
||||
const {
|
||||
appId,
|
||||
uniPlatform
|
||||
} = this.clientInfo
|
||||
this.appId = appId
|
||||
switch (uniPlatform) {
|
||||
case 'app':
|
||||
case 'app-plus':
|
||||
this.platform = 'app'
|
||||
break
|
||||
case 'web':
|
||||
case 'h5':
|
||||
this.platform = 'web'
|
||||
break
|
||||
default:
|
||||
this.platform = uniPlatform
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
getConfigArray () {
|
||||
let configContent
|
||||
try {
|
||||
configContent = require('uni-config-center/uni-id/config.json')
|
||||
} catch (error) {
|
||||
throw new Error('Invalid config file\n' + error.message)
|
||||
}
|
||||
if (configContent[0]) {
|
||||
return Object.values(configContent)
|
||||
}
|
||||
configContent.isDefaultConfig = true
|
||||
return [configContent]
|
||||
}
|
||||
|
||||
getAppConfig () {
|
||||
const configArray = this.getConfigArray()
|
||||
return configArray.find(item => item.dcloudAppid === this.appId) || configArray.find(item => item.isDefaultConfig)
|
||||
}
|
||||
|
||||
getPlatformConfig () {
|
||||
const appConfig = this.getAppConfig()
|
||||
if (!appConfig) {
|
||||
throw new Error(
|
||||
`Config for current app (${this.appId}) was not found, please check your config file or client appId`)
|
||||
}
|
||||
const platform = this.platform
|
||||
if (
|
||||
(this.platform === 'app' && appConfig['app-plus']) ||
|
||||
(this.platform === 'web' && appConfig.h5)
|
||||
) {
|
||||
throw new Error(
|
||||
`Client platform is ${this.platform}, but ${this.platform === 'web' ? 'h5' : 'app-plus'} was found in config. Please refer to: https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary?id=m-to-co`
|
||||
)
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
tokenExpiresIn: 7200,
|
||||
tokenExpiresThreshold: 1200,
|
||||
passwordErrorLimit: 6,
|
||||
passwordErrorRetryTime: 3600
|
||||
}
|
||||
return Object.assign(defaultConfig, appConfig, appConfig[platform])
|
||||
}
|
||||
|
||||
getOauthProvider ({
|
||||
provider
|
||||
} = {}) {
|
||||
const clientPlatform = this.platform
|
||||
let oatuhProivder = provider
|
||||
if (provider === 'weixin' && clientPlatform === 'web') {
|
||||
const weixinPlatform = getWeixinPlatform.call(this.context)
|
||||
if (weixinPlatform === 'h5' || weixinPlatform === 'web') {
|
||||
oatuhProivder = 'weixin-' + weixinPlatform // weixin-h5 公众号,weixin-web pc端
|
||||
}
|
||||
}
|
||||
return oatuhProivder
|
||||
}
|
||||
|
||||
getOauthConfig ({
|
||||
provider
|
||||
} = {}) {
|
||||
const config = this.getPlatformConfig()
|
||||
const clientPlatform = this.platform
|
||||
const oatuhProivder = this.getOauthProvider({
|
||||
provider
|
||||
})
|
||||
const requireConfigKey = requiredConfig[`${clientPlatform}.${oatuhProivder}`] || []
|
||||
if (!config.oauth || !config.oauth[oatuhProivder]) {
|
||||
throw new Error(`Config param required: ${clientPlatform}.oauth.${oatuhProivder}`)
|
||||
}
|
||||
const oauthConfig = config.oauth[oatuhProivder]
|
||||
requireConfigKey.forEach((item) => {
|
||||
if (!oauthConfig[item]) {
|
||||
throw new Error(`Config param required: ${clientPlatform}.oauth.${oatuhProivder}.${item}`)
|
||||
}
|
||||
})
|
||||
return oauthConfig
|
||||
}
|
||||
|
||||
getHooks () {
|
||||
if (uniIdConfig.hasFile('hooks/index.js')) {
|
||||
return require(
|
||||
uniIdConfig.resolve('hooks/index.js')
|
||||
)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConfigUtils
|
||||
@@ -0,0 +1,192 @@
|
||||
const {
|
||||
dbCmd,
|
||||
userCollection
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
/**
|
||||
* 获取随机邀请码,邀请码由大写字母加数字组成,由于存在手动输入邀请码的场景,从可选字符中去除 0、1、I、O
|
||||
* @param {number} len 邀请码长度,默认6位
|
||||
* @returns {string} 随机邀请码
|
||||
*/
|
||||
function getRandomInviteCode (len = 6) {
|
||||
const charArr = ['2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
|
||||
let code = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
code += charArr[Math.floor(Math.random() * charArr.length)]
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的邀请码,至多尝试十次以获取可用邀请码。从10亿可选值中随机,碰撞概率较低
|
||||
* 也有其他方案可以尝试,比如在数据库内设置一个从0开始计数的数字,每次调用此方法时使用updateAndReturn使数字加1并返回加1后的值,根据这个值生成对应的邀请码,比如(22222A + 1 == 22222B),此方式性能理论更好,但是不适用于旧项目
|
||||
* @param {object} param
|
||||
* @param {string} param.inviteCode 初始随机邀请码
|
||||
*/
|
||||
async function getValidInviteCode () {
|
||||
let retry = 10
|
||||
let code
|
||||
let codeValid = false
|
||||
while (retry > 0 && !codeValid) {
|
||||
retry--
|
||||
code = getRandomInviteCode()
|
||||
const getUserRes = await userCollection.where({
|
||||
my_invite_code: code
|
||||
}).limit(1).get()
|
||||
if (getUserRes.data.length === 0) {
|
||||
codeValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!codeValid) {
|
||||
throw {
|
||||
errCode: ERROR.SET_INVITE_CODE_FAILED
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邀请码查询邀请人
|
||||
* @param {object} param
|
||||
* @param {string} param.inviteCode 邀请码
|
||||
* @param {string} param.queryUid 受邀人id,非空时校验不可被下家或自己邀请
|
||||
* @returns
|
||||
*/
|
||||
async function findUserByInviteCode ({
|
||||
inviteCode,
|
||||
queryUid
|
||||
} = {}) {
|
||||
if (typeof inviteCode !== 'string') {
|
||||
throw {
|
||||
errCode: ERROR.SYSTEM_ERROR
|
||||
}
|
||||
}
|
||||
// 根据邀请码查询邀请人
|
||||
let getInviterRes
|
||||
if (queryUid) {
|
||||
getInviterRes = await userCollection.where({
|
||||
_id: dbCmd.neq(queryUid),
|
||||
inviter_uid: dbCmd.not(dbCmd.all([queryUid])),
|
||||
my_invite_code: inviteCode
|
||||
}).get()
|
||||
} else {
|
||||
getInviterRes = await userCollection.where({
|
||||
my_invite_code: inviteCode
|
||||
}).get()
|
||||
}
|
||||
if (getInviterRes.data.length > 1) {
|
||||
// 正常情况下不可能进入此条件,以防用户自行修改数据库出错,在此做出判断
|
||||
throw {
|
||||
errCode: ERROR.SYSTEM_ERROR
|
||||
}
|
||||
}
|
||||
const inviterRecord = getInviterRes.data[0]
|
||||
if (!inviterRecord) {
|
||||
throw {
|
||||
errCode: ERROR.INVALID_INVITE_CODE
|
||||
}
|
||||
}
|
||||
return inviterRecord
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邀请码生成邀请信息
|
||||
* @param {object} param
|
||||
* @param {string} param.inviteCode 邀请码
|
||||
* @param {string} param.queryUid 受邀人id,非空时校验不可被下家或自己邀请
|
||||
* @returns
|
||||
*/
|
||||
async function generateInviteInfo ({
|
||||
inviteCode,
|
||||
queryUid
|
||||
} = {}) {
|
||||
const inviterRecord = await findUserByInviteCode({
|
||||
inviteCode,
|
||||
queryUid
|
||||
})
|
||||
// 倒叙拼接当前用户邀请链
|
||||
const inviterUid = inviterRecord.inviter_uid || []
|
||||
inviterUid.unshift(inviterRecord._id)
|
||||
return {
|
||||
inviterUid,
|
||||
inviteTime: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否可以接受邀请,如果可以返回用户记录
|
||||
* @param {string} uid
|
||||
*/
|
||||
async function checkInviteInfo (uid) {
|
||||
// 检查当前用户是否已有邀请人
|
||||
const getUserRes = await userCollection.doc(uid).field({
|
||||
my_invite_code: true,
|
||||
inviter_uid: true
|
||||
}).get()
|
||||
const userRecord = getUserRes.data[0]
|
||||
if (!userRecord) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_NOT_EXISTS
|
||||
}
|
||||
}
|
||||
if (userRecord.inviter_uid && userRecord.inviter_uid.length > 0) {
|
||||
throw {
|
||||
errCode: ERROR.CHANGE_INVITER_FORBIDDEN
|
||||
}
|
||||
}
|
||||
return userRecord
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定用户接受邀请码邀请
|
||||
* @param {object} param
|
||||
* @param {string} param.uid 用户uid
|
||||
* @param {string} param.inviteCode 邀请人的邀请码
|
||||
* @returns
|
||||
*/
|
||||
async function acceptInvite ({
|
||||
uid,
|
||||
inviteCode
|
||||
} = {}) {
|
||||
await checkInviteInfo(uid)
|
||||
const {
|
||||
inviterUid,
|
||||
inviteTime
|
||||
} = await generateInviteInfo({
|
||||
inviteCode,
|
||||
queryUid: uid
|
||||
})
|
||||
|
||||
if (inviterUid === uid) {
|
||||
throw {
|
||||
errCode: ERROR.INVALID_INVITE_CODE
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前用户的邀请人信息
|
||||
await userCollection.doc(uid).update({
|
||||
inviter_uid: inviterUid,
|
||||
invite_time: inviteTime
|
||||
})
|
||||
|
||||
// 更新当前用户邀请的用户的邀请人信息,这步可能较为耗时
|
||||
await userCollection.where({
|
||||
inviter_uid: uid
|
||||
}).update({
|
||||
inviter_uid: dbCmd.push(inviterUid)
|
||||
})
|
||||
|
||||
return {
|
||||
errCode: 0,
|
||||
errMsg: ''
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
acceptInvite,
|
||||
generateInviteInfo,
|
||||
getValidInviteCode
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
const {
|
||||
findUser
|
||||
} = require('./account')
|
||||
const {
|
||||
userCollection,
|
||||
LOG_TYPE
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
const {
|
||||
logout
|
||||
} = require('./logout')
|
||||
const PasswordUtils = require('./password')
|
||||
|
||||
async function realPreLogin (params = {}) {
|
||||
const {
|
||||
user
|
||||
} = params
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
const {
|
||||
total,
|
||||
userMatched
|
||||
} = await findUser({
|
||||
userQuery: user,
|
||||
authorizedApp: appId
|
||||
})
|
||||
if (userMatched.length === 0) {
|
||||
if (total > 0) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
|
||||
}
|
||||
}
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_NOT_EXISTS
|
||||
}
|
||||
} else if (userMatched.length > 1) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_CONFLICT
|
||||
}
|
||||
}
|
||||
const userRecord = userMatched[0]
|
||||
checkLoginUserRecord(userRecord)
|
||||
return userRecord
|
||||
}
|
||||
|
||||
async function preLogin (params = {}) {
|
||||
const {
|
||||
user
|
||||
} = params
|
||||
try {
|
||||
const user = await realPreLogin.call(this, params)
|
||||
return user
|
||||
} catch (error) {
|
||||
await this.middleware.uniIdLog({
|
||||
success: false,
|
||||
data: user,
|
||||
type: LOG_TYPE.LOGIN
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function preLoginWithPassword (params = {}) {
|
||||
const {
|
||||
user,
|
||||
password
|
||||
} = params
|
||||
try {
|
||||
const userRecord = await realPreLogin.call(this, params)
|
||||
const {
|
||||
passwordErrorLimit,
|
||||
passwordErrorRetryTime
|
||||
} = this.config
|
||||
const {
|
||||
clientIP
|
||||
} = this.getUniversalClientInfo()
|
||||
// 根据ip地址,密码错误次数过多,锁定登录
|
||||
let loginIPLimit = userRecord.login_ip_limit || []
|
||||
// 清理无用记录
|
||||
loginIPLimit = loginIPLimit.filter(item => item.last_error_time > Date.now() - passwordErrorRetryTime * 1000)
|
||||
let currentIPLimit = loginIPLimit.find(item => item.ip === clientIP)
|
||||
if (currentIPLimit && currentIPLimit.error_times >= passwordErrorLimit) {
|
||||
throw {
|
||||
errCode: ERROR.PASSWORD_ERROR_EXCEED_LIMIT
|
||||
}
|
||||
}
|
||||
const passwordUtils = new PasswordUtils({
|
||||
userRecord,
|
||||
clientInfo: this.getUniversalClientInfo(),
|
||||
passwordSecret: this.config.passwordSecret
|
||||
})
|
||||
|
||||
const {
|
||||
success: checkPasswordSuccess,
|
||||
refreshPasswordInfo
|
||||
} = passwordUtils.checkUserPassword({
|
||||
password
|
||||
})
|
||||
if (!checkPasswordSuccess) {
|
||||
// 更新用户ip对应的密码错误记录
|
||||
if (!currentIPLimit) {
|
||||
currentIPLimit = {
|
||||
ip: clientIP,
|
||||
error_times: 1,
|
||||
last_error_time: Date.now()
|
||||
}
|
||||
loginIPLimit.push(currentIPLimit)
|
||||
} else {
|
||||
currentIPLimit.error_times++
|
||||
currentIPLimit.last_error_time = Date.now()
|
||||
}
|
||||
await userCollection.doc(userRecord._id).update({
|
||||
login_ip_limit: loginIPLimit
|
||||
})
|
||||
throw {
|
||||
errCode: ERROR.PASSWORD_ERROR
|
||||
}
|
||||
}
|
||||
const extraData = {}
|
||||
if (refreshPasswordInfo) {
|
||||
extraData.password = refreshPasswordInfo.passwordHash
|
||||
extraData.password_secret_version = refreshPasswordInfo.version
|
||||
}
|
||||
|
||||
const currentIPLimitIndex = loginIPLimit.indexOf(currentIPLimit)
|
||||
if (currentIPLimitIndex > -1) {
|
||||
loginIPLimit.splice(currentIPLimitIndex, 1)
|
||||
}
|
||||
extraData.login_ip_limit = loginIPLimit
|
||||
return {
|
||||
user: userRecord,
|
||||
extraData
|
||||
}
|
||||
} catch (error) {
|
||||
await this.middleware.uniIdLog({
|
||||
success: false,
|
||||
data: user,
|
||||
type: LOG_TYPE.LOGIN
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function checkLoginUserRecord (user) {
|
||||
switch (user.status) {
|
||||
case undefined:
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_BANNED
|
||||
}
|
||||
case 2:
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_AUDITING
|
||||
}
|
||||
case 3:
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_AUDIT_FAILED
|
||||
}
|
||||
case 4:
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_CLOSED
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function thirdPartyLogin (params = {}) {
|
||||
const {
|
||||
user
|
||||
} = params
|
||||
return {
|
||||
mobileConfirmed: !!user.mobile_confirmed,
|
||||
emailConfirmed: !!user.email_confirmed
|
||||
}
|
||||
}
|
||||
|
||||
async function postLogin (params = {}) {
|
||||
const {
|
||||
user,
|
||||
extraData,
|
||||
isThirdParty = false
|
||||
} = params
|
||||
const {
|
||||
clientIP
|
||||
} = this.getUniversalClientInfo()
|
||||
const uniIdToken = this.getUniversalUniIdToken()
|
||||
const uid = user._id
|
||||
const updateData = {
|
||||
last_login_date: Date.now(),
|
||||
last_login_ip: clientIP,
|
||||
...extraData
|
||||
}
|
||||
const createTokenRes = await this.uniIdCommon.createToken({
|
||||
uid
|
||||
})
|
||||
|
||||
const {
|
||||
errCode,
|
||||
token,
|
||||
tokenExpired
|
||||
} = createTokenRes
|
||||
if (errCode) {
|
||||
throw createTokenRes
|
||||
}
|
||||
|
||||
if (uniIdToken) {
|
||||
try {
|
||||
await logout.call(this)
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
await userCollection.doc(uid).update(updateData)
|
||||
await this.middleware.uniIdLog({
|
||||
data: {
|
||||
user_id: uid
|
||||
},
|
||||
type: LOG_TYPE.LOGIN
|
||||
})
|
||||
return {
|
||||
errCode: 0,
|
||||
newToken: {
|
||||
token,
|
||||
tokenExpired
|
||||
},
|
||||
uid,
|
||||
...(
|
||||
isThirdParty
|
||||
? thirdPartyLogin({
|
||||
user
|
||||
})
|
||||
: {}
|
||||
),
|
||||
passwordConfirmed: !!user.password
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
preLogin,
|
||||
postLogin,
|
||||
checkLoginUserRecord,
|
||||
preLoginWithPassword
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
const {
|
||||
dbCmd,
|
||||
LOG_TYPE,
|
||||
deviceCollection,
|
||||
userCollection
|
||||
} = require('../../common/constants')
|
||||
|
||||
async function logout () {
|
||||
const {
|
||||
deviceId
|
||||
} = this.getUniversalClientInfo()
|
||||
const uniIdToken = this.getUniversalUniIdToken()
|
||||
const payload = await this.uniIdCommon.checkToken(
|
||||
uniIdToken,
|
||||
{
|
||||
autoRefresh: false
|
||||
}
|
||||
)
|
||||
if (payload.errCode) {
|
||||
throw payload
|
||||
}
|
||||
const uid = payload.uid
|
||||
|
||||
// 删除token
|
||||
await userCollection.doc(uid).update({
|
||||
token: dbCmd.pull(uniIdToken)
|
||||
})
|
||||
|
||||
// 仅当device表的device_id和user_id均对应时才进行更新
|
||||
await deviceCollection.where({
|
||||
device_id: deviceId,
|
||||
user_id: uid
|
||||
}).update({
|
||||
token_expired: 0
|
||||
})
|
||||
await this.middleware.uniIdLog({
|
||||
data: {
|
||||
user_id: uid
|
||||
},
|
||||
type: LOG_TYPE.LOGOUT
|
||||
})
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
logout
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
const {
|
||||
getType
|
||||
} = require('../../common/utils')
|
||||
const crypto = require('crypto')
|
||||
const createConfig = require('uni-config-center')
|
||||
const shareConfig = createConfig({
|
||||
pluginId: 'uni-id'
|
||||
})
|
||||
let customPassword = {}
|
||||
if (shareConfig.hasFile('custom-password.js')) {
|
||||
customPassword = shareConfig.requireFile('custom-password.js') || {}
|
||||
}
|
||||
|
||||
const passwordAlgorithmMap = {
|
||||
UNI_ID_HMAC_SHA1: 'hmac-sha1',
|
||||
UNI_ID_HMAC_SHA256: 'hmac-sha256',
|
||||
UNI_ID_CUSTOM: 'custom'
|
||||
}
|
||||
|
||||
const passwordAlgorithmKeyMap = Object.keys(passwordAlgorithmMap).reduce((res, item) => {
|
||||
res[passwordAlgorithmMap[item]] = item
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const passwordExtMethod = {
|
||||
[passwordAlgorithmMap.UNI_ID_HMAC_SHA1]: {
|
||||
verify ({ password }) {
|
||||
const { password_secret_version: passwordSecretVersion } = this.userRecord
|
||||
|
||||
const passwordSecret = this._getSecretByVersion({
|
||||
version: passwordSecretVersion
|
||||
})
|
||||
|
||||
const { passwordHash } = this.encrypt({
|
||||
password,
|
||||
passwordSecret
|
||||
})
|
||||
|
||||
return passwordHash === this.userRecord.password
|
||||
},
|
||||
encrypt ({ password, passwordSecret }) {
|
||||
const { value: secret, version } = passwordSecret
|
||||
const hmac = crypto.createHmac('sha1', secret.toString('ascii'))
|
||||
|
||||
hmac.update(password)
|
||||
|
||||
return {
|
||||
passwordHash: hmac.digest('hex'),
|
||||
version
|
||||
}
|
||||
}
|
||||
},
|
||||
[passwordAlgorithmMap.UNI_ID_HMAC_SHA256]: {
|
||||
verify ({ password }) {
|
||||
const parse = this._parsePassword()
|
||||
const passwordHash = crypto.createHmac(parse.algorithm, parse.salt).update(password).digest('hex')
|
||||
|
||||
return passwordHash === parse.hash
|
||||
},
|
||||
encrypt ({ password, passwordSecret }) {
|
||||
const { version } = passwordSecret
|
||||
|
||||
// 默认使用 sha256 加密算法
|
||||
const salt = crypto.randomBytes(10).toString('hex')
|
||||
const sha256Hash = crypto.createHmac(passwordAlgorithmMap.UNI_ID_HMAC_SHA256.substring(5), salt).update(password).digest('hex')
|
||||
const algorithm = passwordAlgorithmKeyMap[passwordAlgorithmMap.UNI_ID_HMAC_SHA256]
|
||||
// B 为固定值,对应 PasswordMethodMaps 中的 sha256算法
|
||||
// hash 格式 $[PasswordMethodFlagMapsKey]$[salt size]$[salt][Hash]
|
||||
const passwordHash = `$${algorithm}$${salt.length}$${salt}${sha256Hash}`
|
||||
|
||||
return {
|
||||
passwordHash,
|
||||
version
|
||||
}
|
||||
}
|
||||
},
|
||||
[passwordAlgorithmMap.UNI_ID_CUSTOM]: {
|
||||
verify ({ password, passwordSecret }) {
|
||||
if (!customPassword.verifyPassword) throw new Error('verifyPassword method not found in custom password file')
|
||||
|
||||
// return true or false
|
||||
return customPassword.verifyPassword({
|
||||
password,
|
||||
passwordSecret,
|
||||
userRecord: this.userRecord,
|
||||
clientInfo: this.clientInfo
|
||||
})
|
||||
},
|
||||
encrypt ({ password, passwordSecret }) {
|
||||
if (!customPassword.encryptPassword) throw new Error('encryptPassword method not found in custom password file')
|
||||
|
||||
// return object<{passwordHash: string, version: number}>
|
||||
return customPassword.encryptPassword({
|
||||
password,
|
||||
passwordSecret,
|
||||
clientInfo: this.clientInfo
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordUtils {
|
||||
constructor ({
|
||||
userRecord = {},
|
||||
clientInfo,
|
||||
passwordSecret
|
||||
} = {}) {
|
||||
if (!clientInfo) throw new Error('Invalid clientInfo')
|
||||
if (!passwordSecret) throw new Error('Invalid password secret')
|
||||
|
||||
this.clientInfo = clientInfo
|
||||
this.userRecord = userRecord
|
||||
this.passwordSecret = this.prePasswordSecret(passwordSecret)
|
||||
}
|
||||
|
||||
/**
|
||||
* passwordSecret 预处理
|
||||
* @param passwordSecret
|
||||
* @return {*[]}
|
||||
*/
|
||||
prePasswordSecret (passwordSecret) {
|
||||
const newPasswordSecret = []
|
||||
if (getType(passwordSecret) === 'string') {
|
||||
newPasswordSecret.push({
|
||||
value: passwordSecret,
|
||||
type: passwordAlgorithmMap.UNI_ID_HMAC_SHA1
|
||||
})
|
||||
} else if (getType(passwordSecret) === 'array') {
|
||||
for (const secret of passwordSecret.sort((a, b) => a.version - b.version)) {
|
||||
newPasswordSecret.push({
|
||||
...secret,
|
||||
// 没有 type 设置默认 type hmac-sha1
|
||||
type: secret.type || passwordAlgorithmMap.UNI_ID_HMAC_SHA1
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid password secret')
|
||||
}
|
||||
|
||||
return newPasswordSecret
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新加密密钥
|
||||
* @return {*}
|
||||
* @private
|
||||
*/
|
||||
_getLastestSecret () {
|
||||
return this.passwordSecret[this.passwordSecret.length - 1]
|
||||
}
|
||||
|
||||
_getOldestSecret () {
|
||||
return this.passwordSecret[0]
|
||||
}
|
||||
|
||||
_getSecretByVersion ({ version } = {}) {
|
||||
if (!version && version !== 0) {
|
||||
return this._getOldestSecret()
|
||||
}
|
||||
if (this.passwordSecret.length === 1) {
|
||||
return this.passwordSecret[0]
|
||||
}
|
||||
return this.passwordSecret.find(item => item.version === version)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取密码的验证/加密方法
|
||||
* @param passwordSecret
|
||||
* @return {*[]}
|
||||
* @private
|
||||
*/
|
||||
_getPasswordExt (passwordSecret) {
|
||||
const ext = passwordExtMethod[passwordSecret.type]
|
||||
if (!ext) {
|
||||
throw new Error(`暂不支持 ${passwordSecret.type} 类型的加密算法`)
|
||||
}
|
||||
|
||||
const passwordExt = Object.create(null)
|
||||
|
||||
for (const key in ext) {
|
||||
passwordExt[key] = ext[key].bind(Object.assign(this, Object.keys(ext).reduce((res, item) => {
|
||||
if (item !== key) {
|
||||
res[item] = ext[item].bind(this)
|
||||
}
|
||||
return res
|
||||
}, {})))
|
||||
}
|
||||
|
||||
return passwordExt
|
||||
}
|
||||
|
||||
_parsePassword () {
|
||||
const [algorithmKey = '', cost = 0, hashStr = ''] = this.userRecord.password.split('$').filter(key => key)
|
||||
const algorithm = passwordAlgorithmMap[algorithmKey] ? passwordAlgorithmMap[algorithmKey].substring(5) : null
|
||||
const salt = hashStr.substring(0, Number(cost))
|
||||
const hash = hashStr.substring(Number(cost))
|
||||
|
||||
return {
|
||||
algorithm,
|
||||
salt,
|
||||
hash
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成加密后的密码
|
||||
* @param {String} password 密码
|
||||
*/
|
||||
generatePasswordHash ({ password }) {
|
||||
if (!password) throw new Error('Invalid password')
|
||||
|
||||
const passwordSecret = this._getLastestSecret()
|
||||
const ext = this._getPasswordExt(passwordSecret)
|
||||
|
||||
const { passwordHash, version } = ext.encrypt({
|
||||
password,
|
||||
passwordSecret
|
||||
})
|
||||
|
||||
return {
|
||||
passwordHash,
|
||||
version
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码校验
|
||||
* @param {String} password
|
||||
* @param {Boolean} autoRefresh
|
||||
* @return {{refreshPasswordInfo: {version: *, passwordHash: *}, success: boolean}|{success: boolean}}
|
||||
*/
|
||||
checkUserPassword ({ password, autoRefresh = true }) {
|
||||
if (!password) throw new Error('Invalid password')
|
||||
|
||||
const { password_secret_version: passwordSecretVersion } = this.userRecord
|
||||
const passwordSecret = this._getSecretByVersion({
|
||||
version: passwordSecretVersion
|
||||
})
|
||||
const ext = this._getPasswordExt(passwordSecret)
|
||||
|
||||
const success = ext.verify({ password, passwordSecret })
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
|
||||
let refreshPasswordInfo
|
||||
if (autoRefresh && passwordSecretVersion !== this._getLastestSecret().version) {
|
||||
refreshPasswordInfo = this.generatePasswordHash({ password })
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
refreshPasswordInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PasswordUtils
|
||||
@@ -0,0 +1,152 @@
|
||||
const {
|
||||
userCollection
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
|
||||
function getQQPlatform () {
|
||||
const platform = this.clientPlatform
|
||||
switch (platform) {
|
||||
case 'app':
|
||||
case 'app-plus':
|
||||
return 'app'
|
||||
case 'mp-qq':
|
||||
return 'mp'
|
||||
default:
|
||||
throw new Error('Unsupported qq platform')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveQQUserKey ({
|
||||
openid,
|
||||
sessionKey, // QQ小程序用户sessionKey
|
||||
accessToken, // App端QQ用户accessToken
|
||||
accessTokenExpired // App端QQ用户accessToken过期时间
|
||||
} = {}) {
|
||||
// 微信公众平台、开放平台refreshToken有效期均为30天(微信没有在网络请求里面返回30天这个值,务必注意未来可能出现调整,需及时更新此处逻辑)。
|
||||
// 此前QQ开放平台有调整过accessToken的过期时间:[access_token有效期由90天缩短至30天](https://wiki.connect.qq.com/%E3%80%90qq%E4%BA%92%E8%81%94%E3%80%91access_token%E6%9C%89%E6%95%88%E6%9C%9F%E8%B0%83%E6%95%B4)
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
const qqPlatform = getQQPlatform.call(this)
|
||||
const keyObj = {
|
||||
dcloudAppid: appId,
|
||||
openid,
|
||||
platform: 'qq-' + qqPlatform
|
||||
}
|
||||
switch (qqPlatform) {
|
||||
case 'mp':
|
||||
await this.uniOpenBridge.setSessionKey(keyObj, {
|
||||
session_key: sessionKey
|
||||
}, 30 * 24 * 60 * 60)
|
||||
break
|
||||
case 'app':
|
||||
case 'h5':
|
||||
case 'web':
|
||||
await this.uniOpenBridge.setUserAccessToken(keyObj, {
|
||||
access_token: accessToken,
|
||||
access_token_expired: accessTokenExpired
|
||||
}, accessTokenExpired
|
||||
? Math.floor((accessTokenExpired - Date.now()) / 1000)
|
||||
: 30 * 24 * 60 * 60
|
||||
)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function generateQQCache ({
|
||||
sessionKey, // QQ小程序用户sessionKey
|
||||
accessToken, // App端QQ用户accessToken
|
||||
accessTokenExpired // App端QQ用户accessToken过期时间
|
||||
} = {}) {
|
||||
const platform = getQQPlatform.call(this)
|
||||
let cache
|
||||
switch (platform) {
|
||||
case 'app':
|
||||
cache = {
|
||||
access_token: accessToken,
|
||||
access_token_expired: accessTokenExpired
|
||||
}
|
||||
break
|
||||
case 'mp':
|
||||
cache = {
|
||||
session_key: sessionKey
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error('Unsupported qq platform')
|
||||
}
|
||||
return {
|
||||
third_party: {
|
||||
[`${platform}_qq`]: cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getQQOpenid ({
|
||||
userRecord
|
||||
} = {}) {
|
||||
const qqPlatform = getQQPlatform.call(this)
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
const qqOpenidObj = userRecord.qq_openid
|
||||
if (!qqOpenidObj) {
|
||||
return
|
||||
}
|
||||
return qqOpenidObj[`${qqPlatform}_${appId}`] || qqOpenidObj[qqPlatform]
|
||||
}
|
||||
|
||||
async function getQQCacheFallback ({
|
||||
userRecord,
|
||||
key
|
||||
} = {}) {
|
||||
const platform = getQQPlatform.call(this)
|
||||
const thirdParty = userRecord && userRecord.third_party
|
||||
if (!thirdParty) {
|
||||
return
|
||||
}
|
||||
const qqCache = thirdParty[`${platform}_qq`]
|
||||
return qqCache && qqCache[key]
|
||||
}
|
||||
|
||||
async function getQQCache ({
|
||||
uid,
|
||||
userRecord,
|
||||
key
|
||||
} = {}) {
|
||||
const qqPlatform = getQQPlatform.call(this)
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
|
||||
if (!userRecord) {
|
||||
const getUserRes = await userCollection.doc(uid).get()
|
||||
userRecord = getUserRes.data[0]
|
||||
}
|
||||
if (!userRecord) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_NOT_EXISTS
|
||||
}
|
||||
}
|
||||
const openid = getQQOpenid.call(this, {
|
||||
userRecord
|
||||
})
|
||||
const getCacheMethod = qqPlatform === 'mp' ? 'getSessionKey' : 'getUserAccessToken'
|
||||
const userKey = await this.uniOpenBridge[getCacheMethod]({
|
||||
dcloudAppid: appId,
|
||||
platform: 'qq-' + qqPlatform,
|
||||
openid
|
||||
})
|
||||
if (userKey) {
|
||||
return userKey[key]
|
||||
}
|
||||
return getQQCacheFallback({
|
||||
userRecord,
|
||||
key
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getQQPlatform,
|
||||
generateQQCache,
|
||||
getQQCache,
|
||||
saveQQUserKey
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
const {
|
||||
userCollection,
|
||||
LOG_TYPE
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
const {
|
||||
findUser
|
||||
} = require('./account')
|
||||
const {
|
||||
getValidInviteCode,
|
||||
generateInviteInfo
|
||||
} = require('./fission')
|
||||
const {
|
||||
logout
|
||||
} = require('./logout')
|
||||
const PasswordUtils = require('./password')
|
||||
const merge = require('lodash.merge')
|
||||
|
||||
async function realPreRegister (params = {}) {
|
||||
const {
|
||||
user
|
||||
} = params
|
||||
const {
|
||||
userMatched
|
||||
} = await findUser({
|
||||
userQuery: user,
|
||||
authorizedApp: this.getUniversalClientInfo().appId
|
||||
})
|
||||
if (userMatched.length > 0) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_EXISTS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function preRegister (params = {}) {
|
||||
try {
|
||||
await realPreRegister.call(this, params)
|
||||
} catch (error) {
|
||||
await this.middleware.uniIdLog({
|
||||
success: false,
|
||||
type: LOG_TYPE.REGISTER
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function preRegisterWithPassword (params = {}) {
|
||||
const {
|
||||
user,
|
||||
password
|
||||
} = params
|
||||
await preRegister.call(this, {
|
||||
user
|
||||
})
|
||||
const passwordUtils = new PasswordUtils({
|
||||
clientInfo: this.getUniversalClientInfo(),
|
||||
passwordSecret: this.config.passwordSecret
|
||||
})
|
||||
const {
|
||||
passwordHash,
|
||||
version
|
||||
} = passwordUtils.generatePasswordHash({
|
||||
password
|
||||
})
|
||||
const extraData = {
|
||||
password: passwordHash,
|
||||
password_secret_version: version
|
||||
}
|
||||
return {
|
||||
user,
|
||||
extraData
|
||||
}
|
||||
}
|
||||
|
||||
async function thirdPartyRegister ({
|
||||
user = {}
|
||||
} = {}) {
|
||||
return {
|
||||
mobileConfirmed: !!(user.mobile && user.mobile_confirmed) || false,
|
||||
emailConfirmed: !!(user.email && user.email_confirmed) || false
|
||||
}
|
||||
}
|
||||
|
||||
async function postRegister (params = {}) {
|
||||
const {
|
||||
user,
|
||||
extraData = {},
|
||||
isThirdParty = false,
|
||||
inviteCode
|
||||
} = params
|
||||
const {
|
||||
appId,
|
||||
appName,
|
||||
appVersion,
|
||||
appVersionCode,
|
||||
channel,
|
||||
scene,
|
||||
clientIP,
|
||||
osName
|
||||
} = this.getUniversalClientInfo()
|
||||
const uniIdToken = this.getUniversalUniIdToken()
|
||||
|
||||
merge(user, extraData)
|
||||
|
||||
const registerChannel = channel || scene
|
||||
user.register_env = {
|
||||
appid: appId || '',
|
||||
uni_platform: this.clientPlatform || '',
|
||||
os_name: osName || '',
|
||||
app_name: appName || '',
|
||||
app_version: appVersion || '',
|
||||
app_version_code: appVersionCode || '',
|
||||
channel: registerChannel ? registerChannel + '' : '', // channel可能为数字,统一存为字符串
|
||||
client_ip: clientIP || ''
|
||||
}
|
||||
|
||||
user.register_date = Date.now()
|
||||
user.dcloud_appid = [appId]
|
||||
|
||||
if (user.username) {
|
||||
user.username = user.username.toLowerCase()
|
||||
}
|
||||
if (user.email) {
|
||||
user.email = user.email.toLowerCase()
|
||||
}
|
||||
|
||||
const {
|
||||
autoSetInviteCode, // 注册时自动设置邀请码
|
||||
forceInviteCode, // 必须有邀请码才允许注册,注意此逻辑不可对admin生效
|
||||
userRegisterDefaultRole // 用户注册时配置的默认角色
|
||||
} = this.config
|
||||
if (autoSetInviteCode) {
|
||||
user.my_invite_code = await getValidInviteCode()
|
||||
}
|
||||
|
||||
// 如果用户注册默认角色配置存在且不为空数组
|
||||
if (userRegisterDefaultRole && userRegisterDefaultRole.length) {
|
||||
// 将用户已有的角色和配置的默认角色合并成一个数组,并去重
|
||||
user.role = Array.from(new Set([...(user.role || []), ...userRegisterDefaultRole]))
|
||||
}
|
||||
|
||||
const isAdmin = user.role && user.role.includes('admin')
|
||||
|
||||
if (forceInviteCode && !isAdmin && !inviteCode) {
|
||||
throw {
|
||||
errCode: ERROR.INVALID_INVITE_CODE
|
||||
}
|
||||
}
|
||||
|
||||
if (inviteCode) {
|
||||
const {
|
||||
inviterUid,
|
||||
inviteTime
|
||||
} = await generateInviteInfo({
|
||||
inviteCode
|
||||
})
|
||||
user.inviter_uid = inviterUid
|
||||
user.invite_time = inviteTime
|
||||
}
|
||||
|
||||
if (uniIdToken) {
|
||||
try {
|
||||
await logout.call(this)
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
const beforeRegister = this.hooks.beforeRegister
|
||||
let userRecord = user
|
||||
if (beforeRegister) {
|
||||
userRecord = await beforeRegister({
|
||||
userRecord,
|
||||
clientInfo: this.getUniversalClientInfo()
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
id: uid
|
||||
} = await userCollection.add(userRecord)
|
||||
|
||||
const createTokenRes = await this.uniIdCommon.createToken({
|
||||
uid
|
||||
})
|
||||
|
||||
const {
|
||||
errCode,
|
||||
token,
|
||||
tokenExpired
|
||||
} = createTokenRes
|
||||
|
||||
if (errCode) {
|
||||
throw createTokenRes
|
||||
}
|
||||
|
||||
await this.middleware.uniIdLog({
|
||||
data: {
|
||||
user_id: uid
|
||||
},
|
||||
type: LOG_TYPE.REGISTER
|
||||
})
|
||||
|
||||
return {
|
||||
errCode: 0,
|
||||
uid,
|
||||
newToken: {
|
||||
token,
|
||||
tokenExpired
|
||||
},
|
||||
...(
|
||||
isThirdParty
|
||||
? thirdPartyRegister({
|
||||
user: {
|
||||
...userRecord,
|
||||
_id: uid
|
||||
}
|
||||
})
|
||||
: {}
|
||||
),
|
||||
passwordConfirmed: !!userRecord.password
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
preRegister,
|
||||
preRegisterWithPassword,
|
||||
postRegister
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
const {
|
||||
findUser
|
||||
} = require('./account')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
const {
|
||||
userCollection, dbCmd, USER_IDENTIFIER
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
getUserIdentifier
|
||||
} = require('../../lib/utils/account')
|
||||
|
||||
const {
|
||||
batchFindObjctValue
|
||||
} = require('../../common/utils')
|
||||
const merge = require('lodash.merge')
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} param
|
||||
* @param {string} param.uid 用户id
|
||||
* @param {string} param.bindAccount 要绑定的三方账户、手机号或邮箱
|
||||
*/
|
||||
async function preBind ({
|
||||
uid,
|
||||
bindAccount,
|
||||
logType
|
||||
} = {}) {
|
||||
const {
|
||||
userMatched
|
||||
} = await findUser({
|
||||
userQuery: bindAccount,
|
||||
authorizedApp: this.getUniversalClientInfo().appId
|
||||
})
|
||||
if (userMatched.length > 0) {
|
||||
await this.middleware.uniIdLog({
|
||||
data: {
|
||||
user_id: uid
|
||||
},
|
||||
type: logType,
|
||||
success: false
|
||||
})
|
||||
throw {
|
||||
errCode: ERROR.BIND_CONFLICT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postBind ({
|
||||
uid,
|
||||
extraData = {},
|
||||
bindAccount,
|
||||
logType
|
||||
} = {}) {
|
||||
await userCollection.doc(uid).update(merge(bindAccount, extraData))
|
||||
await this.middleware.uniIdLog({
|
||||
data: {
|
||||
user_id: uid
|
||||
},
|
||||
type: logType
|
||||
})
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
async function preUnBind ({
|
||||
uid,
|
||||
unBindAccount,
|
||||
logType
|
||||
}) {
|
||||
const notUnBind = ['username', 'mobile', 'email']
|
||||
const userIdentifier = getUserIdentifier(unBindAccount)
|
||||
const condition = Object.keys(userIdentifier).reduce((res, key) => {
|
||||
if (userIdentifier[key]) {
|
||||
if (notUnBind.includes(key)) {
|
||||
throw {
|
||||
errCode: ERROR.UNBIND_NOT_SUPPORTED
|
||||
}
|
||||
}
|
||||
|
||||
res.push({
|
||||
[key]: userIdentifier[key]
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}, [])
|
||||
const currentUnBindAccount = Object.keys(userIdentifier).reduce((res, key) => {
|
||||
if (userIdentifier[key]) {
|
||||
res.push(key)
|
||||
}
|
||||
return res
|
||||
}, [])
|
||||
const { data: users } = await userCollection.where(dbCmd.and(
|
||||
{ _id: uid },
|
||||
dbCmd.or(condition)
|
||||
)).get()
|
||||
|
||||
if (users.length <= 0) {
|
||||
await this.middleware.uniIdLog({
|
||||
data: {
|
||||
user_id: uid
|
||||
},
|
||||
type: logType,
|
||||
success: false
|
||||
})
|
||||
throw {
|
||||
errCode: ERROR.UNBIND_FAIL
|
||||
}
|
||||
}
|
||||
|
||||
const [user] = users
|
||||
const otherAccounts = batchFindObjctValue(user, Object.keys(USER_IDENTIFIER).filter(key => !notUnBind.includes(key) && !currentUnBindAccount.includes(key)))
|
||||
let hasOtherAccountBind = false
|
||||
|
||||
for (const key in otherAccounts) {
|
||||
if (otherAccounts[key]) {
|
||||
hasOtherAccountBind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有其他第三方登录方式
|
||||
if (!hasOtherAccountBind) {
|
||||
// 存在用户名或者邮箱但是没有设置过没密码就提示设置密码
|
||||
if ((user.username || user.email) && !user.password) {
|
||||
throw {
|
||||
errCode: ERROR.UNBIND_PASSWORD_NOT_EXISTS
|
||||
}
|
||||
}
|
||||
// 账号任何登录方式都没有就优先绑定手机号
|
||||
if (!user.mobile) {
|
||||
throw {
|
||||
errCode: ERROR.UNBIND_MOBILE_NOT_EXISTS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postUnBind ({
|
||||
uid,
|
||||
unBindAccount,
|
||||
logType
|
||||
}) {
|
||||
await userCollection.doc(uid).update(unBindAccount)
|
||||
await this.middleware.uniIdLog({
|
||||
data: {
|
||||
user_id: uid
|
||||
},
|
||||
type: logType
|
||||
})
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
preBind,
|
||||
postBind,
|
||||
preUnBind,
|
||||
postUnBind
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
const {
|
||||
setMobileVerifyCode
|
||||
} = require('./verify-code')
|
||||
const {
|
||||
getVerifyCode
|
||||
} = require('../../common/utils')
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
* @param {object} param
|
||||
* @param {string} param.mobile 手机号
|
||||
* @param {object} param.code 可选,验证码
|
||||
* @param {object} param.scene 短信场景
|
||||
* @param {object} param.templateId 可选,短信模板id
|
||||
* @returns
|
||||
*/
|
||||
async function sendSmsCode ({
|
||||
mobile,
|
||||
code,
|
||||
scene,
|
||||
templateId
|
||||
} = {}) {
|
||||
const requiredParams = [
|
||||
'name',
|
||||
'smsKey',
|
||||
'smsSecret',
|
||||
'codeExpiresIn'
|
||||
]
|
||||
const smsConfig = (this.config.service && this.config.service.sms) || {}
|
||||
for (let i = 0; i < requiredParams.length; i++) {
|
||||
const key = requiredParams[i]
|
||||
if (!smsConfig[key]) {
|
||||
throw new Error(`Missing config param: service.sms.${key}`)
|
||||
}
|
||||
}
|
||||
if (!code) {
|
||||
code = getVerifyCode()
|
||||
}
|
||||
let action
|
||||
switch (scene) {
|
||||
case 'login-by-sms':
|
||||
action = this.t('login')
|
||||
break
|
||||
default:
|
||||
action = this.t('verify-mobile')
|
||||
break
|
||||
}
|
||||
const sceneConfig = (smsConfig.scene || {})[scene] || {}
|
||||
if (!templateId) {
|
||||
templateId = sceneConfig.templateId
|
||||
}
|
||||
if (!templateId) {
|
||||
throw new Error('"templateId" is required')
|
||||
}
|
||||
const codeExpiresIn = sceneConfig.codeExpiresIn || smsConfig.codeExpiresIn
|
||||
await setMobileVerifyCode.call(this, {
|
||||
mobile,
|
||||
code,
|
||||
expiresIn: codeExpiresIn,
|
||||
scene
|
||||
})
|
||||
await uniCloud.sendSms({
|
||||
smsKey: smsConfig.smsKey,
|
||||
smsSecret: smsConfig.smsSecret,
|
||||
phone: mobile,
|
||||
templateId,
|
||||
data: {
|
||||
name: smsConfig.name,
|
||||
code,
|
||||
action,
|
||||
expMinute: '' + Math.round(codeExpiresIn / 60)
|
||||
}
|
||||
})
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendSmsCode
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
const {
|
||||
checkLoginUserRecord,
|
||||
postLogin
|
||||
} = require('./login')
|
||||
const {
|
||||
postRegister
|
||||
} = require('./register')
|
||||
const {
|
||||
findUser
|
||||
} = require('./account')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
|
||||
async function realPreUnifiedLogin (params = {}) {
|
||||
const {
|
||||
user,
|
||||
type
|
||||
} = params
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
const {
|
||||
total,
|
||||
userMatched
|
||||
} = await findUser({
|
||||
userQuery: user,
|
||||
authorizedApp: appId
|
||||
})
|
||||
if (userMatched.length === 0) {
|
||||
if (type === 'login') {
|
||||
if (total > 0) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
|
||||
}
|
||||
}
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_NOT_EXISTS
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'register',
|
||||
user
|
||||
}
|
||||
} if (userMatched.length === 1) {
|
||||
if (type === 'register') {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_EXISTS
|
||||
}
|
||||
}
|
||||
const userRecord = userMatched[0]
|
||||
checkLoginUserRecord(userRecord)
|
||||
return {
|
||||
type: 'login',
|
||||
user: userRecord
|
||||
}
|
||||
} else if (userMatched.length > 1) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_CONFLICT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function preUnifiedLogin (params = {}) {
|
||||
try {
|
||||
const result = await realPreUnifiedLogin.call(this, params)
|
||||
return result
|
||||
} catch (error) {
|
||||
await this.middleware.uniIdLog({
|
||||
success: false
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function postUnifiedLogin (params = {}) {
|
||||
const {
|
||||
user,
|
||||
extraData = {},
|
||||
isThirdParty = false,
|
||||
type,
|
||||
inviteCode
|
||||
} = params
|
||||
let result
|
||||
if (type === 'login') {
|
||||
result = await postLogin.call(this, {
|
||||
user,
|
||||
extraData,
|
||||
isThirdParty
|
||||
})
|
||||
} else if (type === 'register') {
|
||||
result = await postRegister.call(this, {
|
||||
user,
|
||||
extraData,
|
||||
isThirdParty,
|
||||
inviteCode
|
||||
})
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
preUnifiedLogin,
|
||||
postUnifiedLogin
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
async function getPhoneNumber ({
|
||||
// eslint-disable-next-line camelcase
|
||||
access_token,
|
||||
openid
|
||||
} = {}) {
|
||||
const requiredParams = ['apiKey', 'apiSecret']
|
||||
const univerifyConfig = (this.config.service && this.config.service.univerify) || {}
|
||||
for (let i = 0; i < requiredParams.length; i++) {
|
||||
const key = requiredParams[i]
|
||||
if (!univerifyConfig[key]) {
|
||||
throw new Error(`Missing config param: service.univerify.${key}`)
|
||||
}
|
||||
}
|
||||
return uniCloud.getPhoneNumber({
|
||||
provider: 'univerify',
|
||||
appid: this.getUniversalClientInfo().appId,
|
||||
apiKey: univerifyConfig.apiKey,
|
||||
apiSecret: univerifyConfig.apiSecret,
|
||||
// eslint-disable-next-line camelcase
|
||||
access_token,
|
||||
openid
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPhoneNumber
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const {
|
||||
userCollection
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
USER_STATUS
|
||||
} = require('../../common/constants')
|
||||
async function setUserStatus (uid, status) {
|
||||
const updateData = {
|
||||
status
|
||||
}
|
||||
if (status !== USER_STATUS.NORMAL) {
|
||||
updateData.valid_token_date = Date.now()
|
||||
}
|
||||
await userCollection.doc(uid).update({
|
||||
status
|
||||
})
|
||||
// TODO 此接口尚不完善,例如注销后其他客户端可能存在有效token,支持Redis后此处会补充额外逻辑
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setUserStatus
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
let redisEnable = null
|
||||
function getRedisEnable() {
|
||||
// 未用到的时候不调用redis接口,节省一些连接数
|
||||
if (redisEnable !== null) {
|
||||
return redisEnable
|
||||
}
|
||||
try {
|
||||
uniCloud.redis()
|
||||
redisEnable = true
|
||||
} catch (error) {
|
||||
redisEnable = false
|
||||
}
|
||||
return redisEnable
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRedisEnable
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
const {
|
||||
dbCmd,
|
||||
verifyCollection
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
const {
|
||||
getVerifyCode
|
||||
} = require('../../common/utils')
|
||||
|
||||
async function setVerifyCode ({
|
||||
mobile,
|
||||
email,
|
||||
code,
|
||||
expiresIn,
|
||||
scene
|
||||
} = {}) {
|
||||
const now = Date.now()
|
||||
const record = {
|
||||
mobile,
|
||||
email,
|
||||
scene,
|
||||
code: code || getVerifyCode(),
|
||||
state: 0,
|
||||
ip: this.getUniversalClientInfo().clientIP,
|
||||
created_date: now,
|
||||
expired_date: now + expiresIn * 1000
|
||||
}
|
||||
await verifyCollection.add(record)
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
async function setEmailVerifyCode ({
|
||||
email,
|
||||
code,
|
||||
expiresIn,
|
||||
scene
|
||||
} = {}) {
|
||||
email = email && email.trim()
|
||||
if (!email) {
|
||||
throw {
|
||||
errCode: ERROR.INVALID_EMAIL
|
||||
}
|
||||
}
|
||||
email = email.toLowerCase()
|
||||
return setVerifyCode.call(this, {
|
||||
email,
|
||||
code,
|
||||
expiresIn,
|
||||
scene
|
||||
})
|
||||
}
|
||||
|
||||
async function setMobileVerifyCode ({
|
||||
mobile,
|
||||
code,
|
||||
expiresIn,
|
||||
scene
|
||||
} = {}) {
|
||||
mobile = mobile && mobile.trim()
|
||||
if (!mobile) {
|
||||
throw {
|
||||
errCode: ERROR.INVALID_MOBILE
|
||||
}
|
||||
}
|
||||
return setVerifyCode.call(this, {
|
||||
mobile,
|
||||
code,
|
||||
expiresIn,
|
||||
scene
|
||||
})
|
||||
}
|
||||
|
||||
async function verifyEmailCode ({
|
||||
email,
|
||||
code,
|
||||
scene
|
||||
} = {}) {
|
||||
email = email && email.trim()
|
||||
if (!email) {
|
||||
throw {
|
||||
errCode: ERROR.INVALID_EMAIL
|
||||
}
|
||||
}
|
||||
email = email.toLowerCase()
|
||||
const {
|
||||
data: codeRecord
|
||||
} = await verifyCollection.where({
|
||||
email,
|
||||
scene,
|
||||
code,
|
||||
state: 0,
|
||||
expired_date: dbCmd.gt(Date.now())
|
||||
}).limit(1).get()
|
||||
|
||||
if (codeRecord.length === 0) {
|
||||
throw {
|
||||
errCode: ERROR.EMAIL_VERIFY_CODE_ERROR
|
||||
}
|
||||
}
|
||||
await verifyCollection.doc(codeRecord[0]._id).update({
|
||||
state: 1
|
||||
})
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyMobileCode ({
|
||||
mobile,
|
||||
code,
|
||||
scene
|
||||
} = {}) {
|
||||
mobile = mobile && mobile.trim()
|
||||
if (!mobile) {
|
||||
throw {
|
||||
errCode: ERROR.INVALID_MOBILE
|
||||
}
|
||||
}
|
||||
const {
|
||||
data: codeRecord
|
||||
} = await verifyCollection.where({
|
||||
mobile,
|
||||
scene,
|
||||
code,
|
||||
state: 0,
|
||||
expired_date: dbCmd.gt(Date.now())
|
||||
}).limit(1).get()
|
||||
|
||||
if (codeRecord.length === 0) {
|
||||
throw {
|
||||
errCode: ERROR.MOBILE_VERIFY_CODE_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
await verifyCollection.doc(codeRecord[0]._id).update({
|
||||
state: 1
|
||||
})
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyEmailCode,
|
||||
verifyMobileCode,
|
||||
setEmailVerifyCode,
|
||||
setMobileVerifyCode
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
const crypto = require('crypto')
|
||||
const {
|
||||
userCollection
|
||||
} = require('../../common/constants')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../../common/error')
|
||||
const {
|
||||
getRedisEnable
|
||||
} = require('./utils')
|
||||
const {
|
||||
openDataCollection
|
||||
} = require('../../common/constants')
|
||||
|
||||
function decryptWeixinData ({
|
||||
encryptedData,
|
||||
sessionKey,
|
||||
iv
|
||||
} = {}) {
|
||||
const oauthConfig = this.configUtils.getOauthConfig({
|
||||
provider: 'weixin'
|
||||
})
|
||||
const decipher = crypto.createDecipheriv(
|
||||
'aes-128-cbc',
|
||||
Buffer.from(sessionKey, 'base64'),
|
||||
Buffer.from(iv, 'base64')
|
||||
)
|
||||
// 设置自动 padding 为 true,删除填充补位
|
||||
decipher.setAutoPadding(true)
|
||||
let decoded
|
||||
decoded = decipher.update(encryptedData, 'base64', 'utf8')
|
||||
decoded += decipher.final('utf8')
|
||||
decoded = JSON.parse(decoded)
|
||||
if (decoded.watermark.appid !== oauthConfig.appid) {
|
||||
throw new Error('Invalid wechat appid in decode content')
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
function getWeixinPlatform () {
|
||||
const platform = this.clientPlatform
|
||||
const userAgent = this.getUniversalClientInfo().userAgent
|
||||
switch (platform) {
|
||||
case 'app':
|
||||
case 'app-plus':
|
||||
return 'app'
|
||||
case 'mp-weixin':
|
||||
return 'mp'
|
||||
case 'h5':
|
||||
case 'web':
|
||||
return userAgent.indexOf('MicroMessenger') > -1 ? 'h5' : 'web'
|
||||
default:
|
||||
throw new Error('Unsupported weixin platform')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWeixinUserKey ({
|
||||
openid,
|
||||
sessionKey, // 微信小程序用户sessionKey
|
||||
accessToken, // App端微信用户accessToken
|
||||
refreshToken, // App端微信用户refreshToken
|
||||
accessTokenExpired // App端微信用户accessToken过期时间
|
||||
} = {}) {
|
||||
// 微信公众平台、开放平台refreshToken有效期均为30天(微信没有在网络请求里面返回30天这个值,务必注意未来可能出现调整,需及时更新此处逻辑)。
|
||||
// 此前QQ开放平台有调整过accessToken的过期时间:[access_token有效期由90天缩短至30天](https://wiki.connect.qq.com/%E3%80%90qq%E4%BA%92%E8%81%94%E3%80%91access_token%E6%9C%89%E6%95%88%E6%9C%9F%E8%B0%83%E6%95%B4)
|
||||
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
const weixinPlatform = getWeixinPlatform.call(this)
|
||||
const keyObj = {
|
||||
dcloudAppid: appId,
|
||||
openid,
|
||||
platform: 'weixin-' + weixinPlatform
|
||||
}
|
||||
switch (weixinPlatform) {
|
||||
case 'mp':
|
||||
await this.uniOpenBridge.setSessionKey(keyObj, {
|
||||
session_key: sessionKey
|
||||
}, 30 * 24 * 60 * 60)
|
||||
break
|
||||
case 'app':
|
||||
case 'h5':
|
||||
case 'web':
|
||||
await this.uniOpenBridge.setUserAccessToken(keyObj, {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
access_token_expired: accessTokenExpired
|
||||
}, 30 * 24 * 60 * 60)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSecureNetworkCache ({
|
||||
code,
|
||||
openid,
|
||||
unionid,
|
||||
sessionKey
|
||||
}) {
|
||||
const {
|
||||
appId
|
||||
} = this.getUniversalClientInfo()
|
||||
const key = `uni-id:${appId}:weixin-mp:code:${code}:secure-network-cache`
|
||||
const value = JSON.stringify({
|
||||
openid,
|
||||
unionid,
|
||||
session_key: sessionKey
|
||||
})
|
||||
// 此处存储的是code的缓存,设置有效期和token一致
|
||||
const expiredSeconds = this.config.tokenExpiresIn || 3 * 24 * 60 * 60
|
||||
|
||||
await openDataCollection.doc(key).set({
|
||||
value,
|
||||
expired: Date.now() + expiredSeconds * 1000
|
||||
})
|
||||
const isRedisEnable = getRedisEnable()
|
||||
if (isRedisEnable) {
|
||||
const redis = uniCloud.redis()
|
||||
await redis.set(key, value, 'EX', expiredSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
function generateWeixinCache ({
|
||||
sessionKey, // 微信小程序用户sessionKey
|
||||
accessToken, // App端微信用户accessToken
|
||||
refreshToken, // App端微信用户refreshToken
|
||||
accessTokenExpired // App端微信用户accessToken过期时间
|
||||
} = {}) {
|
||||
const platform = getWeixinPlatform.call(this)
|
||||
let cache
|
||||
switch (platform) {
|
||||
case 'app':
|
||||
case 'h5':
|
||||
case 'web':
|
||||
cache = {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
access_token_expired: accessTokenExpired
|
||||
}
|
||||
break
|
||||
case 'mp':
|
||||
cache = {
|
||||
session_key: sessionKey
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error('Unsupported weixin platform')
|
||||
}
|
||||
return {
|
||||
third_party: {
|
||||
[`${platform}_weixin`]: cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWeixinOpenid ({
|
||||
userRecord
|
||||
} = {}) {
|
||||
const weixinPlatform = getWeixinPlatform.call(this)
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
const wxOpenidObj = userRecord.wx_openid
|
||||
if (!wxOpenidObj) {
|
||||
return
|
||||
}
|
||||
return wxOpenidObj[`${weixinPlatform}_${appId}`] || wxOpenidObj[weixinPlatform]
|
||||
}
|
||||
|
||||
async function getWeixinCacheFallback ({
|
||||
userRecord,
|
||||
key
|
||||
} = {}) {
|
||||
const platform = getWeixinPlatform.call(this)
|
||||
const thirdParty = userRecord && userRecord.third_party
|
||||
if (!thirdParty) {
|
||||
return
|
||||
}
|
||||
const weixinCache = thirdParty[`${platform}_weixin`]
|
||||
return weixinCache && weixinCache[key]
|
||||
}
|
||||
|
||||
async function getWeixinCache ({
|
||||
uid,
|
||||
userRecord,
|
||||
key
|
||||
} = {}) {
|
||||
const weixinPlatform = getWeixinPlatform.call(this)
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
if (!userRecord) {
|
||||
const getUserRes = await userCollection.doc(uid).get()
|
||||
userRecord = getUserRes.data[0]
|
||||
}
|
||||
if (!userRecord) {
|
||||
throw {
|
||||
errCode: ERROR.ACCOUNT_NOT_EXISTS
|
||||
}
|
||||
}
|
||||
const openid = getWeixinOpenid.call(this, {
|
||||
userRecord
|
||||
})
|
||||
const getCacheMethod = weixinPlatform === 'mp' ? 'getSessionKey' : 'getUserAccessToken'
|
||||
const userKey = await this.uniOpenBridge[getCacheMethod]({
|
||||
dcloudAppid: appId,
|
||||
platform: 'weixin-' + weixinPlatform,
|
||||
openid
|
||||
})
|
||||
if (userKey) {
|
||||
return userKey[key]
|
||||
}
|
||||
return getWeixinCacheFallback({
|
||||
userRecord,
|
||||
key
|
||||
})
|
||||
}
|
||||
|
||||
async function getWeixinAccessToken () {
|
||||
const weixinPlatform = getWeixinPlatform.call(this)
|
||||
const appId = this.getUniversalClientInfo().appId
|
||||
|
||||
const cache = await this.uniOpenBridge.getAccessToken({
|
||||
dcloudAppid: appId,
|
||||
platform: 'weixin-' + weixinPlatform
|
||||
})
|
||||
|
||||
return cache.access_token
|
||||
}
|
||||
module.exports = {
|
||||
decryptWeixinData,
|
||||
getWeixinPlatform,
|
||||
generateWeixinCache,
|
||||
getWeixinCache,
|
||||
saveWeixinUserKey,
|
||||
getWeixinAccessToken,
|
||||
saveSecureNetworkCache
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
const methodPermission = require('../config/permission')
|
||||
const {
|
||||
ERROR
|
||||
} = require('../common/error')
|
||||
|
||||
function isAccessAllowed (user, setting) {
|
||||
const {
|
||||
role: userRole = [],
|
||||
permission: userPermission = []
|
||||
} = user
|
||||
const {
|
||||
role: settingRole = [],
|
||||
permission: settingPermission = []
|
||||
} = setting
|
||||
if (userRole.includes('admin')) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
settingRole.length > 0 &&
|
||||
settingRole.every(item => !userRole.includes(item))
|
||||
) {
|
||||
throw {
|
||||
errCode: ERROR.PERMISSION_ERROR
|
||||
}
|
||||
}
|
||||
if (
|
||||
settingPermission.length > 0 &&
|
||||
settingPermission.every(item => !userPermission.includes(item))
|
||||
) {
|
||||
throw {
|
||||
errCode: ERROR.PERMISSION_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async function () {
|
||||
const methodName = this.getMethodName()
|
||||
if (!(methodName in methodPermission)) {
|
||||
return
|
||||
}
|
||||
const {
|
||||
auth,
|
||||
role,
|
||||
permission
|
||||
} = methodPermission[methodName]
|
||||
if (auth || role || permission) {
|
||||
await this.middleware.auth()
|
||||
}
|
||||
if (role && role.length === 0) {
|
||||
throw new Error('[AccessControl]Empty role array is not supported')
|
||||
}
|
||||
if (permission && permission.length === 0) {
|
||||
throw new Error('[AccessControl]Empty permission array is not supported')
|
||||
}
|
||||
return isAccessAllowed(this.authInfo, {
|
||||
role,
|
||||
permission
|
||||
})
|
||||
}
|
||||