Files
vf_react/src/views/Authentication/SignIn/SignInIllustration.js
2025-10-18 22:32:50 +08:00

538 lines
21 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (
<Flex minH="100vh" position="relative" overflow="hidden">
{/* 背景 */}
<AuthBackground />
{/* 主要内容 */}
<Flex width="100%" align="center" justify="center" position="relative" zIndex={1} px={6} py={12}>
{/* 登录卡片 */}
<Box bg="white" borderRadius="2xl" boxShadow="2xl" p={8} width="100%" maxW="800px" backdropFilter="blur(20px)" border="1px solid rgba(255, 255, 255, 0.2)">
{/* 头部区域 */}
<AuthHeader title="欢迎回来" subtitle="登录价值前沿,继续您的投资之旅" />
{/* 左右布局 */}
<HStack spacing={8} align="stretch">
{/* 左侧:手机号登陆 - 80% 宽度 */}
<Box flex="4">
<form onSubmit={handleTraditionalLogin}>
<VStack spacing={4}>
<Heading size="md" color="gray.700" alignSelf="flex-start">
手机号登陆
</Heading>
<FormControl isRequired isInvalid={!!errors.phone}>
<Input
name="phone"
value={formData.phone}
onChange={handleInputChange}
placeholder="请输入11位手机号"
pr="2.5rem"
/>
<FormErrorMessage>{errors.phone}</FormErrorMessage>
</FormControl>
{/* 密码/验证码输入框 */}
{useVerificationCode ? (
<VerificationCodeInput
value={formData.verificationCode}
onChange={handleInputChange}
onSendCode={sendVerificationCode}
countdown={countdown}
isLoading={isLoading}
isSending={sendingCode}
error={errors.verificationCode}
colorScheme="green"
/>
) : (
<FormControl isRequired isInvalid={!!errors.password}>
<InputGroup>
<Input
name="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={handleInputChange}
pr="3rem"
placeholder="请输入密码"
_focus={{
borderColor: "blue.500",
boxShadow: "0 0 0 1px #667eea"
}}
/>
<InputRightElement width="3rem">
<IconButton
size="sm"
variant="ghost"
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? "Hide password" : "Show password"}
/>
</InputRightElement>
</InputGroup>
<FormErrorMessage>{errors.password}</FormErrorMessage>
</FormControl>
)}
<AuthFooter
linkText="还没有账号,"
linkLabel="去注册"
linkTo="/auth/sign-up"
useVerificationCode={useVerificationCode}
onSwitchMethod={handleChangeMethod}
/>
<Button
type="submit"
width="100%"
size="lg"
colorScheme="green"
color="white"
borderRadius="lg"
_hover={{
transform: "translateY(-2px)",
boxShadow: "lg"
}}
_active={{ transform: "translateY(0)" }}
isLoading={isLoading}
loadingText="登录中..."
fontWeight="bold"
cursor={"pointer"}
>
<Icon as={FaLock} mr={2} />登录
</Button>
</VStack>
</form>
</Box>
{/* 右侧:微信登陆 - 20% 宽度 */}
<Box flex="1">
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
<WechatRegister />
</Center>
</Box>
</HStack>
{/* 底部链接 */}
<VStack spacing={4} mt={6}>
{/* 协议同意勾选框 */}
<Text fontSize="sm" color="gray.600">
注册登录即表示阅读并同意{" "}
<ChakraLink
color="blue.500"
fontSize="sm"
onClick={onUserAgreementModalOpen}
textDecoration="underline"
_hover={{ color: "blue.600" }}
>
用户协议
</ChakraLink>
{" "}{" "}
<ChakraLink
color="blue.500"
fontSize="sm"
onClick={onPrivacyModalOpen}
textDecoration="underline"
_hover={{ color: "blue.600" }}
>
隐私政策
</ChakraLink>
</Text>
</VStack>
</Box>
</Flex>
{/* 隐私政策弹窗 */}
<PrivacyPolicyModal isOpen={isPrivacyModalOpen} onClose={onPrivacyModalClose} />
{/* 用户协议弹窗 */}
<UserAgreementModal isOpen={isUserAgreementModalOpen} onClose={onUserAgreementModalClose} />
</Flex >
);
}