首次完整推送,

V:1.20240808.006
This commit is contained in:
fm453
2024-08-13 18:32:37 +08:00
parent 15be3e9373
commit c62d15b288
939 changed files with 111777 additions and 0 deletions

View File

@ -0,0 +1,181 @@
## 1.1.202024-04-28
- uni-id-co 兼容uni-app-x对客户端uniPlatform的调整uni-app-x内uniPlatform区分app-android、app-ios
## 1.1.192024-03-20
- uni-id-co 修复 实人认证的认证照片在阿里云服务空间没有保存到指定路径下的Bug
- uni-id-co 修复 云对象开发依赖未移除的Bug
## 1.1.182024-02-20
- 修复 PC设置头像无效的问题
## 1.1.172023-12-14
- uni-id-co 移除一键登录、短信的调用凭据
## 1.1.162023-10-18
- 修复 当不满足一键登录时页面回退无法继续登录的问题
## 1.1.152023-07-13
- uni-id-co 修复 QQ登录时不存在头像时报错的问题
## 1.1.142023-05-19
- 修复 退出登录不会跳转至登录页的问题
## 1.1.132023-05-10
- 修复 启用摇树优化 报错的问题
## 1.1.122023-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.112023-03-24
- 修复 tabbar页面因为token无效而强制跳转至登录页面url参数包含`uniIdRedirectUrl`)后无法返回的问题
## 1.1.102023-03-24
- 修复 PC微信扫码登录跳转地址错误
- uni-id-co 新增 请求鉴权支持 uni-cloud-s2s 模块验证签名 [uni-cloud-s2s文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-s2s.html)
## 1.1.92023-03-24
- 修复 跳转至登录页面的url参数包含`uniIdRedirectUrl`后无法返回的问题
## 1.1.82023-03-02
- 修复 调试模式下没有对微信授权手机号登录方式进行配置检测
## 1.1.72023-02-27
- 【重要】新增 实名认证功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#frv)
## 1.1.62023-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.52023-02-23
- 更新 微信小程序端 更新头像信息,如果是使用微信的头像则不再调用裁剪接口
## 1.1.42023-02-21
- 修复 部分情况下 `uniIdRedirectUrl` 参数无效的问题
## 1.1.32023-02-20
- 修复 非微信小程序端报`TypeError: uni.hideHomeButton is not a function`的问题
## 1.1.22023-02-10
- 新增 微信小程序端 首页需强制登录时,隐藏返回首页按钮
- uni-id-co 新增 外部联登后修改用户信息接口(updateUserInfoByExternal) [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo)
- uni-id-co 优化外部联登接口(登录、注册)逻辑
## 1.1.12023-02-02
- 新增 微信小程序端 支持选择使用微信资料的“头像”和“昵称” 设置用户资料 [详情参考](https://wdoc-76491.picgzc.qpic.cn/MTY4ODg1MDUyNzQyMDUxNw_21263_rTNhg68FTngQGdvQ_1647431233?w=1280&h=695.7176470588236)
## 1.1.02023-01-31
- 【重要】优化 小程序端资源包大小运行时大小为731KB发行后为583KB可以直接将本插件作为分包使用
- 更新 微信小程序端 上传头像功能 用`wx.cropImage`实现图片裁剪
- 修复 选择一键登录时会先显示 非密码登录页面的问题
- 修复 一键登录 点击右上角的关闭按钮没有返回上一页的问题
## 1.0.412023-01-16
- 优化 压缩依赖的文件资源大小
## 1.0.402023-01-16
- 更新依赖的 验证码插件`uni-captcha`版本的版本为 0.6.4 修复 部分情况下APP端无法获取验证码的问题 [详情参考](https://ext.dcloud.net.cn/plugin?id=4048)
- 修复 客户端token过期后点击退出登录按钮报错的问题
- uni-id-co 修复 updateUser 接口`手机号``邮箱`参数值为空字符串时,修改无效的问题
## 1.0.392022-12-28
- uni-id-co 修复 URL化时第三方登录无法获取 uniPlatform 参数
- uni-id-co 修复 validator error
## 1.0.382022-12-26
- uni-id-co 优化 手机号与邮箱验证规则为空字符串时不校验
## 1.0.372022-12-09
- 优化admin端样式
## 1.0.362022-12-08
- uni-id-co 修复 `updateUser` 接口部分参数为空时数据修改异常
## 1.0.352022-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.342022-11-29
- 优化 toast 错误提示时间为3秒
- uni-id-co 修复 无法从 clientInfo 中获取 uniIdToken
## 1.0.332022-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.322022-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.312022-11-16
- uni-id-co 修复 验证码可能无法收到的bug
## 1.0.302022-11-11
- uni-id-co 修复 用户只有openid时绑定微信/QQ报错
## 1.0.292022-11-10
- uni-id-co 支持URL化方式请求 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#adapter-http)
## 1.0.282022-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.272022-10-26
- uni-id-co 新增 secureNetworkHandshakeByWeixin 接口,用于建立和微信小程序的安全网络连接
## 1.0.262022-10-18
- 修复 uni-id-pages 导入时异常的Bug
## 1.0.252022-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.242022-10-08
- 修复 报uni-id-users表schema内错误的bug
## 1.0.232022-10-08
- 修复 vue3下vite编译发行打包失败
- 修复 某些情况下注册账号报TypeErroeCannot read properties of undefined reading showToast的错误
## 1.0.222022-09-23
- 修复 某些情况下修改密码报“两次输入密码不一致”的bug
## 1.0.212022-09-21
- 修复 store.hasLogin的值在某些情况下会出错的bug
## 1.0.202022-09-21
- 新增 store 账号信息状态管理,详情:用户中心页面 路径:`/uni_modules/uni-id-pages/pages/userinfo/userinfo`
## 1.0.192022-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.182022-09-20
- 修复 微信小程序端 WXSS 编译报错的bug
## 1.0.172022.09-19
- 修复 无法退出登录的bug
## 1.0.162022-09-19
- 修复 在 Edge 浏览器下 input[type="password"] 会出现浏览器自带的密码查看按钮
- 优化 退出登录重定向页面为 uniIdRouter.loginPage
- 新增 注册账号页面支持返回登录页面
## 1.0.152022-09-19
- 更新表结构解决在uni-admin中部分clientDB操作没有权限的问题
## 1.0.142022-09-16
- 修改 配置项`isAdmin`默认值为`false`
## 1.0.132022-09-16
- 新增 管理员注册页面
- 新增 配置项`isAdmin`区分是否为管理端
- 新增 登录成功后自动跳转;跳转优先级:路由携带(`uniIdRedirectUrl`参数) > 返回上一路由 > 跳转首页
- uni-id-co 优化 注册管理员时管理员存在提示文案
## 1.0.122022-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.112022-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.102022-08-25
- 修复 导入uni-id-pages插件时未自动导入uni-open-bridge-common的Bug
## 1.0.92022-08-23
- 修复 uni-id-co 缺失uni-open-bridge-common依赖的Bug
## 1.0.82022-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.72022-07-19
- 修复 uni-id-co接口 logout时没有删除token的Bug
## 1.0.62022-07-13
- 新增 允许覆盖内置校验规则 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#custom-validator)
- 修复 app端clientInfo.appVersionCode为数字导致校验无法通过的Bug
## 1.0.52022-07-11
修复 微信小程序调用uni-id-co接口报错的Bug [详情](https://ask.dcloud.net.cn/question/148877)
## 1.0.42022-07-06
- uni-id-co增加clientInfo字段类型校验
- 监听token更新时机同步客户端push_clientid至uni-id-device表改为同步客户端push_clientid至uni-id-device表和opendb-device表
## 1.0.32022-07-05
新增监听token更新时机同步客户端push_clientid至uni-id-device表
## 1.0.22022-07-04
修复微信小程序登录时无unionid报错的Bug [详情](https://ask.dcloud.net.cn/question/148016)
## 1.0.12022-06-28
添加相关uni-id表
## 1.0.02022-06-23
正式版

View 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

View 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

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

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

View 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

View File

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

View File

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

View File

@ -0,0 +1,198 @@
<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端用内置的原生裁剪
// #ifdef H5
if (!this.isPC) {
filePath = await new Promise((callback) => {
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
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>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,171 @@
<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" @click="getUserProfile">确定</button>
</view>
</view>
</uni-popup>
</template>
<script>
const db = uniCloud.database();
const usersTable = db.collection('uni-id-users')
let userId = ''
export default {
emits:['next'],
data() {
return {}
},
methods: {
async open(uid){
userId = uid
this.$refs.popup.open()
},
async getUserProfile(){
uni.showLoading();
let res = await new Promise((callBack) => {
uni.getUserProfile({
desc: "用于设置账户昵称和头像",
complete: (e) => {
callBack(e)
}
})
})
if(res.errMsg != "getUserProfile:ok"){
return this.closeMe()
}
let {avatarUrl,nickName} = res.userInfo;
let tempFilePath = await new Promise((callBack)=>{
uni.downloadFile({
url: avatarUrl,
success: (res) => {
if (res.statusCode === 200) {
// console.log('下载成功');
callBack(res.tempFilePath)
}
callBack()
},
fail: (err) => {
console.error(err)
},
complete: (e) => {
// console.log("downloadFile",e);
}
});
})
const extName = tempFilePath.split('.').pop() || 'jpg'
const cloudPath = 'user/avatar/'+ userId+'/'+Date.now()+'-avatar.'+extName;
const result = await uniCloud.uploadFile({
filePath: tempFilePath,
cloudPath,
fileType:'image'
});
let userInfo = {
"nickname":nickName,
"avatar_file":{
name:cloudPath,
extname:"jpg",
url:result.fileID
}
}
this.doUpdate(userInfo,()=>{
this.$refs.popup.close()
})
},
closeMe(e){
uni.showLoading();
this.doUpdate({nickname:"匿名微信用户"},()=>{
uni.hideLoading()
this.$refs.popup.close()
})
},
doUpdate(data,callback){
// 使用 clientDB 提交数据
usersTable.where('_id==$env.uid').update(data).then((res) => {
callback(res)
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
callback(err)
}).finally(() => {
this.$emit('next')
uni.hideLoading()
})
}
}
}
</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>

View 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://public.hiluker.com/private.html', // 用户服务协议链接
privacyUrl: 'https://public.hiluker.com/private.html', // 隐私政策条款链接
// 哪些场景下显示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: 'weak',
/**
* 登录后允许用户设置密码(只针对未设置密码得用户)
* 开启此功能将 setPasswordAfterLogin 设置为 true 即可
* "setPasswordAfterLogin": false
*
* 如果允许用户跳过设置密码 将 allowSkip 设置为 true
* "setPasswordAfterLogin": {
* "allowSkip": true
* }
* */
setPasswordAfterLogin: false
}

View File

@ -0,0 +1,20 @@
export default {
debug: false,
isAdmin: false,
loginTypes: [
'smsCode',
// #ifdef APP
'univerify',
// #endif
'username'
],
agreements: {
serviceUrl: 'https://public.hiluker.com/ctms/client/service.html',
privacyUrl: 'https://public.hiluker.com/ctms/client/private.html',
scope: [
'register', 'login', 'realNameVerify'
]
},
passwordStrength: 'weak',
setPasswordAfterLogin: false
}

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

View File

@ -0,0 +1,103 @@
{
"id": "uni-id-pages",
"displayName": "uni-id-pages",
"version": "1.1.20",
"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",
"alipay": "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": {
}
}

View File

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

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

View File

@ -0,0 +1,257 @@
<!-- 免密登录页 -->
<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 (config.loginTypes.includes('univerify') && this.type == "univerify") {
uni.preLogin({
provider: 'univerify',
success: () => {
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')
},
fail: (err) => {
console.log(err);
if (config.loginTypes.length > 1) {
this.$refs.uniFabLogin.login_before(config.loginTypes[1])
} else {
uni.showModal({
content: err.message,
showCancel: false
});
}
}
})
}
//#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>

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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>
![输入图片说明](https://images.gitee.com/uploads/images/2020/1122/222521_bb543f96_518581.jpeg "微信图片编辑_20201122220352.jpg")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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里的页面的话打包时不会把这些页面打包进去不影响发行后的包体积

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,263 @@
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)
}
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
}

View File

@ -0,0 +1,443 @@
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'
}
}
// 特殊符号 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]
}
_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 = this._isMatchBaseType(val, child)
break
case 'array':
pass = this._isMatchArrayType(val, child)
break
default:
break
}
if (pass) {
return true
}
}
return false
}
_isMatchBaseType(val, rule) {
const method = this.getRealBaseValidator(rule.type)
if (typeof method !== 'function') {
throw new Error(`invalid schema type: ${rule.type}`)
}
const validateRes = method(val)
if (validateRes && validateRes.errCode) {
return false
}
return true
}
_isMatchArrayType(arr, rule) {
if (getType(arr) !== 'array') {
return false
}
if (rule.children && rule.children.length && arr.some(item => !this._isMatchUnionType(item, rule))) {
return false
}
return true
}
get validator() {
const _this = this
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 (!_this._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
}

View File

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

View File

@ -0,0 +1,696 @@
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':
case 'app-android':
case 'app-ios':
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.validate({
// timestamp: 123456789
// }, {
// timestamp: 'timestamp'
// })
// this.validator.validate({
// timestampList: [123456789, 123123123123]
// }, {
// timestampList: 'array<timestamp>'
// })
// // 甚至更复杂的写法
// this.validator.validate({
// timestamp: [123456789123123123, 123123123123]
// }, {
// timestamp: 'timestamp|array<timestamp|number>'
// })
// 挂载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
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
# 说明
此目录内为uni-id-co基础能力不建议直接修改。如果你发现有些逻辑加入会更好或者此部分代码有Bug可以向我们提交PR仓库地址[]()。如果有特殊的需求也可以在[论坛](https://ask.dcloud.net.cn/)提出,我们可以讨论下如何实现。

File diff suppressed because one or more lines are too long

View File

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

View File

@ -0,0 +1,10 @@
module.exports = {
code2Session: {
// args (fromArgs) {
// return fromArgs
// },
returnValue: {
openid: 'userId'
}
}
}

View File

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

View File

@ -0,0 +1,79 @@
const rsaPublicKeyPem = require('../rsa-public-key-pem')
const {
jwtVerify
} = require('../../../npm/index')
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 = jwtVerify(
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
}
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,137 @@
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':
case 'app-android':
case 'app-ios':
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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,154 @@
const {
userCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
function getQQPlatform () {
const platform = this.clientPlatform
switch (platform) {
case 'app':
case 'app-plus':
case 'app-android':
case 'app-ios':
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
}

View File

@ -0,0 +1,231 @@
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('../npm/index')
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
}

View File

@ -0,0 +1,166 @@
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('../npm/index')
/**
*
* @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
}

View File

@ -0,0 +1,79 @@
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',
'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
}

View File

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

View File

@ -0,0 +1,27 @@
async function getPhoneNumber ({
// eslint-disable-next-line camelcase
access_token,
openid
} = {}) {
const requiredParams = []
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
}

View File

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

View File

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

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