diff --git a/src/App.tsx b/src/App.tsx index 54f3770..548cfcc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,33 +1,21 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { HashRouter, Route, Routes } from 'react-router-dom'; import { LoginPage } from '@/pages/LoginPage'; import { NotificationContainer } from '@/components/common/NotificationContainer'; import { ConfirmationModal } from '@/components/common/ConfirmationModal'; -import { SplashScreen } from '@/components/common/SplashScreen'; import { MainLayout } from '@/components/layout/MainLayout'; import { ProtectedRoute } from '@/router/ProtectedRoute'; -import { useAuthStore, useLanguageStore, useThemeStore } from '@/stores'; - -const SPLASH_DURATION = 1500; -const SPLASH_FADE_DURATION = 400; +import { useLanguageStore, useThemeStore } from '@/stores'; function App() { const initializeTheme = useThemeStore((state) => state.initializeTheme); const language = useLanguageStore((state) => state.language); const setLanguage = useLanguageStore((state) => state.setLanguage); - const restoreSession = useAuthStore((state) => state.restoreSession); - - const [splashReadyToFade, setSplashReadyToFade] = useState(false); - const [showSplash, setShowSplash] = useState(true); - const [authReady, setAuthReady] = useState(false); useEffect(() => { const cleanupTheme = initializeTheme(); - void restoreSession().finally(() => { - setAuthReady(true); - }); return cleanupTheme; - }, [initializeTheme, restoreSession]); + }, [initializeTheme]); useEffect(() => { setLanguage(language); @@ -38,27 +26,6 @@ function App() { document.documentElement.lang = language; }, [language]); - useEffect(() => { - const timer = setTimeout(() => { - setSplashReadyToFade(true); - }, SPLASH_DURATION - SPLASH_FADE_DURATION); - - return () => clearTimeout(timer); - }, []); - - const handleSplashFinish = useCallback(() => { - setShowSplash(false); - }, []); - - if (showSplash) { - return ( - - ); - } - return ( diff --git a/src/pages/LoginPage.module.scss b/src/pages/LoginPage.module.scss index 4203ca0..ece7621 100644 --- a/src/pages/LoginPage.module.scss +++ b/src/pages/LoginPage.module.scss @@ -37,9 +37,21 @@ gap: 0; } +// 品牌大字淡入动画 +@keyframes brandFadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: var(--target-opacity, 0.9); + transform: translateY(0); + } +} + // 品牌大字 .brandWord { - font-size: 13vw; + font-size: 14vw; font-weight: 900; color: rgba(255, 255, 255, 0.9); letter-spacing: -0.02em; @@ -48,18 +60,23 @@ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; text-align: right; padding-right: 0; + opacity: 0; + animation: brandFadeIn 0.8s ease-out forwards; - // 不同字有不同的透明度形成层次感 + // 不同字有不同的透明度和延迟,从上到下依次显现 &:nth-child(1) { - color: rgba(255, 255, 255, 0.95); + --target-opacity: 0.95; + animation-delay: 0.1s; } &:nth-child(2) { - color: rgba(255, 255, 255, 0.7); + --target-opacity: 0.7; + animation-delay: 0.35s; } &:nth-child(3) { - color: rgba(255, 255, 255, 0.45); + --target-opacity: 0.45; + animation-delay: 0.6s; } } @@ -94,7 +111,7 @@ .logo { width: 80px; height: 80px; - border-radius: $radius-full; + border-radius: $radius-lg; object-fit: cover; box-shadow: var(--shadow-lg); border: 3px solid var(--border-color); @@ -211,24 +228,101 @@ font-size: 14px; } -// 自动登录提示 -.autoLoginBox { - background: var(--bg-secondary); - border: 1px dashed var(--border-color); - border-radius: $radius-md; - padding: $spacing-md; - display: flex; - flex-direction: column; - gap: $spacing-xs; - text-align: center; +// ========== 启动动画(右侧) ========== - .label { - color: var(--text-secondary); - font-size: 14px; +// 启动动画进入效果 +@keyframes splashEnter { + from { + opacity: 0; + transform: scale(0.9) translateY(20px); } - - .value { - font-weight: 600; - color: var(--text-primary); + to { + opacity: 1; + transform: scale(1) translateY(0); } } + +// Logo 脉冲效果 +@keyframes splashLogoPulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +// 加载条动画 +@keyframes splashLoading { + 0% { + transform: scaleX(0); + transform-origin: left; + } + 50% { + transform: scaleX(1); + transform-origin: left; + } + 50.01% { + transform-origin: right; + } + 100% { + transform: scaleX(0); + transform-origin: right; + } +} + +// 启动动画内容容器 +.splashContent { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-md; + animation: splashEnter 0.6s ease-out; +} + +// 启动动画 Logo +.splashLogo { + height: 80px; + width: auto; + border-radius: $radius-lg; + box-shadow: $shadow-lg; + animation: splashLogoPulse 1.5s ease-in-out infinite; +} + +// 启动动画标题 +.splashTitle { + font-size: 28px; + font-weight: 800; + color: var(--text-primary); + margin: 0; + letter-spacing: -0.5px; +} + +// 启动动画副标题 +.splashSubtitle { + font-size: 16px; + font-weight: 500; + color: var(--text-secondary); + margin: 0; + margin-top: -8px; +} + +// 启动动画加载条容器 +.splashLoader { + width: 120px; + height: 3px; + background: var(--border-color); + border-radius: $radius-full; + overflow: hidden; + margin-top: $spacing-md; +} + +// 启动动画加载条 +.splashLoaderBar { + width: 100%; + height: 100%; + background: var(--primary-color); + border-radius: $radius-full; + animation: splashLoading 1.2s ease-in-out infinite; +} diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 0022079..1f16ab0 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -30,6 +30,7 @@ export function LoginPage() { const [rememberPassword, setRememberPassword] = useState(false); const [loading, setLoading] = useState(false); const [autoLoading, setAutoLoading] = useState(true); + const [autoLoginSuccess, setAutoLoginSuccess] = useState(false); const [error, setError] = useState(''); const detectedBase = useMemo(() => detectApiBaseFromLocation(), []); @@ -39,18 +40,28 @@ export function LoginPage() { const init = async () => { try { const autoLoggedIn = await restoreSession(); - if (!autoLoggedIn) { + if (autoLoggedIn) { + setAutoLoginSuccess(true); + // 延迟跳转,让用户看到成功动画 + setTimeout(() => { + const redirect = (location.state as any)?.from?.pathname || '/'; + navigate(redirect, { replace: true }); + }, 1500); + } else { setApiBase(storedBase || detectedBase); setManagementKey(storedKey || ''); setRememberPassword(storedRememberPassword || Boolean(storedKey)); } } finally { - setAutoLoading(false); + if (!autoLoginSuccess) { + setAutoLoading(false); + } } }; init(); - }, [detectedBase, restoreSession, storedBase, storedKey, storedRememberPassword]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const handleSubmit = useCallback(async () => { if (!managementKey.trim()) { @@ -88,11 +99,14 @@ export function LoginPage() { [loading, handleSubmit] ); - if (isAuthenticated) { + if (isAuthenticated && !autoLoading && !autoLoginSuccess) { const redirect = (location.state as any)?.from?.pathname || '/'; return ; } + // 显示启动动画(自动登录中或自动登录成功) + const showSplash = autoLoading || autoLoginSuccess; + return (
{/* 左侧品牌展示区 */} @@ -106,109 +120,115 @@ export function LoginPage() { {/* 右侧功能交互区 */}
-
- {/* Logo */} - Logo - - {/* 登录表单卡片 */} -
-
-
-
{t('title.login')}
- -
-
{t('login.subtitle')}
+ {showSplash ? ( + /* 启动动画 */ +
+ CPAMC +

CLI Proxy API

+

Management Center

+
+
- -
-
{t('login.connection_current')}
-
{apiBase || detectedBase}
-
{t('login.connection_auto_hint')}
-
- -
- setShowCustomBase(e.target.checked)} - /> - -
- - {showCustomBase && ( - setApiBase(e.target.value)} - hint={t('login.custom_connection_hint')} - /> - )} - - setManagementKey(e.target.value)} - onKeyDown={handleSubmitKeyDown} - rightElement={ - - } - /> - -
- setRememberPassword(e.target.checked)} - /> - -
- - - - {error &&
{error}
} - - {autoLoading && ( -
-
{t('auto_login.title')}
-
{t('auto_login.message')}
-
- )}
-
+ ) : ( + /* 登录表单 */ +
+ {/* Logo */} + Logo + + {/* 登录表单卡片 */} +
+
+
+
{t('title.login')}
+ +
+
{t('login.subtitle')}
+
+ +
+
{t('login.connection_current')}
+
{apiBase || detectedBase}
+
{t('login.connection_auto_hint')}
+
+ +
+ setShowCustomBase(e.target.checked)} + /> + +
+ + {showCustomBase && ( + setApiBase(e.target.value)} + hint={t('login.custom_connection_hint')} + /> + )} + + setManagementKey(e.target.value)} + onKeyDown={handleSubmitKeyDown} + rightElement={ + + } + /> + +
+ setRememberPassword(e.target.checked)} + /> + +
+ + + + {error &&
{error}
} +
+
+ )}
);