import { FormEvent, useEffect, useMemo, useState } from 'react'; import { Loader2, LockKeyhole, RefreshCw } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useYinianStore } from '@/stores/yinian'; import { YinianPanel, yinianPrimaryButton } from '@/components/yinian/ui'; import logoSvg from '@/assets/logo.svg'; import type { YinianImageCaptcha } from '../../../shared/yinian'; import type { YinianServerStatus } from '../../../shared/yinian'; import { useTranslation } from 'react-i18next'; export function YinianLogin() { const navigate = useNavigate(); const { t } = useTranslation('setup'); const status = useYinianStore((state) => state.status); const error = useYinianStore((state) => state.error); const loginWithPassword = useYinianStore((state) => state.loginWithPassword); const [account, setAccount] = useState(''); const [password, setPassword] = useState(''); const [rememberPassword, setRememberPassword] = useState(false); const [captchaCode, setCaptchaCode] = useState(''); const [captcha, setCaptcha] = useState(null); const [captchaLoading, setCaptchaLoading] = useState(false); const [serverStatus, setServerStatus] = useState(null); const isLoading = status === 'loading'; const serverConfigured = Boolean(serverStatus?.apiBaseUrl) || serverStatus?.mode === 'mock'; const captchaSrc = useMemo(() => { if (!captcha) return ''; if (captcha.image?.startsWith('data:')) return captcha.image; if (captcha.image) return captcha.image; if (captcha.imageBase64) { return `data:${captcha.mimeType || 'image/png'};base64,${captcha.imageBase64}`; } return ''; }, [captcha]); const captchaRequired = Boolean(captchaSrc); const refreshCaptcha = async () => { setCaptchaLoading(true); try { const randomStr = crypto.randomUUID(); const next = await window.yinian.auth.createImageCaptcha(randomStr); setCaptcha(next); setCaptchaCode(''); } catch { setCaptcha(null); } finally { setCaptchaLoading(false); } }; useEffect(() => { void window.yinian.app.getServerStatus() .then(setServerStatus) .catch(() => setServerStatus(null)); void refreshCaptcha(); void window.yinian.auth.getSavedCredentials() .then((credentials) => { if (!credentials) return; setAccount(credentials.account); setPassword(credentials.password ?? ''); setRememberPassword(credentials.rememberPassword); }) .catch(() => undefined); }, []); const handlePasswordLogin = async (event: FormEvent) => { event.preventDefault(); await loginWithPassword({ account: account.trim(), password, captchaCode: captchaCode.trim(), randomStr: captcha?.randomStr, }); if (rememberPassword) { void window.yinian.auth.saveCredentials({ account: account.trim(), password, rememberPassword: true, }).catch(() => undefined); } else { void window.yinian.auth.clearSavedCredentials().catch(() => undefined); } navigate('/', { replace: true }); }; return (
{t('login.appName')}

{t('login.slogan')}

{t('login.appName')} {t('login.appName')}

{t('login.title')}

{t('login.slogan')}

{serverStatus && (
{serverConfigured ? t('login.serverEnabled', { baseUrl: serverStatus.apiBaseUrl ? `: ${serverStatus.apiBaseUrl}` : '' }) : serverStatus.message}
)}
setAccount(event.target.value)} placeholder={t('login.accountPlaceholder')} autoComplete="username" />
setPassword(event.target.value)} placeholder={t('login.passwordPlaceholder')} autoComplete="current-password" />
{error &&

{error}

}
); } function CaptchaField({ value, onChange, imageSrc, loading, onRefresh, label, placeholder, refreshTitle, }: { value: string; onChange: (value: string) => void; imageSrc: string; loading: boolean; onRefresh: () => void | Promise; label: string; placeholder: string; refreshTitle: string; }) { if (!imageSrc) return null; return (
onChange(event.target.value)} placeholder={placeholder} />
); } export default YinianLogin;