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'); 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 [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(() => { if (codeRefs[0].current) { codeRefs[0].current.focus(); } }, []); // 倒计时功能 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, '').slice(0, 1); const newCodes = [...codes]; newCodes[index] = sanitizedValue; setCodes(newCodes); // 如果输入了值,自动聚焦到下一个输入框 if (sanitizedValue && 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); // 聚焦到最后一个有值的输入框或最后一个输入框 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); } 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); } // 获取完整用户信息并同步到 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); // 验证成功后自动登录 handleLogin(); } else { setIsCodeValid(false); // 验证失败,清空输入框 setCodes(['', '', '', '']); codeRefs[0].current?.focus(); setFocusedIndex(0); message.error('验证码错误,请重新输入'); } } catch (error) { console.error('验证验证码失败:', error); setIsCodeValid(false); // 验证失败,清空输入框 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); } else { message.error('发送验证码失败,请重试'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : '发送验证码失败'; message.error(errorMessage); } }; return (
{/* 顶部返回按钮 */} {/* 中部 GIF 动画图片 */} cheer animation {/* 验证码发送提示文字 */}
{state?.loginType === 'email' ? `验证码已通过邮箱发送至 ${state?.email || ''}` : `验证码已通过短信发送至 ${state?.phoneNumber || ''}`}
{/* 验证码输入框 */}
{codes.map((code, index) => { const isFocused = focusedIndex === index; return (
handleCodeChange(index, e.target.value)} onKeyDown={(e) => handleKeyDown(index, e)} onPaste={handlePaste} onFocus={() => setFocusedIndex(index)} onBlur={() => setFocusedIndex(null)} disabled={isVerifying || isLoggingIn} />
); })}
{/* 发送验证码按钮 */}
); };