refactor: layout of login related page (#5413)

#### What type of PR is this?

/area ui
/kind improvement
/milestone 2.13.0

#### What this PR does / why we need it:

优化登录相关页面的布局,修复在不同分辨率下的样式问题。

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/5346

#### Special notes for your reviewer:

测试登录或者注册页面,任意放大或者缩小页面,观察页面样式是否正常。

#### Does this PR introduce a user-facing change?

```release-note
优化登录相关页面的布局,修复在不同分辨率下的样式问题。
```
pull/5422/head^2
Ryan Wang 2024-02-27 18:21:14 +08:00 committed by GitHub
parent 2bfa20d316
commit 827030dd68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 310 additions and 288 deletions

View File

@ -0,0 +1,13 @@
<script lang="ts" setup>
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
</script>
<template>
<div
class="flex h-screen flex-col items-center overflow-auto bg-white/90 pt-[30vh]"
>
<IconLogo class="mb-8 flex-none" />
<RouterView />
</div>
</template>

View File

@ -1,98 +0,0 @@
<script lang="ts" setup>
import { computed, watch } from "vue";
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
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 "@console/composables/use-global-info";
import { useTitle } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { AppName } from "@/constants/app";
import MdiKeyboardBackspace from "~icons/mdi/keyboard-backspace";
import LocaleChange from "@/components/common/LocaleChange.vue";
const { globalInfo } = useGlobalInfoFetch();
const { t } = useI18n();
const SIGNUP_TYPE = "signup";
function onLoginSucceed() {
window.location.reload();
}
function onSignupSucceed() {
window.location.href = "/uc";
}
const type = useRouteQuery<string>("type", "");
function handleChangeType() {
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(" - ");
}
);
</script>
<template>
<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'" @succeed="onSignupSucceed" />
<LoginForm v-else @succeed="onLoginSucceed" />
<SocialAuthProviders />
<div class="flex justify-center gap-2 pt-3.5 text-xs">
<div v-if="globalInfo?.allowRegistration" class="space-x-0.5">
<span class="text-slate-500">
{{
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>
<RouterLink
:to="{ name: 'ResetPassword' }"
class="text-secondary hover:text-gray-600"
>
{{ $t("core.login.operations.reset_password.button") }}
</RouterLink>
</div>
<div class="flex justify-center pt-3.5">
<a
class="inline-flex items-center gap-0.5 text-xs text-gray-600 hover:text-gray-900"
href="/"
>
<MdiKeyboardBackspace class="!h-3.5 !w-3.5" />
<span> {{ $t("core.login.operations.return_site") }} </span>
</a>
</div>
</div>
<div
class="bottom-0 mb-10 mt-auto flex items-center justify-center gap-2.5"
>
<LocaleChange />
</div>
</div>
</template>

View File

@ -3,10 +3,8 @@ import BasicLayout from "@console/layouts/BasicLayout.vue";
import UserStatsWidget from "./widgets/UserStatsWidget.vue";
import UserList from "./UserList.vue";
import UserDetail from "./UserDetail.vue";
import Login from "./Login.vue";
import { IconUserSettings } from "@halo-dev/components";
import { markRaw } from "vue";
import Binding from "./Binding.vue";
import NotificationWidget from "./widgets/NotificationWidget.vue";
export default definePlugin({
@ -15,22 +13,6 @@ export default definePlugin({
NotificationWidget,
},
routes: [
{
path: "/login",
name: "Login",
component: Login,
meta: {
title: "core.login.title",
},
},
{
path: "/binding/:provider",
name: "Binding",
component: Binding,
meta: {
title: "core.binding.title",
},
},
{
path: "/users",
name: "UsersRoot",

View File

@ -2,10 +2,13 @@ import type { RouteRecordRaw } from "vue-router";
import NotFound from "@/views/exceptions/NotFound.vue";
import Forbidden from "@/views/exceptions/Forbidden.vue";
import BasicLayout from "@console/layouts/BasicLayout.vue";
import GatewayLayout from "@console/layouts/GatewayLayout.vue";
import Setup from "@console/views/system/Setup.vue";
import Redirect from "@console/views/system/Redirect.vue";
import SetupInitialData from "@console/views/system/SetupInitialData.vue";
import ResetPassword from "@console/views/system/ResetPassword.vue";
import Login from "@console/views/system/Login.vue";
import Binding from "@console/views/system/Binding.vue";
export const routes: Array<RouteRecordRaw> = [
{
@ -24,13 +27,47 @@ export const routes: Array<RouteRecordRaw> = [
},
],
},
{
path: "/login",
component: GatewayLayout,
children: [
{
path: "",
name: "Login",
component: Login,
meta: {
title: "core.login.title",
},
},
],
},
{
path: "/binding/:provider",
component: GatewayLayout,
children: [
{
path: "",
name: "Binding",
component: Binding,
meta: {
title: "core.binding.title",
},
},
],
},
{
path: "/setup",
name: "Setup",
component: Setup,
meta: {
title: "core.setup.title",
},
component: GatewayLayout,
children: [
{
path: "",
name: "Setup",
component: Setup,
meta: {
title: "core.setup.title",
},
},
],
},
{
path: "/setup-initial-data",
@ -47,11 +84,17 @@ export const routes: Array<RouteRecordRaw> = [
},
{
path: "/reset-password",
name: "ResetPassword",
component: ResetPassword,
meta: {
title: "core.reset_password.title",
},
component: GatewayLayout,
children: [
{
path: "",
name: "ResetPassword",
component: ResetPassword,
meta: {
title: "core.reset_password.title",
},
},
],
},
];

View File

@ -1,7 +1,6 @@
<script lang="ts" setup>
import { computed, onBeforeMount, onMounted } from "vue";
import router from "@console/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";
@ -51,41 +50,38 @@ function handleChangeType() {
const isLoginType = computed(() => type.value !== "signup");
</script>
<template>
<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"
<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">
{{
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"
>
<span class="text-slate-500">
{{
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>
{{
isLoginType
? $t("core.login.operations.signup.button")
: $t("core.login.operations.return_login.button")
}}
</span>
</div>
</div>
</template>

View File

@ -0,0 +1,94 @@
<script lang="ts" setup>
import { computed, watch } from "vue";
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 "@console/composables/use-global-info";
import { useTitle } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { AppName } from "@/constants/app";
import MdiKeyboardBackspace from "~icons/mdi/keyboard-backspace";
import LocaleChange from "@/components/common/LocaleChange.vue";
const { globalInfo } = useGlobalInfoFetch();
const { t } = useI18n();
const SIGNUP_TYPE = "signup";
function onLoginSucceed() {
window.location.reload();
}
function onSignupSucceed() {
window.location.href = "/uc";
}
const type = useRouteQuery<string>("type", "");
function handleChangeType() {
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(" - ");
}
);
</script>
<template>
<div class="flex w-72 flex-col">
<SignupForm v-if="type === 'signup'" @succeed="onSignupSucceed" />
<LoginForm v-else @succeed="onLoginSucceed" />
<SocialAuthProviders />
<div class="flex justify-center gap-2 pt-3.5 text-xs">
<div v-if="globalInfo?.allowRegistration" class="space-x-0.5">
<span class="text-slate-500">
{{
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>
<RouterLink
:to="{ name: 'ResetPassword' }"
class="text-secondary hover:text-gray-600"
>
{{ $t("core.login.operations.reset_password.button") }}
</RouterLink>
</div>
<div class="flex justify-center pt-3.5">
<a
class="inline-flex items-center gap-0.5 text-xs text-gray-600 hover:text-gray-900"
href="/"
>
<MdiKeyboardBackspace class="!h-3.5 !w-3.5" />
<span> {{ $t("core.login.operations.return_site") }} </span>
</a>
</div>
</div>
<div
class="bottom-0 mb-10 mt-auto flex items-center justify-center gap-2.5 pt-3.5"
>
<LocaleChange />
</div>
</template>

View File

@ -3,7 +3,6 @@ import { apiClient } from "@/utils/api-client";
import { Toast, VButton } from "@halo-dev/components";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
const { t } = useI18n();
@ -38,48 +37,45 @@ const inputClasses = {
</script>
<template>
<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">
<div class="flex w-72 flex-col">
<FormKit
id="reset-password-form"
name="reset-password-form"
type="form"
:classes="{
form: '!divide-none',
}"
:config="{ validationVisibility: 'submit' }"
@submit="onSubmit"
@keyup.enter="$formkit.submit('reset-password-form')"
>
<FormKit
id="reset-password-form"
name="reset-password-form"
type="form"
:classes="{
form: '!divide-none',
}"
:config="{ validationVisibility: 'submit' }"
@submit="onSubmit"
@keyup.enter="$formkit.submit('reset-password-form')"
>
<FormKit
:classes="inputClasses"
name="username"
:placeholder="$t('core.reset_password.fields.username.label')"
:validation-label="$t('core.reset_password.fields.username.label')"
:autofocus="true"
type="text"
validation="required"
></FormKit>
<FormKit
:classes="inputClasses"
name="email"
:placeholder="$t('core.reset_password.fields.email.label')"
:validation-label="$t('core.reset_password.fields.email.label')"
:autofocus="true"
type="text"
validation="required"
></FormKit>
</FormKit>
<VButton
class="mt-8"
block
:loading="loading"
type="secondary"
@click="$formkit.submit('reset-password-form')"
>
{{ $t("core.reset_password.operations.send.label") }}
</VButton>
</div>
:classes="inputClasses"
name="username"
:placeholder="$t('core.reset_password.fields.username.label')"
:validation-label="$t('core.reset_password.fields.username.label')"
:autofocus="true"
type="text"
validation="required"
></FormKit>
<FormKit
:classes="inputClasses"
name="email"
:placeholder="$t('core.reset_password.fields.email.label')"
:validation-label="$t('core.reset_password.fields.email.label')"
:autofocus="true"
type="text"
validation="required"
></FormKit>
</FormKit>
<VButton
class="mt-8"
block
:loading="loading"
type="secondary"
@click="$formkit.submit('reset-password-form')"
>
{{ $t("core.reset_password.operations.send.label") }}
</VButton>
</div>
</template>

View File

@ -1,5 +1,4 @@
<script lang="ts" setup>
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
import { apiClient } from "@/utils/api-client";
import { Toast, VButton } from "@halo-dev/components";
import { ref } from "vue";
@ -44,87 +43,82 @@ const inputClasses = {
</script>
<template>
<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">
<FormKit
id="setup-form"
v-model="formState"
name="setup-form"
:actions="false"
:classes="{
form: '!divide-none',
}"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="handleSubmit"
@keyup.enter="$formkit.submit('setup-form')"
>
<FormKit
name="siteTitle"
:classes="inputClasses"
:autofocus="true"
type="text"
:validation-label="$t('core.setup.fields.site_title.label')"
:placeholder="$t('core.setup.fields.site_title.label')"
validation="required:trim|length:0,100"
></FormKit>
<FormKit
name="email"
:classes="inputClasses"
type="text"
:validation-label="$t('core.setup.fields.email.label')"
:placeholder="$t('core.setup.fields.email.label')"
validation="required|email|length:0,100"
></FormKit>
<FormKit
name="username"
:classes="inputClasses"
type="text"
:validation-label="$t('core.setup.fields.username.label')"
:placeholder="$t('core.setup.fields.username.label')"
:validation="[
['required'],
['length:0,63'],
[
'matches',
/^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/,
],
]"
></FormKit>
<FormKit
name="password"
:classes="inputClasses"
type="password"
:validation-label="$t('core.setup.fields.password.label')"
:placeholder="$t('core.setup.fields.password.label')"
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
autocomplete="current-password"
></FormKit>
<FormKit
name="password_confirm"
:classes="inputClasses"
type="password"
:validation-label="$t('core.setup.fields.confirm_password.label')"
:placeholder="$t('core.setup.fields.confirm_password.label')"
validation="confirm|required:trim|length:5,100|matches:/^\S.*\S$/"
autocomplete="current-password"
></FormKit>
</FormKit>
<VButton
block
class="mt-8"
type="secondary"
:loading="loading"
@click="$formkit.submit('setup-form')"
>
{{ $t("core.setup.operations.submit.button") }}
</VButton>
</div>
<div
class="bottom-0 mb-10 mt-auto flex items-center justify-center gap-2.5"
<div class="flex w-72 flex-col">
<FormKit
id="setup-form"
v-model="formState"
name="setup-form"
:actions="false"
:classes="{
form: '!divide-none',
}"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="handleSubmit"
@keyup.enter="$formkit.submit('setup-form')"
>
<LocaleChange />
</div>
<FormKit
name="siteTitle"
:classes="inputClasses"
:autofocus="true"
type="text"
:validation-label="$t('core.setup.fields.site_title.label')"
:placeholder="$t('core.setup.fields.site_title.label')"
validation="required:trim|length:0,100"
></FormKit>
<FormKit
name="email"
:classes="inputClasses"
type="text"
:validation-label="$t('core.setup.fields.email.label')"
:placeholder="$t('core.setup.fields.email.label')"
validation="required|email|length:0,100"
></FormKit>
<FormKit
name="username"
:classes="inputClasses"
type="text"
:validation-label="$t('core.setup.fields.username.label')"
:placeholder="$t('core.setup.fields.username.label')"
:validation="[
['required'],
['length:0,63'],
[
'matches',
/^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/,
],
]"
></FormKit>
<FormKit
name="password"
:classes="inputClasses"
type="password"
:validation-label="$t('core.setup.fields.password.label')"
:placeholder="$t('core.setup.fields.password.label')"
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
autocomplete="current-password"
></FormKit>
<FormKit
name="password_confirm"
:classes="inputClasses"
type="password"
:validation-label="$t('core.setup.fields.confirm_password.label')"
:placeholder="$t('core.setup.fields.confirm_password.label')"
validation="confirm|required:trim|length:5,100|matches:/^\S.*\S$/"
autocomplete="current-password"
></FormKit>
</FormKit>
<VButton
block
class="mt-8"
type="secondary"
:loading="loading"
@click="$formkit.submit('setup-form')"
>
{{ $t("core.setup.operations.submit.button") }}
</VButton>
</div>
<div class="bottom-0 mb-10 mt-auto flex items-center justify-center gap-2.5">
<LocaleChange />
</div>
</template>

View File

@ -46,8 +46,10 @@ const inputClasses = {
</script>
<template>
<div class="flex h-screen flex-col items-center bg-white/90 pt-[30vh]">
<IconLogo class="mb-8" />
<div
class="flex h-screen flex-col items-center overflow-auto bg-white/90 pt-[30vh]"
>
<IconLogo class="mb-8 flex-none" />
<div class="flex w-72 flex-col">
<FormKit
id="reset-password-form"