mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +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": "^4.21.2",
|
||||||
"express-http-proxy": "^2.1.1",
|
"express-http-proxy": "^2.1.1",
|
||||||
"gridstack": "^12.6.0",
|
"gridstack": "^12.6.0",
|
||||||
|
"gsap": "^3.15.0",
|
||||||
"http-proxy-middleware": "^3.0.0",
|
"http-proxy-middleware": "^3.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
+233
-65
@@ -4,7 +4,7 @@ import { useAuthStore, useUserStore } from '@/stores'
|
|||||||
import { authState, userState } from '@/stores/types'
|
import { authState, userState } from '@/stores/types'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import router from '@/router'
|
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 { bufferToBase64Url, base64UrlToUint8Array, urlBase64ToUint8Array } from '@/@core/utils/navigator'
|
||||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
|
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
|
||||||
import { getCurrentLocale, setI18nLanguage } from '@/plugins/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 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()
|
const { t, te } = useI18n()
|
||||||
|
|
||||||
@@ -487,16 +523,19 @@ async function subscribeForPushNotifications() {
|
|||||||
|
|
||||||
// 登录后处理
|
// 登录后处理
|
||||||
async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) {
|
async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) {
|
||||||
|
const originalPath = authStore.originalPath
|
||||||
|
authStore.setOriginalPath(null)
|
||||||
|
|
||||||
// 如果需要显示设置向导,跳转到设置向导页面
|
// 如果需要显示设置向导,跳转到设置向导页面
|
||||||
if (userPayload.wizard) {
|
if (userPayload.wizard) {
|
||||||
router.push('/setup-wizard')
|
await router.push('/setup-wizard')
|
||||||
} else {
|
} else {
|
||||||
// 如果有原始路径,优先跳转到原始路径
|
// 原始目标是一次性状态,持久化的旧登录页目标不得重新进入认证流程。
|
||||||
if (authStore.originalPath && authStore.originalPath !== '/') {
|
if (originalPath && originalPath !== '/' && router.resolve(originalPath).path !== '/login') {
|
||||||
router.push(authStore.originalPath)
|
await router.push(originalPath)
|
||||||
} else {
|
} else {
|
||||||
// 跳转到第一个有权限的菜单
|
// 跳转到第一个有权限的菜单
|
||||||
router.push(filteredMenus[0].to)
|
await router.push(filteredMenus[0].to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,6 +714,8 @@ async function initConditionalPasskey() {
|
|||||||
|
|
||||||
// 组件卸载时清理
|
// 组件卸载时清理
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
if (cardLightFrame !== null) window.cancelAnimationFrame(cardLightFrame)
|
||||||
|
|
||||||
if (conditionalAbortController) {
|
if (conditionalAbortController) {
|
||||||
conditionalAbortController.abort()
|
conditionalAbortController.abort()
|
||||||
conditionalAbortController = null
|
conditionalAbortController = null
|
||||||
@@ -688,7 +729,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<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-bg-decor" aria-hidden="true">
|
||||||
<div class="login-orb login-orb--1" />
|
<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">
|
<div v-if="!mfaDialog" class="auth-wrapper d-flex align-center justify-center">
|
||||||
<VCard
|
<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"
|
max-width="24rem"
|
||||||
flat
|
flat
|
||||||
>
|
>
|
||||||
|
<div class="login-card__glass" aria-hidden="true">
|
||||||
|
<span class="login-card__glass-caustic" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 卡片头部:Logo + 标题 + 欢迎语 -->
|
<!-- 卡片头部:Logo + 标题 + 欢迎语 -->
|
||||||
<div class="login-head">
|
<div class="login-head">
|
||||||
<div class="login-logo-wrapper">
|
<OpticalLogoLab class="login-logo" :locale="currentLocale">
|
||||||
<MetalLogo3D class="login-logo" />
|
<h1 class="login-title">MoviePilot</h1>
|
||||||
</div>
|
<p class="login-subtitle">{{ t('login.welcomeBack') || 'Welcome Back' }}</p>
|
||||||
<h1 class="login-title">MoviePilot</h1>
|
</OpticalLogoLab>
|
||||||
<p class="login-subtitle">{{ t('login.welcomeBack') || 'Welcome Back' }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VCardText class="login-body">
|
<VCardText class="login-body">
|
||||||
@@ -897,15 +946,38 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* ===================== 布局根容器 ===================== */
|
/* ===================== 布局根容器 ===================== */
|
||||||
.login-root {
|
.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;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
min-block-size: 100vh;
|
min-block-size: 100vh;
|
||||||
min-block-size: 100dvh;
|
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 {
|
.auth-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
block-size: auto;
|
block-size: auto;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
@@ -1016,43 +1088,115 @@ onUnmounted(() => {
|
|||||||
.login-card {
|
.login-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: var(--app-surface-radius, 20px) !important;
|
border-radius: var(--app-surface-radius, 20px) !important;
|
||||||
box-shadow:
|
box-shadow: 0 20px 54px rgba(var(--app-shadow-rgb, 0, 0, 0), 0.12) !important;
|
||||||
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),
|
> :not(.login-card__glass) {
|
||||||
0 0 0 1px rgba(var(--v-theme-primary), 0.04) !important;
|
position: relative;
|
||||||
transition: box-shadow 300ms ease;
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
/* 顶部高光线,营造立体感 */
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 4;
|
||||||
border-radius: inherit;
|
border-radius: 999px;
|
||||||
background: linear-gradient(90deg, transparent 10%, rgba(255, 255, 255, 35%) 50%, transparent 90%);
|
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;
|
block-size: 1px;
|
||||||
content: '';
|
content: '';
|
||||||
|
filter: drop-shadow(0 1px 3px rgba(var(--v-theme-primary), 0.16));
|
||||||
inset-block-start: 0;
|
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;
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录卡片自身承载固定磨砂效果,避免跟随透明主题设置变化。 */
|
/* 登录卡片拥有独立光学表面,不跟随透明主题的全局模糊开关。 */
|
||||||
.glass-effect {
|
.glass-effect {
|
||||||
backdrop-filter: blur(28px) saturate(170%) !important;
|
backdrop-filter: none !important;
|
||||||
background: rgba(var(--v-theme-surface), 0.75) !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 深色主题上叠一条更亮的描边,区分背景 */
|
.login-card__glass {
|
||||||
:deep(.v-theme--dark) .login-card,
|
position: absolute;
|
||||||
:deep(.v-theme--purple) .login-card,
|
z-index: 0;
|
||||||
:deep(.v-theme--transparent) .login-card {
|
overflow: hidden;
|
||||||
border: 1px solid rgba(255, 255, 255, 8%) !important;
|
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 {
|
.login-card__glass::before {
|
||||||
border: 1px solid rgba(255, 255, 255, 65%) !important;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
inline-size: 100%;
|
||||||
margin-block-end: 12px;
|
margin-block-end: 12px;
|
||||||
text-align: center;
|
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 {
|
.login-logo {
|
||||||
flex: none;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.login-title {
|
||||||
@@ -1089,6 +1224,8 @@ onUnmounted(() => {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-subtitle {
|
.login-subtitle {
|
||||||
@@ -1104,6 +1241,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* ===================== 卡片主体 ===================== */
|
/* ===================== 卡片主体 ===================== */
|
||||||
.login-body {
|
.login-body {
|
||||||
|
animation: text-enter 600ms cubic-bezier(0.16, 1, 0.3, 1) 420ms both;
|
||||||
padding-block: 8px !important;
|
padding-block: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,14 +1249,17 @@ onUnmounted(() => {
|
|||||||
.native-login-field {
|
.native-login-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid rgba(var(--v-border-color), 0.38);
|
border: 1px solid rgba(var(--v-border-color), 0.38);
|
||||||
min-block-size: 56px;
|
min-block-size: 56px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: transparent;
|
background: rgba(var(--v-theme-surface), 0.13);
|
||||||
|
backdrop-filter: blur(10px) saturate(118%);
|
||||||
transition:
|
transition:
|
||||||
border-color 150ms ease,
|
border-color 150ms ease,
|
||||||
box-shadow 150ms ease;
|
box-shadow 150ms ease,
|
||||||
|
background 220ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.native-login-field:focus-within {
|
.native-login-field:focus-within {
|
||||||
@@ -1136,6 +1277,8 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.native-login-field__input {
|
.native-login-field__input {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
display: block;
|
display: block;
|
||||||
border: 0;
|
border: 0;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
@@ -1385,6 +1528,7 @@ onUnmounted(() => {
|
|||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
margin-block-start: 14px;
|
margin-block-start: 14px;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
animation: text-enter 600ms cubic-bezier(0.16, 1, 0.3, 1) 520ms both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-version {
|
.login-version {
|
||||||
@@ -1399,30 +1543,16 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
@keyframes login-enter {
|
@keyframes login-enter {
|
||||||
0% {
|
0% {
|
||||||
filter: blur(4px);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px) scale(0.97);
|
transform: translateY(12px) scale(0.985);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
filter: blur(0);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0) scale(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 {
|
@keyframes text-enter {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -1438,10 +1568,11 @@ onUnmounted(() => {
|
|||||||
/* ===================== 无障碍:尊重减少动态偏好 ===================== */
|
/* ===================== 无障碍:尊重减少动态偏好 ===================== */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.login-card--enter,
|
.login-card--enter,
|
||||||
.login-logo-wrapper,
|
.login-body,
|
||||||
|
.login-foot,
|
||||||
.login-title,
|
.login-title,
|
||||||
.login-subtitle {
|
.login-subtitle {
|
||||||
animation-duration: 1ms !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-submit {
|
.login-submit {
|
||||||
@@ -1455,6 +1586,43 @@ onUnmounted(() => {
|
|||||||
.login-orb {
|
.login-orb {
|
||||||
animation: none !important;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== 小屏适配 ===================== */
|
/* ===================== 小屏适配 ===================== */
|
||||||
|
|||||||
+2
-2
@@ -318,8 +318,8 @@ router.beforeEach(async (to: any, from: any, next: any) => {
|
|||||||
|
|
||||||
// 认证 Store
|
// 认证 Store
|
||||||
const authStore = useAuthStore()
|
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
|
const isAuthenticated = authStore.token !== null
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
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"
|
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.6.0.tgz#acfd8c036b202304712c0562078c86ed2ab6e83f"
|
||||||
integrity sha512-dUrqsormSybFn/2P4Dz8AgprftKD5e/IiV7UmC0XLQU+G+/WtkAeFiCSNLoAGhPDXoJ/O61Xtj3gljY/Ds83yQ==
|
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:
|
has-bigints@^1.0.2:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user