import { useEffect, useState, useRef, useCallback } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { message } from 'antd'; import { setStorage, STORAGE_KEYS, getUserId, getVisitedRoutes, setUserName } from '@/utils/storage'; import { updateUserUIState, verifyCode, loginWithPhone, loginWithEmail, sendCode, getUserInfo } from '@/utils/api'; import { px2vmin, px2vw } from '@/utils/viewport'; import { getOssUrl } from '@/utils/oss'; const sendCodeWaitNo = getOssUrl('send-code-wait-no.png'); const backButton = getOssUrl('backbutn.png'); const YpImage = getOssUrl('yp.png'); interface LocationState { loginType?: 'phone' | 'email'; areaCode?: string; phoneNumber?: string; email?: string; } export const LoginVerificationInputPage = () => { const location = useLocation(); const navigate = useNavigate(); const state = location.state as LocationState | null; // 验证码输入框状态(4个) const [codes, setCodes] = useState(['', '', '', '']); const [_focusedIndex, setFocusedIndex] = useState(0); const [countdown, setCountdown] = useState(60); const [isCodeValid, setIsCodeValid] = useState(false); const [hasCodeError, setHasCodeError] = useState(false); const [isVerifying, setIsVerifying] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); const codeRefs = [ useRef(null), useRef(null), useRef(null), useRef(null), ]; useEffect(() => { // 记录用户当前所在页面 setStorage(STORAGE_KEYS.CURRENT_PAGE, 'login-verification-input'); const visited = getVisitedRoutes(); const userId = getUserId(); if (userId) { updateUserUIState(userId, { current_page: 'login-verification-input', visited_routes: visited, }).catch(() => { // 同步失败不影响前端流程 }); } }, []); // 自动聚焦到第一个输入框 // 延迟聚焦,确保视口高度修复机制已初始化完成,防止键盘弹出时界面缩小 useEffect(() => { // 检查 --vh CSS 变量是否已初始化(视口高度修复机制已就绪) const checkViewportReady = (): boolean => { const vhValue = getComputedStyle(document.documentElement).getPropertyValue('--vh'); const isReady = vhValue !== '' && vhValue !== null; console.log(`[LoginVerificationInputPage] 🔍 检查视口就绪: --vh=${vhValue}, isReady=${isReady}`); return isReady; }; // 尝试聚焦的函数 const attemptFocus = (source: string) => { if (codeRefs[0].current) { const input = codeRefs[0].current; const vhValue = getComputedStyle(document.documentElement).getPropertyValue('--vh'); const initialHeight = window.innerHeight; const currentHeight = window.visualViewport?.height || window.innerHeight; console.log(`[LoginVerificationInputPage] 🎯 尝试聚焦: source=${source}, --vh=${vhValue}, initialHeight=${initialHeight}px, currentHeight=${currentHeight}px, inputMode=${input.inputMode}`); input.focus(); // 聚焦后立即检查状态 setTimeout(() => { const afterFocusVh = getComputedStyle(document.documentElement).getPropertyValue('--vh'); const afterFocusHeight = window.visualViewport?.height || window.innerHeight; console.log(`[LoginVerificationInputPage] ✅ 聚焦后状态: --vh=${afterFocusVh}, currentHeight=${afterFocusHeight}px, document.activeElement=${document.activeElement?.tagName}`); }, 100); } else { console.log(`[LoginVerificationInputPage] ⚠️ 聚焦失败: codeRefs[0].current 为 null`); } }; // 如果视口已就绪,延迟一小段时间后聚焦(确保初始化完成) if (checkViewportReady()) { console.log(`[LoginVerificationInputPage] ✅ 视口已就绪,延迟150ms后聚焦`); // 使用 requestAnimationFrame 确保在下一帧执行,给初始化更多时间 requestAnimationFrame(() => { setTimeout(() => attemptFocus('ready-delayed'), 150); }); } else { console.log(`[LoginVerificationInputPage] ⏳ 视口未就绪,等待初始化...`); // 如果还未就绪,等待一段时间后重试 let checkCount = 0; const checkInterval = setInterval(() => { checkCount++; if (checkViewportReady()) { console.log(`[LoginVerificationInputPage] ✅ 视口已就绪(重试${checkCount}次),延迟150ms后聚焦`); clearInterval(checkInterval); requestAnimationFrame(() => { setTimeout(() => attemptFocus('retry-delayed'), 150); }); } }, 50); // 最多等待 500ms,超时后强制聚焦 setTimeout(() => { clearInterval(checkInterval); console.log(`[LoginVerificationInputPage] ⏰ 等待超时,强制聚焦`); attemptFocus('timeout'); }, 500); } }, []); // 倒计时功能 useEffect(() => { if (countdown > 0) { const timer = setTimeout(() => { setCountdown(countdown - 1); }, 1000); return () => clearTimeout(timer); } }, [countdown]); const handleBack = () => { // 返回登录验证码页面,并带上传入的所有数据 navigate('/login-verification-code', { state: { loginType: state?.loginType, areaCode: state?.areaCode, phoneNumber: state?.phoneNumber, email: state?.email, }, }); }; // 处理验证码输入 const handleCodeChange = (index: number, value: string) => { // 清理输入,只保留数字和字母 const sanitizedValue = value.replace(/[^0-9a-zA-Z]/g, ''); // 如果输入长度大于1,说明是自动填充(从输入法上方点击验证码) if (sanitizedValue.length > 1) { const newCodes = [...codes]; // 将验证码分配到4个输入框中 for (let i = 0; i < 4; i++) { newCodes[i] = sanitizedValue[i] || ''; } setCodes(newCodes); setHasCodeError(false); // 聚焦到最后一个有值的输入框或最后一个输入框 const lastFilledIndex = Math.min(sanitizedValue.length - 1, 3); codeRefs[lastFilledIndex].current?.focus(); setFocusedIndex(lastFilledIndex); return; } // 单个字符输入(正常输入) const singleChar = sanitizedValue.slice(0, 1); const newCodes = [...codes]; newCodes[index] = singleChar; setCodes(newCodes); setHasCodeError(false); // 如果输入了值,自动聚焦到下一个输入框 if (singleChar && index < 3) { codeRefs[index + 1].current?.focus(); setFocusedIndex(index + 1); } }; // 处理删除键 const handleKeyDown = (index: number, e: React.KeyboardEvent) => { if (e.key === 'Backspace' && !codes[index] && index > 0) { // 如果当前输入框为空,删除上一个输入框的内容并聚焦 const newCodes = [...codes]; newCodes[index - 1] = ''; setCodes(newCodes); codeRefs[index - 1].current?.focus(); setFocusedIndex(index - 1); } }; // 处理粘贴 const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); const pastedText = e.clipboardData.getData('text').replace(/[^0-9a-zA-Z]/g, '').slice(0, 4); if (pastedText.length > 0) { const newCodes = [...codes]; for (let i = 0; i < 4; i++) { newCodes[i] = pastedText[i] || ''; } setCodes(newCodes); setHasCodeError(false); // 聚焦到最后一个有值的输入框或最后一个输入框 const lastFilledIndex = Math.min(pastedText.length - 1, 3); codeRefs[lastFilledIndex].current?.focus(); setFocusedIndex(lastFilledIndex); } }; // 检查验证码是否完整 const isCodeComplete = codes.every(code => code !== ''); // 处理登录 const handleLogin = useCallback(async () => { if (isLoggingIn) return; setIsLoggingIn(true); try { let response; if (state?.loginType === 'email' && state?.email) { response = await loginWithEmail(state.email); } else if (state?.phoneNumber) { response = await loginWithPhone(state.phoneNumber, state?.areaCode); } else { message.error('登录信息不完整'); setIsLoggingIn(false); setIsCodeValid(false); return; } if (response.success && response.data) { // 保存基本用户信息 const userId = response.data.user_id; setStorage(STORAGE_KEYS.USER_ID, userId); if (response.data.username) { setStorage(STORAGE_KEYS.USER_NAME, response.data.username); } // 🔥 保存会话 token(登录接口返回) if (response.data.token) { const { setUserToken } = await import('@/utils/storage'); setUserToken(response.data.token); } // 获取完整用户信息并同步到 localStorage try { const userInfo = await getUserInfo(userId); if (userInfo.success && userInfo.data) { // 同步用户名 if (userInfo.data.username) { setUserName(userInfo.data.username); } // 同步机器人名字 if (userInfo.data.bot_name) { setStorage(STORAGE_KEYS.LUMI_NAME, userInfo.data.bot_name); } // 同步用户等级 if (typeof userInfo.data.user_level === 'number') { setStorage(STORAGE_KEYS.USER_LEVEL, String(userInfo.data.user_level)); } // 背景图片会在 getUserInfo 中自动同步 } } catch (error) { // 获取完整用户信息失败不影响登录流程,静默处理 console.warn('获取完整用户信息失败:', error); } message.success('登录成功'); // 跳转到聊天界面 navigate('/chatbot'); } else { message.error('登录失败,请重试'); setIsCodeValid(false); } } catch (error) { const errorMessage = error instanceof Error ? error.message : '登录失败'; message.error(errorMessage); setIsCodeValid(false); } finally { setIsLoggingIn(false); } }, [isLoggingIn, state?.loginType, state?.email, state?.phoneNumber, navigate]); // 验证验证码(当输入完成时自动验证) useEffect(() => { if (isCodeComplete && !isVerifying && !isCodeValid && !isLoggingIn) { const verify = async () => { setIsVerifying(true); try { const codeString = codes.join(''); const sendType = state?.loginType === 'email' ? 'email' : 'domestic_sms'; const response = await verifyCode({ phone: state?.phoneNumber, email: state?.email, code: codeString, send_type: sendType, }); if (response.success && response.data.valid) { setIsCodeValid(true); setHasCodeError(false); // 验证成功后自动登录 handleLogin(); } else { setIsCodeValid(false); setHasCodeError(true); // 验证失败,清空输入框 setCodes(['', '', '', '']); codeRefs[0].current?.focus(); setFocusedIndex(0); message.error('验证码错误,请重新输入'); } } catch (error) { console.error('验证验证码失败:', error); setIsCodeValid(false); setHasCodeError(true); // 验证失败,清空输入框 setCodes(['', '', '', '']); codeRefs[0].current?.focus(); setFocusedIndex(0); message.error('验证码验证失败,请重试'); } finally { setIsVerifying(false); } }; // 延迟一下再验证,避免用户还在输入时触发 const timer = setTimeout(() => { verify(); }, 300); return () => clearTimeout(timer); } }, [isCodeComplete, codes.join(''), state?.phoneNumber, state?.email, state?.loginType, isVerifying, isCodeValid, isLoggingIn, handleLogin]); // 处理发送验证码按钮点击 const handleSendCode = async () => { if (countdown > 0) { return; } // 验证输入 if (state?.loginType === 'phone' && !state?.phoneNumber) { message.error('手机号码不能为空'); return; } if (state?.loginType === 'email' && !state?.email) { message.error('邮箱地址不能为空'); return; } try { // 确定发送类型 const sendType = state?.loginType === 'email' ? 'email' : 'domestic_sms'; // 发送验证码 const response = await sendCode({ area_code: state?.loginType === 'phone' ? state?.areaCode : undefined, phone: state?.loginType === 'phone' ? state?.phoneNumber : undefined, email: state?.loginType === 'email' ? state?.email : undefined, send_type: sendType, }); if (response.success) { message.success('验证码已发送'); // 重新开始倒计时 setCountdown(60); // 清空验证码输入框 setCodes(['', '', '', '']); codeRefs[0].current?.focus(); setFocusedIndex(0); setIsCodeValid(false); setHasCodeError(false); } else { message.error('发送验证码失败,请重试'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : '发送验证码失败'; message.error(errorMessage); } }; return (
{/* 顶部返回按钮 */} {/* 中部 GIF 动画图片 */} cheer animation {/* Yp 图片 - 位于动画形象下方 */}
Yp
{/* 验证码发送提示文字 */}
{state?.loginType === 'email' ? `验证码已通过邮箱发送至 ${state?.email || ''}` : `验证码已通过短信发送至 ${(state?.areaCode || '+86')} ${state?.phoneNumber || ''}`}
{/* 验证码输入框 */}
{codes.map((code, index) => { return (
handleCodeChange(index, e.target.value)} onKeyDown={(e) => handleKeyDown(index, e)} onPaste={handlePaste} onFocus={() => setFocusedIndex(index)} onBlur={() => setFocusedIndex(null)} disabled={isVerifying || isLoggingIn} />
); })}
{/* 发送验证码按钮 */}
); };