Files
vf_react/src/views/Authentication/SignIn/SignInIllustration.js
2025-10-11 16:16:02 +08:00

647 lines
28 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 } from "react";
import {
Box,
Button,
Flex,
FormControl,
Input,
Text,
Heading,
VStack,
HStack,
useToast,
Icon,
InputGroup,
InputRightElement,
IconButton,
Link as ChakraLink,
Center,
useDisclosure
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { FaMobile, FaWeixin, FaLock, FaQrcode } from "react-icons/fa";
import { useNavigate, Link, useLocation } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
import PrivacyPolicyModal from "../../../components/PrivacyPolicyModal";
import UserAgreementModal from "../../../components/UserAgreementModal";
// API配置
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = isProduction ? "" : "http://49.232.185.254:5000";
export default function SignInIllustration() {
const navigate = useNavigate();
const location = useLocation();
const toast = useToast();
const { login, checkSession } = useAuth();
// 页面状态
const [isLoading, setIsLoading] = useState(false);
// 检查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;
if (countdown > 0) {
timer = setInterval(() => {
setCountdown(prev => prev - 1);
}, 1000);
} else if (countdown === 0) {
setVerificationCodeSent(false);
}
return () => 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_BASE_URL}/api/auth/send-verification-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
credential,
type,
purpose: 'login'
}),
});
const data = await response.json();
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) {
toast({
title: "发送验证码失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setSendingCode(false);
}
};
// 获取微信授权URL
const getWechatQRCode = async () => {
};
// 点击扫码,打开微信登录窗口
const openWechatLogin = async() => {
try {
setIsLoading(true);
// 获取微信二维码地址
const response = await fetch(`${API_BASE_URL}/api/auth/wechat/qrcode`);
if (!response.ok) {
throw new Error('获取二维码失败');
}
const data = await response.json();
// 方案1直接跳转推荐
window.location.href = data.auth_url;
} catch (error) {
console.error('获取微信授权失败:', error);
toast({
title: "获取微信授权失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setIsLoading(false);
}
};
// 验证码登录函数
const loginWithVerificationCode = async (credential, verificationCode, authLoginType) => {
try {
const response = await fetch(`${API_BASE_URL}/api/auth/login-with-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
credential,
verification_code: verificationCode,
login_type: authLoginType
}),
});
const data = await response.json();
if (response.ok && data.success) {
// 更新认证状态
await checkSession();
toast({
title: "登录成功",
description: "欢迎回来!",
status: "success",
duration: 3000,
});
return { success: true };
} else {
throw new Error(data.error || '验证码登录失败');
}
} catch (error) {
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) { // 验证码登陆
credential = formData.phone;
authLoginType = 'phone';
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) {
navigate("/home");
}
}
} catch (error) {
console.error('Login error:', error);
} finally {
setIsLoading(false);
}
};
// 切换登录方式
const handleChangeMethod = (status) => {
if (!status) {
setFormData(prev => ({ ...prev, verificationCode: "" }));
}
setUseVerificationCode(!useVerificationCode);
}
return (
<Flex minH="100vh" position="relative" overflow="hidden">
{/* 流体波浪背景 */}
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
zIndex={0}
background={`
linear-gradient(45deg,
rgba(139, 69, 19, 0.9) 0%,
rgba(160, 82, 45, 0.8) 15%,
rgba(205, 133, 63, 0.7) 30%,
rgba(222, 184, 135, 0.8) 45%,
rgba(245, 222, 179, 0.6) 60%,
rgba(255, 228, 196, 0.7) 75%,
rgba(139, 69, 19, 0.8) 100%
)
`}
_before={{
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `
conic-gradient(from 0deg at 30% 20%,
rgba(255, 140, 0, 0.6) 0deg,
rgba(255, 69, 0, 0.4) 60deg,
rgba(139, 69, 19, 0.5) 120deg,
rgba(160, 82, 45, 0.6) 180deg,
rgba(205, 133, 63, 0.4) 240deg,
rgba(255, 140, 0, 0.5) 300deg,
rgba(255, 140, 0, 0.6) 360deg
)
`,
mixBlendMode: 'multiply',
animation: 'fluid-rotate 20s linear infinite'
}}
_after={{
content: '""',
position: 'absolute',
top: '10%',
left: '20%',
width: '60%',
height: '80%',
borderRadius: '50%',
background: 'radial-gradient(ellipse at center, rgba(255, 165, 0, 0.3) 0%, rgba(255, 140, 0, 0.2) 50%, transparent 70%)',
filter: 'blur(40px)',
animation: 'wave-pulse 8s ease-in-out infinite'
}}
sx={{
'@keyframes fluid-rotate': {
'0%': { transform: 'rotate(0deg) scale(1)' },
'50%': { transform: 'rotate(180deg) scale(1.1)' },
'100%': { transform: 'rotate(360deg) scale(1)' }
},
'@keyframes wave-pulse': {
'0%, 100%': { opacity: 0.4, transform: 'scale(1)' },
'50%': { opacity: 0.8, transform: 'scale(1.2)' }
}
}}
/>
{/* 主要内容 */}
<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="600px"
backdropFilter="blur(20px)"
border="1px solid rgba(255, 255, 255, 0.2)"
>
{/* 头部区域 */}
<VStack spacing={6} mb={8}>
<VStack spacing={2}>
<Heading size="xl" color="gray.800" fontWeight="bold">
欢迎回来
</Heading>
<Text color="gray.600" fontSize="md">
登录价值前沿继续您的投资之旅
</Text>
</VStack>
{/* 登录表单 */}
{/* setLoginType */}
<VStack spacing={2} align="stretch">
<HStack justify="center">
{/* 传统登录 */}
<form onSubmit={handleTraditionalLogin}>
<VStack spacing={4}>
<HStack spacing={2} width="100%" align="center"> {/* 设置 HStack 宽度为 100% */}
<Text fontSize="md" fontWeight="bold" color="gray.700" minWidth="70px" mr={2} noOfLines={1} overflow="hidden" textOverflow="ellipsis">
账号
</Text>
<FormControl isRequired flex="1 1 auto">
<InputGroup>
<Input
name={"phone"}
value={formData.phone}
onChange={handleInputChange}
placeholder={"请输入手机号"}
size="lg"
borderRadius="lg"
bg="gray.50"
border="1px solid"
borderColor="gray.200"
_focus={{
borderColor: "blue.500",
boxShadow: "0 0 0 1px #667eea"
}}
/>
<InputRightElement pointerEvents="none">
<Icon as={FaMobile} color="gray.400" />
</InputRightElement>
</InputGroup>
</FormControl>
</HStack>
{/* 密码输入框 */}
{useVerificationCode ? (
// 验证码输入框
<HStack spacing={2}>
<Text fontSize="md" fontWeight="bold" color={"gray.700"} minWidth="80px">验证码</Text>
<VStack spacing={3} align="stretch">
<HStack>
<FormControl isRequired flex="1 1 auto">
<InputGroup size="lg">
<Input
name="verificationCode"
value={formData.verificationCode}
onChange={handleInputChange}
placeholder="请输入验证码"
borderRadius="lg"
bg="gray.50"
border="1px solid"
borderColor="gray.200"
_focus={{
borderColor: "green.500",
boxShadow: "0 0 0 1px #48bb78"
}}
maxLength={6}
/>
{/* <InputRightElement>
<Icon as={FaCode} color="gray.400"/>
</InputRightElement> */}
</InputGroup>
</FormControl>
<Button
flex="0 0 auto" // 让按钮大小根据内容自适应
size="md"
colorScheme="green"
variant="outline"
onClick={sendVerificationCode}
isLoading={sendingCode}
isDisabled={verificationCodeSent && countdown > 0}
borderRadius="lg"
fontSize="sm" // 调整字体大小
whiteSpace="nowrap" // 防止文本换行
minWidth="120px" // 设置按钮最小宽度
>
{sendingCode ? "发送中..." : verificationCodeSent && countdown > 0 ? `${countdown}s` : "发送验证码"}
</Button>
</HStack>
</VStack>
</HStack>
):(
<HStack spacing={2}>
<Text fontSize="md" fontWeight="bold" color="gray.700" minWidth="70px" mr={2} noOfLines={1} overflow="hidden" textOverflow="ellipsis">
密码
</Text>
<FormControl isRequired flex="1 1 auto">
<InputGroup size="lg">
<Input
name="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={handleInputChange}
placeholder="请输入密码"
borderRadius="lg"
bg="gray.50"
border="1px solid"
borderColor="gray.200"
_focus={{
borderColor: "blue.500",
boxShadow: "0 0 0 1px #667eea"
}}
/>
<InputRightElement>
<IconButton
variant="ghost"
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
</InputGroup>
</FormControl>
</HStack>
)}
<HStack justify="space-between" width="100%">
<HStack spacing={1} as={Link} to="/auth/sign-up">
<Text fontSize="sm" color="gray.600">还没有账号</Text>
<Text fontSize="sm" color="blue.500" fontWeight="bold">去注册</Text>
</HStack>
<ChakraLink href="#" fontSize="sm" color="blue.500" fontWeight="bold" onClick={handleChangeMethod}>
{useVerificationCode ? '密码登陆' : '验证码登陆'}
</ChakraLink>
</HStack>
<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>
{/* 微信登录 - 简化版 */}
<VStack spacing={6}>
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
<VStack spacing={6}>
<VStack spacing={2}>
<Text fontSize="lg" fontWeight="bold" color={"gray.700"}>
微信扫一扫
</Text>
</VStack>
<Icon as={FaQrcode} w={20} h={20} color={"green.500"} />
{/* isLoading={isLoading || !wechatAuthUrl} */}
<Button
colorScheme="green"
size="lg"
leftIcon={<Icon as={FaWeixin} />}
onClick={openWechatLogin}
_hover={{ transform: "translateY(-2px)", boxShadow: "lg" }}
_active={{ transform: "translateY(0)" }}
>
扫码登录
</Button>
</VStack>
</Center>
</VStack>
</HStack>
</VStack>
</VStack>
{/* 底部链接 */}
<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 >
);
}