import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from 'react'; import { Lock, User } from 'lucide-react'; import { useLocation, useNavigate } from 'react-router-dom'; import blueLogo from '../../assets/images/login/blue_logo.png'; import loginBackground from '../../assets/images/login/login_bg.png'; import loginIllustration from '../../assets/images/login/logo.png'; import userIcon from '../../assets/images/login/user_icon.png'; import { useI18n } from '../../i18n'; import TitleBar from '../../components/layout/TitleBar'; import { resolvePostLoginPath } from '../../router/auth'; import { createCaptchaState, getCaptchaImageUrl, loginWithPassword, persistLoginTokens, type LoginFormValues, } from './auth'; type FormErrors = Partial>; const INITIAL_FORM: LoginFormValues = { username: '', password: '', code: '', randomStr: '', }; const credentialFieldShellClass = 'flex h-11 items-center rounded-[10px] border border-black/10 bg-gray-50 transition focus-within:border-[#2B7FFF] focus-within:ring-2 focus-within:ring-[#2B7FFF]/10 dark:border-[#2a2a2d] dark:bg-[#222225]'; const credentialFieldIconClass = 'ml-4 mr-3 h-4 w-4 shrink-0 text-[#99A0AE] dark:text-gray-500'; const credentialFieldInputClass = 'h-full min-w-0 flex-1 border-0 bg-transparent pr-4 text-[14px] text-gray-800 outline-none placeholder:text-[#99A0AE] disabled:cursor-not-allowed dark:text-gray-100 dark:placeholder:text-gray-500'; export default function LoginPage() { const { t } = useI18n(); const navigate = useNavigate(); const location = useLocation(); const platform = (window as any).api?.platform ?? ''; const [form, setForm] = useState(INITIAL_FORM); const [errors, setErrors] = useState({}); const [submitting, setSubmitting] = useState(false); const [showDecorations, setShowDecorations] = useState(false); const captchaUrl = useMemo( () => (form.randomStr ? getCaptchaImageUrl(form.randomStr) : ''), [form.randomStr], ); useEffect(() => { const frameId = window.requestAnimationFrame(() => { setShowDecorations(true); setForm((current) => ({ ...current, ...createCaptchaState() })); }); return () => { window.cancelAnimationFrame(frameId); }; }, []); function refreshCaptcha(clearSubmitError = true): void { setForm((current) => ({ ...current, code: '', ...createCaptchaState(), })); setErrors((current) => ({ ...current, code: undefined, submit: clearSubmitError ? undefined : current.submit, })); } function updateField(key: K, value: LoginFormValues[K]): void { setForm((current) => ({ ...current, [key]: value })); setErrors((current) => ({ ...current, [key]: undefined, submit: undefined })); } function validate(values: LoginFormValues): FormErrors { const nextErrors: FormErrors = {}; if (!values.username.trim()) nextErrors.username = t('login.usernameRequired'); if (!values.password.trim()) nextErrors.password = t('login.passwordRequired'); if (!values.code.trim()) nextErrors.code = t('login.codeRequired'); return nextErrors; } async function handleSubmit(event: FormEvent): Promise { event.preventDefault(); const trimmedValues: LoginFormValues = { ...form, username: form.username.trim(), password: form.password.trim(), code: form.code.trim(), }; const nextErrors = validate(trimmedValues); if (Object.keys(nextErrors).length > 0) { setErrors(nextErrors); return; } setSubmitting(true); setErrors({}); try { const result = await loginWithPassword(trimmedValues); persistLoginTokens(result); navigate(resolvePostLoginPath(location.state as { from?: string } | null), { replace: true }); } catch (error) { setErrors({ submit: error instanceof Error ? error.message : t('login.submitFailed'), }); refreshCaptcha(false); } finally { setSubmitting(false); } } function handleInputChange( key: 'username' | 'password' | 'code', event: ChangeEvent, ): void { updateField(key, event.target.value); } return (
{showDecorations ? ( ) : null}
zn-ai
{t('login.title')}
{t('login.subtitle')}
{errors.username ?
{errors.username}
: null}
{errors.password ?
{errors.password}
: null}
handleInputChange('code', event)} />
{errors.code ?
{errors.code}
: null}
{errors.submit ? (
{errors.submit}
) : null}
{showDecorations ? ( ) : null}
); }