// src/components/Auth/AuthFormContent.js // 统一的认证表单组件 import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { Box, Button, FormControl, Input, Heading, VStack, HStack, Stack, useToast, Icon, FormErrorMessage, Center, AlertDialog, AlertDialogBody, AlertDialogFooter, AlertDialogHeader, AlertDialogContent, AlertDialogOverlay, Text, Link as ChakraLink, useBreakpointValue, Divider, IconButton, } from "@chakra-ui/react"; import { FaLock, FaWeixin } from "react-icons/fa"; import { useAuth } from "../../contexts/AuthContext"; import { useAuthModal } from "../../hooks/useAuthModal"; import { useNotification } from "../../contexts/NotificationContext"; import { authService } from "../../services/authService"; import AuthHeader from './AuthHeader'; import VerificationCodeInput from './VerificationCodeInput'; import WechatRegister from './WechatRegister'; import { setCurrentUser } from '../../mocks/data/users'; import { logger } from '../../utils/logger'; import { useAuthEvents } from '../../hooks/useAuthEvents'; // 统一配置对象 const AUTH_CONFIG = { // UI文本 title: "价值前沿", subtitle: "开启您的投资之旅", formTitle: "登陆/注册", buttonText: "登录/注册", loadingText: "验证中...", successTitle: "验证成功", successDescription: "欢迎!", errorTitle: "验证失败", // API配置 api: { endpoint: '/api/auth/register/phone', purpose: 'login', // ⚡ 统一使用 'login' 模式 }, // 功能开关 features: { successDelay: 1000, // 延迟1秒显示成功提示 } }; export default function AuthFormContent() { const toast = useToast(); const navigate = useNavigate(); const { checkSession } = useAuth(); const { handleLoginSuccess } = useAuthModal(); const { showWelcomeGuide } = useNotification(); // 使用统一配置 const config = AUTH_CONFIG; // 追踪组件挂载状态,防止内存泄漏 const isMountedRef = useRef(true); const cancelRef = useRef(); // AlertDialog 需要的 ref // 页面状态 const [isLoading, setIsLoading] = useState(false); const [errors, setErrors] = useState({}); // 昵称设置引导对话框 const [showNicknamePrompt, setShowNicknamePrompt] = useState(false); const [currentPhone, setCurrentPhone] = useState(""); // 事件追踪 const authEvents = useAuthEvents({ component: 'AuthFormContent', isMobile, }); const stackDirection = useBreakpointValue({ base: "column", md: "row" }); const stackSpacing = useBreakpointValue({ base: 4, md: 2 }); // ✅ 桌面端从32px减至8px,更紧凑 // 表单数据 const [formData, setFormData] = useState({ phone: "", verificationCode: "", }); // 验证码状态 const [verificationCodeSent, setVerificationCodeSent] = useState(false); const [sendingCode, setSendingCode] = useState(false); const [countdown, setCountdown] = useState(0); // 输入框变化处理 const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // 追踪用户开始填写手机号 (判断用户选择了手机登录方式) if (name === 'phone' && value.length === 1 && !formData.phone) { authEvents.trackPhoneLoginInitiated(value); } // 追踪验证码输入变化 if (name === 'verificationCode') { authEvents.trackVerificationCodeInputChanged(value.length); } }; // 倒计时逻辑 useEffect(() => { let timer; let isMounted = true; if (countdown > 0) { timer = setInterval(() => { if (isMounted) { setCountdown(prev => prev - 1); } }, 1000); } else if (countdown === 0 && isMounted) { setVerificationCodeSent(false); } return () => { isMounted = false; if (timer) clearInterval(timer); }; }, [countdown]); // 发送验证码 const sendVerificationCode = async () => { const credential = formData.phone; if (!credential) { toast({ title: "请先输入手机号", status: "warning", duration: 3000, }); return; } // 清理手机号格式字符(空格、横线、括号等) const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, ''); if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) { authEvents.trackPhoneNumberValidated(credential, false, 'invalid_format'); authEvents.trackFormValidationError('phone', 'invalid_format', '请输入有效的手机号'); toast({ title: "请输入有效的手机号", status: "warning", duration: 3000, }); return; } // 追踪手机号验证通过 authEvents.trackPhoneNumberValidated(credential, true); try { setSendingCode(true); const requestData = { credential: cleanedCredential, // 使用清理后的手机号 type: 'phone', purpose: config.api.purpose }; const response = await fetch('/api/auth/send-verification-code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(requestData), }); if (!response) { throw new Error('网络请求失败,请检查网络连接'); } const data = await response.json(); if (!isMountedRef.current) return; if (!data) { throw new Error('服务器响应为空'); } if (response.ok && data.success) { // 追踪验证码发送成功 (或重发) const isResend = verificationCodeSent; if (isResend) { authEvents.trackVerificationCodeResent(credential, countdown > 0 ? 2 : 1); } else { authEvents.trackVerificationCodeSent(credential, config.api.purpose); } // ❌ 移除成功 toast,静默处理 logger.info('AuthFormContent', '验证码发送成功', { credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7), dev_code: data.dev_code }); // ✅ 开发环境下在控制台显示验证码 if (data.dev_code) { console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;'); } setVerificationCodeSent(true); setCountdown(60); } else { throw new Error(data.error || '发送验证码失败'); } } catch (error) { // 追踪验证码发送失败 authEvents.trackVerificationCodeSendFailed(credential, error); authEvents.trackError('api', error.message || '发送验证码失败', { endpoint: '/api/auth/send-verification-code', phone_masked: credential.substring(0, 3) + '****' + credential.substring(7) }); logger.api.error('POST', '/api/auth/send-verification-code', error, { credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7) }); // ✅ 显示错误提示给用户 toast({ id: 'send-code-error', title: "发送验证码失败", description: error.message || "请稍后重试", status: "error", duration: 3000, isClosable: true, position: 'top', containerStyle: { zIndex: 10000, } }); } finally { if (isMountedRef.current) { setSendingCode(false); } } }; // 提交处理(登录或注册) const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); try { const { phone, verificationCode } = formData; // 表单验证 if (!phone || !verificationCode) { toast({ title: "请填写完整信息", description: "手机号和验证码不能为空", status: "warning", duration: 3000, }); return; } // 清理手机号格式字符(空格、横线、括号等) const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, ''); if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) { toast({ title: "请输入有效的手机号", status: "warning", duration: 3000, }); return; } // 追踪验证码提交 authEvents.trackVerificationCodeSubmitted(phone); // 构建请求体 const requestBody = { credential: cleanedPhone, // 使用清理后的手机号 verification_code: verificationCode.trim(), // 添加 trim() 防止空格 login_type: 'phone', }; // 调用API(根据模式选择不同的endpoint const response = await fetch('/api/auth/login-with-code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(requestBody), }); if (!response) { throw new Error('网络请求失败,请检查网络连接'); } const data = await response.json(); if (!isMountedRef.current) return; if (!data) { throw new Error('服务器响应为空'); } if (response.ok && data.success) { // ⚡ Mock 模式:先在前端侧写入 localStorage,确保时序正确 if (process.env.REACT_APP_ENABLE_MOCK === 'true' && data.user) { setCurrentUser(data.user); logger.debug('AuthFormContent', '前端侧设置当前用户(Mock模式)', { userId: data.user?.id, phone: data.user?.phone, mockMode: true }); } // 更新session await checkSession(); // ✅ 兼容后端两种命名格式:camelCase (isNewUser) 和 snake_case (is_new_user) const isNewUser = data.isNewUser ?? data.is_new_user ?? false; // 追踪登录成功并识别用户 authEvents.trackLoginSuccess(data.user, 'phone', isNewUser); // ✅ 保留登录成功 toast(关键操作提示) toast({ title: isNewUser ? '注册成功' : '登录成功', description: config.successDescription, status: "success", duration: 2000, }); // 检查是否为新注册用户 if (isNewUser) { // 新注册用户,延迟后显示昵称设置引导 setTimeout(() => { setCurrentPhone(phone); setShowNicknamePrompt(true); // 追踪昵称设置引导显示 authEvents.trackNicknamePromptShown(phone); }, config.features.successDelay); } else { // 已有用户,直接登录成功 setTimeout(() => { handleLoginSuccess({ phone }); }, config.features.successDelay); } // ⚡ 延迟 10 秒显示权限引导(温和、非侵入) setTimeout(() => { if (showWelcomeGuide) { logger.info('AuthFormContent', '显示欢迎引导'); showWelcomeGuide(); } }, 10000); } else { throw new Error(data.error || `${config.errorTitle}`); } } catch (error) { const { phone, verificationCode } = formData; // 追踪登录失败 const errorType = error.message.includes('网络') ? 'network' : error.message.includes('服务器') ? 'api' : 'validation'; authEvents.trackLoginFailed('phone', errorType, error.message, { phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A', has_verification_code: !!verificationCode }); logger.error('AuthFormContent', 'handleSubmit', error, { phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A', hasVerificationCode: !!verificationCode }); // ✅ 显示错误提示给用户 toast({ id: 'auth-verification-error', title: config.errorTitle, description: error.message || "请检查验证码是否正确", status: "error", duration: 3000, isClosable: true, position: 'top', containerStyle: { zIndex: 10000, } }); } finally { if (isMountedRef.current) { setIsLoading(false); } } }; // 微信H5登录处理 const handleWechatH5Login = async () => { // 追踪用户选择微信登录 authEvents.trackWechatLoginInitiated('icon_button'); try { // 1. 构建回调URL const redirectUrl = `${window.location.origin}/home/wechat-callback`; // 2. 显示提示 toast({ title: "即将跳转", description: "正在跳转到微信授权页面...", status: "info", duration: 2000, isClosable: true, }); // 3. 获取微信H5授权URL const response = await authService.getWechatH5AuthUrl(redirectUrl); if (!response || !response.auth_url) { throw new Error('获取授权链接失败'); } // 追踪微信H5跳转 authEvents.trackWechatH5Redirect(); // 4. 延迟跳转,让用户看到提示 setTimeout(() => { window.location.href = response.auth_url; }, 500); } catch (error) { // 追踪跳转失败 authEvents.trackError('api', error.message || '获取微信授权链接失败', { context: 'wechat_h5_redirect' }); logger.error('AuthFormContent', 'handleWechatH5Login', error); toast({ title: "跳转失败", description: error.message || "请稍后重试", status: "error", duration: 3000, isClosable: true, }); } }; // 组件挂载时追踪页面浏览 useEffect(() => { isMountedRef.current = true; // 追踪登录页面浏览 authEvents.trackLoginPageViewed(); return () => { isMountedRef.current = false; }; }, [authEvents]); return ( <>
{config.formTitle} {errors.phone} {/* 验证码输入框 + 移动端微信图标 */} {/* 移动端:验证码下方的微信登录图标 */} {isMobile && ( 其他登录方式: } size="sm" variant="ghost" color="#07C160" borderRadius="md" minW="24px" minH="24px" _hover={{ bg: "green.50", color: "#06AD56" }} _active={{ bg: "green.100" }} onClick={handleWechatH5Login} isDisabled={isLoading} /> )} {/* 隐私声明 */} 登录即表示您同意价值前沿{" "} 《用户协议》 {" "}和{" "} 《隐私政策》
{/* 桌面端:右侧二维码扫描 */} {!isMobile && ( {/* ✅ 桌面端让右侧自适应宽度 */}
{/* ✅ 移除bg和p,WechatRegister自带白色背景和padding */}
)}
{/* 只在需要时才渲染 AlertDialog,避免创建不必要的 Portal */} {showNicknamePrompt && ( { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }} isCentered closeOnEsc={true} closeOnOverlayClick={false}> 完善个人信息 您已成功注册!是否前往个人资料设置昵称和其他信息? )} ); }