// src/views/Authentication/SignIn/SignInIllustration.js - Session版本 import React, { useState, useEffect, useRef } from "react"; import { Box, Button, Flex, FormControl, Input, Text, Heading, VStack, HStack, useToast, Icon, InputGroup, InputRightElement, IconButton, Link as ChakraLink, Center, useDisclosure, FormErrorMessage } from "@chakra-ui/react"; import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"; import { FaMobile, FaLock } from "react-icons/fa"; import { useNavigate, useLocation } from "react-router-dom"; import { useAuth } from "../../../contexts/AuthContext"; import PrivacyPolicyModal from "../../../components/PrivacyPolicyModal"; import UserAgreementModal from "../../../components/UserAgreementModal"; import AuthBackground from "../../../components/Auth/AuthBackground"; import AuthHeader from "../../../components/Auth/AuthHeader"; import AuthFooter from "../../../components/Auth/AuthFooter"; import VerificationCodeInput from "../../../components/Auth/VerificationCodeInput"; import WechatRegister from "../../../components/Auth/WechatRegister"; import { logger } from "../../../utils/logger"; export default function SignInIllustration() { const navigate = useNavigate(); const location = useLocation(); const toast = useToast(); const { login, checkSession } = useAuth(); // 追踪组件挂载状态,防止内存泄漏 const isMountedRef = useRef(true); // 页面状态 const [isLoading, setIsLoading] = useState(false); const [errors, setErrors] = useState({}); // 检查URL参数中的错误信息(微信登录失败时) useEffect(() => { const params = new URLSearchParams(location.search); const error = params.get('error'); if (error) { let errorMessage = '登录失败'; switch (error) { case 'wechat_auth_failed': errorMessage = '微信授权失败'; break; case 'session_expired': errorMessage = '会话已过期,请重新登录'; break; case 'token_failed': errorMessage = '获取微信授权失败'; break; case 'userinfo_failed': errorMessage = '获取用户信息失败'; break; case 'login_failed': errorMessage = '登录处理失败,请重试'; break; default: errorMessage = '登录失败,请重试'; } toast({ title: "登录失败", description: errorMessage, status: "error", duration: 5000, isClosable: true, }); // 清除URL参数 const newUrl = window.location.pathname; window.history.replaceState({}, document.title, newUrl); } }, [location, toast]); // 传统登录数据 // 表单数据初始化 const [formData, setFormData] = useState({ username: "", // 用户名称 email: "", // 邮箱 phone: "", // 电话 password: "", // 密码 verificationCode: "", // 添加验证码字段 }); // 验证码登录状态 是否开启验证码 const [useVerificationCode, setUseVerificationCode] = useState(false); // 密码展示状态 const [showPassword, setShowPassword] = useState(false); const [verificationCodeSent, setVerificationCodeSent] = useState(false); // 验证码发送状态 const [sendingCode, setSendingCode] = useState(false); // 发送验证码状态 // 隐私政策弹窗状态 const { isOpen: isPrivacyModalOpen, onOpen: onPrivacyModalOpen, onClose: onPrivacyModalClose } = useDisclosure(); // 用户协议弹窗状态 const { isOpen: isUserAgreementModalOpen, onOpen: onUserAgreementModalOpen, onClose: onUserAgreementModalClose } = useDisclosure(); // 输入框输入 const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; // ========== 发送验证码逻辑 ============= // 倒计时效果 const [countdown, setCountdown] = useState(0); 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; const type = 'phone'; if (!credential) { toast({ title: "请先输入手机号", status: "warning", duration: 3000, }); return; } // 基本格式验证 if (!/^1[3-9]\d{9}$/.test(credential)) { toast({ title: "请输入有效的手机号", status: "warning", duration: 3000, }); return; } try { setSendingCode(true); const response = await fetch('/api/auth/send-verification-code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ credential, type, purpose: 'login' }), }); // ✅ 安全检查:验证 response 存在 if (!response) { throw new Error('网络请求失败,请检查网络连接'); } const data = await response.json(); // 组件卸载后不再执行后续操作 if (!isMountedRef.current) return; // ✅ 安全检查:验证 data 存在 if (!data) { throw new Error('服务器响应为空'); } if (response.ok && data.success) { toast({ title: "验证码已发送", description: "验证码已发送到您的手机号", status: "success", duration: 3000, }); setVerificationCodeSent(true); setCountdown(60); // 60秒倒计时 } else { throw new Error(data.error || '发送验证码失败'); } } catch (error) { if (isMountedRef.current) { toast({ title: "发送验证码失败", description: error.message || "请稍后重试", status: "error", duration: 3000, }); } } finally { if (isMountedRef.current) { setSendingCode(false); } } }; // 验证码登录函数 const loginWithVerificationCode = async (credential, verificationCode, authLoginType) => { try { const response = await fetch('/api/auth/login-with-code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ credential, verification_code: verificationCode, login_type: authLoginType }), }); // ✅ 安全检查:验证 response 存在 if (!response) { throw new Error('网络请求失败,请检查网络连接'); } const data = await response.json(); // 组件卸载后不再执行后续操作 if (!isMountedRef.current) { return { success: false, error: '操作已取消' }; } // ✅ 安全检查:验证 data 存在 if (!data) { throw new Error('服务器响应为空'); } if (response.ok && data.success) { // 更新认证状态 await checkSession(); if (isMountedRef.current) { toast({ title: "登录成功", description: "欢迎回来!", status: "success", duration: 3000, }); } return { success: true }; } else { throw new Error(data.error || '验证码登录失败'); } } catch (error) { if (isMountedRef.current) { toast({ title: "登录失败", description: error.message || "请检查验证码是否正确", status: "error", duration: 3000, }); } return { success: false, error: error.message }; } }; // 传统行业登陆 const handleTraditionalLogin = async (e) => { e.preventDefault(); setIsLoading(true); try { const credential = formData.phone; const authLoginType = 'phone'; if (useVerificationCode) { // 验证码登陆 if (!credential || !formData.verificationCode) { toast({ title: "请填写完整信息", description: "手机号和验证码不能为空", status: "warning", duration: 3000, }); return; } const result = await loginWithVerificationCode(credential, formData.verificationCode, authLoginType); if (result.success) { navigate("/home"); } } else { // 密码登陆 if (!credential || !formData.password) { toast({ title: "请填写完整信息", description: `手机号和密码不能为空`, status: "warning", duration: 3000, }); return; } const result = await login(credential, formData.password, authLoginType); if (result.success) { // ✅ 显示成功提示 toast({ title: "登录成功", description: "欢迎回来!", status: "success", duration: 3000, isClosable: true, }); navigate("/home"); } else { // ❌ 显示错误提示 toast({ title: "登录失败", description: result.error || "请检查您的登录信息", status: "error", duration: 3000, isClosable: true, }); } } } catch (error) { logger.error('SignInIllustration', 'handleTraditionalLogin', error, { phone: formData.phone ? formData.phone.substring(0, 3) + '****' + formData.phone.substring(7) : 'N/A', useVerificationCode, loginType: 'phone' }); toast({ title: "登录失败", description: error.message || "发生未预期的错误,请重试", status: "error", duration: 3000, isClosable: true, }); } finally { setIsLoading(false); } }; // 切换登录方式 const handleChangeMethod = () => { setUseVerificationCode(!useVerificationCode); // 切换到密码模式时清空验证码 if (useVerificationCode) { setFormData(prev => ({ ...prev, verificationCode: "" })); } }; // 组件卸载时清理 useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); return ( {/* 背景 */} {/* 主要内容 */} {/* 登录卡片 */} {/* 头部区域 */} {/* 左右布局 */} {/* 左侧:手机号登陆 - 80% 宽度 */}
手机号登陆 {errors.phone} {/* 密码/验证码输入框 */} {useVerificationCode ? ( ) : ( : } onClick={() => setShowPassword(!showPassword)} aria-label={showPassword ? "Hide password" : "Show password"} /> {errors.password} )}
{/* 右侧:微信登陆 - 20% 宽度 */}
{/* 底部链接 */} {/* 协议同意勾选框 */} 注册登录即表示阅读并同意{" "} 《用户协议》 {" "}和{" "} 《隐私政策》
{/* 隐私政策弹窗 */} {/* 用户协议弹窗 */}
); }