提交 89745c0e 编写于 作者: DCloud_JSON's avatar DCloud_JSON

init

上级
/.hbuilderx/
/node_modules/
**/node_modules/**
/unpackage/
/vue.config.js
.DS_Store
/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/
<script lang="uts">
let firstBackTime = 0
import uniIdPageInit from '@/uni_modules/uni-id-pages-x/init.uts';
export default {
onLaunch: function(){
console.log('App Launch')
uniIdPageInit()
},
onShow: function () {
console.log('App Show')
},
onHide: function () {
console.log('App Hide')
},
onLastPageBackPress: function () {
console.log('App LastPageBackPress')
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
position: 'bottom',
})
firstBackTime = Date.now()
setTimeout(() => {
firstBackTime = 0
}, 2000)
} else if (Date.now() - firstBackTime < 2000) {
firstBackTime = Date.now()
uni.exit()
}
},
onExit: function () {
console.log('App Exit')
},
}
</script>
<style>
/*每个页面公共css */
.uni-row {
flex-direction: row;
}
.uni-column {
flex-direction: column;
}
</style>
\ No newline at end of file
@font-face {
font-family: "UniIconsLight";
src: url('./uniicons.ttf') format('truetype');
}
.uni-icons {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.uniui-back:before {
content: "\e600";
}
<template>
<view class="uni-navbar">
<view class="uni-navbar-inner" :style="navbarStyle">
<view class="left-content" @click="back">
<text class="uni-icons">{{unicode}}</text>
</view>
<view class="uni-navbar-content" :class="{'is-left':isLeft}">
<slot name="title">{{title}}</slot>
</view>
<view class="right-content">
<slot name="right"></slot>
</view>
</view>
</view>
</template>
<script>
import iconpath from './uniicons.ttf'
export default {
name: "uni-navbar",
props: {
title: {
type: String,
default: ''
},
isLeft: {
type: Boolean,
default: false
}
},
data() {
return {
statusBarHeight: 0
};
},
computed: {
navbarStyle() : string {
return `margin-top:${this.statusBarHeight}px`
},
unicode() : string {
return '\ue600'
}
},
created() {
uni.loadFontFace({
global: false,
family: 'UniIconsFontFamily',
source: iconpath,
success() { },
fail(err) {
console.log(err);
},
})
const sys = uni.getSystemInfoSync()
const statusBarHeight = sys.statusBarHeight
this.statusBarHeight = statusBarHeight
},
mounted() {
// TODO 暂时加定时器,否则不生效
setTimeout(() => {
uni.setNavigationBarColor({
frontColor: '#000000',
backgroundColor: '#ffffff'
})
}, 100)
},
methods: {
back() {
uni.navigateBack({})
}
},
}
</script>
<style>
.uni-icons {
font-family: "UniIconsFontFamily" !important;
font-size: 22px;
font-style: normal;
color: #333;
}
.uni-navbar {
border: 1px #eee solid;
background-color: #fff;
}
.uni-navbar-inner {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 45px;
}
.left-content,
.right-content {
display: flex;
justify-content: center;
align-items: center;
width: 45px;
height: 100%;
}
.uni-navbar-content {
position: absolute;
height: 100%;
top: 0;
bottom: 0;
left: 45px;
right: 45px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.is-left {
justify-content: flex-start;
}
</style>
\ No newline at end of file
<template>
<view class="uni-navbar">
<view class="uni-navbar-inner" :style="navbarStyle">
<view class="left-content" @click="back">
<text class="uni-icons uniui-back"></text>
</view>
<view class="uni-navbar-content">
<slot>{{title}}</slot>
</view>
<view class="right-content">
<slot name="right"></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: "uni-navbar",
props: {
title: {
type: String,
default: ''
}
},
data() {
return {
statusBarHeight: 0
};
},
computed: {
navbarStyle() {
return `margin-top:${this.statusBarHeight}px`
},
},
created() {
const sys = uni.getSystemInfoSync()
const statusBarHeight = sys.statusBarHeight
this.statusBarHeight = statusBarHeight
},
methods: {
back() {
uni.navigateBack({})
}
},
}
</script>
<style>
@import './uni-icons.css';
.uni-icons {
font-family: UniIconsLight;
text-decoration: none;
text-align: center;
font-size: 22px;
font-style: normal;
color: #333;
}
.uni-navbar {
border: 1px #eee solid;
background-color: #fff;
}
.uni-navbar-inner {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 45px;
}
.left-content,
.right-content {
display: flex;
justify-content: center;
align-items: center;
width: 45px;
height: 100%;
}
.uni-navbar-content {
position: absolute;
height: 100%;
top: 0;
bottom: 0;
left: 45px;
right: 45px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
import App from './App'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
\ No newline at end of file
{
"name" : "hello uni-id-pages",
"appid" : "__UNI__17D54C2",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"uni-app-x" : {},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
// {
// "path" : "pages/test/test",
// "style" :
// {
// "navigationBarTitleText" : "",
// "enablePullDownRefresh" : false
// }
// },
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-id-pages-x"
}
}, {
"path": "uni_modules/uni-id-pages-x/pages/login/login",
"style": {
"navigationBarTitleText": "登录账号",
"enablePullDownRefresh": false
}
}, {
"path": "uni_modules/uni-id-pages-x/pages/userinfo/userinfo",
"style": {
"navigationBarTitleText": "我的资料",
"enablePullDownRefresh": false
}
},
{
"path": "uni_modules/uni-id-pages-x/pages/register/register",
"style": {
"navigationBarTitleText": "注册",
"enablePullDownRefresh": false
}
},
{
"path": "uni_modules/uni-id-pages-x/pages/retrieve/retrieve",
"style": {
"navigationBarTitleText": "重置密码",
"enablePullDownRefresh": false
}
},
{
"path": "uni_modules/uni-id-pages-x/pages/common/webview/webview",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
},
{
"path": "uni_modules/uni-id-pages-x/pages/userinfo/deactivate/deactivate",
"style": {
"navigationBarTitleText": "注销账号",
"enablePullDownRefresh": false
}
},
{
"path": "uni_modules/uni-id-pages-x/pages/userinfo/bindMobile/bindMobile",
"style": {
"navigationBarTitleText": "绑定手机号码",
"enablePullDownRefresh": false
}
},
{
"path": "uni_modules/uni-id-pages-x/pages/userinfo/setNickname/setNickname",
"style": {
"navigationBarTitleText": "设置昵称",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
// "app-plus": {
// "titleNView": {
// "buttons": [
// {
// "color": "#fff",
// "text": "完成",
// "float": "right",
// "fontWeight": "normal"
// }
// ]
// }
// }
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}
\ No newline at end of file
<template>
<view class="content">
<view class="userinfo" @click="toUserInfoPage">
<uni-id-pages-x-avatar class="avatar" ref="avatar" width="45px" height="45px" :readOnly="true" :border="false"></uni-id-pages-x-avatar>
<view class="openName">
<text class="openName-text">{{openName}}</text>
</view>
<uni-id-pages-x-icons type="right-arrow" color="#999" />
</view>
<view class="list">
<view class="list-item" hover-class="list-active" @click="toLogin('smsCode')">
<text class="list-item-text">手机验证码登录</text>
<uni-id-pages-x-icons type="right-arrow" color="#999" />
</view>
<view class="list-item" @click="toLogin('username')">
<text class="list-item-text">账号密码登录</text>
<uni-id-pages-x-icons type="right-arrow" color="#999" />
</view>
</view>
</view>
</template>
<script>
import { state } from '@/uni_modules/uni-id-pages-x/store.uts';
export default {
computed: {
userInfo() : UTSJSONObject {
return state.userInfo
},
isLogin() : boolean {
return state.isLogin
},
openName():string{
if(this.isLogin){
const nickname = userInfo['nickname'] as string;
if(nickname.length != 0){
return nickname
}else{
return userInfo['username'] as string
}
}else{
return '未登录'
}
}
},
data() {
return {}
},
onLoad() {},
methods: {
toLogin(loginType:string) {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/login/login?type=" + loginType
})
},
toUserInfoPage() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/userinfo/userinfo?showLoginManage=true"
})
},
}
}
</script>
<style lang="scss">
.content {
height: 100%;
background-color: #f5f5f5;
}
.userinfo {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 15px;
// padding-bottom: 30px;
background-color: #fff;
.openName {
flex: 1;
padding: 0 10px;
.openName-text {
font-size: 14px;
color: #333;
}
}
}
.list {
background-color: #fff;
margin-top: 15px;
.list-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 15px;
height: 50px;
border-bottom: 1px #f5f5f5 solid;
.list-item-text {
font-size: 14px;
}
}
}
.arrow {
}
</style>
\ No newline at end of file
<template>
<view>
<view v-if="inputIsShow">
<template v-for="i in 2" :key="i">
<input type="number" ref="input" class="input" placeholder="input" />
</template>
<button type="primary" @click="doFocus">doFocus</button>
</view>
<button @click="inputIsShow = !inputIsShow">{{inputIsShow?'隐藏':'显示'}}</button>
</view>
</template>
<script>
export default {
data() {
return {
inputIsShow: true
}
},
methods: {
doFocus() {
console.log('doFocus');
(this.$refs["input"] as Element[])[0].focus();
}
}
}
</script>
<style>
.input {
border: 1px solid #aaa;
margin: 15px;
padding: 5;
}
</style>
\ No newline at end of file
<template>
<view>
<input class="input" placeholder="请输入" v-model="text" @input="onInput" />
</view>
</template>
<script>
export default {
data() {
return {
text: ""
}
},
methods: {
onInput(e : InputEvent) {
e.stopPropagation()
e.preventDefault()
}
}
}
</script>
<style>
.input {
border: 1px solid #aaa;
margin: 15px;
padding: 5;
}
</style>
\ No newline at end of file
文件已添加
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;
## 0.7.2(2023-11-07)
- 新增 前端组件:uni-captcha.uvue、uni-popup-captcha
## 0.7.1(2023-11-07)
- 新增 前端组件:uni-captcha.uvue、uni-popup-captcha
## 0.7.0(2023-10-10)
- 新增 支持在`uni-config-center`中配置mode,可选值为svg和bmp,配置成bmp后可以在uniappx的uvue页面正常显示验证码(uvue不支持显示svg验证码)
## 0.6.4(2023-01-16)
- 修复 部分情况下APP端无法获取验证码的问题
## 0.6.3(2023-01-11)
- 修复 抖音小程序无法显示的Bug
- 修复 刷新时兼容 device_uuid
## 0.6.1(2022-06-23)
- 修复:部分返回值,不符合响应体规范的问题
## 0.6.0(2022-05-27)
- 新增:支持在`uni-config-center`中根据场景值配置
- 修复:弹窗式验证码,输入内容后点击取消,重新打开验证码的值仍然存在的问题
## 0.5.2(2022-05-19)
- 修复在Vue3的兼容问题
## 0.5.1(2022-05-18)
- 修复在某些情况下微信小程序端验证码显示错误的问题
## 0.5.0(2022-05-17)
- 新增支持在`uni-captcha-co`->`config`配置验证码
## 0.4.1(2022-05-16)
- 新增示例项目
## 0.4.0(2022-05-16)
- 集成创建、刷新、显示验证码的云端一体验证码组件
- 云对象`uni-captcha-co`集成获取验证码的api,`getImageCaptcha`
## 0.3.1(2022-05-13)
- 新增 返回值符合响应体规范
## 0.3.0(2022-05-13)
- 新增 支持 uni-config-center 配置
## 0.2.2(2022-04-25)
- 修复 0.2.1 版本引起的使用 image 组件验证码不显示的Bug
## 0.2.1(2022-04-18)
- 更新 优化字体
## 0.2.0(2022-04-14)
- 新增 使用 svg 表现形式更好
- 新增 使用字体,可以任意替换默认字体
- 新增 支持设置字体大小
- 新增 支持忽略某些字符
- 注意 更新之后请重新上传公共模块
## 0.1.0(2021-03-01)
- 调整为uni_modules目录规范
<template>
<view class="captcha-box">
<view class="captcha-img-box">
<image class="loding" src="/uni_modules/uni-captcha/static/run.gif" v-if="loging" mode="widthFix" />
<image class="captcha-img" :class="{opacity:loging}" @click="getImageCaptcha(true)" :src="captchaBase64" mode="widthFix" />
</view>
<input @blur="focusCaptchaInput = false" @focus="focusCaptchaInput = true" :focus="focusCaptchaInput" type="digit" class="captcha" :inputBorder="false"
maxlength="4" v-model="val" placeholder="请输入验证码" :cursor-spacing="cursorSpacing" />
</view>
</template>
<script>
export default {
emits: ["modelValue"],
props: {
cursorSpacing: {
type: Number,
default: 100
},
modelValue: {
type: String,
default: ""
},
value: {
type: String,
default: ""
},
scene: {
type: String,
default: ""
},
focus: {
type: Boolean,
default: false
}
},
data() {
return {
focusCaptchaInput: false,
captchaBase64: "" as string,
loging: false,
val: ""
};
},
watch: {
value: {
handler(value : string) {
// console.log('setvue', value);
this.val = value
},
immediate: true
},
modelValue: {
handler(modelValue : string) {
// console.log('setvue', modelValue);
this.val = modelValue
},
immediate: true
},
scene: {
handler(scene : string) {
if (scene.length != 0) {
this.getImageCaptcha(this.focus)
} else {
uni.showToast({
title: 'scene不能为空',
icon: 'none'
});
}
},
immediate: true
},
val(value : string) {
// console.log('setvue', value);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', value);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', value)
// #endif
}
},
methods: {
setFocus(state:boolean){
this.focusCaptchaInput = state
},
getImageCaptcha(focus : boolean) {
this.loging = true
if (focus) {
this.val = ''
this.focusCaptchaInput = true
}
const uniIdCo = uniCloud.importObject("uni-captcha-co", {
customUI: true
})
uniIdCo.getImageCaptcha({
scene: this.scene,
isUniAppX:true
}).then((result : UTSJSONObject) => {
this.captchaBase64 = (result.getString('captchaBase64') as string)
})
.catch<void>((err : any | null) : void => {
const error = err as UniCloudError
console.error(error)
console.error(error.code)
uni.showToast({
title: error.message,
icon: 'none'
});
})
.finally(()=> {
this.loging = false
})
}
}
}
</script>
<style lang="scss" scoped>
.captcha-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: flex-end;
flex: 1;
}
.captcha-img-box,
.captcha {
height: 44px;
}
.captcha {
flex: 1;
}
.captcha-img-box {
position: relative;
background-color: #FEFAE7;
}
.captcha {
background-color: #F8F8F8;
font-size: 14px;
flex: 1;
padding: 0 20rpx;
margin-left: 20rpx;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
}
.captcha-img-box,
.captcha-img,
.loding {
width: 100px;
}
.captcha-img {
/* #ifdef WEB */
cursor: pointer;
/* #endif */
height: 44px;
}
.loding {
z-index: 9;
position: absolute;
width: 30px;
margin:7px 35px;
}
.opacity {
opacity: 0.5;
}
</style>
\ No newline at end of file
<template>
<view class="captcha-box">
<view class="captcha-img-box">
<uni-icons class="loding" size="20px" color="#BBB" v-if="loging" type="spinner-cycle"></uni-icons>
<image class="captcha-img" :class="{opacity:loging}" @click="getImageCaptcha" :src="captchaBase64"
mode="widthFix"></image>
</view>
<input @blur="focusCaptchaInput = false" :focus="focusCaptchaInput" type="text" class="captcha"
:inputBorder="false" maxlength="4" v-model="val" placeholder="请输入验证码">
</view>
</template>
<script>
export default {
props: {
modelValue:String,
value:String,
scene: {
type: String,
default () {
return ""
}
},
focus: {
type: Boolean,
default () {
return false
}
}
},
computed:{
val:{
get(){
return this.value||this.modelValue
},
set(value){
// console.log(value);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', value);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', value)
// #endif
}
}
},
data() {
return {
focusCaptchaInput: false,
captchaBase64: "",
loging: false
};
},
watch: {
scene: {
handler(scene) {
if (scene) {
this.getImageCaptcha(this.focus)
} else {
uni.showToast({
title: 'scene不能为空',
icon: 'none'
});
}
},
immediate:true
}
},
methods: {
getImageCaptcha(focus = true) {
this.loging = true
if (focus) {
this.val = ''
this.focusCaptchaInput = true
}
const uniIdCo = uniCloud.importObject("uni-captcha-co", {
customUI: true
})
uniIdCo.getImageCaptcha({
scene: this.scene
}).then(result => {
// console.log(result);
this.captchaBase64 = result.captchaBase64
})
.catch(e => {
uni.showToast({
title: e.message,
icon: 'none'
});
}).finally(e => {
this.loging = false
})
}
}
}
</script>
<style lang="scss" scoped>
.captcha-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: flex-end;
flex: 1;
}
.captcha-img-box,
.captcha {
height: 44px;
line-height: 44px;
}
.captcha-img-box {
position: relative;
background-color: #FEFAE7;
}
.captcha {
background-color: #F8F8F8;
font-size: 14px;
flex: 1;
padding: 0 20rpx;
margin-left: 20rpx;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
}
.captcha-img-box,
.captcha-img,
.loding {
height: 44px !important;
width: 100px;
}
.captcha-img{
cursor: pointer;
}
.loding {
z-index: 9;
color: #bbb;
position: absolute;
text-align: center;
line-height: 45px;
animation: rotate 1s linear infinite;
}
.opacity {
opacity: 0.5;
}
@keyframes rotate {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
}
</style>
\ No newline at end of file
<template>
<uni-popup ref="popup" @clickMask="cancel">
<view class="popup-captcha">
<view class="content">
<text class="title">{{title}}</text>
<uni-captcha ref="captcha" :focus="focus" :scene="scene" v-model="val" :cursorSpacing="150"></uni-captcha>
</view>
<view class="button-box">
<text @click="cancel" class="btn cancel">取消</text>
<text @click="confirm" class="btn confirm">确认</text>
</view>
</view>
</uni-popup>
</template>
<script>
import uniPopup from './uni-popup/uni-popup.uvue';
let confirmCallBack = ():void=>console.log('未传入回调函数')
export default {
components: {
uniPopup
},
emits:["modelValue","confirm","cancel"],
data() {
return {
focus: false,
val:""
}
},
props: {
modelValue: {
type: String,
default: ""
},
value: {
type: String,
default: ""
},
scene: {
type: String,
default: ""
},
title: {
type: String,
default: "默认标题"
}
},
watch: {
val(val:string) {
// console.log(val);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', val);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', val)
// #endif
if(val.length == 4){
this.confirm()
}
}
},
mounted() {},
methods: {
open(callback: () => void) {
// console.log('callback',callback);
confirmCallBack = callback;
this.focus = true
this.val = "";
(this.$refs['popup'] as ComponentPublicInstance).$callMethod("open");
this.$nextTick(()=>{
(this.$refs['captcha'] as ComponentPublicInstance).$callMethod("getImageCaptcha",true);
})
},
close() {
this.focus = false;
(this.$refs['popup'] as ComponentPublicInstance).$callMethod("close");
},
cancel(){
this.close()
this.$emit("cancel")
},
confirm() {
if (this.val.length != 4) {
return uni.showToast({
title: '请填写验证码',
icon: 'none'
});
}
this.close()
this.$emit('confirm')
confirmCallBack()
}
}
}
</script>
<style lang="scss" scoped>
.popup-captcha {
background-color: #fff;
flex-direction: column;
width: 600rpx;
padding:10px 15px;
border-radius: 10px;
}
.popup-captcha .title {
text-align: center;
font-weight: 700;
margin: 5px 0;
}
.popup-captcha .button-box {
flex-direction: row;
justify-content: space-around;
margin-top: 5px;
}
.popup-captcha .button-box .btn {
flex: 1;
height: 35px;
line-height: 35px;
text-align: center;
}
.popup-captcha .button-box .cancel {
border: 1px solid #eee;
color: #666;
}
.confirm {
background-color: #0070ff;
color: #fff;
margin-left: 5px;
}
</style>
\ No newline at end of file
<template>
<uni-popup ref="popup" type="center">
<view class="popup-captcha">
<view class="content">
<text class="title">{{title}}</text>
<uni-captcha :focus="focus" :scene="scene" v-model="val"></uni-captcha>
</view>
<view class="button-box">
<view @click="close" class="btn">取消</view>
<view @click="confirm" class="btn confirm">确认</view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
data() {
return {
focus: false
}
},
props: {
modelValue:String,
value:String,
scene: {
type: String,
default () {
return ""
}
},
title: {
type: String,
default () {
return ""
}
},
},
computed:{
val:{
get(){
return this.value||this.modelValue
},
set(value){
// console.log(value);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', value);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', value)
// #endif
}
}
},
methods: {
open() {
this.focus = true
this.val = ""
this.$refs.popup.open()
},
close() {
this.focus = false
this.$refs.popup.close()
},
confirm() {
if(!this.val){
return uni.showToast({
title: '请填写验证码',
icon: 'none'
});
}
this.close()
this.$emit('confirm')
}
}
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
}
/* #endif */
.popup-captcha {
/* #ifndef APP-NVUE */
display: flex;
max-width: 600px;
/* #endif */
width: 600rpx;
padding-bottom: 0;
background-color: #FFF;
border-radius: 10px;
flex-direction: column;
position: relative;
}
.popup-captcha .content {
padding: 1.3em 0.8em;
}
.popup-captcha .title {
text-align: center;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
font-weight: 400;
font-size: 18px;
overflow: hidden;
text-overflow: ellipsis;
color: #111;
margin-bottom: 15px;
}
.button-box {
height: 44px;
border-top: solid 1px #eee;
flex-direction: row;
align-items: center;
justify-content: space-around;
}
.button-box ,.btn{
height: 44px;
line-height: 44px;
}
.button-box .btn{
flex: 1;
margin: 1px;
text-align: center;
}
.button-box .confirm{
color: #007aff;
border-left: solid 1px #eee;
}
</style>
<template>
<view class="popup-root" v-if="isOpen" v-show="isShow" @click="clickMask">
<view @click.stop>
<slot></slot>
</view>
</view>
</template>
<script>
type CloseCallBack = ()=> void;
let closeCallBack:CloseCallBack = () :void => {};
export default {
emits:["close","clickMask"],
data() {
return {
isShow:false,
isOpen:false
}
},
watch: {
// 设置show = true 时,如果没有 open 需要设置为 open
isShow:{
handler(isShow) {
// console.log("isShow",isShow)
if(isShow && this.isOpen == false){
this.isOpen = true
}
},
immediate:true
},
// 设置isOpen = true 时,如果没有 isShow 需要设置为 isShow
isOpen:{
handler(isOpen) {
// console.log("isOpen",isOpen)
if(isOpen && this.isShow == false){
this.isShow = true
}
},
immediate:true
}
},
methods:{
open(){
// ...funs : CloseCallBack[]
// if(funs.length > 0){
// closeCallBack = funs[0]
// }
this.isOpen = true;
},
clickMask(){
this.$emit('clickMask')
this.close()
},
close(): void{
this.isOpen = false;
this.$emit('close')
closeCallBack()
},
hiden(){
this.isShow = false
},
show(){
this.isShow = true
}
}
}
</script>
<style>
.popup-root {
position: fixed;
top: 0;
left: 0;
width: 750rpx;
height: 100%;
flex: 1;
background-color: rgba(0, 0, 0, 0.3);
justify-content: center;
align-items: center;
z-index: 99;
}
</style>
\ No newline at end of file
{
"id": "uni-captcha",
"displayName": "uni-captcha",
"version": "0.7.2",
"description": "云端一体图形验证码组件",
"keywords": [
"captcha",
"图形验证码",
"人机验证",
"防刷",
"防脚本"
],
"repository": "https://gitee.com/dcloud/uni-captcha",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}
\ No newline at end of file
<h2>
文档已移至 <a href="https://uniapp.dcloud.io/uniCloud/uni-captcha.html" target="_blank">uni-captcha文档</a>
</h2>
\ No newline at end of file
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
{
"name": "uni-captcha",
"version": "0.7.0",
"description": "uni-captcha",
"main": "index.js",
"homepage": "https://ext.dcloud.net.cn/plugin?id=4048",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uni-captcha"
},
"author": "DCloud",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}
\ No newline at end of file
// 开发文档: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
//导入验证码公共模块
const uniCaptcha = require('uni-captcha')
//获取数据库对象
const db = uniCloud.database();
//获取数据表opendb-verify-codes对象
const verifyCodes = db.collection('opendb-verify-codes')
module.exports = {
async getImageCaptcha({
scene,isUniAppX
}) {
//获取设备id
let {
deviceId,
platform
} = this.getClientInfo();
//根据:设备id、场景值、状态,查找记录是否存在
let res = await verifyCodes.where({
scene,
deviceId,
state: 0
}).limit(1).get()
//如果已存在则调用刷新接口,反之调用插件接口
let action = res.data.length ? 'refresh' : 'create'
//执行并返回结果
let option = {
scene, //来源客户端传递,表示:使用场景值,用于防止不同功能的验证码混用
uniPlatform: platform
}
if(isUniAppX){
option.mode = "bmp"
}
return await uniCaptcha[action](option)
}
}
\ No newline at end of file
{
"name": "uni-captcha-co",
"dependencies": {
"uni-captcha": "file:../common/uni-captcha",
"uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
},
"extensions": {
"uni-cloud-jql": {}
}
}
\ No newline at end of file
{
"bsonType": "object",
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"code": {
"bsonType": "string",
"description": "验证码"
},
"create_date": {
"bsonType": "timestamp",
"description": "创建时间"
},
"device_uuid": {
"bsonType": "string",
"description": "设备UUID,常用于图片验证码"
},
"email": {
"bsonType": "string",
"description": "邮箱"
},
"expired_date": {
"bsonType": "timestamp",
"description": "过期时间"
},
"ip": {
"bsonType": "string",
"description": "请求时客户端IP地址"
},
"mobile": {
"bsonType": "string",
"description": "手机号码"
},
"scene": {
"bsonType": "string",
"description": "使用验证码的场景,如:login, bind, unbind, pay"
},
"state": {
"bsonType": "int",
"description": "验证状态:0 未验证、1 已验证、2 已作废"
}
},
"required": []
}
\ No newline at end of file
## 1.0.1(2023-03-02)
- 修复 方法名错误
{
"id": "uni-cloud-s2s",
"displayName": "服务空间与服务器安全通讯模块",
"version": "1.0.1",
"description": "用于解决服务空间与服务器通讯时互相信任问题",
"keywords": [
"安全通讯",
"服务器请求云函数",
"云函数请求服务器"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "unicloud-template-function",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
\ No newline at end of file
# uni-cloud-s2s
文档见:[外部服务器如何与uniCloud安全通讯](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-s2s.html)
'use strict'; Object.defineProperty(exports, '__esModule', { value: !0 }); const e = require('crypto'); const t = require('path'); function s (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e } }require('fs'); const o = s(e); const n = s(t); const i = 'uni-cloud-s2s'; const r = { code: 5e4, message: 'Config error' }; const c = { code: 51e3, message: 'Access denied' }; class a extends Error {constructor (e) { super(e.message), this.errMsg = e.message || '', this.code = this.errCode = e.code, this.errSubject = e.subject, this.forceReturn = e.forceReturn || !1, this.cause = e.cause, Object.defineProperties(this, { message: { get () { return this.errMsg }, set (e) { this.errMsg = e } } }) }toJSON (e = 0) { if (!(e >= 10)) return e++, { errCode: this.errCode, errMsg: this.errMsg, errSubject: this.errSubject, cause: this.cause && this.cause.toJSON ? this.cause.toJSON(e) : this.cause } }} const d = Object.prototype.toString; const h = 50002; const u = Object.create(null); ['string', 'boolean', 'number', 'null'].forEach(e => { u[e] = function (t, s) { if ((function (e) { return d.call(e).slice(8, -1).toLowerCase() }(t)) !== e) return { code: h, message: `${s} is invalid` } } }); const f = 'Unicloud-S2s-Authorization'; class g {constructor (e) { const { config: t } = e || {}; this.config = t; const { connectCode: s } = t || {}; if (this.connectCode = s, !s || typeof s !== 'string') throw new a({ subject: i, code: r.code, message: 'Invalid connectCode in config' }) }getHeadersValue (e = {}, t, s) { const o = Object.keys(e || {}).find(e => e.toLowerCase() === t.toLowerCase()); return o ? e[o] : s }verifyHttpInfo (e) { const t = this.getHeadersValue(e.headers, f, ''); const [s = '', o = ''] = t.split(' '); if (s.toLowerCase() === 'CONNECTCODE'.toLowerCase() && o === this.config.connectCode) return !0; throw new a({ subject: i, code: c.code, message: `Invalid CONNECTCODE in headers['${f}']` }) }getSecureHeaders (e) { return { [f]: `CONNECTCODE ${this.config.connectCode}` } }} function l (e) { return function (t) { const { content: s, signKey: n } = t || {}; return o.default.createHash(e).update(s + '\n' + n).digest('hex') } } const p = { md5: l('md5'), sha1: l('sha1'), sha256: l('md5'), 'hmac-sha256': function (e) { const { content: t, signKey: s } = e || {}; return o.default.createHmac('sha256', s).update(t).digest('hex') } }; function m (e) { const { timestamp: t, data: s = {}, signKey: o, hashMethod: n = 'hmac-sha256' } = e || {}; const i = p[n]; const r = ['number', 'string', 'boolean']; const c = Object.keys(s).sort(); const a = []; for (let e = 0; e < c.length; e++) { const t = c[e]; const o = s[t]; const n = typeof o; r.includes(n) && a.push(`${t}=${o}`) } return i({ content: `${t}\n${a.join('&')}`, signKey: o }) } class w {constructor (e) { const { config: t } = e || {}; this.config = t; const { signKey: s, hashMethod: o = 'hmac-sha256', timeDiffTolerance: n = 60 } = t; if (!p[o]) throw new a({ subject: i, code: r.code, message: `Invalid hashMethod in config, expected "md5", "sha1", "sha256" or "hmac-sha256", got "${o}"` }); if (!s || typeof s !== 'string') throw new a({ subject: i, code: r.code, message: 'Invalid signKey in config' }); this.signKey = s, this.hashMethod = o, this.timeDiffTolerance = n }getHttpHeaders (e) { return e.headers || {} }getHeadersValue (e, t, s) { const o = Object.keys(e || {}).find(e => e.toLowerCase() === t.toLowerCase()); return o ? e[o] : s }getHttpData (e) { const t = e.httpMethod.toLowerCase(); const s = this.getHttpHeaders(e); const o = this.getHeadersValue(s, 'Content-Type', ''); if (t === 'get') return e.queryStringParameters; if (t !== 'post') throw new a({ subject: i, code: c.code, message: `Invalid http method, expected "POST" or "get", got "${t}"` }); if (o.indexOf('application/json') === 0) return JSON.parse(e.body); if (o.indexOf('application/x-www-form-urlencoded') === 0) return require('querystring').parse(e.body); throw new a({ subject: i, code: c.code, message: `Invalid content type of POST method, expected "application/json" or "application/x-www-form-urlencoded", got "${o}"` }) }verifyHttpInfo (e) { const t = e.headers || {}; const s = this.getHeadersValue(t, 'Unicloud-S2s-Timestamp', '0'); let [o, n] = this.getHeadersValue(t, 'Unicloud-S2s-Signature', '').split(' '); if (o = o.toLowerCase(), o !== this.hashMethod) throw new a({ subject: i, code: c.code, message: `Invalid hash method, expected "${this.hashMethod}", got "${o}"` }); const r = parseInt(s); const d = Date.now(); if (Math.abs(d - r) > 1e3 * this.timeDiffTolerance) throw new a({ subject: i, code: c.code, message: `Invalid timestamp, server timestamp is ${d}, ${r} exceed max timeDiffTolerance(${this.timeDiffTolerance} seconds)` }); return m({ timestamp: r, data: this.getHttpData(e), signKey: this.signKey, hashMethod: this.hashMethod }) === n }getSecureHeaders (e) { const { data: t } = e || {}; const s = Date.now(); const o = m({ timestamp: s, data: t, signKey: this.signKey, hashMethod: this.hashMethod }); return { 'Unicloud-S2s-Timestamp': s + '', 'Unicloud-S2s-Signature': this.hashMethod + ' ' + o } }} const y = require('uni-config-center')({ pluginId: i }); class b {constructor () { this.config = y.config(); const e = n.default.resolve(require.resolve('uni-config-center'), i, 'config.json'); if (!this.config) throw new a({ subject: i, code: r.code, message: `${i} config required, please check your config file: ${e}` }); if (this.config.type === 'connectCode') this.verifier = new g({ config: this.config }); else { if (!(function (e) { return e.type === 'sign' }(this.config))) throw new a({ subject: i, code: r.code, message: `Invalid ${i} config, expected policy is "code" or "sign", got ${this.config.policy}` }); this.verifier = new w({ config: this.config }) } }verifyHttpInfo (e) { if (!e) throw new a({ subject: i, code: c.code, message: 'Access denied, httpInfo required' }); return this.verifier.verifyHttpInfo(e) }getSecureHeaders (e) { return this.verifier.getSecureHeaders(e) }}exports.getSecureHeaders = function (e) { return (new b()).getSecureHeaders(e) }, exports.verifyHttpInfo = function (e) { const t = (new b()).verifyHttpInfo(e); if (!t) throw new a({ subject: i, code: c.code, message: c.message }); return t }
{
"name": "uni-cloud-s2s",
"version": "1.0.1",
"description": "",
"keywords": [],
"author": "DCloud",
"main": "index.js",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}
\ No newline at end of file
## 0.0.3(2022-11-11)
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
## 0.0.2(2021-04-16)
- 修改插件package信息
## 0.0.1(2021-03-15)
- 初始化项目
{
"id": "uni-config-center",
"displayName": "uni-config-center",
"version": "0.0.3",
"description": "uniCloud 配置中心",
"keywords": [
"配置",
"配置中心"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"directories": {
"example": "../../../scripts/dist"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}
# 为什么使用uni-config-center
实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ ├─index.js
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b // plugin-b对应的目录
├─index.js
└─config.json // plugin-b对应的配置文件
```
假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
uni-config-center就是用了统一管理这些配置文件的,使用uni-config-center后的目录结构如下
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ └─index.js
├─plugin-b // plugin-b对应的目录
│ └─index.js
└─uni-config-center
├─index.js // config-center入口文件
├─plugin-a
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b
└─config.json // plugin-b对应的配置文件
```
使用uni-config-center后的优势
- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
- 支持对config.json设置schema,插件使用者在HBuilderX内编写config.json文件时会有更好的提示(后续HBuilderX会提供支持)
# 用法
在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖,请参考:[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
```js
const createConfig = require('uni-config-center')
const uniIdConfig = createConfig({
pluginId: 'uni-id', // 插件id
defaultConfig: { // 默认配置
tokenExpiresIn: 7200,
tokenExpiresThreshold: 600,
},
customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
// defaudltConfig 默认配置
// userConfig 用户配置
return Object.assign(defaultConfig, userConfig)
}
})
// 以如下配置为例
// {
// "tokenExpiresIn": 7200,
// "passwordErrorLimit": 6,
// "bindTokenToDevice": false,
// "passwordErrorRetryTime": 3600,
// "app-plus": {
// "tokenExpiresIn": 2592000
// },
// "service": {
// "sms": {
// "codeExpiresIn": 300
// }
// }
// }
// 获取配置
uniIdConfig.config() // 获取全部配置,注意:uni-config-center内不存在对应插件目录时会返回空对象
uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置,返回:7200
uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置,返回:300
uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置,如果不存在则取传入的默认值,返回:600
// 获取文件绝对路径
uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
// 引用文件(require)
uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined,文件内有其他错误导致require失败时会抛出错误。
// 判断是否包含某文件
uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件,true: 文件存在,false: 文件不存在
```
\ No newline at end of file
## 1.0.14(2023-03-07)
- 修复 admin用户包含其他角色时未包含在token的Bug
## 1.0.13(2022-07-21)
- 修复 创建token时未传角色权限信息生成的token不正确的bug
## 1.0.12(2022-07-15)
- 提升与旧版本uni-id的兼容性(补充读取配置文件时回退平台app-plus、h5),但是仍推荐使用新平台名进行配置(app、web)
## 1.0.11(2022-07-14)
- 修复 部分情况下报`read property 'reduce' of undefined`的错误
## 1.0.10(2022-07-11)
- 将token存储在用户表的token字段内,与旧版本uni-id保持一致
## 1.0.9(2022-07-01)
- checkToken兼容token内未缓存角色权限的情况,此时将查库获取角色权限
## 1.0.8(2022-07-01)
- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug
## 1.0.7(2022-06-30)
- 修复config文件不合法时未抛出具体错误的Bug
## 1.0.6(2022-06-28)
- 移除插件内的数据表schema
## 1.0.5(2022-06-27)
- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug
## 1.0.4(2022-06-27)
- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945)
## 1.0.2(2022-06-23)
- 对齐旧版本uni-id默认配置
## 1.0.1(2022-06-22)
- 补充对uni-config-center的依赖
## 1.0.0(2022-06-21)
- 提供uni-id token创建、校验、刷新接口,简化旧版uni-id公共模块
{
"id": "uni-id-common",
"displayName": "uni-id-common",
"version": "1.0.14",
"description": "包含uni-id token生成、校验、刷新功能的云函数公共模块",
"keywords": [
"uni-id-common",
"uniCloud",
"token",
"权限"
],
"repository": "https://gitcode.net/dcloud/uni-id-common",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-config-center"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
# uni-id-common
文档请参考:[uni-id-common](https://uniapp.dcloud.net.cn/uniCloud/uni-id-common.html)
\ No newline at end of file
"use strict";var e,t=(e=require("crypto"))&&"object"==typeof e&&"default"in e?e.default:e;const n={TOKEN_EXPIRED:"uni-id-token-expired",CHECK_TOKEN_FAILED:"uni-id-check-token-failed",PARAM_REQUIRED:"uni-id-param-required",ACCOUNT_EXISTS:"uni-id-account-exists",ACCOUNT_NOT_EXISTS:"uni-id-account-not-exists",ACCOUNT_CONFLICT:"uni-id-account-conflict",ACCOUNT_BANNED:"uni-id-account-banned",ACCOUNT_AUDITING:"uni-id-account-auditing",ACCOUNT_AUDIT_FAILED:"uni-id-account-audit-failed",ACCOUNT_CLOSED:"uni-id-account-closed"};function i(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}function r(e){if(!e)return;const t=e.match(/^(\d+).(\d+).(\d+)/);return t?t.slice(1,4).map(e=>parseInt(e)):void 0}function o(e,t){const n=r(e),i=r(t);return n?i?function(e,t){const n=Math.max(e.length,t.length);for(let i=0;i<n;i++){const n=e[i],r=t[i];if(n>r)return 1;if(n<r)return-1}return 0}(n,i):1:i?-1:0}const s={"uni-id-token-expired":30203,"uni-id-check-token-failed":30202};function c(e){const{errCode:t,errMsgValue:n}=e;e.errMsg=this._t(t,n),t in s&&(e.code=s[t]),delete e.errMsgValue}function a(e){return"object"===(i=e,Object.prototype.toString.call(i).slice(8,-1).toLowerCase())&&e.errCode&&(t=e.errCode,Object.values(n).includes(t))&&!!e.errCode;var t,i}let u={"zh-Hans":{"uni-id-token-expired":"登录状态失效,token已过期","uni-id-check-token-failed":"token校验未通过","uni-id-param-required":"缺少参数: {param}","uni-id-account-exists":"此账号已注册","uni-id-account-not-exists":"此账号未注册","uni-id-account-conflict":"用户账号冲突","uni-id-account-banned":"从账号已封禁","uni-id-account-auditing":"此账号正在审核中","uni-id-account-audit-failed":"此账号审核失败","uni-id-account-closed":"此账号已注销"},en:{"uni-id-token-expired":"The login status is invalid, token has expired","uni-id-check-token-failed":"Check token failed","uni-id-param-required":"Parameter required: {param}","uni-id-account-exists":"Account exists","uni-id-account-not-exists":"Account does not exists","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"}};try{const e=require.resolve("uni-config-center/uni-id/lang/index.js");u=function(e,t){const n=Object.keys(e);n.push(...Object.keys(t));const i={};for(let r=0;r<n.length;r++){const o=n[r];i[o]=Object.assign({},e[o],t[o])}return i}(u,require(e))}catch(e){}var d=u;function l(e){return e.replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}function h(e){return JSON.parse((t=function(e){var t=4-(e=e.toString()).length%4;if(4!==t)for(var n=0;n<t;++n)e+="=";return e.replace(/-/g,"+").replace(/_/g,"/")}(e),Buffer.from(t,"base64").toString("utf-8")));var t}function f(e){return l((t=JSON.stringify(e),Buffer.from(t,"utf-8").toString("base64")));var t}function p(e,n){return l(t.createHmac("sha256",n).update(e).digest("base64"))}const k=function(e,t){if("string"!=typeof e)throw new Error("Invalid token");const n=e.split(".");if(3!==n.length)throw new Error("Invalid token");const[i,r,o]=n;if(p(i+"."+r,t)!==o)throw new Error("Invalid token");const s=h(i);if("HS256"!==s.alg||"JWT"!==s.typ)throw new Error("Invalid token");const c=h(r);if(1e3*c.exp<Date.now()){const e=new Error("Token expired");throw e.name="TokenExpiredError",e}return c},g=function(e,t,n={}){const{expiresIn:i}=n;if(!i)throw new Error("expiresIn is required");const r=parseInt(Date.now()/1e3),o={...e,iat:r,exp:r+n.expiresIn},s=f({alg:"HS256",typ:"JWT"})+"."+f(o);return s+"."+p(s,t)},I=uniCloud.database(),_=I.command,C=I.collection("uni-id-users"),m=I.collection("uni-id-roles");class T{constructor({uniId:e}={}){this.uid=null,this.userRecord=null,this.userPermission=null,this.oldToken=null,this.oldTokenPayload=null,this.uniId=e,this.config=this.uniId._getConfig(),this.clientInfo=this.uniId._clientInfo,this.checkConfig()}checkConfig(){const{tokenExpiresIn:e,tokenExpiresThreshold:t}=this.config;if(t>e)throw new Error("Config error, tokenExpiresThreshold should be less than tokenExpiresIn")}get customToken(){return this.uniId.interceptorMap.get("customToken")}isTokenInDb(e){return o(e,"1.0.10")>=0}async getUserRecord(){if(this.userRecord)return this.userRecord;const e=await C.doc(this.uid).get();if(this.userRecord=e.data[0],!this.userRecord)throw{errCode:n.ACCOUNT_NOT_EXISTS};switch(this.userRecord.status){case void 0:case 0:break;case 1:throw{errCode:n.ACCOUNT_BANNED};case 2:throw{errCode:n.ACCOUNT_AUDITING};case 3:throw{errCode:n.ACCOUNT_AUDIT_FAILED};case 4:throw{errCode:n.ACCOUNT_CLOSED}}if(this.oldTokenPayload){if(this.isTokenInDb(this.oldTokenPayload.uniIdVersion)){if(-1===(this.userRecord.token||[]).indexOf(this.oldToken))throw{errCode:n.CHECK_TOKEN_FAILED}}if(this.userRecord.valid_token_date&&this.userRecord.valid_token_date>1e3*this.oldTokenPayload.iat)throw{errCode:n.TOKEN_EXPIRED}}return this.userRecord}async updateUserRecord(e){await C.doc(this.uid).update(e)}async getUserPermission(){if(this.userPermission)return this.userPermission;const e=(await this.getUserRecord()).role||[];if(0===e.length)return this.userPermission={role:[],permission:[]},this.userPermission;if(e.includes("admin"))return this.userPermission={role:e,permission:[]},this.userPermission;const t=await m.where({role_id:_.in(e)}).get(),n=(i=t.data.reduce((e,t)=>(t.permission&&e.push(...t.permission),e),[]),Array.from(new Set(i)));var i;return this.userPermission={role:e,permission:n},this.userPermission}async _createToken({uid:e,role:t,permission:i}={}){if(!t||!i){const e=await this.getUserPermission();t=e.role,i=e.permission}let r={uid:e,role:t,permission:i};if(this.uniId.interceptorMap.has("customToken")){const n=this.uniId.interceptorMap.get("customToken");if("function"!=typeof n)throw new Error("Invalid custom token file");r=await n({uid:e,role:t,permission:i})}const o=Date.now(),{tokenSecret:s,tokenExpiresIn:c}=this.config,a=g({...r,uniIdVersion:"1.0.14"},s,{expiresIn:c}),u=await this.getUserRecord(),d=(u.token||[]).filter(e=>{try{const t=this._checkToken(e);if(u.valid_token_date&&u.valid_token_date>1e3*t.iat)return!1}catch(e){if(e.errCode===n.TOKEN_EXPIRED)return!1}return!0});return d.push(a),await this.updateUserRecord({last_login_ip:this.clientInfo.clientIP,last_login_date:o,token:d}),{token:a,tokenExpired:o+1e3*c}}async createToken({uid:e,role:t,permission:i}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"uid"}};this.uid=e;const{token:r,tokenExpired:o}=await this._createToken({uid:e,role:t,permission:i});return{errCode:0,token:r,tokenExpired:o}}async refreshToken({token:e}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const t=this._checkToken(e);this.uid=t.uid,this.oldTokenPayload=t;const{uid:i}=t,{role:r,permission:o}=await this.getUserPermission(),{token:s,tokenExpired:c}=await this._createToken({uid:i,role:r,permission:o});return{errCode:0,token:s,tokenExpired:c}}_checkToken(e){const{tokenSecret:t}=this.config;let i;try{i=k(e,t)}catch(e){if("TokenExpiredError"===e.name)throw{errCode:n.TOKEN_EXPIRED};throw{errCode:n.CHECK_TOKEN_FAILED}}return i}async checkToken(e,{autoRefresh:t=!0}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const i=this._checkToken(e);this.uid=i.uid,this.oldTokenPayload=i;const{tokenExpiresThreshold:r}=this.config,{uid:o,role:s,permission:c}=i,a={role:s,permission:c};if(!s&&!c){const{role:e,permission:t}=await this.getUserPermission();a.role=e,a.permission=t}if(!r||!t){const e={code:0,errCode:0,...i,...a};return delete e.uniIdVersion,e}const u=Date.now();let d={};1e3*i.exp-u<1e3*r&&(d=await this._createToken({uid:o}));const l={code:0,errCode:0,...i,...a,...d};return delete l.uniIdVersion,l}}var E=Object.freeze({__proto__:null,checkToken:async function(e,{autoRefresh:t=!0}={}){return new T({uniId:this}).checkToken(e,{autoRefresh:t})},createToken:async function({uid:e,role:t,permission:n}={}){return new T({uniId:this}).createToken({uid:e,role:t,permission:n})},refreshToken:async function({token:e}={}){return new T({uniId:this}).refreshToken({token:e})}});const w=require("uni-config-center")({pluginId:"uni-id"});class A{constructor({context:e,clientInfo:t,config:n}={}){this._clientInfo=e?function(e){return{appId:e.APPID,platform:e.PLATFORM,locale:e.LOCALE,clientIP:e.CLIENTIP,deviceId:e.DEVICEID}}(e):t,this.config=n||this._getOriginConfig(),this.interceptorMap=new Map,w.hasFile("custom-token.js")&&this.setInterceptor("customToken",require(w.resolve("custom-token.js"))),this._i18n=uniCloud.initI18n({locale:this._clientInfo.locale,fallbackLocale:"zh-Hans",messages:d})}setInterceptor(e,t){this.interceptorMap.set(e,t)}_t(...e){return this._i18n.t(...e)}_parseOriginConfig(e){return Array.isArray(e)?e:e[0]?Object.values(e):e}_getOriginConfig(){if(w.hasFile("config.json")){let e;try{e=w.config()}catch(e){throw new Error("Invalid uni-id config file\n"+e.message)}return this._parseOriginConfig(e)}try{return this._parseOriginConfig(require("uni-id/config.json"))}catch(e){throw new Error("Invalid uni-id config file")}}_getAppConfig(){const e=this._getOriginConfig();return Array.isArray(e)?e.find(e=>e.dcloudAppid===this._clientInfo.appId)||e.find(e=>e.isDefaultConfig):e}_getPlatformConfig(){const e=this._getAppConfig();if(!e)throw new Error(`Config for current app (${this._clientInfo.appId}) was not found, please check your config file or client appId`);let t;switch("app-plus"===this._clientInfo.platform&&(this._clientInfo.platform="app"),"h5"===this._clientInfo.platform&&(this._clientInfo.platform="web"),this._clientInfo.platform){case"web":t="h5";break;case"app":t="app-plus"}const n=[{tokenExpiresIn:7200,tokenExpiresThreshold:1200,passwordErrorLimit:6,passwordErrorRetryTime:3600},e];t&&e[t]&&n.push(e[t]),n.push(e[this._clientInfo.platform]);const i=Object.assign(...n);return["tokenSecret","tokenExpiresIn"].forEach(e=>{if(!i||!i[e])throw new Error(`Config parameter missing, ${e} is required`)}),i}_getConfig(){return this._getPlatformConfig()}}for(const e in E)A.prototype[e]=E[e];function y(e){const t=new A(e);return new Proxy(t,{get(e,t){if(t in e&&0!==t.indexOf("_")){if("function"==typeof e[t])return(n=e[t],function(){let e;try{e=n.apply(this,arguments)}catch(e){if(a(e))return c.call(this,e),e;throw e}return i(e)?e.then(e=>(a(e)&&c.call(this,e),e),e=>{if(a(e))return c.call(this,e),e;throw e}):(a(e)&&c.call(this,e),e)}).bind(e);if("context"!==t&&"config"!==t)return e[t]}var n}})}A.prototype.createInstance=y;const x={createInstance:y};module.exports=x;
{
"name": "uni-id-common",
"version": "1.0.14",
"description": "uni-id token生成、校验、刷新",
"main": "index.js",
"homepage": "https://uniapp.dcloud.io/uniCloud/uni-id-common.html",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uni-id-common.git"
},
"author": "DCloud",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}
\ No newline at end of file
.page {
background-color: #FFF;
width: 750rpx;
flex: 1;
}
.tip {
margin:0 15px;
font-size: 14px;
}
.form {
margin:10px 50rpx 0 50rpx;
}
.my-input {
margin-top:12px;
}
.agreements-box {
padding:8px 0;
}
.uni-btn{
width: 650rpx;
margin:8px 0;
}
\ No newline at end of file
import { state, mutations } from '@/uni_modules/uni-id-pages-x/store.uts';
import config from '@/uni_modules/uni-id-pages-x/config.uts';
export const loginSuccess = (_ : UTSJSONObject) => {
// console.log('loginSuccess', e);
// console.log("新用户uid", e["uid"]);
// state.currentUserInfo = uniCloud.getCurrentUserInfo()
mutations.updateUserInfo(null)
// state.userInfo["_id"] = e["uid"]
state.isLogin = true
uni.$emit('uni-id-pages-x-login-success', '')
// 登录后重定向设置
function loginAfterToPage() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const uniIdRedirectUrl = currentPage.options.get("uniIdRedirectUrl");
if (uniIdRedirectUrl != null) {
// console.log('uniIdRedirectUrl', uniIdRedirectUrl);
uni.redirectTo({
url: uniIdRedirectUrl,
fail() {
console.error("uniIdRouter redirectTo fail");
uni.switchTab({
"url": uniIdRedirectUrl
})
},
success() {
// console.log('uniIdRouter redirectTo success');
}
})
} else {
uni.navigateBack()
}
};
const toastDuration = 1500
uni.showToast({
title: '登录成功',
duration: toastDuration,
icon: 'none'
});
// 避免 showToast 还没结束就跳转引起的崩溃
setTimeout(() => loginAfterToPage(), toastDuration);
}
export const logout = () => {
// console.log("logout");
// 1. 已经过期就不需要调用服务端的注销接口 2.即使调用注销接口失败,不能阻塞客户端
if (uniCloud.getCurrentUserInfo().tokenExpired > Date.now()) {
const uniIdCo = uniCloud.importObject("uni-id-co", { customUI: false })
uniIdCo.logout().finally(() => {
// 调完注销接口 跳转至登录页面
uni.redirectTo({
url: "/uni_modules/uni-id-pages-x/pages/login/login"
})
})
}
uni.removeStorageSync('uni_id_token');
uni.setStorageSync('uni_id_token_expired', 0)
state.userInfo = {}
state.isLogin = false
uni.$emit('uni-id-pages-x-logout', '')
}
export const checkPassword = (password : string) : UTSJSONObject => {
let res : UTSJSONObject = {
"pass": true,
"errMsg": null
}
// console.log("checkPassword", password);
// 根据配置的密码强度校验
let passwordStrength = config.getString("passwordStrength")
if (passwordStrength == null) {
return res
}
// 密码强度表达式
const passwordRules : UTSJSONObject = {
"super": {
"rule": "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&_\\-+=`|\\(){}\\[\\]:;\"'<>,.?/])[0-9a-zA-Z~!@#$%^&_\\-+=`|\\(){}\\[\\]:;\"'<>,.?/]{8,16}$",
"errMsg": "密码必须包含大小写字母、数字和特殊符号,长度范围:8-16位之间"
},
"strong": {
"rule": "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[~!@#$%^&_\\-+=`|\\(){}\\[\\]:;\"'<>,.?/])[0-9a-zA-Z~!@#$%^&_\\-+=`|\\(){}\\[\\]:;\"'<>,.?/]{8,16}$",
"errMsg": "密密码必须包含字母、数字和特殊符号,长度范围:8-16位之间"
},
"medium": {
"rule": "^(?!.*[0-9]+$)(?!.*[a-zA-Z]+$)(?!.*[~!@#$%^&_\\-+=`|\\(){}\\[\\]:;\"'<>,.?/]+$)[0-9a-zA-Z~!@#$%^&_\\-+=`|\\(){}\\[\\]:;\"'<>,.?/]{8,16}$",
"errMsg": "密码必须为字母、数字和特殊符号任意两种的组合,长度范围:8-16位之间"
},
"weak": {
"rule": "^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z~!@#$%^&_\\-+=`|\\(){}\\[\\]:;\"'<>,.?/]{6,16}$",
"errMsg": "密码必须包含字母和数字,长度范围:6-16位之间"
}
}
let passwordRule = passwordRules.getString(passwordStrength + '.rule');
let passwordRegExp = new RegExp(passwordRule as string);
res["pass"] = passwordRegExp.test(password);
res["errMsg"] = passwordRules.getString(passwordStrength + '.errMsg');
return res
}
\ No newline at end of file
<template>
<view @click="onClick" :style="{width,height}" style="justify-content: center;">
<image v-if="cSrc.length != 0" :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: 'widthFix'
},
src: {
type: String,
default: ""
},
width: {
type: String,
default: '100rpx'
},
height: {
type: String,
default: '100rpx'
}
},
watch: {
src: {
handler(src : string) {
if (src.length != 0 && src.substring(0, 8) == "cloud://") {
uniCloud.getTempFileURL({
fileList: [src]
}).then((res : UniCloudGetTempFileURLResult) => {
// console.log('res=====',res);
this.cSrc = res.fileList[0].tempFileURL
})
} else {
this.cSrc = src
}
},
immediate: true
}
},
methods: {
onClick() {
this.$emit('click')
}
},
data() {
return {
cSrc: ""
};
}
}
</script>
\ No newline at end of file
<template>
<view class="agreements-root" v-if="agreements.length != 0">
<template v-if="needAgreements">
<checkbox-group @change="setAgree">
<checkbox class="checkbox" style="transform: scale(0.7);margin-right: -6px;padding-right: 3px;margin-left: 1px;"
:checked="!pendingAgreements" value="agreement">
<text class="checkbox-text">同意</text>
</checkbox>
</checkbox-group>
<view class="content">
<view class="agreements-item" v-for="(agreement,index) in agreements" :key="index">
<text class="agreement text" @click="navigateTo(index)">{{agreement.title}}</text>
<text class="text and" v-if="hasAnd(index)" space="nbsp"> 和 </text>
</view>
</view>
</template>
<!-- 弹出式 -->
<uni-popup ref="popup">
<view class="popup-content">
<view class="popup-header">
<uni-id-pages-x-icons :size="18" color="#E6A23C" type="advert" />
<text class="popup-title">请先阅读并同意</text>
</view>
<view class="content">
<view class="agreements-item" v-for="(agreement,index) in agreements" :key="index">
<text class="agreement popup-text" @click="navigateTo(index)">{{agreement.title}}</text>
<text class="popup-text and" v-if="hasAnd(index)" space="nbsp"> 和 </text>
</view>
</view>
<view class="btn-group">
<text class="btn" @click="cancel">取消</text>
<text class="btn confirm" @click="confirm">同意</text>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import config from '@/uni_modules/uni-id-pages-x/config.uts';
import {state} from '@/uni_modules/uni-id-pages-x/store.uts';
type Agreement = {url:string,title:string};
let confirmCallBack = ():void=>{} //console.log('未传入回调函数,value:')
export default {
name: "uni-id-pages-x-agreements",
computed: {
agreements():Agreement[]{
const agreements = config.getJSON('agreements')
if(agreements === null){
return []
}else{
return [
{
"url":agreements.getString('serviceUrl') as string,
"title":"用户服务协议"
},
{
"url":agreements.getString('privacyUrl') as string,
"title":"隐私政策条款"
}
] as Agreement[]
}
},
pendingAgreements():boolean{
return state.pendingAgreements
}
},
props: {
scope: {
type: String,
default: "login"
},
},
data() {
return {
needAgreements:true,
$popupComponent:null as null | UniPopupComponentPublicInstance
}
},
mounted() {
const scopeList = config.getArray<string>('agreements.scopeList')
if(scopeList == null){
this.needAgreements = false
}else{
this.needAgreements = scopeList.includes(this.scope)
}
state.pendingAgreements = this.needAgreements;
this.$popupComponent = (this.$refs['popup'] as UniPopupComponentPublicInstance)
// this.showPopupAgreements(()=>{
// console.log('showPopupAgreements after');
// })
uni.$on("uni-id-pages-x-agreements-shake",()=>{
// console.log('弹出申请同意隐私协议的框');
(this.$refs['popup-box'] as INode).style.setProperty("top",0)
})
},
methods: {
hasAnd(index:number) : boolean{
return this.agreements.length - 1 > index
},
navigateTo(index:number){
const title = this.agreements[index].title
const url = this.agreements[index].url
uni.navigateTo({
"url":"/uni_modules/uni-id-pages-x/pages/common/webview/webview?url="+url+"&title="+title,
fail:e=>{
console.error(e);
}
})
},
setAgree(event: CheckboxGroupChangeEvent){
state.pendingAgreements = event.detail.value.length == 0
},
showPopupAgreements(callback: () => void){
confirmCallBack = callback;
this.$popupComponent!.open()
},
cancel(){
state.pendingAgreements = true;
this.$popupComponent!.close()
},
confirm(){
state.pendingAgreements = false;
// console.log('state.pendingAgreements',state.pendingAgreements);
confirmCallBack();
this.$popupComponent!.close()
}
},
}
</script>
<style lang="scss" scoped>
.agreements-root {
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin-left: -9px;
}
.checkbox-text {
transform: scale(1.3);
margin-left: 3px;
color: #8a8f8b;
font-size: 12px;
}
.agreements-item {
flex-direction: row;
}
.text {
line-height: 26px;
font-size: 12px;
}
.popup-text{
line-height: 30px;
font-size:14px;
}
.agreement {
color: #04498c;
/* #ifdef WEB */
cursor: pointer;
/* #endif */
}
.content {
padding: 5px 0;
flex-wrap: wrap;
flex-direction: row;
}
.popup-content {
background-color: #FFF;
padding-bottom: 0;
border-radius: 10px;
width: 600rpx;
padding: 15px;
}
.popup-header {
display: flex;
flex-direction: row;
align-items: center;
padding-top: 5px;
}
.popup-title {
font-size: 16px;
// text-align: center;
margin: 15px 0;
padding-left: 5px;
// color: #E6A23C;
}
.popup-content .btn-group {
border-top: 1px solid #eee;
flex-direction: row;
justify-content: space-around;
margin-top: 10px;
}
.popup-content .btn-group .btn {
flex: 1;
text-align: center;
color: #666;
height: 45px;
line-height: 45px;
font-size: 14px;
}
.popup-content .btn-group .btn.confirm {
color: #115ff7;
border-left: solid 1px #eee;
}
</style>
\ No newline at end of file
<template>
<view class="avatar-box-root">
<!-- #ifdef MP-WEIXIN -->
<button open-type="chooseAvatar" @chooseavatar="bindchooseavatar" @click="uploadAvatarImg" class="box"
:class="{'showBorder':border}" :style="{width,height,lineHeight:height}">
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view open-type="chooseAvatar" @click="uploadAvatarImg" class="box" :class="{'showBorder':border}"
:style="{width,height}">
<!-- #endif -->
<!-- {{avatar_file!.getString("url")}} -->
<cloud-image v-if="avatar_file != null" :src="avatar_file!.getString('url')" :width="width" :height="height"></cloud-image>
<image v-else-if="readOnly" class="default-avatarUrl" src="@/uni_modules/uni-id-pages-x/static/default-avatar.png" mode="widthFix"></image>
<view v-else class="upload-img-icon">
<uni-id-pages-x-icons :size="35" color="#eee" type="jiahao"></uni-id-pages-x-icons>
</view>
<!-- #ifndef MP-WEIXIN -->
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
</button>
<!-- #endif -->
</view>
</template>
<script>
import { state, mutations, UserInfo, AvatarFile } from '@/uni_modules/uni-id-pages-x/store.uts';
/**
* uni-id-pages-x-avatar
* @description 用户头像组件
* @property {String} width 图片的宽,默认为:50px
* @property {String} height 图片的高,默认为:50px
*/
export default {
name: "uni-id-pages-x-avatar",
data() {
return {
isPC: false
}
},
props: {
//头像图片宽
width: {
type: String,
default: "50px"
},
//头像图片高
height: {
type: String,
default: "50px"
},
border: {
type: Boolean,
default: false
},
readOnly: {
type: Boolean,
default: false
}
},
mounted() {
// #ifdef WEB
this.isPC = !['ios', 'android'].includes(uni.getSystemInfoSync().platform);
// #endif
},
computed: {
hasLogin() : boolean {
return state.isLogin as Boolean
},
userInfo() : UTSJSONObject {
return state.userInfo
},
avatar_file() : UTSJSONObject | null {
return this.userInfo.getJSON("avatar_file")
}
},
methods: {
setAvatarFile() {//avatar_file : AvatarFile
// 使用 clientDB 提交数据
// mutations.updateUserInfo({avatar_file})
},
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() {
if(this.readOnly){
return
}
// #ifdef MP-WEIXIN
return // 微信小程序走 bindchooseavatar方法
// #endif
// #ifndef MP-WEIXIN
// if(!this.hasLogin){
// uni.navigateTo({
// url:'/uni_modules/uni-id-pages-x/pages/login/login-withoutpwd'
// })
// return
// }
const crop : ChooseImageCropOptions = {
quality: 100,
width: 600,
height: 600,
resize: true
};
uni.chooseImage({
count: 1,
crop,
success(res) : void {
// console.log('res', res);
let tempFiles = res.tempFiles as UTSJSONObject[];
let tempFile = tempFiles[0];
let tempFileName = tempFile.getString('name')
let tempFilePath = tempFile.getString('path')
if (tempFileName == null) {
tempFileName = ""
}
if (tempFilePath == null) {
tempFilePath = ""
}
// console.log(9527,tempFileName.length);
// console.error('tempFileName',tempFileName);
// console.error('tempFilePath',tempFilePath);
let avatar_file = {
// #ifdef H5
extname: tempFileName.split(".")[tempFileName.split(".").length - 1],
// #endif
// #ifndef H5
extname: tempFilePath.split(".")[tempFilePath.split(".").length - 1],
// #endif
name: tempFileName,
url: tempFilePath
} as UTSJSONObject
console.error('avatar_file', avatar_file);
let filePath = res.tempFilePaths[0]
//非app端剪裁头像,app端用内置的原生裁剪
// #ifndef UNI-APP-X
filePath = await new Promise((callback) => {
// #ifdef H5
if (!this.isPC) {
callback(filePath)
}
// #endif
uni.navigateTo({
url: '/uni_modules/uni-id-pages-x/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 _id = this.userInfo["_id"] as string
// if(_id != null){
// _id = "" as string
// }
let cloudPath = _id + '' + Date.now()
avatar_file.name = cloudPath
uni.showLoading({
title: "更新中",
mask: true
});
// console.log('filePath', filePath);
uniCloud.uploadFile({
filePath,
cloudPath,
})
.then<void>(function (res) {
// console.log('res777777777',res);
avatar_file.url = res.fileID
// console.log('avatar_file111', avatar_file);
mutations.updateUserInfo({ avatar_file } as UTSJSONObject)
})
.catch<void>(function (err : any | null) {
const error = err as UniCloudError
uni.showModal({
content: '上传失败,' + error.errMsg,
showCancel: false
});
console.error(error);
}).finally(() => {
uni.hideLoading()
})
},
fail(err) : void {
console.error('chooseImage fail: ', err)
uni.showModal({
content: '失败,' + err.errMsg,
showCancel: false
});
}
})
// #endif
}
}
}
</script>
<style>
.avatar-box-root {
background-color: #fff;
}
.box {
overflow: hidden;
padding: 0;
}
.chooseAvatar {
/* #ifndef UNI-APP-X */
display: inline-block;
box-sizing: border-box;
/* #endif */
border-radius: 100px;
text-align: center;
padding: 1px;
}
.upload-img-icon {
border: 1px dotted #c8c8c8;
width: 55px;
height: 55px;
justify-content: center;
align-items: center;
}
.showBorder {
border: solid 1px #ddd;
}
</style>
\ No newline at end of file
<template>
<view class="fab-login-box">
<template v-for="(item,index) in servicesList" :key="item.id">
<view class="item" @click="changeLoginType(index)">
<image class="logo" :src="item.logo" mode="scaleToFill"></image>
<text class="title">{{item.text}}</text>
</view>
</template>
</view>
</template>
<script>
// import config from '@/uni_modules/uni-id-pages-x/config.js'
//前一个窗口的页面地址。控制点击切换快捷登录方式是创建还是返回
// import {store,mutations} from '@/uni_modules/uni-id-pages-x/common/store.js'
// let allServicesList = []
import config from '@/uni_modules/uni-id-pages-x/config.uts';
import { state } from '@/uni_modules/uni-id-pages-x/store.uts';
type Services = {
id : string,
text : string,
logo : string
}
export default {
name: "uni-id-pages-x-fab-login",
props: {
currentLoginType: {
type: String
}
},
computed: {
agreements() {
// if (!config.agreements) {
// return []
// }
// let {
// serviceUrl,
// privacyUrl
// } = config.agreements
// return [{
// url: serviceUrl,
// title: "用户服务协议"
// },
// {
// url: privacyUrl,
// title: "隐私政策条款"
// }
// ]
},
servicesList() : Services[] {
const servicesList = [{
"id": "username",
"text": "账号登录",
"logo": "/uni_modules/uni-id-pages-x/static/login/uni-fab-login/user.png",
},
{
"id": "smsCode",
"text": "短信验证码",
"logo": "/uni_modules/uni-id-pages-x/static/login/uni-fab-login/sms.png",
},
{
"id": "weixin",
"text": "微信登录",
"logo": "/uni_modules/uni-id-pages-x/static/login/uni-fab-login/weixin.png",
},
{
"id": "weixinMobile",
"text": "微信手机号",
"logo": "/uni_modules/uni-id-pages-x/static/login/uni-fab-login/weixin.png",
},
// #ifndef MP-WEIXIN
{
"id": "apple",
"text": "苹果登录",
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/apple.png",
},
{
"id": "univerify",
"text": "一键登录",
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/univerify.png",
},
{
"id": "taobao",
"text": "淘宝登录", //暂未提供该登录方式的接口示例
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/taobao.png",
},
{
"id": "facebook",
"text": "脸书登录", //暂未提供该登录方式的接口示例
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/facebook.png",
},
{
"id": "alipay",
"text": "支付宝登录", //暂未提供该登录方式的接口示例
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/alipay.png",
},
{
"id": "qq",
"text": "QQ登录", //暂未提供该登录方式的接口示例
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/qq.png",
},
{
"id": "google",
"text": "谷歌登录", //暂未提供该登录方式的接口示例
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/google.png",
},
{
"id": "douyin",
"text": "抖音登录", //暂未提供该登录方式的接口示例
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/douyin.png",
},
{
"id": "sinaweibo",
"text": "新浪微博", //暂未提供该登录方式的接口示例
"logo": "/uni_modules/uni-id-pages-x/static/app-plus/uni-fab-login/sinaweibo.png",
}
// #endif
] as Services[]
const loginTypes = config.getArray<string>('loginTypes')
return servicesList.filter((item) : boolean => {
// #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 item.id != this.currentLoginType && loginTypes!.includes(item.id)
})
}
},
data() {
return {
// 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: {
// TODO----
// agree(agree) {
// this.univerifyStyle.privacyTerms.defaultCheckBoxState = agree
// }
},
created() {
//处理一键登录
if (this.servicesList.map((i) : string => i.id).includes('univerify')) {
// console.log('处理一键登录');
// this.univerifyStyle.privacyTerms.privacyItems = this.agreements
// //设置一键登录功能底下的快捷登录按钮
// servicesList.forEach(({
// id,
// logo,
// }) => {
// if (id != 'univerify') {
// this.univerifyStyle.buttons.list.push({
// "iconPath": logo,
// "provider": id,
// })
// }
// })
}
},
methods: {
changeLoginType(index : number) {
const id = this.servicesList[index].id
if (!["username", "smsCode"].includes(id)) {
return uni.showToast({
title: 'uni-app x暂未支持此登录方式',
icon: 'none',
duration: 3000
});
}
this.$emit('changeLoginType', id)
if (["weixin", "apple", "univerify"].includes(id)) {
this.login_before(id)
}
},
toPage(path : string) {
// console.log(13, path);
},
login_before(type : string) {
// console.log(type);
// 提示空实现
if (["qq", "xiaomi", "sinaweibo", "taobao", "facebook", "google", "alipay", "douyin"].includes(type)) {
return uni.showToast({
title: '该登录方式暂未实现,欢迎提交pr',
icon: 'none',
duration: 3000
});
}
//检查当前环境是否支持这种登录方式
//判断是否需要弹出隐私协议授权框
const scope = config.getAny('agreements.scope') as string[]
// 配置中,当前类型是否需要,用户同意隐私协议
let needAgreements = scope.includes('register')
// 排除一键登录的情况(在一键登录的弹出层中选择) && !this.agree 未同意 && 需要同意
if (type != 'univerify' && needAgreements && !state.pendingAgreements) {
return uni.showToast({
title: '你未同意隐私政策协议',
icon: 'none'
});
// console.log('弹出申请同意隐私协议的框');
// uni.$emit("uni-id-pages-x-agreements-shake",'')
}
uni.showToast({
title: 'login_type:' + type,
icon: 'none'
});
// uni.login({
// "provider": type,
// "onlyAuthorize": true,
// // #ifdef APP
// // "univerifyStyle": this.univerifyStyle,
// // #endif
// success: async (e:any) => {
// console.log('eeeee',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()
// // }
// })
}
}
}
</script>
<style lang="scss" scoped>
.fab-login-box {
width: 750rpx;
position: fixed;
bottom: 30;
left: 0;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
z-index: 10;
}
.fab-login-box .item {
flex-direction: column;
justify-content: center;
align-items: center;
/* #ifdef WEB */
cursor: pointer;
/* #endif */
}
.fab-login-box .item .logo {
width: 30px;
height: 30px;
border-radius: 100px;
border: solid 1px #F6F6F6;
}
.fab-login-box .item .title {
text-align: center;
color: #999;
font-size: 10px;
width: 70px;
height: 20px;
line-height: 20px;
}
</style>
\ No newline at end of file
<template>
<view>
<text class="uni-id-icon" :style="{color,'fontSize':size + 'px'}">{{iconCode}}</text>
</view>
</template>
<script>
export default {
props: {
type: {
type: String,
default: "",
required: true
},
color: {
type: String,
default: "#ccc"
},
size: {
type: Number,
default: 16
},
},
computed: {
iconCode() : string {
const iconCode : UTSJSONObject = {
"clear": "\ue622",
"down": "\ue600",
"right-arrow": "\ue8f1",
"jiahao": "\ue602",
"eyes": "\ue62b",
"advert": "\ue609"
};
return iconCode.getString(this.type) as string
}
},
data() {
return {}
}
}
</script>
<style>
@font-face {
font-family: "uni-id-icon";
src: url('/uni_modules/uni-id-pages-x/static/fonts/uni-id-icon.ttf');
}
.uni-id-icon {
font-family: "uni-id-icon";
font-size: 16px;
font-style: normal;
}
</style>
\ No newline at end of file
<template>
<view class="input-box" :class="{border}">
<!-- <solt></solt> -->
<text v-if="hasTitleAndContent" class="title">{{title}}</text>
<input class="input" ref="input" :placeholder="_placeholder" :class="{'move-down':hasTitleAndContent}"
:value="modelValue" @input="updateValue" :type="type" :maxlength="maxlength"
:password="password && !visiblePwd" :focus="focus" @blur="onBlur" @focus="onFocus" @confirm="confirm"
:confirm-type="confirmType" />
<view class="my-input-icon-box" v-if="modelValue">
<uni-id-pages-x-icons v-if="password" :color="visiblePwd?'#0070ff':'#ddd'"
@click="setVisiblePwd('click')" @touchstart="setVisiblePwd('touchstart')" @touchend="setVisiblePwd('touchend')" @touchcancel="setVisiblePwd('touchcancel')"
type="eyes" />
<view class="password-icon">
<uni-id-pages-x-icons @click="clearValue" type="clear" color="#ddd" />
</view>
</view>
</view>
</template>
<script>
export default {
name: 'MyInput',
emits: ["modelValue", "blur", "focus", "confirm"],
data() {
return {
visiblePwd: false
}
},
computed: {
_placeholder() : string {
if (this.placeholder.length > 0) {
return this.placeholder
} else if (this.title.length > 0) {
return "请输入" + this.title
} else {
return ""
}
},
hasTitleAndContent() : boolean {
return this.title.length > 0 && this.modelValue.length > 0
}
},
props: {
title: {
type: String,
default: ""
},
modelValue: {
type: String,
default: ""
},
type: {
type: String,
default: "text"
},
placeholder: {
type: String,
default: ""
},
maxlength: {
type: Number,
default: 140
},
password: {
type: Boolean,
default: false
},
focus: {
type: Boolean,
default: false
},
border: {
type: Boolean,
default: false
},
confirmType: {
type: String,
default: "done"
},
/**
* @param touch click
*/
visiblePwdOption:{
type: String,
default: "click"
}
},
methods: {
updateValue(event : InputEvent) {
// console.log('event.detail.value', event.detail.value);
this.$emit('update:modelValue', event.detail.value);
},
clearValue() {
this.$emit('update:modelValue', '');
this.setFocus(true)
},
onBlur() {
// this.$emit('update:focus', false);
this.$emit('blur');
},
onFocus() {
this.$emit('focus');
// this.$emit('update:focus', true);
},
setFocus(state : boolean) {
const inputEl = this.$refs['input'] as Element
if (state) {
inputEl.focus();
} else {
inputEl.blur();
}
},
confirm(event : InputConfirmEvent) {
this.$emit('confirm', event)
},
setVisiblePwd(eventName:string){
if(eventName == "click" && this.visiblePwdOption == 'click'){
this.visiblePwd = !this.visiblePwd
}else if(eventName == "touch"){
switch (eventName){
case "touchstart":
this.visiblePwd = true
break;
case "touchend":
this.visiblePwd = false
break;
case "touchcancel":
this.visiblePwd = false
break;
}
}
}
}
}
</script>
<style lang="scss" scoped>
.input-box {
position: relative;
margin: 0;
background-color: #F8F8F8;
height: 50px !important;
border-radius: 5rpx;
// border: 1px solid #000;
}
.border {
border: 1px solid #86868b;
}
.input {
border-radius: 5px;
height: 50px;
padding: 0 16rpx 0 12px;
font-size: 14px;
top: 0;
position: relative;
}
.title {
position: absolute;
top: 0;
left: 16rpx;
color: #555;
font-weight: 400;
font-size: 12px;
padding-left:5px;
height: 30px;
line-height: 30px;
}
.input.move-down {
top: 22px;
height: 22px;
}
.my-input-icon-box {
position: absolute;
right: 10px;
bottom:14px;
flex-direction: row;
}
.password-icon {
margin-left: 5px;
}
</style>
\ No newline at end of file
<template>
<view class="form">
<text class="pwd-login-title">账号密码登录</text>
<uni-id-pages-x-input class="my-input" title="账号" placeholder="请输入手机号/用户名/邮箱" v-model="username" ref="username"></uni-id-pages-x-input>
<uni-id-pages-x-input class="my-input" title="密码" v-model="password" :password="true" ref="password"></uni-id-pages-x-input>
<!-- 同意隐私政策协议 -->
<uni-id-pages-x-agreements class="agreements-box" ref="agreements" scope="login"/>
<button class="uni-btn" :type="password.length > 0 && username.length >0 ? 'primary' : '' " @click="loginByPwd">登录</button>
</view>
<view class="link-group">
<text class="link" @click="toRetrievePwd">忘记密码</text>
<text class="link" @click="toRegister">注册账号</text>
</view>
<!-- 多次登录错误时,获取登录时专用的图形验证码 (悬浮) -->
<uni-popup-captcha ref="popup-captcha" scene="login-by-pwd" v-model="loginCaptcha" title="请输入验证码"></uni-popup-captcha>
</template>
<script>
const uniIdCo = uniCloud.importObject("uni-id-co", { customUI: true })
import { state } from '@/uni_modules/uni-id-pages-x/store.uts';
import { loginSuccess } from '@/uni_modules/uni-id-pages-x/common/common.uts';
export default {
name: "uni-id-pages-x-loginByPwd",
data() {
return {
username: "",
password: "",
needCaptcha: false,
loginCaptcha: ""
}
},
mounted() {
setTimeout(() => {
(this.$refs['username'] as UniIdPagesXInputComponentPublicInstance).setFocus(true);
}, 300)
},
methods: {
showPopupCaptcha(callback : () => void) {
(this.$refs["agreements"] as UniIdPagesXAgreementsComponentPublicInstance).showPopupAgreements(callback)
},
loginByPwd() {
if (this.username.length == 0) {
(this.$refs['username'] as UniIdPagesXInputComponentPublicInstance).setFocus(true);
return uni.showToast({
title: '请输入手机号/用户名/邮箱',
icon: 'none',
duration: 3000
});
}
if (this.password.length == 0) {
(this.$refs['password'] as UniIdPagesXInputComponentPublicInstance).setFocus(true);
return uni.showToast({
title: '请输入密码',
icon: 'none',
duration: 3000
});
}
if (this.needCaptcha && this.loginCaptcha.length != 4) {
(this.$refs['popup-captcha'] as UniPopupCaptchaComponentPublicInstance).open(() => {
this.loginByPwd()
});
return uni.showToast({
title: '请输入验证码',
icon: 'none',
duration: 3000
});
}
const data : UTSJSONObject = {
"password": this.password,
"captcha": this.loginCaptcha
}
let keyName = 'username'
if (/^1\d{10}$/.test(this.username)) {
keyName = "mobile"
} else if (/@/.test(this.username)) {
keyName = "email"
}
data.set(keyName, this.username)
// console.log('data',data);
if (state.pendingAgreements) {
this.showPopupCaptcha(() => {
// console.log('state.pendingAgreements', state.pendingAgreements);
if (!state.pendingAgreements) {
this.loginByPwd()
}
})
return
}
uni.showLoading({
title: '登录中',
mask: false
});
uniIdCo.login(data)
.finally(() : void => {
uni.hideLoading()
this.loginCaptcha = ""
})
.then(e => {
// console.log('uniIdCo.login res',e);
loginSuccess(e)
})
.catch<void>((err : any | null) : void => {
const error = err as UniCloudError
console.error(error)
console.error(error.code)
if (error.code == 'uni-id-captcha-required') {
this.needCaptcha = true;
//登录失败,自动重新获取验证码
(this.$refs['popup-captcha'] as UniPopupCaptchaComponentPublicInstance).open(() => {
this.loginByPwd()
});
} else {
uni.showToast({
title: error.message,
icon: 'none',
duration: 3000
});
}
})
},
toRegister() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/register/register"
})
},
toRetrievePwd() {
let url = "/uni_modules/uni-id-pages-x/pages/retrieve/retrieve"
if (/^1\d{10}$/.test(this.username)) {
url += "?mobile=" + this.username
} else if (/@/.test(this.username)) {
url += "?email=" + this.username
}
uni.navigateTo({ url })
},
}
}
</script>
<style lang="scss" scoped>
.pwd-login-title {
font-size: 14px;
font-weight: 700;
}
.link-group {
flex-direction: row;
width: 600rpx;
margin-left: 75rpx;
margin-top: 15px;
justify-content: space-between;
}
.link-group .link {
color: #083679;
font-size: 12px;
}
</style>
\ No newline at end of file
<template>
<view>
<text class="tip">未注册的账号验证通过后将自动注册</text>
<view class="form">
<!-- 获取验证码组件(输入手机号码+发送短信验证码所需的图形验证码,获得短信验证码) -->
<uni-id-pages-x-smsCode ref="smsCode" @input="smsCodeInput" ></uni-id-pages-x-smsCode>
<!-- 同意隐私政策协议 -->
<uni-id-pages-x-agreements class="agreements-box" ref="agreements" scope="login"/>
<button class="uni-btn" type="primary" @click="sendSmsCode">获取手机验证码</button>
</view>
<!-- 多次登录错误时,获取登录时专用的图形验证码 (悬浮) -->
<uni-popup-captcha ref="captcha" scene="login-by-sms" v-model="captcha" title="请输入验证码"></uni-popup-captcha>
</view>
</template>
<script>
import { state } from '@/uni_modules/uni-id-pages-x/store.uts';
import { loginSuccess } from '@/uni_modules/uni-id-pages-x/common/common.uts';
const uniIdCo = uniCloud.importObject("uni-id-co", { "customUI": true })
export default {
name: "uni-id-pages-x-loginBySmsCode",
data() {
return {
captcha: "",
$smsCodeRef: null as null | UniIdPagesXSmsCodeComponentPublicInstance
}
},
computed: {
},
mounted() {
this.$smsCodeRef = (this.$refs["smsCode"] as UniIdPagesXSmsCodeComponentPublicInstance)
},
methods: {
sendSmsCode() {
this.$smsCodeRef!.$callMethod('sendSmsCode');
},
smsCodeInput(param : UTSJSONObject) {
// console.log('smsCodeInput param', param);
const mobile = param.getString("mobile") as string;
const code = param.getString("code") as string;
if (mobile.length == 11 && code.length == 6) {
this.login(param)
}
},
showPopupCaptcha(callback : () => void) {
(this.$refs["agreements"] as UniIdPagesXAgreementsComponentPublicInstance).showPopupAgreements(callback)
},
login(param : UTSJSONObject) {
uni.showLoading({ "title": "登录中" })
uniIdCo.loginBySms(param)
.finally(() : void => {
uni.hideLoading()
})
.then<void>((e : UTSJSONObject) : void => {
// console.log('then');
// console.log(e);
this.$smsCodeRef!.$callMethod("hideCodeInput");
uni.showToast({
title: '登录成功',
icon: 'none'
});
loginSuccess(e)
this.$smsCodeRef!.$callMethod('clearCodeInput')
})
.catch<void>((err : any | null) : void => {
const error = err as UniCloudError
console.error(error)
console.error(error.code)
if (["uni-id-captcha-required", "uni-captcha-verify-fail"].includes(error.code as string)) {
(this.$refs["captcha"] as UniPopupCaptchaComponentPublicInstance).open(() => {
this.login(param)
});
} else {
uni.showToast({
title: error.message,
icon: 'none',
mask: false
});
this.$smsCodeRef!.$callMethod('clearCodeInput')
this.captcha = ""
}
})
}
}
}
</script>
<style>
.tip {
font-size: 12px;
padding: 0 0 5px 10px;
}
</style>
\ No newline at end of file
<template>
<uni-popup ref="popup">
<view class="dialog-box">
<text class="title">{{title}}</text>
<input class="input" ref="input" type="text" v-model="value" cursor-spacing="210" />
<view class="btn-group">
<text class="btn" @click="cancel">取消</text>
<text class="btn confirm" @click="confirm">确认</text>
</view>
</view>
</uni-popup>
</template>
<script>
let confirmCallBack = (_ : string) : void => {}// console.log('未传入回调函数,value:', value)
export default {
emits: ["confirm"],
data() {
return {
title: "默认标题" as string,
value: "" as string
}
},
methods: {
open(param : UTSJSONObject, callback : (value : string) => void) : void {
// console.log('param', param);
this.title = (param.getString("title") as string);
let value = param.getString("value")
if (value != null) {
this.value = value
}
confirmCallBack = callback;
(this.$refs['popup'] as UniPopupComponentPublicInstance).open();
this.$nextTick(() => {
(this.$refs['input'] as Element).focus()
})
},
cancel() {
(this.$refs['popup'] as UniPopupComponentPublicInstance).close();
this.value = "";
this.$emit('cancel')
},
confirm() {
if (this.value.length > 0) {
this.$emit('confirm', this.value)
confirmCallBack(this.value);
(this.$refs['popup'] as UniPopupComponentPublicInstance).close();
this.value = "";
} else {
uni.showToast({
title: '不能为空',
icon: 'none'
});
}
}
}
}
</script>
<style>
.dialog-box {
background-color: #FFF;
width: 600rpx;
border-radius: 5px;
}
.dialog-box .title {
font-weight: 700;
font-size: 16px;
text-align: center;
margin-top: 15px;
}
.dialog-box .input {
border: 1px solid #eee;
border-radius: 5px;
margin: 10px 20px;
height: 38px;
padding: 0 30rpx;
}
.dialog-box .btn-group {
border-top: 1px solid #eee;
flex-direction: row;
justify-content: space-around;
}
.dialog-box .btn-group .btn {
flex: 1;
text-align: center;
color: #666;
height: 45px;
line-height: 45px;
}
.dialog-box .btn-group .btn.confirm {
color: #115ff7;
border-left: solid 1px #eee;
}
</style>
\ No newline at end of file
<template>
<uni-popup ref="popup" :mask-click="false">
<view class="sms-code-content">
<!-- 顶部文字 -->
<text class="sms-code-title">{{title}}</text>
<text class="sms-code-tip">短信验证码已发送至{{mobile}}</text>
<view class="code-input-list">
<template v-for="(item,i) in smsCodeList" :key="i">
<input @input="setSmsCode(i,$event as InputEvent)" :value="item" type="number" ref="code-input"
class="code-input" maxlength="7" @focus="onFocus(i)" />
</template>
</view>
<view class="fab-sms-code-input-foot">
<text class="close" @click="clear();hide();">关闭</text>
<text @click="reGetSmsCode" :class="'get-sms-code-tip'+ ((reverseNumber == 0) ? '-active' : '')">{{reGetSmsCodeTip}}</text>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
emits: ["modelValue", "blur", "focus", "reGetSmsCode"],
data() {
return {
smsCodeList: ["\u200b", "\u200b", "\u200b", "\u200b", "\u200b", "\u200b"],
isOpen: false
}
},
props: {
title: {
type: String,
default: "请输入验证码"
},
mobile: {
type: String,
default: ""
},
modelValue: {
type: String,
default: ""
},
reverseNumber: {
type: Number,
default: 0
},
},
computed: {
reGetSmsCodeTip() : string {
if (this.reverseNumber == 0) return "重新获取";
return "重新发送" + '(' + this.reverseNumber + 's)';
},
smsCode() : string {
return this.smsCodeList.map((item) : string => item.replace(/\u200b/g, '')).join("")
}
},
watch: {
smsCode(smsCode) {
this.$emit('update:modelValue', smsCode);
},
},
methods: {
show() {
(this.$refs['popup'] as UniPopupComponentPublicInstance).open();
this.$nextTick(() => {
(this.$refs["code-input"] as Element[])[0].focus();
})
this.isOpen = true
},
hide() {
(this.$refs['popup'] as UniPopupComponentPublicInstance).close();
this.isOpen = false
// console.log('hide success');
},
clear() {
this.smsCodeList = ["\u200b", "\u200b", "\u200b", "\u200b", "\u200b", "\u200b"];
if (this.isOpen) {
(this.$refs["code-input"] as Element[])[0].focus();
// console.log('clear success');
} else {
// console.log('已经关了,不能清空');
}
},
setSmsCode(i : number, e : InputEvent) {
const { value } = e.detail
// console.log('~~',value,value.length);
// 已满6位数就直接调登录
let $value = value.replace(/\u200b/g, '')
if ($value.length == 6) {
// (this.$refs["code-input"] as Element[])[0].focus();
// console.log('~~~~~', $value, $value.split(''));
$value.split('').forEach((item : string, index : number) => {
// console.log('index', index, item);
this.smsCodeList[index] = "\u200b" + item
})
return
}
// 限制每个空格内的文字不超过2位
if (value.length > 2) {
this.$nextTick(() => {
let newValue = value.slice(value.length - 1)
// console.log('newValue', newValue);
this.smsCodeList[i] = newValue
})
}
// 被删除完就直接后退一格
if (value.length == 0) {
this.smsCodeList[i] = ""
this.$nextTick(() => {
this.smsCodeList[i] = '\u200b'
})
if (i != 0) {
(this.$refs["code-input"] as Element[])[i - 1].focus();
this.smsCodeList[i - 1] = ""
}
} else if (value != "\u200b") {
this.smsCodeList[i] = value;
if (i != (this.smsCodeList.length - 1)) {
(this.$refs["code-input"] as Element[])[i + 1].focus();
} else {
// console.log(i, (this.smsCodeList.length - 1));
}
}
},
codeChange(i : number, e : InputEvent) {
// console.log(i, e);
},
onFocus(i : number) {
if (this.smsCodeList[i].length == 0) {
// console.log('i', i);
this.smsCodeList[i] = '\u200b'
}
},
reGetSmsCode() {
this.$emit("reGetSmsCode")
}
}
}
</script>
<style lang="scss" scoped>
.sms-code-content {
background-color: #FFF;
width: 550rpx;
justify-content: center;
align-items: center;
padding: 15px 20px;
border-radius: 5px;
}
.sms-code-title,
.sms-code-tip {
text-align: left;
padding-left:20px;
width: 550rpx;
}
.sms-code-tip {
color: #555;
font-weight: 400;
font-size: 12px;
margin-top: 5px;
}
.code-input-list {
flex-direction: row;
}
.code-input-list .code-input {
width: 30px;
height: 30px;
border: 1px solid #eee;
border-radius: 5px;
margin: 10px 5px;
text-align: center;
}
.fab-sms-code-input-foot {
margin-top: 5px;
margin-left: 300rpx;
position: relative;
left: -15px;
width: 250rpx;
flex-direction: row;
justify-content: flex-end;
}
.fab-sms-code-input-foot .close {
width: 80rpx;
font-size: 12px;
color: #888;
text-align: center;
margin-right: 26rpx;
}
.get-sms-code-tip,
.get-sms-code-tip-active {
font-size: 12px;
text-align: right;
color: #AAA;
}
.get-sms-code-tip-active {
color: #005eca;
}
</style>
\ No newline at end of file
<template>
<view class="mobile-box">
<view @click="chooseArea" class="area">
<text>+86</text>
<uni-id-pages-x-icons :size="10" color="#666" @click="chooseArea" type="down" />
</view>
<view class="input-box">
<uni-id-pages-x-input type="number" :border="false" v-model="mobile" :maxlength="11" placeholder="请输入11位手机号"
ref="mobileInput"></uni-id-pages-x-input>
</view>
</view>
<!-- 获取发送短信的验证码的图形验证码组件 -->
<view class="send-sms-captcha-box">
<uni-captcha ref="sendSmsCaptcha" scene="send-sms-code" v-model="sendSmsCaptcha" />
</view>
<!-- 悬浮的短信验证码输入框 -->
<fab-sms-code-input v-model="smsCode" :mobile="mobile" :reverseNumber="reverseNumber" ref="fab-sms-code-input"
@reGetSmsCode="reGetSmsCode" title="验证并登录" />
</template>
<script>
import fabSmsCodeInput from './fab-sms-code-input.uvue';
import { state } from '@/uni_modules/uni-id-pages-x/store.uts';
export default {
name: "uni-id-pages-x-smsCode",
components: { fabSmsCodeInput },
data() {
return {
mobile: "",
sendSmsCaptcha: "",
smsCode: "",
$fabSmsCodeInputEl: null as null | ComponentPublicInstance,
$sendSmsCaptchaCP: null as null | UniCaptchaComponentPublicInstance,
reverseNumber: 0
}
},
props: {
scene: {
type: String,
default: "login-by-sms"
},
autoSend: {
type: Boolean,
default: true
}
},
computed: {
},
watch: {
// 手机号输满11位时,自动给发送短信验证码的图形验证码“获取焦点”
mobile(mobile) {
this.emitInput()
if (mobile.length == 11) {
this.$sendSmsCaptchaCP!.setFocus(true);
// 倒计时归零,允许再次发送
this.reverseNumber = 0
}
},
// 图形验证码填好直接自动发送 短信验证码
sendSmsCaptcha() {
this.emitInput()
if (this.autoSend && this.sendSmsCaptcha.length == 4 && this.mobile.length == 11) {
this.sendSmsCode()
}
},
smsCode() {
this.emitInput()
}
},
mounted() {
this.$fabSmsCodeInputEl = this.$refs["fab-sms-code-input"] as ComponentPublicInstance;
this.$sendSmsCaptchaCP = this.$refs["sendSmsCaptcha"] as UniCaptchaComponentPublicInstance;
// 加载好,手机号码输入框就自动获取焦点
// TO 临时方案解决 this.$nextTick 无效,由setTimeout 300 代替
setTimeout(() => {
this.setMobileInputFocus(true)
}, 300)
},
methods: {
setMobileInputFocus(val:boolean){
(this.$refs['mobileInput'] as UniIdPagesXInputComponentPublicInstance).setFocus(val);
},
showCodeInput() {
if (reverseNumber == 0) {
this.reverseNumber = 6
//开始倒计时
let reverseTimer = function () { }
reverseTimer = () => {
if (this.reverseNumber != 0) {
this.reverseNumber--;
setTimeout(() => {
reverseTimer()
}, 1000)
}
}
reverseTimer()
}
this.$fabSmsCodeInputEl?.$callMethod('show', true);
},
emitInput() {
const param : UTSJSONObject = {
"mobile": this.mobile,
"code": this.smsCode,
"sendSmsCaptcha": this.sendSmsCaptcha,
}
this.$emit("input", param)
},
chooseArea() {
uni.showToast({
title: "目前只支持中国",
icon: "none"
});
},
// 重新获取短信验证码
reGetSmsCode() {
this.hideCodeInput();
this.clearCodeInput();
this.$sendSmsCaptchaCP!.getImageCaptcha(true);
},
hideCodeInput() {
this.$fabSmsCodeInputEl!.$callMethod("hide");
},
clearCodeInput() {
this.$fabSmsCodeInputEl!.$callMethod('clear')
},
reset() {
this.smsCode = ""
this.sendSmsCaptcha = ""
this.clearCodeInput()
this.hideCodeInput()
// console.log('reset');
},
sendSmsCode() {
// console.log("state.pendingAgreements", state.pendingAgreements);
if (state.pendingAgreements) {
// uni.hideKeyboard();
this.$sendSmsCaptchaCP!.setFocus(false);
(this.$parent as ComponentPublicInstance).$callMethod("showPopupCaptcha", () => {
// console.log('state.pendingAgreements', state.pendingAgreements);
if (!state.pendingAgreements) {
this.sendSmsCode()
}
});
uni.showToast({
title: '未同意隐私政策协议',
icon: 'none'
});
return
}
// console.log('sendSmsCode');
// 如果还在倒计时就显示出来输入框,阻止发送
if (this.reverseNumber != 0) {
return this.showCodeInput()
}
let reg_mobile = /^1\d{10}$/;
if (!reg_mobile.test(this.mobile)) {
(this.$refs['mobileInput'] as UniIdPagesXInputComponentPublicInstance).setFocus(true)
uni.showToast({
title: "手机号格式错误",
icon: 'none',
duration: 3000
})
return
}
if (this.sendSmsCaptcha.length != 4) {
this.$sendSmsCaptchaCP!.setFocus(true)
uni.showToast({
title: '请先输入图形验证码',
icon: 'none',
duration: 3000
});
return
}
// const param : UTSJSONObject
// console.log('sendSmsCode',{
// "mobile": this.mobile,
// "scene": "login-by-sms",
// "captcha": this.sendSmsCaptcha
// });
uni.showLoading({ "title": "发送中" })
const uniIdCo = uniCloud.importObject("uni-id-co", { "customUI": true })
uniIdCo.sendSmsCode({
"mobile": this.mobile,
"scene": this.scene,
"captcha": this.sendSmsCaptcha
})
.finally(() : void => {
uni.hideLoading()
})
.then((_ : UTSJSONObject) => {
uni.showToast({
title: "短信验证码发送成功",
icon: 'none',
duration: 3000
});
// console.log('result', result);
this.showCodeInput()
})
.catch<void>((err : any | null) : void => {
const error = err as UniCloudError
// console.error(error.message)
// console.error(error.code)
switch (error.code) {
case "uni-captcha-verify-fail":
uni.showToast({
title: error.message,
icon: 'none',
duration: 3000,
mask: false
});
this.$sendSmsCaptchaCP!.getImageCaptcha(true);
this.sendSmsCaptcha = "";
break;
case "uni-id-invalid-sms-template-id":
this.showCodeInput()
uni.showToast({
title: ' 当前为测试模式,详情【控制台信息】',
icon: 'none',
duration: 3000
});
console.warn(error.message);
break;
default:
uni.showToast({
title: error.message,
icon: 'none',
duration: 3000
});
break;
}
})
},
}
}
</script>
<style>
.mobile-box {
position: relative;
flex-direction: row;
background-color: #F8F8F8;
justify-content: center;
align-items: center;
border-radius: 5rpx;
}
.area {
width: 90rpx;
justify-content: space-around;
align-items: center;
flex-direction: row;
margin-left: 10px;
}
.mobile-box .input-box {
flex: 1;
}
.send-sms-captcha-box {
margin-top: 10px;
}
</style>
\ No newline at end of file
export default {
"debug":true,
"agreements":{
"serviceUrl": "https://uniapp.dcloud.io/", // 用户服务协议链接
"privacyUrl": "https://uniapp.dcloud.io/", // 隐私政策条款链接
/*
* 哪些场景下显示
* 1. register注册(包括登录并注册,如:短信验证码登录,一键登录)
* 2. login登录(如:用户名密码登录,短信验证码登录)
*/
"scopeList": [
"register","login"
]
},
"needLogin":[
"/uni_modules/uni-id-pages-x/pages/userinfo/*"
],
"loginTypes":[
"username",
"smsCode",
// 以下登录方式 uni-app x 暂不支持
// "qq",
// "xiaomi",
// "sinaweibo",
// "taobao",
// "facebook",
// "google",
// "alipay",
// "douyin",
// "weixin",
// #ifdef APP
// "univerify",
// "apple"
// #endif
],
/**
* 密码强度
* super(超强:密码必须包含大小写字母、数字和特殊符号,长度范围:8-16位之间)
* strong(强: 密密码必须包含字母、数字和特殊符号,长度范围:8-16位之间)
* medium (中:密码必须为字母、数字和特殊符号任意两种的组合,长度范围:8-16位之间)
* weak(弱:密码必须包含字母和数字,长度范围:6-16位之间)
* 为空或false则不验证密码强度
*/
passwordStrength: 'weak'
} as UTSJSONObject
// 导入 autoReportPushClientId 模块,将再重新登录会 token 续期后自动将客户端上报到服务端
// import reportPushClientId from '@/uni_modules/uni-id-pages-x/lib/autoReportPushClientId.uts'
// 导入配置
import config from '@/uni_modules/uni-id-pages-x/config.uts'
import {state} from '@/uni_modules/uni-id-pages-x/store.uts';
// uni-id的云对象
const uniIdCo = uniCloud.importObject('uni-id-co', {
customUI: true
})
// 用户配置的登录方式、是否打开调试模式
const loginTypes = config.getArray<string>('loginTypes');
const debug = config.getBoolean('debug') as boolean;
export default async function () {
// 有打开调试模式的情况下
if (debug) {
// 1. 检查本地uni-id-pages中配置的登录方式,服务器端是否已经配置正确。否则提醒并引导去配置
// 调用云对象,获取服务端已正确配置的登录方式
const res = await uniIdCo.getSupportedLoginType()
let supportedLoginType = res.getArray<string>('supportedLoginType')
if(supportedLoginType == null){
supportedLoginType = []
}
// 登录方式,服务端和客户端的映射关系
const data:UTSJSONObject = {
"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:string):boolean =>{
let currentType = data.getString(type);
if(currentType == null){
currentType = ""
}
return !(supportedLoginType.includes(currentType))
})
if (list?.length != 0) {
console.error(
`错误:前端启用的登录方式:${list?.join(',')};没有在服务端完成配置。配置文件路径:"/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json"`
)
}
}
// #ifdef UNI-APP-X
// 如果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)
const checkNeedLogin = (url:string):boolean=>{
if(state.isLogin){
return false
}
const needLogin = config.getArray<string>('needLogin')
if (needLogin != null) {
return needLogin.every((item:string):boolean => {
return item == url || new RegExp(item).test(url)
})
}else{
return false
}
}
uni.addInterceptor('navigateTo',{
invoke:(e:NavigateToOptions)=>{
if (checkNeedLogin(e.url)) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
e.url = "/uni_modules/uni-id-pages-x/pages/login/login?uniIdRedirectUrl=" + encodeURIComponent(e.url)
}
}
})
uni.addInterceptor('redirectTo',{
invoke:(e:RedirectToOptions)=>{
if (checkNeedLogin(e.url)) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
e.url = "/uni_modules/uni-id-pages-x/pages/login/login?uniIdRedirectUrl=" + encodeURIComponent(e.url)
}
}
})
}
//同步客户端push_clientid至device表
const uniIdCo = uniCloud.importObject('uni-id-co', {
customUI: true
})
const reportPushClientId = () => {
uni.getPushClientId({
success: function (e) {
// console.log(e)
const pushClientId = e.cid
uniIdCo.setPushCid({
pushClientId
})
.then(res => {
// console.log('getPushClientId', res);
})
},
fail(e) {
// console.log(e)
}
})
}
export default reportPushClientId;
// console.log('已导入 autoReportPushClientId 模块,将再重新登录会 token 续期后自动将客户端上报到服务端');
uniCloud.onRefreshToken((_) => {
// console.log('onRefreshToken');
reportPushClientId()
})
\ No newline at end of file
{
"id": "uni-id-pages-x",
"displayName": "uni-id-pages-x",
"version": "0.0.0",
"description": "适用于 uni-app-x 的云端一体简单、统一、可扩展的用户中心页面模版",
"keywords": [
"用户管理",
"用户中心",
"短信验证码",
"login",
"登录",
"uni-id"
],
"repository": "",
"engines": {
"HBuilderX": "^3.97"
},
"dcloudext": {
"type": "uniapp-template-page",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "n",
"vue3": "y"
},
"App": {
"app-vue": "n",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "n",
"Android Browser": "n",
"微信浏览器(Android)": "n",
"QQ浏览器(Android)": "n"
},
"H5-pc": {
"Chrome": "n",
"IE": "n",
"Edge": "n",
"Firefox": "n",
"Safari": "n"
},
"小程序": {
"微信": "n",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}
\ No newline at end of file
<!-- 网络链接内容展示页(uni-id-pages-x中用于展示隐私政策协议内容) -->
<template>
<web-view class="web-view" v-if="url.length != 0" :src="url"></web-view>
</template>
<script>
export default {
onLoad(param : Map<string, string>) {
const url = param.get("url") as string;
let title = param.get("title") as string;
// console.log('url', url);
if (url.substring(0, 4) != 'http') {
uni.showModal({
title: "错误",
content: '不是一个有效的网站链接,' + '"' + url + '"',
showCancel: false,
confirmText: "知道了",
complete: () => {
uni.navigateBack()
}
});
title = "页面路径错误"
} else {
this.url = url;
}
if (title.length != 0) {
uni.setNavigationBarTitle({ title });
}
},
data() {
return {
url: ""
};
}
}
</script>
<style>
.web-view {
width: 750rpx;
flex: 1;
height: 100%;
}
</style>
\ No newline at end of file
<template>
<view class="page">
<!-- 顶部文字 -->
<!-- <text class="title">请选择登录方式{{loginType}}--isAgree:{{pendingAgreements}}</text> -->
<!-- 应用图标 -->
<image class="app-logo" :src="logo" mode="aspectFit"></image>
<!-- 苹果账号 微信账号 微信账号手机号 登录 -->
<view class="big-login-box" v-if="['apple','weixin', 'weixinMobile'].includes(loginType)">
<text class="tip">将根据第三方账号服务平台的授权范围获取你的信息</text>
<image v-if="loginType !== 'weixinMobile'" @click="login" :src="imgSrc" mode="widthFix" class="big-login-img">
</image>
<button v-else type="primary" open-type="getmobileNumber" @getmobilenumber="login"
class="uni-btn">微信授权手机号登录</button>
<!-- 同意隐私政策协议 -->
<uni-id-pages-x-agreements class="agreements-box" scope="login" />
</view>
<!-- 密码登录 -->
<uni-id-pages-x-loginByPwd v-if="loginType == 'username'" />
<!-- 手机号验证码登录 -->
<uni-id-pages-x-loginBySmsCode v-if="loginType == 'smsCode'" />
<!-- 固定定位的快捷登录按钮 -->
<uni-id-pages-x-fab-login :currentLoginType="loginType"
@changeLoginType="changeLoginType"></uni-id-pages-x-fab-login>
</view>
</template>
<script>
import { state } from '@/uni_modules/uni-id-pages-x/store.uts';
import config from '@/uni_modules/uni-id-pages-x/config.uts';
export default {
computed: {
//大快捷登录按钮图
imgSrc() : string {
return this.loginType == 'weixin' ? '/uni_modules/uni-id-pages-x/static/login/weixin.png' : '/uni_modules/uni-id-pages-x/static/app-plus/apple.png'
},
pendingAgreements() : boolean {
return state.pendingAgreements
}
},
data() {
return {
loginType: "username" as string,
logo: "/static/logo.png"
}
},
onLoad(param : Map<string, string>) {
const type = param.get("type")
// 如果传递了参数就按参数指定的登录方式,否则指定为 config 中配置的第一项
if (type != null) {
this.loginType = type
} else {
let loginTypes = config.getArray<string>("loginTypes");
this.loginType = loginTypes![0];
}
},
methods: {
login() {
},
changeLoginType(type : string) {
// if (["weixin", "apple", "univerify"].includes(type)) {
// }
this.loginType = type
}
}
}
</script>
<style lang="scss" scoped>
@import url("/uni_modules/uni-id-pages-x/common/common.scss");
.app-logo {
width: 150rpx;
height: 150rpx;
margin: 30px 300rpx;
}
.big-login-box .agreements-box {
justify-content: center;
}
.big-login-img {
margin: 10px 25rpx;
height: 60px;
width: 700rpx;
}
</style>
\ No newline at end of file
<template>
<view class="page">
<view class="form">
<view class="item">
<text class="required">*</text>
<uni-id-pages-x-input class="my-input" title="用户名" placeholder="请输入用户名(数字+字母的3~20位字符)" v-model="username"
ref="username"></uni-id-pages-x-input>
</view>
<view class="item">
<uni-id-pages-x-input class="my-input" title="昵称" placeholder="请输入用户名(限中文、数字、字母组成的3~20位字符)"
v-model="nickname" ref="nickname"></uni-id-pages-x-input>
</view>
<view class="item">
<text class="required">*</text>
<uni-id-pages-x-input class="my-input" title="设置密码" placeholder="请输入6位数密码" v-model="password"
:password="true" ref="password"></uni-id-pages-x-input>
</view>
<view class="item">
<text class="required">*</text>
<uni-id-pages-x-input class="my-input" title="重复密码" placeholder="请重新输入密码" v-model="password2" :password="true"
ref="password2"></uni-id-pages-x-input>
</view>
<view class="item mt">
<uni-captcha ref="captcha" scene="register" v-model="captcha" />
</view>
<!-- 隐私政策协议授权申请框 -->
<uni-id-pages-x-agreements class="agreements-box" ref="agreements" />
<button @click="registerBefore" type="primary">注册并登录</button>
</view>
</view>
</template>
<script>
import { loginSuccess, checkPassword } from '@/uni_modules/uni-id-pages-x/common/common.uts';
import { state } from '@/uni_modules/uni-id-pages-x/store.uts';
export default {
data() {
return {
username: "",
nickname: "",
password: "",
password2: "",
captcha: ""
}
},
methods: {
showPopupCaptcha(callback : () => void) {
(this.$refs["agreements"] as UniIdPagesXAgreementsComponentPublicInstance).showPopupAgreements(callback)
},
registerBefore() {
// TODO 为了自动化测试构建一个对象
let myForm:UTSJSONObject = {
username:this.username,
password:this.password,
password2: this.password2,
captcha:this.captcha
};
// 校验是否为空,并聚焦 (注意:这里使用数组格式,以确保校验按顺序执行)
[
["username", "用户名",],
["password", "密码",],
["captcha", "验证码"]
].forEach((item) => {
let key = item[0];
let errMsg = item[1] + "不能为空";
if (myForm.get(key) == "") {
// 给出错的项聚焦
(this.$refs[key] as ComponentPublicInstance).$callMethod('setFocus',true);
uni.showToast({
title: errMsg,
icon: "none",
duration: 3000
});
throw Error(errMsg)
}
});
// 校验用户名规则
const usernameRegExp = new RegExp("^(?=.*[a-z])(?=.*\\d)[a-z\\d]{3,20}$");
if (!usernameRegExp.test(this.username)) {
// 给出错的项聚焦
(this.$refs['username'] as UniIdPagesXInputComponentPublicInstance).setFocus(true);
return uni.showModal({
content: "用户名由数字和小写字母的3-20个字符组成",
showCancel: false,
confirmText: "知道了"
});
}
// 校验昵称规则
if (this.nickname.length > 0) {
const nicknameRegExp = new RegExp("^[\u4e00-\u9fa5a-z0-9]{3,20}$");
if (!nicknameRegExp.test(this.username)) {
// 给出错的项聚焦
(this.$refs['username'] as UniIdPagesXInputComponentPublicInstance).setFocus(true);
return uni.showModal({
content: "昵称由小写字母、数字、中文组成的3-20位字符串",
showCancel: false,
confirmText: "知道了"
});
}
}
let checkPasswordRes = checkPassword(this.password);
let isPass = checkPasswordRes.getBoolean("pass") as Boolean
if (!isPass) {
const errMsg = checkPasswordRes.getString("errMsg")
return uni.showModal({
content: errMsg,
showCancel: false,
confirmText: "知道了",
complete() {
(this.$refs['password'] as UniIdPagesXInputComponentPublicInstance).setFocus(true);
}
});
}
if (this.password != this.password2) {
return uni.showModal({
content: "两次输入的密码不一致",
showCancel: false,
confirmText: "知道了",
complete() {
(this.$refs["password2"] as UniIdPagesXInputComponentPublicInstance).setFocus(true);
}
});
}
if (this.captcha.length != 4) {
return uni.showModal({
content: "请输入验证码",
showCancel: false,
confirmText: "知道了",
complete() {
(this.$refs["captcha"] as UniCaptchaComponentPublicInstance).setFocus(true);
}
});
}
if (state.pendingAgreements) {
this.showPopupCaptcha(() => {
// console.log('state.pendingAgreements', state.pendingAgreements);
if (!state.pendingAgreements) {
this.registerBefore()
}
})
return
}
this.register()
},
register() {
const data: UTSJSONObject = {
"username": this.username,
"nickname": this.nickname,
"password": this.password,
"password2": this.password2,
"captcha": this.captcha
}
const uniIdCo = uniCloud.importObject("uni-id-co", { customUI: true });
uni.showLoading({ "title": "请求中" })
uniIdCo.registerUser(data)
.finally(() => uni.hideLoading())
.then(e => {
// console.log('e: ',e);
uni.showToast({
title: '注册成功',
icon: 'none'
});
loginSuccess(e)
})
.catch<void>((err : any | null) : void => {
const error = err as UniCloudError
console.error(error)
console.error(error.code)
if (error.code == 'uni-captcha-verify-fail') {
(this.$refs["captcha"] as UniCaptchaComponentPublicInstance).getImageCaptcha(true);
}
uni.showToast({
title: error.message,
icon: 'none'
});
})
}
}
}
</script>
<style lang="scss" scoped>
@import url("/uni_modules/uni-id-pages-x/common/common.scss");
.form {
padding-left: 15px;
}
.item {
margin-top: 8px;
position: relative;
padding-left: 15px;
margin-left: -15px;
}
.mt{
margin-top: 16px;
}
.required {
position: absolute;
color: red;
left: 0px;
top: 15px;
}
</style>
\ No newline at end of file
此差异已折叠。
此差异已折叠。
文档详情查看:[]()
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册