import { Check } 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 { isWechatMiniProgramWebViewRuntime } from '../../services/authService'; import { LegalDocumentModal } from '../common/LegalDocumentModal'; import { getLegalDocument, type LegalDocumentId, persistLegalConsent, readStoredLegalConsent, } from '../common/legalDocuments'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { CaptchaChallengeField } from './CaptchaChallengeField'; import { PlatformAuthModalShell } from './PlatformAuthModalShell'; type SmsScene = 'login' | 'reset_password'; type LoginTab = 'phone' | 'password'; const LOGIN_TAB_ITEMS: Array<{ id: LoginTab; label: string }> = [ { id: 'phone', label: '短信登录' }, { id: 'password', label: '密码登录' }, ]; 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 miniProgramRuntime = isWechatMiniProgramWebViewRuntime(); 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 ( <> {isResetPanelOpen ? ( setIsResetPanelOpen(false)} onSendCode={async () => { const result = await onSendCode(resetPhone, 'reset_password'); setResetCooldownSeconds(result.cooldownSeconds); }} onSubmit={() => onResetPassword(resetPhone, resetCode, resetPasswordValue) } /> ) : (
{phoneLoginEnabled ? ( ) : null} {passwordLoginEnabled && activeLoginTab === 'password' ? (
{ event.preventDefault(); if ( submitDisabled || !phone.trim() || !password.trim() || !legalConsentChecked ) { return; } void onPasswordSubmit(phone, password); }} > {error ? : null} {legalConsentRow}
{loggingIn ? '登录中' : '登录'}
{wechatLoginEnabled && !miniProgramRuntime ? ( ) : 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 && !miniProgramRuntime ? ( 当前登录入口暂不可用。 ) : 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 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} {loggingIn ? '处理中' : submitLabel} ); } 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}
返回 {loggingIn ? '处理中' : '重置密码'}
); } function WechatButton({ loading, disabled, onClick, }: { loading: boolean; disabled: boolean; onClick: () => Promise; }) { return ( void onClick()} > {loading ? '跳转中' : '微信登录'} ); } function ErrorBanner({ message }: { message: string }) { return ( {message} ); } function SuccessBanner({ message }: { message: string }) { return ( {message} ); }