mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-10 16:13:28 +08:00
feat: 增强登录页交互式 3D Logo (#536)
* feat(login): add interactive optical logo lab * feat(login): add prismatic logo component * fix(login): refine optical logo colors and rendering * feat(login): add matte logo material and sizing * feat(login): add logo palette toggle * feat(login): add configurable logo presets * fix(login): ignore lab route after authentication * fix(login): keep prism idle motion
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"express": "^4.21.2",
|
||||
"express-http-proxy": "^2.1.1",
|
||||
"gridstack": "^12.6.0",
|
||||
"gsap": "^3.15.0",
|
||||
"http-proxy-middleware": "^3.0.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
3538
src/components/misc/OpticalLogoLab.vue
Normal file
3538
src/components/misc/OpticalLogoLab.vue
Normal file
File diff suppressed because it is too large
Load Diff
247
src/components/misc/PrismaticLogo.vue
Normal file
247
src/components/misc/PrismaticLogo.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import logoUrl from '@images/logo.svg'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
animate?: boolean
|
||||
intensity?: number
|
||||
}>(),
|
||||
{
|
||||
animate: true,
|
||||
intensity: 45,
|
||||
},
|
||||
)
|
||||
|
||||
const rootRef = ref<HTMLSpanElement | null>(null)
|
||||
const logoMaskStyle = computed(() => ({
|
||||
'--logo-mask': `url("${logoUrl}")`,
|
||||
'--prism-intensity': Math.min(1, Math.max(0, props.intensity / 100)),
|
||||
}))
|
||||
|
||||
let pointerFrame: number | null = null
|
||||
let pendingPointerX = 0.5
|
||||
let pendingPointerY = 0.42
|
||||
|
||||
/** 将指针位置映射为同一套棱镜反射和轻微空间倾角。 */
|
||||
function renderPointerResponse() {
|
||||
pointerFrame = null
|
||||
const root = rootRef.value
|
||||
if (!root) return
|
||||
|
||||
root.style.setProperty('--logo-light-x', `${(pendingPointerX * 100).toFixed(2)}%`)
|
||||
root.style.setProperty('--logo-light-y', `${(pendingPointerY * 100).toFixed(2)}%`)
|
||||
root.style.setProperty('--logo-tilt-x', `${((0.5 - pendingPointerY) * 7).toFixed(2)}deg`)
|
||||
root.style.setProperty('--logo-tilt-y', `${((pendingPointerX - 0.5) * 9).toFixed(2)}deg`)
|
||||
}
|
||||
|
||||
function queuePointerResponse() {
|
||||
if (pointerFrame === null) pointerFrame = window.requestAnimationFrame(renderPointerResponse)
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch') return
|
||||
const bounds = rootRef.value?.getBoundingClientRect()
|
||||
if (!bounds?.width || !bounds.height) return
|
||||
|
||||
pendingPointerX = Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width))
|
||||
pendingPointerY = Math.min(1, Math.max(0, (event.clientY - bounds.top) / bounds.height))
|
||||
queuePointerResponse()
|
||||
}
|
||||
|
||||
function resetPointerResponse() {
|
||||
pendingPointerX = 0.5
|
||||
pendingPointerY = 0.42
|
||||
queuePointerResponse()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pointerFrame !== null) window.cancelAnimationFrame(pointerFrame)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
ref="rootRef"
|
||||
class="prismatic-logo"
|
||||
:class="{ 'prismatic-logo--animated': props.animate }"
|
||||
:style="logoMaskStyle"
|
||||
role="img"
|
||||
aria-label="MoviePilot"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerleave="resetPointerResponse"
|
||||
>
|
||||
<img :src="logoUrl" class="prismatic-logo__base" alt="" draggable="false" aria-hidden="true" />
|
||||
<span class="prismatic-logo__spectrum" aria-hidden="true" />
|
||||
<span class="prismatic-logo__specular" aria-hidden="true" />
|
||||
<span class="prismatic-logo__reveal" aria-hidden="true" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.prismatic-logo {
|
||||
--logo-light-x: 50%;
|
||||
--logo-light-y: 42%;
|
||||
--logo-tilt-x: 0deg;
|
||||
--logo-tilt-y: 0deg;
|
||||
|
||||
position: relative;
|
||||
display: grid;
|
||||
isolation: isolate;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
place-items: center;
|
||||
transform: perspective(520px) rotateX(var(--logo-tilt-x)) rotateY(var(--logo-tilt-y));
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.prismatic-logo--animated {
|
||||
animation: prismatic-logo-enter 620ms cubic-bezier(0.16, 1, 0.3, 1) 80ms backwards;
|
||||
}
|
||||
|
||||
.prismatic-logo::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(var(--v-theme-primary), 0.34),
|
||||
rgba(var(--v-theme-primary), 0.1) 44%,
|
||||
transparent 72%
|
||||
);
|
||||
content: '';
|
||||
filter: blur(13px);
|
||||
inset: 17%;
|
||||
opacity: calc(0.36 + var(--prism-intensity) * 0.56);
|
||||
transform: translate3d(0, 8px, -16px) scaleX(1.18);
|
||||
}
|
||||
|
||||
.prismatic-logo__base,
|
||||
.prismatic-logo__spectrum,
|
||||
.prismatic-logo__specular,
|
||||
.prismatic-logo__reveal {
|
||||
position: absolute;
|
||||
display: block;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.prismatic-logo__base {
|
||||
filter:
|
||||
drop-shadow(0 8px 12px rgba(24, 8, 52, 0.3))
|
||||
drop-shadow(0 0 10px rgba(139, 92, 246, 0.22));
|
||||
object-fit: contain;
|
||||
padding: 7px;
|
||||
transform: translateZ(10px);
|
||||
}
|
||||
|
||||
.prismatic-logo__spectrum,
|
||||
.prismatic-logo__specular,
|
||||
.prismatic-logo__reveal {
|
||||
-webkit-mask: var(--logo-mask) center / calc(100% - 14px) calc(100% - 14px) no-repeat;
|
||||
mask: var(--logo-mask) center / calc(100% - 14px) calc(100% - 14px) no-repeat;
|
||||
}
|
||||
|
||||
.prismatic-logo__spectrum {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at var(--logo-light-x) var(--logo-light-y),
|
||||
rgba(255, 255, 255, 0.95),
|
||||
rgba(210, 188, 255, 0.66) 13%,
|
||||
transparent 36%
|
||||
),
|
||||
conic-gradient(
|
||||
from 218deg at var(--logo-light-x) var(--logo-light-y),
|
||||
rgba(255, 105, 210, 0.72),
|
||||
rgba(117, 212, 255, 0.72),
|
||||
rgba(177, 139, 255, 0.82),
|
||||
rgba(255, 214, 246, 0.7),
|
||||
rgba(255, 105, 210, 0.72)
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
opacity: calc(0.22 + var(--prism-intensity) * 0.58);
|
||||
transform: translateZ(14px);
|
||||
}
|
||||
|
||||
.prismatic-logo__specular {
|
||||
background: radial-gradient(
|
||||
ellipse 28% 20% at var(--logo-light-x) var(--logo-light-y),
|
||||
rgba(255, 255, 255, 0.96),
|
||||
rgba(232, 219, 255, 0.52) 28%,
|
||||
transparent 72%
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
opacity: calc(0.3 + var(--prism-intensity) * 0.66);
|
||||
transform: translateZ(18px);
|
||||
}
|
||||
|
||||
.prismatic-logo__reveal {
|
||||
background: linear-gradient(
|
||||
112deg,
|
||||
transparent 30%,
|
||||
rgba(255, 255, 255, 0.18) 39%,
|
||||
rgba(255, 255, 255, 0.98) 48%,
|
||||
rgba(125, 211, 252, 0.62) 54%,
|
||||
transparent 66%
|
||||
);
|
||||
background-position: 100% 50%;
|
||||
background-size: 300% 100%;
|
||||
mix-blend-mode: screen;
|
||||
opacity: 0;
|
||||
transform: translateZ(20px);
|
||||
}
|
||||
|
||||
.prismatic-logo--animated .prismatic-logo__reveal {
|
||||
animation: prismatic-logo-reveal 840ms cubic-bezier(0.2, 0.76, 0.18, 1) 160ms both;
|
||||
}
|
||||
|
||||
@keyframes prismatic-logo-enter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: perspective(520px) translateY(10px) scale(0.82) rotateX(-8deg) rotateY(10deg);
|
||||
}
|
||||
|
||||
62% {
|
||||
opacity: 1;
|
||||
transform: perspective(520px) translateY(-2px) scale(1.025) rotateX(1deg) rotateY(-1deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: perspective(520px) translateY(0) scale(1) rotateX(0) rotateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes prismatic-logo-reveal {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.prismatic-logo,
|
||||
.prismatic-logo__reveal {
|
||||
animation: none;
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.prismatic-logo__reveal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ import { useAuthStore, useUserStore } from '@/stores'
|
||||
import { authState, userState } from '@/stores/types'
|
||||
import api from '@/api'
|
||||
import router from '@/router'
|
||||
import MetalLogo3D from '@/components/misc/MetalLogo3D.vue'
|
||||
import OpticalLogoLab from '@/components/misc/OpticalLogoLab.vue'
|
||||
import { bufferToBase64Url, base64UrlToUint8Array, urlBase64ToUint8Array } from '@/@core/utils/navigator'
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
|
||||
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
|
||||
@@ -16,6 +16,42 @@ import { loadRemoteComponentFromModule, type RemoteModule } from '@/utils/federa
|
||||
|
||||
const LoginMfaDialog = defineAsyncComponent(() => import('@/components/dialog/LoginMfaDialog.vue'))
|
||||
|
||||
const loginRootRef = ref<HTMLElement | null>(null)
|
||||
let cardLightFrame: number | null = null
|
||||
let pendingCardLightX = 0.5
|
||||
let pendingCardLightY = 0
|
||||
let pendingCardLightEnergy = 0
|
||||
|
||||
/** 卡片顶部反射与指针共用光源位置,避免通过响应式状态触发页面重渲染。 */
|
||||
function renderCardLight() {
|
||||
cardLightFrame = null
|
||||
const root = loginRootRef.value
|
||||
root?.style.setProperty('--login-card-light-x', `${(pendingCardLightX * 100).toFixed(2)}%`)
|
||||
root?.style.setProperty('--login-card-light-y', `${(pendingCardLightY * 100).toFixed(2)}%`)
|
||||
root?.style.setProperty('--login-card-highlight-alpha', (0.035 + pendingCardLightEnergy * 0.08).toFixed(3))
|
||||
root?.style.setProperty('--login-card-primary-alpha', (0.018 + pendingCardLightEnergy * 0.04).toFixed(3))
|
||||
root?.style.setProperty('--login-card-top-prism-alpha', (0.2 + pendingCardLightEnergy * 0.32).toFixed(3))
|
||||
}
|
||||
|
||||
function handlePointerLight(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch') return
|
||||
const bounds = loginRootRef.value?.querySelector<HTMLElement>('.login-card')?.getBoundingClientRect()
|
||||
if (!bounds?.width || !bounds.height) return
|
||||
|
||||
pendingCardLightX = Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width))
|
||||
pendingCardLightY = Math.min(1, Math.max(0, (event.clientY - bounds.top) / bounds.height))
|
||||
const centerDistance = Math.hypot(pendingCardLightX - 0.5, pendingCardLightY - 0.5)
|
||||
pendingCardLightEnergy = Math.max(0.16, 1 - centerDistance * 1.2)
|
||||
if (cardLightFrame === null) cardLightFrame = window.requestAnimationFrame(renderCardLight)
|
||||
}
|
||||
|
||||
function resetPointerLight() {
|
||||
pendingCardLightX = 0.5
|
||||
pendingCardLightY = 0
|
||||
pendingCardLightEnergy = 0
|
||||
if (cardLightFrame === null) cardLightFrame = window.requestAnimationFrame(renderCardLight)
|
||||
}
|
||||
|
||||
// 国际化
|
||||
const { t, te } = useI18n()
|
||||
|
||||
@@ -487,16 +523,19 @@ async function subscribeForPushNotifications() {
|
||||
|
||||
// 登录后处理
|
||||
async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) {
|
||||
const originalPath = authStore.originalPath
|
||||
authStore.setOriginalPath(null)
|
||||
|
||||
// 如果需要显示设置向导,跳转到设置向导页面
|
||||
if (userPayload.wizard) {
|
||||
router.push('/setup-wizard')
|
||||
await router.push('/setup-wizard')
|
||||
} else {
|
||||
// 如果有原始路径,优先跳转到原始路径
|
||||
if (authStore.originalPath && authStore.originalPath !== '/') {
|
||||
router.push(authStore.originalPath)
|
||||
// 原始目标是一次性状态,持久化的旧登录页目标不得重新进入认证流程。
|
||||
if (originalPath && originalPath !== '/' && router.resolve(originalPath).path !== '/login') {
|
||||
await router.push(originalPath)
|
||||
} else {
|
||||
// 跳转到第一个有权限的菜单
|
||||
router.push(filteredMenus[0].to)
|
||||
await router.push(filteredMenus[0].to)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,6 +714,8 @@ async function initConditionalPasskey() {
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (cardLightFrame !== null) window.cancelAnimationFrame(cardLightFrame)
|
||||
|
||||
if (conditionalAbortController) {
|
||||
conditionalAbortController.abort()
|
||||
conditionalAbortController = null
|
||||
@@ -688,7 +729,12 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<!-- 登录页面容器 -->
|
||||
<div class="login-root">
|
||||
<div
|
||||
ref="loginRootRef"
|
||||
class="login-root"
|
||||
@pointermove="handlePointerLight"
|
||||
@pointerleave="resetPointerLight"
|
||||
>
|
||||
<!-- 装饰性背景光晕 -->
|
||||
<div class="login-bg-decor" aria-hidden="true">
|
||||
<div class="login-orb login-orb--1" />
|
||||
@@ -726,17 +772,20 @@ onUnmounted(() => {
|
||||
<!-- 登录表单 -->
|
||||
<div v-if="!mfaDialog" class="auth-wrapper d-flex align-center justify-center">
|
||||
<VCard
|
||||
class="auth-card login-card glass-effect pa-7 pa-sm-9 w-full h-full login-card--enter"
|
||||
class="auth-card login-card glass-effect no-blur pa-7 pa-sm-9 w-full h-full login-card--enter"
|
||||
max-width="24rem"
|
||||
flat
|
||||
>
|
||||
<div class="login-card__glass" aria-hidden="true">
|
||||
<span class="login-card__glass-caustic" />
|
||||
</div>
|
||||
|
||||
<!-- 卡片头部:Logo + 标题 + 欢迎语 -->
|
||||
<div class="login-head">
|
||||
<div class="login-logo-wrapper">
|
||||
<MetalLogo3D class="login-logo" />
|
||||
</div>
|
||||
<h1 class="login-title">MoviePilot</h1>
|
||||
<p class="login-subtitle">{{ t('login.welcomeBack') || 'Welcome Back' }}</p>
|
||||
<OpticalLogoLab class="login-logo" :locale="currentLocale">
|
||||
<h1 class="login-title">MoviePilot</h1>
|
||||
<p class="login-subtitle">{{ t('login.welcomeBack') || 'Welcome Back' }}</p>
|
||||
</OpticalLogoLab>
|
||||
</div>
|
||||
|
||||
<VCardText class="login-body">
|
||||
@@ -897,15 +946,38 @@ onUnmounted(() => {
|
||||
|
||||
/* ===================== 布局根容器 ===================== */
|
||||
.login-root {
|
||||
--login-card-light-x: 50%;
|
||||
--login-card-light-y: 0%;
|
||||
--login-card-highlight-alpha: 0.035;
|
||||
--login-card-primary-alpha: 0.018;
|
||||
--login-card-top-prism-alpha: 0.2;
|
||||
--optical-glass-x: 50%;
|
||||
--optical-glass-y: 22%;
|
||||
--optical-glass-blur: 24px;
|
||||
--optical-glass-saturate: 146%;
|
||||
--optical-glass-contrast: 104%;
|
||||
--optical-glass-glow-opacity: 0.36;
|
||||
--optical-glass-caustic-opacity: 0.28;
|
||||
--optical-glass-caustic-scale: 0.9;
|
||||
--optical-glass-pulse-blur: 14px;
|
||||
--optical-glass-shift-x: 0px;
|
||||
--optical-glass-shift-y: 0px;
|
||||
--optical-glass-rotation: -5deg;
|
||||
--optical-glass-scale: 1;
|
||||
--optical-glass-caustic-highlight-alpha: 0.16;
|
||||
--optical-glass-caustic-primary-alpha: 0.12;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
isolation: isolate;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
padding-block: calc(env(safe-area-inset-top, 0px) + 24px) calc(env(safe-area-inset-bottom, 0px) + 24px);
|
||||
}
|
||||
|
||||
/* ===================== 装饰性背景光晕 ===================== */
|
||||
@@ -1006,7 +1078,7 @@ onUnmounted(() => {
|
||||
.auth-wrapper {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
block-size: auto;
|
||||
inline-size: 100%;
|
||||
padding-inline: 16px;
|
||||
@@ -1016,43 +1088,115 @@ onUnmounted(() => {
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border: none !important;
|
||||
border-radius: var(--app-surface-radius, 20px) !important;
|
||||
box-shadow:
|
||||
0 20px 50px rgba(var(--app-shadow-rgb, 0, 0, 0), 0.12),
|
||||
0 8px 20px rgba(var(--app-shadow-rgb, 0, 0, 0), 0.06),
|
||||
0 0 0 1px rgba(var(--v-theme-primary), 0.04) !important;
|
||||
transition: box-shadow 300ms ease;
|
||||
box-shadow: 0 20px 54px rgba(var(--app-shadow-rgb, 0, 0, 0), 0.12) !important;
|
||||
|
||||
> :not(.login-card__glass) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 顶部高光线,营造立体感 */
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, transparent 10%, rgba(255, 255, 255, 35%) 50%, transparent 90%);
|
||||
z-index: 4;
|
||||
border-radius: 999px;
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(255, 255, 255, 0.92),
|
||||
rgba(232, 219, 255, 0.48) 28%,
|
||||
transparent 72%
|
||||
),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(117, 212, 255, 0.16) 30%,
|
||||
rgba(177, 139, 255, 0.22) 50%,
|
||||
rgba(255, 105, 210, 0.12) 70%,
|
||||
transparent
|
||||
);
|
||||
block-size: 1px;
|
||||
content: '';
|
||||
filter: drop-shadow(0 1px 3px rgba(var(--v-theme-primary), 0.16));
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
inset-inline-start: clamp(44px, var(--login-card-light-x), calc(100% - 44px));
|
||||
inline-size: 88px;
|
||||
mix-blend-mode: screen;
|
||||
opacity: var(--login-card-top-prism-alpha);
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录卡片自身承载固定磨砂效果,避免跟随透明主题设置变化。 */
|
||||
/* 登录卡片拥有独立光学表面,不跟随透明主题的全局模糊开关。 */
|
||||
.glass-effect {
|
||||
backdrop-filter: blur(28px) saturate(170%) !important;
|
||||
background: rgba(var(--v-theme-surface), 0.75) !important;
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 深色主题上叠一条更亮的描边,区分背景 */
|
||||
:deep(.v-theme--dark) .login-card,
|
||||
:deep(.v-theme--purple) .login-card,
|
||||
:deep(.v-theme--transparent) .login-card {
|
||||
border: 1px solid rgba(255, 255, 255, 8%) !important;
|
||||
.login-card__glass {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
backdrop-filter: blur(var(--optical-glass-blur)) saturate(var(--optical-glass-saturate))
|
||||
contrast(var(--optical-glass-contrast));
|
||||
background:
|
||||
radial-gradient(
|
||||
180px 150px at var(--login-card-light-x) var(--login-card-light-y),
|
||||
rgba(255, 255, 255, var(--login-card-highlight-alpha)),
|
||||
rgba(var(--v-theme-primary), var(--login-card-primary-alpha)) 44%,
|
||||
transparent 76%
|
||||
),
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.075), transparent 34%),
|
||||
rgba(var(--v-theme-surface), 0.7);
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
:deep(.v-theme--light) .login-card {
|
||||
border: 1px solid rgba(255, 255, 255, 65%) !important;
|
||||
.login-card__glass::before {
|
||||
position: absolute;
|
||||
border-radius: 44% 56% 61% 39% / 42% 39% 61% 58%;
|
||||
background:
|
||||
radial-gradient(circle at 36% 32%, rgba(255, 255, 255, 0.2), transparent 24%),
|
||||
conic-gradient(
|
||||
from 218deg,
|
||||
transparent,
|
||||
rgba(var(--v-theme-primary), 0.13),
|
||||
transparent 38%,
|
||||
rgba(255, 255, 255, 0.09),
|
||||
transparent 72%
|
||||
);
|
||||
content: '';
|
||||
filter: blur(var(--optical-glass-pulse-blur));
|
||||
inset: -28%;
|
||||
opacity: var(--optical-glass-glow-opacity);
|
||||
transform: translate(var(--optical-glass-shift-x), var(--optical-glass-shift-y))
|
||||
rotate(var(--optical-glass-rotation)) scale(var(--optical-glass-scale));
|
||||
transition: opacity 220ms ease;
|
||||
}
|
||||
|
||||
.login-card__glass-caustic {
|
||||
position: absolute;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
ellipse,
|
||||
rgba(255, 255, 255, var(--optical-glass-caustic-highlight-alpha)),
|
||||
rgba(var(--v-theme-primary), var(--optical-glass-caustic-primary-alpha)) 34%,
|
||||
transparent 72%
|
||||
);
|
||||
filter: blur(var(--optical-glass-pulse-blur));
|
||||
inset-block-start: calc(var(--optical-glass-y) - 88px);
|
||||
inset-inline-start: calc(var(--optical-glass-x) - 112px);
|
||||
block-size: 176px;
|
||||
inline-size: 224px;
|
||||
opacity: var(--optical-glass-caustic-opacity);
|
||||
pointer-events: none;
|
||||
transform: scale(var(--optical-glass-caustic-scale));
|
||||
}
|
||||
|
||||
/* ===================== 卡片头部 ===================== */
|
||||
@@ -1060,22 +1204,13 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
inline-size: 100%;
|
||||
margin-block-end: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-logo-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: logo-enter 700ms cubic-bezier(0.16, 1, 0.3, 1) 100ms both;
|
||||
margin-block-end: 8px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
flex: none;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
@@ -1089,6 +1224,8 @@ onUnmounted(() => {
|
||||
line-height: 1.2;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
@@ -1104,6 +1241,7 @@ onUnmounted(() => {
|
||||
|
||||
/* ===================== 卡片主体 ===================== */
|
||||
.login-body {
|
||||
animation: text-enter 600ms cubic-bezier(0.16, 1, 0.3, 1) 420ms both;
|
||||
padding-block: 8px !important;
|
||||
}
|
||||
|
||||
@@ -1111,14 +1249,17 @@ onUnmounted(() => {
|
||||
.native-login-field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(var(--v-border-color), 0.38);
|
||||
min-block-size: 56px;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
background: rgba(var(--v-theme-surface), 0.13);
|
||||
backdrop-filter: blur(10px) saturate(118%);
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
box-shadow 150ms ease;
|
||||
box-shadow 150ms ease,
|
||||
background 220ms ease;
|
||||
}
|
||||
|
||||
.native-login-field:focus-within {
|
||||
@@ -1136,6 +1277,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.native-login-field__input {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
border: 0;
|
||||
appearance: none;
|
||||
@@ -1385,6 +1528,7 @@ onUnmounted(() => {
|
||||
letter-spacing: 0.03em;
|
||||
margin-block-start: 14px;
|
||||
opacity: 0.75;
|
||||
animation: text-enter 600ms cubic-bezier(0.16, 1, 0.3, 1) 520ms both;
|
||||
}
|
||||
|
||||
.login-version {
|
||||
@@ -1399,30 +1543,16 @@ onUnmounted(() => {
|
||||
|
||||
@keyframes login-enter {
|
||||
0% {
|
||||
filter: blur(4px);
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.97);
|
||||
transform: translateY(12px) scale(0.985);
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes logo-enter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes text-enter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -1438,10 +1568,11 @@ onUnmounted(() => {
|
||||
/* ===================== 无障碍:尊重减少动态偏好 ===================== */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.login-card--enter,
|
||||
.login-logo-wrapper,
|
||||
.login-body,
|
||||
.login-foot,
|
||||
.login-title,
|
||||
.login-subtitle {
|
||||
animation-duration: 1ms !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
@@ -1455,6 +1586,43 @@ onUnmounted(() => {
|
||||
.login-orb {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.login-card__glass::before,
|
||||
.login-card__glass-caustic {
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.login-card__glass,
|
||||
.native-login-field {
|
||||
backdrop-filter: none !important;
|
||||
background: rgb(var(--v-theme-surface)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
.login-card__glass {
|
||||
background: rgba(var(--v-theme-surface), 0.94);
|
||||
}
|
||||
|
||||
.native-login-field {
|
||||
border-color: rgba(var(--v-theme-on-surface), 0.68);
|
||||
}
|
||||
}
|
||||
|
||||
@supports not ((backdrop-filter: blur(1px))) {
|
||||
.login-card__glass,
|
||||
.native-login-field {
|
||||
background: rgba(var(--v-theme-surface), 0.96) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (height <= 760px) {
|
||||
.login-root {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== 小屏适配 ===================== */
|
||||
|
||||
@@ -318,8 +318,8 @@ router.beforeEach(async (to: any, from: any, next: any) => {
|
||||
|
||||
// 认证 Store
|
||||
const authStore = useAuthStore()
|
||||
// 总是记录非login路由
|
||||
if (to.fullPath != '/login') authStore.originalPath = to.fullPath
|
||||
// 登录页的实验参数不是登录后可恢复的业务目标。
|
||||
if (to.path !== '/login') authStore.originalPath = to.fullPath
|
||||
const isAuthenticated = authStore.token !== null
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
|
||||
@@ -5364,6 +5364,11 @@ gridstack@^12.6.0:
|
||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.6.0.tgz#acfd8c036b202304712c0562078c86ed2ab6e83f"
|
||||
integrity sha512-dUrqsormSybFn/2P4Dz8AgprftKD5e/IiV7UmC0XLQU+G+/WtkAeFiCSNLoAGhPDXoJ/O61Xtj3gljY/Ds83yQ==
|
||||
|
||||
gsap@^3.15.0:
|
||||
version "3.15.0"
|
||||
resolved "https://registry.yarnpkg.com/gsap/-/gsap-3.15.0.tgz#7851baaffc77642f2db3b1749d3634f9b5a19d14"
|
||||
integrity sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==
|
||||
|
||||
has-bigints@^1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user