refactor(auth): AuthLayout supports optional narrative slot
New slot 'narrative' enables split-screen layout (50/50 desktop, collapses to single column on mobile <900px). Backward compatibility: - Pages that don't pass a narrative slot still render the original centered-card layout with siteName + logo + copyright - ForgotPassword, ResetPassword, EmailVerify unaffected To be used in Tasks 8 and 9 (LoginView, RegisterView). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,69 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex min-h-screen items-center justify-center overflow-hidden p-4">
|
<div class="auth-shell" :class="{ 'auth-shell-split': hasNarrative }">
|
||||||
<!-- Background -->
|
<div class="bg-glow soft"></div>
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-gradient-to-br from-gray-50 via-primary-50/30 to-gray-100 dark:from-dark-950 dark:via-dark-900 dark:to-dark-950"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Decorative Elements -->
|
<!-- LEFT: Narrative (split mode only, hidden on mobile) -->
|
||||||
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
<aside v-if="hasNarrative" class="auth-narrative">
|
||||||
<!-- Gradient Orbs -->
|
<slot name="narrative"></slot>
|
||||||
<div
|
</aside>
|
||||||
class="absolute -right-40 -top-40 h-80 w-80 rounded-full bg-primary-400/20 blur-3xl"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-primary-500/15 blur-3xl"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="absolute left-1/2 top-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-300/10 blur-3xl"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Grid Pattern -->
|
<!-- RIGHT: Form -->
|
||||||
<div
|
<main class="auth-main">
|
||||||
class="absolute inset-0 bg-[linear-gradient(rgba(20,184,166,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(20,184,166,0.03)_1px,transparent_1px)] bg-[size:64px_64px]"
|
<div class="auth-main-inner">
|
||||||
></div>
|
<!-- Legacy centered-card header (only when no narrative slot) -->
|
||||||
</div>
|
<div class="mb-8 text-center" v-if="!hasNarrative && settingsLoaded">
|
||||||
|
|
||||||
<!-- Content Container -->
|
|
||||||
<div class="relative z-10 w-full max-w-md">
|
|
||||||
<!-- Logo/Brand -->
|
|
||||||
<div class="mb-8 text-center">
|
|
||||||
<!-- Custom Logo or Default Logo -->
|
|
||||||
<template v-if="settingsLoaded">
|
|
||||||
<div
|
<div
|
||||||
class="mb-4 inline-flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl shadow-lg shadow-primary-500/30"
|
class="mb-4 inline-flex h-14 w-14 items-center justify-center overflow-hidden rounded-2xl"
|
||||||
>
|
>
|
||||||
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
|
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-gradient mb-2 text-3xl font-bold">
|
<h1 class="text-2xl font-bold">{{ siteName }}</h1>
|
||||||
{{ siteName }}
|
<p class="text-sm text-gray-500 dark:text-dark-400" v-if="siteSubtitle">{{ siteSubtitle }}</p>
|
||||||
</h1>
|
</div>
|
||||||
<p class="text-sm text-gray-500 dark:text-dark-400">
|
|
||||||
{{ siteSubtitle }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card Container -->
|
<!-- Form content -->
|
||||||
<div class="card-glass rounded-2xl p-8 shadow-glass">
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer Links -->
|
<!-- Footer link slot (e.g., "没有账户?注册") -->
|
||||||
<div class="mt-6 text-center text-sm">
|
<div class="mt-6 text-center text-sm">
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Copyright -->
|
<!-- Copyright (legacy mode only) -->
|
||||||
<div class="mt-8 text-center text-xs text-gray-400 dark:text-dark-500">
|
<div class="mt-8 text-center text-xs text-gray-400 dark:text-dark-500" v-if="!hasNarrative">
|
||||||
© {{ currentYear }} {{ siteName }}. All rights reserved.
|
© {{ currentYear }} {{ siteName }}. All rights reserved.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, useSlots } from 'vue'
|
||||||
import { useAppStore } from '@/stores'
|
import { useAppStore } from '@/stores'
|
||||||
import { sanitizeUrl } from '@/utils/url'
|
import { sanitizeUrl } from '@/utils/url'
|
||||||
|
|
||||||
@@ -76,6 +52,9 @@ const settingsLoaded = computed(() => appStore.publicSettingsLoaded)
|
|||||||
|
|
||||||
const currentYear = computed(() => new Date().getFullYear())
|
const currentYear = computed(() => new Date().getFullYear())
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
const hasNarrative = computed(() => !!slots.narrative)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
appStore.fetchPublicSettings()
|
appStore.fetchPublicSettings()
|
||||||
})
|
})
|
||||||
@@ -85,4 +64,57 @@ onMounted(() => {
|
|||||||
.text-gradient {
|
.text-gradient {
|
||||||
@apply bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent;
|
@apply bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.auth-shell-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.auth-shell-split {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.auth-narrative {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.auth-narrative {
|
||||||
|
position: relative;
|
||||||
|
padding: 48px 56px;
|
||||||
|
background: linear-gradient(135deg, #0a0e1a 0%, #1e1b4b 100%);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--text-0);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
.auth-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.auth-main-inner {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy-mode (no narrative slot) background — keep existing gradient decorative look */
|
||||||
|
.auth-shell:not(.auth-shell-split) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(to bottom right, #f9fafb, rgba(240,253,250,0.3), #f3f4f6);
|
||||||
|
}
|
||||||
|
:global(.dark) .auth-shell:not(.auth-shell-split) {
|
||||||
|
background: linear-gradient(to bottom right, #020617, #0f172a, #020617);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user