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";
|
} from "overlayscrollbars-vue";
|
||||||
import type { FormKitConfig } from "@formkit/core";
|
import type { FormKitConfig } from "@formkit/core";
|
||||||
import { i18n } from "./locales";
|
import { i18n } from "./locales";
|
||||||
|
import { AppName } from "./constants/app";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { configMap } = storeToRefs(useSystemConfigMapStore());
|
const { configMap } = storeToRefs(useSystemConfigMapStore());
|
||||||
|
|
||||||
const AppName = "Halo";
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const title = useTitle();
|
const title = useTitle();
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,18 @@ import { submitForm } from "@formkit/core";
|
||||||
import { JSEncrypt } from "jsencrypt";
|
import { JSEncrypt } from "jsencrypt";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
|
||||||
import type {
|
|
||||||
GlobalInfo,
|
|
||||||
SocialAuthProvider,
|
|
||||||
} from "@/modules/system/actuator/types";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
buttonText?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
buttonText: "core.login.button",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "succeed"): void;
|
(event: "succeed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
@ -104,22 +108,9 @@ onMounted(() => {
|
||||||
handleGenerateToken();
|
handleGenerateToken();
|
||||||
});
|
});
|
||||||
|
|
||||||
// auth providers
|
const inputClasses = {
|
||||||
// fixme: Needs to be saved in Pinia.
|
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||||
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,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -129,16 +120,18 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
||||||
name="login-form"
|
name="login-form"
|
||||||
:actions="false"
|
:actions="false"
|
||||||
type="form"
|
type="form"
|
||||||
|
:classes="{
|
||||||
|
form: '!divide-none',
|
||||||
|
}"
|
||||||
:config="{ validationVisibility: 'submit' }"
|
:config="{ validationVisibility: 'submit' }"
|
||||||
@submit="handleLogin"
|
@submit="handleLogin"
|
||||||
@keyup.enter="submitForm('login-form')"
|
@keyup.enter="submitForm('login-form')"
|
||||||
>
|
>
|
||||||
<FormKit
|
<FormKit
|
||||||
:validation-messages="{
|
:classes="inputClasses"
|
||||||
required: $t('core.login.fields.username.validation'),
|
|
||||||
}"
|
|
||||||
name="username"
|
name="username"
|
||||||
:placeholder="$t('core.login.fields.username.placeholder')"
|
:placeholder="$t('core.login.fields.username.placeholder')"
|
||||||
|
:validation-label="$t('core.login.fields.username.placeholder')"
|
||||||
:autofocus="true"
|
:autofocus="true"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required"
|
validation="required"
|
||||||
|
@ -146,11 +139,10 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
id="passwordInput"
|
id="passwordInput"
|
||||||
:validation-messages="{
|
:classes="inputClasses"
|
||||||
required: $t('core.login.fields.password.validation'),
|
|
||||||
}"
|
|
||||||
name="password"
|
name="password"
|
||||||
:placeholder="$t('core.login.fields.password.placeholder')"
|
:placeholder="$t('core.login.fields.password.placeholder')"
|
||||||
|
:validation-label="$t('core.login.fields.password.placeholder')"
|
||||||
type="password"
|
type="password"
|
||||||
validation="required"
|
validation="required"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
|
@ -158,31 +150,12 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
||||||
</FormKit>
|
</FormKit>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<VButton
|
<VButton
|
||||||
class="mt-6"
|
class="mt-8"
|
||||||
block
|
block
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@click="submitForm('login-form')"
|
@click="submitForm('login-form')"
|
||||||
>
|
>
|
||||||
{{ $t("core.login.button") }}
|
{{ $t(buttonText) }}
|
||||||
</VButton>
|
</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>
|
</template>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Toast, VModal } from "@halo-dev/components";
|
||||||
import LoginForm from "@/components/login/LoginForm.vue";
|
import LoginForm from "@/components/login/LoginForm.vue";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import SocialAuthProviders from "./SocialAuthProviders.vue";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -27,5 +28,6 @@ const onLoginSucceed = () => {
|
||||||
@update:visible="onVisibleChange"
|
@update:visible="onVisibleChange"
|
||||||
>
|
>
|
||||||
<LoginForm v-if="userStore.loginModalVisible" @succeed="onLoginSucceed" />
|
<LoginForm v-if="userStore.loginModalVisible" @succeed="onLoginSucceed" />
|
||||||
|
<SocialAuthProviders />
|
||||||
</VModal>
|
</VModal>
|
||||||
</template>
|
</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>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { submitForm, reset } from "@formkit/core";
|
import { submitForm } from "@formkit/core";
|
||||||
import { Toast, VButton } from "@halo-dev/components";
|
import { Toast, VButton } from "@halo-dev/components";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
@ -8,6 +8,15 @@ import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
buttonText?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
buttonText: "core.signup.operations.submit.button",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const formState = ref({
|
const formState = ref({
|
||||||
password: "",
|
password: "",
|
||||||
user: {
|
user: {
|
||||||
|
@ -23,6 +32,7 @@ const formState = ref({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "succeed"): void;
|
(event: "succeed"): void;
|
||||||
|
@ -42,6 +52,8 @@ onMounted(() => {
|
||||||
|
|
||||||
const handleSignup = async () => {
|
const handleSignup = async () => {
|
||||||
try {
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
await apiClient.common.user.signUp({
|
await apiClient.common.user.signUp({
|
||||||
signUpRequest: formState.value,
|
signUpRequest: formState.value,
|
||||||
});
|
});
|
||||||
|
@ -51,8 +63,14 @@ const handleSignup = async () => {
|
||||||
emit("succeed");
|
emit("succeed");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to sign up", error);
|
console.error("Failed to sign up", error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputClasses = {
|
||||||
|
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -73,6 +91,7 @@ const handleSignup = async () => {
|
||||||
name="username"
|
name="username"
|
||||||
:placeholder="$t('core.signup.fields.username.placeholder')"
|
:placeholder="$t('core.signup.fields.username.placeholder')"
|
||||||
:validation-label="$t('core.signup.fields.username.placeholder')"
|
:validation-label="$t('core.signup.fields.username.placeholder')"
|
||||||
|
:classes="inputClasses"
|
||||||
:autofocus="true"
|
:autofocus="true"
|
||||||
type="text"
|
type="text"
|
||||||
:validation="[
|
:validation="[
|
||||||
|
@ -93,6 +112,7 @@ const handleSignup = async () => {
|
||||||
name="displayName"
|
name="displayName"
|
||||||
:placeholder="$t('core.signup.fields.display_name.placeholder')"
|
:placeholder="$t('core.signup.fields.display_name.placeholder')"
|
||||||
:validation-label="$t('core.signup.fields.display_name.placeholder')"
|
:validation-label="$t('core.signup.fields.display_name.placeholder')"
|
||||||
|
:classes="inputClasses"
|
||||||
:autofocus="true"
|
:autofocus="true"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required"
|
validation="required"
|
||||||
|
@ -103,6 +123,7 @@ const handleSignup = async () => {
|
||||||
name="password"
|
name="password"
|
||||||
:placeholder="$t('core.signup.fields.password.placeholder')"
|
:placeholder="$t('core.signup.fields.password.placeholder')"
|
||||||
:validation-label="$t('core.signup.fields.password.placeholder')"
|
:validation-label="$t('core.signup.fields.password.placeholder')"
|
||||||
|
:classes="inputClasses"
|
||||||
type="password"
|
type="password"
|
||||||
validation="required|length:0,100"
|
validation="required|length:0,100"
|
||||||
>
|
>
|
||||||
|
@ -111,17 +132,19 @@ const handleSignup = async () => {
|
||||||
name="password_confirm"
|
name="password_confirm"
|
||||||
:placeholder="$t('core.signup.fields.password_confirm.placeholder')"
|
:placeholder="$t('core.signup.fields.password_confirm.placeholder')"
|
||||||
:validation-label="$t('core.signup.fields.password_confirm.placeholder')"
|
:validation-label="$t('core.signup.fields.password_confirm.placeholder')"
|
||||||
|
:classes="inputClasses"
|
||||||
type="password"
|
type="password"
|
||||||
validation="required|confirm|length:0,100"
|
validation="required|confirm|length:0,100"
|
||||||
>
|
>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<VButton
|
<VButton
|
||||||
class="mt-6"
|
class="mt-8"
|
||||||
block
|
block
|
||||||
type="secondary"
|
type="secondary"
|
||||||
|
:loading="loading"
|
||||||
@click="submitForm('signup-form')"
|
@click="submitForm('signup-form')"
|
||||||
>
|
>
|
||||||
{{ $t("core.signup.operations.submit.button") }}
|
{{ $t(buttonText) }}
|
||||||
</VButton>
|
</VButton>
|
||||||
</template>
|
</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:
|
fields:
|
||||||
username:
|
username:
|
||||||
placeholder: Username
|
placeholder: Username
|
||||||
validation: Please enter username
|
|
||||||
password:
|
password:
|
||||||
placeholder: Password
|
placeholder: Password
|
||||||
validation: Please enter password
|
|
||||||
operations:
|
operations:
|
||||||
submit:
|
submit:
|
||||||
toast_success: Login successful
|
toast_success: Login successful
|
||||||
toast_failed: Login failed, incorrect username or password
|
toast_failed: Login failed, incorrect username or password
|
||||||
toast_csrf: CSRF Token expired, please try again
|
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
|
button: Login
|
||||||
modal:
|
modal:
|
||||||
title: Re-login
|
title: Re-login
|
||||||
other_login: "Other login:"
|
|
||||||
signup:
|
signup:
|
||||||
title: Sign up
|
title: Sign up
|
||||||
fields:
|
fields:
|
||||||
|
@ -34,6 +37,17 @@ core:
|
||||||
toast_success: Sign up successfully
|
toast_success: Sign up successfully
|
||||||
binding:
|
binding:
|
||||||
title: Account 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:
|
sidebar:
|
||||||
search:
|
search:
|
||||||
placeholder: Search
|
placeholder: Search
|
||||||
|
@ -891,6 +905,7 @@ core:
|
||||||
disable_privileged:
|
disable_privileged:
|
||||||
tooltip: The authentication method reserved by the system cannot be disabled
|
tooltip: The authentication method reserved by the system cannot be disabled
|
||||||
detail:
|
detail:
|
||||||
|
title: Identity authentication detail
|
||||||
fields:
|
fields:
|
||||||
display_name: Display name
|
display_name: Display name
|
||||||
description: Description
|
description: Description
|
||||||
|
@ -1051,6 +1066,8 @@ core:
|
||||||
pagination:
|
pagination:
|
||||||
page_label: page
|
page_label: page
|
||||||
size_label: items per page
|
size_label: items per page
|
||||||
|
social_auth_providers:
|
||||||
|
title: Third-party login
|
||||||
composables:
|
composables:
|
||||||
content_cache:
|
content_cache:
|
||||||
toast_recovered: Recovered unsaved content from cache
|
toast_recovered: Recovered unsaved content from cache
|
||||||
|
|
|
@ -4,19 +4,22 @@ core:
|
||||||
fields:
|
fields:
|
||||||
username:
|
username:
|
||||||
placeholder: 用户名
|
placeholder: 用户名
|
||||||
validation: 请输入用户名
|
|
||||||
password:
|
password:
|
||||||
placeholder: 密码
|
placeholder: 密码
|
||||||
validation: 请输入密码
|
|
||||||
operations:
|
operations:
|
||||||
submit:
|
submit:
|
||||||
toast_success: 登录成功
|
toast_success: 登录成功
|
||||||
toast_failed: 登录失败,用户名或密码错误
|
toast_failed: 登录失败,用户名或密码错误
|
||||||
toast_csrf: CSRF Token 失效,请重新尝试
|
toast_csrf: CSRF Token 失效,请重新尝试
|
||||||
|
signup:
|
||||||
|
label: 没有账号
|
||||||
|
button: 立即注册
|
||||||
|
return_login:
|
||||||
|
label: 已有账号
|
||||||
|
button: 立即登录
|
||||||
button: 登录
|
button: 登录
|
||||||
modal:
|
modal:
|
||||||
title: 重新登录
|
title: 重新登录
|
||||||
other_login: 其他登录:
|
|
||||||
signup:
|
signup:
|
||||||
title: 注册
|
title: 注册
|
||||||
fields:
|
fields:
|
||||||
|
@ -34,6 +37,17 @@ core:
|
||||||
toast_success: 注册成功
|
toast_success: 注册成功
|
||||||
binding:
|
binding:
|
||||||
title: 账号绑定
|
title: 账号绑定
|
||||||
|
common:
|
||||||
|
toast:
|
||||||
|
mounted: 当前登录方式未绑定账号,请先绑定或注册新账号
|
||||||
|
operations:
|
||||||
|
login_and_bind:
|
||||||
|
button: 登录并绑定
|
||||||
|
signup_and_bind:
|
||||||
|
button: 注册并绑定
|
||||||
|
bind:
|
||||||
|
toast_success: 绑定成功
|
||||||
|
toast_failed: 绑定失败,没有找到已启用的登录方式
|
||||||
sidebar:
|
sidebar:
|
||||||
search:
|
search:
|
||||||
placeholder: 搜索
|
placeholder: 搜索
|
||||||
|
@ -891,6 +905,7 @@ core:
|
||||||
disable_privileged:
|
disable_privileged:
|
||||||
tooltip: 系统保留的认证方式,无法禁用
|
tooltip: 系统保留的认证方式,无法禁用
|
||||||
detail:
|
detail:
|
||||||
|
title: 身份认证详情
|
||||||
fields:
|
fields:
|
||||||
display_name: 名称
|
display_name: 名称
|
||||||
description: 描述
|
description: 描述
|
||||||
|
@ -1051,6 +1066,8 @@ core:
|
||||||
pagination:
|
pagination:
|
||||||
page_label: 页
|
page_label: 页
|
||||||
size_label: 条 / 页
|
size_label: 条 / 页
|
||||||
|
social_auth_providers:
|
||||||
|
title: 三方登录
|
||||||
composables:
|
composables:
|
||||||
content_cache:
|
content_cache:
|
||||||
toast_recovered: 已从缓存中恢复未保存的内容
|
toast_recovered: 已从缓存中恢复未保存的内容
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default definePlugin({
|
||||||
name: "AuthProviders",
|
name: "AuthProviders",
|
||||||
component: AuthProviders,
|
component: AuthProviders,
|
||||||
meta: {
|
meta: {
|
||||||
title: "认证方式",
|
title: "core.identity_authentication.title",
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -23,7 +23,7 @@ export default definePlugin({
|
||||||
name: "AuthProviderDetail",
|
name: "AuthProviderDetail",
|
||||||
component: AuthProviderDetail,
|
component: AuthProviderDetail,
|
||||||
meta: {
|
meta: {
|
||||||
title: "认证方式详情",
|
title: "core.identity_authentication.detail.title",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeMount } from "vue";
|
import { computed, onBeforeMount, onMounted } from "vue";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import LoginForm from "@/components/login/LoginForm.vue";
|
import LoginForm from "@/components/login/LoginForm.vue";
|
||||||
import { useRoute } from "vue-router";
|
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 { Toast } from "@halo-dev/components";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import SignupForm from "@/components/signup/SignupForm.vue";
|
import SignupForm from "@/components/signup/SignupForm.vue";
|
||||||
|
import { useGlobalInfoFetch } from "@/composables/use-global-info";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (!userStore.isAnonymous) {
|
if (!userStore.isAnonymous) {
|
||||||
|
@ -21,34 +21,25 @@ onBeforeMount(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
const { globalInfo } = useGlobalInfoFetch();
|
||||||
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;
|
onMounted(() => {
|
||||||
},
|
Toast.warning(t("core.binding.common.toast.mounted"));
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleBinding() {
|
function handleBinding() {
|
||||||
const authProvider = socialAuthProviders.value?.find(
|
const authProvider = globalInfo.value?.socialAuthProviders.find(
|
||||||
(p) => p.name === route.params.provider
|
(p) => p.name === route.params.provider
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!authProvider?.bindingUrl) {
|
if (!authProvider?.bindingUrl) {
|
||||||
Toast.error("绑定失败,没有找到已启用的登录方式");
|
Toast.error(t("core.binding.operations.bind.toast_failed"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = authProvider?.bindingUrl;
|
window.location.href = authProvider?.bindingUrl;
|
||||||
|
|
||||||
Toast.success("绑定成功");
|
Toast.success(t("core.binding.operations.bind.toast_success"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = useRouteQuery<string>("type", "");
|
const type = useRouteQuery<string>("type", "");
|
||||||
|
@ -56,22 +47,42 @@ const type = useRouteQuery<string>("type", "");
|
||||||
function handleChangeType() {
|
function handleChangeType() {
|
||||||
type.value = type.value === "signup" ? "" : "signup";
|
type.value = type.value === "signup" ? "" : "signup";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLoginType = computed(() => type.value !== "signup");
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<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]">
|
||||||
<div class="mb-4">
|
<IconLogo class="mb-8" />
|
||||||
<IconLogo />
|
<div class="flex w-72 flex-col">
|
||||||
</div>
|
<SignupForm
|
||||||
<div class="login-form flex w-72 flex-col">
|
v-if="type === 'signup'"
|
||||||
<div class="mb-4 flex">
|
button-text="core.binding.operations.signup_and_bind.button"
|
||||||
<h1 class="text-sm">{{ $t("core.binding.title") }}</h1>
|
@succeed="handleBinding"
|
||||||
</div>
|
/>
|
||||||
<SignupForm v-if="type === 'signup'" @succeed="handleBinding" />
|
<LoginForm
|
||||||
<LoginForm v-else @succeed="handleBinding" />
|
v-else
|
||||||
<div class="flex">
|
button-text="core.binding.operations.login_and_bind.button"
|
||||||
<span class="mt-4 text-sm text-indigo-600" @click="handleChangeType">
|
@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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeMount, watch } from "vue";
|
import { onBeforeMount, computed, watch } from "vue";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import LoginForm from "@/components/login/LoginForm.vue";
|
import LoginForm from "@/components/login/LoginForm.vue";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import SignupForm from "@/components/signup/SignupForm.vue";
|
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 { locales, getBrowserLanguage, i18n } from "@/locales";
|
||||||
import MdiTranslate from "~icons/mdi/translate";
|
import MdiTranslate from "~icons/mdi/translate";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const { globalInfo } = useGlobalInfoFetch();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const SIGNUP_TYPE = "signup";
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (!userStore.isAnonymous) {
|
if (!userStore.isAnonymous) {
|
||||||
|
@ -25,9 +34,23 @@ function onLoginSucceed() {
|
||||||
const type = useRouteQuery<string>("type", "");
|
const type = useRouteQuery<string>("type", "");
|
||||||
|
|
||||||
function handleChangeType() {
|
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
|
// setup locale
|
||||||
const currentLocale = useLocalStorage(
|
const currentLocale = useLocalStorage(
|
||||||
"locale",
|
"locale",
|
||||||
|
@ -45,15 +68,31 @@ watch(
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<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" />
|
<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" />
|
<SignupForm v-if="type === 'signup'" @succeed="onLoginSucceed" />
|
||||||
<LoginForm v-else @succeed="onLoginSucceed" />
|
<LoginForm v-else @succeed="onLoginSucceed" />
|
||||||
<div class="flex">
|
<SocialAuthProviders />
|
||||||
<span class="mt-4 text-sm text-indigo-600" @click="handleChangeType">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -77,6 +77,7 @@ watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
(visible) => {
|
(visible) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
if (props.user) formState.value = cloneDeep(props.user);
|
||||||
setFocus(isUpdateMode.value ? "displayNameInput" : "userNameInput");
|
setFocus(isUpdateMode.value ? "displayNameInput" : "userNameInput");
|
||||||
} else {
|
} else {
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
|
@ -84,20 +85,6 @@ watch(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.user,
|
|
||||||
(user) => {
|
|
||||||
if (user) {
|
|
||||||
formState.value = cloneDeep(user);
|
|
||||||
} else {
|
|
||||||
handleResetForm();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
const onVisibleChange = (visible: boolean) => {
|
||||||
emit("update:visible", visible);
|
emit("update:visible", visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
|
|
Loading…
Reference in New Issue