Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View File

@@ -0,0 +1,932 @@
// src/views/Authentication/SignIn/SignInIllustration.js - Session版本
import React, { useState, useEffect } from "react";
import {
Box,
Button,
Flex,
FormControl,
Input,
Stack,
Text,
Heading,
VStack,
HStack,
Checkbox,
useToast,
Icon,
InputGroup,
InputRightElement,
IconButton,
Tab,
TabList,
Tabs,
Link as ChakraLink,
Center,
Spinner,
Divider,
Alert,
AlertIcon,
useDisclosure
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { FaUser, FaEnvelope, FaMobile, FaWeixin, FaLock, FaQrcode, FaCode } 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 [loginType, setLoginType] = useState(0); // 0: 微信, 1: 手机号
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [agreeToTerms, setAgreeToTerms] = useState(false);
// 传统登录表单数据
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "",
password: "",
verificationCode: "", // 添加验证码字段
});
// 验证码登录相关状态
const [useVerificationCode, setUseVerificationCode] = useState(false);
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
const [countdown, setCountdown] = useState(0);
const [sendingCode, setSendingCode] = useState(false);
// 微信登录相关状态
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
// 隐私政策弹窗状态
const {
isOpen: isPrivacyModalOpen,
onOpen: onPrivacyModalOpen,
onClose: onPrivacyModalClose
} = useDisclosure();
// 用户协议弹窗状态
const {
isOpen: isUserAgreementModalOpen,
onOpen: onUserAgreementModalOpen,
onClose: onUserAgreementModalClose
} = useDisclosure();
const navigate = useNavigate();
const location = useLocation();
const toast = useToast();
const { login, checkSession } = useAuth();
// 倒计时效果
useEffect(() => {
let timer;
if (countdown > 0) {
timer = setInterval(() => {
setCountdown(prev => prev - 1);
}, 1000);
} else if (countdown === 0) {
setVerificationCodeSent(false);
}
return () => clearInterval(timer);
}, [countdown]);
// 检查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]);
// 获取微信授权URL
const getWechatQRCode = 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();
setWechatAuthUrl(data.auth_url);
} catch (error) {
console.error('获取微信授权失败:', error);
toast({
title: "获取微信授权失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setIsLoading(false);
}
};
// 切换到微信登录时获取二维码
useEffect(() => {
if (loginType === 0) {
getWechatQRCode();
}
}, [loginType]);
// 打开微信登录窗口
const openWechatLogin = () => {
if (!agreeToTerms) {
toast({
title: "请先同意协议",
description: "请勾选同意用户协议和隐私政策后再使用微信登录",
status: "warning",
duration: 3000,
isClosable: true,
});
return;
}
if (wechatAuthUrl) {
// 方案1直接跳转推荐
window.location.href = wechatAuthUrl;
// 方案2新窗口打开备选
// const width = 600;
// const height = 600;
// const left = (window.innerWidth - width) / 2;
// const top = (window.innerHeight - height) / 2;
// window.open(
// wechatAuthUrl,
// 'wechat_login',
// `width=${width},height=${height},left=${left},top=${top}`
// );
}
};
// 发送验证码
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);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleTraditionalLogin = async (e) => {
e.preventDefault();
if (!agreeToTerms) {
toast({
title: "请先同意协议",
description: "请勾选同意用户协议和隐私政策后再登录",
status: "warning",
duration: 3000,
isClosable: true,
});
return;
}
setIsLoading(true);
try {
let credential = '';
let authLoginType = '';
if (loginType === 1) {
credential = formData.phone;
authLoginType = 'phone';
}
// 验证码登录
if (useVerificationCode && loginType === 1) {
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: `${getCredentialName()}和密码不能为空`,
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 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 getCredentialName = () => {
return "手机号";
};
const getInputPlaceholder = () => {
return "请输入手机号";
};
const getInputName = () => {
return "phone";
};
const getInputValue = () => {
return formData.phone;
};
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="480px"
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>
{/* 登录方式选择 */}
<Box width="100%">
<Tabs
index={loginType}
onChange={setLoginType}
variant="soft-rounded"
colorScheme="blue"
isFitted
>
<TabList bg="gray.100" borderRadius="xl" p={1}>
<Tab
borderRadius="lg"
_selected={{
bg: "linear-gradient(135deg, #00bcd4 0%, #2196f3 100%)",
color: "white",
transform: "scale(1.02)"
}}
transition="all 0.2s"
fontSize="sm"
>
<Icon as={FaWeixin} mr={1} />
微信
</Tab>
<Tab
borderRadius="lg"
_selected={{
bg: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
color: "white",
transform: "scale(1.02)"
}}
transition="all 0.2s"
fontSize="sm"
>
<Icon as={FaMobile} mr={1} />
手机号
</Tab>
</TabList>
</Tabs>
</Box>
</VStack>
{/* 登录内容 */}
{loginType === 0 ? (
// 微信登录 - 简化版
<VStack spacing={6}>
{/* 协议同意勾选框 - 微信登录 */}
<Box width="100%">
<Checkbox
isChecked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
colorScheme="green"
size="sm"
>
<Text fontSize="sm" color="gray.600">
我已阅读并同意{" "}
<ChakraLink
color="green.500"
fontSize="sm"
onClick={onUserAgreementModalOpen}
textDecoration="underline"
_hover={{ color: "green.600" }}
>
用户协议
</ChakraLink>
{" "}{" "}
<ChakraLink
color="green.500"
fontSize="sm"
onClick={onPrivacyModalOpen}
textDecoration="underline"
_hover={{ color: "green.600" }}
>
隐私政策
</ChakraLink>
</Text>
</Checkbox>
{!agreeToTerms && (
<Text fontSize="xs" color="red.500" mt={1} ml={6}>
请先同意用户协议和隐私政策
</Text>
)}
</Box>
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
<VStack spacing={6}>
<Icon as={FaQrcode} w={20} h={20} color={agreeToTerms ? "green.500" : "gray.400"} />
<VStack spacing={2}>
<Text fontSize="lg" fontWeight="bold" color={agreeToTerms ? "gray.700" : "gray.400"}>
微信扫码登录
</Text>
<Text fontSize="sm" color={agreeToTerms ? "gray.500" : "gray.400"} textAlign="center">
{agreeToTerms ? "使用微信扫一扫,安全快速登录" : "请先同意协议后使用微信登录"}
</Text>
</VStack>
<Button
colorScheme="green"
size="lg"
leftIcon={<Icon as={FaWeixin} />}
onClick={openWechatLogin}
isLoading={isLoading || !wechatAuthUrl}
loadingText="准备中..."
isDisabled={!wechatAuthUrl || !agreeToTerms}
_hover={agreeToTerms ? {
transform: "translateY(-2px)",
boxShadow: "lg"
} : {}}
_active={agreeToTerms ? {
transform: "translateY(0)"
} : {}}
opacity={agreeToTerms ? 1 : 0.6}
>
立即扫码登录
</Button>
{!wechatAuthUrl && !isLoading && agreeToTerms && (
<Button
size="sm"
variant="link"
colorScheme="blue"
onClick={getWechatQRCode}
>
重新获取
</Button>
)}
</VStack>
</Center>
<Alert
status={agreeToTerms ? "info" : "warning"}
borderRadius="lg"
bg={agreeToTerms ? "blue.50" : "orange.50"}
border="1px solid"
borderColor={agreeToTerms ? "blue.200" : "orange.200"}
>
<AlertIcon color={agreeToTerms ? "blue.500" : "orange.500"} />
<Text fontSize="sm" color={agreeToTerms ? "blue.700" : "orange.700"}>
{agreeToTerms
? "点击按钮后将跳转到微信授权页面"
: "请先同意用户协议和隐私政策"}
</Text>
</Alert>
</VStack>
) : (
// 传统登录
<form onSubmit={handleTraditionalLogin}>
<VStack spacing={4}>
<FormControl isRequired>
<InputGroup>
<Input
name={getInputName()}
value={getInputValue()}
onChange={handleInputChange}
placeholder={getInputPlaceholder()}
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>
{/* 登录方式切换(仅手机号显示) */}
{loginType === 1 && (
<VStack spacing={2} align="stretch">
<HStack justify="center">
<Button
size="sm"
variant={useVerificationCode ? "ghost" : "solid"}
colorScheme="blue"
onClick={() => {
setUseVerificationCode(false);
setFormData(prev => ({ ...prev, verificationCode: "" }));
}}
>
密码登录
</Button>
<Button
size="sm"
variant={useVerificationCode ? "solid" : "ghost"}
colorScheme="green"
onClick={() => setUseVerificationCode(true)}
>
验证码登录
</Button>
</HStack>
</VStack>
)}
{/* 密码输入框 */}
{!useVerificationCode ? (
<FormControl isRequired>
<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>
) : (
// 验证码输入框
<VStack spacing={3} align="stretch">
<HStack>
<FormControl isRequired flex={2}>
<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={1}
size="lg"
colorScheme="green"
variant="outline"
onClick={sendVerificationCode}
isLoading={sendingCode}
isDisabled={verificationCodeSent && countdown > 0}
borderRadius="lg"
>
{sendingCode
? "发送中..."
: verificationCodeSent && countdown > 0
? `${countdown}s`
: "发送验证码"
}
</Button>
</HStack>
</VStack>
)}
<HStack justify="space-between" width="100%">
<Checkbox
isChecked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
colorScheme="blue"
>
<Text fontSize="sm" color="gray.600">记住我</Text>
</Checkbox>
<ChakraLink href="#" color="blue.500" fontSize="sm">
忘记密码
</ChakraLink>
</HStack>
{/* 协议同意勾选框 */}
<Box width="100%">
<Checkbox
isChecked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
colorScheme="blue"
size="sm"
>
<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>
</Checkbox>
{!agreeToTerms && (
<Text fontSize="xs" color="red.500" mt={1} ml={6}>
请先同意用户协议和隐私政策
</Text>
)}
</Box>
<Button
type="submit"
width="100%"
size="lg"
background={agreeToTerms
? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
: "gray.300"
}
color="white"
borderRadius="lg"
_hover={agreeToTerms ? {
transform: "translateY(-2px)",
boxShadow: "lg"
} : {}}
_active={agreeToTerms ? {
transform: "translateY(0)"
} : {}}
isLoading={isLoading}
loadingText="登录中..."
fontWeight="bold"
isDisabled={!agreeToTerms}
cursor={agreeToTerms ? "pointer" : "not-allowed"}
>
<Icon as={FaLock} mr={2} />
登录
</Button>
</VStack>
</form>
)}
{/* 底部链接 */}
<VStack spacing={4} mt={6}>
<HStack>
<Divider />
<Text fontSize="sm" color="gray.500" px={4}></Text>
<Divider />
</HStack>
<Button
as={Link}
to="/auth/sign-up"
width="100%"
size="lg"
bg="gray.800"
color="white"
borderRadius="lg"
_hover={{
bg: "gray.700",
transform: "translateY(-2px)",
boxShadow: "lg"
}}
_active={{
transform: "translateY(0)"
}}
fontWeight="bold"
>
没有账号立即注册
</Button>
</VStack>
</Box>
</Flex>
{/* 底部导航链接 */}
<Box
position="absolute"
bottom={8}
left="50%"
transform="translateX(-50%)"
zIndex={1}
>
<HStack spacing={6}>
<ChakraLink
color="white"
fontSize="sm"
opacity={0.8}
_hover={{ opacity: 1 }}
onClick={onUserAgreementModalOpen}
>
用户协议
</ChakraLink>
<ChakraLink
color="white"
fontSize="sm"
opacity={0.8}
_hover={{ opacity: 1 }}
onClick={onPrivacyModalOpen}
>
隐私政策
</ChakraLink>
</HStack>
</Box>
{/* 隐私政策弹窗 */}
<PrivacyPolicyModal
isOpen={isPrivacyModalOpen}
onClose={onPrivacyModalClose}
/>
{/* 用户协议弹窗 */}
<UserAgreementModal
isOpen={isUserAgreementModalOpen}
onClose={onUserAgreementModalClose}
/>
</Flex>
);
}