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