import { X } from 'lucide-react'; import { useEffect, useState } from 'react'; import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime'; import type { AuthCaptchaChallenge, AuthLoginMethod, } from '../../services/authService'; import { getStoredLastLoginPhone } from '../../services/authService'; import { CaptchaChallengeField } from './CaptchaChallengeField'; type SmsScene = 'login' | 'reset_password'; type LoginTab = 'phone' | 'password' | 'register'; type LoginScreenProps = { isOpen: boolean; platformTheme: PlatformTheme; availableLoginMethods: AuthLoginMethod[]; sendingCode: boolean; loggingIn: boolean; wechatLoading: boolean; error: string; captchaChallenge: AuthCaptchaChallenge | null; initialMode?: 'login' | 'register'; initialInviteCode?: string; onClose: () => void; onSendCode: ( phone: string, scene: SmsScene, captcha?: { challengeId?: string; answer?: string; }, ) => Promise<{ cooldownSeconds: number; expiresInSeconds: number; }>; onPhoneSubmit: ( phone: string, code: string, inviteCode?: string, ) => Promise; onPasswordSubmit: (phone: string, password: string) => Promise; onResetPassword: ( phone: string, code: string, newPassword: string, ) => Promise; onStartWechatLogin: () => Promise; }; export function LoginScreen({ isOpen, platformTheme, availableLoginMethods, sendingCode, loggingIn, wechatLoading, error, captchaChallenge, initialMode = 'login', initialInviteCode = '', onClose, onSendCode, onPhoneSubmit, onPasswordSubmit, onResetPassword, onStartWechatLogin, }: LoginScreenProps) { const [isResetPanelOpen, setIsResetPanelOpen] = useState(false); const [phone, setPhone] = useState(() => getStoredLastLoginPhone()); const [password, setPassword] = useState(''); const [code, setCode] = useState(''); const [resetPhone, setResetPhone] = useState(''); const [resetCode, setResetCode] = useState(''); const [resetPasswordValue, setResetPasswordValue] = useState(''); const [inviteCode, setInviteCode] = useState(initialInviteCode); const [captchaAnswer, setCaptchaAnswer] = useState(''); const [cooldownSeconds, setCooldownSeconds] = useState(0); const [resetCooldownSeconds, setResetCooldownSeconds] = useState(0); const [hint, setHint] = useState(''); const passwordLoginEnabled = availableLoginMethods.includes('password'); const phoneLoginEnabled = availableLoginMethods.includes('phone'); const wechatLoginEnabled = availableLoginMethods.includes('wechat'); const [activeLoginTab, setActiveLoginTab] = useState('phone'); useEffect(() => { if (!isOpen) { return; } // 每次重新打开弹窗都丢弃上一次未完成的表单草稿,只保留最近成功登录手机号回填。 setIsResetPanelOpen(false); setPhone(getStoredLastLoginPhone()); setPassword(''); setCode(''); setResetPhone(''); setResetCode(''); setResetPasswordValue(''); setInviteCode(initialInviteCode); setCaptchaAnswer(''); setCooldownSeconds(0); setResetCooldownSeconds(0); setHint(''); setActiveLoginTab( initialMode === 'register' && phoneLoginEnabled ? 'register' : phoneLoginEnabled ? 'phone' : 'password', ); }, [initialInviteCode, initialMode, isOpen, phoneLoginEnabled]); useEffect(() => { if ( (activeLoginTab === 'phone' || activeLoginTab === 'register') && !phoneLoginEnabled && passwordLoginEnabled ) { setActiveLoginTab('password'); return; } if ( activeLoginTab === 'password' && !passwordLoginEnabled && phoneLoginEnabled ) { setActiveLoginTab('phone'); } }, [activeLoginTab, passwordLoginEnabled, phoneLoginEnabled]); useEffect(() => { if (cooldownSeconds <= 0) { return; } const timeoutId = window.setTimeout(() => { setCooldownSeconds((current) => Math.max(0, current - 1)); }, 1000); return () => window.clearTimeout(timeoutId); }, [cooldownSeconds]); useEffect(() => { if (resetCooldownSeconds <= 0) { return; } const timeoutId = window.setTimeout(() => { setResetCooldownSeconds((current) => Math.max(0, current - 1)); }, 1000); return () => window.clearTimeout(timeoutId); }, [resetCooldownSeconds]); if (!isOpen) { return null; } const submitDisabled = loggingIn || sendingCode; return (
event.stopPropagation()} >
{isResetPanelOpen ? '重置密码' : '账号入口'}
{isResetPanelOpen ? ( setIsResetPanelOpen(false)} onSendCode={async () => { const result = await onSendCode(resetPhone, 'reset_password'); setResetCooldownSeconds(result.cooldownSeconds); }} onSubmit={() => onResetPassword(resetPhone, resetCode, resetPasswordValue) } /> ) : (
{phoneLoginEnabled ? (
setActiveLoginTab('phone')} > 短信登录 {passwordLoginEnabled ? ( setActiveLoginTab('password')} > 密码登录 ) : null} setActiveLoginTab('register')} > 注册
) : null} {passwordLoginEnabled && activeLoginTab === 'password' ? (
{ event.preventDefault(); void onPasswordSubmit(phone, password); }} > {error ? : null}
{wechatLoginEnabled ? ( ) : null} ) : null} {phoneLoginEnabled && activeLoginTab === 'phone' ? ( { setHint(''); const result = await onSendCode(phone, 'login', { challengeId: captchaChallenge?.challengeId, answer: captchaAnswer, }); setCooldownSeconds(result.cooldownSeconds); setHint( `短信请求已提交,验证码有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`, ); setCaptchaAnswer(''); }} onSubmit={() => onPhoneSubmit(phone, code)} /> ) : null} {phoneLoginEnabled && activeLoginTab === 'register' ? ( { setHint(''); const result = await onSendCode(phone, 'login', { challengeId: captchaChallenge?.challengeId, answer: captchaAnswer, }); setCooldownSeconds(result.cooldownSeconds); setHint( `短信请求已提交,验证码有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`, ); setCaptchaAnswer(''); }} onSubmit={() => onPhoneSubmit(phone, code, inviteCode)} /> ) : null} {!passwordLoginEnabled && !phoneLoginEnabled && !wechatLoginEnabled ? (
当前登录入口暂不可用。
) : null}
)}
); } function LoginTabButton({ active, children, onClick, }: { active: boolean; children: string; onClick: () => void; }) { return ( ); } function PhoneCodeForm({ phone, code, inviteCode = '', captchaAnswer, captchaChallenge, cooldownSeconds, sendingCode, loggingIn, error, hint, submitLabel, enabled, showPhoneField, showInviteCodeField = false, onPhoneChange, onCodeChange, onInviteCodeChange, onCaptchaAnswerChange, onSendCode, onSubmit, }: { phone: string; code: string; inviteCode?: string; captchaAnswer: string; captchaChallenge: AuthCaptchaChallenge | null; cooldownSeconds: number; sendingCode: boolean; loggingIn: boolean; error: string; hint: string; submitLabel: string; enabled: boolean; showPhoneField: boolean; showInviteCodeField?: boolean; onPhoneChange: (value: string) => void; onCodeChange: (value: string) => void; onInviteCodeChange?: (value: string) => void; onCaptchaAnswerChange: (value: string) => void; onSendCode: () => Promise; onSubmit: () => Promise; }) { if (!enabled) { return null; } return (
{ event.preventDefault(); void onSubmit(); }} > {showPhoneField ? ( ) : null} {showInviteCodeField ? ( ) : null} {hint ? : null} {error ? : null} ); } function PasswordResetPanel({ phone, code, password, sendingCode, loggingIn, cooldownSeconds, error, onPhoneChange, onCodeChange, onPasswordChange, onBack, onSendCode, onSubmit, }: { phone: string; code: string; password: string; sendingCode: boolean; loggingIn: boolean; cooldownSeconds: number; error: string; onPhoneChange: (value: string) => void; onCodeChange: (value: string) => void; onPasswordChange: (value: string) => void; onBack: () => void; onSendCode: () => Promise; onSubmit: () => Promise; }) { return (
{ event.preventDefault(); void onSubmit(); }} > {error ? : null}
); } function WechatButton({ loading, disabled, onClick, }: { loading: boolean; disabled: boolean; onClick: () => Promise; }) { return ( ); } function ErrorBanner({ message }: { message: string }) { return (
{message}
); } function SuccessBanner({ message }: { message: string }) { return (
{message}
); }