import { Check, X } from 'lucide-react'; import { type ReactNode, 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 { LegalDocumentModal } from '../common/LegalDocumentModal'; import { getLegalDocument, type LegalDocumentId, persistLegalConsent, readStoredLegalConsent, } from '../common/legalDocuments'; import { CaptchaChallengeField } from './CaptchaChallengeField'; type SmsScene = 'login' | 'reset_password'; type LoginTab = 'phone' | 'password'; type LoginScreenProps = { isOpen: boolean; platformTheme: PlatformTheme; availableLoginMethods: AuthLoginMethod[]; sendingCode: boolean; loggingIn: boolean; wechatLoading: boolean; error: string; captchaChallenge: AuthCaptchaChallenge | null; onClose: () => void; onSendCode: ( phone: string, scene: SmsScene, captcha?: { challengeId?: string; answer?: string; }, ) => Promise<{ cooldownSeconds: number; expiresInSeconds: number; }>; onPhoneSubmit: (phone: string, code: 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, 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 [captchaAnswer, setCaptchaAnswer] = useState(''); const [cooldownSeconds, setCooldownSeconds] = useState(0); const [resetCooldownSeconds, setResetCooldownSeconds] = useState(0); const [hint, setHint] = useState(''); const [legalConsentChecked, setLegalConsentChecked] = useState(false); const [activeLegalDocumentId, setActiveLegalDocumentId] = useState(null); const passwordLoginEnabled = true; const phoneLoginEnabled = true; const wechatLoginEnabled = availableLoginMethods.includes('wechat'); const [activeLoginTab, setActiveLoginTab] = useState('phone'); useEffect(() => { if (!isOpen) { return; } // 每次重新打开弹窗都丢弃上一次未完成的表单草稿,只保留最近成功登录手机号回填。 setIsResetPanelOpen(false); setPhone(getStoredLastLoginPhone()); setPassword(''); setCode(''); setResetPhone(''); setResetCode(''); setResetPasswordValue(''); setCaptchaAnswer(''); setCooldownSeconds(0); setResetCooldownSeconds(0); setHint(''); setLegalConsentChecked(readStoredLegalConsent()); setActiveLegalDocumentId(null); setActiveLoginTab(phoneLoginEnabled ? 'phone' : 'password'); }, [isOpen, phoneLoginEnabled]); useEffect(() => { if ( activeLoginTab === 'phone' && !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; const activeLegalDocument = activeLegalDocumentId ? getLegalDocument(activeLegalDocumentId) : null; const toggleLegalConsent = () => { setLegalConsentChecked((current) => { const nextChecked = !current; if (nextChecked) { persistLegalConsent(); } return nextChecked; }); }; const legalConsentRow = ( ); 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}
) : null} {passwordLoginEnabled && activeLoginTab === 'password' ? (
{ event.preventDefault(); if ( submitDisabled || !phone.trim() || !password.trim() || !legalConsentChecked ) { return; } void onPasswordSubmit(phone, password); }} > {error ? : null} {legalConsentRow}
{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} {!passwordLoginEnabled && !phoneLoginEnabled && !wechatLoginEnabled ? (
当前登录入口暂不可用。
) : null}
)}
setActiveLegalDocumentId(null)} /> ); } function LegalConsentRow({ checked, onToggle, onOpenDocument, }: { checked: boolean; onToggle: () => void; onOpenDocument: (documentId: LegalDocumentId) => void; }) { const openDocument = (documentId: LegalDocumentId) => { onOpenDocument(documentId); }; return (
我已阅读并同意 openDocument('user-agreement')} /> openDocument('privacy-policy')} /> 和 openDocument('disclaimer')} />
); } function LegalLink({ label, onClick, }: { label: string; onClick: () => void; }) { return ( ); } function LoginTabButton({ active, children, onClick, }: { active: boolean; children: string; onClick: () => void; }) { return ( ); } function PhoneCodeForm({ phone, code, captchaAnswer, captchaChallenge, cooldownSeconds, sendingCode, loggingIn, error, hint, submitLabel, enabled, legalConsentChecked, legalConsentNode, showPhoneField, onPhoneChange, onCodeChange, onCaptchaAnswerChange, onSendCode, onSubmit, }: { phone: string; code: string; captchaAnswer: string; captchaChallenge: AuthCaptchaChallenge | null; cooldownSeconds: number; sendingCode: boolean; loggingIn: boolean; error: string; hint: string; submitLabel: string; enabled: boolean; legalConsentChecked: boolean; legalConsentNode: ReactNode; showPhoneField: boolean; onPhoneChange: (value: string) => void; onCodeChange: (value: string) => void; onCaptchaAnswerChange: (value: string) => void; onSendCode: () => Promise; onSubmit: () => Promise; }) { if (!enabled) { return null; } const submitBlocked = loggingIn || !phone.trim() || !code.trim() || !legalConsentChecked; return (
{ event.preventDefault(); if (submitBlocked) { return; } void onSubmit(); }} > {showPhoneField ? ( ) : null} {hint ? : null} {error ? : null} {legalConsentNode} ); } 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}
); }