feat(login): add auto-login splash UI and simplify app startup

This commit is contained in:
Supra4E8C
2026-01-31 15:21:13 +08:00
parent c93030370e
commit 70968bbc4c
3 changed files with 245 additions and 164 deletions

View File

@@ -1,33 +1,21 @@
import { useCallback, useEffect, useState } from 'react'; import { useEffect } from 'react';
import { HashRouter, Route, Routes } from 'react-router-dom'; import { HashRouter, Route, Routes } from 'react-router-dom';
import { LoginPage } from '@/pages/LoginPage'; import { LoginPage } from '@/pages/LoginPage';
import { NotificationContainer } from '@/components/common/NotificationContainer'; import { NotificationContainer } from '@/components/common/NotificationContainer';
import { ConfirmationModal } from '@/components/common/ConfirmationModal'; import { ConfirmationModal } from '@/components/common/ConfirmationModal';
import { SplashScreen } from '@/components/common/SplashScreen';
import { MainLayout } from '@/components/layout/MainLayout'; import { MainLayout } from '@/components/layout/MainLayout';
import { ProtectedRoute } from '@/router/ProtectedRoute'; import { ProtectedRoute } from '@/router/ProtectedRoute';
import { useAuthStore, useLanguageStore, useThemeStore } from '@/stores'; import { useLanguageStore, useThemeStore } from '@/stores';
const SPLASH_DURATION = 1500;
const SPLASH_FADE_DURATION = 400;
function App() { function App() {
const initializeTheme = useThemeStore((state) => state.initializeTheme); const initializeTheme = useThemeStore((state) => state.initializeTheme);
const language = useLanguageStore((state) => state.language); const language = useLanguageStore((state) => state.language);
const setLanguage = useLanguageStore((state) => state.setLanguage); 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(() => { useEffect(() => {
const cleanupTheme = initializeTheme(); const cleanupTheme = initializeTheme();
void restoreSession().finally(() => {
setAuthReady(true);
});
return cleanupTheme; return cleanupTheme;
}, [initializeTheme, restoreSession]); }, [initializeTheme]);
useEffect(() => { useEffect(() => {
setLanguage(language); setLanguage(language);
@@ -38,27 +26,6 @@ function App() {
document.documentElement.lang = language; document.documentElement.lang = language;
}, [language]); }, [language]);
useEffect(() => {
const timer = setTimeout(() => {
setSplashReadyToFade(true);
}, SPLASH_DURATION - SPLASH_FADE_DURATION);
return () => clearTimeout(timer);
}, []);
const handleSplashFinish = useCallback(() => {
setShowSplash(false);
}, []);
if (showSplash) {
return (
<SplashScreen
fadeOut={splashReadyToFade && authReady}
onFinish={handleSplashFinish}
/>
);
}
return ( return (
<HashRouter> <HashRouter>
<NotificationContainer /> <NotificationContainer />

View File

@@ -37,9 +37,21 @@
gap: 0; gap: 0;
} }
// 品牌大字淡入动画
@keyframes brandFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: var(--target-opacity, 0.9);
transform: translateY(0);
}
}
// 品牌大字 // 品牌大字
.brandWord { .brandWord {
font-size: 13vw; font-size: 14vw;
font-weight: 900; font-weight: 900;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
letter-spacing: -0.02em; letter-spacing: -0.02em;
@@ -48,18 +60,23 @@
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
text-align: right; text-align: right;
padding-right: 0; padding-right: 0;
opacity: 0;
animation: brandFadeIn 0.8s ease-out forwards;
// 不同字有不同的透明度形成层次感 // 不同字有不同的透明度和延迟,从上到下依次显现
&:nth-child(1) { &:nth-child(1) {
color: rgba(255, 255, 255, 0.95); --target-opacity: 0.95;
animation-delay: 0.1s;
} }
&:nth-child(2) { &:nth-child(2) {
color: rgba(255, 255, 255, 0.7); --target-opacity: 0.7;
animation-delay: 0.35s;
} }
&:nth-child(3) { &:nth-child(3) {
color: rgba(255, 255, 255, 0.45); --target-opacity: 0.45;
animation-delay: 0.6s;
} }
} }
@@ -94,7 +111,7 @@
.logo { .logo {
width: 80px; width: 80px;
height: 80px; height: 80px;
border-radius: $radius-full; border-radius: $radius-lg;
object-fit: cover; object-fit: cover;
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
border: 3px solid var(--border-color); border: 3px solid var(--border-color);
@@ -211,24 +228,101 @@
font-size: 14px; 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); @keyframes splashEnter {
font-size: 14px; from {
opacity: 0;
transform: scale(0.9) translateY(20px);
} }
to {
.value { opacity: 1;
font-weight: 600; transform: scale(1) translateY(0);
color: var(--text-primary);
} }
} }
// 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;
}

View File

@@ -30,6 +30,7 @@ export function LoginPage() {
const [rememberPassword, setRememberPassword] = useState(false); const [rememberPassword, setRememberPassword] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [autoLoading, setAutoLoading] = useState(true); const [autoLoading, setAutoLoading] = useState(true);
const [autoLoginSuccess, setAutoLoginSuccess] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const detectedBase = useMemo(() => detectApiBaseFromLocation(), []); const detectedBase = useMemo(() => detectApiBaseFromLocation(), []);
@@ -39,18 +40,28 @@ export function LoginPage() {
const init = async () => { const init = async () => {
try { try {
const autoLoggedIn = await restoreSession(); 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); setApiBase(storedBase || detectedBase);
setManagementKey(storedKey || ''); setManagementKey(storedKey || '');
setRememberPassword(storedRememberPassword || Boolean(storedKey)); setRememberPassword(storedRememberPassword || Boolean(storedKey));
} }
} finally { } finally {
if (!autoLoginSuccess) {
setAutoLoading(false); setAutoLoading(false);
} }
}
}; };
init(); init();
}, [detectedBase, restoreSession, storedBase, storedKey, storedRememberPassword]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(async () => {
if (!managementKey.trim()) { if (!managementKey.trim()) {
@@ -88,11 +99,14 @@ export function LoginPage() {
[loading, handleSubmit] [loading, handleSubmit]
); );
if (isAuthenticated) { if (isAuthenticated && !autoLoading && !autoLoginSuccess) {
const redirect = (location.state as any)?.from?.pathname || '/'; const redirect = (location.state as any)?.from?.pathname || '/';
return <Navigate to={redirect} replace />; return <Navigate to={redirect} replace />;
} }
// 显示启动动画(自动登录中或自动登录成功)
const showSplash = autoLoading || autoLoginSuccess;
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* 左侧品牌展示区 */} {/* 左侧品牌展示区 */}
@@ -106,6 +120,18 @@ export function LoginPage() {
{/* 右侧功能交互区 */} {/* 右侧功能交互区 */}
<div className={styles.formPanel}> <div className={styles.formPanel}>
{showSplash ? (
/* 启动动画 */
<div className={styles.splashContent}>
<img src={INLINE_LOGO_JPEG} alt="CPAMC" className={styles.splashLogo} />
<h1 className={styles.splashTitle}>CLI Proxy API</h1>
<p className={styles.splashSubtitle}>Management Center</p>
<div className={styles.splashLoader}>
<div className={styles.splashLoaderBar} />
</div>
</div>
) : (
/* 登录表单 */
<div className={styles.formContent}> <div className={styles.formContent}>
{/* Logo */} {/* Logo */}
<img src={INLINE_LOGO_JPEG} alt="Logo" className={styles.logo} /> <img src={INLINE_LOGO_JPEG} alt="Logo" className={styles.logo} />
@@ -200,16 +226,10 @@ export function LoginPage() {
</Button> </Button>
{error && <div className={styles.errorBox}>{error}</div>} {error && <div className={styles.errorBox}>{error}</div>}
</div>
{autoLoading && (
<div className={styles.autoLoginBox}>
<div className={styles.label}>{t('auto_login.title')}</div>
<div className={styles.value}>{t('auto_login.message')}</div>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div>
</div>
); );
} }