perf: 登录界面重做

This commit is contained in:
2026-03-11 20:28:42 +08:00
parent 99f792f6de
commit 0a8a7c8046
13 changed files with 726 additions and 383 deletions

View File

@@ -1,12 +1,12 @@
<template>
<div class="auth-title mb-9 sm:mx-auto sm:w-full sm:max-w-md">
<div class="auth-title mb-8 w-full">
<h2
class="text-foreground mb-2.5 text-3xl font-semibold leading-[1.22] tracking-[-0.015em] lg:text-[2.15rem]"
class="text-foreground mb-2.5 text-[1.9rem] font-semibold leading-[1.12] tracking-[-0.03em] sm:text-[2.1rem]"
>
<slot></slot>
</h2>
<p class="text-muted-foreground text-[0.95rem] leading-7 lg:text-base">
<p class="text-muted-foreground max-w-[34rem] text-[0.97rem] leading-7 sm:text-[1rem]">
<slot name="desc"></slot>
</p>
</div>

View File

@@ -1,15 +1,14 @@
<script setup lang="ts">
import type { Recordable } from '@easyflow/types';
import type {Recordable} from '@easyflow/types';
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import {computed, reactive} from 'vue';
import {useRouter} from 'vue-router';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import {$t} from '@easyflow/locales';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';
@@ -89,10 +88,10 @@ defineExpose({
</script>
<template>
<div>
<div class="auth-code-login">
<Title>
<slot name="title">
{{ title || $t('authentication.welcomeBack') }} 📲
{{ title || $t('authentication.welcomeBack') }}
</slot>
<template #desc>
<span class="text-muted-foreground">
@@ -102,13 +101,15 @@ defineExpose({
</span>
</template>
</Title>
<Form />
<div class="auth-form-group">
<Form />
</div>
<EasyFlowButton
:class="{
'cursor-wait': loading,
}"
:loading="loading"
class="w-full"
class="mt-6 h-11 w-full rounded-xl text-base font-medium"
@click="handleSubmit"
>
<slot name="submitButtonText">
@@ -117,7 +118,7 @@ defineExpose({
</EasyFlowButton>
<EasyFlowButton
v-if="showBack"
class="mt-4 w-full"
class="mt-3 h-11 w-full rounded-xl"
variant="outline"
@click="goToLogin()"
>
@@ -125,3 +126,9 @@ defineExpose({
</EasyFlowButton>
</div>
</template>
<style scoped>
.auth-form-group :deep(.easyflow-form-ui + .easyflow-form-ui) {
margin-top: 0.95rem;
}
</style>

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import {computed, reactive} from 'vue';
import {useRouter} from 'vue-router';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import {$t} from '@easyflow/locales';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';
@@ -82,10 +81,10 @@ defineExpose({
</script>
<template>
<div>
<div class="auth-forget-password">
<Title>
<slot name="title">
{{ title || $t('authentication.forgetPassword') }} 🤦🏻
{{ title || $t('authentication.forgetPassword') }}
</slot>
<template #desc>
<slot name="subTitle">
@@ -93,7 +92,9 @@ defineExpose({
</slot>
</template>
</Title>
<Form />
<div class="auth-form-group">
<Form />
</div>
<div>
<EasyFlowButton
@@ -101,16 +102,26 @@ defineExpose({
'cursor-wait': loading,
}"
aria-label="submit"
class="mt-2 w-full"
class="mt-6 h-11 w-full rounded-xl text-base font-medium"
@click="handleSubmit"
>
<slot name="submitButtonText">
{{ submitButtonText || $t('authentication.sendResetLink') }}
</slot>
</EasyFlowButton>
<EasyFlowButton class="mt-4 w-full" variant="outline" @click="goToLogin()">
<EasyFlowButton
class="mt-3 h-11 w-full rounded-xl"
variant="outline"
@click="goToLogin()"
>
{{ $t('common.back') }}
</EasyFlowButton>
</div>
</div>
</template>
<style scoped>
.auth-form-group :deep(.easyflow-form-ui + .easyflow-form-ui) {
margin-top: 0.95rem;
}
</style>

View File

@@ -1,17 +1,16 @@
<script setup lang="ts">
import type { Recordable } from '@easyflow/types';
import type {Recordable} from '@easyflow/types';
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import type { AuthenticationProps } from './types';
import type {AuthenticationProps} from './types';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import {computed, onMounted, reactive, ref} from 'vue';
import {useRouter} from 'vue-router';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton, EasyFlowCheckbox } from '@easyflow-core/shadcn-ui';
import {$t} from '@easyflow/locales';
import {EasyFlowButton, EasyFlowCheckbox} from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';
import ThirdPartyLogin from './third-party-login.vue';
@@ -33,6 +32,7 @@ const props = withDefaults(defineProps<Props>(), {
registerPath: '/auth/register',
showCodeLogin: false,
showForgetPassword: false,
showHeader: true,
showQrcodeLogin: false,
showRegister: false,
showRememberMe: false,
@@ -58,11 +58,9 @@ const [Form, formApi] = useEasyFlowForm(
);
const router = useRouter();
const REMEMBER_ME_KEY = `REMEMBER_ME_USERNAME_${location.hostname}`;
const localUsername = localStorage.getItem(REMEMBER_ME_KEY) || '';
const rememberMe = ref(!!localUsername);
const REMEMBER_ME_KEY = `REMEMBER_ME_ACCOUNT_${location.hostname}`;
const localAccount = localStorage.getItem(REMEMBER_ME_KEY) || '';
const rememberMe = ref(!!localAccount);
async function handleSubmit() {
const { valid } = await formApi.validate();
@@ -70,7 +68,7 @@ async function handleSubmit() {
if (valid) {
localStorage.setItem(
REMEMBER_ME_KEY,
rememberMe.value ? values?.username : '',
rememberMe.value ? (values?.account ?? '') : '',
);
emit('submit', values);
}
@@ -81,8 +79,8 @@ function handleGo(path: string) {
}
onMounted(() => {
if (localUsername) {
formApi.setFieldValue('username', localUsername);
if (localAccount) {
formApi.setFieldValue('account', localAccount);
}
});
@@ -93,65 +91,69 @@ defineExpose({
<template>
<div class="auth-login" @keydown.enter.prevent="handleSubmit">
<slot name="title">
<Title>
<slot name="title">
{{ title || $t('authentication.welcomeBack') }}
</slot>
<template #desc>
<span class="text-muted-foreground">
<slot name="subTitle">
{{ subTitle || $t('authentication.loginSubtitle') }}
</slot>
</span>
</template>
</Title>
</slot>
<template v-if="showHeader">
<slot name="title">
<Title>
<slot name="title">
{{ title || $t('authentication.welcomeBack') }}
</slot>
<template v-if="subTitle || $t('authentication.loginSubtitle')" #desc>
<span class="text-muted-foreground">
<slot name="subTitle">
{{ subTitle || $t('authentication.loginSubtitle') }}
</slot>
</span>
</template>
</Title>
</slot>
</template>
<Form />
<div class="auth-form-group">
<Form />
</div>
<div
v-if="showRememberMe || showForgetPassword"
class="auth-login-options mb-7 mt-2 flex justify-between"
class="auth-login-options mt-1 flex items-center justify-between gap-3"
>
<div class="flex-center">
<EasyFlowCheckbox
v-if="showRememberMe"
v-model="rememberMe"
name="rememberMe"
>
{{ $t('authentication.rememberMe') }}
</EasyFlowCheckbox>
</div>
<EasyFlowCheckbox
v-if="showRememberMe"
v-model="rememberMe"
class="auth-checkbox"
name="rememberMe"
>
{{ $t('authentication.rememberMe') }}
</EasyFlowCheckbox>
<span
<button
v-if="showForgetPassword"
class="easyflow-link text-sm font-normal"
class="auth-inline-action"
type="button"
@click="handleGo(forgetPasswordPath)"
>
{{ $t('authentication.forgetPassword') }}
</span>
</button>
</div>
<EasyFlowButton
:class="{
'cursor-wait': loading,
}"
:class="{ 'cursor-wait': loading }"
:loading="loading"
aria-label="login"
class="auth-submit-button h-11 w-full rounded-xl text-base font-medium"
class="auth-submit-button auth-brand-submit mt-6 h-11 w-full rounded-xl text-base font-medium"
@click="handleSubmit"
>
{{ submitButtonText || $t('common.login') }}
</EasyFlowButton>
<slot name="overlay"></slot>
<div
v-if="showCodeLogin || showQrcodeLogin"
class="auth-login-quick mb-2 mt-5 flex items-center justify-between"
class="auth-alt-modes mt-5 grid gap-3 sm:grid-cols-2"
>
<EasyFlowButton
v-if="showCodeLogin"
class="w-1/2"
class="auth-secondary-button h-11 w-full rounded-xl"
variant="outline"
@click="handleGo(codeLoginPath)"
>
@@ -159,7 +161,7 @@ defineExpose({
</EasyFlowButton>
<EasyFlowButton
v-if="showQrcodeLogin"
class="ml-4 w-1/2"
class="auth-secondary-button h-11 w-full rounded-xl"
variant="outline"
@click="handleGo(qrCodeLoginPath)"
>
@@ -172,21 +174,113 @@ defineExpose({
</slot>
<slot name="to-register">
<div v-if="showRegister" class="mt-4 text-center text-sm">
<div v-if="showRegister" class="auth-footer-copy mt-5 text-center text-sm">
{{ $t('authentication.accountTip') }}
<span
class="easyflow-link text-sm font-normal"
<button
class="auth-inline-action"
type="button"
@click="handleGo(registerPath)"
>
{{ $t('authentication.createAccount') }}
</span>
</button>
</div>
</slot>
</div>
</template>
<style scoped>
.auth-login :deep(.easyflow-form-ui + .easyflow-form-ui) {
.auth-login {
position: relative;
}
.auth-form-group :deep(.easyflow-form-ui + .easyflow-form-ui) {
margin-top: 0.95rem;
}
.auth-form-group :deep(.easyflow-form-ui .text-destructive) {
font-size: 0.84rem;
margin-top: 0.45rem;
}
.auth-login-options {
min-height: 1.5rem;
}
.auth-checkbox {
color: hsl(var(--text-muted));
font-size: 0.92rem;
}
.auth-inline-action {
color: hsl(var(--nav-item-active-foreground));
font-size: 0.92rem;
font-weight: 500;
transition: opacity 0.18s ease;
}
.auth-inline-action:hover {
opacity: 0.76;
}
.auth-secondary-button {
background: hsl(var(--surface-elevated));
border-color: hsl(var(--line-subtle));
}
.auth-brand-submit {
background:
linear-gradient(
120deg,
rgb(11 111 211) 0%,
rgb(22 159 200) 38%,
rgb(38 199 193) 62%,
rgb(11 111 211) 100%
);
background-size: 200% 200%;
border: none;
box-shadow:
0 22px 34px -22px rgb(11 111 211 / 0.56),
inset 0 1px 0 rgb(255 255 255 / 0.24);
color: rgb(255 255 255);
transition:
transform 180ms ease,
box-shadow 180ms ease,
filter 180ms ease;
animation: auth-brand-gradient 6s ease infinite;
}
.auth-brand-submit:hover {
box-shadow:
0 24px 38px -22px rgb(11 111 211 / 0.62),
inset 0 1px 0 rgb(255 255 255 / 0.28);
filter: saturate(1.04);
transform: translateY(-1px);
}
.auth-brand-submit:active {
transform: translateY(0);
}
.auth-brand-submit:focus-visible {
outline: 2px solid rgb(78 176 255 / 0.8);
outline-offset: 2px;
}
.auth-footer-copy {
color: hsl(var(--text-muted));
}
@keyframes auth-brand-gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import {ref} from 'vue';
import {useRouter} from 'vue-router';
import { $t } from '@easyflow/locales';
import {$t} from '@easyflow/locales';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import { useQRCode } from '@vueuse/integrations/useQRCode';
import {useQRCode} from '@vueuse/integrations/useQRCode';
import Title from './auth-title.vue';
@@ -70,10 +70,10 @@ function goToLogin() {
</script>
<template>
<div>
<div class="auth-qrcode-login">
<Title>
<slot name="title">
{{ title || $t('authentication.welcomeBack') }} 📱
{{ title || $t('authentication.welcomeBack') }}
</slot>
<template #desc>
<span class="text-muted-foreground">
@@ -84,9 +84,11 @@ function goToLogin() {
</template>
</Title>
<div class="flex-col-center mt-6">
<img :src="qrcode" alt="qrcode" class="w-1/2" />
<p class="text-muted-foreground mt-4 text-sm">
<div class="auth-qrcode-panel mt-6">
<div class="auth-qrcode-frame">
<img :src="qrcode" alt="qrcode" class="auth-qrcode-image" />
</div>
<p class="text-muted-foreground mt-4 text-sm leading-6">
<slot name="description">
{{ description || $t('authentication.qrcodePrompt') }}
</slot>
@@ -95,7 +97,7 @@ function goToLogin() {
<EasyFlowButton
v-if="showBack"
class="mt-4 w-full"
class="mt-6 h-11 w-full rounded-xl"
variant="outline"
@click="goToLogin()"
>
@@ -103,3 +105,32 @@ function goToLogin() {
</EasyFlowButton>
</div>
</template>
<style scoped>
.auth-qrcode-panel {
text-align: center;
}
.auth-qrcode-frame {
align-items: center;
background: linear-gradient(180deg, rgb(255 255 255 / 0.92), rgb(244 249 255 / 0.96));
border: 1px solid hsl(var(--line-subtle));
border-radius: 1.5rem;
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
display: flex;
justify-content: center;
margin: 0 auto;
max-width: 17rem;
padding: 1rem;
}
.auth-qrcode-image {
aspect-ratio: 1;
border-radius: 1rem;
width: min(100%, 13rem);
}
.dark .auth-qrcode-frame {
background: linear-gradient(180deg, rgb(14 22 36 / 0.92), rgb(11 19 31 / 0.96));
}
</style>

View File

@@ -1,15 +1,14 @@
<script setup lang="ts">
import type { Recordable } from '@easyflow/types';
import type {Recordable} from '@easyflow/types';
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import {computed, reactive} from 'vue';
import {useRouter} from 'vue-router';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import {$t} from '@easyflow/locales';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';
@@ -85,10 +84,10 @@ defineExpose({
</script>
<template>
<div>
<div class="auth-register">
<Title>
<slot name="title">
{{ title || $t('authentication.createAnAccount') }} 🚀
{{ title || $t('authentication.createAnAccount') }}
</slot>
<template #desc>
<slot name="subTitle">
@@ -96,7 +95,9 @@ defineExpose({
</slot>
</template>
</Title>
<Form />
<div class="auth-form-group">
<Form />
</div>
<EasyFlowButton
:class="{
@@ -104,18 +105,35 @@ defineExpose({
}"
:loading="loading"
aria-label="register"
class="mt-2 w-full"
class="mt-6 h-11 w-full rounded-xl text-base font-medium"
@click="handleSubmit"
>
<slot name="submitButtonText">
{{ submitButtonText || $t('authentication.signUp') }}
</slot>
</EasyFlowButton>
<div class="mt-4 text-center text-sm">
<div class="mt-5 text-center text-sm text-[hsl(var(--text-muted))]">
{{ $t('authentication.alreadyHaveAccount') }}
<span class="easyflow-link text-sm font-normal" @click="goToLogin()">
<button class="auth-inline-action" type="button" @click="goToLogin()">
{{ $t('authentication.goToLogin') }}
</span>
</button>
</div>
</div>
</template>
<style scoped>
.auth-form-group :deep(.easyflow-form-ui + .easyflow-form-ui) {
margin-top: 0.95rem;
}
.auth-inline-action {
color: hsl(var(--nav-item-active-foreground));
font-size: 0.92rem;
font-weight: 500;
transition: opacity 0.18s ease;
}
.auth-inline-action:hover {
opacity: 0.76;
}
</style>

View File

@@ -52,6 +52,11 @@ interface AuthenticationProps {
*/
showThirdPartyLogin?: boolean;
/**
* @zh_CN 是否显示登录头部
*/
showHeader?: boolean;
/**
* @zh_CN 登录框子标题
*/