first commit
This commit is contained in:
commit
db5762abd4
687 files changed
+171307
No files matched your search
@@ -0,0 +1,568 @@
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { useHead, useRoute, useRouter } from "#app";
|
||||
import { NuxtLink } from "#components";
|
||||
import Button from "primevue/button";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Message from "primevue/message";
|
||||
import Password from "primevue/password";
|
||||
import AuthProviders from "~/components/AuthProviders.vue";
|
||||
import CaptchaField from "~/components/CaptchaField.vue";
|
||||
import GenericOAuthProviders from "~/components/GenericOAuthProviders.vue";
|
||||
import SecurityVerificationDialog from "~/components/SecurityVerificationDialog.vue";
|
||||
import { loginErrorMessage, securityChallenge, useAuth } from "~/lib/auth.js";
|
||||
import { r as safeRedirect, t as withRedirect } from "~/lib/auth-redirect.js";
|
||||
import { t as useCaptcha } from "~/lib/useCaptcha.js";
|
||||
import { t as useSiteConfig } from "~/lib/useSiteConfig.js";
|
||||
import { t as useVerifyCode } from "~/lib/useVerifyCode.js";
|
||||
import "~/assets/styles/SecurityVerificationDialog.skYFo4v7.css";
|
||||
import "~/assets/styles/GenericOAuthProviders.Bv8oNwXP.css";
|
||||
import "~/assets/styles/GeetestField.DiieTibK.css";
|
||||
import "~/assets/styles/CaptchaField.BSN3ZbsD.css";
|
||||
import "~/assets/styles/AuthProviders.CgAWouSf.css";
|
||||
import "~/assets/styles/login.Clyo2_3T.css";
|
||||
|
||||
definePageMeta({ layout: "blank" });
|
||||
useHead({ title: "登录 - 破碎工坊网络" });
|
||||
|
||||
const auth = useAuth();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const site = useSiteConfig();
|
||||
const { config, error: configError } = site;
|
||||
const form = reactive({ account: "", password: "", code: "" });
|
||||
const loginMode = ref("password");
|
||||
const phoneCode = ref("86");
|
||||
const errorMessage = ref("");
|
||||
const submitting = ref(false);
|
||||
const authProviders = ref(null);
|
||||
const securityDialogVisible = ref(false);
|
||||
const challenge = ref({});
|
||||
const pendingCredentials = ref(null);
|
||||
const pendingVerification = ref(null);
|
||||
let attemptVersion = 0;
|
||||
|
||||
const phonePasswordEnabled = computed(
|
||||
() =>
|
||||
config.value?.login_phone_password === undefined ||
|
||||
site.on(config.value.login_phone_password),
|
||||
);
|
||||
const emailPasswordEnabled = computed(
|
||||
() =>
|
||||
config.value?.login_email_password === undefined ||
|
||||
site.on(config.value.login_email_password),
|
||||
);
|
||||
const passwordLoginEnabled = computed(
|
||||
() => phonePasswordEnabled.value || emailPasswordEnabled.value,
|
||||
);
|
||||
const codeLoginEnabled = computed(() =>
|
||||
site.on(config.value?.login_phone_verify),
|
||||
);
|
||||
const registrationEnabled = computed(
|
||||
() =>
|
||||
config.value?.register_phone === undefined ||
|
||||
config.value?.register_email === undefined ||
|
||||
site.on(config.value.register_phone) ||
|
||||
site.on(config.value.register_email),
|
||||
);
|
||||
const isPhoneAccount = computed(
|
||||
() => loginMode.value === "code" || !form.account.includes("@"),
|
||||
);
|
||||
const formLocked = computed(
|
||||
() =>
|
||||
submitting.value ||
|
||||
securityDialogVisible.value ||
|
||||
!!pendingVerification.value,
|
||||
);
|
||||
const securityContext = computed(() => ({
|
||||
scene: "login",
|
||||
account: pendingCredentials.value?.account,
|
||||
phoneCode: pendingCredentials.value?.phoneCode,
|
||||
}));
|
||||
const passwordWasReset = computed(() => route.query.reset === "1");
|
||||
const registrationSucceeded = computed(() => route.query.registered === "1");
|
||||
|
||||
const captcha = useCaptcha("login");
|
||||
const {
|
||||
needed: captchaNeeded,
|
||||
provider: captchaProvider,
|
||||
captchaId,
|
||||
img: captchaImage,
|
||||
token: captchaToken,
|
||||
value: captchaValue,
|
||||
loading: captchaLoading,
|
||||
configLoading: captchaConfigLoading,
|
||||
error: captchaError,
|
||||
load: loadCaptcha,
|
||||
complete: completeCaptcha,
|
||||
invalidate: invalidateCaptcha,
|
||||
} = captcha;
|
||||
const {
|
||||
sending: sendingCode,
|
||||
countdown: codeCountdown,
|
||||
send: sendCode,
|
||||
} = useVerifyCode({
|
||||
action: "login",
|
||||
captcha,
|
||||
error: errorMessage,
|
||||
phoneCode: () => phoneCode.value,
|
||||
});
|
||||
const fieldsDisabled = computed(() => formLocked.value || sendingCode.value);
|
||||
const captchaBusy = computed(
|
||||
() => captchaConfigLoading.value || captchaLoading.value,
|
||||
);
|
||||
|
||||
onMounted(() => site.load());
|
||||
watch(
|
||||
config,
|
||||
(settings) => {
|
||||
if (formLocked.value || sendingCode.value) return;
|
||||
if (
|
||||
codeLoginEnabled.value &&
|
||||
(settings?.first_login_method === "code" || !passwordLoginEnabled.value)
|
||||
) {
|
||||
loginMode.value = "code";
|
||||
} else if (!codeLoginEnabled.value) {
|
||||
loginMode.value = "password";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
watch([() => form.account, loginMode], () => {
|
||||
form.code = "";
|
||||
errorMessage.value = "";
|
||||
});
|
||||
|
||||
function setLoginMode(mode) {
|
||||
if (
|
||||
fieldsDisabled.value ||
|
||||
!(mode === "code" ? codeLoginEnabled.value : passwordLoginEnabled.value)
|
||||
)
|
||||
return;
|
||||
loginMode.value = mode;
|
||||
}
|
||||
|
||||
function sendLoginCode() {
|
||||
if (loginMode.value !== "code" || fieldsDisabled.value) return;
|
||||
if (!config.value) {
|
||||
errorMessage.value = "登录设置暂不可用,请重新加载";
|
||||
return;
|
||||
}
|
||||
if (!codeLoginEnabled.value) {
|
||||
errorMessage.value = "当前登录方式未开放,请选择其他方式";
|
||||
return;
|
||||
}
|
||||
const account = form.account.trim();
|
||||
if (account.includes("@")) {
|
||||
errorMessage.value = "请输入手机号";
|
||||
return;
|
||||
}
|
||||
return sendCode("phone", account, "请输入手机号");
|
||||
}
|
||||
|
||||
function cancelLogin() {
|
||||
attemptVersion += 1;
|
||||
securityDialogVisible.value = false;
|
||||
pendingCredentials.value = null;
|
||||
pendingVerification.value = null;
|
||||
submitting.value = false;
|
||||
errorMessage.value = "";
|
||||
}
|
||||
onBeforeUnmount(cancelLogin);
|
||||
|
||||
async function attemptLogin(credentials, verification) {
|
||||
const methodEnabled =
|
||||
credentials.type === "code"
|
||||
? codeLoginEnabled.value
|
||||
: credentials.account.includes("@")
|
||||
? emailPasswordEnabled.value
|
||||
: phonePasswordEnabled.value;
|
||||
if (!config.value || !methodEnabled) {
|
||||
cancelLogin();
|
||||
errorMessage.value = config.value
|
||||
? "当前登录方式未开放,请选择其他方式"
|
||||
: "登录设置暂不可用,请重新加载";
|
||||
return;
|
||||
}
|
||||
|
||||
const version = attemptVersion;
|
||||
submitting.value = true;
|
||||
errorMessage.value = "";
|
||||
try {
|
||||
await auth.login(
|
||||
credentials.account,
|
||||
credentials.password,
|
||||
credentials.captcha,
|
||||
credentials.token,
|
||||
credentials.type,
|
||||
credentials.code,
|
||||
credentials.phoneCode,
|
||||
verification,
|
||||
);
|
||||
if (version !== attemptVersion) return;
|
||||
authProviders.value?.resetQr();
|
||||
securityDialogVisible.value = false;
|
||||
pendingCredentials.value = null;
|
||||
pendingVerification.value = null;
|
||||
await router.push(safeRedirect(route.query.redirect));
|
||||
} catch (error) {
|
||||
if (version !== attemptVersion) return;
|
||||
const nextChallenge = securityChallenge(error);
|
||||
if (nextChallenge) {
|
||||
pendingCredentials.value = credentials;
|
||||
pendingVerification.value = null;
|
||||
challenge.value = nextChallenge;
|
||||
securityDialogVisible.value = true;
|
||||
} else {
|
||||
errorMessage.value = loginErrorMessage(error);
|
||||
const captchaRequired = await captcha.handleError(error);
|
||||
if (version !== attemptVersion) return;
|
||||
if (
|
||||
captchaRequired &&
|
||||
verification?.security_verify_method === "certification"
|
||||
) {
|
||||
pendingCredentials.value = credentials;
|
||||
pendingVerification.value = { ...verification };
|
||||
securityDialogVisible.value = false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (version === attemptVersion) submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSecurityVerification(verification) {
|
||||
const credentials = pendingCredentials.value;
|
||||
if (!credentials || submitting.value || pendingVerification.value) return;
|
||||
if (
|
||||
verification.security_verify_method === "certification" &&
|
||||
credentials.type === "password" &&
|
||||
captchaNeeded.value
|
||||
) {
|
||||
// Certification returns to this form for a fresh CAPTCHA before continuing the same login.
|
||||
pendingVerification.value = { ...verification };
|
||||
securityDialogVisible.value = false;
|
||||
errorMessage.value = "";
|
||||
invalidateCaptcha();
|
||||
await loadCaptcha();
|
||||
return;
|
||||
}
|
||||
await attemptLogin(credentials, verification);
|
||||
}
|
||||
|
||||
async function submitLogin() {
|
||||
if (submitting.value || sendingCode.value || securityDialogVisible.value)
|
||||
return;
|
||||
errorMessage.value = "";
|
||||
if (pendingVerification.value && pendingCredentials.value) {
|
||||
const validationError = captcha.validationError();
|
||||
if (validationError) {
|
||||
errorMessage.value = validationError;
|
||||
return;
|
||||
}
|
||||
await attemptLogin(
|
||||
{
|
||||
...pendingCredentials.value,
|
||||
captcha: captchaValue.value.trim(),
|
||||
token: captchaToken.value,
|
||||
},
|
||||
pendingVerification.value,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!form.account.trim()) {
|
||||
errorMessage.value = "请输入手机号或邮箱";
|
||||
return;
|
||||
}
|
||||
if (!config.value) {
|
||||
errorMessage.value = "登录设置暂不可用,请重新加载";
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(loginMode.value === "code" && !codeLoginEnabled.value) ||
|
||||
(loginMode.value === "password" &&
|
||||
!(isPhoneAccount.value
|
||||
? phonePasswordEnabled.value
|
||||
: emailPasswordEnabled.value))
|
||||
) {
|
||||
errorMessage.value = "当前登录方式未开放,请选择其他方式";
|
||||
return;
|
||||
}
|
||||
if (isPhoneAccount.value && !/^[1-9]\d{0,3}$/.test(phoneCode.value)) {
|
||||
errorMessage.value = "请输入有效的国际电话区号";
|
||||
return;
|
||||
}
|
||||
if (loginMode.value === "code" && form.account.includes("@")) {
|
||||
errorMessage.value = "请输入手机号";
|
||||
return;
|
||||
}
|
||||
if (loginMode.value === "code" && !form.code.trim()) {
|
||||
errorMessage.value = "请输入验证码";
|
||||
return;
|
||||
}
|
||||
if (loginMode.value === "password" && !form.password) {
|
||||
errorMessage.value = "请输入密码";
|
||||
return;
|
||||
}
|
||||
const validationError =
|
||||
loginMode.value === "password" ? captcha.validationError() : "";
|
||||
if (validationError) {
|
||||
errorMessage.value = validationError;
|
||||
return;
|
||||
}
|
||||
attemptVersion += 1;
|
||||
await attemptLogin({
|
||||
account: form.account.trim(),
|
||||
password: loginMode.value === "password" ? form.password : "",
|
||||
captcha: captchaValue.value.trim(),
|
||||
token: captchaToken.value,
|
||||
type: loginMode.value,
|
||||
code: form.code.trim(),
|
||||
phoneCode: isPhoneAccount.value ? phoneCode.value : "",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-shell">
|
||||
<div class="auth-card login-card">
|
||||
<div class="login-main">
|
||||
<div class="login-brand"><h1>欢迎回来</h1></div>
|
||||
<form class="login-form" @submit.prevent="submitLogin">
|
||||
<div class="seg" role="group" aria-label="登录方式">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: loginMode === 'password' }"
|
||||
:aria-pressed="loginMode === 'password'"
|
||||
:disabled="!passwordLoginEnabled || fieldsDisabled"
|
||||
@click="setLoginMode('password')"
|
||||
>
|
||||
密码登录
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: loginMode === 'code' }"
|
||||
:aria-pressed="loginMode === 'code'"
|
||||
:disabled="!codeLoginEnabled || fieldsDisabled"
|
||||
@click="setLoginMode('code')"
|
||||
>
|
||||
验证码登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Message
|
||||
v-if="passwordWasReset && !errorMessage"
|
||||
severity="success"
|
||||
:closable="false"
|
||||
icon="pi pi-check-circle"
|
||||
>密码已重置,请使用新密码登录</Message
|
||||
>
|
||||
<Message
|
||||
v-if="registrationSucceeded && !errorMessage"
|
||||
severity="success"
|
||||
:closable="false"
|
||||
>注册成功,请登录</Message
|
||||
>
|
||||
<Message v-if="configError" severity="error" :closable="false">
|
||||
{{ configError }}
|
||||
<Button label="重新加载" text @click="site.load" />
|
||||
</Message>
|
||||
<Message
|
||||
v-else-if="!passwordLoginEnabled && !codeLoginEnabled"
|
||||
severity="info"
|
||||
:closable="false"
|
||||
>账号登录暂未开放,请使用已开放的其他登录方式</Message
|
||||
>
|
||||
<Message v-if="pendingVerification" severity="info" :closable="false"
|
||||
>实名认证已通过,请完成新的人机验证后继续登录。</Message
|
||||
>
|
||||
|
||||
<div class="field">
|
||||
<label for="account">账号</label>
|
||||
<InputText
|
||||
id="account"
|
||||
v-model="form.account"
|
||||
:placeholder="
|
||||
loginMode === 'code' ? '输入手机号' : '输入手机号/邮箱'
|
||||
"
|
||||
autocomplete="username"
|
||||
:invalid="!!errorMessage"
|
||||
:aria-describedby="errorMessage ? 'login-error' : undefined"
|
||||
:disabled="fieldsDisabled"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
<div v-if="loginMode === 'code'" class="field">
|
||||
<label for="login-code">验证码</label>
|
||||
<!-- Retain the deployed scoped selectors without changing the shared stylesheet. -->
|
||||
<div class="code-row" data-v-7896f676>
|
||||
<InputText
|
||||
id="login-code"
|
||||
v-model="form.code"
|
||||
autocomplete="one-time-code"
|
||||
placeholder="请输入验证码"
|
||||
:disabled="fieldsDisabled"
|
||||
fluid
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
:label="codeCountdown > 0 ? `${codeCountdown}s` : '获取验证码'"
|
||||
:loading="sendingCode"
|
||||
:disabled="
|
||||
codeCountdown > 0 ||
|
||||
fieldsDisabled ||
|
||||
!codeLoginEnabled ||
|
||||
captchaBusy
|
||||
"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="sendLoginCode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loginMode === 'password'" class="field">
|
||||
<div class="field-label-row">
|
||||
<label for="password">密码</label>
|
||||
<NuxtLink
|
||||
:to="withRedirect('/forgot-password', route.query.redirect)"
|
||||
class="field-link"
|
||||
>忘记密码?</NuxtLink
|
||||
>
|
||||
</div>
|
||||
<Password
|
||||
input-id="password"
|
||||
v-model="form.password"
|
||||
placeholder="请输入密码"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
autocomplete="current-password"
|
||||
:invalid="!!errorMessage"
|
||||
:input-props="{
|
||||
'aria-describedby': errorMessage ? 'login-error' : undefined,
|
||||
}"
|
||||
:disabled="fieldsDisabled"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="login-captcha" data-v-7896f676 :aria-busy="captchaBusy">
|
||||
<button
|
||||
v-if="captchaBusy || captchaError"
|
||||
type="button"
|
||||
class="captcha-placeholder"
|
||||
data-v-7896f676
|
||||
:class="{ 'has-error': !!captchaError }"
|
||||
:disabled="
|
||||
captchaBusy ||
|
||||
submitting ||
|
||||
sendingCode ||
|
||||
securityDialogVisible ||
|
||||
!captchaNeeded
|
||||
"
|
||||
:aria-label="captchaError || '正在加载验证…'"
|
||||
@click="loadCaptcha"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
captchaError
|
||||
? 'pi pi-exclamation-circle'
|
||||
: 'pi pi-spin pi-spinner'
|
||||
"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
:role="captchaError ? 'alert' : 'status'"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>{{ captchaError || "正在加载验证…" }}</span
|
||||
>
|
||||
</button>
|
||||
<CaptchaField
|
||||
v-else-if="captchaNeeded"
|
||||
v-model="captchaValue"
|
||||
:img="captchaImage"
|
||||
:provider="captchaProvider"
|
||||
:captcha-id="captchaId"
|
||||
:disabled="submitting || sendingCode || securityDialogVisible"
|
||||
@verified="completeCaptcha"
|
||||
@invalid="invalidateCaptcha"
|
||||
@refresh="loadCaptcha"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Message
|
||||
v-if="errorMessage"
|
||||
id="login-error"
|
||||
severity="error"
|
||||
:closable="false"
|
||||
icon="pi pi-exclamation-circle"
|
||||
class="login-error"
|
||||
>{{ errorMessage }}</Message
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="
|
||||
submitting
|
||||
? '正在登录…'
|
||||
: pendingVerification
|
||||
? '继续登录'
|
||||
: '登录'
|
||||
"
|
||||
:loading="submitting"
|
||||
:disabled="
|
||||
submitting ||
|
||||
sendingCode ||
|
||||
securityDialogVisible ||
|
||||
captchaBusy ||
|
||||
!config ||
|
||||
(!passwordLoginEnabled && !codeLoginEnabled)
|
||||
"
|
||||
class="login-btn"
|
||||
fluid
|
||||
/>
|
||||
<Button
|
||||
v-if="pendingVerification"
|
||||
label="取消本次登录"
|
||||
type="button"
|
||||
severity="secondary"
|
||||
text
|
||||
:disabled="submitting"
|
||||
@click="cancelLogin"
|
||||
/>
|
||||
<Button
|
||||
v-if="registrationEnabled"
|
||||
v-slot="{ class: buttonClass, a11yAttrs }"
|
||||
as-child
|
||||
severity="secondary"
|
||||
fluid
|
||||
>
|
||||
<NuxtLink
|
||||
:to="withRedirect('/register', route.query.redirect)"
|
||||
:class="[buttonClass, 'register-btn']"
|
||||
v-bind="a11yAttrs"
|
||||
><span class="p-button-label">注册账号</span></NuxtLink
|
||||
>
|
||||
</Button>
|
||||
<SecurityVerificationDialog
|
||||
v-model:visible="securityDialogVisible"
|
||||
:challenge="challenge"
|
||||
:context="securityContext"
|
||||
:busy="submitting"
|
||||
:request-error="errorMessage"
|
||||
@verified="confirmSecurityVerification"
|
||||
@cancel="cancelLogin"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<AuthProviders ref="authProviders" />
|
||||
<GenericOAuthProviders />
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user