mirror of https://github.com/halo-dev/halo
refactor: re-design login-related form (#3608)
#### What type of PR is this? /kind improvement /area console /milestone 2.4.x #### What this PR does / why we need it: 重新设计登录页面的样式和用户体验。 #### Which issue(s) this PR fixes: Fixes #3572 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 优化 Console 端登录页面样式 ```pull/3648/head
parent
bb2b1bcae2
commit
2d56aaeb93
|
@ -13,12 +13,12 @@ import {
|
|||
} from "overlayscrollbars-vue";
|
||||
import type { FormKitConfig } from "@formkit/core";
|
||||
import { i18n } from "./locales";
|
||||
import { AppName } from "./constants/app";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { configMap } = storeToRefs(useSystemConfigMapStore());
|
||||
|
||||
const AppName = "Halo";
|
||||
const route = useRoute();
|
||||
const title = useTitle();
|
||||
|
||||
|
|
|
@ -11,14 +11,18 @@ import { submitForm } from "@formkit/core";
|
|||
import { JSEncrypt } from "jsencrypt";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type {
|
||||
GlobalInfo,
|
||||
SocialAuthProvider,
|
||||
} from "@/modules/system/actuator/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
buttonText?: string;
|
||||
}>(),
|
||||
{
|
||||
buttonText: "core.login.button",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "succeed"): void;
|
||||
}>();
|
||||
|
@ -104,22 +108,9 @@ onMounted(() => {
|
|||
handleGenerateToken();
|
||||
});
|
||||
|
||||
// auth providers
|
||||
// fixme: Needs to be saved in Pinia.
|
||||
const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
||||
queryKey: ["social-auth-providers"],
|
||||
queryFn: async () => {
|
||||
const { data } = await axios.get<GlobalInfo>(
|
||||
`${import.meta.env.VITE_API_URL}/actuator/globalinfo`,
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
|
||||
return data.socialAuthProviders;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const inputClasses = {
|
||||
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -129,16 +120,18 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
|||
name="login-form"
|
||||
:actions="false"
|
||||
type="form"
|
||||
:classes="{
|
||||
form: '!divide-none',
|
||||
}"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="handleLogin"
|
||||
@keyup.enter="submitForm('login-form')"
|
||||
>
|
||||
<FormKit
|
||||
:validation-messages="{
|
||||
required: $t('core.login.fields.username.validation'),
|
||||
}"
|
||||
:classes="inputClasses"
|
||||
name="username"
|
||||
:placeholder="$t('core.login.fields.username.placeholder')"
|
||||
:validation-label="$t('core.login.fields.username.placeholder')"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
validation="required"
|
||||
|
@ -146,11 +139,10 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
|||
</FormKit>
|
||||
<FormKit
|
||||
id="passwordInput"
|
||||
:validation-messages="{
|
||||
required: $t('core.login.fields.password.validation'),
|
||||
}"
|
||||
:classes="inputClasses"
|
||||
name="password"
|
||||
:placeholder="$t('core.login.fields.password.placeholder')"
|
||||
:validation-label="$t('core.login.fields.password.placeholder')"
|
||||
type="password"
|
||||
validation="required"
|
||||
autocomplete="current-password"
|
||||
|
@ -158,31 +150,12 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
|||
</FormKit>
|
||||
</FormKit>
|
||||
<VButton
|
||||
class="mt-6"
|
||||
class="mt-8"
|
||||
block
|
||||
:loading="loading"
|
||||
type="secondary"
|
||||
@click="submitForm('login-form')"
|
||||
>
|
||||
{{ $t("core.login.button") }}
|
||||
{{ $t(buttonText) }}
|
||||
</VButton>
|
||||
|
||||
<div v-if="socialAuthProviders?.length" class="mt-3 flex items-center">
|
||||
<span class="text-sm text-slate-600">
|
||||
{{ $t("core.login.other_login") }}
|
||||
</span>
|
||||
<ul class="flex items-center">
|
||||
<li
|
||||
v-for="(socialAuthProvider, index) in socialAuthProviders"
|
||||
:key="index"
|
||||
>
|
||||
<a
|
||||
:href="socialAuthProvider.authenticationUrl"
|
||||
class="block h-6 w-6 rounded-full bg-gray-200 p-1"
|
||||
>
|
||||
<img class="rounded-full" :src="socialAuthProvider.logo" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Toast, VModal } from "@halo-dev/components";
|
|||
import LoginForm from "@/components/login/LoginForm.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import SocialAuthProviders from "./SocialAuthProviders.vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
@ -27,5 +28,6 @@ const onLoginSucceed = () => {
|
|||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<LoginForm v-if="userStore.loginModalVisible" @succeed="onLoginSucceed" />
|
||||
<SocialAuthProviders />
|
||||
</VModal>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SocialAuthProvider } from "@/modules/system/actuator/types";
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
authProvider: SocialAuthProvider;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
function handleSocialLogin() {
|
||||
loading.value = true;
|
||||
window.location.href = props.authProvider.authenticationUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="group inline-flex select-none flex-row items-center gap-2 rounded bg-white px-2.5 py-1.5 ring-1 ring-gray-200 transition-all hover:bg-gray-100 hover:shadow hover:ring-gray-900"
|
||||
@click="handleSocialLogin"
|
||||
>
|
||||
<svg
|
||||
v-if="loading"
|
||||
class="h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:alt="authProvider.displayName"
|
||||
class="h-4 w-4 rounded-full"
|
||||
:src="authProvider.logo"
|
||||
/>
|
||||
|
||||
<span class="text-xs text-gray-800 group-hover:text-gray-900">
|
||||
{{ authProvider.displayName }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts" setup>
|
||||
// auth providers
|
||||
|
||||
import { useGlobalInfoFetch } from "@/composables/use-global-info";
|
||||
import SocialAuthProviderItem from "./SocialAuthProviderItem.vue";
|
||||
|
||||
const { globalInfo } = useGlobalInfoFetch();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition v-if="globalInfo?.socialAuthProviders.length" appear name="fade">
|
||||
<div>
|
||||
<div
|
||||
class="my-4 flex items-center before:ml-1 before:mt-0.5 before:flex-1 before:border-t before:border-gray-200 after:mr-1 after:mt-0.5 after:flex-1 after:border-t after:border-gray-200"
|
||||
>
|
||||
<p class="mx-4 mb-0 text-center text-xs dark:text-neutral-600">
|
||||
{{ $t("core.components.social_auth_providers.title") }}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="flex flex-row flex-wrap justify-center gap-2">
|
||||
<li
|
||||
v-for="(socialAuthProvider, index) in globalInfo.socialAuthProviders"
|
||||
:key="index"
|
||||
>
|
||||
<SocialAuthProviderItem :auth-provider="socialAuthProvider" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { submitForm, reset } from "@formkit/core";
|
||||
import { submitForm } from "@formkit/core";
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
|
@ -8,6 +8,15 @@ import { useI18n } from "vue-i18n";
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
buttonText?: string;
|
||||
}>(),
|
||||
{
|
||||
buttonText: "core.signup.operations.submit.button",
|
||||
}
|
||||
);
|
||||
|
||||
const formState = ref({
|
||||
password: "",
|
||||
user: {
|
||||
|
@ -23,6 +32,7 @@ const formState = ref({
|
|||
},
|
||||
},
|
||||
});
|
||||
const loading = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "succeed"): void;
|
||||
|
@ -42,6 +52,8 @@ onMounted(() => {
|
|||
|
||||
const handleSignup = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
await apiClient.common.user.signUp({
|
||||
signUpRequest: formState.value,
|
||||
});
|
||||
|
@ -51,8 +63,14 @@ const handleSignup = async () => {
|
|||
emit("succeed");
|
||||
} catch (error) {
|
||||
console.error("Failed to sign up", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const inputClasses = {
|
||||
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -73,6 +91,7 @@ const handleSignup = async () => {
|
|||
name="username"
|
||||
:placeholder="$t('core.signup.fields.username.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.username.placeholder')"
|
||||
:classes="inputClasses"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
:validation="[
|
||||
|
@ -93,6 +112,7 @@ const handleSignup = async () => {
|
|||
name="displayName"
|
||||
:placeholder="$t('core.signup.fields.display_name.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.display_name.placeholder')"
|
||||
:classes="inputClasses"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
validation="required"
|
||||
|
@ -103,6 +123,7 @@ const handleSignup = async () => {
|
|||
name="password"
|
||||
:placeholder="$t('core.signup.fields.password.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.password.placeholder')"
|
||||
:classes="inputClasses"
|
||||
type="password"
|
||||
validation="required|length:0,100"
|
||||
>
|
||||
|
@ -111,17 +132,19 @@ const handleSignup = async () => {
|
|||
name="password_confirm"
|
||||
:placeholder="$t('core.signup.fields.password_confirm.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.password_confirm.placeholder')"
|
||||
:classes="inputClasses"
|
||||
type="password"
|
||||
validation="required|confirm|length:0,100"
|
||||
>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
<VButton
|
||||
class="mt-6"
|
||||
class="mt-8"
|
||||
block
|
||||
type="secondary"
|
||||
:loading="loading"
|
||||
@click="submitForm('signup-form')"
|
||||
>
|
||||
{{ $t("core.signup.operations.submit.button") }}
|
||||
{{ $t(buttonText) }}
|
||||
</VButton>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import type { GlobalInfo } from "@/modules/system/actuator/types";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import axios from "axios";
|
||||
|
||||
export function useGlobalInfoFetch() {
|
||||
const { data } = useQuery<GlobalInfo>({
|
||||
queryKey: ["globalinfo"],
|
||||
queryFn: async () => {
|
||||
const { data } = await axios.get<GlobalInfo>(
|
||||
`${import.meta.env.VITE_API_URL}/actuator/globalinfo`,
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return {
|
||||
globalInfo: data,
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const AppName = "Halo";
|
|
@ -4,19 +4,22 @@ core:
|
|||
fields:
|
||||
username:
|
||||
placeholder: Username
|
||||
validation: Please enter username
|
||||
password:
|
||||
placeholder: Password
|
||||
validation: Please enter password
|
||||
operations:
|
||||
submit:
|
||||
toast_success: Login successful
|
||||
toast_failed: Login failed, incorrect username or password
|
||||
toast_csrf: CSRF Token expired, please try again
|
||||
signup:
|
||||
label: No account
|
||||
button: Sign up
|
||||
return_login:
|
||||
label: Already have an account
|
||||
button: Sign in
|
||||
button: Login
|
||||
modal:
|
||||
title: Re-login
|
||||
other_login: "Other login:"
|
||||
signup:
|
||||
title: Sign up
|
||||
fields:
|
||||
|
@ -34,6 +37,17 @@ core:
|
|||
toast_success: Sign up successfully
|
||||
binding:
|
||||
title: Account binding
|
||||
common:
|
||||
toast:
|
||||
mounted: The current login method is not bound to an account, Please bind or sign up a new account first
|
||||
operations:
|
||||
login_and_bind:
|
||||
button: Login and Bind
|
||||
signup_and_bind:
|
||||
button: Signup and Bind
|
||||
bind:
|
||||
toast_success: Binding successfully
|
||||
toast_failed: Binding failed, no enabled login method found
|
||||
sidebar:
|
||||
search:
|
||||
placeholder: Search
|
||||
|
@ -891,6 +905,7 @@ core:
|
|||
disable_privileged:
|
||||
tooltip: The authentication method reserved by the system cannot be disabled
|
||||
detail:
|
||||
title: Identity authentication detail
|
||||
fields:
|
||||
display_name: Display name
|
||||
description: Description
|
||||
|
@ -1051,6 +1066,8 @@ core:
|
|||
pagination:
|
||||
page_label: page
|
||||
size_label: items per page
|
||||
social_auth_providers:
|
||||
title: Third-party login
|
||||
composables:
|
||||
content_cache:
|
||||
toast_recovered: Recovered unsaved content from cache
|
||||
|
|
|
@ -4,19 +4,22 @@ core:
|
|||
fields:
|
||||
username:
|
||||
placeholder: 用户名
|
||||
validation: 请输入用户名
|
||||
password:
|
||||
placeholder: 密码
|
||||
validation: 请输入密码
|
||||
operations:
|
||||
submit:
|
||||
toast_success: 登录成功
|
||||
toast_failed: 登录失败,用户名或密码错误
|
||||
toast_csrf: CSRF Token 失效,请重新尝试
|
||||
signup:
|
||||
label: 没有账号
|
||||
button: 立即注册
|
||||
return_login:
|
||||
label: 已有账号
|
||||
button: 立即登录
|
||||
button: 登录
|
||||
modal:
|
||||
title: 重新登录
|
||||
other_login: 其他登录:
|
||||
signup:
|
||||
title: 注册
|
||||
fields:
|
||||
|
@ -34,6 +37,17 @@ core:
|
|||
toast_success: 注册成功
|
||||
binding:
|
||||
title: 账号绑定
|
||||
common:
|
||||
toast:
|
||||
mounted: 当前登录方式未绑定账号,请先绑定或注册新账号
|
||||
operations:
|
||||
login_and_bind:
|
||||
button: 登录并绑定
|
||||
signup_and_bind:
|
||||
button: 注册并绑定
|
||||
bind:
|
||||
toast_success: 绑定成功
|
||||
toast_failed: 绑定失败,没有找到已启用的登录方式
|
||||
sidebar:
|
||||
search:
|
||||
placeholder: 搜索
|
||||
|
@ -891,6 +905,7 @@ core:
|
|||
disable_privileged:
|
||||
tooltip: 系统保留的认证方式,无法禁用
|
||||
detail:
|
||||
title: 身份认证详情
|
||||
fields:
|
||||
display_name: 名称
|
||||
description: 描述
|
||||
|
@ -1051,6 +1066,8 @@ core:
|
|||
pagination:
|
||||
page_label: 页
|
||||
size_label: 条 / 页
|
||||
social_auth_providers:
|
||||
title: 三方登录
|
||||
composables:
|
||||
content_cache:
|
||||
toast_recovered: 已从缓存中恢复未保存的内容
|
||||
|
|
|
@ -14,7 +14,7 @@ export default definePlugin({
|
|||
name: "AuthProviders",
|
||||
component: AuthProviders,
|
||||
meta: {
|
||||
title: "认证方式",
|
||||
title: "core.identity_authentication.title",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
|
@ -23,7 +23,7 @@ export default definePlugin({
|
|||
name: "AuthProviderDetail",
|
||||
component: AuthProviderDetail,
|
||||
meta: {
|
||||
title: "认证方式详情",
|
||||
title: "core.identity_authentication.detail.title",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<script lang="ts" setup>
|
||||
import { onBeforeMount } from "vue";
|
||||
import { computed, onBeforeMount, onMounted } from "vue";
|
||||
import router from "@/router";
|
||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import LoginForm from "@/components/login/LoginForm.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { GlobalInfo, SocialAuthProvider } from "../actuator/types";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import axios from "axios";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import SignupForm from "@/components/signup/SignupForm.vue";
|
||||
import { useGlobalInfoFetch } from "@/composables/use-global-info";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!userStore.isAnonymous) {
|
||||
|
@ -21,34 +21,25 @@ onBeforeMount(() => {
|
|||
}
|
||||
});
|
||||
|
||||
const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
||||
queryKey: ["social-auth-providers"],
|
||||
queryFn: async () => {
|
||||
const { data } = await axios.get<GlobalInfo>(
|
||||
`${import.meta.env.VITE_API_URL}/actuator/globalinfo`,
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
const { globalInfo } = useGlobalInfoFetch();
|
||||
|
||||
return data.socialAuthProviders;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
onMounted(() => {
|
||||
Toast.warning(t("core.binding.common.toast.mounted"));
|
||||
});
|
||||
|
||||
function handleBinding() {
|
||||
const authProvider = socialAuthProviders.value?.find(
|
||||
const authProvider = globalInfo.value?.socialAuthProviders.find(
|
||||
(p) => p.name === route.params.provider
|
||||
);
|
||||
|
||||
if (!authProvider?.bindingUrl) {
|
||||
Toast.error("绑定失败,没有找到已启用的登录方式");
|
||||
Toast.error(t("core.binding.operations.bind.toast_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = authProvider?.bindingUrl;
|
||||
|
||||
Toast.success("绑定成功");
|
||||
Toast.success(t("core.binding.operations.bind.toast_success"));
|
||||
}
|
||||
|
||||
const type = useRouteQuery<string>("type", "");
|
||||
|
@ -56,22 +47,42 @@ const type = useRouteQuery<string>("type", "");
|
|||
function handleChangeType() {
|
||||
type.value = type.value === "signup" ? "" : "signup";
|
||||
}
|
||||
|
||||
const isLoginType = computed(() => type.value !== "signup");
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex h-screen flex-col items-center justify-center">
|
||||
<div class="mb-4">
|
||||
<IconLogo />
|
||||
</div>
|
||||
<div class="login-form flex w-72 flex-col">
|
||||
<div class="mb-4 flex">
|
||||
<h1 class="text-sm">{{ $t("core.binding.title") }}</h1>
|
||||
</div>
|
||||
<SignupForm v-if="type === 'signup'" @succeed="handleBinding" />
|
||||
<LoginForm v-else @succeed="handleBinding" />
|
||||
<div class="flex">
|
||||
<span class="mt-4 text-sm text-indigo-600" @click="handleChangeType">
|
||||
<div class="flex h-screen flex-col items-center bg-white/90 pt-[30vh]">
|
||||
<IconLogo class="mb-8" />
|
||||
<div class="flex w-72 flex-col">
|
||||
<SignupForm
|
||||
v-if="type === 'signup'"
|
||||
button-text="core.binding.operations.signup_and_bind.button"
|
||||
@succeed="handleBinding"
|
||||
/>
|
||||
<LoginForm
|
||||
v-else
|
||||
button-text="core.binding.operations.login_and_bind.button"
|
||||
@succeed="handleBinding"
|
||||
/>
|
||||
<div
|
||||
v-if="globalInfo?.allowRegistration"
|
||||
class="flex justify-center gap-1 pt-3.5 text-xs"
|
||||
>
|
||||
<span class="text-slate-500">
|
||||
{{
|
||||
type === "signup" ? $t("core.login.title") : $t("core.signup.title")
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.label")
|
||||
: $t("core.login.operations.return_login.label")
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="cursor-pointer text-secondary hover:text-gray-600"
|
||||
@click="handleChangeType"
|
||||
>
|
||||
{{
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.button")
|
||||
: $t("core.login.operations.return_login.button")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
<script lang="ts" setup>
|
||||
import { onBeforeMount, watch } from "vue";
|
||||
import { onBeforeMount, computed, watch } from "vue";
|
||||
import router from "@/router";
|
||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import LoginForm from "@/components/login/LoginForm.vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import SignupForm from "@/components/signup/SignupForm.vue";
|
||||
import SocialAuthProviders from "@/components/login/SocialAuthProviders.vue";
|
||||
import { useGlobalInfoFetch } from "@/composables/use-global-info";
|
||||
import { useTitle } from "@vueuse/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AppName } from "@/constants/app";
|
||||
import { locales, getBrowserLanguage, i18n } from "@/locales";
|
||||
import MdiTranslate from "~icons/mdi/translate";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { globalInfo } = useGlobalInfoFetch();
|
||||
const { t } = useI18n();
|
||||
|
||||
const SIGNUP_TYPE = "signup";
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!userStore.isAnonymous) {
|
||||
|
@ -25,9 +34,23 @@ function onLoginSucceed() {
|
|||
const type = useRouteQuery<string>("type", "");
|
||||
|
||||
function handleChangeType() {
|
||||
type.value = type.value === "signup" ? "" : "signup";
|
||||
type.value = type.value === SIGNUP_TYPE ? "" : SIGNUP_TYPE;
|
||||
}
|
||||
|
||||
const isLoginType = computed(() => type.value !== SIGNUP_TYPE);
|
||||
|
||||
// page title
|
||||
const title = useTitle();
|
||||
watch(
|
||||
() => type.value,
|
||||
(value) => {
|
||||
const routeTitle = t(
|
||||
`core.${value === SIGNUP_TYPE ? SIGNUP_TYPE : "login"}.title`
|
||||
);
|
||||
title.value = [routeTitle, AppName].join(" - ");
|
||||
}
|
||||
);
|
||||
|
||||
// setup locale
|
||||
const currentLocale = useLocalStorage(
|
||||
"locale",
|
||||
|
@ -45,15 +68,31 @@ watch(
|
|||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex h-screen flex-col items-center justify-center">
|
||||
<div class="flex h-screen flex-col items-center bg-white/90 pt-[30vh]">
|
||||
<IconLogo class="mb-8" />
|
||||
<div class="login-form flex w-72 flex-col">
|
||||
<div class="flex w-72 flex-col">
|
||||
<SignupForm v-if="type === 'signup'" @succeed="onLoginSucceed" />
|
||||
<LoginForm v-else @succeed="onLoginSucceed" />
|
||||
<div class="flex">
|
||||
<span class="mt-4 text-sm text-indigo-600" @click="handleChangeType">
|
||||
<SocialAuthProviders />
|
||||
<div
|
||||
v-if="globalInfo?.allowRegistration"
|
||||
class="flex justify-center gap-1 pt-3.5 text-xs"
|
||||
>
|
||||
<span class="text-slate-500">
|
||||
{{
|
||||
type === "signup" ? $t("core.login.title") : $t("core.signup.title")
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.label")
|
||||
: $t("core.login.operations.return_login.label")
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="cursor-pointer text-secondary hover:text-gray-600"
|
||||
@click="handleChangeType"
|
||||
>
|
||||
{{
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.button")
|
||||
: $t("core.login.operations.return_login.button")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -77,6 +77,7 @@ watch(
|
|||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.user) formState.value = cloneDeep(props.user);
|
||||
setFocus(isUpdateMode.value ? "displayNameInput" : "userNameInput");
|
||||
} else {
|
||||
handleResetForm();
|
||||
|
@ -84,20 +85,6 @@ watch(
|
|||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.user,
|
||||
(user) => {
|
||||
if (user) {
|
||||
formState.value = cloneDeep(user);
|
||||
} else {
|
||||
handleResetForm();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit("update:visible", visible);
|
||||
if (!visible) {
|
||||
|
|
Loading…
Reference in New Issue