import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { Navigate, useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { IconEye, IconEyeOff } from '@/components/ui/icons'; import { useAuthStore, useLanguageStore, useNotificationStore } from '@/stores'; import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection'; import { INLINE_LOGO_JPEG } from '@/assets/logoInline'; import type { ApiError } from '@/types'; import styles from './LoginPage.module.scss'; /** * 将 API 错误转换为本地化的用户友好消息 */ function getLocalizedErrorMessage(error: any, t: (key: string) => string): string { const apiError = error as ApiError; const status = apiError?.status; const code = apiError?.code; const message = apiError?.message || ''; // 根据 HTTP 状态码判断 if (status === 401) { return t('login.error_unauthorized'); } if (status === 403) { return t('login.error_forbidden'); } if (status === 404) { return t('login.error_not_found'); } if (status && status >= 500) { return t('login.error_server'); } // 根据 axios 错误码判断 if (code === 'ECONNABORTED' || message.toLowerCase().includes('timeout')) { return t('login.error_timeout'); } if (code === 'ERR_NETWORK' || message.toLowerCase().includes('network error')) { return t('login.error_network'); } if (code === 'ERR_CERT_AUTHORITY_INVALID' || message.toLowerCase().includes('certificate')) { return t('login.error_ssl'); } // 检查 CORS 错误 if (message.toLowerCase().includes('cors') || message.toLowerCase().includes('cross-origin')) { return t('login.error_cors'); } // 默认错误消息 return t('login.error_invalid'); } export function LoginPage() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const { showNotification } = useNotificationStore(); const language = useLanguageStore((state) => state.language); const toggleLanguage = useLanguageStore((state) => state.toggleLanguage); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const login = useAuthStore((state) => state.login); const restoreSession = useAuthStore((state) => state.restoreSession); const storedBase = useAuthStore((state) => state.apiBase); const storedKey = useAuthStore((state) => state.managementKey); const storedRememberPassword = useAuthStore((state) => state.rememberPassword); const [apiBase, setApiBase] = useState(''); const [managementKey, setManagementKey] = useState(''); const [showCustomBase, setShowCustomBase] = useState(false); const [showKey, setShowKey] = useState(false); 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(), []); const nextLanguageLabel = language === 'zh-CN' ? t('language.english') : t('language.chinese'); useEffect(() => { const init = async () => { try { const autoLoggedIn = await restoreSession(); if (autoLoggedIn) { setAutoLoginSuccess(true); // 延迟跳转,让用户看到成功动画 setTimeout(() => { const redirect = (location.state as any)?.from?.pathname || '/'; navigate(redirect, { replace: true }); }, 1300); } else { setApiBase(storedBase || detectedBase); setManagementKey(storedKey || ''); setRememberPassword(storedRememberPassword || Boolean(storedKey)); } } finally { if (!autoLoginSuccess) { setAutoLoading(false); } } }; init(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSubmit = useCallback(async () => { if (!managementKey.trim()) { setError(t('login.error_required')); return; } const baseToUse = apiBase ? normalizeApiBase(apiBase) : detectedBase; setLoading(true); setError(''); try { await login({ apiBase: baseToUse, managementKey: managementKey.trim(), rememberPassword }); showNotification(t('common.connected_status'), 'success'); navigate('/', { replace: true }); } catch (err: any) { const message = getLocalizedErrorMessage(err, t); setError(message); showNotification(`${t('notification.login_failed')}: ${message}`, 'error'); } finally { setLoading(false); } }, [apiBase, detectedBase, login, managementKey, navigate, rememberPassword, showNotification, t]); const handleSubmitKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === 'Enter' && !loading) { event.preventDefault(); handleSubmit(); } }, [loading, handleSubmit] ); if (isAuthenticated && !autoLoading && !autoLoginSuccess) { const redirect = (location.state as any)?.from?.pathname || '/'; return ; } // 显示启动动画(自动登录中或自动登录成功) const showSplash = autoLoading || autoLoginSuccess; return (
{/* 左侧品牌展示区 */}
CLI PROXY API
{/* 右侧功能交互区 */}
{showSplash ? ( /* 启动动画 */
CPAMC

CLI Proxy API

Management Center

) : ( /* 登录表单 */
{/* 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}
}
)}
); }