From 4378c4843b9c19d1d1eac38e5e9158276cde12be Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:33:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E4=BA=A4=E4=BA=92=E5=BC=8F=203D=20Logo=20(#536)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- package.json | 1 + src/components/misc/OpticalLogoLab.vue | 3538 ++++++++++++++++++++++++ src/components/misc/PrismaticLogo.vue | 247 ++ src/pages/login.vue | 298 +- src/router/index.ts | 4 +- yarn.lock | 5 + 6 files changed, 4026 insertions(+), 67 deletions(-) create mode 100644 src/components/misc/OpticalLogoLab.vue create mode 100644 src/components/misc/PrismaticLogo.vue diff --git a/package.json b/package.json index cf546b8e..2f8ccf40 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/misc/OpticalLogoLab.vue b/src/components/misc/OpticalLogoLab.vue new file mode 100644 index 00000000..1afae0fa --- /dev/null +++ b/src/components/misc/OpticalLogoLab.vue @@ -0,0 +1,3538 @@ + + + + + diff --git a/src/components/misc/PrismaticLogo.vue b/src/components/misc/PrismaticLogo.vue new file mode 100644 index 00000000..44f25b76 --- /dev/null +++ b/src/components/misc/PrismaticLogo.vue @@ -0,0 +1,247 @@ + + + + + diff --git a/src/pages/login.vue b/src/pages/login.vue index f237770a..a7f7d8ea 100644 --- a/src/pages/login.vue +++ b/src/pages/login.vue @@ -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(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('.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(() => {