952 lines
25 KiB
Vue
952 lines
25 KiB
Vue
<template>
|
||
<div class="flex h-screen overflow-hidden sm:bg-gray-50">
|
||
<div class="w-full overflow-auto">
|
||
<LoginBox
|
||
:title="title"
|
||
:class="{ 'pointer-events-none': $resources.signup.loading }"
|
||
>
|
||
<template v-slot:default>
|
||
<div v-if="!(resetPasswordEmailSent || otpRequested)">
|
||
<form class="flex flex-col" @submit.prevent="submitForm">
|
||
<!-- 2FA 部分 -->
|
||
<template v-if="is2FA">
|
||
<FormControl
|
||
label="来自您的身份验证应用的2FA代码"
|
||
placeholder="123456"
|
||
v-model="twoFactorCode"
|
||
required
|
||
/>
|
||
<Button
|
||
class="mt-4"
|
||
:loading="
|
||
$resources.verify2FA.loading ||
|
||
$session.login.loading ||
|
||
$resources.resetPassword.loading
|
||
"
|
||
variant="solid"
|
||
@click="
|
||
$resources.verify2FA.submit({
|
||
user: email,
|
||
totp_code: twoFactorCode,
|
||
})
|
||
"
|
||
>
|
||
验证
|
||
</Button>
|
||
<ErrorMessage
|
||
class="mt-2"
|
||
:message="$resources.verify2FA.error"
|
||
/>
|
||
</template>
|
||
|
||
<!-- 忘记密码部分 -->
|
||
<template v-else-if="hasForgotPassword">
|
||
<FormControl
|
||
label="邮箱"
|
||
type="email"
|
||
placeholder="email@example.com"
|
||
autocomplete="email"
|
||
v-model="email"
|
||
required
|
||
/>
|
||
<router-link
|
||
class="mt-2 text-sm"
|
||
v-if="hasForgotPassword"
|
||
:to="{
|
||
name: 'Login',
|
||
query: { ...$route.query, forgot: undefined },
|
||
}"
|
||
>
|
||
我记得我的密码
|
||
</router-link>
|
||
<Button
|
||
class="mt-4"
|
||
:loading="$resources.resetPassword.loading"
|
||
variant="solid"
|
||
>
|
||
重置密码
|
||
</Button>
|
||
</template>
|
||
|
||
<!-- 登录部分 -->
|
||
<template v-else-if="isLogin">
|
||
<FormControl
|
||
label="用户名或邮箱"
|
||
placeholder="email@example.com"
|
||
autocomplete="email"
|
||
v-model="email"
|
||
:disabled="otpSent && useEmail"
|
||
required
|
||
/>
|
||
|
||
<!-- 密码验证 -->
|
||
<template v-if="!isOauthLogin && !useEmail">
|
||
<FormControl
|
||
class="mt-4"
|
||
label="密码"
|
||
type="password"
|
||
placeholder="•••••"
|
||
v-model="password"
|
||
name="password"
|
||
autocomplete="current-password"
|
||
required
|
||
/>
|
||
<div class="mt-2 flex flex-col gap-2">
|
||
<router-link
|
||
class="text-sm"
|
||
:to="{
|
||
name: 'Login',
|
||
query: { ...$route.query, forgot: 1 },
|
||
}"
|
||
>
|
||
忘记密码?
|
||
</router-link>
|
||
</div>
|
||
<Button
|
||
class="mt-4"
|
||
variant="solid"
|
||
:loading="$session.login.loading"
|
||
type="submit"
|
||
>
|
||
登录
|
||
</Button>
|
||
</template>
|
||
|
||
<!-- OTP 验证 -->
|
||
<template v-else-if="useEmail">
|
||
<!-- OTP 验证输入(当 OTP 已发送时) -->
|
||
<template v-if="otpSent">
|
||
<FormControl
|
||
class="mt-4"
|
||
label="验证码"
|
||
placeholder="123456"
|
||
v-model="otp"
|
||
required
|
||
/>
|
||
<div class="mt-4 space-y-2">
|
||
<Button
|
||
class="w-full"
|
||
:loading="$resources.verifyOTPAndLogin.loading"
|
||
variant="solid"
|
||
@click="verifyOTPAndLogin"
|
||
>
|
||
登陆
|
||
</Button>
|
||
<Button
|
||
class="w-full"
|
||
:loading="$resources.sendOTP.loading"
|
||
variant="outline"
|
||
:disabled="otpResendCountdown > 0"
|
||
@click="$resources.sendOTP.submit()"
|
||
>
|
||
重新发送验证码
|
||
{{
|
||
otpResendCountdown > 0
|
||
? `在 ${otpResendCountdown} 秒后`
|
||
: ''
|
||
}}
|
||
</Button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 初始 OTP 请求按钮 -->
|
||
<template v-else>
|
||
<Button
|
||
class="mt-4"
|
||
:loading="$resources.sendOTP.loading"
|
||
variant="solid"
|
||
@click="$resources.sendOTP.submit()"
|
||
>
|
||
发送验证码
|
||
</Button>
|
||
</template>
|
||
</template>
|
||
|
||
<!-- OAuth 验证 -->
|
||
<template v-else>
|
||
<Button class="mt-4" variant="solid">
|
||
使用 {{ oauthProviderName }} 登录
|
||
</Button>
|
||
</template>
|
||
|
||
<!-- 错误信息 -->
|
||
<ErrorMessage
|
||
class="mt-2"
|
||
:message="
|
||
$session.login.error ||
|
||
$resources.is2FAEnabled.error ||
|
||
$resources.sendOTP.error ||
|
||
$resources.verifyOTPAndLogin.error
|
||
"
|
||
/>
|
||
</template>
|
||
|
||
<!-- 注册部分 -->
|
||
<template v-else>
|
||
<FormControl
|
||
label="用户名"
|
||
type="text"
|
||
placeholder="设置用户名"
|
||
v-model="username"
|
||
autocomplete="username"
|
||
required
|
||
/>
|
||
<FormControl
|
||
label="邮箱 (可选)"
|
||
type="email"
|
||
placeholder="email@example.com"
|
||
autocomplete="email"
|
||
class="mt-4"
|
||
v-model="email"
|
||
/>
|
||
<FormControl
|
||
label="手机号"
|
||
type="tel"
|
||
placeholder="请输入您的手机号码"
|
||
class="mt-4"
|
||
v-model="phoneNumber"
|
||
autocomplete="tel"
|
||
required
|
||
@blur="validatePhoneNumber"
|
||
/>
|
||
<div v-if="phoneNumberFormatError" class="mt-2 text-sm text-red-600">
|
||
请输入正确的手机号码格式
|
||
</div>
|
||
<FormControl
|
||
label="密码"
|
||
type="password"
|
||
placeholder="设置登录密码"
|
||
class="mt-4"
|
||
v-model="signupPassword"
|
||
autocomplete="new-password"
|
||
required
|
||
/>
|
||
<div class="mt-1 text-sm text-gray-600">
|
||
密码必须至少8个字符,并包含大小写字母和数字
|
||
</div>
|
||
<div class="mt-2" v-if="signupPassword">
|
||
<div class="flex items-center">
|
||
<div class="text-sm w-20">密码强度:</div>
|
||
<div class="flex-1 bg-gray-200 rounded-full h-2">
|
||
<div
|
||
class="h-2 rounded-full transition-all"
|
||
:class="passwordStrengthClass"
|
||
:style="{width: passwordStrength + '%'}"></div>
|
||
</div>
|
||
<div class="ml-2 text-sm" :class="passwordStrengthTextClass">{{passwordStrengthText}}</div>
|
||
</div>
|
||
</div>
|
||
<FormControl
|
||
label="确认密码"
|
||
type="password"
|
||
placeholder="再次输入密码"
|
||
class="mt-4"
|
||
v-model="confirmPassword"
|
||
autocomplete="new-password"
|
||
required
|
||
/>
|
||
<div v-if="passwordMismatch" class="mt-2 text-sm text-red-600">
|
||
两次输入的密码不一致
|
||
</div>
|
||
<Button
|
||
class="mt-4"
|
||
:loading="$resources.signupWithUsername.loading"
|
||
variant="solid"
|
||
type="submit"
|
||
:disabled="(passwordMismatch && confirmPassword) || phoneNumberFormatError"
|
||
>
|
||
注册
|
||
</Button>
|
||
</template>
|
||
|
||
<ErrorMessage class="mt-2" :message="error" />
|
||
</form>
|
||
<div
|
||
class="flex flex-col"
|
||
v-if="!hasForgotPassword && !isOauthLogin && !is2FA"
|
||
>
|
||
<div class="-mb-2 mt-6 border-t text-center">
|
||
<div class="-translate-y-1/2 transform">
|
||
<span
|
||
class="relative bg-white px-2 text-sm font-medium leading-8 text-gray-800"
|
||
>
|
||
或
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-2">
|
||
<Button
|
||
v-if="isLogin && !useEmail"
|
||
@click="switchToEmailLogin"
|
||
icon-left="mail"
|
||
>
|
||
使用邮箱验证码登陆
|
||
</Button>
|
||
<Button
|
||
v-else-if="isLogin && useEmail"
|
||
@click="switchToPasswordLogin"
|
||
icon-left="key"
|
||
>
|
||
使用密码登陆
|
||
</Button>
|
||
</div>
|
||
<div
|
||
class="mt-6 text-center"
|
||
v-if="!(otpRequested || resetPasswordEmailSent)"
|
||
>
|
||
<router-link
|
||
class="text-center text-base font-medium text-gray-900 hover:text-gray-700"
|
||
:to="{
|
||
name: $route.name == 'Login' ? 'Signup' : 'Login',
|
||
query: { ...$route.query, forgot: undefined },
|
||
}"
|
||
>
|
||
{{
|
||
$route.name == 'Login'
|
||
? '没有账户?创建一个账户。'
|
||
: '已有账户?登录。'
|
||
}}
|
||
</router-link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="otpRequested">
|
||
<form class="flex flex-col">
|
||
<FormControl
|
||
label="邮箱"
|
||
type="email"
|
||
placeholder="email@example.com"
|
||
autocomplete="email"
|
||
v-model="email"
|
||
required
|
||
/>
|
||
<FormControl
|
||
label="验证码"
|
||
type="text"
|
||
class="mt-4"
|
||
placeholder="123456"
|
||
maxlength="6"
|
||
v-model="otp"
|
||
required
|
||
/>
|
||
<ErrorMessage
|
||
class="mt-2"
|
||
:message="$resources.verifyOTP.error"
|
||
/>
|
||
<Button
|
||
class="mt-4"
|
||
variant="solid"
|
||
:loading="$resources.verifyOTP.loading"
|
||
@click="$resources.verifyOTP.submit()"
|
||
>
|
||
验证
|
||
</Button>
|
||
<Button
|
||
class="mt-2"
|
||
variant="outline"
|
||
:loading="$resources.resendOTP.loading"
|
||
@click="$resources.resendOTP.submit()"
|
||
:disabled="otpResendCountdown > 0"
|
||
>
|
||
重新发送验证码
|
||
{{
|
||
otpResendCountdown > 0
|
||
? `在 ${otpResendCountdown} 秒后`
|
||
: ''
|
||
}}
|
||
</Button>
|
||
</form>
|
||
<div class="mt-6 text-center">
|
||
<router-link
|
||
class="text-center text-base font-medium text-gray-900 hover:text-gray-700"
|
||
:to="{
|
||
name: $route.name == 'Login' ? 'Signup' : 'Login',
|
||
query: { ...$route.query, forgot: undefined },
|
||
}"
|
||
>
|
||
{{
|
||
$route.name == 'Login'
|
||
? '新用户?创建一个新账户。'
|
||
: '已有账户?登录。'
|
||
}}
|
||
</router-link>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="text-p-base text-gray-700"
|
||
v-else-if="resetPasswordEmailSent"
|
||
>
|
||
<p>
|
||
我们已向
|
||
<span class="font-semibold">{{ email }}</span
|
||
>发送了一封邮件。请点击收到的链接重置您的密码。
|
||
</p>
|
||
</div>
|
||
</template>
|
||
<template v-slot:logo v-if="saasProduct">
|
||
<div class="mx-auto flex items-center space-x-2">
|
||
<img
|
||
class="inline-block h-7 w-7 rounded-sm"
|
||
:src="saasProduct?.logo"
|
||
/>
|
||
<span
|
||
class="select-none text-xl font-semibold tracking-tight text-gray-900"
|
||
>
|
||
{{ saasProduct?.title }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
<template v-slot:footer v-if="saasProduct">
|
||
<div
|
||
class="mt-2 flex w-full items-center justify-center text-sm text-gray-600"
|
||
>
|
||
由 今果 Jingrow 提供支持
|
||
</div>
|
||
</template>
|
||
</LoginBox>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import LoginBox from '../components/auth/LoginBox.vue';
|
||
import { toast } from 'vue-sonner';
|
||
import { getToastErrorMessage } from '../utils/toast';
|
||
|
||
export default {
|
||
name: 'Signup',
|
||
components: {
|
||
LoginBox,
|
||
},
|
||
data() {
|
||
return {
|
||
email: '',
|
||
username: '',
|
||
phoneNumber: '',
|
||
signupPassword: '',
|
||
account_request: '',
|
||
otpRequested: false,
|
||
otp: '',
|
||
otpSent: false,
|
||
twoFactorCode: '',
|
||
password: null,
|
||
otpResendCountdown: 0,
|
||
resetPasswordEmailSent: false,
|
||
confirmPassword: '',
|
||
phoneNumberFormatError: false,
|
||
};
|
||
},
|
||
mounted() {
|
||
this.email = localStorage.getItem('login_email');
|
||
setInterval(() => {
|
||
if (this.otpResendCountdown > 0) {
|
||
this.otpResendCountdown -= 1;
|
||
}
|
||
}, 1000);
|
||
},
|
||
watch: {
|
||
email() {
|
||
this.resetSignupState();
|
||
},
|
||
},
|
||
resources: {
|
||
signup() {
|
||
return {
|
||
url: 'jcloud.api.account.signup',
|
||
params: {
|
||
email: this.email,
|
||
referrer: this.getReferrerIfAny(),
|
||
product: this.$route.query.product,
|
||
},
|
||
onSuccess(account_request) {
|
||
this.account_request = account_request;
|
||
this.otpRequested = true;
|
||
this.otpResendCountdown = 30;
|
||
toast.success('验证码已发送至您的邮箱');
|
||
},
|
||
onError: (error) => {
|
||
if (error?.exc_type !== 'ValidationError') {
|
||
return;
|
||
}
|
||
let errorMessage = '';
|
||
if ((error?.messages ?? []).length) {
|
||
errorMessage = error?.messages?.[0];
|
||
}
|
||
// check if error message has `is already registered` substring
|
||
if (errorMessage.includes('is already registered')) {
|
||
localStorage.setItem('login_email', this.email);
|
||
|
||
if (this.$route.query?.product) {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
query: {
|
||
redirect: `/dashboard/create-site/${this.$route.query.product}/setup`,
|
||
},
|
||
});
|
||
} else {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
});
|
||
}
|
||
}
|
||
},
|
||
};
|
||
},
|
||
verifyOTP() {
|
||
return {
|
||
url: 'jcloud.api.account.verify_otp',
|
||
params: {
|
||
account_request: this.account_request,
|
||
otp: this.otp,
|
||
},
|
||
onSuccess(key) {
|
||
window.open(`/dashboard/setup-account/${key}`, '_self');
|
||
},
|
||
};
|
||
},
|
||
resendOTP() {
|
||
return {
|
||
url: 'jcloud.api.account.resend_otp',
|
||
params: {
|
||
account_request: this.account_request,
|
||
},
|
||
onSuccess() {
|
||
this.otp = '';
|
||
this.otpResendCountdown = 30;
|
||
toast.success('验证码已发送至您的邮箱');
|
||
},
|
||
onError(err) {
|
||
toast.error(
|
||
getToastErrorMessage(err, '验证码重发失败'),
|
||
);
|
||
},
|
||
};
|
||
},
|
||
sendOTP() {
|
||
return {
|
||
url: 'jcloud.api.account.send_otp',
|
||
params: {
|
||
email: this.email,
|
||
},
|
||
onSuccess() {
|
||
this.otpSent = true;
|
||
this.otpResendCountdown = 30;
|
||
toast.success('验证码已发送至您的邮箱');
|
||
},
|
||
onError(err) {
|
||
toast.error(
|
||
getToastErrorMessage(err, '验证码发送失败'),
|
||
);
|
||
},
|
||
};
|
||
},
|
||
verifyOTPAndLogin() {
|
||
return {
|
||
url: 'jcloud.api.account.verify_otp_and_login',
|
||
params: {
|
||
email: this.email,
|
||
otp: this.otp,
|
||
},
|
||
onSuccess(res) {
|
||
this.afterLogin(res);
|
||
},
|
||
};
|
||
},
|
||
oauthLogin() {
|
||
return {
|
||
url: 'jcloud.api.oauth.oauth_authorize_url',
|
||
onSuccess(url) {
|
||
localStorage.setItem('login_email', this.email);
|
||
window.location.href = url;
|
||
},
|
||
};
|
||
},
|
||
resetPassword() {
|
||
return {
|
||
url: 'jcloud.api.account.send_reset_password_email',
|
||
onSuccess() {
|
||
this.resetPasswordEmailSent = true;
|
||
},
|
||
};
|
||
},
|
||
signupSettings() {
|
||
return {
|
||
url: 'jcloud.api.account.signup_settings',
|
||
params: {
|
||
product: this.$route.query.product,
|
||
},
|
||
auto: true,
|
||
};
|
||
},
|
||
is2FAEnabled() {
|
||
return {
|
||
url: 'jcloud.api.account.is_2fa_enabled',
|
||
};
|
||
},
|
||
verify2FA() {
|
||
return {
|
||
url: 'jcloud.api.account.verify_2fa',
|
||
onSuccess: async () => {
|
||
if (this.isLogin) {
|
||
if (!this.useEmail) {
|
||
await this.$resources.verifyOTPAndLogin.submit();
|
||
} else {
|
||
await this.login();
|
||
}
|
||
} else if (this.hasForgotPassword) {
|
||
await this.$resources.resetPassword.submit({
|
||
email: this.email,
|
||
});
|
||
}
|
||
},
|
||
};
|
||
},
|
||
signupWithUsername() {
|
||
return {
|
||
url: 'jcloud.api.account.signup_with_username',
|
||
params: {
|
||
username: this.username,
|
||
email: this.email || null,
|
||
phone_number: this.phoneNumber || null,
|
||
password: this.signupPassword,
|
||
referrer: this.getReferrerIfAny(),
|
||
product: this.$route.query.product,
|
||
},
|
||
onSuccess(res) {
|
||
if (res && res.success === false) {
|
||
toast.error(res.message || '注册失败,请检查您的信息');
|
||
return;
|
||
}
|
||
|
||
localStorage.setItem('login_email', this.username || this.email);
|
||
|
||
if (res && res.dashboard_route) {
|
||
window.location.href = res.dashboard_route;
|
||
} else {
|
||
window.location.href = '/dashboard/welcome';
|
||
}
|
||
},
|
||
onError(err) {
|
||
const errorMessage = err?.messages?.length
|
||
? err.messages.join('\n')
|
||
: (err?.message || '注册失败,请检查您的信息');
|
||
toast.error(errorMessage);
|
||
},
|
||
};
|
||
},
|
||
},
|
||
methods: {
|
||
resetSignupState() {
|
||
if (!this.isLogin && !this.hasForgotPassword && this.otpRequested) {
|
||
this.otpRequested = false;
|
||
this.account_request = '';
|
||
this.otp = '';
|
||
}
|
||
},
|
||
async submitForm() {
|
||
if (this.isLogin) {
|
||
if (this.isOauthLogin) {
|
||
this.$resources.oauthLogin.submit({
|
||
provider: this.socialLoginKey,
|
||
});
|
||
} else if (this.useEmail && this.otpSent) {
|
||
return;
|
||
} else if (!this.useEmail && this.email && this.password) {
|
||
this.checkTwoFactorAndLogin();
|
||
} else if (this.useEmail && !this.otpSent) {
|
||
this.$resources.sendOTP.submit();
|
||
}
|
||
} else if (this.hasForgotPassword) {
|
||
this.checkTwoFactorAndResetPassword();
|
||
} else {
|
||
if (!this.username) {
|
||
toast.error('用户名不能为空');
|
||
return;
|
||
}
|
||
if (!this.phoneNumber) {
|
||
toast.error('手机号不能为空');
|
||
return;
|
||
}
|
||
if (!this.isPhoneNumberValid) {
|
||
toast.error('请输入正确的手机号码格式');
|
||
return;
|
||
}
|
||
if (!this.signupPassword) {
|
||
toast.error('密码不能为空');
|
||
return;
|
||
}
|
||
if (this.signupPassword !== this.confirmPassword) {
|
||
toast.error('两次输入的密码不一致');
|
||
return;
|
||
}
|
||
if (!this.isPasswordValid) {
|
||
toast.error('密码必须至少8个字符,并包含大小写字母和数字');
|
||
return;
|
||
}
|
||
this.$resources.signupWithUsername.submit();
|
||
}
|
||
},
|
||
|
||
async checkTwoFactorAndLogin() {
|
||
await this.$resources.is2FAEnabled.submit(
|
||
{ user: this.email },
|
||
{
|
||
onSuccess: async (two_factor_enabled) => {
|
||
if (two_factor_enabled) {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
query: {
|
||
...this.$route.query,
|
||
two_factor: 1,
|
||
},
|
||
});
|
||
} else {
|
||
await this.login();
|
||
}
|
||
},
|
||
},
|
||
);
|
||
},
|
||
|
||
async checkTwoFactorAndResetPassword() {
|
||
await this.$resources.is2FAEnabled.submit(
|
||
{ user: this.email },
|
||
{
|
||
onSuccess: async (two_factor_enabled) => {
|
||
if (two_factor_enabled) {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
query: {
|
||
two_factor: 1,
|
||
forgot: 1,
|
||
},
|
||
});
|
||
} else {
|
||
await this.$resources.resetPassword.submit({
|
||
email: this.email,
|
||
});
|
||
}
|
||
},
|
||
},
|
||
);
|
||
},
|
||
|
||
verifyOTPAndLogin() {
|
||
this.$resources.is2FAEnabled.submit(
|
||
{ user: this.email },
|
||
{
|
||
onSuccess: async (two_factor_enabled) => {
|
||
if (two_factor_enabled) {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
query: {
|
||
...this.$route.query,
|
||
two_factor: 1,
|
||
},
|
||
});
|
||
} else {
|
||
await this.$resources.verifyOTPAndLogin.submit();
|
||
}
|
||
},
|
||
},
|
||
);
|
||
},
|
||
getReferrerIfAny() {
|
||
const params = location.search;
|
||
const searchParams = new URLSearchParams(params);
|
||
return searchParams.get('referrer');
|
||
},
|
||
async login() {
|
||
await this.$session.login.submit(
|
||
{
|
||
email: this.email,
|
||
password: this.password,
|
||
},
|
||
{
|
||
onSuccess: (res) => {
|
||
// 直接在此处处理重定向,不调用afterLogin
|
||
localStorage.setItem('login_email', this.email || this.username);
|
||
|
||
// 老用户直接进入主仪表板
|
||
window.location.href = '/dashboard';
|
||
},
|
||
onError: (err) => {
|
||
if (this.$route.name === 'Login' && this.$route.query.two_factor) {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
query: {
|
||
two_factor: undefined,
|
||
},
|
||
});
|
||
this.twoFactorCode = '';
|
||
}
|
||
},
|
||
},
|
||
);
|
||
},
|
||
afterLogin(res) {
|
||
let loginRoute = `/dashboard${res.dashboard_route || '/'}`;
|
||
// if query param redirect is present, redirect to that route
|
||
if (this.$route.query.redirect) {
|
||
loginRoute = this.$route.query.redirect;
|
||
}
|
||
localStorage.setItem('login_email', this.email);
|
||
window.location.href = loginRoute;
|
||
},
|
||
switchToEmailLogin() {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
query: {
|
||
...this.$route.query,
|
||
use_email: 1
|
||
}
|
||
});
|
||
},
|
||
switchToPasswordLogin() {
|
||
this.$router.push({
|
||
name: 'Login',
|
||
query: {
|
||
...this.$route.query,
|
||
use_email: undefined
|
||
}
|
||
});
|
||
},
|
||
validatePhoneNumber() {
|
||
// 失焦时验证手机号格式
|
||
if (this.phoneNumber && this.phoneNumber.length > 0) {
|
||
this.phoneNumberFormatError = !this.isPhoneNumberValid;
|
||
} else {
|
||
this.phoneNumberFormatError = false;
|
||
}
|
||
},
|
||
},
|
||
computed: {
|
||
error() {
|
||
if (this.$resources.signup.error) {
|
||
return this.$resources.signup.error;
|
||
}
|
||
|
||
if (this.$resources.resetPassword.error) {
|
||
return this.$resources.resetPassword.error;
|
||
}
|
||
},
|
||
saasProduct() {
|
||
return this.$resources.signupSettings.data?.product_trial;
|
||
},
|
||
isLogin() {
|
||
return this.$route.name == 'Login' && !this.$route.query.forgot;
|
||
},
|
||
hasForgotPassword() {
|
||
return this.$route.name == 'Login' && this.$route.query.forgot;
|
||
},
|
||
is2FA() {
|
||
return this.$route.name == 'Login' && this.$route.query.two_factor;
|
||
},
|
||
emailDomain() {
|
||
return this.email?.includes('@') ? this.email?.split('@').pop() : '';
|
||
},
|
||
isOauthLogin() {
|
||
return (
|
||
this.oauthEmailDomains.has(this.emailDomain) &&
|
||
this.emailDomain.length > 0
|
||
);
|
||
},
|
||
oauthProviders() {
|
||
const domains = this.$resources.signupSettings.data?.oauth_domains;
|
||
let providers = {};
|
||
|
||
if (domains) {
|
||
domains.map(
|
||
(d) =>
|
||
(providers[d.email_domain] = {
|
||
social_login_key: d.social_login_key,
|
||
provider_name: d.provider_name,
|
||
}),
|
||
);
|
||
}
|
||
|
||
return providers;
|
||
},
|
||
oauthEmailDomains() {
|
||
return new Set(Object.keys(this.oauthProviders));
|
||
},
|
||
socialLoginKey() {
|
||
return this.oauthProviders[this.emailDomain].social_login_key;
|
||
},
|
||
oauthProviderName() {
|
||
return this.oauthProviders[this.emailDomain].provider_name;
|
||
},
|
||
title() {
|
||
if (this.hasForgotPassword) {
|
||
return '重置密码';
|
||
} else if (this.isLogin) {
|
||
if (this.saasProduct) {
|
||
return `登录您的账户以开始使用 ${this.saasProduct.title}`;
|
||
}
|
||
return '登录您的账户';
|
||
} else {
|
||
if (this.saasProduct) {
|
||
return `注册以创建您的 ${this.saasProduct.title} 站点`;
|
||
}
|
||
return '创建新账户';
|
||
}
|
||
},
|
||
useEmail() {
|
||
// 默认使用密码登录,只有当明确指定use_email时才使用邮箱验证码
|
||
return Boolean(this.$route.query.use_email);
|
||
},
|
||
passwordMismatch() {
|
||
return this.signupPassword !== this.confirmPassword && this.confirmPassword;
|
||
},
|
||
passwordStrength() {
|
||
if (!this.signupPassword) return 0;
|
||
|
||
let strength = 0;
|
||
|
||
// 密码长度
|
||
if (this.signupPassword.length >= 8) strength += 25;
|
||
|
||
// 包含小写字母
|
||
if (/[a-z]/.test(this.signupPassword)) strength += 25;
|
||
|
||
// 包含大写字母
|
||
if (/[A-Z]/.test(this.signupPassword)) strength += 25;
|
||
|
||
// 包含数字
|
||
if (/[0-9]/.test(this.signupPassword)) strength += 25;
|
||
|
||
return strength;
|
||
},
|
||
passwordStrengthClass() {
|
||
if (this.passwordStrength < 40) return 'bg-red-500';
|
||
if (this.passwordStrength < 60) return 'bg-yellow-500';
|
||
if (this.passwordStrength < 80) return 'bg-blue-500';
|
||
return 'bg-green-500';
|
||
},
|
||
passwordStrengthText() {
|
||
if (this.passwordStrength < 40) return '弱';
|
||
if (this.passwordStrength < 60) return '一般';
|
||
if (this.passwordStrength < 80) return '强';
|
||
return '非常强';
|
||
},
|
||
passwordStrengthTextClass() {
|
||
if (this.passwordStrength < 40) return 'text-red-500';
|
||
if (this.passwordStrength < 60) return 'text-yellow-500';
|
||
if (this.passwordStrength < 80) return 'text-blue-500';
|
||
return 'text-green-500';
|
||
},
|
||
isPasswordValid() {
|
||
return this.signupPassword &&
|
||
this.signupPassword.length >= 8 &&
|
||
/[a-z]/.test(this.signupPassword) &&
|
||
/[A-Z]/.test(this.signupPassword) &&
|
||
/[0-9]/.test(this.signupPassword);
|
||
},
|
||
isPhoneNumberValid() {
|
||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||
return phoneRegex.test(this.phoneNumber);
|
||
},
|
||
},
|
||
};
|
||
</script> |