538 lines
21 KiB
JavaScript
Executable File
538 lines
21 KiB
JavaScript
Executable File
// 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 >
|
||
);
|
||
} |