mirror of
https://github.com/kuaifan/dootask.git
synced 2026-03-07 18:07:05 +00:00
feat: 二维码登录
This commit is contained in:
parent
a3c0decaf0
commit
49ac519a5e
@ -144,6 +144,40 @@ class UsersController extends AbstractController
|
||||
return Base::retSuccess($type == 'reg' ? "注册成功" : "登录成功", $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/login/qrcode 02. 二维码登录
|
||||
*
|
||||
* @apiDescription 通过二维码code登录(或:是否登录成功)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName login__qrcode
|
||||
*
|
||||
* @apiParam {String} type 类型
|
||||
* - login: 登录(用于:app登录)
|
||||
* - status: 状态 (默认,用于:网页、客户端获取)
|
||||
* @apiParam {String} code 二维码 code
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1需要、0不需要)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function login__qrcode()
|
||||
{
|
||||
$type = trim(Request::input('type'));
|
||||
$code = trim(Request::input('code'));
|
||||
//
|
||||
if (strlen($code) < 32) {
|
||||
return Base::retError("参数错误");
|
||||
}
|
||||
if ($type === 'login') {
|
||||
$user = User::auth();
|
||||
Cache::put("User::qrcode:" . $code, $user->userid, Carbon::now()->addMinute());
|
||||
return Base::retSuccess("扫码成功");
|
||||
}
|
||||
// todo 登录成功
|
||||
return Base::retError("No identity");
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/login/needcode 02. 是否需要验证码
|
||||
*
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"url": "git+https://github.com/kuaifan/dootask.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.2",
|
||||
"axios": "^0.24.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.7.2",
|
||||
|
||||
@ -4,82 +4,99 @@
|
||||
<div class="login-body">
|
||||
<div class="login-logo no-dark-content" :class="{'can-click':needStartHome}" @click="goHome"></div>
|
||||
<div class="login-box">
|
||||
<div class="login-mode-switch">
|
||||
<div class="login-mode-switch-box">
|
||||
<ETooltip :disabled="windowSmall || $isEEUiApp" :content="$L(loginMode=='qrcode' ? '帐号登录' : '扫码登录')" placement="left">
|
||||
<span class="login-mode-switch-icon" @click="switchLoginMode">
|
||||
<svg v-if="loginMode=='qrcode'" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-icon="PcOutlined"><path d="M23 16a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h18a2 2 0 0 1 2 2v12ZM21 4H3v9h18V4ZM3 15v1h18v-1H3Zm3 6a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H7a1 1 0 0 1-1-1Z" fill="currentColor"></path></svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-icon="QrOutlined"><path d="M6.5 7.5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1Z" fill="currentColor"></path><path d="M4.5 2.5c-1.1 0-2 .9-2 2v7c0 1.1.9 2 2 2h7c1.1 0 2-.9 2-2v-7c0-1.1-.9-2-2-2h-7Zm0 2h7v7h-7v-7ZM11 16a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm0 3.5a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm4-7.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm3.5 0a1 1 0 0 1 1-1h1a1 1 0 1 1 0 2h-1a1 1 0 0 1-1-1ZM15 17c0-1.1.9-2 2-2h2.5c1.1 0 2 .9 2 2v2.5c0 1.1-.9 2-2 2H17c-1.1 0-2-.9-2-2V17Zm4.5 0H17v2.5h2.5V17Zm-15-2c-1.1 0-2 .9-2 2v2.5c0 1.1.9 2 2 2H7c1.1 0 2-.9 2-2V17c0-1.1-.9-2-2-2H4.5Zm0 2H7v2.5H4.5V17ZM15 4.5c0-1.1.9-2 2-2h2.5c1.1 0 2 .9 2 2V7c0 1.1-.9 2-2 2H17c-1.1 0-2-.9-2-2V4.5Zm4.5 0H17V7h2.5V4.5Z" fill="currentColor"></path></svg>
|
||||
</span>
|
||||
</ETooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-title">{{welcomeTitle}}</div>
|
||||
|
||||
<div v-if="loginType=='reg'" class="login-subtitle">{{$L('输入您的信息以创建帐户。')}}</div>
|
||||
<div v-else class="login-subtitle">{{$L('输入您的凭证以访问您的帐户。')}}</div>
|
||||
<div class="login-subtitle">{{$L(subTitle)}}</div>
|
||||
|
||||
<div class="login-input">
|
||||
<Input
|
||||
v-if="isSoftware && cacheServerUrl"
|
||||
:value="$A.getDomain(cacheServerUrl)"
|
||||
prefix="ios-globe-outline"
|
||||
size="large"
|
||||
readonly
|
||||
clearable
|
||||
@on-clear="setServerUrl('')"/>
|
||||
<transition name="login-mode">
|
||||
<div v-if="loginMode=='qrcode'" class="login-qrcode" @click="qrcodeRefresh">
|
||||
<VueQrcode :value="qrcodeUrl" :options="{width:200,margin:0}"></VueQrcode>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="login-mode">
|
||||
<div v-if="loginMode=='access'" class="login-access">
|
||||
<Input
|
||||
v-if="isSoftware && cacheServerUrl"
|
||||
:value="$A.getDomain(cacheServerUrl)"
|
||||
prefix="ios-globe-outline"
|
||||
size="large"
|
||||
readonly
|
||||
clearable
|
||||
@on-clear="setServerUrl('')"/>
|
||||
|
||||
<Input
|
||||
v-model="email"
|
||||
ref="email"
|
||||
prefix="ios-mail-outline"
|
||||
:placeholder="$L('输入您的电子邮件')"
|
||||
type="email"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
@on-blur="onBlur"
|
||||
clearable/>
|
||||
<Input
|
||||
v-model="email"
|
||||
ref="email"
|
||||
prefix="ios-mail-outline"
|
||||
:placeholder="$L('输入您的电子邮件')"
|
||||
type="email"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
@on-blur="onBlur"
|
||||
clearable/>
|
||||
|
||||
<Input
|
||||
v-model="password"
|
||||
ref="password"
|
||||
prefix="ios-lock-outline"
|
||||
:placeholder="$L('输入您的密码')"
|
||||
type="password"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable/>
|
||||
<Input
|
||||
v-model="password"
|
||||
ref="password"
|
||||
prefix="ios-lock-outline"
|
||||
:placeholder="$L('输入您的密码')"
|
||||
type="password"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable/>
|
||||
|
||||
<Input
|
||||
v-if="loginType=='reg'"
|
||||
v-model="password2"
|
||||
ref="password2"
|
||||
prefix="ios-lock-outline"
|
||||
:placeholder="$L('输入确认密码')"
|
||||
type="password"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable/>
|
||||
<Input
|
||||
v-if="loginType=='reg' && needInvite"
|
||||
v-model="invite"
|
||||
ref="invite"
|
||||
class="login-code"
|
||||
:placeholder="$L('请输入注册邀请码')"
|
||||
type="text"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable><span slot="prepend"> {{$L('邀请码')}} </span></Input>
|
||||
<Input
|
||||
v-if="loginType=='reg'"
|
||||
v-model="password2"
|
||||
ref="password2"
|
||||
prefix="ios-lock-outline"
|
||||
:placeholder="$L('输入确认密码')"
|
||||
type="password"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable/>
|
||||
<Input
|
||||
v-if="loginType=='reg' && needInvite"
|
||||
v-model="invite"
|
||||
ref="invite"
|
||||
class="login-code"
|
||||
:placeholder="$L('请输入注册邀请码')"
|
||||
type="text"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable><span slot="prepend"> {{$L('邀请码')}} </span></Input>
|
||||
|
||||
<Input
|
||||
v-if="loginType=='login' && codeNeed"
|
||||
v-model="code"
|
||||
ref="code"
|
||||
class="login-code"
|
||||
:placeholder="$L('输入图形验证码')"
|
||||
type="text"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable>
|
||||
<Icon type="ios-checkmark-circle-outline" class="login-icon" slot="prepend"></Icon>
|
||||
<div slot="append" class="login-code-end" @click="reCode"><img :src="codeUrl"/></div>
|
||||
</Input>
|
||||
<Input
|
||||
v-if="loginType=='login' && codeNeed"
|
||||
v-model="code"
|
||||
ref="code"
|
||||
class="login-code"
|
||||
:placeholder="$L('输入图形验证码')"
|
||||
type="text"
|
||||
size="large"
|
||||
@on-enter="onLogin"
|
||||
clearable>
|
||||
<Icon type="ios-checkmark-circle-outline" class="login-icon" slot="prepend"></Icon>
|
||||
<div slot="append" class="login-code-end" @click="reCode"><img :src="codeUrl"/></div>
|
||||
</Input>
|
||||
|
||||
<Button type="primary" :loading="loadIng > 0 || loginJump" size="large" long @click="onLogin">{{$L(loginText)}}</Button>
|
||||
<Button type="primary" :loading="loadIng > 0 || loginJump" size="large" long @click="onLogin">{{$L(loginText)}}</Button>
|
||||
|
||||
<div v-if="loginType=='reg'" class="login-switch">{{$L('已经有帐号?')}}<a href="javascript:void(0)" @click="loginType='login'">{{$L('登录帐号')}}</a></div>
|
||||
<div v-else class="login-switch">{{$L('还没有帐号?')}}<a href="javascript:void(0)" @click="loginType='reg'">{{$L('注册帐号')}}</a></div>
|
||||
</div>
|
||||
<div v-if="loginType=='reg'" class="login-switch">{{$L('已经有帐号?')}}<a href="javascript:void(0)" @click="loginType='login'">{{$L('登录帐号')}}</a></div>
|
||||
<div v-else class="login-switch">{{$L('还没有帐号?')}}<a href="javascript:void(0)" @click="loginType='reg'">{{$L('注册帐号')}}</a></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="login-bottom">
|
||||
<Dropdown trigger="click" placement="bottom-start">
|
||||
@ -145,8 +162,10 @@
|
||||
import {mapState} from "vuex";
|
||||
import {Store} from "le5le-store";
|
||||
import {languageList, languageType, setLanguage} from "../language";
|
||||
import VueQrcode from "@chenfengyuan/vue-qrcode";
|
||||
|
||||
export default {
|
||||
components: {VueQrcode},
|
||||
data() {
|
||||
return {
|
||||
loadIng: 0,
|
||||
@ -154,9 +173,14 @@ export default {
|
||||
languageList,
|
||||
languageType,
|
||||
|
||||
qrcodeVal: '',
|
||||
qrcodeTimer: null,
|
||||
qrcodeLoad: false,
|
||||
|
||||
codeNeed: false,
|
||||
codeUrl: $A.apiUrl('users/login/codeimg?_=' + Math.random()),
|
||||
|
||||
loginMode: 'access',
|
||||
loginType: 'login',
|
||||
loginJump: false,
|
||||
|
||||
@ -189,12 +213,15 @@ export default {
|
||||
this.setServerUrl('').catch(_ => {});
|
||||
}
|
||||
//
|
||||
this.qrcodeTimer = setInterval(this.qrcodeStatus, 2000);
|
||||
//
|
||||
this.subscribe = Store.subscribe('useSSOLogin', () => {
|
||||
this.inputServerUrl();
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearInterval(this.qrcodeTimer);
|
||||
if (this.subscribe) {
|
||||
this.subscribe.unsubscribe();
|
||||
this.subscribe = null;
|
||||
@ -234,17 +261,35 @@ export default {
|
||||
},
|
||||
|
||||
welcomeTitle() {
|
||||
let title = window.systemInfo.title || "DooTask";
|
||||
if (this.loginMode == 'qrcode') {
|
||||
return this.$L("扫码登录")
|
||||
}
|
||||
const title = window.systemInfo.title || "DooTask";
|
||||
return "Welcome " + title
|
||||
},
|
||||
|
||||
subTitle() {
|
||||
const title = window.systemInfo.title || "DooTask";
|
||||
if (this.loginMode == 'qrcode') {
|
||||
return this.$L(`请使用${title}移动端扫描二维码。`)
|
||||
}
|
||||
if (this.loginType=='reg') {
|
||||
return this.$L(`输入您的信息以创建帐户。`)
|
||||
}
|
||||
return this.$L(`输入您的凭证以访问您的帐户。`)
|
||||
},
|
||||
|
||||
loginText() {
|
||||
let text = this.loginType == 'login' ? '登录' : '注册';
|
||||
if (this.loginJump) {
|
||||
text += "成功..."
|
||||
}
|
||||
return text
|
||||
}
|
||||
},
|
||||
|
||||
qrcodeUrl() {
|
||||
return $A.apiUrl('../login?qrcode=' + this.qrcodeVal)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -255,11 +300,14 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
loginMode() {
|
||||
this.qrcodeRefresh()
|
||||
},
|
||||
loginType(val) {
|
||||
if (val == 'reg') {
|
||||
this.getNeedInvite();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -312,6 +360,36 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
switchLoginMode() {
|
||||
this.chackServerUrl(true).then(() => {
|
||||
if (this.loginMode === 'qrcode') {
|
||||
this.loginMode = 'access'
|
||||
} else {
|
||||
this.loginMode = 'qrcode'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
qrcodeRefresh() {
|
||||
if (this.loginMode == 'qrcode') {
|
||||
this.qrcodeVal = $A.randomString(32)
|
||||
}
|
||||
},
|
||||
|
||||
qrcodeStatus() {
|
||||
if (this.qrcodeLoad || this.loginMode != 'qrcode') {
|
||||
return;
|
||||
}
|
||||
this.qrcodeLoad = true
|
||||
this.$store.dispatch("call", {
|
||||
url: 'users/login/qrcode?code=' + this.qrcodeVal,
|
||||
}).then(({data}) => {
|
||||
this.$store.dispatch("handleClearCache", data).then(this.goNext);
|
||||
}).finally(_ => {
|
||||
this.qrcodeLoad = false
|
||||
});
|
||||
},
|
||||
|
||||
forgotPassword() {
|
||||
$A.modalWarning("请联系管理员!");
|
||||
},
|
||||
@ -461,11 +539,7 @@ export default {
|
||||
}).then(({data}) => {
|
||||
$A.IDBSave("cacheLoginEmail", this.email)
|
||||
this.codeNeed = false;
|
||||
this.$store.dispatch("handleClearCache", data).then(() => {
|
||||
this.goNext();
|
||||
}).catch(_ => {
|
||||
this.goNext();
|
||||
});
|
||||
this.$store.dispatch("handleClearCache", data).then(this.goNext);
|
||||
}).catch(({data, msg}) => {
|
||||
if (data.code === 'email') {
|
||||
$A.modalWarning(msg);
|
||||
|
||||
@ -729,8 +729,6 @@ export default {
|
||||
this.$store.dispatch("handleClearCache", null).then(async () => {
|
||||
await $A.IDBSet("clearCache", $A.randomString(6))
|
||||
$A.reloadUrl()
|
||||
}).catch(() => {
|
||||
$A.reloadUrl()
|
||||
});
|
||||
return;
|
||||
case 'logout':
|
||||
|
||||
@ -141,8 +141,6 @@ export default {
|
||||
this.$store.dispatch("handleClearCache", null).then(async () => {
|
||||
await $A.IDBSet("clearCache", $A.randomString(6))
|
||||
$A.reloadUrl()
|
||||
}).catch(() => {
|
||||
$A.reloadUrl()
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
16
resources/assets/sass/pages/common.scss
vendored
16
resources/assets/sass/pages/common.scss
vendored
@ -698,3 +698,19 @@ body {
|
||||
50% { transform: translate3d(-4px, 0, 0); }
|
||||
}
|
||||
|
||||
/*登录右侧滑入*/
|
||||
.login-mode-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.login-mode-leave-active {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-mode-enter,
|
||||
.login-mode-leave-to {
|
||||
transform: translate(100%, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
69
resources/assets/sass/pages/page-login.scss
vendored
69
resources/assets/sass/pages/page-login.scss
vendored
@ -27,17 +27,58 @@
|
||||
}
|
||||
}
|
||||
.login-box {
|
||||
position: relative;
|
||||
margin-top: 36px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
border-radius: 12px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 0 10px #e6ecfa;
|
||||
overflow: hidden;
|
||||
.login-mode-switch {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 1;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.login-mode-switch-box {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
transform: translate(40px,-40px) rotate(45deg);
|
||||
cursor: pointer;
|
||||
background-color: rgba($primary-color, 0.8);
|
||||
transition: background-color .3s;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
background-color: $primary-color;
|
||||
}
|
||||
.login-mode-switch-icon {
|
||||
position: absolute;
|
||||
font-size: 32px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
color: #ffffff;
|
||||
bottom: -20px;
|
||||
left: 16px;
|
||||
transform: rotate(-45deg);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
> svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 13px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-top: 36px;
|
||||
margin-top: 46px;
|
||||
}
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
@ -46,8 +87,14 @@
|
||||
padding: 0 12px;
|
||||
color: #AAAAAA;
|
||||
}
|
||||
.login-input {
|
||||
margin: 32px 40px;
|
||||
.login-qrcode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 52px auto 49px;
|
||||
}
|
||||
.login-access {
|
||||
margin: 26px 40px 30px;
|
||||
> * {
|
||||
margin-top: 26px;
|
||||
}
|
||||
@ -90,16 +137,6 @@
|
||||
.login-switch {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.login-input-tips-box{
|
||||
position: relative;
|
||||
.login-input-tips{
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -20px;
|
||||
color: #c7c7c7;
|
||||
}
|
||||
}
|
||||
> .ivu-poptip {
|
||||
width: 100%;
|
||||
> .ivu-poptip-rel {
|
||||
@ -179,6 +216,10 @@
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
.login-mode-switch {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 26px;
|
||||
}
|
||||
@ -187,7 +228,7 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
.login-access {
|
||||
margin: 20px 36px;
|
||||
|
||||
.ivu-input-large {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user